@@ -685,22 +685,29 @@ async def stream_to_final(
685
685
s : models .StreamedResponse ,
686
686
) -> FinalResult [models .StreamedResponse ] | None :
687
687
result_schema = graph_ctx .deps .result_schema
688
- parts_seen : list [_messages .ModelResponsePart ] = []
689
- has_tool_call = False
688
+ has_seen_empty_tool_call = False
690
689
async for maybe_part_event in streamed_response :
691
690
if isinstance (maybe_part_event , _messages .PartStartEvent ):
692
691
new_part = maybe_part_event .part
693
- parts_seen .append (new_part )
694
- if isinstance (new_part , _messages .ToolCallPart ) and result_schema :
695
- has_tool_call = True
692
+ if isinstance (new_part , _messages .TextPart ):
693
+ if _agent_graph .allow_text_result (result_schema ):
694
+ # Return final result for text parts (even empty ones)
695
+ return FinalResult (s , None , None )
696
+ elif isinstance (new_part , _messages .ToolCallPart ) and result_schema :
697
+ # Skip empty tool calls from Ollama
698
+ if not new_part .has_content ():
699
+ has_seen_empty_tool_call = True
700
+ continue
701
+
702
+ # For non-empty tool calls, find tools that match and return final result
696
703
for call , _ in result_schema .find_tool ([new_part ]):
697
704
return FinalResult (s , call .tool_name , call .tool_call_id )
698
- # Only check for final result after seeing all parts and no tool calls
699
- if not has_tool_call and len ( parts_seen ) > 0 :
700
- # For text-only responses, we need all parts to be TextPart
701
- if all ( isinstance ( p , _messages . TextPart ) for p in parts_seen ) :
702
- if _agent_graph . allow_text_result ( result_schema ):
703
- return FinalResult ( s , None , None )
705
+
706
+ # If we've seen empty tool calls but no other parts returned a result,
707
+ # return None to keep streaming
708
+ if has_seen_empty_tool_call :
709
+ return None
710
+
704
711
return None
705
712
706
713
final_result_details = await stream_to_final (streamed_response )
0 commit comments