orchestrion

built from scratch

A multi-agent orchestrator in raw Python — one agent loop, typed inter-agent messages, hard budget reservations, and runs you can replay event by event. No LangChain, no LangGraph, no CrewAI, no vendor SDK. Every abstraction is one I designed and can defend.

What it does

One sentence in, a verified deliverable out

You give it a task in plain language. A planner decomposes it into a dependency graph of subtasks. Workers execute the independent ones concurrently, each with its own restricted toolset. A critic reviews every result against that subtask's own success criteria and sends work back when it falls short. The whole run is bounded by a hard budget and recorded as a trace you can step through afterwards.

Planner

Emits a task DAG. Rejected and repaired in-loop if it has a cycle, an orphan node, a dangling dependency, or a success criterion no reviewer could actually check.

Workers

researcher · writer · coder. Profiles are tool-name lists, not subclasses. Each sees only its own subtask brief — never the whole run history.

Critic

Sees one node's criteria and artifacts, nothing else. Structurally cannot return APPROVE while any criterion is marked unmet.

Replay

The hardest showcase, event by event

This is not a re-enactment — it is the actual trace.jsonl from a 10-node run, played back in your browser. Every state transition, tool call, critic verdict, compaction and dollar is in the file. If the replay can't explain why the system did something, the trace has a hole in it.

225 narratable events out of 471 total; per-call bookkeeping is recorded but hidden by default.

The same run, rendered to video

Generated programmatically by scripts/make_demos.py: one frame per trace event → headless Chromium screenshots → VP8/WebM. No screen recording.
Results

Six eval tasks, three runs each

Scored by a mechanical rubric — never an LLM judge. A model grading a model drifts with the model and would make the eval as unreliable as the thing it measures. Every check is a predicate over the run state, the workspace and the trace.

Ablations — proving the rubric measures something

A deterministic backend scores 100% with zero variance, which on its own proves nothing. So each ablation removes exactly one guard and the rubric has to notice. All five are detected. A guard whose removal costs no score is either untested or not earning its place.

Architecture

Where each decision lives

llm/ — provider-agnostic client
  client.py · error taxonomy
  anthropic_adapter.py · raw httpx
  openai_adapter.py · same protocol
  retry.py · backoff + jitter
  budget.py · reserve / settle
agents/
  base.py · the one agent loop
  planner.py · worker.py · critic.py
tools/ registry · files · python_exec
  web_search · web_fetch · memory_search
memory/ shortterm compaction · sqlite FTS5
orchestrator/ engine.py · recovery.py
obs/ tracer.py · timeline.py · svg.py
evals/ tasks · harness · ablations

Three rules the code holds to

  • One loop. Every agent runs the same function in agents/base.py. Roles differ only by system prompt, allowed tools, and output schema.
  • Typed everywhere. Anything crossing a boundary is a pydantic model. No loose dicts between agents. mypy --strict is clean.
  • The orchestrator is the only component that talks to more than one agent.

Layered client

AnthropicAdapter   wire format + error class
      ↓
RetryingClient     backoff, Retry-After, abort hook
      ↓
BudgetedClient     reserve → call → settle

Budget sits outside retry so one reservation covers the whole retry saga. Flip the order and a transient 429 becomes a hard budget failure — a different failure class, routed to a different recovery.

Design decisions

Six choices and what I rejected

Finishing via a submit_result tool

Rejected: structured outputs. They guarantee valid JSON but are provider-specific, constrain the whole response in a loop that also needs tool calls, and move the "I'm done" signal out of the tool trace. Lock-in decided it — the finish contract must survive a provider swap.

Budget reservations, not a soft check

With N concurrent agents, N can each see "budget remains" and collectively overshoot by N×. reserve() charges the worst case before dispatch. A test starts three agents against a cap that fits one and proves exactly one reaches the model.

Vague criteria are a validation error

The critic sees only a node's criteria and artifacts, so "sources are high-quality" makes it rubber-stamp or revise forever. Prompt guidance alone didn't hold, so the validator rejects soft words with nothing countable alongside — and says how to rewrite.

FTS5, not a vector database

A run produces ~100 facts, and query and documents share a vocabulary, so the mismatch embeddings solve is largely absent. A vector store adds an embedding call per fact and makes retrieval non-deterministic — which would break eval comparability.

Hand-rolled backoff, not tenacity

Needed three things its callback model makes awkward: honour Retry-After, emit one trace event per attempt, and consult an abort hook so a budget breach stops a retry storm mid-sequence. ~40 lines I can defend line by line.

Content-addressed trace blobs

Events stay small enough to stream into the live view; full prompts go to blobs/<sha256>. The system prompt repeated across 82 calls is stored once. A resumed run continues the same sequence rather than restarting it.

Bugs worth keeping

Four the tests and demos caught

Also found: compaction erases an agent's record of what it already did. An agent that re-derives progress from message history then repeats a call until loop detection kills it. The system behaved correctly — the lesson is that durable state belongs in the workspace or long-term memory, never only in the context window.
Honest limitations

What these numbers do and don't show

Read this first. Every result on this page was produced by a simulated model backend — a rule-based stand-in plus a fixed offline corpus. Everything above the adapter is the real code path: loop, guards, registry, sandbox, budget ledger, scheduler, state machine, critic, memory and tracer. Only the token generator is fake. The identical commands run against the real API with a key set; live results simply hadn't been run when this was written.

Full versions in docs/limitations.md, docs/security.md, docs/memory.md and docs/failure-policy.md.

Run it

Two commands, no credentials needed

git clone https://github.com/Gariyuuu/orchestrion && cd orchestrion
uv venv --python 3.11 && uv pip install -e ".[dev]"

.venv/bin/python run.py --showcase deep      # 10-node run, simulated backend
.venv/bin/python replay.py runs/<run_id>      # step through what happened

export ANTHROPIC_API_KEY=sk-ant-...          # the real thing
.venv/bin/python run.py "Research X and write a sourced report."

213 tests, mypy --strict clean, ~5.6k lines of library code and ~3.1k lines of tests.