The quickstart.
One Python agent, from install to a run you can resume after a crash, replay without the model, and verify from the command line. Every snippet on this page targets version 0.5.
1 · Install
One package, with the extra for your model provider. 0.5 is not yet a packaged release, so install it from source. You need Python 3.9 or later and a Rust toolchain.
your terminal
git clone -b release/0.5.0 https://github.com/pragmaticco/pragmatic
pip install "./pragmatic/crates/pragmatic-python[anthropic]" # or [openai]
2 · Wrap your client
An agent is a function that takes ctx. Wrap your client with ctx.wrap: what you get back works exactly like the client, and every call through it is written to the journal before your agent sees the response.
agent.py
import anthropic
import pragmatic
TOOLS = [{"name": "search", "description": "Search the corpus",
"input_schema": {"type": "object",
"properties": {"q": {"type": "string"}}}}]
def search(q: str) -> list[str]:
... # your tool
rt = pragmatic.Runtime("./journals")
def research(ctx):
client = ctx.wrap(anthropic.Anthropic()) # wrap the client you already use
messages = [{"role": "user",
"content": "Find papers on durable execution."}]
while True:
msg = client.messages.create(
model="claude-sonnet-5-5", max_tokens=1024,
tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": msg.content})
if msg.stop_reason != "tool_use":
return msg.content[0].text
messages += ctx.run_tools(msg, {"search": search})
3 · Run tools on the record
ctx.run_tools(response, tools) performs every tool call in a model response. Each call is written to the journal before the tool runs, and its result after. You get back the messages to append, already in your provider’s format. A tool that raises is recorded and reported to the model as an error, instead of crashing the run.
For a single function outside a model response, use ctx.tool:
agent.py
page = ctx.tool("fetch", fetch_url, url) # any function, on the record
4 · Run, resume, replay
agent.py
report = rt.run("research-42", research) # record
# The process died mid-run? Resume it:
report = rt.resume("research-42", research) # finished calls are not repeated
# Replay the whole run, for debugging or an audit:
audit = rt.replay("research-42", research) # the model is never called
assert audit.output == report.output
assert rt.verify("research-42") # the hash chain is intact
Resume reads the recorded part of the run back into your agent and carries on recording from where it stopped. Replay serves everything from the journal and calls nothing. If a step your agent takes differs from the record, replay raises JournalDesync at that step instead of quietly drifting. Mark the agent with @pragmatic.durable and replay refuses a changed agent before it starts.
If the crash cut off a tool call in the middle, it runs again by default. Pass a recover policy to rt.resume to mark it done instead. A model call that was still waiting for its reply is sent again.
5 · Verify a run
The pragmatic command-line tool reads the same journal files. Install it from the repository you cloned.
your terminal
cargo install --path pragmatic/crates/pragmatic-cli pragmatic runs --dir ./journals # every run and its status pragmatic verify research-42 --dir ./journals # research-42: verified (n entries) pragmatic verify --all --dir ./journals pragmatic show research-42 --dir ./journals # the timeline, in your terminal pragmatic export research-42 --dir ./journals -o research-42.html pragmatic serve --dir ./journals # a console on 127.0.0.1:7171
verify recomputes every fingerprint in the chain and names the first entry that does not match. For a keyed journal, pass the same key with --key. export writes one run as a self-contained HTML file you can attach to a ticket or send to a reviewer.
Evidence bundles, which add independent timestamps an outside auditor can check, come with 0.6 and are in design-partner preview. See how an auditor checks a run.
What 0.5 records
ctx.wrap records these calls, from sync and async clients:
anthropic.AnthropicandAsyncAnthropic:messages.createandbeta.messages.create.openai.OpenAI,AsyncOpenAIandAzureOpenAI:chat.completions.createandresponses.create.
Streaming and .parse() are refused inside a run in 0.5, rather than quietly bypassing the journal. Anything else on the client passes through unrecorded. For another model client, record calls yourself with ctx.draw(prompt, sample), where prompt fully identifies the request. ctx.now() and ctx.recv() record clock reads and incoming messages.
Transport settings such as timeouts and extra headers are never recorded, so API keys stay out of journals.
Async agents
Make the agent async def, use the async client, and await ctx.arun_tools and ctx.atool. rt.run drives it on its own event loop. In 0.5, recorded steps run one at a time, so the order they land in the journal can be reproduced; parallel branches arrive with 0.6.
Rust
The same runtime, natively. Point Cargo at the release/0.5.0 branch of the repository.
Cargo.toml
[dependencies.pragmatic] git = "https://github.com/pragmaticco/pragmatic" branch = "release/0.5.0" [dependencies.pragmatic-anthropic] git = "https://github.com/pragmaticco/pragmatic" branch = "release/0.5.0"
src/main.rs
use pragmatic::{Ctx, Fault, Runtime, Value};
use pragmatic_anthropic::AnthropicOracle;
#[pragmatic::durable]
fn research(ctx: &mut Ctx) -> Result<Value, Fault> {
let plan = ctx.oracle("plan the research task")?; // on the record
// a side effect, recorded before it runs
ctx.effect("publish", format!("report: {plan}"), |arg| {
Ok(Value::from(format!("s3://reports/{arg}")))
})
}
let oracle = AnthropicOracle::from_env()?.model("claude-sonnet-5-5");
let mut rt = Runtime::on_dir("./journals", oracle)?;
rt.run("research-42", research)?;
rt.resume("research-42", research)?; // after a crash
rt.replay("research-42", research)?; // the model is never called
Where next
- How it works: the journal, recovery, and what 0.6 adds.
- Questions: how Pragmatic compares to tracing tools and durable execution, and what a record can prove.
- The repository’s
release/0.5.0branch: the full usage guide, and runnable examples that need no API key.