Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/coding-agent/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,10 @@ Register tools the LLM can call via `pi.registerTool()`. Tools appear in the sys

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.

- `description` — detailed description sent to the LLM via the API tool listing (can be multi-line).
- `shortDescription` — optional short one-liner for the system prompt tool list. If provided, the tool appears in the "Available tools" section. If omitted, the tool is not listed in the system prompt (but still callable via the API tool listing).
- `systemGuidelines` — optional bullet points added to the system prompt guidelines section when the tool is active.

### Tool Definition

```typescript
Expand All @@ -1167,7 +1171,9 @@ import { Text } from "@mariozechner/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
description: "Detailed description sent to the LLM via the API tool listing. Can be multi-line.",
shortDescription: "One-liner shown in the system prompt 'Available tools' list",
systemGuidelines: ["Guideline bullet appended to system prompt Guidelines section"],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
Expand Down Expand Up @@ -1201,6 +1207,8 @@ pi.registerTool({
});
```

See the field descriptions [above](#custom-tools) for how `shortDescription`, `description`, and `systemGuidelines` map to the system prompt and API tool listing.

**Important:** Use `StringEnum` from `@mariozechner/pi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.

### Overriding Built-in Tools
Expand Down
6 changes: 5 additions & 1 deletion packages/coding-agent/docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,9 @@ import { createAgentSession, type ToolDefinition } from "@mariozechner/pi-coding
const myTool: ToolDefinition = {
name: "my_tool",
label: "My Tool",
description: "Does something useful",
description: "Detailed description sent to the LLM via the API tool listing. Can be multi-line.",
shortDescription: "One-liner shown in the system prompt 'Available tools' list",
systemGuidelines: ["Guideline bullet appended to system prompt Guidelines section"],
parameters: Type.Object({
input: Type.String({ description: "Input value" }),
}),
Expand All @@ -462,6 +464,8 @@ const { session } = await createAgentSession({
});
```

`description` is the detailed text sent to the LLM via the API tool listing. `shortDescription` is an optional one-liner — if provided, the tool appears in the system prompt "Available tools" list; if omitted, the tool is not listed in the system prompt. `systemGuidelines` optionally adds bullet points to the system prompt guidelines section.

Custom tools passed via `customTools` are combined with extension-registered tools. Extensions loaded by the ResourceLoader can also register tools via `pi.registerTool()`.

> See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)
Expand Down
46 changes: 42 additions & 4 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.j
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
import type { SettingsManager } from "./settings-manager.js";
import { BUILTIN_SLASH_COMMANDS, type SlashCommandInfo, type SlashCommandLocation } from "./slash-commands.js";
import { buildSystemPrompt } from "./system-prompt.js";
import {
BUILTIN_TOOL_ORDER,
BUILTIN_TOOL_SHORT_DESCRIPTIONS,
type BuiltinToolName,
buildSystemPrompt,
type SystemPromptToolInfo,
} from "./system-prompt.js";
import type { BashOperations } from "./tools/bash.js";
import { createAllTools } from "./tools/index.js";

Expand Down Expand Up @@ -259,6 +265,7 @@ export class AgentSession {

// Tool registry for extension getTools/setTools
private _toolRegistry: Map<string, AgentTool> = new Map();
private _systemPromptToolInfo: Map<string, SystemPromptToolInfo> = new Map();

// Base system prompt (without extension appends) - used to apply fresh appends each turn
private _baseSystemPrompt = "";
Expand Down Expand Up @@ -618,7 +625,7 @@ export class AgentSession {
}

private _rebuildSystemPrompt(toolNames: string[]): string {
const validToolNames = toolNames.filter((name) => this._baseToolRegistry.has(name));
const systemPromptTools = this._buildSystemPromptTools(toolNames);
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
const appendSystemPrompt =
Expand All @@ -632,10 +639,26 @@ export class AgentSession {
contextFiles: loadedContextFiles,
customPrompt: loaderSystemPrompt,
appendSystemPrompt,
selectedTools: validToolNames,
tools: systemPromptTools,
});
}

