diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 2970a4a2..a1cb8f12 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -1695,15 +1695,26 @@ function mapAgentRunEvent(event, mapping = {}) { if (event.type === "tool_call") { const commandExecution = agentRunCommandExecutionEvent(base, payload); if (commandExecution) return commandExecution; - const toolName = firstNonEmpty(payload.toolName, payload.name, payload.item?.type, payload.type, payload.method, "call"); - const command = firstNonEmpty(payload.command, payload.commandLine, payload.item?.command, payload.item?.commandLine); - const output = firstNonEmpty(payload.outputSummary, payload.stdoutSummary, payload.summary?.text, payload.itemPreview); + const preview = agentRunToolCallPreviewSummary(payload); + const toolName = firstNonEmpty( + payload.toolName, + payload.name, + payload.item?.type, + payload.type, + preview.toolName, + preview.name, + preview.type, + isProtocolToolLifecycleMethod(payload.method) ? null : payload.method, + "tool call" + ); + const command = firstNonEmpty(payload.command, payload.commandLine, payload.item?.command, payload.item?.commandLine, preview.command, preview.commandLine); + const output = firstNonEmpty(payload.outputSummary, payload.stdoutSummary, preview.outputSummary, preview.stdoutSummary, payload.summary?.text, payload.itemPreview); return { ...base, type: "tool_call", - status: String(payload.status ?? (payload.method === "item/completed" ? "completed" : "running")), + status: String(payload.status ?? preview.status ?? (payload.method === "item/completed" ? "completed" : "running")), label: `agentrun:tool:${String(toolName)}`, - itemId: payload.item?.id ?? payload.itemId ?? null, + itemId: payload.item?.id ?? payload.itemId ?? preview.itemId ?? null, toolName, command, exitCode: Number.isInteger(payload.item?.exitCode) ? payload.item.exitCode : Number.isInteger(payload.exitCode) ? payload.exitCode : undefined, @@ -1732,6 +1743,38 @@ function mapAgentRunEvent(event, mapping = {}) { return { ...base, type: "backend", status: "running", label: `agentrun:event:${String(event.type ?? "unknown")}`, message: textPayload(payload, "AgentRun event") }; } +function isProtocolToolLifecycleMethod(value) { + return /^item\/(started|completed)$/u.test(String(value ?? "")); +} + +function agentRunToolCallPreviewSummary(payload = {}) { + const text = firstNonEmpty(payload.itemPreview, payload.summary?.text); + return { + itemId: jsonPreviewStringField(text, "itemId"), + type: jsonPreviewStringField(text, "type"), + toolName: jsonPreviewStringField(text, "toolName"), + name: jsonPreviewStringField(text, "name"), + status: jsonPreviewStringField(text, "status"), + command: jsonPreviewStringField(text, "command"), + commandLine: jsonPreviewStringField(text, "commandLine"), + outputSummary: jsonPreviewStringField(text, "outputSummary"), + stdoutSummary: jsonPreviewStringField(text, "stdoutSummary") + }; +} + +function jsonPreviewStringField(text, key) { + const body = String(text ?? ""); + if (!body || !/^[A-Za-z][A-Za-z0-9_]*$/u.test(key)) return null; + const match = body.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "u")); + if (!match) return null; + try { + const parsed = JSON.parse(`"${match[1]}"`); + return typeof parsed === "string" && parsed.trim() ? parsed.trim() : null; + } catch { + return match[1]?.trim() || null; + } +} + function agentRunBackendStatusDetails(phase, payload = {}) { if (phase === "initial-prompt-assembly") { return compactObject({ diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 0cc1924b..b4ffeece 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -126,7 +126,7 @@ interface AssistantSnapshotDecision { function traceToolCallRow(trace: Record, event: TraceEvent): TraceEventRow { const command = cleanShellCommand(event.command); const status = traceStatusToken(event); - const toolName = nonEmptyString(event.toolName) ?? readableTraceLabel(event).replace(/^item\//u, ""); + const toolName = traceToolName(event, command); const body = traceToolCallBody(event, command); return { rowId: `tool:${event.itemId ?? event.seq ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`, @@ -139,6 +139,46 @@ function traceToolCallRow(trace: Record, event: TraceEvent): Tr }; } +function traceToolName(event: TraceEvent, command: string | null): string { + const direct = nonEmptyString(event.toolName); + if (direct && !isProtocolToolLifecycleName(direct)) return direct; + const label = readableTraceLabel(event).replace(/^agentrun:tool:/u, "").replace(/^tool:/u, "").replace(/^item\//u, ""); + if (label && !isProtocolToolLifecycleName(label)) return label; + const commandName = traceToolNameFromCommand(command); + if (commandName) return commandName; + const previewName = traceToolNameFromPreview(event); + if (previewName) return previewName; + return "tool call"; +} + +function isProtocolToolLifecycleName(value: unknown): boolean { + return /^(?:item\/)?(?:started|completed)$/iu.test(String(value ?? "").trim()) || /^item\/(?:started|completed)$/iu.test(String(value ?? "").trim()); +} + +function traceToolNameFromCommand(command: string | null): string | null { + const text = cleanTraceText(command); + const match = text.match(/^([A-Za-z][A-Za-z0-9_.:-]{1,80})(?:\s|$)/u); + return match?.[1] ?? null; +} + +function traceToolNameFromPreview(event: TraceEvent): string | null { + const preview = traceEventText(event, ["output", "outputText", "outputSummary", "stdoutSummary", "message"]); + return jsonPreviewStringField(preview, "toolName") ?? jsonPreviewStringField(preview, "type") ?? jsonPreviewStringField(preview, "name"); +} + +function jsonPreviewStringField(text: string | null, key: string): string | null { + const body = String(text ?? ""); + if (!body || !/^[A-Za-z][A-Za-z0-9_]*$/u.test(key)) return null; + const match = body.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "u")); + if (!match) return null; + try { + const parsed = JSON.parse(`"${match[1]}"`); + return typeof parsed === "string" && parsed.trim() ? parsed.trim() : null; + } catch { + return match[1]?.trim() || null; + } +} + function traceToolCallBody(event: TraceEvent, command: string | null): string | null { const lines = [ command,