agentloop-profiler 0.4.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.
Potentially problematic release.
This version of agentloop-profiler might be problematic. Click here for more details.
- agentloop/__init__.py +68 -0
- agentloop/audit.py +34 -0
- agentloop/autoinstrument.py +102 -0
- agentloop/ci.py +135 -0
- agentloop/cli.py +876 -0
- agentloop/client.py +170 -0
- agentloop/config.py +50 -0
- agentloop/costs.py +15 -0
- agentloop/decorators.py +160 -0
- agentloop/demo.py +283 -0
- agentloop/doctor.py +344 -0
- agentloop/events.py +49 -0
- agentloop/exporters.py +60 -0
- agentloop/findings.py +215 -0
- agentloop/graph.py +312 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/langgraph.py +140 -0
- agentloop/integrations/openai.py +369 -0
- agentloop/integrations/openai_agents.py +212 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/issues.py +88 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +123 -0
- agentloop/optimizer.py +323 -0
- agentloop/otel.py +302 -0
- agentloop/otel_ids.py +81 -0
- agentloop/patches.py +583 -0
- agentloop/plan_export.py +72 -0
- agentloop/quality.py +222 -0
- agentloop/replay.py +424 -0
- agentloop/runtime.py +244 -0
- agentloop/savings.py +135 -0
- agentloop/server.py +292 -0
- agentloop/store.py +840 -0
- agentloop/timing.py +124 -0
- agentloop/tracer.py +356 -0
- agentloop/value.py +243 -0
- agentloop/version.py +3 -0
- agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
- agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
- agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
- agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +1 -0
- dashboard/app.py +698 -0
- dashboard/value_view.py +101 -0
agentloop/timing.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def timestamp_ms(value: Any) -> float | None:
|
|
10
|
+
"""Parse an ISO-8601 timestamp into milliseconds since the Unix epoch."""
|
|
11
|
+
|
|
12
|
+
if not isinstance(value, str) or not value:
|
|
13
|
+
return None
|
|
14
|
+
try:
|
|
15
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
16
|
+
except ValueError:
|
|
17
|
+
return None
|
|
18
|
+
if parsed.tzinfo is None:
|
|
19
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
20
|
+
result = parsed.timestamp() * 1000
|
|
21
|
+
return result if math.isfinite(result) else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def duration_ms(value: Any) -> float:
|
|
25
|
+
"""Return a finite, non-negative duration for an event-like object."""
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = float(getattr(value, "duration_ms", 0.0) or 0.0)
|
|
29
|
+
except (TypeError, ValueError):
|
|
30
|
+
return 0.0
|
|
31
|
+
return result if math.isfinite(result) and result >= 0 else 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def event_interval_ms(event: Any) -> tuple[float, float] | None:
|
|
35
|
+
"""Return a trustworthy timestamp interval for an event, when available.
|
|
36
|
+
|
|
37
|
+
Older adapters sometimes filled both timestamps with the current time while
|
|
38
|
+
carrying the real duration in ``duration_ms``. Treat a large disagreement
|
|
39
|
+
between those values as missing timing data so legacy traces can use the
|
|
40
|
+
cumulative-duration fallback instead of reporting an almost-zero runtime.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
started = timestamp_ms(getattr(event, "started_at", None))
|
|
44
|
+
ended = timestamp_ms(getattr(event, "ended_at", None))
|
|
45
|
+
if started is None or ended is None or ended < started:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
recorded_duration = duration_ms(event)
|
|
49
|
+
observed_duration = ended - started
|
|
50
|
+
if recorded_duration > 0:
|
|
51
|
+
tolerance = max(5.0, recorded_duration * 0.10)
|
|
52
|
+
if abs(observed_duration - recorded_duration) > tolerance:
|
|
53
|
+
return None
|
|
54
|
+
return started, ended
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def event_time_bounds(events: Iterable[Any]) -> tuple[float, float] | None:
|
|
58
|
+
"""Return the earliest reliable start and latest reliable end.
|
|
59
|
+
|
|
60
|
+
A positive-duration event without trustworthy timestamps makes the complete
|
|
61
|
+
trace envelope unknowable, so callers should fall back to cumulative work.
|
|
62
|
+
Zero-duration events without timestamps do not affect either result.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
earliest: float | None = None
|
|
66
|
+
latest: float | None = None
|
|
67
|
+
for event in events:
|
|
68
|
+
interval = event_interval_ms(event)
|
|
69
|
+
if interval is None:
|
|
70
|
+
if duration_ms(event) > 0:
|
|
71
|
+
return None
|
|
72
|
+
continue
|
|
73
|
+
started, ended = interval
|
|
74
|
+
earliest = started if earliest is None else min(earliest, started)
|
|
75
|
+
latest = ended if latest is None else max(latest, ended)
|
|
76
|
+
if earliest is None or latest is None:
|
|
77
|
+
return None
|
|
78
|
+
return earliest, latest
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cumulative_span_time_ms(events: Iterable[Any]) -> float:
|
|
82
|
+
"""Return total instrumented work, including overlap and nesting."""
|
|
83
|
+
|
|
84
|
+
return sum(duration_ms(event) for event in events)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def elapsed_runtime_ms(trace: Any) -> float:
|
|
88
|
+
"""Resolve end-to-end trace duration with backward-compatible fallbacks."""
|
|
89
|
+
|
|
90
|
+
explicit = _non_negative_float(getattr(trace, "elapsed_ms", None))
|
|
91
|
+
if explicit is not None:
|
|
92
|
+
return explicit
|
|
93
|
+
|
|
94
|
+
if getattr(trace, "_timing_active", False):
|
|
95
|
+
started_perf = _non_negative_float(getattr(trace, "_start_perf", None))
|
|
96
|
+
if started_perf is not None:
|
|
97
|
+
return max(0.0, (time.perf_counter() - started_perf) * 1000)
|
|
98
|
+
|
|
99
|
+
trace_started = timestamp_ms(getattr(trace, "started_at", None))
|
|
100
|
+
trace_ended = timestamp_ms(getattr(trace, "ended_at", None))
|
|
101
|
+
if trace_started is not None and trace_ended is not None and trace_ended >= trace_started:
|
|
102
|
+
return trace_ended - trace_started
|
|
103
|
+
|
|
104
|
+
events = list(getattr(trace, "events", []))
|
|
105
|
+
bounds = event_time_bounds(events)
|
|
106
|
+
if bounds is not None:
|
|
107
|
+
earliest, latest = bounds
|
|
108
|
+
if trace_started is not None and trace_started <= latest:
|
|
109
|
+
earliest = min(earliest, trace_started)
|
|
110
|
+
return max(0.0, latest - earliest)
|
|
111
|
+
|
|
112
|
+
return cumulative_span_time_ms(events)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _non_negative_float(value: Any) -> float | None:
|
|
116
|
+
if value is None or value == "":
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
result = float(value)
|
|
120
|
+
except (TypeError, ValueError):
|
|
121
|
+
return None
|
|
122
|
+
if not math.isfinite(result) or result < 0:
|
|
123
|
+
return None
|
|
124
|
+
return result
|
agentloop/tracer.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
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.events import AgentEvent, new_event_id, new_run_id, utc_now_iso
|
|
11
|
+
from agentloop.metrics import build_report
|
|
12
|
+
|
|
13
|
+
_current_trace: ContextVar["AgentTrace | None"] = ContextVar(
|
|
14
|
+
"agentloop_current_trace", default=None
|
|
15
|
+
)
|
|
16
|
+
_current_event_id: ContextVar[str | None] = ContextVar("agentloop_current_event_id", default=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _count_tokens(text: str | None) -> int:
|
|
20
|
+
if not text:
|
|
21
|
+
return 0
|
|
22
|
+
return max(1, len(text.split()))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentTrace:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
run_id: str | None = None,
|
|
30
|
+
metadata: dict[str, Any] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
started_at: str | None = None,
|
|
33
|
+
ended_at: str | None = None,
|
|
34
|
+
elapsed_ms: float | None = None,
|
|
35
|
+
):
|
|
36
|
+
self.name = name
|
|
37
|
+
self.run_id = run_id or new_run_id()
|
|
38
|
+
self.metadata = metadata or {}
|
|
39
|
+
self.events: list[AgentEvent] = []
|
|
40
|
+
self.started_at = started_at or utc_now_iso()
|
|
41
|
+
self.ended_at = ended_at
|
|
42
|
+
self.elapsed_ms = float(elapsed_ms) if elapsed_ms is not None else None
|
|
43
|
+
self._start_perf = time.perf_counter()
|
|
44
|
+
self._timing_active = False
|
|
45
|
+
self.finalize_result: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
def add_event(self, event: AgentEvent) -> None:
|
|
48
|
+
self.events.append(event)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"name": self.name,
|
|
53
|
+
"run_id": self.run_id,
|
|
54
|
+
"started_at": self.started_at,
|
|
55
|
+
"ended_at": self.ended_at,
|
|
56
|
+
"elapsed_ms": self.elapsed_ms,
|
|
57
|
+
"metadata": self.metadata,
|
|
58
|
+
"events": [event.to_dict() for event in self.events],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: dict[str, Any]) -> "AgentTrace":
|
|
63
|
+
trace = cls(
|
|
64
|
+
name=data["name"],
|
|
65
|
+
run_id=data["run_id"],
|
|
66
|
+
metadata=data.get("metadata", {}),
|
|
67
|
+
started_at=data.get("started_at"),
|
|
68
|
+
ended_at=data.get("ended_at"),
|
|
69
|
+
elapsed_ms=data.get("elapsed_ms"),
|
|
70
|
+
)
|
|
71
|
+
trace.events = [AgentEvent.from_dict(item) for item in data.get("events", [])]
|
|
72
|
+
return trace
|
|
73
|
+
|
|
74
|
+
def finish(self) -> None:
|
|
75
|
+
"""Finalize trace timing once, using a monotonic clock when available."""
|
|
76
|
+
|
|
77
|
+
if self.ended_at is not None and self.elapsed_ms is not None:
|
|
78
|
+
self._timing_active = False
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if self.ended_at is None:
|
|
82
|
+
self.ended_at = utc_now_iso()
|
|
83
|
+
if self.elapsed_ms is None:
|
|
84
|
+
if self._timing_active:
|
|
85
|
+
self.elapsed_ms = max(0.0, (time.perf_counter() - self._start_perf) * 1000)
|
|
86
|
+
else:
|
|
87
|
+
from agentloop.timing import elapsed_runtime_ms
|
|
88
|
+
|
|
89
|
+
self.elapsed_ms = elapsed_runtime_ms(self)
|
|
90
|
+
self._timing_active = False
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_json(cls, path: str | Path) -> "AgentTrace":
|
|
94
|
+
return cls.from_dict(json.loads(Path(path).read_text(encoding="utf-8")))
|
|
95
|
+
|
|
96
|
+
def export_json(self, path: str | Path) -> Path:
|
|
97
|
+
out = Path(path)
|
|
98
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
out.write_text(json.dumps(self.to_dict(), indent=2), encoding="utf-8")
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
def report(self) -> dict[str, Any]:
|
|
103
|
+
return build_report(self)
|
|
104
|
+
|
|
105
|
+
def print_report(self) -> None:
|
|
106
|
+
report = self.report()
|
|
107
|
+
print(f"AgentLoop Report: {self.name}")
|
|
108
|
+
print(f"Run ID: {self.run_id}")
|
|
109
|
+
print(f"Total runtime: {report['total_runtime_ms'] / 1000:.2f}s")
|
|
110
|
+
print(f"Estimated cost: ${report['estimated_cost_usd']:.4f}")
|
|
111
|
+
print(f"Model time: {report['model_time_ms'] / 1000:.2f}s")
|
|
112
|
+
print(f"Tool time: {report['tool_time_ms'] / 1000:.2f}s")
|
|
113
|
+
print(f"Retry time: {report['retry_time_ms'] / 1000:.2f}s")
|
|
114
|
+
print(f"Input tokens: {report['input_tokens']}")
|
|
115
|
+
print(f"Output tokens: {report['output_tokens']}")
|
|
116
|
+
print(f"Repeated context ratio: {report['repeated_context_ratio']:.1%}")
|
|
117
|
+
print("\nRecommendations:")
|
|
118
|
+
for rec in report["recommendations"]:
|
|
119
|
+
print(f"- {rec['title']}: {rec['description']}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def current_trace() -> AgentTrace | None:
|
|
123
|
+
"""Return the active AgentLoop trace, if one is running in this context."""
|
|
124
|
+
|
|
125
|
+
return _current_trace.get()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def current_event_id() -> str | None:
|
|
129
|
+
"""Return the event id currently being recorded under, if any."""
|
|
130
|
+
|
|
131
|
+
return _current_event_id.get()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def trace_agent(name: str, metadata: dict[str, Any] | None = None) -> Iterator[AgentTrace]:
|
|
136
|
+
trace = AgentTrace(name=name, metadata=metadata)
|
|
137
|
+
trace._timing_active = True
|
|
138
|
+
token = _current_trace.set(trace)
|
|
139
|
+
event_token = _current_event_id.set(None)
|
|
140
|
+
try:
|
|
141
|
+
yield trace
|
|
142
|
+
finally:
|
|
143
|
+
trace.finish()
|
|
144
|
+
_current_event_id.reset(event_token)
|
|
145
|
+
_current_trace.reset(token)
|
|
146
|
+
from agentloop.runtime import finalize_trace, should_auto_export
|
|
147
|
+
|
|
148
|
+
if should_auto_export():
|
|
149
|
+
trace.finalize_result = finalize_trace(trace)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def record_model_call(
|
|
153
|
+
name: str,
|
|
154
|
+
*,
|
|
155
|
+
duration_ms: float,
|
|
156
|
+
started_at: str,
|
|
157
|
+
ended_at: str | None = None,
|
|
158
|
+
model: str | None = None,
|
|
159
|
+
input_tokens: int = 0,
|
|
160
|
+
output_tokens: int = 0,
|
|
161
|
+
input_text: str | None = None,
|
|
162
|
+
output_text: str | None = None,
|
|
163
|
+
status: str = "ok",
|
|
164
|
+
error: str | None = None,
|
|
165
|
+
metadata: dict[str, Any] | None = None,
|
|
166
|
+
event_id: str | None = None,
|
|
167
|
+
parent_id: str | None = None,
|
|
168
|
+
trace: AgentTrace | None = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Record a completed model call into a trace.
|
|
171
|
+
|
|
172
|
+
Integrations use this when token counts are only known after a framework call
|
|
173
|
+
returns. User code should usually prefer `trace_model_call(...)`.
|
|
174
|
+
|
|
175
|
+
By default the event is recorded into the trace active in the current context.
|
|
176
|
+
Pass ``trace`` to record into a specific trace instead — for example an
|
|
177
|
+
integration that captured trace ownership when a streaming call was invoked and
|
|
178
|
+
finalizes it later, possibly in a different context. When ``trace`` is given the
|
|
179
|
+
ambient event-id context is not used for the parent.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
target = trace if trace is not None else _require_trace()
|
|
183
|
+
resolved_parent = (
|
|
184
|
+
parent_id
|
|
185
|
+
if parent_id is not None
|
|
186
|
+
else (None if trace is not None else _current_event_id.get())
|
|
187
|
+
)
|
|
188
|
+
target.add_event(
|
|
189
|
+
AgentEvent(
|
|
190
|
+
event_id=event_id or new_event_id(),
|
|
191
|
+
run_id=target.run_id,
|
|
192
|
+
event_type="model_call",
|
|
193
|
+
name=name,
|
|
194
|
+
started_at=started_at,
|
|
195
|
+
ended_at=ended_at or utc_now_iso(),
|
|
196
|
+
duration_ms=duration_ms,
|
|
197
|
+
parent_id=resolved_parent,
|
|
198
|
+
model=model,
|
|
199
|
+
input_tokens=input_tokens,
|
|
200
|
+
output_tokens=output_tokens,
|
|
201
|
+
input_text=input_text,
|
|
202
|
+
output_text=output_text,
|
|
203
|
+
status=status,
|
|
204
|
+
error=error,
|
|
205
|
+
metadata=metadata or {},
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def record_tool_call(
|
|
211
|
+
name: str,
|
|
212
|
+
*,
|
|
213
|
+
duration_ms: float,
|
|
214
|
+
started_at: str,
|
|
215
|
+
ended_at: str | None = None,
|
|
216
|
+
status: str = "ok",
|
|
217
|
+
error: str | None = None,
|
|
218
|
+
metadata: dict[str, Any] | None = None,
|
|
219
|
+
event_id: str | None = None,
|
|
220
|
+
parent_id: str | None = None,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Record a completed tool/framework step into the active trace."""
|
|
223
|
+
|
|
224
|
+
trace = _require_trace()
|
|
225
|
+
trace.add_event(
|
|
226
|
+
AgentEvent(
|
|
227
|
+
event_id=event_id or new_event_id(),
|
|
228
|
+
run_id=trace.run_id,
|
|
229
|
+
event_type="tool_call",
|
|
230
|
+
name=name,
|
|
231
|
+
started_at=started_at,
|
|
232
|
+
ended_at=ended_at or utc_now_iso(),
|
|
233
|
+
duration_ms=duration_ms,
|
|
234
|
+
parent_id=parent_id if parent_id is not None else _current_event_id.get(),
|
|
235
|
+
status=status,
|
|
236
|
+
error=error,
|
|
237
|
+
metadata=metadata or {},
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@contextmanager
|
|
243
|
+
def trace_model_call(
|
|
244
|
+
name: str,
|
|
245
|
+
model: str | None = None,
|
|
246
|
+
input_tokens: int | None = None,
|
|
247
|
+
output_tokens: int | None = None,
|
|
248
|
+
input_text: str | None = None,
|
|
249
|
+
output_text: str | None = None,
|
|
250
|
+
metadata: dict[str, Any] | None = None,
|
|
251
|
+
) -> Iterator[None]:
|
|
252
|
+
_require_trace()
|
|
253
|
+
event_id = new_event_id()
|
|
254
|
+
parent_id = _current_event_id.get()
|
|
255
|
+
event_token = _current_event_id.set(event_id)
|
|
256
|
+
started_at = utc_now_iso()
|
|
257
|
+
start = time.perf_counter()
|
|
258
|
+
status = "ok"
|
|
259
|
+
error = None
|
|
260
|
+
try:
|
|
261
|
+
yield
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
status = "error"
|
|
264
|
+
error = str(exc)
|
|
265
|
+
raise
|
|
266
|
+
finally:
|
|
267
|
+
ended_at = utc_now_iso()
|
|
268
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
269
|
+
_current_event_id.reset(event_token)
|
|
270
|
+
record_model_call(
|
|
271
|
+
name,
|
|
272
|
+
started_at=started_at,
|
|
273
|
+
ended_at=ended_at,
|
|
274
|
+
duration_ms=duration_ms,
|
|
275
|
+
model=model,
|
|
276
|
+
input_tokens=input_tokens if input_tokens is not None else _count_tokens(input_text),
|
|
277
|
+
output_tokens=output_tokens
|
|
278
|
+
if output_tokens is not None
|
|
279
|
+
else _count_tokens(output_text),
|
|
280
|
+
input_text=input_text,
|
|
281
|
+
output_text=output_text,
|
|
282
|
+
status=status,
|
|
283
|
+
error=error,
|
|
284
|
+
metadata=metadata or {},
|
|
285
|
+
event_id=event_id,
|
|
286
|
+
parent_id=parent_id,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@contextmanager
|
|
291
|
+
def trace_tool_call(name: str, metadata: dict[str, Any] | None = None) -> Iterator[None]:
|
|
292
|
+
_require_trace()
|
|
293
|
+
event_id = new_event_id()
|
|
294
|
+
parent_id = _current_event_id.get()
|
|
295
|
+
event_token = _current_event_id.set(event_id)
|
|
296
|
+
started_at = utc_now_iso()
|
|
297
|
+
start = time.perf_counter()
|
|
298
|
+
status = "ok"
|
|
299
|
+
error = None
|
|
300
|
+
try:
|
|
301
|
+
yield
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
status = "error"
|
|
304
|
+
error = str(exc)
|
|
305
|
+
raise
|
|
306
|
+
finally:
|
|
307
|
+
ended_at = utc_now_iso()
|
|
308
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
309
|
+
_current_event_id.reset(event_token)
|
|
310
|
+
record_tool_call(
|
|
311
|
+
name,
|
|
312
|
+
started_at=started_at,
|
|
313
|
+
ended_at=ended_at,
|
|
314
|
+
duration_ms=duration_ms,
|
|
315
|
+
status=status,
|
|
316
|
+
error=error,
|
|
317
|
+
metadata=metadata or {},
|
|
318
|
+
event_id=event_id,
|
|
319
|
+
parent_id=parent_id,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@contextmanager
|
|
324
|
+
def trace_retry(name: str, metadata: dict[str, Any] | None = None) -> Iterator[None]:
|
|
325
|
+
trace = _require_trace()
|
|
326
|
+
event_id = new_event_id()
|
|
327
|
+
parent_id = _current_event_id.get()
|
|
328
|
+
event_token = _current_event_id.set(event_id)
|
|
329
|
+
started_at = utc_now_iso()
|
|
330
|
+
start = time.perf_counter()
|
|
331
|
+
try:
|
|
332
|
+
yield
|
|
333
|
+
finally:
|
|
334
|
+
ended_at = utc_now_iso()
|
|
335
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
336
|
+
_current_event_id.reset(event_token)
|
|
337
|
+
trace.add_event(
|
|
338
|
+
AgentEvent(
|
|
339
|
+
event_id=event_id,
|
|
340
|
+
run_id=trace.run_id,
|
|
341
|
+
event_type="retry",
|
|
342
|
+
name=name,
|
|
343
|
+
started_at=started_at,
|
|
344
|
+
ended_at=ended_at,
|
|
345
|
+
duration_ms=duration_ms,
|
|
346
|
+
parent_id=parent_id,
|
|
347
|
+
metadata=metadata or {},
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _require_trace() -> AgentTrace:
|
|
353
|
+
trace = _current_trace.get()
|
|
354
|
+
if trace is None:
|
|
355
|
+
raise RuntimeError("No active AgentLoop trace. Use `with trace_agent(...):` first.")
|
|
356
|
+
return trace
|