agent-tracegraph 0.7.0__tar.gz → 0.9.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.
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/.gitignore +4 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/CHANGELOG.md +258 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/PKG-INFO +113 -20
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/README.md +110 -19
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/SECURITY.md +2 -1
- agent_tracegraph-0.9.0/contracts/phoenix-runtime.json +8 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/pyproject.toml +4 -2
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/langgraph_checkpoint.py +184 -25
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/otlp_spans.py +79 -18
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/phoenix_export.py +42 -1
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/check.py +8 -1
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/diagnose.py +48 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/explain.py +3 -3
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/memo.py +27 -2
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/patterns.py +52 -33
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/artifact.py +14 -5
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/cli.py +31 -12
- agent_tracegraph-0.9.0/src/tracegraph/input_validation.py +107 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/model.py +26 -7
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/normalize.py +6 -2
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/review_candidates.py +29 -12
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/sqlite_snapshot.py +57 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/ladybug.py +44 -15
- agent_tracegraph-0.7.0/contracts/phoenix-runtime.json +0 -8
- agent_tracegraph-0.7.0/src/tracegraph/input_validation.py +0 -26
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/LICENSE +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/analysis-report.schema.json +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/check-report.schema.json +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/contracts/review-candidates.schema.json +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/__init__.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/_atomic_write.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/_errors.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/__init__.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/adapters/base.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/__init__.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/ahu.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/analysis/motifs.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/graph_ref.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/py.typed +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/__init__.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/base.py +0 -0
- {agent_tracegraph-0.7.0 → agent_tracegraph-0.9.0}/src/tracegraph/store/in_memory.py +0 -0
|
@@ -6,6 +6,264 @@ 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.0 - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **JSON nested more than 9000 levels deep is refused on every Python version** (#167). Whether
|
|
14
|
+
deep input was refused depended on the interpreter: 3.12 and 3.13's `json` refuse at about
|
|
15
|
+
10,000 levels less the C frames already on the stack (9,984 through the CLI), while 3.14's
|
|
16
|
+
parses far deeper, so 3.14 accepted input that 3.12 refused. `input_validation.MAX_JSON_DEPTH`
|
|
17
|
+
(9000) is now checked on the text before it is parsed, wherever tracegraph parses JSON:
|
|
18
|
+
artifacts, OTLP JSON and JSONL, Phoenix exports and `px` output. It counts the text rather
|
|
19
|
+
than the parsed value, so a deep value that a later duplicate key overwrites still counts, and
|
|
20
|
+
brackets inside strings do not. `ingest --sqlite` checks each checkpoint's metadata, each
|
|
21
|
+
checkpoint body stored as JSON, and each JSON write the saver reads in the same way, before
|
|
22
|
+
decoding, and refuses with `checkpoint history cannot be decoded (JSON nesting too deep)`. The
|
|
23
|
+
scan counts against the snapshot `timeout`. The message for text input is unchanged,
|
|
24
|
+
`JSON nesting too deep`. On 3.12 and 3.13 this newly refuses nesting from 9001 levels up to
|
|
25
|
+
their own limit; on 3.14 it refuses everything above 9000. Objects already decoded by the
|
|
26
|
+
caller (`artifact.from_obj`, the adapter constructors, a caller-supplied checkpoint saver) are
|
|
27
|
+
not checked (#168). The scan adds about 2.4 s to `ingest-otlp` on a 120 MB input dense with
|
|
28
|
+
brackets and quotes (3.5 s before); the artifact is byte-identical.
|
|
29
|
+
- **The Phoenix + SyncMill E2E runs against Phoenix server 20.24.0** (#129), pinned by its index
|
|
30
|
+
digest in `contracts/phoenix-runtime.json`; it was 18.0.0. The body-free evidence (diagnosis
|
|
31
|
+
`tool-retry-failure@v2`, the diff, one review candidate and the board import) is the same under
|
|
32
|
+
both servers once run-specific ids and timings are masked. The Phoenix CLI pin stays 1.18.8.
|
|
33
|
+
- **CI runs the suite on Python 3.13 and 3.14** (#128), which `requires-python = ">=3.12"` already
|
|
34
|
+
allowed, and the package now lists both classifiers. A new `full suite (Python 3.13|3.14)` job
|
|
35
|
+
runs the `[cypher]` suite on each; the existing jobs and their names are unchanged and stay on
|
|
36
|
+
3.12. Both versions failed before: a test docstring with a lone-surrogate escape does not compile
|
|
37
|
+
on 3.13 and later, and Python 3.14's `json` parses nesting that earlier versions refuse with
|
|
38
|
+
`RecursionError`; the explicit nesting cap above makes every version refuse the same input.
|
|
39
|
+
- **`analyze` and `export-incident-memo` show recorded task markers** (#127). A run paused by
|
|
40
|
+
`interrupt()` or stopped by the recursion limit has trace status `ok` unless a step failed,
|
|
41
|
+
because the status reports whether a recorded step failed; `inspect` marked the recorded ones
|
|
42
|
+
(a recursion stop has a marker only when ingest is given the graph), but `analyze` and the
|
|
43
|
+
memo said nothing. When a trace records any, `analyze` adds the warning `<label>: recorded
|
|
44
|
+
task markers: 1 interrupted · 1 scheduled · 1 undecided interrupt; trace status reports
|
|
45
|
+
whether a recorded step failed, not whether the run completed.` (non-zero parts only, for the
|
|
46
|
+
current trace and the baseline), and the memo adds a `Recorded Task Markers` line after Trace
|
|
47
|
+
Status. `undecided interrupt` counts the `langgraph.undecided_interrupt` capture gap. The
|
|
48
|
+
markers are records, not completion: see README. This change leaves the artifact, its schema
|
|
49
|
+
version, the trace status and the report schema as they were, and adds nothing to a report or
|
|
50
|
+
memo of a trace without markers.
|
|
51
|
+
- **`ReviewCandidateReport` and `ReviewCandidate` drop fields they do not know** (#126). Both
|
|
52
|
+
kept them, so reading a review-candidate document that carried, say, a `prompt` or `error_msg`
|
|
53
|
+
and writing it back with `dumps` passed the text through. They now discard unknown fields at
|
|
54
|
+
both levels, as the SyncMill and Toolgraph readers do, and a `ValidationError` from either model
|
|
55
|
+
no longer prints the rejected input; `errors()` still includes it unless called with
|
|
56
|
+
`include_input=False`. From the same artifacts, `export-review-candidates` writes the same
|
|
57
|
+
output; re-ingesting a source can change the artifact, and with it each candidate's
|
|
58
|
+
`artifact_digest`. The contract schema stays at version 1 and still allows additional fields.
|
|
59
|
+
When `build_report` builds a candidate that fails model validation, it still raises
|
|
60
|
+
`ValidationError`, now titled with that candidate's trace, since the rejected value is no
|
|
61
|
+
longer shown.
|
|
62
|
+
- **A LangGraph subgraph return is recorded on its edge** (#150). When the parent graph continues
|
|
63
|
+
after a subgraph, LangGraph declares the checkpoint that launched the subgraph as the
|
|
64
|
+
continuation's parent, and ingest links the continuation to a subgraph checkpoint instead. That
|
|
65
|
+
edge used to say `checkpoint_parent`, like a parent LangGraph declared. It now has origin
|
|
66
|
+
`checkpoint_subgraph_return`. `analyze` still counts it as explicit ancestry and the derived
|
|
67
|
+
tree ranks it as a declared checkpoint parent, so the new origin by itself changes neither
|
|
68
|
+
classification nor the tree (the return-edge entries under Fixed can);
|
|
69
|
+
the memo prints the new origin, and `summarize` groups it apart from a declared checkpoint
|
|
70
|
+
parent.
|
|
71
|
+
- **Artifact schema 9.** An artifact with a `checkpoint_subgraph_return` edge declares
|
|
72
|
+
`schema_version` 9, so 0.8.0 and earlier refuse it by version instead of failing on the new
|
|
73
|
+
origin value. An artifact written by 0.8.0 keeps its version and bytes when it is loaded and
|
|
74
|
+
saved again, and its return edges still say `checkpoint_parent`; a schema 1 artifact is still
|
|
75
|
+
migrated when it is loaded.
|
|
76
|
+
This entry changes nothing else in a fresh ingest; the OTLP and LangGraph entries in this release
|
|
77
|
+
can.
|
|
78
|
+
- **`explain` and `query --explain` describe fan-in as "more than one recorded cause" for every
|
|
79
|
+
lossy chain** (#143). A trace without a heuristic-parent disclosure used to print "multiple real
|
|
80
|
+
causes … trust this raw chain, not the tree", and `query --explain` said the lossy step was "in
|
|
81
|
+
this match" although it can be an ancestor outside the match. Both now print the wording that
|
|
82
|
+
traces with `otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback` have printed since
|
|
83
|
+
0.8.0, because a recorded cause may have been reconstructed by the adapter without a mark. The
|
|
84
|
+
heuristic-parent caveat itself is unchanged and still appears only for those traces.
|
|
85
|
+
- **A loop parent chosen by start order is recorded on its edge, and `analyze` no longer treats
|
|
86
|
+
it as declared ancestry** (#142). When several spans carry the `graph.node.id` a span names as
|
|
87
|
+
its `graph.node.parent_id`, OTLP and Phoenix ingest take the nearest preceding one by start
|
|
88
|
+
time or start number. That edge used to say `graph_parent`, like a parent with one candidate,
|
|
89
|
+
so a failing loop run that happened to be picked made the child's failure "propagated" instead
|
|
90
|
+
of primary. The edge now has origin `graph_parent_heuristic`: `analyze` splits primary and
|
|
91
|
+
propagated failures only through declared origins, the edge still appears among a finding's
|
|
92
|
+
causal steps and edges with that origin, the memo writes it "preceded by (graph parent, chosen
|
|
93
|
+
heuristically)", and `summarize` groups it apart from a declared graph parent. The derived tree
|
|
94
|
+
is unchanged (the origin ranks as a graph parent does). When the span also links to the parent
|
|
95
|
+
that was picked, the link declares the edge: it stays `graph_parent` and is not counted under
|
|
96
|
+
`otlp.graph_parent_by_timing`, so a trace whose every timed choice is linked no longer carries
|
|
97
|
+
that code, and loses its heuristic-parent warnings unless it also has
|
|
98
|
+
`otlp.graph_parent_fallback`. The `otlp.graph_parent_by_timing` gap is otherwise recorded as
|
|
99
|
+
before.
|
|
100
|
+
- **Artifact schema 8.** An artifact with a `graph_parent_heuristic` edge declares
|
|
101
|
+
`schema_version` 8, or 9 if it also has a `checkpoint_subgraph_return` edge, so 0.8.0 and
|
|
102
|
+
earlier refuse it by version instead of failing on the new origin value. As far as this change
|
|
103
|
+
goes, every other artifact keeps the version it had, and its bytes too, except a fresh ingest
|
|
104
|
+
whose timed choices are all linked, which drops that gap as above. Artifacts written by
|
|
105
|
+
earlier releases are read as before: their timed edges say `graph_parent`, so `analyze`
|
|
106
|
+
classifies them as it did, and the memo keeps qualifying every graph parent in a trace with
|
|
107
|
+
`otlp.graph_parent_by_timing`.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
|
|
111
|
+
- **`--backend ladybug` no longer hangs on gap patterns or slows down on long ones** (#176,
|
|
112
|
+
#173). `LadybugStore` ran every pattern the Cypher compiler accepts as one native query. A
|
|
113
|
+
bounded gap compiles to a variable-length relationship, and LadybugDB enumerates every causal
|
|
114
|
+
path within it, exponentially many once steps have several causes: `query
|
|
115
|
+
tool-retry-failure-near --backend ladybug` (`gap=(2, 30)`) gave no result within 120 s on a
|
|
116
|
+
200-step trace with two causes per step, where the default backend takes 0.02 s. A large
|
|
117
|
+
result could also end in `RuntimeError: Buffer manager exception`. Planning cost also grows
|
|
118
|
+
with the predicate count: 17.5 s for 32 predicates on a one-step trace, and a `RuntimeError`
|
|
119
|
+
at 1000. `LadybugStore` now runs a pattern as Cypher only if it has no gap after its first
|
|
120
|
+
predicate and at most six predicates (`MAX_NATIVE_PREDICATES`); every other pattern runs on
|
|
121
|
+
the pure-Python matcher, with the same matches, and `fell_back_to_python` is set.
|
|
122
|
+
`compile_to_cypher` is unchanged and still compiles bounded gaps.
|
|
123
|
+
- `query tool-retry-failure-near --backend ladybug` now prints the fallback note on stderr.
|
|
124
|
+
It used to run as Cypher without one. The other presets behave as before: the strict ones
|
|
125
|
+
run as Cypher, and the two with an unbounded gap already fell back.
|
|
126
|
+
- The note now reads `LadybugDB does not run patterns with a gap or more than 6 predicates;
|
|
127
|
+
ran the pure-Python matcher over the raw causal graph — the results are the same.` It
|
|
128
|
+
said `pattern not Cypher-compilable (unbounded gap)`.
|
|
129
|
+
- **A `PathPattern` of about 1000 or more predicates no longer fails with `RecursionError`**
|
|
130
|
+
(#170). The pure-Python matcher behind `find_matches`, `iter_matches` and `search` recursed
|
|
131
|
+
once per predicate; it now walks the predicates with an explicit stack. Matches and the order
|
|
132
|
+
`iter_matches` yields them in are unchanged. CLI `query` and `analyze` run short presets and
|
|
133
|
+
were not affected. `LadybugStore` runs long patterns on the same matcher (#173).
|
|
134
|
+
- **Deeply nested input no longer crashes ingestion with `RecursionError`** (#168). An entry point
|
|
135
|
+
that reads a document now reports a `RecursionError` raised while reading it as the
|
|
136
|
+
`ValueError` it promises, so the CLI exits with a usage error (2) instead of a traceback.
|
|
137
|
+
- The entry points are `artifact.from_obj`, `OTLPSpanAdapter.from_documents`, `.discover` and
|
|
138
|
+
`.ingest`, `PhoenixExportAdapter(document)` and `.ingest`, and
|
|
139
|
+
`LangGraphCheckpointAdapter.discover` and `.ingest`, which `ingest --sqlite` runs.
|
|
140
|
+
- Messages: `artifact cannot be read: recursion limit exceeded`, `trace document cannot be
|
|
141
|
+
read: recursion limit exceeded` (OTLP and Phoenix), and `checkpoint history cannot be read:
|
|
142
|
+
recursion limit exceeded`. They name the limit, not a cause, because a caller-supplied saver,
|
|
143
|
+
graph or object can recurse on its own.
|
|
144
|
+
- Reached from the CLI: `ingest --sqlite` on a crafted checkpoint body that decodes to a value
|
|
145
|
+
nested past the recursion limit, which then crashed while the error channel value was
|
|
146
|
+
formatted. The limit is about 10,000 levels on 3.12 and 3.13 and about 70,000 on 3.14,
|
|
147
|
+
where it is the C stack.
|
|
148
|
+
- Reached from the library: documents the caller decoded, which the #167 text cap does not see,
|
|
149
|
+
with a deep `exception.message`, a deep `schema_version` (formatted into the refusal),
|
|
150
|
+
a deep error channel or pending-write value, or a schedule graph whose `get_state` returns a
|
|
151
|
+
deep config.
|
|
152
|
+
- Decoded documents still have no explicit depth cap: within the recursion limit they are read
|
|
153
|
+
as before. The analysis functions are outside this guarantee.
|
|
154
|
+
- **A copy written after an edit inside a LangGraph subgraph no longer returns through the edit**
|
|
155
|
+
(#158). 0.8.0 returned a `__copy__` of a checkpoint that applied a subgraph's output through
|
|
156
|
+
the invocation's last subgraph checkpoint when that checkpoint preceded the copy, so an edit
|
|
157
|
+
made in between, which the copied checkpoint never saw, could become its cause, with any error
|
|
158
|
+
that edit wrote. A copy now returns through that checkpoint only when every earlier return
|
|
159
|
+
recorded from the same launcher into the same subgraph namespace went through it too, or none
|
|
160
|
+
was recorded. Otherwise nothing records which result it holds: it keeps its declared parent
|
|
161
|
+
without that return, and ingest records the new gap `langgraph.ambiguous_subgraph_return` (one
|
|
162
|
+
per return left out), which makes `check`'s multi-step path rules and `topology-unchanged`
|
|
163
|
+
insufficient evidence; the failure count rule is unaffected, since a copy records no new error.
|
|
164
|
+
The rule applies only to copies: loop checkpoints, which run the subgraph on from its latest
|
|
165
|
+
checkpoint, and `update_state` checkpoints, which apply the caller's values or the launcher's
|
|
166
|
+
stored output, are not withheld, though the other LangGraph entries here change how they
|
|
167
|
+
return. The gap needs no newer schema by itself, but a history that records it also has return
|
|
168
|
+
edges, so its artifact declares schema 9, which 0.8.0 and earlier refuse.
|
|
169
|
+
- **A later run of a LangGraph persistent subgraph is in its continuation's ancestry** (#154). A
|
|
170
|
+
subgraph compiled with `checkpointer=True` keeps one checkpoint namespace across the parent's
|
|
171
|
+
runs, so a later run's input checkpoint declares the previous run's last checkpoint as its
|
|
172
|
+
parent. Ingest treated that run as part of the first one: the parent graph's continuation was
|
|
173
|
+
linked straight to the checkpoint that launched the subgraph, and the later run was missing
|
|
174
|
+
from its ancestry. A subgraph checkpoint whose `metadata.parents` names a launcher the
|
|
175
|
+
namespace has not had yet now begins a new run, which gets that launcher as a second
|
|
176
|
+
`checkpoint_parent` (an error the launcher held is not counted as inherited), and the
|
|
177
|
+
continuation returns through the run's own checkpoints with origin
|
|
178
|
+
`checkpoint_subgraph_return`. This also covers a loop calling the subgraph again, a resume
|
|
179
|
+
after a root `update_state`, and time travel into a subgraph checkpoint. One case is still
|
|
180
|
+
missed: a run finished by editing a subgraph checkpoint after a root `update_state` records
|
|
181
|
+
the original launcher, so the continuation, which declares the root edit, is not linked
|
|
182
|
+
through the run. A persistent subgraph inside a plain subgraph has a namespace without the
|
|
183
|
+
enclosing call's task id (`mid|inner`); it is now linked to that call's checkpoint, which
|
|
184
|
+
launched it, instead of the root's, so the enclosing subgraph's continuation returns through
|
|
185
|
+
it, on the first run too. A replay or edit of a persistent subgraph's launcher is judged by
|
|
186
|
+
`versions_seen` as for other subgraphs (#155). Re-ingesting such a history adds these edges. A
|
|
187
|
+
caller's edit of an earlier run's checkpoint still joins the run in progress, since LangGraph
|
|
188
|
+
resumes from the namespace's latest checkpoint.
|
|
189
|
+
- **LangGraph time travel from a subgraph's launching checkpoint no longer gains the subgraph**
|
|
190
|
+
(#155). An `update_state` of the checkpoint that launched a subgraph as another node, or the
|
|
191
|
+
fork a replay from it writes, holds the state from before the subgraph ran, but ingest linked
|
|
192
|
+
it to the subgraph's last checkpoint. Such a checkpoint now returns through the subgraph only
|
|
193
|
+
if its `versions_seen` shows it applied the subgraph node's task, as an
|
|
194
|
+
`update_state(..., as_node=END)` applying the finished subgraph's output or a copy of the
|
|
195
|
+
continuation does; otherwise it keeps its declared parent. An `update_state` there that writes
|
|
196
|
+
the error channel is therefore recorded as introducing the error even when the subgraph had
|
|
197
|
+
failed. Such a checkpoint has no return edge, so it does not by itself make an artifact
|
|
198
|
+
declare schema 9. `Send`-launched invocations, and nodes whose task id the adapter cannot
|
|
199
|
+
recompute (a plain-channel trigger, some barrier configurations), skip this check; the
|
|
200
|
+
return-checkpoint choice in the #150 entry below still applies to them.
|
|
201
|
+
- **A later edit of a subgraph checkpoint no longer rewrites an earlier return** (#150). The
|
|
202
|
+
return used to go through the subgraph invocation's last checkpoint overall, so an
|
|
203
|
+
`update_state` of one of its checkpoints after the run made the original continuation lose
|
|
204
|
+
the subgraph from its ancestry on the next ingest. A continuation that returns through the
|
|
205
|
+
subgraph under the rules in the entries above now returns through the last subgraph checkpoint
|
|
206
|
+
written before it, which is also what LangGraph resumes from after an interrupted subgraph is
|
|
207
|
+
edited.
|
|
208
|
+
|
|
209
|
+
## 0.8.0 - 2026-10-20
|
|
210
|
+
|
|
211
|
+
### Changed
|
|
212
|
+
|
|
213
|
+
- **`analyze` and the incident memo say when a causal parent was chosen heuristically** (#138).
|
|
214
|
+
Ingest records `otlp.graph_parent_by_timing` (a loop parent taken by start time or, since #136,
|
|
215
|
+
by start number) or `otlp.graph_parent_fallback` (a declared graph parent not honored, so the
|
|
216
|
+
span parent is used) in the artifact, and `check` and `summarize` read those codes, but
|
|
217
|
+
`analyze` and `export-incident-memo` did not, so a report or memo built from the artifact did
|
|
218
|
+
not say that a parent may have been chosen heuristically, and the memo wrote a graph parent
|
|
219
|
+
taken by start order as "caused by". `analyze` now adds a warning for each side that
|
|
220
|
+
carries either code, and the memo adds a fidelity disclosure. The record does not say which
|
|
221
|
+
edge was chosen that way, so in a trace with `otlp.graph_parent_by_timing` the memo writes every
|
|
222
|
+
`graph_parent` edge in its causal ancestry graph as "preceded by (graph parent, possibly chosen heuristically)" instead of
|
|
223
|
+
"caused by"; checkpoint parents and span links keep "caused by". Reports and memos of traces
|
|
224
|
+
without these codes are unchanged.
|
|
225
|
+
|
|
226
|
+
- **`explain`, `query --explain` and `inspect` say when a causal parent was chosen heuristically**
|
|
227
|
+
(#140). They read the same two codes: `inspect` prints the caveat `analyze` uses under its
|
|
228
|
+
tree, and `explain` and `query --explain` under every chain with at least one cause (a root
|
|
229
|
+
step has no parent to qualify). In such a trace their fan-in note no longer says "multiple real
|
|
230
|
+
causes" or "trust this raw chain, not the tree"; it says the single-parent tree drops some
|
|
231
|
+
recorded causes and the raw chain lists every recorded ancestor, and the `query --explain` note
|
|
232
|
+
says "in this chain" rather than "in this match", since the step with several causes can be an
|
|
233
|
+
ancestor outside the match. Output for traces without these codes is unchanged. `analyze` still
|
|
234
|
+
classifies failures through a `graph_parent` edge as it did.
|
|
235
|
+
|
|
236
|
+
- **A Phoenix loop parent is chosen by start numbers whose unit is unknown again** (#136). Since
|
|
237
|
+
0.7.0 a Phoenix time given as a number below 10^17 is reported as unavailable, and with it the
|
|
238
|
+
only evidence for choosing among several spans that carry the `graph.node.id` a span names as
|
|
239
|
+
its `graph.node.parent_id`. The span then fell back to its `parent_id`, or the export was
|
|
240
|
+
refused without one. Now, when that span and every candidate start at such a number, the
|
|
241
|
+
nearest preceding candidate by those numbers is taken. 0.6.1 also chose by them, read as
|
|
242
|
+
nanoseconds, but it dropped a float's fraction first, so with fractional seconds (`a` at
|
|
243
|
+
1783987200.25, `b` at 1783987200.75, the span at 1783987200.5) it took `b` where this release
|
|
244
|
+
takes `a`. The times themselves stay unavailable. The choice is counted under the existing
|
|
245
|
+
`otlp.graph_parent_by_timing` gap with its own warning line, which says it assumes the export
|
|
246
|
+
gives every time in one unit; `check` treats it as a heuristic edge, as it does a choice by
|
|
247
|
+
readable time. Exports with ISO or nanosecond times are read as before, and so is a set of
|
|
248
|
+
candidates where any one has a readable time, no start, or no number below the floor. The
|
|
249
|
+
0.7.0 example (spans `a` and `b` at 1783987200 and 1783987201, `c` at 1783987202 with no
|
|
250
|
+
`parent_id`) is accepted again with `b` as `c`'s parent. Where 0.7.0 fell back to the span's
|
|
251
|
+
`parent_id`, the parent taken now can differ, and so can what follows from it. Each case below
|
|
252
|
+
is a separate export built from that example, adding only what it names:
|
|
253
|
+
- The export can be refused if the new choice closes a causal cycle. Add a root span `r` at
|
|
254
|
+
1783987199 and give `c` the `parent_id` `r` and `b` the `parent_id` `c`: 0.7.0 kept `r` as
|
|
255
|
+
`c`'s parent and exited 0; this release takes `b`, so `ingest-phoenix` and `analyze` exit 2
|
|
256
|
+
and `phoenix diagnose` exits 1 (0.6.1 refused that export too).
|
|
257
|
+
- `analyze` can classify failures differently, because the edge is now a `graph_parent`, which
|
|
258
|
+
it reads as explicit ancestry (#142). Give `c` the `parent_id` `b` and let `b` and `c` fail:
|
|
259
|
+
0.7.0 reported both as primary failures; this release reports `b` as primary and `c` as
|
|
260
|
+
propagated.
|
|
261
|
+
- Step numbers can change. Add spans `x`, `y` (`parent_id` `x`) and `z` with no start time,
|
|
262
|
+
and give `b` the `parent_id` `y` and `c` the `parent_id` `z`: 0.7.0 numbered `c` 3 and `b` 5;
|
|
263
|
+
this release numbers `b` 4 and `c` 5.
|
|
264
|
+
|
|
265
|
+
Ties are still broken with these spans at time 0, so step order can also differ from 0.6.1.
|
|
266
|
+
|
|
9
267
|
## 0.7.0 - 2026-10-20
|
|
10
268
|
|
|
11
269
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-tracegraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -15,6 +15,8 @@ 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
|
|
@@ -38,7 +40,7 @@ tracegraph ingests agent execution traces that already exist (LangGraph checkpoi
|
|
|
38
40
|
Phoenix CLI exports, and OpenInference/OTLP spans) into a **normalized causal graph**,
|
|
39
41
|
then runs analyses the mainstream observability tools don't:
|
|
40
42
|
|
|
41
|
-
- **`explain`** — walk backward from a failure over the *raw* causal graph to every
|
|
43
|
+
- **`explain`** — walk backward from a failure over the *raw* causal graph to every recorded cause.
|
|
42
44
|
- **`diff`** — exact structural regression diff between two runs (rooted-tree / AHU isomorphism).
|
|
43
45
|
- **cross-trace pattern matching** — e.g. "tool X → retry → tool X → failure" across all runs.
|
|
44
46
|
|
|
@@ -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
|
|
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*
|
|
78
|
-
explicitly **derived, lossy projection**. Any step
|
|
79
|
-
tree view is flagged `projection_lossy`, so RCA never
|
|
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
|
|---|---|
|
|
@@ -221,6 +234,17 @@ Phoenix exports preserve span parents but currently do not expose the original O
|
|
|
221
234
|
links. These artifacts are explicitly marked `causal_fidelity=parent_only`; tracegraph warns
|
|
222
235
|
that additional fan-in causes may be missing instead of claiming a complete DAG.
|
|
223
236
|
|
|
237
|
+
Phoenix's own export gives times as ISO strings. A time given as a number is read as epoch
|
|
238
|
+
nanoseconds only from 10^17 up; a smaller one (seconds, milliseconds, a relative offset) is in
|
|
239
|
+
a unit tracegraph cannot tell, so it is reported as unavailable and disclosed as
|
|
240
|
+
`otlp.unreadable_timestamp`. Such numbers still order spans for one purpose: when several
|
|
241
|
+
spans carry the `graph.node.id` that a span names as its `graph.node.parent_id`, and that span
|
|
242
|
+
and every candidate start at such a number, the nearest preceding candidate by those numbers is
|
|
243
|
+
the parent. That assumes the export gives every time in one unit, and it is disclosed under
|
|
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.
|
|
247
|
+
|
|
224
248
|
The exact Phoenix CLI and server versions used by CI are centrally pinned. See the
|
|
225
249
|
[Phoenix runtime pin maintenance runbook](docs/phoenix-cli-contract.md) for the controlled
|
|
226
250
|
upgrade and contract-verification procedure, and the
|
|
@@ -304,8 +328,10 @@ The official `tool-retry-failure@v2` matches three consecutive causal steps:
|
|
|
304
328
|
retry marker and the endpoint must name the same tool. This strict pattern is review-exportable.
|
|
305
329
|
`tool-repeat-failure-heuristic` uses an unbounded gap and
|
|
306
330
|
`tool-retry-failure-near` a bounded gap (up to 30); both are query-only heuristics.
|
|
307
|
-
The generic matcher still supports gaps and back-references.
|
|
308
|
-
|
|
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.
|
|
309
335
|
|
|
310
336
|
Every shipped preset has a stable `pattern_id` and positive integer `pattern_version`, shown
|
|
311
337
|
as `pattern-id@vN` by `presets` and `query`. Increment the version when match semantics or
|
|
@@ -317,8 +343,10 @@ A group is one observed shape. Two matches share a group when, for every matched
|
|
|
317
343
|
status, source and safe-v1 name agree, and for every edge between them the `CAUSED_BY` origin
|
|
318
344
|
agrees, under the same source kind, causal fidelity and link setting, and the same answer to
|
|
319
345
|
whether the trace discloses causal edges chosen heuristically at ingest (`motif-signature/v1`).
|
|
320
|
-
That last answer is per trace:
|
|
321
|
-
|
|
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
|
|
322
350
|
answer is `disclosed` whenever a recognized heuristic-edge gap is present, else `not_disclosed`.
|
|
323
351
|
It is `unknown` for a trace with no capture record, or with a gap code this build does not know
|
|
324
352
|
and no recognized heuristic-edge gap. It is `not_applicable` for a single-step preset, which has
|
|
@@ -354,7 +382,13 @@ A `*.json` file in a directory that is not a valid artifact is skipped with a wa
|
|
|
354
382
|
`query`; pass `--strict` to fail the batch instead.
|
|
355
383
|
The public schema is a repository-vendored contract (not wheel package data) at
|
|
356
384
|
[`contracts/review-candidates.schema.json`](contracts/review-candidates.schema.json); G3
|
|
357
|
-
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.
|
|
358
392
|
|
|
359
393
|
SyncMill can consume the report without importing this package:
|
|
360
394
|
|
|
@@ -394,16 +428,59 @@ Known limits:
|
|
|
394
428
|
it such a run reads `ok` unless some other step recorded an error. Trace status stays `ok`
|
|
395
429
|
either way. A task paused by `interrupt()` is reported without a graph. Both are reported
|
|
396
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.
|
|
397
445
|
- Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
|
|
398
446
|
causes may be missing.
|
|
399
447
|
- Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
|
|
400
448
|
producer's names, and its error messages unless ingested with `--redact-errors`.
|
|
401
|
-
- CI covers Python 3.12 on Linux.
|
|
449
|
+
- CI covers Python 3.12, 3.13 and 3.14 on Linux.
|
|
402
450
|
|
|
403
451
|
Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
|
|
404
452
|
super-step), and node names/kinds are best-effort metadata. Phoenix/OTLP adapters are
|
|
405
453
|
span-level. Subgraph checkpoints are namespaced, but cross-namespace causality remains
|
|
406
|
-
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.
|
|
407
484
|
|
|
408
485
|
## Develop
|
|
409
486
|
|
|
@@ -456,6 +533,18 @@ origins, fidelity disclosures, and the digest of the exact artifact bytes. Logic
|
|
|
456
533
|
comparison keys are JSON-encoded typed paths; consumers should treat them as opaque strings.
|
|
457
534
|
Containment and unknown edges preserve all error investigation candidates, deeper first.
|
|
458
535
|
Explicit causal ancestry supplies context, not proof that an exception propagated.
|
|
536
|
+
When the artifact records that a causal parent was chosen heuristically at ingest
|
|
537
|
+
(`otlp.graph_parent_by_timing` or `otlp.graph_parent_fallback`), `analyze` adds a warning for
|
|
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
|
|
545
|
+
same caveat under its tree, and `explain` and `query --explain` under every chain that has at
|
|
546
|
+
least one cause; there their fan-in note no longer calls the causes real or says to trust the
|
|
547
|
+
chain.
|
|
459
548
|
Explicit OTLP OK takes precedence over handled exception events on new ingestion; old
|
|
460
549
|
artifacts are not reinterpreted.
|
|
461
550
|
A span whose status is UNSET and that carries no exception event (one with a message or
|
|
@@ -525,7 +614,9 @@ managed values, cache-key functions — though no node runs. It is never inferre
|
|
|
525
614
|
Every ingest records what the adapter could not observe: `trace.capture` is an object whose
|
|
526
615
|
`gaps` list holds a fixed code and count per ingest disclosure (empty when there was none),
|
|
527
616
|
and a step's `unknown_fields` names a kind, name or error message the adapter substituted. That
|
|
528
|
-
record makes a fresh artifact declare `schema_version` 7
|
|
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
|
|
529
620
|
content, which is what an artifact written by an earlier release keeps when it is loaded and
|
|
530
621
|
saved again: a graph read (`--graph`) needs 6, interrupt steps 5, a derived tree that relies on
|
|
531
622
|
EdgeOrigin precedence 4, derived task steps 3, and anything else 2, byte for byte (a v1 file is
|
|
@@ -546,7 +637,7 @@ withheld, and a single unparseable cost withholds the whole total instead of und
|
|
|
546
637
|
An OTLP or Phoenix import records an unreadable total cost as `unreadable` for that reason, and
|
|
547
638
|
reports token counts, other costs, currencies and evaluation scores it cannot read as
|
|
548
639
|
unavailable, disclosing each on stderr; the producer's value is never kept. File input, stdin
|
|
549
|
-
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
|
|
550
641
|
artifacts one after another and keeps only the matches it prints (plus the traces `--explain`
|
|
551
642
|
renders). With `--limit`, its peak memory therefore grows with the number of artifacts only
|
|
552
643
|
by a small per-file record (path, trace id); without it, by the matches it prints. Inside one
|
|
@@ -554,16 +645,18 @@ trace, too, it keeps only the matches that still fit under `--limit`, but it sti
|
|
|
554
645
|
and counts every match to report the exact total, so time grows with the match count: one
|
|
555
646
|
2500-step trace with 3.1M matches takes about 3 s at `--limit 1`, in 70 MiB. The search's own
|
|
556
647
|
working state (steps reached at each distance of a gap) does not depend on `--limit`. With
|
|
557
|
-
`--backend ladybug` on a pattern that
|
|
648
|
+
`--backend ladybug` on a pattern that runs as Cypher, LadybugDB itself buffers the
|
|
558
649
|
query's full result set: a 3001-step trace with 2.25M `tool-retry-failure` matches peaked
|
|
559
650
|
at 757 MiB, where the store alone takes about 300 MiB. A plain read, `count(*)` and
|
|
560
|
-
`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.
|
|
561
654
|
|
|
562
655
|
Both stores isolate returned mutable objects. InMemory node upsert replaces by ID;
|
|
563
656
|
Ladybug's existing insertion semantics are unchanged. Use `is_isomorphic` or `diff` for
|
|
564
657
|
deep-tree equality; `canonical()` preserves nested tuples, whose external Python equality
|
|
565
|
-
can still reach the interpreter recursion limit. CI currently covers Python 3.12/Linux;
|
|
566
|
-
other
|
|
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.
|
|
567
660
|
|
|
568
661
|
## Contributing, security, and license
|
|
569
662
|
|