@@ -178,19 +178,114 @@ export function getTracing(
178
178
return 'enabled_without_data' ;
179
179
}
180
180
181
+ function toAgentInputList (
182
+ originalInput : string | AgentInputItem [ ] ,
183
+ ) : AgentInputItem [ ] {
184
+ if ( typeof originalInput === 'string' ) {
185
+ return [ { type : 'message' , role : 'user' , content : originalInput } ] ;
186
+ }
187
+
188
+ return [ ...originalInput ] ;
189
+ }
190
+
191
+ /**
192
+ * Internal module for tracking the items in turns and ensuring that we don't send duplicate items.
193
+ * This logic is vital for properly handling the items to send during multiple turns
194
+ * when you use either `conversationId` or `previousResponseId`.
195
+ * Both scenarios expect an agent loop to send only the new items for each Responses API cal.
196
+ *
197
+ * see also: https://platform.openai.com/docs/guides/conversation-state?api-mode=responses
198
+ */
199
+ class ServerConversationTracker {
200
+ // Conversation ID:
201
+ // - https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#using-the-conversations-api
202
+ // - https://platform.openai.com/docs/api-reference/conversations/create
203
+ public conversationId ?: string ;
204
+
205
+ // Previous Response ID:
206
+ // https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#passing-context-from-the-previous-response
207
+ public previousResponseId ?: string ;
208
+
209
+ // Using this flag because WeakSet does not provides a way to check its size
210
+ private sentInitialInput = false ;
211
+ // The items already sent to the model; using WeakSet for memory efficiency
212
+ private sentItems = new WeakSet < object > ( ) ;
213
+ // The items received from the server; using WeakSet for memory efficiency
214
+ private serverItems = new WeakSet < object > ( ) ;
215
+
216
+ constructor ( {
217
+ conversationId,
218
+ previousResponseId,
219
+ } : {
220
+ conversationId ?: string ;
221
+ previousResponseId ?: string ;
222
+ } ) {
223
+ this . conversationId = conversationId ?? undefined ;
224
+ this . previousResponseId = previousResponseId ?? undefined ;
225
+ }
226
+
227
+ trackServerItems ( modelResponse : ModelResponse | undefined ) {
228
+ if ( ! modelResponse ) {
229
+ return ;
230
+ }
231
+ for ( const item of modelResponse . output ) {
232
+ if ( item && typeof item === 'object' ) {
233
+ this . serverItems . add ( item ) ;
234
+ }
235
+ }
236
+ if (
237
+ ! this . conversationId &&
238
+ this . previousResponseId !== undefined &&
239
+ modelResponse . responseId
240
+ ) {
241
+ this . previousResponseId = modelResponse . responseId ;
242
+ }
243
+ }
244
+
245
+ prepareInput (
246
+ originalInput : string | AgentInputItem [ ] ,
247
+ generatedItems : RunItem [ ] ,
248
+ ) : AgentInputItem [ ] {
249
+ const inputItems : AgentInputItem [ ] = [ ] ;
250
+
251
+ if ( ! this . sentInitialInput ) {
252
+ const initialItems = toAgentInputList ( originalInput ) ;
253
+ for ( const item of initialItems ) {
254
+ inputItems . push ( item ) ;
255
+ if ( item && typeof item === 'object' ) {
256
+ this . sentItems . add ( item ) ;
257
+ }
258
+ }
259
+ this . sentInitialInput = true ;
260
+ }
261
+
262
+ for ( const item of generatedItems ) {
263
+ if ( item . type === 'tool_approval_item' ) {
264
+ continue ;
265
+ }
266
+ const rawItem = item . rawItem ;
267
+ if ( ! rawItem || typeof rawItem !== 'object' ) {
268
+ continue ;
269
+ }
270
+ if ( this . sentItems . has ( rawItem ) || this . serverItems . has ( rawItem ) ) {
271
+ continue ;
272
+ }
273
+ inputItems . push ( rawItem as AgentInputItem ) ;
274
+ this . sentItems . add ( rawItem ) ;
275
+ }
276
+
277
+ return inputItems ;
278
+ }
279
+ }
280
+
181
281
export function getTurnInput (
182
282
originalInput : string | AgentInputItem [ ] ,
183
283
generatedItems : RunItem [ ] ,
184
284
) : AgentInputItem [ ] {
185
285
const rawItems = generatedItems
186
286
. filter ( ( item ) => item . type !== 'tool_approval_item' ) // don't include approval items to avoid double function calls
187
287
. map ( ( item ) => item . rawItem ) ;
188
-
189
- if ( typeof originalInput === 'string' ) {
190
- originalInput = [ { type : 'message' , role : 'user' , content : originalInput } ] ;
191
- }
192
-
193
- return [ ...originalInput , ...rawItems ] ;
288
+ return [ ...toAgentInputList ( originalInput ) , ...rawItems ] ;
194
289
}
195
290
196
291
/**
@@ -254,6 +349,14 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
254
349
options . maxTurns ?? DEFAULT_MAX_TURNS ,
255
350
) ;
256
351
352
+ const serverConversationTracker =
353
+ options . conversationId || options . previousResponseId
354
+ ? new ServerConversationTracker ( {
355
+ conversationId : options . conversationId ,
356
+ previousResponseId : options . previousResponseId ,
357
+ } )
358
+ : undefined ;
359
+
257
360
try {
258
361
while ( true ) {
259
362
const explictlyModelSet =
@@ -355,10 +458,12 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
355
458
await this . #runInputGuardrails( state ) ;
356
459
}
357
460
358
- const turnInput = getTurnInput (
359
- state . _originalInput ,
360
- state . _generatedItems ,
361
- ) ;
461
+ const turnInput = serverConversationTracker
462
+ ? serverConversationTracker . prepareInput (
463
+ state . _originalInput ,
464
+ state . _generatedItems ,
465
+ )
466
+ : getTurnInput ( state . _originalInput , state . _generatedItems ) ;
362
467
363
468
if ( state . _noActiveAgentRun ) {
364
469
state . _currentAgent . emit (
@@ -385,14 +490,21 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
385
490
state . _toolUseTracker ,
386
491
modelSettings ,
387
492
) ;
493
+ const previousResponseId =
494
+ serverConversationTracker ?. previousResponseId ??
495
+ options . previousResponseId ;
496
+ const conversationId =
497
+ serverConversationTracker ?. conversationId ??
498
+ options . conversationId ;
499
+
388
500
state . _lastTurnResponse = await model . getResponse ( {
389
501
systemInstructions : await state . _currentAgent . getSystemPrompt (
390
502
state . _context ,
391
503
) ,
392
504
prompt : await state . _currentAgent . getPrompt ( state . _context ) ,
393
505
input : turnInput ,
394
- previousResponseId : options . previousResponseId ,
395
- conversationId : options . conversationId ,
506
+ previousResponseId,
507
+ conversationId,
396
508
modelSettings,
397
509
tools : serializedTools ,
398
510
outputType : convertAgentOutputTypeToSerializable (
@@ -409,6 +521,10 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
409
521
state . _context . usage . add ( state . _lastTurnResponse . usage ) ;
410
522
state . _noActiveAgentRun = false ;
411
523
524
+ serverConversationTracker ?. trackServerItems (
525
+ state . _lastTurnResponse ,
526
+ ) ;
527
+
412
528
const processedResponse = processModelResponse (
413
529
state . _lastTurnResponse ,
414
530
state . _currentAgent ,
@@ -623,6 +739,14 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
623
739
result : StreamedRunResult < TContext , TAgent > ,
624
740
options : StreamRunOptions < TContext > ,
625
741
) : Promise < void > {
742
+ const serverConversationTracker =
743
+ options . conversationId || options . previousResponseId
744
+ ? new ServerConversationTracker ( {
745
+ conversationId : options . conversationId ,
746
+ previousResponseId : options . previousResponseId ,
747
+ } )
748
+ : undefined ;
749
+
626
750
try {
627
751
while ( true ) {
628
752
const currentAgent = result . state . _currentAgent ;
@@ -739,7 +863,12 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
739
863
modelSettings ,
740
864
) ;
741
865
742
- const turnInput = getTurnInput ( result . input , result . newItems ) ;
866
+ const turnInput = serverConversationTracker
867
+ ? serverConversationTracker . prepareInput (
868
+ result . input ,
869
+ result . newItems ,
870
+ )
871
+ : getTurnInput ( result . input , result . newItems ) ;
743
872
744
873
if ( result . state . _noActiveAgentRun ) {
745
874
currentAgent . emit (
@@ -752,14 +881,20 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
752
881
753
882
let finalResponse : ModelResponse | undefined = undefined ;
754
883
884
+ const previousResponseId =
885
+ serverConversationTracker ?. previousResponseId ??
886
+ options . previousResponseId ;
887
+ const conversationId =
888
+ serverConversationTracker ?. conversationId ?? options . conversationId ;
889
+
755
890
for await ( const event of model . getStreamedResponse ( {
756
891
systemInstructions : await currentAgent . getSystemPrompt (
757
892
result . state . _context ,
758
893
) ,
759
894
prompt : await currentAgent . getPrompt ( result . state . _context ) ,
760
895
input : turnInput ,
761
- previousResponseId : options . previousResponseId ,
762
- conversationId : options . conversationId ,
896
+ previousResponseId,
897
+ conversationId,
763
898
modelSettings,
764
899
tools : serializedTools ,
765
900
handoffs : serializedHandoffs ,
@@ -798,6 +933,7 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
798
933
}
799
934
800
935
result . state . _lastTurnResponse = finalResponse ;
936
+ serverConversationTracker ?. trackServerItems ( finalResponse ) ;
801
937
result . state . _modelResponses . push ( result . state . _lastTurnResponse ) ;
802
938
803
939
const processedResponse = processModelResponse (
0 commit comments