Where agents are summoned, bound, and set to work.
A minimal, local-first orchestration engine for AI agents: cyclic state graphs executed by supersteps (Pregel/BSP), built from the ground up for models that run on your machine.
$ pip install sanctum-engineSanctum executes cyclic state
graphs — think → act → observe → think — with
parallel nodes, deterministic state merging, and a superstep budget so no
loop runs away. summon()
assembles the full ReAct loop from the same public API you build on.
Tools are plain typed functions; their JSON schemas are inferred. Point it at any local backend and invoke.
from sanctum import Tome, spell, summon from sanctum.oracle.openai_compat import OpenAICompatibleOracle @spell def add(a: float, b: float) -> float: """Add two numbers.""" return a + b oracle = OpenAICompatibleOracle( arcana="qwen2.5:7b", base_url="http://127.0.0.1:8080/v1", # llama-server, Ollama, vLLM… ) entity = summon(oracle, Tome([add]), spell_calling="auto") result = entity.invoke({"messages": [ {"role": "user", "content": "What is 19 + 23?"}, ]})
The core is pure Python standard library. Everything below ships in it.
BSP supersteps: active nodes run in parallel, return partial deltas,
conditional edges close the loops. Bounded by recursion_limit.
Not a DAG — on purpose.
Every state channel declares its reducer (overwrite, append, add, or yours). Deltas merge in node-registration order — identical runs, even under parallelism.
A JSON checkpoint per superstep (memory, SQLite, Postgres). Resume,
pause with interrupt() for human approval, or replay from
any historic Seal.
Typed, timestamped events for every boundary of the run — plus live tokens from inside a node. Combinable modes, ready for SSE.
Declarative per-node timeout, retries with jittered backoff, and
fallback nodes that read the reserved __errors__ channel.
No try/except soup.
Intercept every state delta before it merges: audit to JSONL, tally token usage, redact secrets, or veto outright. A pipeline, in registration order.
# the model emits a broken call… assistant: {"spell": "calculate", "arguments": {"expr": "19+23" # …sanctum repairs it, or answers with a correction the model understands repaired: calculate({"expr": "19+23"}) ← unclosed JSON recovered assistant: calls unknown spell "compute" corrected: "Unknown Spell 'compute'. Available: calculate. Write the call again using one of those exact names." # no native tool support at all? schemas travel in the prompt: prompted: <spell_call>{"spell": …}</spell_call> ← parsed from plain text
Small local models break
tool-calls: malformed JSON, hallucinated tools, missing arguments.
Most frameworks assume a frontier API that never fails. Sanctum treats
those failures as the normal case — a repair layer fixes what it can
and converses the model through the rest, bounded by
max_repair_rounds.
Validated end-to-end against llama.cpp: native grammar-constrained tools when the server supports them, automatic fallback to prompted calling when it doesn't. Never a proprietary API in the core.
Record a run with TraceRecorder
and render it to a single self-contained HTML file: the graph, the
timeline, every delta, every tool call. No accounts, no telemetry, no
external requests — it works on a plane.
Sanctum doesn't chase feature parity with LangGraph. It competes on a niche: reliability with local 7–14B models, an impeccable out-of-the-box experience, and observability that never leaves your disk.
| Sanctum | LangGraph | n8n | |
|---|---|---|---|
| Core dependencies | 0 (stdlib) | langchain-core + | Node.js stack |
| Local models as the default | yes — the design center | possible | via integrations |
| Repairs broken tool-calls | first-class layer | — | — |
| Tracing without a SaaS | one local HTML file | LangSmith (cloud) | self-host UI |
| Cyclic graphs / supersteps | yes (Pregel/BSP) | yes (Pregel/BSP) | workflow DAG |
Full 13-row comparison, including where LangGraph is the better choice →
The orchestration engine — this project. Graphs, state, checkpoints, streaming, oracles, resilience, tracing.
A community spellbook of local-first tools. Each spell carries a JSON
manifest; Tome.load_from_directory() loads the whole tree
into any agent in one line.