@aliou/pi-dev-kit 0.4.9 → 0.5.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 +60 -0
- package/package.json +59 -2
- package/src/commands/index.ts +6 -0
- package/src/commands/update.ts +144 -0
- package/src/index.ts +8 -0
- package/src/prompts/setup-demo.md +35 -0
- package/src/skills/demo-setup/SKILL.md +217 -0
- package/src/skills/pi-extension/SKILL.md +140 -0
- package/src/skills/pi-extension/references/additional-apis.md +264 -0
- package/src/skills/pi-extension/references/commands.md +100 -0
- package/src/skills/pi-extension/references/components.md +166 -0
- package/src/skills/pi-extension/references/documentation.md +54 -0
- package/src/skills/pi-extension/references/hooks.md +244 -0
- package/src/skills/pi-extension/references/messages.md +169 -0
- package/src/skills/pi-extension/references/modes.md +156 -0
- package/src/skills/pi-extension/references/providers.md +134 -0
- package/src/skills/pi-extension/references/publish.md +139 -0
- package/src/skills/pi-extension/references/state.md +56 -0
- package/src/skills/pi-extension/references/structure.md +408 -0
- package/src/skills/pi-extension/references/testing.md +54 -0
- package/src/skills/pi-extension/references/tools.md +430 -0
- package/src/tools/changelog-tool.ts +596 -0
- package/src/tools/docs-tool.ts +240 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/package-manager-tool.ts +223 -0
- package/src/tools/utils.ts +62 -0
- package/src/tools/version-tool.ts +77 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Tools are functions the LLM can call. They are the primary way extensions add capabilities to pi.
|
|
4
|
+
|
|
5
|
+
## Registration
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Type, type ExtensionAPI, type ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
const myTool: ToolDefinition = {
|
|
11
|
+
name: "my_tool",
|
|
12
|
+
description: "What this tool does. The LLM reads this to decide when to call it.",
|
|
13
|
+
parameters: Type.Object({
|
|
14
|
+
query: Type.String({ description: "Search query" }),
|
|
15
|
+
limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
|
|
16
|
+
}),
|
|
17
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
18
|
+
const results = await doSomething(params.query, params.limit);
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
21
|
+
details: { results },
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function (pi: ExtensionAPI) {
|
|
27
|
+
pi.registerTool(myTool);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Execute Signature
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
execute(
|
|
35
|
+
toolCallId: string,
|
|
36
|
+
params: Static<TParams>, // Typed from the parameters schema
|
|
37
|
+
signal: AbortSignal | undefined,
|
|
38
|
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
39
|
+
ctx: ExtensionContext,
|
|
40
|
+
): Promise<AgentToolResult<TDetails>>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Parameter order matters.** The signal comes before onUpdate.
|
|
44
|
+
|
|
45
|
+
Always use optional chaining when calling `onUpdate`:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
onUpdate?.({ output: "partial result", details: { progress: 50 } });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The `onUpdate` parameter can be `undefined`. Calling it without optional chaining will throw.
|
|
52
|
+
|
|
53
|
+
## Tool Overrides and Delegation
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
## Return Value
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
return {
|
|
61
|
+
content: (TextContent | ImageContent)[], // Content blocks sent to the LLM
|
|
62
|
+
details?: TDetails, // Arbitrary data available in the renderer
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `content` is what the LLM sees. Each block is `{ type: "text", text: "..." }` or an image. Keep it structured and concise.
|
|
67
|
+
- `details` is what the renderer sees. Put rich data here for custom display.
|
|
68
|
+
|
|
69
|
+
Common pattern:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
74
|
+
details: { results },
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Error Handling
|
|
79
|
+
|
|
80
|
+
To report a tool call failure, **throw an error**. The framework catches it, sets `isError: true` on the tool result, and sends the error message to the LLM.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
84
|
+
const result = await fetchData(params.query);
|
|
85
|
+
if (!result) {
|
|
86
|
+
throw new Error("No results found. Try a different query.");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
90
|
+
details: { result },
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Do not try to return `isError` in the result object. The `AgentToolResult` type does not have an `isError` field. Only throwing sets `isError: true` on the tool result event sent to the LLM.
|
|
96
|
+
|
|
97
|
+
### Error rendering in `renderResult`
|
|
98
|
+
|
|
99
|
+
When a tool throws, the framework still calls `renderResult`. It passes:
|
|
100
|
+
- `content`: an array with the error message as a text block
|
|
101
|
+
- `details`: an empty object `{}` (not `undefined`)
|
|
102
|
+
|
|
103
|
+
Your `renderResult` must detect this and display the error. Check for missing expected fields in `details` -- do not check `!details` since the framework always provides an object.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Full example: a tool that can fail, with proper error rendering.
|
|
107
|
+
import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
108
|
+
import type {
|
|
109
|
+
AgentToolResult,
|
|
110
|
+
ExtensionAPI,
|
|
111
|
+
ExtensionContext,
|
|
112
|
+
Theme,
|
|
113
|
+
ToolRenderResultOptions,
|
|
114
|
+
} from "@mariozechner/pi-coding-agent";
|
|
115
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
116
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
117
|
+
|
|
118
|
+
interface DivideDetails {
|
|
119
|
+
result?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parameters = Type.Object({
|
|
123
|
+
dividend: Type.Number({ description: "The number to divide" }),
|
|
124
|
+
divisor: Type.Number({ description: "The number to divide by" }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
type DivideParams = Static<typeof parameters>;
|
|
128
|
+
|
|
129
|
+
const divideTool = {
|
|
130
|
+
name: "divide",
|
|
131
|
+
label: "Divide",
|
|
132
|
+
description: "Divide two numbers.",
|
|
133
|
+
parameters,
|
|
134
|
+
|
|
135
|
+
async execute(
|
|
136
|
+
_toolCallId: string,
|
|
137
|
+
params: DivideParams,
|
|
138
|
+
_signal: AbortSignal | undefined,
|
|
139
|
+
_onUpdate: undefined,
|
|
140
|
+
_ctx: ExtensionContext,
|
|
141
|
+
): Promise<AgentToolResult<DivideDetails>> {
|
|
142
|
+
if (params.divisor === 0) {
|
|
143
|
+
throw new Error("Division by zero");
|
|
144
|
+
}
|
|
145
|
+
const result = params.dividend / params.divisor;
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: `${result}` }],
|
|
148
|
+
details: { result },
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
renderCall(args: DivideParams, theme: Theme) {
|
|
153
|
+
return new ToolCallHeader(
|
|
154
|
+
{ toolName: "Divide", mainArg: `${args.dividend} / ${args.divisor}` },
|
|
155
|
+
theme,
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
renderResult(
|
|
160
|
+
result: AgentToolResult<DivideDetails>,
|
|
161
|
+
options: ToolRenderResultOptions,
|
|
162
|
+
theme: Theme,
|
|
163
|
+
) {
|
|
164
|
+
if (options.isPartial) {
|
|
165
|
+
return new Text(theme.fg("muted", "Dividing..."), 0, 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const details = result.details;
|
|
169
|
+
const container = new Container();
|
|
170
|
+
|
|
171
|
+
// Detect error: details is {} when the tool threw.
|
|
172
|
+
// Check for missing expected fields, not !details.
|
|
173
|
+
if (details?.result === undefined) {
|
|
174
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
175
|
+
const errorMsg =
|
|
176
|
+
(textBlock?.type === "text" && textBlock.text) || "Division failed";
|
|
177
|
+
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
178
|
+
return container;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
container.addChild(
|
|
182
|
+
new Text(theme.fg("success", `Result: ${details.result}`), 0, 0),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
container.addChild(new Text("", 0, 0));
|
|
186
|
+
container.addChild(
|
|
187
|
+
new ToolFooter(theme, {
|
|
188
|
+
items: [{ label: "result", value: `${details.result}` }],
|
|
189
|
+
separator: " | ",
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return container;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default function (pi: ExtensionAPI) {
|
|
198
|
+
pi.registerTool(divideTool);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Parameters Schema
|
|
203
|
+
|
|
204
|
+
Use TypeBox (`Type.*`) for parameter schemas. The LLM sees the schema to know what arguments to provide.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { Type } from "@mariozechner/pi-coding-agent";
|
|
208
|
+
|
|
209
|
+
// Required string
|
|
210
|
+
Type.String({ description: "File path to read" })
|
|
211
|
+
|
|
212
|
+
// Optional with default
|
|
213
|
+
Type.Optional(Type.Number({ description: "Max results", default: 10 }))
|
|
214
|
+
|
|
215
|
+
// Enum (string union)
|
|
216
|
+
Type.StringEnum(["created", "updated", "relevance"], { description: "Sort order" })
|
|
217
|
+
|
|
218
|
+
// Boolean
|
|
219
|
+
Type.Boolean({ description: "Include hidden files" })
|
|
220
|
+
|
|
221
|
+
// Nested object
|
|
222
|
+
Type.Object({
|
|
223
|
+
name: Type.String(),
|
|
224
|
+
value: Type.String(),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Array
|
|
228
|
+
Type.Array(Type.String(), { description: "List of tags" })
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Always provide `description` on parameters. The LLM uses these to understand what to pass.
|
|
232
|
+
|
|
233
|
+
## Streaming Updates
|
|
234
|
+
|
|
235
|
+
Use `onUpdate` to stream partial results while the tool executes. This gives the user feedback during long operations.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
239
|
+
for (const chunk of chunks) {
|
|
240
|
+
const partial = processChunk(chunk);
|
|
241
|
+
onUpdate?.({
|
|
242
|
+
content: [{ type: "text", text: partial }],
|
|
243
|
+
details: { progress: chunk.index / chunks.length },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: finalResult }],
|
|
248
|
+
details: { complete: true },
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Custom Rendering
|
|
254
|
+
|
|
255
|
+
Override how a tool's invocation and result appear in the TUI.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const myTool: ToolDefinition = {
|
|
259
|
+
name: "my_tool",
|
|
260
|
+
// ... parameters, execute ...
|
|
261
|
+
|
|
262
|
+
renderCall(params, theme) {
|
|
263
|
+
const header = [`My Tool: search`, JSON.stringify(params.query), `limit=${params.limit ?? 10}`].join(" ");
|
|
264
|
+
return theme.fg("toolTitle", header);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
268
|
+
if (isPartial) {
|
|
269
|
+
return theme.fg("muted", "My Tool: running...");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const items: string[] = result.details?.results ?? [];
|
|
273
|
+
const visible = expanded ? items : items.slice(0, 5);
|
|
274
|
+
|
|
275
|
+
const lines = [
|
|
276
|
+
theme.fg("success", `Found ${items.length} results`),
|
|
277
|
+
...visible.map((r) => ` ${r}`),
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
if (!expanded && items.length > visible.length) {
|
|
281
|
+
lines.push(theme.fg("dim", ` ...and ${items.length - visible.length} more`));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`renderCall` receives the params the LLM passed and returns a string shown when the tool is invoked.
|
|
290
|
+
|
|
291
|
+
`renderResult` receives the result (with `details`) and rendering options:
|
|
292
|
+
- `expanded`: Whether the entry is expanded in the TUI.
|
|
293
|
+
- `isPartial`: Whether this is a streaming update (from `onUpdate`) or the final result.
|
|
294
|
+
|
|
295
|
+
Both return a string or undefined (falls back to default rendering).
|
|
296
|
+
|
|
297
|
+
## Tool UI Rendering Guidelines
|
|
298
|
+
|
|
299
|
+
When customizing tool rendering, keep call/result UI predictable and scannable.
|
|
300
|
+
|
|
301
|
+
### `renderCall` format
|
|
302
|
+
|
|
303
|
+
Use this line model:
|
|
304
|
+
|
|
305
|
+
- First line: `[Tool Name]: [Action] [Main arg] [Option args]`
|
|
306
|
+
- Additional lines: long args only
|
|
307
|
+
|
|
308
|
+
Guidelines:
|
|
309
|
+
- Tool name should be a human display label, not a raw internal identifier.
|
|
310
|
+
- Show `action` only when it adds meaning (multi-action tools like process managers).
|
|
311
|
+
- Main arg should be the primary thing user cares about (query, session id, target id/name).
|
|
312
|
+
- Option args should be compact key-value pairs (`limit=10`, `cwd=/path`).
|
|
313
|
+
- Long text (prompt/task/question/context/instructions) goes to additional lines.
|
|
314
|
+
- Prefer wrapping to preserve full meaning over aggressive truncation.
|
|
315
|
+
- For tools without actions, omit colon suffix after tool name if that reads better in your UI system.
|
|
316
|
+
|
|
317
|
+
### `renderCall` design guide (process-style)
|
|
318
|
+
|
|
319
|
+
The `process` extension is a good baseline (`../pi-processes/src/tools/index.ts`). Its call renderer is deterministic and keeps headers readable.
|
|
320
|
+
|
|
321
|
+
Use this extraction order when building header parts:
|
|
322
|
+
|
|
323
|
+
1. **Action first**: always show action for multi-action tools (`start`, `list`, `kill`, ...).
|
|
324
|
+
2. **Pick one main arg**: choose the single value the user scans for first (name, id, or short command).
|
|
325
|
+
3. **Promote short fields to options**: compact values become option args (`end=true`, `limit=10`).
|
|
326
|
+
4. **Demote long fields to long args**: commands/prompts/instructions move to labeled follow-up lines.
|
|
327
|
+
5. **Keep it stable**: same inputs should produce same ordering and formatting.
|
|
328
|
+
|
|
329
|
+
Implementation pattern:
|
|
330
|
+
- Build `mainArg`, `optionArgs`, `longArgs` first, then pass to one renderer.
|
|
331
|
+
- If you use `@aliou/pi-utils-ui`, prefer `ToolCallHeader` to avoid hand-built string drift.
|
|
332
|
+
- Quote user-provided names (`"backend"`) when that improves visual parsing.
|
|
333
|
+
- Cap inline length (e.g. 60-80 chars), then spill to `longArgs`.
|
|
334
|
+
### `renderResult` layout
|
|
335
|
+
|
|
336
|
+
- Handle `isPartial` first. Return a short stable loading state.
|
|
337
|
+
- Keep the first non-loading line as a status summary (`Found N results`, `Updated 3 files`, `Failed: reason`).
|
|
338
|
+
- Use `expanded` to switch between compact and full output. Compact should show the top few items plus an omission hint.
|
|
339
|
+
- Keep body content focused on state + key output; avoid dumping raw JSON unless it is the actual output.
|
|
340
|
+
- If you render a footer (stats, backend, counts), keep one blank line above it.
|
|
341
|
+
- Keep footer concise and stable across states.
|
|
342
|
+
- Return `undefined` when custom rendering adds no value; fallback rendering is better than noisy UI.
|
|
343
|
+
|
|
344
|
+
## Tool Call + UI Consistency Contract
|
|
345
|
+
|
|
346
|
+
Use this contract to keep tool UX consistent across extensions:
|
|
347
|
+
|
|
348
|
+
1. **Call line is for scanability**: `renderCall` first line follows `[Tool Name]: [Action] [Main arg] [Option args]`.
|
|
349
|
+
2. **Result starts with state**: `renderResult` starts with a clear state line (running/success/error) before details.
|
|
350
|
+
3. **Long text moves down**: prompts, instructions, and context go to follow-up lines, not the call header.
|
|
351
|
+
4. **Partial updates stay compact**: `isPartial` output should be short and stable to prevent visual churn.
|
|
352
|
+
5. **Expanded controls density**: compact view shows summary + subset; expanded view shows full detail.
|
|
353
|
+
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`).
|
|
354
|
+
|
|
355
|
+
Related references:
|
|
356
|
+
- `references/modes.md` for `custom()` fallback behavior and RPC/Print handling.
|
|
357
|
+
- `references/components.md` for interactive component authoring.
|
|
358
|
+
- `references/messages.md` for persistent display via `sendMessage` + `registerMessageRenderer`.
|
|
359
|
+
|
|
360
|
+
## Naming Conventions
|
|
361
|
+
|
|
362
|
+
For extensions wrapping a third-party API, prefix tool names with the API name to avoid conflicts:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
linkup_web_search
|
|
366
|
+
linkup_web_fetch
|
|
367
|
+
synthetic_web_search
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For internal/custom tools, no prefix is needed:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
get_current_time
|
|
374
|
+
processes
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Use snake_case for all tool names.
|
|
378
|
+
|
|
379
|
+
## Abort Signal
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
385
|
+
const response = await fetch(url, { signal });
|
|
386
|
+
// If the user cancels, fetch throws an AbortError
|
|
387
|
+
return { content: [{ type: "text", text: await response.text() }], details: {} };
|
|
388
|
+
},
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Pass `signal` to every async operation that supports it: `fetch()` calls, `pi.exec()` calls, SDK clients, etc.
|
|
392
|
+
|
|
393
|
+
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()`:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// In the tool:
|
|
397
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
398
|
+
const result = await client.search({ query: params.query, signal });
|
|
399
|
+
// ...
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// In the client:
|
|
403
|
+
async search(params: { query: string; signal?: AbortSignal }) {
|
|
404
|
+
return this.request("/search", { method: "POST", body: ... }, params.signal);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async request<T>(endpoint: string, options: RequestInit = {}, signal?: AbortSignal) {
|
|
408
|
+
return fetch(`${BASE_URL}${endpoint}`, { ...options, signal, headers: { ... } });
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
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.
|
|
413
|
+
|
|
414
|
+
## Output Truncation
|
|
415
|
+
|
|
416
|
+
For tools that may return large outputs, use the `truncateHead` utility:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { truncateHead } from "@mariozechner/pi-coding-agent";
|
|
420
|
+
|
|
421
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
422
|
+
const fullOutput = await getLargeOutput();
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: truncateHead(fullOutput, 50000) }], // Keep last 50KB
|
|
425
|
+
details: {},
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`truncateHead` keeps the tail of the output (most recent content), which is usually most relevant.
|