CONCEPT · 60 minutes
Split a monolithic triage agent into three scoped micro-agents, wire them together with a shared context dict, and learn when the overhead is worth it.
By the time the support-triage agent from prior modules reached production use in the de-identified support-automation platform, it was holding a lot at once: the system prompt, the incoming ticket, the conversation history, the knowledge base snippets retrieved for context, and the escalation rules. Every request forced the model to juggle all of it before it could write a single word. The context window was half-full before the first user message arrived.
The agent worked. But when retrieval failed — a stale KB index, a timeout, a malformed article — I had no idea where the breakdown happened. The classify logic, the retrieval, and the reply were all entangled in one call. Every failure looked the same from the outside: the response was wrong. Debugging a function that did everything was slow and unpredictable.
The fix was not a new prompt. It was splitting the agent into three smaller ones.
Factor 6 — Build Small, Focused Agents. An agent that does one thing can be tested in isolation, replaced without rebuilding the whole pipeline, and observed without sifting through unrelated output.
Think of micro-agents like microservices. Just as you would not put authentication, billing, and email delivery into one service, you do not need one agent that classifies tickets, retrieves KB articles, and composes responses. The independence is a trade-off — you gain debuggability and replaceability, but you pay in coordination overhead. The question is not "should I always use micro-agents?" It is "does this boundary earn its keep?"
The orchestration framework I use for the SDLC harness answers that question every run. The harness decomposes a task spec into three micro-agents — plan, implement, review — each responsible for exactly one job. When a review step fails, I can replay it in isolation with the same context. That replay speed is the return on the coordination overhead investment. When I was building the support-automation platform, the same pattern applied to the triage flow: classify, retrieve, respond are three distinct jobs that benefit from being independently observable and independently testable.
By the end of this module you will be able to:
Here is the core of the monolithic triage agent. Notice how much it tries to do in one prompt:
This works until retrieval breaks. When the KB index is stale, the classify logic breaks with it. When the reply format changes, the classification prompt changes too. Everything is coupled — one function doing three jobs.
There is a subtler problem too. The monolith asks the model to do three distinct cognitive tasks in one pass — categorize, select, compose — inside a single prompt instruction. Models are good at each of these tasks individually. Asking for all three at once means the prompt has to be long enough to cover each task well, and the output format has to encode all three results simultaneously. In practice, the model often gets the classification right but the reply wrong, or gets both right but formats the JSON inconsistently. There is no way to tell which task failed without reading the output carefully.
Split it into three agents. First, the classifier:
One model call, one structured output. The classifier has no knowledge of KB articles or reply format — its contract is ticket-in, category-out.
Next, the retrieval agent, which receives only the category:
The retrieval agent does not know about tickets or replies. It receives a category and an index, and returns articles. If the KB index is empty, it returns an empty list — and that failure is isolated from everything else.
Isolation is not just about debugging. It also means the retrieval agent can be called from other contexts — a search interface, a supervisor agent, a batch job that pre-fetches articles for common categories. When an agent has a single, well-defined contract, it becomes reusable outside the pipeline it was built for.
Finally, the response agent:
The respond agent receives its inputs explicitly — ticket, classification, articles — rather than deriving them itself. Its job is composition, not retrieval or classification.
What splitting bought us: Each agent can be tested with mock inputs, replaced with a different model, or retried on failure — without touching the others. When a retrieval failure happened in production, I could re-run just the retrieve agent with the same ticket category and reproduce the problem in isolation.
In the support-automation platform, this split exposed a bug I had missed for weeks. The monolith was occasionally returning replies that ignored the most relevant KB article and defaulted to a generic answer. After splitting, I ran classify and retrieve in isolation on a batch of recent tickets and discovered the retrieval agent was consistently returning the wrong articles for the "billing" category — the KB index had a naming inconsistency between the category the classifier returned and the key the retrieval index used. The bug was invisible in the monolith; it became obvious when I could run the retrieval agent alone and inspect its output.
Three agents that run independently are useful. Three agents that share a context dict are a pipeline. The key principle is that each function should be stateless and replayable — the state lives in the dict, not inside the function.
Factor 7 — Stateless Agent Design. Each micro-agent is a pure function: given the same inputs, it produces the same output. The context dict is the mutable state. You can replay any step by feeding it the same slice of context.
Here is the pipeline runner:
The dict carries everything forward. If classify fails, retrieve still runs against the fallback category. If retrieve fails, respond still runs with empty articles. Each step's output is visible in the final context — you can inspect exactly what each agent saw and returned.
Notice the error handling choice here: errors are collected in a list, and the pipeline continues rather than stopping at the first failure. This is a deliberate trade-off. The alternative — stopping on the first exception — would mean that a classify failure prevents any reply from being generated. For a support system, a degraded reply is better than no reply. If your system has different priorities — for example, if a wrong classification is worse than no response — you would want to raise instead of append and stop the pipeline early.
This replayability is what made the SDLC harness debuggable. When a plan→implement→review pipeline failed at the review step, I could re-run just the review agent with the same context dict and reproduce the failure in isolation. The whole system became a sequence of pure functions threaded through an explicit state object — not a tangle of interdependent calls.
Think of the context dict like a patient chart that travels with each step. Without it, every agent starts from scratch; with it, each agent inherits the full picture of what came before.
Before moving on: if classify returns the wrong category — say "general" for a billing dispute — the retrieve step will pull irrelevant articles, and the respond step will write an off-topic reply. The error propagates silently.
What would you add to catch this cascade early? Consider: a validation step after classify, a confidence field in the classification output, or a fallback to a different model. There is no single right answer. Think about where you would put the check and what the cost of adding it would be compared to the cost of letting an off-topic reply reach a customer.
Splitting into three agents gains debuggability — but only if each agent logs its inputs and outputs. Structured logging is what makes a micro-agent system observable.
This start/success/error pattern — with structured extras — makes it easy to trace a ticket through all three agents using log correlation. In the support-automation platform, every failed ticket had a ticket_id in the logs so I could replay the full trace in under a minute.
Each field in the structured extras carries specific diagnostic value:
ticket_length at start reveals whether long or complex inputs correlate with classification failures — a useful signal when some ticket formats trip the model more reliably than short onescategory and urgency at success are stored as top-level extras rather than nested inside the response JSON, so a single log query returns all high-urgency billing tickets without parsing JSONduration_ms enables latency alerting: if classify consistently exceeds two seconds, the model is receiving more context than it needs and the prompt should be trimmederror on failure distinguishes a JSON parse error (the model returned an unexpected format) from a network timeout (the API was unreachable), which require different remediation pathsTo thread a ticket across all three agents, pass a ticket_id through every extras dict.
A single log query on that ID returns the full classify→retrieve→respond trace in timestamp
order, without sifting through interleaved output from other concurrent tickets.
That correlation is the payoff of consistent structured logging across all three agents.
In the support-automation platform, adding ticket_id to every log call was a one-hour
change that cut mean time to diagnose a failed triage from twenty minutes to under two.
Apply the same pattern to the retrieve and respond agents. The specific extras will differ — retrieve logs the category it received and the number of articles returned; respond logs the reply length and whether the KB was empty — but the start/success/error shape stays the same. Consistent structure means a single log query can show you the full timeline for any ticket, in order, without guessing what each log line means.
Now add a circuit breaker to the retrieval agent. A circuit breaker trips after repeated failures and returns a safe default rather than hammering a broken downstream service:
A circuit breaker has three states: closed (normal operation), open (failing fast), and half-open (testing whether the service has recovered). The implementation below uses the closed and open states. The 60-second reset window is the half-open mechanism — after that window, the next call goes through to test whether the retrieval service is back. If it succeeds, the breaker resets to closed. If it fails again, the window extends.
When the KB service is down, the circuit breaker opens after three failures and returns empty lists immediately. The respond agent already handles empty articles — it says "No KB articles available" and writes a general reply. The system degrades gracefully rather than hammering a broken service and waiting for timeouts on every ticket.
The key design insight here is that the circuit breaker's safe default — an empty list — is the same value the retrieval agent would return for an empty KB index. The respond agent was already written to handle that case. Adding the circuit breaker required no changes downstream. When you design each agent to handle its zero-value inputs gracefully, adding reliability patterns like circuit breakers becomes straightforward rather than requiring cascading changes.
Circuit breakers are observable, not invisible. The logger.warning("circuit_breaker.tripped") call is not optional. Without it, the breaker silently swallows failures and you will not notice the retrieval service is down until customers start complaining.
Factor 12 — The Bleeding Edge Is Costly. Adding architecture before you have a problem is overhead, not engineering. Measure before you split.
When I applied the micro-agent pattern to the SDLC harness, I split a single "implement and review" step into plan, implement, and review micro-agents. The debuggability gain was real — failed review steps were reproducible in isolation. But the coordination overhead was also real:
For the triage pipeline, the monolith handled a simple billing inquiry in one call in about two seconds. The three-agent pipeline takes three to five seconds. That is acceptable at the support-automation platform's scale because the debuggability gain is worth the latency cost. For a simple FAQ bot serving low-stakes questions, it would not have been worth it.
Reflective prompt: When would a single linear agent beat three micro-agents? What is the cross-over point for your current project? Consider: ticket volume, failure rate, time spent debugging per incident, and the cost of added latency. If you have never had to debug a multi-step failure in production, start with the monolith and split only when you have a failure mode that demands it.
The micro-agent pattern earns its overhead when:
It does not earn its overhead when:
A useful heuristic: if you have had to debug a production failure twice and each time you could not immediately pinpoint which step failed, that is the signal to split. If you have never had a production failure in this pipeline, keep the monolith and invest the saved complexity budget somewhere else.
Across five modules, the support-triage system grew from a raw JSON extraction call into a three-agent pipeline with explicit state, per-agent logging, a circuit breaker, and a human escalation gate. Each layer was earned by a concrete failure mode:
The 12-factor framework is not a checklist to apply all at once. It is a set of answers to specific problems that appear when agents reach production. The goal is to recognize the problem, apply the right factor, and measure whether the fix was worth it. That judgment — when to add complexity and when to resist it — is the engineering work.
Application-level questions on micro-agent architecture and the trade-offs of splitting agents.
1. The classify step returns `{"category": "general", ...}` for a billing dispute. The retrieve step uses that category to look up KB articles. What happens next?
Correct Answer: B
A wrong category is not an exception — it is a logic error. The pipeline continues normally, each step trusting the previous one. Silent propagation is the key risk in pipelines: a wrong intermediate output produces a wrong final output with no error raised. This is why mid-pipeline validation or a confidence field on classification output can be worth adding.
2. Factor 7 says each micro-agent should be stateless between invocations. What does this mean in practice for the classify function?
Correct Answer: B
Stateless means pure: same inputs, same output. The classify function reads only its argument (ticket) and the Anthropic API. It does not read from or write to a mutable cache, session, or global. This makes it replayable — you can re-run it with the same ticket and reproduce the result — which is the foundation of debuggable pipelines.
3. When `retrieval_breaker.is_open()` returns True, what does retrieve_articles return?
Correct Answer: B
The circuit breaker returns [] immediately — the same value that would result from an empty KB index. The respond agent already handles empty articles gracefully. This means the pipeline degrades without crashing: tickets still get replies, just without KB support, until the retrieval service recovers and the breaker resets.
4. What is the main benefit of passing a shared context dict through the pipeline rather than having agents call each other directly?
Correct Answer: B
When agents call each other directly, state is implicit — hidden inside call stacks and return values. When state flows through an explicit dict, you can print it at any point, serialize it to disk, or feed it to a replay runner. This explicitness is what makes micro-agent pipelines debuggable rather than just fast.
5. When would a single monolithic agent be a better choice than three micro-agents?
Correct Answer: B
Micro-agents earn their coordination overhead only when the failure isolation, independent replaceability, or multi-context reuse is worth the latency and maintenance cost. If you have never debugged a multi-step failure in production, the monolith is the right starting point. Split when you have a failure mode that demands it.
6. In the pipeline runner, `ctx['errors']` is a list and each step catches its own exception and continues. What would happen if classify raised instead of appending to errors?
Correct Answer: B
If any step raises and the exception is not caught by the pipeline runner, the caller receives an exception rather than a context dict. The errors-list pattern is a deliberate degradation strategy: the system produces a fallback reply even when individual steps fail. Whether to use this pattern depends on your SLA — sometimes you want a hard fail, not a silent degradation.
7. You want to add an escalation router: a micro-agent that reads the classification output and decides whether to send the ticket to a human or let it proceed to respond. Where does it belong in the pipeline?
Correct Answer: B
The escalation router needs the classification output (category and urgency) to decide whether human review is needed. Running it before respond but after classify — and potentially after retrieve, if the KB articles influence the decision — gives it the right information. Running it in parallel with classify would mean it cannot use the classification result.
8. Why is `logger.warning('circuit_breaker.tripped', ...)` described as non-optional in the retrieve agent?
Correct Answer: B
An open circuit breaker is invisible if it does not emit a log event. The pipeline continues producing replies, but without KB support. Without the warning, you would see subtly wrong replies in customer complaints before you noticed the retrieval service was down. Observable degradation is what separates a well-engineered failure mode from a silent one.
Nice work. Here's what's next.
Back to this pathOr book the diagnostic call