Give an agent a pencil.
We built models that genuinely see — with real spatial reasoning — and then
hand them only linear text for the problems that are hard precisely because
they are spatial. whiteboard is a small capability that closes that gap: it
deterministically renders a tangled structured problem to an image, feeds
that image back to the model, and lets the model perceive the answer the way
an engineer reads it off a whiteboard.
- A database with circular foreign keys → render the ER diagram and the cycle is a red loop you can point at, instead of 2,000 lines of DDL to trace by hand.
- Six hours of logs → render a density timeline and the 3 a.m. burst is a bright smear, instead of 400k lines to scroll.
- A distributed trace → render the span waterfall and the slow span is obvious, instead of arithmetic over timestamps.
The reflexive answer to "too much information" is usually bigger context or
more retrieval — pump in more text. whiteboard is the opposite move: render
it smaller and look at the shape.
The agent draws in order to reason. Perception, turned inward.
Two ideas do all the work:
-
A tool result can be a picture. When the model calls the
render_whiteboardtool, the result handed back is an image content block (a base64 PNG), not text. A vision model then reasons over the rendered structure directly. That single mechanism — tool-result-as-image — is the whole trick. -
Renderers encode structure but never verbalize the conclusion. The schema renderer runs a real cycle-detection algorithm — and uses the result only to colour the cycle edges red. It does not return the sentence "there is a cycle between orders, invoices and payments." If it did, we would be back to text reasoning and the capability would be pointless. The algorithm makes the answer visible; the model's job is to see it.
See docs/HOW_IT_WORKS.md for the full mechanism.
The rendering core needs matplotlib, numpy, and Graphviz. Graphviz has
two parts: the Python bindings (pip) and the dot system binary.
# 1. The Graphviz system binary (provides `dot`)
# macOS: brew install graphviz
# Debian/Ubuntu: sudo apt-get install graphviz
# Fedora: sudo dnf install graphviz
# Windows: choco install graphviz (or download from graphviz.org)
# 2. The Python package + its deps
pip install -e .
# 3. (optional) the agent loop needs the Anthropic SDK
pip install -e ".[agent]"
# 4. (optional) tests
pip install -e ".[dev]"Verify the binary is visible: dot -V should print a version. If whiteboard render later complains it can't find dot, that system binary is missing.
This is the fastest way to see the idea. It calls the renderers directly:
python examples/run_examples.py
# then open the PNGs in examples/rendered/or one at a time via the CLI:
whiteboard render --type schema --data examples/circular_schema.json -o schema.png
whiteboard render --type logs --data examples/sample_logs.json -o logs.png
whiteboard render --type trace --data examples/sample_trace.json -o trace.pngexport ANTHROPIC_API_KEY=sk-ant-...
whiteboard solve \
--data examples/circular_schema.json \
--question "Are there any circular foreign-key dependencies? If so, name every table in each cycle."The agent decides on its own to call render_whiteboard, looks at the returned
image, and answers from what it sees. Every image it looked at is saved to
whiteboard_session/ so you can see exactly what it saw.
import whiteboard
# Offline rendering (deterministic, no network):
result = whiteboard.render("trace", trace_dict, "trace.png")
print(result.caption) # neutral description — never the answer
print(result.legend) # how to read the encoding
# The agent loop (needs the anthropic extra + an API key):
from whiteboard import WhiteboardAgent
agent = WhiteboardAgent()
out = agent.solve("What is the slowest span and why?", data=trace_dict)
print(out.answer)
print(out.images) # paths to what the agent renderedBuild your own loop instead? whiteboard.whiteboard_tool_spec() gives you the
Anthropic tool definition and whiteboard.run_tool(...) executes a call and
returns the content blocks (image + legend) ready to drop into a tool_result.
render_type |
Input shape | What it draws | The structure it surfaces |
|---|---|---|---|
schema |
{ "tables": [...] } |
ER diagram (Graphviz) | foreign-key dependency cycles in red |
logs |
{ "events": [...] } |
service × time density heatmap + error strip | activity bursts and error spikes |
trace |
{ "spans": [...] } |
span waterfall | the dominant call chain (where the time went) |
Full input formats and algorithms: docs/RENDERERS.md.
Two cycles are visible at a glance: orders → invoices → payments → orders and
customers ↔ addresses. Acyclic relationships stay grey.
A payments error burst at 03:00 is a bright vertical smear, with a matching
spike in the error-rate strip below — found by looking, not scrolling.
The dominant chain (GET /feed → feed.build → recsys.rank → model.inference) is
saturated and outlined; the 830 ms model.inference span is the obvious pole.
whiteboard/
├── README.md
├── pyproject.toml / requirements.txt / LICENSE
├── docs/
│ ├── ARCHITECTURE.md # components and data flow
│ ├── HOW_IT_WORKS.md # the tool-result-as-image mechanism, in depth
│ ├── RENDERERS.md # per-renderer input format + algorithm
│ └── EXTENDING.md # how to add your own renderer
├── src/whiteboard/
│ ├── types.py # validated input models (Schema / LogSet / Trace)
│ ├── analysis.py # pure algorithms: SCC, cycle edges, dominant path
│ ├── renderers/ # base + schema / logs / trace
│ ├── registry.py # render_type -> renderer
│ ├── tool.py # Anthropic tool spec + dispatch (returns an image)
│ ├── agent.py # the tool-use loop
│ └── cli.py # `whiteboard list | render | solve`
├── examples/ # input files, a deterministic log generator, runner
└── tests/ # 22 tests; the analysis tests are the important ones
- Don't expand the model — re-encode the problem. Match the input to the modality the model is already strong at.
- Encode, don't verbalize. Renderers make the answer visible; they never
state it. (
RenderResultcarries the image, a neutral caption, and a visual legend — never the finding.) - The deterministic core runs offline. No model, no network, identical pixels for identical input. The model is only in the loop to look.
- Adding a renderer is a one-file change. Implement
Renderer, register it, and it appears in the CLI and the agent tool at once.
pip install -e ".[dev]"
pytest -qThe suite covers the deterministic pieces directly (cycle detection across self-loops, multiple independent cycles, a 5,000-node graph; the dominant-path selection; input validation) and asserts the renderers emit valid PNGs and that the tool result actually carries an image block.
MIT — see LICENSE.


