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,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
crewAI integration — event-bus listener and span enrichment.
|
|
3
|
+
|
|
4
|
+
Subscribes to crewAI's process-wide event bus (``crewai.events.crewai_event_bus``)
|
|
5
|
+
and emits agent-trace spans for each crew kickoff, agent execution, task, LLM
|
|
6
|
+
call, and tool invocation — the crewAI-native equivalent of what
|
|
7
|
+
``LangGraphTracer`` does for LangGraph's callback-handler list and
|
|
8
|
+
``AgentTraceHook`` does for the OpenAI Agents SDK's hooks interface.
|
|
9
|
+
|
|
10
|
+
Unlike those two frameworks, crewAI has no per-invocation "pass a callback
|
|
11
|
+
object to this call" extension point. Instead it exposes a global event bus
|
|
12
|
+
(confirmed via a live install of ``crewai==1.15.2``:
|
|
13
|
+
``crewai/events/event_bus.py``) that every ``Crew``/``Agent``/``Task``/``LLM``
|
|
14
|
+
emits onto via ``crewai_event_bus.emit(source, event)``. Handlers register with
|
|
15
|
+
``crewai_event_bus.register_handler(event_type, handler)`` and receive
|
|
16
|
+
``(source, event)`` — ``source`` is the emitting object itself (the ``Crew``,
|
|
17
|
+
``Agent``, or ``LLM`` instance), and ``event`` is a typed Pydantic event model
|
|
18
|
+
(``crewai.events.types.*``).
|
|
19
|
+
|
|
20
|
+
Span pairing uses crewAI's own event-scope bookkeeping rather than an ad hoc
|
|
21
|
+
run-id: every "started" event type in this integration is one of crewAI's
|
|
22
|
+
``SCOPE_STARTING_EVENTS`` (``crewai/events/event_context.py``), and its
|
|
23
|
+
matching "completed"/"failed"/"error" event is a ``SCOPE_ENDING_EVENT`` whose
|
|
24
|
+
``started_event_id`` field is set (by ``CrewAIEventsBus._prepare_event``) to
|
|
25
|
+
the ``event_id`` of the started event it closes. Spans are therefore keyed by
|
|
26
|
+
``event.event_id`` on open and looked up by ``event.started_event_id`` on
|
|
27
|
+
close — the same mechanism crewAI itself uses internally to validate
|
|
28
|
+
start/end pairing. Parent/child nesting likewise reuses crewAI's own
|
|
29
|
+
``event.parent_event_id`` (the enclosing open scope's ``event_id`` at
|
|
30
|
+
emission time), so a task span opened while a crew-kickoff span is open is
|
|
31
|
+
automatically parented under it with no bookkeeping of our own.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
from agent_trace import Tracer
|
|
35
|
+
from agent_trace.integrations.crewai import CrewAITracer
|
|
36
|
+
|
|
37
|
+
t = Tracer()
|
|
38
|
+
with t.start_trace("my_crew", record=True) as trace:
|
|
39
|
+
with CrewAITracer(tracer=t, trace=trace):
|
|
40
|
+
result = crew.kickoff()
|
|
41
|
+
|
|
42
|
+
Known limitation — process-wide event bus:
|
|
43
|
+
Because ``crewai_event_bus`` is a process-wide singleton, a
|
|
44
|
+
``CrewAITracer`` instance receives events from *every* Crew/Agent/Task run
|
|
45
|
+
in the process while its handlers are registered — not just the run
|
|
46
|
+
wrapped by the enclosing ``start_trace()`` block. If your process runs
|
|
47
|
+
multiple concurrent ``crew.kickoff()`` calls, a single ``CrewAITracer``
|
|
48
|
+
will see spans from all of them interleaved into one trace. This mirrors
|
|
49
|
+
the concurrent-recording isolation gap already tracked in this repo for
|
|
50
|
+
``Tracer._install_recording_transport`` — treat both as "one in-flight
|
|
51
|
+
run per process" until addressed. Always use ``CrewAITracer`` as a context
|
|
52
|
+
manager (or call ``.close()`` explicitly) so handlers are unregistered
|
|
53
|
+
when the trace ends; leaving it open leaks handlers onto the shared bus
|
|
54
|
+
for the remaining lifetime of the process.
|
|
55
|
+
|
|
56
|
+
Known limitation — parent linkage can be dropped under concurrent handler
|
|
57
|
+
dispatch:
|
|
58
|
+
Confirmed via a live end-to-end run (``examples/04-crewai-research-crew``)
|
|
59
|
+
against a real (deliberately invalid) OpenAI key: ``CrewAIEventsBus.emit``
|
|
60
|
+
computes ``event.parent_event_id`` synchronously on the emitting thread
|
|
61
|
+
(correct and stable by the time any handler sees it), but *dispatches*
|
|
62
|
+
each registered sync handler via a 10-worker ``ThreadPoolExecutor``
|
|
63
|
+
(``crewai/events/event_bus.py``) with no ordering guarantee between
|
|
64
|
+
handlers for different events. A child event's handler (e.g.
|
|
65
|
+
``llm_call_started``) can therefore run — and look up its parent span in
|
|
66
|
+
``self._spans`` — *before* the parent event's handler (e.g.
|
|
67
|
+
``agent_execution_started``) has finished registering that span,
|
|
68
|
+
especially under an agent's internal retry loop, which fires several
|
|
69
|
+
started/error pairs in quick succession. When this race is lost the
|
|
70
|
+
child span still opens, closes, and records errors correctly — only its
|
|
71
|
+
``parent_id`` falls back to ``None`` (a flat span instead of a nested
|
|
72
|
+
one) rather than being silently dropped or mis-attached to the wrong
|
|
73
|
+
parent.
|
|
74
|
+
|
|
75
|
+
Known race (fixed) — a "close" event's handler can run before its own
|
|
76
|
+
"open" event's handler:
|
|
77
|
+
The same unordered ``ThreadPoolExecutor`` dispatch above has a second,
|
|
78
|
+
more severe consequence, reproduced live (a scripted, fully-offline
|
|
79
|
+
``BaseLLM`` making two sequential calls within one ReAct loop):
|
|
80
|
+
``llm_call_completed``'s handler was observed running — and printing its
|
|
81
|
+
debug line — *before* the matching ``llm_call_started``'s handler for
|
|
82
|
+
the very same call had run at all, on a different pool worker thread.
|
|
83
|
+
Naively, this would mean the "started" handler eventually creates the
|
|
84
|
+
span (via ``_open_span``) but no "completed" handler is left to ever
|
|
85
|
+
close it — a span silently, permanently stuck at ``SpanStatus.UNSET``
|
|
86
|
+
with ``end_time=None``, worse than the parent-linkage degradation above
|
|
87
|
+
since a genuinely open span (not just a flat one) sits in the trace
|
|
88
|
+
forever. ``_open_span``/``_close_span``/``_close_span_with_error`` below
|
|
89
|
+
handle this: a close that finds no span yet is stashed in
|
|
90
|
+
``self._pending_closes`` instead of being dropped, and ``_open_span``
|
|
91
|
+
checks (and immediately applies) any pending close for its own key the
|
|
92
|
+
moment it does run — so the span still always ends up correctly closed,
|
|
93
|
+
regardless of which of the pair's two handlers happens to run first.
|
|
94
|
+
|
|
95
|
+
Known limitation (fixed by ``close()``) — ``kickoff()`` returns before its
|
|
96
|
+
own last events are processed:
|
|
97
|
+
Confirmed via a live, fully-offline ``crew.kickoff()`` reproduction (a
|
|
98
|
+
custom ``BaseLLM`` subclass returning canned responses, no network
|
|
99
|
+
calls): because ``crewai_event_bus.emit()`` dispatches sync handlers onto
|
|
100
|
+
a background ``ThreadPoolExecutor`` and nothing in crewAI's own call
|
|
101
|
+
chain awaits the returned ``Future``, ``Crew.kickoff()`` routinely
|
|
102
|
+
returns to its caller *before* the handlers for its own final events
|
|
103
|
+
(typically the last ``llm_call_completed``/``agent_execution_completed``/
|
|
104
|
+
``crew_kickoff_completed`` trio) have finished running — reproduced with
|
|
105
|
+
spans still ``SpanStatus.UNSET`` (open) immediately after ``kickoff()``
|
|
106
|
+
returned, which then closed correctly a moment later on a background
|
|
107
|
+
thread. ``CrewAITracer.close()`` calls ``crewai_event_bus.flush()``
|
|
108
|
+
(crewAI's own public API for exactly this "wait for pending handlers at
|
|
109
|
+
the end of an operation like kickoff" case) before unregistering, so a
|
|
110
|
+
caller using the documented ``with CrewAITracer(...): crew.kickoff()``
|
|
111
|
+
pattern is guaranteed a fully-closed span tree by the time that ``with``
|
|
112
|
+
block exits — do not skip calling ``close()`` (e.g. by holding a
|
|
113
|
+
``CrewAITracer`` open across multiple ``kickoff()`` calls without ever
|
|
114
|
+
exiting it) if you need the trace to be complete before you read or
|
|
115
|
+
export it.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
from __future__ import annotations
|
|
119
|
+
|
|
120
|
+
import logging
|
|
121
|
+
import threading
|
|
122
|
+
from collections.abc import Callable
|
|
123
|
+
from typing import TYPE_CHECKING, Any
|
|
124
|
+
|
|
125
|
+
from agent_trace.core.span import Span, SpanStatus
|
|
126
|
+
|
|
127
|
+
if TYPE_CHECKING:
|
|
128
|
+
from agent_trace import Trace, Tracer
|
|
129
|
+
|
|
130
|
+
__all__ = ["CrewAITracer"]
|
|
131
|
+
|
|
132
|
+
logger = logging.getLogger(__name__)
|
|
133
|
+
|
|
134
|
+
_INSTALL_HINT = (
|
|
135
|
+
"CrewAITracer requires crewai.\n"
|
|
136
|
+
"Install it with:\n\n"
|
|
137
|
+
" pip install crewai\n"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _require_crewai_events() -> Any:
|
|
142
|
+
"""Lazy import guard — raises a clear error if crewai is absent."""
|
|
143
|
+
try:
|
|
144
|
+
import crewai.events as crewai_events
|
|
145
|
+
|
|
146
|
+
return crewai_events
|
|
147
|
+
except ImportError as exc:
|
|
148
|
+
raise ImportError(_INSTALL_HINT) from exc
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class CrewAITracer:
|
|
152
|
+
"""Registers agent-trace span handlers on crewAI's global event bus.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
tracer:
|
|
157
|
+
The active :class:`~agent_trace.Tracer` instance.
|
|
158
|
+
trace:
|
|
159
|
+
The :class:`~agent_trace.Trace` that spans will be registered on.
|
|
160
|
+
|
|
161
|
+
``crewai`` is imported lazily — importing this module succeeds even when
|
|
162
|
+
``crewai`` is not installed; only constructing ``CrewAITracer`` requires
|
|
163
|
+
it.
|
|
164
|
+
|
|
165
|
+
Use as a context manager (or call :meth:`close`) so handlers are removed
|
|
166
|
+
from the shared event bus once the traced run finishes::
|
|
167
|
+
|
|
168
|
+
with CrewAITracer(tracer=t, trace=trace):
|
|
169
|
+
crew.kickoff()
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, tracer: Tracer, trace: Trace) -> None:
|
|
173
|
+
events = _require_crewai_events()
|
|
174
|
+
|
|
175
|
+
self._tracer: Tracer = tracer
|
|
176
|
+
self._trace: Trace = trace
|
|
177
|
+
# Span registry keyed by the crewAI event_id of the *_started event
|
|
178
|
+
# that opened the span (see module docstring — this mirrors crewAI's
|
|
179
|
+
# own started_event_id pairing rather than inventing a new run-id).
|
|
180
|
+
self._spans: dict[str, Span] = {}
|
|
181
|
+
# Keyed by the *_started event's event_id, same as self._spans.
|
|
182
|
+
# Holds a (status, finisher) pair for a "close" event that arrived
|
|
183
|
+
# before its matching "open" event's handler had a chance to run —
|
|
184
|
+
# see the module docstring's "close arrives before its own open"
|
|
185
|
+
# known-and-fixed race for why this exists. Consumed (and popped)
|
|
186
|
+
# the moment the matching _open_span() call for that key runs.
|
|
187
|
+
self._pending_closes: dict[str, tuple[SpanStatus, Callable[[Span], None]]] = {}
|
|
188
|
+
self._lock: threading.Lock = threading.Lock()
|
|
189
|
+
self._bus = events.crewai_event_bus
|
|
190
|
+
self._registered: list[tuple[type, Any]] = []
|
|
191
|
+
self._closed = False
|
|
192
|
+
|
|
193
|
+
self._register(events.CrewKickoffStartedEvent, self._on_crew_started)
|
|
194
|
+
self._register(events.CrewKickoffCompletedEvent, self._on_crew_completed)
|
|
195
|
+
self._register(events.CrewKickoffFailedEvent, self._on_crew_failed)
|
|
196
|
+
|
|
197
|
+
self._register(events.AgentExecutionStartedEvent, self._on_agent_started)
|
|
198
|
+
self._register(events.AgentExecutionCompletedEvent, self._on_agent_completed)
|
|
199
|
+
self._register(events.AgentExecutionErrorEvent, self._on_agent_error)
|
|
200
|
+
|
|
201
|
+
self._register(events.TaskStartedEvent, self._on_task_started)
|
|
202
|
+
self._register(events.TaskCompletedEvent, self._on_task_completed)
|
|
203
|
+
self._register(events.TaskFailedEvent, self._on_task_failed)
|
|
204
|
+
|
|
205
|
+
self._register(events.LLMCallStartedEvent, self._on_llm_started)
|
|
206
|
+
self._register(events.LLMCallCompletedEvent, self._on_llm_completed)
|
|
207
|
+
self._register(events.LLMCallFailedEvent, self._on_llm_failed)
|
|
208
|
+
|
|
209
|
+
self._register(events.ToolUsageStartedEvent, self._on_tool_started)
|
|
210
|
+
self._register(events.ToolUsageFinishedEvent, self._on_tool_finished)
|
|
211
|
+
self._register(events.ToolUsageErrorEvent, self._on_tool_error)
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Registration / lifecycle
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def _register(self, event_type: type, handler: Any) -> None:
|
|
218
|
+
self._bus.register_handler(event_type, handler)
|
|
219
|
+
self._registered.append((event_type, handler))
|
|
220
|
+
|
|
221
|
+
def close(self) -> None:
|
|
222
|
+
"""Flush pending event-bus work, then unregister all handlers from
|
|
223
|
+
the shared crewAI event bus.
|
|
224
|
+
|
|
225
|
+
Confirmed via a live, fully-offline ``crew.kickoff()`` reproduction
|
|
226
|
+
(a custom ``BaseLLM`` subclass, no network): ``crewai_event_bus.emit()``
|
|
227
|
+
dispatches sync handlers onto a background ``ThreadPoolExecutor`` and
|
|
228
|
+
returns a ``Future`` that nothing in crewAI's own call chain awaits —
|
|
229
|
+
``Crew.kickoff()`` itself returns to its caller *before* the handlers
|
|
230
|
+
for its own last few events (typically the final ``llm_call_completed``/
|
|
231
|
+
``agent_execution_completed``/``crew_kickoff_completed`` trio) have
|
|
232
|
+
necessarily finished running. Without draining that queue here, a
|
|
233
|
+
caller doing::
|
|
234
|
+
|
|
235
|
+
with CrewAITracer(tracer=t, trace=trace):
|
|
236
|
+
result = crew.kickoff()
|
|
237
|
+
# trace.spans / trace.json written right here could still show
|
|
238
|
+
# dangling open llm:/tool: spans that close moments later,
|
|
239
|
+
# invisibly, on a background thread.
|
|
240
|
+
|
|
241
|
+
would non-deterministically get an incomplete trace — worse, a
|
|
242
|
+
*silently* incomplete one, since every span that does eventually
|
|
243
|
+
close still ends up correct; only the *timing* relative to
|
|
244
|
+
``kickoff()`` returning is wrong. ``crewai_event_bus.flush()`` (a
|
|
245
|
+
public method documented for exactly this "at the end of operations
|
|
246
|
+
like kickoff" use case) blocks until every pending handler future
|
|
247
|
+
bus-wide has completed, so calling it here — before unregistering —
|
|
248
|
+
guarantees every event this instance's handlers were going to see for
|
|
249
|
+
the just-completed run has, in fact, been seen by the time ``close()``
|
|
250
|
+
returns.
|
|
251
|
+
|
|
252
|
+
Safe to call more than once. After ``close()``, this instance no
|
|
253
|
+
longer receives crewAI events; any span that is still open at that
|
|
254
|
+
point (e.g. because the wrapped run itself was cut off, such as by
|
|
255
|
+
an unhandled exception outside crewAI's own try/except) is simply
|
|
256
|
+
left open, matching what LangGraph's ``on_chain_end`` not firing
|
|
257
|
+
would look like.
|
|
258
|
+
"""
|
|
259
|
+
if self._closed:
|
|
260
|
+
return
|
|
261
|
+
self._closed = True
|
|
262
|
+
self._bus.flush()
|
|
263
|
+
for event_type, handler in self._registered:
|
|
264
|
+
self._bus.off(event_type, handler)
|
|
265
|
+
self._registered.clear()
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> CrewAITracer:
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
271
|
+
self.close()
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
# Internal span helpers (same pattern as LangGraphTracer / AgentTraceHook)
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def _open_span(
|
|
278
|
+
self,
|
|
279
|
+
key: str,
|
|
280
|
+
name: str,
|
|
281
|
+
parent_key: str | None = None,
|
|
282
|
+
) -> Span:
|
|
283
|
+
"""Open a span for *key* (a ``*_started`` event's ``event_id``).
|
|
284
|
+
|
|
285
|
+
If a ``_close_span``/``_close_span_with_error`` call for this exact
|
|
286
|
+
*key* already ran and found nothing to close (see
|
|
287
|
+
``self._pending_closes`` and the module docstring's "close arrives
|
|
288
|
+
before its own open" race), that pending close is applied
|
|
289
|
+
immediately here — the span still goes through a real open-then-close
|
|
290
|
+
transition, just compressed into this one call, instead of being
|
|
291
|
+
left open forever because the handler that would have closed it
|
|
292
|
+
already ran and gave up.
|
|
293
|
+
"""
|
|
294
|
+
parent_span_id: str | None = None
|
|
295
|
+
if parent_key is not None:
|
|
296
|
+
with self._lock:
|
|
297
|
+
parent_span = self._spans.get(parent_key)
|
|
298
|
+
if parent_span is not None:
|
|
299
|
+
parent_span_id = parent_span.span_id
|
|
300
|
+
|
|
301
|
+
span = self._tracer.start_span(name, parent_id=parent_span_id)
|
|
302
|
+
with self._lock:
|
|
303
|
+
pending = self._pending_closes.pop(key, None)
|
|
304
|
+
if pending is None:
|
|
305
|
+
self._spans[key] = span
|
|
306
|
+
|
|
307
|
+
if pending is not None:
|
|
308
|
+
status, finisher = pending
|
|
309
|
+
finisher(span)
|
|
310
|
+
if span.end_time is None:
|
|
311
|
+
span.end(status)
|
|
312
|
+
return span
|
|
313
|
+
|
|
314
|
+
def _close_span(
|
|
315
|
+
self,
|
|
316
|
+
key: str,
|
|
317
|
+
status: SpanStatus = SpanStatus.OK,
|
|
318
|
+
finisher: Callable[[Span], None] | None = None,
|
|
319
|
+
) -> Span | None:
|
|
320
|
+
"""Close the span opened under *key*, applying *finisher* (if given)
|
|
321
|
+
before ending it — attributes are always set on a still-open span,
|
|
322
|
+
never after ``end()`` (``Span`` documents post-``end()`` mutation as
|
|
323
|
+
undefined for exporters).
|
|
324
|
+
|
|
325
|
+
If no span is registered under *key* yet — the ``_open_span`` call
|
|
326
|
+
that would have created it hasn't run yet, confirmed via a live,
|
|
327
|
+
fully-offline reproduction to be a real, reproducible race in
|
|
328
|
+
crewAI's own thread-pool handler dispatch, not a hypothetical one —
|
|
329
|
+
the close is stashed in ``self._pending_closes`` instead of being
|
|
330
|
+
silently dropped, so ``_open_span`` can apply it retroactively the
|
|
331
|
+
moment it does run.
|
|
332
|
+
"""
|
|
333
|
+
with self._lock:
|
|
334
|
+
span = self._spans.pop(key, None)
|
|
335
|
+
if span is None:
|
|
336
|
+
self._pending_closes[key] = (status, finisher or (lambda _span: None))
|
|
337
|
+
if span is not None:
|
|
338
|
+
if finisher is not None:
|
|
339
|
+
finisher(span)
|
|
340
|
+
if span.end_time is None:
|
|
341
|
+
span.end(status)
|
|
342
|
+
return span
|
|
343
|
+
|
|
344
|
+
def _close_span_with_error(self, key: str | None, error_message: str) -> None:
|
|
345
|
+
"""Close the span opened under *key* (if the started_event_id key is
|
|
346
|
+
known) as ERROR, recording *error_message* as an exception event —
|
|
347
|
+
same open-before-close-arrives handling as ``_close_span``.
|
|
348
|
+
|
|
349
|
+
crewAI's failure events carry ``error: str`` rather than a raised
|
|
350
|
+
exception object, so a synthetic ``RuntimeError`` is used to satisfy
|
|
351
|
+
``Span.record_exception``'s ``BaseException`` signature.
|
|
352
|
+
"""
|
|
353
|
+
if key is None:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
def finisher(span: Span) -> None:
|
|
357
|
+
span.record_exception(RuntimeError(error_message))
|
|
358
|
+
|
|
359
|
+
self._close_span(key, SpanStatus.ERROR, finisher=finisher)
|
|
360
|
+
|
|
361
|
+
# ------------------------------------------------------------------
|
|
362
|
+
# Crew kickoff
|
|
363
|
+
# ------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def _on_crew_started(self, source: Any, event: Any) -> None:
|
|
366
|
+
crew_name = event.crew_name or "crew"
|
|
367
|
+
span = self._open_span(
|
|
368
|
+
event.event_id, f"crew:{crew_name}", event.parent_event_id
|
|
369
|
+
)
|
|
370
|
+
span.set_attribute("crew.name", crew_name)
|
|
371
|
+
|
|
372
|
+
def _on_crew_completed(self, source: Any, event: Any) -> None:
|
|
373
|
+
def finisher(span: Span) -> None:
|
|
374
|
+
try:
|
|
375
|
+
span.set_attribute("crew.total_tokens", int(event.total_tokens or 0))
|
|
376
|
+
except Exception:
|
|
377
|
+
logger.debug(
|
|
378
|
+
"agent-trace: failed to record crew total_tokens", exc_info=True
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
self._close_span(event.started_event_id, SpanStatus.OK, finisher=finisher)
|
|
382
|
+
|
|
383
|
+
def _on_crew_failed(self, source: Any, event: Any) -> None:
|
|
384
|
+
self._close_span_with_error(event.started_event_id, event.error)
|
|
385
|
+
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
# Agent execution
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def _on_agent_started(self, source: Any, event: Any) -> None:
|
|
391
|
+
agent_role = getattr(event.agent, "role", None) or "agent"
|
|
392
|
+
span = self._open_span(
|
|
393
|
+
event.event_id, f"agent:{agent_role}", event.parent_event_id
|
|
394
|
+
)
|
|
395
|
+
span.set_attribute("agent.role", agent_role)
|
|
396
|
+
|
|
397
|
+
def _on_agent_completed(self, source: Any, event: Any) -> None:
|
|
398
|
+
self._close_span(event.started_event_id, SpanStatus.OK)
|
|
399
|
+
|
|
400
|
+
def _on_agent_error(self, source: Any, event: Any) -> None:
|
|
401
|
+
self._close_span_with_error(event.started_event_id, event.error)
|
|
402
|
+
|
|
403
|
+
# ------------------------------------------------------------------
|
|
404
|
+
# Task
|
|
405
|
+
# ------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def _on_task_started(self, source: Any, event: Any) -> None:
|
|
408
|
+
task_name = event.task_name or "task"
|
|
409
|
+
span = self._open_span(
|
|
410
|
+
event.event_id, f"task:{task_name}", event.parent_event_id
|
|
411
|
+
)
|
|
412
|
+
span.set_attribute("task.name", task_name)
|
|
413
|
+
if event.task_id:
|
|
414
|
+
span.set_attribute("task.id", event.task_id)
|
|
415
|
+
|
|
416
|
+
def _on_task_completed(self, source: Any, event: Any) -> None:
|
|
417
|
+
self._close_span(event.started_event_id, SpanStatus.OK)
|
|
418
|
+
|
|
419
|
+
def _on_task_failed(self, source: Any, event: Any) -> None:
|
|
420
|
+
self._close_span_with_error(event.started_event_id, event.error)
|
|
421
|
+
|
|
422
|
+
# ------------------------------------------------------------------
|
|
423
|
+
# LLM calls
|
|
424
|
+
# ------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
def _on_llm_started(self, source: Any, event: Any) -> None:
|
|
427
|
+
model = event.model or "unknown"
|
|
428
|
+
span = self._open_span(event.event_id, f"llm:{model}", event.parent_event_id)
|
|
429
|
+
span.set_attribute("llm.model", model)
|
|
430
|
+
|
|
431
|
+
def _on_llm_completed(self, source: Any, event: Any) -> None:
|
|
432
|
+
def finisher(span: Span) -> None:
|
|
433
|
+
try:
|
|
434
|
+
usage = event.usage or {}
|
|
435
|
+
if usage.get("prompt_tokens") is not None:
|
|
436
|
+
span.set_attribute(
|
|
437
|
+
"llm.usage.prompt_tokens", int(usage["prompt_tokens"])
|
|
438
|
+
)
|
|
439
|
+
if usage.get("completion_tokens") is not None:
|
|
440
|
+
span.set_attribute(
|
|
441
|
+
"llm.usage.completion_tokens", int(usage["completion_tokens"])
|
|
442
|
+
)
|
|
443
|
+
if usage.get("total_tokens") is not None:
|
|
444
|
+
span.set_attribute(
|
|
445
|
+
"llm.usage.total_tokens", int(usage["total_tokens"])
|
|
446
|
+
)
|
|
447
|
+
if event.finish_reason:
|
|
448
|
+
span.set_attribute("llm.finish_reason", event.finish_reason)
|
|
449
|
+
except Exception:
|
|
450
|
+
logger.debug(
|
|
451
|
+
"agent-trace: failed to record token usage for crewAI LLM call",
|
|
452
|
+
exc_info=True,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
self._close_span(event.started_event_id, SpanStatus.OK, finisher=finisher)
|
|
456
|
+
|
|
457
|
+
def _on_llm_failed(self, source: Any, event: Any) -> None:
|
|
458
|
+
self._close_span_with_error(event.started_event_id, event.error)
|
|
459
|
+
|
|
460
|
+
# ------------------------------------------------------------------
|
|
461
|
+
# Tool usage
|
|
462
|
+
# ------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
def _on_tool_started(self, source: Any, event: Any) -> None:
|
|
465
|
+
tool_name = event.tool_name or "tool"
|
|
466
|
+
span = self._open_span(
|
|
467
|
+
event.event_id, f"tool:{tool_name}", event.parent_event_id
|
|
468
|
+
)
|
|
469
|
+
span.set_attribute("tool.name", tool_name)
|
|
470
|
+
|
|
471
|
+
def _on_tool_finished(self, source: Any, event: Any) -> None:
|
|
472
|
+
def finisher(span: Span) -> None:
|
|
473
|
+
try:
|
|
474
|
+
span.set_attribute(
|
|
475
|
+
"tool.output_length",
|
|
476
|
+
len(str(event.output)) if event.output is not None else 0,
|
|
477
|
+
)
|
|
478
|
+
except Exception:
|
|
479
|
+
logger.debug(
|
|
480
|
+
"agent-trace: failed to record tool output length", exc_info=True
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
self._close_span(event.started_event_id, SpanStatus.OK, finisher=finisher)
|
|
484
|
+
|
|
485
|
+
def _on_tool_error(self, source: Any, event: Any) -> None:
|
|
486
|
+
self._close_span_with_error(event.started_event_id, str(event.error))
|