@@ -357,6 +357,146 @@ async def recurse_event_loop(agent: "Agent", invocation_state: dict[str, Any]) -
357357 recursive_trace .end ()
358358
359359
360+ def _prepare_tools_for_execution (
361+ message : Message ,
362+ ) -> tuple [list [ToolUse ], list [ToolResult ], list [str ]]:
363+ """Prepare and validate tools from a message for execution.
364+
365+ Args:
366+ message: The message from the model that may contain tool use requests.
367+
368+ Returns:
369+ A tuple containing:
370+ - List of valid tool uses
371+ - List of tool results for invalid tools
372+ - List of invalid tool use IDs
373+ """
374+ tool_uses : list [ToolUse ] = []
375+ tool_results : list [ToolResult ] = []
376+ invalid_tool_use_ids : list [str ] = []
377+
378+ validate_and_prepare_tools (message , tool_uses , tool_results , invalid_tool_use_ids )
379+ valid_tool_uses = [tool_use for tool_use in tool_uses if tool_use .get ("toolUseId" ) not in invalid_tool_use_ids ]
380+
381+ return valid_tool_uses , tool_results , invalid_tool_use_ids
382+
383+
384+ def _create_tool_result_message (tool_results : list [ToolResult ]) -> Message :
385+ """Create a tool result message for conversation history.
386+
387+ Args:
388+ tool_results: List of tool results to include in the message.
389+
390+ Returns:
391+ A formatted message containing the tool results.
392+ """
393+ return {
394+ "role" : "user" ,
395+ "content" : [{"toolResult" : result } for result in tool_results ],
396+ }
397+
398+
399+ def _process_tool_result_message (
400+ agent : "Agent" ,
401+ tool_result_message : Message ,
402+ ) -> None :
403+ """Process and add a tool result message to the agent's conversation history.
404+
405+ Args:
406+ agent: The agent to add the message to.
407+ tool_result_message: The tool result message to process.
408+ """
409+ agent .messages .append (tool_result_message )
410+ agent .hooks .invoke_callbacks (MessageAddedEvent (agent = agent , message = tool_result_message ))
411+
412+
413+ def _cleanup_event_loop_cycle (
414+ cycle_span : Any ,
415+ cycle_start_time : float ,
416+ cycle_trace : Trace ,
417+ agent : "Agent" ,
418+ message : Message ,
419+ tool_result_message : Message ,
420+ ) -> None :
421+ """Clean up tracing and metrics for an event loop cycle.
422+
423+ Args:
424+ cycle_span: Span object for tracing the cycle.
425+ cycle_start_time: Start time of the current cycle.
426+ cycle_trace: Trace object for the current event loop cycle.
427+ agent: The agent for which the cycle is being cleaned up.
428+ message: The original message from the model.
429+ tool_result_message: The tool result message.
430+ """
431+ if cycle_span :
432+ tracer = get_tracer ()
433+ tracer .end_event_loop_cycle_span (
434+ span = cycle_span ,
435+ message = message ,
436+ tool_result_message = tool_result_message
437+ )
438+
439+
440+ async def _handle_structured_output (
441+ output_schema : "OutputSchema" ,
442+ invocation_state : dict [str , Any ],
443+ tool_uses : list [ToolUse ],
444+ tool_results : list [ToolResult ],
445+ stop_reason : StopReason ,
446+ message : Message ,
447+ agent : "Agent" ,
448+ cycle_start_time : float ,
449+ cycle_trace : Trace ,
450+ cycle_span : Any ,
451+ ) -> AsyncGenerator [tuple [TypedEvent , bool ], None ]:
452+ """Handle structured output processing and emit appropriate events.
453+
454+ Args:
455+ output_schema: The output schema to process.
456+ invocation_state: Current invocation state.
457+ tool_uses: List of tool uses.
458+ tool_results: List of tool results.
459+ stop_reason: The reason the model stopped generating.
460+ message: The original message from the model.
461+ agent: The agent processing the structured output.
462+ cycle_start_time: Start time of the current cycle.
463+ cycle_trace: Trace object for the current event loop cycle.
464+ cycle_span: Span object for tracing the cycle.
465+
466+ Yields:
467+ A tuple of (event, should_stop) where should_stop indicates if processing should halt.
468+ """
469+ from ..tools .structured_output_tool import _extract_structured_output_from_state , _cleanup_structured_outputs
470+
471+ structured_output_result = _extract_structured_output_from_state (
472+ invocation_state , tool_uses , output_schema .type .__name__
473+ )
474+
475+ # Cleanup any remaining structured outputs
476+ tool_use_ids = [str (tool_use .get ("toolUseId" , "" )) for tool_use in tool_uses ]
477+ _cleanup_structured_outputs (invocation_state , tool_use_ids )
478+
479+ if structured_output_result is not None :
480+ yield StructuredOutputEvent (structured_output = structured_output_result ), False
481+
482+ # Create and process tool result message
483+ tool_result_message = _create_tool_result_message (tool_results )
484+ _process_tool_result_message (agent , tool_result_message )
485+ yield ToolResultMessageEvent (message = tool_result_message ), False
486+
487+ # End the event loop cycle with structured output
488+ agent .event_loop_metrics .end_cycle (cycle_start_time , cycle_trace , {})
489+ _cleanup_event_loop_cycle (cycle_span , cycle_start_time , cycle_trace , agent , message , tool_result_message )
490+
491+ yield EventLoopStopEvent (
492+ stop_reason ,
493+ message ,
494+ agent .event_loop_metrics ,
495+ invocation_state ["request_state" ],
496+ structured_output = structured_output_result
497+ ), True
498+
499+
360500async def _handle_tool_execution (
361501 stop_reason : StopReason ,
362502 message : Message ,
@@ -385,92 +525,52 @@ async def _handle_tool_execution(
385525 - The updated event loop metrics,
386526 - The updated request state.
387527 """
388- tool_uses : list [ToolUse ] = []
389- tool_results : list [ToolResult ] = []
390- invalid_tool_use_ids : list [str ] = []
391-
392- validate_and_prepare_tools (message , tool_uses , tool_results , invalid_tool_use_ids )
393- tool_uses = [tool_use for tool_use in tool_uses if tool_use .get ("toolUseId" ) not in invalid_tool_use_ids ]
528+ # Prepare and validate tools
529+ tool_uses , tool_results , invalid_tool_use_ids = _prepare_tools_for_execution (message )
530+
394531 if not tool_uses :
395532 yield EventLoopStopEvent (stop_reason , message , agent .event_loop_metrics , invocation_state ["request_state" ], None )
396533 return
397534
398- # Check for structured output tool calls
399- output_schema : OutputSchema = invocation_state .get ("output_schema" )
400- structured_output_tool_names = set ()
401- if output_schema :
402- structured_output_tool_names .add (output_schema .type .__name__ )
403-
404- # Execute all tools (including structured output tools) through normal pipeline
535+ # Execute all tools through normal pipeline
405536 tool_events = agent .tool_executor ._execute (
406537 agent , tool_uses , tool_results , cycle_trace , cycle_span , invocation_state
407538 )
408539 async for tool_event in tool_events :
409540 yield tool_event
410541
411- # Check if any structured output tools were executed successfully
412- structured_output_result = None
413- if output_schema and structured_output_tool_names :
414- from ..tools .structured_output_tool import _extract_structured_output_from_state , _cleanup_structured_outputs
415-
416- structured_output_result = _extract_structured_output_from_state (
417- invocation_state , tool_uses , output_schema .type .__name__
418- )
419-
420- # Cleanup any remaining structured outputs
421- tool_use_ids = [str (tool_use .get ("toolUseId" , "" )) for tool_use in tool_uses ]
422- _cleanup_structured_outputs (invocation_state , tool_use_ids )
423-
424- # If we found structured output, emit the event and stop the event loop # TODO I think everything after the ` yield StructuredOutpu...` is extra...
425- if structured_output_result is not None :
426- yield StructuredOutputEvent (structured_output = structured_output_result )
427-
428- # Create tool result message for conversation history
429- tool_result_message : Message = {
430- "role" : "user" ,
431- "content" : [{"toolResult" : result } for result in tool_results ],
432- }
433-
434- agent .messages .append (tool_result_message )
435- agent .hooks .invoke_callbacks (MessageAddedEvent (agent = agent , message = tool_result_message ))
436- yield ToolResultMessageEvent (message = tool_result_message )
437-
438- # End the event loop cycle with structured output
439- agent .event_loop_metrics .end_cycle (cycle_start_time , cycle_trace , {})
440- if cycle_span :
441- tracer = get_tracer ()
442- tracer .end_event_loop_cycle_span (span = cycle_span , message = message , tool_result_message = tool_result_message )
443-
444- yield EventLoopStopEvent (
445- stop_reason ,
446- message ,
447- agent .event_loop_metrics ,
448- invocation_state ["request_state" ],
449- structured_output = structured_output_result
450- )
451- return
542+ # Handle structured output if present
543+ output_schema : "OutputSchema" = invocation_state .get ("output_schema" )
544+ if output_schema :
545+ structured_output_tool_names = {output_schema .type .__name__ }
546+ if structured_output_tool_names : # Only proceed if we have structured output tool names
547+ async for event , should_stop in _handle_structured_output (
548+ output_schema , invocation_state , tool_uses , tool_results ,
549+ stop_reason , message , agent , cycle_start_time , cycle_trace , cycle_span
550+ ):
551+ yield event
552+ # Check if we should stop processing
553+ if should_stop :
554+ return
452555
453556 # Store parent cycle ID for the next cycle
454557 invocation_state ["event_loop_parent_cycle_id" ] = invocation_state ["event_loop_cycle_id" ]
455558
456- tool_result_message : Message = {
457- "role" : "user" ,
458- "content" : [{"toolResult" : result } for result in tool_results ],
459- }
460-
461- agent .messages .append (tool_result_message )
462- agent .hooks .invoke_callbacks (MessageAddedEvent (agent = agent , message = tool_result_message ))
559+ # Create and process tool result message
560+ tool_result_message = _create_tool_result_message (tool_results )
561+ _process_tool_result_message (agent , tool_result_message )
463562 yield ToolResultMessageEvent (message = tool_result_message )
464563
465- if cycle_span :
466- tracer = get_tracer ()
467- tracer .end_event_loop_cycle_span (span = cycle_span , message = message , tool_result_message = tool_result_message )
564+ # Cleanup tracing
565+ _cleanup_event_loop_cycle (cycle_span , cycle_start_time , cycle_trace , agent , message , tool_result_message )
468566
567+ # Check if we should stop the event loop
469568 if invocation_state ["request_state" ].get ("stop_event_loop" , False ):
470569 agent .event_loop_metrics .end_cycle (cycle_start_time , cycle_trace )
471570 yield EventLoopStopEvent (stop_reason , message , agent .event_loop_metrics , invocation_state ["request_state" ], None )
472571 return
473572
573+ # Continue with recursive event loop processing
474574 events = recurse_event_loop (agent = agent , invocation_state = invocation_state )
475575 async for event in events :
476576 yield event
0 commit comments