From 8d4a49487a8c69f8e86f93289b94027fe64545ff Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 2 Mar 2026 22:50:08 +0100 Subject: [PATCH] fix(coding-agent): add tool promptGuidelines support fixes #1720 --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/docs/extensions.md | 8 +++++ .../examples/extensions/README.md | 2 +- .../examples/extensions/dynamic-tools.ts | 1 + .../coding-agent/src/core/agent-session.ts | 31 +++++++++++++++++ .../coding-agent/src/core/extensions/types.ts | 2 ++ .../coding-agent/src/core/system-prompt.ts | 34 ++++++++++++++----- .../test/agent-session-dynamic-tools.test.ts | 2 ++ .../coding-agent/test/system-prompt.test.ts | 24 +++++++++++++ 9 files changed, 96 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e4c5c69c..d54e5eb1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added optional `promptSnippet` to `ToolDefinition` for one-line entries in the default system prompt's `Available tools` section. Active extension tools now appear there, including tools registered dynamically after startup. +- Added optional `promptSnippet` and `promptGuidelines` to `ToolDefinition`. `promptSnippet` customizes one-line entries in the default system prompt's `Available tools` section, and `promptGuidelines` appends tool-specific bullets to the default `Guidelines` section while the tool is active. Active extension tools now appear in `Available tools`, including tools registered dynamically after startup. ### Fixed diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index fbfd35a3..7f625703 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -884,6 +884,8 @@ Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) fo Use `pi.setActiveTools()` to enable or disable tools (including dynamically added tools) at runtime. +Use `promptSnippet` to customize that tool's one-line entry in `Available tools`, and `promptGuidelines` to append tool-specific bullets to the default `Guidelines` section when the tool is active. + See [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) for a full example. ```typescript @@ -895,6 +897,7 @@ pi.registerTool({ label: "My Tool", description: "What this tool does", promptSnippet: "Summarize or transform text according to action", + promptGuidelines: ["Use this tool when the user asks to summarize previously generated text."], parameters: Type.Object({ action: StringEnum(["list", "add"] as const), text: Type.Optional(Type.String()), @@ -1285,6 +1288,8 @@ Register tools the LLM can call via `pi.registerTool()`. Tools appear in the sys Use `promptSnippet` for a short one-line entry in the `Available tools` section in the default system prompt. If omitted, pi falls back to `description`. +Use `promptGuidelines` to add tool-specific bullets to the default system prompt `Guidelines` section. These bullets are included only while the tool is active (for example, after `pi.setActiveTools([...])`). + Note: Some models are idiots and include the @ prefix in tool path arguments. Built-in tools strip a leading @ before resolving paths. If your custom tool accepts a path, normalize a leading @ as well. ### Tool Definition @@ -1299,6 +1304,9 @@ pi.registerTool({ label: "My Tool", description: "What this tool does (shown to LLM)", promptSnippet: "List or add items in the project todo list", + promptGuidelines: [ + "Use this tool for todo planning instead of direct file edits when the user asks for a task list." + ], parameters: Type.Object({ action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility text: Type.Optional(Type.String()), diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 17091392..6e9b35f9 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -33,7 +33,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI | | `questionnaire.ts` | Multi-question input with tab bar navigation between questions | | `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) | -| `dynamic-tools.ts` | Register tools after startup (`session_start`) and at runtime via command | +| `dynamic-tools.ts` | Register tools after startup (`session_start`) and at runtime via command, with prompt snippets and tool-specific prompt guidelines | | `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior | | `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) | | `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) | diff --git a/packages/coding-agent/examples/extensions/dynamic-tools.ts b/packages/coding-agent/examples/extensions/dynamic-tools.ts index d5e3e404..ec130ee2 100644 --- a/packages/coding-agent/examples/extensions/dynamic-tools.ts +++ b/packages/coding-agent/examples/extensions/dynamic-tools.ts @@ -35,6 +35,7 @@ export default function dynamicToolsExtension(pi: ExtensionAPI) { label, description: `Echo a message with prefix: ${prefix}`, promptSnippet: `Echo back user-provided text with ${prefix.trim()} prefix`, + promptGuidelines: ["Use this tool when the user asks for exact echo output."], parameters: ECHO_PARAMS, async execute(_toolCallId, params) { return { diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 0e84a830..029ce440 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -269,6 +269,7 @@ export class AgentSession { // Tool registry for extension getTools/setTools private _toolRegistry: Map = new Map(); private _toolPromptSnippets: Map = new Map(); + private _toolPromptGuidelines: Map = new Map(); // Base system prompt (without extension appends) - used to apply fresh appends each turn private _baseSystemPrompt = ""; @@ -692,14 +693,35 @@ export class AgentSession { return oneLine.length > 0 ? oneLine : undefined; } + private _normalizePromptGuidelines(guidelines: string[] | undefined): string[] { + if (!guidelines || guidelines.length === 0) { + return []; + } + + const unique = new Set(); + for (const guideline of guidelines) { + const normalized = guideline.trim(); + if (normalized.length > 0) { + unique.add(normalized); + } + } + return Array.from(unique); + } + private _rebuildSystemPrompt(toolNames: string[]): string { const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name)); const toolSnippets: Record = {}; + const promptGuidelines: string[] = []; for (const name of validToolNames) { const snippet = this._toolPromptSnippets.get(name); if (snippet) { toolSnippets[name] = snippet; } + + const toolGuidelines = this._toolPromptGuidelines.get(name); + if (toolGuidelines) { + promptGuidelines.push(...toolGuidelines); + } } const loaderSystemPrompt = this._resourceLoader.getSystemPrompt(); @@ -717,6 +739,7 @@ export class AgentSession { appendSystemPrompt, selectedTools: validToolNames, toolSnippets, + promptGuidelines, }); } @@ -2008,6 +2031,14 @@ export class AgentSession { }) .filter((entry): entry is readonly [string, string] => entry !== undefined), ); + this._toolPromptGuidelines = new Map( + allCustomTools + .map((registeredTool) => { + const guidelines = this._normalizePromptGuidelines(registeredTool.definition.promptGuidelines); + return guidelines.length > 0 ? ([registeredTool.definition.name, guidelines] as const) : undefined; + }) + .filter((entry): entry is readonly [string, string[]] => entry !== undefined), + ); const wrappedExtensionTools = this._extensionRunner ? wrapRegisteredTools(allCustomTools, this._extensionRunner) : []; diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index aa88e0ee..7aad355f 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -341,6 +341,8 @@ export interface ToolDefinition; + /** Additional guideline bullets appended to the default system prompt guidelines. */ + promptGuidelines?: string[]; /** Text to append to system prompt. */ appendSystemPrompt?: string; /** Working directory. Default: process.cwd() */ @@ -39,6 +41,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin customPrompt, selectedTools, toolSnippets, + promptGuidelines, appendSystemPrompt, cwd, contextFiles: providedContextFiles, @@ -112,6 +115,14 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin // Build guidelines based on which tools are actually available const guidelinesList: string[] = []; + const guidelinesSet = new Set(); + const addGuideline = (guideline: string): void => { + if (guidelinesSet.has(guideline)) { + return; + } + guidelinesSet.add(guideline); + guidelinesList.push(guideline); + }; const hasBash = tools.includes("bash"); const hasEdit = tools.includes("edit"); @@ -123,36 +134,43 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin // File exploration guidelines if (hasBash && !hasGrep && !hasFind && !hasLs) { - guidelinesList.push("Use bash for file operations like ls, rg, find"); + addGuideline("Use bash for file operations like ls, rg, find"); } else if (hasBash && (hasGrep || hasFind || hasLs)) { - guidelinesList.push("Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)"); + addGuideline("Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)"); } // Read before edit guideline if (hasRead && hasEdit) { - guidelinesList.push("Use read to examine files before editing. You must use this tool instead of cat or sed."); + addGuideline("Use read to examine files before editing. You must use this tool instead of cat or sed."); } // Edit guideline if (hasEdit) { - guidelinesList.push("Use edit for precise changes (old text must match exactly)"); + addGuideline("Use edit for precise changes (old text must match exactly)"); } // Write guideline if (hasWrite) { - guidelinesList.push("Use write only for new files or complete rewrites"); + addGuideline("Use write only for new files or complete rewrites"); } // Output guideline (only when actually writing or executing) if (hasEdit || hasWrite) { - guidelinesList.push( + addGuideline( "When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did", ); } + for (const guideline of promptGuidelines ?? []) { + const normalized = guideline.trim(); + if (normalized.length > 0) { + addGuideline(normalized); + } + } + // Always include these - guidelinesList.push("Be concise in your responses"); - guidelinesList.push("Show file paths clearly when working with files"); + addGuideline("Be concise in your responses"); + addGuideline("Show file paths clearly when working with files"); const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n"); diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index d9a0223e..0127bad1 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -41,6 +41,7 @@ describe("AgentSession dynamic tool registration", () => { label: "Dynamic Tool", description: "Tool registered from session_start", promptSnippet: "Run dynamic test behavior", + promptGuidelines: ["Use dynamic_tool when the user asks for dynamic behavior tests."], parameters: Type.Object({}), execute: async () => ({ content: [{ type: "text", text: "ok" }], @@ -69,6 +70,7 @@ describe("AgentSession dynamic tool registration", () => { expect(session.getAllTools().map((tool) => tool.name)).toContain("dynamic_tool"); 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."); session.dispose(); }); diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index 586bf8f8..db266296 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -52,4 +52,28 @@ describe("buildSystemPrompt", () => { expect(prompt).toContain("- dynamic_tool: Run dynamic test behavior"); }); }); + + describe("prompt guidelines", () => { + test("appends promptGuidelines to default guidelines", () => { + const prompt = buildSystemPrompt({ + selectedTools: ["read", "dynamic_tool"], + promptGuidelines: ["Use dynamic_tool for project summaries."], + contextFiles: [], + skills: [], + }); + + expect(prompt).toContain("- Use dynamic_tool for project summaries."); + }); + + test("deduplicates and trims promptGuidelines", () => { + const prompt = buildSystemPrompt({ + selectedTools: ["read", "dynamic_tool"], + promptGuidelines: ["Use dynamic_tool for summaries.", " Use dynamic_tool for summaries. ", " "], + contextFiles: [], + skills: [], + }); + + expect(prompt.match(/- Use dynamic_tool for summaries\./g)).toHaveLength(1); + }); + }); });