diff --git a/src/agent/__tests__/tool-result-policy.test.ts b/src/agent/__tests__/tool-result-policy.test.ts new file mode 100644 index 0000000..cf5b3e4 --- /dev/null +++ b/src/agent/__tests__/tool-result-policy.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; + +import { getToolResultPolicy } from "../tool-result-policy"; + +describe("getToolResultPolicy", () => { + test("returns summary-first policy for search and filesystem inspection tools", () => { + expect(getToolResultPolicy("list_files")).toEqual({ + preferSummaryOnly: true, + includeData: false, + maxStringLength: 1000, + uiSummaryOnly: true, + }); + }); + + test("returns data-carrying policy for read_file", () => { + expect(getToolResultPolicy("read_file")).toMatchObject({ + preferSummaryOnly: false, + includeData: true, + maxStringLength: 12000, + }); + }); + + test("returns default policy for unknown tools", () => { + expect(getToolResultPolicy("unknown_tool")).toMatchObject({ + preferSummaryOnly: false, + includeData: true, + maxStringLength: 4000, + }); + }); +}); diff --git a/src/agent/__tests__/tool-result-runtime.test.ts b/src/agent/__tests__/tool-result-runtime.test.ts new file mode 100644 index 0000000..1f3ed3e --- /dev/null +++ b/src/agent/__tests__/tool-result-runtime.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, test } from "bun:test"; + +import { formatToolResultForMessage, inferToolErrorKind, normalizeToolResult } from "../tool-result-runtime"; + +describe("inferToolErrorKind", () => { + test("maps common tool error code families", () => { + expect(inferToolErrorKind("INVALID_PATH")).toBe("invalid_input"); + expect(inferToolErrorKind("DELETE_NOT_SUPPORTED")).toBe("unsupported"); + expect(inferToolErrorKind("FILE_NOT_FOUND")).toBe("not_found"); + expect(inferToolErrorKind("RG_NOT_FOUND")).toBe("environment_missing"); + expect(inferToolErrorKind("PATCH_APPLY_FAILED")).toBe("execution_failed"); + }); +}); + +describe("normalizeToolResult", () => { + test("preserves structured success results", () => { + const result = normalizeToolResult({ + ok: true, + summary: "Read file: /tmp/demo.ts", + data: { path: "/tmp/demo.ts", content: "const x = 1;" }, + }); + + expect(result).toMatchObject({ + ok: true, + summary: "Read file: /tmp/demo.ts", + data: { path: "/tmp/demo.ts", content: "const x = 1;" }, + }); + }); + + test("preserves structured errors and infers error kind", () => { + const result = normalizeToolResult({ + ok: false, + summary: "File not found", + error: "File not found", + code: "FILE_NOT_FOUND", + }); + + expect(result).toMatchObject({ + ok: false, + summary: "File not found", + error: "File not found", + code: "FILE_NOT_FOUND", + errorKind: "not_found", + }); + }); + + test("normalizes legacy string errors", () => { + const result = normalizeToolResult("Error: something failed"); + expect(result).toMatchObject({ + ok: false, + summary: "something failed", + error: "something failed", + }); + }); + + test("normalizes plain success strings", () => { + const result = normalizeToolResult("done"); + expect(result).toMatchObject({ + ok: true, + summary: "done", + data: "done", + }); + }); +}); + +describe("formatToolResultForMessage", () => { + test("omits data for summary-first tools", () => { + const formatted = formatToolResultForMessage({ + toolName: "list_files", + result: { + ok: true, + summary: "Listed 5 entries under /tmp/demo", + data: { entries: ["a", "b"] }, + }, + }); + + expect(JSON.parse(formatted)).toEqual({ + ok: true, + summary: "Listed 5 entries under /tmp/demo", + }); + }); + + test("preserves data for content-carrying tools", () => { + const formatted = formatToolResultForMessage({ + toolName: "read_file", + result: { + ok: true, + summary: "Read file: /tmp/demo.ts", + data: { path: "/tmp/demo.ts", content: "const x = 1;" }, + }, + }); + + expect(JSON.parse(formatted)).toEqual({ + ok: true, + summary: "Read file: /tmp/demo.ts", + data: { path: "/tmp/demo.ts", content: "const x = 1;" }, + }); + }); + + test("passes through raw read_file text results", () => { + const content = ["1: const x = 1;", "2: const y = 2;"].join("\n"); + const formatted = formatToolResultForMessage({ + toolName: "read_file", + result: content, + }); + + expect(formatted).toBe(content); + }); + + test("passes through read_file text that starts with Error: verbatim", () => { + const content = "Error: this line is part of the file"; + const formatted = formatToolResultForMessage({ + toolName: "read_file", + result: content, + }); + + expect(formatted).toBe(content); + }); + + test("formats errors with stable structured shape", () => { + const formatted = formatToolResultForMessage({ + toolName: "grep_search", + result: { + ok: false, + summary: "Failed to run rg", + error: "Failed to run rg", + code: "RG_NOT_FOUND", + }, + }); + + expect(JSON.parse(formatted)).toEqual({ + ok: false, + summary: "Failed to run rg", + error: "Failed to run rg", + code: "RG_NOT_FOUND", + }); + }); + + test("always returns valid json when payload exceeds limits", () => { + const formatted = formatToolResultForMessage({ + toolName: "apply_patch", + result: { + ok: true, + summary: "Applied patch", + data: { patch: "x".repeat(10000) }, + }, + }); + + expect(() => JSON.parse(formatted)).not.toThrow(); + expect(JSON.parse(formatted)).toEqual({ + ok: true, + summary: "Applied patch", + }); + }); +}); diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 8b03180..4535d82 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -12,6 +12,7 @@ import type { import type { AgentEvent } from "./agent-event"; import type { AgentMiddleware } from "./agent-middleware"; import type { SkillFrontmatter } from "./skills/types"; +import { formatToolResultForMessage } from "./tool-result-runtime"; /** * A context that is used to invoke a React agent. @@ -226,14 +227,14 @@ export class Agent { if (!tool) throw new Error(`Tool ${toolUse.name} not found`); const beforeResult = await this._beforeToolUse(toolUse); if (beforeResult.skip) { - return { index, toolUseId: toolUse.id, result: beforeResult.result }; + return { index, toolUseId: toolUse.id, toolName: toolUse.name, result: beforeResult.result }; } const result = await tool.invoke(toolUse.input, signal); await this._afterToolUse(toolUse, result); - return { index, toolUseId: toolUse.id, result }; + return { index, toolUseId: toolUse.id, toolName: toolUse.name, result }; } catch (error) { const message = error instanceof Error ? error.message : String(error); - return { index, toolUseId: toolUse.id, result: `Error: ${message}` }; + return { index, toolUseId: toolUse.id, toolName: toolUse.name, result: `Error: ${message}` }; } }); @@ -261,7 +262,7 @@ export class Agent { { type: "tool_result", tool_use_id: resolved.toolUseId, - content: stringifyToolResult(resolved.result), + content: formatToolResultForMessage({ toolName: resolved.toolName, result: resolved.result }), }, ], }; @@ -359,9 +360,3 @@ export class Agent { } } -function stringifyToolResult(result: unknown): string { - if (result === undefined) return "undefined"; - if (result === null) return "null"; - if (typeof result === "object") return JSON.stringify(result); - return String(result); -} diff --git a/src/agent/tool-result-policy.ts b/src/agent/tool-result-policy.ts new file mode 100644 index 0000000..e582722 --- /dev/null +++ b/src/agent/tool-result-policy.ts @@ -0,0 +1,45 @@ +export type ToolResultPolicy = { + preferSummaryOnly: boolean; + includeData: boolean; + maxStringLength?: number; + uiSummaryOnly?: boolean; +}; + +const DEFAULT_POLICY: ToolResultPolicy = { + preferSummaryOnly: false, + includeData: true, + maxStringLength: 4000, +}; + +export function getToolResultPolicy(toolName: string): ToolResultPolicy { + switch (toolName) { + case "list_files": + case "glob_search": + case "grep_search": + case "file_info": + case "mkdir": + case "move_path": + return { + preferSummaryOnly: true, + includeData: false, + maxStringLength: 1000, + uiSummaryOnly: true, + }; + case "read_file": + return { + preferSummaryOnly: false, + includeData: true, + maxStringLength: 12000, + }; + case "apply_patch": + case "write_file": + case "str_replace": + return { + preferSummaryOnly: false, + includeData: true, + maxStringLength: 4000, + }; + default: + return DEFAULT_POLICY; + } +} diff --git a/src/agent/tool-result-runtime.ts b/src/agent/tool-result-runtime.ts new file mode 100644 index 0000000..b87194a --- /dev/null +++ b/src/agent/tool-result-runtime.ts @@ -0,0 +1,187 @@ +import type { StructuredToolError, StructuredToolResult, StructuredToolSuccess } from "@/foundation"; + +import { getToolResultPolicy } from "./tool-result-policy"; + +export type ToolErrorKind = + | "invalid_input" + | "unsupported" + | "not_found" + | "environment_missing" + | "execution_failed" + | "unknown"; + +export type NormalizedToolSuccess = StructuredToolSuccess & { + raw: unknown; +}; + +export type NormalizedToolError = StructuredToolError & { + errorKind: ToolErrorKind; + raw: unknown; +}; + +export type NormalizedToolResult = NormalizedToolSuccess | NormalizedToolError; + +export function inferToolErrorKind(code?: string): ToolErrorKind { + if (!code) return "unknown"; + if (code.startsWith("INVALID_")) return "invalid_input"; + if (code.endsWith("_NOT_SUPPORTED")) return "unsupported"; + if (code === "RG_NOT_FOUND") return "environment_missing"; + if (code === "FILE_NOT_FOUND" || code.endsWith("_NOT_FOUND")) return "not_found"; + if (code.endsWith("_FAILED")) return "execution_failed"; + return "unknown"; +} + +function isStructuredToolSuccess(value: unknown): value is StructuredToolSuccess { + return ( + typeof value === "object" && + value !== null && + "ok" in value && + value.ok === true && + "summary" in value && + typeof value.summary === "string" + ); +} + +function isStructuredToolError(value: unknown): value is StructuredToolError { + return ( + typeof value === "object" && + value !== null && + "ok" in value && + value.ok === false && + "summary" in value && + typeof value.summary === "string" && + "error" in value && + typeof value.error === "string" + ); +} + +export function normalizeToolResult(result: unknown): NormalizedToolResult { + if (isStructuredToolSuccess(result)) { + return { + ok: true, + summary: result.summary, + ...(result.data !== undefined ? { data: result.data } : {}), + raw: result, + }; + } + + if (isStructuredToolError(result)) { + return { + ok: false, + summary: result.summary, + error: result.error, + ...(result.code ? { code: result.code } : {}), + ...(result.details ? { details: result.details } : {}), + errorKind: inferToolErrorKind(result.code), + raw: result, + }; + } + + if (typeof result === "string" && result.startsWith("Error:")) { + const error = result.slice("Error:".length).trim() || "Tool execution failed."; + return { + ok: false, + summary: error, + error, + errorKind: "unknown", + raw: result, + }; + } + + const summary = stringifyValue(result); + return { + ok: true, + summary, + ...(result !== undefined ? { data: result } : {}), + raw: result, + }; +} + +export function formatToolResultForMessage({ toolName, result }: { toolName: string; result: unknown }): string { + if (toolName === "read_file" && typeof result === "string") { + return result; + } + + const normalized = normalizeToolResult(result); + const policy = getToolResultPolicy(toolName); + + if (!normalized.ok) { + return stringifyWithinLimit( + { + ok: false, + summary: normalized.summary, + error: normalized.error, + ...(normalized.code ? { code: normalized.code } : {}), + ...(normalized.details ? { details: normalized.details } : {}), + }, + policy.maxStringLength, + { + ok: false, + summary: truncateSummary(normalized.summary), + error: truncateSummary(normalized.error), + ...(normalized.code ? { code: normalized.code } : {}), + }, + ); + } + + if (policy.preferSummaryOnly || !policy.includeData) { + return JSON.stringify({ ok: true, summary: truncateSummary(normalized.summary) } satisfies StructuredToolResult); + } + + return stringifyWithinLimit( + { + ok: true, + summary: normalized.summary, + ...(normalized.data !== undefined ? { data: normalized.data } : {}), + }, + policy.maxStringLength, + { + ok: true, + summary: truncateSummary(normalized.summary), + }, + ); +} + +function stringifyValue(value: unknown) { + if (value === undefined) return "undefined"; + if (value === null) return "null"; + if (typeof value === "string") return value; + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return "[unserializable object]"; + } + } + return String(value); +} + +function stringifyWithinLimit(payload: StructuredToolResult, maxLength: number | undefined, fallback: StructuredToolResult): string { + const serialized = JSON.stringify(payload); + if (!maxLength || serialized.length <= maxLength) { + return serialized; + } + + const fallbackSerialized = JSON.stringify(fallback); + if (!maxLength || fallbackSerialized.length <= maxLength) { + return fallbackSerialized; + } + + if (fallback.ok) { + return JSON.stringify({ ok: true, summary: fallback.summary.slice(0, Math.max(0, maxLength - 32)) } satisfies StructuredToolResult); + } + + return JSON.stringify({ + ok: false, + summary: fallback.summary.slice(0, Math.max(0, maxLength - 64)), + error: fallback.error.slice(0, Math.max(0, maxLength - 64)), + ...(fallback.code ? { code: fallback.code } : {}), + } satisfies StructuredToolResult); +} + +function truncateSummary(value: string, maxLength = 500): string { + if (value.length <= maxLength) { + return value; + } + return `${value.slice(0, maxLength)}... [truncated ${value.length - maxLength} chars]`; +} diff --git a/src/agent/tool-result-summary.ts b/src/agent/tool-result-summary.ts new file mode 100644 index 0000000..bf35cac --- /dev/null +++ b/src/agent/tool-result-summary.ts @@ -0,0 +1,28 @@ +export function summarizeToolResultText(content: string): string | null { + if (content.startsWith("Error:")) { + return content; + } + + try { + const parsed = JSON.parse(content) as { + ok?: boolean; + summary?: unknown; + error?: unknown; + code?: unknown; + }; + + if (parsed.ok === true && typeof parsed.summary === "string") { + return parsed.summary; + } + + if (parsed.ok === false) { + const message = typeof parsed.summary === "string" ? parsed.summary : typeof parsed.error === "string" ? parsed.error : content; + const code = typeof parsed.code === "string" ? parsed.code : null; + return code ? `Error [${code}]: ${message}` : `Error: ${message}`; + } + } catch { + return null; + } + + return null; +} diff --git a/src/cli/tui/app.tsx b/src/cli/tui/app.tsx index b6cff77..4b8ad6c 100644 --- a/src/cli/tui/app.tsx +++ b/src/cli/tui/app.tsx @@ -32,7 +32,7 @@ export function App({ const { streaming, messages, onSubmit, abort } = useAgentLoop(); const { approvalRequest, respondToApproval } = useApprovalManager(); const { askUserQuestionRequest, respondWithAnswers } = useAskUserQuestionManager(); - const { latestTodos, todoSnapshots, toolUses } = useMemo(() => buildTodoViewState(messages), [messages]); + const { latestTodos, todoSnapshots } = useMemo(() => buildTodoViewState(messages), [messages]); const nextTodo = getNextTodo(latestTodos)?.content; const hideTodos = !streaming && allDone(latestTodos); @@ -55,7 +55,6 @@ export function App({ message={lastMessage} messageIndex={messages.length - 1} todoSnapshots={todoSnapshots} - toolUses={toolUses} /> )} {approvalRequest || askUserQuestionRequest ? null : ( diff --git a/src/cli/tui/components/message-history.tsx b/src/cli/tui/components/message-history.tsx index 249ae19..24ffbd2 100644 --- a/src/cli/tui/components/message-history.tsx +++ b/src/cli/tui/components/message-history.tsx @@ -1,7 +1,7 @@ import { Box, Text } from "ink"; import { memo } from "react"; -import type { AssistantMessage, NonSystemMessage, ToolMessage, ToolUseContent, UserMessage } from "@/foundation"; +import type { AssistantMessage, NonSystemMessage, ToolUseContent, UserMessage } from "@/foundation"; import { currentTheme } from "../themes"; import { getCurrentTodo, getNextTodo, snapshotKey, type TodoItemView } from "../todo-view"; @@ -12,12 +12,10 @@ export const MessageHistory = memo(function MessageHistory({ messages, startIndex = 0, todoSnapshots, - toolUses, }: { messages: NonSystemMessage[]; startIndex?: number; todoSnapshots: Map; - toolUses: Map; }) { return ( @@ -28,7 +26,6 @@ export const MessageHistory = memo(function MessageHistory({ message={message} messageIndex={startIndex + index} todoSnapshots={todoSnapshots} - toolUses={toolUses} /> ); })} @@ -40,12 +37,10 @@ export const MessageHistoryItem = memo(function MessageHistoryItem({ message, messageIndex, todoSnapshots, - toolUses, }: { message: NonSystemMessage; messageIndex: number; todoSnapshots: Map; - toolUses: Map; }) { switch (message.role) { case "user": @@ -53,7 +48,7 @@ export const MessageHistoryItem = memo(function MessageHistoryItem({ case "assistant": return ; case "tool": - return ; + return null; default: return null; } @@ -217,33 +212,6 @@ const ToolUseContentItem = memo(function ToolUseContentItem({ } }); -const ToolMessageItem = memo(function ToolMessageItem({ - message, - toolUses, -}: { - message: ToolMessage; - toolUses: Map; -}) { - const visibleContent = message.content.flatMap((content) => { - const toolUse = toolUses.get(content.tool_use_id); - const rendered = summarizeToolResult(content.content, toolUse); - return rendered ? [{ ...content, content: rendered }] : []; - }); - if (visibleContent.length === 0) return null; - - return ( - - {visibleContent.map((content, i) => ( - - โœ“ - - {content.content} - - - ))} - - ); -}); function getMessageKey(message: NonSystemMessage, index: number) { switch (message.role) { @@ -259,72 +227,3 @@ function getMessageKey(message: NonSystemMessage, index: number) { return `${index}`; } } - -function summarizeAskUserQuestionResult(content: string) { - try { - const parsed = JSON.parse(content) as { - answers?: { question_index?: number; selected_labels?: string[] }[]; - }; - if (!parsed.answers?.length) return content; - return parsed.answers - .map((a) => { - const labels = a.selected_labels?.join(", ") ?? ""; - return `Q${(a.question_index ?? 0) + 1}: ${labels}`; - }) - .join(" ยท "); - } catch { - return content; - } -} - -function summarizeToolResult(content: string, toolUse?: ToolUseContent) { - if (!toolUse) return content; - if (content.startsWith("Error:")) return content; - - switch (toolUse.name) { - case "todo_write": - case "bash": - case "write_file": - case "str_replace": - return null; - case "read_file": - case "list_files": - case "glob_search": - case "grep_search": - case "file_info": - case "mkdir": - case "move_path": - case "apply_patch": - return summarizeStructuredToolResult(content); - case "ask_user_question": - return summarizeAskUserQuestionResult(content); - default: - return content; - } -} - -function summarizeStructuredToolResult(content: string) { - try { - const parsed = JSON.parse(content) as { - ok?: boolean; - summary?: unknown; - error?: unknown; - code?: unknown; - }; - - if (parsed.ok === true && typeof parsed.summary === "string") { - return parsed.summary; - } - - if (parsed.ok === false) { - const message = - typeof parsed.summary === "string" ? parsed.summary : typeof parsed.error === "string" ? parsed.error : content; - const code = typeof parsed.code === "string" ? parsed.code : null; - return code ? `Error [${code}]: ${message}` : `Error: ${message}`; - } - } catch { - return content; - } - - return content; -} diff --git a/src/cli/tui/message-text.ts b/src/cli/tui/message-text.ts index 194158d..3a89644 100644 --- a/src/cli/tui/message-text.ts +++ b/src/cli/tui/message-text.ts @@ -1,3 +1,4 @@ +import { summarizeToolResultText } from "@/agent/tool-result-summary"; import type { AssistantMessage, NonSystemMessage, ToolMessage, ToolUseContent, UserMessage } from "@/foundation"; const ESC = "\x1b["; @@ -18,7 +19,7 @@ export function messageToPlainText(message: NonSystemMessage): string | null { case "assistant": return assistantMessageText(message); case "tool": - return toolMessageText(message); + return null; default: return null; } @@ -78,8 +79,9 @@ function toolUseText(content: ToolUseContent): string { function toolMessageText(message: ToolMessage): string | null { const parts: string[] = []; for (const content of message.content) { - if (content.content.startsWith("Error:")) { - parts.push(`${dim("โœ“")} ${dim(content.content)}`); + const summary = summarizeToolResultText(content.content); + if (summary) { + parts.push(`${dim("โœ“")} ${dim(summary)}`); } } return parts.length > 0 ? parts.join("\n") : null; diff --git a/src/coding/agents/lead-agent.ts b/src/coding/agents/lead-agent.ts index 46d7e00..736b4f5 100644 --- a/src/coding/agents/lead-agent.ts +++ b/src/coding/agents/lead-agent.ts @@ -83,6 +83,17 @@ Use the given tools and skills to perform parallel/sequential operations and sol + +- Inspect directories before assuming file paths. +- Prefer list_files or glob_search to discover files. +- Prefer grep_search to locate relevant content. +- Read a file before editing it. +- Prefer apply_patch for targeted edits. +- If apply_patch fails, re-read the file and choose a safer edit strategy. +- Do not repeat the same failing tool call with unchanged invalid input. +- Use tool result summaries and error codes to decide the next step. + + - Never try to start a local static server. Let the user do it. - If the user's input is a simple task or a greeting, you should just respond with a simple answer and then stop. diff --git a/src/coding/tools/__tests__/file-info.test.ts b/src/coding/tools/__tests__/file-info.test.ts index 8ef7b94..858072f 100644 --- a/src/coding/tools/__tests__/file-info.test.ts +++ b/src/coding/tools/__tests__/file-info.test.ts @@ -36,8 +36,8 @@ describe("fileInfoTool", () => { }); if (result.ok) { - expect(result.data.modifiedTime).toMatch(/T/); - expect(result.data.createdTime).toMatch(/T/); + expect(result.data!.modifiedTime).toMatch(/T/); + expect(result.data!.createdTime).toMatch(/T/); } }); diff --git a/src/coding/tools/__tests__/glob-search.test.ts b/src/coding/tools/__tests__/glob-search.test.ts index e8266b5..1f0f1ef 100644 --- a/src/coding/tools/__tests__/glob-search.test.ts +++ b/src/coding/tools/__tests__/glob-search.test.ts @@ -39,8 +39,8 @@ describe("globSearchTool", () => { }); if (result.ok) { - expect(result.data.matches).toEqual([join(tempDir, "src", "index.ts")]); - expect(result.data.content).toContain(join(tempDir, "src", "index.ts")); + expect(result.data!.matches).toEqual([join(tempDir, "src", "index.ts")]); + expect(result.data!.content).toContain(join(tempDir, "src", "index.ts")); } }); diff --git a/src/coding/tools/__tests__/grep-search.test.ts b/src/coding/tools/__tests__/grep-search.test.ts index 22149e5..aecaa29 100644 --- a/src/coding/tools/__tests__/grep-search.test.ts +++ b/src/coding/tools/__tests__/grep-search.test.ts @@ -57,8 +57,8 @@ describe("grepSearchTool", () => { }); if (result.ok) { - expect(result.data.matches.some((line) => line.includes("alpha.txt:1:needle"))).toBe(true); - expect(result.data.matches.some((line) => line.includes("beta.txt:1:NEEDLE"))).toBe(true); + expect(result.data!.matches.some((line) => line.includes("alpha.txt:1:needle"))).toBe(true); + expect(result.data!.matches.some((line) => line.includes("beta.txt:1:NEEDLE"))).toBe(true); } }); }); diff --git a/src/coding/tools/__tests__/list-files.test.ts b/src/coding/tools/__tests__/list-files.test.ts index 7bf8db9..b510951 100644 --- a/src/coding/tools/__tests__/list-files.test.ts +++ b/src/coding/tools/__tests__/list-files.test.ts @@ -39,8 +39,8 @@ describe("listFilesTool", () => { }); if (result.ok) { - expect(result.data.entries).toEqual(["README.md", "src/", "src/index.ts"]); - expect(result.data.content).toContain("src/index.ts"); + expect(result.data!.entries).toEqual(["README.md", "src/", "src/index.ts"]); + expect(result.data!.content).toContain("src/index.ts"); } }); diff --git a/src/coding/tools/tool-result.ts b/src/coding/tools/tool-result.ts index 4801084..976e55c 100644 --- a/src/coding/tools/tool-result.ts +++ b/src/coding/tools/tool-result.ts @@ -1,16 +1,6 @@ -export type ToolResult = - | { - ok: true; - summary: string; - data: T; - } - | { - ok: false; - summary: string; - error: string; - code?: string; - details?: Record; - }; +import type { StructuredToolResult } from "@/foundation"; + +export type ToolResult = StructuredToolResult; export function okToolResult(summary: string, data: T): ToolResult { return { ok: true, summary, data }; diff --git a/src/foundation/tools/index.ts b/src/foundation/tools/index.ts index d977232..cb8f47c 100644 --- a/src/foundation/tools/index.ts +++ b/src/foundation/tools/index.ts @@ -1,5 +1,6 @@ import type { FunctionTool } from "./function-tool"; export * from "./function-tool"; +export * from "./structured-tool-result"; export type Tool = FunctionTool; diff --git a/src/foundation/tools/structured-tool-result.ts b/src/foundation/tools/structured-tool-result.ts new file mode 100644 index 0000000..32b5bfc --- /dev/null +++ b/src/foundation/tools/structured-tool-result.ts @@ -0,0 +1,15 @@ +export type StructuredToolSuccess = { + ok: true; + summary: string; + data?: T; +}; + +export type StructuredToolError = { + ok: false; + summary: string; + error: string; + code?: string; + details?: Record; +}; + +export type StructuredToolResult = StructuredToolSuccess | StructuredToolError;