Each is under ~100 lines of Python. Open any one - say
joke_agent.py - and you’ll see a small configuration that wires a language model (openai/gpt-4o-mini, dispatched through OpenRouter) to a few lines of instructions (“tell jokes, refuse other requests”). Narrow scope on purpose so mistakes are visible.
Gateway is already running from the previous chapter; don’t restart it.
A three-agent question
Paste this into your curl terminal. It asks something that genuinely needs three agents to answer:task.started events, in order - research first, then math, then poet. Abbreviated output from a real run:
How it chose
The planner saw three tools available (one per agent-skill combination):Where do those tool names come from? The gateway builds them automatically from the
name and skills[].id fields in your request: call_<agent-name>_<skill-id>.call_research_web_research. It waited for the reply, re-read the question with the new context, decided the next step was math, picked call_math_solve, and so on.
All of this happens inside one HTTP request. The SSE stream is the gateway narrating what the planner decided.
What if you added a fourth agent it doesn’t need?
Try it. Add the joke agent to the catalog above and re-run:task.started events for research, math, poet. The joke tool sat there unused.
The planner only calls what it needs. This matters in production: you can hand the gateway a catalog of 50 agents, and only the 2 or 3 relevant to a given question will actually be invoked.
What is the planner, actually?
Inside the gateway, there’s a single agent configuration file calledgateway/agents/planner.md. It’s a markdown file with YAML frontmatter — this is the real shape from the repo:
/plan request, the gateway does this:
1
Read the planner's system prompt from the cached agent registry.
The registry is built once at boot by scanning
gateway/agents/*.md. There’s no per-request reload — see gateway/src/agent/index.ts.2
Add the user's question as a new user message.
Plus any prior turns the client sent in
history (the gateway is stateless, so history travels on the request).3
Build the tool list from your agents[] catalog.
One tool per
agent.skill pair, named call_<agent>_<skill>.4
Hand all of that to OpenRouter with streamText().
Claude (or whatever model you picked) drives the loop.
5
Stream the output back to you as SSE.
Text deltas + tool calls + tool results.