From 0e5b6e54f073003fc114cb754080482d7a3af07f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 22 Apr 2026 14:27:03 +0200 Subject: [PATCH] docs(coding-agent): add terminating structured output example closes #3525 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/extensions.md | 6 ++ .../examples/extensions/README.md | 1 + .../examples/extensions/structured-output.ts | 65 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 packages/coding-agent/examples/extensions/structured-output.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 26e7e86e..b821b465 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index b5cd5d9f..86a0b385 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -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** ||| diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 664716e7..5654162e 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -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) | diff --git a/packages/coding-agent/examples/extensions/structured-output.ts b/packages/coding-agent/examples/extensions/structured-output.ts new file mode 100644 index 00000000..9fb6a426 --- /dev/null +++ b/packages/coding-agent/examples/extensions/structured-output.ts @@ -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); +}