I drew a tree of AI agents on my phone and then built it: meet swarm-tree
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

Posted on Oct 23 • Originally published at github.com
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.
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... Enter fullscreen mode Exit fullscreen mode Then:
swarm-tree run --tree tree.yaml --task "Design a weekend hackathon project" Enter fullscreen mode Exit fullscreen mode The semantics are dead simple:
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.
swarm-tree run --tree tree.yaml --task "..." --dry-run Enter fullscreen mode Exit fullscreen mode 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.
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.
Two features landed since the first version, both aimed at the same enemy: context burn.
Token accounting. Every LLM call's input/output tokens are now recorded in transcript.json, rolled up per node (own calls + descendants), and shown on the ASCII tree:
● orchestrator [anthropic/claude-sonnet-4-5] ok 0.0s 757/480 Enter fullscreen mode Exit fullscreen mode You can see at a glance which branch is eating your quota. --dry-run uses deterministic mock usage so the whole path works with zero API keys.
Summary mode (on by default). This is the real fix for the re-read problem: after each child subtree finishes, the parent makes one short summarize call to that child's own model (~150 words: what was accomplished, key decisions, where the full output lives). The parent's synthesize prompt then receives only these summaries plus pointers — never the raw child transcripts. Full outputs stay in transcript.json, nothing is lost, but the parent stops paying to re-read everything. If you ever want the old behavior, --full-context brings it back.
The math that motivated it: one 150-word summary call per child vs. re-reading every child's full output at every level of the tree. For a 3-level tree, that's the difference between linear and roughly quadratic token spend.