agentloop-profiler 0.1.0__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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Cursor encoding and validation shared by the storage backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_VALID_CURSOR_ARITIES = frozenset({2, 3})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvalidCursorError(ValueError):
|
|
14
|
+
"""Raised when a pagination cursor cannot be decoded."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def encode_cursor(parts: list[Any]) -> str:
|
|
18
|
+
raw = json.dumps(parts, separators=(",", ":")).encode("utf-8")
|
|
19
|
+
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def decode_cursor(cursor: str, *, expected_parts: int | None = None) -> list[str]:
|
|
23
|
+
"""Decode a pagination cursor and reject malformed payload shapes."""
|
|
24
|
+
try:
|
|
25
|
+
padded = cursor + "=" * (-len(cursor) % 4)
|
|
26
|
+
raw = base64.urlsafe_b64decode(padded.encode("ascii"))
|
|
27
|
+
parts: Any = json.loads(raw)
|
|
28
|
+
except (
|
|
29
|
+
binascii.Error,
|
|
30
|
+
UnicodeDecodeError,
|
|
31
|
+
UnicodeEncodeError,
|
|
32
|
+
ValueError,
|
|
33
|
+
json.JSONDecodeError,
|
|
34
|
+
) as exc:
|
|
35
|
+
raise InvalidCursorError(f"invalid pagination cursor: {cursor!r}") from exc
|
|
36
|
+
|
|
37
|
+
if not isinstance(parts, list):
|
|
38
|
+
raise InvalidCursorError(f"invalid pagination cursor: {cursor!r}")
|
|
39
|
+
valid_arity = (
|
|
40
|
+
len(parts) == expected_parts
|
|
41
|
+
if expected_parts is not None
|
|
42
|
+
else len(parts) in _VALID_CURSOR_ARITIES
|
|
43
|
+
)
|
|
44
|
+
if not valid_arity:
|
|
45
|
+
raise InvalidCursorError(f"invalid pagination cursor: {cursor!r}")
|
|
46
|
+
if any(not isinstance(part, str) or not part for part in parts):
|
|
47
|
+
raise InvalidCursorError(f"invalid pagination cursor: {cursor!r}")
|
|
48
|
+
return parts
|
agentloop/decorators.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncGenerator, Callable, Generator, Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, TypeVar, overload
|
|
9
|
+
|
|
10
|
+
from agentloop.events import format_exception_detail, new_event_id, utc_now_iso
|
|
11
|
+
from agentloop.tracer import (
|
|
12
|
+
AgentTrace,
|
|
13
|
+
bind_trace_context,
|
|
14
|
+
current_event_id,
|
|
15
|
+
current_trace,
|
|
16
|
+
record_model_call,
|
|
17
|
+
record_tool_call,
|
|
18
|
+
trace_agent,
|
|
19
|
+
trace_model_call,
|
|
20
|
+
trace_tool_call,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _call_metadata(args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
metadata: dict[str, Any] = {"integration": "agentloop.decorator"}
|
|
28
|
+
if args:
|
|
29
|
+
metadata["arg_count"] = len(args)
|
|
30
|
+
if kwargs:
|
|
31
|
+
metadata["kwarg_keys"] = sorted(str(key) for key in kwargs.keys())
|
|
32
|
+
return metadata
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _finalize_owned_trace(trace: AgentTrace) -> None:
|
|
36
|
+
"""Finish and optionally export a root trace created for a generator."""
|
|
37
|
+
|
|
38
|
+
trace.finish()
|
|
39
|
+
from agentloop.runtime import finalize_trace, should_auto_export
|
|
40
|
+
|
|
41
|
+
if should_auto_export():
|
|
42
|
+
trace.finalize_result = finalize_trace(trace)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _GeneratorSpan:
|
|
46
|
+
"""One trace event whose context is activated separately for every resume."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
span_name: str,
|
|
52
|
+
run_name: str,
|
|
53
|
+
kind: str,
|
|
54
|
+
model: str | None,
|
|
55
|
+
root: bool,
|
|
56
|
+
metadata: dict[str, Any],
|
|
57
|
+
) -> None:
|
|
58
|
+
self._span_name = span_name
|
|
59
|
+
self._run_name = run_name
|
|
60
|
+
self._kind = kind
|
|
61
|
+
self._model = model
|
|
62
|
+
self._metadata = metadata
|
|
63
|
+
self._trace = current_trace()
|
|
64
|
+
self._parent_id = current_event_id()
|
|
65
|
+
self._owns_trace = root and self._trace is None
|
|
66
|
+
if self._trace is None and not self._owns_trace:
|
|
67
|
+
raise RuntimeError("No active AgentLoop trace. Use `with trace_agent(...):` first.")
|
|
68
|
+
|
|
69
|
+
self._event_id: str | None = None
|
|
70
|
+
self._started_at: str | None = None
|
|
71
|
+
self._start_perf: float | None = None
|
|
72
|
+
self._started = False
|
|
73
|
+
self._done = False
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def done(self) -> bool:
|
|
77
|
+
return self._done
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def activate(self) -> Iterator[None]:
|
|
81
|
+
if self._done:
|
|
82
|
+
raise RuntimeError("Generator trace span is already finalized.")
|
|
83
|
+
self._start()
|
|
84
|
+
trace, event_id, _, _ = self._started_state()
|
|
85
|
+
with bind_trace_context(trace, event_id):
|
|
86
|
+
yield
|
|
87
|
+
|
|
88
|
+
def finish(self) -> None:
|
|
89
|
+
self._finalize(status="ok", error=None)
|
|
90
|
+
|
|
91
|
+
def fail(self, exc: BaseException) -> None:
|
|
92
|
+
self._finalize(status="error", error=format_exception_detail(exc))
|
|
93
|
+
|
|
94
|
+
def _start(self) -> None:
|
|
95
|
+
if self._started:
|
|
96
|
+
return
|
|
97
|
+
if self._trace is None:
|
|
98
|
+
self._trace = AgentTrace(
|
|
99
|
+
name=self._run_name,
|
|
100
|
+
metadata={"entrypoint": self._span_name},
|
|
101
|
+
)
|
|
102
|
+
self._trace._timing_active = True
|
|
103
|
+
self._event_id = new_event_id()
|
|
104
|
+
self._started_at = utc_now_iso()
|
|
105
|
+
self._start_perf = time.perf_counter()
|
|
106
|
+
self._started = True
|
|
107
|
+
|
|
108
|
+
def _started_state(self) -> tuple[AgentTrace, str, str, float]:
|
|
109
|
+
trace = self._trace
|
|
110
|
+
event_id = self._event_id
|
|
111
|
+
started_at = self._started_at
|
|
112
|
+
start_perf = self._start_perf
|
|
113
|
+
if trace is None or event_id is None or started_at is None or start_perf is None:
|
|
114
|
+
raise RuntimeError("Generator trace span failed to initialize.")
|
|
115
|
+
return trace, event_id, started_at, start_perf
|
|
116
|
+
|
|
117
|
+
def _finalize(self, *, status: str, error: str | None) -> None:
|
|
118
|
+
if self._done:
|
|
119
|
+
return
|
|
120
|
+
self._done = True
|
|
121
|
+
if not self._started:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
trace, event_id, started_at, start_perf = self._started_state()
|
|
125
|
+
ended_at = utc_now_iso()
|
|
126
|
+
duration_ms = max(0.0, (time.perf_counter() - start_perf) * 1000)
|
|
127
|
+
try:
|
|
128
|
+
if self._kind == "model":
|
|
129
|
+
record_model_call(
|
|
130
|
+
self._span_name,
|
|
131
|
+
started_at=started_at,
|
|
132
|
+
ended_at=ended_at,
|
|
133
|
+
duration_ms=duration_ms,
|
|
134
|
+
model=self._model,
|
|
135
|
+
status=status,
|
|
136
|
+
error=error,
|
|
137
|
+
metadata=self._metadata,
|
|
138
|
+
event_id=event_id,
|
|
139
|
+
parent_id=self._parent_id,
|
|
140
|
+
trace=trace,
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
record_tool_call(
|
|
144
|
+
self._span_name,
|
|
145
|
+
started_at=started_at,
|
|
146
|
+
ended_at=ended_at,
|
|
147
|
+
duration_ms=duration_ms,
|
|
148
|
+
status=status,
|
|
149
|
+
error=error,
|
|
150
|
+
metadata=self._metadata,
|
|
151
|
+
event_id=event_id,
|
|
152
|
+
parent_id=self._parent_id,
|
|
153
|
+
trace=trace,
|
|
154
|
+
)
|
|
155
|
+
finally:
|
|
156
|
+
if self._owns_trace:
|
|
157
|
+
_finalize_owned_trace(trace)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _resume_generator(
|
|
161
|
+
generator: Generator[Any, Any, Any],
|
|
162
|
+
span: _GeneratorSpan,
|
|
163
|
+
operation: Callable[..., Any],
|
|
164
|
+
*args: Any,
|
|
165
|
+
) -> tuple[bool, Any]:
|
|
166
|
+
try:
|
|
167
|
+
with span.activate():
|
|
168
|
+
return False, operation(*args)
|
|
169
|
+
except StopIteration as stop:
|
|
170
|
+
span.finish()
|
|
171
|
+
return True, stop.value
|
|
172
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
173
|
+
span.fail(exc)
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _close_generator(generator: Generator[Any, Any, Any], span: _GeneratorSpan) -> None:
|
|
178
|
+
if span.done:
|
|
179
|
+
generator.close()
|
|
180
|
+
return
|
|
181
|
+
try:
|
|
182
|
+
with span.activate():
|
|
183
|
+
generator.close()
|
|
184
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
185
|
+
span.fail(exc)
|
|
186
|
+
raise
|
|
187
|
+
span.finish()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _delegate_generator(
|
|
191
|
+
generator: Generator[Any, Any, Any],
|
|
192
|
+
span: _GeneratorSpan,
|
|
193
|
+
) -> Generator[Any, Any, Any]:
|
|
194
|
+
done, value = _resume_generator(generator, span, next, generator)
|
|
195
|
+
if done:
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
while True:
|
|
199
|
+
try:
|
|
200
|
+
sent = yield value
|
|
201
|
+
except GeneratorExit:
|
|
202
|
+
_close_generator(generator, span)
|
|
203
|
+
raise
|
|
204
|
+
except BaseException as exc: # noqa: BLE001 - forward caller exceptions unchanged
|
|
205
|
+
done, value = _resume_generator(generator, span, generator.throw, exc)
|
|
206
|
+
else:
|
|
207
|
+
done, value = _resume_generator(generator, span, generator.send, sent)
|
|
208
|
+
if done:
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def _resume_async_generator(
|
|
213
|
+
generator: AsyncGenerator[Any, Any],
|
|
214
|
+
span: _GeneratorSpan,
|
|
215
|
+
operation: Callable[..., Any],
|
|
216
|
+
*args: Any,
|
|
217
|
+
) -> tuple[bool, Any]:
|
|
218
|
+
try:
|
|
219
|
+
with span.activate():
|
|
220
|
+
return False, await operation(*args)
|
|
221
|
+
except StopAsyncIteration:
|
|
222
|
+
span.finish()
|
|
223
|
+
return True, None
|
|
224
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
225
|
+
span.fail(exc)
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def _close_async_generator(
|
|
230
|
+
generator: AsyncGenerator[Any, Any],
|
|
231
|
+
span: _GeneratorSpan,
|
|
232
|
+
) -> None:
|
|
233
|
+
if span.done:
|
|
234
|
+
await generator.aclose()
|
|
235
|
+
return
|
|
236
|
+
try:
|
|
237
|
+
with span.activate():
|
|
238
|
+
await generator.aclose()
|
|
239
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
240
|
+
span.fail(exc)
|
|
241
|
+
raise
|
|
242
|
+
span.finish()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def _delegate_async_generator(
|
|
246
|
+
generator: AsyncGenerator[Any, Any],
|
|
247
|
+
span: _GeneratorSpan,
|
|
248
|
+
) -> AsyncGenerator[Any, Any]:
|
|
249
|
+
done, value = await _resume_async_generator(generator, span, generator.__anext__)
|
|
250
|
+
if done:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
while True:
|
|
254
|
+
try:
|
|
255
|
+
sent = yield value
|
|
256
|
+
except GeneratorExit:
|
|
257
|
+
await _close_async_generator(generator, span)
|
|
258
|
+
return
|
|
259
|
+
except BaseException as exc: # noqa: BLE001 - forward caller exceptions unchanged
|
|
260
|
+
done, value = await _resume_async_generator(generator, span, generator.athrow, exc)
|
|
261
|
+
else:
|
|
262
|
+
done, value = await _resume_async_generator(generator, span, generator.asend, sent)
|
|
263
|
+
if done:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@overload
|
|
268
|
+
def traceable(fn: F) -> F: ...
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@overload
|
|
272
|
+
def traceable(
|
|
273
|
+
fn: None = None,
|
|
274
|
+
*,
|
|
275
|
+
name: str | None = None,
|
|
276
|
+
kind: str = "tool",
|
|
277
|
+
model: str | None = None,
|
|
278
|
+
root: bool = False,
|
|
279
|
+
agent_name: str | None = None,
|
|
280
|
+
) -> Callable[[F], F]: ...
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def traceable(
|
|
284
|
+
fn: F | None = None,
|
|
285
|
+
*,
|
|
286
|
+
name: str | None = None,
|
|
287
|
+
kind: str = "tool",
|
|
288
|
+
model: str | None = None,
|
|
289
|
+
root: bool = False,
|
|
290
|
+
agent_name: str | None = None,
|
|
291
|
+
) -> F | Callable[[F], F]:
|
|
292
|
+
"""Trace a normal Python function without changing its implementation.
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import agentloop
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@agentloop.traceable(root=True, agent_name="research_agent")
|
|
301
|
+
def run_agent(question: str):
|
|
302
|
+
return plan(question)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@agentloop.traceable(kind="model", model="gpt-4.1-mini")
|
|
306
|
+
def plan(question: str):
|
|
307
|
+
return client.responses.create(...)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@agentloop.traceable(kind="tool")
|
|
311
|
+
def search_web(query: str):
|
|
312
|
+
return search(query)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`root=True` starts an AgentLoop run when no active run exists. Nested calls are
|
|
316
|
+
recorded as spans inside that run. This gives teams a low-friction migration
|
|
317
|
+
path when they do not use LangGraph, CrewAI, or another supported framework.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
normalized_kind = kind.lower().replace("-", "_")
|
|
321
|
+
if normalized_kind not in {"tool", "model"}:
|
|
322
|
+
raise ValueError("kind must be 'tool' or 'model'")
|
|
323
|
+
|
|
324
|
+
def decorator(func: F) -> F:
|
|
325
|
+
span_name = name or getattr(
|
|
326
|
+
func, "__name__", getattr(func, "__qualname__", "agentloop.call")
|
|
327
|
+
)
|
|
328
|
+
run_name = agent_name or span_name
|
|
329
|
+
|
|
330
|
+
if inspect.isasyncgenfunction(func):
|
|
331
|
+
|
|
332
|
+
@wraps(func)
|
|
333
|
+
def async_gen_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
334
|
+
span = _GeneratorSpan(
|
|
335
|
+
span_name=span_name,
|
|
336
|
+
run_name=run_name,
|
|
337
|
+
kind=normalized_kind,
|
|
338
|
+
model=model,
|
|
339
|
+
root=root,
|
|
340
|
+
metadata=_call_metadata(args, kwargs),
|
|
341
|
+
)
|
|
342
|
+
return _delegate_async_generator(func(*args, **kwargs), span)
|
|
343
|
+
|
|
344
|
+
return async_gen_wrapper # type: ignore[return-value]
|
|
345
|
+
|
|
346
|
+
if inspect.iscoroutinefunction(func):
|
|
347
|
+
|
|
348
|
+
@wraps(func)
|
|
349
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
350
|
+
if root and current_trace() is None:
|
|
351
|
+
with trace_agent(run_name, metadata={"entrypoint": span_name}):
|
|
352
|
+
return await _run_async_span(
|
|
353
|
+
func, span_name, normalized_kind, model, args, kwargs
|
|
354
|
+
)
|
|
355
|
+
return await _run_async_span(func, span_name, normalized_kind, model, args, kwargs)
|
|
356
|
+
|
|
357
|
+
return async_wrapper # type: ignore[return-value]
|
|
358
|
+
|
|
359
|
+
if inspect.isgeneratorfunction(func):
|
|
360
|
+
|
|
361
|
+
@wraps(func)
|
|
362
|
+
def gen_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
363
|
+
span = _GeneratorSpan(
|
|
364
|
+
span_name=span_name,
|
|
365
|
+
run_name=run_name,
|
|
366
|
+
kind=normalized_kind,
|
|
367
|
+
model=model,
|
|
368
|
+
root=root,
|
|
369
|
+
metadata=_call_metadata(args, kwargs),
|
|
370
|
+
)
|
|
371
|
+
return _delegate_generator(func(*args, **kwargs), span)
|
|
372
|
+
|
|
373
|
+
return gen_wrapper # type: ignore[return-value]
|
|
374
|
+
|
|
375
|
+
@wraps(func)
|
|
376
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
377
|
+
if root and current_trace() is None:
|
|
378
|
+
with trace_agent(run_name, metadata={"entrypoint": span_name}):
|
|
379
|
+
return _run_sync_span(func, span_name, normalized_kind, model, args, kwargs)
|
|
380
|
+
return _run_sync_span(func, span_name, normalized_kind, model, args, kwargs)
|
|
381
|
+
|
|
382
|
+
return wrapper # type: ignore[return-value]
|
|
383
|
+
|
|
384
|
+
if fn is not None:
|
|
385
|
+
return decorator(fn)
|
|
386
|
+
return decorator
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _span_cm(
|
|
390
|
+
span_name: str,
|
|
391
|
+
kind: str,
|
|
392
|
+
model: str | None,
|
|
393
|
+
args: tuple[Any, ...],
|
|
394
|
+
kwargs: dict[str, Any],
|
|
395
|
+
) -> Any:
|
|
396
|
+
"""Return the span context manager for a regular model or tool call."""
|
|
397
|
+
|
|
398
|
+
metadata = _call_metadata(args, kwargs)
|
|
399
|
+
if kind == "model":
|
|
400
|
+
return trace_model_call(span_name, model=model, metadata=metadata)
|
|
401
|
+
return trace_tool_call(span_name, metadata=metadata)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def _run_async_span(
|
|
405
|
+
func: Callable[..., Any],
|
|
406
|
+
span_name: str,
|
|
407
|
+
kind: str,
|
|
408
|
+
model: str | None,
|
|
409
|
+
args: tuple[Any, ...],
|
|
410
|
+
kwargs: dict[str, Any],
|
|
411
|
+
) -> Any:
|
|
412
|
+
with _span_cm(span_name, kind, model, args, kwargs):
|
|
413
|
+
return await func(*args, **kwargs)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _run_sync_span(
|
|
417
|
+
func: Callable[..., Any],
|
|
418
|
+
span_name: str,
|
|
419
|
+
kind: str,
|
|
420
|
+
model: str | None,
|
|
421
|
+
args: tuple[Any, ...],
|
|
422
|
+
kwargs: dict[str, Any],
|
|
423
|
+
) -> Any:
|
|
424
|
+
with _span_cm(span_name, kind, model, args, kwargs):
|
|
425
|
+
return func(*args, **kwargs)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def trace_model(
|
|
429
|
+
*,
|
|
430
|
+
name: str | None = None,
|
|
431
|
+
model: str | None = None,
|
|
432
|
+
root: bool = False,
|
|
433
|
+
agent_name: str | None = None,
|
|
434
|
+
) -> Callable[[F], F]:
|
|
435
|
+
"""Convenience decorator for model-call-like functions."""
|
|
436
|
+
|
|
437
|
+
return traceable(name=name, kind="model", model=model, root=root, agent_name=agent_name)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def trace_tool(
|
|
441
|
+
*, name: str | None = None, root: bool = False, agent_name: str | None = None
|
|
442
|
+
) -> Callable[[F], F]:
|
|
443
|
+
"""Convenience decorator for tool-call-like functions."""
|
|
444
|
+
|
|
445
|
+
return traceable(name=name, kind="tool", root=root, agent_name=agent_name)
|