agent-tracegraph 0.6.0__tar.gz → 0.7.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 → agent_tracegraph-0.7.0}/CHANGELOG.md +64 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/PKG-INFO +17 -10
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/README.md +15 -8
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/SECURITY.md +18 -10
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/pyproject.toml +5 -2
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/langgraph_checkpoint.py +16 -5
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/otlp_spans.py +1 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/phoenix_export.py +35 -6
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/artifact.py +1 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/cli.py +19 -3
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/sqlite_snapshot.py +11 -2
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/.gitignore +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/LICENSE +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/contracts/analysis-report.schema.json +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/contracts/check-report.schema.json +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/contracts/phoenix-runtime.json +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/contracts/review-candidates.schema.json +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/__init__.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/_atomic_write.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/_errors.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/__init__.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/base.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/__init__.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/ahu.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/check.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/diagnose.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/explain.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/memo.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/motifs.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/analysis/patterns.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/graph_ref.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/input_validation.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/model.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/normalize.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/py.typed +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/review_candidates.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/store/__init__.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/store/base.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/store/in_memory.py +0 -0
- {agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/store/ladybug.py +0 -0
|
@@ -6,6 +6,70 @@ 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.7.0 - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`--redact-errors` on `ingest-otlp` and `ingest`** (#125) leaves error messages out of the
|
|
14
|
+
artifact. The OTLP and LangGraph adapters record the producer's error text by default
|
|
15
|
+
(`status.message` and exception events; the error channel's value and a raised exception's
|
|
16
|
+
`repr`), and `inspect` and `explain` print it; until now only Phoenix ingestion could drop it.
|
|
17
|
+
With the flag a failed step keeps its `error` status and lists `error_msg` in
|
|
18
|
+
`unknown_fields`, and nothing else in the artifact changes. A rule that reads the message
|
|
19
|
+
therefore treats it as unknown, not absent: `check --rule pattern-not-increased:timeout` reports
|
|
20
|
+
`INSUFFICIENT_EVIDENCE` for a redacted current run, and against a redacted baseline it can still
|
|
21
|
+
pass but not fail. Names are still recorded as given. The default is unchanged. In Python,
|
|
22
|
+
`LangGraphCheckpointAdapter` and `ingest_snapshot` take `include_error_messages=False`, as
|
|
23
|
+
`OTLPSpanAdapter` already did.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`phoenix diagnose` exits 1, not 2, when `px` is unavailable or its answer cannot be used**
|
|
28
|
+
(#122). That covers a `px` that is missing or cannot run, fails, times out, prints too much,
|
|
29
|
+
prints output that is not UTF-8 or not valid JSON, reports no recognizable version or one
|
|
30
|
+
older than the supported floor, or returns an export tracegraph cannot read, and a project
|
|
31
|
+
with no traces. The command now
|
|
32
|
+
prints `FAIL <reason>` on stderr, as `phoenix doctor` does, instead of the usage banner.
|
|
33
|
+
`diagnose` used 2 for these, the same code as an invalid invocation, so a wrapper could not
|
|
34
|
+
tell "called wrong" from "Phoenix is not reachable". Some exports 0.6.1 accepted are now refused
|
|
35
|
+
as unreadable, so `diagnose` goes from 0 to 1 on them; see the Phoenix time fix below. 2 still
|
|
36
|
+
means invalid arguments or output paths, such as `--limit 0`, two outputs naming the same file,
|
|
37
|
+
or a destination that cannot be written. `doctor` is unchanged.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **A Phoenix time given as a number too small to be epoch nanoseconds is no longer read as one**
|
|
42
|
+
(#124). The Phoenix adapter took every numeric `start_time` / `end_time` as nanoseconds, so a
|
|
43
|
+
time in seconds, milliseconds or microseconds became a moment near 1970, with its sub-unit
|
|
44
|
+
digits truncated and no warning: a 500 ms span given in float seconds was recorded at
|
|
45
|
+
`1970-01-01T00:00:01.783987+00:00` with a duration of 0.0. A number is now read as nanoseconds
|
|
46
|
+
only from 10^17 (1973-03-03T09:46:40Z) up. Below that, seconds, milliseconds and microseconds
|
|
47
|
+
for any date before the year 5138 cannot be told apart, so the time is reported as unavailable
|
|
48
|
+
and disclosed as `otlp.unreadable_timestamp`, the code an unreadable time already uses, which
|
|
49
|
+
0.6.x readers know. That applies to numbers, digit strings, and strings `int()` reads with a
|
|
50
|
+
sign, surrounding whitespace or underscores. ISO strings are read as before, including compact
|
|
51
|
+
forms such as `20260714_123456`, and so are nanosecond values. Phoenix's own `trace get` export
|
|
52
|
+
uses ISO strings, so artifacts change only for exports that carried such numbers.
|
|
53
|
+
|
|
54
|
+
For an export that carries such numbers, the step sequence, the parent chosen among several
|
|
55
|
+
spans sharing a `graph.node.id`, and whether the export is accepted at all can also differ from
|
|
56
|
+
0.6.1, which read the numbers as nanoseconds and ordered spans by them. For example, take spans
|
|
57
|
+
`a` and `b` with `graph.node.id` `loop`, starting at 1783987200 and 1783987201, and a span `c`
|
|
58
|
+
starting at 1783987202 with `graph.node.parent_id` `loop` and no `parent_id`. 0.6.1 took `b` as
|
|
59
|
+
`c`'s parent and exited 0; now `ingest-phoenix` and `analyze` exit 2 and `phoenix diagnose`
|
|
60
|
+
exits 1.
|
|
61
|
+
|
|
62
|
+
## 0.6.1 - 2026-10-20
|
|
63
|
+
|
|
64
|
+
### Dependencies
|
|
65
|
+
|
|
66
|
+
- `langgraph-checkpoint-sqlite` now requires `>=3.1.1` (was `>=2`), the first release with the
|
|
67
|
+
fix for GHSA-47pj-3jcm-6whg, where store namespace prefixes could match across segment
|
|
68
|
+
boundaries. Tracegraph reads checkpoints through `SqliteSaver` and never uses the store; the
|
|
69
|
+
floor keeps an install from resolving a flagged version. The source, commands and JSON
|
|
70
|
+
contracts are the same as 0.6.0, but the install range is narrower: an environment that pins
|
|
71
|
+
`langgraph-checkpoint-sqlite==3.1.0` resolves with 0.6.0 and not with 0.6.1.
|
|
72
|
+
|
|
9
73
|
## 0.6.0 - 2026-10-20
|
|
10
74
|
|
|
11
75
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-tracegraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Debuggers
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.12
|
|
22
|
-
Requires-Dist: langgraph-checkpoint-sqlite>=
|
|
22
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=3.1.1
|
|
23
23
|
Requires-Dist: langgraph-checkpoint>=4.1
|
|
24
24
|
Requires-Dist: pydantic<3,>=2
|
|
25
25
|
Requires-Dist: rich>=13
|
|
@@ -201,8 +201,9 @@ trace. If none failed, it says so and diagnoses the newest trace, so a first suc
|
|
|
201
201
|
still useful. Explicit ids bypass that selection. The command invokes only read-only
|
|
202
202
|
`px trace list/get --include-annotations` exports, analyzes them in memory, and does not persist
|
|
203
203
|
the raw Phoenix response. Phoenix connection details and credentials remain owned by `px`
|
|
204
|
-
profiles or environment variables; tracegraph never accepts an API key option.
|
|
205
|
-
|
|
204
|
+
profiles or environment variables; tracegraph never accepts an API key option. Both commands exit
|
|
205
|
+
1 when `px` is missing, cannot run, or returns a response tracegraph cannot use, and 2 for
|
|
206
|
+
invalid arguments or output paths. For an already exported file or a shell pipeline:
|
|
206
207
|
|
|
207
208
|
Automatic retry diagnosis intentionally requires an upstream `retry:` CHAIN marker. SyncMill's
|
|
208
209
|
controlled retry telemetry emits this contract; generic OTLP and LangGraph-checkpoint producers
|
|
@@ -237,8 +238,11 @@ identifiers, and credentials. Missing metrics are reported as `unavailable`, nev
|
|
|
237
238
|
(identifier-shaped span names only, error messages dropped), but the OTLP and LangGraph
|
|
238
239
|
adapters record the span name and error text they were given: OTLP keeps `status.message`
|
|
239
240
|
and exception messages, and LangGraph stringifies the configured error state channel. Those
|
|
240
|
-
values reach the artifact on disk and are printed verbatim by `inspect` and `explain`.
|
|
241
|
-
|
|
241
|
+
values reach the artifact on disk and are printed verbatim by `inspect` and `explain`.
|
|
242
|
+
`ingest-otlp --redact-errors` and `ingest --redact-errors` leave the error text out: a failed
|
|
243
|
+
step keeps its `error` status and lists `error_msg` in `unknown_fields`, so a rule that reads
|
|
244
|
+
the message (the `timeout` preset) reports it as unknown rather than absent. Names are still
|
|
245
|
+
recorded as given. If your producer puts prompt text or secrets in span names, treat artifacts
|
|
242
246
|
from those two adapters as sensitive, or ingest through Phoenix. See "Analysis and output
|
|
243
247
|
guarantees" below.
|
|
244
248
|
|
|
@@ -253,14 +257,16 @@ tracegraph analyze full-dag.json
|
|
|
253
257
|
```
|
|
254
258
|
|
|
255
259
|
`analyze` reads an OTLP span export (JSON or Collector JSONL) directly, with the same adapter
|
|
256
|
-
settings as `ingest-otlp`, so the report is byte-identical to analysing the ingested artifact
|
|
260
|
+
settings as `ingest-otlp`, so the report is byte-identical to analysing the ingested artifact
|
|
261
|
+
(one ingested with `--redact-errors` can differ where a rule reads the message);
|
|
257
262
|
nothing is written unless `--json-out` asks for a report. The input is identified by shape alone
|
|
258
263
|
(`schema_version` → artifact, `resourceSpans` → OTLP, `spans`/`traces`/`data` → Phoenix); a
|
|
259
264
|
payload carrying both OTLP and Phoenix records is refused, naming the explicit ingest command
|
|
260
265
|
for each format (or, for a list, JSONL or envelope, asking you to extract one format's records). Already have an export? The shortest offline first run is
|
|
261
266
|
`tracegraph analyze your-export.otlp.json` (add `--trace <id>` when the file holds several), then
|
|
262
267
|
copy the `explain` command it prints after saving the artifact it names. The artifact caveat
|
|
263
|
-
above still applies: `ingest-otlp` keeps error text
|
|
268
|
+
above still applies: `ingest-otlp` keeps error text unless given `--redact-errors`, and
|
|
269
|
+
reports stay `safe-v1`.
|
|
264
270
|
|
|
265
271
|
The JSON analysis-report contract is versioned at
|
|
266
272
|
[`contracts/analysis-report.schema.json`](contracts/analysis-report.schema.json).
|
|
@@ -391,7 +397,7 @@ Known limits:
|
|
|
391
397
|
- Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
|
|
392
398
|
causes may be missing.
|
|
393
399
|
- Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
|
|
394
|
-
producer's names and error messages
|
|
400
|
+
producer's names, and its error messages unless ingested with `--redact-errors`.
|
|
395
401
|
- CI covers Python 3.12 on Linux.
|
|
396
402
|
|
|
397
403
|
Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
|
|
@@ -437,7 +443,8 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
|
|
|
437
443
|
the report. Trace/step identifiers remain; this is not anonymization.
|
|
438
444
|
Raw artifact names and bytes are not rewritten for report privacy or matching, so the
|
|
439
445
|
artifact — unlike the report — can still carry whatever the producer put in a span name or
|
|
440
|
-
error message
|
|
446
|
+
error message (`--redact-errors` on `ingest` and `ingest-otlp` drops the message, not the
|
|
447
|
+
name). Phoenix ingestion is the exception: it filters at the source, keeping span
|
|
441
448
|
names and annotation names and labels only when they are identifier-shaped, and dropping
|
|
442
449
|
status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
|
|
443
450
|
`export-review-candidates` output is not passed through the report redactor: its `tool_key`
|
|
@@ -170,8 +170,9 @@ trace. If none failed, it says so and diagnoses the newest trace, so a first suc
|
|
|
170
170
|
still useful. Explicit ids bypass that selection. The command invokes only read-only
|
|
171
171
|
`px trace list/get --include-annotations` exports, analyzes them in memory, and does not persist
|
|
172
172
|
the raw Phoenix response. Phoenix connection details and credentials remain owned by `px`
|
|
173
|
-
profiles or environment variables; tracegraph never accepts an API key option.
|
|
174
|
-
|
|
173
|
+
profiles or environment variables; tracegraph never accepts an API key option. Both commands exit
|
|
174
|
+
1 when `px` is missing, cannot run, or returns a response tracegraph cannot use, and 2 for
|
|
175
|
+
invalid arguments or output paths. For an already exported file or a shell pipeline:
|
|
175
176
|
|
|
176
177
|
Automatic retry diagnosis intentionally requires an upstream `retry:` CHAIN marker. SyncMill's
|
|
177
178
|
controlled retry telemetry emits this contract; generic OTLP and LangGraph-checkpoint producers
|
|
@@ -206,8 +207,11 @@ identifiers, and credentials. Missing metrics are reported as `unavailable`, nev
|
|
|
206
207
|
(identifier-shaped span names only, error messages dropped), but the OTLP and LangGraph
|
|
207
208
|
adapters record the span name and error text they were given: OTLP keeps `status.message`
|
|
208
209
|
and exception messages, and LangGraph stringifies the configured error state channel. Those
|
|
209
|
-
values reach the artifact on disk and are printed verbatim by `inspect` and `explain`.
|
|
210
|
-
|
|
210
|
+
values reach the artifact on disk and are printed verbatim by `inspect` and `explain`.
|
|
211
|
+
`ingest-otlp --redact-errors` and `ingest --redact-errors` leave the error text out: a failed
|
|
212
|
+
step keeps its `error` status and lists `error_msg` in `unknown_fields`, so a rule that reads
|
|
213
|
+
the message (the `timeout` preset) reports it as unknown rather than absent. Names are still
|
|
214
|
+
recorded as given. If your producer puts prompt text or secrets in span names, treat artifacts
|
|
211
215
|
from those two adapters as sensitive, or ingest through Phoenix. See "Analysis and output
|
|
212
216
|
guarantees" below.
|
|
213
217
|
|
|
@@ -222,14 +226,16 @@ tracegraph analyze full-dag.json
|
|
|
222
226
|
```
|
|
223
227
|
|
|
224
228
|
`analyze` reads an OTLP span export (JSON or Collector JSONL) directly, with the same adapter
|
|
225
|
-
settings as `ingest-otlp`, so the report is byte-identical to analysing the ingested artifact
|
|
229
|
+
settings as `ingest-otlp`, so the report is byte-identical to analysing the ingested artifact
|
|
230
|
+
(one ingested with `--redact-errors` can differ where a rule reads the message);
|
|
226
231
|
nothing is written unless `--json-out` asks for a report. The input is identified by shape alone
|
|
227
232
|
(`schema_version` → artifact, `resourceSpans` → OTLP, `spans`/`traces`/`data` → Phoenix); a
|
|
228
233
|
payload carrying both OTLP and Phoenix records is refused, naming the explicit ingest command
|
|
229
234
|
for each format (or, for a list, JSONL or envelope, asking you to extract one format's records). Already have an export? The shortest offline first run is
|
|
230
235
|
`tracegraph analyze your-export.otlp.json` (add `--trace <id>` when the file holds several), then
|
|
231
236
|
copy the `explain` command it prints after saving the artifact it names. The artifact caveat
|
|
232
|
-
above still applies: `ingest-otlp` keeps error text
|
|
237
|
+
above still applies: `ingest-otlp` keeps error text unless given `--redact-errors`, and
|
|
238
|
+
reports stay `safe-v1`.
|
|
233
239
|
|
|
234
240
|
The JSON analysis-report contract is versioned at
|
|
235
241
|
[`contracts/analysis-report.schema.json`](contracts/analysis-report.schema.json).
|
|
@@ -360,7 +366,7 @@ Known limits:
|
|
|
360
366
|
- Phoenix exports carry no span links, so their artifacts are parent-only; additional fan-in
|
|
361
367
|
causes may be missing.
|
|
362
368
|
- Report privacy (`safe-v1`) applies to reports; OTLP and LangGraph artifacts keep the
|
|
363
|
-
producer's names and error messages
|
|
369
|
+
producer's names, and its error messages unless ingested with `--redact-errors`.
|
|
364
370
|
- CI covers Python 3.12 on Linux.
|
|
365
371
|
|
|
366
372
|
Caveat for the checkpoint adapter: it is a **checkpoint-level** view (one node per
|
|
@@ -406,7 +412,8 @@ with deterministic SHA-256 aliases, and disclose in `warnings` when an alias act
|
|
|
406
412
|
the report. Trace/step identifiers remain; this is not anonymization.
|
|
407
413
|
Raw artifact names and bytes are not rewritten for report privacy or matching, so the
|
|
408
414
|
artifact — unlike the report — can still carry whatever the producer put in a span name or
|
|
409
|
-
error message
|
|
415
|
+
error message (`--redact-errors` on `ingest` and `ingest-otlp` drops the message, not the
|
|
416
|
+
name). Phoenix ingestion is the exception: it filters at the source, keeping span
|
|
410
417
|
names and annotation names and labels only when they are identifier-shaped, and dropping
|
|
411
418
|
status messages. Identifier-shaped is not the same as anonymous: an e-mail address passes.
|
|
412
419
|
`export-review-candidates` output is not passed through the report redactor: its `tool_key`
|
|
@@ -42,19 +42,23 @@ prompts, inputs and outputs, tool arguments and results, raw errors and stacktra
|
|
|
42
42
|
session/user/project identifiers. Other display text is replaced with a deterministic
|
|
43
43
|
SHA-256 alias, and the report discloses in `warnings` when an alias actually reached it.
|
|
44
44
|
|
|
45
|
-
The artifact on disk is a different thing. Phoenix ingestion filters at the source, but
|
|
46
|
-
OTLP and LangGraph adapters record what they were given:
|
|
45
|
+
The artifact on disk is a different thing. Phoenix ingestion filters at the source, but by
|
|
46
|
+
default the OTLP and LangGraph adapters record what they were given:
|
|
47
47
|
|
|
48
48
|
- OTLP keeps `status.message` and exception messages;
|
|
49
49
|
- LangGraph stringifies the configured error state channel, **and** the `repr` of an
|
|
50
50
|
exception a node raised, which it reads from checkpoint pending writes.
|
|
51
51
|
|
|
52
|
-
Those values reach the artifact and are printed verbatim by `inspect` and `explain
|
|
52
|
+
Those values reach the artifact and are printed verbatim by `inspect` and `explain`, unless
|
|
53
|
+
the trace is ingested with `--redact-errors` (`ingest-otlp` and `ingest` both take it), which
|
|
54
|
+
leaves every error message out and marks it unknown on the step. Span and node names are
|
|
55
|
+
recorded as given either way.
|
|
53
56
|
`export-review-candidates` is likewise not passed through the report redactor: its
|
|
54
57
|
`tool_key` is the raw endpoint span name.
|
|
55
58
|
|
|
56
59
|
**So: if your producer puts prompt text, customer data, or credentials into span names or
|
|
57
|
-
exception messages, those land in the artifact
|
|
60
|
+
exception messages, those land in the artifact** — the messages only when ingested without
|
|
61
|
+
`--redact-errors`, the names always. Treat artifacts with the same care as the
|
|
58
62
|
traces they came from. Reports, not artifacts, are the artifact you share.
|
|
59
63
|
|
|
60
64
|
Identifiers are not anonymized in either form — trace and step ids are preserved by design,
|
|
@@ -64,12 +68,16 @@ because they are what makes a finding traceable back to the run.
|
|
|
64
68
|
|
|
65
69
|
Artifact and span-export loading is a parsing boundary, not a trust boundary: input is
|
|
66
70
|
validated, never executed. Malformed input raises a clean error rather than a traceback, and
|
|
67
|
-
tree walks are iterative so a deeply nested document cannot exhaust the stack. **CLI
|
|
68
|
-
files named on the command line and `-` for stdin
|
|
69
|
-
(`TRACEGRAPH_MAX_ARTIFACT_BYTES` changes it, and
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
tree walks are iterative so a deeply nested document cannot exhaust the stack. **CLI JSON
|
|
72
|
+
inputs** (artifacts and span exports), files named on the command line and `-` for stdin
|
|
73
|
+
alike, are size-capped by default at 256 MiB (`TRACEGRAPH_MAX_ARTIFACT_BYTES` changes it, and
|
|
74
|
+
`0` disables it). A LangGraph checkpoint database (`ingest --sqlite`) is not byte-capped: it
|
|
75
|
+
is copied to a private snapshot under a deadline (30 seconds by default) before it is read.
|
|
76
|
+
The cap is a property of the command-line entry points, not of the library:
|
|
77
|
+
`artifact.load()`, `OTLPSpanAdapter.from_file()`, `PhoenixExportAdapter.from_file()`, and
|
|
78
|
+
`InMemoryStore.load_artifact()` and `LadybugStore.load_artifact()` (which call
|
|
79
|
+
`artifact.load()`) read what they are given, so a library caller handling untrusted input
|
|
80
|
+
should bound it themselves. Still, analyzing an artifact you did not produce means reading
|
|
73
81
|
strings an attacker may have chosen; render them as data.
|
|
74
82
|
|
|
75
83
|
Subprocess use is limited to one call site, the Phoenix CLI (`px`), invoked as an argument
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# the schema `kind` constants and the TRACEGRAPH_* environment variables — those are package
|
|
5
5
|
# identity, not distribution identity, and renaming them would break published contracts.
|
|
6
6
|
name = "agent-tracegraph"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Time-travel debugging and causal RCA for LangGraph agent traces, on a causal graph."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -30,7 +30,10 @@ classifiers = [
|
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
32
32
|
"pydantic>=2,<3",
|
|
33
|
-
|
|
33
|
+
# 3.1.1 fixes GHSA-47pj-3jcm-6whg (store namespace prefixes crossing segment boundaries).
|
|
34
|
+
# Tracegraph reads only SqliteSaver, never the store; the floor keeps installs off the
|
|
35
|
+
# flagged versions.
|
|
36
|
+
"langgraph-checkpoint-sqlite>=3.1.1",
|
|
34
37
|
# Declared directly, not left transitive, because `ingest` now calls its serializer API:
|
|
35
38
|
# `JsonPlusSerializer(allowed_msgpack_modules=...)` is what stops a checkpoint payload
|
|
36
39
|
# invoking arbitrary callables, and it does not exist before 4.1 (4.0.0 raises TypeError,
|
{agent_tracegraph-0.6.0 → agent_tracegraph-0.7.0}/src/tracegraph/adapters/langgraph_checkpoint.py
RENAMED
|
@@ -209,6 +209,7 @@ class LangGraphCheckpointAdapter:
|
|
|
209
209
|
error_channel: str = "error",
|
|
210
210
|
source_kind: str = "langgraph",
|
|
211
211
|
schedule_graph: Any = None,
|
|
212
|
+
include_error_messages: bool = True,
|
|
212
213
|
) -> None:
|
|
213
214
|
self._saver = saver
|
|
214
215
|
self._error_channel = error_channel
|
|
@@ -216,6 +217,9 @@ class LangGraphCheckpointAdapter:
|
|
|
216
217
|
#: A compiled LangGraph graph to ask which tasks are scheduled at the head; see the
|
|
217
218
|
#: module docstring. ``None`` leaves scheduled-but-unwritten tasks unreported.
|
|
218
219
|
self._schedule_graph = schedule_graph
|
|
220
|
+
#: Off, a failed step keeps its ERROR status but not the error channel's value or the
|
|
221
|
+
#: exception text, and lists ``error_msg`` as unknown, as OTLP's option of that name does.
|
|
222
|
+
self._include_error_messages = include_error_messages
|
|
219
223
|
|
|
220
224
|
# --- TraceAdapter ---
|
|
221
225
|
|
|
@@ -388,14 +392,18 @@ class LangGraphCheckpointAdapter:
|
|
|
388
392
|
rewritten_errors += self._rewrote_error(checkpoints[step_id], history)
|
|
389
393
|
if err is not None:
|
|
390
394
|
step.status = StepStatus.ERROR
|
|
391
|
-
|
|
395
|
+
if self._include_error_messages:
|
|
396
|
+
step.error_msg = err
|
|
397
|
+
else:
|
|
398
|
+
step.unknown_fields.insert(0, "error_msg") # sorts before kind and name
|
|
392
399
|
|
|
393
400
|
# Derived task steps come last: everything above indexes `steps` by checkpoint id
|
|
394
401
|
# (`checkpoints[step_id]`, `key_by_step_id[step_id]`), and a derived id has no entry
|
|
395
402
|
# in either. Building them here also means they inherit each carrying step's *final*
|
|
396
403
|
# seq, after `_topo_seq` may have re-sequenced the whole thread.
|
|
397
404
|
disclosures = self._derive_task_steps(
|
|
398
|
-
trace_id, steps, checkpoints, key_by_step_id, original_seq, writes_by_step, edges
|
|
405
|
+
trace_id, steps, checkpoints, key_by_step_id, original_seq, writes_by_step, edges,
|
|
406
|
+
include_error_messages=self._include_error_messages,
|
|
399
407
|
)
|
|
400
408
|
disclosures.extend(
|
|
401
409
|
self._derive_interrupt_steps(
|
|
@@ -463,6 +471,8 @@ class LangGraphCheckpointAdapter:
|
|
|
463
471
|
original_seq: dict[str, int],
|
|
464
472
|
writes_by_step: dict[str, dict[str, list[tuple[str, Any]]]],
|
|
465
473
|
edges: list[Edge],
|
|
474
|
+
*,
|
|
475
|
+
include_error_messages: bool,
|
|
466
476
|
) -> Disclosures:
|
|
467
477
|
"""Add one ERROR step per failed task and return any ingestion disclosures.
|
|
468
478
|
|
|
@@ -506,8 +516,9 @@ class LangGraphCheckpointAdapter:
|
|
|
506
516
|
"in LangGraph's task-id algorithm would look like.",
|
|
507
517
|
)
|
|
508
518
|
unknown: list[UnknownField] = [] if name else ["kind", "name"]
|
|
509
|
-
if error is None and not cancelled:
|
|
510
|
-
# _error_message's placeholder
|
|
519
|
+
if (error is None or not include_error_messages) and not cancelled:
|
|
520
|
+
# _error_message's placeholder (the checkpointer kept no exception text),
|
|
521
|
+
# or a message withheld on request.
|
|
511
522
|
unknown.insert(0, "error_msg")
|
|
512
523
|
derived_id = f"{step_id}#task:{task_id}"
|
|
513
524
|
if derived_id in steps or derived_id in derived_ids:
|
|
@@ -533,7 +544,7 @@ class LangGraphCheckpointAdapter:
|
|
|
533
544
|
# without claiming it failed, and leaves the trace status to the task
|
|
534
545
|
# that actually raised.
|
|
535
546
|
status=StepStatus.UNSET if cancelled else StepStatus.ERROR,
|
|
536
|
-
error_msg=None if cancelled else message,
|
|
547
|
+
error_msg=None if cancelled or not include_error_messages else message,
|
|
537
548
|
unknown_fields=unknown,
|
|
538
549
|
),
|
|
539
550
|
))
|
|
@@ -423,6 +423,7 @@ class OTLPSpanAdapter:
|
|
|
423
423
|
|
|
424
424
|
@classmethod
|
|
425
425
|
def from_file(cls, path: str | Path, **kwargs: Any) -> "OTLPSpanAdapter":
|
|
426
|
+
"""Reads the whole file: the CLI's ``TRACEGRAPH_MAX_ARTIFACT_BYTES`` cap does not apply here."""
|
|
426
427
|
return cls.from_json(Path(path).read_text(encoding="utf-8"), **kwargs)
|
|
427
428
|
|
|
428
429
|
# --- TraceAdapter ---
|
|
@@ -9,7 +9,13 @@ import re
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from tracegraph.adapters.base import Disclosures
|
|
12
|
-
from tracegraph.adapters.otlp_spans import
|
|
12
|
+
from tracegraph.adapters.otlp_spans import (
|
|
13
|
+
OTLPSpanAdapter,
|
|
14
|
+
_attributes,
|
|
15
|
+
_first,
|
|
16
|
+
_parse_nanos,
|
|
17
|
+
_span_array,
|
|
18
|
+
)
|
|
13
19
|
from tracegraph.input_validation import loads, object_field
|
|
14
20
|
from tracegraph.model import RawTrace
|
|
15
21
|
|
|
@@ -30,22 +36,44 @@ _SAFE_ATTRIBUTES = {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
|
|
39
|
+
#: The smallest number read as epoch nanoseconds: 1973-03-03T09:46:40Z. Seconds, milliseconds and
|
|
40
|
+
#: microseconds for any date before the year 5138 all fall below it, so a smaller number is in
|
|
41
|
+
#: some other unit, and which one cannot be told from its size.
|
|
42
|
+
_MIN_EPOCH_NANOS = 10**17
|
|
43
|
+
#: Handed on for a number below the floor. ``int()`` cannot read it, so the span builder reports
|
|
44
|
+
#: the time as unavailable and counts it under ``otlp.unreadable_timestamp``. It is fixed text, so
|
|
45
|
+
#: no producer code (a ``__repr__``) runs, and only the parsed time ever reaches the artifact.
|
|
46
|
+
_NOT_EPOCH_NANOS = "not epoch nanoseconds"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _epoch_nanos(number: int) -> str:
|
|
50
|
+
return str(number) if number >= _MIN_EPOCH_NANOS else _NOT_EPOCH_NANOS
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
def _nanos(value: Any) -> Any:
|
|
34
|
-
"""Phoenix's time to OTLP nanoseconds. A value that cannot be converted
|
|
35
|
-
|
|
54
|
+
"""Phoenix's time to OTLP nanoseconds. A value that cannot be converted, or a number too
|
|
55
|
+
small to be epoch nanoseconds, is handed on in a form the span builder cannot read, so it
|
|
56
|
+
reports the time as unavailable *and* discloses it."""
|
|
36
57
|
if value is None:
|
|
37
58
|
return None
|
|
38
59
|
if isinstance(value, float) and not math.isfinite(value):
|
|
39
60
|
return value
|
|
40
61
|
if isinstance(value, (int, float)):
|
|
41
|
-
return
|
|
62
|
+
return _epoch_nanos(int(value))
|
|
42
63
|
text = str(value)
|
|
43
64
|
if text.isdigit():
|
|
44
|
-
|
|
65
|
+
try:
|
|
66
|
+
return _epoch_nanos(int(text))
|
|
67
|
+
except ValueError: # a non-ASCII digit, or past the interpreter's int-string limit
|
|
68
|
+
return text
|
|
45
69
|
try:
|
|
46
70
|
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
47
71
|
except ValueError:
|
|
48
|
-
|
|
72
|
+
# Whatever the span builder would still read as a time (a sign, whitespace, underscores,
|
|
73
|
+
# bytes) is a number, so the floor applies to it too. Asking the builder's own parser
|
|
74
|
+
# keeps the two from disagreeing about what is a number.
|
|
75
|
+
number = _parse_nanos(value)
|
|
76
|
+
return value if number is None else _epoch_nanos(number)
|
|
49
77
|
if parsed.tzinfo is None:
|
|
50
78
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
51
79
|
return str(int(parsed.timestamp() * 1_000_000_000))
|
|
@@ -151,6 +179,7 @@ class PhoenixExportAdapter:
|
|
|
151
179
|
|
|
152
180
|
@classmethod
|
|
153
181
|
def from_file(cls, path: str | Path) -> "PhoenixExportAdapter":
|
|
182
|
+
"""Reads the whole file: the CLI's ``TRACEGRAPH_MAX_ARTIFACT_BYTES`` cap does not apply here."""
|
|
154
183
|
return cls.from_json(Path(path).read_text(encoding="utf-8"))
|
|
155
184
|
|
|
156
185
|
def discover(self) -> list[str]:
|
|
@@ -243,6 +243,7 @@ def save_atomic(nt: NormalizedTrace, path: str | Path, *, replace: bool = True)
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
def load(path: str | Path) -> NormalizedTrace:
|
|
246
|
+
"""Reads the whole file: the CLI's ``TRACEGRAPH_MAX_ARTIFACT_BYTES`` cap does not apply here."""
|
|
246
247
|
return loads(Path(path).read_text(encoding="utf-8"))
|
|
247
248
|
|
|
248
249
|
|
|
@@ -1280,6 +1280,12 @@ def _render_tree(nt: NormalizedTrace) -> None:
|
|
|
1280
1280
|
)
|
|
1281
1281
|
|
|
1282
1282
|
|
|
1283
|
+
_REDACT_ERRORS_HELP = (
|
|
1284
|
+
"Leave error messages out of the artifact: a failed step keeps its error status and "
|
|
1285
|
+
"lists its message as unknown. Step names are still recorded as given."
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
|
|
1283
1289
|
@app.command()
|
|
1284
1290
|
def ingest(
|
|
1285
1291
|
sqlite: Path = typer.Option(..., help="Path to a LangGraph SqliteSaver DB file."),
|
|
@@ -1300,14 +1306,18 @@ def ingest(
|
|
|
1300
1306
|
"module, and the graph's state callbacks while reading; never a node."
|
|
1301
1307
|
),
|
|
1302
1308
|
),
|
|
1309
|
+
redact_errors: bool = typer.Option(False, "--redact-errors", help=_REDACT_ERRORS_HELP),
|
|
1303
1310
|
) -> None:
|
|
1304
1311
|
"""Ingest a LangGraph thread's checkpoint history into a portable artifact."""
|
|
1305
1312
|
from tracegraph.sqlite_snapshot import ingest_snapshot
|
|
1306
1313
|
|
|
1307
1314
|
target = out or artifact.default_path(thread)
|
|
1308
1315
|
_distinct_paths([sqlite], [target])
|
|
1309
|
-
# Passed only when given, so an ingest without --graph makes exactly the
|
|
1316
|
+
# Passed only when given, so an ingest without --graph or --redact-errors makes exactly the
|
|
1317
|
+
# call it always has.
|
|
1310
1318
|
extra: dict[str, Any] = {}
|
|
1319
|
+
if redact_errors:
|
|
1320
|
+
extra["include_error_messages"] = False
|
|
1311
1321
|
if graph is not None:
|
|
1312
1322
|
from tracegraph.graph_ref import load_graph
|
|
1313
1323
|
|
|
@@ -1328,6 +1338,7 @@ def ingest_otlp(
|
|
|
1328
1338
|
file: str = typer.Option(..., "--file", "-f", help="OTLP JSON/JSONL span export (resourceSpans), or '-' for stdin."),
|
|
1329
1339
|
trace: str = typer.Option(None, "--trace", "-t", help="traceId to ingest (default: the file's only trace)."),
|
|
1330
1340
|
out: Path = typer.Option(None, "--out", "-o", help="Artifact path (default <traceId>.json)."),
|
|
1341
|
+
redact_errors: bool = typer.Option(False, "--redact-errors", help=_REDACT_ERRORS_HELP),
|
|
1331
1342
|
) -> None:
|
|
1332
1343
|
"""Ingest one trace from an OpenInference/OTLP span export into a portable artifact."""
|
|
1333
1344
|
from tracegraph.adapters import OTLPSpanAdapter
|
|
@@ -1336,7 +1347,9 @@ def ingest_otlp(
|
|
|
1336
1347
|
# Bounded read through the common size-limited boundary (from_file would
|
|
1337
1348
|
# read_text the whole file with no ceiling). KeyError joins the usual load
|
|
1338
1349
|
# errors: the adapter raises it for an unknown/missing trace id.
|
|
1339
|
-
adapter = OTLPSpanAdapter.from_json(
|
|
1350
|
+
adapter = OTLPSpanAdapter.from_json(
|
|
1351
|
+
_read_source(file), include_error_messages=not redact_errors
|
|
1352
|
+
)
|
|
1340
1353
|
trace = _select_trace(adapter, trace, what="file")
|
|
1341
1354
|
nt = _normalize_read(adapter.ingest(trace))
|
|
1342
1355
|
except (*_LOAD_ERRORS, KeyError) as exc:
|
|
@@ -1511,7 +1524,10 @@ def phoenix_diagnose(
|
|
|
1511
1524
|
)
|
|
1512
1525
|
baseline_nt = _px_trace(baseline, project=project) if baseline else None
|
|
1513
1526
|
except _PxError as exc:
|
|
1514
|
-
|
|
1527
|
+
# Phoenix or `px` is unavailable or answered unusably: report it as `doctor` does
|
|
1528
|
+
# (exit 1), keeping 2 for an invalid invocation.
|
|
1529
|
+
err_console.print(f"[bold red]FAIL[/] {escape(str(exc))}")
|
|
1530
|
+
raise typer.Exit(1) from exc
|
|
1515
1531
|
report = build_analysis(nt, baseline=baseline_nt)
|
|
1516
1532
|
_render_analysis(report, limit=limit)
|
|
1517
1533
|
if save_artifact:
|
|
@@ -36,7 +36,13 @@ def _inert_serde():
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def ingest_snapshot(
|
|
39
|
-
source: Path,
|
|
39
|
+
source: Path,
|
|
40
|
+
thread: str,
|
|
41
|
+
*,
|
|
42
|
+
error_channel: str,
|
|
43
|
+
timeout: float = 30,
|
|
44
|
+
graph: Any = None,
|
|
45
|
+
include_error_messages: bool = True,
|
|
40
46
|
) -> RawTrace:
|
|
41
47
|
from langgraph.checkpoint.sqlite import SqliteSaver
|
|
42
48
|
|
|
@@ -64,7 +70,10 @@ def ingest_snapshot(
|
|
|
64
70
|
with closing(sqlite3.connect(str(snapshot), check_same_thread=False)) as conn:
|
|
65
71
|
saver = SqliteSaver(conn, serde=_inert_serde())
|
|
66
72
|
return LangGraphCheckpointAdapter(
|
|
67
|
-
saver,
|
|
73
|
+
saver,
|
|
74
|
+
error_channel=error_channel,
|
|
75
|
+
schedule_graph=graph,
|
|
76
|
+
include_error_messages=include_error_messages,
|
|
68
77
|
).ingest(thread)
|
|
69
78
|
except sqlite3.Error as exc:
|
|
70
79
|
raise ValueError("cannot read SQLite checkpoint snapshot") from exc
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|