Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions plugins/tracing/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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) {
Expand Down Expand Up @@ -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") {
Expand Down
87 changes: 87 additions & 0 deletions plugins/tracing/test/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" } },
Expand Down