fix: improve tool summaries

This commit is contained in:
Peter Steinberger
2026-01-23 00:59:44 +00:00
parent 51a9053387
commit 52b6bf04af
10 changed files with 134 additions and 17 deletions

View File

@@ -30,6 +30,7 @@ Docs: https://docs.clawd.bot
- Agents: surface concrete API error details instead of generic AI service errors.
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
- Agents: make tool summaries more readable and only show optional params when set.
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.

View File

@@ -51,7 +51,7 @@ You can tune console verbosity independently via:
## Tool summary redaction
Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the
Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)

View File

@@ -127,7 +127,7 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads).toHaveLength(1);
expect(payloads[0]?.isError).toBe(true);
expect(payloads[0]?.text).toContain("browser");
expect(payloads[0]?.text).toContain("Browser");
expect(payloads[0]?.text).toContain("tab not found");
});

View File

@@ -44,7 +44,7 @@ describe("subscribeEmbeddedPiSession", () => {
expect(onToolResult).toHaveBeenCalledTimes(1);
const payload = onToolResult.mock.calls[0][0];
expect(payload.text).toContain("🖼️");
expect(payload.text).toContain("canvas");
expect(payload.text).toContain("Canvas");
expect(payload.text).toContain("A2UI push");
expect(payload.text).toContain("/tmp/a2ui.jsonl");
});

View File

