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 @@
1
+ from __future__ import annotations
@@ -0,0 +1,268 @@
1
+ """
2
+ ReplayEngine — orchestrates deterministic replay of a recorded agent run.
3
+
4
+ The engine does three things in the right order:
5
+ 1. Installs FixtureClock so all get_time() calls return recorded timestamps.
6
+ 2. Patches httpx.Client, requests.Session, and (when installed) grpc's
7
+ channel factories so outbound HTTP/gRPC calls are served from the fixture
8
+ instead of hitting real endpoints.
9
+ 3. Tears everything down in a finally block so no patch leaks out.
10
+
11
+ Why monkey-patch rather than dependency-inject?
12
+ AI SDKs construct their own httpx.Client / requests.Session instances
13
+ internally. We cannot inject transports into them without forking each SDK.
14
+ Patching httpx.Client.__init__ and requests.Session.get_adapter is the least
15
+ invasive approach that works across Anthropic, OpenAI, and other SDK clients
16
+ without modification.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import unittest.mock
23
+ from collections.abc import Generator
24
+ from contextlib import contextmanager, nullcontext
25
+ from contextvars import Token
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import httpx
30
+
31
+ from agent_trace._replay.fixture import Fixture
32
+ from agent_trace.core.clock import FixtureClock, restore_clock, set_clock
33
+ from agent_trace.interceptor.httpx_hook import AsyncReplayTransport, ReplayTransport
34
+
35
+ __all__ = ["ReplayEngine", "replay_context"]
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def _build_grpc_replay_patch(fixture: Fixture) -> Any:
41
+ """Return a context manager that patches grpc's sync channel factories.
42
+
43
+ grpc is an optional dependency (installed transitively by SDKs such as
44
+ google-generativeai / google-cloud-aiplatform). grpc.insecure_channel /
45
+ secure_channel are plain module-level functions rather than a shared
46
+ base-class method, so we patch the module attributes directly -- see
47
+ grpc_hook.py's module docstring for why this is the correct interception
48
+ point. Returns nullcontext() when grpc isn't installed.
49
+ """
50
+ try:
51
+ import grpc as _grpc
52
+
53
+ from agent_trace.interceptor.grpc_hook import GRPCReplayInterceptor
54
+
55
+ orig_insecure = _grpc.insecure_channel
56
+ orig_secure = _grpc.secure_channel
57
+
58
+ def _patched_insecure(
59
+ target: str, options: Any = None, compression: Any = None
60
+ ) -> Any:
61
+ channel = orig_insecure(target, options=options, compression=compression)
62
+ return _grpc.intercept_channel(
63
+ channel, GRPCReplayInterceptor(fixture, target)
64
+ )
65
+
66
+ def _patched_secure(
67
+ target: str,
68
+ credentials: Any,
69
+ options: Any = None,
70
+ compression: Any = None,
71
+ ) -> Any:
72
+ channel = orig_secure(
73
+ target, credentials, options=options, compression=compression
74
+ )
75
+ return _grpc.intercept_channel(
76
+ channel, GRPCReplayInterceptor(fixture, target)
77
+ )
78
+
79
+ return unittest.mock.patch.multiple(
80
+ _grpc,
81
+ insecure_channel=_patched_insecure,
82
+ secure_channel=_patched_secure,
83
+ )
84
+ except ImportError:
85
+ return nullcontext()
86
+
87
+
88
+ def _build_grpc_aio_replay_patch(fixture: Fixture) -> Any:
89
+ """Return a context manager that patches grpc.aio's channel factories.
90
+
91
+ Unary-unary only -- see grpc_hook.py's module docstring for why async
92
+ streaming RPCs are out of scope for this pass. Returns nullcontext()
93
+ when grpc isn't installed.
94
+ """
95
+ try:
96
+ from grpc import aio as _grpc_aio
97
+
98
+ from agent_trace.interceptor.grpc_hook import AsyncGRPCReplayInterceptor
99
+
100
+ orig_insecure = _grpc_aio.insecure_channel
101
+ orig_secure = _grpc_aio.secure_channel
102
+
103
+ def _patched_insecure(target: str, **kwargs: Any) -> Any:
104
+ interceptors = list(kwargs.pop("interceptors", None) or [])
105
+ interceptors.append(AsyncGRPCReplayInterceptor(fixture, target))
106
+ return orig_insecure(target, interceptors=interceptors, **kwargs)
107
+
108
+ def _patched_secure(target: str, credentials: Any, **kwargs: Any) -> Any:
109
+ interceptors = list(kwargs.pop("interceptors", None) or [])
110
+ interceptors.append(AsyncGRPCReplayInterceptor(fixture, target))
111
+ return orig_secure(target, credentials, interceptors=interceptors, **kwargs)
112
+
113
+ return unittest.mock.patch.multiple(
114
+ _grpc_aio,
115
+ insecure_channel=_patched_insecure,
116
+ secure_channel=_patched_secure,
117
+ )
118
+ except ImportError:
119
+ return nullcontext()
120
+
121
+
122
+ class ReplayEngine:
123
+ """Coordinates fixture loading, clock replacement, and transport patching.
124
+
125
+ Parameters
126
+ ----------
127
+ fixture_path:
128
+ Path to the SQLite fixture file produced by a recording run.
129
+ """
130
+
131
+ def __init__(self, fixture_path: Path) -> None:
132
+ self._fixture_path = fixture_path
133
+
134
+ @contextmanager
135
+ def replay(self) -> Generator[Fixture, None, None]:
136
+ """Context manager that activates full replay mode.
137
+
138
+ Yields the open Fixture so callers can inspect exchange counts or
139
+ advance the FixtureClock manually between spans.
140
+
141
+ Usage::
142
+
143
+ engine = ReplayEngine(Path("fixtures/run.db"))
144
+ with engine.replay() as fixture:
145
+ # All httpx and requests calls are served from fixture.
146
+ # All get_time() calls return recorded timestamps.
147
+ result = my_agent.run(prompt)
148
+ """
149
+ with Fixture(self._fixture_path) as fixture:
150
+ fixture.reset_read_cursor()
151
+
152
+ clock = FixtureClock(initial=fixture.earliest_timestamp())
153
+ token: Token[Any] = set_clock(clock)
154
+
155
+ # --- httpx patch -----------------------------------------------
156
+ # httpx.Client (sync) uses ReplayTransport; httpx.AsyncClient uses
157
+ # AsyncReplayTransport. Injecting a sync BaseTransport into an
158
+ # AsyncClient silently succeeds at init but raises AttributeError on
159
+ # the first request — hence the separate async variant.
160
+ # The clock is threaded through so each served exchange advances
161
+ # the FixtureClock, reproducing recorded execution timing.
162
+ original_httpx_init = httpx.Client.__init__
163
+ original_httpx_async_init = httpx.AsyncClient.__init__
164
+
165
+ def patched_httpx_init(
166
+ client_self: httpx.Client, *args: Any, **kwargs: Any
167
+ ) -> None:
168
+ kwargs.setdefault("transport", ReplayTransport(fixture, clock=clock))
169
+ original_httpx_init(client_self, *args, **kwargs)
170
+
171
+ def patched_httpx_async_init(
172
+ client_self: httpx.AsyncClient, *args: Any, **kwargs: Any
173
+ ) -> None:
174
+ kwargs.setdefault(
175
+ "transport", AsyncReplayTransport(fixture, clock=clock)
176
+ )
177
+ original_httpx_async_init(client_self, *args, **kwargs)
178
+
179
+ # --- requests patch (optional) ---------------------------------
180
+ # requests is an optional dependency. When absent, use nullcontext
181
+ # so the with-statement below is a no-op.
182
+ try:
183
+ import requests as _requests
184
+
185
+ from agent_trace.interceptor.requests_patch import ReplayAdapter
186
+
187
+ def patched_get_adapter(
188
+ session_self: Any, url: str, **kwargs: Any
189
+ ) -> Any:
190
+ return ReplayAdapter(fixture)
191
+
192
+ requests_patch: Any = unittest.mock.patch.object(
193
+ _requests.Session, "get_adapter", patched_get_adapter
194
+ )
195
+ except ImportError:
196
+ requests_patch = nullcontext()
197
+
198
+ # --- grpc patch (optional) --------------------------------------
199
+ grpc_patch = _build_grpc_replay_patch(fixture)
200
+ grpc_aio_patch = _build_grpc_aio_replay_patch(fixture)
201
+
202
+ # --- botocore patch (optional) ----------------------------------
203
+ # botocore/boto3 is an optional dependency (AWS Bedrock,
204
+ # SageMaker, ...). When absent, use nullcontext. Every service
205
+ # client's outbound request goes through the single class method
206
+ # URLLib3Session.send (see interceptor/botocore_hook.py's module
207
+ # docstring), so patching it here — like requests.get_adapter
208
+ # above — covers every boto3 client regardless of service.
209
+ try:
210
+ import botocore.httpsession as _botocore_httpsession
211
+
212
+ from agent_trace.interceptor.botocore_hook import ReplaySession
213
+
214
+ def patched_botocore_send(session_self: Any, request: Any) -> Any:
215
+ return ReplaySession(fixture, clock=clock).send(request)
216
+
217
+ botocore_patch: Any = unittest.mock.patch.object(
218
+ _botocore_httpsession.URLLib3Session,
219
+ "send",
220
+ patched_botocore_send,
221
+ )
222
+ except ImportError:
223
+ botocore_patch = nullcontext()
224
+
225
+ try:
226
+ with (
227
+ unittest.mock.patch.object(
228
+ httpx.Client, "__init__", patched_httpx_init
229
+ ),
230
+ unittest.mock.patch.object(
231
+ httpx.AsyncClient, "__init__", patched_httpx_async_init
232
+ ),
233
+ requests_patch,
234
+ grpc_patch,
235
+ grpc_aio_patch,
236
+ botocore_patch,
237
+ ):
238
+ logger.debug(
239
+ "agent-trace replay active: fixture=%s exchanges=%d",
240
+ self._fixture_path,
241
+ fixture.exchange_count(),
242
+ )
243
+ yield fixture
244
+ finally:
245
+ restore_clock(token)
246
+
247
+ def fixture_exchange_count(self) -> int:
248
+ """Return the number of recorded exchanges in the fixture.
249
+
250
+ Opens and closes the fixture transiently — use sparingly; prefer
251
+ checking inside a replay() block where the fixture is already open.
252
+ """
253
+ with Fixture(self._fixture_path) as f:
254
+ return f.exchange_count()
255
+
256
+
257
+ @contextmanager
258
+ def replay_context(fixture_path: Path) -> Generator[Fixture, None, None]:
259
+ """Convenience wrapper around ReplayEngine.replay().
260
+
261
+ Usage::
262
+
263
+ with replay_context(Path("fixtures/run.db")) as fixture:
264
+ result = my_agent.run(prompt)
265
+ """
266
+ engine = ReplayEngine(fixture_path)
267
+ with engine.replay() as fixture:
268
+ yield fixture