From f13e6a88ab50abf90e28a929c5673a2b61bf2863 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 9 May 2026 19:55:48 +0200 Subject: [PATCH] test(agent): pin harness resource formatting --- .../agent/src/harness/prompt-templates.ts | 2 +- packages/agent/src/harness/system-prompt.ts | 2 +- packages/agent/src/harness/types.ts | 19 ++-- ...on.test.ts => resource-formatting.test.ts} | 0 .../agent/test/harness/system-prompt.test.ts | 89 ++++++++++++------- 5 files changed, 69 insertions(+), 43 deletions(-) rename packages/agent/test/harness/{resource-expansion.test.ts => resource-formatting.test.ts} (100%) diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index c7c06ccd..988008e9 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -169,7 +169,7 @@ function errorMessage(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; } -/** Parse slash-command arguments using simple shell-style single and double quotes. */ +/** Parse an argument string using simple shell-style single and double quotes. */ export function parseCommandArgs(argsString: string): string[] { const args: string[] = []; let current = ""; diff --git a/packages/agent/src/harness/system-prompt.ts b/packages/agent/src/harness/system-prompt.ts index 7a1d1542..44b8f623 100644 --- a/packages/agent/src/harness/system-prompt.ts +++ b/packages/agent/src/harness/system-prompt.ts @@ -6,7 +6,7 @@ export function formatSkillsForSystemPrompt(skills: Skill[]): string { const lines = [ "The following skills provide specialized instructions for specific tasks.", - "Use the read tool to load a skill's file when the task matches its description.", + "Read the full skill file when the task matches its description.", "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", "", "", diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 2e93ef32..5c768d78 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -2,9 +2,14 @@ import type { ImageContent, Model, TextContent } from "@earendil-works/pi-ai"; import type { AgentEvent, AgentMessage, AgentTool, ThinkingLevel } from "../index.js"; import type { Session } from "./session/session.js"; -/** Model-visible skill loaded from a `SKILL.md` file or provided by an application. */ +/** + * Skill loaded from a `SKILL.md` file or provided by an application. + * + * `name`, `description`, and `filePath` are inserted into the system prompt in an XML-formatted block as suggested by agentskills.io. + * Use {@link formatSkillsForSystemPrompt} to generate the spec-compatible system prompt block. + */ export interface Skill { - /** Skill command name. */ + /** Stable skill name used for lookup and model-visible listings. */ name: string; /** Short model-visible description of when to use the skill. */ description: string; @@ -16,19 +21,19 @@ export interface Skill { disableModelInvocation?: boolean; } -/** Prompt template that can expand slash-command text before a prompt is sent. */ +/** Prompt template that can be formatted into a prompt for explicit invocation. */ export interface PromptTemplate { - /** Slash-command name without the leading `/`. */ + /** Stable template name used for lookup or application command routing. */ name: string; /** Optional description for command lists or autocomplete. */ description?: string; - /** Template content. Argument placeholders are expanded by `formatPromptTemplateInvocation`. */ + /** Template content. Argument placeholders are formatted by `formatPromptTemplateInvocation`. */ content: string; } -/** Resources made available to harness prompt expansion and system-prompt callbacks. */ +/** Resources made available to explicit invocation methods and system-prompt callbacks. */ export interface AgentHarnessResources { - /** Prompt templates used to expand `/template args` input. */ + /** Prompt templates available for explicit invocation. */ promptTemplates?: PromptTemplate[]; /** Skills available to the model and explicit skill invocation. */ skills?: Skill[]; diff --git a/packages/agent/test/harness/resource-expansion.test.ts b/packages/agent/test/harness/resource-formatting.test.ts similarity index 100% rename from packages/agent/test/harness/resource-expansion.test.ts rename to packages/agent/test/harness/resource-formatting.test.ts diff --git a/packages/agent/test/harness/system-prompt.test.ts b/packages/agent/test/harness/system-prompt.test.ts index 5e9dc8e6..46ce884b 100644 --- a/packages/agent/test/harness/system-prompt.test.ts +++ b/packages/agent/test/harness/system-prompt.test.ts @@ -1,45 +1,66 @@ import { describe, expect, it } from "vitest"; import { formatSkillsForSystemPrompt } from "../../src/harness/system-prompt.js"; +const visibleSkill = { + name: "visible", + description: "Use & that", + content: "visible content", + filePath: "/skills/visible/SKILL.md", +}; + +const secondSkill = { + name: "second", + description: "Second skill", + content: "second content", + filePath: "/skills/second/SKILL.md", +}; + +const disabledSkill = { + name: "hidden", + description: "Hidden", + content: "hidden content", + filePath: "/skills/hidden/SKILL.md", + disableModelInvocation: true, +}; + describe("formatSkillsForSystemPrompt", () => { - it("formats visible skills and skips model-disabled skills", () => { + it("formats visible skills in order and skips model-disabled skills", () => { + expect(formatSkillsForSystemPrompt([visibleSkill, disabledSkill, secondSkill])).toBe( + `The following skills provide specialized instructions for specific tasks. +Read the full skill file when the task matches its description. +When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands. + + + + visible + Use <this> & that + /skills/visible/SKILL.md + + + second + Second skill + /skills/second/SKILL.md + +`, + ); + }); + + it("returns an empty string when no skills are model-visible", () => { + expect(formatSkillsForSystemPrompt([disabledSkill])).toBe(""); + }); + + it("escapes XML in all model-visible skill fields", () => { expect( formatSkillsForSystemPrompt([ { - name: "visible", - description: "Use & that", - content: "visible content", - filePath: "/skills/visible/SKILL.md", - }, - { - name: "hidden", - description: "Hidden", - content: "hidden content", - filePath: "/skills/hidden/SKILL.md", - disableModelInvocation: true, + name: "a&b", + description: `Quote "double" and 'single'`, + content: "content", + filePath: '/skills/&"quote"/SKILL.md', }, ]), - ).toContain("visible"); - expect( - formatSkillsForSystemPrompt([ - { - name: "visible", - description: "Use & that", - content: "visible content", - filePath: "/skills/visible/SKILL.md", - }, - ]), - ).toContain("Use <this> & that"); - expect( - formatSkillsForSystemPrompt([ - { - name: "hidden", - description: "Hidden", - content: "hidden content", - filePath: "/skills/hidden/SKILL.md", - disableModelInvocation: true, - }, - ]), - ).toBe(""); + ).toContain( + "a&b\n Quote "double" and 'single'\n /skills/<bad>&"quote"/SKILL.md", + ); }); });