diff --git a/src/agent/cli/output.test.ts b/src/agent/cli/output.test.ts index 3a6f6030..f292dd0e 100644 --- a/src/agent/cli/output.test.ts +++ b/src/agent/cli/output.test.ts @@ -112,6 +112,14 @@ describe("output", () => { expect(extractResultDetails(result)).toEqual(result); }); + it("should prefer details when present", () => { + const result = { + content: [{ type: "text", text: "not json" }], + details: { count: 3, truncated: false }, + }; + expect(extractResultDetails(result)).toEqual({ count: 3, truncated: false }); + }); + it("should return direct object if no content array", () => { const result = { count: 10, truncated: true }; expect(extractResultDetails(result)).toEqual({ count: 10, truncated: true }); diff --git a/src/agent/cli/output.ts b/src/agent/cli/output.ts index 3c6c9835..f7b2437d 100644 --- a/src/agent/cli/output.ts +++ b/src/agent/cli/output.ts @@ -118,6 +118,11 @@ export function extractResultDetails(result: unknown): Record | } } + const withDetails = result as { details?: unknown }; + if (withDetails.details && typeof withDetails.details === "object") { + return withDetails.details as Record; + } + // Try direct object access return result as Record; } @@ -252,8 +257,18 @@ export function createAgentOutput(params: { } case "tool_execution_end": { // Stop spinner and show final result with summary - if (event.isError) { - const errorText = extractText(event.result) || "Tool failed"; + const details = extractResultDetails(event.result); + const errorField = details?.error; + const hasError = + event.isError || + Boolean(errorField) || + details?.success === false; + if (hasError) { + const errorText = + (typeof details?.message === "string" && details.message) || + (typeof errorField === "string" && errorField) || + extractText(event.result) || + "Tool failed"; const bullet = colors.toolError("✗"); const title = colors.toolName(toolDisplayName(event.toolName)); spinner.stop(`${bullet} ${title}: ${colors.toolError(errorText)}`); diff --git a/src/agent/tools.ts b/src/agent/tools.ts index 6578a238..967a2d54 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -1,13 +1,14 @@ import type { AgentOptions } from "./types.js"; import { getModel } from "@mariozechner/pi-ai"; import { createCodingTools } from "@mariozechner/pi-coding-agent"; -import type { AgentTool } from "@mariozechner/pi-agent-core"; +import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core"; import { createExecTool } from "./tools/exec.js"; import { createProcessTool } from "./tools/process.js"; import { createGlobTool } from "./tools/glob.js"; import { createWebFetchTool, createWebSearchTool } from "./tools/web/index.js"; import { createMemoryTools } from "./tools/memory/index.js"; import { filterTools } from "./tools/policy.js"; +import { isMulticaError, isRetryableError } from "../shared/errors.js"; export function resolveModel(options: AgentOptions) { if (options.provider && options.model) { @@ -29,6 +30,66 @@ export interface CreateToolsOptions { profileBaseDir?: string; } +type ToolErrorPayload = { + error: true; + message: string; + name?: string; + code?: string; + retryable?: boolean; + details?: Record; +}; + +function toToolErrorPayload(error: unknown): ToolErrorPayload { + if (isMulticaError(error)) { + return { + error: true, + message: error.message, + name: error.name, + code: error.code, + retryable: error.retryable, + details: error.details, + }; + } + + if (error instanceof Error) { + return { + error: true, + message: error.message, + name: error.name, + retryable: isRetryableError(error), + }; + } + + return { + error: true, + message: String(error), + }; +} + +function toolErrorResult(error: unknown): AgentToolResult { + const payload = toToolErrorPayload(error); + return { + content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], + details: payload, + }; +} + +function wrapTool( + tool: AgentTool, +): AgentTool { + const execute = tool.execute; + return { + ...tool, + execute: async (...args) => { + try { + return await execute(...args); + } catch (error) { + return toolErrorResult(error) as AgentToolResult; + } + }, + }; +} + /** * Create all available tools. * This returns the full set before policy filtering. @@ -95,7 +156,7 @@ export function resolveTools(options: AgentOptions): AgentTool[] { isSubagent: options.isSubagent, }); - return filtered; + return filtered.map((tool) => wrapTool(tool)); } /**