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
agentloop/tracer.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterator
|
|
9
|
+
|
|
10
|
+
from agentloop.costs import format_cost_usd
|
|
11
|
+
from agentloop.events import (
|
|
12
|
+
AgentEvent,
|
|
13
|
+
format_exception_detail,
|
|
14
|
+
new_event_id,
|
|
15
|
+
new_run_id,
|
|
16
|
+
utc_now_iso,
|
|
17
|
+
)
|
|
18
|
+
from agentloop.metrics import build_report
|
|
19
|
+
from agentloop.schema import SCHEMA_VERSION, validate_elapsed_ms, validate_trace_dict
|
|
20
|
+
from agentloop.tokens import (
|
|
21
|
+
ESTIMATED_WORDS,
|
|
22
|
+
UNAVAILABLE,
|
|
23
|
+
USER_SUPPLIED,
|
|
24
|
+
describe_token_status,
|
|
25
|
+
is_token_basis_exact,
|
|
26
|
+
validate_provenance,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_current_trace: ContextVar["AgentTrace | None"] = ContextVar(
|
|
30
|
+
"agentloop_current_trace", default=None
|
|
31
|
+
)
|
|
32
|
+
_current_event_id: ContextVar[str | None] = ContextVar("agentloop_current_event_id", default=None)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _count_tokens(text: str | None) -> int:
|
|
36
|
+
"""Approximate a token count from whitespace words.
|
|
37
|
+
|
|
38
|
+
This is a proxy, not a token count. Every caller must record
|
|
39
|
+
:data:`agentloop.tokens.ESTIMATED_WORDS` provenance alongside the number so
|
|
40
|
+
downstream cost, replay, and research output never present it as exact usage.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if not text:
|
|
44
|
+
return 0
|
|
45
|
+
return max(1, len(text.split()))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_token_counts(
|
|
49
|
+
input_tokens: int | None,
|
|
50
|
+
output_tokens: int | None,
|
|
51
|
+
input_text: str | None,
|
|
52
|
+
output_text: str | None,
|
|
53
|
+
) -> tuple[int, int, str]:
|
|
54
|
+
"""Resolve a model call's token counts and record where they came from.
|
|
55
|
+
|
|
56
|
+
Explicit counts are used verbatim and marked ``user_supplied``. A side with no
|
|
57
|
+
explicit count falls back to the word-count proxy; if that proxy actually
|
|
58
|
+
contributes a number, the whole event is marked ``estimated_words`` — the
|
|
59
|
+
weaker of the two provenances, because a total mixing a real count with an
|
|
60
|
+
approximation is itself an approximation. With neither counts nor text there
|
|
61
|
+
is nothing to report, which is ``unavailable`` rather than a confident zero.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
resolved_input = input_tokens if input_tokens is not None else _count_tokens(input_text)
|
|
65
|
+
resolved_output = output_tokens if output_tokens is not None else _count_tokens(output_text)
|
|
66
|
+
estimated = (input_tokens is None and resolved_input > 0) or (
|
|
67
|
+
output_tokens is None and resolved_output > 0
|
|
68
|
+
)
|
|
69
|
+
if estimated:
|
|
70
|
+
provenance = ESTIMATED_WORDS
|
|
71
|
+
elif input_tokens is None and output_tokens is None:
|
|
72
|
+
provenance = UNAVAILABLE
|
|
73
|
+
else:
|
|
74
|
+
provenance = USER_SUPPLIED
|
|
75
|
+
return resolved_input, resolved_output, provenance
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AgentTrace:
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
name: str,
|
|
82
|
+
run_id: str | None = None,
|
|
83
|
+
metadata: dict[str, Any] | None = None,
|
|
84
|
+
*,
|
|
85
|
+
started_at: str | None = None,
|
|
86
|
+
ended_at: str | None = None,
|
|
87
|
+
elapsed_ms: float | None = None,
|
|
88
|
+
):
|
|
89
|
+
self.name = name
|
|
90
|
+
self.run_id = run_id or new_run_id()
|
|
91
|
+
self.metadata = metadata or {}
|
|
92
|
+
self.events: list[AgentEvent] = []
|
|
93
|
+
self.started_at = started_at or utc_now_iso()
|
|
94
|
+
self.ended_at = ended_at
|
|
95
|
+
self.elapsed_ms = validate_elapsed_ms(elapsed_ms)
|
|
96
|
+
self._start_perf = time.perf_counter()
|
|
97
|
+
self._timing_active = False
|
|
98
|
+
self.finalize_result: dict[str, Any] | None = None
|
|
99
|
+
|
|
100
|
+
def add_event(self, event: AgentEvent) -> None:
|
|
101
|
+
self.events.append(event)
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict[str, Any]:
|
|
104
|
+
return {
|
|
105
|
+
"schema_version": SCHEMA_VERSION,
|
|
106
|
+
"name": self.name,
|
|
107
|
+
"run_id": self.run_id,
|
|
108
|
+
"started_at": self.started_at,
|
|
109
|
+
"ended_at": self.ended_at,
|
|
110
|
+
"elapsed_ms": self.elapsed_ms,
|
|
111
|
+
"metadata": self.metadata,
|
|
112
|
+
"events": [event.to_dict() for event in self.events],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_dict(cls, data: dict[str, Any]) -> "AgentTrace":
|
|
117
|
+
"""Deserialize a trace, validating the shared schema contract.
|
|
118
|
+
|
|
119
|
+
Raises :class:`agentloop.schema.TraceValidationError` (a ``ValueError``)
|
|
120
|
+
when the envelope or any event violates the schema. Traces without a
|
|
121
|
+
``schema_version`` (0.4-era files) are accepted for backward
|
|
122
|
+
compatibility.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
validate_trace_dict(data)
|
|
126
|
+
trace = cls(
|
|
127
|
+
name=data["name"],
|
|
128
|
+
run_id=data["run_id"],
|
|
129
|
+
metadata=data.get("metadata", {}),
|
|
130
|
+
started_at=data.get("started_at"),
|
|
131
|
+
ended_at=data.get("ended_at"),
|
|
132
|
+
elapsed_ms=data.get("elapsed_ms"),
|
|
133
|
+
)
|
|
134
|
+
trace.events = [
|
|
135
|
+
AgentEvent.from_dict(item, index=index)
|
|
136
|
+
for index, item in enumerate(data.get("events", []))
|
|
137
|
+
]
|
|
138
|
+
return trace
|
|
139
|
+
|
|
140
|
+
def finish(self) -> None:
|
|
141
|
+
"""Finalize trace timing once, using a monotonic clock when available."""
|
|
142
|
+
|
|
143
|
+
if self.ended_at is not None and self.elapsed_ms is not None:
|
|
144
|
+
self._timing_active = False
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if self.ended_at is None:
|
|
148
|
+
self.ended_at = utc_now_iso()
|
|
149
|
+
if self.elapsed_ms is None:
|
|
150
|
+
if self._timing_active:
|
|
151
|
+
self.elapsed_ms = max(0.0, (time.perf_counter() - self._start_perf) * 1000)
|
|
152
|
+
else:
|
|
153
|
+
from agentloop.timing import elapsed_runtime_ms
|
|
154
|
+
|
|
155
|
+
self.elapsed_ms = elapsed_runtime_ms(self)
|
|
156
|
+
self._timing_active = False
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_json(cls, path: str | Path) -> "AgentTrace":
|
|
160
|
+
return cls.from_dict(json.loads(Path(path).read_text(encoding="utf-8")))
|
|
161
|
+
|
|
162
|
+
def export_json(self, path: str | Path) -> Path:
|
|
163
|
+
out = Path(path)
|
|
164
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
out.write_text(json.dumps(self.to_dict(), indent=2), encoding="utf-8")
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
def report(self) -> dict[str, Any]:
|
|
169
|
+
return build_report(self)
|
|
170
|
+
|
|
171
|
+
def print_report(self) -> None:
|
|
172
|
+
report = self.report()
|
|
173
|
+
print(f"AgentLoop Report: {self.name}")
|
|
174
|
+
print(f"Run ID: {self.run_id}")
|
|
175
|
+
from agentloop.timing import format_duration_ms
|
|
176
|
+
|
|
177
|
+
print(f"Total runtime: {format_duration_ms(report['total_runtime_ms'])}")
|
|
178
|
+
token_status = report.get("token_status", "unspecified")
|
|
179
|
+
print(
|
|
180
|
+
"Estimated cost: "
|
|
181
|
+
+ format_cost_usd(
|
|
182
|
+
report.get("estimated_cost_usd"),
|
|
183
|
+
report.get("cost_status", "complete"),
|
|
184
|
+
token_status=token_status,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
print(f"Model time: {format_duration_ms(report['model_time_ms'])}")
|
|
188
|
+
print(f"Tool time: {format_duration_ms(report['tool_time_ms'])}")
|
|
189
|
+
print(f"Retry time: {format_duration_ms(report['retry_time_ms'])}")
|
|
190
|
+
token_note = "" if is_token_basis_exact(token_status) else f" [{token_status}]"
|
|
191
|
+
print(f"Input tokens: {report['input_tokens']}{token_note}")
|
|
192
|
+
print(f"Output tokens: {report['output_tokens']}{token_note}")
|
|
193
|
+
if not is_token_basis_exact(token_status):
|
|
194
|
+
print(f" Token basis: {describe_token_status(token_status)}")
|
|
195
|
+
ratio = report["repeated_context_ratio"]
|
|
196
|
+
print("Repeated context ratio: " + ("unavailable" if ratio is None else f"{ratio:.1%}"))
|
|
197
|
+
print("\nRecommendations:")
|
|
198
|
+
for rec in report["recommendations"]:
|
|
199
|
+
print(f"- {rec['title']}: {rec['description']}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def current_trace() -> AgentTrace | None:
|
|
203
|
+
"""Return the active AgentLoop trace, if one is running in this context."""
|
|
204
|
+
|
|
205
|
+
return _current_trace.get()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def current_event_id() -> str | None:
|
|
209
|
+
"""Return the event id currently being recorded under, if any."""
|
|
210
|
+
|
|
211
|
+
return _current_event_id.get()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@contextmanager
|
|
215
|
+
def bind_trace_context(trace: AgentTrace | None, event_id: str | None = None) -> Iterator[None]:
|
|
216
|
+
"""Temporarily bind a captured trace and parent event in the current context.
|
|
217
|
+
|
|
218
|
+
``trace=None`` clears the ambient trace; ``event_id=None`` clears its parent.
|
|
219
|
+
On exit, both previous bindings
|
|
220
|
+
are restored, including when an exception or cancellation propagates. This
|
|
221
|
+
helper does not create events, start timers, finish traces, or export them.
|
|
222
|
+
Enter and exit each binding in the same execution context; generator adapters
|
|
223
|
+
should use a fresh binding for each resume/close operation, ending it before
|
|
224
|
+
yielding to their caller.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
trace_token = _current_trace.set(trace)
|
|
228
|
+
event_token = _current_event_id.set(event_id)
|
|
229
|
+
try:
|
|
230
|
+
yield
|
|
231
|
+
finally:
|
|
232
|
+
_current_event_id.reset(event_token)
|
|
233
|
+
_current_trace.reset(trace_token)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@contextmanager
|
|
237
|
+
def _trace_execution(name: str, metadata: dict[str, Any] | None = None) -> Iterator[AgentTrace]:
|
|
238
|
+
trace = AgentTrace(name=name, metadata=metadata)
|
|
239
|
+
trace._timing_active = True
|
|
240
|
+
token = _current_trace.set(trace)
|
|
241
|
+
event_token = _current_event_id.set(None)
|
|
242
|
+
try:
|
|
243
|
+
yield trace
|
|
244
|
+
finally:
|
|
245
|
+
trace.finish()
|
|
246
|
+
_current_event_id.reset(event_token)
|
|
247
|
+
_current_trace.reset(token)
|
|
248
|
+
from agentloop.runtime import finalize_trace, should_auto_export
|
|
249
|
+
|
|
250
|
+
if should_auto_export():
|
|
251
|
+
trace.finalize_result = finalize_trace(trace)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@contextmanager
|
|
255
|
+
def trace_agent(name: str, metadata: dict[str, Any] | None = None) -> Iterator[AgentTrace]:
|
|
256
|
+
"""Backward-compatible agent scope sharing the generic execution lifecycle."""
|
|
257
|
+
with _trace_execution(name, metadata) as trace:
|
|
258
|
+
yield trace
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def record_model_call(
|
|
262
|
+
name: str,
|
|
263
|
+
*,
|
|
264
|
+
duration_ms: float,
|
|
265
|
+
started_at: str,
|
|
266
|
+
ended_at: str | None = None,
|
|
267
|
+
model: str | None = None,
|
|
268
|
+
input_tokens: int = 0,
|
|
269
|
+
output_tokens: int = 0,
|
|
270
|
+
token_provenance: str | None = None,
|
|
271
|
+
input_text: str | None = None,
|
|
272
|
+
output_text: str | None = None,
|
|
273
|
+
status: str = "ok",
|
|
274
|
+
error: str | None = None,
|
|
275
|
+
metadata: dict[str, Any] | None = None,
|
|
276
|
+
event_id: str | None = None,
|
|
277
|
+
parent_id: str | None = None,
|
|
278
|
+
trace: AgentTrace | None = None,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Record a completed model call into a trace.
|
|
281
|
+
|
|
282
|
+
Integrations use this when token counts are only known after a framework call
|
|
283
|
+
returns. User code should usually prefer `trace_model_call(...)`.
|
|
284
|
+
|
|
285
|
+
``token_provenance`` records where the token counts came from; an integration
|
|
286
|
+
that read a provider usage object should pass
|
|
287
|
+
:data:`agentloop.tokens.PROVIDER`. When omitted it is inferred from the counts
|
|
288
|
+
themselves: any non-zero count is treated as ``user_supplied`` (this entry
|
|
289
|
+
point takes explicit integers, so a caller passing one is supplying it), and
|
|
290
|
+
an all-zero call is recorded as ``unavailable`` rather than as a confident
|
|
291
|
+
zero. Passing a value AgentLoop does not define raises
|
|
292
|
+
:class:`agentloop.tokens.TokenProvenanceError`.
|
|
293
|
+
|
|
294
|
+
By default the event is recorded into the trace active in the current context.
|
|
295
|
+
Pass ``trace`` to record into a specific trace instead — for example an
|
|
296
|
+
integration that captured trace ownership when a streaming call was invoked and
|
|
297
|
+
finalizes it later, possibly in a different context. When ``trace`` is given the
|
|
298
|
+
ambient event-id context is not used for the parent.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
target = trace if trace is not None else _require_trace()
|
|
302
|
+
resolved_parent = (
|
|
303
|
+
parent_id
|
|
304
|
+
if parent_id is not None
|
|
305
|
+
else (None if trace is not None else _current_event_id.get())
|
|
306
|
+
)
|
|
307
|
+
if token_provenance is None:
|
|
308
|
+
resolved_provenance = USER_SUPPLIED if (input_tokens or output_tokens) else UNAVAILABLE
|
|
309
|
+
else:
|
|
310
|
+
resolved_provenance = validate_provenance(token_provenance)
|
|
311
|
+
target.add_event(
|
|
312
|
+
AgentEvent(
|
|
313
|
+
event_id=event_id or new_event_id(),
|
|
314
|
+
run_id=target.run_id,
|
|
315
|
+
event_type="model_call",
|
|
316
|
+
name=name,
|
|
317
|
+
started_at=started_at,
|
|
318
|
+
ended_at=ended_at or utc_now_iso(),
|
|
319
|
+
duration_ms=duration_ms,
|
|
320
|
+
parent_id=resolved_parent,
|
|
321
|
+
model=model,
|
|
322
|
+
input_tokens=input_tokens,
|
|
323
|
+
output_tokens=output_tokens,
|
|
324
|
+
token_provenance=resolved_provenance,
|
|
325
|
+
input_text=input_text,
|
|
326
|
+
output_text=output_text,
|
|
327
|
+
status=status,
|
|
328
|
+
error=error,
|
|
329
|
+
metadata=metadata or {},
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def record_tool_call(
|
|
335
|
+
name: str,
|
|
336
|
+
*,
|
|
337
|
+
duration_ms: float,
|
|
338
|
+
started_at: str,
|
|
339
|
+
ended_at: str | None = None,
|
|
340
|
+
status: str = "ok",
|
|
341
|
+
error: str | None = None,
|
|
342
|
+
metadata: dict[str, Any] | None = None,
|
|
343
|
+
event_id: str | None = None,
|
|
344
|
+
parent_id: str | None = None,
|
|
345
|
+
trace: AgentTrace | None = None,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Record a completed tool/framework step into a trace.
|
|
348
|
+
|
|
349
|
+
Without ``trace``, use the active trace and inherit its current parent unless
|
|
350
|
+
``parent_id`` is supplied. An explicit ``trace`` ignores the ambient parent:
|
|
351
|
+
pass a captured ``parent_id`` to attach the event, or omit it for a root event.
|
|
352
|
+
This matches ``record_model_call`` and never changes the active context.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
target = trace if trace is not None else _require_trace()
|
|
356
|
+
resolved_parent = (
|
|
357
|
+
parent_id
|
|
358
|
+
if parent_id is not None
|
|
359
|
+
else (None if trace is not None else _current_event_id.get())
|
|
360
|
+
)
|
|
361
|
+
target.add_event(
|
|
362
|
+
AgentEvent(
|
|
363
|
+
event_id=event_id or new_event_id(),
|
|
364
|
+
run_id=target.run_id,
|
|
365
|
+
event_type="tool_call",
|
|
366
|
+
name=name,
|
|
367
|
+
started_at=started_at,
|
|
368
|
+
ended_at=ended_at or utc_now_iso(),
|
|
369
|
+
duration_ms=duration_ms,
|
|
370
|
+
parent_id=resolved_parent,
|
|
371
|
+
status=status,
|
|
372
|
+
error=error,
|
|
373
|
+
metadata=metadata or {},
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@contextmanager
|
|
379
|
+
def trace_model_call(
|
|
380
|
+
name: str,
|
|
381
|
+
model: str | None = None,
|
|
382
|
+
input_tokens: int | None = None,
|
|
383
|
+
output_tokens: int | None = None,
|
|
384
|
+
input_text: str | None = None,
|
|
385
|
+
output_text: str | None = None,
|
|
386
|
+
metadata: dict[str, Any] | None = None,
|
|
387
|
+
) -> Iterator[None]:
|
|
388
|
+
_require_trace()
|
|
389
|
+
event_id = new_event_id()
|
|
390
|
+
parent_id = _current_event_id.get()
|
|
391
|
+
event_token = _current_event_id.set(event_id)
|
|
392
|
+
started_at = utc_now_iso()
|
|
393
|
+
start = time.perf_counter()
|
|
394
|
+
status = "ok"
|
|
395
|
+
error = None
|
|
396
|
+
try:
|
|
397
|
+
yield
|
|
398
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
399
|
+
status = "error"
|
|
400
|
+
error = format_exception_detail(exc)
|
|
401
|
+
raise
|
|
402
|
+
finally:
|
|
403
|
+
ended_at = utc_now_iso()
|
|
404
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
405
|
+
_current_event_id.reset(event_token)
|
|
406
|
+
resolved_input, resolved_output, provenance = _resolve_token_counts(
|
|
407
|
+
input_tokens, output_tokens, input_text, output_text
|
|
408
|
+
)
|
|
409
|
+
record_model_call(
|
|
410
|
+
name,
|
|
411
|
+
started_at=started_at,
|
|
412
|
+
ended_at=ended_at,
|
|
413
|
+
duration_ms=duration_ms,
|
|
414
|
+
model=model,
|
|
415
|
+
input_tokens=resolved_input,
|
|
416
|
+
output_tokens=resolved_output,
|
|
417
|
+
token_provenance=provenance,
|
|
418
|
+
input_text=input_text,
|
|
419
|
+
output_text=output_text,
|
|
420
|
+
status=status,
|
|
421
|
+
error=error,
|
|
422
|
+
metadata=metadata or {},
|
|
423
|
+
event_id=event_id,
|
|
424
|
+
parent_id=parent_id,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@contextmanager
|
|
429
|
+
def trace_tool_call(name: str, metadata: dict[str, Any] | None = None) -> Iterator[None]:
|
|
430
|
+
_require_trace()
|
|
431
|
+
event_id = new_event_id()
|
|
432
|
+
parent_id = _current_event_id.get()
|
|
433
|
+
event_token = _current_event_id.set(event_id)
|
|
434
|
+
started_at = utc_now_iso()
|
|
435
|
+
start = time.perf_counter()
|
|
436
|
+
status = "ok"
|
|
437
|
+
error = None
|
|
438
|
+
try:
|
|
439
|
+
yield
|
|
440
|
+
except BaseException as exc: # noqa: BLE001 - record then propagate unchanged
|
|
441
|
+
status = "error"
|
|
442
|
+
error = format_exception_detail(exc)
|
|
443
|
+
raise
|
|
444
|
+
finally:
|
|
445
|
+
ended_at = utc_now_iso()
|
|
446
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
447
|
+
_current_event_id.reset(event_token)
|
|
448
|
+
record_tool_call(
|
|
449
|
+
name,
|
|
450
|
+
started_at=started_at,
|
|
451
|
+
ended_at=ended_at,
|
|
452
|
+
duration_ms=duration_ms,
|
|
453
|
+
status=status,
|
|
454
|
+
error=error,
|
|
455
|
+
metadata=metadata or {},
|
|
456
|
+
event_id=event_id,
|
|
457
|
+
parent_id=parent_id,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@contextmanager
|
|
462
|
+
def trace_retry(name: str, metadata: dict[str, Any] | None = None) -> Iterator[None]:
|
|
463
|
+
trace = _require_trace()
|
|
464
|
+
event_id = new_event_id()
|
|
465
|
+
parent_id = _current_event_id.get()
|
|
466
|
+
event_token = _current_event_id.set(event_id)
|
|
467
|
+
started_at = utc_now_iso()
|
|
468
|
+
start = time.perf_counter()
|
|
469
|
+
try:
|
|
470
|
+
yield
|
|
471
|
+
finally:
|
|
472
|
+
ended_at = utc_now_iso()
|
|
473
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
474
|
+
_current_event_id.reset(event_token)
|
|
475
|
+
trace.add_event(
|
|
476
|
+
AgentEvent(
|
|
477
|
+
event_id=event_id,
|
|
478
|
+
run_id=trace.run_id,
|
|
479
|
+
event_type="retry",
|
|
480
|
+
name=name,
|
|
481
|
+
started_at=started_at,
|
|
482
|
+
ended_at=ended_at,
|
|
483
|
+
duration_ms=duration_ms,
|
|
484
|
+
parent_id=parent_id,
|
|
485
|
+
metadata=metadata or {},
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _require_trace() -> AgentTrace:
|
|
491
|
+
trace = _current_trace.get()
|
|
492
|
+
if trace is None:
|
|
493
|
+
raise RuntimeError("No active AgentLoop trace. Use `with trace_agent(...):` first.")
|
|
494
|
+
return trace
|