private _buildSystemPromptTools(toolNames: string[]): SystemPromptToolInfo[] {
const orderedNames = this._orderSystemPromptTools(toolNames);
return orderedNames.flatMap((name) => {
const toolInfo = this._systemPromptToolInfo.get(name);
return toolInfo ? [toolInfo] : [];
});
}

private _orderSystemPromptTools(toolNames: string[]): string[] {
const uniqueNames = Array.from(new Set(toolNames));
const builtinNames = new Set<string>(BUILTIN_TOOL_ORDER);
const orderedBuiltin = BUILTIN_TOOL_ORDER.filter((name) => uniqueNames.includes(name));
const customNames = uniqueNames.filter((name) => !builtinNames.has(name)).sort();
return [...orderedBuiltin, ...customNames];
}

// =========================================================================
// Prompting
// =========================================================================
Expand Down Expand Up @@ -1945,6 +1968,21 @@ export class AgentSession {
toolRegistry.set(tool.name, tool);
}

const systemPromptToolInfo = new Map<string, SystemPromptToolInfo>();
for (const [name, tool] of Object.entries(baseTools)) {
const shortDescription = BUILTIN_TOOL_SHORT_DESCRIPTIONS[name as BuiltinToolName] ?? tool.description;
systemPromptToolInfo.set(name, { name, shortDescription });
}
for (const customTool of allCustomTools) {
const { definition } = customTool;
systemPromptToolInfo.set(definition.name, {
name: definition.name,
shortDescription: definition.shortDescription,
systemGuidelines: definition.systemGuidelines,
});
}
this._systemPromptToolInfo = systemPromptToolInfo;

const defaultActiveToolNames = this._baseToolsOverride
? Object.keys(this._baseToolsOverride)
: ["read", "bash", "edit", "write"];
Expand Down Expand Up @@ -1974,7 +2012,7 @@ export class AgentSession {
this._toolRegistry = toolRegistry;
}

const systemPromptToolNames = Array.from(activeToolNameSet).filter((name) => this._baseToolRegistry.has(name));
const systemPromptToolNames = Array.from(activeToolNameSet);
this._baseSystemPrompt = this._rebuildSystemPrompt(systemPromptToolNames);
this.agent.setSystemPrompt(this._baseSystemPrompt);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/coding-agent/src/core/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,12 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
name: string;
/** Human-readable label for UI */
label: string;
/** Description for LLM */
/** Description sent to the LLM via the API tool listing (can be detailed/long) */
description: string;
/** Short one-line description for the system prompt tool list. If provided, the tool appears in the "Available tools" section of the system prompt. If omitted, the tool is not listed in the system prompt (but still available to the LLM via the API tool listing). */
shortDescription?: string;
/** Additional guideline bullets appended to the system prompt guidelines section when tool is active */
systemGuidelines?: string[];
/** Parameter schema (TypeBox) */
parameters: TParams;

Expand Down
100 changes: 77 additions & 23 deletions packages/coding-agent/src/core/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
import { formatSkillsForPrompt, type Skill } from "./skills.js";

