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 +77 -0
- spanweave/adapters/__init__.py +161 -0
- spanweave/adapters/base.py +58 -0
- spanweave/adapters/openinference.py +544 -0
- spanweave/adapters/otel_genai.py +709 -0
- spanweave/annotate.py +132 -0
- spanweave/api.py +43 -0
- spanweave/build.py +595 -0
- spanweave/cli.py +264 -0
- spanweave/diagnostics.py +116 -0
- spanweave/errors.py +97 -0
- spanweave/graph.py +321 -0
- spanweave/ids.py +97 -0
- spanweave/model.py +319 -0
- spanweave/read.py +185 -0
- spanweave/seam.py +142 -0
- spanweave/serialize.py +293 -0
- spanweave/version.py +19 -0
- spanweave-0.9.0.dist-info/METADATA +301 -0
- spanweave-0.9.0.dist-info/RECORD +23 -0
- spanweave-0.9.0.dist-info/WHEEL +4 -0
- spanweave-0.9.0.dist-info/entry_points.txt +2 -0
- spanweave-0.9.0.dist-info/licenses/LICENSE +21 -0
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
|
+
...
|