diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 1c245cbb4b0..5bdfe2c4f89 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -42,6 +42,11 @@ const bashSchema = Type.Object({ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })), }); +export const bashToolSystemPromptContribution = { + snippet: "Execute bash commands (ls, grep, find, etc.)", + guidelines: ["Inspect PI_* environment variables for current model and session details."], +} as const; + export type BashToolInput = Static; export interface BashToolDetails { @@ -325,10 +330,8 @@ export function createBashToolDefinition( name: "bash", label: "bash", description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`, - promptSnippet: "Execute bash commands (ls, grep, find, etc.)", - promptGuidelines: exposeSessionEnvironment - ? ["Inspect PI_* environment variables for current model and session details."] - : undefined, + promptSnippet: bashToolSystemPromptContribution.snippet, + promptGuidelines: exposeSessionEnvironment ? [...bashToolSystemPromptContribution.guidelines] : undefined, parameters: bashSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index feaa7176f8f..01fd4145391 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -52,6 +52,16 @@ const editSchema = Type.Object( {}, ); +export const editToolSystemPromptContribution = { + snippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call", + guidelines: [ + "Use edit for precise changes (edits[].oldText must match exactly)", + "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls", + "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.", + "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.", + ], +} as const; + export type EditToolInput = Static; type LegacyEditToolInput = EditToolInput & { oldText?: unknown; @@ -294,14 +304,8 @@ export function createEditToolDefinition( label: "edit", description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.", - promptSnippet: - "Make precise file edits with exact text replacement, including multiple disjoint edits in one call", - promptGuidelines: [ - "Use edit for precise changes (edits[].oldText must match exactly)", - "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls", - "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.", - "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.", - ], + promptSnippet: editToolSystemPromptContribution.snippet, + promptGuidelines: [...editToolSystemPromptContribution.guidelines], parameters: editSchema, renderShell: "self", prepareArguments: prepareEditArguments, diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index ce6caf6727b..a14f327e840 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -34,6 +34,11 @@ const findSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })), }); +export const findToolSystemPromptContribution = { + snippet: "Find files by glob pattern (respects .gitignore)", + guidelines: [], +} as const; + export type FindToolInput = Static; const DEFAULT_LIMIT = 1000; @@ -124,7 +129,7 @@ export function createFindToolDefinition( name: "find", label: "find", description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, - promptSnippet: "Find files by glob pattern (respects .gitignore)", + promptSnippet: findToolSystemPromptContribution.snippet, parameters: findSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index e4ed36d1b68..2274e706449 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -35,6 +35,11 @@ const grepSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })), }); +export const grepToolSystemPromptContribution = { + snippet: "Search file contents for patterns (respects .gitignore)", + guidelines: [], +} as const; + export type GrepToolInput = Static; const DEFAULT_LIMIT = 100; @@ -129,7 +134,7 @@ export function createGrepToolDefinition( name: "grep", label: "grep", description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`, - promptSnippet: "Search file contents for patterns (respects .gitignore)", + promptSnippet: grepToolSystemPromptContribution.snippet, parameters: grepSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index 8a689e8da2b..893e13c250d 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -16,6 +16,11 @@ const lsSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })), }); +export const lsToolSystemPromptContribution = { + snippet: "List directory contents", + guidelines: [], +} as const; + export type LsToolInput = Static; const DEFAULT_LIMIT = 500; @@ -101,7 +106,7 @@ export function createLsToolDefinition( name: "ls", label: "ls", description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, - promptSnippet: "List directory contents", + promptSnippet: lsToolSystemPromptContribution.snippet, parameters: lsSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 9442e98737b..130e6a2ea4c 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -23,6 +23,11 @@ const readSchema = Type.Object({ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })), }); +export const readToolSystemPromptContribution = { + snippet: "Read file contents", + guidelines: ["Use read to examine files instead of cat or sed."], +} as const; + export type ReadToolInput = Static; export interface ReadToolDetails { @@ -210,8 +215,8 @@ export function createReadToolDefinition( name: "read", label: "read", description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`, - promptSnippet: "Read file contents", - promptGuidelines: ["Use read to examine files instead of cat or sed."], + promptSnippet: readToolSystemPromptContribution.snippet, + promptGuidelines: [...readToolSystemPromptContribution.guidelines], parameters: readSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index 12668e61a76..876aa6d8001 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -16,6 +16,11 @@ const writeSchema = Type.Object({ content: Type.String({ description: "Content to write to the file" }), }); +export const writeToolSystemPromptContribution = { + snippet: "Create or overwrite files", + guidelines: ["Use write only for new files or complete rewrites."], +} as const; + export type WriteToolInput = Static; /** @@ -188,8 +193,8 @@ export function createWriteToolDefinition( label: "write", description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.", - promptSnippet: "Create or overwrite files", - promptGuidelines: ["Use write only for new files or complete rewrites."], + promptSnippet: writeToolSystemPromptContribution.snippet, + promptGuidelines: [...writeToolSystemPromptContribution.guidelines], parameters: writeSchema, async execute( _toolCallId, diff --git a/packages/coding-agent/test/tool-system-prompt-contributions.test.ts b/packages/coding-agent/test/tool-system-prompt-contributions.test.ts new file mode 100644 index 00000000000..4a2927c165f --- /dev/null +++ b/packages/coding-agent/test/tool-system-prompt-contributions.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "vitest"; +import { bashToolSystemPromptContribution, createBashToolDefinition } from "../src/core/tools/bash.ts"; +import { createEditToolDefinition, editToolSystemPromptContribution } from "../src/core/tools/edit.ts"; +import { createFindToolDefinition, findToolSystemPromptContribution } from "../src/core/tools/find.ts"; +import { createGrepToolDefinition, grepToolSystemPromptContribution } from "../src/core/tools/grep.ts"; +import { createLsToolDefinition, lsToolSystemPromptContribution } from "../src/core/tools/ls.ts"; +import { createReadToolDefinition, readToolSystemPromptContribution } from "../src/core/tools/read.ts"; +import { createWriteToolDefinition, writeToolSystemPromptContribution } from "../src/core/tools/write.ts"; + +const cases = [ + ["read", readToolSystemPromptContribution, createReadToolDefinition], + ["bash", bashToolSystemPromptContribution, createBashToolDefinition], + ["edit", editToolSystemPromptContribution, createEditToolDefinition], + ["write", writeToolSystemPromptContribution, createWriteToolDefinition], + ["grep", grepToolSystemPromptContribution, createGrepToolDefinition], + ["find", findToolSystemPromptContribution, createFindToolDefinition], + ["ls", lsToolSystemPromptContribution, createLsToolDefinition], +] as const; + +describe("built-in tool system prompt contributions", () => { + test.each(cases)( + "keeps the %s tool definition aligned with its contribution", + (_name, contribution, createDefinition) => { + const definition = createDefinition("/workspace"); + + expect(definition.promptSnippet).toBe(contribution.snippet); + expect(definition.promptGuidelines ?? []).toEqual(contribution.guidelines); + }, + ); + + test("keeps bash session-environment guidance conditional", () => { + const definition = createBashToolDefinition("/workspace", { exposeSessionEnvironment: false }); + + expect(definition.promptGuidelines).toBeUndefined(); + }); +});