Skip to content

Commit 6346f3b

Browse files
committed
fix local codex review comment:
- [P0] Convert stored tool outputs back to function results — packages/agents-openai/src/memory/openaiConversationsSession.ts:99-110 When we fetch history from an OpenAI conversation we run each item through convertToOutputItem here. Items that we previously added via session.addItems for tool results arrive from the Conversations API as type === "function_call_output", but convertToOutputItem only understands real response output types and falls back to returning an UnknownItem. On the very next turn the runner feeds these history items back through getInputItems, which throws UserError: Unsupported item { type: "unknown" }, so any tool usage breaks the session after the first turn. Please deserialize function_call_output into a proper FunctionCallResultItem before returning it from getItems.
1 parent 6eb1a57 commit 6346f3b

File tree

3 files changed

+172
-8
lines changed

3 files changed

+172
-8
lines changed

packages/agents-core/src/types/protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ export const OutputModelItem = z.discriminatedUnion('type', [
606606
HostedToolCallItem,
607607
FunctionCallItem,
608608
ComputerUseCallItem,
609+
FunctionCallResultItem,
609610
ReasoningItem,
610611
UnknownItem,
611612
]);

packages/agents-openai/src/openaiResponsesModel.ts

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ type ResponseFunctionCallOutputListItem =
5151
}
5252
| {
5353
type: 'input_image';
54-
image_url?: string;
55-
file_id?: string;
56-
detail?: 'low' | 'high' | 'auto';
54+
image_url?: string | null;
55+
file_id?: string | null;
56+
detail?: 'low' | 'high' | 'auto' | null;
5757
}
5858
| {
5959
type: 'input_file';
60-
file_data?: string;
61-
file_url?: string;
62-
file_id?: string;
63-
filename?: string;
60+
file_data?: string | null;
61+
file_url?: string | null;
62+
file_id?: string | null;
63+
filename?: string | null;
6464
};
6565

6666
type ExtendedFunctionCallOutput = Omit<
@@ -70,6 +70,13 @@ type ExtendedFunctionCallOutput = Omit<
7070
output: string | ResponseFunctionCallOutputListItem[];
7171
};
7272

