diff --git a/packages/coding-agent/src/core/custom-tools/index.ts b/packages/coding-agent/src/core/custom-tools/index.ts index 2bfe0ead..d78b6858 100644 --- a/packages/coding-agent/src/core/custom-tools/index.ts +++ b/packages/coding-agent/src/core/custom-tools/index.ts @@ -4,6 +4,7 @@ export { discoverAndLoadCustomTools, loadCustomTools } from "./loader.js"; export type { + AgentToolUpdateCallback, CustomAgentTool, CustomToolFactory, CustomToolsLoadResult, diff --git a/packages/coding-agent/src/core/custom-tools/types.ts b/packages/coding-agent/src/core/custom-tools/types.ts index dc90c65a..bc4d1d74 100644 --- a/packages/coding-agent/src/core/custom-tools/types.ts +++ b/packages/coding-agent/src/core/custom-tools/types.ts @@ -5,7 +5,7 @@ * They can provide custom rendering for tool calls and results in the TUI. */ -import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai"; +import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-ai"; import type { Component } from "@mariozechner/pi-tui"; import type { Static, TSchema } from "@sinclair/typebox"; import type { Theme } from "../../modes/interactive/theme/theme.js"; @@ -15,6 +15,9 @@ import type { SessionEntry } from "../session-manager.js"; /** Alias for clarity */ export type ToolUIContext = HookUIContext; +/** Re-export for custom tools to use in execute signature */ +export type { AgentToolUpdateCallback }; + export interface ExecResult { stdout: string; stderr: string; @@ -62,7 +65,35 @@ export interface RenderResultOptions { isPartial: boolean; } -/** Custom tool with optional lifecycle and rendering methods */ +/** + * Custom tool with optional lifecycle and rendering methods. + * + * The execute signature inherited from AgentTool includes an optional onUpdate callback + * for streaming progress updates during long-running operations: + * - The callback emits partial results to subscribers (e.g. TUI/RPC), not to the LLM. + * - Partial updates should use the same TDetails type as the final result (use a union if needed). + * + * @example + * ```typescript + * type Details = + * | { status: "running"; step: number; total: number } + * | { status: "done"; count: number }; + * + * async execute(toolCallId, params, signal, onUpdate) { + * const items = params.items || []; + * for (let i = 0; i < items.length; i++) { + * onUpdate?.({ + * content: [{ type: "text", text: `Step ${i + 1}/${items.length}...` }], + * details: { status: "running", step: i + 1, total: items.length }, + * }); + * await processItem(items[i], signal); + * } + * return { content: [{ type: "text", text: "Done" }], details: { status: "done", count: items.length } }; + * } + * ``` + * + * Progress updates are rendered via renderResult with isPartial: true. + */ export interface CustomAgentTool extends AgentTool { /** Called on session start/switch/branch/clear - use to reconstruct state from entries */ diff --git a/packages/coding-agent/src/core/hooks/tool-wrapper.ts b/packages/coding-agent/src/core/hooks/tool-wrapper.ts index a36fe8d6..b9e518d2 100644 --- a/packages/coding-agent/src/core/hooks/tool-wrapper.ts +++ b/packages/coding-agent/src/core/hooks/tool-wrapper.ts @@ -2,7 +2,7 @@ * Tool wrapper - wraps tools with hook callbacks for interception. */ -import type { AgentTool } from "@mariozechner/pi-ai"; +import type { AgentTool, AgentToolUpdateCallback } from "@mariozechner/pi-ai"; import type { HookRunner } from "./runner.js"; import type { ToolCallEventResult, ToolResultEventResult } from "./types.js"; @@ -10,11 +10,17 @@ import type { ToolCallEventResult, ToolResultEventResult } from "./types.js"; * Wrap a tool with hook callbacks. * - Emits tool_call event before execution (can block) * - Emits tool_result event after execution (can modify result) + * - Forwards onUpdate callback to wrapped tool for progress streaming */ export function wrapToolWithHooks(tool: AgentTool, hookRunner: HookRunner): AgentTool { return { ...tool, - execute: async (toolCallId: string, params: Record, signal?: AbortSignal) => { + execute: async ( + toolCallId: string, + params: Record, + signal?: AbortSignal, + onUpdate?: AgentToolUpdateCallback, + ) => { // Emit tool_call event - hooks can block execution // If hook errors/times out, block by default (fail-safe) if (hookRunner.hasHandlers("tool_call")) { @@ -39,8 +45,8 @@ export function wrapToolWithHooks(tool: AgentTool, hookRunner: HookRu } } - // Execute the actual tool - const result = await tool.execute(toolCallId, params, signal); + // Execute the actual tool, forwarding onUpdate for progress streaming + const result = await tool.execute(toolCallId, params, signal, onUpdate); // Emit tool_result event - hooks can modify the result if (hookRunner.hasHandlers("tool_result")) { diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index a9bf29a6..aa5a4bbb 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -24,6 +24,7 @@ export { } from "./core/compaction.js"; // Custom tools export type { + AgentToolUpdateCallback, CustomAgentTool, CustomToolFactory, CustomToolsLoadResult,