fix(codex): restore transcript tool calls

This commit is contained in:
weishu
2026-07-22 09:02:06 +08:00
parent af962fc61f
commit 3dd425b15c
4 changed files with 465 additions and 27 deletions
+184 -6
View File
@@ -17,7 +17,7 @@ describe('convertCodexEvent', () => {
payload: { type: 'agent_message', message: 'hello' }
});
expect(result?.message).toMatchObject({
expect(result?.messages?.[0]).toMatchObject({
type: 'message',
message: 'hello'
});
@@ -42,7 +42,7 @@ describe('convertCodexEvent', () => {
}
});
expect(result?.message).toMatchObject({
expect(result?.messages?.[0]).toMatchObject({
type: 'proposed_plan',
plan: '## Plan\n\n1. Inspect\n2. Implement',
id: 'plan-1',
@@ -127,7 +127,7 @@ describe('convertCodexEvent', () => {
payload: { type: 'agent_reasoning', text: 'thinking' }
});
expect(result?.message).toMatchObject({
expect(result?.messages?.[0]).toMatchObject({
type: 'reasoning',
message: 'thinking'
});
@@ -139,7 +139,7 @@ describe('convertCodexEvent', () => {
payload: { type: 'agent_reasoning_delta', delta: 'step' }
});
expect(result?.message).toEqual({
expect(result?.messages?.[0]).toEqual({
type: 'reasoning-delta',
delta: 'step'
});
@@ -156,7 +156,7 @@ describe('convertCodexEvent', () => {
}
});
expect(result?.message).toMatchObject({
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call',
name: 'ToolName',
callId: 'call-1',
@@ -174,10 +174,188 @@ describe('convertCodexEvent', () => {
}
});
expect(result?.message).toMatchObject({
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call-result',
callId: 'call-2',
output: { ok: true }
});
});
it.each([
['exec', 'ls -la'],
['apply_patch', '*** Begin Patch\n*** End Patch']
])('converts %s custom_tool_call items', (name, input) => {
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'custom_tool_call',
name,
call_id: `call-${name}`,
input
}
});
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call',
name,
callId: `call-${name}`,
input
});
});
it.each([
['string', 'command output'],
['array', [{ type: 'input_text', text: 'patch applied' }]]
])('preserves %s custom_tool_call_output values', (_name, output) => {
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'custom_tool_call_output',
call_id: 'call-custom-output',
output
}
});
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call-result',
callId: 'call-custom-output',
output
});
});
it('converts tool_search_call items', () => {
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'tool_search_call',
call_id: 'call-tool-search',
arguments: { query: 'hapi change title', limit: 5 }
}
});
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call',
name: 'ToolSearch',
callId: 'call-tool-search',
input: { query: 'hapi change title', limit: 5 }
});
});
it('converts tool_search_output items', () => {
const tools = [{ name: 'mcp__hapi', description: 'Hapi tools' }];
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'tool_search_output',
call_id: 'call-tool-search',
execution: 'client',
status: 'completed',
tools
}
});
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call-result',
callId: 'call-tool-search',
output: { execution: 'client', tools }
});
});
it('converts a completed web_search_call into a paired call and result', () => {
const action = {
type: 'search',
query: 'Codex transcript format',
queries: ['Codex transcript format']
};
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'web_search_call',
status: 'completed',
action
}
});
expect(result?.messages).toEqual([{
type: 'tool-call',
name: 'WebSearch',
callId: expect.any(String),
input: action,
id: expect.any(String)
}, {
type: 'tool-call-result',
callId: expect.any(String),
output: null,
id: expect.any(String)
}]);
expect(result?.messages?.[1]).toMatchObject({
callId: result?.messages?.[0]?.type === 'tool-call'
? result.messages[0].callId
: undefined
});
});
it('uses an empty input for older web_search_call items without an action', () => {
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'web_search_call',
status: 'completed'
}
});
expect(result?.messages?.[0]).toMatchObject({
type: 'tool-call',
name: 'WebSearch',
input: {}
});
});
it.each(['failed', 'error'])('marks a %s web_search_call result as an error', (status) => {
const result = convertCodexEvent({
type: 'response_item',
payload: {
type: 'web_search_call',
status,
action: { type: 'search', query: 'failing query' }
}
});
expect(result?.messages?.[1]).toMatchObject({
type: 'tool-call-result',
output: null,
is_error: true
});
});
it.each([
['custom tool call without a name', {
type: 'custom_tool_call',
call_id: 'call-missing-name',
input: 'pwd'
}],
['custom tool call without a call id', {
type: 'custom_tool_call',
name: 'exec',
input: 'pwd'
}],
['custom tool output without a call id', {
type: 'custom_tool_call_output',
output: 'done'
}],
['tool search call without a call id', {
type: 'tool_search_call',
arguments: { query: 'missing id' }
}],
['tool search output without a call id', {
type: 'tool_search_output',
execution: 'client',
tools: []
}]
])('ignores %s', (_name, payload) => {
expect(convertCodexEvent({
type: 'response_item',
payload
})).toBeNull();
});
});
+105 -16
View File
@@ -41,11 +41,12 @@ export type CodexMessage = {
callId: string;
output: unknown;
id: string;
is_error?: boolean;
};
export type CodexConversionResult = {
sessionId?: string;
message?: CodexMessage;
messages?: CodexMessage[];
userMessage?: string;
userActivity?: true;
finishedTurnId?: string;
@@ -72,7 +73,7 @@ function parseArguments(value: unknown): unknown {
try {
return JSON.parse(trimmed);
} catch (error) {
logger.debug('[codexEventConverter] Failed to parse function_call arguments as JSON:', error);
logger.debug('[codexEventConverter] Failed to parse tool call input as JSON:', error);
}
}
@@ -141,11 +142,11 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'message',
message,
id: randomUUID()
}
}]
};
}
@@ -158,12 +159,12 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'proposed_plan',
plan: message,
id: asString(item?.id) ?? randomUUID(),
turnId
}
}]
};
}
@@ -178,11 +179,11 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'reasoning',
message,
id: randomUUID()
}
}]
};
}
@@ -192,10 +193,10 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'reasoning-delta',
delta
}
}]
};
}
@@ -205,11 +206,11 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'token_count',
info,
id: randomUUID()
}
}]
};
}
@@ -234,13 +235,13 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'tool-call',
name,
callId,
input: parseArguments(payloadRecord.arguments),
id: randomUUID()
}
}]
};
}
@@ -250,12 +251,100 @@ export function convertCodexEvent(rawEvent: unknown): CodexConversionResult | nu
return null;
}
return {
message: {
messages: [{
type: 'tool-call-result',
callId,
output: payloadRecord.output,
id: randomUUID()
}
}]
};
}
if (itemType === 'custom_tool_call') {
const name = asString(payloadRecord.name);
const callId = extractCallId(payloadRecord);
if (!name || !callId) {
return null;
}
return {
messages: [{
type: 'tool-call',
name,
callId,
input: parseArguments(payloadRecord.input),
id: randomUUID()
}]
};
}
if (itemType === 'custom_tool_call_output') {
const callId = extractCallId(payloadRecord);
if (!callId) {
return null;
}
return {
messages: [{
type: 'tool-call-result',
callId,
output: payloadRecord.output,
id: randomUUID()
}]
};
}
if (itemType === 'tool_search_call') {
const callId = extractCallId(payloadRecord);
if (!callId) {
return null;
}
return {
messages: [{
type: 'tool-call',
name: 'ToolSearch',
callId,
input: parseArguments(payloadRecord.arguments),
id: randomUUID()
}]
};
}
if (itemType === 'tool_search_output') {
const callId = extractCallId(payloadRecord);
if (!callId) {
return null;
}
return {
messages: [{
type: 'tool-call-result',
callId,
output: {
execution: payloadRecord.execution,
tools: payloadRecord.tools
},
id: randomUUID()
}]
};
}
if (itemType === 'web_search_call') {
// Transcript web searches have neither a call id nor a separate output item.
const callId = randomUUID();
const status = asString(payloadRecord.status)?.toLowerCase();
const isError = status === 'failed' || status === 'error';
return {
messages: [{
type: 'tool-call',
name: 'WebSearch',
callId,
input: payloadRecord.action ?? {},
id: randomUUID()
}, {
type: 'tool-call-result',
callId,
output: null,
id: randomUUID(),
...(isError ? { is_error: true } : {})
}]
};
}