@@ -12,6 +12,7 @@ import type {
1212 ToolStreamEvent ,
1313 TurnContext ,
1414} from './tool-types.js' ;
15+ import { ToolEventBroadcaster } from './tool-event-broadcaster.js' ;
1516
1617import { betaResponsesSend } from '../funcs/betaResponsesSend.js' ;
1718import {
@@ -106,7 +107,11 @@ export class ModelResult<TTools extends readonly Tool[]> {
106107 private initPromise : Promise < void > | null = null ;
107108 private toolExecutionPromise : Promise < void > | null = null ;
108109 private finalResponse : models . OpenResponsesNonStreamingResponse | null = null ;
109- private preliminaryResults : Map < string , unknown [ ] > = new Map ( ) ;
110+ private toolEventBroadcaster : ToolEventBroadcaster < {
111+ type : 'preliminary_result' ;
112+ toolCallId : string ;
113+ result : InferToolEventsUnion < TTools > ;
114+ } > | null = null ;
110115 private allToolExecutionRounds : Array < {
111116 round : number ;
112117 toolCalls : ParsedToolCall < Tool > [ ] ;
@@ -163,8 +168,7 @@ export class ModelResult<TTools extends readonly Tool[]> {
163168 // Already resolved, extract non-function fields
164169 // Since request is CallModelInput, we need to filter out stopWhen
165170 // Note: tools are already in API format at this point (converted in callModel())
166- // eslint-disable-next-line @typescript-eslint/no-unused-vars
167- const { stopWhen, ...rest } = this . options . request ;
171+ const { stopWhen : _ , ...rest } = this . options . request ;
168172 // Cast to ResolvedCallModelInput - we know it's resolved if hasAsyncFunctions returned false
169173 baseRequest = rest as ResolvedCallModelInput ;
170174 }
@@ -325,12 +329,18 @@ export class ModelResult<TTools extends readonly Tool[]> {
325329 continue ;
326330 }
327331
328- const result = await executeTool ( tool , toolCall , turnContext ) ;
332+ // Create callback for real-time preliminary results
333+ const onPreliminaryResult = this . toolEventBroadcaster
334+ ? ( callId : string , resultValue : unknown ) => {
335+ this . toolEventBroadcaster ! . push ( {
336+ type : 'preliminary_result' as const ,
337+ toolCallId : callId ,
338+ result : resultValue as InferToolEventsUnion < TTools > ,
339+ } ) ;
340+ }
341+ : undefined ;
329342
330- // Store preliminary results
331- if ( result . preliminaryResults && result . preliminaryResults . length > 0 ) {
332- this . preliminaryResults . set ( toolCall . id , result . preliminaryResults ) ;
333- }
343+ const result = await executeTool ( tool , toolCall , turnContext , onPreliminaryResult ) ;
334344
335345 toolResults . push ( {
336346 type : 'function_call_output' as const ,
@@ -480,7 +490,7 @@ export class ModelResult<TTools extends readonly Tool[]> {
480490 /**
481491 * Stream all response events as they arrive.
482492 * Multiple consumers can iterate over this stream concurrently.
483- * Includes preliminary tool result events after tool execution .
493+ * Preliminary tool results are streamed in REAL-TIME as generator tools yield .
484494 */
485495 getFullResponsesStream ( ) : AsyncIterableIterator < ResponseStreamEvent < InferToolEventsUnion < TTools > > > {
486496 return async function * ( this : ModelResult < TTools > ) {
@@ -489,27 +499,34 @@ export class ModelResult<TTools extends readonly Tool[]> {
489499 throw new Error ( 'Stream not initialized' ) ;
490500 }
491501
502+ // Create broadcaster for real-time tool events
503+ this . toolEventBroadcaster = new ToolEventBroadcaster ( ) ;
504+ const toolEventConsumer = this . toolEventBroadcaster . createConsumer ( ) ;
505+
506+ // Start tool execution in background (completes broadcaster when done)
507+ const executionPromise = this . executeToolsIfNeeded ( ) . finally ( ( ) => {
508+ this . toolEventBroadcaster ?. complete ( ) ;
509+ } ) ;
510+
492511 const consumer = this . reusableStream . createConsumer ( ) ;
493512
494- // Yield original events directly
513+ // Yield original API events
495514 for await ( const event of consumer ) {
496515 yield event ;
497516 }
498517
499- // After stream completes, check if tools were executed and emit preliminary results
500- await this . executeToolsIfNeeded ( ) ;
501-
502- // Emit all preliminary results as new event types
503- for ( const [ toolCallId , results ] of this . preliminaryResults ) {
504- for ( const result of results ) {
505- yield {
506- type : 'tool.preliminary_result' as const ,
507- toolCallId,
508- result : result as InferToolEventsUnion < TTools > ,
509- timestamp : Date . now ( ) ,
510- } ;
511- }
518+ // Yield tool preliminary results as they arrive (real-time!)
519+ for await ( const event of toolEventConsumer ) {
520+ yield {
521+ type : 'tool.preliminary_result' as const ,
522+ toolCallId : event . toolCallId ,
523+ result : event . result ,
524+ timestamp : Date . now ( ) ,
525+ } ;
512526 }
527+
528+ // Ensure execution completed (handles errors)
529+ await executionPromise ;
513530 } . call ( this ) ;
514531 }
515532
@@ -587,7 +604,7 @@ export class ModelResult<TTools extends readonly Tool[]> {
587604
588605 /**
589606 * Stream tool call argument deltas and preliminary results.
590- * This filters the full event stream to yield:
607+ * Preliminary results are streamed in REAL-TIME as generator tools yield.
591608 * - Tool call argument deltas as { type: "delta", content: string }
592609 * - Preliminary results as { type: "preliminary_result", toolCallId, result }
593610 */
@@ -598,27 +615,30 @@ export class ModelResult<TTools extends readonly Tool[]> {
598615 throw new Error ( 'Stream not initialized' ) ;
599616 }
600617
601- // Yield tool deltas as structured events
618+ // Create broadcaster for real-time tool events
619+ this . toolEventBroadcaster = new ToolEventBroadcaster ( ) ;
620+ const toolEventConsumer = this . toolEventBroadcaster . createConsumer ( ) ;
621+
622+ // Start tool execution in background (completes broadcaster when done)
623+ const executionPromise = this . executeToolsIfNeeded ( ) . finally ( ( ) => {
624+ this . toolEventBroadcaster ?. complete ( ) ;
625+ } ) ;
626+
627+ // Yield tool deltas from API stream
602628 for await ( const delta of extractToolDeltas ( this . reusableStream ) ) {
603629 yield {
604630 type : 'delta' as const ,
605631 content : delta ,
606632 } ;
607633 }
608634
609- // After stream completes, check if tools were executed and emit preliminary results
610- await this . executeToolsIfNeeded ( ) ;
611-
612- // Emit all preliminary results
613- for ( const [ toolCallId , results ] of this . preliminaryResults ) {
614- for ( const result of results ) {
615- yield {
616- type : 'preliminary_result' as const ,
617- toolCallId,
618- result : result as InferToolEventsUnion < TTools > ,
619- } ;
620- }
635+ // Yield tool events as they arrive (real-time!)
636+ for await ( const event of toolEventConsumer ) {
637+ yield event ;
621638 }
639+
640+ // Ensure execution completed (handles errors)
641+ await executionPromise ;
622642 } . call ( this ) ;
623643 }
624644
0 commit comments