c18ca4693c
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
180 lines
5.1 KiB
TypeScript
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;
|
|
}
|