I was sketching an idea on my phone last night: a tree. One circle at the top, three circles under it, and one of those with two circles of its own. Each circle is an AI agent. The top one coordinates, the ones below it do the work, and — here's the part I liked — each circle can run on a different LLM provider. The contrarian analyst on Grok, the researcher on Gemini, the planner on GPT, all coordinated by Claude at the top.
I build a lot of small agent tools, and the orchestration story is usually "one agent, one model, one long prompt." Multi-agent frameworks exist, but they tend to drag in SDKs, servers, and config formats that need their own tutorial. I wanted the thing I drew: a tree, in a YAML file, run from a CLI.
So I built swarm-tree. It's tiny — standard library only, no vendor SDKs, every provider call goes through plain urllib.
How it works
You describe the tree in tree.yaml. Each node has a name, a provider, a model, a role (system prompt), and optional children:
name: orchestrator provider: anthropic model: claude-sonnet-4-5 role: You are the top-level coordinator... children: - name: grok-agent provider: xai model: grok-4 role: You are a contrarian analyst... - name: gpt-agent provider: openai model: gpt-5 role: You are a planner... children: - name: code-reviewer provider: deepseek model: deepseek-chat role: You review code diffs... Then:
swarm-tree run --tree tree.yaml --task "Design a weekend hackathon project" The semantics are dead simple:
- Leaf nodes make one provider call with (role + task) and return text.
- Internal nodes do three things: ask their provider to split the task into one subtask per child, run all children in parallel, then ask their provider again to merge the child results into a final answer.
After the run you get the finished tree as ASCII art (each node: name, provider/model, status, timing, truncated result), the root's final answer printed out, and a full JSON transcript — every prompt, every raw response, every timing — so the whole run is auditable.
The part I'm proudest of: --dry-run
swarm-tree run --tree tree.yaml --task "..." --dry-run Dry-run swaps every provider for a deterministic mock. No API keys, no cost, and the whole pipeline — decompose, parallel children, synthesize, transcript — runs exactly like the live path. It's how the 36 unit tests run, and it's how you demo the tool to someone without handing them your API keys.
What I learned building it
Decompose is a parsing problem. I ask the coordinator model for "a numbered list with exactly N items," and then I have to deal with the reality that models don't always comply. There's a small parser that extracts numbered items and a fallback that distributes raw lines when the numbering is off. Robustness lives in the fallback, not the prompt.
Partial transcripts matter more than clean exits. When a child agent fails mid-run, I wanted to still see everything that happened — the decompose call, the sibling results, the exact error. So child records are attached to the tree before threads start, and execute_tree always returns the root record even on failure. Debugging a failed run from its transcript is the whole point of having a transcript.
Five providers is really one client. Anthropic has its own messages API and Gemini has its own generateContent endpoint, but OpenAI, xAI, and DeepSeek all speak the chat-completions wire format. One _build function, one _extract function, urllib underneath. No SDKs means no dependency surprises and no version pinning.
The repo is MIT, the package is swarm-tree on PyPI (well — the dist is built, I'm uploading on my usual drip schedule), and I'd love to hear what tree shapes people actually want to run. The weirdest useful tree wins.
Update: token accounting, honest summary mode, and a review that humbled me
Two things shipped since the first version — plus a correction I owe you.
Token accounting. Every LLM call's input/output tokens are recorded in transcript.json, rolled up per node (own calls + descendants), and shown on the ASCII tree next to an estimated cost:
● orchestrator [anthropic/claude-sonnet-4-5] ok 0.0s 757/480 ~$0.004 There's a small built-in price table (marked approximate, overridable in config). --dry-run uses deterministic mock usage so the whole path works with zero API keys.
Summary mode, corrected. I originally pitched this as "saves tokens." A reviewer (Claude, who read the whole repo) proved that claim backwards: in full-context mode the parent already receives each child's capped synthesis, so nothing was ever re-read uncompressed — summary mode adds a call and total tokens go up, not down. The honest version, now shipped in v0.2.1:
- It's cost arbitrage, not token savings: a cheap child model compresses its output before an expensive parent reads it. Parent on Opus, leaves on DeepSeek — that's where it wins.
- It only summarizes long answers (over ~2400 chars); short ones pass through raw, because summarizing a 60-word answer into 150 words is pure waste.
- The summarize call is capped at ~300 tokens, and you can point it at a cheap model via
summarizer: {provider, model}instead of defaulting to the child's own model. -
--full-contextrestores the old behavior. There's even a test asserting the honest trade-off: summary mode wins on dollars while total tokens go up.
That correction stung, but it's exactly why I asked for the review. The same round also fixed: a SyntaxError on Python 3.9–3.11 (now CI-tested 3.9–3.13), max_completion_tokens for OpenAI reasoning models, Ctrl-C no longer losing the transcript, retries with backoff on 429s, a global concurrency cap (default 8 — an uncapped tree could fire 1000+ concurrent requests), fail-fast mode, and structured subtask parsing. v0.2.1, 125/125 tests green.
