@@ -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 latest_agent_invocation (self ) -> Optional [AgentInvocation ]:
211+ """Get the 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,14 @@ 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+ self .agent_invocations [- 1 ].cycles .append (
238+ EventLoopCycleMetric (
239+ event_loop_cycle_id = attributes ["event_loop_cycle_id" ],
240+ usage = Usage (inputTokens = 0 , outputTokens = 0 , totalTokens = 0 ),
241+ )
242+ )
243+
197244 return start_time , cycle_trace
198245
199246 def end_cycle (self , start_time : float , cycle_trace : Trace , attributes : Optional [Dict [str , Any ]] = None ) -> None :
@@ -252,32 +299,53 @@ def add_tool_usage(
252299 )
253300 tool_trace .end ()
254301
302+ def _accumulate_usage (self , target : Usage , source : Usage ) -> None :
303+ """Helper method to accumulate usage from source to target.
304+
305+ Args:
306+ target: The Usage object to accumulate into.
307+ source: The Usage object to accumulate from.
308+ """
309+ target ["inputTokens" ] += source ["inputTokens" ]
310+ target ["outputTokens" ] += source ["outputTokens" ]
311+ target ["totalTokens" ] += source ["totalTokens" ]
312+
313+ if "cacheReadInputTokens" in source :
314+ target ["cacheReadInputTokens" ] = target .get ("cacheReadInputTokens" , 0 ) + source ["cacheReadInputTokens" ]
315+
316+ if "cacheWriteInputTokens" in source :
317+ target ["cacheWriteInputTokens" ] = target .get ("cacheWriteInputTokens" , 0 ) + source ["cacheWriteInputTokens" ]
318+
255319 def update_usage (self , usage : Usage ) -> None :
256320 """Update the accumulated token usage with new usage data.
257321
258322 Args:
259323 usage: The usage data to add to the accumulated totals.
260324 """
325+ # Record metrics to OpenTelemetry
261326 self ._metrics_client .event_loop_input_tokens .record (usage ["inputTokens" ])
262327 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" ]
266328
267- # Handle optional cached token metrics
329+ # Handle optional cached token metrics for OpenTelemetry
268330 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-
331+ self ._metrics_client .event_loop_cache_read_input_tokens .record (usage ["cacheReadInputTokens" ])
275332 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- )
333+ self ._metrics_client .event_loop_cache_write_input_tokens .record (usage ["cacheWriteInputTokens" ])
334+
335+ self ._accumulate_usage (self .accumulated_usage , usage )
336+ self ._accumulate_usage (self .agent_invocations [- 1 ].usage , usage )
337+
338+ if self .agent_invocations [- 1 ].cycles :
339+ current_cycle = self .agent_invocations [- 1 ].cycles [- 1 ]
340+ self ._accumulate_usage (current_cycle .usage , usage )
341+
342+ def reset_usage_metrics (self ) -> None :
343+ """Start a new agent invocation by creating a new AgentInvocation.
344+
345+ This should be called at the start of a new request to begin tracking
346+ a new agent invocation with fresh usage and cycle data.
347+ """
348+ self .agent_invocations .append (AgentInvocation ())
281349
282350 def update_metrics (self , metrics : Metrics ) -> None :
283351 """Update the accumulated performance metrics with new metrics data.
@@ -322,6 +390,16 @@ def get_summary(self) -> Dict[str, Any]:
322390 "traces" : [trace .to_dict () for trace in self .traces ],
323391 "accumulated_usage" : self .accumulated_usage ,
324392 "accumulated_metrics" : self .accumulated_metrics ,
393+ "agent_invocations" : [
394+ {
395+ "usage" : invocation .usage ,
396+ "cycles" : [
397+ {"event_loop_cycle_id" : cycle .event_loop_cycle_id , "usage" : cycle .usage }
398+ for cycle in invocation .cycles
399+ ],
400+ }
401+ for invocation in self .agent_invocations
402+ ],
325403 }
326404 return summary
327405
0 commit comments