spanweave 0.9.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.
spanweave/__init__.py ADDED
@@ -0,0 +1,77 @@
1
+ """spanweave — agentic-system telemetry into a deterministic, neutral graph.
2
+
3
+ The public API is exactly what this module exports; everything else is
4
+ internal and may be refactored freely (``CLAUDE.md``).
5
+
6
+ The library reads trace files and writes a graph. It assigns no roles, no
7
+ judgement, and no domain interpretation of any kind (``SPEC.md`` §1).
8
+
9
+ The error types are part of that surface. ``SPEC.md`` §3.10 tells callers to
10
+ match on an error's ``code`` and never on its message; until a caller can write
11
+ ``except SpanweaveError`` and read ``.code``, that instruction is impossible to
12
+ obey through the public API, and the only alternative is ``except Exception``
13
+ plus ``getattr(error, "code", None)`` -- which puts a missing file, a
14
+ permissions error and a trace the library deliberately refused in the same
15
+ branch. Found by the Phase 2b consumer (`TASKS.md` F4), which had written
16
+ exactly that workaround.
17
+ """
18
+
19
+ from spanweave.annotate import Annotation, AnnotationStore
20
+ from spanweave.api import build
21
+ from spanweave.errors import (
22
+ AdapterSelectionError,
23
+ DuplicateNodeIdError,
24
+ SpanweaveError,
25
+ UnknownAdapterError,
26
+ )
27
+ from spanweave.graph import Graph
28
+ from spanweave.model import (
29
+ Diagnostic,
30
+ DiagnosticLevel,
31
+ Edge,
32
+ EdgeKind,
33
+ Meta,
34
+ Node,
35
+ NodeKind,
36
+ Payload,
37
+ PayloadState,
38
+ Provenance,
39
+ RawRecord,
40
+ Status,
41
+ Usage,
42
+ Warrant,
43
+ )
44
+ from spanweave.serialize import dump, dumps, to_document, validate
45
+ from spanweave.version import SCHEMA_FROZEN, SCHEMA_VERSION, __version__
46
+
47
+ __all__ = [
48
+ "SCHEMA_FROZEN",
49
+ "SCHEMA_VERSION",
50
+ "AdapterSelectionError",
51
+ "Annotation",
52
+ "AnnotationStore",
53
+ "Diagnostic",
54
+ "DiagnosticLevel",
55
+ "DuplicateNodeIdError",
56
+ "Edge",
57
+ "EdgeKind",
58
+ "Graph",
59
+ "Meta",
60
+ "Node",
61
+ "NodeKind",
62
+ "Payload",
63
+ "PayloadState",
64
+ "Provenance",
65
+ "RawRecord",
66
+ "SpanweaveError",
67
+ "Status",
68
+ "UnknownAdapterError",
69
+ "Usage",
70
+ "Warrant",
71
+ "__version__",
72
+ "build",
73
+ "dump",
74
+ "dumps",
75
+ "to_document",
76
+ "validate",
77
+ ]
@@ -0,0 +1,161 @@
1
+ """The adapter registry and dialect selection.
2
+
3
+ Selection is the one place this library can fail in the way it least wants
4
+ to: quietly. A mis-detected input produces a **plausible but wrong graph**,
5
+ and nothing downstream can tell. So ambiguity is a hard error here, never a
6
+ first-wins race and never a fallback to a default (`SPEC.md` §6.1).
7
+
8
+ That hard error is also what makes the weaker half of the mechanism
9
+ survivable. Adapters self-report their confidence and could inflate it
10
+ (`OPEN_QUESTIONS.md` §3); failing loudly on a tie means an inflated claim
11
+ collides visibly instead of winning silently.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Sequence
17
+ from dataclasses import dataclass, field
18
+
19
+ from spanweave.adapters.base import Adapter
20
+ from spanweave.errors import (
21
+ ADAPTER_DETECT_FAILED,
22
+ ADAPTER_UNCONFIDENT,
23
+ DUPLICATE_ADAPTER_ID,
24
+ NO_ADAPTERS_REGISTERED,
25
+ AdapterSelectionError,
26
+ UnknownAdapterError,
27
+ )
28
+ from spanweave.model import JsonValue
29
+
30
+ __all__ = [
31
+ "DETECTION_SAMPLE_SIZE",
32
+ "MINIMUM_CONFIDENCE",
33
+ "AdapterRegistry",
34
+ "detect",
35
+ "get",
36
+ "register",
37
+ "registered",
38
+ ]
39
+
40
+ #: `detect()` sees a bounded sample, so selection costs the same on a
41
+ #: 10-record trace and a 10-million-record one.
42
+ DETECTION_SAMPLE_SIZE = 50
43
+
44
+ #: Below this, nobody is confident enough and the caller is told so.
45
+ MINIMUM_CONFIDENCE = 0.5
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class AdapterRegistry:
50
+ """The registered adapters. Registration order never affects selection."""
51
+
52
+ _adapters: dict[str, Adapter] = field(default_factory=dict)
53
+
54
+ def register(self, adapter: Adapter) -> None:
55
+ existing = self._adapters.get(adapter.id)
56
+ if existing is not None and existing is not adapter:
57
+ raise AdapterSelectionError(
58
+ f"two different adapters both claim the id {adapter.id!r}; "
59
+ f"ids must be unique",
60
+ code=DUPLICATE_ADAPTER_ID,
61
+ )
62
+ self._adapters[adapter.id] = adapter
63
+
64
+ def registered(self) -> tuple[Adapter, ...]:
65
+ """Every adapter, ordered by id -- never by registration order."""
66
+ return tuple(self._adapters[key] for key in sorted(self._adapters))
67
+
68
+ def get(self, adapter_id: str) -> Adapter:
69
+ try:
70
+ return self._adapters[adapter_id]
71
+ except KeyError:
72
+ known = ", ".join(sorted(self._adapters)) or "none registered"
73
+ raise UnknownAdapterError(
74
+ f"no adapter with id {adapter_id!r}; registered: {known}"
75
+ ) from None
76
+
77
+ def confidences(self, sample: Sequence[JsonValue]) -> tuple[tuple[str, float], ...]:
78
+ """Every adapter's confidence in this input, ordered by adapter id."""
79
+ results = []
80
+ for adapter in self.registered():
81
+ try:
82
+ confidence = float(adapter.detect(sample))
83
+ except Exception as failure:
84
+ # `detect()` must not raise (ADAPTERS.md §2). One that does is
85
+ # a broken adapter, not malformed input, and swallowing it
86
+ # would let a different adapter win by default -- which is
87
+ # exactly the silent-wrong-graph outcome this module exists to
88
+ # prevent. So it is reported, loudly, naming the adapter.
89
+ raise AdapterSelectionError(
90
+ f"adapter {adapter.id!r} raised during detection "
91
+ f"({failure!r}); detect() must be pure and must not raise",
92
+ code=ADAPTER_DETECT_FAILED,
93
+ ) from failure
94
+ results.append((adapter.id, confidence))
95
+ return tuple(results)
96
+
97
+ def detect(self, records: Sequence[JsonValue]) -> tuple[Adapter, float]:
98
+ """Choose the adapter for this input, or refuse to (`SPEC.md` §6.1)."""
99
+ if not self._adapters:
100
+ raise AdapterSelectionError(
101
+ "no adapters are registered, so nothing can read this input",
102
+ code=NO_ADAPTERS_REGISTERED,
103
+ )
104
+ sample = list(records[:DETECTION_SAMPLE_SIZE])
105
+ measured = self.confidences(sample)
106
+ best = max(confidence for _, confidence in measured)
107
+ winners = [name for name, confidence in measured if confidence == best]
108
+
109
+ if best < MINIMUM_CONFIDENCE:
110
+ raise AdapterSelectionError(
111
+ f"no adapter is confident enough about this input "
112
+ f"(highest {best:.2f}, minimum {MINIMUM_CONFIDENCE:.2f}). "
113
+ f"{_report(measured)} "
114
+ f"Name one explicitly with --adapter if you know the dialect.",
115
+ code=ADAPTER_UNCONFIDENT,
116
+ )
117
+ if len(winners) > 1:
118
+ tied = ", ".join(winners)
119
+ raise AdapterSelectionError(
120
+ f"this input is ambiguous: {tied} are equally confident "
121
+ f"({best:.2f}). {_report(measured)} "
122
+ f"Name one explicitly with --adapter; guessing between them "
123
+ f"would produce a plausible graph from possibly the wrong "
124
+ f"dialect."
125
+ )
126
+ return self.get(winners[0]), best
127
+
128
+
129
+ def _report(measured: Sequence[tuple[str, float]]) -> str:
130
+ listed = ", ".join(f"{name} {confidence:.2f}" for name, confidence in measured)
131
+ return f"Confidence declared by each adapter: {listed}."
132
+
133
+
134
+ #: The registry the CLI and the public API use.
135
+ REGISTRY = AdapterRegistry()
136
+
137
+
138
+ def register(adapter: Adapter) -> None:
139
+ REGISTRY.register(adapter)
140
+
141
+
142
+ def registered() -> tuple[Adapter, ...]:
143
+ return REGISTRY.registered()
144
+
145
+
146
+ def get(adapter_id: str) -> Adapter:
147
+ return REGISTRY.get(adapter_id)
148
+
149
+
150
+ def detect(records: Sequence[JsonValue]) -> tuple[Adapter, float]:
151
+ return REGISTRY.detect(records)
152
+
153
+
154
+ # Registered here, at the end of the module, so that importing the registry
155
+ # also makes the shipped dialects available -- and so that an adapter file
156
+ # never has to import the registry back (ADAPTERS.md §4).
157
+ from spanweave.adapters.openinference import OpenInferenceAdapter # noqa: E402
158
+ from spanweave.adapters.otel_genai import OtelGenAiAdapter # noqa: E402
159
+
160
+ REGISTRY.register(OpenInferenceAdapter())
161
+ REGISTRY.register(OtelGenAiAdapter())
@@ -0,0 +1,58 @@
1
+ """What an adapter is.
2
+
3
+ An adapter teaches `spanweave` exactly one telemetry dialect, and it is the
4
+ only place dialect knowledge may live. Full authoring guide: ``ADAPTERS.md``.
5
+
6
+ The seam types are defined in ``spanweave.seam`` and re-exported here, because
7
+ an adapter author should need one import and because the builder must be able
8
+ to name ``NormalizedSpan`` without importing anything from this package
9
+ (``DESIGN.md`` §2).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Iterable, Iterator, Sequence
15
+ from typing import Protocol, runtime_checkable
16
+
17
+ from spanweave.model import JsonValue
18
+ from spanweave.seam import CallRole, DeclaredDataEdge, NormalizedSpan, SpanLink
19
+
20
+ __all__ = [
21
+ "Adapter",
22
+ "CallRole",
23
+ "DeclaredDataEdge",
24
+ "NormalizedSpan",
25
+ "SpanLink",
26
+ ]
27
+
28
+
29
+ @runtime_checkable
30
+ class Adapter(Protocol):
31
+ """One dialect, translated. Nothing more (`SPEC.md` §6)."""
32
+
33
+ #: Stable, lowercase, no spaces.
34
+ id: str
35
+ #: The adapter's own version, independent of the library's.
36
+ version: str
37
+
38
+ def detect(self, sample: Sequence[JsonValue]) -> float:
39
+ """Confidence in ``[0.0, 1.0]`` that this adapter handles the input.
40
+
41
+ Pure, and it must not raise. Key on distinctive marker keys, not on
42
+ generic ones. Be honest about partial matches, and **do not return
43
+ 1.0 defensively**: inflated confidence turns detection into a race,
44
+ and a wrong adapter silently producing a plausible graph is far worse
45
+ than an honest "ambiguous input" error.
46
+ """
47
+ ...
48
+
49
+ def parse(self, records: Iterable[JsonValue]) -> Iterator[NormalizedSpan]:
50
+ """Translate records into spans.
51
+
52
+ Pure, lazy, order-independent, and it never raises on malformed
53
+ input: what cannot be mapped becomes a diagnostic on the span, or an
54
+ ``unknown`` span carrying the record verbatim. Transcribe, don't
55
+ interpret -- every temptation to infer something the dialect did not
56
+ say is answered with a ``Diagnostic`` or a ``None``.
57
+ """
58
+ ...