agent-tracegraph 0.8.0__tar.gz → 0.9.1__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 (42) hide show
  1. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/.gitignore +4 -0
  2. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/CHANGELOG.md +228 -0
  3. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/PKG-INFO +100 -24
  4. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/README.md +95 -21
  5. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/SECURITY.md +2 -1
  6. agent_tracegraph-0.9.1/contracts/phoenix-runtime.json +8 -0
  7. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/pyproject.toml +13 -10
  8. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/adapters/langgraph_checkpoint.py +184 -25
  9. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/adapters/otlp_spans.py +44 -16
  10. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/adapters/phoenix_export.py +8 -1
  11. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/check.py +6 -0
  12. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/diagnose.py +35 -0
  13. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/explain.py +3 -3
  14. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/memo.py +17 -4
  15. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/patterns.py +52 -33
  16. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/artifact.py +14 -5
  17. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/cli.py +13 -20
  18. agent_tracegraph-0.9.1/src/tracegraph/input_validation.py +107 -0
  19. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/model.py +22 -7
  20. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/normalize.py +6 -2
  21. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/review_candidates.py +29 -12
  22. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/sqlite_snapshot.py +57 -0
  23. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/store/ladybug.py +44 -15
  24. agent_tracegraph-0.8.0/contracts/phoenix-runtime.json +0 -8
  25. agent_tracegraph-0.8.0/src/tracegraph/input_validation.py +0 -26
  26. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/LICENSE +0 -0
  27. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/contracts/analysis-report.schema.json +0 -0
  28. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/contracts/check-report.schema.json +0 -0
  29. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/contracts/review-candidates.schema.json +0 -0
  30. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/__init__.py +0 -0
  31. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/_atomic_write.py +0 -0
  32. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/_errors.py +0 -0
  33. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/adapters/__init__.py +0 -0
  34. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/adapters/base.py +0 -0
  35. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/__init__.py +0 -0
  36. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/ahu.py +0 -0
  37. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/analysis/motifs.py +0 -0
  38. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/graph_ref.py +0 -0
  39. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/py.typed +0 -0
  40. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/store/__init__.py +0 -0
  41. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/store/base.py +0 -0
  42. {agent_tracegraph-0.8.0 → agent_tracegraph-0.9.1}/src/tracegraph/store/in_memory.py +0 -0
@@ -12,3 +12,7 @@ wheels/
12
12
  # Local Codex/dev-trio review artifacts (logs, manifests, run metadata).
13
13
  .dev-trio/
14
14
  .DS_Store
15
+
16
+ # memtomem local artifacts (ADR-0011 project_local tier)
17
+ .memtomem/*.local/
18
+ .memtomem/.staging/
@@ -6,6 +6,234 @@ The **artifact** schema version and the **report**/**review-candidate** JSON sch
6
6
  separate contracts from this version number; changes to any of them are called out
7
7
  explicitly.
8
8
 
