fix(coding-agent): unify source provenance, closes #1734
This commit is contained in:
@@ -67,7 +67,23 @@ describe("AgentSession dynamic tool registration", () => {
|
||||
|
||||
await session.bindExtensions({});
|
||||
|
||||
expect(session.getAllTools().map((tool) => tool.name)).toContain("dynamic_tool");
|
||||
const allTools = session.getAllTools();
|
||||
const dynamicTool = allTools.find((tool) => tool.name === "dynamic_tool");
|
||||
const readTool = allTools.find((tool) => tool.name === "read");
|
||||
|
||||
expect(allTools.map((tool) => tool.name)).toContain("dynamic_tool");
|
||||
expect(dynamicTool?.sourceInfo).toMatchObject({
|
||||
path: "<inline:1>",
|
||||
source: "inline",
|
||||
scope: "temporary",
|
||||
origin: "top-level",
|
||||
});
|
||||
expect(readTool?.sourceInfo).toMatchObject({
|
||||
path: "<builtin:read>",
|
||||
source: "builtin",
|
||||
scope: "temporary",
|
||||
origin: "top-level",
|
||||
});
|
||||
expect(session.getActiveToolNames()).toContain("dynamic_tool");
|
||||
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
||||
expect(session.systemPrompt).toContain("- Use dynamic_tool when the user asks for dynamic behavior tests.");
|
||||
@@ -75,6 +91,49 @@ describe("AgentSession dynamic tool registration", () => {
|
||||
session.dispose();
|
||||
});
|
||||
|
||||
it("returns source metadata for SDK custom tools", async () => {
|
||||
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd: tempDir,
|
||||
agentDir,
|
||||
settingsManager,
|
||||
});
|
||||
await resourceLoader.reload();
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
cwd: tempDir,
|
||||
agentDir,
|
||||
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
||||
settingsManager,
|
||||
sessionManager,
|
||||
resourceLoader,
|
||||
customTools: [
|
||||
{
|
||||
name: "sdk_tool",
|
||||
label: "SDK Tool",
|
||||
description: "Tool registered through createAgentSession",
|
||||
parameters: Type.Object({}),
|
||||
execute: async () => ({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sdkTool = session.getAllTools().find((tool) => tool.name === "sdk_tool");
|
||||
expect(sdkTool?.sourceInfo).toMatchObject({
|
||||
path: "<sdk:sdk_tool>",
|
||||
source: "sdk",
|
||||
scope: "temporary",
|
||||
origin: "top-level",
|
||||
});
|
||||
expect(session.getActiveToolNames()).toContain("sdk_tool");
|
||||
|
||||
session.dispose();
|
||||
});
|
||||
|
||||
it("keeps custom tools active but omits them from available tools when promptSnippet is not provided", async () => {
|
||||
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
|
||||
import { codingTools } from "../src/core/tools/index.js";
|
||||
import { createTestResourceLoader } from "./utilities.js";
|
||||
|
||||
@@ -74,6 +75,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
||||
return {
|
||||
path: "test-extension",
|
||||
resolvedPath: "/test/test-extension.ts",
|
||||
sourceInfo: createSyntheticSourceInfo("<test:test-extension>", { source: "test" }),
|
||||
handlers,
|
||||
tools: new Map(),
|
||||
messageRenderers: new Map(),
|
||||
@@ -228,6 +230,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
||||
const throwingExtension: Extension = {
|
||||
path: "throwing-extension",
|
||||
resolvedPath: "/test/throwing-extension.ts",
|
||||
sourceInfo: createSyntheticSourceInfo("<test:throwing-extension>", { source: "test" }),
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session_before_compact",
|
||||
@@ -276,6 +279,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
||||
const extension1: Extension = {
|
||||
path: "extension1",
|
||||
resolvedPath: "/test/extension1.ts",
|
||||
sourceInfo: createSyntheticSourceInfo("<test:extension1>", { source: "test" }),
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session_before_compact",
|
||||
@@ -306,6 +310,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
||||
const extension2: Extension = {
|
||||
path: "extension2",
|
||||
resolvedPath: "/test/extension2.ts",
|
||||
sourceInfo: createSyntheticSourceInfo("<test:extension2>", { source: "test" }),
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session_before_compact",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DefaultResourceLoader } from "../src/core/resource-loader.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
import type { Skill } from "../src/core/skills.js";
|
||||
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
|
||||
|
||||
describe("DefaultResourceLoader", () => {
|
||||
let tempDir: string;
|
||||
@@ -411,7 +412,7 @@ Content`,
|
||||
description: "Injected skill",
|
||||
filePath: "/fake/path",
|
||||
baseDir: "/fake",
|
||||
source: "custom",
|
||||
sourceInfo: createSyntheticSourceInfo("/fake/path", { source: "custom" }),
|
||||
disableModelInvocation: false,
|
||||
};
|
||||
const loader = new DefaultResourceLoader({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createExtensionRuntime } from "../src/core/extensions/loader.js";
|
||||
import type { ResourceLoader } from "../src/core/resource-loader.js";
|
||||
import { createAgentSession } from "../src/core/sdk.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
|
||||
|
||||
describe("createAgentSession skills option", () => {
|
||||
let tempDir: string;
|
||||
@@ -79,7 +80,7 @@ This is a test skill.
|
||||
description: "A custom skill",
|
||||
filePath: "/fake/path/SKILL.md",
|
||||
baseDir: "/fake/path",
|
||||
source: "custom" as const,
|
||||
sourceInfo: createSyntheticSourceInfo("/fake/path/SKILL.md", { source: "sdk" }),
|
||||
disableModelInvocation: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,29 @@ import { join, resolve } from "path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ResourceDiagnostic } from "../src/core/diagnostics.js";
|
||||
import { formatSkillsForPrompt, loadSkills, loadSkillsFromDir, type Skill } from "../src/core/skills.js";
|
||||
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
|
||||
|
||||
const fixturesDir = resolve(__dirname, "fixtures/skills");
|
||||
const collisionFixturesDir = resolve(__dirname, "fixtures/skills-collision");
|
||||
|
||||
function createTestSkill(options: {
|
||||
name: string;
|
||||
description: string;
|
||||
filePath: string;
|
||||
baseDir: string;
|
||||
disableModelInvocation?: boolean;
|
||||
source?: string;
|
||||
}): Skill {
|
||||
return {
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
filePath: options.filePath,
|
||||
baseDir: options.baseDir,
|
||||
sourceInfo: createSyntheticSourceInfo(options.filePath, { source: options.source ?? "test" }),
|
||||
disableModelInvocation: options.disableModelInvocation ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("skills", () => {
|
||||
describe("loadSkillsFromDir", () => {
|
||||
it("should load a valid skill", () => {
|
||||
@@ -18,7 +37,7 @@ describe("skills", () => {
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe("valid-skill");
|
||||
expect(skills[0].description).toBe("A valid skill for testing purposes.");
|
||||
expect(skills[0].source).toBe("test");
|
||||
expect(skills[0].sourceInfo.source).toBe("test");
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -210,14 +229,12 @@ describe("skills", () => {
|
||||
|
||||
it("should format skills as XML", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "test-skill",
|
||||
description: "A test skill.",
|
||||
filePath: "/path/to/skill/SKILL.md",
|
||||
baseDir: "/path/to/skill",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -232,14 +249,12 @@ describe("skills", () => {
|
||||
|
||||
it("should include intro text before XML", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "test-skill",
|
||||
description: "A test skill.",
|
||||
filePath: "/path/to/skill/SKILL.md",
|
||||
baseDir: "/path/to/skill",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -252,14 +267,12 @@ describe("skills", () => {
|
||||
|
||||
it("should escape XML special characters", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "test-skill",
|
||||
description: 'A skill with <special> & "characters".',
|
||||
filePath: "/path/to/skill/SKILL.md",
|
||||
baseDir: "/path/to/skill",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -271,22 +284,18 @@ describe("skills", () => {
|
||||
|
||||
it("should format multiple skills", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "skill-one",
|
||||
description: "First skill.",
|
||||
filePath: "/path/one/SKILL.md",
|
||||
baseDir: "/path/one",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createTestSkill({
|
||||
name: "skill-two",
|
||||
description: "Second skill.",
|
||||
filePath: "/path/two/SKILL.md",
|
||||
baseDir: "/path/two",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -298,22 +307,19 @@ describe("skills", () => {
|
||||
|
||||
it("should exclude skills with disableModelInvocation from prompt", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "visible-skill",
|
||||
description: "A visible skill.",
|
||||
filePath: "/path/visible/SKILL.md",
|
||||
baseDir: "/path/visible",
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createTestSkill({
|
||||
name: "hidden-skill",
|
||||
description: "A hidden skill.",
|
||||
filePath: "/path/hidden/SKILL.md",
|
||||
baseDir: "/path/hidden",
|
||||
source: "test",
|
||||
disableModelInvocation: true,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -325,14 +331,13 @@ describe("skills", () => {
|
||||
|
||||
it("should return empty string when all skills have disableModelInvocation", () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
createTestSkill({
|
||||
name: "hidden-skill",
|
||||
description: "A hidden skill.",
|
||||
filePath: "/path/hidden/SKILL.md",
|
||||
baseDir: "/path/hidden",
|
||||
source: "test",
|
||||
disableModelInvocation: true,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = formatSkillsForPrompt(skills);
|
||||
@@ -351,7 +356,7 @@ describe("skills", () => {
|
||||
skillPaths: [join(fixturesDir, "valid-skill")],
|
||||
});
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].source).toBe("path");
|
||||
expect(skills[0].sourceInfo.scope).toBe("temporary");
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -415,7 +420,7 @@ describe("skills", () => {
|
||||
}
|
||||
|
||||
expect(skillMap.size).toBe(1);
|
||||
expect(skillMap.get("calendar")?.source).toBe("first");
|
||||
expect(skillMap.get("calendar")?.sourceInfo.source).toBe("first");
|
||||
expect(collisionWarnings).toHaveLength(1);
|
||||
expect(collisionWarnings[0].message).toContain("name collision");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user