@mariozechner/pi-coding-agent 0.22.5 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +54 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +64 -11
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/custom-tools/index.d.ts +6 -0
- package/dist/core/custom-tools/index.d.ts.map +1 -0
- package/dist/core/custom-tools/index.js +5 -0
- package/dist/core/custom-tools/index.js.map +1 -0
- package/dist/core/custom-tools/loader.d.ts +24 -0
- package/dist/core/custom-tools/loader.d.ts.map +1 -0
- package/dist/core/custom-tools/loader.js +233 -0
- package/dist/core/custom-tools/loader.js.map +1 -0
- package/dist/core/custom-tools/types.d.ts +81 -0
- package/dist/core/custom-tools/types.d.ts.map +1 -0
- package/dist/core/custom-tools/types.js +8 -0
- package/dist/core/custom-tools/types.js.map +1 -0
- package/dist/core/hooks/index.d.ts +1 -1
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/types.d.ts +14 -19
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +4 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +71 -20
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +66 -12
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +26 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +27 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/custom-tools.md +354 -0
- package/docs/hooks.md +54 -34
- package/examples/custom-tools/README.md +101 -0
- package/examples/custom-tools/hello.ts +20 -0
- package/examples/custom-tools/question.ts +83 -0
- package/examples/custom-tools/todo.ts +192 -0
- package/examples/hooks/README.md +79 -0
- package/examples/hooks/file-trigger.ts +36 -0
- package/examples/hooks/git-checkpoint.ts +48 -0
- package/examples/hooks/permission-gate.ts +38 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/package.json +7 -6
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# Custom Tools
|
|
2
|
+
|
|
3
|
+
Custom tools extend pi with new capabilities beyond the built-in read/write/edit/bash tools. They are TypeScript modules that define one or more tools with optional custom rendering for the TUI.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Create a file `~/.pi/agent/tools/hello.ts`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
const factory: CustomToolFactory = (pi) => ({
|
|
14
|
+
name: "hello",
|
|
15
|
+
label: "Hello",
|
|
16
|
+
description: "A simple greeting tool",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
name: Type.String({ description: "Name to greet" }),
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
async execute(toolCallId, params) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: `Hello, ${params.name}!` }],
|
|
24
|
+
details: { greeted: params.name },
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default factory;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The tool is automatically discovered and available in your next pi session.
|
|
33
|
+
|
|
34
|
+
## Tool Locations
|
|
35
|
+
|
|
36
|
+
| Location | Scope | Auto-discovered |
|
|
37
|
+
|----------|-------|-----------------|
|
|
38
|
+
| `~/.pi/agent/tools/*.ts` | Global (all projects) | Yes |
|
|
39
|
+
| `.pi/tools/*.ts` | Project-local | Yes |
|
|
40
|
+
| `settings.json` `customTools` array | Configured paths | Yes |
|
|
41
|
+
| `--tool <path>` CLI flag | One-off/debugging | No |
|
|
42
|
+
|
|
43
|
+
**Priority:** Later sources win on name conflicts. CLI `--tool` takes highest priority.
|
|
44
|
+
|
|
45
|
+
**Reserved names:** Custom tools cannot use built-in tool names (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`).
|
|
46
|
+
|
|
47
|
+
## Available Imports
|
|
48
|
+
|
|
49
|
+
Custom tools can import from these packages (automatically resolved by pi):
|
|
50
|
+
|
|
51
|
+
| Package | Purpose |
|
|
52
|
+
|---------|---------|
|
|
53
|
+
| `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) |
|
|
54
|
+
| `@mariozechner/pi-coding-agent` | Types (`CustomToolFactory`, `ToolSessionEvent`, etc.) |
|
|
55
|
+
| `@mariozechner/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) |
|
|
56
|
+
| `@mariozechner/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) |
|
|
57
|
+
|
|
58
|
+
Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
|
|
59
|
+
|
|
60
|
+
## Tool Definition
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { Type } from "@sinclair/typebox";
|
|
64
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
65
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
66
|
+
import type { CustomToolFactory, ToolSessionEvent } from "@mariozechner/pi-coding-agent";
|
|
67
|
+
|
|
68
|
+
const factory: CustomToolFactory = (pi) => ({
|
|
69
|
+
name: "my_tool",
|
|
70
|
+
label: "My Tool",
|
|
71
|
+
description: "What this tool does (be specific for LLM)",
|
|
72
|
+
parameters: Type.Object({
|
|
73
|
+
// Use StringEnum for string enums (Google API compatible)
|
|
74
|
+
action: StringEnum(["list", "add", "remove"] as const),
|
|
75
|
+
text: Type.Optional(Type.String()),
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
79
|
+
// signal - AbortSignal for cancellation
|
|
80
|
+
// onUpdate - Callback for streaming partial results
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text", text: "Result for LLM" }],
|
|
83
|
+
details: { /* structured data for rendering */ },
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Optional: Session lifecycle callback
|
|
88
|
+
onSession(event) { /* reconstruct state from entries */ },
|
|
89
|
+
|
|
90
|
+
// Optional: Custom rendering
|
|
91
|
+
renderCall(args, theme) { /* return Component */ },
|
|
92
|
+
renderResult(result, options, theme) { /* return Component */ },
|
|
93
|
+
|
|
94
|
+
// Optional: Cleanup on session end
|
|
95
|
+
dispose() { /* save state, close connections */ },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export default factory;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Important:** Use `StringEnum` from `@mariozechner/pi-ai` instead of `Type.Union`/`Type.Literal` for string enums. The latter doesn't work with Google's API.
|
|
102
|
+
|
|
103
|
+
## ToolAPI Object
|
|
104
|
+
|
|
105
|
+
The factory receives a `ToolAPI` object (named `pi` by convention):
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface ToolAPI {
|
|
109
|
+
cwd: string; // Current working directory
|
|
110
|
+
exec(command: string, args: string[]): Promise<ExecResult>;
|
|
111
|
+
ui: {
|
|
112
|
+
select(title: string, options: string[]): Promise<string | null>;
|
|
113
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
114
|
+
input(title: string, placeholder?: string): Promise<string | null>;
|
|
115
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
116
|
+
};
|
|
117
|
+
hasUI: boolean; // false in --print or --mode rpc
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Always check `pi.hasUI` before using UI methods.
|
|
122
|
+
|
|
123
|
+
## Session Lifecycle
|
|
124
|
+
|
|
125
|
+
Tools can implement `onSession` to react to session changes:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface ToolSessionEvent {
|
|
129
|
+
entries: SessionEntry[]; // All session entries
|
|
130
|
+
sessionFile: string | null; // Current session file
|
|
131
|
+
previousSessionFile: string | null; // Previous session file
|
|
132
|
+
reason: "start" | "switch" | "branch" | "clear";
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Reasons:**
|
|
137
|
+
- `start`: Initial session load on startup
|
|
138
|
+
- `switch`: User switched to a different session (`/session`)
|
|
139
|
+
- `branch`: User branched from a previous message (`/branch`)
|
|
140
|
+
- `clear`: User cleared the session (`/clear`)
|
|
141
|
+
|
|
142
|
+
### State Management Pattern
|
|
143
|
+
|
|
144
|
+
Tools that maintain state should store it in `details` of their results, not external files. This allows branching to work correctly, as the state is reconstructed from the session history.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface MyToolDetails {
|
|
148
|
+
items: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const factory: CustomToolFactory = (pi) => {
|
|
152
|
+
// In-memory state
|
|
153
|
+
let items: string[] = [];
|
|
154
|
+
|
|
155
|
+
// Reconstruct state from session entries
|
|
156
|
+
const reconstructState = (event: ToolSessionEvent) => {
|
|
157
|
+
items = [];
|
|
158
|
+
for (const entry of event.entries) {
|
|
159
|
+
if (entry.type !== "message") continue;
|
|
160
|
+
const msg = entry.message;
|
|
161
|
+
if (msg.role !== "toolResult") continue;
|
|
162
|
+
if (msg.toolName !== "my_tool") continue;
|
|
163
|
+
|
|
164
|
+
const details = msg.details as MyToolDetails | undefined;
|
|
165
|
+
if (details) {
|
|
166
|
+
items = details.items;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
name: "my_tool",
|
|
173
|
+
label: "My Tool",
|
|
174
|
+
description: "...",
|
|
175
|
+
parameters: Type.Object({ ... }),
|
|
176
|
+
|
|
177
|
+
onSession: reconstructState,
|
|
178
|
+
|
|
179
|
+
async execute(toolCallId, params) {
|
|
180
|
+
// Modify items...
|
|
181
|
+
items.push("new item");
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: "Added item" }],
|
|
185
|
+
// Store current state in details for reconstruction
|
|
186
|
+
details: { items: [...items] },
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This pattern ensures:
|
|
194
|
+
- When user branches, state is correct for that point in history
|
|
195
|
+
- When user switches sessions, state matches that session
|
|
196
|
+
- When user clears, state resets
|
|
197
|
+
|
|
198
|
+
## Custom Rendering
|
|
199
|
+
|
|
200
|
+
Custom tools can provide `renderCall` and `renderResult` methods to control how they appear in the TUI. Both are optional.
|
|
201
|
+
|
|
202
|
+
### How It Works
|
|
203
|
+
|
|
204
|
+
Tool output is wrapped in a `Box` component that handles:
|
|
205
|
+
- Padding (1 character horizontal, 1 line vertical)
|
|
206
|
+
- Background color based on state (pending/success/error)
|
|
207
|
+
|
|
208
|
+
Your render methods return `Component` instances (typically `Text`) that go inside this box. Use `Text(content, 0, 0)` since the Box handles padding.
|
|
209
|
+
|
|
210
|
+
### renderCall
|
|
211
|
+
|
|
212
|
+
Renders the tool call (before/during execution):
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
renderCall(args, theme) {
|
|
216
|
+
let text = theme.fg("toolTitle", theme.bold("my_tool "));
|
|
217
|
+
text += theme.fg("muted", args.action);
|
|
218
|
+
if (args.text) {
|
|
219
|
+
text += " " + theme.fg("dim", `"${args.text}"`);
|
|
220
|
+
}
|
|
221
|
+
return new Text(text, 0, 0);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Called when:
|
|
226
|
+
- Tool call starts (may have partial args during streaming)
|
|
227
|
+
- Args are updated during streaming
|
|
228
|
+
|
|
229
|
+
### renderResult
|
|
230
|
+
|
|
231
|
+
Renders the tool result:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
235
|
+
const { details } = result;
|
|
236
|
+
|
|
237
|
+
// Handle streaming/partial results
|
|
238
|
+
if (isPartial) {
|
|
239
|
+
return new Text(theme.fg("warning", "Processing..."), 0, 0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle errors
|
|
243
|
+
if (details?.error) {
|
|
244
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Normal result
|
|
248
|
+
let text = theme.fg("success", "✓ ") + theme.fg("muted", "Done");
|
|
249
|
+
|
|
250
|
+
// Support expanded view (Ctrl+O)
|
|
251
|
+
if (expanded && details?.items) {
|
|
252
|
+
for (const item of details.items) {
|
|
253
|
+
text += "\n" + theme.fg("dim", ` ${item}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return new Text(text, 0, 0);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Options:**
|
|
262
|
+
- `expanded`: User pressed Ctrl+O to expand
|
|
263
|
+
- `isPartial`: Result is from `onUpdate` (streaming), not final
|
|
264
|
+
|
|
265
|
+
### Best Practices
|
|
266
|
+
|
|
267
|
+
1. **Use `Text` with padding `(0, 0)`** - The Box handles padding
|
|
268
|
+
2. **Use `\n` for multi-line content** - Not multiple Text components
|
|
269
|
+
3. **Handle `isPartial`** - Show progress during streaming
|
|
270
|
+
4. **Support `expanded`** - Show more detail when user requests
|
|
271
|
+
5. **Use theme colors** - For consistent appearance
|
|
272
|
+
6. **Keep it compact** - Show summary by default, details when expanded
|
|
273
|
+
|
|
274
|
+
### Theme Colors
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// Foreground
|
|
278
|
+
theme.fg("toolTitle", text) // Tool names
|
|
279
|
+
theme.fg("accent", text) // Highlights
|
|
280
|
+
theme.fg("success", text) // Success
|
|
281
|
+
theme.fg("error", text) // Errors
|
|
282
|
+
theme.fg("warning", text) // Warnings
|
|
283
|
+
theme.fg("muted", text) // Secondary text
|
|
284
|
+
theme.fg("dim", text) // Tertiary text
|
|
285
|
+
theme.fg("toolOutput", text) // Output content
|
|
286
|
+
|
|
287
|
+
// Styles
|
|
288
|
+
theme.bold(text)
|
|
289
|
+
theme.italic(text)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Fallback Behavior
|
|
293
|
+
|
|
294
|
+
If `renderCall` or `renderResult` is not defined or throws an error:
|
|
295
|
+
- `renderCall`: Shows tool name
|
|
296
|
+
- `renderResult`: Shows raw text output from `content`
|
|
297
|
+
|
|
298
|
+
## Execute Function
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
async execute(toolCallId, args, signal, onUpdate) {
|
|
302
|
+
// Type assertion for params (TypeBox schema doesn't flow through)
|
|
303
|
+
const params = args as { action: "list" | "add"; text?: string };
|
|
304
|
+
|
|
305
|
+
// Check for abort
|
|
306
|
+
if (signal?.aborted) {
|
|
307
|
+
return { content: [...], details: { status: "aborted" } };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Stream progress
|
|
311
|
+
onUpdate?.({
|
|
312
|
+
content: [{ type: "text", text: "Working..." }],
|
|
313
|
+
details: { progress: 50 },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Return final result
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: "Done" }], // Sent to LLM
|
|
319
|
+
details: { data: result }, // For rendering only
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Multiple Tools from One File
|
|
325
|
+
|
|
326
|
+
Return an array to share state between related tools:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const factory: CustomToolFactory = (pi) => {
|
|
330
|
+
// Shared state
|
|
331
|
+
let connection = null;
|
|
332
|
+
|
|
333
|
+
return [
|
|
334
|
+
{ name: "db_connect", ... },
|
|
335
|
+
{ name: "db_query", ... },
|
|
336
|
+
{
|
|
337
|
+
name: "db_close",
|
|
338
|
+
dispose() { connection?.close(); }
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
};
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Examples
|
|
345
|
+
|
|
346
|
+
See [`examples/custom-tools/todo.ts`](../examples/custom-tools/todo.ts) for a complete example with:
|
|
347
|
+
- `onSession` for state reconstruction
|
|
348
|
+
- Custom `renderCall` and `renderResult`
|
|
349
|
+
- Proper branching support via details storage
|
|
350
|
+
|
|
351
|
+
Test with:
|
|
352
|
+
```bash
|
|
353
|
+
pi --tool packages/coding-agent/examples/custom-tools/todo.ts
|
|
354
|
+
```
|
package/docs/hooks.md
CHANGED
|
@@ -43,8 +43,8 @@ A hook is a TypeScript file that exports a default function. The function receiv
|
|
|
43
43
|
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
44
44
|
|
|
45
45
|
export default function (pi: HookAPI) {
|
|
46
|
-
pi.on("
|
|
47
|
-
ctx.ui.notify(`Session: ${ctx.sessionFile ?? "ephemeral"}`, "info");
|
|
46
|
+
pi.on("session", async (event, ctx) => {
|
|
47
|
+
ctx.ui.notify(`Session ${event.reason}: ${ctx.sessionFile ?? "ephemeral"}`, "info");
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
```
|
|
@@ -76,7 +76,7 @@ Hooks are loaded using [jiti](https://github.com/unjs/jiti), so TypeScript works
|
|
|
76
76
|
```
|
|
77
77
|
pi starts
|
|
78
78
|
│
|
|
79
|
-
├─►
|
|
79
|
+
├─► session (reason: "start")
|
|
80
80
|
│
|
|
81
81
|
▼
|
|
82
82
|
user sends prompt ─────────────────────────────────────────┐
|
|
@@ -98,36 +98,54 @@ user sends prompt ────────────────────
|
|
|
98
98
|
│
|
|
99
99
|
user sends another prompt ◄────────────────────────────────┘
|
|
100
100
|
|
|
101
|
-
user branches
|
|
101
|
+
user branches (/branch)
|
|
102
102
|
│
|
|
103
|
-
|
|
103
|
+
├─► branch (BEFORE branch, can control)
|
|
104
|
+
└─► session (reason: "switch", AFTER branch)
|
|
105
|
+
|
|
106
|
+
user switches session (/session)
|
|
107
|
+
│
|
|
108
|
+
└─► session (reason: "switch")
|
|
109
|
+
|
|
110
|
+
user clears session (/clear)
|
|
111
|
+
│
|
|
112
|
+
└─► session (reason: "clear")
|
|
104
113
|
```
|
|
105
114
|
|
|
106
115
|
A **turn** is one LLM response plus any tool calls. Complex tasks loop through multiple turns until the LLM responds without calling tools.
|
|
107
116
|
|
|
108
|
-
###
|
|
117
|
+
### session
|
|
109
118
|
|
|
110
|
-
Fired
|
|
119
|
+
Fired on startup and when session changes.
|
|
111
120
|
|
|
112
121
|
```typescript
|
|
113
|
-
pi.on("
|
|
114
|
-
//
|
|
115
|
-
//
|
|
122
|
+
pi.on("session", async (event, ctx) => {
|
|
123
|
+
// event.entries: SessionEntry[] - all session entries
|
|
124
|
+
// event.sessionFile: string | null - current session file
|
|
125
|
+
// event.previousSessionFile: string | null - previous session file
|
|
126
|
+
// event.reason: "start" | "switch" | "clear"
|
|
116
127
|
});
|
|
117
128
|
```
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
**Reasons:**
|
|
131
|
+
- `start`: Initial session load on startup
|
|
132
|
+
- `switch`: User switched sessions (`/session`) or branched (`/branch`)
|
|
133
|
+
- `clear`: User cleared the session (`/clear`)
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
### branch
|
|
136
|
+
|
|
137
|
+
Fired BEFORE a branch happens. Can control branch behavior.
|
|
122
138
|
|
|
123
139
|
```typescript
|
|
124
|
-
pi.on("
|
|
125
|
-
// event.
|
|
126
|
-
// event.
|
|
127
|
-
|
|
140
|
+
pi.on("branch", async (event, ctx) => {
|
|
141
|
+
// event.targetTurnIndex: number
|
|
142
|
+
// event.entries: SessionEntry[]
|
|
143
|
+
return { skipConversationRestore: true }; // or undefined
|
|
128
144
|
});
|
|
129
145
|
```
|
|
130
146
|
|
|
147
|
+
Note: After branch completes, a `session` event fires with `reason: "switch"`.
|
|
148
|
+
|
|
131
149
|
### agent_start / agent_end
|
|
132
150
|
|
|
133
151
|
Fired once per user prompt.
|
|
@@ -192,18 +210,6 @@ pi.on("tool_result", async (event, ctx) => {
|
|
|
192
210
|
});
|
|
193
211
|
```
|
|
194
212
|
|
|
195
|
-
### branch
|
|
196
|
-
|
|
197
|
-
Fired when user branches via `/branch`.
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
pi.on("branch", async (event, ctx) => {
|
|
201
|
-
// event.targetTurnIndex: number
|
|
202
|
-
// event.entries: SessionEntry[]
|
|
203
|
-
return { skipConversationRestore: true }; // or undefined
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
213
|
## Context API
|
|
208
214
|
|
|
209
215
|
Every event handler receives a context object with these methods:
|
|
@@ -309,7 +315,9 @@ import * as fs from "node:fs";
|
|
|
309
315
|
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
310
316
|
|
|
311
317
|
export default function (pi: HookAPI) {
|
|
312
|
-
pi.on("
|
|
318
|
+
pi.on("session", async (event, ctx) => {
|
|
319
|
+
if (event.reason !== "start") return;
|
|
320
|
+
|
|
313
321
|
// Watch a trigger file
|
|
314
322
|
const triggerFile = "/tmp/agent-trigger.txt";
|
|
315
323
|
|
|
@@ -339,7 +347,9 @@ import * as http from "node:http";
|
|
|
339
347
|
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
340
348
|
|
|
341
349
|
export default function (pi: HookAPI) {
|
|
342
|
-
pi.on("
|
|
350
|
+
pi.on("session", async (event, ctx) => {
|
|
351
|
+
if (event.reason !== "start") return;
|
|
352
|
+
|
|
343
353
|
const server = http.createServer((req, res) => {
|
|
344
354
|
let body = "";
|
|
345
355
|
req.on("data", chunk => body += chunk);
|
|
@@ -563,7 +573,7 @@ Key behaviors:
|
|
|
563
573
|
Mode initialization:
|
|
564
574
|
-> hookRunner.setUIContext(ctx, hasUI)
|
|
565
575
|
-> hookRunner.setSessionFile(path)
|
|
566
|
-
-> hookRunner.emit({ type: "
|
|
576
|
+
-> hookRunner.emit({ type: "session", reason: "start", ... })
|
|
567
577
|
|
|
568
578
|
User sends prompt:
|
|
569
579
|
-> AgentSession.prompt()
|
|
@@ -581,9 +591,19 @@ User sends prompt:
|
|
|
581
591
|
-> [repeat if more tool calls]
|
|
582
592
|
-> hookRunner.emit({ type: "agent_end", messages })
|
|
583
593
|
|
|
584
|
-
Branch
|
|
585
|
-
-> AgentSession.branch()
|
|
586
|
-
-> hookRunner.emit({ type: "
|
|
594
|
+
Branch:
|
|
595
|
+
-> AgentSession.branch()
|
|
596
|
+
-> hookRunner.emit({ type: "branch", ... }) # BEFORE branch
|
|
597
|
+
-> [branch happens]
|
|
598
|
+
-> hookRunner.emit({ type: "session", reason: "switch", ... }) # AFTER
|
|
599
|
+
|
|
600
|
+
Session switch:
|
|
601
|
+
-> AgentSession.switchSession()
|
|
602
|
+
-> hookRunner.emit({ type: "session", reason: "switch", ... })
|
|
603
|
+
|
|
604
|
+
Clear:
|
|
605
|
+
-> AgentSession.reset()
|
|
606
|
+
-> hookRunner.emit({ type: "session", reason: "clear", ... })
|
|
587
607
|
```
|
|
588
608
|
|
|
589
609
|
## UI Context by Mode
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Custom Tools Examples
|
|
2
|
+
|
|
3
|
+
Example custom tools for pi-coding-agent.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### hello.ts
|
|
8
|
+
Minimal example showing the basic structure of a custom tool.
|
|
9
|
+
|
|
10
|
+
### question.ts
|
|
11
|
+
Demonstrates `pi.ui.select()` for asking the user questions with options.
|
|
12
|
+
|
|
13
|
+
### todo.ts
|
|
14
|
+
Full-featured example demonstrating:
|
|
15
|
+
- `onSession` for state reconstruction from session history
|
|
16
|
+
- Custom `renderCall` and `renderResult`
|
|
17
|
+
- Proper branching support via details storage
|
|
18
|
+
- State management without external files
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Test directly
|
|
24
|
+
pi --tool examples/custom-tools/todo.ts
|
|
25
|
+
|
|
26
|
+
# Or copy to tools directory for persistent use
|
|
27
|
+
cp todo.ts ~/.pi/agent/tools/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then in pi:
|
|
31
|
+
```
|
|
32
|
+
> add a todo "test custom tools"
|
|
33
|
+
> list todos
|
|
34
|
+
> toggle todo #1
|
|
35
|
+
> clear todos
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Writing Custom Tools
|
|
39
|
+
|
|
40
|
+
See [docs/custom-tools.md](../../docs/custom-tools.md) for full documentation.
|
|
41
|
+
|
|
42
|
+
### Key Points
|
|
43
|
+
|
|
44
|
+
**Factory pattern:**
|
|
45
|
+
```typescript
|
|
46
|
+
import { Type } from "@sinclair/typebox";
|
|
47
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
48
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
49
|
+
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
|
50
|
+
|
|
51
|
+
const factory: CustomToolFactory = (pi) => ({
|
|
52
|
+
name: "my_tool",
|
|
53
|
+
label: "My Tool",
|
|
54
|
+
description: "Tool description for LLM",
|
|
55
|
+
parameters: Type.Object({
|
|
56
|
+
action: StringEnum(["list", "add"] as const),
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
// Called on session start/switch/branch/clear
|
|
60
|
+
onSession(event) {
|
|
61
|
+
// Reconstruct state from event.entries
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async execute(toolCallId, params) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: "Result" }],
|
|
67
|
+
details: { /* for rendering and state reconstruction */ },
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default factory;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Custom rendering:**
|
|
76
|
+
```typescript
|
|
77
|
+
renderCall(args, theme) {
|
|
78
|
+
return new Text(
|
|
79
|
+
theme.fg("toolTitle", theme.bold("my_tool ")) + args.action,
|
|
80
|
+
0, 0 // No padding - Box handles it
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
85
|
+
if (isPartial) {
|
|
86
|
+
return new Text(theme.fg("warning", "Working..."), 0, 0);
|
|
87
|
+
}
|
|
88
|
+
return new Text(theme.fg("success", "✓ Done"), 0, 0);
|
|
89
|
+
},
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Use StringEnum for string parameters** (required for Google API compatibility):
|
|
93
|
+
```typescript
|
|
94
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
95
|
+
|
|
96
|
+
// Good
|
|
97
|
+
action: StringEnum(["list", "add"] as const)
|
|
98
|
+
|
|
99
|
+
// Bad - doesn't work with Google
|
|
100
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("add")])
|
|
101
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
const factory: CustomToolFactory = (pi) => ({
|
|
5
|
+
name: "hello",
|
|
6
|
+
label: "Hello",
|
|
7
|
+
description: "A simple greeting tool",
|
|
8
|
+
parameters: Type.Object({
|
|
9
|
+
name: Type.String({ description: "Name to greet" }),
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
async execute(toolCallId, params) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: `Hello, ${params.name}!` }],
|
|
15
|
+
details: { greeted: params.name },
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default factory;
|