mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
fix(codex): restore transcript tool calls
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 } : {})
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user