From d907a2998a7ab2925612f5c1204fde5171b6b5e7 Mon Sep 17 00:00:00 2001
From: changyuan <1330482928@qq.com>
Date: Fri, 10 Apr 2026 17:25:45 +0800
Subject: [PATCH 1/4] feat: for agent, add structured tool-result runtime and
tool-calling guidance
---
.../__tests__/tool-result-policy.test.ts | 30 +++
.../__tests__/tool-result-runtime.test.ts | 135 +++++++++++++
src/agent/agent.ts | 15 +-
src/agent/tool-result-policy.ts | 45 +++++
src/agent/tool-result-runtime.ts | 183 ++++++++++++++++++
src/agent/tool-result-summary.ts | 28 +++
src/cli/tui/components/message-history.tsx | 3 +-
src/cli/tui/message-text.ts | 6 +-
src/coding/agents/lead-agent.ts | 11 ++
src/coding/tools/__tests__/file-info.test.ts | 4 +-
.../tools/__tests__/glob-search.test.ts | 4 +-
.../tools/__tests__/grep-search.test.ts | 4 +-
src/coding/tools/__tests__/list-files.test.ts | 4 +-
src/coding/tools/tool-result.ts | 16 +-
src/foundation/tools/index.ts | 1 +
.../tools/structured-tool-result.ts | 15 ++
16 files changed, 470 insertions(+), 34 deletions(-)
create mode 100644 src/agent/__tests__/tool-result-policy.test.ts
create mode 100644 src/agent/__tests__/tool-result-runtime.test.ts
create mode 100644 src/agent/tool-result-policy.ts
create mode 100644 src/agent/tool-result-runtime.ts
create mode 100644 src/agent/tool-result-summary.ts
create mode 100644 src/foundation/tools/structured-tool-result.ts
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..1af6d15
--- /dev/null
+++ b/src/agent/__tests__/tool-result-runtime.test.ts
@@ -0,0 +1,135 @@
+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("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..4d4dc2a
--- /dev/null
+++ b/src/agent/tool-result-runtime.ts
@@ -0,0 +1,183 @@
+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 {
+ 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/components/message-history.tsx b/src/cli/tui/components/message-history.tsx
index 249ae19..80c6507 100644
--- a/src/cli/tui/components/message-history.tsx
+++ b/src/cli/tui/components/message-history.tsx
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
import { memo } from "react";
+import { summarizeToolResultText } from "@/agent/tool-result-summary";
import type { AssistantMessage, NonSystemMessage, ToolMessage, ToolUseContent, UserMessage } from "@/foundation";
import { currentTheme } from "../themes";
@@ -295,7 +296,7 @@ function summarizeToolResult(content: string, toolUse?: ToolUseContent) {
case "mkdir":
case "move_path":
case "apply_patch":
- return summarizeStructuredToolResult(content);
+ return summarizeToolResultText(content) ?? content;
case "ask_user_question":
return summarizeAskUserQuestionResult(content);
default:
diff --git a/src/cli/tui/message-text.ts b/src/cli/tui/message-text.ts
index 194158d..9adfc24 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[";
@@ -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;
From a4ef85e2b3bfcdf7a93d83cf4ad7c63d37819e1d Mon Sep 17 00:00:00 2001
From: changyuan <1330482928@qq.com>
Date: Sat, 11 Apr 2026 14:55:16 +0800
Subject: [PATCH 2/4] =?UTF-8?q?fix=EF=BC=9Aremove=20tool=20result=20tui?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/cli/tui/components/message-history.tsx | 2 +-
src/cli/tui/message-text.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/cli/tui/components/message-history.tsx b/src/cli/tui/components/message-history.tsx
index 80c6507..fa1ed4d 100644
--- a/src/cli/tui/components/message-history.tsx
+++ b/src/cli/tui/components/message-history.tsx
@@ -54,7 +54,7 @@ export const MessageHistoryItem = memo(function MessageHistoryItem({
case "assistant":
return ;
case "tool":
- return ;
+ return null;
default:
return null;
}
diff --git a/src/cli/tui/message-text.ts b/src/cli/tui/message-text.ts
index 9adfc24..3a89644 100644
--- a/src/cli/tui/message-text.ts
+++ b/src/cli/tui/message-text.ts
@@ -19,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;
}
From 58f37fc21d684fe1ca6e5fccbd4f889a320e7a90 Mon Sep 17 00:00:00 2001
From: changyuan <1330482928@qq.com>
Date: Sat, 11 Apr 2026 15:10:51 +0800
Subject: [PATCH 3/4] =?UTF-8?q?fix=EF=BC=9Aremove=20json=20structure=20for?=
=?UTF-8?q?=20read=5Ffile?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/agent/__tests__/tool-result-runtime.test.ts | 10 ++++++++++
src/agent/tool-result-runtime.ts | 4 ++++
2 files changed, 14 insertions(+)
diff --git a/src/agent/__tests__/tool-result-runtime.test.ts b/src/agent/__tests__/tool-result-runtime.test.ts
index 1af6d15..40a75cf 100644
--- a/src/agent/__tests__/tool-result-runtime.test.ts
+++ b/src/agent/__tests__/tool-result-runtime.test.ts
@@ -97,6 +97,16 @@ describe("formatToolResultForMessage", () => {
});
});
+ 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("formats errors with stable structured shape", () => {
const formatted = formatToolResultForMessage({
toolName: "grep_search",
diff --git a/src/agent/tool-result-runtime.ts b/src/agent/tool-result-runtime.ts
index 4d4dc2a..8abc2f4 100644
--- a/src/agent/tool-result-runtime.ts
+++ b/src/agent/tool-result-runtime.ts
@@ -98,6 +98,10 @@ export function normalizeToolResult(result: unknown): NormalizedToolResult {
}
export function formatToolResultForMessage({ toolName, result }: { toolName: string; result: unknown }): string {
+ if (toolName === "read_file" && typeof result === "string" && !result.startsWith("Error:")) {
+ return result;
+ }
+
const normalized = normalizeToolResult(result);
const policy = getToolResultPolicy(toolName);
From 6c162fe2513be000030caf3e79c518f59a3fe6f4 Mon Sep 17 00:00:00 2001
From: changyuan <1330482928@qq.com>
Date: Sat, 11 Apr 2026 15:44:20 +0800
Subject: [PATCH 4/4] fix: remove unnecessary code
---
.../__tests__/tool-result-runtime.test.ts | 10 ++
src/agent/tool-result-runtime.ts | 2 +-
src/cli/tui/app.tsx | 3 +-
src/cli/tui/components/message-history.tsx | 104 +-----------------
4 files changed, 13 insertions(+), 106 deletions(-)
diff --git a/src/agent/__tests__/tool-result-runtime.test.ts b/src/agent/__tests__/tool-result-runtime.test.ts
index 40a75cf..1f3ed3e 100644
--- a/src/agent/__tests__/tool-result-runtime.test.ts
+++ b/src/agent/__tests__/tool-result-runtime.test.ts
@@ -107,6 +107,16 @@ describe("formatToolResultForMessage", () => {
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",
diff --git a/src/agent/tool-result-runtime.ts b/src/agent/tool-result-runtime.ts
index 8abc2f4..b87194a 100644
--- a/src/agent/tool-result-runtime.ts
+++ b/src/agent/tool-result-runtime.ts
@@ -98,7 +98,7 @@ export function normalizeToolResult(result: unknown): NormalizedToolResult {
}
export function formatToolResultForMessage({ toolName, result }: { toolName: string; result: unknown }): string {
- if (toolName === "read_file" && typeof result === "string" && !result.startsWith("Error:")) {
+ if (toolName === "read_file" && typeof result === "string") {
return result;
}
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 fa1ed4d..24ffbd2 100644
--- a/src/cli/tui/components/message-history.tsx
+++ b/src/cli/tui/components/message-history.tsx
@@ -1,8 +1,7 @@
import { Box, Text } from "ink";
import { memo } from "react";
-import { summarizeToolResultText } from "@/agent/tool-result-summary";
-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";
@@ -13,12 +12,10 @@ export const MessageHistory = memo(function MessageHistory({
messages,
startIndex = 0,
todoSnapshots,
- toolUses,
}: {
messages: NonSystemMessage[];
startIndex?: number;
todoSnapshots: Map;
- toolUses: Map;
}) {
return (
@@ -29,7 +26,6 @@ export const MessageHistory = memo(function MessageHistory({
message={message}
messageIndex={startIndex + index}
todoSnapshots={todoSnapshots}
- toolUses={toolUses}
/>
);
})}
@@ -41,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":
@@ -218,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) {
@@ -260,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 summarizeToolResultText(content) ?? 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;
-}