Files
EvoScientist-WebUI/src/lib/toolLabel.ts
T
2026-08-11 17:44:52 +08:00

180 lines
5.1 KiB
TypeScript

/**
* Human-readable tool-call labels and compact argument previews.
*
* Overrides:
* - read_file / write_file / edit_file on a `/memories/...` path →
* "Reading memory" / "Updating memory".
* - read_file on a `/skills/<name>/...` path → "Skill: <name>".
* - 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/<name>/...`), return `<name>`.
* 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<string, unknown> {
if (args && typeof args === "object") return args as Record<string, unknown>;
if (typeof args === "string") {
try {
const parsed = JSON.parse(args);
if (parsed && typeof parsed === "object") {
return parsed as Record<string, unknown>;
}
} catch {
// Partial / non-JSON args while still streaming — treat as empty.
}
}
return {};
}
/** Translated strings for {@link formatToolLabelWith}; matches the
* `chat.toolLabels.*` catalog keys. */
export interface ToolLabelStrings {
unknownTool: string;
adaptiveToolSelection: string;
reflection: string;
readingMemory: string;
updatingMemory: string;
/** Skill label; `{name}` is the skill directory name. */
skill: (name: string) => string;
execute: string;
grep: string;
readFile: string;
writeFile: string;
editFile: string;
listFiles: string;
task: string;
}
const EN_TOOL_LABELS: ToolLabelStrings = {
unknownTool: "Unknown Tool",
adaptiveToolSelection: "Adaptive tool selection",
reflection: "Reflection",
readingMemory: "Reading memory",
updatingMemory: "Updating memory",
skill: (name) => `Skill: ${name}`,
execute: "Running command",
grep: "Searching files",
readFile: "Reading file",
writeFile: "Writing file",
editFile: "Editing file",
listFiles: "Listing files",
task: "Running subagent",
};
export function formatToolLabelWith(
labels: ToolLabelStrings,
name: string,
args?: unknown
): string {
if (!name) return labels.unknownTool;
// The tool-selector middleware's internal structured-output call.
if (name === "ToolSelectionResponse") return labels.adaptiveToolSelection;
const nameLower = name.toLowerCase();
if (nameLower === "think_tool") return labels.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"
? labels.readingMemory
: labels.updatingMemory;
}
if (nameLower === "read_file") {
const skill = skillNameFromPath(path);
if (skill) return labels.skill(skill);
}
}
switch (nameLower) {
case "execute":
return labels.execute;
case "grep":
return labels.grep;
case "read_file":
return labels.readFile;
case "write_file":
return labels.writeFile;
case "edit_file":
return labels.editFile;
case "ls":
case "list_files":
return labels.listFiles;
case "task":
return labels.task;
}
// Everything else keeps its raw tool name.
return name;
}
export function formatToolLabel(name: string, args?: unknown): string {
return formatToolLabelWith(EN_TOOL_LABELS, name, args);
}
function compactValue(value: unknown): string {
if (typeof value === "string") return value.replace(/\s+/g, " ").trim();
if (value === null || value === undefined) return "";
try {
return JSON.stringify(value).replace(/\s+/g, " ").trim();
} catch {
return String(value).replace(/\s+/g, " ").trim();
}
}
/** The most useful argument for a one-line activity row. */
export function toolArgumentPreview(name: string, args?: unknown): string {
const a = asArgsObject(args);
const nameLower = name.toLowerCase();
let value: unknown;
if (nameLower === "execute") value = a.command ?? a.cmd;
else if (nameLower === "grep") value = a.pattern ?? a.query;
else if (
nameLower === "read_file" ||
nameLower === "write_file" ||
nameLower === "edit_file" ||
nameLower === "ls" ||
nameLower === "list_files"
) {
value = a.path ?? a.file_path;
} else if (nameLower === "task") {
value = a.subagent_type;
}
if (value === undefined) {
value = Object.values(a).find(
(candidate) =>
typeof candidate === "string" || typeof candidate === "number"
);
}
const preview = compactValue(value);
const maxLength = 88;
return preview.length > maxLength
? `${preview.slice(0, maxLength - 1)}…`
: preview;
}