agent-observability-trace-cli 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. agent_observability_trace_cli-0.1.2.dist-info/METADATA +336 -0
  2. agent_observability_trace_cli-0.1.2.dist-info/RECORD +51 -0
  3. agent_observability_trace_cli-0.1.2.dist-info/WHEEL +4 -0
  4. agent_observability_trace_cli-0.1.2.dist-info/entry_points.txt +2 -0
  5. agent_observability_trace_cli-0.1.2.dist-info/licenses/LICENSE +198 -0
  6. agent_trace/__init__.py +1182 -0
  7. agent_trace/_cli.py +1377 -0
  8. agent_trace/_inspect.py +2020 -0
  9. agent_trace/_replay/__init__.py +1 -0
  10. agent_trace/_replay/engine.py +268 -0
  11. agent_trace/_replay/fixture.py +868 -0
  12. agent_trace/core/__init__.py +1 -0
  13. agent_trace/core/clock.py +91 -0
  14. agent_trace/core/exceptions.py +51 -0
  15. agent_trace/core/span.py +271 -0
  16. agent_trace/core/trace.py +92 -0
  17. agent_trace/exporters/__init__.py +1 -0
  18. agent_trace/exporters/file.py +80 -0
  19. agent_trace/exporters/otlp.py +199 -0
  20. agent_trace/exporters/remote_fixture.py +307 -0
  21. agent_trace/exporters/stdout.py +258 -0
  22. agent_trace/integrations/__init__.py +69 -0
  23. agent_trace/integrations/agno.py +489 -0
  24. agent_trace/integrations/autogen.py +581 -0
  25. agent_trace/integrations/crewai.py +486 -0
  26. agent_trace/integrations/google_genai.py +731 -0
  27. agent_trace/integrations/haystack.py +254 -0
  28. agent_trace/integrations/langchain_core.py +361 -0
  29. agent_trace/integrations/langgraph.py +2093 -0
  30. agent_trace/integrations/langgraph_checkpoint.py +763 -0
  31. agent_trace/integrations/langgraph_state_diff.py +345 -0
  32. agent_trace/integrations/langgraph_stream_debug.py +202 -0
  33. agent_trace/integrations/llama_index.py +472 -0
  34. agent_trace/integrations/mcp.py +281 -0
  35. agent_trace/integrations/openai_agents.py +811 -0
  36. agent_trace/integrations/pydantic_ai.py +504 -0
  37. agent_trace/integrations/streaming.py +218 -0
  38. agent_trace/interceptor/__init__.py +64 -0
  39. agent_trace/interceptor/aiohttp_hook.py +152 -0
  40. agent_trace/interceptor/botocore_hook.py +223 -0
  41. agent_trace/interceptor/grpc_hook.py +651 -0
  42. agent_trace/interceptor/httpx_hook.py +866 -0
  43. agent_trace/interceptor/logging_hook.py +137 -0
  44. agent_trace/interceptor/requests_patch.py +184 -0
  45. agent_trace/interceptor/sse.py +176 -0
  46. agent_trace/interceptor/stdio_hook.py +245 -0
  47. agent_trace/interceptor/warnings_hook.py +132 -0
  48. agent_trace/interceptor/websocket_hook.py +323 -0
  49. agent_trace/plugins/__init__.py +35 -0
  50. agent_trace/plugins/base.py +111 -0
  51. agent_trace/py.typed +0 -0
