Durable AI agents, by construction.

You write the workflow once as a Burr Application. Theodosia serves it over Model Context Protocol; any standards-compliant MCP client can drive it (verified against Claude Code, Cursor, fast-agent, and Gemini CLI), and the server only lets the agent take steps the graph allows. Every step it takes, and every step it tried but couldn't, is on the record.

$ pip install theodosia
Read the docs View on GitHub

No API key needed. theodosia primer walks a tiny coffee-order example offline and prints the same output every run.

⊢ Live run Kimi K2.6 on rails through an SRE investigation A real Kimi K2.6 run stepping through a gated SRE investigation on rails Per-session ledger · 12 steps · 1 refusal recorded
How it works

The state machine decides which steps are legal.

Theodosia sits between any MCP client and your workflow, enforcing the legal-step boundary at the server.

Only legal steps.

Your Burr workflow defines what's allowed from each state. A step that isn't reachable from the current state is refused before the action body runs. The agent gets back a structured refusal naming the legal next moves and self-corrects.

⊢ legal step only

Refused attempts are kept.

Burr's tracker logs the actions that ran. Theodosia adds a refusals.jsonl sidecar so the attempts that were refused are also on the record. The trail reflects what the agent tried, not its own account.

refusals.jsonl → on the record

Forkable, chain-verified sessions.

Every entry is hash-chained with the session identity in the payload, so theodosia verify catches edits, reorders, and copies between sessions. Resume or branch from any past step with the fork_at / fork_from_past MCP tools.

fork_at(seq) → same workflow
How it works

A state machine you can see, and a log you can read.

An incident-response workflow: five states, four allowed transitions, one refusal. The agent attempted close_incident from engaged; the machine refused, recorded the attempt, and did not advance.

⊢ Same source, three surfaces

The diagram below is generated from the same Burr Application that theodosia.mount() serves over MCP and that theodosia verify checks the ledger against. One source of truth, three views.

incident = build_application() # see Quickstart for the 15 lines
theodosia.mount(incident).run()

Got a Mermaid diagram already? Philip, a sibling library, lifts .mmd files into the same Burr Application.

incident-response · state machine
⊢ 4 allowed ○ 1 refused ★ current: engaged
allowed. Transitions defined by Burr. State advances; entry is recorded with outcome: "allowed".
refused. The proposed action was not in legal(state). Recorded with a reason; state does not advance. The agent learns of the refusal after it lands in the log.
current. The workflow's persisted state across calls. Theodosia is the source of truth; the agent's idea of the state is incidental.
~/.theodosia/incident-response/<app-id>/ledger.jsonl · excerpt
TIMESTAMP
ACTION
FROM → TO
REASON
OUTCOME
2026-05-25T14:30:12.183Z
page_oncall
triaged engaged
⊢ allowed
2026-05-25T14:31:45.902Z
close_incident
engaged
not allowed from engaged; requires verified first
○ refused
2026-05-25T14:33:20.418Z
verify
engaged verified
⊢ allowed
2026-05-25T14:40:01.077Z
resolve
verified resolved
⊢ allowed

Quickstart in 30 seconds.

Install the adapter, point it at a Burr state machine, and serve it over MCP. Your existing agent connects unchanged.

1Install the package from PyPI.
2Write a Burr Application with actions and gated transitions.
3Serve it as an MCP server with one command.
4Connect any MCP client; verify the audit log anytime.
app.py
# A minimum-viable Burr workflow: 5 states, 4 transitions.
from burr.core import ApplicationBuilder, State, action

@action(reads=["status"], writes=["status"])
def page_oncall(state): return state.update(status="engaged")

@action(reads=["status"], writes=["status"])
def verify(state): return state.update(status="verified")

# plus resolve, archive ...

incident = (
    ApplicationBuilder()
    .with_actions(page_oncall, verify, resolve, archive)
    .with_transitions(
        ("triaged",  "engaged",  "page_oncall"),
        ("engaged",  "verified", "verify"),
        ("verified", "resolved", "resolve"),
        ("resolved", "closed",   "archive"),
    )
    .with_state(status="triaged")
    .with_terminal("closed")
    .build()
)
bash · theodosia
$ pip install theodosia
# mount the Application as an MCP server
$ theodosia serve app.py:incident
   serving incident-response on stdio
   4 actions, 1 terminal ('closed')
   ledger → ~/.theodosia/incident-response/<app-id>/

# later, confirm the ledger chain is intact
$ theodosia verify
   ledger intact · incident-response/<app-id>