@aliou/pi-dev-kit 0.6.5 → 0.7.0
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/README.md +2 -2
- package/package.json +11 -10
- package/src/commands/index.ts +5 -1
- package/src/commands/update.ts +139 -91
- package/src/skills/pi-extension/SKILL.md +108 -137
- package/src/skills/pi-extension/references/additional-apis.md +252 -208
- package/src/skills/pi-extension/references/commands.md +113 -33
- package/src/skills/pi-extension/references/components.md +267 -102
- package/src/skills/pi-extension/references/hooks.md +229 -156
- package/src/skills/pi-extension/references/messages.md +94 -106
- package/src/skills/pi-extension/references/modes.md +80 -90
- package/src/skills/pi-extension/references/providers.md +255 -96
- package/src/skills/pi-extension/references/publish.md +76 -62
- package/src/skills/pi-extension/references/state.md +80 -33
- package/src/skills/pi-extension/references/structure.md +126 -269
- package/src/skills/pi-extension/references/testing.md +1 -1
- package/src/skills/pi-extension/references/tools.md +198 -823
- package/src/tools/changelog-tool.ts +15 -3
- package/src/tools/docs-tool.ts +3 -3
- package/src/tools/index.ts +5 -1
- package/src/tools/package-manager-tool.ts +8 -4
- package/src/tools/utils.ts +33 -23
- package/src/tools/version-tool.ts +8 -4
- package/src/index.ts +0 -8
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
# Tools
|
|
2
2
|
|
|
3
|
-
Tools are functions the LLM can call. They are the
|
|
3
|
+
Tools are functions the LLM can call. They are the main way extensions add capabilities to Pi.
|
|
4
4
|
|
|
5
5
|
## Imports
|
|
6
6
|
|
|
7
|
-
Use
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
Use the namespace that matches the target Pi package. Examples use the forward namespace.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { ToolBody, ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
11
|
+
import type { AgentToolResult, ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
defineTool,
|
|
14
|
+
formatSize,
|
|
15
|
+
getMarkdownTheme,
|
|
16
|
+
keyHint,
|
|
17
|
+
truncateHead,
|
|
18
|
+
truncateTail,
|
|
19
|
+
withFileMutationQueue,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
22
|
+
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
21
23
|
import { type Static, Type } from "typebox";
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
Use legacy `@mariozechner/*` imports only when the target `@earendil-works/*` package is not available yet.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
## Minimal Tool Entry Point
|
|
29
|
+
|
|
30
|
+
Tool entry points are normal Pi extension entry points. Export a default function and list the file in `package.json` `pi.extensions`.
|
|
27
31
|
|
|
28
32
|
```typescript
|
|
29
33
|
const parameters = Type.Object({
|
|
@@ -32,29 +36,25 @@ const parameters = Type.Object({
|
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
type MyToolParams = Static<typeof parameters>;
|
|
39
|
+
|
|
35
40
|
interface MyToolDetails {
|
|
36
41
|
results: string[];
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const myTool = defineTool({
|
|
40
45
|
name: "my_tool",
|
|
41
|
-
label: "My Tool",
|
|
42
|
-
description: "
|
|
43
|
-
promptSnippet: "Search for items by query",
|
|
44
|
-
promptGuidelines: [
|
|
45
|
-
"Use my_tool when the user asks
|
|
46
|
-
"Prefer specific queries
|
|
46
|
+
label: "My Tool",
|
|
47
|
+
description: "Search for items by query.",
|
|
48
|
+
promptSnippet: "Search for items by query",
|
|
49
|
+
promptGuidelines: [
|
|
50
|
+
"Use my_tool when the user asks to search these items.",
|
|
51
|
+
"Prefer specific queries when calling my_tool.",
|
|
47
52
|
],
|
|
48
53
|
parameters,
|
|
49
54
|
|
|
50
|
-
async execute(
|
|
51
|
-
|
|
52
|
-
params,
|
|
53
|
-
signal,
|
|
54
|
-
onUpdate,
|
|
55
|
-
ctx,
|
|
56
|
-
): Promise<AgentToolResult<MyToolDetails>> {
|
|
57
|
-
const results = await doSomething(params.query, params.limit);
|
|
55
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
|
|
56
|
+
onUpdate?.({ content: [{ type: "text", text: "Searching..." }] });
|
|
57
|
+
const results = await searchItems(params.query, { limit: params.limit, signal });
|
|
58
58
|
return {
|
|
59
59
|
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
60
60
|
details: { results },
|
|
@@ -62,234 +62,113 @@ const myTool = defineTool({
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
export default
|
|
66
|
-
await configLoader.load();
|
|
67
|
-
const config = configLoader.getConfig();
|
|
68
|
-
if (!config.enabled) return;
|
|
69
|
-
|
|
65
|
+
export default function toolsExtension(pi: ExtensionAPI) {
|
|
70
66
|
pi.registerTool(myTool);
|
|
71
67
|
}
|
|
72
68
|
```
|
|
73
69
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
| Field | Type | Required | Description |
|
|
77
|
-
|-------|------|----------|-------------|
|
|
78
|
-
| `name` | `string` | Yes | Snake_case identifier used in tool calls |
|
|
79
|
-
| `label` | `string` | Yes | Human-readable name shown in UI |
|
|
80
|
-
| `description` | `string` | Yes | What the tool does (LLM reads this) |
|
|
81
|
-
| `parameters` | `TSchema` | Yes | TypeBox schema for arguments |
|
|
82
|
-
| `promptSnippet` | `string` | No | One-liner injected into "Available tools" system prompt. Custom tools without this are omitted from that section. |
|
|
83
|
-
| `promptGuidelines` | `string[]` | No | Guideline bullets appended verbatim to the global "Guidelines" system prompt section when this tool is active. Write each bullet so it still makes sense standalone. |
|
|
84
|
-
| `execute` | `function` | Yes | Implementation |
|
|
85
|
-
| `renderCall` | `function` | No | Custom call rendering |
|
|
86
|
-
| `renderResult` | `function` | No | Custom result rendering |
|
|
70
|
+
## `defineTool()`
|
|
87
71
|
|
|
88
|
-
|
|
72
|
+
Use `defineTool({...})` for standalone tool objects. It preserves parameter inference from the `parameters` field when the tool is stored in a variable or passed through arrays.
|
|
89
73
|
|
|
90
|
-
|
|
74
|
+
Rules:
|
|
91
75
|
|
|
92
|
-
|
|
76
|
+
- Do not pass explicit generic arguments to `defineTool`.
|
|
77
|
+
- Do not annotate callback parameters unless TypeScript needs help.
|
|
78
|
+
- Add a `Static<typeof parameters>` alias for helper functions and action modules.
|
|
79
|
+
- If you want `renderResult` to know the `details` shape, annotate `execute` with `Promise<AgentToolResult<MyDetails>>`.
|
|
93
80
|
|
|
94
|
-
|
|
81
|
+
## Tool Definition Fields
|
|
95
82
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
| Field | Required | Notes |
|
|
84
|
+
|---|---:|---|
|
|
85
|
+
| `name` | Yes | Snake_case identifier used in tool calls. |
|
|
86
|
+
| `label` | Yes | Human-readable display name. |
|
|
87
|
+
| `description` | Yes | Model-facing description. |
|
|
88
|
+
| `parameters` | Yes | TypeBox 1.x schema from `typebox`. |
|
|
89
|
+
| `execute` | Yes | `(toolCallId, params, signal, onUpdate, ctx)`. |
|
|
90
|
+
| `promptSnippet` | No | One-line entry in Available tools. Custom tools are omitted there when absent. |
|
|
91
|
+
| `promptGuidelines` | No | Raw bullets appended to the global Guidelines section. Each bullet must name the tool. |
|
|
92
|
+
| `prepareArguments` | No | Compatibility shim before schema validation. |
|
|
93
|
+
| `executionMode` | No | Use `"sequential"` for shared-state tools that cannot run concurrently. |
|
|
94
|
+
| `renderCall` / `renderResult` | No | Custom TUI component renderers. |
|
|
95
|
+
| `renderShell` | No | Use `"self"` only when the default boxed shell gets in the way. |
|
|
105
96
|
|
|
106
97
|
## Execute Signature
|
|
107
98
|
|
|
108
99
|
```typescript
|
|
109
|
-
execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>
|
|
100
|
+
async execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>
|
|
110
101
|
```
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
execute(
|
|
116
|
-
toolCallId: string,
|
|
117
|
-
params: Static<TParams>,
|
|
118
|
-
signal: AbortSignal | undefined,
|
|
119
|
-
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
120
|
-
ctx: ExtensionContext,
|
|
121
|
-
): Promise<AgentToolResult<TDetails>>
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
You usually do not need to write those parameter types yourself inside `defineTool()`.
|
|
125
|
-
|
|
126
|
-
**Parameter order matters.** The signal comes before onUpdate.
|
|
103
|
+
Parameter order matters. `signal` comes before `onUpdate`.
|
|
127
104
|
|
|
128
|
-
|
|
105
|
+
Use optional chaining for updates:
|
|
129
106
|
|
|
130
107
|
```typescript
|
|
131
|
-
onUpdate?.({
|
|
108
|
+
onUpdate?.({
|
|
109
|
+
content: [{ type: "text", text: "Working..." }],
|
|
110
|
+
details: { progress: 50 },
|
|
111
|
+
});
|
|
132
112
|
```
|
|
133
113
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## Tool Overrides and Delegation
|
|
137
|
-
|
|
138
|
-
If you override a built-in tool or wrap another tool, audit any delegated `tool.execute(...)` calls during upgrades. These forwarders often pass through `signal`, `onUpdate`, or `ctx` and can silently break when the execute signature changes. Always recheck the delegate call parameter order and include optional parameters that the target tool expects.
|
|
139
|
-
|
|
140
|
-
Prompt metadata is not inherited automatically when you override a built-in tool. If the original tool had `promptSnippet` or `promptGuidelines` and you still want that system prompt behavior, define those fields explicitly on the override.
|
|
141
|
-
|
|
142
|
-
## Return Value
|
|
114
|
+
Forward `signal` to all abort-aware work:
|
|
143
115
|
|
|
144
116
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
details?: TDetails, // Arbitrary data available in the renderer
|
|
148
|
-
terminate?: boolean, // Optional: skip follow-up LLM call when all finalized results in the batch terminate
|
|
149
|
-
};
|
|
117
|
+
const response = await fetch(url, { signal });
|
|
118
|
+
const result = await pi.exec("git", ["status", "--porcelain"], { signal, cwd: ctx.cwd });
|
|
150
119
|
```
|
|
151
120
|
|
|
152
|
-
|
|
153
|
-
- `details` is what the renderer sees. Put rich data here for custom display.
|
|
154
|
-
- `terminate: true` is for final/structured-output tools that should end the turn without an automatic follow-up LLM call. It only applies when every finalized tool result in the current batch also returns `terminate: true`.
|
|
121
|
+
Do not use Node `child_process` for normal commands. Use `pi.exec(command, args, options)` so Pi handles CWD, cancellation, output capture, and lifecycle integration.
|
|
155
122
|
|
|
156
|
-
|
|
123
|
+
## Return Value and Errors
|
|
157
124
|
|
|
158
125
|
```typescript
|
|
159
126
|
return {
|
|
160
|
-
content: [{ type: "text", text:
|
|
161
|
-
details: {
|
|
127
|
+
content: [{ type: "text", text: "Text sent to the LLM" }],
|
|
128
|
+
details: { rich: "data for rendering and state" },
|
|
129
|
+
terminate: true, // optional final/structured-output tools only
|
|
162
130
|
};
|
|
163
131
|
```
|
|
164
132
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
171
|
-
const result = await fetchData(params.query);
|
|
172
|
-
if (!result) {
|
|
173
|
-
throw new Error("No results found. Try a different query.");
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
177
|
-
details: { result },
|
|
178
|
-
};
|
|
179
|
-
},
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The framework's `createErrorToolResult` sets `details: {}` (empty object, not `undefined`) and puts the error message in `content`:
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
// Framework produces when tool throws:
|
|
186
|
-
{
|
|
187
|
-
content: [{ type: "text", text: errorMessage }],
|
|
188
|
-
details: {}
|
|
189
|
-
}
|
|
190
|
-
// And sets context.isError = true in renderResult
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Error rendering in `renderResult`
|
|
194
|
-
|
|
195
|
-
The framework passes `isError` on the 4th `context` parameter to `renderResult`, but `ToolRenderContext` is not currently exported from the public API. Two practical approaches:
|
|
196
|
-
|
|
197
|
-
**Approach 1: Check for missing expected fields in `details`** (recommended for extensions)
|
|
198
|
-
|
|
199
|
-
When a tool throws, the framework sets `details: {}` (empty object). Check for missing expected fields:
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
renderResult(
|
|
203
|
-
result: AgentToolResult<MyToolDetails>,
|
|
204
|
-
options: ToolRenderResultOptions,
|
|
205
|
-
theme: Theme,
|
|
206
|
-
) {
|
|
207
|
-
const { details } = result;
|
|
208
|
-
|
|
209
|
-
// details is {} when tool threw — expected fields are missing
|
|
210
|
-
if (!details?.results) {
|
|
211
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
212
|
-
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Operation failed";
|
|
213
|
-
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
214
|
-
}
|
|
215
|
-
// ... normal rendering
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**Approach 2: Use the 4th context parameter** (used by native tools)
|
|
220
|
-
|
|
221
|
-
Define a minimal interface locally:
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
interface RenderContext { isError: boolean }
|
|
225
|
-
|
|
226
|
-
renderResult(
|
|
227
|
-
result: AgentToolResult<MyToolDetails>,
|
|
228
|
-
options: ToolRenderResultOptions,
|
|
229
|
-
theme: Theme,
|
|
230
|
-
context: RenderContext,
|
|
231
|
-
) {
|
|
232
|
-
if (context.isError) {
|
|
233
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
234
|
-
const errorMsg = textBlock?.type === "text" ? textBlock.text : "Operation failed";
|
|
235
|
-
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
236
|
-
}
|
|
237
|
-
// ... normal rendering
|
|
238
|
-
}
|
|
239
|
-
```
|
|
133
|
+
- `content` is sent to the LLM. Keep it concise and useful.
|
|
134
|
+
- `details` is persisted in the session and used by renderers. Put state snapshots here when the tool mutates extension state.
|
|
135
|
+
- `terminate: true` skips the automatic follow-up LLM call only when every finalized tool in the current batch also terminates.
|
|
136
|
+
- Throw an error to mark a tool result as failed. Returning an object never sets `isError`.
|
|
240
137
|
|
|
241
|
-
|
|
138
|
+
When a tool throws, Pi creates an error result with `details: {}` and the error message in `content`. Renderers should check for missing expected fields or use the 4th render context's `isError` field.
|
|
242
139
|
|
|
243
|
-
## Parameters
|
|
140
|
+
## Parameters
|
|
244
141
|
|
|
245
|
-
Use TypeBox 1.x
|
|
142
|
+
Use TypeBox 1.x from `typebox`. Always add useful descriptions.
|
|
246
143
|
|
|
247
144
|
```typescript
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Type.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Type.Optional(Type.
|
|
255
|
-
|
|
256
|
-
// Enum (string union)
|
|
257
|
-
Type.StringEnum(["created", "updated", "relevance"], { description: "Sort order" })
|
|
258
|
-
|
|
259
|
-
// Boolean
|
|
260
|
-
Type.Boolean({ description: "Include hidden files" })
|
|
261
|
-
|
|
262
|
-
// Nested object
|
|
263
|
-
Type.Object({
|
|
264
|
-
name: Type.String(),
|
|
265
|
-
value: Type.String(),
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
// Array
|
|
269
|
-
Type.Array(Type.String(), { description: "List of tags" })
|
|
145
|
+
const parameters = Type.Object({
|
|
146
|
+
path: Type.String({ description: "File path" }),
|
|
147
|
+
includeHidden: Type.Optional(Type.Boolean({ description: "Include hidden files", default: false })),
|
|
148
|
+
sort: Type.Optional(StringEnum(["created", "updated", "relevance"] as const, {
|
|
149
|
+
description: "Sort order",
|
|
150
|
+
})),
|
|
151
|
+
tags: Type.Optional(Type.Array(Type.String({ description: "Tag" }))),
|
|
152
|
+
});
|
|
270
153
|
```
|
|
271
154
|
|
|
272
|
-
|
|
155
|
+
Use `StringEnum` from `pi-ai` for model-facing string enums. Avoid `Type.Union([Type.Literal(...)])`; it is not compatible with all providers.
|
|
273
156
|
|
|
274
157
|
## Prompt Metadata
|
|
275
158
|
|
|
276
|
-
`promptSnippet` and `promptGuidelines` affect different
|
|
277
|
-
|
|
278
|
-
- `promptSnippet` adds a one-line entry to `Available tools`.
|
|
279
|
-
- `promptGuidelines` appends raw bullets to the global `Guidelines` section.
|
|
159
|
+
`promptSnippet` and `promptGuidelines` affect different system prompt sections:
|
|
280
160
|
|
|
281
|
-
|
|
161
|
+
- `promptSnippet` adds one line under Available tools.
|
|
162
|
+
- `promptGuidelines` appends raw bullets to the global Guidelines section when the tool is active.
|
|
282
163
|
|
|
283
|
-
|
|
284
|
-
- Write bullets so they still make sense when read out of context.
|
|
285
|
-
- Prefer explicit tool names over phrases like `this tool`.
|
|
164
|
+
Because `promptGuidelines` are not grouped under the tool, each bullet must name the exact tool.
|
|
286
165
|
|
|
287
166
|
Good:
|
|
288
167
|
|
|
289
168
|
```typescript
|
|
290
169
|
promptGuidelines: [
|
|
291
|
-
"Use
|
|
292
|
-
"
|
|
170
|
+
"Use repo_tree to inspect repository structure before reading individual files.",
|
|
171
|
+
"Start repo_tree at the root path, then drill into directories as needed.",
|
|
293
172
|
]
|
|
294
173
|
```
|
|
295
174
|
|
|
@@ -297,16 +176,16 @@ Weak:
|
|
|
297
176
|
|
|
298
177
|
```typescript
|
|
299
178
|
promptGuidelines: [
|
|
300
|
-
"Use this tool
|
|
301
|
-
"
|
|
179
|
+
"Use this tool before reading files.",
|
|
180
|
+
"Start at the root path.",
|
|
302
181
|
]
|
|
303
182
|
```
|
|
304
183
|
|
|
305
|
-
Use
|
|
184
|
+
Use per-tool metadata for simple guidance. Use a `before_agent_start` hook for dynamic config, cross-tool workflows, or complex orchestration.
|
|
306
185
|
|
|
307
|
-
## Argument Compatibility
|
|
186
|
+
## Argument Compatibility
|
|
308
187
|
|
|
309
|
-
Use `prepareArguments(args)`
|
|
188
|
+
Use `prepareArguments(args)` for backward-compatible migrations of old session tool-call shapes. It runs before validation. Keep the public schema strict.
|
|
310
189
|
|
|
311
190
|
```typescript
|
|
312
191
|
prepareArguments(args) {
|
|
@@ -319,21 +198,18 @@ prepareArguments(args) {
|
|
|
319
198
|
}
|
|
320
199
|
```
|
|
321
200
|
|
|
322
|
-
|
|
201
|
+
## Paths and File Mutation
|
|
202
|
+
|
|
203
|
+
If a tool accepts file paths, strip a leading `@` before resolving. Built-in tools do this because models sometimes include `@` from path mentions.
|
|
323
204
|
|
|
324
205
|
```typescript
|
|
325
206
|
const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
|
|
207
|
+
const absolutePath = resolve(ctx.cwd, normalizedPath);
|
|
326
208
|
```
|
|
327
209
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Tool calls can run in parallel. If your custom tool mutates files, use `withFileMutationQueue()` so it participates in the same per-file queue as built-in `edit` and `write`.
|
|
210
|
+
If a custom tool mutates files, wrap the whole read-modify-write window in `withFileMutationQueue()` so sibling parallel tool calls cannot overwrite each other.
|
|
331
211
|
|
|
332
212
|
```typescript
|
|
333
|
-
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
334
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
335
|
-
import { dirname, resolve } from "node:path";
|
|
336
|
-
|
|
337
213
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
338
214
|
const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
|
|
339
215
|
const absolutePath = resolve(ctx.cwd, normalizedPath);
|
|
@@ -343,120 +219,94 @@ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
|
343
219
|
const current = await readFile(absolutePath, "utf8");
|
|
344
220
|
const next = current.replace(params.oldText, params.newText);
|
|
345
221
|
await writeFile(absolutePath, next, "utf8");
|
|
346
|
-
|
|
347
222
|
return {
|
|
348
223
|
content: [{ type: "text", text: `Updated ${normalizedPath}` }],
|
|
349
|
-
details: {},
|
|
224
|
+
details: { path: normalizedPath },
|
|
350
225
|
};
|
|
351
226
|
});
|
|
352
227
|
}
|
|
353
228
|
```
|
|
354
229
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
## Streaming Updates
|
|
230
|
+
## Concurrency
|
|
358
231
|
|
|
359
|
-
|
|
232
|
+
Pi runs sibling tool calls in parallel by default. Add `executionMode: "sequential"` when this tool must serialize with sibling tool calls, for example when it mutates shared in-memory state, drives a cursor/game, or depends on strict call order.
|
|
360
233
|
|
|
361
234
|
```typescript
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
details: { complete: true },
|
|
373
|
-
};
|
|
374
|
-
},
|
|
235
|
+
const statefulTool = defineTool({
|
|
236
|
+
name: "game_move",
|
|
237
|
+
label: "Game Move",
|
|
238
|
+
description: "Apply a move to the current game state.",
|
|
239
|
+
executionMode: "sequential",
|
|
240
|
+
parameters,
|
|
241
|
+
async execute(_toolCallId, params) {
|
|
242
|
+
// Mutates shared in-memory state safely.
|
|
243
|
+
},
|
|
244
|
+
});
|
|
375
245
|
```
|
|
376
246
|
|
|
377
|
-
|
|
247
|
+
Prefer making core operations concurrency-safe. Use sequential mode only when call order is inherently meaningful.
|
|
378
248
|
|
|
379
|
-
|
|
249
|
+
## Rendering
|
|
380
250
|
|
|
381
|
-
|
|
251
|
+
Custom renderers return TUI `Component` objects.
|
|
382
252
|
|
|
383
|
-
|
|
384
|
-
renderCall(args: MyToolParams, theme: Theme): Component
|
|
385
|
-
```
|
|
253
|
+
### `renderCall`
|
|
386
254
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
Use `ToolCallHeader` from `@aliou/pi-utils-ui`:
|
|
255
|
+
Use `ToolCallHeader` for a stable, scannable call line.
|
|
390
256
|
|
|
391
257
|
```typescript
|
|
392
|
-
renderCall(args
|
|
258
|
+
renderCall(args, theme) {
|
|
393
259
|
return new ToolCallHeader(
|
|
394
260
|
{
|
|
395
|
-
toolName: "
|
|
396
|
-
action:
|
|
397
|
-
mainArg:
|
|
398
|
-
optionArgs: [
|
|
399
|
-
longArgs: [
|
|
400
|
-
showColon: true, // Whether to show colon after tool name
|
|
261
|
+
toolName: "Repo Tree",
|
|
262
|
+
action: args.action,
|
|
263
|
+
mainArg: `${args.owner}/${args.repo}`,
|
|
264
|
+
optionArgs: args.path ? [{ label: "path", value: args.path }] : [],
|
|
265
|
+
longArgs: args.instructions ? [{ label: "instructions", value: args.instructions }] : [],
|
|
401
266
|
},
|
|
402
267
|
theme,
|
|
403
268
|
);
|
|
404
269
|
}
|
|
405
270
|
```
|
|
406
271
|
|
|
407
|
-
|
|
272
|
+
Header extraction order:
|
|
408
273
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
```
|
|
274
|
+
1. Action for multi-action tools.
|
|
275
|
+
2. One main argument users scan for first.
|
|
276
|
+
3. Short option args.
|
|
277
|
+
4. Long args on follow-up lines.
|
|
278
|
+
|
|
279
|
+
### `renderResult`
|
|
416
280
|
|
|
417
|
-
|
|
418
|
-
- `result.details` is `{}` (empty object) when the tool threw an error
|
|
419
|
-
- A 4th `context` param is available (see Error Handling above) but not required
|
|
281
|
+
Handle partial state first, then errors, then normal output.
|
|
420
282
|
|
|
421
283
|
```typescript
|
|
422
|
-
renderResult(
|
|
423
|
-
result: AgentToolResult<MyToolDetails>,
|
|
424
|
-
options: ToolRenderResultOptions,
|
|
425
|
-
theme: Theme,
|
|
426
|
-
) {
|
|
427
|
-
// 1. Handle partial state first with stable message
|
|
284
|
+
renderResult(result, options, theme) {
|
|
428
285
|
if (options.isPartial) {
|
|
429
|
-
return new Text(theme.fg("muted", "
|
|
286
|
+
return new Text(theme.fg("muted", "Repo Tree: fetching..."), 0, 0);
|
|
430
287
|
}
|
|
431
288
|
|
|
432
289
|
const { details } = result;
|
|
433
|
-
|
|
434
|
-
// 2. Handle errors: details is {} when tool threw
|
|
435
|
-
if (!details?.results) {
|
|
290
|
+
if (!details?.entries) {
|
|
436
291
|
const textBlock = result.content.find((c) => c.type === "text");
|
|
437
|
-
const
|
|
438
|
-
return new Text(theme.fg("error",
|
|
292
|
+
const message = textBlock?.type === "text" ? textBlock.text : "Repo Tree failed";
|
|
293
|
+
return new Text(theme.fg("error", message), 0, 0);
|
|
439
294
|
}
|
|
440
295
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const fields: Array<{ label: string; value: string; showCollapsed?: boolean } | Text> = [
|
|
445
|
-
{ label: "Results", value: `${items.length} items`, showCollapsed: true },
|
|
296
|
+
const fields = [
|
|
297
|
+
{ label: "Location", value: `${details.owner}/${details.repo}`, showCollapsed: true },
|
|
298
|
+
{ label: "Entries", value: `${details.entries.length}`, showCollapsed: true },
|
|
446
299
|
];
|
|
447
300
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
footerItems.push({ label: "count", value: `${items.length}` });
|
|
301
|
+
const footerItems = [];
|
|
302
|
+
if (!options.expanded && details.entries.length > 5) {
|
|
303
|
+
footerItems.push({ value: keyHint("app.tools.expand", "to expand") });
|
|
452
304
|
}
|
|
453
305
|
|
|
454
306
|
return new ToolBody(
|
|
455
307
|
{
|
|
456
308
|
fields,
|
|
457
|
-
footer: footerItems.length > 0
|
|
458
|
-
? new ToolFooter(theme, { items: footerItems, separator: " | " })
|
|
459
|
-
: undefined,
|
|
309
|
+
footer: footerItems.length > 0 ? new ToolFooter(theme, { items: footerItems }) : undefined,
|
|
460
310
|
includeSpacerBeforeFooter: fields.length > 0,
|
|
461
311
|
},
|
|
462
312
|
options,
|
|
@@ -465,590 +315,115 @@ renderResult(
|
|
|
465
315
|
}
|
|
466
316
|
```
|
|
467
317
|
|
|
468
|
-
### `renderCall` Design Guide
|
|
469
|
-
|
|
470
|
-
The `process` extension is a good baseline. Its call renderer is deterministic and keeps headers readable.
|
|
471
|
-
|
|
472
|
-
Use this extraction order when building header parts:
|
|
473
|
-
|
|
474
|
-
1. **Action first**: always show action for multi-action tools (`start`, `list`, `kill`, ...).
|
|
475
|
-
2. **Pick one main arg**: choose the single value the user scans for first (name, id, or short command).
|
|
476
|
-
3. **Promote short fields to options**: compact values become option args (`end=true`, `limit=10`).
|
|
477
|
-
4. **Demote long fields to long args**: commands/prompts/instructions move to labeled follow-up lines.
|
|
478
|
-
5. **Keep it stable**: same inputs should produce same ordering and formatting.
|
|
479
|
-
|
|
480
|
-
Implementation pattern:
|
|
481
|
-
- Build `mainArg`, `optionArgs`, `longArgs` first, then pass to `ToolCallHeader`.
|
|
482
|
-
- Quote user-provided names (`"backend"`) when that improves visual parsing.
|
|
483
|
-
- Cap inline length (e.g. 60-80 chars), then spill to `longArgs`.
|
|
484
|
-
|
|
485
|
-
### `renderResult` Guidelines
|
|
486
|
-
|
|
487
|
-
- Handle `isPartial` first with a **stable, tool-scoped message** like `"MyTool: fetching..."`. Do NOT echo streaming content.
|
|
488
|
-
- Handle errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw). Alternatively use the 4th `context` param with `context.isError`.
|
|
489
|
-
- Use `ToolBody` with `showCollapsed: true` fields for collapsed/expanded filtering.
|
|
490
|
-
- Use `ToolFooter` for stats/metadata. Omit when empty: `footer: items.length > 0 ? new ToolFooter(...) : undefined`.
|
|
491
|
-
- Use `includeSpacerBeforeFooter: fields.length > 0` to avoid blank line when body is empty.
|
|
492
|
-
- Remove redundant success footer items — don't show `status: ok` when success is obvious.
|
|
493
|
-
- Use `Container` + `addChild()` for multi-element results instead of string concatenation.
|
|
494
|
-
- Use `Markdown` component for rich markdown content:
|
|
495
|
-
```typescript
|
|
496
|
-
new Markdown(text, 0, 0, getMarkdownTheme(), { color: (t) => theme.fg("toolOutput", t) })
|
|
497
|
-
```
|
|
498
|
-
- Use `keyHint("app.tools.expand", "to expand")` for expand hints.
|
|
499
|
-
- Collapsed result should show **actionable preview** (last N lines, first N items with status), not just a status badge.
|
|
500
|
-
- Humanize error messages with names first: `"Could not get X for "name" (id)"`.
|
|
501
|
-
|
|
502
|
-
## Tool UI Rendering Guidelines
|
|
503
|
-
|
|
504
|
-
When customizing tool rendering, keep call/result UI predictable and scannable.
|
|
505
|
-
|
|
506
|
-
### `renderCall` format
|
|
507
|
-
|
|
508
|
-
Use this line model:
|
|
509
|
-
|
|
510
|
-
- First line: `[Tool Name]: [Action] [Main arg] [Option args]`
|
|
511
|
-
- Additional lines: long args only
|
|
512
|
-
|
|
513
318
|
Guidelines:
|
|
514
|
-
- Tool name should be a human display label (`label`), not a raw internal identifier (`name`).
|
|
515
|
-
- Show `action` only when it adds meaning (multi-action tools like process managers).
|
|
516
|
-
- Main arg should be the primary thing user cares about (query, session id, target id/name).
|
|
517
|
-
- Option args should be compact key-value pairs (`limit=10`, `cwd=/path`).
|
|
518
|
-
- Long text (prompt/task/question/context/instructions) goes to additional lines.
|
|
519
|
-
- Prefer wrapping to preserve full meaning over aggressive truncation.
|
|
520
|
-
- For tools without actions, omit colon suffix after tool name if that reads better in your UI system.
|
|
521
|
-
|
|
522
|
-
### `renderResult` layout
|
|
523
|
-
|
|
524
|
-
- Handle `isPartial` first. Return a short stable loading state with tool-scoped message.
|
|
525
|
-
- Keep the first non-loading line as a status summary (`Found N results`, `Updated 3 files`, `Failed: reason`).
|
|
526
|
-
- Use `expanded` to switch between compact and full output. Compact should show the top few items plus an omission hint.
|
|
527
|
-
- Keep body content focused on state + key output; avoid dumping raw JSON unless it is the actual output.
|
|
528
|
-
- If you render a footer (stats, backend, counts), use `includeSpacerBeforeFooter` to control blank line.
|
|
529
|
-
- Keep footer concise and stable across states.
|
|
530
|
-
- Return `undefined` when custom rendering adds no value; fallback rendering is better than noisy UI.
|
|
531
|
-
|
|
532
|
-
## Tool Call + UI Consistency Contract
|
|
533
|
-
|
|
534
|
-
Use this contract to keep tool UX consistent across extensions:
|
|
535
|
-
|
|
536
|
-
1. **Call line is for scanability**: `renderCall` first line follows `[Tool Name]: [Action] [Main arg] [Option args]`.
|
|
537
|
-
2. **Result detects errors** by checking for missing expected fields in `details` (framework sets `details: {}` on throw).
|
|
538
|
-
3. **Long text moves down**: prompts, instructions, and context go to follow-up lines, not the call header.
|
|
539
|
-
4. **Partial updates use a fixed tool-scoped string**, not echoed streaming content.
|
|
540
|
-
5. **Expanded controls density**: compact view shows summary + subset; expanded view shows full detail.
|
|
541
|
-
6. **No mode leaks in tool renderers**: `renderCall`/`renderResult` should not branch on mode. Mode-specific behavior belongs in command/tool logic (`references/modes.md`).
|
|
542
|
-
|
|
543
|
-
Related references:
|
|
544
|
-
- `references/modes.md` for `custom()` fallback behavior and RPC/Print handling.
|
|
545
|
-
- `references/components.md` for interactive component authoring.
|
|
546
|
-
- `references/messages.md` for persistent display via `sendMessage` + `registerMessageRenderer`.
|
|
547
|
-
|
|
548
|
-
## Naming Conventions
|
|
549
|
-
|
|
550
|
-
For extensions wrapping a third-party API, prefix tool names with the API name to avoid conflicts:
|
|
551
|
-
|
|
552
|
-
```
|
|
553
|
-
linkup_web_search
|
|
554
|
-
linkup_web_fetch
|
|
555
|
-
synthetic_web_search
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
For internal/custom tools, no prefix is needed:
|
|
559
319
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
Use
|
|
566
|
-
|
|
567
|
-
## Abort Signal
|
|
568
|
-
|
|
569
|
-
The `signal` parameter lets you cancel long-running operations when the user interrupts (e.g. pressing Escape). If the tool does not forward the signal, the underlying operation keeps running even after the user cancels, wasting resources and API credits.
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
573
|
-
const response = await fetch(url, { signal });
|
|
574
|
-
// If the user cancels, fetch throws an AbortError
|
|
575
|
-
return { content: [{ type: "text", text: await response.text() }], details: {} };
|
|
576
|
-
},
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
Pass `signal` to every async operation that supports it: `fetch()` calls, `pi.exec()` calls, SDK clients, etc.
|
|
580
|
-
|
|
581
|
-
When wrapping an API client, thread the signal through the entire call chain. The client methods should accept an optional `signal` and forward it to the underlying `fetch()`:
|
|
582
|
-
|
|
583
|
-
```typescript
|
|
584
|
-
// In the tool:
|
|
585
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
586
|
-
const result = await client.search({ query: params.query, signal });
|
|
587
|
-
// ...
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// In the client:
|
|
591
|
-
async search(params: { query: string; signal?: AbortSignal }) {
|
|
592
|
-
return this.request("/search", { method: "POST", body: ... }, params.signal);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private async request<T>(endpoint: string, options: RequestInit = {}, signal?: AbortSignal) {
|
|
596
|
-
return fetch(`${BASE_URL}${endpoint}`, { ...options, signal, headers: { ... } });
|
|
597
|
-
}
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
Do not prefix signal with underscore (`_signal`) unless the tool genuinely cannot use it. A dangling `_signal` is a sign of a missing cancellation path.
|
|
320
|
+
- Partial output uses a fixed tool-scoped message; do not echo streaming content.
|
|
321
|
+
- Collapsed view should show useful preview, not just `ok`.
|
|
322
|
+
- Expanded view can add full details.
|
|
323
|
+
- Omit empty footers.
|
|
324
|
+
- Use `Markdown` with `getMarkdownTheme()` for rich markdown.
|
|
325
|
+
- Use `context.lastComponent` only when reusing a component instance buys real performance.
|
|
326
|
+
- Set `renderShell: "self"` only when the default boxed shell prevents the desired layout.
|
|
601
327
|
|
|
602
328
|
## Output Truncation
|
|
603
329
|
|
|
604
|
-
|
|
330
|
+
Tools must avoid overwhelming model context. Use `truncateHead` for content where the beginning matters and `truncateTail` for logs or command output where the end matters.
|
|
605
331
|
|
|
606
332
|
```typescript
|
|
607
|
-
import { truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
|
|
608
|
-
import { createWriteStream } from "fs";
|
|
609
|
-
import { tmpdir } from "os";
|
|
610
|
-
import { join } from "path";
|
|
611
|
-
|
|
612
333
|
interface FetchDetails {
|
|
613
|
-
content: string;
|
|
614
334
|
url: string;
|
|
335
|
+
content: string;
|
|
615
336
|
truncated: boolean;
|
|
616
337
|
totalLines: number;
|
|
617
|
-
totalBytes: number;
|
|
618
338
|
outputLines: number;
|
|
339
|
+
totalBytes: number;
|
|
619
340
|
outputBytes: number;
|
|
620
341
|
tempFile?: string;
|
|
621
342
|
}
|
|
622
343
|
|
|
623
|
-
async execute(
|
|
624
|
-
_toolCallId: string,
|
|
625
|
-
params: FetchParams,
|
|
626
|
-
signal: AbortSignal | undefined,
|
|
627
|
-
_onUpdate: undefined,
|
|
628
|
-
_ctx: ExtensionContext,
|
|
629
|
-
): Promise<AgentToolResult<FetchDetails>> {
|
|
344
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<FetchDetails>> {
|
|
630
345
|
const response = await fetch(params.url, { signal });
|
|
631
346
|
const fullContent = await response.text();
|
|
632
|
-
|
|
633
|
-
// truncateHead keeps the tail (most recent content)
|
|
634
|
-
const truncated = truncateHead(fullContent, {
|
|
635
|
-
maxBytes: 50000,
|
|
636
|
-
maxLines: 2000,
|
|
637
|
-
});
|
|
347
|
+
const truncated = truncateHead(fullContent, { maxBytes: 50_000, maxLines: 2_000 });
|
|
638
348
|
|
|
639
349
|
const details: FetchDetails = {
|
|
640
|
-
content: truncated.content,
|
|
641
350
|
url: params.url,
|
|
351
|
+
content: truncated.content,
|
|
642
352
|
truncated: truncated.truncated,
|
|
643
353
|
totalLines: truncated.totalLines,
|
|
644
|
-
totalBytes: truncated.totalBytes,
|
|
645
354
|
outputLines: truncated.outputLines,
|
|
355
|
+
totalBytes: truncated.totalBytes,
|
|
646
356
|
outputBytes: truncated.outputBytes,
|
|
647
357
|
};
|
|
648
358
|
|
|
649
|
-
|
|
359
|
+
let text = truncated.content;
|
|
650
360
|
if (truncated.truncated) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
details.tempFile = tempFile;
|
|
361
|
+
details.tempFile = await writeTempFile(fullContent);
|
|
362
|
+
text += `\n\n[Output truncated: ${truncated.outputLines}/${truncated.totalLines} lines, `;
|
|
363
|
+
text += `${formatSize(truncated.outputBytes)}/${formatSize(truncated.totalBytes)}. `;
|
|
364
|
+
text += `Full output: ${details.tempFile}]`;
|
|
656
365
|
}
|
|
657
366
|
|
|
658
|
-
return {
|
|
659
|
-
content: [{ type: "text", text: truncated.content }],
|
|
660
|
-
details,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### Truncation Result Fields
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
interface TruncationResult {
|
|
669
|
-
content: string; // The truncated content (or full if not truncated)
|
|
670
|
-
truncated: boolean; // Whether truncation occurred
|
|
671
|
-
totalLines: number; // Original total lines
|
|
672
|
-
totalBytes: number; // Original total bytes
|
|
673
|
-
outputLines: number; // Lines in output
|
|
674
|
-
outputBytes: number; // Bytes in output
|
|
367
|
+
return { content: [{ type: "text", text }], details };
|
|
675
368
|
}
|
|
676
369
|
```
|
|
677
370
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
renderResult(
|
|
682
|
-
result: AgentToolResult<FetchDetails>,
|
|
683
|
-
options: ToolRenderResultOptions,
|
|
684
|
-
theme: Theme,
|
|
685
|
-
) {
|
|
686
|
-
if (options.isPartial) {
|
|
687
|
-
return new Text(theme.fg("muted", "WebFetch: fetching..."), 0, 0);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const { details } = result;
|
|
691
|
-
|
|
692
|
-
if (!details?.content) {
|
|
693
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
694
|
-
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Fetch failed";
|
|
695
|
-
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
696
|
-
}
|
|
697
|
-
const container = new Container();
|
|
698
|
-
|
|
699
|
-
// Body with content preview
|
|
700
|
-
const fields = [
|
|
701
|
-
{ label: "URL", value: details.url, showCollapsed: true },
|
|
702
|
-
{ label: "Size", value: formatSize(details.totalBytes), showCollapsed: true },
|
|
703
|
-
];
|
|
704
|
-
|
|
705
|
-
container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
|
|
706
|
-
|
|
707
|
-
// Footer with truncation info
|
|
708
|
-
const footerItems = [];
|
|
709
|
-
|
|
710
|
-
if (details.truncated) {
|
|
711
|
-
footerItems.push(
|
|
712
|
-
{ label: "showing", value: `${details.outputLines}/${details.totalLines} lines` },
|
|
713
|
-
{ label: "full output", value: details.tempFile ?? "temp file" },
|
|
714
|
-
);
|
|
715
|
-
} else {
|
|
716
|
-
footerItems.push({ label: "lines", value: `${details.totalLines}` });
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
container.addChild(
|
|
720
|
-
new ToolFooter(theme, {
|
|
721
|
-
items: footerItems,
|
|
722
|
-
separator: " | ",
|
|
723
|
-
includeSpacerBeforeFooter: fields.length > 0,
|
|
724
|
-
}),
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
return container;
|
|
728
|
-
}
|
|
729
|
-
```
|
|
371
|
+
Always tell the LLM when output was truncated and where full output was saved.
|
|
730
372
|
|
|
731
373
|
## Multi-Action Tools
|
|
732
374
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
### File Structure
|
|
375
|
+
Use a directory when one tool has several actions.
|
|
736
376
|
|
|
737
377
|
```
|
|
738
|
-
tools/
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
render.ts // renderCall and renderResult (when complex)
|
|
746
|
-
types.ts // Shared types and param schema
|
|
378
|
+
tools/my_tool/
|
|
379
|
+
index.ts
|
|
380
|
+
actions/start.ts
|
|
381
|
+
actions/list.ts
|
|
382
|
+
actions/kill.ts
|
|
383
|
+
render.ts
|
|
384
|
+
types.ts
|
|
747
385
|
```
|
|
748
386
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
Each action takes an SDK client and typed params, returns typed results:
|
|
387
|
+
Pattern:
|
|
752
388
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
command: string;
|
|
758
|
-
cwd?: string;
|
|
759
|
-
}
|
|
389
|
+
- `types.ts`: parameter schema, `Static` alias, details/result types.
|
|
390
|
+
- `actions/*`: Pi-free or mostly Pi-free action logic.
|
|
391
|
+
- `render.ts`: `renderCall`/`renderResult` when complex.
|
|
392
|
+
- `index.ts`: `defineTool`, execute switch, registration.
|
|
760
393
|
|
|
761
|
-
|
|
762
|
-
sessionId: string;
|
|
763
|
-
pid: number;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
export async function start(
|
|
767
|
-
client: MyClient,
|
|
768
|
-
params: StartParams,
|
|
769
|
-
signal?: AbortSignal,
|
|
770
|
-
): Promise<StartResult> {
|
|
771
|
-
return client.startSession({
|
|
772
|
-
name: params.name,
|
|
773
|
-
command: params.command,
|
|
774
|
-
cwd: params.cwd,
|
|
775
|
-
signal,
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
### Execute Delegation
|
|
394
|
+
Inside the `defineTool` execute callback, inference is enough. If action functions need typed inputs, use the alias from `types.ts`.
|
|
781
395
|
|
|
782
396
|
```typescript
|
|
783
|
-
|
|
784
|
-
async execute(
|
|
785
|
-
toolCallId: string,
|
|
786
|
-
params: MyToolParams,
|
|
787
|
-
signal: AbortSignal | undefined,
|
|
788
|
-
onUpdate: AgentToolUpdateCallback<MyToolDetails> | undefined,
|
|
789
|
-
ctx: ExtensionContext,
|
|
790
|
-
): Promise<AgentToolResult<MyToolDetails>> {
|
|
397
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
|
|
791
398
|
switch (params.action) {
|
|
792
399
|
case "start":
|
|
793
400
|
return startAction(client, params, signal, onUpdate, ctx);
|
|
794
401
|
case "list":
|
|
795
402
|
return listAction(client, params, signal);
|
|
796
|
-
case "kill":
|
|
797
|
-
return killAction(client, params, signal);
|
|
798
403
|
default:
|
|
799
404
|
throw new Error(`Unknown action: ${params.action}`);
|
|
800
405
|
}
|
|
801
406
|
}
|
|
802
407
|
```
|
|
803
408
|
|
|
804
|
-
|
|
409
|
+
## Naming
|
|
805
410
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
export function renderCall(args: MyToolParams, theme: Theme) {
|
|
811
|
-
return new ToolCallHeader(
|
|
812
|
-
{
|
|
813
|
-
toolName: "MyTool",
|
|
814
|
-
action: args.action,
|
|
815
|
-
mainArg: getMainArg(args),
|
|
816
|
-
optionArgs: getOptionArgs(args),
|
|
817
|
-
longArgs: getLongArgs(args),
|
|
818
|
-
},
|
|
819
|
-
theme,
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
export function renderResult(
|
|
824
|
-
result: AgentToolResult<MyToolDetails>,
|
|
825
|
-
options: ToolRenderResultOptions,
|
|
826
|
-
theme: Theme,
|
|
827
|
-
) {
|
|
828
|
-
// Complex rendering logic here
|
|
829
|
-
}
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
```typescript
|
|
833
|
-
// index.ts
|
|
834
|
-
import { renderCall, renderResult } from "./render.js";
|
|
835
|
-
|
|
836
|
-
const myTool = {
|
|
837
|
-
// ...
|
|
838
|
-
renderCall,
|
|
839
|
-
renderResult,
|
|
840
|
-
};
|
|
841
|
-
```
|
|
411
|
+
- Use snake_case tool names.
|
|
412
|
+
- Prefix third-party integrations with the API/product name (`linkup_web_search`, `linear_issue`).
|
|
413
|
+
- Internal tools can use direct names (`get_current_time`, `process`).
|
|
414
|
+
- Display names in UI should use `label`, not raw tool IDs.
|
|
842
415
|
|
|
843
|
-
|
|
844
|
-
- `pi-processes`: Multi-action process management with complex rendering
|
|
845
|
-
- `pi-linear`: Multi-action Linear API integration
|
|
416
|
+
## Checklist
|
|
846
417
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
} from "@mariozechner/pi-coding-agent";
|
|
860
|
-
import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
|
|
861
|
-
import { Container, Text } from "@mariozechner/pi-tui";
|
|
862
|
-
import { type Static, Type } from "typebox";
|
|
863
|
-
|
|
864
|
-
// Schema
|
|
865
|
-
const parameters = Type.Object({
|
|
866
|
-
owner: Type.String({ description: "Repository owner" }),
|
|
867
|
-
repo: Type.String({ description: "Repository name" }),
|
|
868
|
-
path: Type.Optional(Type.String({ description: "File or directory path", default: "" })),
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
// Typed aliases
|
|
872
|
-
type RepoTreeParams = Static<typeof parameters>;
|
|
873
|
-
|
|
874
|
-
interface RepoTreeDetails {
|
|
875
|
-
owner: string;
|
|
876
|
-
repo: string;
|
|
877
|
-
path: string;
|
|
878
|
-
entries: Array<{ name: string; type: "file" | "dir"; size?: number }>;
|
|
879
|
-
truncated: boolean;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Tool definition
|
|
883
|
-
const repoTreeTool = {
|
|
884
|
-
name: "repo_tree",
|
|
885
|
-
label: "Repo Tree",
|
|
886
|
-
description: "List files and directories in a GitHub repository.",
|
|
887
|
-
promptSnippet: "Browse repository file structure",
|
|
888
|
-
promptGuidelines: [
|
|
889
|
-
"Use repo_tree to explore repository structure before reading files.",
|
|
890
|
-
"Start repo_tree at the root path, then drill down into directories.",
|
|
891
|
-
],
|
|
892
|
-
parameters,
|
|
893
|
-
|
|
894
|
-
async execute(
|
|
895
|
-
_toolCallId: string,
|
|
896
|
-
params: RepoTreeParams,
|
|
897
|
-
signal: AbortSignal | undefined,
|
|
898
|
-
_onUpdate: undefined,
|
|
899
|
-
_ctx: ExtensionContext,
|
|
900
|
-
): Promise<AgentToolResult<RepoTreeDetails>> {
|
|
901
|
-
const response = await fetch(
|
|
902
|
-
`https://api.github.com/repos/${params.owner}/${params.repo}/contents/${params.path ?? ""}`,
|
|
903
|
-
{ signal, headers: { Accept: "application/vnd.github.v3+json" } },
|
|
904
|
-
);
|
|
905
|
-
|
|
906
|
-
if (!response.ok) {
|
|
907
|
-
if (response.status === 404) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
`Could not find repository "${params.owner}/${params.repo}" or path "${params.path ?? ""}"`,
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const data = await response.json();
|
|
916
|
-
|
|
917
|
-
// GitHub returns a single object for files, array for directories
|
|
918
|
-
const entries = Array.isArray(data) ? data : [data];
|
|
919
|
-
|
|
920
|
-
const processed = entries.map((entry: any) => ({
|
|
921
|
-
name: entry.name,
|
|
922
|
-
type: entry.type === "dir" ? "dir" as const : "file" as const,
|
|
923
|
-
size: entry.size,
|
|
924
|
-
}));
|
|
925
|
-
|
|
926
|
-
// Sort: directories first, then files, alphabetically within each
|
|
927
|
-
processed.sort((a, b) => {
|
|
928
|
-
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
929
|
-
return a.name.localeCompare(b.name);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
const truncated = processed.length > 100;
|
|
933
|
-
|
|
934
|
-
return {
|
|
935
|
-
content: [
|
|
936
|
-
{
|
|
937
|
-
type: "text",
|
|
938
|
-
text: JSON.stringify(truncated ? processed.slice(0, 100) : processed, null, 2),
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
details: {
|
|
942
|
-
owner: params.owner,
|
|
943
|
-
repo: params.repo,
|
|
944
|
-
path: params.path ?? "",
|
|
945
|
-
entries: processed,
|
|
946
|
-
truncated,
|
|
947
|
-
},
|
|
948
|
-
};
|
|
949
|
-
},
|
|
950
|
-
|
|
951
|
-
renderCall(args: RepoTreeParams, theme: Theme) {
|
|
952
|
-
return new ToolCallHeader(
|
|
953
|
-
{
|
|
954
|
-
toolName: "Repo Tree",
|
|
955
|
-
mainArg: `${args.owner}/${args.repo}`,
|
|
956
|
-
optionArgs: args.path ? [`path=${args.path}`] : [],
|
|
957
|
-
longArgs: [],
|
|
958
|
-
},
|
|
959
|
-
theme,
|
|
960
|
-
);
|
|
961
|
-
},
|
|
962
|
-
|
|
963
|
-
renderResult(
|
|
964
|
-
result: AgentToolResult<RepoTreeDetails>,
|
|
965
|
-
options: ToolRenderResultOptions,
|
|
966
|
-
theme: Theme,
|
|
967
|
-
) {
|
|
968
|
-
// 1. Stable partial message
|
|
969
|
-
if (options.isPartial) {
|
|
970
|
-
return new Text(theme.fg("muted", "RepoTree: fetching..."), 0, 0);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const { details } = result;
|
|
974
|
-
|
|
975
|
-
// 2. Error handling: details is {} when tool threw
|
|
976
|
-
if (!details?.entries) {
|
|
977
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
978
|
-
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Failed to list repository";
|
|
979
|
-
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
980
|
-
}
|
|
981
|
-
const container = new Container();
|
|
982
|
-
const entries = details?.entries ?? [];
|
|
983
|
-
const dirs = entries.filter((e) => e.type === "dir");
|
|
984
|
-
const files = entries.filter((e) => e.type === "file");
|
|
985
|
-
|
|
986
|
-
// 3. Body fields with showCollapsed for collapsed/expanded control
|
|
987
|
-
const fields: Array<{ label: string; value: string; showCollapsed: boolean }> = [
|
|
988
|
-
{
|
|
989
|
-
label: "Location",
|
|
990
|
-
value: `${details.owner}/${details.repo}${details.path ? `/${details.path}` : ""}`,
|
|
991
|
-
showCollapsed: true,
|
|
992
|
-
},
|
|
993
|
-
{
|
|
994
|
-
label: "Contents",
|
|
995
|
-
value: `${dirs.length} dirs, ${files.length} files`,
|
|
996
|
-
showCollapsed: true,
|
|
997
|
-
},
|
|
998
|
-
];
|
|
999
|
-
|
|
1000
|
-
container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
|
|
1001
|
-
|
|
1002
|
-
// 4. Entry list (in expanded view)
|
|
1003
|
-
if (options.expanded && entries.length > 0) {
|
|
1004
|
-
const entryLines = entries
|
|
1005
|
-
.slice(0, 50)
|
|
1006
|
-
.map((e) => {
|
|
1007
|
-
const icon = e.type === "dir" ? "📁" : "📄";
|
|
1008
|
-
const size = e.size !== undefined ? ` (${formatSize(e.size)})` : "";
|
|
1009
|
-
return `${icon} ${e.name}${size}`;
|
|
1010
|
-
})
|
|
1011
|
-
.join("\n");
|
|
1012
|
-
|
|
1013
|
-
container.addChild(new Text(entryLines, 0, 0));
|
|
1014
|
-
|
|
1015
|
-
if (entries.length > 50) {
|
|
1016
|
-
container.addChild(
|
|
1017
|
-
new Text(theme.fg("muted", `... and ${entries.length - 50} more`), 0, 0),
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// 5. Conditional footer
|
|
1023
|
-
const footerItems = [];
|
|
1024
|
-
|
|
1025
|
-
if (details?.truncated) {
|
|
1026
|
-
footerItems.push({ label: "showing", value: "first 100 entries" });
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (entries.length > 0) {
|
|
1030
|
-
footerItems.push({ label: "total", value: `${entries.length} items` });
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (!options.expanded && entries.length > 5) {
|
|
1034
|
-
footerItems.push({ label: "", value: keyHint("app.tools.expand", "to expand") });
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (footerItems.length > 0) {
|
|
1038
|
-
container.addChild(
|
|
1039
|
-
new ToolFooter(theme, {
|
|
1040
|
-
items: footerItems,
|
|
1041
|
-
separator: " | ",
|
|
1042
|
-
includeSpacerBeforeFooter: fields.length > 0,
|
|
1043
|
-
}),
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
return container;
|
|
1048
|
-
},
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
export default function (pi: ExtensionAPI) {
|
|
1052
|
-
pi.registerTool(repoTreeTool);
|
|
1053
|
-
}
|
|
1054
|
-
```
|
|
418
|
+
- [ ] Tool uses `defineTool({...})` with no explicit generic args.
|
|
419
|
+
- [ ] Params infer from `parameters`; helper APIs use `Static<typeof parameters>` alias.
|
|
420
|
+
- [ ] `label`, `description`, and useful parameter descriptions are present.
|
|
421
|
+
- [ ] `promptGuidelines` bullets name the exact tool.
|
|
422
|
+
- [ ] Execute signature is `(toolCallId, params, signal, onUpdate, ctx)`.
|
|
423
|
+
- [ ] `onUpdate` uses optional chaining.
|
|
424
|
+
- [ ] `signal` is forwarded to async operations.
|
|
425
|
+
- [ ] Errors are thrown, not returned as fake success payloads.
|
|
426
|
+
- [ ] Path tools strip leading `@`; mutating tools use `withFileMutationQueue()`.
|
|
427
|
+
- [ ] Shared-state tools use safe concurrency or `executionMode: "sequential"`.
|
|
428
|
+
- [ ] Large outputs are truncated with a full-output temp file.
|
|
429
|
+
- [ ] Renderers return components, handle partial first, handle errors, and omit empty footers.
|