@@ -560,3 +560,193 @@ func (m *mockStreamingLLMProvider) Stream(ctx context.Context, messages []Messag
560560 close (ch )
561561 return ch , nil
562562}
563+
564+ // mockStreamingTool implements StreamingTool for testing
565+ type mockStreamingTool struct {
566+ name string
567+ chunks []tools.ToolChunk
568+ }
569+
570+ func (m * mockStreamingTool ) Name () string {
571+ return m .name
572+ }
573+
574+ func (m * mockStreamingTool ) Description () string {
575+ return "A mock streaming tool"
576+ }
577+
578+ func (m * mockStreamingTool ) Schema () * tools.Schema {
579+ return & tools.Schema {
580+ Inputs : & tools.ParameterSchema {
581+ Type : "object" ,
582+ },
583+ Outputs : & tools.ParameterSchema {
584+ Type : "object" ,
585+ },
586+ }
587+ }
588+
589+ func (m * mockStreamingTool ) Execute (ctx context.Context , inputs map [string ]interface {}) (map [string ]interface {}, error ) {
590+ // For non-streaming execution, collect all chunks and return the final result
591+ ch , err := m .ExecuteStream (ctx , inputs )
592+ if err != nil {
593+ return nil , err
594+ }
595+
596+ var result map [string ]interface {}
597+ var execError error
598+ for chunk := range ch {
599+ if chunk .IsFinal {
600+ result = chunk .Result
601+ execError = chunk .Error
602+ }
603+ }
604+
605+ if execError != nil {
606+ return nil , execError
607+ }
608+ return result , nil
609+ }
610+
611+ func (m * mockStreamingTool ) ExecuteStream (ctx context.Context , inputs map [string ]interface {}) (<- chan tools.ToolChunk , error ) {
612+ ch := make (chan tools.ToolChunk , len (m .chunks ))
613+ go func () {
614+ defer close (ch )
615+ for _ , chunk := range m .chunks {
616+ ch <- chunk
617+ }
618+ }()
619+ return ch , nil
620+ }
621+
622+ func TestAgent_ToolStreamingExecution (t * testing.T ) {
623+ // Create a streaming tool that emits chunks
624+ streamingTool := & mockStreamingTool {
625+ name : "streaming-tool" ,
626+ chunks : []tools.ToolChunk {
627+ {
628+ Data : "Line 1\n " ,
629+ Stream : "stdout" ,
630+ },
631+ {
632+ Data : "Line 2\n " ,
633+ Stream : "stdout" ,
634+ },
635+ {
636+ Data : "Error message\n " ,
637+ Stream : "stderr" ,
638+ },
639+ {
640+ IsFinal : true ,
641+ Result : map [string ]interface {}{
642+ "exit_code" : 0 ,
643+ "duration" : 100 ,
644+ },
645+ },
646+ },
647+ }
648+
649+ registry := tools .NewRegistry ()
650+ if err := registry .Register (streamingTool ); err != nil {
651+ t .Fatalf ("Failed to register tool: %v" , err )
652+ }
653+
654+ llm := & mockLLMProvider {
655+ responses : []Response {
656+ {
657+ Content : "Using streaming tool" ,
658+ FinishReason : "tool_calls" ,
659+ ToolCalls : []ToolCall {
660+ {
661+ ID : "call-1" ,
662+ Name : "streaming-tool" ,
663+ Arguments : map [string ]interface {}{},
664+ },
665+ },
666+ Usage : TokenUsage {TotalTokens : 10 },
667+ },
668+ {
669+ Content : "Completed with streaming output" ,
670+ FinishReason : "stop" ,
671+ Usage : TokenUsage {TotalTokens : 10 },
672+ },
673+ },
674+ }
675+
676+ // Track events emitted via callback
677+ var capturedEvents []map [string ]interface {}
678+ agent := NewAgent (llm , registry ).WithEventCallback (func (eventType string , data interface {}) {
679+ if eventType == "tool.output" {
680+ if eventData , ok := data .(map [string ]interface {}); ok {
681+ capturedEvents = append (capturedEvents , eventData )
682+ }
683+ }
684+ })
685+
686+ ctx := context .Background ()
687+ result , err := agent .Run (ctx , "System" , "Task" )
688+ if err != nil {
689+ t .Fatalf ("Run() error = %v" , err )
690+ }
691+
692+ // Verify tool execution
693+ if len (result .ToolExecutions ) != 1 {
694+ t .Fatalf ("ToolExecutions count = %d, want 1" , len (result .ToolExecutions ))
695+ }
696+
697+ execution := result .ToolExecutions [0 ]
698+
699+ // Verify output chunks are captured in execution
700+ if len (execution .OutputChunks ) != 4 {
701+ t .Errorf ("OutputChunks count = %d, want 4" , len (execution .OutputChunks ))
702+ }
703+
704+ // Verify chunk content
705+ if execution .OutputChunks [0 ].Data != "Line 1\n " {
706+ t .Errorf ("Chunk 0 data = %q, want %q" , execution .OutputChunks [0 ].Data , "Line 1\n " )
707+ }
708+ if execution .OutputChunks [0 ].Stream != "stdout" {
709+ t .Errorf ("Chunk 0 stream = %q, want %q" , execution .OutputChunks [0 ].Stream , "stdout" )
710+ }
711+
712+ if execution .OutputChunks [2 ].Stream != "stderr" {
713+ t .Errorf ("Chunk 2 stream = %q, want %q" , execution .OutputChunks [2 ].Stream , "stderr" )
714+ }
715+
716+ // Verify final chunk
717+ if ! execution .OutputChunks [3 ].IsFinal {
718+ t .Error ("Last chunk should have IsFinal=true" )
719+ }
720+
721+ // Verify events were emitted
722+ if len (capturedEvents ) != 4 {
723+ t .Errorf ("Captured %d events, want 4" , len (capturedEvents ))
724+ }
725+
726+ // Verify event structure
727+ if len (capturedEvents ) > 0 {
728+ firstEvent := capturedEvents [0 ]
729+ if firstEvent ["tool_name" ] != "streaming-tool" {
730+ t .Errorf ("Event tool_name = %q, want %q" , firstEvent ["tool_name" ], "streaming-tool" )
731+ }
732+ if firstEvent ["tool_call_id" ] != "call-1" {
733+ t .Errorf ("Event tool_call_id = %q, want %q" , firstEvent ["tool_call_id" ], "call-1" )
734+ }
735+ if firstEvent ["data" ] != "Line 1\n " {
736+ t .Errorf ("Event data = %q, want %q" , firstEvent ["data" ], "Line 1\n " )
737+ }
738+ }
739+
740+ // Verify execution succeeded
741+ if ! execution .Success {
742+ t .Error ("Tool execution should have succeeded" )
743+ }
744+
745+ // Verify final result
746+ if execution .Outputs == nil {
747+ t .Error ("Execution outputs should not be nil" )
748+ }
749+ if exitCode , ok := execution .Outputs ["exit_code" ].(int ); ! ok || exitCode != 0 {
750+ t .Errorf ("Exit code = %v, want 0" , execution .Outputs ["exit_code" ])
751+ }
752+ }
0 commit comments