@@ -166,7 +166,7 @@ describe("subscribeEmbeddedPiSession", () => {
expect(onToolResult).toHaveBeenCalledTimes(1);
const payload = onToolResult.mock.calls[0][0];
expect(payload.text).toContain("🌐");
expect(payload.text).toContain("browser");
expect(payload.text).toContain("Browser");
expect(payload.text).toContain("snapshot");
expect(payload.text).toContain("https://example.com");
});
@@ -200,7 +200,7 @@ describe("subscribeEmbeddedPiSession", () => {
expect(onToolResult).toHaveBeenCalledTimes(1);
const summary = onToolResult.mock.calls[0][0];
expect(summary.text).toContain("exec");
expect(summary.text).toContain("Exec");
expect(summary.text).toContain("pty");
handler?.({

View File

@@ -31,7 +31,8 @@ const subagentRuns = new Map<string, SubagentRunRecord>();
let sweeper: NodeJS.Timeout | null = null;
let listenerStarted = false;
let listenerStop: (() => void) | null = null;
let restoreAttempted = false;
// Use var to avoid TDZ when init runs across circular imports during bootstrap.
var restoreAttempted = false;
function persistSubagentRuns() {
try {

View File

@@ -256,17 +256,26 @@
"sessions_history": {
"emoji": "🧾",
"title": "Session History",
"detailKeys": ["sessionKey", "limit"]
"detailKeys": ["sessionKey", "limit", "includeTools"]
},
"sessions_send": {
"emoji": "📨",
"title": "Session Send",
"detailKeys": ["sessionKey", "timeoutSeconds"]
"detailKeys": ["label", "sessionKey", "agentId", "timeoutSeconds"]
},
"sessions_spawn": {
"emoji": "🧑‍🔧",
"title": "Sub-agent",
"detailKeys": ["label", "agentId", "thinking", "runTimeoutSeconds", "cleanup"]
"detailKeys": [
"label",
"task",
"agentId",
"model",
"thinking",
"runTimeoutSeconds",
"cleanup",
"timeoutSeconds"
]
},
"session_status": {
"emoji": "📊",

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { formatToolDetail, resolveToolDisplay } from "./tool-display.js";
describe("tool display details", () => {
it("skips zero/false values for optional detail fields", () => {
const detail = formatToolDetail(
resolveToolDisplay({
name: "sessions_spawn",
args: {
task: "double-message-bug-gpt",
label: 0,
runTimeoutSeconds: 0,
timeoutSeconds: 0,
},
}),
);
expect(detail).toBe("double-message-bug-gpt");
});
it("includes only truthy boolean details", () => {
const detail = formatToolDetail(
resolveToolDisplay({
name: "message",
args: {
action: "react",
provider: "discord",
to: "chan-1",
remove: false,
},
}),
);
expect(detail).toContain("provider discord");
expect(detail).toContain("to chan-1");
expect(detail).not.toContain("remove");
});
it("keeps positive numbers and true booleans", () => {
const detail = formatToolDetail(
resolveToolDisplay({
name: "sessions_history",
args: {
sessionKey: "agent:main:main",
limit: 20,
includeTools: true,
},
}),
);
expect(detail).toContain("session agent:main:main");
expect(detail).toContain("limit 20");
expect(detail).toContain("tools true");
});
});

View File

@@ -33,6 +33,25 @@ export type ToolDisplay = {
const TOOL_DISPLAY_CONFIG = TOOL_DISPLAY_JSON as ToolDisplayConfig;
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { emoji: "🧩" };
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
const DETAIL_LABEL_OVERRIDES: Record<string, string> = {
agentId: "agent",
sessionKey: "session",
targetId: "target",
targetUrl: "url",
nodeId: "node",
requestId: "request",
messageId: "message",
threadId: "thread",
channelId: "channel",
guildId: "guild",
userId: "user",
runTimeoutSeconds: "timeout",
timeoutSeconds: "timeout",
includeTools: "tools",
pollQuestion: "poll",
maxChars: "max chars",
};
const MAX_DETAIL_ENTRIES = 8;
function normalizeToolName(name?: string): string {
return (name ?? "tool").trim();
@@ -66,7 +85,11 @@ function coerceDisplayValue(value: unknown): string | undefined {
if (!firstLine) return undefined;
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}` : firstLine;
}
if (typeof value === "number" || typeof value === "boolean") {
if (typeof value === "boolean") {
return value ? "true" : undefined;
}
if (typeof value === "number") {
if (!Number.isFinite(value) || value === 0) return undefined;
return String(value);
}
if (Array.isArray(value)) {
@@ -92,13 +115,40 @@ function lookupValueByPath(args: unknown, path: string): unknown {
return current;
}
function formatDetailKey(raw: string): string {
const segments = raw.split(".").filter(Boolean);
const last = segments.at(-1) ?? raw;
const override = DETAIL_LABEL_OVERRIDES[last];
if (override) return override;
const cleaned = last.replace(/_/g, " ").replace(/-/g, " ");
const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
return spaced.trim().toLowerCase() || last.toLowerCase();
}
function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined {
const entries: Array<{ label: string; value: string }> = [];
for (const key of keys) {
const value = lookupValueByPath(args, key);
const display = coerceDisplayValue(value);
if (display) return display;
if (!display) continue;
entries.push({ label: formatDetailKey(key), value: display });
}
return undefined;
if (entries.length === 0) return undefined;
if (entries.length === 1) return entries[0].value;
const seen = new Set<string>();
const unique: Array<{ label: string; value: string }> = [];
for (const entry of entries) {
const token = `${entry.label}:${entry.value}`;
if (seen.has(token)) continue;
seen.add(token);
unique.push(entry);
}
if (unique.length === 0) return undefined;
return unique
.slice(0, MAX_DETAIL_ENTRIES)
.map((entry) => `${entry.label} ${entry.value}`)
.join(" · ");
}
function resolveReadDetail(args: unknown): string | undefined {
@@ -139,7 +189,7 @@ export function resolveToolDisplay(params: {
const spec = TOOL_MAP[key];
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? name;
const label = spec?.label ?? title;
const actionRaw =
params.args && typeof params.args === "object"
? ((params.args as Record<string, unknown>).action as string | undefined)

View File

@@ -30,7 +30,7 @@ describe("tool meta formatting", () => {
"note",
"a→b",
]);
expect(out).toMatch(/^🧩 fs/);
expect(out).toMatch(/^🧩 Fs/);
expect(out).toContain("~/dir/{a.txt, b.txt}");
expect(out).toContain("note");
expect(out).toContain("a→b");
@@ -47,12 +47,12 @@ describe("tool meta formatting", () => {
const out = formatToolAggregate("exec", ["cd /Users/test/dir && gemini 2>&1 · elevated"], {
markdown: true,
});
expect(out).toBe("🛠️ exec: elevated · `cd ~/dir && gemini 2>&1`");
expect(out).toBe("🛠️ Exec: elevated · `cd ~/dir && gemini 2>&1`");
});
it("formats prefixes with default labels", () => {
vi.stubEnv("HOME", "/Users/test");
expect(formatToolPrefix(undefined, undefined)).toBe("🧩 tool");
expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("🧩 x: ~/a.txt");
expect(formatToolPrefix(undefined, undefined)).toBe("🧩 Tool");
expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("🧩 X: ~/a.txt");
});
});