Pragmatic

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.Anthropic and AsyncAnthropic: messages.create and beta.messages.create.
  • openai.OpenAI, AsyncOpenAI and AzureOpenAI: chat.completions.create and responses.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

Next Questions