wizardflow 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {wizardflow-0.1.0 → wizardflow-0.2.0}/.gitignore +5 -3
  2. {wizardflow-0.1.0 → wizardflow-0.2.0}/CONTRIBUTING.md +7 -6
  3. {wizardflow-0.1.0 → wizardflow-0.2.0}/PKG-INFO +147 -61
  4. {wizardflow-0.1.0 → wizardflow-0.2.0}/README.md +146 -60
  5. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/multibranch.html +16 -16
  6. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/multibranch.md +15 -15
  7. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/quickstart.html +10 -10
  8. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/quickstart.md +9 -9
  9. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/quickstart.py +1 -1
  10. {wizardflow-0.1.0 → wizardflow-0.2.0}/pyproject.toml +1 -1
  11. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/__init__.py +4 -0
  12. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/404.html +1 -1
  13. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/__next.__PAGE__.txt +2 -2
  14. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/__next._full.txt +2 -2
  15. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/__next._head.txt +1 -1
  16. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/__next._index.txt +1 -1
  17. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/__next._tree.txt +1 -1
  18. wizardflow-0.1.0/src/wizardflow/_ui/_next/static/chunks/0vq8h3vx9uqg5.js → wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/4158u-399exxn.js +4 -3
  19. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._full.txt +1 -1
  20. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._head.txt +1 -1
  21. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._index.txt +1 -1
  22. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._not-found/__PAGE__.txt +1 -1
  23. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._not-found.txt +1 -1
  24. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found/__next._tree.txt +1 -1
  25. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found.html +1 -1
  26. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_not-found.txt +1 -1
  27. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/index.html +2 -2
  28. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/index.txt +2 -2
  29. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/sitemap.xml +1 -1
  30. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/cli.py +70 -16
  31. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/client.py +108 -66
  32. wizardflow-0.2.0/src/wizardflow/constants.py +55 -0
  33. wizardflow-0.2.0/src/wizardflow/reader.py +134 -0
  34. {wizardflow-0.1.0 → wizardflow-0.2.0}/tests/test_cli.py +69 -15
  35. {wizardflow-0.1.0 → wizardflow-0.2.0}/tests/test_html.py +13 -4
  36. {wizardflow-0.1.0 → wizardflow-0.2.0}/tests/test_markdown.py +13 -4
  37. wizardflow-0.2.0/tests/test_reader.py +154 -0
  38. {wizardflow-0.1.0 → wizardflow-0.2.0}/tests/test_trace.py +81 -32
  39. wizardflow-0.1.0/src/wizardflow/constants.py +0 -44
  40. {wizardflow-0.1.0 → wizardflow-0.2.0}/LICENSE +0 -0
  41. {wizardflow-0.1.0 → wizardflow-0.2.0}/assets/demo.gif +0 -0
  42. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/data_types.py +0 -0
  43. {wizardflow-0.1.0 → wizardflow-0.2.0}/examples/multibranch.py +0 -0
  44. {wizardflow-0.1.0 → wizardflow-0.2.0}/scripts/build_ui.py +0 -0
  45. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_render.py +0 -0
  46. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF}/_buildManifest.js +0 -0
  47. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF}/_clientMiddlewareManifest.js +0 -0
  48. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF}/_ssgManifest.js +0 -0
  49. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/05-c3ty_6dwfk.js +0 -0
  50. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/09s72r58ijxwx.js +0 -0
  51. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/0cz1d0mv5g_q7.js +0 -0
  52. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/0mz_v1wicwnmn.css +0 -0
  53. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/0u1x9l49cgb30.js +0 -0
  54. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/0ux_7aev0a2kt.js +0 -0
  55. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/142r2alz4_x6t.js +0 -0
  56. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/14mrh2-p_w84d.js +0 -0
  57. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/18clj6xsyhc_j.css +0 -0
  58. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/2-j1b74l3lxxu.css +0 -0
  59. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/27jktro2p5rq9.js +0 -0
  60. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/2gj5bmeov-r8f.js +0 -0
  61. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/310vm2bl3xxpt.js +0 -0
  62. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/3n7dm2ojtyzwn.js +0 -0
  63. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/chunks/turbopack-2fblmukzx7kws.js +0 -0
  64. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  65. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  66. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  67. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  68. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  69. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  70. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  71. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  72. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  73. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  74. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  75. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/_next/static/media/icon.3okpzkln1vq00.svg +0 -0
  76. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/icon.svg +0 -0
  77. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/opengraph-image +0 -0
  78. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/_ui/robots.txt +0 -0
  79. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/html.py +0 -0
  80. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/markdown.py +0 -0
  81. {wizardflow-0.1.0 → wizardflow-0.2.0}/src/wizardflow/py.typed +0 -0
  82. {wizardflow-0.1.0 → wizardflow-0.2.0}/tests/conftest.py +0 -0
