Skip to main content

Python SDK for recording agent flows into the WizardFlow / AgentTrace file format.

Reason this release was yanked:

Superseded by the JSONL format — use >=0.2

Project description

WizardFlow Python SDK

A lightweight tracer for Python agents. Drop three calls into your code — init, log, end_message — and turn a messy multi-agent run into a portable trace you can replay as an interactive graph or export to Markdown/HTML. Because the trace is just a JSON file, anyone can replay it: hand it to a teammate or PM and they drop it into getwizardflow.com in the browser — no Python, no install. Lightweight by design: pure Python, zero runtime dependencies, no daemon, no setup.

WizardFlow replaying an agent run

The JSON it produces is a small, documented schema: a graph { nodes, edges } plus messages[] → steps[] → payloads[] { label, value }.

Install

pip install wizardflow

No runtime dependencies. Developing the SDK itself? See CONTRIBUTING.md.

Quickstart

import wizardflow

wizardflow.init(
    output_dir="traces",                # where trace files are written
    file_prefix="run",                  # optional; defaults to "wizardflow"
    description="A small router-based agent run.",
    nodes=["user_input", "router", "planner", "tool_node", "final_response"],
    edges=[("user_input", "router"), ("router", "planner")],
)

# Every log names its message in the first argument.
wizardflow.log("msg-1", "router", "llm_input", prompt)    # same node, two payloads ->
wizardflow.log("msg-1", "router", "llm_output", output)   #   folded into one step
wizardflow.log("msg-1", "tool_node")                       # visited, no payloads
wizardflow.end_message("msg-1")                            # -> writes the trace

There is no save() and no autosave: log() only accumulates in memory, and end_message(id) is the one call that writes the trace. Writes happen at message boundaries — one write per finished message, however many log() calls it contains. init() returns a client and also stashes it as the module default, so the bare wizardflow.log(...) form above works.

Concurrency-safe. Multi-agent setups end messages from many threads/tasks at once; an internal lock serializes the mutation-and-write so the shared part file is never corrupted and no message is lost or duplicated. Each write is atomic, so the file on disk is always a complete, valid trace.

Targeting a message

The first argument to log is the message id, so overlapping messages never collide — interleave them freely (as concurrent agents do) and each step routes to the right message:

wizardflow.log("msg-1", "classifier", "input", text_a)
wizardflow.log("msg-2", "classifier", "input", text_b)   # a different message
wizardflow.log("msg-1", "generator", "output", answer_a)
wizardflow.end_message("msg-1")          # writes msg-1
wizardflow.end_message("msg-2")          # writes msg-2

Pass the string id, never a handle object — safe to hand to a callback or across threads. A message is created on first reference and finalized by end_message; end_message(id, title="...") optionally gives it a human title.

LangGraph: automatic topology

Instead of listing nodes/edges by hand, read them straight from a compiled LangGraph app:

app = workflow.compile(checkpointer=memory)

wizardflow.init_from_langgraph(app, output_dir="traces", file_prefix="trace")

wizardflow.log("msg-1", "planner", "Input", state)   # runtime logging unchanged
wizardflow.end_message("msg-1")

You can keep the extracted LangGraph topology and still choose node accent colors for important nodes:

wizardflow.init_from_langgraph(
    app,
    output_dir="traces",
    file_prefix="trace",
    node_colors={
        "router": "#A78BFA",
        "retriever": "#22D3EE",
        "generator": "#60A5FA",
    },
)

By default, a color key that does not match an extracted node id raises a WizardFlowError so typos fail fast. With silent=True, unknown color keys are ignored.

It extracts node ids (keeping __start__ / __end__) and directed edges, and marks runtime branches with "conditional": true (deterministic and parallel fan-out edges stay plain):

{
  "edges": [
    { "source": "__start__", "target": "router" },
    { "source": "router", "target": "planner", "conditional": true },
    { "source": "planner", "target": "final_response" }
  ]
}

LangGraph is not a dependency — extraction is duck-typed on app.get_graph(). A non-LangGraph object raises LangGraphExtractionError; missing conditional metadata never fails extraction (the edge is just emitted plain). Call it after compile(), when the topology actually exists.

API (v0.1)

  • init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, meta=, silent=False, max_bytes=256_000) -> Client
    • description lands in meta.description (matches the schema field).
    • nodes= enables fast-fail: log() to an undeclared node raises UnknownNodeError immediately (unless silenced).
    • output_dir is optional; omitted, traces are written in cwd.
    • file_prefix is optional; omitted, filenames start with wizardflow.
    • max_bytes caps each part file before rotation (see below).
  • init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_colors=, silent=False, max_bytes=...) -> Client
    • same as init, but nodes/edges come from app.get_graph().
    • node_colors maps extracted node ids to CSS colors such as "#A78BFA".
  • log(id, node, label=None, content=None) — the first positional is the message id, the second is the node. With label/content it records a payload; bare log(id, "node") records a visit with no payloads. The message is created on first reference; this only accumulates in memory.
  • end_message(id, title=None) — finalize a message and write the trace; returns the current trace path. The only call that touches disk. Optional title sets the message's human title. Idempotent.
  • Client.current_path — the trace file currently being written.
  • to_dict() / to_json() — inspect the active part (completed messages).

How saving works

end_message triggers an atomic write: write to <part>.tmp, then os.replace over the part file. The file is always a complete, loadable AgentTraceFile. Only completed messages are written; an in-progress message lives in memory until it ends.

