From 1a288ec0383af8d8f83c2c9e822cb44bb762bcc8 Mon Sep 17 00:00:00 2001 From: Lakshya Dhariwal <57823363+lakshya-dhariwal@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:48:26 +0530 Subject: [PATCH] fix(tracing): capture completed McpToolCall items as named tool calls Desktop code-mode turns surface nested MCP calls only as item_completed McpToolCall items - no mcp_tool_call_begin/end events and no model-visible call to enrich - so they never became observations. Map them to server.tool tool calls with arguments/result, record failures as errors, and deduplicate by call ID against existing calls. --- plugins/tracing/src/parse.ts | 52 ++++++++++++++++++ plugins/tracing/test/parse.test.ts | 87 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/plugins/tracing/src/parse.ts b/plugins/tracing/src/parse.ts index e4804f6..906ebbb 100644 --- a/plugins/tracing/src/parse.ts +++ b/plugins/tracing/src/parse.ts @@ -76,6 +76,16 @@ export function parseArgs(raw: string): unknown { } } +function mcpToolCallError(result: unknown): string { + if (typeof result === "string" && result) return result; + if (result !== null && typeof result === "object") { + const r = result as Record; + if (typeof r.Err === "string" && r.Err) return r.Err; + if (typeof r.error === "string" && r.error) return r.error; + } + return "MCP tool call failed"; +} + function extractToolError(payload: EventMsgPayload): string | undefined { const explicit = payload.error ?? payload.codex_error_info; if (explicit != null) { @@ -375,6 +385,48 @@ export function parseSession(lines: RolloutLine[]): { } else if (et === "item_completed" && p.item?.type === "UserMessage") { const text = extractMessageText(p.item.content); if (text && !turn!.userInput) turn!.userInput = text; + } else if (et === "item_completed" && p.item?.type === "McpToolCall") { + // Why: Desktop code-mode surfaces nested MCP calls only as completed McpToolCall items - + // no mcp_tool_call_begin/end events and no model-visible call to enrich. + const item = p.item as { + id?: unknown; + server?: unknown; + tool?: unknown; + arguments?: unknown; + result?: unknown; + status?: unknown; + }; + if ( + typeof item.id === "string" && + typeof item.server === "string" && + typeof item.tool === "string" + ) { + const failed = item.status === "failed"; + const existing = toolCallsById.get(item.id); + if (existing) { + existing.mcp = existing.mcp ?? { server: item.server, tool: item.tool }; + if (!failed && existing.output == null && item.result !== undefined) { + existing.output = item.result; + } + existing.endTime = Math.max(existing.endTime ?? ts, ts); + if (failed && !existing.error) existing.error = mcpToolCallError(item.result); + } else { + // startTime is the completion time: the item's duration has no documented + // serialized shape, so don't guess units from it. + const s = ensureStep(ts); + const tc: ToolCall = { + callId: item.id, + name: `${item.server}.${item.tool}`, + args: item.arguments, + startTime: ts, + endTime: ts, + ...(failed ? { error: mcpToolCallError(item.result) } : { output: item.result }), + mcp: { server: item.server, tool: item.tool }, + }; + s.toolCalls.push(tc); + toolCallsById.set(tc.callId, tc); + } + } } else if (et === "agent_message" && typeof p.message === "string") { turn!.lastAgentMessage = p.message; } else if (et === "token_count") { diff --git a/plugins/tracing/test/parse.test.ts b/plugins/tracing/test/parse.test.ts index 66e11df..e8444bf 100644 --- a/plugins/tracing/test/parse.test.ts +++ b/plugins/tracing/test/parse.test.ts @@ -536,6 +536,93 @@ describe("parseSession", () => { expect(mcp?.mcp).toEqual({ server: "linear", tool: "create_issue" }); }); + it("captures completed McpToolCall items from Desktop code-mode turns", () => { + const lines: RolloutLine[] = [ + { timestamp: "2026-06-03T12:00:00.000Z", type: "session_meta", payload: { id: "s" } }, + { + timestamp: "2026-06-03T12:00:01.000Z", + type: "event_msg", + payload: { type: "task_started", turn_id: "t" }, + }, + { + timestamp: "2026-06-03T12:00:02.000Z", + type: "event_msg", + payload: { + type: "item_completed", + item: { + type: "McpToolCall", + id: "mcp_1", + server: "codex_app", + tool: "list_projects", + arguments: { limit: 5 }, + result: { content: [{ type: "text", text: "[]" }] }, + status: "completed", + }, + }, + }, + { + timestamp: "2026-06-03T12:00:03.000Z", + type: "event_msg", + payload: { type: "task_complete", turn_id: "t" }, + }, + ]; + const { turns } = parseSession(lines); + const tools = turns[0].steps.flatMap((s) => s.toolCalls); + expect(tools).toHaveLength(1); + expect(tools[0].name).toBe("codex_app.list_projects"); + expect(tools[0].mcp).toEqual({ server: "codex_app", tool: "list_projects" }); + expect(tools[0].args).toEqual({ limit: 5 }); + expect(tools[0].output).toEqual({ content: [{ type: "text", text: "[]" }] }); + }); + + it("enriches a model-visible call from a completed McpToolCall item without duplicating it", () => { + const lines: RolloutLine[] = [ + { timestamp: "2026-06-03T12:00:00.000Z", type: "session_meta", payload: { id: "s" } }, + { + timestamp: "2026-06-03T12:00:01.000Z", + type: "event_msg", + payload: { type: "task_started", turn_id: "t" }, + }, + { + timestamp: "2026-06-03T12:00:02.000Z", + type: "response_item", + payload: { + type: "function_call", + call_id: "mcp_2", + name: "linear__create_issue", + arguments: '{"title":"bug"}', + }, + }, + { + timestamp: "2026-06-03T12:00:03.000Z", + type: "event_msg", + payload: { + type: "item_completed", + item: { + type: "McpToolCall", + id: "mcp_2", + server: "linear", + tool: "create_issue", + arguments: { title: "bug" }, + result: { Err: "boom" }, + status: "failed", + }, + }, + }, + { + timestamp: "2026-06-03T12:00:04.000Z", + type: "event_msg", + payload: { type: "task_complete", turn_id: "t" }, + }, + ]; + const { turns } = parseSession(lines); + const tools = turns[0].steps.flatMap((s) => s.toolCalls); + expect(tools).toHaveLength(1); + expect(tools[0].mcp).toEqual({ server: "linear", tool: "create_issue" }); + expect(tools[0].error).toBe("boom"); + expect(tools[0].output).toBeUndefined(); + }); + it("merges a web_search_call item with a later web_search_end event", () => { const lines: RolloutLine[] = [ { timestamp: "2026-06-03T12:00:00.000Z", type: "session_meta", payload: { id: "s" } },