/** Tool descriptions for system prompt */
const toolDescriptions: Record<string, string> = {
export const BUILTIN_TOOL_ORDER = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
export type BuiltinToolName = (typeof BUILTIN_TOOL_ORDER)[number];

/** Short one-liner descriptions for built-in tools, used in the system prompt tool list */
export const BUILTIN_TOOL_SHORT_DESCRIPTIONS: Record<BuiltinToolName, string> = {
read: "Read file contents",
bash: "Execute bash commands (ls, grep, find, etc.)",
edit: "Make surgical edits to files (find exact text and replace)",
Expand All @@ -16,11 +19,34 @@ const toolDescriptions: Record<string, string> = {
ls: "List directory contents",
};

export interface SystemPromptToolInfo {
/** Tool name (used in the Available tools list) */
name: string;
/** Short one-line description for the system prompt tool list. If provided, the tool is shown. If undefined, the tool is hidden. */
shortDescription?: string;
/** Additional guideline bullets appended to the system prompt guidelines section */
systemGuidelines?: string[];
}

const DEFAULT_SELECTED_TOOLS = ["read", "bash", "edit", "write"] as const;

const buildDefaultToolEntries = (selectedTools?: string[]): SystemPromptToolInfo[] => {
const toolNames = selectedTools ?? DEFAULT_SELECTED_TOOLS;
return toolNames.flatMap((name) => {
const shortDescription = BUILTIN_TOOL_SHORT_DESCRIPTIONS[name as BuiltinToolName];
return shortDescription ? [{ name, shortDescription }] : [];
});
};

export interface BuildSystemPromptOptions {
/** Custom system prompt (replaces default). */
customPrompt?: string;
/** Tools to include in prompt. Default: [read, bash, edit, write] */
selectedTools?: string[];
/** Full tool info for the system prompt (preferred over selectedTools). */
tools?: SystemPromptToolInfo[];
/** Additional guideline bullets appended to the base guidelines. */
systemGuidelines?: string[];
/** Text to append to system prompt. */
appendSystemPrompt?: string;
/** Working directory. Default: process.cwd() */
Expand All @@ -36,6 +62,8 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const {
customPrompt,
selectedTools,
tools: providedTools,
systemGuidelines,
appendSystemPrompt,
cwd,
contextFiles: providedContextFiles,
Expand All @@ -59,6 +87,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin

const contextFiles = providedContextFiles ?? [];
const skills = providedSkills ?? [];
const tools = providedTools ?? buildDefaultToolEntries(selectedTools);
const toolNames = tools.map((tool) => tool.name);
const toolSystemGuidelines = tools.flatMap((tool) => tool.systemGuidelines ?? []);

if (customPrompt) {
let prompt = customPrompt;
Expand All @@ -77,7 +108,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
}

// Append skills section (only if read tool is available)
const customPromptHasRead = !selectedTools || selectedTools.includes("read");
const customPromptHasRead = toolNames.includes("read");
if (customPromptHasRead && skills.length > 0) {
prompt += formatSkillsForPrompt(skills);
}
Expand All @@ -94,57 +125,80 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const docsPath = getDocsPath();
const examplesPath = getExamplesPath();

// Build tools list based on selected tools (only built-in tools with known descriptions)
const tools = (selectedTools || ["read", "bash", "edit", "write"]).filter((t) => t in toolDescriptions);
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n") : "(none)";
// Build tools list based on provided tool info (short one-liners)
const visibleTools = tools.filter((tool) => tool.shortDescription != null);
const toolsList =
visibleTools.length > 0
? visibleTools.map((tool) => `- ${tool.name}: ${tool.shortDescription}`).join("\n")
: "(none)";

// Build guidelines based on which tools are actually available
const guidelinesList: string[] = [];

const hasBash = tools.includes("bash");
const hasEdit = tools.includes("edit");
const hasWrite = tools.includes("write");
const hasGrep = tools.includes("grep");
const hasFind = tools.includes("find");
const hasLs = tools.includes("ls");
const hasRead = tools.includes("read");
const guidelinesSet = new Set<string>();
const addGuideline = (guideline: string): void => {
if (!guideline || guidelinesSet.has(guideline)) {
return;
}
guidelinesSet.add(guideline);
guidelinesList.push(guideline);
};

const hasBash = toolNames.includes("bash");
const hasEdit = toolNames.includes("edit");
const hasWrite = toolNames.includes("write");
const hasGrep = toolNames.includes("grep");
const hasFind = toolNames.includes("find");
const hasLs = toolNames.includes("ls");
const hasRead = toolNames.includes("read");

// 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 systemGuidelines ?? []) {
addGuideline(guideline);
}

for (const guideline of toolSystemGuidelines) {
addGuideline(guideline);
}

// 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");

let prompt = `You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
const baseDescription =
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
const description = baseDescription;

let prompt = `${description}

Available tools:
${toolsList}
Expand Down
Loading