wizardflow 0.1.0__tar.gz → 0.3.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 (92) hide show
  1. {wizardflow-0.1.0 → wizardflow-0.3.0}/.gitignore +5 -3
  2. {wizardflow-0.1.0 → wizardflow-0.3.0}/CONTRIBUTING.md +7 -6
  3. wizardflow-0.3.0/PKG-INFO +530 -0
  4. wizardflow-0.3.0/README.md +483 -0
  5. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.html +16 -16
  6. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.md +15 -15
  7. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.html +10 -10
  8. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.md +9 -9
  9. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.py +1 -1
  10. {wizardflow-0.1.0 → wizardflow-0.3.0}/pyproject.toml +1 -1
  11. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/__init__.py +24 -0
  12. wizardflow-0.3.0/src/wizardflow/_ui/404.html +25 -0
  13. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next.__PAGE__.txt +3 -3
  14. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._full.txt +4 -4
  15. wizardflow-0.3.0/src/wizardflow/_ui/__next._head.txt +6 -0
  16. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._index.txt +1 -1
  17. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._tree.txt +2 -2
  18. wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/2q_qu9w0vej6s.js +115 -0
  19. wizardflow-0.1.0/src/wizardflow/_ui/_next/static/chunks/0mz_v1wicwnmn.css → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/3lt2ss7dftc2n.css +1 -1
  20. wizardflow-0.3.0/src/wizardflow/_ui/_next/static/media/icon.09qublg69ek7b.svg +34 -0
  21. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._full.txt +2 -2
  22. wizardflow-0.3.0/src/wizardflow/_ui/_not-found/__next._head.txt +6 -0
  23. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._index.txt +1 -1
  24. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._not-found/__PAGE__.txt +1 -1
  25. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._not-found.txt +1 -1
  26. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._tree.txt +1 -1
  27. wizardflow-0.3.0/src/wizardflow/_ui/_not-found.html +25 -0
  28. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found.txt +2 -2
  29. wizardflow-0.3.0/src/wizardflow/_ui/icon.svg +34 -0
  30. wizardflow-0.3.0/src/wizardflow/_ui/index.html +25 -0
  31. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/index.txt +4 -4
  32. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/sitemap.xml +1 -1
  33. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/cli.py +70 -16
  34. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/client.py +231 -78
  35. wizardflow-0.3.0/src/wizardflow/constants.py +55 -0
  36. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/html.py +25 -0
  37. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/markdown.py +23 -3
  38. wizardflow-0.3.0/src/wizardflow/reader.py +134 -0
  39. {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_cli.py +69 -15
  40. {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_html.py +30 -4
  41. {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_markdown.py +40 -4
  42. wizardflow-0.3.0/tests/test_reader.py +154 -0
  43. {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_trace.py +331 -32
  44. wizardflow-0.1.0/PKG-INFO +0 -340
  45. wizardflow-0.1.0/README.md +0 -293
  46. wizardflow-0.1.0/src/wizardflow/_ui/404.html +0 -25
  47. wizardflow-0.1.0/src/wizardflow/_ui/__next._head.txt +0 -6
  48. wizardflow-0.1.0/src/wizardflow/_ui/_next/static/chunks/0vq8h3vx9uqg5.js +0 -114
  49. wizardflow-0.1.0/src/wizardflow/_ui/_next/static/media/icon.3okpzkln1vq00.svg +0 -19
  50. wizardflow-0.1.0/src/wizardflow/_ui/_not-found/__next._head.txt +0 -6
  51. wizardflow-0.1.0/src/wizardflow/_ui/_not-found.html +0 -25
  52. wizardflow-0.1.0/src/wizardflow/_ui/icon.svg +0 -19
  53. wizardflow-0.1.0/src/wizardflow/_ui/index.html +0 -25
  54. wizardflow-0.1.0/src/wizardflow/constants.py +0 -44
  55. {wizardflow-0.1.0 → wizardflow-0.3.0}/LICENSE +0 -0
  56. {wizardflow-0.1.0 → wizardflow-0.3.0}/assets/demo.gif +0 -0
  57. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/data_types.py +0 -0
  58. {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.py +0 -0
  59. {wizardflow-0.1.0 → wizardflow-0.3.0}/scripts/build_ui.py +0 -0
  60. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_render.py +0 -0
  61. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_buildManifest.js +0 -0
  62. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_clientMiddlewareManifest.js +0 -0
  63. {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_ssgManifest.js +0 -0
  64. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/05-c3ty_6dwfk.js +0 -0
  65. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/09s72r58ijxwx.js +0 -0
  66. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0cz1d0mv5g_q7.js +0 -0
  67. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0u1x9l49cgb30.js +0 -0
  68. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0ux_7aev0a2kt.js +0 -0
  69. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/142r2alz4_x6t.js +0 -0
  70. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/14mrh2-p_w84d.js +0 -0
  71. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/18clj6xsyhc_j.css +0 -0
  72. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/2-j1b74l3lxxu.css +0 -0
  73. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/27jktro2p5rq9.js +0 -0
  74. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/2gj5bmeov-r8f.js +0 -0
  75. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/310vm2bl3xxpt.js +0 -0
  76. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/3n7dm2ojtyzwn.js +0 -0
  77. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/turbopack-2fblmukzx7kws.js +0 -0
  78. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  79. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  80. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  81. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  82. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  83. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  84. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  85. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  86. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  87. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  88. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  89. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/opengraph-image +0 -0
  90. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/robots.txt +0 -0
  91. {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/py.typed +0 -0
  92. {wizardflow-0.1.0 → wizardflow-0.3.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
 
@@ -0,0 +1,530 @@
1
+ Metadata-Version: 2.4
2
+ Name: wizardflow
3
+ Version: 0.3.0
4
+ Summary: Python SDK for recording agent flows into the WizardFlow / AgentTrace file format.
5
+ Project-URL: Homepage, https://getwizardflow.com
6
+ Project-URL: Documentation, https://getwizardflow.com
7
+ Author: Leon Koch
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Leon Koch
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: agents,langgraph,llm,observability,tracing
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Classifier: Typing :: Typed
43
+ Requires-Python: >=3.9
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest>=7; extra == 'dev'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # WizardFlow Python SDK
49
+
50
+ **A lightweight tracer for Python agents.** Drop three calls into your code —
51
+ `init`, `log`, `end_message` — and turn a messy multi-agent run into a portable
52
+ trace you can replay as an interactive graph or export to Markdown/HTML. Because
53
+ the trace is just a JSONL file, anyone can replay it: hand it to a teammate or PM
54
+ and they drop it into **[getwizardflow.com](https://getwizardflow.com)** in the
55
+ browser — no Python, no install. Lightweight by design: pure Python, **zero
56
+ runtime dependencies**, no daemon, no setup.
57
+
58
+ ![WizardFlow replaying an agent run](https://raw.githubusercontent.com/lkleonk/wizardflow/main/sdk/python/assets/demo.gif)
59
+
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.
88
+
89
+ ## Install
90
+
91
+ ```bash
92
+ pip install wizardflow
93
+ ```
94
+
95
+ No runtime dependencies. Developing the SDK itself? See
96
+ [CONTRIBUTING.md](https://github.com/lkleonk/wizardflow/blob/main/sdk/python/CONTRIBUTING.md).
97
+
98
+ ## Quickstart
99
+
100
+ ```python
101
+ import uuid
102
+
103
+ import wizardflow
104
+
105
+ wizardflow.init(
106
+ output_dir="traces", # where trace files are written
107
+ file_prefix="run", # optional; defaults to "wizardflow"
108
+ description="A small router-based agent run.",
109
+ nodes=["user_input", "router", "planner", "tool_node", "final_response"],
110
+ edges=[("user_input", "router"), ("router", "planner")],
111
+ )
112
+
113
+ # Every log names its message in the first argument:
114
+ # log(message_id, node, payload_label, payload_value)
115
+ msg_id = str(uuid.uuid4()) # one fresh id per message
116
+ wizardflow.log(msg_id, "router", "llm_input", prompt) # same node, two payloads ->
117
+ wizardflow.log(msg_id, "router", "llm_output", output) # folded into one step
118
+ wizardflow.log(msg_id, "tool_node") # visited, no payloads
119
+ wizardflow.end_message(msg_id) # -> writes the trace
120
+ ```
121
+
122
+ There is **no `save()`** and no autosave: `log()` only accumulates in memory,
123
+ and `end_message(id)` is the one call that writes the trace. Writes happen at
124
+ message boundaries — one **append** per finished message, however many `log()`
125
+ calls it contains, so a write costs the same no matter how large the trace has
126
+ grown. `init()` returns a client and also stashes it as the module default, so
127
+ the bare `wizardflow.log(...)` form above works.
128
+
129
+ **Concurrency-safe.** Multi-agent setups end messages from many threads/tasks at
130
+ once; an internal lock serializes the appends so the shared part file is never
131
+ corrupted and no message is lost or duplicated. Every ended message is durable
132
+ on disk the moment `end_message` returns — a crash can at worst tear the line
133
+ being appended, and readers drop a torn final line and load everything before
134
+ it.
135
+
136
+ ## Targeting a message
137
+
138
+ The first argument to `log` is the message id, so overlapping messages never
139
+ collide — interleave them freely (as concurrent agents do) and each step routes
140
+ to the right message:
141
+
142
+ ```python
143
+ # log(message_id, node, payload_label, payload_value)
144
+ wizardflow.log("msg-1", "classifier", "input", text_a)
145
+ wizardflow.log("msg-2", "classifier", "input", text_b) # a different message
146
+ wizardflow.log("msg-1", "generator", "output", answer_a)
147
+ wizardflow.end_message("msg-1") # writes msg-1
148
+ wizardflow.end_message("msg-2") # writes msg-2
149
+ ```
150
+
151
+ Pass the **string `id`**, never a handle object — safe to hand to a callback or
152
+ across threads. A message is created on first reference and finalized by
153
+ `end_message`; `end_message(id, title="...")` optionally gives it a human title.
154
+
155
+ ### Choosing message ids
156
+
157
+ A message is **one unit of work** — one user turn, one run through the graph.
158
+ Its id only has to be unique within the run, and it **cannot be reused** once
159
+ `end_message` has been called on it. The simplest safe choice is a fresh UUID
160
+ per message:
161
+
162
+ ```python
163
+ import uuid
164
+
165
+ msg_id = str(uuid.uuid4())
166
+ ```
167
+
168
+ Do **not** use a session id, user id, or thread id as the message id. Those
169
+ identify a *conversation*, and a conversation contains many messages — after
170
+ the first `end_message(session_id)`, every further `log(session_id, ...)` in
171
+ that session raises, because that "message" has already ended. If you want the
172
+ session visible in the trace, put it in `init(meta=...)` or in the message
173
+ title (`end_message(msg_id, title=...)`) — never in the id. Ids are opaque:
174
+ the viewer only displays and groups by them, so encoding meaning into them
175
+ buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
176
+ is the right default for real applications.
177
+
178
+ ## One flow, or several at once
179
+
180
+ `init()` installs the client it returns as the module default. With a single
181
+ flow — the common case — **ignore the return value** and call
182
+ `wizardflow.log(...)` / `wizardflow.end_message(...)` directly: they work from
183
+ any module, with no handle to thread through your code.
184
+
185
+ Running several agent flows side by side (say, a doctor flow and a patient
186
+ flow) means several traces, and each needs its own instance. Keep the returned
187
+ client per flow and log through it — and name it **`tracer`**, not `client`,
188
+ since `client` in an agent codebase is almost always the LLM client:
189
+
190
+ ```python
191
+ doctor_tracer = wizardflow.init(file_prefix="doctor", nodes=[...], edges=[...])
192
+ patient_tracer = wizardflow.init(file_prefix="patient", nodes=[...], edges=[...])
193
+
194
+ doctor_tracer.log(doc_msg, "diagnose", "llm_input", prompt)
195
+ patient_tracer.log(pat_msg, "intake", "llm_input", prompt)
196
+ ```
197
+
198
+ With more than one tracer, don't mix in the bare module-level calls:
199
+ `wizardflow.log(...)` always targets the most recently initialized tracer.
200
+
201
+ ## Node descriptions, labels & colors
202
+
203
+ Nodes can carry a short human description of what they do. In the viewer it
204
+ stays out of the way: when a node is selected, a small info icon appears next
205
+ to its name in the inspector — click it to read the description. Nodes without
206
+ one show no icon, and `wizardflow md` / `wizardflow html` list the described
207
+ nodes under the graph.
208
+
209
+ ```python
210
+ wizardflow.init(
211
+ nodes=["router", "retriever", "generator"],
212
+ edges=[("router", "retriever"), ("retriever", "generator")],
213
+ node_descriptions={
214
+ "router": "Chooses the next step.",
215
+ "retriever": "Fetches relevant documents.",
216
+ "generator": "Writes the final answer.",
217
+ },
218
+ )
219
+ ```
220
+
221
+ Two sibling kwargs work the same way: `node_labels` maps ids to the display
222
+ name the viewer shows instead of the raw id (especially useful with
223
+ `init_from_langgraph`, where the extracted ids are your function names), and
224
+ `node_colors` sets accent colors. All three are validated against the declared
225
+ nodes: an unknown id raises a `WizardFlowError` at `init()` time so typos fail
226
+ fast; with `silent=True` it is logged as a warning on the `wizardflow` logger
227
+ and skipped instead. All three also work on `init_from_langgraph`, where they
228
+ attach to the extracted node ids — `log()` always targets the **id**, never
229
+ the label.
230
+
231
+ ## LangGraph: automatic topology
232
+
233
+ Instead of listing `nodes`/`edges` by hand, read them straight from a compiled
234
+ LangGraph app:
235
+
236
+ ```python
237
+ app = workflow.compile(checkpointer=memory)
238
+
239
+ wizardflow.init_from_langgraph(app, output_dir="traces", file_prefix="trace")
240
+
241
+ wizardflow.log("msg-1", "planner", "Input", state) # runtime logging unchanged
242
+ wizardflow.end_message("msg-1")
243
+ ```
244
+
245
+ You can keep the extracted LangGraph topology and still choose node accent
246
+ colors for important nodes:
247
+
248
+ ```python
249
+ wizardflow.init_from_langgraph(
250
+ app,
251
+ output_dir="traces",
252
+ file_prefix="trace",
253
+ node_colors={
254
+ "router": "#A78BFA",
255
+ "retriever": "#22D3EE",
256
+ "generator": "#60A5FA",
257
+ },
258
+ )
259
+ ```
260
+
261
+ By default, a color key that does not match an extracted node id raises a
262
+ `WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys
263
+ are skipped and logged as a warning instead.
264
+
265
+ It extracts node ids (keeping `__start__` / `__end__`) and directed edges, and
266
+ marks runtime branches with `"conditional": true` (deterministic and parallel
267
+ fan-out edges stay plain):
268
+
269
+ ```json
270
+ {
271
+ "edges": [
272
+ { "source": "__start__", "target": "router" },
273
+ { "source": "router", "target": "planner", "conditional": true },
274
+ { "source": "planner", "target": "final_response" }
275
+ ]
276
+ }
277
+ ```
278
+
279
+ LangGraph is **not** a dependency — extraction is duck-typed on `app.get_graph()`.
280
+ A non-LangGraph object raises `LangGraphExtractionError`; missing conditional
281
+ metadata never fails extraction (the edge is just emitted plain). Call it
282
+ **after `compile()`**, when the topology actually exists.
283
+
284
+ ## API
285
+
286
+ - `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, node_labels=, node_colors=, node_descriptions=, meta=, silent=False, max_bytes=16_000_000, max_messages=2_000) -> Client`
287
+ - `description` lands in `meta.description` (matches the schema field).
288
+ - `nodes=` enables fast-fail: `log()` to an undeclared node raises
289
+ `UnknownNodeError` immediately (unless silenced).
290
+ - `node_labels` / `node_colors` / `node_descriptions` map declared node ids
291
+ to a display name / CSS color / short description; an unknown id raises
292
+ unless silenced (then it's logged as a warning and skipped).
293
+ - `output_dir` is optional; omitted, traces are written in cwd.
294
+ - `file_prefix` is optional; omitted, filenames start with `wizardflow`.
295
+ - `max_bytes` / `max_messages` cap each part file before rotation (see below).
296
+ - `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_labels=, node_colors=, node_descriptions=, silent=False, max_bytes=..., max_messages=...) -> Client`
297
+ - same as `init`, but `nodes`/`edges` come from `app.get_graph()`.
298
+ - `node_labels` renames extracted ids for display (they're your function
299
+ names); `node_colors` maps them to CSS colors such as `"#A78BFA"`;
300
+ `node_descriptions` to short descriptions.
301
+ - `log(id, node, label=None, content=None)` — the first positional is the
302
+ **message id**, the second is the node. With `label`/`content` it records a
303
+ payload; bare `log(id, "node")` records a visit with no payloads. The message
304
+ is created on first reference; this only accumulates in memory.
305
+ - `end_message(id, title=None)` — finalize a message and append it to the
306
+ trace; returns the current trace path. The **only** call that touches disk.
307
+ Optional `title` sets the message's human title. Idempotent.
308
+ - `reinit(name=None, description=None, meta=None)` — start a **new trace
309
+ file** (fresh timestamped name), keeping the graph and output configuration.
310
+ Use it at natural boundaries of a long-lived process — a new user session, a
311
+ new day. The old file is left as-is (**no seal**: it's a finished run, not a
312
+ rotated part). Open messages carry over and are written wherever they end;
313
+ completed message ids become reusable. `name`/`description`/`meta` replace
314
+ the current values when given. Returns the new trace path.
315
+ - `Client.current_path` — the trace file currently being written.
316
+ - `to_dict()` / `to_json()` — inspect the active part (completed messages),
317
+ assembled into one `AgentTraceFile` object.
318
+
319
+ ### The file format
320
+
321
+ A trace part is JSON Lines: one JSON object per line, each with a `type`:
322
+
323
+ ```jsonl
324
+ {"type":"header","version":"0.2","name":"run","meta":{...},"graph":{"nodes":[...],"edges":[...]}}
325
+ {"type":"message","id":"msg-1","label":"First question","steps":[...]}
326
+ {"type":"message","id":"msg-2","steps":[...]}
327
+ {"type":"seal","nextPart":"run__...__part2.jsonl"}
328
+ ```
329
+
330
+ - **`header`** (always line 1) — everything about the run except the messages.
331
+ - **`message`** — one completed message, appended by `end_message`.
332
+ - **`seal`** — only on a part that rotated away; its presence means "this part
333
+ is complete, continue at `nextPart`". The active part has no seal.
334
+
335
+ Readers skip records with an unknown `type` (forward compat) and drop an
336
+ unparseable final line (a crash mid-append leaves a torn tail; everything
337
+ before it is intact). Only **completed** messages are written; an in-progress
338
+ message lives in memory until it ends.
339
+
340
+ > Early SDK versions (0.1) wrote a trace as one single-document `.json` file
341
+ > instead. Newer versions write JSONL only, but every reader — the CLI
342
+ > subcommands and the web UI at getwizardflow.com — still opens both formats.
343
+
344
+ ### How saving works
345
+
346
+ `end_message` **appends one line** to the active part — O(1) no matter how
347
+ large the part already is, and the moment it returns, that message is durable
348
+ on disk. Nothing is ever rewritten.
349
+
350
+ ### Rotation (no single huge file)
351
+
352
+ There's no natural "end" to a chatbot trace, so the SDK caps part size instead.
353
+ The cap exists for the *reader*: a part is what you drop into the viewer, and an
354
+ oversized file makes the browser tab sluggish. Each run writes a timestamped
355
+ entry file whose name carries the run-start time:
356
+
357
+ ```
358
+ wizardflow__2026-06-08T16-29-09-123Z.jsonl
359
+ ```
360
+
361
+ The name's `wizardflow` is the `file_prefix`; the timestamp is captured when
362
+ `init()` creates the client. If the next message would push the active part past
363
+ `max_bytes` (16 MB by default) — or past `max_messages` (2,000 by default; many
364
+ tiny messages strain the viewer before many bytes do) — the part is sealed and
365
+ the message starts a fresh one:
366
+
367
+ ```
368
+ wizardflow__2026-06-08T16-29-09-123Z.jsonl
369
+ wizardflow__2026-06-08T16-29-09-123Z__part2.jsonl
370
+ wizardflow__2026-06-08T16-29-09-123Z__part3.jsonl
371
+ ```
372
+
373
+ Rotation only ever happens at a **message boundary**, never mid-message (a lone
374
+ message larger than the cap gets its own oversized part). `max_bytes` is
375
+ clamped to a hard ceiling (64 MB) — past that, parsed-object inflation makes
376
+ the viewer slow on ordinary hardware. Each part is **self-contained** (full
377
+ graph in its header + its slice of messages), chained backward via the header's
378
+ `meta.prevPart` and forward via the seal record's `nextPart`.
379
+
380
+ A single-part trace stays clean (no `part` metadata, no seal). There's no
381
+ `partCount` — the total is genuinely unknown while a continuous run is still
382
+ logging; follow the seal records to walk to the end. Since names are
383
+ timestamped, read the real file back from the client's `current_path` (also the
384
+ path `end_message` returns).
385
+
386
+ ### Logging
387
+
388
+ Rotation emits an `INFO` notice on the `wizardflow` logger. Following library
389
+ convention, the SDK attaches a `NullHandler` and configures nothing — you see
390
+ nothing unless you opt in:
391
+
392
+ ```python
393
+ import logging
394
+ logging.getLogger("wizardflow").setLevel(logging.INFO)
395
+ ```
396
+
397
+ This is separate from `silent=` (which governs raise-vs-swallow for *errors*).
398
+
399
+ ### Step folding
400
+
401
+ Consecutive `log()` calls to the **same** node within a message fold into a
402
+ single step with multiple payloads (e.g. a router step carrying both
403
+ `llm_input` and `llm_output`). A `log()` to a different node starts a new step.
404
+
405
+ ## What to log (conventions)
406
+
407
+ Payload labels are free-form strings — the schema reserves none of them. These
408
+ conventions just keep traces consistent and readable:
409
+
410
+ - **LLM-backed nodes** — log the exact prompt as `llm_input` and the raw
411
+ completion as `llm_output`. When a run goes wrong, the trace then shows
412
+ precisely what the model saw and what it said — most of what you need to
413
+ debug an agent — and step folding merges the pair into one step.
414
+ - **Everything else** — log the node's real domain data under honest labels: a
415
+ retriever logs its `query` and `results`, a tool node `tool_input` and
416
+ `tool_output`, a deterministic router its `decision`. Don't force
417
+ `llm_input`/`llm_output` onto nodes that never call a model.
418
+
419
+ ## Examples
420
+
421
+ Fuller runnable examples live in
422
+ [`examples/`](https://github.com/lkleonk/wizardflow/tree/main/sdk/python/examples):
423
+
424
+ - **`quickstart.py`** — a small linear agent; two messages, the second finalized
425
+ with an `end_message(..., title=...)` title.
426
+ - **`multibranch.py`** — `router` fans out into a planner/tool path and a
427
+ retriever path that rejoin at `generator`. Two messages take different
428
+ branches, so each logs only the nodes it actually visited.
429
+
430
+ ## CLI
431
+
432
+ The `wizardflow` command has four subcommands: `ui`, `md`, `html`, and `json`.
433
+ Every one takes the trace file as a positional argument **or** via `--path`
434
+ (pass one, not both):
435
+
436
+ ```bash
437
+ wizardflow ui run.jsonl
438
+ wizardflow md run.jsonl
439
+ wizardflow html run.jsonl
440
+ wizardflow json run.jsonl
441
+ # --path is equivalent everywhere:
442
+ wizardflow ui --path run.jsonl
443
+ ```
444
+
445
+ The SDK only ever **writes** JSONL, but these commands **read** either framing —
446
+ a `.jsonl` part or a single-document `.json` (what `wizardflow json` emits) — so
447
+ they stay at parity with the web viewer, which also accepts both.
448
+
449
+ ### `wizardflow ui` — local viewer
450
+
451
+ ```bash
452
+ wizardflow ui run.jsonl [--host 127.0.0.1] [--port 0] [--no-open]
453
+ ```
454
+
455
+ Binds a stdlib HTTP server, serves the static WizardFlow UI bundled in the SDK
456
+ package, and opens the selected trace in your browser (the JSONL is assembled
457
+ server-side and served to the UI as one JSON document, re-read on refresh — so
458
+ you can watch a still-running trace grow).
459
+
460
+ | flag | default | meaning |
461
+ | --- | --- | --- |
462
+ | `--host` | `127.0.0.1` | interface to bind |
463
+ | `--port` | `0` | port to bind; `0` asks the OS for a free port |
464
+ | `--no-open` | off | print the local URL instead of launching a browser |
465
+
466
+ ### `wizardflow md` — export to Markdown
467
+
468
+ ```bash
469
+ wizardflow md run.jsonl # -> stdout
470
+ wizardflow md run.jsonl -o run.md # -> file
471
+ wizardflow md run.jsonl --no-mermaid # omit the graph diagram
472
+ ```
473
+
474
+ Renders the full trace as Markdown: a metadata table, a Mermaid `flowchart` of
475
+ the graph, then each message's steps and payloads (scalars inline; multi-line
476
+ strings and dict/list values in fenced code blocks; conditional edges drawn
477
+ dashed).
478
+
479
+ | flag | default | meaning |
480
+ | --- | --- | --- |
481
+ | `-o`, `--output` | — | write to this file instead of stdout |
482
+ | `--mermaid` | on | include the Mermaid graph diagram |
483
+ | `--no-mermaid` | — | omit the Mermaid graph diagram |
484
+
485
+ ### `wizardflow html` — export to HTML
486
+
487
+ ```bash
488
+ wizardflow html run.jsonl # -> stdout
489
+ wizardflow html run.jsonl -o run.html # -> file
490
+ ```
491
+
492
+ Emits a single self-contained document — inline CSS, **no JavaScript, no
493
+ external assets** — that opens offline in any browser and follows your OS
494
+ light/dark mode. Messages-only by design: no graph/Mermaid (use `md` for that).
495
+
496
+ | flag | default | meaning |
497
+ | --- | --- | --- |
498
+ | `-o`, `--output` | — | write to this file instead of stdout |
499
+
500
+ ### `wizardflow json` — assemble to one pretty-printed JSON document
501
+
502
+ ```bash
503
+ wizardflow json run.jsonl # -> stdout
504
+ wizardflow json run.jsonl -o run.json # -> file
505
+ ```
506
+
507
+ The inverse of how the SDK writes. JSONL is built for appending, so the raw
508
+ file is one long line per record and not much fun to read. This assembles the
509
+ part — header plus all its message records, with the seal's `nextPart` folded
510
+ into `meta` — into the indented, single-document `AgentTraceFile` shape, for
511
+ eyeballing, diffing in review, or handing someone a canonical JSON. (The web UI
512
+ reads that single-document form too.)
513
+
514
+ | flag | default | meaning |
515
+ | --- | --- | --- |
516
+ | `-o`, `--output` | — | write to this file instead of stdout |
517
+
518
+ Already have `jq`? You don't need this for a quick look — `jq . run.jsonl`
519
+ pretty-prints every record, and `jq 'select(.type=="message")' run.jsonl` just
520
+ the messages. `wizardflow json` differs in that it *assembles* the part into one
521
+ document (and walks no external tool).
522
+
523
+ Rendered samples (`*.md`, `*.html`) live in the [repo's
524
+ `examples/`](https://github.com/lkleonk/wizardflow/tree/main/sdk/python/examples).
525
+
526
+ ## Status / not yet
527
+
528
+ - **Timestamps** are wall-clock at log time — fine for slow/live runs, wrong if
529
+ steps fire faster than ms resolution or you import after the fact. (Open
530
+ design item: explicit per-step timestamps.)