diff --git a/package-lock.json b/package-lock.json index 2d68571..5b39bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@evoscientist/webui", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@evoscientist/webui", - "version": "0.0.3", + "version": "0.0.4", "license": "Apache-2.0", "dependencies": { "@langchain/core": "1.1.19", diff --git a/package.json b/package.json index ad910de..3f2081f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@evoscientist/webui", - "version": "0.0.3", + "version": "0.0.4", "description": "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.", "author": "Xi Zhang ", "type": "module", diff --git a/src/app/components/ToolCallBox.tsx b/src/app/components/ToolCallBox.tsx index 5e706f1..b444e08 100644 --- a/src/app/components/ToolCallBox.tsx +++ b/src/app/components/ToolCallBox.tsx @@ -21,6 +21,7 @@ import { ToolCall, ActionRequest, ReviewConfig } from "@/app/types/types"; import { cn } from "@/lib/utils"; import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui"; import { ToolApprovalInterrupt } from "@/app/components/ToolApprovalInterrupt"; +import { formatToolLabel } from "@/lib/toolLabel"; interface ToolCallBoxProps { toolCall: ToolCall; @@ -70,9 +71,19 @@ export const ToolCallBox = React.memo( }, [actionRequest, autoApprove]); const { name, args, result, status } = useMemo(() => { + // Streaming can deliver args as a (possibly partial) JSON string, not an + // object — treat it as `unknown` and only expose a real object to the + // args view, so Object.keys/entries never run on a string. + const rawArgs: unknown = toolCall.args; return { - name: toolCall.name || "Unknown Tool", - args: toolCall.args || {}, + // Targeted label overrides only (read_file/write_file/edit_file on + // /memories → "Reading memory"/"Updating memory", think_tool → + // "Reflection"); every other tool keeps its raw name. + name: formatToolLabel(toolCall.name, rawArgs), + args: + rawArgs && typeof rawArgs === "object" + ? (rawArgs as Record) + : {}, result: toolCall.result, status: toolCall.status || "completed", }; diff --git a/src/lib/toolLabel.ts b/src/lib/toolLabel.ts new file mode 100644 index 0000000..feebbe9 --- /dev/null +++ b/src/lib/toolLabel.ts @@ -0,0 +1,76 @@ +/** + * Targeted tool-call label overrides. + * + * We deliberately do NOT reformat tool calls in general (no `ls(path)` / + * `execute(cmd)` argument summaries). Only a few specific names are remapped; + * every other tool keeps its raw name. + * + * Overrides: + * - read_file / write_file / edit_file on a `/memories/...` path → + * "Reading memory" / "Updating memory". + * - read_file on a `/skills//...` path → "Skill: ". + * - think_tool → "Reflection". + * - the tool-selector's internal ToolSelectionResponse → "Adaptive tool + * selection". + */ + +/** True when a virtual path targets the global memories directory. */ +function isMemoryPath(path: string): boolean { + const p = (path || "").trim(); + return p === "/memories" || p.startsWith("/memories/"); +} + +/** If a virtual path targets a skill (`/skills//...`), return ``. + * An empty first segment (e.g. `/skills//x`) is treated as no match. */ +function skillNameFromPath(path: string): string { + const p = (path || "").trim(); + if (!p.startsWith("/skills/")) return ""; + const [skill] = p.slice("/skills/".length).split("/"); + return skill || ""; +} + +/** Coerce tool args to an object — they may stream in as a JSON string. */ +function asArgsObject(args: unknown): Record { + if (args && typeof args === "object") return args as Record; + if (typeof args === "string") { + try { + const parsed = JSON.parse(args); + if (parsed && typeof parsed === "object") { + return parsed as Record; + } + } catch { + // Partial / non-JSON args while still streaming — treat as empty. + } + } + return {}; +} + +export function formatToolLabel(name: string, args?: unknown): string { + if (!name) return "Unknown Tool"; + + // The tool-selector middleware's internal structured-output call. + if (name === "ToolSelectionResponse") return "Adaptive tool selection"; + + const nameLower = name.toLowerCase(); + + if (nameLower === "think_tool") return "Reflection"; + + if ( + nameLower === "read_file" || + nameLower === "write_file" || + nameLower === "edit_file" + ) { + const a = asArgsObject(args); + const path = String(a.path ?? a.file_path ?? ""); + if (isMemoryPath(path)) { + return nameLower === "read_file" ? "Reading memory" : "Updating memory"; + } + if (nameLower === "read_file") { + const skill = skillNameFromPath(path); + if (skill) return `Skill: ${skill}`; + } + } + + // Everything else keeps its raw tool name. + return name; +}