agent-tracegraph 0.2.0__tar.gz → 0.3.0__tar.gz

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 (36) hide show
  1. agent_tracegraph-0.3.0/CHANGELOG.md +274 -0
  2. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/PKG-INFO +48 -17
  3. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/README.md +46 -15
  4. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/contracts/phoenix-runtime.json +1 -1
  5. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/pyproject.toml +14 -2
  6. agent_tracegraph-0.3.0/src/tracegraph/_atomic_write.py +41 -0
  7. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/adapters/langgraph_checkpoint.py +15 -8
  8. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/adapters/otlp_spans.py +100 -35
  9. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/adapters/phoenix_export.py +25 -34
  10. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/analysis/ahu.py +13 -11
  11. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/analysis/diagnose.py +118 -94
  12. agent_tracegraph-0.3.0/src/tracegraph/analysis/memo.py +193 -0
  13. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/analysis/patterns.py +15 -6
  14. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/artifact.py +45 -37
  15. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/cli.py +98 -90
  16. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/model.py +13 -2
  17. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/normalize.py +134 -40
  18. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/review_candidates.py +6 -19
  19. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/store/in_memory.py +22 -0
  20. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/store/ladybug.py +16 -3
  21. agent_tracegraph-0.2.0/CHANGELOG.md +0 -117
  22. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/.gitignore +0 -0
  23. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/LICENSE +0 -0
  24. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/SECURITY.md +0 -0
  25. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/contracts/analysis-report.schema.json +0 -0
  26. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/contracts/review-candidates.schema.json +0 -0
  27. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/__init__.py +0 -0
  28. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/adapters/__init__.py +0 -0
  29. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/adapters/base.py +0 -0
  30. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/analysis/__init__.py +0 -0
  31. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/analysis/explain.py +0 -0
  32. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/input_validation.py +0 -0
  33. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/py.typed +0 -0
  34. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/sqlite_snapshot.py +0 -0
  35. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/store/__init__.py +0 -0
  36. {agent_tracegraph-0.2.0 → agent_tracegraph-0.3.0}/src/tracegraph/store/base.py +0 -0
