feat(HAI-082): display tool call details in AgentLogViewer

- Extend AgentLogEntry type with optional detail field for tool metadata
- Thread tool args through engine executor callbacks to populate detail
- Render tool detail (name, args) in AgentLogViewer component
- Add unit tests for store, executor, and AgentLogViewer changes
This commit is contained in:
Dustin Byrne
2026-03-26 00:59:11 -04:00
parent 93a79a5bfa
commit 2baa119969
10 changed files with 150 additions and 7 deletions

View File

@@ -794,3 +794,44 @@ describe("buildExecutionPrompt", () => {
expect(agentPrompt).toContain("- **Build:** `npm run build`");
});
});
// Import the summarizeToolArgs helper directly (not affected by mocks above)
describe("summarizeToolArgs", () => {
// Dynamic import to avoid mock interference
let summarizeToolArgs: (name: string, args?: Record<string, unknown>) => string | undefined;
beforeEach(async () => {
const mod = await vi.importActual<typeof import("./executor.js")>("./executor.js");
summarizeToolArgs = mod.summarizeToolArgs;
});
it("returns command for bash tool", () => {
expect(summarizeToolArgs("Bash", { command: "ls -la" })).toBe("ls -la");
expect(summarizeToolArgs("bash", { command: "echo hello" })).toBe("echo hello");
});
it("truncates long bash commands to 80 chars", () => {
const longCmd = "a".repeat(100);
const result = summarizeToolArgs("Bash", { command: longCmd });
expect(result).toBe("a".repeat(80) + "…");
});
it("returns path for read/edit/write tools", () => {
expect(summarizeToolArgs("Read", { path: "src/types.ts" })).toBe("src/types.ts");
expect(summarizeToolArgs("edit", { path: "src/store.ts" })).toBe("src/store.ts");
expect(summarizeToolArgs("Write", { path: "out.txt", content: "data" })).toBe("out.txt");
});
it("returns first string arg for unknown tools", () => {
expect(summarizeToolArgs("task_update", { step: 1, status: "done" })).toBe("done");
});
it("returns undefined when no args provided", () => {
expect(summarizeToolArgs("Bash")).toBeUndefined();
expect(summarizeToolArgs("Bash", {})).toBeUndefined();
});
it("returns undefined when no string args found", () => {
expect(summarizeToolArgs("unknown", { count: 42, flag: true })).toBeUndefined();
});
});

View File

@@ -13,6 +13,34 @@ import type { WorktreePool } from "./worktree-pool.js";
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
/**
* Produce a short human-readable summary from tool arguments.
* Returns `undefined` for unknown tools or when no meaningful arg is found.
*/
export function summarizeToolArgs(name: string, args?: Record<string, unknown>): string | undefined {
if (!args) return undefined;
const lowerName = name.toLowerCase();
if (lowerName === "bash") {
const cmd = args.command;
if (typeof cmd === "string") {
return cmd.length > 80 ? cmd.slice(0, 80) + "…" : cmd;
}
}
if (lowerName === "read" || lowerName === "edit" || lowerName === "write") {
const p = args.path;
if (typeof p === "string") return p;
}
// Fallback: return first string-valued arg if short enough
for (const val of Object.values(args)) {
if (typeof val === "string" && val.length <= 80) return val;
}
return undefined;
}
// ── Tool parameter schemas (module-level for reuse in ToolDefinition generics) ──
const taskUpdateParams = Type.Object({
@@ -355,12 +383,13 @@ export class TaskExecutor {
scheduleFlush();
}
},
onToolStart: (name) => {
onToolStart: (name, args) => {
this.options.onAgentTool?.(task.id, name);
// Flush any pending text before recording the tool entry
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
flushTextBuffer();
this.store.appendAgentLog(task.id, name, "tool").catch(() => {});
const detail = summarizeToolArgs(name, args);
this.store.appendAgentLog(task.id, name, "tool", detail).catch(() => {});
},
});

View File

@@ -190,7 +190,7 @@ export async function aiMergeTask(
systemPrompt: MERGE_SYSTEM_PROMPT,
tools: "coding",
onText: (delta) => options.onAgentText?.(delta),
onToolStart: (name) => options.onAgentTool?.(name),
onToolStart: (name, _args) => options.onAgentTool?.(name),
});
try {

View File

@@ -28,7 +28,7 @@ export interface AgentOptions {
tools?: "coding" | "readonly";
customTools?: ToolDefinition[];
onText?: (delta: string) => void;
onToolStart?: (name: string) => void;
onToolStart?: (name: string, args?: Record<string, unknown>) => void;
onToolEnd?: (name: string, isError: boolean) => void;
}
@@ -75,7 +75,7 @@ export async function createHaiAgent(options: AgentOptions): Promise<AgentResult
options.onText?.(event.assistantMessageEvent.delta);
}
if (event.type === "tool_execution_start") {
options.onToolStart?.(event.toolName);
options.onToolStart?.(event.toolName, event.args as Record<string, unknown> | undefined);
}
if (event.type === "tool_execution_end") {
options.onToolEnd?.(event.toolName, event.isError);

View File

@@ -257,7 +257,7 @@ export class TriageProcessor {
tools: "coding",
customTools: this.createTriageTools(),
onText: (delta) => this.options.onAgentText?.(task.id, delta),
onToolStart: (name) =>
onToolStart: (name, _args) =>
console.log(`[triage] ${task.id} tool: ${name}`),
});