9
+ ## 0.9.1 - 2026-11-04
10
+
11
+ ### Fixed
12
+
13
+ - **A LangGraph failure stored as an allowed constructor call this machine cannot run is no
14
+ longer dropped** (#184). The checkpoint serializer stores some objects as a call to rebuild
15
+ them, such as a `pathlib.WindowsPath` written on Windows; read on macOS or Linux, that call
16
+ raises. langgraph-checkpoint before 4.3.0 read such a value in the error channel as `None`,
17
+ so the failing step came out `ok`, the trace `ok`, with no capture gap. The new
18
+ `langgraph-checkpoint>=4.3.0` floor (below) means the serializer returns the call's stored
19
+ argument instead: the step is recorded as an error whose message is that argument's text
20
+ (`['C:\\', 'data', 'failed-input.csv']` for the path above).
21
+
22
+ Only that case changed. An error value that fails to decode any other way can still read
23
+ as `None` and be dropped — measured on langgraph-checkpoint 4.3.1 for a `WindowsPath` in an
24
+ older `json`-format checkpoint — and so can a stored argument that is itself empty or
25
+ otherwise falsey. Where the error is recorded, nothing in the artifact says the value could
26
+ not be rebuilt. These are tracked in #188.
27
+
28
+ ### Dependencies
29
+
30
+ - `langgraph-checkpoint` now requires `>=4.3.0` (was `>=4.1`), for the fix above, and
31
+ `langgraph-checkpoint-sqlite` requires `>=3.1.2` (was `>=3.1.1`), the first release with the
32
+ fix for GHSA-6hmw-9432-67qq, where store namespace labels containing `.` reached other
33
+ namespaces. Tracegraph never uses the store. 3.1.2 itself requires `langgraph-checkpoint>=4.3.0`.
34
+ The install range is narrower: an environment that pins `langgraph-checkpoint==4.1.1` or
35
+ `==4.2.0` resolves with 0.9.0 and not with this release.
36
+
37
+ ## 0.9.0 - 2026-10-24
38
+
39
+ ### Changed
40
+
41
+ - **JSON nested more than 9000 levels deep is refused on every Python version** (#167). Whether
42
+ deep input was refused depended on the interpreter: 3.12 and 3.13's `json` refuse at about
43
+ 10,000 levels less the C frames already on the stack (9,984 through the CLI), while 3.14's
44
+ parses far deeper, so 3.14 accepted input that 3.12 refused. `input_validation.MAX_JSON_DEPTH`
45
+ (9000) is now checked on the text before it is parsed, wherever tracegraph parses JSON:
46
+ artifacts, OTLP JSON and JSONL, Phoenix exports and `px` output. It counts the text rather
47
+ than the parsed value, so a deep value that a later duplicate key overwrites still counts, and
48
+ brackets inside strings do not. `ingest --sqlite` checks each checkpoint's metadata, each
49
+ checkpoint body stored as JSON, and each JSON write the saver reads in the same way, before
50
+ decoding, and refuses with `checkpoint history cannot be decoded (JSON nesting too deep)`. The
51
+ scan counts against the snapshot `timeout`. The message for text input is unchanged,
52
+ `JSON nesting too deep`. On 3.12 and 3.13 this newly refuses nesting from 9001 levels up to
53
+ their own limit; on 3.14 it refuses everything above 9000. Objects already decoded by the
54
+ caller (`artifact.from_obj`, the adapter constructors, a caller-supplied checkpoint saver) are
55
+ not checked (#168). The scan adds about 2.4 s to `ingest-otlp` on a 120 MB input dense with
56
+ brackets and quotes (3.5 s before); the artifact is byte-identical.
57
+ - **The Phoenix + SyncMill E2E runs against Phoenix server 20.24.0** (#129), pinned by its index
58
+ digest in `contracts/phoenix-runtime.json`; it was 18.0.0. The body-free evidence (diagnosis
59
+ `tool-retry-failure@v2`, the diff, one review candidate and the board import) is the same under
60
+ both servers once run-specific ids and timings are masked. The Phoenix CLI pin stays 1.18.8.
61
+ - **CI runs the suite on Python 3.13 and 3.14** (#128), which `requires-python = ">=3.12"` already
62
+ allowed, and the package now lists both classifiers. A new `full suite (Python 3.13|3.14)` job
63
+ runs the `[cypher]` suite on each; the existing jobs and their names are unchanged and stay on
64
+ 3.12. Both versions failed before: a test docstring with a lone-surrogate escape does not compile
65
+ on 3.13 and later, and Python 3.14's `json` parses nesting that earlier versions refuse with
66
+ `RecursionError`; the explicit nesting cap above makes every version refuse the same input.
67
+ - **`analyze` and `export-incident-memo` show recorded task markers** (#127). A run paused by
68
+ `interrupt()` or stopped by the recursion limit has trace status `ok` unless a step failed,
69
+ because the status reports whether a recorded step failed; `inspect` marked the recorded ones
70
+ (a recursion stop has a marker only when ingest is given the graph), but `analyze` and the
71
+ memo said nothing. When a trace records any, `analyze` adds the warning `<label>: recorded
72
+ task markers: 1 interrupted · 1 scheduled · 1 undecided interrupt; trace status reports
73
+ whether a recorded step failed, not whether the run completed.` (non-zero parts only, for the
74
+ current trace and the baseline), and the memo adds a `Recorded Task Markers` line after Trace
75
+ Status. `undecided interrupt` counts the `langgraph.undecided_interrupt` capture gap. The
76
+ markers are records, not completion: see README. This change leaves the artifact, its schema
77
+ version, the trace status and the report schema as they were, and adds nothing to a report or
78
+ memo of a trace without markers.
79
+ - **`ReviewCandidateReport` and `ReviewCandidate` drop fields they do not know** (#126). Both
80
+ kept them, so reading a review-candidate document that carried, say, a `prompt` or `error_msg`
81
+ and writing it back with `dumps` passed the text through. They now discard unknown fields at
82
+ both levels, as the SyncMill and Toolgraph readers do, and a `ValidationError` from either model
83
+ no longer prints the rejected input; `errors()` still includes it unless called with
84
+ `include_input=False`. From the same artifacts, `export-review-candidates` writes the same
85
+ output; re-ingesting a source can change the artifact, and with it each candidate's
86
+ `artifact_digest`. The contract schema stays at version 1 and still allows additional fields.
87
+ When `build_report` builds a candidate that fails model validation, it still raises
88
+ `ValidationError`, now titled with that candidate's trace, since the rejected value is no
89
+ longer shown.
90
+ - **A LangGraph subgraph return is recorded on its edge** (#150). When the parent graph continues
91
+ after a subgraph, LangGraph declares the checkpoint that launched the subgraph as the
92
+ continuation's parent, and ingest links the continuation to a subgraph checkpoint instead. That
93
+ edge used to say `checkpoint_parent`, like a parent LangGraph declared. It now has origin
94
+ `checkpoint_subgraph_return`. `analyze` still counts it as explicit ancestry and the derived
95
+ tree ranks it as a declared checkpoint parent, so the new origin by itself changes neither
96
+ classification nor the tree (the return-edge entries under Fixed can);
97
+ the memo prints the new origin, and `summarize` groups it apart from a declared checkpoint
98
+ parent.
99
+ - **Artifact schema 9.** An artifact with a `checkpoint_subgraph_return` edge declares
100
+ `schema_version` 9, so 0.8.0 and earlier refuse it by version instead of failing on the new
101
+ origin value. An artifact written by 0.8.0 keeps its version and bytes when it is loaded and
102
+ saved again, and its return edges still say `checkpoint_parent`; a schema 1 artifact is still
103
+ migrated when it is loaded.
104
+ This entry changes nothing else in a fresh ingest; the OTLP and LangGraph entries in this release
105
+ can.
106
+ - **`explain` and `query --explain` describe fan-in as "more than one recorded cause" for every
107
+ lossy chain** (#143). A trace without a heuristic-parent disclosure used to print "multiple real
108
+ causes … trust this raw chain, not the tree", and `query --explain` said the lossy step was "in
109
+ this match" although it can be an ancestor outside the match. Both now print the wording that
110
+ traces with `otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback` have printed since
111
+ 0.8.0, because a recorded cause may have been reconstructed by the adapter without a mark. The
112
+ heuristic-parent caveat itself is unchanged and still appears only for those traces.
113
+ - **A loop parent chosen by start order is recorded on its edge, and `analyze` no longer treats
114
+ it as declared ancestry** (#142). When several spans carry the `graph.node.id` a span names as
115
+ its `graph.node.parent_id`, OTLP and Phoenix ingest take the nearest preceding one by start
116
+ time or start number. That edge used to say `graph_parent`, like a parent with one candidate,
117
+ so a failing loop run that happened to be picked made the child's failure "propagated" instead
118
+ of primary. The edge now has origin `graph_parent_heuristic`: `analyze` splits primary and
119
+ propagated failures only through declared origins, the edge still appears among a finding's
120
+ causal steps and edges with that origin, the memo writes it "preceded by (graph parent, chosen
121
+ heuristically)", and `summarize` groups it apart from a declared graph parent. The derived tree
122
+ is unchanged (the origin ranks as a graph parent does). When the span also links to the parent
123
+ that was picked, the link declares the edge: it stays `graph_parent` and is not counted under
124
+ `otlp.graph_parent_by_timing`, so a trace whose every timed choice is linked no longer carries
125
+ that code, and loses its heuristic-parent warnings unless it also has
126
+ `otlp.graph_parent_fallback`. The `otlp.graph_parent_by_timing` gap is otherwise recorded as
127
+ before.
128
+ - **Artifact schema 8.** An artifact with a `graph_parent_heuristic` edge declares
129
+ `schema_version` 8, or 9 if it also has a `checkpoint_subgraph_return` edge, so 0.8.0 and
130
+ earlier refuse it by version instead of failing on the new origin value. As far as this change
131
+ goes, every other artifact keeps the version it had, and its bytes too, except a fresh ingest
132
+ whose timed choices are all linked, which drops that gap as above. Artifacts written by
133
+ earlier releases are read as before: their timed edges say `graph_parent`, so `analyze`
134
+ classifies them as it did, and the memo keeps qualifying every graph parent in a trace with
135
+ `otlp.graph_parent_by_timing`.
136
+
137
+ ### Fixed
138
+
139
+ - **`--backend ladybug` no longer hangs on gap patterns or slows down on long ones** (#176,
140
+ #173). `LadybugStore` ran every pattern the Cypher compiler accepts as one native query. A
141
+ bounded gap compiles to a variable-length relationship, and LadybugDB enumerates every causal
142
+ path within it, exponentially many once steps have several causes: `query
143
+ tool-retry-failure-near --backend ladybug` (`gap=(2, 30)`) gave no result within 120 s on a
144
+ 200-step trace with two causes per step, where the default backend takes 0.02 s. A large
145
+ result could also end in `RuntimeError: Buffer manager exception`. Planning cost also grows
146
+ with the predicate count: 17.5 s for 32 predicates on a one-step trace, and a `RuntimeError`
147
+ at 1000. `LadybugStore` now runs a pattern as Cypher only if it has no gap after its first
148
+ predicate and at most six predicates (`MAX_NATIVE_PREDICATES`); every other pattern runs on
149
+ the pure-Python matcher, with the same matches, and `fell_back_to_python` is set.
150
+ `compile_to_cypher` is unchanged and still compiles bounded gaps.
151
+ - `query tool-retry-failure-near --backend ladybug` now prints the fallback note on stderr.
152
+ It used to run as Cypher without one. The other presets behave as before: the strict ones
153
+ run as Cypher, and the two with an unbounded gap already fell back.
154
+ - The note now reads `LadybugDB does not run patterns with a gap or more than 6 predicates;
155
+ ran the pure-Python matcher over the raw causal graph — the results are the same.` It
156
+ said `pattern not Cypher-compilable (unbounded gap)`.
157
+ - **A `PathPattern` of about 1000 or more predicates no longer fails with `RecursionError`**
158
+ (#170). The pure-Python matcher behind `find_matches`, `iter_matches` and `search` recursed
159
+ once per predicate; it now walks the predicates with an explicit stack. Matches and the order
160
+ `iter_matches` yields them in are unchanged. CLI `query` and `analyze` run short presets and
161
+ were not affected. `LadybugStore` runs long patterns on the same matcher (#173).
162
+ - **Deeply nested input no longer crashes ingestion with `RecursionError`** (#168). An entry point
163
+ that reads a document now reports a `RecursionError` raised while reading it as the
164
+ `ValueError` it promises, so the CLI exits with a usage error (2) instead of a traceback.
165
+ - The entry points are `artifact.from_obj`, `OTLPSpanAdapter.from_documents`, `.discover` and
166
+ `.ingest`, `PhoenixExportAdapter(document)` and `.ingest`, and
167
+ `LangGraphCheckpointAdapter.discover` and `.ingest`, which `ingest --sqlite` runs.
168
+ - Messages: `artifact cannot be read: recursion limit exceeded`, `trace document cannot be
169
+ read: recursion limit exceeded` (OTLP and Phoenix), and `checkpoint history cannot be read:
170
+ recursion limit exceeded`. They name the limit, not a cause, because a caller-supplied saver,
171
+ graph or object can recurse on its own.
172
+ - Reached from the CLI: `ingest --sqlite` on a crafted checkpoint body that decodes to a value
173
+ nested past the recursion limit, which then crashed while the error channel value was
174
+ formatted. The limit is about 10,000 levels on 3.12 and 3.13 and about 70,000 on 3.14,
175
+ where it is the C stack.
176
+ - Reached from the library: documents the caller decoded, which the #167 text cap does not see,
177
+ with a deep `exception.message`, a deep `schema_version` (formatted into the refusal),
178
+ a deep error channel or pending-write value, or a schedule graph whose `get_state` returns a
179
+ deep config.
180
+ - Decoded documents still have no explicit depth cap: within the recursion limit they are read
181
+ as before. The analysis functions are outside this guarantee.
182
+ - **A copy written after an edit inside a LangGraph subgraph no longer returns through the edit**
183
+ (#158). 0.8.0 returned a `__copy__` of a checkpoint that applied a subgraph's output through
184
+ the invocation's last subgraph checkpoint when that checkpoint preceded the copy, so an edit
185
+ made in between, which the copied checkpoint never saw, could become its cause, with any error
186
+ that edit wrote. A copy now returns through that checkpoint only when every earlier return
187
+ recorded from the same launcher into the same subgraph namespace went through it too, or none
188
+ was recorded. Otherwise nothing records which result it holds: it keeps its declared parent
189
+ without that return, and ingest records the new gap `langgraph.ambiguous_subgraph_return` (one
190
+ per return left out), which makes `check`'s multi-step path rules and `topology-unchanged`
191
+ insufficient evidence; the failure count rule is unaffected, since a copy records no new error.
192
+ The rule applies only to copies: loop checkpoints, which run the subgraph on from its latest
193
+ checkpoint, and `update_state` checkpoints, which apply the caller's values or the launcher's
194
+ stored output, are not withheld, though the other LangGraph entries here change how they
195
+ return. The gap needs no newer schema by itself, but a history that records it also has return
196
+ edges, so its artifact declares schema 9, which 0.8.0 and earlier refuse.
197
+ - **A later run of a LangGraph persistent subgraph is in its continuation's ancestry** (#154). A
198
+ subgraph compiled with `checkpointer=True` keeps one checkpoint namespace across the parent's
199
+ runs, so a later run's input checkpoint declares the previous run's last checkpoint as its
200
+ parent. Ingest treated that run as part of the first one: the parent graph's continuation was
201
+ linked straight to the checkpoint that launched the subgraph, and the later run was missing
202
+ from its ancestry. A subgraph checkpoint whose `metadata.parents` names a launcher the
203
+ namespace has not had yet now begins a new run, which gets that launcher as a second
204
+ `checkpoint_parent` (an error the launcher held is not counted as inherited), and the
205
+ continuation returns through the run's own checkpoints with origin
206
+ `checkpoint_subgraph_return`. This also covers a loop calling the subgraph again, a resume
207
+ after a root `update_state`, and time travel into a subgraph checkpoint. One case is still
208
+ missed: a run finished by editing a subgraph checkpoint after a root `update_state` records
209
+ the original launcher, so the continuation, which declares the root edit, is not linked
210
+ through the run. A persistent subgraph inside a plain subgraph has a namespace without the
211
+ enclosing call's task id (`mid|inner`); it is now linked to that call's checkpoint, which
212
+ launched it, instead of the root's, so the enclosing subgraph's continuation returns through
213
+ it, on the first run too. A replay or edit of a persistent subgraph's launcher is judged by
214
+ `versions_seen` as for other subgraphs (#155). Re-ingesting such a history adds these edges. A
215
+ caller's edit of an earlier run's checkpoint still joins the run in progress, since LangGraph
216
+ resumes from the namespace's latest checkpoint.
217
+ - **LangGraph time travel from a subgraph's launching checkpoint no longer gains the subgraph**
218
+ (#155). An `update_state` of the checkpoint that launched a subgraph as another node, or the
219
+ fork a replay from it writes, holds the state from before the subgraph ran, but ingest linked
220
+ it to the subgraph's last checkpoint. Such a checkpoint now returns through the subgraph only
221
+ if its `versions_seen` shows it applied the subgraph node's task, as an
222
+ `update_state(..., as_node=END)` applying the finished subgraph's output or a copy of the
223
+ continuation does; otherwise it keeps its declared parent. An `update_state` there that writes
224
+ the error channel is therefore recorded as introducing the error even when the subgraph had
225
+ failed. Such a checkpoint has no return edge, so it does not by itself make an artifact
226
+ declare schema 9. `Send`-launched invocations, and nodes whose task id the adapter cannot
227
+ recompute (a plain-channel trigger, some barrier configurations), skip this check; the
228
+ return-checkpoint choice in the #150 entry below still applies to them.
229
+ - **A later edit of a subgraph checkpoint no longer rewrites an earlier return** (#150). The
230
+ return used to go through the subgraph invocation's last checkpoint overall, so an
231
+ `update_state` of one of its checkpoints after the run made the original continuation lose
232
+ the subgraph from its ancestry on the next ingest. A continuation that returns through the
233
+ subgraph under the rules in the entries above now returns through the last subgraph checkpoint
234
+ written before it, which is also what LangGraph resumes from after an interrupted subgraph is
235
+ edited.
236
+
9
237
  ## 0.8.0 - 2026-10-20
10
238
 
11
239
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tracegraph
3
- Version: 0.8.0
3
+ Version: 0.9.1
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
@@ -15,12 +15,14 @@ Classifier: Development Status :: 3 - Alpha
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
18
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
21
  Classifier: Topic :: Software Development :: Debuggers
20
22
  Classifier: Typing :: Typed
21
23
  Requires-Python: >=3.12
22
- Requires-Dist: langgraph-checkpoint-sqlite>=3.1.1
23
- Requires-Dist: langgraph-checkpoint>=4.1
24
+ Requires-Dist: langgraph-checkpoint-sqlite>=3.1.2
25
+ Requires-Dist: langgraph-checkpoint>=4.3.0
24
26
  Requires-Dist: pydantic<3,>=2
25
27
  Requires-Dist: rich>=13
26
28
  Requires-Dist: typer>=0.12
@@ -68,15 +70,26 @@ uvx --from agent-tracegraph tracegraph --version
68
70
  `tracegraph`.** The plain name was already taken on PyPI by an unrelated project, so only the
69
71
  name you install differs — you still run `tracegraph` and still `import tracegraph`.
70
72
 
71
- Requires Python 3.12. CI exercises 3.12 on Linux; other versions and platforms are untested
72
- rather than unsupported.
73
+ Requires Python 3.12 or newer. CI exercises 3.12, 3.13 and 3.14 on Linux; other versions and
74
+ platforms are untested rather than unsupported.
73
75
 
74
76
  ## Design commitment: causality is never faked
75
77
 
76
78
  We persist the **raw multi-parent causal graph** as the system of record (`CAUSED_BY`,
77
- keeping *every* real predecessor) and treat the single-parent tree (`TREE_PARENT`) as an
78
- explicitly **derived, lossy projection**. Any step whose causality is collapsed for the
79
- tree view is flagged `projection_lossy`, so RCA never silently presents a fabricated cause.
79
+ keeping *every* recorded predecessor — every edge the ingest adapter emitted) and treat the
80
+ single-parent tree (`TREE_PARENT`) as an explicitly **derived, lossy projection**. Any step
81
+ whose causality is collapsed for the tree view is flagged `projection_lossy`, so RCA never
82
+ silently presents the tree's single parent as "the" cause.
83
+
84
+ Recorded is not the same as declared by the source. Adapters reconstruct some edges: OTLP and
85
+ Phoenix pick which run of a looped node is the parent by start time or start order, or fall back
86
+ to `parentSpanId`; LangGraph routes a subgraph's return through the last checkpoint that
87
+ subgraph invocation wrote before the parent graph continued. Some sources lose edges: OTLP span links that resolve to no span in the trace,
88
+ and every link in a Phoenix export, which is ingested parents only. `projection_lossy` measures
89
+ projection loss (more than one recorded parent), not capture fidelity. Some known limitations are
90
+ disclosed per trace (`trace.capture` gaps, `causal_fidelity`, `links_preserved`) or per edge (its
91
+ `origin`, such as `checkpoint_subgraph_return` for the LangGraph return edge), and an empty gap
92
+ list does not mean the capture is complete.
80
93
 
81
94
  | analysis | reads |
82
95
  |---|---|
@@ -229,6 +242,8 @@ spans carry the `graph.node.id` that a span names as its `graph.node.parent_id`,
229
242
  and every candidate start at such a number, the nearest preceding candidate by those numbers is
230
243
  the parent. That assumes the export gives every time in one unit, and it is disclosed under
231
244
  `otlp.graph_parent_by_timing` with its own warning line, so `check` treats the edge as heuristic.
245
+ Either way the chosen edge has origin `graph_parent_heuristic` rather than `graph_parent`, unless
246
+ the span also links to that parent, which declares it.
232
247
 
233
248
  The exact Phoenix CLI and server versions used by CI are centrally pinned. See the
234
249
  [Phoenix runtime pin maintenance runbook](docs/phoenix-cli-contract.md) for the controlled
@@ -313,8 +328,10 @@ The official `tool-retry-failure@v2` matches three consecutive causal steps:
313
328
  retry marker and the endpoint must name the same tool. This strict pattern is review-exportable.
314
329
  `tool-repeat-failure-heuristic` uses an unbounded gap and
315
330
  `tool-retry-failure-near` a bounded gap (up to 30); both are query-only heuristics.
316
- The generic matcher still supports gaps and back-references. Unbounded or oversized
317
- Cypher patterns fall back to the Python matcher without truncating results.
331
+ The generic matcher still supports gaps and back-references. With `--backend ladybug`, only
332
+ patterns without a gap and of at most six predicates run as Cypher; every other pattern runs
333
+ on the Python matcher, with the same results. A gap makes LadybugDB enumerate every causal
334
+ path within it, which grows exponentially once steps have several causes.
318
335
 
319
336
  Every shipped preset has a stable `pattern_id` and positive integer `pattern_version`, shown
320
337
  as `pattern-id@vN` by `presets` and `query`. Increment the version when match semantics or
@@ -326,8 +343,10 @@ A group is one observed shape. Two matches share a group when, for every matched
326
343
  status, source and safe-v1 name agree, and for every edge between them the `CAUSED_BY` origin
327
344
  agrees, under the same source kind, causal fidelity and link setting, and the same answer to
328
345
  whether the trace discloses causal edges chosen heuristically at ingest (`motif-signature/v1`).
329
- That last answer is per trace: an OTLP export records a parent picked by timing with the same
330
- origin as an unambiguous one, and says only that some edge was picked that way, not which. The
346
+ That last answer is per trace: the gap says only that some edge was picked heuristically, not
347
+ which. (A parent picked by start order now has its own origin, `graph_parent_heuristic`, so the
348
+ edge comparison already separates it; an artifact written before that records it as
349
+ `graph_parent`, like an unambiguous one, and a fallback edge is `span_parent_fallback`.) The
331
350
  answer is `disclosed` whenever a recognized heuristic-edge gap is present, else `not_disclosed`.
332
351
  It is `unknown` for a trace with no capture record, or with a gap code this build does not know
333
352
  and no recognized heuristic-edge gap. It is `not_applicable` for a single-step preset, which has
@@ -363,7 +382,13 @@ A `*.json` file in a directory that is not a valid artifact is skipped with a wa
363
382
  `query`; pass `--strict` to fail the batch instead.
364
383
  The public schema is a repository-vendored contract (not wheel package data) at
365
384
  [`contracts/review-candidates.schema.json`](contracts/review-candidates.schema.json); G3
366
- consumers should vendor that file explicitly.
385
+ consumers should vendor that file explicitly. The schema allows additional fields at both
386
+ levels so that version 1 can grow, which means a document that validates can still carry text
387
+ tracegraph never writes, such as a prompt or an error message. A consumer must ignore fields it
388
+ does not know and must not store, display or forward them, as SyncMill and Toolgraph do; a field
389
+ that carries trace body text would need a new major version. `ReviewCandidateReport` in the
390
+ `tracegraph.review_candidates` module reads that way too: it drops unknown fields and keeps rejected
391
+ input out of the `ValidationError` message.
367
392
 
368
393
  SyncMill can consume the report without importing this package:
369
394
 
@@ -403,16 +428,59 @@ Known limits:
403
428
  it such a run reads `ok` unless some other step recorded an error. Trace status stays `ok`
404
429
  either way. A task paused by `interrupt()` is reported without a graph. Both are reported
405
430
  only at the root graph: the subgraph node is named, not the node inside it.
431
+ - Trace status reports whether a recorded step failed, not whether the run completed: a
432
+ paused or stopped run reads `ok` unless a step failed. Next to it, `analyze` (as a warning,
433
+ also in `--json-out`) and `export-incident-memo` show the recorded task markers:
434
+ `interrupted`, `scheduled`, and `undecided interrupt` (an interrupt recorded with a resume or
435
+ an error and no result; it gets no interrupt or scheduled step and is counted from its
436
+ capture gap).
437
+ `inspect` shows the first two. They count what the newest root checkpoint records, per root
438
+ task: an interrupt record outlives a revised graph that finished without writing a
439
+ checkpoint, a resumed subgraph can leave its parent's record, several paused inner tasks
440
+ count as one subgraph task, and `scheduled` is the answer for the graph you passed. A
441
+ malformed pending write is ignored and disclosed, so it can hide a marker; with `--graph`,
442
+ unapplied `Command(goto=...)` input and `Send` packets to a node the graph lacks are
443
+ disclosed rather than counted as scheduled. `check` judges each rule's evidence, not
444
+ completion.
406
445
  - Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
407
446
  causes may be missing.
408
447
  - Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
409
448
  producer's names, and its error messages unless ingested with `--redact-errors`.
410
- - CI covers Python 3.12 on Linux.
449
+ - CI covers Python 3.12, 3.13 and 3.14 on Linux.
411
450
 
412
451
  Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
413
452
  super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
414
453
  span-level. Subgraph checkpoints are namespaced, but cross-namespace causality remains
415
- limited to parent links declared by LangGraph checkpoint metadata.
454
+ limited to parent links declared by LangGraph checkpoint metadata, plus the return edge the
455
+ adapter reconstructs when the parent namespace continues after a subgraph invocation. That edge
456
+ runs through the last checkpoint the invocation wrote before the continuation (a resumed
457
+ subgraph continues from, and returns, its namespace's latest checkpoint) and has origin
458
+ `checkpoint_subgraph_return`, which `analyze` counts as explicit ancestry and the derived tree
459
+ ranks as a declared checkpoint parent. A checkpoint a caller wrote from the checkpoint that
460
+ launched the subgraph (`update_state`, a replay's fork) is linked this way only if its
461
+ `versions_seen` shows it applied the subgraph node's task, so an edit as another node or a
462
+ replay keeps its declared parent, while `update_state(..., as_node=END)` applying the finished
463
+ subgraph's output, or a copy of the continuation, returns through the subgraph. Checkpoints do
464
+ not show everything, so the edge can still be wrong where they cannot: an `update_state` as the
465
+ subgraph node itself (metadata does not record `as_node`), clearing tasks with
466
+ `as_node=END` while the subgraph itself is interrupted, fresh input on that launcher, time
467
+ travel around a `Send` or a node whose task id the adapter cannot recompute (one triggered by a
468
+ plain channel, some barrier configurations), and a subgraph checkpoint written
469
+ after the subgraph finished but before the parent graph continued (a node editing its own
470
+ subgraph while it is still running, or an outside `update_state` of a finished subgraph while a
471
+ sibling task is interrupted). A persistent subgraph (`compile(checkpointer=True)`) keeps one
472
+ namespace across the parent graph's runs; each run is told apart by the launcher its
473
+ checkpoints record in `metadata.parents`. A caller's edit of an earlier run's checkpoint joins
474
+ the run in progress, because the subgraph resumes from its namespace's latest checkpoint. A
475
+ `__copy__` of a continuation holds output an earlier checkpoint applied, and nothing records
476
+ which one: when earlier checkpoints from that launcher returned through a different subgraph
477
+ checkpoint than the last one written before the copy (a subgraph checkpoint was edited in
478
+ between, or the launcher was resumed twice), the copy keeps its declared parent without that
479
+ return, and the gap `langgraph.ambiguous_subgraph_return` is recorded, which makes `check`'s
480
+ multi-step path rules and topology rules insufficient evidence. An `update_state` from the launcher still returns
481
+ through the last subgraph checkpoint written before it, edited or not. When an interrupted run is
482
+ finished by an `update_state` of the subgraph after the launcher itself was edited, the
483
+ continuation keeps the edited launcher as its parent and does not return through the subgraph.
416
484
 
417
485
  ## Develop
418
486
 
@@ -467,9 +535,13 @@ Containment and unknown edges preserve all error investigation candidates, deepe
467
535
  Explicit causal ancestry supplies context, not proof that an exception propagated.
468
536
  When the artifact records that a causal parent was chosen heuristically at ingest
469
537
  (`otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback`), `analyze` adds a warning for
470
- that side and the memo a fidelity disclosure. The record does not say which edge, so a memo of
471
- a trace with `otlp.graph_parent_by_timing` writes every `graph_parent` edge as "preceded by
472
- (graph parent, possibly chosen heuristically)" instead of "caused by". `inspect` prints the
538
+ that side and the memo a fidelity disclosure. A loop parent picked by start order has origin
539
+ `graph_parent_heuristic`: `analyze` does not count it as explicit ancestry when it splits primary
540
+ from propagated failures, and the memo writes it "preceded by (graph parent, chosen
541
+ heuristically)". An artifact written before that origin existed does not say which edge, so a
542
+ memo of such a trace with `otlp.graph_parent_by_timing` writes every `graph_parent` edge as
543
+ "preceded by (graph parent, possibly chosen heuristically)" instead of "caused by", and
544
+ `analyze` classifies it as before. `inspect` prints the
473
545
  same caveat under its tree, and `explain` and `query --explain` under every chain that has at
474
546
  least one cause; there their fan-in note no longer calls the causes real or says to trust the
475
547
  chain.
@@ -542,7 +614,9 @@ managed values, cache-key functions — though no node runs. It is never inferre
542
614
  Every ingest records what the adapter could not observe: `trace.capture` is an object whose
543
615
  `gaps` list holds a fixed code and count per ingest disclosure (empty when there was none),
544
616
  and a step's `unknown_fields` names a kind, name or error message the adapter substituted. That
545
- record makes a fresh artifact declare `schema_version` 7. The envelope otherwise follows
617
+ record makes a fresh artifact declare `schema_version` 7, 8 when a causal edge has origin
618
+ `graph_parent_heuristic`, or 9 when one has origin `checkpoint_subgraph_return`. The envelope
619
+ otherwise follows
546
620
  content, which is what an artifact written by an earlier release keeps when it is loaded and
547
621
  saved again: a graph read (`--graph`) needs 6, interrupt steps 5, a derived tree that relies on
548
622
  EdgeOrigin precedence 4, derived task steps 3, and anything else 2, byte for byte (a v1 file is
@@ -563,7 +637,7 @@ withheld, and a single unparseable cost withholds the whole total instead of und
563
637
  An OTLP or Phoenix import records an unreadable total cost as `unreadable` for that reason, and
564
638
  reports token counts, other costs, currencies and evaluation scores it cannot read as
565
639
  unavailable, disclosing each on stderr; the producer's value is never kept. File input, stdin
566
- and Phoenix `px` output share one size limit (`TRACEGRAPH_MAX_ARTIFACT_BYTES`). `query` reads
640
+ and Phoenix `px` output share one size limit (`TRACEGRAPH_MAX_ARTIFACT_BYTES`). JSON nested more than 9000 levels deep is refused as `JSON nesting too deep` on every Python version, counted on the text before it is parsed; `ingest --sqlite` checks the JSON in the thread's checkpoint rows the same way. A document the caller decoded and passes to the library is not depth-capped, but reading one too deep to process is refused as `ValueError` (`... cannot be read: recursion limit exceeded`), not a crash. `query` reads
567
641
  artifacts one after another and keeps only the matches it prints (plus the traces `--explain`
568
642
  renders). With `--limit`, its peak memory therefore grows with the number of artifacts only
569
643
  by a small per-file record (path, trace id); without it, by the matches it prints. Inside one
@@ -571,16 +645,18 @@ trace, too, it keeps only the matches that still fit under `--limit`, but it sti
571
645
  and counts every match to report the exact total, so time grows with the match count: one
572
646
  2500-step trace with 3.1M matches takes about 3 s at `--limit 1`, in 70 MiB. The search's own
573
647
  working state (steps reached at each distance of a gap) does not depend on `--limit`. With
574
- `--backend ladybug` on a pattern that compiles to Cypher, LadybugDB itself buffers the
648
+ `--backend ladybug` on a pattern that runs as Cypher, LadybugDB itself buffers the
575
649
  query's full result set: a 3001-step trace with 2.25M `tool-retry-failure` matches peaked
576
650
  at 757 MiB, where the store alone takes about 300 MiB. A plain read, `count(*)` and
577
- `ORDER BY … LIMIT` all buffered it in our measurements.
651
+ `ORDER BY … LIMIT` all buffered it in our measurements. The six-predicate limit on Cypher
652
+ bounds the query's planning, not its result: a short strict pattern over a graph where steps
653
+ have many effects can still have a large result, and LadybugDB buffers all of it.
578
654
 
579
655
  Both stores isolate returned mutable objects. InMemory node upsert replaces by ID;
580
656
  Ladybug's existing insertion semantics are unchanged. Use `is_isomorphic` or `diff` for
581
657
  deep-tree equality; `canonical()` preserves nested tuples, whose external Python equality
582
- can still reach the interpreter recursion limit. CI currently covers Python 3.12/Linux;
583
- other supported Python versions and platforms require separate validation.
658
+ can still reach the interpreter recursion limit. CI currently covers Python 3.12–3.14/Linux;
659
+ other Python versions and platforms require separate validation.
584
660
 
585
661
  ## Contributing, security, and license
586
662