Mount Burr state-machine Applications as MCP servers.
Project description
Theodosia
Theodosia puts an AI agent on rails. You define a workflow once as a Burr state machine, and Theodosia serves it over MCP so the agent can only take the next allowed step, with every step recorded and replayable. The model can be wrong; the model cannot lie about state.
An open 1T-parameter model (Kimi K2.6) investigating a live incident on rails: each Grafana query is recorded as evidence, out-of-phase calls are refused, and the conclusion stays gated until the evidence cross-references. The investigation FSM (Phoebe) is the workflow; Theodosia is what makes the model drive it.
| What you get | Why it holds |
|---|---|
| Stays on the rails | The server enforces the graph. An unreachable action returns a structured refusal listing the ones that are reachable, and the agent self-corrects from it. |
| Auditable by default | Every step, its inputs, the state change, refusals, and timing are recorded to a replayable trace through Burr's tracker and UI. |
| One portable contract | Drive the same graph from your own Python or hand it to an external LLM over MCP. The workflow is a versioned artifact, not tied to either. |
| Built on mature parts | Apache Burr is the workflow engine; FastMCP is the MCP layer. Theodosia is the thin layer that makes one drive the other. |
Why this shape works
LLM agents fail at procedural work in predictable ways: they skip steps, stop too early or not at all, and declare success without verifying. IBM Research found prompt-level fixes buy about 15.6%, a state machine to enforce termination up to 53%, and recommends finite state machines outright. Theodosia is that state machine, served over the wire. It removes the structural failures, not reasoning errors inside a valid step: the agent keeps its full toolset (including other MCP servers via upstream) and chooses freely within each step.
More: IBM IT-Bench + MAST · MAST, UC Berkeley · Microsoft AIOpsLab · Grafana o11y-bench
What the rails do, shown
The clearest evidence is a case study:
the same model (Kimi K2.6) on the same o11y-bench incident
tasks, run free-ranging with the raw Grafana toolset versus on rails through
Phoebe. Free-ranging, it repeatedly
investigated and then never produced an answer. On rails, the conclude gate
forced it to commit, and it reached the correct root cause. o11y-bench's own
grader is the witness, on one free-ranging run: "There is no final response
message in the transcript, it ends with tool calls and thinking blocks." Three
grader-verified pairs are in the case study.
A full head-to-head aggregate on o11y-bench's 11-task investigation category is still being finalized. A first complete run is in hand, but it used an earlier Phoebe configuration that over-gated exploration (since fixed), so a clean re-run is pending and no aggregate number is published until it lands. Treat the case study as the load-bearing evidence; the aggregate is pending. The design rationale (and what rails do not fix) is in the research foundation.
Install
uv pip install theodosia # or: pip install theodosia
Python 3.11 through 3.13. Optional extras: theodosia[observability], theodosia[ui], theodosia[all].
Quickstart
from theodosia import mount
mount(application, name="coffee").run() # `application` is any Burr Application
A client that calls pay before take_order gets a refusal it can recover from: the valid actions ride on every response.
{ "error": "invalid_transition", "valid_next_actions": ["take_order"] }
A smaller example, the same mechanism: an agent ordering coffee, refused when it tries to pay before ordering and recovering from the refusal.
Command line
theodosia serve module:app # mount a Burr graph as an MCP server
theodosia render module:app # draw the state machine in the terminal (--mermaid / --dot)
theodosia doctor module:app # statically validate the graph; exits nonzero for CI
theodosia sessions show <id> # full timeline: per-step state diff + timing
theodosia watch # live-tail a running session
theodosia logs --refusals # only the steps that were refused
theodosia verify # check the session's tamper-evident ledger
A downstream package can ship its own command (my-fsm serve, my-fsm doctor, ...) with build_cli.
Observability
Every session is recorded through Burr's tracker. Tail a live run, replay a finished one step by step with its state diffs and timing, or open it in the Burr UI for the transition graph and state inspection. Refusals are recorded too: they appear in the timeline like any other step.
Documentation
Full docs at msradam.github.io/theodosia.
| Section | What it covers |
|---|---|
| Authoring a graph | Build a Burr Application from scratch and serve it, with the traps newcomers hit |
| Examples | Standalone agents built with Theodosia (Phoebe, triage, deploy-gate, coffee) and the in-repo FSMs |
| Architecture | The four-tool surface, structured refusals, how mount() drives Burr |
| What works through mount() | Typed state, persistence, hooks, parallelism, sub-applications, telemetry |
| Observability | The theodosia:// resources, the CLI, the Burr UI, OpenTelemetry |
| Security model | The agent trust boundary: what Theodosia enforces, and what it does not |
| Case study | Same model, on rails vs free-ranging: where the rails make the agent finish, grader-verified |
| Research foundation | The published evidence behind the design, and what rails do not fix |
| Driving other MCP servers | upstream: a Burr action calling tools on other MCP servers |
| CLI | serve / doctor / render / sessions / watch / logs, and build_cli |
Agents built with Theodosia
Standalone repositories, each a real agent you can clone and run:
| Repo | What it is |
|---|---|
| Phoebe | SRE incident-investigation FSM (the hero above). Keeps the full Grafana toolset; the FSM gates the procedure and the audit trail. Ships a Harbor agent for Grafana's o11y-bench. |
| triage-agent | Support triage: investigate before you decide, enforced by the graph. |
| deploy-gate-agent | A change/deploy gate: ordered gates, a health gate, an audit trail, and a call out to a filesystem MCP server via upstream. |
| coffee-agent | The toy: a coffee-order state machine an LLM drives one enforced step at a time. |
Examples and tests
examples/ ships self-contained FSMs (pure-FSM, typed state, hooks, persistence, real shellouts, LLM-in-the-graph, SKILL-to-FSM, upstream, multi-graph), each runnable with uv run python examples/<file>.py. Run the suite with uv run pytest.
Acknowledgements
Theodosia is glue between two libraries that do the hard parts: Apache Burr provides the state-machine Application, the transition graph, and the tracking UI; FastMCP provides the MCP server, the transforms, and the client behind upstream. The SKILL demos under examples/skills/ are reproduced verbatim from Anthropic and Trail of Bits with attribution.
On the name: Theodosia was Aaron Burr's daughter, known for her correspondence with him. The project sits in the same family as Burr and reaches it, which is the role it plays here.
Theodosia is an independent project, not affiliated with or endorsed by the Apache Software Foundation, DAGWorks, the Apache Burr project, or FastMCP.
License and notice
Apache 2.0. Theodosia is independent open-source work by Adam Munawar Rahman and does not represent the views of IBM Corporation or any other employer. See NOTICE.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file theodosia-0.2.0.tar.gz.
File metadata
- Download URL: theodosia-0.2.0.tar.gz
- Upload date:
- Size: 3.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2881ec515c6da50537a686e0ae3a5db0566ea7658033451b4a644efc0995d11d
|
|
| MD5 |
e4ece0f85cef93cea54eaf6f2ad275fc
|
|
| BLAKE2b-256 |
5b03641700af39a755f1fb762cee6693bcdebb1243641b24a3d9f6254e50d8e1
|
Provenance
The following attestation bundles were made for theodosia-0.2.0.tar.gz:
Publisher:
release.yml on msradam/theodosia
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theodosia-0.2.0.tar.gz -
Subject digest:
2881ec515c6da50537a686e0ae3a5db0566ea7658033451b4a644efc0995d11d - Sigstore transparency entry: 1631821030
- Sigstore integration time:
-
Permalink:
msradam/theodosia@dbe2469a1852277ebe111102271c8be807980f22 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dbe2469a1852277ebe111102271c8be807980f22 -
Trigger Event:
push
-
Statement type:
File details
Details for the file theodosia-0.2.0-py3-none-any.whl.
File metadata
- Download URL: theodosia-0.2.0-py3-none-any.whl
- Upload date:
- Size: 72.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d5e4dd671bcf6e7f6b59f7b1fc2c128499461a7a65cffd621a0e07565a94f93
|
|
| MD5 |
09005977d29b0189b92922b86a3699c6
|
|
| BLAKE2b-256 |
18da28b02bd43e314f63ae0c631623ec3d91389e27a5c50cd6c562a33fa97fba
|
Provenance
The following attestation bundles were made for theodosia-0.2.0-py3-none-any.whl:
Publisher:
release.yml on msradam/theodosia
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theodosia-0.2.0-py3-none-any.whl -
Subject digest:
9d5e4dd671bcf6e7f6b59f7b1fc2c128499461a7a65cffd621a0e07565a94f93 - Sigstore transparency entry: 1631821036
- Sigstore integration time:
-
Permalink:
msradam/theodosia@dbe2469a1852277ebe111102271c8be807980f22 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dbe2469a1852277ebe111102271c8be807980f22 -
Trigger Event:
push
-
Statement type: