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/types.py ADDED
@@ -0,0 +1,696 @@
1
+ """Namespaced event type registry for spanforge SDK (RFC-0001 v2.0).
2
+
3
+ All built-in event types follow the pattern::
4
+
5
+ llm.<namespace>.<entity>.<action>
6
+
7
+ Third-party extension types MUST use a reverse-domain prefix outside the
8
+ ``llm.*`` tree (e.g. ``com.example.entity.action``) and MUST NOT claim any
9
+ reserved namespace listed in :data:`_RESERVED_NAMESPACES`.
10
+
11
+ Built-in namespaces (RFC-0001 §7.2)
12
+ -------------------------------------
13
+
14
+ ==================== ======================================
15
+ Namespace Purpose
16
+ ==================== ======================================
17
+ ``llm.trace.*`` Span tracing, agent runs, reasoning
18
+ ``llm.cost.*`` Token cost recording and attribution
19
+ ``llm.cache.*`` Semantic cache hit/miss/eviction
20
+ ``llm.eval.*`` Evaluation scores and regression
21
+ ``llm.guard.*`` Input/output safety classifiers
22
+ ``llm.fence.*`` Structured output constraint loops
23
+ ``llm.prompt.*`` Prompt rendering and version lifecycle
24
+ ``llm.redact.*`` PII/PHI detection and redaction audit
25
+ ``llm.diff.*`` Prompt/response delta analysis
26
+ ``llm.template.*`` Template registry lifecycle
27
+ ``llm.audit.*`` HMAC key rotation and chain audit
28
+ ==================== ======================================
29
+
30
+ Reserved (future) namespaces (RFC-0001 §7.4)
31
+ ---------------------------------------------
32
+ ``llm.rag.*``, ``llm.memory.*``, ``llm.planning.*``,
33
+ ``llm.multimodal.*``, ``llm.finetune.*``
34
+
35
+ Design
36
+ ------
37
+ :class:`EventType` is a ``str`` subclass so values can be compared with plain
38
+ strings, used as dict keys, and serialised without conversion while still
39
+ providing autocomplete and type safety.
40
+
41
+ :func:`is_registered` and :func:`namespace_of` provide runtime introspection.
42
+ :func:`validate_custom` validates third-party extension types.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import re
48
+ from enum import Enum
49
+ from typing import Final, Literal
50
+
51
+ from spanforge.exceptions import EventTypeError
52
+
53
+ __all__ = [
54
+ "EVENT_TYPE_PATTERN",
55
+ "EventType",
56
+ "RFC_SPANFORGE_NAMESPACES",
57
+ "SpanErrorCategory",
58
+ "is_registered",
59
+ "namespace_of",
60
+ "validate_custom",
61
+ ]
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Validation patterns (RFC-0001 §7)
65
+ # ---------------------------------------------------------------------------
66
+ # Built-in: llm.<namespace>.<entity>.<action> where namespace is one of the
67
+ # RFC-registered namespaces from §7.2.
68
+ # RFC-0001 SPANFORGE: 10 new namespaces (decision, tool_call, chain, confidence,
69
+ # consent, drift, latency, hitl, playbook, audit extension)
70
+ # Extension: reverse-domain prefix outside llm.*
71
+ # (e.g. com.example.<entity>.<action>).
72
+ EVENT_TYPE_PATTERN: Final[str] = (
73
+ r"^(?:llm\.(?:trace|cost|cache|eval|guard|fence|prompt|redact|diff|template|audit)\.(?:[a-z][a-z0-9_]*|[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*)"
74
+ r"|(?:decision|tool_call|chain|confidence|consent|drift|latency|hitl|playbook|audit|model_registry|explanation)\.(?:[a-z][a-z0-9_]*|[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*)"
75
+ r"|(?!llm\.)[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*)$"
76
+ )
77
+ _EVENT_TYPE_RE: Final[re.Pattern[str]] = re.compile(EVENT_TYPE_PATTERN)
78
+
79
+ # RFC-0001 §7.2 — reserved namespaces (built-in).
80
+ _RESERVED_NAMESPACES: Final[frozenset[str]] = frozenset(
81
+ [
82
+ # Legacy llm.* namespaces
83
+ "llm.audit",
84
+ "llm.cache",
85
+ "llm.cost",
86
+ "llm.diff",
87
+ "llm.eval",
88
+ "llm.fence",
89
+ "llm.guard",
90
+ "llm.prompt",
91
+ "llm.redact",
92
+ "llm.template",
93
+ "llm.trace",
94
+ # RFC-0001 SPANFORGE — 12 first-class namespaces
95
+ "decision",
96
+ "tool_call",
97
+ "chain",
98
+ "confidence",
99
+ "consent",
100
+ "drift",
101
+ "latency",
102
+ "hitl",
103
+ "playbook",
104
+ "audit",
105
+ "model_registry",
106
+ "explanation",
107
+ ]
108
+ )
109
+
110
+ # RFC-0001 SPANFORGE — the 10 new namespaces that require auto-signing
111
+ RFC_SPANFORGE_NAMESPACES: Final[frozenset[str]] = frozenset(
112
+ [
113
+ "decision",
114
+ "tool_call",
115
+ "chain",
116
+ "confidence",
117
+ "consent",
118
+ "drift",
119
+ "latency",
120
+ "hitl",
121
+ "playbook",
122
+ "audit",
123
+ "model_registry",
124
+ "explanation",
125
+ ]
126
+ )
127
+
128
+ # RFC-0001 §7.4 — reserved for future standardisation.
129
+ _FUTURE_NAMESPACES: Final[frozenset[str]] = frozenset(
130
+ [
131
+ "llm.rag",
132
+ "llm.memory",
133
+ "llm.planning",
134
+ "llm.multimodal",
135
+ "llm.finetune",
136
+ ]
137
+ )
138
+
139
+
140
+ class EventType(str, Enum):
141
+ """RFC-0001 Appendix B — canonical SpanForge event type registry.
142
+
143
+ 67 first-party event types across 21 namespaces:
144
+ - 11 legacy ``llm.*`` namespaces (RFC-0001 v1.x, retained for compatibility)
145
+ - 10 RFC-0001 SPANFORGE namespaces (decision, tool_call, chain, confidence,
146
+ consent, drift, latency, hitl, playbook, audit)
147
+
148
+ Example::
149
+
150
+ et = EventType.TRACE_SPAN_COMPLETED
151
+ assert et == "llm.trace.span.completed"
152
+ assert et.namespace == "llm.trace"
153
+
154
+ et2 = EventType.DECISION_MADE
155
+ assert et2 == "decision.made"
156
+ assert et2.namespace == "decision"
157
+ """
158
+
159
+ def __new__(cls, value: str, description: str = "") -> EventType:
160
+ """Construct a new enum member with the given value and description."""
161
+ obj = str.__new__(cls, value)
162
+ obj._value_ = value
163
+ return obj
164
+
165
+ def __init__(self, value: str, description: str = "") -> None:
166
+ self._description = description
167
+
168
+ def __str__(self) -> str: # type: ignore[override]
169
+ return self.value # type: ignore[return-value]
170
+
171
+ def __eq__(self, other: object) -> bool:
172
+ if isinstance(other, str):
173
+ return str.__eq__(self, other)
174
+ return NotImplemented
175
+
176
+ def __hash__(self) -> int:
177
+ return str.__hash__(self)
178
+
179
+ # ------------------------------------------------------------------
180
+ # llm.trace.* — RFC-0001 §8.1-§8.5
181
+ # ------------------------------------------------------------------
182
+ TRACE_SPAN_STARTED = (
183
+ "llm.trace.span.started",
184
+ "A new LLM call/tool-execution span was opened.",
185
+ )
186
+ TRACE_SPAN_COMPLETED = (
187
+ "llm.trace.span.completed",
188
+ "A span completed successfully.",
189
+ )
190
+ TRACE_SPAN_FAILED = (
191
+ "llm.trace.span.failed",
192
+ "A span terminated with an error or timeout.",
193
+ )
194
+ TRACE_AGENT_STEP = (
195
+ "llm.trace.agent.step",
196
+ "One iteration of a multi-step agent loop (RFC-0001 §8.4).",
197
+ )
198
+ TRACE_AGENT_COMPLETED = (
199
+ "llm.trace.agent.completed",
200
+ "A multi-step agent run resolved (RFC-0001 §8.5).",
201
+ )
202
+ TRACE_REASONING_STEP = (
203
+ "llm.trace.reasoning.step",
204
+ "One chain-of-thought reasoning step (v2.0+, RFC-0001 §8.2).",
205
+ )
206
+
207
+ # ------------------------------------------------------------------
208
+ # llm.cost.* — RFC-0001 §9.3
209
+ # ------------------------------------------------------------------
210
+ COST_TOKEN_RECORDED = (
211
+ "llm.cost.token.recorded",
212
+ "Per-call token cost recorded.",
213
+ )
214
+ COST_SESSION_RECORDED = (
215
+ "llm.cost.session.recorded",
216
+ "Session-level cost rollup recorded.",
217
+ )
218
+ COST_ATTRIBUTED = (
219
+ "llm.cost.attributed",
220
+ "Cost manually attributed to a feature, team, or budget centre.",
221
+ )
222
+
223
+ # ------------------------------------------------------------------
224
+ # llm.cache.* — RFC-0001 §7.2
225
+ # ------------------------------------------------------------------
226
+ CACHE_HIT = (
227
+ "llm.cache.hit",
228
+ "Semantic cache returned a cached result without a new model call.",
229
+ )
230
+ CACHE_MISS = (
231
+ "llm.cache.miss",
232
+ "Semantic cache lookup found no matching entry.",
233
+ )
234
+ CACHE_EVICTED = (
235
+ "llm.cache.evicted",
236
+ "A cache entry was evicted (TTL, LRU, or manual invalidation).",
237
+ )
238
+ CACHE_WRITTEN = (
239
+ "llm.cache.written",
240
+ "A new entry was written to the semantic cache.",
241
+ )
242
+
243
+ # ------------------------------------------------------------------
244
+ # llm.eval.* — RFC-0001 §7.2
245
+ # ------------------------------------------------------------------
246
+ EVAL_SCORE_RECORDED = (
247
+ "llm.eval.score.recorded",
248
+ "A quality score was attached to a span or agent run.",
249
+ )
250
+ EVAL_REGRESSION_DETECTED = (
251
+ "llm.eval.regression.detected",
252
+ "A quality regression relative to baseline was detected.",
253
+ )
254
+ EVAL_SCENARIO_STARTED = (
255
+ "llm.eval.scenario.started",
256
+ "An evaluation scenario run started.",
257
+ )
258
+ EVAL_SCENARIO_COMPLETED = (
259
+ "llm.eval.scenario.completed",
260
+ "An evaluation scenario run completed.",
261
+ )
262
+
263
+ # ------------------------------------------------------------------
264
+ # llm.guard.* — RFC-0001 §7.2
265
+ # ------------------------------------------------------------------
266
+ GUARD_INPUT_BLOCKED = (
267
+ "llm.guard.input.blocked",
268
+ "A model input was blocked by the safety classifier.",
269
+ )
270
+ GUARD_INPUT_PASSED = (
271
+ "llm.guard.input.passed",
272
+ "A model input passed the safety classifier.",
273
+ )
274
+ GUARD_OUTPUT_BLOCKED = (
275
+ "llm.guard.output.blocked",
276
+ "A model output was blocked by the safety classifier.",
277
+ )
278
+ GUARD_OUTPUT_PASSED = (
279
+ "llm.guard.output.passed",
280
+ "A model output passed the safety classifier.",
281
+ )
282
+
283
+ # ------------------------------------------------------------------
284
+ # llm.fence.* — RFC-0001 §7.2
285
+ # ------------------------------------------------------------------
286
+ FENCE_VALIDATED = (
287
+ "llm.fence.validated",
288
+ "Model output passed all structural constraint checks.",
289
+ )
290
+ FENCE_RETRY_TRIGGERED = (
291
+ "llm.fence.retry.triggered",
292
+ "Model output failed schema validation; retry initiated.",
293
+ )
294
+ FENCE_MAX_RETRIES_EXCEEDED = (
295
+ "llm.fence.max_retries.exceeded",
296
+ "All retry attempts exhausted without conforming output.",
297
+ )
298
+
299
+ # ------------------------------------------------------------------
300
+ # llm.prompt.* — RFC-0001 §7.2
301
+ # ------------------------------------------------------------------
302
+ PROMPT_RENDERED = (
303
+ "llm.prompt.rendered",
304
+ "A prompt template was instantiated with variable values.",
305
+ )
306
+ PROMPT_TEMPLATE_LOADED = (
307
+ "llm.prompt.template.loaded",
308
+ "A prompt template was loaded from the registry.",
309
+ )
310
+ PROMPT_VERSION_CHANGED = (
311
+ "llm.prompt.version.changed",
312
+ "The active version of a prompt template was updated.",
313
+ )
314
+
315
+ # ------------------------------------------------------------------
316
+ # llm.redact.* — RFC-0001 §12
317
+ # ------------------------------------------------------------------
318
+ REDACT_PII_DETECTED = (
319
+ "llm.redact.pii.detected",
320
+ "PII categories were found in one or more event fields.",
321
+ )
322
+ REDACT_PHI_DETECTED = (
323
+ "llm.redact.phi.detected",
324
+ "PHI categories (HIPAA-regulated) were found.",
325
+ )
326
+ REDACT_APPLIED = (
327
+ "llm.redact.applied",
328
+ "A RedactionPolicy was applied; sensitive values replaced.",
329
+ )
330
+
331
+ # ------------------------------------------------------------------
332
+ # llm.diff.* — RFC-0001 §7.2
333
+ # ------------------------------------------------------------------
334
+ DIFF_COMPUTED = (
335
+ "llm.diff.computed",
336
+ "A textual or semantic diff was computed between two events.",
337
+ )
338
+ DIFF_REGRESSION_FLAGGED = (
339
+ "llm.diff.regression.flagged",
340
+ "A diff computation exceeded the regression similarity threshold.",
341
+ )
342
+
343
+ # ------------------------------------------------------------------
344
+ # llm.template.* — RFC-0001 §7.2
345
+ # ------------------------------------------------------------------
346
+ TEMPLATE_REGISTERED = (
347
+ "llm.template.registered",
348
+ "A new template or version was added to the registry.",
349
+ )
350
+ TEMPLATE_VARIABLE_BOUND = (
351
+ "llm.template.variable.bound",
352
+ "A variable was bound to a template for a specific rendering.",
353
+ )
354
+ TEMPLATE_VALIDATION_FAILED = (
355
+ "llm.template.validation.failed",
356
+ "A template could not be loaded or rendered due to validation errors.",
357
+ )
358
+
359
+ # ------------------------------------------------------------------
360
+ # llm.audit.* — RFC-0001 §11
361
+ # ------------------------------------------------------------------
362
+ AUDIT_KEY_ROTATED = (
363
+ "llm.audit.key.rotated",
364
+ "The HMAC signing key was rotated (RFC-0001 §11.5).",
365
+ )
366
+
367
+ # ------------------------------------------------------------------
368
+ # RFC-0001 SPANFORGE — decision.*
369
+ # ------------------------------------------------------------------
370
+ DECISION_MADE = (
371
+ "decision.made",
372
+ "An agent made a decision (classification, routing, generation, or tool selection).",
373
+ )
374
+ DECISION_REVISED = (
375
+ "decision.revised",
376
+ "A prior decision was revised based on new information or feedback.",
377
+ )
378
+ DECISION_REJECTED = (
379
+ "decision.rejected",
380
+ "A proposed decision was rejected by a safety guardrail or HITL reviewer.",
381
+ )
382
+
383
+ # ------------------------------------------------------------------
384
+ # RFC-0001 SPANFORGE — tool_call.*
385
+ # ------------------------------------------------------------------
386
+ TOOL_CALL_INVOKED = (
387
+ "tool_call.invoked",
388
+ "An external tool was invoked by the agent.",
389
+ )
390
+ TOOL_CALL_COMPLETED = (
391
+ "tool_call.completed",
392
+ "A tool invocation completed successfully with outputs.",
393
+ )
394
+ TOOL_CALL_FAILED = (
395
+ "tool_call.failed",
396
+ "A tool invocation terminated with an error or timeout.",
397
+ )
398
+
399
+ # ------------------------------------------------------------------
400
+ # RFC-0001 SPANFORGE — chain.*
401
+ # ------------------------------------------------------------------
402
+ CHAIN_STARTED = (
403
+ "chain.started",
404
+ "A multi-step prompt chain or workflow was started.",
405
+ )
406
+ CHAIN_STEP_COMPLETED = (
407
+ "chain.step_completed",
408
+ "One step of a chain completed; cumulative state updated.",
409
+ )
410
+ CHAIN_COMPLETED = (
411
+ "chain.completed",
412
+ "All chain steps resolved successfully.",
413
+ )
414
+ CHAIN_FAILED = (
415
+ "chain.failed",
416
+ "A chain step failed; error propagated to chain level.",
417
+ )
418
+
419
+ # ------------------------------------------------------------------
420
+ # RFC-0001 SPANFORGE — confidence.*
421
+ # ------------------------------------------------------------------
422
+ CONFIDENCE_SAMPLE = (
423
+ "confidence.sample",
424
+ "A confidence score was sampled from a model output.",
425
+ )
426
+ CONFIDENCE_THRESHOLD_BREACH = (
427
+ "confidence.threshold_breach",
428
+ "A confidence score fell below the configured threshold.",
429
+ )
430
+
431
+ # ------------------------------------------------------------------
432
+ # RFC-0001 SPANFORGE — consent.*
433
+ # ------------------------------------------------------------------
434
+ CONSENT_GRANTED = (
435
+ "consent.granted",
436
+ "Consent was granted for the specified data access scope.",
437
+ )
438
+ CONSENT_REVOKED = (
439
+ "consent.revoked",
440
+ "A previously granted consent was revoked by the user.",
441
+ )
442
+ CONSENT_VIOLATION = (
443
+ "consent.violation",
444
+ "An agent action exceeded the declared consent boundary.",
445
+ )
446
+
447
+ # ------------------------------------------------------------------
448
+ # RFC-0001 SPANFORGE — drift.*
449
+ # ------------------------------------------------------------------
450
+ DRIFT_DETECTED = (
451
+ "drift.detected",
452
+ "A statistical drift signal was detected against the deployment baseline.",
453
+ )
454
+ DRIFT_THRESHOLD_BREACH = (
455
+ "drift.threshold_breach",
456
+ "Drift exceeded the configured Z-score or KL-divergence threshold.",
457
+ )
458
+ DRIFT_RESOLVED = (
459
+ "drift.resolved",
460
+ "A previously detected drift signal returned within normal bounds.",
461
+ )
462
+
463
+ # ------------------------------------------------------------------
464
+ # RFC-0001 SPANFORGE — latency.*
465
+ # ------------------------------------------------------------------
466
+ LATENCY_SAMPLE = (
467
+ "latency.sample",
468
+ "An end-to-end or per-step latency measurement was recorded.",
469
+ )
470
+ LATENCY_SLA_BREACH = (
471
+ "latency.sla_breach",
472
+ "Measured latency exceeded the configured SLA target.",
473
+ )
474
+
475
+ # ------------------------------------------------------------------
476
+ # RFC-0001 SPANFORGE — hitl.*
477
+ # ------------------------------------------------------------------
478
+ HITL_QUEUED = (
479
+ "hitl.queued",
480
+ "An agent action was queued for human review.",
481
+ )
482
+ HITL_REVIEWED = (
483
+ "hitl.reviewed",
484
+ "A human reviewer made a decision on a queued item.",
485
+ )
486
+ HITL_ESCALATED = (
487
+ "hitl.escalated",
488
+ "A queued item was escalated to the next reviewer tier.",
489
+ )
490
+ HITL_TIMEOUT = (
491
+ "hitl.timeout",
492
+ "SLA timer expired before a human review decision was made.",
493
+ )
494
+
495
+ # ------------------------------------------------------------------
496
+ # RFC-0001 SPANFORGE — playbook.*
497
+ # ------------------------------------------------------------------
498
+ PLAYBOOK_TRIGGERED = (
499
+ "playbook.triggered",
500
+ "A playbook was activated by a matching event.",
501
+ )
502
+ PLAYBOOK_STEP_EXECUTED = (
503
+ "playbook.step_executed",
504
+ "One playbook step executed; outcome recorded.",
505
+ )
506
+ PLAYBOOK_COMPLETED = (
507
+ "playbook.completed",
508
+ "All playbook steps completed successfully.",
509
+ )
510
+ PLAYBOOK_FAILED = (
511
+ "playbook.failed",
512
+ "A playbook step failed; execution halted.",
513
+ )
514
+
515
+ # ------------------------------------------------------------------
516
+ # RFC-0001 SPANFORGE — model_registry.*
517
+ # ------------------------------------------------------------------
518
+ MODEL_REGISTERED = (
519
+ "model_registry.registered",
520
+ "A model was registered in the model registry with lifecycle metadata.",
521
+ )
522
+ MODEL_DEPRECATED = (
523
+ "model_registry.deprecated",
524
+ "A registered model was marked as deprecated.",
525
+ )
526
+ MODEL_RETIRED = (
527
+ "model_registry.retired",
528
+ "A registered model was fully retired from production.",
529
+ )
530
+
531
+ # ------------------------------------------------------------------
532
+ # RFC-0001 SPANFORGE — explanation.*
533
+ # ------------------------------------------------------------------
534
+ EXPLANATION_GENERATED = (
535
+ "explanation.generated",
536
+ "An explainability record was generated for an agent decision.",
537
+ )
538
+
539
+ # ------------------------------------------------------------------
540
+ # RFC-0001 SPANFORGE — audit.* (tamper-evident chain events)
541
+ # ------------------------------------------------------------------
542
+ AUDIT_EVENT_SIGNED = (
543
+ "audit.event_signed",
544
+ "An event was cross-referenced into the tamper-evident audit chain.",
545
+ )
546
+ AUDIT_CHAIN_VERIFIED = (
547
+ "audit.chain_verified",
548
+ "An audit chain segment was verified to be intact.",
549
+ )
550
+ AUDIT_TAMPER_DETECTED = (
551
+ "audit.tamper_detected",
552
+ "A break in the audit chain HMAC sequence was detected.",
553
+ )
554
+
555
+ # ------------------------------------------------------------------
556
+ # v1.0 — Compliance layer event types
557
+ # ------------------------------------------------------------------
558
+ AUDIT_TOMBSTONE = (
559
+ "llm.audit.tombstone",
560
+ "A GDPR erasure tombstone replacing a scrubbed event in the chain.",
561
+ )
562
+ AUDIT_KEY_EXPIRED = (
563
+ "llm.audit.key_expired",
564
+ "The signing key has passed its configured expiry date.",
565
+ )
566
+ AUDIT_CHAIN_ROTATED = (
567
+ "llm.audit.chain_rotated",
568
+ "Audit log file was rotated; chain continues in a new file.",
569
+ )
570
+
571
+ # ------------------------------------------------------------------
572
+ # Properties
573
+ # ------------------------------------------------------------------
574
+
575
+ @property
576
+ def namespace(self) -> str:
577
+ """Return the namespace prefix.
578
+
579
+ For ``llm.*`` namespaces returns ``"llm.<ns>"`` (e.g. ``"llm.trace"``).
580
+ For RFC-0001 SPANFORGE namespaces returns the first segment
581
+ (e.g. ``"decision"``, ``"tool_call"``).
582
+ """
583
+ parts = self.value.split(".")
584
+ if parts[0] == "llm":
585
+ return f"{parts[0]}.{parts[1]}"
586
+ return parts[0]
587
+
588
+ @property
589
+ def description(self) -> str:
590
+ """Return the one-line RFC description for this event type."""
591
+ return self._description
592
+
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # Module-level helpers
596
+ # ---------------------------------------------------------------------------
597
+
598
+ _REGISTERED: Final[frozenset[str]] = frozenset(et.value for et in EventType)
599
+
600
+
601
+ def is_registered(event_type: str) -> bool:
602
+ """Return ``True`` if *event_type* is a first-party registered type (RFC Appendix B)."""
603
+ return event_type in _REGISTERED
604
+
605
+
606
+ def namespace_of(event_type: str) -> str:
607
+ """Extract the ``llm.<ns>`` namespace prefix from *event_type*.
608
+
609
+ Works for both registered RFC types and extension types.
610
+
611
+ Raises:
612
+ EventTypeError: If *event_type* does not match the expected pattern.
613
+
614
+ Example::
615
+
616
+ namespace_of("llm.trace.span.completed") # "llm.trace"
617
+ namespace_of("decision.made") # "decision"
618
+ namespace_of("com.example.myns.event.action") # "com.example"
619
+ """
620
+ if not _EVENT_TYPE_RE.match(event_type):
621
+ raise EventTypeError(
622
+ event_type,
623
+ f"does not match required pattern {EVENT_TYPE_PATTERN!r}",
624
+ )
625
+ parts = event_type.split(".")
626
+ # RFC-0001 SPANFORGE new namespaces: single-word prefix (e.g. "decision.made")
627
+ if parts[0] != "llm" and parts[0] in RFC_SPANFORGE_NAMESPACES:
628
+ return parts[0]
629
+ return f"{parts[0]}.{parts[1]}"
630
+
631
+
632
+ def validate_custom(event_type: str) -> None:
633
+ """Validate a third-party extension event type string (RFC-0001 §7.3).
634
+
635
+ Extension types MUST use a reverse-domain prefix (e.g. ``com.example.…``)
636
+ and MUST NOT claim a reserved ``llm.*`` namespace.
637
+
638
+ Raises:
639
+ EventTypeError: If the type is malformed or claims a reserved namespace.
640
+
641
+ Example::
642
+
643
+ validate_custom("com.example.model.call.completed") # OK
644
+ validate_custom("llm.trace.span.completed") # raises — reserved
645
+ """
646
+ if not _EVENT_TYPE_RE.match(event_type):
647
+ raise EventTypeError(
648
+ event_type,
649
+ f"does not match the required pattern {EVENT_TYPE_PATTERN!r}. "
650
+ "Extension types must use a reverse-domain prefix outside 'llm.*'.",
651
+ )
652
+
653
+ ns = namespace_of(event_type)
654
+ if ns in _RESERVED_NAMESPACES and not is_registered(event_type):
655
+ raise EventTypeError(
656
+ event_type,
657
+ f"namespace '{ns}' is reserved by RFC-0001. "
658
+ "Use a reverse-domain prefix (e.g. 'com.example.…') for custom types.",
659
+ )
660
+ if ns in _FUTURE_NAMESPACES:
661
+ raise EventTypeError(
662
+ event_type,
663
+ f"namespace '{ns}' is reserved for future spanforge standardisation (RFC-0001 §7.4).",
664
+ )
665
+
666
+
667
+ def get_by_value(value: str) -> EventType | None:
668
+ """Return the :class:`EventType` matching *value*, or ``None``.
669
+
670
+ Example::
671
+
672
+ et = get_by_value("llm.trace.span.completed")
673
+ assert et is EventType.TRACE_SPAN_COMPLETED
674
+ """
675
+ try:
676
+ return EventType(value)
677
+ except ValueError:
678
+ return None
679
+
680
+
681
+ # ---------------------------------------------------------------------------
682
+ # Span error category
683
+ # ---------------------------------------------------------------------------
684
+
685
+ SpanErrorCategory = Literal[
686
+ "agent_error",
687
+ "llm_error",
688
+ "tool_error",
689
+ "timeout_error",
690
+ "unknown_error",
691
+ ]
692
+ """Valid values for :attr:`~spanforge._span.Span.error_category`.
693
+
694
+ Automatically set by :meth:`~spanforge._span.Span.record_error` based
695
+ on the exception type, or supplied explicitly by the caller.
696
+ """