@@ -10,7 +10,9 @@ venv/
10
10
  # uv resolves this in CI (uv run / uv build); a zero-dependency library doesn't
11
11
  # ship a lockfile, and tracking it would only sweep it into the sdist.
12
12
  uv.lock
13
- # Generated traces from the example scripts (e.g. examples/quickstart.json) —
14
- # regenerated on each run, not tracked. Scoped to examples/ so it never touches
15
- # the embedded UI bundle (src/wizardflow/_ui/), which must ship in the package.
13
+ # Generated traces from the example scripts (e.g. examples/quickstart__*.jsonl)
14
+ # — regenerated on each run, not tracked. Scoped to examples/ so it never
15
+ # touches the embedded UI bundle (src/wizardflow/_ui/), which must ship in the
16
+ # package. (*.json covers traces from pre-JSONL SDK versions.)
16
17
  /examples/*.json
18
+ /examples/*.jsonl
@@ -23,15 +23,16 @@ pytest
23
23
  ```
24
24
 
25
25
  The tests pin the emitted schema shape and the recording semantics (step
26
- folding, completed-only persistence, atomic write, `id=` targeting, unknown-node
27
- fast-fail, …).
26
+ folding, completed-only persistence, append-only durability, `id=` targeting,
27
+ unknown-node fast-fail, …).
28
28
 
29
29
  ## Schema contract
30
30
 
31
- The JSON this SDK serializes to is defined by `src/types/agenttrace.ts` in the
32
- monorepo frontend — that TypeScript type is the schema of record. Change it and
33
- the serializer (`src/wizardflow/client.py`) plus its tests must change in
34
- lockstep; they must not drift.
31
+ The JSONL this SDK serializes to is defined by `src/types/agenttrace.ts` in the
32
+ monorepo frontend — that TypeScript type (including the `header` / `message` /
33
+ `seal` record types) is the schema of record. Change it and the serializer
34
+ (`src/wizardflow/client.py`) plus the reader (`src/wizardflow/reader.py`) and
35
+ their tests must change in lockstep; they must not drift.
35
36
 
36
37
  ## Refreshing the bundled UI
37
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizardflow
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for recording agent flows into the WizardFlow / AgentTrace file format.
5
5
  Project-URL: Homepage, https://getwizardflow.com
6
6
  Project-URL: Documentation, https://getwizardflow.com
@@ -50,15 +50,41 @@ Description-Content-Type: text/markdown
50
50
  **A lightweight tracer for Python agents.** Drop three calls into your code —
51
51
  `init`, `log`, `end_message` — and turn a messy multi-agent run into a portable
52
52
  trace you can replay as an interactive graph or export to Markdown/HTML. Because
53
- the trace is just a JSON file, anyone can replay it: hand it to a teammate or PM
53
+ the trace is just a JSONL file, anyone can replay it: hand it to a teammate or PM
54
54
  and they drop it into **[getwizardflow.com](https://getwizardflow.com)** in the
55
55
  browser — no Python, no install. Lightweight by design: pure Python, **zero
56
56
  runtime dependencies**, no daemon, no setup.
57
57
 
58
58
  ![WizardFlow replaying an agent run](https://raw.githubusercontent.com/lkleonk/wizardflow/main/sdk/python/assets/demo.gif)
59
59
 
60
- The JSON it produces is a small, documented schema: a `graph { nodes, edges }`
61
- plus `messages[] → steps[] → payloads[] { label, value }`.
60
+ The file it produces is JSON Lines with a small, documented schema: line 1 is a
61
+ `header` record carrying the `graph { nodes, edges }`, then one `message`
62
+ record per line (`steps[] → payloads[] { label, value }`). Every line is plain
63
+ JSON — `head -1 run.jsonl | jq .graph` just works.
64
+
65
+ ## Why WizardFlow?
66
+
67
+ Most agent tracing means an observability platform (LangSmith, Langfuse, …): a
68
+ server or SaaS account, an ingestion pipeline, auto-instrumentation, dashboards
69
+ behind a login. WizardFlow takes the opposite bet:
70
+
71
+ - **The trace is a file.** No server, no account, no daemon — your run becomes
72
+ a `.jsonl` you can commit, diff, grep, or attach to a bug report.
73
+ - **Anyone can replay it.** Hand the file to a teammate or PM and they drop it
74
+ into [getwizardflow.com](https://getwizardflow.com) — no Python, no install,
75
+ and nothing is uploaded (the viewer is fully client-side).
76
+ - **Three calls, zero dependencies.** The whole API is `init`, `log`,
77
+ `end_message` — pure Python that pulls nothing into your environment, and no
78
+ framework required (LangGraph support just calls `app.get_graph()` on
79
+ whatever you pass — langgraph itself is never imported).
80
+ - **Explicit by design.** You place every `log()` call yourself, so a trace
81
+ contains exactly what you logged — nothing else. No auto-instrumentation
82
+ capturing things behind your back, no surprise PII in the payloads, no
83
+ guessing why a span exists.
84
+
85
+ If you need fleet-wide monitoring, token-cost dashboards, or eval pipelines,
86
+ use an observability platform. WizardFlow is for **understanding one run** —
87
+ and being able to hand that run to anybody.
62
88
 
63
89
  ## Install
64
90
 
@@ -82,7 +108,8 @@ wizardflow.init(
82
108
  edges=[("user_input", "router"), ("router", "planner")],
83
109
  )
84
110
 
85
- # Every log names its message in the first argument.
111
+ # Every log names its message in the first argument:
112
+ # log(message_id, node, payload_label, payload_value)
86
113
  wizardflow.log("msg-1", "router", "llm_input", prompt) # same node, two payloads ->
87
114
  wizardflow.log("msg-1", "router", "llm_output", output) # folded into one step
88
115
  wizardflow.log("msg-1", "tool_node") # visited, no payloads
@@ -91,14 +118,17 @@ wizardflow.end_message("msg-1") # -> writes the trace
91
118
 
92
119
  There is **no `save()`** and no autosave: `log()` only accumulates in memory,
93
120
  and `end_message(id)` is the one call that writes the trace. Writes happen at
94
- message boundaries — one write per finished message, however many `log()` calls
95
- it contains. `init()` returns a client and also stashes it as the module
96
- default, so the bare `wizardflow.log(...)` form above works.
121
+ message boundaries — one **append** per finished message, however many `log()`
122
+ calls it contains, so a write costs the same no matter how large the trace has
123
+ grown. `init()` returns a client and also stashes it as the module default, so
124
+ the bare `wizardflow.log(...)` form above works.
97
125
 
98
126
  **Concurrency-safe.** Multi-agent setups end messages from many threads/tasks at
99
- once; an internal lock serializes the mutation-and-write so the shared part file
100
- is never corrupted and no message is lost or duplicated. Each write is atomic, so
101
- the file on disk is always a complete, valid trace.
127
+ once; an internal lock serializes the appends so the shared part file is never
128
+ corrupted and no message is lost or duplicated. Every ended message is durable
129
+ on disk the moment `end_message` returns — a crash can at worst tear the line
130
+ being appended, and readers drop a torn final line and load everything before
131
+ it.
102
132
 
103
133
  ## Targeting a message
104
134
 
@@ -107,6 +137,7 @@ collide — interleave them freely (as concurrent agents do) and each step route
107
137
  to the right message:
108
138
 
109
139
  ```python
140
+ # log(message_id, node, payload_label, payload_value)
110
141
  wizardflow.log("msg-1", "classifier", "input", text_a)
111
142
  wizardflow.log("msg-2", "classifier", "input", text_b) # a different message
112
143
  wizardflow.log("msg-1", "generator", "output", answer_a)
@@ -171,71 +202,96 @@ A non-LangGraph object raises `LangGraphExtractionError`; missing conditional
171
202
  metadata never fails extraction (the edge is just emitted plain). Call it
172
203
  **after `compile()`**, when the topology actually exists.
173
204
 
174
- ## API (v0.1)
205
+ ## API (v0.2)
175
206
 
176
- - `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, meta=, silent=False, max_bytes=256_000) -> Client`
207
+ > **Breaking change in 0.2:** traces are now JSON Lines (`.jsonl`), written by
208
+ > appending — the old single-document `.json` format is gone from the SDK
209
+ > (writer *and* CLI readers). The web UI at getwizardflow.com still opens old
210
+ > `.json` traces.
211
+
212
+ - `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, meta=, silent=False, max_bytes=16_000_000, max_messages=2_000) -> Client`
177
213
  - `description` lands in `meta.description` (matches the schema field).
178
214
  - `nodes=` enables fast-fail: `log()` to an undeclared node raises
179
215
  `UnknownNodeError` immediately (unless silenced).
180
216
  - `output_dir` is optional; omitted, traces are written in cwd.
181
217
  - `file_prefix` is optional; omitted, filenames start with `wizardflow`.
182
- - `max_bytes` caps each part file before rotation (see below).
183
- - `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_colors=, silent=False, max_bytes=...) -> Client`
218
+ - `max_bytes` / `max_messages` cap each part file before rotation (see below).
219
+ - `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_colors=, silent=False, max_bytes=..., max_messages=...) -> Client`
184
220
  - same as `init`, but `nodes`/`edges` come from `app.get_graph()`.
185
221
  - `node_colors` maps extracted node ids to CSS colors such as `"#A78BFA"`.
186
222
  - `log(id, node, label=None, content=None)` — the first positional is the
187
223
  **message id**, the second is the node. With `label`/`content` it records a
188
224
  payload; bare `log(id, "node")` records a visit with no payloads. The message
189
225
  is created on first reference; this only accumulates in memory.
190
- - `end_message(id, title=None)` — finalize a message and write the trace;
191
- returns the current trace path. The **only** call that touches disk. Optional `title`
192
- sets the message's human title. Idempotent.
226
+ - `end_message(id, title=None)` — finalize a message and append it to the
227
+ trace; returns the current trace path. The **only** call that touches disk.
228
+ Optional `title` sets the message's human title. Idempotent.
193
229
  - `Client.current_path` — the trace file currently being written.
194
- - `to_dict()` / `to_json()` — inspect the active part (completed messages).
230
+ - `to_dict()` / `to_json()` — inspect the active part (completed messages),
231
+ assembled into one `AgentTraceFile` object.
195
232
 
196
- ### How saving works
233
+ ### The file format
234
+
235
+ A trace part is JSON Lines: one JSON object per line, each with a `type`:
236
+
237
+ ```jsonl
238
+ {"type":"header","version":"0.2","name":"run","meta":{...},"graph":{"nodes":[...],"edges":[...]}}
239
+ {"type":"message","id":"msg-1","label":"First question","steps":[...]}
240
+ {"type":"message","id":"msg-2","steps":[...]}
241
+ {"type":"seal","nextPart":"run__...__part2.jsonl"}
242
+ ```
243
+
244
+ - **`header`** (always line 1) — everything about the run except the messages.
245
+ - **`message`** — one completed message, appended by `end_message`.
246
+ - **`seal`** — only on a part that rotated away; its presence means "this part
247
+ is complete, continue at `nextPart`". The active part has no seal.
197
248
 
198
- `end_message` triggers an **atomic write**: write to `<part>.tmp`, then
199
- `os.replace` over the part file. The file is always a complete, loadable
200
- `AgentTraceFile`. Only **completed** messages are written; an in-progress
249
+ Readers skip records with an unknown `type` (forward compat) and drop an
250
+ unparseable final line (a crash mid-append leaves a torn tail; everything
251
+ before it is intact). Only **completed** messages are written; an in-progress
201
252
  message lives in memory until it ends.
202
253
 
254
+ ### How saving works
255
+
256
+ `end_message` **appends one line** to the active part — O(1) no matter how
257
+ large the part already is, and the moment it returns, that message is durable
258
+ on disk. Nothing is ever rewritten.
259
+
203
260
  ### Rotation (no single huge file)
204
261
 
205
- There's no natural "end" to a chatbot trace, so the SDK caps file size instead.
206
- Each run writes a timestamped entry file whose name carries the run-start time:
262
+ There's no natural "end" to a chatbot trace, so the SDK caps part size instead.
263
+ The cap exists for the *reader*: a part is what you drop into the viewer, and an
264
+ oversized file makes the browser tab sluggish. Each run writes a timestamped
265
+ entry file whose name carries the run-start time:
207
266
 
208
267
  ```
209
- wizardflow__2026-06-08T16-29-09-123Z.json
268
+ wizardflow__2026-06-08T16-29-09-123Z.jsonl
210
269
  ```
211
270
 
212
271
  The name's `wizardflow` is the `file_prefix`; the timestamp is captured when
213
- `init()` creates the client. If the active part would exceed `max_bytes` (~256
214
- KB by default), it's sealed and the next message starts a fresh part:
272
+ `init()` creates the client. If the next message would push the active part past
273
+ `max_bytes` (16 MB by default) — or past `max_messages` (2,000 by default; many
274
+ tiny messages strain the viewer before many bytes do) — the part is sealed and
275
+ the message starts a fresh one:
215
276
 
216
277
  ```
217
- wizardflow__2026-06-08T16-29-09-123Z.json
218
- wizardflow__2026-06-08T16-29-09-123Z__part2.json
219
- wizardflow__2026-06-08T16-29-09-123Z__part3.json
278
+ wizardflow__2026-06-08T16-29-09-123Z.jsonl
279
+ wizardflow__2026-06-08T16-29-09-123Z__part2.jsonl
280
+ wizardflow__2026-06-08T16-29-09-123Z__part3.jsonl
220
281
  ```
221
282
 
222
283
  Rotation only ever happens at a **message boundary**, never mid-message (a lone
223
- message larger than the cap gets its own oversized part). A smaller cap keeps
224
- each rewrite — and the write lock held across it — short, which matters when
225
- many agents end messages concurrently; raise it for fewer files at the cost of
226
- heavier rewrites. `max_bytes` is clamped to a hard ceiling (1 MB) so an
227
- accidental huge value can't stall concurrent writers. Each part is a
228
- **self-contained, valid trace** (full graph + its slice of messages), chained
229
- via `meta`:
230
-
231
- ```json
232
- "meta": { "part": 2, "prevPart": "...Z.json", "nextPart": "...__part3.json" }
233
- ```
234
-
235
- A single-part trace stays clean (no `part` metadata). There's no `partCount` —
236
- the total is genuinely unknown while a continuous run is still logging; follow
237
- `nextPart` to walk to the end. Since names are timestamped, read the real file
238
- back from the client's `current_path` (also the path `end_message` returns).
284
+ message larger than the cap gets its own oversized part). `max_bytes` is
285
+ clamped to a hard ceiling (64 MB) — past that, parsed-object inflation makes
286
+ the viewer slow on ordinary hardware. Each part is **self-contained** (full
287
+ graph in its header + its slice of messages), chained backward via the header's
288
+ `meta.prevPart` and forward via the seal record's `nextPart`.
289
+
290
+ A single-part trace stays clean (no `part` metadata, no seal). There's no
291
+ `partCount` — the total is genuinely unknown while a continuous run is still
292
+ logging; follow the seal records to walk to the end. Since names are
293
+ timestamped, read the real file back from the client's `current_path` (also the
294
+ path `end_message` returns).
239
295
 
240
296
  ### Logging
241
297
 
@@ -269,26 +325,33 @@ Fuller runnable examples live in
269
325
 
270
326
  ## CLI
271
327
 
272
- The `wizardflow` command has three subcommands: `ui`, `md`, and `html`. Every
273
- one takes the trace file as a positional argument **or** via `--path` (pass one,
274
- not both):
328
+ The `wizardflow` command has four subcommands: `ui`, `md`, `html`, and `json`.
329
+ Every one takes the trace file as a positional argument **or** via `--path`
330
+ (pass one, not both):
275
331
 
276
332
  ```bash
277
- wizardflow ui run.json
278
- wizardflow md run.json
279
- wizardflow html run.json
333
+ wizardflow ui run.jsonl
334
+ wizardflow md run.jsonl
335
+ wizardflow html run.jsonl
336
+ wizardflow json run.jsonl
280
337
  # --path is equivalent everywhere:
281
- wizardflow ui --path run.json
338
+ wizardflow ui --path run.jsonl
282
339
  ```
283
340
 
341
+ The SDK only ever **writes** JSONL, but these commands **read** either framing —
342
+ a `.jsonl` part or a single-document `.json` (what `wizardflow json` emits) — so
343
+ they stay at parity with the web viewer, which also accepts both.
344
+
284
345
  ### `wizardflow ui` — local viewer
285
346
 
286
347
  ```bash
287
- wizardflow ui run.json [--host 127.0.0.1] [--port 0] [--no-open]
348
+ wizardflow ui run.jsonl [--host 127.0.0.1] [--port 0] [--no-open]
288
349
  ```
289
350
 
290
351
  Binds a stdlib HTTP server, serves the static WizardFlow UI bundled in the SDK
291
- package, and opens the selected trace in your browser.
352
+ package, and opens the selected trace in your browser (the JSONL is assembled
353
+ server-side and served to the UI as one JSON document, re-read on refresh — so
354
+ you can watch a still-running trace grow).
292
355
 
293
356
  | flag | default | meaning |
294
357
  | --- | --- | --- |
@@ -299,9 +362,9 @@ package, and opens the selected trace in your browser.
299
362
  ### `wizardflow md` — export to Markdown
300
363
 
301
364
  ```bash
302
- wizardflow md run.json # -> stdout
303
- wizardflow md run.json -o run.md # -> file
304
- wizardflow md run.json --no-mermaid # omit the graph diagram
365
+ wizardflow md run.jsonl # -> stdout
366
+ wizardflow md run.jsonl -o run.md # -> file
367
+ wizardflow md run.jsonl --no-mermaid # omit the graph diagram
305
368
  ```
306
369
 
307
370
  Renders the full trace as Markdown: a metadata table, a Mermaid `flowchart` of
@@ -318,8 +381,8 @@ dashed).
318
381
  ### `wizardflow html` — export to HTML
319
382
 
320
383
  ```bash
321
- wizardflow html run.json # -> stdout
322
- wizardflow html run.json -o run.html # -> file
384
+ wizardflow html run.jsonl # -> stdout
385
+ wizardflow html run.jsonl -o run.html # -> file
323
386
  ```
324
387
 
325
388
  Emits a single self-contained document — inline CSS, **no JavaScript, no
@@ -330,6 +393,29 @@ light/dark mode. Messages-only by design: no graph/Mermaid (use `md` for that).
330
393
  | --- | --- | --- |
331
394
  | `-o`, `--output` | — | write to this file instead of stdout |
332
395
 
396
+ ### `wizardflow json` — assemble to one pretty-printed JSON document
397
+
398
+ ```bash
399
+ wizardflow json run.jsonl # -> stdout
400
+ wizardflow json run.jsonl -o run.json # -> file
401
+ ```
402
+
403
+ The inverse of how the SDK writes. JSONL is built for appending, so the raw
404
+ file is one long line per record and not much fun to read. This assembles the
405
+ part — header plus all its message records, with the seal's `nextPart` folded
406
+ into `meta` — into the indented, single-document `AgentTraceFile` shape, for
407
+ eyeballing, diffing in review, or handing someone a canonical JSON. (The web UI
408
+ reads that single-document form too.)
409
+
410
+ | flag | default | meaning |
411
+ | --- | --- | --- |
412
+ | `-o`, `--output` | — | write to this file instead of stdout |
413
+
414
+ Already have `jq`? You don't need this for a quick look — `jq . run.jsonl`
415
+ pretty-prints every record, and `jq 'select(.type=="message")' run.jsonl` just
416
+ the messages. `wizardflow json` differs in that it *assembles* the part into one
417
+ document (and walks no external tool).
418
+
333
419
  Rendered samples (`*.md`, `*.html`) live in the [repo's
334
420
  `examples/`](https://github.com/lkleonk/wizardflow/tree/main/sdk/python/examples).
335
421