The Problem
Complex tasks — writing a research report, generating a codebase, planning a project — are too large for a single agent context window and too diverse for one generalist prompt. A monolithic agent either produces shallow results or runs out of tokens trying to do everything at once.
The Solution
The Orchestrator-Subagent pattern decomposes the task into focused subtasks, each handled by a specialized subagent. The orchestrator coordinates: it fans out independent subtasks concurrently using Go’s sync.WaitGroup, collects the results, and sequences dependent steps. Each subagent is a narrow expert — it knows nothing about the other subagents, only its own task.
Structure
Task Decomposition
The orchestrator receives the raw task and decides which subagents to invoke and in what order. Decomposition logic lives here — subagents are unaware of the broader goal.
flowchart TD User["User Task"] Orch["Orchestrator"] Fan["fanOut() — concurrent"] Researcher["ResearchAgent"] Summarizer["SummarizerAgent"] Context["Aggregate Context"] Writer["WriterAgent"] Output["Final Output"] User --> Orch Orch --> Fan Fan -->|"goroutine"| Researcher Fan -->|"goroutine"| Summarizer Researcher --> Context Summarizer --> Context Context --> Writer Writer --> Output
Implementation
package main
import "context"
// Agent is a unit of autonomous work that accepts a task and returns a result.
type Agent interface {
Name() string
Run(ctx context.Context, task string) (string, error)
}
// SubtaskResult holds the output from one subagent.
type SubtaskResult struct {
AgentName string
Output string
Err error
} Real-World Analogy
A film production: the director (orchestrator) assigns the cinematographer, sound engineer, and production designer to work in parallel on their specialties. The director then synthesizes their outputs into the final cut. No specialist needs to understand the others’ work — only the director holds the full picture.
Pros and Cons
| Pros | Cons |
|---|---|
| Parallel subagents dramatically reduce wall-clock time | Orchestration logic adds complexity — requires careful error handling |
| Each subagent has a narrow, focused context — higher quality output | Subagent failures must be handled explicitly; partial results can mislead synthesis |
| Easy to add new specializations without changing the orchestrator core | Debugging multi-agent failures requires tracing across multiple LLM calls |
| Maps naturally to Go’s goroutine + WaitGroup concurrency model | Context aggregation must be designed carefully to avoid token bloat |
Best Practices
- Use
errgroupfromgolang.org/x/sync/errgroupinstead of a manualWaitGroupfor cleaner error propagation in the fan-out phase. - Give each subagent a timeout via
context.WithTimeout— a stalled LLM call should not block the orchestrator indefinitely. - Log the input task and output of every subagent at INFO level; the full trace is essential for debugging multi-agent workflows.
- Keep subagent prompts self-contained — pass all necessary context in the task string, not via shared global state.
- Design the aggregation step to handle partial results gracefully; prefer degraded output over a hard failure when one subagent errors.
When to Use
- Research, writing, or analysis tasks that benefit from specialization and parallelism.
- Workflows where the total token count exceeds a single model’s context window.
- Systems where different subtasks need different model temperatures, tools, or personas.
When NOT to Use
- Simple linear tasks — a single agent loop is easier to reason about.
- Tasks where subagent results are tightly interdependent and can’t be parallelized.
- Low-latency applications where spinning up multiple LLM calls is too expensive.