docs(coding-agent): add terminating structured output example closes #3525

This commit is contained in:
Mario Zechner
2026-04-22 14:27:03 +02:00
parent b4f9b618d3
commit 0e5b6e54f0
4 changed files with 73 additions and 0 deletions

View File

@@ -4,6 +4,7 @@
### Added
- Added `structured-output.ts` extension example plus extension docs for terminating tool results, showing how a custom tool can return `terminate: true` so the agent ends on the tool call without an extra follow-up LLM turn ([#3525](https://github.com/badlogic/pi-mono/issues/3525))
- Added OSC 9;4 terminal progress indicators during agent streaming and compaction, so terminals like iTerm2, WezTerm, Windows Terminal, and Kitty show activity in their tab bar
### Breaking Changes

View File

@@ -1691,6 +1691,9 @@ pi.registerTool({
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
@@ -1702,6 +1705,8 @@ pi.registerTool({
**Signaling errors:** To mark a tool execution as failed (sets `isError: true` on the result and reports it to the LLM), throw an error from `execute`. Returning a value never sets the error flag regardless of what properties you include in the return object.
**Early termination:** Return `terminate: true` from `execute()` to hint that the automatic follow-up LLM call should be skipped after the current tool batch. This only takes effect when every finalized tool result in that batch is terminating. See [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts) for a minimal example where the agent ends on a final structured-output tool call.
```typescript
// Correct: throw to signal an error
async execute(toolCallId, params) {
@@ -2383,6 +2388,7 @@ All examples in [examples/extensions/](../examples/extensions/).
| `questionnaire.ts` | Multi-step wizard tool | `registerTool`, `ui.custom` |
| `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events |
| `dynamic-tools.ts` | Register tools after startup and during commands | `registerTool`, `session_start`, `registerCommand` |
| `structured-output.ts` | Final structured-output tool with `terminate: true` | `registerTool`, terminating tool results |
| `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` |
| `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) |
| **Commands** |||

View File

@@ -34,6 +34,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
| `dynamic-tools.ts` | Register tools after startup (`session_start`) and at runtime via command, with prompt snippets and tool-specific prompt guidelines |
| `structured-output.ts` | Final structured-output tool that returns `terminate: true` so the agent can end on the tool call |
| `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior |
| `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) |
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |

View File

@@ -0,0 +1,65 @@
/**
* Structured Output Tool
*
* Demonstrates `terminate: true` so the agent can end on a tool call
* without paying for an extra follow-up LLM turn.
*/
import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
interface StructuredOutputDetails {
headline: string;
summary: string;
actionItems: string[];
}
const structuredOutputTool = defineTool({
name: "structured_output",
label: "Structured Output",
description:
"Return a final structured answer. Use this as your last action when the user asks for structured output or a machine-readable summary.",
promptSnippet: "Emit a final structured answer as a terminating tool result",
promptGuidelines: [
"Use structured_output as your final action when the user asks for structured output, JSON-like output, or a machine-readable summary.",
"After calling structured_output, do not emit another assistant response in the same turn.",
],
parameters: Type.Object({
headline: Type.String({ description: "Short title for the result" }),
summary: Type.String({ description: "One-paragraph summary" }),
actionItems: Type.Array(Type.String(), { description: "Concrete next steps or key bullets" }),
}),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Saved structured output: ${params.headline}` }],
details: {
headline: params.headline,
summary: params.summary,
actionItems: params.actionItems,
} satisfies StructuredOutputDetails,
terminate: true,
};
},
renderResult(result, _options, theme) {
const details = result.details as StructuredOutputDetails | undefined;
if (!details) {
const text = result.content[0];
return new Text(text?.type === "text" ? text.text : "", 0, 0);
}
const lines = [
theme.fg("toolTitle", theme.bold(details.headline)),
theme.fg("text", details.summary),
"",
...details.actionItems.map((item, index) => theme.fg("muted", `${index + 1}. ${item}`)),
];
return new Text(lines.join("\n"), 0, 0);
},
});
export default function (pi: ExtensionAPI) {
pi.registerTool(structuredOutputTool);
}