refactor(agent): make resource invocation explicit
This commit is contained in:
@@ -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<AssistantMessage> {
|
||||
async skill(name: string, additionalInstructions?: string): Promise<AssistantMessage> {
|
||||
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<AssistantMessage> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${dirnameEnvPath(skill.filePath)}.\n\n${skill.content}\n</skill>`;
|
||||
return additionalInstructions ? `${skillBlock}\n\n${additionalInstructions}` : skillBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from one or more directories.
|
||||
*
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
24
packages/agent/test/harness/resource-expansion.test.ts
Normal file
24
packages/agent/test/harness/resource-expansion.test.ts
Normal file
@@ -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(
|
||||
'<skill name="inspect" location="/project/.pi/skills/inspect/SKILL.md">\nReferences are relative to /project/.pi/skills/inspect.\n\nUse inspection tools.\n</skill>\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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user