From e1647aaa00489ac74e80cf2d320e8360a0e83d68 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 8 May 2026 13:35:54 +0200 Subject: [PATCH] refactor(agent): make resource invocation explicit --- packages/agent/src/harness/agent-harness.ts | 32 +++++++------------ .../agent/src/harness/prompt-templates.ts | 12 ++----- packages/agent/src/harness/skills.ts | 6 ++++ .../test/harness/prompt-templates.test.ts | 2 +- .../test/harness/resource-expansion.test.ts | 24 ++++++++++++++ 5 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 packages/agent/test/harness/resource-expansion.test.ts diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index f5ca4e84..4c042e1f 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -5,6 +5,7 @@ import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../type import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js"; import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js"; import { expandPromptTemplate } from "./prompt-templates.js"; +import { expandSkillCommand } from "./skills.js"; import type { AbortResult, AgentHarnessContext, @@ -18,7 +19,6 @@ import type { ExecutionEnv, NavigateTreeResult, Session, - Skill, } from "./types.js"; function createUserMessage(text: string, images?: ImageContent[]): AgentMessage { @@ -297,11 +297,7 @@ export class AgentHarness { this.operation.idle = false; this.operation.liveOperationId = randomUUID(); const resources = await this.resolveResources(this.agent.signal); - const expanded = this.expandSkillCommand( - expandPromptTemplate(text, resources.promptTemplates ?? []), - resources.skills ?? [], - ); - let messages: AgentMessage[] = [createUserMessage(expanded, options?.images)]; + let messages: AgentMessage[] = [createUserMessage(text, options?.images)]; if (this.conversation.nextTurnQueue.length > 0) { messages = [messages[0]!, ...this.conversation.nextTurnQueue]; this.conversation.nextTurnQueue = []; @@ -312,7 +308,7 @@ export class AgentHarness { "before_agent_start", { type: "before_agent_start", - prompt: expanded, + prompt: text, images: options?.images, systemPrompt: this.agent.state.systemPrompt, resources, @@ -335,12 +331,18 @@ export class AgentHarness { return response; } - async skill(name: string, args?: string): Promise { + async skill(name: string, additionalInstructions?: string): Promise { const resources = await this.resolveResources(); const skill = (resources.skills ?? []).find((candidate) => candidate.name === name); if (!skill) throw new Error(`Unknown skill: ${name}`); - const prompt = args ? `${skill.content}\n\n${args}` : skill.content; - return await this.prompt(prompt); + return await this.prompt(expandSkillCommand(skill, additionalInstructions)); + } + + async promptFromTemplate(name: string, args: string[] = []): Promise { + const resources = await this.resolveResources(); + const template = (resources.promptTemplates ?? []).find((candidate) => candidate.name === name); + if (!template) throw new Error(`Unknown prompt template: ${name}`); + return await this.prompt(expandPromptTemplate(template, args)); } steer(message: AgentMessage): void { @@ -601,14 +603,4 @@ export class AgentHarness { handlers.add(handler as any); return () => handlers!.delete(handler as any); } - - private expandSkillCommand(text: string, skills: Skill[]): string { - if (!text.startsWith("/skill:")) return text; - const spaceIndex = text.indexOf(" "); - const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex); - const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim(); - const skill = skills.find((candidate) => candidate.name === skillName); - if (!skill) return text; - return args ? `${skill.content}\n\n${args}` : skill.content; - } } diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index 442c220a..24227a19 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -211,13 +211,7 @@ export function substituteArgs(content: string, args: string[]): string { return result; } -/** Expand `/template args` text using the matching prompt template, or return the original text when no template matches. */ -export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string { - if (!text.startsWith("/")) return text; - const spaceIndex = text.indexOf(" "); - const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1); - const template = templates.find((candidate) => candidate.name === commandName); - if (!template) return text; - return substituteArgs(template.content, parseCommandArgs(argsString)); +/** Expand a prompt template with positional command arguments. */ +export function expandPromptTemplate(template: PromptTemplate, args: string[] = []): string { + return substituteArgs(template.content, args); } diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index d93fd87b..27b852bb 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -25,6 +25,12 @@ interface SkillFrontmatter { [key: string]: unknown; } +/** Expand a skill into a prompt, optionally appending additional user instructions. */ +export function expandSkillCommand(skill: Skill, additionalInstructions?: string): string { + const skillBlock = `\nReferences are relative to ${dirnameEnvPath(skill.filePath)}.\n\n${skill.content}\n`; + return additionalInstructions ? `${skillBlock}\n\n${additionalInstructions}` : skillBlock; +} + /** * Load skills from one or more directories. * diff --git a/packages/agent/test/harness/prompt-templates.test.ts b/packages/agent/test/harness/prompt-templates.test.ts index c774c565..1d2c7eb8 100644 --- a/packages/agent/test/harness/prompt-templates.test.ts +++ b/packages/agent/test/harness/prompt-templates.test.ts @@ -83,7 +83,7 @@ describe("loadPromptTemplates", () => { describe("expandPromptTemplate", () => { it("substitutes command arguments", () => { const content = "$1 $" + "{@:2} $ARGUMENTS"; - expect(expandPromptTemplate('/one "hello world" test', [{ name: "one", content }])).toBe( + expect(expandPromptTemplate({ name: "one", content }, ["hello world", "test"])).toBe( "hello world test hello world test", ); }); diff --git a/packages/agent/test/harness/resource-expansion.test.ts b/packages/agent/test/harness/resource-expansion.test.ts new file mode 100644 index 00000000..74bf95b5 --- /dev/null +++ b/packages/agent/test/harness/resource-expansion.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { expandPromptTemplate } from "../../src/harness/prompt-templates.js"; +import { expandSkillCommand } from "../../src/harness/skills.js"; + +describe("resource expansion helpers", () => { + it("expands skills with additional instructions", () => { + const skill = { + name: "inspect", + description: "Inspect things", + content: "Use inspection tools.", + filePath: "/project/.pi/skills/inspect/SKILL.md", + }; + + expect(expandSkillCommand(skill, "Check errors.")).toBe( + '\nReferences are relative to /project/.pi/skills/inspect.\n\nUse inspection tools.\n\n\nCheck errors.', + ); + }); + + it("expands prompt templates with positional arguments", () => { + expect(expandPromptTemplate({ name: "review", content: "Review $1 with $ARGUMENTS" }, ["a.ts", "care"])).toBe( + "Review a.ts with a.ts care", + ); + }); +});