fix: parse codex sdk event shapes

This commit is contained in:
Andrey Avtomonov 2026-06-01 17:43:16 +02:00
parent d86d4b05c7
commit 2b84b8b173
2 changed files with 26 additions and 9 deletions

View file

@ -35,7 +35,10 @@ function usageFrom(value: unknown): LlmTokenUsage {
}
const inputTokens = numberValue(usage.input_tokens ?? usage.inputTokens);
const outputTokens = numberValue(usage.output_tokens ?? usage.outputTokens);
const totalTokens = numberValue(usage.total_tokens ?? usage.totalTokens);
const explicitTotalTokens = numberValue(usage.total_tokens ?? usage.totalTokens);
const totalTokens =
explicitTotalTokens ??
(inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
return {
...(inputTokens !== undefined ? { inputTokens } : {}),
...(outputTokens !== undefined ? { outputTokens } : {}),
@ -126,7 +129,7 @@ export function summarizeCodexExecEvents(
}
if (eventType === 'item.completed' && itemType === 'mcp_tool_call' && item.error !== undefined) {
const name = text(item.name) ?? text(item.tool_name) ?? 'unknown';
const name = text(item.name) ?? text(item.tool) ?? text(item.tool_name) ?? 'unknown';
toolFailures.push(`${name}: ${errorMessageFrom(item.error)}`);
}
}

View file

@ -5,13 +5,21 @@ import {
} from '../../../src/context/llm/codex-exec-events.js';
describe('Codex exec event parsing', () => {
it('captures final agent text, usage, steps, and natural completion', () => {
it('captures final agent text, SDK usage, steps, and natural completion', () => {
const summary = summarizeCodexExecEvents(
[
{ type: 'thread.started', thread: { id: 'thr_1' } },
{ type: 'thread.started', thread_id: 'thr_1' },
{ type: 'turn.started' },
{ type: 'item.completed', item: { id: 'item_1', type: 'agent_message', text: 'hello from codex' } },
{ type: 'turn.completed', usage: { input_tokens: 12, output_tokens: 5, total_tokens: 17 } },
{
type: 'turn.completed',
usage: {
input_tokens: 12,
cached_input_tokens: 4,
output_tokens: 5,
reasoning_output_tokens: 2,
},
},
],
{ startedAt: 100, now: () => 125 },
);
@ -37,7 +45,7 @@ describe('Codex exec event parsing', () => {
expect(summary.error?.message).toContain('Codex could not connect to required MCP server');
});
it('maps max-turns terminal reasons into budget stop reason', () => {
it('maps max-turns terminal reasons into budget stop reason when Codex emits one', () => {
const summary = summarizeCodexExecEvents([
{ type: 'turn.started' },
{ type: 'turn.completed', reason: 'max_turns', usage: { input_tokens: 1, output_tokens: 1 } },
@ -46,11 +54,17 @@ describe('Codex exec event parsing', () => {
expect(summary.stopReason).toBe('budget');
});
it('counts MCP tool calls and failed MCP tool calls', () => {
it('counts SDK-shaped MCP tool calls and failed MCP tool calls', () => {
const summary = summarizeCodexExecEvents([
{ type: 'turn.started' },
{ type: 'item.started', item: { id: 'call_1', type: 'mcp_tool_call', name: 'search' } },
{ type: 'item.completed', item: { id: 'call_1', type: 'mcp_tool_call', name: 'search', error: 'denied' } },
{
type: 'item.started',
item: { id: 'call_1', type: 'mcp_tool_call', server: 'ktx', tool: 'search', arguments: { query: 'revenue' }, status: 'in_progress' },
},
{
type: 'item.completed',
item: { id: 'call_1', type: 'mcp_tool_call', server: 'ktx', tool: 'search', arguments: { query: 'revenue' }, status: 'failed', error: { message: 'denied' } },
},
{ type: 'turn.completed' },
]);