agent-tracegraph 0.4.0__tar.gz → 0.6.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.6.0/CHANGELOG.md +774 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/PKG-INFO +167 -17
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/README.md +166 -16
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/SECURITY.md +56 -10
- agent_tracegraph-0.6.0/contracts/check-report.schema.json +396 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/pyproject.toml +5 -1
- agent_tracegraph-0.6.0/src/tracegraph/_errors.py +35 -0
- agent_tracegraph-0.6.0/src/tracegraph/adapters/base.py +59 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/langgraph_checkpoint.py +328 -33
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/otlp_spans.py +115 -33
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/phoenix_export.py +7 -2
- agent_tracegraph-0.6.0/src/tracegraph/analysis/check.py +603 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/diagnose.py +61 -5
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/memo.py +4 -1
- agent_tracegraph-0.6.0/src/tracegraph/analysis/motifs.py +272 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/patterns.py +179 -87
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/artifact.py +83 -31
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/cli.py +920 -128
- agent_tracegraph-0.6.0/src/tracegraph/graph_ref.py +136 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/input_validation.py +8 -1
- agent_tracegraph-0.6.0/src/tracegraph/model.py +452 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/sqlite_snapshot.py +7 -2
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/ladybug.py +48 -8
- agent_tracegraph-0.4.0/CHANGELOG.md +0 -354
- agent_tracegraph-0.4.0/src/tracegraph/adapters/base.py +0 -22
- agent_tracegraph-0.4.0/src/tracegraph/model.py +0 -242
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/.gitignore +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/LICENSE +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/analysis-report.schema.json +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/phoenix-runtime.json +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/contracts/review-candidates.schema.json +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/__init__.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/_atomic_write.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/adapters/__init__.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/__init__.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/ahu.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/analysis/explain.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/normalize.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/py.typed +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/review_candidates.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/__init__.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/base.py +0 -0
- {agent_tracegraph-0.4.0 → agent_tracegraph-0.6.0}/src/tracegraph/store/in_memory.py +0 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Tracegraph are documented here.
|
|
4
|
+
|
|
5
|
+
The **artifact** schema version and the **report**/**review-candidate** JSON schemas are
|
|
6
|
+
separate contracts from this version number; changes to any of them are called out
|
|
7
|
+
explicitly.
|
|
8
|
+
|
|
9
|
+
## 0.6.0 - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`tracegraph summarize <preset> <artifacts...>`** (#60) groups a preset's matches across an
|
|
14
|
+
artifact batch by observed shape, so an operator can pick the recurring failure shape to look at
|
|
15
|
+
first. A group's signature (`motif-signature/v1`) is the matched steps' kind, status, source and
|
|
16
|
+
safe-v1 name, the `CAUSED_BY` origins between them, and the trace's source kind, causal
|
|
17
|
+
fidelity, link setting and whether it discloses heuristically chosen edges (per trace, not per
|
|
18
|
+
edge). It has no ids, error text or artifact file paths; an identifier-shaped name that looks
|
|
19
|
+
like a path is kept, as in `analyze` reports. Each group reports its match count and trace count
|
|
20
|
+
separately, plus up to three representatives (artifact digest and step ids). Only matches
|
|
21
|
+
confirmed by `check`'s rule are grouped; the rest are counted as unconfirmed. Fields that are
|
|
22
|
+
not known are kept as unknown. A coverage section always reports skipped artifacts, traces that
|
|
23
|
+
may hide a match, disclosed capture gaps, traces with no capture record and unconfirmed matches.
|
|
24
|
+
`--out` writes the `tracegraph.motif-summary` schema-v1 JSON with the same input protection as
|
|
25
|
+
`export-review-candidates`. Only strictly adjacent presets are accepted; the three gap presets
|
|
26
|
+
exit 2. Memory holds one trace and one entry per group, never the matches. A 160-wide retry fan
|
|
27
|
+
(25,600 matches) adds about 27 KiB over a 40-wide one, where collecting the matches adds about
|
|
28
|
+
2 MiB. The issue suggested validating demand first; this is the minimal version.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **`query --limit` no longer holds every match of the trace it is reading** (#79). Each
|
|
33
|
+
trace's matches were collected and sorted in full before the cap was applied, so one trace
|
|
34
|
+
with millions of matches took about 1 GiB at `--limit 1`. The matchers now produce matches
|
|
35
|
+
one at a time and `query` keeps only those still under the cap. The footer's total stays
|
|
36
|
+
exact, so every match is still counted. Measured on macOS/arm64 at `--limit 1` with
|
|
37
|
+
synthetic traces: a 2500-step chain of failing `x` agents (`repeated-agent-failure`, 3.1M
|
|
38
|
+
matches), and 1500 tools feeding one `retry:x` step that feeds 1500 failing tools
|
|
39
|
+
(`tool-retry-failure` and `tool-retry-failure-near`, 2.25M matches each):
|
|
40
|
+
|
|
41
|
+
| trace, preset | memory backend | `--backend ladybug` |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| chain, `repeated-agent-failure` | 976 → 70 MiB, 5.5 → 2.8 s | 1226 → 308 MiB (Python fallback) |
|
|
44
|
+
| fan, `tool-retry-failure` | 1097 → 74 MiB, 2.8 → 1.6 s | 1753 → 757 MiB (Cypher) |
|
|
45
|
+
| fan, `tool-retry-failure-near` | 927 → 74 MiB, 3.2 → 2.1 s | 1470 → 613 MiB (Cypher) |
|
|
46
|
+
|
|
47
|
+
The Cypher rows stay high because LadybugDB buffers the full result set; an empty store
|
|
48
|
+
already takes about 300 MiB. The matches selected, their order, the footer total, exit codes
|
|
49
|
+
and error precedence are unchanged, and `find_matches` returns the same list for every caller.
|
|
50
|
+
Time still grows with the match
|
|
51
|
+
count. A mode that stops early and reports no total was considered and not built.
|
|
52
|
+
`LadybugStore` gains `first_matches(pattern, n)`; `tracegraph.analysis.patterns` gains
|
|
53
|
+
`iter_matches` and `first_matches`.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Boundaries that promise `ValueError` no longer leak an error from describing the one they
|
|
58
|
+
caught** (#109). Naming a caught exception runs caller code: `str(exc)` calls its `__str__`,
|
|
59
|
+
and a metaclass can make the type's `__name__` a property. When that raised, its error
|
|
60
|
+
escaped instead: from LangGraph ingest when the saver's listing or a supplied `schedule_graph`
|
|
61
|
+
fails, and from `--graph` when the module fails to import. The validated copy that
|
|
62
|
+
`artifact.dumps()` and `analyze()` use since #82 and #93 (both in this release) named its
|
|
63
|
+
caught exception the same way and is hardened too. These now describe the exception through
|
|
64
|
+
one internal guard (`tracegraph._errors`) that falls back to fixed text, and uses only a name or
|
|
65
|
+
message that is a plain `str`.
|
|
66
|
+
Ordinary exceptions are described exactly as before. `artifact.from_obj()` on a schema-v1
|
|
67
|
+
payload also refuses, as `ValueError`, an edge sequence that raises `SystemExit` or another
|
|
68
|
+
`BaseException` (the current schema already did for `SystemExit`), or whose type check
|
|
69
|
+
raises; `KeyboardInterrupt` still propagates.
|
|
70
|
+
|
|
71
|
+
- **`--graph` loads a module file whose path is not valid UTF-8** (#110). On POSIX such a path
|
|
72
|
+
arrives surrogate-escaped, and naming the module from its strict UTF-8 encoding refused it:
|
|
73
|
+
`ingest --graph` exited 2 with a `UnicodeEncodeError` for a file it could import. Such a path
|
|
74
|
+
is now named from its file-system bytes, under its own prefix so it cannot take another
|
|
75
|
+
file's name. Every path that loaded before keeps its module name.
|
|
76
|
+
|
|
77
|
+
- **Cost totals and their baseline delta are exact** (#111). They were computed at `Decimal`'s
|
|
78
|
+
default precision of 28 significant digits and rounded without a note, while the total was
|
|
79
|
+
still reported as complete: a cost of `0.12345678901234567890123456789` was totalled as
|
|
80
|
+
`0.1234567890123456789012345679`, and `10000000000000000000000000000` plus `0.01` as
|
|
81
|
+
`10000000000000000000000000000`. They are now computed at a precision of 160, which is exact
|
|
82
|
+
for any sum of costs within the 64-character bound up to about 10^32 steps. Past that, the total
|
|
83
|
+
(or only the delta) is withheld with a note instead of rounded. Ordinary costs report the same
|
|
84
|
+
digits as before (`1.00 + 1.00` is still `2.00`).
|
|
85
|
+
|
|
86
|
+
- **A cost too long to write out is no longer summed or stored** (#100). A few characters of
|
|
87
|
+
exponent stand for a number of any size. A model-valid `total_cost` of `"1e1000000"` made
|
|
88
|
+
`analyze` fail with `decimal.Overflow`, an `ArithmeticError` rather than the promised
|
|
89
|
+
`ValueError`, with or without a baseline; `"9e999999"` did not overflow but wrote a
|
|
90
|
+
million-digit total into the report. OTLP ingest wrote such a value out in full, so a 10-byte
|
|
91
|
+
attribute `"1e99999999"` became a 100,000,000-character `total_cost` in the artifact. A cost is
|
|
92
|
+
now read only if, written out in full, it is at most 64 characters (`model.MAX_COST_CHARS`),
|
|
93
|
+
checked from its digits and exponent without writing it out (`model.cost_is_bounded(Decimal)`).
|
|
94
|
+
`analyze` withholds the total
|
|
95
|
+
with its own note, as for an unreadable cost; OTLP ingest treats a longer cost as unreadable,
|
|
96
|
+
so the total and component costs are disclosed as `otlp.unreadable_total_cost` and
|
|
97
|
+
`otlp.unreadable_component_cost` as before, and a readable alias is still used. Artifacts are
|
|
98
|
+
not refused at load. The bound is on each cost read, not on the total: summed exactly (#111),
|
|
99
|
+
two 64-character costs can make a 127-character total, and a million of them 133.
|
|
100
|
+
|
|
101
|
+
- **A lone surrogate in a producer string no longer ends a command in a traceback** (#101). JSON
|
|
102
|
+
can carry one (`"\ud800"`), and the artifact loads and validates, but strict UTF-8 refused it
|
|
103
|
+
in several places: `analyze` (and `check` and the new `summarize`, which share the helper)
|
|
104
|
+
hashing a name into its `redacted:` alias; `ingest-otlp` without `--out` hashing the trace id
|
|
105
|
+
into the default file
|
|
106
|
+
name; `export-incident-memo` hashing the run id; `inspect`, `explain` and `query` writing the
|
|
107
|
+
name to the terminal; and LangGraph ingest hashing a routing channel into a candidate task id.
|
|
108
|
+
The three hashes now encode with `surrogatepass`, so every other string hashes to the same
|
|
109
|
+
alias, file name and digest as before. The LangGraph candidate is skipped, because LangGraph
|
|
110
|
+
could not have produced a task id from it. The CLI escapes what its output streams cannot
|
|
111
|
+
encode instead of failing.
|
|
112
|
+
|
|
113
|
+
- **CLI output prints an emoji code in a producer string as written** (#102). The CLI's Rich
|
|
114
|
+
consoles kept emoji substitution on, and escaping markup does not turn it off, so a step named
|
|
115
|
+
`:ok:` printed as 🆗 in `query`, `inspect`, `explain` and `analyze` while the artifact and the
|
|
116
|
+
JSON reports held `:ok:`. Both consoles now have it off, so the new `summarize` prints the same
|
|
117
|
+
way. No text the CLI prints itself used an emoji code.
|
|
118
|
+
|
|
119
|
+
- **`artifact.from_obj()` on a schema-v1 payload raises only `ValueError`** (#103). When reading
|
|
120
|
+
a caller-supplied lazy edge sequence failed, the migration formatted the exception into its
|
|
121
|
+
message, so an exception whose `__str__` raises escaped as that error instead. The message no
|
|
122
|
+
longer formats the exception at all (its type's name can run caller code too, through a
|
|
123
|
+
metaclass); the original stays on the chain as `__cause__`. `loads()` and the CLI parse JSON
|
|
124
|
+
into lists, so neither could reach this.
|
|
125
|
+
|
|
126
|
+
- **`analyze()`, `check.evaluate()` and `build_incident_memo()` raise only `ValueError` on a
|
|
127
|
+
trace the model refuses** (#93). On a trace changed in place after it was built they raised a
|
|
128
|
+
raw `TypeError` or `AttributeError` (`seq = "zero"`, `steps = None`, `trace = None`, a step
|
|
129
|
+
`kind = "bogus"`, an integer `error_msg`), mostly from analysis code past their structural
|
|
130
|
+
check, and the memo rendered a step `status = "maybe"` without complaint. Each now analyzes the
|
|
131
|
+
copy the model validates, the same one `artifact.dumps()` and the writers use, so a value the
|
|
132
|
+
model coerces (`"0"` for a `seq`) is analyzed as it would be read and the caller's trace is
|
|
133
|
+
left unchanged. `analyze()` and the memo still accept structurally sound but non-canonical
|
|
134
|
+
traces; `check.evaluate()` still requires the canonical form. The validated copy does not change
|
|
135
|
+
the report for a valid trace; cost totals change separately in this release (#100, #111). That
|
|
136
|
+
copy is now made from a Python-mode dump: the JSON-mode dump
|
|
137
|
+
turned a NaN in `trace.run_id` or `trace.links_preserved` into `null`, which validated, so
|
|
138
|
+
`dumps()` and both writers erased the value instead of refusing it. It no longer depends on
|
|
139
|
+
the warnings filter either: under `-W error` a coercible value used to be refused through
|
|
140
|
+
pydantic's serializer warning. Cost on a 6000-step chain, best of 7, measured in PR #95 against
|
|
141
|
+
its parent `2eaa403` (which already had #82): `analyze()` 0.25–0.27 s
|
|
142
|
+
→ 0.30–0.32 s, with a baseline 0.57–0.58 s → 0.69–0.70 s, `check.evaluate()` 0.65–0.67 s →
|
|
143
|
+
0.78–0.79 s, `build_incident_memo()` 0.016 s → 0.044 s.
|
|
144
|
+
|
|
145
|
+
- **`artifact.dumps()` raises only `ValueError`** (#82). A trace changed in place after it was
|
|
146
|
+
built (pydantic does not revalidate on attribute assignment) could make it raise a raw
|
|
147
|
+
`TypeError` (`seq = "zero"`, `steps = None`, `edges = None`) or write a field the readers
|
|
148
|
+
reject (`status = "maybe"`, an integer `step_id`). `dumps()` now serializes a copy the model
|
|
149
|
+
has validated, as the writers already did, and raises `ValueError` for anything the model
|
|
150
|
+
refuses or cannot serialize: pydantic's `ValidationError` for a mistyped field, a
|
|
151
|
+
serialization error (also a `ValueError`) for a value it cannot dump at all, such as
|
|
152
|
+
`trace = None`. A value the model coerces (`"0"` for a `seq`) is written as it would be read.
|
|
153
|
+
A lazy sequence that fails while being serialized, through `dumps()` or either writer, is now
|
|
154
|
+
reported as a `ValueError` too, instead of whatever it raised. `dumps()` still checks fields,
|
|
155
|
+
not structure: a dangling edge or a stale trace status is written. Checking structure there
|
|
156
|
+
would refuse the structurally sound but non-canonical traces that `analyze()` digests, such as
|
|
157
|
+
one with a stale trace status (`analyze()` itself refuses a dangling edge). Unknown-field flags
|
|
158
|
+
without a capture record now reach a `dumps()`
|
|
159
|
+
caller as `ValidationError`, as they already did for the writers. A valid trace is
|
|
160
|
+
serialized byte for byte as before. Cost on a 6000-step chain, measured in PR #94 (its helper
|
|
161
|
+
was changed again by #93): `dumps()` 0.15 s → 0.20 s;
|
|
162
|
+
`analyze()` digests the current trace through it, a baseline comparison and `check` digest
|
|
163
|
+
both.
|
|
164
|
+
|
|
165
|
+
- **An error already in the parent graph is no longer recorded again around a subgraph** (#85).
|
|
166
|
+
A LangGraph checkpoint is an error where the error channel holds one and none of the
|
|
167
|
+
checkpoints it was built from did, but the check read only a checkpoint's causal parents, and
|
|
168
|
+
three shapes hid the earlier error from it. A subgraph's `__start__` was compared only with
|
|
169
|
+
the subgraph's input checkpoint, which holds its values as pending writes. A persistent
|
|
170
|
+
subgraph (`checkpointer=True`) entered again was compared with its previous invocation, not
|
|
171
|
+
with the outer checkpoint that launched it. And the parent-graph checkpoint after a subgraph
|
|
172
|
+
was compared only with the subgraph's exit, so a subgraph whose state has no error channel, or
|
|
173
|
+
that cleared its own copy, made the outer error look new. One failure was recorded two or more
|
|
174
|
+
times: three with nested subgraphs, or when the subgraph cleared its copy. The check now also
|
|
175
|
+
reads each checkpoint's declared parents, and for a `__start__`, its input checkpoint's
|
|
176
|
+
declared parent and its launch parent (`metadata.parents`). A former duplicate written by a
|
|
177
|
+
node that rewrote the still-set channel is disclosed as `langgraph.rewritten_error_channel`
|
|
178
|
+
instead, with its stderr line, as for any such write since #47; `check` reads that gap as
|
|
179
|
+
before.
|
|
180
|
+
|
|
181
|
+
- **A fork copy is no longer a second failure.** `update_state(..., as_node="__copy__")`
|
|
182
|
+
copies a checkpoint and runs no node, but it is linked to the copied checkpoint's parent, so
|
|
183
|
+
copying the checkpoint where an error first appeared (in the root graph, or a subgraph's
|
|
184
|
+
`__start__`) recorded that error again. A fork now never records an error.
|
|
185
|
+
|
|
186
|
+
For both: steps, edges, names and kinds are unchanged; what changes is which steps are errors
|
|
187
|
+
(with their `error_msg`) and the disclosure above. An error first written inside a subgraph, a
|
|
188
|
+
`Send` payload error below a clean state, and an error a caller's `update_state` writes are
|
|
189
|
+
recorded as before. Artifacts change only for histories that held such a duplicate. The
|
|
190
|
+
schema stays v7, and the committed fixtures are byte-identical. One shape still records the
|
|
191
|
+
error twice: a fork copy of a subgraph's *input* checkpoint, then resumed (#90).
|
|
192
|
+
|
|
193
|
+
### Known limitations
|
|
194
|
+
|
|
195
|
+
- Resuming after a fork copy of a subgraph's input checkpoint
|
|
196
|
+
(`update_state(..., as_node="__copy__")` on it) records an error again at the subgraph's next
|
|
197
|
+
start: one the parent graph held (#90) or one a `Send` payload carried (#96). Four ways of
|
|
198
|
+
recognizing such a copy were tried. Three classified the fork by its content: an unread
|
|
199
|
+
`__start__` value, a `__start__` version absent from `versions_seen`, and pending writes from
|
|
200
|
+
LangGraph's `__start__` task. The fourth matched it to an input checkpoint with identical
|
|
201
|
+
`channel_values`, `channel_versions` and `versions_seen`, and compared its run with the
|
|
202
|
+
original's. Each hid a genuine failure on a plain `Pregel` graph, so none shipped. A
|
|
203
|
+
`Pregel` input's first step can run a user node, and a no-op `update_state(...,
|
|
204
|
+
as_node="__input__")` can write an input identical to a loop checkpoint.
|
|
205
|
+
|
|
206
|
+
## 0.5.0 - 2026-10-18
|
|
207
|
+
|
|
208
|
+
### Added
|
|
209
|
+
|
|
210
|
+
- **Dropped OTLP span links are disclosed** (#49). A link the OTLP importer could not record as a
|
|
211
|
+
cause was skipped silently, while the header still read `causal_fidelity=declared_dag` and
|
|
212
|
+
`links_preserved=true`. Each kind now prints one stderr line with its count and becomes a
|
|
213
|
+
capture gap: `otlp.unresolved_span_link` (names this trace but no span in the export,
|
|
214
|
+
possibly a partial export), `otlp.unscoped_span_link` (names no trace, missing or empty `traceId`; it may
|
|
215
|
+
refer to a span of this trace, so it is no longer treated as foreign) and
|
|
216
|
+
`otlp.foreign_trace_link` (names another trace). `check` treats the first two as gaps that may
|
|
217
|
+
have left a step out (one-sided for the count rules, insufficient for topology) and the third
|
|
218
|
+
as having no effect. The schema stays v7. Only artifacts ingested from exports with such links
|
|
219
|
+
change, and with them the digests of reports made from them; every committed fixture resolves
|
|
220
|
+
all its links. An artifact holding none of these codes reads and checks as before.
|
|
221
|
+
- **`--strict` for `query` and `export-review-candidates`** (#50). A `*.json` file in a directory
|
|
222
|
+
that is not a valid artifact is skipped with a warning, so a truncated artifact holding the
|
|
223
|
+
failure let a "pattern must not appear" gate pass with exit 1 ("no matches"), and let
|
|
224
|
+
`export-review-candidates` write a report without it. With `--strict`, any skipped directory
|
|
225
|
+
file fails the command with exit 2 (`--strict: N discovered file(s) could not be loaded as
|
|
226
|
+
tracegraph artifacts`) after the usual skip warning, and no report is written. An explicitly named bad
|
|
227
|
+
file still fails first, as before; `--strict` is checked before "no valid artifacts" and
|
|
228
|
+
duplicate trace ids. Without the flag nothing changes, and with it and no strays the output
|
|
229
|
+
is byte-identical.
|
|
230
|
+
- **`tracegraph check`, an explicit regression verdict** (#59): one current run, one baseline the
|
|
231
|
+
caller names, and rules the caller names (`failures-not-increased`,
|
|
232
|
+
`pattern-not-increased:<preset>`, `topology-unchanged[:shape]`; no default set). Exits 0 PASS,
|
|
233
|
+
1 FAIL, 3 INSUFFICIENT_EVIDENCE, 2 invalid input (no report written). Rules compare counts
|
|
234
|
+
per step `(kind, name)`; unknown evidence the selected rule depends on — an `unset` status, a
|
|
235
|
+
substituted value or missing name on a step it could count, a capture gap that may have
|
|
236
|
+
left a step out of the artifact — blocks PASS on the current side and
|
|
237
|
+
FAIL on the baseline side instead of counting as zero (a step-omitting gap on either side makes
|
|
238
|
+
the topology rule insufficient, since it compares trees, not counts). Pairs from different
|
|
239
|
+
sources, or with either artifact lacking capture provenance (written before artifact v7),
|
|
240
|
+
are insufficient for every rule, and pairs read with different causal settings for the
|
|
241
|
+
rules that read causal structure. New contract `tracegraph.check` v1
|
|
242
|
+
(`contracts/check-report.schema.json`), bound to both canonical artifact digests and each
|
|
243
|
+
rule's id, version and parameters. `diff`, `query` and `analyze` keep their exit codes.
|
|
244
|
+
- **Capture provenance, artifact schema version 7** (groundwork for #59): every ingest now
|
|
245
|
+
records in the artifact what the adapter could not observe, so a later comparison can tell
|
|
246
|
+
"recorded, nothing disclosed" from "not recorded". `trace.capture.gaps` keeps one fixed code and count
|
|
247
|
+
per ingest disclosure (the stderr text is unchanged and stays out of the artifact; the code
|
|
248
|
+
carries no checkpoint id, task id or producer value), and `capture` is present with an empty
|
|
249
|
+
list when the read raised no disclosure; values substituted on one step are recorded
|
|
250
|
+
separately. `steps[].unknown_fields` names values an adapter substituted:
|
|
251
|
+
`kind` and `name` (a span without a recognized OpenInference kind or a usable name, including
|
|
252
|
+
Phoenix names dropped as non-identifiers; an unattributed LangGraph task; and every LangGraph
|
|
253
|
+
checkpoint step except the input checkpoint and the loop step that applies it, named or not,
|
|
254
|
+
because a checkpoint does not record its writer and its name is inferred from the parent's
|
|
255
|
+
routing) and `error_msg` (OTLP's `"error"` fallback, withheld messages, LangGraph's
|
|
256
|
+
unserialized-exception label). Task, interrupt and scheduled steps named from LangGraph's own
|
|
257
|
+
task ids are not flagged. Two OTLP parent resolutions that were silent are now recorded too: a `graph.node.parent_id` resolved by start time among
|
|
258
|
+
several spans, and one not honored in favour of `parentSpanId`; each also prints a new stderr
|
|
259
|
+
warning. Every newly ingested artifact declares 7 and 0.4.x refuses it by name; an artifact
|
|
260
|
+
written by an earlier release re-serializes exactly as the previous release wrote it (v2–v6
|
|
261
|
+
byte for byte, v1 migrated to v2 as before) and never gains provenance. Newly ingested
|
|
262
|
+
artifacts therefore have new digests, as do analysis reports and incident memos built from
|
|
263
|
+
them; report and memo shapes, other CLI output and exit codes are unchanged.
|
|
264
|
+
- **`analyze` reads OTLP span exports directly** (#57): a JSON or Collector JSONL export
|
|
265
|
+
(`resourceSpans`) is diagnosed in memory with the same adapter settings as `ingest-otlp`, so
|
|
266
|
+
the report is byte-identical to analysing the ingested artifact; `--trace` selects one trace of
|
|
267
|
+
a multi-trace export and `--baseline` accepts an OTLP export too. Inputs are routed by shape
|
|
268
|
+
before any adapter runs (`schema_version` → artifact, `resourceSpans` → OTLP, `spans` or a
|
|
269
|
+
root `traces`/`data` list → Phoenix); record validity stays the adapter's call, so the direct
|
|
270
|
+
path accepts and rejects what the explicit ingest commands do. A payload carrying both OTLP
|
|
271
|
+
and Phoenix records exits 2: a single object names the ingest command that reads each format,
|
|
272
|
+
and a list, JSONL or Phoenix envelope says to extract one format's records first. An unknown
|
|
273
|
+
shape exits 2 listing the supported ones instead of failing as a Phoenix export.
|
|
274
|
+
`ingest-otlp --file -` reads stdin, and stdin now goes through the same
|
|
275
|
+
`TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling as files for `analyze`, `ingest-otlp` and
|
|
276
|
+
`ingest-phoenix` (the stdin half of #53; the `px` output half is the "Phoenix `px` output is bounded" entry below);
|
|
277
|
+
`ingest-phoenix` reads its input with the Phoenix adapter only (its accepted inputs are
|
|
278
|
+
unchanged). Next investigation steps after a direct OTLP
|
|
279
|
+
diagnosis print an `ingest-otlp` save template. No file is written without `--json-out`;
|
|
280
|
+
analysis JSON schemas, exit codes and privacy boundaries are unchanged.
|
|
281
|
+
- **Guided investigation after `analyze` and `phoenix diagnose`** (#58): displayed failure
|
|
282
|
+
candidates include full step IDs, with POSIX-quoted `explain` commands for existing artifacts
|
|
283
|
+
or explicitly labelled save/replay templates for exports, stdin, and unsaved Phoenix traces.
|
|
284
|
+
Evidence IDs, paths and commands preserve literal emoji-like tokens and are not forcibly
|
|
285
|
+
line-wrapped; consumed pipe inputs and descriptor aliases (`/dev/stdin`, `/dev/fd/N`,
|
|
286
|
+
`/proc/<pid>/fd/N`, or a symlink to one, including regular-file stdin redirects; a symlink
|
|
287
|
+
to a directory descriptor is not detected) require replay. Nonprinting arguments are shown as
|
|
288
|
+
escaped data instead of copyable commands. Hidden
|
|
289
|
+
candidates show the required `--limit`; report paths are acknowledged only after successful
|
|
290
|
+
writes, without duplicate report-path notices. Existing edge-origin/fidelity warnings and
|
|
291
|
+
missing-telemetry disclosures continue to distinguish evidence from causal claims; this
|
|
292
|
+
change adds navigation, not new causal inference. Analysis JSON schemas, exit codes, and
|
|
293
|
+
artifact privacy boundaries are unchanged.
|
|
294
|
+
- **`ingest --graph` reports LangGraph tasks that were scheduled but recorded nothing.**
|
|
295
|
+
Given the compiled graph (`path/to/file.py:attr` or `pkg.module:attr`), LangGraph's
|
|
296
|
+
`get_state` lists the tasks it schedules at the thread's newest root checkpoint, and each one
|
|
297
|
+
with no write there becomes an `unset` step with the new source value `scheduled`, linked by
|
|
298
|
+
`CAUSED_BY` to that checkpoint: the recursion limit, a killed process, a static breakpoint,
|
|
299
|
+
one step per `Send` packet. Finished, failed and paused tasks are still read from the
|
|
300
|
+
checkpoint's writes, so these steps never duplicate `task` or `interrupt` steps. The result
|
|
301
|
+
is LangGraph's answer for the supplied graph; the trace header records its structural
|
|
302
|
+
fingerprint as `schedule_graph`, also when nothing is scheduled. An unapplied
|
|
303
|
+
`Command(goto=...)` input and `Send` packets to a node the graph lacks are disclosed at
|
|
304
|
+
ingest. `--graph` imports the module and runs the graph's state callbacks while reading,
|
|
305
|
+
but no node. `inspect` marks such a step `◌ scheduled` and adds `· N scheduled` to the
|
|
306
|
+
summary.
|
|
307
|
+
- **Artifact schema version 6**, stamped by content: an artifact read with `--graph` (it has
|
|
308
|
+
`schedule_graph`, or a `scheduled` step) declares 6, and 0.4.x refuses it by name. The header
|
|
309
|
+
field is omitted, not written as `null`. (A fresh ingest by this release declares 7 for its
|
|
310
|
+
capture record, which outranks 6.)
|
|
311
|
+
- **A hands-on getting-started notebook in Korean**, `examples/getting_started_ko.ipynb`, for
|
|
312
|
+
first-time users working from a repository checkout. It runs the example agent and the
|
|
313
|
+
offline commands other than `check` and `export-review-candidates`, interprets each output, and works through two applied scenarios:
|
|
314
|
+
checking a change against a baseline, and aggregating failures across many runs from
|
|
315
|
+
`analyze --json-out` reports. Its outputs are committed, and `tests/test_notebooks.py`
|
|
316
|
+
executes it so its commands and exit codes cannot drift from the CLI. `nbclient` and
|
|
317
|
+
`ipykernel` join the development dependencies. Nothing ships in the wheel or sdist, and no
|
|
318
|
+
artifact or report contract changes.
|
|
319
|
+
|
|
320
|
+
### Changed
|
|
321
|
+
|
|
322
|
+
- **`artifact.save()` and `artifact.save_atomic()` refuse a trace the readers would reject**
|
|
323
|
+
(#67). Pydantic does not revalidate on attribute assignment, so a `NormalizedTrace` changed in
|
|
324
|
+
place could be written in a form tracegraph's own readers refuse: `save()` wrote a mistyped
|
|
325
|
+
`status`, and both wrote a `CAUSED_BY` edge to a missing step (which `artifact.load` accepted
|
|
326
|
+
and only `validate` / `analyze` rejected), a trace status that disagrees with its steps, a
|
|
327
|
+
wrong `projection_lossy` flag, or a missing `TREE_PARENT`. Both writers now round-trip the
|
|
328
|
+
model and run `validate_normalized`, the check the stores and `validate` apply on load
|
|
329
|
+
(legacy projections included), before touching the destination, and raise `ValueError`
|
|
330
|
+
(pydantic's `ValidationError` is one), leaving an existing file untouched. A mistyped `seq` used to escape
|
|
331
|
+
every writer as a raw `TypeError`; it is now a `ValidationError` too. A valid trace is written
|
|
332
|
+
byte for byte as before, from the validated copy, so a value the model coerces (`"0"` for a
|
|
333
|
+
`seq`) is written as it would be read. Cost on a 6000-step chain: `save()` 0.16 s → 0.29 s,
|
|
334
|
+
`save_atomic()` 0.19 s → 0.30 s; `ingest` already validated before saving, so it now checks
|
|
335
|
+
twice. Because the model now checks first, a capture-provenance
|
|
336
|
+
violation reaches a writer's caller as pydantic's `ValidationError` rather than the
|
|
337
|
+
`check_provenance` message (`artifact.dumps()` still raises the latter). `artifact.dumps()`
|
|
338
|
+
stays a serializer without the model and structure checks (its `TypeError` on a mistyped
|
|
339
|
+
`seq` is #82).
|
|
340
|
+
- `inspect` marks a step paused by `interrupt()` as `⏸ interrupted`, in yellow rather than
|
|
341
|
+
the green used for finished steps, and the summary line gains `· N interrupted` when there
|
|
342
|
+
is one. The output for every trace with neither interrupted nor scheduled steps is unchanged.
|
|
343
|
+
|
|
344
|
+
### Fixed
|
|
345
|
+
|
|
346
|
+
- **A LangGraph node writing the error channel while it is still set is disclosed** (#47). A
|
|
347
|
+
checkpoint is an error only where the error channel is set and every causal parent's is
|
|
348
|
+
empty, so a node failing again before the key was cleared stayed `ok`, silently, and the
|
|
349
|
+
retry presets could not match. The rule is unchanged: a checkpoint does not record which
|
|
350
|
+
node raised, and both a value-change rule and the issue's version rule were measured to
|
|
351
|
+
invent failures (a node returning the whole state, a reducer that grows the value, a
|
|
352
|
+
handler rewording the error). Instead, a node-written checkpoint whose error channel was
|
|
353
|
+
written again while set (its `channel_versions` entry moved, or without versions its value
|
|
354
|
+
changed) counts toward a stderr disclosure and the capture gap
|
|
355
|
+
`langgraph.rewritten_error_channel`. `check` reads it for the count rules (one-sided on that
|
|
356
|
+
side) and for success-dependent patterns such as `gate-failure-after-success` (insufficient
|
|
357
|
+
on either side), not for topology. Input, `__start__`, update and fork-copy checkpoints, and
|
|
358
|
+
parents in another namespace (a subgraph's entry or exit), are not compared, so propagation
|
|
359
|
+
and a caller's input disclose nothing; a propagated error recorded again at a subgraph's
|
|
360
|
+
`__start__` is pre-existing and filed as #85. Measured on LangGraph 1.2.2 and 1.2.15 with
|
|
361
|
+
InMemorySaver and SqliteSaver. Artifacts change only for histories with such writes; the
|
|
362
|
+
committed fixture has none. Schema stays v7.
|
|
363
|
+
- **`analyze --trace` and `--baseline-trace` no longer ignore the selector on an artifact**
|
|
364
|
+
(#64). An artifact holds one trace, and a selector naming a different one was accepted: the
|
|
365
|
+
command exited 0 with a report on the artifact's trace, not the one requested. It now exits 2
|
|
366
|
+
with `Invalid value for --trace: <input> holds trace '<id>', not '<selector>'` (or
|
|
367
|
+
`--baseline-trace`) and writes no report, as `check` already did and as a missing trace in an
|
|
368
|
+
OTLP or Phoenix export does. A selector naming the artifact's own trace is still accepted, and
|
|
369
|
+
the report is byte-identical to the one without it.
|
|
370
|
+
- **`ingest-phoenix --trace` no longer ignores the selector on a Phoenix-sourced artifact** (#69).
|
|
371
|
+
Re-ingesting such an artifact with a selector naming another trace exited 0 and re-saved the
|
|
372
|
+
artifact's own trace. It now exits 2 with the same message as `analyze` and writes nothing, to
|
|
373
|
+
`--out` or to the default path. A matching selector re-saves the artifact byte for byte, as
|
|
374
|
+
before. An artifact from any other source keeps its existing error, `--file must contain a
|
|
375
|
+
Phoenix CLI trace export`.
|
|
376
|
+
- **`phoenix doctor` and `phoenix diagnose` no longer crash when `px` cannot be run** (#56). A
|
|
377
|
+
`px` on PATH without the exec bit, a directory named `px`, or a file the OS cannot execute
|
|
378
|
+
raised `PermissionError` or `OSError` with a traceback. They now report that `px` failed with
|
|
379
|
+
an OS error, naming the operating system's text for the error number and never the exception
|
|
380
|
+
message: `doctor` prints `FAIL` and exits 1, `diagnose` exits 2, as for their other `px`
|
|
381
|
+
failures. When the OS refused to launch `px`, the message adds a hint to check that `px` on
|
|
382
|
+
PATH is an executable file.
|
|
383
|
+
- **Phoenix `px` output is bounded** (#53, the half #63 left open). `phoenix doctor` and
|
|
384
|
+
`phoenix diagnose` held all of `px`'s stdout and stderr in memory with only a time limit. Stdout
|
|
385
|
+
now goes to an unlinked temporary file held to the same `TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling
|
|
386
|
+
as file and stdin input (default 256 MiB, `0` disables it). `px` is killed and reaped once the
|
|
387
|
+
file passes the limit, and the command reports `... output exceeded the N-byte limit` without
|
|
388
|
+
echoing the output: doctor `FAIL` exit 1, diagnose exit 2. Stderr is discarded; it was never
|
|
389
|
+
shown. Two smaller changes come with it. Output that is not UTF-8 is now reported as such:
|
|
390
|
+
before, `doctor` printed the codec error and `diagnose` crashed with a traceback. And a
|
|
391
|
+
descendant of `px` that keeps stdout open no longer holds the call until the timeout, and
|
|
392
|
+
the capture stops at the size sampled once `px` has exited, read by position on POSIX so a
|
|
393
|
+
descendant's later writes cannot overwrite it. Such descendants are not
|
|
394
|
+
stopped: `px` is trusted like any installed executable. Timeout, nonzero-exit and
|
|
395
|
+
launch-error messages are unchanged.
|
|
396
|
+
- **`SECURITY.md` now states the `ingest --graph` trust boundary** (#55). It said tracegraph
|
|
397
|
+
"does not execute graphs" and called a checkpoint database the one input that can run code,
|
|
398
|
+
but `--graph` imports the module you name, running its top-level code, and then runs the
|
|
399
|
+
graph's state callbacks while reading. A new section says what runs (no node does), that
|
|
400
|
+
checkpoint payloads stay under the empty serializer allowlist while the named code itself is
|
|
401
|
+
unsandboxed, and that the graph is never inferred. A table at the top separates JSON parsing,
|
|
402
|
+
hardened checkpoint deserialization and explicitly supplied graph code. The same file no
|
|
403
|
+
longer says piped stdin is read without a size cap: since direct OTLP analysis (#63), `-` goes
|
|
404
|
+
through the same `TRACEGRAPH_MAX_ARTIFACT_BYTES` ceiling as a named file. Documentation only.
|
|
405
|
+
- **`query` no longer keeps every trace and every match in memory** (part of #54). It loaded
|
|
406
|
+
and validated all artifacts, matched them all, and only then sliced the result to `--limit`,
|
|
407
|
+
so `--limit 1` over 200 artifacts of 2000 steps (248 MB on disk) peaked at 1374 MiB. It now
|
|
408
|
+
loads, validates and matches one artifact after another, keeps only the matches it prints
|
|
409
|
+
(and, with `--explain`, the traces they belong to), and builds labels only for those. With
|
|
410
|
+
`--limit`, peak memory grows with the number of artifacts only by a small per-file record:
|
|
411
|
+
70 MiB for the same query. Without `--limit` it holds the matches it prints: 105 MiB. Output, exit codes and error precedence are
|
|
412
|
+
unchanged: every input is still validated, a later file's load error or a duplicate
|
|
413
|
+
trace_id still wins over a matching failure, and the footer's total stays exact. Matches past
|
|
414
|
+
`--limit` are counted but no longer labelled, so a labelling fault there (an internal bug:
|
|
415
|
+
labels look up step ids the matcher just returned) would no longer surface. One trace
|
|
416
|
+
with very many matches still enumerates them all (3.1M matches: 971 MiB, down from 986 MiB
|
|
417
|
+
but 5.4 s instead of 17 s); bounding that needs a decision about the `--limit` total and is
|
|
418
|
+
tracked as #79. An interrupt or `MemoryError` now surfaces at the artifact being read,
|
|
419
|
+
after the skip warnings collected so far, rather than after every file has loaded.
|
|
420
|
+
- **`repeated-agent-failure` no longer takes quadratic time when no names can match** (#52).
|
|
421
|
+
The unbounded two-step matcher it shares with `tool-repeat-failure-heuristic`, and which
|
|
422
|
+
`analyze` runs on every trace, walked and sorted every failing endpoint's whole causal
|
|
423
|
+
history before checking the name back-reference: a 2000-step chain of failing agents with distinct names
|
|
424
|
+
took 1.3s for zero matches, 4x per doubling. It now skips an endpoint no earlier candidate
|
|
425
|
+
can pair with and walks back only as far as the earliest one (2000 steps: 0.001s; a chain
|
|
426
|
+
whose only repeats are disjoint adjacent pairs is linear too). The longest-path pass also stopped rescanning a
|
|
427
|
+
high-fan-out cause once per endpoint. Matches, their order and every report are unchanged.
|
|
428
|
+
|
|
429
|
+
## 0.4.0 - 2026-10-14
|
|
430
|
+
|
|
431
|
+
### Added
|
|
432
|
+
|
|
433
|
+
- **LangGraph tasks paused by `interrupt()` are reported.** A task whose only write on the
|
|
434
|
+
thread's newest root checkpoint is `__interrupt__` — it paused and the history holds no
|
|
435
|
+
resume and no result — becomes an `unset` step with the new source value `interrupt`,
|
|
436
|
+
linked by `CAUSED_BY` to that checkpoint and named by the same task-id reconstruction as
|
|
437
|
+
failed tasks. The step describes the recorded history, not whether LangGraph would resume
|
|
438
|
+
the task now. An interrupt recorded beside a resume or an error and no result is ambiguous
|
|
439
|
+
in the checkpoint (under LangGraph 1.2.2 a resumed task that wrote nothing looks exactly
|
|
440
|
+
like one paused twice; 1.2.15 marks it finished), so it is disclosed at ingest instead. Trace `status` is unchanged: a pause is not an error.
|
|
441
|
+
- **Artifact schema version 5**, stamped by content: only an artifact carrying an
|
|
442
|
+
`interrupt` step declares it, so every other artifact serializes byte for byte as before.
|
|
443
|
+
0.3.1 and earlier refuse a version 5 artifact by name; this build reads versions 1 to 5,
|
|
444
|
+
and still rejects an envelope that declares less than its content needs.
|
|
445
|
+
|
|
446
|
+
### Known limitations
|
|
447
|
+
|
|
448
|
+
- Tasks scheduled but never started — the recursion limit, a killed process, an
|
|
449
|
+
`interrupt_before`/`interrupt_after` breakpoint — leave no write and are still not
|
|
450
|
+
reported. Telling them apart from finished work needs the compiled graph.
|
|
451
|
+
- An interrupt inside a subgraph is reported as the subgraph's node at the root, not as the
|
|
452
|
+
node inside the subgraph that paused.
|
|
453
|
+
|
|
454
|
+
## 0.3.1 - 2026-10-13
|
|
455
|
+
|
|
456
|
+
### Fixed
|
|
457
|
+
|
|
458
|
+
- **An unreadable cost no longer understates the run's total.** The importers dropped a
|
|
459
|
+
`llm.cost.total` that was not a finite non-negative decimal (`"abc"`, `-1`, a boolean), so
|
|
460
|
+
`analyze` summed the remaining costs and printed a total that was knowably too low, with no
|
|
461
|
+
warning. The documented rule that a single unparseable cost withholds the whole total only
|
|
462
|
+
applied to artifacts built by hand. An unreadable total cost is now stored as the fixed
|
|
463
|
+
marker `"unreadable"`, never the producer's value, and the report withholds `total_cost`
|
|
464
|
+
and its delta with the existing note.
|
|
465
|
+
- Token counts, prompt and completion costs, cost currencies and Phoenix evaluation scores
|
|
466
|
+
that cannot be read were dropped without a trace. They are still reported as unavailable,
|
|
467
|
+
and `ingest-otlp`, `ingest-phoenix`, `analyze` and `phoenix diagnose` now say so on stderr,
|
|
468
|
+
naming the kind of value and how many spans (for scores, evaluations) carried it, never
|
|
469
|
+
the value itself. A boolean score still reads as `1.0` or `0.0`, as before.
|
|
470
|
+
- An OTLP `intValue` sent as a fractional JSON number was truncated before validation:
|
|
471
|
+
`1.5` became `1` and `-0.5` became `0`, so an invalid token count looked valid. It is now
|
|
472
|
+
validated as sent.
|
|
473
|
+
- **A checkpoint history LangGraph could not have written is a usage error, not a
|
|
474
|
+
traceback or a different trace.** About half of 64 one-field mutations of a real history
|
|
475
|
+
(a non-object `metadata` or `channel_values`, a numeric `checkpoint_ns`, a boolean `v`, an
|
|
476
|
+
unhashable parent checkpoint id, ...) crashed `ingest` with a traceback, and several were
|
|
477
|
+
read silently as something else: a malformed parent config or `metadata.parents` dropped
|
|
478
|
+
the declared parent and made the checkpoint a root, and `metadata.step` of `true` or `1.5`
|
|
479
|
+
was read as step 1. Each checkpoint is now checked against the types LangGraph declares
|
|
480
|
+
for the fields tracegraph reads, strictly, and rejected with a message naming the field.
|
|
481
|
+
A config `checkpoint_id` that disagrees with the checkpoint's own id is rejected too.
|
|
482
|
+
- A checkpoint row the saver cannot decode (an unknown serialization type, metadata nested
|
|
483
|
+
past the recursion limit, a non-string metadata column) crashed `ingest` and `discover`
|
|
484
|
+
with a traceback, except that `discover` reported an unknown serialization type as "does
|
|
485
|
+
not support listing all threads". Both now report the history as undecodable.
|
|
486
|
+
- Pending writes that are not a `(task id, channel, value)` triple were skipped without a
|
|
487
|
+
word, and any 3-character string was unpacked as one (`"abc"` became task `a`). They are
|
|
488
|
+
still ignored, but `ingest` now says how many on stderr, since one could have carried a
|
|
489
|
+
task failure.
|
|
490
|
+
|
|
491
|
+
### Compatibility notes for imports
|
|
492
|
+
|
|
493
|
+
- An artifact imported from a span with an unreadable total cost now carries
|
|
494
|
+
`"total_cost": "unreadable"` instead of no cost, and its reports withhold the run's
|
|
495
|
+
total cost. No schema change: 0.3.0 reads such an artifact and withholds the same way.
|
|
496
|
+
- Because `intValue` is no longer truncated, `llm.cost.total = {"intValue": 1.5}` is stored as
|
|
497
|
+
`"1.5"` instead of `"1"`, and a `toolgraph.graph_generation` of `{"intValue": 1.5}` is
|
|
498
|
+
rejected like any other invalid generation instead of being read as `1`.
|
|
499
|
+
- The committed Phoenix fixture carries an unreadable `llm.cost.prompt`; reading it now prints
|
|
500
|
+
one stderr disclosure. Every committed fixture's artifact, report, memo, candidate file and
|
|
501
|
+
CLI stdout is unchanged.
|
|
502
|
+
- LangGraph histories that `ingest` used to accept with a malformed field listed above are
|
|
503
|
+
now refused. Histories written by LangGraph's own savers are unaffected: the checks cover
|
|
504
|
+
only fields tracegraph reads (`counters_since_delta_snapshot`, which savers return as
|
|
505
|
+
lists although it is declared as tuples, is not checked), a missing `updated_channels`
|
|
506
|
+
and a legacy `pending_sends` are accepted, and an unknown `metadata.source` string still
|
|
507
|
+
reads as a loop. The committed LangGraph fixture is byte-identical.
|
|
508
|
+
|
|
509
|
+
## 0.3.0 - 2026-10-13
|
|
510
|
+
|
|
511
|
+
### Fixed
|
|
512
|
+
|
|
513
|
+
- An artifact whose content was not canonical (for example a stored trace status that
|
|
514
|
+
disagreed with its steps) was stamped `schema_version` 4 even when it carried no edge
|
|
515
|
+
origins. Version 4 is now stamped only when the file's TREE_PARENT edges are the
|
|
516
|
+
origin-priority projection of its causal edges and that differs from the temporal one; the
|
|
517
|
+
rest of the trace does not affect the stamp. **Digest impact:** canonical artifacts are
|
|
518
|
+
unchanged, but re-serializing a non-canonical trace (one `validate` rejects) whose tree is
|
|
519
|
+
not the origin-priority one, such as a trace with its derived edges missing, now writes
|
|
520
|
+
version 2 or 3 instead of 4. Any digest computed from that re-serialization changes,
|
|
521
|
+
including the `artifact_digest` that `analyze` reports for such a trace.
|
|
522
|
+
- Loading now rejects an envelope that understates its content: a `schema_version` 2 file
|
|
523
|
+
holding an origin-priority projection or derived task steps. Every reader that trusts the
|
|
524
|
+
envelope would have read such a file under the wrong rules, and a re-save rewrote it as a
|
|
525
|
+
different file. An envelope may still overstate what its content needs.
|
|
526
|
+
- `schema_version` must be an integer. `true`, `1.0`, `2.0` and `4.0` were accepted because
|
|
527
|
+
they compare equal, and `true` was migrated as a version 1 file.
|
|
528
|
+
- Evidence floats (`duration_ms`, evaluation `score`) must be finite, and the writer refuses
|
|
529
|
+
to emit `NaN`/`Infinity`, which are not JSON.
|
|
530
|
+
- `--backend ladybug` crashed with a raw traceback on a step `seq` outside the INT64 range
|
|
531
|
+
the backend stores. It now exits with a usage error naming the limit.
|
|
532
|
+
- `InMemoryStore.upsert_edges` accepted a `CAUSED_BY`/`TREE_PARENT` edge whose endpoint was
|
|
533
|
+
not a stored step, or a `BELONGS_TO` edge pointing at another trace; the failure surfaced
|
|
534
|
+
later as a `KeyError` from `ancestors`. It now raises `ValueError` and inserts nothing from
|
|
535
|
+
that batch, matching the LadybugDB store. `WROTE`/`READ` channel edges are unchecked as
|
|
536
|
+
before, and node upsert still replaces by ID.
|
|
537
|
+
- OTLP and Phoenix imports crashed with a raw traceback on a timestamp of `Infinity`, `NaN`
|
|
538
|
+
or a number beyond the representable date range, on a `spanId`/`parentSpanId` given as a
|
|
539
|
+
list, and on Phoenix `annotations` that were not an array. A timestamp that cannot be read
|
|
540
|
+
— including garbage text, which was silently dropped before — is now reported as
|
|
541
|
+
unavailable and disclosed on stderr by `ingest-otlp`, `ingest-phoenix`, `analyze` and
|
|
542
|
+
`phoenix diagnose`. Malformed ids and annotations are rejected with a usage error.
|
|
543
|
+
- An OTLP `status.code` of `true` was read as OK because `true == 1`, and `false` as UNSET
|
|
544
|
+
because `false == 0`; both are now rejected, for OTLP and Phoenix imports alike.
|
|
545
|
+
- LangGraph: two derived task steps could receive the same id (checkpoint `A` with task
|
|
546
|
+
`x#task:y`, checkpoint `A#task:x` with task `y`); the second silently replaced the first
|
|
547
|
+
and gained a second cause it never had. The collision is now rejected like a collision
|
|
548
|
+
with a checkpoint.
|
|
549
|
+
- LangGraph: a failed task that no candidate node reproduces is still reported unnamed, and
|
|
550
|
+
the read now says so on stderr. An unnamed failure is also what a change in LangGraph's
|
|
551
|
+
task-id algorithm would look like, so it is no longer silent.
|
|
552
|
+
|
|
553
|
+
- `diff` and `validate` printed producer text through Rich markup: a step named
|
|
554
|
+
`tool[bold]` showed as `tool`, and an invalid edge naming `ghost[red]X[/red]` showed as
|
|
555
|
+
`ghostX`. Both now print the text literally, as `inspect`, `explain` and `query` already
|
|
556
|
+
did. Output paths are printed literally too: an `--out` path containing `[/]` used to
|
|
557
|
+
raise a markup error after the file had been written, so the command exited 1 for work
|
|
558
|
+
it had completed.
|
|
559
|
+
- `diff --structure` paired any two leftover subtrees because every label is empty under
|
|
560
|
+
that option, then reported them under an empty path (`only in B under > : subtree ·`).
|
|
561
|
+
An empty label is no longer treated as evidence of correspondence, and the reported
|
|
562
|
+
subtrees now show step names so they can be found, while the comparison still ignores
|
|
563
|
+
them. The identical/not-identical verdict and exit code are unchanged.
|
|
564
|
+
- `explain` and `query --explain` labeled a nameless step by its `source`, which for
|
|
565
|
+
OTLP and Phoenix spans is always `loop` and for LangGraph is checkpoint metadata
|
|
566
|
+
(`input`, `loop`), not what the step was. They now use the step kind, like every other
|
|
567
|
+
view.
|
|
568
|
+
- `export-incident-memo` wrote `links_preserved` as `true`/`false`/`null` in its front
|
|
569
|
+
matter but as `True`/`False`/`None` in its summary. Both now use the front-matter form.
|
|
570
|
+
- The Phoenix CLI version-floor message is derived from the floor constant instead of
|
|
571
|
+
repeating it.
|
|
572
|
+
|
|
573
|
+
- `analyze --baseline` published token deltas, and cost deltas, from totals that covered
|
|
574
|
+
only some steps on one side: with tokens on 3 of 3 steps in the baseline and 1 of 3 in
|
|
575
|
+
the current run it reported `-200`, although the silent steps could have used any amount.
|
|
576
|
+
Like the duration delta, these deltas are now withheld with a note unless every step on
|
|
577
|
+
both sides reports the metric. In traces where only model calls carry token counts this
|
|
578
|
+
withholds the token delta. A cost total or delta withheld because steps or runs report
|
|
579
|
+
different or unknown currencies is now disclosed too; before, `analyze` printed
|
|
580
|
+
`cost=unavailable` with no reason.
|
|
581
|
+
|
|
582
|
+
- Writing an artifact or an analysis report left a hidden temporary file behind when the
|
|
583
|
+
write or fsync failed, because the temporary name was recorded only after writing. All
|
|
584
|
+
four output writers now share one helper that removes it on every failure.
|
|
585
|
+
|
|
586
|
+
### Compatibility notes for imports
|
|
587
|
+
|
|
588
|
+
These change what an input that imported successfully before now produces or whether it
|
|
589
|
+
imports at all. No committed fixture is affected.
|
|
590
|
+
|
|
591
|
+
- An OTLP token count given as a boolean (`true`, or `{"intValue": true}`) was stored as `1`;
|
|
592
|
+
it is now unavailable, matching how an unusable cost is already treated. A non-finite
|
|
593
|
+
token count is unavailable too instead of crashing the import. An `intValue` holding a
|
|
594
|
+
boolean is no longer converted to `0`/`1` for any attribute.
|
|
595
|
+
- Raw OTLP input could inject evaluation summaries through the private
|
|
596
|
+
`_tracegraph_evaluations` span key, bypassing the length limit the Phoenix importer
|
|
597
|
+
applies. Only the Phoenix importer reads that key now.
|
|
598
|
+
- An empty-string `traceId` or `spanId` is rejected. A `parentSpanId` of `0`, `false` or
|
|
599
|
+
`[]` used to be read as "no parent", which made a span with a malformed declared parent a
|
|
600
|
+
root; it is now rejected. A missing, `null` or empty-string `parentSpanId` still marks a
|
|
601
|
+
root span.
|
|
602
|
+
- A Phoenix evaluation score too large to represent crashed the import; it is now
|
|
603
|
+
unavailable, like a non-finite score.
|
|
604
|
+
- When several spans carry the `graph.node.parent_id` a span declares (a loop), the importer
|
|
605
|
+
picks the nearest execution that started earlier. A span with a missing or unreadable
|
|
606
|
+
start time sorted as time 0 and could win that choice, so a timestamp nobody recorded
|
|
607
|
+
decided a causal edge. Such spans no longer take part: the choice is treated as
|
|
608
|
+
ambiguous, and the span falls back to its declared `parentSpanId`, or the import is
|
|
609
|
+
rejected if it has none.
|
|
610
|
+
- The CLA workflow refused every pull request from the maintainer's own commit-author
|
|
611
|
+
account, which is not the account that owns the repository. Both are allowlisted now.
|
|
612
|
+
- The release documentation asked for a personal access token with *read* access to pull
|
|
613
|
+
requests. Posting the CLA comment failed with that configuration and succeeded after
|
|
614
|
+
raising it to write; the cause remains unconfirmed, since GitHub documents either Issues
|
|
615
|
+
write or Pull requests write as sufficient. The documentation now asks for write on
|
|
616
|
+
Contents, Issues and Pull requests alike and records the observation rather than
|
|
617
|
+
inferring a rule.
|
|
618
|
+
- It also did not say that a Trusted Publisher's "Environment name" is the GitHub
|
|
619
|
+
environment rather than the index, which failed the first 0.2.0 rehearsal.
|
|
620
|
+
|
|
621
|
+
### Dependencies
|
|
622
|
+
|
|
623
|
+
- The optional `[cypher]` extra now accepts LadybugDB `>=0.18,<0.22` (was `<0.19`), and the
|
|
624
|
+
lockfile moves to 0.21.2. The full test suite and every fixture's output are unchanged
|
|
625
|
+
on 0.21.2.
|
|
626
|
+
- CI now tests against Phoenix CLI 1.18.8 (was 1.8.1). The runtime floor stays 1.0.4: this
|
|
627
|
+
pin is what CI proves, not what tracegraph requires.
|
|
628
|
+
|
|
629
|
+
### Added
|
|
630
|
+
|
|
631
|
+
- `tracegraph export-incident-memo <artifact> -o memo.md` writes a Markdown post-mortem
|
|
632
|
+
intended for long-term memory stores. Its output is a strict allowlist: the run id appears
|
|
633
|
+
only as a truncated SHA-256 `run_digest`, steps appear as `step_<n>_<kind>` aliases, and
|
|
634
|
+
no step name, error message, evaluation label or payload is written. Edges that only show
|
|
635
|
+
containment or have no recorded origin are rendered as "preceded by", never "caused by".
|
|
636
|
+
The memo carries the SHA-256 of the exact artifact bytes it was built from.
|
|
637
|
+
|
|
638
|
+
### Changed
|
|
639
|
+
|
|
640
|
+
- **Artifact schema version 4.** When a step has several causes, its single TREE_PARENT is
|
|
641
|
+
now chosen by edge origin first — a declared graph or checkpoint parent over span
|
|
642
|
+
containment, over a span link, over an unknown origin — and only then by time. An artifact
|
|
643
|
+
whose tree differs from the purely temporal choice is stamped version 4; every other
|
|
644
|
+
artifact serializes as version 2 or 3, byte for byte as before. **Readers from 0.2.0 and
|
|
645
|
+
older refuse version 4 files** with an explicit unsupported-version error. This build reads
|
|
646
|
+
both projections, and the LadybugDB store re-emits a legacy file unchanged. The derived
|
|
647
|
+
tree feeds `inspect`, `diff`, and `analyze --baseline`, where it decides both the topology
|
|
648
|
+
changes and how steps are matched between the two runs: a step whose tree parent moved
|
|
649
|
+
can now appear as one removed and one added behavior change, and its logical step key
|
|
650
|
+
changes. Raw causal queries (`explain`, `query`) are unaffected.
|
|
651
|
+
- Phoenix timestamps without a timezone are now read as UTC rather than in the local
|
|
652
|
+
timezone of the machine running the import. **Digest impact:** artifacts built from such
|
|
653
|
+
exports now hash the same on every machine, and differ from any artifact previously built
|
|
654
|
+
where the local timezone applied a nonzero offset to those timestamps. Timestamps that
|
|
655
|
+
carry a `Z` or an offset are unchanged.
|
|
656
|
+
- **Phoenix artifacts no longer carry free-text annotation names or labels.** The Phoenix
|
|
657
|
+
import is documented as body-free and already kept only identifier-shaped span names, but
|
|
658
|
+
annotation names and labels were kept as free text (whitespace-collapsed, cut to 200
|
|
659
|
+
characters), so an evaluator's note such as `user said: my password is hunter2` reached
|
|
660
|
+
the artifact. They now pass the same identifier filter as span names: an evaluation whose
|
|
661
|
+
name fails it is dropped, and a label that fails it becomes `null` while the score is
|
|
662
|
+
kept. **Artifact content change:** a re-import of a Phoenix export with such annotations
|
|
663
|
+
produces different bytes and digests. Identifier-shaped text, which can still be personal
|
|
664
|
+
data such as an e-mail address, is kept.
|
|
665
|
+
|
|
666
|
+
## 0.2.0 - 2026-09-12
|
|
667
|
+
|
|
668
|
+
First published release. `0.1.0` was tagged but never uploaded.
|
|
669
|
+
|
|
670
|
+
### Fixed
|
|
671
|
+
|
|
672
|
+
- **A LangGraph node that raised an exception was reported as a successful run**
|
|
673
|
+
(`status=ok`, `error_count=0`). LangGraph does not route such a failure through a state
|
|
674
|
+
channel: it records it in the checkpoint's pending writes and writes no further checkpoint,
|
|
675
|
+
so reading channel values alone could not see it and no `--error-channel` value recovered
|
|
676
|
+
it. Each failed task now becomes its own step, hanging off the checkpoint that scheduled it
|
|
677
|
+
rather than marking that checkpoint — which is named after the node that *produced* it, so
|
|
678
|
+
marking it reported the wrong node.
|
|
679
|
+
- A task cancelled because a *sibling* failed is no longer counted as a second failure.
|
|
680
|
+
LangGraph records sibling cancellation through the same `__error__` channel as a genuine
|
|
681
|
+
fault; those tasks are reported as `unset` with no message, so one fault stays one failure
|
|
682
|
+
and a healthy node is never a primary candidate.
|
|
683
|
+
|
|
684
|
+
### Security
|
|
685
|
+
|
|
686
|
+
- **Reading a checkpoint database no longer executes what it contains.** LangGraph's
|
|
687
|
+
deserializer revives stored objects by importing a module and calling a name, both taken
|
|
688
|
+
from the payload, and by default an unrecognized target is logged and then called — so a
|
|
689
|
+
crafted checkpoint naming `os.system` or `subprocess.run` ran on the machine doing the
|
|
690
|
+
analysis (both verified against the pinned dependency). `tracegraph ingest` now passes a
|
|
691
|
+
serializer with an empty allowlist.
|
|
692
|
+
|
|
693
|
+
**Behaviour change:** objects outside the framework's registered types no longer revive
|
|
694
|
+
through `ingest`; they come back as their raw arguments. If you were relying on a custom
|
|
695
|
+
class being reconstructed from checkpoint state, it will now surface as plain data.
|
|
696
|
+
Constructing `LangGraphCheckpointAdapter` yourself is unaffected — the serializer is
|
|
697
|
+
whichever one your saver carries.
|
|
698
|
+
|
|
699
|
+
### Added
|
|
700
|
+
|
|
701
|
+
- The failing node's name, recovered by recomputing LangGraph's task id from checkpoint data.
|
|
702
|
+
The match is self-verifying, so a task nothing identifies keeps no name rather than
|
|
703
|
+
borrowing a neighbour's.
|
|
704
|
+
- `StepSource.TASK`, marking a step derived from pending writes rather than from a checkpoint.
|
|
705
|
+
- `RawTrace.ingest_warnings`, printed by `ingest` to stderr — disclosures about the *read*
|
|
706
|
+
(for example `Send` packets that could not be decoded), which are not part of the run and so
|
|
707
|
+
never enter the artifact.
|
|
708
|
+
- First public release metadata: Apache-2.0 `LICENSE`, `CLA.md`, `CODE_OF_CONDUCT.md`,
|
|
709
|
+
`CONTRIBUTING.md`, `SECURITY.md`, and PyPI classifiers, keywords, authors and project URLs.
|
|
710
|
+
- `tracegraph --version` (`-V`), which prints the bare version and nothing else. There was no
|
|
711
|
+
way to ask an installed build what it was.
|
|
712
|
+
- An sdist allowlist. Previously the source distribution shipped everything git tracked —
|
|
713
|
+
tests, docs, CI workflows, the lockfile and the checkpoint fixtures.
|
|
714
|
+
- A tag-triggered release workflow publishing through PyPI Trusted Publishing: `test-v*`
|
|
715
|
+
rehearses against TestPyPI, `v*` publishes to PyPI. Only the publishing job holds an OIDC
|
|
716
|
+
token, and it checks out nothing, installs nothing, and uploads only files whose digests
|
|
717
|
+
match what the build job recorded. Before any of that, the build refuses a tag whose
|
|
718
|
+
commit is not on the default branch, has no green `tests` run of its own, disagrees with
|
|
719
|
+
the packaged version, or has no dated changelog entry.
|
|
720
|
+
- `scripts/verify_artifacts.py`, which checks that the built distributions are exactly the
|
|
721
|
+
two files that will be uploaded and that the source distribution carries only what the
|
|
722
|
+
packaging allowlist intends. It also refuses tests, docs, CI configuration and the
|
|
723
|
+
lockfile outright, so widening that allowlist cannot quietly start shipping them, and
|
|
724
|
+
refuses any archive member whose path does not go where it reads — a `..` segment is
|
|
725
|
+
what would let a member satisfy both lists and still land somewhere else, or outside
|
|
726
|
+
the archive entirely, when someone extracts it.
|
|
727
|
+
- `scripts/require_ci_success.py`, the release gate described above.
|
|
728
|
+
- A [release runbook](docs/releasing.md) and a
|
|
729
|
+
[public release checklist](docs/public-release-checklist.md).
|
|
730
|
+
- The installed-wheel CI jobs now pin the version they expect and run the artifact
|
|
731
|
+
verifier, so the release checks are exercised on every pull request rather than first at
|
|
732
|
+
tag time.
|
|
733
|
+
|
|
734
|
+
### Changed
|
|
735
|
+
|
|
736
|
+
- **The distribution is now `agent-tracegraph`.** The plain `tracegraph` name on PyPI belongs
|
|
737
|
+
to an unrelated project. The import package, the console script, the schema `kind`
|
|
738
|
+
constants and the `TRACEGRAPH_*` environment variables are **unchanged** — only the name
|
|
739
|
+
you install differs. Installing from source or from a checkout is unaffected.
|
|
740
|
+
- The build backend version is pinned (`hatchling==1.32.0`), so a release rehearsal and the
|
|
741
|
+
production tag cannot run *different backend versions* against the same commit. This is
|
|
742
|
+
drift control, not reproducibility: hatchling's own dependencies still float. The uv
|
|
743
|
+
version used to build is pinned in the same way.
|
|
744
|
+
- `twine check` is a gate again, pinned to `twine@7.0.0`. It was disabled because twine 6
|
|
745
|
+
rejected the `Metadata-Version: 2.5` this backend emits; twine 7 accepts it.
|
|
746
|
+
- **Artifact schema version 3**, stamped *by content*: an artifact carrying derived task steps
|
|
747
|
+
declares 3 so an older reader refuses it by name instead of failing on an enum, while
|
|
748
|
+
artifacts without them still serialize as version 2 byte for byte. The envelope is inside
|
|
749
|
+
the digest `export-review-candidates` binds to, so a blanket bump would have invalidated
|
|
750
|
+
digests already published for unchanged artifacts.
|
|
751
|
+
- The LangGraph capture disclosure in every report now names both the error channel and task
|
|
752
|
+
pending writes. It remains unconditional: an artifact ingested by an older build, or one
|
|
753
|
+
using an unrecognized checkpoint layout, carries no derived step and is indistinguishable
|
|
754
|
+
from a genuinely clean run.
|
|
755
|
+
- `xxhash` is now a direct runtime dependency (LangGraph hashes task ids with it, but
|
|
756
|
+
`langgraph` itself is only a development dependency here).
|
|
757
|
+
- `langgraph-checkpoint>=4.1` is now declared directly rather than left transitive, because
|
|
758
|
+
ingestion calls its serializer API. `allowed_msgpack_modules` does not exist before 4.1
|
|
759
|
+
(4.0.0 raises `TypeError`), so the floor is what makes the hardening above guaranteed
|
|
760
|
+
rather than incidental.
|
|
761
|
+
|
|
762
|
+
### Known limitations
|
|
763
|
+
|
|
764
|
+
- Tasks that were *scheduled but never committed* — a run paused by `interrupt()`, stopped by
|
|
765
|
+
the recursion limit, or killed — are not reported. Trace `status` still means "did a step
|
|
766
|
+
error", so such a run reports `ok`.
|
|
767
|
+
- Only the latest persisted error survives per task, so repeated attempts are not
|
|
768
|
+
reconstructed.
|
|
769
|
+
- `Send` packets can only be named where `langgraph.types` is importable; an ordinary install
|
|
770
|
+
has only the `langgraph.checkpoint` half of that namespace, and the gap is disclosed.
|
|
771
|
+
|
|
772
|
+
## 0.1.0 - 2026-05-30
|
|
773
|
+
|
|
774
|
+
Tagged but never published. Initial read-path causal-graph analysis of LangGraph agent traces.
|