
The Short Answer
Running sub-agents in parallel means their outputs land in no guaranteed order and with no guaranteed shape. One returns an array, another returns an object, and a third might silently fail. The aggregation reducer pattern solves this by funneling every envelope through a single typed merge function before the root agent sees any result.
Why This Matters Now
Claude Code documentation distinguishes between single-session sub-agents and background agents that run as independent sessions. Background agents are explicitly designed for parallel fan-out, with results collected centrally. AWS recommends the same approach under the name 'scatter-gather pattern,' combining distributed execution with intelligent result synthesis.
The synthesis step is where things break. Without a typed reducer, a simple spread or Object.assign silently lets later arrivals overwrite earlier ones. You lose the ability to trace which agent wrote which key and whether any partial failure affected the final answer. A dedicated reducer layer eliminates this class of bug structurally.
Step-by-Step Implementation
-
Define an envelope schema: every sub-agent wraps its response in
{ agentId: string, status: 'ok' | 'error', payload: unknown, ts: number }before returning. -
Wrap at the agent boundary: each sub-agent is responsible for producing a valid envelope. The root agent receives an array of envelopes, not raw payloads.
-
Write the reducer: accept an envelope array, validate each entry, apply merge rules, and return a typed result. In TypeScript:
envelopes.reduce<AggregatedResult>((acc, env) => ..., emptyResult). -
Declare conflict-resolution rules explicitly: for each key, choose one of sum, last-write-wins, union (array merge), or intersection. Example:
const rules = { issues: 'union', riskScore: 'max', checkedAt: 'max' }. -
Handle partial failures gracefully: envelopes with
status: 'error'are skipped inside the reducer and appended to anerrorLogarray. IncludepartialFailures: numberin the final output so the caller can decide whether to retry.
Real-World Example
Scenario: three sub-agents analyze different parts of a codebase simultaneously — one for security issues, one for performance bottlenecks, one for dependency risks.
Envelope from the security agent: { agentId: 'security', status: 'ok', payload: { issues: ['SQL injection risk x2'] }, ts: 1718260000 }
Reducer pseudo-code: iterate envelopes, skip error envelopes into errorLog, call mergeByKey(acc.result, env.payload, rules) for each ok envelope, return { result, errorLog, partialFailures: errorLog.length }.
If the performance agent times out, the security and dependency results still merge cleanly. The root agent reads partialFailures: 1 and decides whether to requeue the failed agent.
Common Mistakes
Spreading payload directly without type validation is the most frequent error. If one agent sends null instead of an array, the spread silently clears existing keys. Validate the envelope schema at the reducer's entry point before any merge logic runs.
Omitting explicit conflict-resolution rules and defaulting to last-write-wins is equally risky. Agent completion order varies with network conditions, so the last arrival is not guaranteed to be the most accurate.
Letting the reducer throw an unhandled exception wipes all partial results. Wrap each envelope's processing in a try-catch, push the failed agentId to errorLog, and continue with the remaining envelopes.
Checklist
- All sub-agents produce envelopes matching the shared schema
- The reducer validates each envelope's type at entry
- Conflict-resolution rules are declared explicitly per key
- Error envelopes are skipped and logged, not propagated as exceptions
- The final result includes a partialFailures count
- The reducer never throws; it always returns a result object
- An empty input array returns a safe empty result, not an error
FAQ
Q. Does reducer performance degrade as the number of sub-agents grows?
The array iteration is O(N) where N is the number of envelopes. At the scale of dozens of agents, the reducer is almost never the bottleneck — agent execution time is. If per-envelope processing is slow, the conflict-resolution rules are likely too complex; simplify them first.
Q. Can this pattern work when each agent returns a completely different payload structure?
Yes. Fix the outer envelope fields (agentId, status, ts) as a shared contract, then dispatch to agent-specific merge handlers inside the reducer based on agentId. For example: if (env.agentId === 'security') mergeSecurityPayload(acc, env.payload). This keeps the entry point uniform while allowing heterogeneous payloads.
Q. What should the reducer return when every agent has failed?
Return an empty result object alongside a full errorLog and a partialFailures count equal to the total agent count. The root agent compares these numbers and routes to a full retry or user-facing error report. The reducer itself must not throw — its contract is always to return a structured object.
Wrapping Up
The aggregation reducer is the layer that makes parallel sub-agent architectures trustworthy. By fixing an envelope schema, validating types at entry, declaring conflict rules, and isolating partial failures, the root agent gains a reliable foundation for its next decision. As the number of agents scales up, only the reducer needs updating — not every caller that depends on the merged result.
🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Advanced
💌 Subscribe: Follow on X or grab the RSS
댓글
댓글 쓰기