Multi-agent systems · 2026 · completed
Skellington
A multi-agent coding orchestrator with a voting review panel.

Plan, route, delegate, synthesize — back to one voice. A request goes in; Jack plans it, routes each step to a specialist — a builder, a researcher, a codebase navigator, a three-way validation panel, a reporter that synthesizes the result — and every transition emits a typed event, streamed live over WebSockets. The specialists are named after The Nightmare Before Christmas, but the names are identifiers, not decoration: they map directly to classes in the codebase.
A personal learning project — not production-ready, not affiliated with any LLM provider. The point was never to ship an agent framework; it was to find out what actually breaks when you build one from scratch: routing, cost, consensus, and how to test something whose main dependency talks back.
The instrument

A 2-of-3 panel, not a single gate
Lint, test, and security review run in parallel and vote. A crashing validator is a failed vote, not a panel-wide failure — the review survives one check breaking.

Toolkit injection, not a global filesystem call
Agents take their tools as an argument — real ones in production, mocks in tests — so nothing in an agent body reaches around the interface to touch disk directly.

LLM for judgement, Python for facts
Diffs come from `difflib`, counts from typed state — the model narrates what happened, it never computes what happened.

158 tests, zero live model calls
Every provider is mocked at the boundary, so the whole suite runs in under two seconds and never spends a token to tell you something broke.
The movement
The engineering underneath
The interesting parts aren't the agents — they're the seams: how work gets routed, checked, and paid for.
Capability flags, not model names
A model's quirks are declared once as flags on a registry entry; prompt fragments and request shaping key off the flag, never the model id. Swapping in a new model is one registry entry, not a new prompt file per model family.
class ModelCard(BaseModel):
id: str
provider: LLMProvider
context_window: int
supports_native_json: bool = False
prefers_xml_tags: bool = False
supports_thinking: bool = False
supports_prompt_caching: bool = False
supports_batch_api: bool = False
MODELS: dict[str, ModelCard] = {
"claude-opus-4-7": ModelCard(
id="claude-opus-4-7",
provider=LLMProvider.ANTHROPIC,
context_window=200_000,
prefers_xml_tags=True,
supports_thinking=True,
supports_prompt_caching=True,
supports_batch_api=True,
),
"gpt-4o": ModelCard(
id="gpt-4o",
provider=LLMProvider.OPENAI,
context_window=128_000,
supports_native_json=True,
),
}
def get_model_card(model_id: str) -> ModelCard:
card = MODELS.get(model_id)
if card is not None:
return card
return _FALLBACK.model_copy(update={"id": model_id})Graceful degradation is the default, not the exception
No search key configured — the research agent falls back to LLM-imagined results instead of erroring. An empty request short-circuits before spending an LLM call at all.
- Language
- Python — Pydantic v2, async-first
- Interfaces
- CLI and a FastAPI/WebSocket web UI, same orchestrator underneath
- Tooling
- Six in-tree MCP servers — filesystem, websearch, git, exec, database, docs
- Testing
- 158 tests, mocked provider boundary — zero live model calls
- Cost levers
- Prompt caching, per-subagent model routing, keyword short-circuit, batch API