spanweave
Turn agentic-system telemetry into a graph you can reason over — without inheriting anyone's opinions about what the telemetry means.
spanweave ingests execution traces from agent frameworks and instrumentors —
OpenInference and OTel GenAI today — and produces one normalized,
deterministic, semantically neutral graph. It assigns no roles, no severity,
no cost, no risk. It tells you what the telemetry observed and how it knows —
and then gets out of your way. A dialect it does not read yet is a new
adapter, never a change to the graph model (ADAPTERS.md).
Point it at a trace. Every path below is a file that ships in this repository, so the whole of this section runs as written from a checkout:
$ spanweave inspect fixtures/conformance/llm_tool_llm/dialects/openinference.jsonl
trace: t1
schema: 0.1 (NOT FROZEN)
adapters: openinference 0.1.0
nodes: 4
agent: 1
llm: 2
tool: 1
edges: 7
call_result (explicit): 1
data (explicit): 1
parent (explicit): 3
temporal (derived): 2
payloads:
inputs present: 4
outputs absent: 1
outputs present: 3
diagnostics: 2
unmapped_attributes: 2
That output is the library in one screen, and it is worth reading before writing any code against it:
- Every edge says how it was established.
explicitmeans the telemetry asserted the relation;derivedmeans spanweave computed it from a stated rule. The twotemporaledges are inferences and are labelled as inferences. Nothing is presented as observed when it was computed. absentis a state, not a blank. One output payload was never recorded, and that is a different fact from an empty one or a redacted one. The distinction survives into the graph rather than being flattened.- Two attributes could not be mapped, and they are diagnostics, not discards. "We didn't understand it" is a reportable outcome here. Nothing vanishes quietly.
- The schema is not frozen, and it says so on every run until
1.0.0.
Then build one and query it:
$ spanweave build fixtures/conformance/llm_tool_llm/dialects/openinference.jsonl -o graph.json
wrote graph.json
import spanweave
# Any OpenInference or OTel GenAI trace. This one ships with the repository.
trace = "fixtures/conformance/llm_tool_llm/dialects/openinference.jsonl"
graph = spanweave.build(trace)
for node in graph.nodes(kind="tool"):
# `name` is the one field two dialects may spell differently, and the
# corpus does not compare it. See Conformance, below, before you match on it.
print(node.name, node.inputs.state, node.outputs.state)
# Traverse only the edges you trust:
causal = graph.subgraph(edge_kinds={"parent", "call_result"})
print(len(list(causal.edges())), "edges you can defend")
tool.lookup present present
4 edges you can defend
Install
$ pip install spanweave
From a checkout:
$ git clone https://github.com/SigorMatt/spanweave
$ cd spanweave
$ pip install .
From a wheel you build yourself:
$ uv build
$ pip install dist/spanweave-0.9.1-py3-none-any.whl
Any of the three gives you import spanweave and the spanweave command with
no runtime dependencies; Python 3.11+ is the only requirement. Nothing is
pulled in behind them: on a fresh pip install spanweave, pip show spanweave
reports an empty Requires.
The conformance corpus in fixtures/ is deliberately not in the wheel — it
is development data, not library code. The paths in the section above therefore
resolve from a checkout or an unpacked sdist, which is where a first look
belongs anyway. Installed from a wheel alone, spanweave reads your own traces
and ships no sample of ours.
Why this exists
Every instrumentor disagrees about attribute keys for the same three facts: what was called, with what, returning what. Reconciling those — plus tool-call↔result pairing, agent nesting, retries, errors, partial payloads — is tedious, genuinely hard, and completely opinion-free work that every downstream tool currently redoes badly.
spanweave does that work once. What you build on top is yours.
The central design idea: typed edges with a stated warrant
spanweave never emits "an edge." Every edge declares what kind of relation
it is and how that relation was established:
| Edge kind | Meaning | Typical warrant |
|---|---|---|
parent |
span hierarchy | explicit — the telemetry said so |
call_result |
a requested tool call and the span that fulfilled it | explicit — id linkage |
data |
an output feeds an input | explicit only — never inferred |
link |
cross-trace span link | explicit |
temporal |
one operation started before another | derived — computed from timestamps |
A consumer that needs causal grounding walks parent + call_result. A
consumer that just needs a timeline walks temporal. Nobody is forced to accept
an inference they didn't ask for, and nothing is presented as observed when it
was computed.
This is what makes one graph shape serve uses it wasn't designed for.
What spanweave does not do
These are permanent non-goals, not a backlog. See SPEC.md §9.
- No semantics. No roles (source / sensitive / sink), no severity, no risk, no quality scores, no dollar costs.
- No inferred data flow. It will not guess that A's output reached B's input. That is your analysis, on top of the graph.
- No enforcement, no runtime. It never sits in a request path.
- No network, ever. It reads files and stdin; it writes files and stdout.
- No execution of payload content. Trace payloads are treated as hostile
data (
SECURITY.md).
Guarantees
- Deterministic. Same input bytes → byte-identical graph. Sorted adjacency, explicit tie-breaks, no clocks, no randomness, no salted hashing.
- Lossless. Every node keeps its verbatim source record. Anything that can't be mapped becomes a diagnostic, never a silent discard.
- Zero runtime dependencies. Core is stdlib-pure and readable in one sitting.
- Dialect-agnostic core. Adding a dialect is a new adapter, never a change to the graph model.
Conformance
A scenario in fixtures/conformance/ is one run, rendered in each dialect
that can express it, and every rendering must produce that scenario's single
canonical graph. That equivalence is the library's entire reason to exist, and
it is a test, not a claim: make conformance.
What it covers today, in numbers rather than adjectives. The corpus holds
21 scenarios. 17 are rendered in both dialects and compared across them.
The other 4 are rendered in one, because the second dialect genuinely cannot
express them — and each says so in a coverage.json file carrying the reason,
because silence would be indistinguishable from an adapter nobody got round to
(FIXTURES.md §4.3).
One field is set aside, said here rather than found later. A scenario may
declare a field dialect-varying — a reviewable file in the corpus, never a
branch in the comparison code (FIXTURES.md §4.4). One field is declared almost
everywhere: name, the span name, which two instrumentors are least likely to
spell the same way. 16 of those 17 cross-dialect scenarios declare it, so
the equivalence claim above is a statement about everything else — ids, kinds,
operations, timestamps, statuses, payload states and values, usage, and every
edge with its warrant and basis. If you are matching nodes by name across
dialects, nothing here has tested that for you. CONTRACTS.md carries the
measurement.
Status
Early development. This is 0.9.1. 0.9.0 was the first version published
for anyone outside this repository (pip install spanweave), and 0.9 rather
than 1.0 is on purpose: see the schema note below. 0.9.1 changes one thing
— what the CLI prints on stderr when a file it was asked to open is not there.
The library, the graph and the schema are byte-for-byte what 0.9.0 shipped.
What exists. Two adapters, openinference and otel_genai, both registered
and both run against the whole corpus (spanweave adapters). The graph model,
the query surface, annotations, canonical serialization, the CLI (build,
inspect, validate, adapters), and the conformance corpus described above.
Three consumers in examples/ — a trajectory dumper, a cost/latency attributor,
and a fleet aggregator — each reading committed fixtures through the public API
and nothing else.
What that second adapter was for. It exists to falsify this model rather
than confirm it: if one graph shape cannot carry two independently designed
instrumentors, the shape is wrong, and the corpus is where that would show. It
did show things — the four scenarios above that the second dialect cannot
express, and the name bound — which is the mechanism working, not a result to
round off. The examples were the same exercise pointed the other way. The two
confirmatory ones needed no change to the library; the adversarial one, written
to break it, produced nine findings, one of which is why the error types are on
the public API today.
What does not exist yet. A third dialect, the binary OTLP form, streaming or any receiver, and a frozen schema. None of these is on a date.
The graph schema is not frozen.
It stays unfrozen through the 0.9.x release: publishing is reversible and
freezing is not, so the launch happens first and the freeze happens on evidence
(ROADMAP.md). Until 1.0.0, treat the schema as subject to change.
Pin on the spanweave version, not on schema_version. While unfrozen,
schema_version is a single bucket for the whole of 0.x and does not
track changes to the serialized graph — it has not moved across two of them
already, and it will not move before the freeze (SPEC.md §3.9). The field
that does move is the library version, and meta.spanweave_version carries it
in every graph document, so you can read it from the file itself.
What stops a change to the serialized graph shipping unnoticed is not that
field but a committed shape artifact (tests/serialized_shape.json): the
document's field names, types and nesting are pinned, and moving any of them
fails the build until the change is regenerated into the diff.
Development
uv sync --extra dev
make check # the gate: lint, types, tests, the invariant gates, the CLI
make conformance # the corpus: every scenario, against its canonical graph
make install-check # build the wheel, install it, run it from OUTSIDE this repo
make shape # regenerate tests/serialized_shape.json (see above)
make stranger # walk and time the install path above, from a clean venv
make capture is human-run only and makes a real model call — see
capture/README.md.
Documents
| File | Purpose |
|---|---|
SPEC.md |
Behavior. Source of truth. |
DESIGN.md |
Architecture and technology decisions. |
CLAUDE.md |
Operating contract + non-negotiable invariants. |
AGENT.md |
Autonomous build brief (run loop, halt points). |
ROADMAP.md |
Phase sequencing. |
TASKS.md |
PR-sized checklist. |
FIXTURES.md |
The conformance corpus contract. |
ADAPTERS.md |
How to write an adapter (the contribution path). |
CONTRACTS.md |
What every permissively-typed serialized field states and asserts — including where the answer is nothing. |
ENVIRONMENT.md |
Runtime & toolchain contract. |
GLOSSARY.md |
Terms of art, used precisely. |
OPEN_QUESTIONS.md |
Deliberately unresolved decisions. |
PREDICTIONS.md |
Where this model is predicted to be wrong — written before the test. |
SECURITY.md |
Threat model and reporting. |
CONTRIBUTING.md |
How to contribute. |
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spanweave-0.9.1.tar.gz.
File metadata
- Download URL: spanweave-0.9.1.tar.gz
- Upload date:
- Size: 669.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a47a01a59cdb62d393ab4f17e28826f147ed39cb9e30ca53a06505dafc6a875
|
|
| MD5 |
4b052e8b25f2e21707a75ef916c01b31
|
|
| BLAKE2b-256 |
350708b94d9bbdc9bc00208cb971b3bdef53333bf4be789378282c60cf605afe
|
File details
Details for the file spanweave-0.9.1-py3-none-any.whl.
File metadata
- Download URL: spanweave-0.9.1-py3-none-any.whl
- Upload date:
- Size: 65.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9a947e82a81b1bfa2cedfe15c017bc29ee62f421422b929a46b5196d5aa63a7
|
|
| MD5 |
373ecb06fa95ba9aff78fdef81644f74
|
|
| BLAKE2b-256 |
1a43793579fb2bfd42f6aa78106ae403dc753df90729bd1e2bab7dc0b4ed7c1a
|