1818
1919import logging
2020import timeit
21+ import uuid
22+ from dataclasses import dataclass , field
2123from functools import wraps
2224from typing import Any , AsyncGenerator
2325
26+ from opentelemetry .context import get_current as _get_current_context
27+ from opentelemetry .util .genai ._extended_common .common_types import (
28+ ReactStepInvocation ,
29+ )
2430from opentelemetry .util .genai .extended_handler import ExtendedTelemetryHandler
2531from opentelemetry .util .genai .types import Error , LLMInvocation
2632
3440
3541logger = logging .getLogger (__name__ )
3642
43+ _REACT_STEP_HOOK_PREFIX = "otel_react_step"
44+
45+
46+ def _is_react_agent (agent_instance : Any ) -> bool :
47+ """Check if an agent instance is a ReAct agent by duck-typing."""
48+ return hasattr (agent_instance , "_instance_pre_reasoning_hooks" )
49+
50+
51+ @dataclass
52+ class _ReactStepState :
53+ """Per-agent-call state for React step span lifecycle."""
54+
55+ hook_name : str = field (
56+ default_factory = lambda : f"{ _REACT_STEP_HOOK_PREFIX } _{ uuid .uuid4 ().hex [:8 ]} "
57+ )
58+ react_round : int = 0
59+ active_step : ReactStepInvocation | None = None
60+ original_context : Any = field (default = None )
61+ pending_acting_count : int = 0
62+
63+
64+ def _make_pre_reasoning_hook (
65+ handler : ExtendedTelemetryHandler ,
66+ ) -> Any :
67+ """Create a pre_reasoning hook that opens a new React step span.
68+
69+ Also closes any leftover step from a previous iteration as a fallback
70+ (normal path closes via post_acting).
71+ """
72+
73+ def hook (agent_self : Any , kwargs : dict ) -> None :
74+ state : _ReactStepState | None = getattr (
75+ agent_self , "_react_step_state" , None
76+ )
77+ if state is None :
78+ return None
79+
80+ if state .active_step :
81+ state .active_step .finish_reason = "tool_calls"
82+ handler .stop_react_step (state .active_step )
83+ state .active_step = None
84+
85+ state .react_round += 1
86+ inv = ReactStepInvocation (round = state .react_round )
87+ handler .start_react_step (inv , context = state .original_context )
88+ state .active_step = inv
89+ state .pending_acting_count = 0
90+ return None
91+
92+ return hook
93+
94+
95+ def _make_post_reasoning_hook (
96+ handler : ExtendedTelemetryHandler ,
97+ ) -> Any :
98+ """Create a post_reasoning hook that counts tool_use blocks
99+ to initialize the pending_acting_count for the current step."""
100+
101+ def hook (agent_self : Any , kwargs : dict , output : Any ) -> None :
102+ state : _ReactStepState | None = getattr (
103+ agent_self , "_react_step_state" , None
104+ )
105+ if state is None or output is None :
106+ return None
107+
108+ tool_blocks = (
109+ output .get_content_blocks ("tool_use" )
110+ if hasattr (output , "get_content_blocks" )
111+ else []
112+ )
113+ state .pending_acting_count = len (tool_blocks )
114+ return None
115+
116+ return hook
117+
118+
119+ def _make_post_acting_hook (
120+ handler : ExtendedTelemetryHandler ,
121+ ) -> Any :
122+ """Create a post_acting hook that decrements pending_acting_count
123+ and closes the step span when all acting calls are done."""
124+
125+ def hook (agent_self : Any , kwargs : dict , output : Any ) -> None :
126+ state : _ReactStepState | None = getattr (
127+ agent_self , "_react_step_state" , None
128+ )
129+ if state is None or state .active_step is None :
130+ return None
131+
132+ state .pending_acting_count -= 1
133+ if state .pending_acting_count <= 0 :
134+ state .active_step .finish_reason = "tool_calls"
135+ handler .stop_react_step (state .active_step )
136+ state .active_step = None
137+ return None
138+
139+ return hook
140+
141+
142+ def _register_react_hooks (
143+ agent : Any , state : _ReactStepState , handler : ExtendedTelemetryHandler
144+ ) -> None :
145+ """Register React step tracking hooks on an agent instance."""
146+ agent .register_instance_hook (
147+ "pre_reasoning" ,
148+ state .hook_name ,
149+ _make_pre_reasoning_hook (handler ),
150+ )
151+ agent .register_instance_hook (
152+ "post_reasoning" ,
153+ state .hook_name ,
154+ _make_post_reasoning_hook (handler ),
155+ )
156+ agent .register_instance_hook (
157+ "post_acting" ,
158+ state .hook_name ,
159+ _make_post_acting_hook (handler ),
160+ )
161+
162+
163+ def _remove_react_hooks (agent : Any , state : _ReactStepState ) -> None :
164+ """Remove React step tracking hooks from an agent instance."""
165+ for hook_type in ("pre_reasoning" , "post_reasoning" , "post_acting" ):
166+ try :
167+ agent .remove_instance_hook (hook_type , state .hook_name )
168+ except (ValueError , KeyError ):
169+ pass
170+
37171
38172class AgentScopeChatModelWrapper :
39173 """Wrapper for ChatModelBase that hijacks __init__ to replace __call__."""
@@ -228,11 +362,25 @@ async def async_wrapped_call(
228362 function_name = f"{ call_self .__class__ .__name__ } .__call__"
229363 invocation .attributes ["rpc" ] = function_name
230364
365+ is_react = _is_react_agent (call_self )
366+ state : _ReactStepState | None = None
367+ if is_react :
368+ state = _ReactStepState (
369+ original_context = _get_current_context (),
370+ )
371+ call_self ._react_step_state = state
372+ _register_react_hooks (call_self , state , self ._handler )
373+
231374 try :
232375 result = await original_call (
233376 call_self , * call_args , ** call_kwargs
234377 )
235378
379+ if is_react and state and state .active_step :
380+ state .active_step .finish_reason = "stop"
381+ self ._handler .stop_react_step (state .active_step )
382+ state .active_step = None
383+
236384 invocation .output_messages = (
237385 convert_agent_response_to_output_messages (result )
238386 )
@@ -244,11 +392,24 @@ async def async_wrapped_call(
244392 return result
245393
246394 except Exception as e :
395+ if is_react and state and state .active_step :
396+ self ._handler .fail_react_step (
397+ state .active_step ,
398+ Error (message = str (e ), type = type (e )),
399+ )
400+ state .active_step = None
401+
247402 self ._handler .fail_invoke_agent (
248403 invocation , Error (message = str (e ), type = type (e ))
249404 )
250405 raise
251406
407+ finally :
408+ if is_react and state :
409+ _remove_react_hooks (call_self , state )
410+ if hasattr (call_self , "_react_step_state" ):
411+ del call_self ._react_step_state
412+
252413 except Exception as e :
253414 logger .exception ("Error in agent instrumentation: %s" , e )
254415 return await original_call (
0 commit comments