mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 19:00:44 +00:00
fix(coding-agent): add tool promptGuidelines support fixes #1720
This commit is contained in:
parent
bc2fa8d6d0
commit
8d4a49487a
9 changed files with 96 additions and 10 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
### Added
|
### 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
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 `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.
|
See [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) for a full example.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -895,6 +897,7 @@ pi.registerTool({
|
||||||
label: "My Tool",
|
label: "My Tool",
|
||||||
description: "What this tool does",
|
description: "What this tool does",
|
||||||
promptSnippet: "Summarize or transform text according to action",
|
promptSnippet: "Summarize or transform text according to action",
|
||||||
|
promptGuidelines: ["Use this tool when the user asks to summarize previously generated text."],
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
action: StringEnum(["list", "add"] as const),
|
action: StringEnum(["list", "add"] as const),
|
||||||
text: Type.Optional(Type.String()),
|
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 `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.
|
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
|
### Tool Definition
|
||||||
|
|
@ -1299,6 +1304,9 @@ pi.registerTool({
|
||||||
label: "My Tool",
|
label: "My Tool",
|
||||||
description: "What this tool does (shown to LLM)",
|
description: "What this tool does (shown to LLM)",
|
||||||
promptSnippet: "List or add items in the project todo list",
|
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({
|
parameters: Type.Object({
|
||||||
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
|
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
|
||||||
text: Type.Optional(Type.String()),
|
text: Type.Optional(Type.String()),
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `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 |
|
| `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`) |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export default function dynamicToolsExtension(pi: ExtensionAPI) {
|
||||||
label,
|
label,
|
||||||
description: `Echo a message with prefix: ${prefix}`,
|
description: `Echo a message with prefix: ${prefix}`,
|
||||||
promptSnippet: `Echo back user-provided text with ${prefix.trim()} 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,
|
parameters: ECHO_PARAMS,
|
||||||
async execute(_toolCallId, params) {
|
async execute(_toolCallId, params) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ export class AgentSession {
|
||||||
// Tool registry for extension getTools/setTools
|
// Tool registry for extension getTools/setTools
|
||||||
private _toolRegistry: Map<string, AgentTool> = new Map();
|
private _toolRegistry: Map<string, AgentTool> = new Map();
|
||||||
private _toolPromptSnippets: Map<string, string> = new Map();
|
private _toolPromptSnippets: Map<string, string> = new Map();
|
||||||
|
private _toolPromptGuidelines: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
||||||
private _baseSystemPrompt = "";
|
private _baseSystemPrompt = "";
|
||||||
|
|
@ -692,14 +693,35 @@ export class AgentSession {
|
||||||
return oneLine.length > 0 ? oneLine : undefined;
|
return oneLine.length > 0 ? oneLine : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _normalizePromptGuidelines(guidelines: string[] | undefined): string[] {
|
||||||
|
if (!guidelines || guidelines.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const unique = new Set<string>();
|
||||||
|
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 {
|
private _rebuildSystemPrompt(toolNames: string[]): string {
|
||||||
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
||||||
const toolSnippets: Record<string, string> = {};
|
const toolSnippets: Record<string, string> = {};
|
||||||
|
const promptGuidelines: string[] = [];
|
||||||
for (const name of validToolNames) {
|
for (const name of validToolNames) {
|
||||||
const snippet = this._toolPromptSnippets.get(name);
|
const snippet = this._toolPromptSnippets.get(name);
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
toolSnippets[name] = snippet;
|
toolSnippets[name] = snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolGuidelines = this._toolPromptGuidelines.get(name);
|
||||||
|
if (toolGuidelines) {
|
||||||
|
promptGuidelines.push(...toolGuidelines);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
||||||
|
|
@ -717,6 +739,7 @@ export class AgentSession {
|
||||||
appendSystemPrompt,
|
appendSystemPrompt,
|
||||||
selectedTools: validToolNames,
|
selectedTools: validToolNames,
|
||||||
toolSnippets,
|
toolSnippets,
|
||||||
|
promptGuidelines,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2008,6 +2031,14 @@ export class AgentSession {
|
||||||
})
|
})
|
||||||
.filter((entry): entry is readonly [string, string] => entry !== undefined),
|
.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
|
const wrappedExtensionTools = this._extensionRunner
|
||||||
? wrapRegisteredTools(allCustomTools, this._extensionRunner)
|
? wrapRegisteredTools(allCustomTools, this._extensionRunner)
|
||||||
: [];
|
: [];
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
||||||
description: string;
|
description: string;
|
||||||
/** Optional one-line snippet for the Available tools section in the default system prompt. Falls back to description when omitted. */
|
/** Optional one-line snippet for the Available tools section in the default system prompt. Falls back to description when omitted. */
|
||||||
promptSnippet?: string;
|
promptSnippet?: string;
|
||||||
|
/** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
|
||||||
|
promptGuidelines?: string[];
|
||||||
/** Parameter schema (TypeBox) */
|
/** Parameter schema (TypeBox) */
|
||||||
parameters: TParams;
|
parameters: TParams;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export interface BuildSystemPromptOptions {
|
||||||
selectedTools?: string[];
|
selectedTools?: string[];
|
||||||
/** Optional one-line tool snippets keyed by tool name. */
|
/** Optional one-line tool snippets keyed by tool name. */
|
||||||
toolSnippets?: Record<string, string>;
|
toolSnippets?: Record<string, string>;
|
||||||
|
/** Additional guideline bullets appended to the default system prompt guidelines. */
|
||||||
|
promptGuidelines?: string[];
|
||||||
/** Text to append to system prompt. */
|
/** Text to append to system prompt. */
|
||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
/** Working directory. Default: process.cwd() */
|
/** Working directory. Default: process.cwd() */
|
||||||
|
|
@ -39,6 +41,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
||||||
customPrompt,
|
customPrompt,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
toolSnippets,
|
toolSnippets,
|
||||||
|
promptGuidelines,
|
||||||
appendSystemPrompt,
|
appendSystemPrompt,
|
||||||
cwd,
|
cwd,
|
||||||
contextFiles: providedContextFiles,
|
contextFiles: providedContextFiles,
|
||||||
|
|
@ -112,6 +115,14 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
||||||
|
|
||||||
// Build guidelines based on which tools are actually available
|
// Build guidelines based on which tools are actually available
|
||||||
const guidelinesList: string[] = [];
|
const guidelinesList: string[] = [];
|
||||||
|
const guidelinesSet = new Set<string>();
|
||||||
|
const addGuideline = (guideline: string): void => {
|
||||||
|
if (guidelinesSet.has(guideline)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
guidelinesSet.add(guideline);
|
||||||
|
guidelinesList.push(guideline);
|
||||||
|
};
|
||||||
|
|
||||||
const hasBash = tools.includes("bash");
|
const hasBash = tools.includes("bash");
|
||||||
const hasEdit = tools.includes("edit");
|
const hasEdit = tools.includes("edit");
|
||||||
|
|
@ -123,36 +134,43 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
||||||
|
|
||||||
// File exploration guidelines
|
// File exploration guidelines
|
||||||
if (hasBash && !hasGrep && !hasFind && !hasLs) {
|
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)) {
|
} 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
|
// Read before edit guideline
|
||||||
if (hasRead && hasEdit) {
|
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
|
// Edit guideline
|
||||||
if (hasEdit) {
|
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
|
// Write guideline
|
||||||
if (hasWrite) {
|
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)
|
// Output guideline (only when actually writing or executing)
|
||||||
if (hasEdit || hasWrite) {
|
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",
|
"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
|
// Always include these
|
||||||
guidelinesList.push("Be concise in your responses");
|
addGuideline("Be concise in your responses");
|
||||||
guidelinesList.push("Show file paths clearly when working with files");
|
addGuideline("Show file paths clearly when working with files");
|
||||||
|
|
||||||
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
|
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ describe("AgentSession dynamic tool registration", () => {
|
||||||
label: "Dynamic Tool",
|
label: "Dynamic Tool",
|
||||||
description: "Tool registered from session_start",
|
description: "Tool registered from session_start",
|
||||||
promptSnippet: "Run dynamic test behavior",
|
promptSnippet: "Run dynamic test behavior",
|
||||||
|
promptGuidelines: ["Use dynamic_tool when the user asks for dynamic behavior tests."],
|
||||||
parameters: Type.Object({}),
|
parameters: Type.Object({}),
|
||||||
execute: async () => ({
|
execute: async () => ({
|
||||||
content: [{ type: "text", text: "ok" }],
|
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.getAllTools().map((tool) => tool.name)).toContain("dynamic_tool");
|
||||||
expect(session.getActiveToolNames()).toContain("dynamic_tool");
|
expect(session.getActiveToolNames()).toContain("dynamic_tool");
|
||||||
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
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();
|
session.dispose();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,28 @@ describe("buildSystemPrompt", () => {
|
||||||
expect(prompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue