@@ -521,16 +521,66 @@ describe('streamChatWithTools', () => {
521521 events . push ( event ) ;
522522 }
523523
524- // Should have tool-call event followed by text-delta + finish (no double call)
524+ // Should have tool-call + tool-result events followed by text-delta + finish
525525 const toolCallEvents = events . filter ( e => e . type === 'tool-call' ) ;
526526 expect ( toolCallEvents ) . toHaveLength ( 1 ) ;
527527 expect ( ( toolCallEvents [ 0 ] as any ) . toolName ) . toBe ( 'get_weather' ) ;
528528
529+ const toolResultEvents = events . filter ( e => e . type === 'tool-result' ) ;
530+ expect ( toolResultEvents ) . toHaveLength ( 1 ) ;
531+ expect ( ( toolResultEvents [ 0 ] as any ) . toolCallId ) . toBe ( 'call_1' ) ;
532+ expect ( ( toolResultEvents [ 0 ] as any ) . toolName ) . toBe ( 'get_weather' ) ;
533+
529534 const finishEvent = events . find ( e => e . type === 'finish' ) ;
530535 expect ( finishEvent ) . toBeDefined ( ) ;
531536 expect ( adapter . chat ) . toHaveBeenCalledTimes ( 2 ) ;
532537 } ) ;
533538
539+ it ( 'should yield tool-result events with tool output' , async ( ) => {
540+ const toolCall : ToolCallPart = {
541+ type : 'tool-call' ,
542+ toolCallId : 'call_weather' ,
543+ toolName : 'get_weather' ,
544+ input : { city : 'Paris' } ,
545+ } ;
546+
547+ let chatCallIndex = 0 ;
548+ const adapter : LLMAdapter = {
549+ name : 'mock-stream' ,
550+ chat : vi . fn ( async ( ) => {
551+ chatCallIndex ++ ;
552+ if ( chatCallIndex === 1 ) {
553+ return { content : '' , toolCalls : [ toolCall ] } ;
554+ }
555+ return { content : 'Paris is 22°C' } ;
556+ } ) ,
557+ complete : vi . fn ( async ( ) => ( { content : '' } ) ) ,
558+ } ;
559+
560+ const service = new AIService ( { adapter, logger : silentLogger , toolRegistry : registry } ) ;
561+ const events : TextStreamPart < ToolSet > [ ] = [ ] ;
562+ for await ( const event of service . streamChatWithTools (
563+ [ { role : 'user' , content : 'Weather in Paris?' } ] ,
564+ ) ) {
565+ events . push ( event ) ;
566+ }
567+
568+ // Verify the tool-result contains actual tool output
569+ const toolResultEvents = events . filter ( e => e . type === 'tool-result' ) ;
570+ expect ( toolResultEvents ) . toHaveLength ( 1 ) ;
571+ const toolResult = toolResultEvents [ 0 ] as any ;
572+ expect ( toolResult . toolCallId ) . toBe ( 'call_weather' ) ;
573+ expect ( toolResult . toolName ) . toBe ( 'get_weather' ) ;
574+ expect ( toolResult . output ) . toEqual ( { type : 'text' , value : JSON . stringify ( { temp : 22 , city : 'Paris' } ) } ) ;
575+
576+ // Verify order: tool-call comes before tool-result
577+ const toolCallIdx = events . findIndex ( e => e . type === 'tool-call' ) ;
578+ const toolResultIdx = events . findIndex ( e => e . type === 'tool-result' ) ;
579+ expect ( toolCallIdx ) . toBeGreaterThanOrEqual ( 0 ) ;
580+ expect ( toolResultIdx ) . toBeGreaterThanOrEqual ( 0 ) ;
581+ expect ( toolCallIdx ) . toBeLessThan ( toolResultIdx ) ;
582+ } ) ;
583+
534584 it ( 'should fall back to non-streaming when adapter has no streamChat' , async ( ) => {
535585 const adapter : LLMAdapter = {
536586 name : 'no-stream' ,
0 commit comments