@@ -139,25 +139,29 @@ async def event_loop_cycle(
139139 )
140140 invocation_state ["event_loop_cycle_span" ] = cycle_span
141141
142- with trace_api .use_span (cycle_span , end_on_exit = True ):
143- # Skipping model invocation if in interrupt state as interrupts are currently only supported for tool calls.
144- if agent ._interrupt_state .activated :
145- stop_reason : StopReason = "tool_use"
146- message = agent ._interrupt_state .context ["tool_use_message" ]
147- # Skip model invocation if the latest message contains ToolUse
148- elif _has_tool_use_in_latest_message (agent .messages ):
149- stop_reason = "tool_use"
150- message = agent .messages [- 1 ]
151- else :
152- model_events = _handle_model_execution (
153- agent , cycle_span , cycle_trace , invocation_state , tracer , structured_output_context
154- )
155- async for model_event in model_events :
156- if not isinstance (model_event , ModelStopReason ):
157- yield model_event
142+ with trace_api .use_span (cycle_span , end_on_exit = False ):
143+ try :
144+ # Skipping model invocation if in interrupt state as interrupts are currently only supported for tool calls.
145+ if agent ._interrupt_state .activated :
146+ stop_reason : StopReason = "tool_use"
147+ message = agent ._interrupt_state .context ["tool_use_message" ]
148+ # Skip model invocation if the latest message contains ToolUse
149+ elif _has_tool_use_in_latest_message (agent .messages ):
150+ stop_reason = "tool_use"
151+ message = agent .messages [- 1 ]
152+ else :
153+ model_events = _handle_model_execution (
154+ agent , cycle_span , cycle_trace , invocation_state , tracer , structured_output_context
155+ )
156+ async for model_event in model_events :
157+ if not isinstance (model_event , ModelStopReason ):
158+ yield model_event
158159
159- stop_reason , message , * _ = model_event ["stop" ]
160- yield ModelMessageEvent (message = message )
160+ stop_reason , message , * _ = model_event ["stop" ]
161+ yield ModelMessageEvent (message = message )
162+ except Exception as e :
163+ tracer .end_span_with_error (cycle_span , str (e ), e )
164+ raise
161165
162166 try :
163167 if stop_reason == "max_tokens" :
@@ -196,42 +200,45 @@ async def event_loop_cycle(
196200
197201 # End the cycle and return results
198202 agent .event_loop_metrics .end_cycle (cycle_start_time , cycle_trace , attributes )
199- # Set attributes before span auto-closes
203+
204+ # Force structured output tool call if LLM didn't use it automatically
205+ if structured_output_context .is_enabled and stop_reason == "end_turn" :
206+ if structured_output_context .force_attempted :
207+ raise StructuredOutputException (
208+ "The model failed to invoke the structured output tool even after it was forced."
209+ )
210+ structured_output_context .set_forced_mode ()
211+ logger .debug ("Forcing structured output tool" )
212+ await agent ._append_messages (
213+ {"role" : "user" , "content" : [{"text" : structured_output_context .structured_output_prompt }]}
214+ )
215+
216+ tracer .end_event_loop_cycle_span (cycle_span , message )
217+ events = recurse_event_loop (
218+ agent = agent , invocation_state = invocation_state , structured_output_context = structured_output_context
219+ )
220+ async for typed_event in events :
221+ yield typed_event
222+ return
223+
200224 tracer .end_event_loop_cycle_span (cycle_span , message )
201- except EventLoopException :
202- # Don't yield or log the exception - we already did it when we
203- # raised the exception and we don't need that duplication.
225+ yield EventLoopStopEvent (stop_reason , message , agent .event_loop_metrics , invocation_state ["request_state" ])
226+ except (
227+ StructuredOutputException ,
228+ EventLoopException ,
229+ ContextWindowOverflowException ,
230+ MaxTokensReachedException ,
231+ ) as e :
232+ # These exceptions should bubble up directly rather than get wrapped in an EventLoopException
233+ tracer .end_span_with_error (cycle_span , str (e ), e )
204234 raise
205- except (ContextWindowOverflowException , MaxTokensReachedException ) as e :
206- # Special cased exceptions which we want to bubble up rather than get wrapped in an EventLoopException
207- raise e
208235 except Exception as e :
236+ tracer .end_span_with_error (cycle_span , str (e ), e )
209237 # Handle any other exceptions
210238 yield ForceStopEvent (reason = e )
211239 logger .exception ("cycle failed" )
212240 raise EventLoopException (e , invocation_state ["request_state" ]) from e
213241
214- # Force structured output tool call if LLM didn't use it automatically
215- if structured_output_context .is_enabled and stop_reason == "end_turn" :
216- if structured_output_context .force_attempted :
217- raise StructuredOutputException (
218- "The model failed to invoke the structured output tool even after it was forced."
219- )
220- structured_output_context .set_forced_mode ()
221- logger .debug ("Forcing structured output tool" )
222- await agent ._append_messages (
223- {"role" : "user" , "content" : [{"text" : structured_output_context .structured_output_prompt }]}
224- )
225-
226- events = recurse_event_loop (
227- agent = agent , invocation_state = invocation_state , structured_output_context = structured_output_context
228- )
229- async for typed_event in events :
230- yield typed_event
231- return
232-
233- yield EventLoopStopEvent (stop_reason , message , agent .event_loop_metrics , invocation_state ["request_state" ])
234-
235242
236243async def recurse_event_loop (
237244 agent : "Agent" ,
@@ -316,20 +323,21 @@ async def _handle_model_execution(
316323 system_prompt = agent .system_prompt ,
317324 system_prompt_content = agent ._system_prompt_content ,
318325 )
319- with trace_api .use_span (model_invoke_span , end_on_exit = True ):
320- await agent .hooks .invoke_callbacks_async (
321- BeforeModelCallEvent (
322- agent = agent ,
323- invocation_state = invocation_state ,
326+ with trace_api .use_span (model_invoke_span , end_on_exit = False ):
327+ try :
328+ await agent .hooks .invoke_callbacks_async (
329+ BeforeModelCallEvent (
330+ agent = agent ,
331+ invocation_state = invocation_state ,
332+ )
324333 )
325- )
326334
327- if structured_output_context .forced_mode :
328- tool_spec = structured_output_context .get_tool_spec ()
329- tool_specs = [tool_spec ] if tool_spec else []
330- else :
331- tool_specs = agent .tool_registry .get_all_tool_specs ()
332- try :
335+ if structured_output_context .forced_mode :
336+ tool_spec = structured_output_context .get_tool_spec ()
337+ tool_specs = [tool_spec ] if tool_spec else []
338+ else :
339+ tool_specs = agent .tool_registry .get_all_tool_specs ()
340+
333341 async for event in stream_messages (
334342 agent .model ,
335343 agent .system_prompt ,
@@ -363,17 +371,17 @@ async def _handle_model_execution(
363371 "stop_reason=<%s>, retry_requested=<True> | hook requested model retry" ,
364372 stop_reason ,
365373 )
374+ tracer .end_model_invoke_span (model_invoke_span , message , usage , metrics , stop_reason )
366375 continue # Retry the model call
367376
368377 if stop_reason == "max_tokens" :
369378 message = recover_message_on_max_tokens_reached (message )
370379
371- # Set attributes before span auto-closes
372380 tracer .end_model_invoke_span (model_invoke_span , message , usage , metrics , stop_reason )
373381 break # Success! Break out of retry loop
374382
375383 except Exception as e :
376- # Exception is automatically recorded by use_span with end_on_exit=True
384+ tracer . end_span_with_error ( model_invoke_span , str ( e ), e )
377385 after_model_call_event = AfterModelCallEvent (
378386 agent = agent ,
379387 invocation_state = invocation_state ,
@@ -541,7 +549,7 @@ async def _handle_tool_execution(
541549 interrupts ,
542550 structured_output = structured_output_result ,
543551 )
544- # Set attributes before span auto-closes (span is managed by use_span in event_loop_cycle)
552+ # End the cycle span before yielding the recursive cycle.
545553 if cycle_span :
546554 tracer .end_event_loop_cycle_span (span = cycle_span , message = message )
547555
@@ -559,7 +567,7 @@ async def _handle_tool_execution(
559567
560568 yield ToolResultMessageEvent (message = tool_result_message )
561569
562- # Set attributes before span auto-closes (span is managed by use_span in event_loop_cycle)
570+ # End the cycle span before yielding the recursive cycle.
563571 if cycle_span :
564572 tracer .end_event_loop_cycle_span (span = cycle_span , message = message , tool_result_message = tool_result_message )
565573
0 commit comments