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 176 unit tests run (including the 32 serve-mode ones), and it's how you demo the tool to someone without handing them your API keys.
New in v0.3: serve mode — a tree that stays up
run executes one task and exits. But I kept thinking about the people who want the tree resident — a CEO agent at the top, department agents below it, each running on its own schedule, the CEO rolling everything into a daily briefing. (If that shape sounds familiar: yes, I saw that viral CEO-agent dashboard video too, and it validated the idea.)
So v0.3 adds serve:
name: ceo
provider: anthropic
model: claude-sonnet-4-5
role: You are the chief-of-staff. Aggregate department briefings.
rollup: true
children:
- name: shop-agent
provider: deepseek
model: deepseek-chat
role: You watch shop metrics and flag anomalies.
schedule: 30m # interval: 30s | 5m | 2h | 1d
task: Check today's orders, refunds and GMV; flag anything unusual.
- name: research-agent
provider: gemini
model: gemini-2.5-flash
role: You track the market each morning.
schedule: "08:05" # daily at 08:05 local time
task: Summarize overnight market moves.
swarm-tree serve --tree tree.yaml --dry-run # resident loop, $0.00
swarm-tree serve --tree tree.yaml --once # one tick, then exit (for cron)
swarm-tree status --tree tree.yaml # schedule table, no keys needed
swarm-tree report --tree tree.yaml # latest daily briefing
The semantics stay boring on purpose: the first run fires immediately, then each node follows its schedule. Missed ticks are skipped, never backfilled — a restart resumes the cadence instead of replaying the backlog. State lives in serve-data/state.json, transcripts in serve-data/runs/<node>/, briefings in serve-data/report-YYYY-MM-DD.md. enabled: false takes a node out of rotation without deleting it.
New in v0.4: the governance trio
Running a tree 24/7 taught me three lessons fast, so v0.4 adds three small mechanisms — all per-node config, all stdlib, all visible in swarm-tree status:
Trigger condition. A scheduled node that polls an API every 30 minutes burns money to learn "nothing changed" 47 times a day. Now a node can carry trigger: — a shell command whose stdout gets hashed before each tick. Same hash as last time? The LLM call is skipped for $0.00. And a failing check fails open: the node runs anyway, because a broken sensor must never be able to silence an agent.
Owner action queue. Resident agents shouldn't make decisions that belong to a human — but they should prepare them. Any node output line starting with DECISION: gets harvested into a queue in state.json. Every briefing ends with a ## 待你拍板 (Owner Action Queue) section, and swarm-tree queue lists what's pending. Agents do everything up to the last click; I make the call.
Per-node budget. Each node can set budget_usd: — a cumulative lifetime cap, tracked in state.json across restarts. Hit the cap and the node's ticks skip with a budget-exceeded status until I raise it. It's the simplest possible circuit breaker, and status shows $spent/$budget per node so there's never a surprise bill.
Test count is up to 176, all green, still zero dependencies.
v0.5: the tree gets a website
The CLI was always a developer tool. v0.5 adds swarm-tree web — a local website (still stdlib-only: http.server + sqlite3, no frameworks, no new dependencies) where you sign up, configure trees in the visual editor, chat with them, and generate images.
What it does: accounts with salted/stretched passwords and signed session cookies; a tree editor built on the existing studio page with a Save button wired to the server; chat that runs your tree through the same engine the CLI uses, with a live progress stream and full transcripts per run; image generation via OpenAI (gpt-image-2.5-flare) or Gemini (gemini-3.1-flash-image) with PNGs saved locally.
The part I thought hardest about was API keys. The web app is BYOK: you paste your own provider keys into Settings, they're stored in a 0600 SQLite file, used server-side only, and never rendered back or logged. Chat and image calls spend your keys, per request. The README is honest about the limits: for anything beyond localhost, terminate TLS in front, pull the session secret from a real secret manager, and consider a KMS for the keys.
Test count is up to 197, all green — 21 new ones covering the web layer (auth, sessions, ownership checks, and HTTP smoke tests with mocked providers; no live API calls in tests).
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.
Top comments (0)