@@ -0,0 +1,489 @@
1
+ """
2
+ Agno framework integration — Agent/Team run and model-stream lifecycle.
3
+
4
+ Agno (``agno-agi/agno``) has no LangChain-style callback-handler interface to
5
+ subclass. Its officially supported observation surface is instead a
6
+ structured event stream: ``Agent.run``/``Agent.arun`` (and the identical
7
+ ``Team.run``/``Team.arun``) accept ``stream=True, stream_events=True`` and
8
+ yield a sequence of ``RunOutputEvent``/``TeamRunOutputEvent`` objects —
9
+ ``RunStarted`` -> ``ModelRequestStarted``/``ModelRequestCompleted`` ->
10
+ ``ToolCallStarted``/``ToolCallCompleted``/``ToolCallError`` ->
11
+ ``RunCompleted``/``RunError``/``RunCancelled`` (confirmed against the
12
+ installed ``agno==2.7.1`` package: ``agno/run/agent.py``,
13
+ ``agno/run/team.py``, ``agno/agent/_run.py``).
14
+
15
+ Two properties of this event stream make it the right integration point
16
+ (rather than monkey-patching ``Model.aresponse``/``aresponse_stream``
17
+ directly, which is unexported, private implementation surface):
18
+
19
+ * Exceptions raised entirely inside Agno's own in-process response handling
20
+ (never reaching the wire — e.g. a bug in ``agno/models/base.py`` itself)
21
+ are caught by Agno's own streaming loop and re-surfaced as a
22
+ ``RunErrorEvent`` on this same stream, not merely as an HTTP error the
23
+ framework-agnostic interceptor could catch.
24
+ * When a ``Team`` delegates a task to a member ``Agent`` (via the built-in
25
+ ``delegate_task_to_member`` tool), the member's own run events are
26
+ forwarded through the team's event stream with ``parent_run_id`` set to
27
+ the team run's ``run_id`` and the member's own ``agent_id``/``agent_name``
28
+ populated — giving per-team-member attribution for free (confirmed via
29
+ ``agno/team/_default_tools.py``'s ``delegate_task_to_member``).
30
+
31
+ Usage (hook-based — recommended)::
32
+
33
+ from agno.agent import Agent
34
+ from agent_trace import Tracer
35
+ from agent_trace.integrations.agno import AgnoTracer
36
+
37
+ t = Tracer()
38
+ with t.start_trace("my_agno_run", record=True) as trace:
39
+ hook = AgnoTracer(tracer=t, trace=trace)
40
+ async for event in agent.arun("hello", stream=True, stream_events=True):
41
+ hook.process_event(event)
42
+
43
+ Usage (convenience wrapper)::
44
+
45
+ from agent_trace.integrations.agno import instrument_agent_arun
46
+
47
+ result = await instrument_agent_arun(agent, "hello", tracer=t, trace=trace)
48
+
49
+ The same ``AgnoTracer``/wrappers work for a ``Team`` instance unchanged —
50
+ Agno's ``Team.run``/``Team.arun`` share the identical ``stream_events``
51
+ contract and event shape (``agno/team/team.py`` mirrors
52
+ ``agno/agent/agent.py``'s ``run``/``arun`` overloads).
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import logging
58
+ import threading
59
+ from typing import TYPE_CHECKING, Any, ClassVar
60
+
61
+ from agent_trace.core.span import Span, SpanStatus
62
+
63
+ if TYPE_CHECKING:
64
+ from agent_trace import Trace, Tracer
65
+
66
+ __all__ = [
67
+ "AgnoTracer",
68
+ "instrument_agent_arun",
69
+ "instrument_agent_run",
70
+ ]
71
+
72
+ logger = logging.getLogger(__name__)
73
+
74
+ _INSTALL_HINT = (
75
+ "The Agno integration requires the agno package.\n"
76
+ "Install it with:\n\n"
77
+ " pip install agno\n"
78
+ )
79
+
80
+
81
+ def _require_agno() -> Any:
82
+ """Lazy import guard — raises a clear error if agno is absent."""
83
+ try:
84
+ import agno
85
+
86
+ return agno
87
+ except ImportError as exc:
88
+ raise ImportError(_INSTALL_HINT) from exc
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Event classification
93
+ # ---------------------------------------------------------------------------
94
+ #
95
+ # agno.run.agent.RunEvent and agno.run.team.TeamRunEvent enumerate the same
96
+ # lifecycle in parallel: every Team event name is exactly "Team" + the
97
+ # corresponding Agent event name (e.g. "RunStarted" / "TeamRunStarted",
98
+ # "ToolCallStarted" / "TeamToolCallStarted" — confirmed against
99
+ # agno/run/agent.py:143-192 and agno/run/team.py:130-187). Stripping a
100
+ # leading "Team" prefix lets one classifier handle both without importing
101
+ # agno.run.agent / agno.run.team directly, keeping this module import-light.
102
+
103
+ _RUN_STARTED = "RunStarted"
104
+ _RUN_COMPLETED = "RunCompleted"
105
+ _RUN_ERROR = "RunError"
106
+ _RUN_CANCELLED = "RunCancelled"
107
+ _TOOL_CALL_STARTED = "ToolCallStarted"
108
+ _TOOL_CALL_COMPLETED = "ToolCallCompleted"
109
+ _TOOL_CALL_ERROR = "ToolCallError"
110
+ _MODEL_REQUEST_STARTED = "ModelRequestStarted"
111
+ _MODEL_REQUEST_COMPLETED = "ModelRequestCompleted"
112
+
113
+
114
+ def _normalize_event_name(name: str) -> str:
115
+ """Strip Team's "Team" prefix so Agent- and Team-level events compare equal."""
116
+ return name[len("Team") :] if name.startswith("Team") else name
117
+
118
+
119
+ def _actor(event: Any) -> tuple[str, str]:
120
+ """Return ``(kind, display_name)`` for the event's originating Agent or Team.
121
+
122
+ ``kind`` is ``"agent"`` or ``"team"`` — used as the span-name prefix so a
123
+ team leader's run and a delegated member's run are both visible and
124
+ distinguishable in the span tree (e.g. ``team:my-team`` parenting
125
+ ``agent:researcher``).
126
+ """
127
+ agent_name = getattr(event, "agent_name", None) or getattr(event, "agent_id", None)
128
+ if agent_name:
129
+ return "agent", str(agent_name)
130
+ team_name = getattr(event, "team_name", None) or getattr(event, "team_id", None)
131
+ return "team", str(team_name or "team")
132
+
133
+
134
+ def _make_exception(message: str, type_name: str | None) -> Exception:
135
+ """Synthesize an exception carrying Agno's reported error type/message.
136
+
137
+ Agno's ``RunErrorEvent``/``ToolCallErrorEvent`` carry only strings — by
138
+ the time the exception reaches this hook, Agno's own streaming loop has
139
+ already caught it and converted it to event data (see
140
+ ``agno/agent/_run.py``'s ``except Exception as e`` handler around the
141
+ streaming tool-call loop, which calls ``create_run_error_event(...,
142
+ error=str(e))``). There is no real ``BaseException`` object to forward,
143
+ so build a lightweight one so ``Span.record_exception`` still populates
144
+ ``exception.type``/``exception.message`` correctly.
145
+ """
146
+ cleaned = "".join(ch for ch in (type_name or "") if ch.isalnum() or ch == "_")
147
+ safe_name = cleaned or "AgnoRunError"
148
+ exc_cls: type[Exception] = type(safe_name, (RuntimeError,), {})
149
+ return exc_cls(message or "")
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # AgnoTracer
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ class AgnoTracer:
158
+ """Consumes an Agno event stream and emits agent-trace spans.
159
+
160
+ Feed it every event yielded by ``agent.run``/``agent.arun`` (or
161
+ ``team.run``/``team.arun``) called with ``stream=True,
162
+ stream_events=True`` via :meth:`process_event`. It maintains its own
163
+ span registry keyed by Agno's ``run_id`` (for run spans), ``tool_call_id``
164
+ (for tool spans), and a per-run stack of open model-request spans (model
165
+ calls within one run never overlap, but a tool-calling loop can issue
166
+ several in sequence).
167
+
168
+ Parameters
169
+ ----------
170
+ tracer:
171
+ The active :class:`~agent_trace.Tracer` instance.
172
+ trace:
173
+ The :class:`~agent_trace.Trace` that spans will be registered on.
174
+ """
175
+
176
+ def __init__(self, tracer: Tracer, trace: Trace) -> None:
177
+ self._tracer: Tracer = tracer
178
+ self._trace: Trace = trace
179
+ self._run_spans: dict[str, Span] = {}
180
+ self._tool_spans: dict[str, Span] = {}
181
+ self._llm_stacks: dict[str, list[Span]] = {}
182
+ self._lock: threading.Lock = threading.Lock()
183
+
184
+ # ------------------------------------------------------------------
185
+ # Public API
186
+ # ------------------------------------------------------------------
187
+
188
+ def process_event(self, event: Any) -> None:
189
+ """Handle a single event from an Agno run's event stream.
190
+
191
+ Never raises — a malformed/unrecognized event is logged at debug
192
+ level and skipped rather than aborting the caller's run loop.
193
+ """
194
+ try:
195
+ self._process_event(event)
196
+ except Exception:
197
+ logger.debug(
198
+ "agent-trace: failed to process Agno event %r",
199
+ event,
200
+ exc_info=True,
201
+ )
202
+
203
+ def close_open_spans(self) -> None:
204
+ """Force-close any spans left open (e.g. the caller's loop broke early
205
+ or the run was interrupted by an exception the event stream never
206
+ reported). Safety net so a partially-consumed stream doesn't leak
207
+ permanently-open spans into the trace.
208
+ """
209
+ with self._lock:
210
+ run_spans = list(self._run_spans.values())
211
+ self._run_spans.clear()
212
+ tool_spans = list(self._tool_spans.values())
213
+ self._tool_spans.clear()
214
+ llm_spans = [s for stack in self._llm_stacks.values() for s in stack]
215
+ self._llm_stacks.clear()
216
+ for span in (*llm_spans, *tool_spans, *run_spans):
217
+ if span.end_time is None:
218
+ span.end(SpanStatus.ERROR)
219
+
220
+ # ------------------------------------------------------------------
221
+ # Internal helpers
222
+ # ------------------------------------------------------------------
223
+
224
+ def _open_run_span(self, event: Any) -> Span | None:
225
+ run_id = getattr(event, "run_id", None)
226
+ if not run_id:
227
+ return None
228
+ kind, name = _actor(event)
229
+ parent_run_id = getattr(event, "parent_run_id", None)
230
+ parent_span_id: str | None = None
231
+ if parent_run_id:
232
+ with self._lock:
233
+ parent_span = self._run_spans.get(str(parent_run_id))
234
+ if parent_span is not None:
235
+ parent_span_id = parent_span.span_id
236
+
237
+ span = self._tracer.start_span(f"{kind}:{name}", parent_id=parent_span_id)
238
+ span.set_attribute(f"agno.{kind}.name", name)
239
+ span.set_attribute("agno.run_id", str(run_id))
240
+ model = getattr(event, "model", None)
241
+ if model:
242
+ span.set_attribute("agno.model", str(model))
243
+ with self._lock:
244
+ self._run_spans[str(run_id)] = span
245
+ return span
246
+
247
+ def _pop_tool_span(self, tool_call_id: Any) -> Span | None:
248
+ if not tool_call_id:
249
+ return None
250
+ with self._lock:
251
+ return self._tool_spans.pop(str(tool_call_id), None)
252
+
253
+ def _process_event(self, event: Any) -> None:
254
+ raw_name = getattr(event, "event", None) or type(event).__name__
255
+ name = _normalize_event_name(str(raw_name))
256
+
257
+ if name == _RUN_STARTED:
258
+ self._open_run_span(event)
259
+ return
260
+
261
+ run_id = getattr(event, "run_id", None)
262
+ if run_id is None:
263
+ return
264
+ run_key = str(run_id)
265
+
266
+ handler = self._EVENT_HANDLERS.get(name)
267
+ if handler is not None:
268
+ handler(self, event, run_key)
269
+
270
+ def _handle_run_completed(self, event: Any, run_key: str) -> None:
271
+ with self._lock:
272
+ span = self._run_spans.pop(run_key, None)
273
+ if span is not None and span.end_time is None:
274
+ span.end(SpanStatus.OK)
275
+
276
+ def _handle_run_error(self, event: Any, run_key: str) -> None:
277
+ with self._lock:
278
+ span = self._run_spans.pop(run_key, None)
279
+ if span is not None:
280
+ message = getattr(event, "content", None) or "Agno run error"
281
+ exc = _make_exception(str(message), getattr(event, "error_type", None))
282
+ span.record_exception(exc)
283
+ if span.end_time is None:
284
+ span.end(SpanStatus.ERROR)
285
+
286
+ def _handle_run_cancelled(self, event: Any, run_key: str) -> None:
287
+ with self._lock:
288
+ span = self._run_spans.pop(run_key, None)
289
+ if span is not None:
290
+ span.set_attribute("agno.cancelled", True)
291
+ reason = getattr(event, "reason", None)
292
+ if reason:
293
+ span.set_attribute("agno.cancel_reason", str(reason))
294
+ if span.end_time is None:
295
+ span.end(SpanStatus.OK)
296
+
297
+ def _handle_model_request_started(self, event: Any, run_key: str) -> None:
298
+ with self._lock:
299
+ parent_span = self._run_spans.get(run_key)
300
+ parent_span_id = parent_span.span_id if parent_span is not None else None
301
+ model = getattr(event, "model", None) or "unknown"
302
+ span = self._tracer.start_span(f"llm:{model}", parent_id=parent_span_id)
303
+ span.set_attribute("llm.model", str(model))
304
+ provider = getattr(event, "model_provider", None)
305
+ if provider:
306
+ span.set_attribute("llm.provider", str(provider))
307
+ with self._lock:
308
+ self._llm_stacks.setdefault(run_key, []).append(span)
309
+
310
+ def _handle_model_request_completed(self, event: Any, run_key: str) -> None:
311
+ with self._lock:
312
+ stack = self._llm_stacks.get(run_key)
313
+ span = stack.pop() if stack else None
314
+ if span is None:
315
+ return
316
+ for attribute, field_name in (
317
+ ("llm.usage.prompt_tokens", "input_tokens"),
318
+ ("llm.usage.completion_tokens", "output_tokens"),
319
+ ("llm.usage.total_tokens", "total_tokens"),
320
+ ):
321
+ value = getattr(event, field_name, None)
322
+ if value is not None:
323
+ span.set_attribute(attribute, int(value))
324
+ ttft = getattr(event, "time_to_first_token", None)
325
+ if ttft is not None:
326
+ span.set_attribute("llm.time_to_first_token_s", float(ttft))
327
+ if span.end_time is None:
328
+ span.end(SpanStatus.OK)
329
+
330
+ def _handle_tool_call_started(self, event: Any, run_key: str) -> None:
331
+ tool = getattr(event, "tool", None)
332
+ tool_call_id = getattr(tool, "tool_call_id", None) or f"{run_key}:{id(event)}"
333
+ tool_name = getattr(tool, "tool_name", None) or "tool"
334
+ with self._lock:
335
+ parent_span = self._run_spans.get(run_key)
336
+ parent_span_id = parent_span.span_id if parent_span is not None else None
337
+ span = self._tracer.start_span(f"tool:{tool_name}", parent_id=parent_span_id)
338
+ span.set_attribute("tool.name", str(tool_name))
339
+ tool_args = getattr(tool, "tool_args", None)
340
+ if tool_args:
341
+ span.set_attribute("tool.args", str(tool_args)[:2000])
342
+ with self._lock:
343
+ self._tool_spans[str(tool_call_id)] = span
344
+
345
+ def _handle_tool_call_completed(self, event: Any, run_key: str) -> None:
346
+ tool = getattr(event, "tool", None)
347
+ span = self._pop_tool_span(getattr(tool, "tool_call_id", None))
348
+ if span is None:
349
+ return
350
+ result = getattr(tool, "result", None)
351
+ if result is not None:
352
+ span.set_attribute("tool.result_length", len(str(result)))
353
+ child_run_id = getattr(tool, "child_run_id", None)
354
+ if child_run_id:
355
+ # Set when this tool call spawned a nested Agent/Team run (e.g.
356
+ # Team's built-in delegate_task_to_member) — correlates this
357
+ # tool span with the child run span.
358
+ span.set_attribute("agno.child_run_id", str(child_run_id))
359
+ if span.end_time is None:
360
+ span.end(SpanStatus.OK)
361
+
362
+ def _handle_tool_call_error(self, event: Any, run_key: str) -> None:
363
+ tool = getattr(event, "tool", None)
364
+ span = self._pop_tool_span(getattr(tool, "tool_call_id", None))
365
+ if span is None:
366
+ return
367
+ message = getattr(event, "error", None) or "Agno tool call error"
368
+ exc = _make_exception(str(message), "AgnoToolCallError")
369
+ span.record_exception(exc)
370
+ if span.end_time is None:
371
+ span.end(SpanStatus.ERROR)
372
+
373
+ _EVENT_HANDLERS: ClassVar[dict[str, Any]] = {
374
+ _RUN_COMPLETED: _handle_run_completed,
375
+ _RUN_ERROR: _handle_run_error,
376
+ _RUN_CANCELLED: _handle_run_cancelled,
377
+ _MODEL_REQUEST_STARTED: _handle_model_request_started,
378
+ _MODEL_REQUEST_COMPLETED: _handle_model_request_completed,
379
+ _TOOL_CALL_STARTED: _handle_tool_call_started,
380
+ _TOOL_CALL_COMPLETED: _handle_tool_call_completed,
381
+ _TOOL_CALL_ERROR: _handle_tool_call_error,
382
+ }
383
+
384
+
385
+ # ---------------------------------------------------------------------------
386
+ # instrument_agent_run / instrument_agent_arun — convenience wrappers
387
+ # ---------------------------------------------------------------------------
388
+
389
+
390
+ def instrument_agent_run(
391
+ runnable: Any,
392
+ input: Any,
393
+ *,
394
+ tracer: Tracer,
395
+ trace: Trace,
396
+ **kwargs: Any,
397
+ ) -> Any:
398
+ """Run an Agno ``Agent`` or ``Team`` synchronously with span instrumentation.
399
+
400
+ Combines :class:`AgnoTracer` with ``runnable.run(input, stream=True,
401
+ stream_events=True, ...)``, draining the event stream into spans and
402
+ returning the final ``RunOutput``/``TeamRunOutput``.
403
+
404
+ Parameters
405
+ ----------
406
+ runnable:
407
+ An Agno ``Agent`` or ``Team`` instance.
408
+ input:
409
+ The input passed through to ``runnable.run``.
410
+ tracer:
411
+ The active :class:`~agent_trace.Tracer`.
412
+ trace:
413
+ The current :class:`~agent_trace.Trace`.
414
+ **kwargs:
415
+ Additional keyword arguments forwarded to ``runnable.run`` (e.g.
416
+ ``user_id``, ``session_id``).
417
+ """
418
+ _require_agno()
419
+
420
+ hook = AgnoTracer(tracer=tracer, trace=trace)
421
+ root_span = tracer.start_span("agno_run")
422
+ result: Any = None
423
+
424
+ try:
425
+ for event in runnable.run(
426
+ input, stream=True, stream_events=True, yield_run_output=True, **kwargs
427
+ ):
428
+ event_kind = type(event).__name__
429
+ if event_kind in ("RunOutput", "TeamRunOutput"):
430
+ result = event
431
+ continue
432
+ hook.process_event(event)
433
+ root_span.end(SpanStatus.OK)
434
+ except Exception as exc:
435
+ root_span.record_exception(exc)
436
+ if root_span.end_time is None:
437
+ root_span.end(SpanStatus.ERROR)
438
+ raise
439
+ finally:
440
+ # Safety net: an event stream that ends mid-run (a RunError event
441
+ # closes the run span but never fires the matching
442
+ # ModelRequestCompleted/ToolCallCompleted for whatever was in
443
+ # flight, or the caller's loop broke early) must not leave spans
444
+ # open forever.
445
+ hook.close_open_spans()
446
+
447
+ return result
448
+
449
+
450
+ async def instrument_agent_arun(
451
+ runnable: Any,
452
+ input: Any,
453
+ *,
454
+ tracer: Tracer,
455
+ trace: Trace,
456
+ **kwargs: Any,
457
+ ) -> Any:
458
+ """Async equivalent of :func:`instrument_agent_run`.
459
+
460
+ Combines :class:`AgnoTracer` with ``runnable.arun(input, stream=True,
461
+ stream_events=True, ...)``, draining the async event stream into spans
462
+ and returning the final ``RunOutput``/``TeamRunOutput``.
463
+ """
464
+ _require_agno()
465
+
466
+ hook = AgnoTracer(tracer=tracer, trace=trace)
467
+ root_span = tracer.start_span("agno_run")
468
+ result: Any = None
469
+
470
+ try:
471
+ async for event in runnable.arun(
472
+ input, stream=True, stream_events=True, yield_run_output=True, **kwargs
473
+ ):
474
+ event_kind = type(event).__name__
475
+ if event_kind in ("RunOutput", "TeamRunOutput"):
476
+ result = event
477
+ continue
478
+ hook.process_event(event)
479
+ root_span.end(SpanStatus.OK)
480
+ except Exception as exc:
481
+ root_span.record_exception(exc)
482
+ if root_span.end_time is None:
483
+ root_span.end(SpanStatus.ERROR)
484
+ raise
485
+ finally:
486
+ # Safety net — see the matching comment in instrument_agent_run.
487
+ hook.close_open_spans()
488
+
489
+ return result