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[]
This commit is contained in:
Mario Zechner 2025-12-17 21:27:28 +01:00
parent 5cc0126991
commit 5e5bdadbf9
8 changed files with 232 additions and 141 deletions

View file

@ -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

View file

@ -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

View file

@ -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:
```
<available_skills>
- pdf-extract: Extract text and tables from PDF files
File: ~/.pi/agent/skills/pdf-extract/SKILL.md
Base directory: ~/.pi/agent/skills/pdf-extract
</available_skills>
```
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

View file

@ -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<string, unknown>
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

View file

@ -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. `<cwd>/.claude/skills/*/SKILL.md` (Claude Code project skills)
4. `~/.pi/agent/skills/**/SKILL.md` (Pi user skills, recursive)
5. `<cwd>/.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 <input>
\`\`\`
## 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. `<cwd>/.claude/skills/*/SKILL.md` (Claude Code project, one level)
4. `~/.pi/agent/skills/**/SKILL.md` (Pi user, recursive)
5. `<cwd>/.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`
- `<cwd>/.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
```

View file

@ -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',

View file

@ -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[];
}
/**

View file

@ -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;