@@ -151,6 +151,34 @@ def add_call(
151151 metrics_client .tool_error_count .add (1 , attributes = attributes )
152152
153153
154+ @dataclass
155+ class EventLoopCycleMetric :
156+ """Aggregated metrics for a single event loop cycle.
157+
158+ Attributes:
159+ event_loop_cycle_id: Current eventLoop cycle id.
160+ usage: Total token usage for the entire cycle (succeeded model invocation, excluding tool invocations).
161+ """
162+
163+ event_loop_cycle_id : str
164+ usage : Usage
165+
166+
167+ @dataclass
168+ class AgentInvocation :
169+ """Metrics for a single agent invocation.
170+
171+ AgentInvocation contains all the event loop cycles and accumulated token usage for that invocation.
172+
173+ Attributes:
174+ cycles: List of event loop cycles that occurred during this invocation.
175+ usage: Accumulated token usage for this invocation across all cycles.
176+ """
177+
178+ cycles : list [EventLoopCycleMetric ] = field (default_factory = list )
179+ usage : Usage = field (default_factory = lambda : Usage (inputTokens = 0 , outputTokens = 0 , totalTokens = 0 ))
180+
181+
154182@dataclass
155183class EventLoopMetrics :
156184 """Aggregated metrics for an event loop's execution.
@@ -159,15 +187,17 @@ class EventLoopMetrics:
159187 cycle_count: Number of event loop cycles executed.
160188 tool_metrics: Metrics for each tool used, keyed by tool name.
161189 cycle_durations: List of durations for each cycle in seconds.
190+ agent_invocations: Agent invocation metrics containing cycles and usage data.
162191 traces: List of execution traces.
163- accumulated_usage: Accumulated token usage across all model invocations.
192+ accumulated_usage: Accumulated token usage across all model invocations (across all requests) .
164193 accumulated_metrics: Accumulated performance metrics across all model invocations.
165194 """
166195
167196 cycle_count : int = 0
168- tool_metrics : Dict [str , ToolMetrics ] = field (default_factory = dict )
169- cycle_durations : List [float ] = field (default_factory = list )
170- traces : List [Trace ] = field (default_factory = list )
197+ tool_metrics : dict [str , ToolMetrics ] = field (default_factory = dict )
198+ cycle_durations : list [float ] = field (default_factory = list )
199+ agent_invocations : list [AgentInvocation ] = field (default_factory = list )
200+ traces : list [Trace ] = field (default_factory = list )
171201 accumulated_usage : Usage = field (default_factory = lambda : Usage (inputTokens = 0 , outputTokens = 0 , totalTokens = 0 ))
172202 accumulated_metrics : Metrics = field (default_factory = lambda : Metrics (latencyMs = 0 ))
173203
@@ -176,14 +206,23 @@ def _metrics_client(self) -> "MetricsClient":
176206 """Get the singleton MetricsClient instance."""
177207 return MetricsClient ()
178208
209+ @property
210+ def current_agent_invocation (self ) -> Optional [AgentInvocation ]:
211+ """Get the current (most recent) agent invocation.
212+
213+ Returns:
214+ The most recent AgentInvocation, or None if no invocations exist.
215+ """
216+ return self .agent_invocations [- 1 ] if self .agent_invocations else None
217+
179218 def start_cycle (
180219 self ,
181- attributes : Optional [ Dict [str , Any ]] = None ,
220+ attributes : Dict [str , Any ],
182221 ) -> Tuple [float , Trace ]:
183222 """Start a new event loop cycle and create a trace for it.
184223
185224 Args:
186- attributes: attributes of the metrics.
225+ attributes: attributes of the metrics, including event_loop_cycle_id .
187226
188227 Returns:
189228 A tuple containing the start time and the cycle trace object.
@@ -194,6 +233,18 @@ def start_cycle(
194233 start_time = time .time ()
195234 cycle_trace = Trace (f"Cycle { self .cycle_count } " , start_time = start_time )
196235 self .traces .append (cycle_trace )
236+
237+ # Ensure there's at least one agent invocation
238+ if not self .agent_invocations :
239+ self .agent_invocations .append (AgentInvocation ())
240+
241+ self .agent_invocations [- 1 ].cycles .append (
242+ EventLoopCycleMetric (
243+ event_loop_cycle_id = attributes ["event_loop_cycle_id" ],
244+ usage = Usage (inputTokens = 0 , outputTokens = 0 , totalTokens = 0 ),
245+ )
246+ )
247+
197248 return start_time , cycle_trace
198249
199250 def end_cycle (self , start_time : float , cycle_trace : Trace , attributes : Optional [Dict [str , Any ]] = None ) -> None :
@@ -252,32 +303,57 @@ def add_tool_usage(
252303 )
253304 tool_trace .end ()
254305
306+ def _accumulate_usage (self , target : Usage , source : Usage ) -> None :
307+ """Helper method to accumulate usage from source to target.
308+
309+ Args:
310+ target: The Usage object to accumulate into.
311+ source: The Usage object to accumulate from.
312+ """
313+ target ["inputTokens" ] += source ["inputTokens" ]
314+ target ["outputTokens" ] += source ["outputTokens" ]
315+ target ["totalTokens" ] += source ["totalTokens" ]
316+
317+ if "cacheReadInputTokens" in source :
318+ target ["cacheReadInputTokens" ] = target .get ("cacheReadInputTokens" , 0 ) + source ["cacheReadInputTokens" ]
319+
320+ if "cacheWriteInputTokens" in source :
321+ target ["cacheWriteInputTokens" ] = target .get ("cacheWriteInputTokens" , 0 ) + source ["cacheWriteInputTokens" ]
322+
255323 def update_usage (self , usage : Usage ) -> None :
256324 """Update the accumulated token usage with new usage data.
257325
258326 Args:
259327 usage: The usage data to add to the accumulated totals.
260328 """
329+ # Record metrics to OpenTelemetry
261330 self ._metrics_client .event_loop_input_tokens .record (usage ["inputTokens" ])
262331 self ._metrics_client .event_loop_output_tokens .record (usage ["outputTokens" ])
263- self .accumulated_usage ["inputTokens" ] += usage ["inputTokens" ]
264- self .accumulated_usage ["outputTokens" ] += usage ["outputTokens" ]
265- self .accumulated_usage ["totalTokens" ] += usage ["totalTokens" ]
266332
267- # Handle optional cached token metrics
333+ # Handle optional cached token metrics for OpenTelemetry
268334 if "cacheReadInputTokens" in usage :
269- cache_read_tokens = usage ["cacheReadInputTokens" ]
270- self ._metrics_client .event_loop_cache_read_input_tokens .record (cache_read_tokens )
271- self .accumulated_usage ["cacheReadInputTokens" ] = (
272- self .accumulated_usage .get ("cacheReadInputTokens" , 0 ) + cache_read_tokens
273- )
274-
335+ self ._metrics_client .event_loop_cache_read_input_tokens .record (usage ["cacheReadInputTokens" ])
275336 if "cacheWriteInputTokens" in usage :
276- cache_write_tokens = usage ["cacheWriteInputTokens" ]
277- self ._metrics_client .event_loop_cache_write_input_tokens .record (cache_write_tokens )
278- self .accumulated_usage ["cacheWriteInputTokens" ] = (
279- self .accumulated_usage .get ("cacheWriteInputTokens" , 0 ) + cache_write_tokens
280- )
337+ self ._metrics_client .event_loop_cache_write_input_tokens .record (usage ["cacheWriteInputTokens" ])
338+
339+ self ._accumulate_usage (self .accumulated_usage , usage )
340+
341+ if not self .agent_invocations :
342+ self .agent_invocations .append (AgentInvocation ())
343+
344+ self ._accumulate_usage (self .agent_invocations [- 1 ].usage , usage )
345+
346+ if self .agent_invocations [- 1 ].cycles :
347+ current_cycle = self .agent_invocations [- 1 ].cycles [- 1 ]
348+ self ._accumulate_usage (current_cycle .usage , usage )
349+
350+ def reset_usage_metrics (self ) -> None :
351+ """Start a new agent invocation by creating a new AgentInvocation.
352+
353+ This should be called at the start of a new request to begin tracking
354+ a new agent invocation with fresh usage and cycle data.
355+ """
356+ self .agent_invocations .append (AgentInvocation ())
281357
282358 def update_metrics (self , metrics : Metrics ) -> None :
283359 """Update the accumulated performance metrics with new metrics data.
@@ -322,6 +398,16 @@ def get_summary(self) -> Dict[str, Any]:
322398 "traces" : [trace .to_dict () for trace in self .traces ],
323399 "accumulated_usage" : self .accumulated_usage ,
324400 "accumulated_metrics" : self .accumulated_metrics ,
401+ "agent_invocations" : [
402+ {
403+ "usage" : invocation .usage ,
404+ "cycles" : [
405+ {"event_loop_cycle_id" : cycle .event_loop_cycle_id , "usage" : cycle .usage }
406+ for cycle in invocation .cycles
407+ ],
408+ }
409+ for invocation in self .agent_invocations
410+ ],
325411 }
326412 return summary
327413
0 commit comments