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 @@
|
|
|
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
|