73+
type ResponseOutputItemWithFunctionResult =
74+
| OpenAI.Responses.ResponseOutputItem
75+
| (OpenAI.Responses.ResponseFunctionToolCallOutputItem & {
76+
name?: string;
77+
function_name?: string;
78+
});
79+
7380
const HostedToolChoice = z.enum([
7481
'file_search',
7582
'web_search',
@@ -394,6 +401,73 @@ function convertStructuredOutputToRequestItem(
394401
);
395402
}
396403

404+
function convertResponseFunctionCallOutputItemToStructured(
405+
item: ResponseFunctionCallOutputListItem,
406+
): protocol.ToolCallStructuredOutput {
407+
if (item.type === 'input_text') {
408+
return {
409+
type: 'input_text',
410+
text: item.text,
411+
};
412+
}
413+
414+
if (item.type === 'input_image') {
415+
const structured: protocol.InputImage = { type: 'input_image' };
416+
417+
if (typeof item.image_url === 'string' && item.image_url.length > 0) {
418+
structured.image = item.image_url;
419+
} else if (typeof item.file_id === 'string' && item.file_id.length > 0) {
420+
structured.image = { id: item.file_id };
421+
}
422+
423+
if (item.detail) {
424+
structured.detail = item.detail;
425+
}
426+
427+
return structured;
428+
}
429+
430+
if (item.type === 'input_file') {
431+
const structured: protocol.InputFile = { type: 'input_file' };
432+
433+
if (typeof item.file_id === 'string' && item.file_id.length > 0) {
434+
structured.file = { id: item.file_id };
435+
} else if (typeof item.file_url === 'string' && item.file_url.length > 0) {
436+
structured.file = { url: item.file_url };
437+
} else if (
438+
typeof item.file_data === 'string' &&
439+
item.file_data.length > 0
440+
) {
441+
structured.file = item.file_data;
442+
}
443+
444+
if (item.filename) {
445+
structured.filename = item.filename;
446+
}
447+
448+
return structured;
449+
}
450+
451+
const exhaustive: never = item;
452+
throw new UserError(
453+
`Unsupported structured tool output: ${JSON.stringify(exhaustive)}`,
454+
);
455+
}
456+
457+
function convertFunctionCallOutputToProtocol(
458+
output: OpenAI.Responses.ResponseFunctionToolCallOutputItem['output'],
459+
): protocol.FunctionCallResultItem['output'] {
460+
if (typeof output === 'string') {
461+
return output;
462+
}
463+
464+
if (Array.isArray(output)) {
465+
return output.map(convertResponseFunctionCallOutputItemToStructured);
466+
}
467+
468+
return '';
469+
}
470+
397471
function normalizeLegacyFileFromOutput(value: Record<string, any>): {
398472
file?: protocol.InputFile['file'];
399473
filename?: string;
@@ -1090,7 +1164,7 @@ function convertToMessageContentItem(
10901164
}
10911165

10921166
function convertToOutputItem(
1093-
items: OpenAI.Responses.ResponseOutputItem[],
1167+
items: ResponseOutputItemWithFunctionResult[],
10941168
): protocol.OutputModelItem[] {
10951169
return items.map((item) => {
10961170
if (item.type === 'message') {
@@ -1137,6 +1211,28 @@ function convertToOutputItem(
11371211
providerData,
11381212
};
11391213
return output;
1214+
} else if (item.type === 'function_call_output') {
1215+
const {
1216+
call_id,
1217+
status,
1218+
output: rawOutput,
1219+
name: toolName,
1220+
function_name: functionName,
1221+
...providerData
1222+
} = item as OpenAI.Responses.ResponseFunctionToolCallOutputItem & {
1223+
name?: string;
1224+
function_name?: string;
1225+
};
1226+
const output: protocol.FunctionCallResultItem = {
1227+
type: 'function_call_result',
1228+
id: item.id,
1229+
callId: call_id,
1230+
name: toolName ?? functionName ?? call_id,
1231+
status: status ?? 'completed',
1232+
output: convertFunctionCallOutputToProtocol(rawOutput),
1233+
providerData,
1234+
};
1235+
return output;
11401236
} else if (item.type === 'computer_call') {
11411237
const { call_id, status, action, ...providerData } = item;
11421238
const output: protocol.ComputerUseCallItem = {
@@ -1204,6 +1300,7 @@ function convertToOutputItem(
12041300

12051301
return {
12061302
type: 'unknown',
1303+
id: item.id,
12071304
providerData: item,
12081305
};
12091306
});

packages/agents-openai/test/openaiResponsesModel.helpers.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,4 +512,70 @@ describe('convertToOutputItem', () => {
512512
] as any),
513513
).toThrow();
514514
});
515+
516+
it('converts function_call_output items into function_call_result entries', () => {
517+
const out = convertToOutputItem([
518+
{
519+
type: 'function_call_output',
520+
id: 'out-1',
521+
call_id: 'call-1',
522+
name: 'lookup',
523+
output: 'done',
524+
} as any,
525+
]);
526+
527+
expect(out[0]).toMatchObject({
528+
type: 'function_call_result',
529+
id: 'out-1',
530+
callId: 'call-1',
531+
name: 'lookup',
532+
output: 'done',
533+
status: 'completed',
534+
});
535+
});
536+
537+
it('converts structured function_call_output payloads into structured outputs', () => {
538+
const out = convertToOutputItem([
539+
{
540+
type: 'function_call_output',
541+
id: 'out-2',
542+
call_id: 'call-2',
543+
function_name: 'search',
544+
status: 'in_progress',
545+
output: [
546+
{ type: 'input_text', text: 'hello' },
547+
{
548+
type: 'input_image',
549+
image_url: 'https://example.com/img.png',
550+
detail: 'high',
551+
},
552+
{
553+
type: 'input_file',
554+
file_url: 'https://example.com/file.txt',
555+
filename: 'file.txt',
556+
},
557+
],
558+
} as any,
559+
]);
560+
561+
expect(out[0]).toMatchObject({
562+
type: 'function_call_result',
563+
callId: 'call-2',
564+
name: 'search',
565+
status: 'in_progress',
566+
output: [
567+
{ type: 'input_text', text: 'hello' },
568+
{
569+
type: 'input_image',
570+
image: 'https://example.com/img.png',
571+
detail: 'high',
572+
},
573+
{
574+
type: 'input_file',
575+
file: { url: 'https://example.com/file.txt' },
576+
filename: 'file.txt',
577+
},
578+
],
579+
});
580+
});
515581
});

0 commit comments

Comments
 (0)