Production Agent Architecture: The Coordinator Subagent (Part 2)
Lessons Learned While Preparing for the Claude Architect Certification
From one agent to many
In Part 1, our customer support agent grew a second tool without much effort. Once get_order_status existed, adding get_return_policy was just one more entry in the tools list. The loop already knew how to call any tool and use the result. Nothing about the control flow had to change.
Now the product team wants more. Users also want to know if they could still get a better price on what they bought. That means a new tool, get_price_history. It also means a new policy to explain, plus new edge cases. For example: what if the order is too old for price data to exist?
Nothing stops you from adding this as a third tool on the same agent. But a few problems start to show up:
- The system prompt now covers three topics at once: shipping, returns, and pricing. The model has to keep all of this in mind on every turn, even for a question about just one topic.
- Tool descriptions from different topics sit in the same list and start to compete for attention. This can make the model’s tool choice less accurate.
- A bug in the pricing logic can now break a conversation that was only about shipping. Everything shares the same context and the same loop.
- The problem doesn’t stop at three tools. Every new feature adds the same kind of strain again, and worse.
A bigger prompt doesn’t fix this. The real fix is structural. You split the single agent into a coordinator and a set of subagents. The coordinator only delegates work and combines the answers. Each subagent is its own agentic loop from Part 1, focused on one topic, with its own tools and its own separate context.
This is the coordinator-subagent pattern. It isn’t a new kind of loop. It’s the same loop from Part 1, used one level deeper.
A pseudocode example
coordinator_loop(user_message):
messages = [user_message]
loop:
response = call_model(messages, tools=SUBAGENT_TOOLS)
if response.stop_reason == "end_turn":
return response.text
if response.stop_reason == "tool_use":
tool_results = []
for tool_call in response.tool_calls:
result = run_agentic_loop(tool_call.subtask)
tool_results.append(tool_result(tool_call.id, result))
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
continue
raise UnhandledStopReason(response.stop_reason)
This looks a lot like Part 1’s pseudocode. The only real change is what a “tool” does.
In Part 1, a tool call reached into a fake backend and returned a small dict. Here, each tool call reaches into run_agentic_loop() — the same function from Part 1. It returns whatever that inner loop produces once it reaches its own end_turn.
The coordinator doesn’t know, and doesn’t need to know, that its “tool” is actually a full conversation with the model. From the coordinator’s side, it’s just a function: send a question, get an answer back.
One more detail is worth pointing out. The loop collects every tool_use block from a single response before it calls the model again. So the coordinator can ask for several subagents in the same turn. What you do with that — run them one after another, or run them at the same time — is exactly the “parallel vs. sequential” question below.
The example project (coordinator_subagent_minimal.py) shows this in real code. run_agentic_loop() is written once. The coordinator, the order subagent, and the pricing subagent all call through it. Only the system prompt, the tools, and the tool implementations change between roles.
Critical design decisions
In Part 1, the hard question was: when is a single loop done? In Part 2, the hard question is different: how many loops do you need, and how do they talk to each other?
- Task decomposition: How does the coordinator decide what to delegate, and to whom? In the example project, this is entirely up to the model’s judgment, based on the tool descriptions (
consult_order_subagentvs.consult_pricing_subagent). In a real system, a vague tool description can easily make the coordinator delegate too much or too little — or decompose the topic too narrowly to begin with. More on that below. - Context isolation: What does each subagent actually see? This is the key point the code shows: every subagent starts with a fresh, narrow message history. It only gets the question it was given, not the coordinator’s full conversation. That’s why the pricing subagent never sees order data it doesn’t need: smaller context, narrower tool access, less room for mistakes.
- Communication protocol: What shape does a delegation and its answer take? The example project keeps it simple: a plain text question in, a plain text answer out. Bigger systems often need structured data instead. Then the coordinator can react to a partial failure directly, instead of reading it out of a sentence.
- Parallel vs. sequential execution: Can subagents run at the same time? A single response can hold more than one
tool_useblock, so independent subagents could in principle start together, before either one finishes. But if one subagent needs another one’s result first, they still have to run in order. - Error propagation: What happens if a subagent fails? It might run out of steps, hit a tool error, or get a question it can’t answer. Does the coordinator try again with a different question? Continue with a partial answer? Tell the user something went wrong? If you don’t decide this in advance, the coordinator can end up giving a confident answer built on missing information.
- Aggregation and synthesis: How does the coordinator turn several subagent answers into one clear response? This takes real reasoning, not just formatting. If two subagents give answers that don’t fully agree, something has to sort that out — in this case, the coordinator’s own model call.
- Depth and cost control: What stops a subagent from becoming a coordinator itself, and starting its own subagents again and again? By default, nothing does. You need a clear depth limit, a rule that says “this subagent can’t delegate further,” and a cost or token budget per branch. These are decisions the architect has to make on purpose.
When two subagents aren’t enough
The order and pricing example only has two subagents, and with two subagents decomposition can’t really go wrong — there’s nowhere else to send a question. The moment you add a third, a different kind of problem shows up. Not a bug in the code. A gap in the decomposition itself.
The example project adds that third subagent: returns. Same shape as the other two — its own tool, its own isolated context, its own narrow job.
Now ask the coordinator a question that touches all three topics: “Where’s my order A1001, is it still a good price, and can I return it if I don’t like it?”
If the coordinator’s system prompt and tool list were only ever built around order and pricing, the third part of that question just disappears. Nothing crashes. Nothing errors out. The coordinator confidently answers two-thirds of the question and never mentions the part it had no subagent for. From the user’s side, the missing return policy doesn’t look like a failure — it looks like it was never asked about.
That’s the real risk of narrow decomposition: not that the coordinator makes a wrong call, but that its own tool list quietly becomes a ceiling on what it can ever cover. run_narrow_coordinator() reproduces exactly this. Give it the three-topic question above and watch the third topic vanish from the answer.
The fix isn’t to give the coordinator every subagent “just in case.” That just means synthesizing three answers for a question that only needed one. The real fix is two things done well: build a subagent set that actually spans the domain, and let the coordinator choose dynamically which of those subagents a given question needs — not a fixed subset, and not all of them by default.
run_refining_coordinator() shows both halves of that. It has all three subagents available, but its system prompt tells it to call only the ones a question actually touches. It also tells the model to check its own draft answer against the original question before finishing: if a part is still unaddressed, delegate again instead of stopping.
That second instruction is what an iterative refinement loop really is. It isn’t a new mechanism bolted onto the architecture — it’s the same loop from Part 1, given a reason to use more than one round. The coordinator was always allowed to call a subagent, look at the result, and call another one before finishing. Telling it to check its own coverage just makes it actually do that, instead of stopping the first time it has something to say.
One more thing worth keeping in mind as a system grows: each subagent should own a distinct, non-overlapping slice of the domain. Order status, pricing, and returns don’t overlap here, so there’s no risk of two subagents doing the same work twice. In a research-style system with more subagents and broader topics, that’s not automatic — partitioning the scope so subagents don’t duplicate each other’s work is its own design decision, the same way task decomposition is.
Consequences
Skipping these decisions doesn’t make the system simpler. It just hides the problems until they show up somewhere else.
- No decomposition rule -> the coordinator delegates a simple question that never needed a subagent, wasting time and cost. Or it hands a subagent work it was never scoped to handle.
- No coverage check -> the coordinator stops as soon as it has something to say, not once it has addressed everything the user asked. Parts of a multi-topic question silently disappear from the answer, and nothing signals that they were dropped.
- No context isolation -> subagents start sharing context they shouldn’t. This defeats the whole point of splitting them apart. A bug in one topic can now affect an answer about a different topic, the same way it would in a single overloaded agent.
- No clear communication protocol -> the coordinator has to guess, from plain text, whether a subagent actually answered the question. A response that sounds fine but says nothing useful can pass as a success.
- No sequencing rule -> dependent subtasks run before the data they need is ready. Or independent subtasks run one after another for no reason, wasting time.
- No error propagation rule -> a subagent can fail quietly, and the coordinator still gives a confident final answer. The user can’t tell the difference between a correct answer and a wrong one built on missing data.
- No aggregation rule -> the “final answer” is just the subagent answers glued together, contradictions included. The user is left to sort it out.
- No depth or cost control -> a subagent that keeps spawning more subagents multiplies the cost and the time at every level, with no limit. A bill or a timeout catches the problem before the architecture does.
These decisions don’t replace the ones from Part 1. Every subagent is still its own loop, and it still needs its own rules for stopping and handling errors. Orchestration adds a second layer on top of that. It doesn’t remove the first one.