spanforge 2.0.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 (101) hide show
  1. spanforge/__init__.py +695 -0
  2. spanforge/_batch_exporter.py +322 -0
  3. spanforge/_cli.py +3081 -0
  4. spanforge/_hooks.py +340 -0
  5. spanforge/_server.py +953 -0
  6. spanforge/_span.py +1015 -0
  7. spanforge/_store.py +287 -0
  8. spanforge/_stream.py +654 -0
  9. spanforge/_trace.py +334 -0
  10. spanforge/_tracer.py +253 -0
  11. spanforge/actor.py +141 -0
  12. spanforge/alerts.py +464 -0
  13. spanforge/auto.py +181 -0
  14. spanforge/baseline.py +336 -0
  15. spanforge/config.py +460 -0
  16. spanforge/consent.py +227 -0
  17. spanforge/consumer.py +379 -0
  18. spanforge/core/__init__.py +5 -0
  19. spanforge/core/compliance_mapping.py +1060 -0
  20. spanforge/cost.py +597 -0
  21. spanforge/debug.py +514 -0
  22. spanforge/drift.py +488 -0
  23. spanforge/egress.py +63 -0
  24. spanforge/eval.py +575 -0
  25. spanforge/event.py +1052 -0
  26. spanforge/exceptions.py +246 -0
  27. spanforge/explain.py +181 -0
  28. spanforge/export/__init__.py +50 -0
  29. spanforge/export/append_only.py +342 -0
  30. spanforge/export/cloud.py +349 -0
  31. spanforge/export/datadog.py +495 -0
  32. spanforge/export/grafana.py +331 -0
  33. spanforge/export/jsonl.py +198 -0
  34. spanforge/export/otel_bridge.py +291 -0
  35. spanforge/export/otlp.py +817 -0
  36. spanforge/export/otlp_bridge.py +231 -0
  37. spanforge/export/redis_backend.py +282 -0
  38. spanforge/export/webhook.py +302 -0
  39. spanforge/exporters/__init__.py +29 -0
  40. spanforge/exporters/console.py +271 -0
  41. spanforge/exporters/jsonl.py +144 -0
  42. spanforge/hitl.py +297 -0
  43. spanforge/inspect.py +429 -0
  44. spanforge/integrations/__init__.py +39 -0
  45. spanforge/integrations/_pricing.py +277 -0
  46. spanforge/integrations/anthropic.py +388 -0
  47. spanforge/integrations/bedrock.py +306 -0
  48. spanforge/integrations/crewai.py +251 -0
  49. spanforge/integrations/gemini.py +349 -0
  50. spanforge/integrations/groq.py +444 -0
  51. spanforge/integrations/langchain.py +349 -0
  52. spanforge/integrations/llamaindex.py +370 -0
  53. spanforge/integrations/ollama.py +286 -0
  54. spanforge/integrations/openai.py +370 -0
  55. spanforge/integrations/together.py +485 -0
  56. spanforge/metrics.py +393 -0
  57. spanforge/metrics_export.py +342 -0
  58. spanforge/migrate.py +278 -0
  59. spanforge/model_registry.py +282 -0
  60. spanforge/models.py +407 -0
  61. spanforge/namespaces/__init__.py +215 -0
  62. spanforge/namespaces/audit.py +253 -0
  63. spanforge/namespaces/cache.py +209 -0
  64. spanforge/namespaces/chain.py +74 -0
  65. spanforge/namespaces/confidence.py +69 -0
  66. spanforge/namespaces/consent.py +85 -0
  67. spanforge/namespaces/cost.py +175 -0
  68. spanforge/namespaces/decision.py +135 -0
  69. spanforge/namespaces/diff.py +146 -0
  70. spanforge/namespaces/drift.py +79 -0
  71. spanforge/namespaces/eval_.py +232 -0
  72. spanforge/namespaces/fence.py +180 -0
  73. spanforge/namespaces/guard.py +104 -0
  74. spanforge/namespaces/hitl.py +92 -0
  75. spanforge/namespaces/latency.py +69 -0
  76. spanforge/namespaces/prompt.py +185 -0
  77. spanforge/namespaces/redact.py +172 -0
  78. spanforge/namespaces/template.py +197 -0
  79. spanforge/namespaces/tool_call.py +76 -0
  80. spanforge/namespaces/trace.py +1006 -0
  81. spanforge/normalizer.py +183 -0
  82. spanforge/presidio_backend.py +149 -0
  83. spanforge/processor.py +258 -0
  84. spanforge/prompt_registry.py +415 -0
  85. spanforge/py.typed +0 -0
  86. spanforge/redact.py +780 -0
  87. spanforge/sampling.py +500 -0
  88. spanforge/schemas/v1.0/schema.json +170 -0
  89. spanforge/schemas/v2.0/schema.json +536 -0
  90. spanforge/signing.py +1152 -0
  91. spanforge/stream.py +559 -0
  92. spanforge/testing.py +376 -0
  93. spanforge/trace.py +199 -0
  94. spanforge/types.py +696 -0
  95. spanforge/ulid.py +304 -0
  96. spanforge/validate.py +383 -0
  97. spanforge-2.0.0.dist-info/METADATA +1777 -0
  98. spanforge-2.0.0.dist-info/RECORD +101 -0
  99. spanforge-2.0.0.dist-info/WHEEL +4 -0
  100. spanforge-2.0.0.dist-info/entry_points.txt +5 -0
  101. spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