Rotation (no single huge file)

There's no natural "end" to a chatbot trace, so the SDK caps file size instead. Each run writes a timestamped entry file whose name carries the run-start time:

wizardflow__2026-06-08T16-29-09-123Z.json

The name's wizardflow is the file_prefix; the timestamp is captured when init() creates the client. If the active part would exceed max_bytes (~256 KB by default), it's sealed and the next message starts a fresh part:

wizardflow__2026-06-08T16-29-09-123Z.json
wizardflow__2026-06-08T16-29-09-123Z__part2.json
wizardflow__2026-06-08T16-29-09-123Z__part3.json

Rotation only ever happens at a message boundary, never mid-message (a lone message larger than the cap gets its own oversized part). A smaller cap keeps each rewrite — and the write lock held across it — short, which matters when many agents end messages concurrently; raise it for fewer files at the cost of heavier rewrites. max_bytes is clamped to a hard ceiling (1 MB) so an accidental huge value can't stall concurrent writers. Each part is a self-contained, valid trace (full graph + its slice of messages), chained via meta:

"meta": { "part": 2, "prevPart": "...Z.json", "nextPart": "...__part3.json" }

A single-part trace stays clean (no part metadata). There's no partCount — the total is genuinely unknown while a continuous run is still logging; follow nextPart to walk to the end. Since names are timestamped, read the real file back from the client's current_path (also the path end_message returns).

Logging

Rotation emits an INFO notice on the wizardflow logger. Following library convention, the SDK attaches a NullHandler and configures nothing — you see nothing unless you opt in:

import logging
logging.getLogger("wizardflow").setLevel(logging.INFO)

This is separate from silent= (which governs raise-vs-swallow for errors).

Step folding

Consecutive log() calls to the same node within a message fold into a single step with multiple payloads (e.g. a router step carrying both llm_input and llm_output). A log() to a different node starts a new step.

Examples

Fuller runnable examples live in examples/:

  • quickstart.py — a small linear agent; two messages, the second finalized with an end_message(..., title=...) title.
  • multibranch.pyrouter fans out into a planner/tool path and a retriever path that rejoin at generator. Two messages take different branches, so each logs only the nodes it actually visited.

CLI

The wizardflow command has three subcommands: ui, md, and html. Every one takes the trace file as a positional argument or via --path (pass one, not both):

wizardflow ui   run.json
wizardflow md   run.json
wizardflow html run.json
# --path is equivalent everywhere:
wizardflow ui --path run.json

wizardflow ui — local viewer

wizardflow ui run.json [--host 127.0.0.1] [--port 0] [--no-open]

Binds a stdlib HTTP server, serves the static WizardFlow UI bundled in the SDK package, and opens the selected trace in your browser.

flag default meaning
--host 127.0.0.1 interface to bind
--port 0 port to bind; 0 asks the OS for a free port
--no-open off print the local URL instead of launching a browser

wizardflow md — export to Markdown

wizardflow md run.json                 # -> stdout
wizardflow md run.json -o run.md       # -> file
wizardflow md run.json --no-mermaid    # omit the graph diagram

Renders the full trace as Markdown: a metadata table, a Mermaid flowchart of the graph, then each message's steps and payloads (scalars inline; multi-line strings and dict/list values in fenced code blocks; conditional edges drawn dashed).

flag default meaning
-o, --output write to this file instead of stdout
--mermaid on include the Mermaid graph diagram
--no-mermaid omit the Mermaid graph diagram

wizardflow html — export to HTML

wizardflow html run.json               # -> stdout
wizardflow html run.json -o run.html   # -> file

Emits a single self-contained document — inline CSS, no JavaScript, no external assets — that opens offline in any browser and follows your OS light/dark mode. Messages-only by design: no graph/Mermaid (use md for that).

flag default meaning
-o, --output write to this file instead of stdout

Rendered samples (*.md, *.html) live in the repo's examples/.

Status / not yet

  • Timestamps are wall-clock at log time — fine for slow/live runs, wrong if steps fire faster than ms resolution or you import after the fact. (Open design item: explicit per-step timestamps.)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wizardflow-0.1.0.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wizardflow-0.1.0-py3-none-any.whl (648.4 kB view details)

Uploaded Python 3

File details

Details for the file wizardflow-0.1.0.tar.gz.

File metadata

  • Download URL: wizardflow-0.1.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wizardflow-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ab38663da05afbe47209556d770108b6c3a0ecdb4e981207563fa99b28520f34
MD5 1deffb8065c9fd6fc09baa9833bea8da
BLAKE2b-256 97fe0095d84cce46e14294b12c64c3ed8f871a3c164700c1048037d37097cd49

See more details on using hashes here.

Provenance

The following attestation bundles were made for wizardflow-0.1.0.tar.gz:

Publisher: publish.yml on lkleonk/wizardflow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wizardflow-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: wizardflow-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 648.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wizardflow-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38b336500d0ef84a820cfaa79cc124c7d12b8ab9c15fb3b47146796c0e3633f9
MD5 fa1422085bd936fedcafd6db27463dd3
BLAKE2b-256 c88f2c45d3614a14e072536091ef0a6f509f61c8841a1bae334de9cb5b335ea6

See more details on using hashes here.

Provenance

The following attestation bundles were made for wizardflow-0.1.0-py3-none-any.whl:

Publisher: publish.yml on lkleonk/wizardflow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page