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.
- agent_observability_trace_cli-0.1.2.dist-info/METADATA +336 -0
- agent_observability_trace_cli-0.1.2.dist-info/RECORD +51 -0
- agent_observability_trace_cli-0.1.2.dist-info/WHEEL +4 -0
- agent_observability_trace_cli-0.1.2.dist-info/entry_points.txt +2 -0
- agent_observability_trace_cli-0.1.2.dist-info/licenses/LICENSE +198 -0
- agent_trace/__init__.py +1182 -0
- agent_trace/_cli.py +1377 -0
- agent_trace/_inspect.py +2020 -0
- agent_trace/_replay/__init__.py +1 -0
- agent_trace/_replay/engine.py +268 -0
- agent_trace/_replay/fixture.py +868 -0
- agent_trace/core/__init__.py +1 -0
- agent_trace/core/clock.py +91 -0
- agent_trace/core/exceptions.py +51 -0
- agent_trace/core/span.py +271 -0
- agent_trace/core/trace.py +92 -0
- agent_trace/exporters/__init__.py +1 -0
- agent_trace/exporters/file.py +80 -0
- agent_trace/exporters/otlp.py +199 -0
- agent_trace/exporters/remote_fixture.py +307 -0
- agent_trace/exporters/stdout.py +258 -0
- agent_trace/integrations/__init__.py +69 -0
- agent_trace/integrations/agno.py +489 -0
- agent_trace/integrations/autogen.py +581 -0
- agent_trace/integrations/crewai.py +486 -0
- agent_trace/integrations/google_genai.py +731 -0
- agent_trace/integrations/haystack.py +254 -0
- agent_trace/integrations/langchain_core.py +361 -0
- agent_trace/integrations/langgraph.py +2093 -0
- agent_trace/integrations/langgraph_checkpoint.py +763 -0
- agent_trace/integrations/langgraph_state_diff.py +345 -0
- agent_trace/integrations/langgraph_stream_debug.py +202 -0
- agent_trace/integrations/llama_index.py +472 -0
- agent_trace/integrations/mcp.py +281 -0
- agent_trace/integrations/openai_agents.py +811 -0
- agent_trace/integrations/pydantic_ai.py +504 -0
- agent_trace/integrations/streaming.py +218 -0
- agent_trace/interceptor/__init__.py +64 -0
- agent_trace/interceptor/aiohttp_hook.py +152 -0
- agent_trace/interceptor/botocore_hook.py +223 -0
- agent_trace/interceptor/grpc_hook.py +651 -0
- agent_trace/interceptor/httpx_hook.py +866 -0
- agent_trace/interceptor/logging_hook.py +137 -0
- agent_trace/interceptor/requests_patch.py +184 -0
- agent_trace/interceptor/sse.py +176 -0
- agent_trace/interceptor/stdio_hook.py +245 -0
- agent_trace/interceptor/warnings_hook.py +132 -0
- agent_trace/interceptor/websocket_hook.py +323 -0
- agent_trace/plugins/__init__.py +35 -0
- agent_trace/plugins/base.py +111 -0
- 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
|