Skip to content

Commit 18265e1

Browse files
committed
fix(realtime): mark messages from output_item.done as completed (fallback when status missing)
1 parent 8287b5f commit 18265e1

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

.changeset/public-eels-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-realtime': patch
3+
---
4+
5+
Fix: ensure assistant message items from `response.output_item.done` preserve API status and default to `"completed"` when missing, so `history_updated` no longer stays `"in_progress"` after completion.

packages/agents-realtime/src/openaiRealtimeBase.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,10 @@ export abstract class OpenAIRealtimeBase
329329
type: parsed.item.type,
330330
role: parsed.item.role,
331331
content: parsed.item.content,
332-
status: 'in_progress',
332+
status:
333+
parsed.type === 'response.output_item.done'
334+
? (item.status ?? 'completed')
335+
: (item.status ?? 'in_progress')
333336
});
334337
this.emit('item_update', realtimeItem);
335338
return;

packages/agents-realtime/test/realtimeSession.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@openai/agents-core';
1212
import * as utils from '../src/utils';
1313
import type { TransportToolCallEvent } from '../src/transportLayerEvents';
14+
import { OpenAIRealtimeBase } from '../src/openaiRealtimeBase';
1415

1516
function createMessage(id: string, text: string): RealtimeItem {
1617
return {
@@ -316,4 +317,85 @@ describe('RealtimeSession', () => {
316317
expect(last.inputAudioFormat).toBe('g711_ulaw');
317318
expect(last.outputAudioFormat).toBe('g711_ulaw');
318319
});
320+
321+
it('defaults item status to completed for done output items without status', async () => {
322+
class TestTransport extends OpenAIRealtimeBase {
323+
status: 'connected' | 'disconnected' | 'connecting' | 'disconnecting' =
324+
'connected';
325+
connect = vi.fn(async () => {});
326+
sendEvent = vi.fn();
327+
mute = vi.fn();
328+
close = vi.fn();
329+
interrupt = vi.fn();
330+
get muted() {
331+
return false;
332+
}
333+
}
334+
const transport = new TestTransport();
335+
const agent = new RealtimeAgent({ name: 'A', handoffs: [] });
336+
const session = new RealtimeSession(agent, { transport });
337+
await session.connect({ apiKey: 'test' });
338+
const historyEvents: RealtimeItem[][] = [];
339+
session.on('history_updated', (h) => historyEvents.push([...h]));
340+
(transport as any)._onMessage({
341+
data: JSON.stringify({
342+
type: 'response.output_item.done',
343+
event_id: 'e',
344+
item: {
345+
id: 'm1',
346+
type: 'message',
347+
role: 'assistant',
348+
content: [{ type: 'text', text: 'hi' }],
349+
},
350+
output_index: 0,
351+
response_id: 'r1',
352+
}),
353+
});
354+
const latest = historyEvents.at(-1)!;
355+
const msg = latest.find(
356+
(i): i is Extract<RealtimeItem, { type: 'message'; role: 'assistant' }> =>
357+
i.type === 'message' && i.role === 'assistant' && (i as any).itemId === 'm1'
358+
);
359+
expect(msg).toBeDefined();
360+
expect(msg!.status).toBe('completed');
361+
});
362+
363+
it('preserves explicit completed status on done', async () => {
364+
class TestTransport extends OpenAIRealtimeBase {
365+
status: 'connected' | 'disconnected' | 'connecting' | 'disconnecting' = 'connected';
366+
connect = vi.fn(async () => {});
367+
sendEvent = vi.fn(); mute = vi.fn(); close = vi.fn(); interrupt = vi.fn();
368+
get muted() { return false; }
369+
}
370+
const transport = new TestTransport();
371+
const session = new RealtimeSession(new RealtimeAgent({ name: 'A', handoffs: [] }), { transport });
372+
await session.connect({ apiKey: 'test' });
373+
374+
const historyEvents: RealtimeItem[][] = [];
375+
session.on('history_updated', (h) => historyEvents.push([...h]));
376+
377+
(transport as any)._onMessage({
378+
data: JSON.stringify({
379+
type: 'response.output_item.done',
380+
event_id: 'e',
381+
item: {
382+
id: 'm2',
383+
type: 'message',
384+
role: 'assistant',
385+
status: 'completed',
386+
content: [{ type: 'text', text: 'hi again' }],
387+
},
388+
output_index: 0,
389+
response_id: 'r2',
390+
}),
391+
});
392+
393+
const latest = historyEvents.at(-1)!;
394+
const msg = latest.find(
395+
(i): i is Extract<RealtimeItem, { type: 'message'; role: 'assistant' }> =>
396+
i.type === 'message' && i.role === 'assistant' && (i as any).itemId === 'm2'
397+
);
398+
expect(msg).toBeDefined();
399+
expect(msg!.status).toBe('completed'); // ensure we didn't overwrite server status
319400
});
401+
});

0 commit comments

Comments
 (0)