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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP-aware integration module.
|
|
3
|
+
|
|
4
|
+
Today's two framework integrations (``langgraph.py``, ``openai_agents.py``)
|
|
5
|
+
both assume an agent/graph invocation is already underway when they attach —
|
|
6
|
+
they hook callbacks that fire *during* ``graph.invoke()``/``Runner.run()``.
|
|
7
|
+
MCP tool-loading failures frequently happen *before* any of that: at
|
|
8
|
+
``ClientSession``/``MultiServerMCPClient`` construction time, while the
|
|
9
|
+
client is initializing the connection or listing tools, with no agent
|
|
10
|
+
invocation in progress at all.
|
|
11
|
+
|
|
12
|
+
This module instruments an MCP client's lifecycle directly — independent of
|
|
13
|
+
whether an agent/graph invocation is running — by wrapping the instance
|
|
14
|
+
methods ``mcp.client.session.ClientSession`` actually exposes:
|
|
15
|
+
``initialize``, ``list_tools``, and ``call_tool``.
|
|
16
|
+
|
|
17
|
+
Usage (``ClientSession`` directly)::
|
|
18
|
+
|
|
19
|
+
from mcp import ClientSession
|
|
20
|
+
from agent_trace import Tracer
|
|
21
|
+
from agent_trace.integrations.mcp import instrument_session
|
|
22
|
+
|
|
23
|
+
t = Tracer()
|
|
24
|
+
with t.start_trace("mcp_run") as trace:
|
|
25
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
26
|
+
instrument_session(session, tracer=t, trace=trace)
|
|
27
|
+
await session.initialize()
|
|
28
|
+
tools = await session.list_tools()
|
|
29
|
+
result = await session.call_tool("search", {"query": "agents"})
|
|
30
|
+
|
|
31
|
+
Usage (``MultiServerMCPClient`` from ``langchain-mcp-adapters``)::
|
|
32
|
+
|
|
33
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
34
|
+
from agent_trace.integrations.mcp import instrument_multi_server_client
|
|
35
|
+
|
|
36
|
+
client = MultiServerMCPClient({...})
|
|
37
|
+
instrument_multi_server_client(client, tracer=t, trace=trace)
|
|
38
|
+
all_tools = await client.get_tools() # now spanned + traces every session
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
from collections.abc import Callable
|
|
45
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
46
|
+
|
|
47
|
+
from agent_trace.core.span import SpanStatus
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from agent_trace import Trace, Tracer
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"instrument_multi_server_client",
|
|
54
|
+
"instrument_session",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
T = TypeVar("T")
|
|
60
|
+
|
|
61
|
+
# Instance attribute set on any object this module has already wrapped, so
|
|
62
|
+
# repeated instrument_*() calls on the same instance are a no-op instead of
|
|
63
|
+
# double-wrapping (each wrap layer would otherwise double-count spans).
|
|
64
|
+
_INSTRUMENTED_MARKER = "_agent_trace_instrumented"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _tool_names(list_tools_result: Any) -> list[str]:
|
|
68
|
+
"""Best-effort extraction of tool names from a ListToolsResult."""
|
|
69
|
+
tools = getattr(list_tools_result, "tools", None) or []
|
|
70
|
+
names: list[str] = []
|
|
71
|
+
for tool in tools:
|
|
72
|
+
name = getattr(tool, "name", None)
|
|
73
|
+
if name is not None:
|
|
74
|
+
names.append(str(name))
|
|
75
|
+
return names
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _make_traced_initialize(tracer: Tracer, original: Callable[..., Any]) -> Any:
|
|
79
|
+
"""Build the ``mcp:initialize``-spanned replacement for ``session.initialize``."""
|
|
80
|
+
|
|
81
|
+
async def traced_initialize(*args: Any, **kwargs: Any) -> Any:
|
|
82
|
+
span = tracer.start_span("mcp:initialize")
|
|
83
|
+
try:
|
|
84
|
+
result = await original(*args, **kwargs)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
span.record_exception(exc)
|
|
87
|
+
if span.end_time is None:
|
|
88
|
+
span.end(SpanStatus.ERROR)
|
|
89
|
+
raise
|
|
90
|
+
try:
|
|
91
|
+
protocol_version = getattr(result, "protocolVersion", None)
|
|
92
|
+
if protocol_version is not None:
|
|
93
|
+
span.set_attribute("mcp.protocol_version", str(protocol_version))
|
|
94
|
+
server_info = getattr(result, "serverInfo", None)
|
|
95
|
+
server_name = getattr(server_info, "name", None)
|
|
96
|
+
if server_name is not None:
|
|
97
|
+
span.set_attribute("mcp.server_name", str(server_name))
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.debug(
|
|
100
|
+
"agent-trace: failed to record MCP initialize result",
|
|
101
|
+
exc_info=True,
|
|
102
|
+
)
|
|
103
|
+
span.end(SpanStatus.OK)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
return traced_initialize
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _make_traced_list_tools(tracer: Tracer, original: Callable[..., Any]) -> Any:
|
|
110
|
+
"""Build the ``mcp:list_tools``-spanned replacement for ``session.list_tools``."""
|
|
111
|
+
|
|
112
|
+
async def traced_list_tools(*args: Any, **kwargs: Any) -> Any:
|
|
113
|
+
span = tracer.start_span("mcp:list_tools")
|
|
114
|
+
try:
|
|
115
|
+
result = await original(*args, **kwargs)
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
span.record_exception(exc)
|
|
118
|
+
if span.end_time is None:
|
|
119
|
+
span.end(SpanStatus.ERROR)
|
|
120
|
+
raise
|
|
121
|
+
try:
|
|
122
|
+
names = _tool_names(result)
|
|
123
|
+
span.set_attribute("mcp.tool_count", len(names))
|
|
124
|
+
if names:
|
|
125
|
+
span.set_attribute("mcp.tool_names", ",".join(names))
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.debug(
|
|
128
|
+
"agent-trace: failed to record MCP list_tools result",
|
|
129
|
+
exc_info=True,
|
|
130
|
+
)
|
|
131
|
+
span.end(SpanStatus.OK)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
return traced_list_tools
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _make_traced_call_tool(tracer: Tracer, original: Callable[..., Any]) -> Any:
|
|
138
|
+
"""Build the ``mcp:tool:<name>``-spanned replacement for ``session.call_tool``."""
|
|
139
|
+
|
|
140
|
+
async def traced_call_tool(
|
|
141
|
+
name: str, arguments: dict[str, Any] | None = None, *args: Any, **kwargs: Any
|
|
142
|
+
) -> Any:
|
|
143
|
+
span = tracer.start_span(f"mcp:tool:{name}")
|
|
144
|
+
span.set_attribute("tool.name", name)
|
|
145
|
+
if arguments:
|
|
146
|
+
span.set_attribute("tool.argument_keys", ",".join(sorted(arguments)))
|
|
147
|
+
try:
|
|
148
|
+
result = await original(name, arguments, *args, **kwargs)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
span.record_exception(exc)
|
|
151
|
+
if span.end_time is None:
|
|
152
|
+
span.end(SpanStatus.ERROR)
|
|
153
|
+
raise
|
|
154
|
+
try:
|
|
155
|
+
is_error = bool(getattr(result, "isError", False))
|
|
156
|
+
span.set_attribute("tool.is_error", is_error)
|
|
157
|
+
span.end(SpanStatus.ERROR if is_error else SpanStatus.OK)
|
|
158
|
+
except Exception:
|
|
159
|
+
logger.debug(
|
|
160
|
+
"agent-trace: failed to record MCP call_tool result",
|
|
161
|
+
exc_info=True,
|
|
162
|
+
)
|
|
163
|
+
span.end(SpanStatus.OK)
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
return traced_call_tool
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def instrument_session(
|
|
170
|
+
session: Any,
|
|
171
|
+
*,
|
|
172
|
+
tracer: Tracer,
|
|
173
|
+
trace: Trace,
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Wrap a ``ClientSession``'s lifecycle methods to emit agent-trace spans.
|
|
176
|
+
|
|
177
|
+
Patches the *instance* (not the class), so other, uninstrumented sessions
|
|
178
|
+
are unaffected. Idempotent — calling this twice on the same session
|
|
179
|
+
returns it unchanged the second time.
|
|
180
|
+
|
|
181
|
+
Spans emitted:
|
|
182
|
+
|
|
183
|
+
- ``mcp:initialize`` — around ``session.initialize()``; records the
|
|
184
|
+
negotiated protocol version and server name once known.
|
|
185
|
+
- ``mcp:list_tools`` — around ``session.list_tools()``; records the
|
|
186
|
+
number of tools returned and their names.
|
|
187
|
+
- ``mcp:tool:<name>`` — around each ``session.call_tool(name, ...)``;
|
|
188
|
+
records the tool name, argument keys, and whether the call errored.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
session:
|
|
193
|
+
An ``mcp.ClientSession`` instance (or anything duck-typed the same
|
|
194
|
+
way — ``initialize``/``list_tools``/``call_tool`` coroutine methods).
|
|
195
|
+
tracer:
|
|
196
|
+
The active :class:`~agent_trace.Tracer` instance.
|
|
197
|
+
trace:
|
|
198
|
+
The :class:`~agent_trace.Trace` that spans will be registered on.
|
|
199
|
+
Accepted for API symmetry with ``LangGraphTracer``/``AgentTraceHook``
|
|
200
|
+
(spans attach to whichever trace is active via
|
|
201
|
+
``tracer.start_span`` — same as those integrations).
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
Any
|
|
206
|
+
The same *session* instance, for chaining.
|
|
207
|
+
"""
|
|
208
|
+
if getattr(session, _INSTRUMENTED_MARKER, False):
|
|
209
|
+
return session
|
|
210
|
+
|
|
211
|
+
session.initialize = _make_traced_initialize(tracer, session.initialize)
|
|
212
|
+
session.list_tools = _make_traced_list_tools(tracer, session.list_tools)
|
|
213
|
+
session.call_tool = _make_traced_call_tool(tracer, session.call_tool)
|
|
214
|
+
setattr(session, _INSTRUMENTED_MARKER, True)
|
|
215
|
+
return session
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def instrument_multi_server_client(
|
|
219
|
+
client: Any,
|
|
220
|
+
*,
|
|
221
|
+
tracer: Tracer,
|
|
222
|
+
trace: Trace,
|
|
223
|
+
) -> Any:
|
|
224
|
+
"""Instrument a ``langchain_mcp_adapters.client.MultiServerMCPClient``.
|
|
225
|
+
|
|
226
|
+
Wraps ``client.session()`` so that every ``ClientSession`` it yields is
|
|
227
|
+
automatically passed through :func:`instrument_session` — covering both
|
|
228
|
+
the "new session per tool call" usage (``client.get_tools()``) and the
|
|
229
|
+
"explicit session" usage (``async with client.session(name) as s``),
|
|
230
|
+
since both paths route through ``session()`` internally.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
client:
|
|
235
|
+
A ``MultiServerMCPClient`` instance (duck-typed: anything exposing an
|
|
236
|
+
async-context-manager ``session(server_name)`` method).
|
|
237
|
+
tracer:
|
|
238
|
+
The active :class:`~agent_trace.Tracer` instance.
|
|
239
|
+
trace:
|
|
240
|
+
The :class:`~agent_trace.Trace` that spans will be registered on.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
Any
|
|
245
|
+
The same *client* instance, for chaining.
|
|
246
|
+
"""
|
|
247
|
+
if getattr(client, _INSTRUMENTED_MARKER, False):
|
|
248
|
+
return client
|
|
249
|
+
|
|
250
|
+
original_session_cm: Callable[..., Any] = client.session
|
|
251
|
+
|
|
252
|
+
def traced_session(*args: Any, **kwargs: Any) -> Any:
|
|
253
|
+
# client.session() is itself an @asynccontextmanager — call it to get
|
|
254
|
+
# the underlying async context manager, wrap what it yields.
|
|
255
|
+
inner_cm = original_session_cm(*args, **kwargs)
|
|
256
|
+
return _InstrumentingSessionContext(inner_cm, tracer=tracer, trace=trace)
|
|
257
|
+
|
|
258
|
+
client.session = traced_session
|
|
259
|
+
setattr(client, _INSTRUMENTED_MARKER, True)
|
|
260
|
+
return client
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class _InstrumentingSessionContext:
|
|
264
|
+
"""Async context manager that instruments whatever session it yields.
|
|
265
|
+
|
|
266
|
+
Wraps the async context manager returned by
|
|
267
|
+
``MultiServerMCPClient.session()`` so the ``ClientSession`` it yields is
|
|
268
|
+
run through :func:`instrument_session` before being handed to the caller.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, inner_cm: Any, *, tracer: Tracer, trace: Trace) -> None:
|
|
272
|
+
self._inner_cm = inner_cm
|
|
273
|
+
self._tracer = tracer
|
|
274
|
+
self._trace = trace
|
|
275
|
+
|
|
276
|
+
async def __aenter__(self) -> Any:
|
|
277
|
+
session = await self._inner_cm.__aenter__()
|
|
278
|
+
return instrument_session(session, tracer=self._tracer, trace=self._trace)
|
|
279
|
+
|
|
280
|
+
async def __aexit__(self, *exc_info: Any) -> Any:
|
|
281
|
+
return await self._inner_cm.__aexit__(*exc_info)
|