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.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. 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
@@ -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)