From 5e5bdadbf9f94b141735785487d886ece7da1342 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 17 Dec 2025 21:27:28 +0100 Subject: [PATCH] Improve system prompt docs, clean up theme/skills/hooks docs, fix toolResults type - System prompt: clearer pointers to specific doc files - theme.md: added thinkingXhigh, bashMode tokens, fixed Theme class methods - skills.md: rewrote with better framing, examples, and skill repositories - hooks.md: fixed timeout/error handling docs, added custom tool interception note - Breaking: turn_end event toolResults changed from AppMessage[] to ToolResultMessage[] --- packages/agent/src/types.ts | 3 +- packages/coding-agent/CHANGELOG.md | 10 + packages/coding-agent/README.md | 63 ++---- packages/coding-agent/docs/hooks.md | 44 ++-- packages/coding-agent/docs/skills.md | 200 +++++++++++++----- packages/coding-agent/docs/theme.md | 44 ++-- packages/coding-agent/src/core/hooks/types.ts | 3 +- .../coding-agent/src/core/system-prompt.ts | 6 +- 8 files changed, 232 insertions(+), 141 deletions(-) diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 231a4110..ad4da188 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -4,6 +4,7 @@ import type { AssistantMessageEvent, Message, Model, + ToolResultMessage, UserMessage, } from "@mariozechner/pi-ai"; @@ -87,7 +88,7 @@ export type AgentEvent = | { type: "agent_end"; messages: AppMessage[] } // Turn lifecycle - a turn is one assistant response + any tool calls/results | { type: "turn_start" } - | { type: "turn_end"; message: AppMessage; toolResults: AppMessage[] } + | { type: "turn_end"; message: AppMessage; toolResults: ToolResultMessage[] } // Message lifecycle - emitted for user, assistant, and toolResult messages | { type: "message_start"; message: AppMessage } // Only emitted for assistant messages during streaming diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f56e413..e10c392e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Changed + +- Improved system prompt documentation section with clearer pointers to specific doc files for custom models, themes, skills, hooks, custom tools, and RPC. + +- Cleaned up documentation: `theme.md` (added missing color tokens), `skills.md` (rewrote with better framing and examples), `hooks.md` (fixed timeout/error handling docs, added examples). + +### Breaking Changes + +- **Hooks**: `turn_end` event's `toolResults` type changed from `AppMessage[]` to `ToolResultMessage[]`. If you have hooks that handle `turn_end` events and explicitly type the results, update your type annotations. + ## [0.23.2] - 2025-12-17 ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 822e8207..72fd2aa7 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -488,66 +488,41 @@ Usage: `/component Button "onClick handler" "disabled support"` ### Skills -Skills are instruction files loaded on-demand when tasks match their descriptions. Compatible with Claude Code and Codex CLI skill formats. +Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. Skills are loaded when the agent decides a task matches the description, or when you explicitly ask to use one. **Skill locations:** -- Pi user: `~/.pi/agent/skills/**/SKILL.md` (recursive, colon-separated names) -- Pi project: `.pi/skills/**/SKILL.md` (recursive, colon-separated names) -- Claude Code user: `~/.claude/skills/*/SKILL.md` (one level) -- Claude Code project: `.claude/skills/*/SKILL.md` (one level) +- Pi user: `~/.pi/agent/skills/**/SKILL.md` (recursive) +- Pi project: `.pi/skills/**/SKILL.md` (recursive) +- Claude Code: `~/.claude/skills/*/SKILL.md` and `.claude/skills/*/SKILL.md` - Codex CLI: `~/.codex/skills/**/SKILL.md` (recursive) -Later locations win on name collisions (Pi skills override Claude/Codex). - -Pi skills in subdirectories use colon-separated names: `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate` - **Format:** ```markdown --- -description: Extract text and tables from PDF files +description: Web search via Brave Search API. Use for documentation, facts, or web content. --- -# PDF Processing +# Brave Search -Use `pdftotext` for plain text extraction. -For tables, use `tabula-py`. +## Setup +\`\`\`bash +cd {baseDir} && npm install +\`\`\` -Helper scripts: {baseDir}/scripts/ +## Usage +\`\`\`bash +{baseDir}/search.js "query" # Basic search +{baseDir}/search.js "query" --content # Include page content +\`\`\` ``` -- `description`: Required. Shown in system prompt for agent to decide when to load. -- `name`: Optional. Overrides directory name. -- `{baseDir}`: Placeholder for the skill's directory. Agent substitutes it when following instructions. +- `description`: Required. Determines when the skill is loaded. +- `{baseDir}`: Placeholder for the skill's directory. -**How it works:** +**Disable skills:** `pi --no-skills` or set `skills.enabled: false` in settings. -Skills are listed in the system prompt with descriptions: - -``` - -- pdf-extract: Extract text and tables from PDF files - File: ~/.pi/agent/skills/pdf-extract/SKILL.md - Base directory: ~/.pi/agent/skills/pdf-extract - -``` - -Agent uses `read` tool to load full instructions when needed. - -**Disable skills:** - -CLI: `pi --no-skills` - -Settings (`~/.pi/agent/settings.json`): -```json -{ - "skills": { - "enabled": false - } -} -``` - -See [docs/skills.md](docs/skills.md) for details. +See [docs/skills.md](docs/skills.md) for details, examples, and links to skill repositories. ### Hooks diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index c08f1bd3..768f5f15 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -2,6 +2,15 @@ Hooks are TypeScript modules that extend the coding agent's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user for input, modify results, and more. +**Example use cases:** +- Block dangerous commands (permission gates for `rm -rf`, `sudo`, etc.) +- Checkpoint code state (git stash at each turn, restore on `/branch`) +- Protect paths (block writes to `.env`, `node_modules/`, etc.) +- Modify tool output (filter or transform results before the LLM sees them) +- Inject messages from external sources (file watchers, webhooks, CI systems) + +See [examples/hooks/](../examples/hooks/) for working implementations. + ## Hook Locations Hooks are automatically discovered from two locations: @@ -33,7 +42,7 @@ You can also add explicit hook paths in `~/.pi/agent/settings.json`: ``` - `hooks`: Additional hook file paths (supports `~` expansion) -- `hookTimeout`: Timeout in milliseconds for non-interactive hook operations (default: 30000) +- `hookTimeout`: Timeout in milliseconds for hook operations (default: 30000). Does not apply to `tool_call` events, which have no timeout since they may prompt the user. ## Writing a Hook @@ -51,23 +60,17 @@ export default function (pi: HookAPI) { ### Setup -Create a hooks directory and initialize it: +Create a hooks directory: ```bash # Global hooks mkdir -p ~/.pi/agent/hooks -cd ~/.pi/agent/hooks -npm init -y -npm install @mariozechner/pi-coding-agent # Or project-local hooks mkdir -p .pi/hooks -cd .pi/hooks -npm init -y -npm install @mariozechner/pi-coding-agent ``` -Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation. +Then create `.ts` files directly in these directories. Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation. The import from `@mariozechner/pi-coding-agent/hooks` resolves to the globally installed package automatically. ## Events @@ -121,7 +124,7 @@ Fired on startup and when session changes. ```typescript pi.on("session", async (event, ctx) => { // event.entries: SessionEntry[] - all session entries - // event.sessionFile: string | null - current session file + // event.sessionFile: string | null - current session file (null with --no-session) // event.previousSessionFile: string | null - previous session file // event.reason: "start" | "switch" | "clear" }); @@ -171,24 +174,24 @@ pi.on("turn_start", async (event, ctx) => { pi.on("turn_end", async (event, ctx) => { // event.turnIndex: number // event.message: AppMessage - assistant's response - // event.toolResults: AppMessage[] + // event.toolResults: ToolResultMessage[] - tool results from this turn }); ``` ### tool_call -Fired before tool executes. **Can block.** +Fired before tool executes. **Can block.** No timeout (user prompts can take any time). ```typescript pi.on("tool_call", async (event, ctx) => { - // event.toolName: "bash" | "read" | "write" | "edit" | "ls" | "find" | "grep" + // event.toolName: string (built-in or custom tool name) // event.toolCallId: string // event.input: Record return { block: true, reason: "..." }; // or undefined to allow }); ``` -Tool inputs: +Built-in tool inputs: - `bash`: `{ command, timeout? }` - `read`: `{ path, offset?, limit? }` - `write`: `{ path, content }` @@ -197,6 +200,8 @@ Tool inputs: - `find`: `{ pattern, path?, limit? }` - `grep`: `{ pattern, path?, glob?, ignoreCase?, literal?, context?, limit? }` +Custom tools are also intercepted with their own names and input schemas. + ### tool_result Fired after tool executes. **Can modify result.** @@ -274,7 +279,7 @@ console.log(`Working in: ${ctx.cwd}`); ### ctx.sessionFile -Path to the session file, or `null` if running with `--no-session`. +Path to the current session file, or `null` when running with `--no-session` (ephemeral mode). ```typescript if (ctx.sessionFile) { @@ -490,7 +495,8 @@ In print mode, `select()` returns `null`, `confirm()` returns `false`, and `inpu ## Error Handling - If a hook throws an error, it's logged and the agent continues -- If a `tool_call` hook errors or times out, the tool is **blocked** (fail-safe) +- If a `tool_call` hook throws an error, the tool is **blocked** (fail-safe) +- Other events have a timeout (default 30s); timeout errors are logged but don't block - Hook errors are displayed in the UI with the hook path and error message ## Debugging @@ -563,9 +569,9 @@ class HookRunner { Key behaviors: - `emit()` has a timeout (default 30s) for safety -- `emitToolCall()` has **no timeout** (user prompts can take any amount of time) -- Errors in `emit()` are caught and reported via `onError()` -- Errors in `emitToolCall()` propagate (causing tool to be blocked) +- `emitToolCall()` has **no timeout** (user prompts can take any time) +- Errors in `emit()` are caught, logged via `onError()`, and execution continues +- Errors in `emitToolCall()` propagate, causing the tool to be blocked (fail-safe) ## Event Flow diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 210886eb..745906f7 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -1,72 +1,190 @@ # Skills -Skills are instruction files that the agent loads on-demand for specific tasks. +Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks. -## Skill Locations +**Example use cases:** +- Web search and content extraction (Brave Search API) +- Browser automation via Chrome DevTools Protocol +- Google Calendar, Gmail, Drive integration +- PDF/DOCX processing and creation +- Speech-to-text transcription +- YouTube transcript extraction -Skills are discovered from these locations (in order of priority, later wins on name collision): +See [Skill Repositories](#skill-repositories) for ready-to-use skills. -1. `~/.codex/skills/**/SKILL.md` (Codex CLI user skills, recursive) -2. `~/.claude/skills/*/SKILL.md` (Claude Code user skills) -3. `/.claude/skills/*/SKILL.md` (Claude Code project skills) -4. `~/.pi/agent/skills/**/SKILL.md` (Pi user skills, recursive) -5. `/.pi/skills/**/SKILL.md` (Pi project skills, recursive) +## When to Use Skills -Skill names and descriptions are listed in the system prompt. When a task matches a skill's description, the agent uses the `read` tool to load it. +| Need | Solution | +|------|----------| +| Always-needed context (conventions, commands) | AGENTS.md | +| User triggers a specific prompt template | Slash command | +| Additional tool directly callable by the LLM (like read/write/edit/bash) | Custom tool | +| On-demand capability package (workflows, scripts, setup) | Skill | -## Creating Skills +Skills are loaded when: +- The agent decides the task matches a skill's description +- The user explicitly asks to use a skill (e.g., "use the pdf skill to extract tables") -A skill is a markdown file with YAML frontmatter containing a `description` field: +**Good skill examples:** +- Browser automation with helper scripts and CDP workflow +- Google Calendar CLI with setup instructions and usage patterns +- PDF processing with multiple tools and extraction patterns +- Speech-to-text transcription with API setup + +**Not a good fit for skills:** +- "Always use TypeScript strict mode" → put in AGENTS.md +- "Review my code" → make a slash command +- Need user confirmation dialogs or custom TUI rendering → make a custom tool + +## Skill Structure + +A skill is a directory with a `SKILL.md` file. Everything else is freeform. Example structure: + +``` +my-skill/ +├── SKILL.md # Required: frontmatter + instructions +├── scripts/ # Helper scripts (bash, python, node) +│ └── process.sh +├── references/ # Detailed docs loaded on-demand +│ └── api-reference.md +└── assets/ # Templates, images, etc. + └── template.json +``` + +### SKILL.md Format ```markdown --- -description: Extract text and tables from PDF files +name: my-skill +description: What this skill does and when to use it. Be specific. --- -# PDF Processing Instructions +# My Skill -1. Use `pdftotext` to extract plain text -2. For tables, use `tabula-py` or similar -3. Always verify extraction quality +## Setup -Scripts are in: {baseDir}/scripts/ +Run once before first use: +\`\`\`bash +cd {baseDir} +npm install +\`\`\` + +## Usage + +\`\`\`bash +{baseDir}/scripts/process.sh +\`\`\` + +## Workflow + +1. First step +2. Second step +3. Third step ``` ### Frontmatter Fields | Field | Required | Description | |-------|----------|-------------| -| `description` | Yes | Short description for skill selection | -| `name` | No | Override skill name (defaults to filename or directory name) | +| `description` | Yes | What the skill does and when to use it | +| `name` | No | Override skill name (defaults to directory name) | -The parser only supports single-line `key: value` syntax. Multiline YAML blocks are not supported. +The `description` is critical. It's shown in the system prompt and determines when the agent loads the skill. Be specific about both what it does and when to use it. -### Variables +### The `{baseDir}` Placeholder -Use `{baseDir}` as a placeholder for the skill's directory. The agent is told each skill's base directory and will substitute it when following the instructions. +Use `{baseDir}` to reference files in the skill's directory. The agent sees each skill's base directory and substitutes it when following instructions: -### Subdirectories +```markdown +Helper scripts: {baseDir}/scripts/ +Config template: {baseDir}/assets/config.json +``` -Pi and Codex skills in subdirectories use colon-separated names: +## Skill Locations + +Skills are discovered from these locations (later wins on name collision): + +1. `~/.codex/skills/**/SKILL.md` (Codex CLI, recursive) +2. `~/.claude/skills/*/SKILL.md` (Claude Code user, one level) +3. `/.claude/skills/*/SKILL.md` (Claude Code project, one level) +4. `~/.pi/agent/skills/**/SKILL.md` (Pi user, recursive) +5. `/.pi/skills/**/SKILL.md` (Pi project, recursive) + +### Subdirectory Naming + +Pi skills in subdirectories use colon-separated names: - `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate` - `/.pi/skills/aws/s3/upload/SKILL.md` → `aws:s3:upload` -## Claude Code Compatibility +## How Skills Work -Pi reads Claude Code skills from `~/.claude/skills/*/SKILL.md`. The `allowed-tools` and `model` frontmatter fields are ignored since Pi cannot enforce them. +1. At startup, pi scans skill locations and extracts names + descriptions +2. The system prompt includes a list of available skills with their descriptions +3. When a task matches, the agent uses `read` to load the full SKILL.md +4. The agent follows the instructions, using `{baseDir}` to reference scripts/assets -## Codex CLI Compatibility +This is progressive disclosure: only descriptions are always in context, full instructions load on-demand. -Pi reads Codex CLI skills from `~/.codex/skills/`. Unlike Claude Code skills (one level deep), Codex skills are scanned recursively, matching Codex CLI's behavior. Hidden files/directories (starting with `.`) and symlinks are skipped. +## Example: Web Search Skill + +``` +brave-search/ +├── SKILL.md +├── search.js +└── content.js +``` + +**SKILL.md:** +```markdown +--- +name: brave-search +description: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content. +--- + +# Brave Search + +## Setup + +\`\`\`bash +cd {baseDir} +npm install +\`\`\` + +## Search + +\`\`\`bash +{baseDir}/search.js "query" # Basic search +{baseDir}/search.js "query" --content # Include page content +\`\`\` + +## Extract Page Content + +\`\`\`bash +{baseDir}/content.js https://example.com +\`\`\` +``` + +## Compatibility + +**Claude Code**: Pi reads skills from `~/.claude/skills/*/SKILL.md`. The `allowed-tools` and `model` frontmatter fields are ignored. + +**Codex CLI**: Pi reads skills from `~/.codex/skills/` recursively. Hidden files/directories and symlinks are skipped. + +## Skill Repositories + +For inspiration and ready-to-use skills: + +- [Anthropic Skills](https://github.com/anthropics/skills) - Official skills for document processing (docx, pdf, pptx, xlsx), web development, and more +- [Pi Skills](https://github.com/badlogic/pi-skills) - Skills for web search, browser automation, Google APIs, transcription ## Disabling Skills -CLI flag: +CLI: ```bash pi --no-skills ``` -Or in `~/.pi/agent/settings.json`: +Settings (`~/.pi/agent/settings.json`): ```json { "skills": { @@ -74,25 +192,3 @@ Or in `~/.pi/agent/settings.json`: } } ``` - -## Example - -```markdown ---- -description: Perform code review with security and performance analysis ---- - -# Code Review - -Analyze: - -## Security -- Input validation -- SQL injection -- XSS vulnerabilities - -## Performance -- Algorithm complexity -- Memory usage -- Query efficiency -``` diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md index 2bf01781..06f674cb 100644 --- a/packages/coding-agent/docs/theme.md +++ b/packages/coding-agent/docs/theme.md @@ -74,7 +74,7 @@ Future-proofing for syntax highlighting support: | `syntaxOperator` | Operators (`+`, `-`, etc) | | `syntaxPunctuation` | Punctuation (`;`, `,`, etc) | -### Thinking Level Borders (5 colors) +### Thinking Level Borders (6 colors) Editor border colors that indicate the current thinking/reasoning level: @@ -84,11 +84,18 @@ Editor border colors that indicate the current thinking/reasoning level: | `thinkingMinimal` | Border for minimal thinking | | `thinkingLow` | Border for low thinking | | `thinkingMedium` | Border for medium thinking | -| `thinkingHigh` | Border for high thinking (most prominent) | +| `thinkingHigh` | Border for high thinking | +| `thinkingXhigh` | Border for xhigh thinking (most prominent, OpenAI codex-max only) | -These create a visual hierarchy: off → minimal → low → medium → high +These create a visual hierarchy: off → minimal → low → medium → high → xhigh -**Total: 44 color tokens** (all required) +### Bash Mode (1 color) + +| Token | Purpose | +|-------|---------| +| `bashMode` | Editor border color when in bash mode (! prefix) | + +**Total: 46 color tokens** (all required) ## Theme Format @@ -420,8 +427,8 @@ class Theme { // Text attributes (preserve current colors) bold(text: string): string - dim(text: string): string italic(text: string): string + underline(text: string): string } ``` @@ -453,6 +460,7 @@ TUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@mariozechn export interface MarkdownTheme { heading: (text: string) => string; link: (text: string) => string; + linkUrl: (text: string) => string; code: (text: string) => string; codeBlock: (text: string) => string; codeBlockBorder: (text: string) => string; @@ -460,21 +468,10 @@ export interface MarkdownTheme { quoteBorder: (text: string) => string; hr: (text: string) => string; listBullet: (text: string) => string; -} - -export class Markdown { - constructor( - text: string, - paddingX: number, - paddingY: number, - defaultTextStyle?: DefaultTextStyle, - theme?: MarkdownTheme // Optional theme functions - ) - - // Usage in component - renderHeading(text: string) { - return this.theme.heading(text); // Applies color - } + bold: (text: string) => string; + italic: (text: string) => string; + strikethrough: (text: string) => string; + underline: (text: string) => string; } ``` @@ -490,6 +487,7 @@ function getMarkdownTheme(): MarkdownTheme { return { heading: (text) => theme.fg('mdHeading', text), link: (text) => theme.fg('mdLink', text), + linkUrl: (text) => theme.fg('mdLinkUrl', text), code: (text) => theme.fg('mdCode', text), codeBlock: (text) => theme.fg('mdCodeBlock', text), codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text), @@ -497,6 +495,10 @@ function getMarkdownTheme(): MarkdownTheme { quoteBorder: (text) => theme.fg('mdQuoteBorder', text), hr: (text) => theme.fg('mdHr', text), listBullet: (text) => theme.fg('mdListBullet', text), + bold: (text) => theme.bold(text), + italic: (text) => theme.italic(text), + underline: (text) => theme.underline(text), + strikethrough: (text) => chalk.strikethrough(text), }; } @@ -530,7 +532,7 @@ theme.bg('toolSuccessBg', output) // Combine styles theme.bold(theme.fg('accent', 'Title')) -theme.dim(theme.fg('muted', 'metadata')) +theme.italic(theme.fg('muted', 'metadata')) // Nested foreground + background const userMsg = theme.bg('userMessageBg', diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 546897fc..b47e1857 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -6,6 +6,7 @@ */ import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core"; +import type { ToolResultMessage } from "@mariozechner/pi-ai"; import type { SessionEntry } from "../session-manager.js"; // ============================================================================ @@ -121,7 +122,7 @@ export interface TurnEndEvent { type: "turn_end"; turnIndex: number; message: AppMessage; - toolResults: AppMessage[]; + toolResults: ToolResultMessage[]; } /** diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index eaaf22b2..1b3d6ac0 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -235,9 +235,9 @@ Guidelines: ${guidelines} Documentation: -- Your own documentation (including custom model setup and theme creation) is at: ${readmePath} -- Additional documentation (hooks, themes, RPC, etc.) is in: ${docsPath} -- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, create a custom theme, or write a hook.`; +- Main documentation: ${readmePath} +- Additional docs: ${docsPath} +- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)`; if (appendSection) { prompt += appendSection;