@@ -0,0 +1,274 @@
1
+ # Changelog
2
+
3
+ All notable changes to Tracegraph are documented here.
4
+
5
+ The **artifact** schema version and the **report**/**review-candidate** JSON schemas are
6
+ separate contracts from this version number; changes to any of them are called out
7
+ explicitly.
8
+
9
+ ## 0.3.0 - 2026-10-13
10
+
11
+ ### Fixed
12
+
13
+ - An artifact whose content was not canonical (for example a stored trace status that
14
+ disagreed with its steps) was stamped `schema_version` 4 even when it carried no edge
15
+ origins. Version 4 is now stamped only when the file's TREE_PARENT edges are the
16
+ origin-priority projection of its causal edges and that differs from the temporal one; the
17
+ rest of the trace does not affect the stamp. **Digest impact:** canonical artifacts are
18
+ unchanged, but re-serializing a non-canonical trace (one `validate` rejects) whose tree is
19
+ not the origin-priority one, such as a trace with its derived edges missing, now writes
20
+ version 2 or 3 instead of 4. Any digest computed from that re-serialization changes,
21
+ including the `artifact_digest` that `analyze` reports for such a trace.
22
+ - Loading now rejects an envelope that understates its content: a `schema_version` 2 file
23
+ holding an origin-priority projection or derived task steps. Every reader that trusts the
24
+ envelope would have read such a file under the wrong rules, and a re-save rewrote it as a
25
+ different file. An envelope may still overstate what its content needs.
26
+ - `schema_version` must be an integer. `true`, `1.0`, `2.0` and `4.0` were accepted because
27
+ they compare equal, and `true` was migrated as a version 1 file.
28
+ - Evidence floats (`duration_ms`, evaluation `score`) must be finite, and the writer refuses
29
+ to emit `NaN`/`Infinity`, which are not JSON.
30
+ - `--backend ladybug` crashed with a raw traceback on a step `seq` outside the INT64 range
31
+ the backend stores. It now exits with a usage error naming the limit.
32
+ - `InMemoryStore.upsert_edges` accepted a `CAUSED_BY`/`TREE_PARENT` edge whose endpoint was
33
+ not a stored step, or a `BELONGS_TO` edge pointing at another trace; the failure surfaced
34
+ later as a `KeyError` from `ancestors`. It now raises `ValueError` and inserts nothing from
35
+ that batch, matching the LadybugDB store. `WROTE`/`READ` channel edges are unchecked as
36
+ before, and node upsert still replaces by ID.
37
+ - OTLP and Phoenix imports crashed with a raw traceback on a timestamp of `Infinity`, `NaN`
38
+ or a number beyond the representable date range, on a `spanId`/`parentSpanId` given as a
39
+ list, and on Phoenix `annotations` that were not an array. A timestamp that cannot be read
40
+ — including garbage text, which was silently dropped before — is now reported as
41
+ unavailable and disclosed on stderr by `ingest-otlp`, `ingest-phoenix`, `analyze` and
42
+ `phoenix diagnose`. Malformed ids and annotations are rejected with a usage error.
43
+ - An OTLP `status.code` of `true` was read as OK because `true == 1`, and `false` as UNSET
44
+ because `false == 0`; both are now rejected, for OTLP and Phoenix imports alike.
45
+ - LangGraph: two derived task steps could receive the same id (checkpoint `A` with task
46
+ `x#task:y`, checkpoint `A#task:x` with task `y`); the second silently replaced the first
47
+ and gained a second cause it never had. The collision is now rejected like a collision
48
+ with a checkpoint.
49
+ - LangGraph: a failed task that no candidate node reproduces is still reported unnamed, and
50
+ the read now says so on stderr. An unnamed failure is also what a change in LangGraph's
51
+ task-id algorithm would look like, so it is no longer silent.
52
+
53
+ - `diff` and `validate` printed producer text through Rich markup: a step named
54
+ `tool[bold]` showed as `tool`, and an invalid edge naming `ghost[red]X[/red]` showed as
55
+ `ghostX`. Both now print the text literally, as `inspect`, `explain` and `query` already
56
+ did. Output paths are printed literally too: an `--out` path containing `[/]` used to
57
+ raise a markup error after the file had been written, so the command exited 1 for work
58
+ it had completed.
59
+ - `diff --structure` paired any two leftover subtrees because every label is empty under
60
+ that option, then reported them under an empty path (`only in B under > : subtree ·`).
61
+ An empty label is no longer treated as evidence of correspondence, and the reported
62
+ subtrees now show step names so they can be found, while the comparison still ignores
63
+ them. The identical/not-identical verdict and exit code are unchanged.
64
+ - `explain` and `query --explain` labeled a nameless step by its `source`, which for
65
+ OTLP and Phoenix spans is always `loop` and for LangGraph is checkpoint metadata
66
+ (`input`, `loop`), not what the step was. They now use the step kind, like every other
67
+ view.
68
+ - `export-incident-memo` wrote `links_preserved` as `true`/`false`/`null` in its front
69
+ matter but as `True`/`False`/`None` in its summary. Both now use the front-matter form.
70
+ - The Phoenix CLI version-floor message is derived from the floor constant instead of
71
+ repeating it.
72
+
73
+ - `analyze --baseline` published token deltas, and cost deltas, from totals that covered
74
+ only some steps on one side: with tokens on 3 of 3 steps in the baseline and 1 of 3 in
75
+ the current run it reported `-200`, although the silent steps could have used any amount.
76
+ Like the duration delta, these deltas are now withheld with a note unless every step on
77
+ both sides reports the metric. In traces where only model calls carry token counts this
78
+ withholds the token delta. A cost total or delta withheld because steps or runs report
79
+ different or unknown currencies is now disclosed too; before, `analyze` printed
80
+ `cost=unavailable` with no reason.
81
+
82
+ - Writing an artifact or an analysis report left a hidden temporary file behind when the
83
+ write or fsync failed, because the temporary name was recorded only after writing. All
84
+ four output writers now share one helper that removes it on every failure.
85
+
86
+ ### Compatibility notes for imports
87
+
88
+ These change what an input that imported successfully before now produces or whether it
89
+ imports at all. No committed fixture is affected.
90
+
91
+ - An OTLP token count given as a boolean (`true`, or `{"intValue": true}`) was stored as `1`;
92
+ it is now unavailable, matching how an unusable cost is already treated. A non-finite
93
+ token count is unavailable too instead of crashing the import. An `intValue` holding a
94
+ boolean is no longer converted to `0`/`1` for any attribute.
95
+ - Raw OTLP input could inject evaluation summaries through the private
96
+ `_tracegraph_evaluations` span key, bypassing the length limit the Phoenix importer
97
+ applies. Only the Phoenix importer reads that key now.
98
+ - An empty-string `traceId` or `spanId` is rejected. A `parentSpanId` of `0`, `false` or
99
+ `[]` used to be read as "no parent", which made a span with a malformed declared parent a
100
+ root; it is now rejected. A missing, `null` or empty-string `parentSpanId` still marks a
101
+ root span.
102
+ - A Phoenix evaluation score too large to represent crashed the import; it is now
103
+ unavailable, like a non-finite score.
104
+ - When several spans carry the `graph.node.parent_id` a span declares (a loop), the importer
105
+ picks the nearest execution that started earlier. A span with a missing or unreadable
106
+ start time sorted as time 0 and could win that choice, so a timestamp nobody recorded
107
+ decided a causal edge. Such spans no longer take part: the choice is treated as
108
+ ambiguous, and the span falls back to its declared `parentSpanId`, or the import is
109
+ rejected if it has none.
110
+ - The CLA workflow refused every pull request from the maintainer's own commit-author
111
+ account, which is not the account that owns the repository. Both are allowlisted now.
112
+ - The release documentation asked for a personal access token with *read* access to pull
113
+ requests. Posting the CLA comment failed with that configuration and succeeded after
114
+ raising it to write; the cause remains unconfirmed, since GitHub documents either Issues
115
+ write or Pull requests write as sufficient. The documentation now asks for write on
116
+ Contents, Issues and Pull requests alike and records the observation rather than
117
+ inferring a rule.
118
+ - It also did not say that a Trusted Publisher's "Environment name" is the GitHub
119
+ environment rather than the index, which failed the first 0.2.0 rehearsal.
120
+
121
+ ### Dependencies
122
+
123
+ - The optional `[cypher]` extra now accepts LadybugDB `>=0.18,<0.22` (was `<0.19`), and the
124
+ lockfile moves to 0.21.2. The full test suite and every fixture's output are unchanged
125
+ on 0.21.2.
126
+ - CI now tests against Phoenix CLI 1.18.8 (was 1.8.1). The runtime floor stays 1.0.4: this
127
+ pin is what CI proves, not what tracegraph requires.
128
+
129
+ ### Added
130
+
131
+ - `tracegraph export-incident-memo <artifact> -o memo.md` writes a Markdown post-mortem
132
+ intended for long-term memory stores. Its output is a strict allowlist: the run id appears
133
+ only as a truncated SHA-256 `run_digest`, steps appear as `step_<n>_<kind>` aliases, and
134
+ no step name, error message, evaluation label or payload is written. Edges that only show
135
+ containment or have no recorded origin are rendered as "preceded by", never "caused by".
136
+ The memo carries the SHA-256 of the exact artifact bytes it was built from.
137
+
138
+ ### Changed
139
+
140
+ - **Artifact schema version 4.** When a step has several causes, its single TREE_PARENT is
141
+ now chosen by edge origin first — a declared graph or checkpoint parent over span
142
+ containment, over a span link, over an unknown origin — and only then by time. An artifact
143
+ whose tree differs from the purely temporal choice is stamped version 4; every other
144
+ artifact serializes as version 2 or 3, byte for byte as before. **Readers from 0.2.0 and
145
+ older refuse version 4 files** with an explicit unsupported-version error. This build reads
146
+ both projections, and the LadybugDB store re-emits a legacy file unchanged. The derived
147
+ tree feeds `inspect`, `diff`, and `analyze --baseline`, where it decides both the topology
148
+ changes and how steps are matched between the two runs: a step whose tree parent moved
149
+ can now appear as one removed and one added behavior change, and its logical step key
150
+ changes. Raw causal queries (`explain`, `query`) are unaffected.
151
+ - Phoenix timestamps without a timezone are now read as UTC rather than in the local
152
+ timezone of the machine running the import. **Digest impact:** artifacts built from such
153
+ exports now hash the same on every machine, and differ from any artifact previously built
154
+ where the local timezone applied a nonzero offset to those timestamps. Timestamps that
155
+ carry a `Z` or an offset are unchanged.
156
+ - **Phoenix artifacts no longer carry free-text annotation names or labels.** The Phoenix
157
+ import is documented as body-free and already kept only identifier-shaped span names, but
158
+ annotation names and labels were kept as free text (whitespace-collapsed, cut to 200
159
+ characters), so an evaluator's note such as `user said: my password is hunter2` reached
160
+ the artifact. They now pass the same identifier filter as span names: an evaluation whose
161
+ name fails it is dropped, and a label that fails it becomes `null` while the score is
162
+ kept. **Artifact content change:** a re-import of a Phoenix export with such annotations
163
+ produces different bytes and digests. Identifier-shaped text, which can still be personal
164
+ data such as an e-mail address, is kept.
165
+
166
+ ## 0.2.0 - 2026-09-12
167
+
168
+ First published release. `0.1.0` was tagged but never uploaded.
169
+
170
+ ### Fixed
171
+
172
+ - **A LangGraph node that raised an exception was reported as a successful run**
173
+ (`status=ok`, `error_count=0`). LangGraph does not route such a failure through a state
174
+ channel: it records it in the checkpoint's pending writes and writes no further checkpoint,
175
+ so reading channel values alone could not see it and no `--error-channel` value recovered
176
+ it. Each failed task now becomes its own step, hanging off the checkpoint that scheduled it
177
+ rather than marking that checkpoint — which is named after the node that *produced* it, so
178
+ marking it reported the wrong node.
179
+ - A task cancelled because a *sibling* failed is no longer counted as a second failure.
180
+ LangGraph records sibling cancellation through the same `__error__` channel as a genuine
181
+ fault; those tasks are reported as `unset` with no message, so one fault stays one failure
182
+ and a healthy node is never a primary candidate.
183
+
184
+ ### Security
185
+
186
+ - **Reading a checkpoint database no longer executes what it contains.** LangGraph's
187
+ deserializer revives stored objects by importing a module and calling a name, both taken
188
+ from the payload, and by default an unrecognized target is logged and then called — so a
189
+ crafted checkpoint naming `os.system` or `subprocess.run` ran on the machine doing the
190
+ analysis (both verified against the pinned dependency). `tracegraph ingest` now passes a
191
+ serializer with an empty allowlist.
192
+
193
+ **Behaviour change:** objects outside the framework's registered types no longer revive
194
+ through `ingest`; they come back as their raw arguments. If you were relying on a custom
195
+ class being reconstructed from checkpoint state, it will now surface as plain data.
196
+ Constructing `LangGraphCheckpointAdapter` yourself is unaffected — the serializer is
197
+ whichever one your saver carries.
198
+
199
+ ### Added
200
+
201
+ - The failing node's name, recovered by recomputing LangGraph's task id from checkpoint data.
202
+ The match is self-verifying, so a task nothing identifies keeps no name rather than
203
+ borrowing a neighbour's.
204
+ - `StepSource.TASK`, marking a step derived from pending writes rather than from a checkpoint.
205
+ - `RawTrace.ingest_warnings`, printed by `ingest` to stderr — disclosures about the *read*
206
+ (for example `Send` packets that could not be decoded), which are not part of the run and so
207
+ never enter the artifact.
208
+ - First public release metadata: Apache-2.0 `LICENSE`, `CLA.md`, `CODE_OF_CONDUCT.md`,
209
+ `CONTRIBUTING.md`, `SECURITY.md`, and PyPI classifiers, keywords, authors and project URLs.
210
+ - `tracegraph --version` (`-V`), which prints the bare version and nothing else. There was no
211
+ way to ask an installed build what it was.
212
+ - An sdist allowlist. Previously the source distribution shipped everything git tracked —
213
+ tests, docs, CI workflows, the lockfile and the checkpoint fixtures.
214
+ - A tag-triggered release workflow publishing through PyPI Trusted Publishing: `test-v*`
215
+ rehearses against TestPyPI, `v*` publishes to PyPI. Only the publishing job holds an OIDC
216
+ token, and it checks out nothing, installs nothing, and uploads only files whose digests
217
+ match what the build job recorded. Before any of that, the build refuses a tag whose
218
+ commit is not on the default branch, has no green `tests` run of its own, disagrees with
219
+ the packaged version, or has no dated changelog entry.
220
+ - `scripts/verify_artifacts.py`, which checks that the built distributions are exactly the
221
+ two files that will be uploaded and that the source distribution carries only what the
222
+ packaging allowlist intends. It also refuses tests, docs, CI configuration and the
223
+ lockfile outright, so widening that allowlist cannot quietly start shipping them, and
224
+ refuses any archive member whose path does not go where it reads — a `..` segment is
225
+ what would let a member satisfy both lists and still land somewhere else, or outside
226
+ the archive entirely, when someone extracts it.
227
+ - `scripts/require_ci_success.py`, the release gate described above.
228
+ - A [release runbook](docs/releasing.md) and a
229
+ [public release checklist](docs/public-release-checklist.md).
230
+ - The installed-wheel CI jobs now pin the version they expect and run the artifact
231
+ verifier, so the release checks are exercised on every pull request rather than first at
232
+ tag time.
233
+
234
+ ### Changed
235
+
236
+ - **The distribution is now `agent-tracegraph`.** The plain `tracegraph` name on PyPI belongs
237
+ to an unrelated project. The import package, the console script, the schema `kind`
238
+ constants and the `TRACEGRAPH_*` environment variables are **unchanged** — only the name
239
+ you install differs. Installing from source or from a checkout is unaffected.
240
+ - The build backend version is pinned (`hatchling==1.32.0`), so a release rehearsal and the
241
+ production tag cannot run *different backend versions* against the same commit. This is
242
+ drift control, not reproducibility: hatchling's own dependencies still float. The uv
243
+ version used to build is pinned in the same way.
244
+ - `twine check` is a gate again, pinned to `twine@7.0.0`. It was disabled because twine 6
245
+ rejected the `Metadata-Version: 2.5` this backend emits; twine 7 accepts it.
246
+ - **Artifact schema version 3**, stamped *by content*: an artifact carrying derived task steps
247
+ declares 3 so an older reader refuses it by name instead of failing on an enum, while
248
+ artifacts without them still serialize as version 2 byte for byte. The envelope is inside
249
+ the digest `export-review-candidates` binds to, so a blanket bump would have invalidated
250
+ digests already published for unchanged artifacts.
251
+ - The LangGraph capture disclosure in every report now names both the error channel and task
252
+ pending writes. It remains unconditional: an artifact ingested by an older build, or one
253
+ using an unrecognized checkpoint layout, carries no derived step and is indistinguishable
254
+ from a genuinely clean run.
255
+ - `xxhash` is now a direct runtime dependency (LangGraph hashes task ids with it, but
256
+ `langgraph` itself is only a development dependency here).
257
+ - `langgraph-checkpoint>=4.1` is now declared directly rather than left transitive, because
258
+ ingestion calls its serializer API. `allowed_msgpack_modules` does not exist before 4.1
259
+ (4.0.0 raises `TypeError`), so the floor is what makes the hardening above guaranteed
260
+ rather than incidental.
261
+
262
+ ### Known limitations
263
+
264
+ - Tasks that were *scheduled but never committed* — a run paused by `interrupt()`, stopped by
265
+ the recursion limit, or killed — are not reported. Trace `status` still means "did a step
266
+ error", so such a run reports `ok`.
267
+ - Only the latest persisted error survives per task, so repeated attempts are not
268
+ reconstructed.
269
+ - `Send` packets can only be named where `langgraph.types` is importable; an ordinary install
270
+ has only the `langgraph.checkpoint` half of that namespace, and the gap is disclosed.
271
+
272
+ ## 0.1.0 - 2026-05-30
273
+
274
+ Tagged but never published. Initial read-path causal-graph analysis of LangGraph agent traces.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tracegraph
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph.
5
5
  Project-URL: Homepage, https://github.com/memtomem/tracegraph
6
6
  Project-URL: Repository, https://github.com/memtomem/tracegraph
@@ -26,7 +26,7 @@ Requires-Dist: rich>=13
26
26
  Requires-Dist: typer>=0.12
27
27
  Requires-Dist: xxhash>=3
28
28
  Provides-Extra: cypher
29
- Requires-Dist: ladybug<0.19,>=0.18; extra == 'cypher'
29
+ Requires-Dist: ladybug<0.22,>=0.18; extra == 'cypher'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # tracegraph
@@ -110,6 +110,9 @@ tracegraph query tool-failure --backend ladybug A.json B.json # use the option
110
110
 
111
111
  # export versioned, body-free governance evidence (an empty match set is a valid report)
112
112
  tracegraph export-review-candidates tool-retry-failure *.json -o candidates.json
113
+
114
+ # export an allowlisted incident post-mortem (Markdown) for a long-term memory store
115
+ tracegraph export-incident-memo A.json -o incident.md
113
116
  ```
114
117
 
115
118
  Frequently-used options, beyond `--out` / `--json-out`:
@@ -273,17 +276,27 @@ result, blast radius, preflight result, or graph state.
273
276
 
274
277
  ## Status
275
278
 
276
- **MVP works** — LangGraph/Phoenix/OTLP ingest plus inspect, explain, analyze, diff, and query.
279
+ Alpha (`0.x`): the artifact, report and review-candidate formats are versioned contracts,
280
+ and any change to them is called out in the [changelog](CHANGELOG.md).
277
281
 
278
- - **Phase 0 (frozen contract):** `RawTrace` vs `NormalizedTrace`, portable JSON artifact (system of record), `validate_raw` → projection → `validate_tree`/`validate_normalized`, in-memory store, `explain`.
279
- - **Phase 1 (ingestion):** `LangGraphCheckpointAdapter` reads any `BaseCheckpointSaver`, including subgraph checkpoint namespaces, and reconstructs declared checkpoint parentage; `examples/tiny_agent.py` generates real traces.
280
- - **Phase 2 (analysis + CLI):** AHU rooted-tree diff and the Typer CLI.
281
- - **Phase 3 (cross-trace queries):** backend-neutral `PathPattern` matcher over the raw causal graph + `query`/`presets` CLI — pure-Python, proving the "graph queries" value before any Cypher backend. The official `tool-retry-failure@v2` requires an explicit producer marker; variable-length **gaps** and **back-references** (`same_name_as`) remain available for query-only heuristics. Uncompilable unbounded patterns degrade honestly rather than truncate.
282
- - **Phase 5 (OTLP/OpenInference adapter):** `OTLPSpanAdapter` ingests Collector JSON/JSONL spans into the same causal model — the source that exercises full link-preserving raw/derived causality.
283
- - **Phoenix diagnosis:** `PhoenixExportAdapter`, `analyze`, and `phoenix diagnose` provide body-free automatic failure selection, retry detection, telemetry/evaluation summaries, and explicit parent-only fidelity warnings.
284
- - **Phase 6 (optional Cypher backend):** `agent-tracegraph[cypher]` ships a `LadybugStore` that compiles the **same** `PathPattern` spec to Cypher (`compile_to_cypher`); equivalence with the pure-Python matcher is the test contract, so the Cypher path is an accelerator, never a second source of truth.
285
- - **Ecosystem T3/P4 review slice:** OTLP `syncmill.run_id` correlation, versioned presets, deterministic body-free `export-review-candidates`, SyncMill human-review board intake, Toolgraph G3 artifact annotation, and the pinned live single-failure review path are complete. Explicit retry causality and SyncMill-to-Phoenix streaming are covered by the controlled E2E workflow; broader operational coverage remains separate.
286
- - **SyncMill contract completion:** route/pipeline/compete/council/decompose plus cancellation fixtures, stable span naming, body-free artifact digests, fail-open exporter reference behavior, operational failure presets, and non-causal Toolgraph preflight evidence are covered by executable tests.
282
+ | Area | What exists |
283
+ |---|---|
284
+ | Sources | LangGraph checkpoint savers (including subgraph namespaces and native task failures), OTLP/OpenInference span exports, Phoenix CLI exports |
285
+ | Commands | `ingest`, `ingest-otlp`, `ingest-phoenix`, `inspect`, `validate`, `explain`, `diff`, `analyze`, `presets`, `query`, `export-review-candidates`, `export-incident-memo`, `phoenix doctor`, `phoenix diagnose` |
286
+ | Patterns | Versioned presets matched over the raw causal graph; the governance-eligible `tool-retry-failure@v2` requires an explicit producer retry marker, and repetition-only heuristics are query-only |
287
+ | Backends | Pure-Python in-memory store (default); optional LadybugDB/Cypher accelerator (`[cypher]`), whose results must equal the pure-Python matcher's |
288
+ | Ecosystem | SyncMill run correlation (`syncmill.run_id`), Toolgraph preflight evidence as non-causal metadata, a weekly Phoenix + SyncMill end-to-end workflow (Phoenix CLI and server pinned; SyncMill tracks its default branch) |
289
+
290
+ Known limits:
291
+
292
+ - A LangGraph task that was scheduled but left no result and no recorded error (a run
293
+ paused by `interrupt()`, stopped by the recursion limit, or killed) is not reported, so
294
+ such a run reads `ok` unless some other step recorded an error.
295
+ - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
296
+ causes may be missing.
297
+ - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
298
+ producer's names and error messages.
299
+ - CI covers Python 3.12 on Linux.
287
300
 
288
301
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
289
302
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
@@ -328,14 +341,30 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
328
341
  the report. Trace/step identifiers remain; this is not anonymization.
329
342
  Raw artifact names and bytes are not rewritten for report privacy or matching, so the
330
343
  artifact — unlike the report — can still carry whatever the producer put in a span name or
331
- error message (Phoenix ingestion excepted; it filters at the source).
344
+ error message. Phoenix ingestion is the exception: it filters at the source, keeping span
345
+ names and annotation names and labels only when they are identifier-shaped, and dropping
346
+ status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
332
347
  `export-review-candidates` output is not passed through the report redactor: its `tool_key`
333
- is the raw endpoint span name. Logical
348
+ is the raw endpoint span name. `export-incident-memo` goes further than `safe-v1`: it writes
349
+ no step names, error messages or evaluation labels at all. Steps appear as kind-based
350
+ aliases and the run id as a truncated digest; the rest is structural metadata such as trace
351
+ status, causal fidelity, step and error counts, preset pattern ids and versions, edge
352
+ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logical
334
353
  comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
335
354
  Containment and unknown edges preserve all error investigation candidates, deeper first.
336
355
  Explicit causal ancestry supplies context, not proof that an exception propagated.
337
356
  Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
338
357
  artifacts are not reinterpreted.
358
+ A span whose status is UNSET and that carries no exception event (one with a message or
359
+ type makes it `error` in both importers) is read differently by source. `ingest-otlp`
360
+ records it as `ok`, because UNSET is OpenTelemetry's default status and producers such as
361
+ SyncMill leave successful spans unset. Phoenix ingestion keeps it as `unset`. Trace status
362
+ is the same either way, but step statuses differ wherever they are shown, and a preset that
363
+ requires an `ok` step does not match an unset one: a SyncMill run where an agent attempt
364
+ left UNSET directly causes a failing `gate:` tool reports `gate-failure-after-success`
365
+ through `ingest-otlp` and not through Phoenix. Compare a run with a baseline from the same
366
+ source; mixing them can report a step whose status differs only by source as a behavior
367
+ change (`ok` to `unset`, or the reverse).
339
368
 
340
369
  LangGraph ingestion reads both the configured error state channel and native task failures
341
370
  from checkpoint pending writes. A node that raises becomes a derived `task` step under the
@@ -346,9 +375,11 @@ borrowing a neighbour's name. When one task raises, LangGraph
346
375
  cancels its siblings and records that through the same channel; a cancelled task is reported
347
376
  as an `unset` step with no message, because it was torn down rather than at fault. Only the
348
377
  latest persisted error survives per task, so repeated attempts are not reconstructed, and
349
- `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install discloses them instead. An artifact carrying derived task
350
- steps declares artifact `schema_version` 3, so an older reader refuses it by name; artifacts
351
- without them still serialize as version 2, byte for byte. Missing observed errors still do not
378
+ `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install
379
+ discloses them instead. An artifact whose derived tree relies on EdgeOrigin precedence declares
380
+ artifact `schema_version` 4; otherwise, an artifact carrying derived task steps declares
381
+ `schema_version` 3, and artifacts without them still serialize as version 2, byte for byte.
382
+ Missing observed errors still do not
352
383
  prove success: an artifact ingested by an older build, or one using an unrecognized checkpoint
353
384
  layout, carries no derived step and looks exactly like a clean run, which is why the capture
354
385
  disclosure is unconditional. Link preservation
@@ -79,6 +79,9 @@ tracegraph query tool-failure --backend ladybug A.json B.json # use the option
79
79
 
80
80
  # export versioned, body-free governance evidence (an empty match set is a valid report)
81
81
  tracegraph export-review-candidates tool-retry-failure *.json -o candidates.json
82
+
83
+ # export an allowlisted incident post-mortem (Markdown) for a long-term memory store
84
+ tracegraph export-incident-memo A.json -o incident.md
82
85
  ```
83
86
 
84
87
  Frequently-used options, beyond `--out` / `--json-out`:
@@ -242,17 +245,27 @@ result, blast radius, preflight result, or graph state.
242
245
 
243
246
  ## Status
244
247
 
245
- **MVP works** — LangGraph/Phoenix/OTLP ingest plus inspect, explain, analyze, diff, and query.
248
+ Alpha (`0.x`): the artifact, report and review-candidate formats are versioned contracts,
249
+ and any change to them is called out in the [changelog](CHANGELOG.md).
246
250
 
247
- - **Phase 0 (frozen contract):** `RawTrace` vs `NormalizedTrace`, portable JSON artifact (system of record), `validate_raw` → projection → `validate_tree`/`validate_normalized`, in-memory store, `explain`.
248
- - **Phase 1 (ingestion):** `LangGraphCheckpointAdapter` reads any `BaseCheckpointSaver`, including subgraph checkpoint namespaces, and reconstructs declared checkpoint parentage; `examples/tiny_agent.py` generates real traces.
249
- - **Phase 2 (analysis + CLI):** AHU rooted-tree diff and the Typer CLI.
250
- - **Phase 3 (cross-trace queries):** backend-neutral `PathPattern` matcher over the raw causal graph + `query`/`presets` CLI — pure-Python, proving the "graph queries" value before any Cypher backend. The official `tool-retry-failure@v2` requires an explicit producer marker; variable-length **gaps** and **back-references** (`same_name_as`) remain available for query-only heuristics. Uncompilable unbounded patterns degrade honestly rather than truncate.
251
- - **Phase 5 (OTLP/OpenInference adapter):** `OTLPSpanAdapter` ingests Collector JSON/JSONL spans into the same causal model — the source that exercises full link-preserving raw/derived causality.
252
- - **Phoenix diagnosis:** `PhoenixExportAdapter`, `analyze`, and `phoenix diagnose` provide body-free automatic failure selection, retry detection, telemetry/evaluation summaries, and explicit parent-only fidelity warnings.
253
- - **Phase 6 (optional Cypher backend):** `agent-tracegraph[cypher]` ships a `LadybugStore` that compiles the **same** `PathPattern` spec to Cypher (`compile_to_cypher`); equivalence with the pure-Python matcher is the test contract, so the Cypher path is an accelerator, never a second source of truth.
254
- - **Ecosystem T3/P4 review slice:** OTLP `syncmill.run_id` correlation, versioned presets, deterministic body-free `export-review-candidates`, SyncMill human-review board intake, Toolgraph G3 artifact annotation, and the pinned live single-failure review path are complete. Explicit retry causality and SyncMill-to-Phoenix streaming are covered by the controlled E2E workflow; broader operational coverage remains separate.
255
- - **SyncMill contract completion:** route/pipeline/compete/council/decompose plus cancellation fixtures, stable span naming, body-free artifact digests, fail-open exporter reference behavior, operational failure presets, and non-causal Toolgraph preflight evidence are covered by executable tests.
251
+ | Area | What exists |
252
+ |---|---|
253
+ | Sources | LangGraph checkpoint savers (including subgraph namespaces and native task failures), OTLP/OpenInference span exports, Phoenix CLI exports |
254
+ | Commands | `ingest`, `ingest-otlp`, `ingest-phoenix`, `inspect`, `validate`, `explain`, `diff`, `analyze`, `presets`, `query`, `export-review-candidates`, `export-incident-memo`, `phoenix doctor`, `phoenix diagnose` |
255
+ | Patterns | Versioned presets matched over the raw causal graph; the governance-eligible `tool-retry-failure@v2` requires an explicit producer retry marker, and repetition-only heuristics are query-only |
256
+ | Backends | Pure-Python in-memory store (default); optional LadybugDB/Cypher accelerator (`[cypher]`), whose results must equal the pure-Python matcher's |
257
+ | Ecosystem | SyncMill run correlation (`syncmill.run_id`), Toolgraph preflight evidence as non-causal metadata, a weekly Phoenix + SyncMill end-to-end workflow (Phoenix CLI and server pinned; SyncMill tracks its default branch) |
258
+
259
+ Known limits:
260
+
261
+ - A LangGraph task that was scheduled but left no result and no recorded error (a run
262
+ paused by `interrupt()`, stopped by the recursion limit, or killed) is not reported, so
263
+ such a run reads `ok` unless some other step recorded an error.
264
+ - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
265
+ causes may be missing.
266
+ - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
267
+ producer's names and error messages.
268
+ - CI covers Python 3.12 on Linux.
256
269
 
257
270
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
258
271
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
@@ -297,14 +310,30 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
297
310
  the report. Trace/step identifiers remain; this is not anonymization.
298
311
  Raw artifact names and bytes are not rewritten for report privacy or matching, so the
299
312
  artifact — unlike the report — can still carry whatever the producer put in a span name or
300
- error message (Phoenix ingestion excepted; it filters at the source).
313
+ error message. Phoenix ingestion is the exception: it filters at the source, keeping span
314
+ names and annotation names and labels only when they are identifier-shaped, and dropping
315
+ status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
301
316
  `export-review-candidates` output is not passed through the report redactor: its `tool_key`
302
- is the raw endpoint span name. Logical
317
+ is the raw endpoint span name. `export-incident-memo` goes further than `safe-v1`: it writes
318
+ no step names, error messages or evaluation labels at all. Steps appear as kind-based
319
+ aliases and the run id as a truncated digest; the rest is structural metadata such as trace
320
+ status, causal fidelity, step and error counts, preset pattern ids and versions, edge
321
+ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logical
303
322
  comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
304
323
  Containment and unknown edges preserve all error investigation candidates, deeper first.
305
324
  Explicit causal ancestry supplies context, not proof that an exception propagated.
306
325
  Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
307
326
  artifacts are not reinterpreted.
327
+ A span whose status is UNSET and that carries no exception event (one with a message or
328
+ type makes it `error` in both importers) is read differently by source. `ingest-otlp`
329
+ records it as `ok`, because UNSET is OpenTelemetry's default status and producers such as
330
+ SyncMill leave successful spans unset. Phoenix ingestion keeps it as `unset`. Trace status
331
+ is the same either way, but step statuses differ wherever they are shown, and a preset that
332
+ requires an `ok` step does not match an unset one: a SyncMill run where an agent attempt
333
+ left UNSET directly causes a failing `gate:` tool reports `gate-failure-after-success`
334
+ through `ingest-otlp` and not through Phoenix. Compare a run with a baseline from the same
335
+ source; mixing them can report a step whose status differs only by source as a behavior
336
+ change (`ok` to `unset`, or the reverse).
308
337
 
309
338
  LangGraph ingestion reads both the configured error state channel and native task failures
310
339
  from checkpoint pending writes. A node that raises becomes a derived `task` step under the
@@ -315,9 +344,11 @@ borrowing a neighbour's name. When one task raises, LangGraph
315
344
  cancels its siblings and records that through the same channel; a cancelled task is reported
316
345
  as an `unset` step with no message, because it was torn down rather than at fault. Only the
317
346
  latest persisted error survives per task, so repeated attempts are not reconstructed, and
318
- `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install discloses them instead. An artifact carrying derived task
319
- steps declares artifact `schema_version` 3, so an older reader refuses it by name; artifacts
320
- without them still serialize as version 2, byte for byte. Missing observed errors still do not
347
+ `Send` packets are nameable only where `langgraph.types` is importable — an ordinary install
348
+ discloses them instead. An artifact whose derived tree relies on EdgeOrigin precedence declares
349
+ artifact `schema_version` 4; otherwise, an artifact carrying derived task steps declares
350
+ `schema_version` 3, and artifacts without them still serialize as version 2, byte for byte.
351
+ Missing observed errors still do not
321
352
  prove success: an artifact ingested by an older build, or one using an unrecognized checkpoint
322
353
  layout, carries no derived step and looks exactly like a clean run, which is why the capture
323
354
  disclosure is unconditional. Link preservation
@@ -1,5 +1,5 @@
1
1
  {
2
- "phoenix_cli": "1.8.1",
2
+ "phoenix_cli": "1.18.8",
3
3
  "phoenix_server": {
4
4
  "image": "arizephoenix/phoenix",
5
5
  "version": "18.0.0",
@@ -4,7 +4,7 @@
4
4
  # the schema `kind` constants and the TRACEGRAPH_* environment variables — those are package
5
5
  # identity, not distribution identity, and renaming them would break published contracts.
6
6
  name = "agent-tracegraph"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -58,7 +58,7 @@ tracegraph = "tracegraph.cli:main"
58
58
  # LadybugDB is the actively maintained successor to the archived Kùzu project. The JSON
59
59
  # artifact remains authoritative, so an accelerator cache is rebuilt across DB versions.
60
60
  [project.optional-dependencies]
61
- cypher = ["ladybug>=0.18,<0.19"]
61
+ cypher = ["ladybug>=0.18,<0.22"]
62
62
 
63
63
  [dependency-groups]
64
64
  dev = [
@@ -70,6 +70,9 @@ dev = [
70
70
  # a dependency bump there would turn that test red for a reason nothing in it names.
71
71
  "pyyaml>=6",
72
72
  ]
73
+ # Kept out of `dev` so the default environment stays what the test jobs prove; CI's lint
74
+ # job installs it explicitly. Pinned: a new mypy release is a new set of rules.
75
+ typing = ["mypy==1.18.2"]
73
76
 
74
77
  [build-system]
75
78
  # Pinned exactly. A release rehearsal and the production tag are two builds on two days, and
@@ -115,6 +118,15 @@ src = ["src", "tests"]
115
118
  select = ["F", "E4", "E7", "E9", "B"]
116
119
  ignore = ["B008"] # typer declares option defaults in the signature by design
117
120
 
121
+ [tool.mypy]
122
+ # The wheel ships py.typed, so its annotations are a contract. Not `strict`: the goal is
123
+ # that what is annotated is checked and correct, not that every helper gains a signature.
124
+ python_version = "3.12"
125
+ files = ["src/tracegraph"]
126
+ check_untyped_defs = true
127
+ warn_unused_ignores = true
128
+ warn_redundant_casts = true
129
+
118
130
  [tool.pytest.ini_options]
119
131
  testpaths = ["tests"]
120
132
  pythonpath = ["examples"]
@@ -0,0 +1,41 @@
1
+ """Write a text file atomically: a reader sees either the previous file or the complete new one."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import tempfile
8
+
9
+
10
+ def write_text_atomic(
11
+ path: str | Path, text: str, *, replace: bool = True, newline: str | None = None
12
+ ) -> None:
13
+ """Write ``text`` to a temporary file beside ``path``, fsync it, then publish it.
14
+
15
+ ``replace=False`` publishes with a hard link, which fails with ``FileExistsError``
16
+ instead of overwriting an existing file or following a symlink at ``path``. The
17
+ temporary file is removed on every exit path, including a failed write or fsync.
18
+ """
19
+ target = Path(path)
20
+ target.parent.mkdir(parents=True, exist_ok=True)
21
+ temp_name: str | None = None
22
+ try:
23
+ with tempfile.NamedTemporaryFile(
24
+ mode="w",
25
+ encoding="utf-8",
26
+ newline=newline,
27
+ dir=target.parent,
28
+ prefix=f".{target.name}.",
29
+ delete=False,
30
+ ) as handle:
31
+ temp_name = handle.name
32
+ handle.write(text)
33
+ handle.flush()
34
+ os.fsync(handle.fileno())
35
+ if replace:
36
+ os.replace(temp_name, target)
37
+ else:
38
+ os.link(temp_name, target)
39
+ finally:
40
+ if temp_name is not None:
41
+ Path(temp_name).unlink(missing_ok=True)