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,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)