Custom tools with session lifecycle, examples for hooks and tools

- Custom tools: TypeScript modules that extend pi with new tools
  - Custom TUI rendering via renderCall/renderResult
  - User interaction via pi.ui (select, confirm, input, notify)
  - Session lifecycle via onSession callback for state reconstruction
  - Examples: todo.ts, question.ts, hello.ts

- Hook examples: permission-gate, git-checkpoint, protected-paths

- Session lifecycle centralized in AgentSession
  - Works across all modes (interactive, print, RPC)
  - Unified session event for hooks (replaces session_start/session_switch)

- Box component added to pi-tui

- Examples bundled in npm and binary releases

Fixes #190
This commit is contained in:
Mario Zechner 2025-12-17 16:03:23 +01:00
parent 295f51b53f
commit e7097d911a
33 changed files with 1926 additions and 117 deletions

View file

@ -0,0 +1,101 @@
# Custom Tools Examples
Example custom tools for pi-coding-agent.
## Examples
### hello.ts
Minimal example showing the basic structure of a custom tool.
### question.ts
Demonstrates `pi.ui.select()` for asking the user questions with options.
### todo.ts
Full-featured example demonstrating:
- `onSession` for state reconstruction from session history
- Custom `renderCall` and `renderResult`
- Proper branching support via details storage
- State management without external files
## Usage
```bash
# Test directly
pi --tool examples/custom-tools/todo.ts
# Or copy to tools directory for persistent use
cp todo.ts ~/.pi/agent/tools/
```
Then in pi:
```
> add a todo "test custom tools"
> list todos
> toggle todo #1
> clear todos
```
## Writing Custom Tools
See [docs/custom-tools.md](../../docs/custom-tools.md) for full documentation.
### Key Points
**Factory pattern:**
```typescript
import { Type } from "@sinclair/typebox";
import { StringEnum } from "@mariozechner/pi-ai";
import { Text } from "@mariozechner/pi-tui";
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
const factory: CustomToolFactory = (pi) => ({
name: "my_tool",
label: "My Tool",
description: "Tool description for LLM",
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
}),
// Called on session start/switch/branch/clear
onSession(event) {
// Reconstruct state from event.entries
},
async execute(toolCallId, params) {
return {
content: [{ type: "text", text: "Result" }],
details: { /* for rendering and state reconstruction */ },
};
},
});
export default factory;
```
**Custom rendering:**
```typescript
renderCall(args, theme) {
return new Text(
theme.fg("toolTitle", theme.bold("my_tool ")) + args.action,
0, 0 // No padding - Box handles it
);
},
renderResult(result, { expanded, isPartial }, theme) {
if (isPartial) {
return new Text(theme.fg("warning", "Working..."), 0, 0);
}
return new Text(theme.fg("success", "✓ Done"), 0, 0);
},
```
**Use StringEnum for string parameters** (required for Google API compatibility):
```typescript
import { StringEnum } from "@mariozechner/pi-ai";
// Good
action: StringEnum(["list", "add"] as const)
// Bad - doesn't work with Google
action: Type.Union([Type.Literal("list"), Type.Literal("add")])
```