spanforge/_trace.py ADDED
@@ -0,0 +1,334 @@
1
+ """spanforge._trace — :class:`Trace` object and :func:`start_trace` entry point.
2
+
3
+ A :class:`Trace` wraps a *root* :class:`~spanforge._span.AgentRunContextManager`
4
+ and gives callers a convenient imperative handle to the entire agent execution::
5
+
6
+ trace = spanforge.start_trace("research_agent")
7
+
8
+ with trace.llm_call(model="gpt-4o") as s:
9
+ s.set_attribute("query", "latest AI papers")
10
+
11
+ with trace.tool_call("search") as s:
12
+ s.set_attribute("results_count", 5)
13
+
14
+ trace.end()
15
+
16
+ The :class:`Trace` object also provides convenience methods for serialisation
17
+ (:meth:`to_json`, :meth:`save`) that will become richer in later phases when
18
+ debug utilities (:meth:`print_tree`, :meth:`summary`) are added.
19
+
20
+ Design notes
21
+ ------------
22
+ * :func:`start_trace` opens the underlying :class:`AgentRunContextManager`
23
+ immediately (``__enter__``), so all subsequently created spans automatically
24
+ inherit the trace's ``trace_id``.
25
+ * :meth:`Trace.end` closes that context manager (``__exit__``). Callers
26
+ *must* call :meth:`end` or use the :class:`Trace` as a context manager::
27
+
28
+ with spanforge.start_trace("my-agent") as trace:
29
+ ...
30
+ # trace.end() called automatically on exit
31
+
32
+ * :class:`Trace` collects emitted :class:`~spanforge._span.Span` instances via
33
+ :meth:`_record_span` which is called by the stream module when a
34
+ ``_TRACE_COLLECTOR`` attribute is present on the active
35
+ :class:`~spanforge._span.AgentRunContext`. This allows :meth:`to_json` and
36
+ :meth:`save` to operate on in-memory data without re-reading from a file.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import time
43
+ from dataclasses import dataclass, field
44
+ from typing import TYPE_CHECKING, Any
45
+
46
+ from spanforge._span import (
47
+ AgentRunContext,
48
+ AgentRunContextManager,
49
+ Span,
50
+ SpanContextManager,
51
+ _span_id,
52
+ _trace_id,
53
+ _now_ns,
54
+ )
55
+
56
+ if TYPE_CHECKING:
57
+ from types import TracebackType
58
+
59
+ __all__ = ["Trace", "start_trace"]
60
+
61
+
62
+ @dataclass
63
+ class Trace:
64
+ """A handle to a complete agent trace.
65
+
66
+ Created by :func:`start_trace`; do not construct directly.
67
+
68
+ Attributes:
69
+ agent_name: Name of the agent being traced.
70
+ trace_id: 32-char hex OTel-compatible trace ID.
71
+ start_time: Unix timestamp (float seconds) when the trace started.
72
+ """
73
+
74
+ agent_name: str
75
+ trace_id: str
76
+ start_time: float
77
+ _run_ctx: AgentRunContext = field(repr=False)
78
+ _run_cm: AgentRunContextManager = field(repr=False)
79
+ attributes: dict[str, Any] = field(default_factory=dict)
80
+ _spans: list[Span] = field(default_factory=list, repr=False)
81
+ _ended: bool = field(default=False, init=False, repr=False)
82
+
83
+ # ------------------------------------------------------------------
84
+ # Span convenience methods
85
+ # ------------------------------------------------------------------
86
+
87
+ def llm_call(
88
+ self,
89
+ model: str | None = None,
90
+ *,
91
+ operation: str = "chat",
92
+ temperature: float | None = None,
93
+ top_p: float | None = None,
94
+ max_tokens: int | None = None,
95
+ attributes: dict[str, Any] | None = None,
96
+ ) -> SpanContextManager:
97
+ """Open a child span for an LLM call within this trace.
98
+
99
+ Example::
100
+
101
+ with trace.llm_call(model="gpt-4o", temperature=0.7) as s:
102
+ s.set_attribute("prompt_tokens", 512)
103
+ """
104
+ name = f"llm_call:{model}" if model else "llm_call"
105
+ return SpanContextManager(
106
+ name=name,
107
+ model=model,
108
+ operation=operation,
109
+ temperature=temperature,
110
+ top_p=top_p,
111
+ max_tokens=max_tokens,
112
+ attributes=attributes,
113
+ )
114
+
115
+ def tool_call(
116
+ self,
117
+ tool_name: str,
118
+ *,
119
+ attributes: dict[str, Any] | None = None,
120
+ ) -> SpanContextManager:
121
+ """Open a child span for a tool call within this trace.
122
+
123
+ Example::
124
+
125
+ with trace.tool_call("search") as s:
126
+ results = search(query)
127
+ """
128
+ return SpanContextManager(
129
+ name=f"tool_call:{tool_name}",
130
+ operation="tool",
131
+ attributes=attributes,
132
+ )
133
+
134
+ def span(
135
+ self,
136
+ name: str,
137
+ *,
138
+ model: str | None = None,
139
+ operation: str = "chat",
140
+ temperature: float | None = None,
141
+ top_p: float | None = None,
142
+ max_tokens: int | None = None,
143
+ attributes: dict[str, Any] | None = None,
144
+ ) -> SpanContextManager:
145
+ """Open a generic child span within this trace."""
146
+ return SpanContextManager(
147
+ name=name,
148
+ model=model,
149
+ operation=operation,
150
+ temperature=temperature,
151
+ top_p=top_p,
152
+ max_tokens=max_tokens,
153
+ attributes=attributes,
154
+ )
155
+
156
+ # ------------------------------------------------------------------
157
+ # Internal span collection
158
+ # ------------------------------------------------------------------
159
+
160
+ def _record_span(self, span: Span) -> None:
161
+ """Called by _stream.emit_span when this trace is the active run context."""
162
+ self._spans.append(span)
163
+
164
+ # ------------------------------------------------------------------
165
+ # Lifecycle
166
+ # ------------------------------------------------------------------
167
+
168
+ def end(self) -> None:
169
+ """Close the trace and emit the agent-run completion event.
170
+
171
+ Idempotent — subsequent calls are no-ops.
172
+ """
173
+ if self._ended:
174
+ return
175
+ self._ended = True
176
+ self._run_cm.__exit__(None, None, None)
177
+
178
+ # ------------------------------------------------------------------
179
+ # Context manager protocol (allows ``with start_trace(...) as trace:``)
180
+ # ------------------------------------------------------------------
181
+
182
+ def __enter__(self) -> "Trace":
183
+ return self
184
+
185
+ def __exit__(
186
+ self,
187
+ exc_type: type[BaseException] | None,
188
+ exc_val: BaseException | None,
189
+ exc_tb: TracebackType | None,
190
+ ) -> bool:
191
+ if not self._ended:
192
+ self._run_cm.__exit__(exc_type, exc_val, exc_tb)
193
+ self._ended = True
194
+ return False
195
+
196
+ async def __aenter__(self) -> "Trace":
197
+ return self
198
+
199
+ async def __aexit__(
200
+ self,
201
+ exc_type: type[BaseException] | None,
202
+ exc_val: BaseException | None,
203
+ exc_tb: TracebackType | None,
204
+ ) -> bool:
205
+ return self.__exit__(exc_type, exc_val, exc_tb)
206
+
207
+ # ------------------------------------------------------------------
208
+ # Serialisation
209
+ # ------------------------------------------------------------------
210
+
211
+ def to_json(self, *, indent: int | None = None) -> str:
212
+ """Serialise the trace's collected spans to a JSON string.
213
+
214
+ Returns a JSON object with ``trace_id``, ``agent_name``, ``start_time``,
215
+ and a ``spans`` array of span payload dicts.
216
+
217
+ Args:
218
+ indent: Optional JSON indentation level (``None`` = compact).
219
+ """
220
+ return json.dumps(self._to_dict(), indent=indent, sort_keys=True, default=str)
221
+
222
+ def save(self, path: str) -> None:
223
+ """Write the trace as NDJSON (one span per line) to *path*.
224
+
225
+ Args:
226
+ path: File path to write to. The file is created or overwritten.
227
+ """
228
+ lines = [
229
+ json.dumps(span.to_span_payload().to_dict(), sort_keys=True, default=str)
230
+ for span in self._spans
231
+ ]
232
+ with open(path, "w", encoding="utf-8") as fh:
233
+ fh.write("\n".join(lines))
234
+ if lines:
235
+ fh.write("\n")
236
+
237
+ def _to_dict(self) -> dict[str, Any]:
238
+ d: dict[str, Any] = {
239
+ "trace_id": self.trace_id,
240
+ "agent_name": self.agent_name,
241
+ "start_time": self.start_time,
242
+ "spans": [span.to_span_payload().to_dict() for span in self._spans],
243
+ }
244
+ if self.attributes:
245
+ d["attributes"] = self.attributes
246
+ return d
247
+
248
+ # Placeholder methods for Phase 3 debug utilities.
249
+ def print_tree(self, *, file: Any = None) -> None:
250
+ """Pretty-print a hierarchical tree of spans to stdout.
251
+
252
+ Delegates to :func:`spanforge.debug.print_tree`.
253
+ Requires the trace to have ended (or have accumulated spans).
254
+ """
255
+ from spanforge.debug import print_tree # noqa: PLC0415
256
+ print_tree(self._spans, file=file)
257
+
258
+ def summary(self) -> dict[str, Any]:
259
+ """Return an aggregated statistics dict for the trace's spans.
260
+
261
+ Delegates to :func:`spanforge.debug.summary`.
262
+ """
263
+ from spanforge.debug import summary # noqa: PLC0415
264
+ return summary(self._spans)
265
+
266
+ def visualize(self, *, output: str = "html", path: str | None = None) -> str:
267
+ """Generate a self-contained HTML Gantt timeline for this trace.
268
+
269
+ Delegates to :func:`spanforge.debug.visualize`.
270
+
271
+ Args:
272
+ output: Output format — currently only ``"html"``.
273
+ path: Optional file path to write the HTML to.
274
+
275
+ Returns:
276
+ HTML string.
277
+ """
278
+ from spanforge.debug import visualize # noqa: PLC0415
279
+ return visualize(self._spans, output=output, path=path)
280
+
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # start_trace()
284
+ # ---------------------------------------------------------------------------
285
+
286
+
287
+ def start_trace(agent_name: str, **attributes: Any) -> Trace:
288
+ """Start a new agent trace and return a :class:`Trace` handle.
289
+
290
+ Opens a root :class:`~spanforge._span.AgentRunContextManager` so all spans
291
+ created within the trace automatically inherit the correct ``trace_id``.
292
+
293
+ Must be closed by calling :meth:`Trace.end` or by using the returned
294
+ object as a context manager::
295
+
296
+ # Imperative style
297
+ trace = spanforge.start_trace("research_agent")
298
+ with trace.llm_call(model="gpt-4o"):
299
+ ...
300
+ trace.end()
301
+
302
+ # Context manager style (recommended)
303
+ with spanforge.start_trace("research_agent") as trace:
304
+ with trace.llm_call(model="gpt-4o"):
305
+ ...
306
+
307
+ Args:
308
+ agent_name: Name of the agent being executed.
309
+ **attributes: Optional key-value attributes stored on the
310
+ :attr:`Trace.attributes` dict and included in
311
+ :meth:`Trace.to_json` output.
312
+
313
+ Returns:
314
+ A :class:`Trace` object that acts as the root execution context.
315
+ """
316
+ if not isinstance(agent_name, str) or not agent_name:
317
+ raise ValueError("start_trace: agent_name must be a non-empty string")
318
+
319
+ cm = AgentRunContextManager(agent_name=agent_name)
320
+ run_ctx = cm.__enter__()
321
+
322
+ # Attach a back-reference so _stream can call _record_span on this Trace.
323
+ trace = Trace(
324
+ agent_name=agent_name,
325
+ trace_id=run_ctx.trace_id,
326
+ start_time=time.time(),
327
+ _run_ctx=run_ctx,
328
+ _run_cm=cm,
329
+ attributes=dict(attributes) if attributes else {},
330
+ )
331
+ # Store the Trace on the AgentRunContext so _stream.emit_span can find it.
332
+ run_ctx._trace_collector = trace # type: ignore[attr-defined]
333
+
334
+ return trace
spanforge/_tracer.py ADDED
@@ -0,0 +1,253 @@
1
+ """spanforge._tracer — :class:`Tracer` class and module-level ``tracer`` singleton.
2
+
3
+ The :class:`Tracer` is the primary entry point for instrumenting code with
4
+ SpanForge. Import the module-level singleton ``tracer`` and use its context
5
+ managers to create spans and agent traces::
6
+
7
+ from spanforge import tracer, configure
8
+
9
+ configure(exporter="console")
10
+
11
+ with tracer.span("chat", model="gpt-4o") as s:
12
+ s.set_attribute("prompt_tokens", 512)
13
+
14
+ with tracer.agent_run("research-agent") as run:
15
+ with tracer.agent_step("web-search") as step:
16
+ step.set_attribute("query", "what is RAG?")
17
+ with tracer.agent_step("summarize"):
18
+ pass
19
+
20
+ # Imperative trace API (Phase 1)
21
+ trace = tracer.start_trace("research-agent")
22
+ with trace.llm_call(model="gpt-4o"):
23
+ ...
24
+ trace.end()
25
+
26
+ All context managers support both ``with`` and ``async with``. Async usage
27
+ is safe because the span stack is backed by :mod:`contextvars` so each asyncio
28
+ task sees its own stack slice.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from typing import Any
34
+
35
+ from spanforge._span import (
36
+ AgentRunContextManager,
37
+ AgentStepContextManager,
38
+ SpanContextManager,
39
+ )
40
+ from spanforge._trace import Trace, start_trace as _start_trace
41
+ from spanforge.trace import trace as _trace_decorator, _TraceDecorator
42
+
43
+ __all__ = ["Tracer", "tracer"]
44
+
45
+
46
+ class Tracer:
47
+ """The SpanForge tracing façade.
48
+
49
+ A single module-level instance is created as :data:`tracer` and is the
50
+ recommended way to instrument code. Creating additional :class:`Tracer`
51
+ instances is supported but shares the same thread-local context stacks.
52
+
53
+ All ``span``/``agent_run``/``agent_step`` methods return context managers
54
+ that push the new context onto the thread-local stack on ``__enter__`` and
55
+ pop it (and emit the event) on ``__exit__``.
56
+ """
57
+
58
+ # ------------------------------------------------------------------
59
+ # Span API (Phase 2)
60
+ # ------------------------------------------------------------------
61
+
62
+ def span(
63
+ self,
64
+ name: str,
65
+ *,
66
+ model: str | None = None,
67
+ operation: str = "chat",
68
+ temperature: float | None = None,
69
+ top_p: float | None = None,
70
+ max_tokens: int | None = None,
71
+ attributes: dict[str, Any] | None = None,
72
+ ) -> SpanContextManager:
73
+ """Create a new :class:`~spanforge._span.SpanContextManager`.
74
+
75
+ Use as a context manager::
76
+
77
+ with tracer.span("llm-call", model="gpt-4o", temperature=0.7) as s:
78
+ s.set_attribute("prompt_tokens", 512)
79
+
80
+ Args:
81
+ name: Human-readable span name (non-empty string).
82
+ model: Model name string (e.g. ``"gpt-4o"``). Used to infer
83
+ the provider when no integration has set
84
+ :attr:`~spanforge._span.Span.token_usage`.
85
+ operation: GenAI operation name (default ``"chat"``). Any
86
+ :class:`~spanforge.namespaces.trace.GenAIOperationName`
87
+ value or a custom string.
88
+ temperature: Sampling temperature forwarded to :class:`SpanPayload`.
89
+ top_p: Nucleus sampling ``top_p`` value.
90
+ max_tokens: Maximum token limit for this LLM call.
91
+ attributes: Initial key-value attributes. Additional attributes
92
+ can be added inside the block via
93
+ :meth:`~spanforge._span.Span.set_attribute`.
94
+
95
+ Returns:
96
+ A :class:`~spanforge._span.SpanContextManager` that yields a
97
+ :class:`~spanforge._span.Span` on ``__enter__``.
98
+ """
99
+ return SpanContextManager(
100
+ name=name,
101
+ model=model,
102
+ operation=operation,
103
+ temperature=temperature,
104
+ top_p=top_p,
105
+ max_tokens=max_tokens,
106
+ attributes=attributes,
107
+ )
108
+
109
+ # ------------------------------------------------------------------
110
+ # Agent API (Phase 4)
111
+ # ------------------------------------------------------------------
112
+
113
+ def agent_run(self, agent_name: str) -> AgentRunContextManager:
114
+ """Create a root agent-run context manager.
115
+
116
+ Use as an outer context that wraps one or more ``agent_step`` calls::
117
+
118
+ with tracer.agent_run("my-agent") as run:
119
+ with tracer.agent_step("step-1"):
120
+ ...
121
+
122
+ On exit, emits an
123
+ :data:`~spanforge.types.EventType.TRACE_AGENT_COMPLETED` event with
124
+ aggregated totals across all child steps.
125
+
126
+ Args:
127
+ agent_name: Name of the agent (non-empty string).
128
+
129
+ Returns:
130
+ :class:`~spanforge._span.AgentRunContextManager`
131
+ """
132
+ return AgentRunContextManager(agent_name=agent_name)
133
+
134
+ def agent_step(
135
+ self,
136
+ step_name: str,
137
+ *,
138
+ operation: str = "invoke_agent",
139
+ attributes: dict[str, Any] | None = None,
140
+ ) -> AgentStepContextManager:
141
+ """Create a single agent-step context manager.
142
+
143
+ Must be used inside an ``agent_run`` block::
144
+
145
+ with tracer.agent_run("my-agent"):
146
+ with tracer.agent_step("search") as step:
147
+ step.set_attribute("query", "hello")
148
+
149
+ On exit, emits an
150
+ :data:`~spanforge.types.EventType.TRACE_AGENT_STEP` event.
151
+
152
+ Args:
153
+ step_name: Human-readable step name.
154
+ operation: GenAI operation name (default ``"invoke_agent"``).
155
+ attributes: Initial key-value attributes.
156
+
157
+ Returns:
158
+ :class:`~spanforge._span.AgentStepContextManager`
159
+
160
+ Raises:
161
+ RuntimeError: If called outside an ``agent_run`` context.
162
+ """
163
+ return AgentStepContextManager(
164
+ step_name=step_name,
165
+ operation=operation,
166
+ attributes=attributes,
167
+ )
168
+
169
+ # ------------------------------------------------------------------
170
+ # Trace API (Phase 1)
171
+ # ------------------------------------------------------------------
172
+
173
+ def start_trace(self, agent_name: str, **attributes: Any) -> Trace:
174
+ """Start a new agent trace and return a :class:`~spanforge._trace.Trace` handle.
175
+
176
+ Convenience wrapper around the module-level :func:`~spanforge._trace.start_trace`.
177
+ The returned :class:`Trace` must be closed with :meth:`~spanforge._trace.Trace.end`
178
+ or used as a context manager::
179
+
180
+ with tracer.start_trace("research-agent") as trace:
181
+ with trace.llm_call(model="gpt-4o") as s:
182
+ ...
183
+
184
+ Args:
185
+ agent_name: Name of the agent being executed.
186
+ **attributes: Optional key-value attributes for the root run context.
187
+
188
+ Returns:
189
+ :class:`~spanforge._trace.Trace`
190
+ """
191
+ return _start_trace(agent_name, **attributes)
192
+
193
+ def trace(
194
+ self,
195
+ fn: Any = None,
196
+ *,
197
+ name: str | None = None,
198
+ model: str | None = None,
199
+ operation: str = "chat",
200
+ tool: bool = False,
201
+ capture_args: bool = False,
202
+ capture_return: bool = False,
203
+ attributes: dict[str, Any] | None = None,
204
+ ) -> Any:
205
+ """Decorator that instruments a function as an SpanForge span.
206
+
207
+ Delegates to :func:`~spanforge.trace.trace`. Provided as a convenience
208
+ method so callers who already hold a :class:`Tracer` reference do not
209
+ need a separate import::
210
+
211
+ @tracer.trace(name="my-step", model="gpt-4o")
212
+ def call_llm(prompt: str) -> str: ...
213
+
214
+ @tracer.trace(name="async-step")
215
+ async def async_step(x: int) -> dict: ...
216
+
217
+ Args:
218
+ fn: Function to wrap when used bare (``@tracer.trace``).
219
+ name: Span name (defaults to ``fn.__qualname__``).
220
+ model: Model identifier string.
221
+ operation: GenAI operation name (default ``"chat"``).
222
+ tool: Mark as tool call; sets operation to ``"execute_tool"``.
223
+ capture_args: Record call arguments as span attributes.
224
+ capture_return: Record return value as a span attribute.
225
+ attributes: Static key-value attributes on every span.
226
+
227
+ Returns:
228
+ Decorated callable, or a single-argument decorator.
229
+ """
230
+ return _trace_decorator(
231
+ fn,
232
+ name=name,
233
+ model=model,
234
+ operation=operation,
235
+ tool=tool,
236
+ capture_args=capture_args,
237
+ capture_return=capture_return,
238
+ attributes=attributes,
239
+ )
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Module-level singleton — ``from spanforge import tracer``
244
+ # ---------------------------------------------------------------------------
245
+
246
+ #: The default :class:`Tracer` singleton.
247
+ #:
248
+ #: Import this directly for convenience::
249
+ #:
250
+ #: from spanforge import tracer
251
+ #: with tracer.span("my-span"):
252
+ #: ...
253
+ tracer: Tracer = Tracer()