GoAgent Source Deep Dive 03: Agent System — The Leader/Sub-Agent Collaboration Skeleton
GoAgent Source Deep Dive 03: Agent System — The Leader/Sub-Agent Collaboration Skeleton
The Problem: One Agent Can't Handle Everything
You've built an agent with an LLM that understands user input, calls tools, and returns results. But you quickly discover — when a user says "plan a trip to Tokyo, budget 5000, and recommend some restaurants," a single agent trying to do itinerary planning, budget calculation, restaurant recommendation, and information retrieval all at once either becomes impossibly complex or does each task poorly.
You need to split tasks across different agents. But who splits them? Who coordinates the results?
Limitations of Existing Approaches
Approach A: One giant agent with if-else
A monolithic agent with branching logic. Problems: hard to test, tasks run serially, adding capabilities requires modifying core agent code.
Approach B: Let the LLM decide everything via function calling
Let the LLM dynamically choose tools. Problems: LLM decisions are unpredictable, no task decomposition, result aggregation depends entirely on the LLM with no engineering guarantees.
Both approaches lack an orchestration layer — a component responsible for understanding input, decomposing tasks, dispatching execution, and aggregating results.
GoAgent's Approach
The Leader/Sub-agent pattern:
- Leader Agent is the orchestrator, not the executor. It understands input, plans tasks, dispatches to Sub Agents, aggregates results.
- Sub Agent is the execution unit, not the decision maker. It receives a concrete task, calls LLM and tools to complete it.
This division isn't a "designed architecture" — it naturally emerges from the task decomposition requirement: decomposition implies separation of planner and executor.
Orchestrator] Leader --> Parse[Parse User Profile] Parse --> Plan[Plan Task List] Plan --> Dispatch[Parallel Dispatch] Dispatch --> SubA[Sub Agent A
Itinerary] Dispatch --> SubB[Sub Agent B
Restaurants] Dispatch --> SubC[Sub Agent C
Budget] SubA --> Results[TaskResults] SubB --> Results SubC --> Results Results --> Aggregate[Aggregate] Aggregate --> Output[Final Output]
Architecture Naturally Emerges
Interface Hierarchy
// base.Agent — foundation for all agents
type Agent interface {
ID() string
Type() models.AgentType
Status() models.AgentStatus
Start(ctx context.Context) error
Stop(ctx context.Context) error
Process(ctx context.Context, input any) (any, error)
}
// sub.Agent — adds Execute
type Agent interface {
base.Agent
Execute(ctx context.Context, task *models.Task) (*models.TaskResult, error)
}Leader's Four Replaceable Components
Leader isn't an "all-powerful LLM object" — it's an orchestrator depending on four strategy interfaces:
type ProfileParser interface {
Parse(ctx context.Context, input string) (*models.UserProfile, error)
}
type TaskPlanner interface {
Plan(ctx context.Context, profile *models.UserProfile, inputText string) ([]*models.Task, error)
}
type TaskDispatcher interface {
Dispatch(ctx context.Context, tasks []*models.Task) ([]*models.TaskResult, error)
RegisterExecutor(agentType models.AgentType, fn TaskExecutorFunc)
}
type ResultAggregator interface {
Aggregate(ctx context.Context, results []*models.TaskResult, tasks []*models.Task) (*models.RecommendResult, error)
}Dispatcher: The Concurrency Core
func (d *taskDispatcher) Dispatch(ctx context.Context, tasks []*models.Task) ([]*models.TaskResult, error) {
g, ctx := errgroup.WithContext(ctx)
sem := make(chan struct{}, d.maxParallel)
var resultsMu sync.Mutex
results := make([]*models.TaskResult, len(tasks))
for i, task := range tasks {
task := task
g.Go(func() error {
select {
case sem <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
defer func() { <-sem }()
execResult := d.executeTask(ctx, task)
resultsMu.Lock()
results[i] = execResult
resultsMu.Unlock()
return nil
})
}
// ...
}
Three concurrency decisions: errgroup for cancellation propagation, semaphore for bounded parallelism, mutex for safe result collection.
Task Planning Pragmatics
The planner matches trigger keywords to sub-agents. If no match and fallback is enabled, all sub-agents are dispatched. Better to over-dispatch than leave input with no response.
Design Trade-offs
- Strategy pattern vs inheritance: Composition + interfaces, replaceable at runtime.
- errgroup vs worker pool: errgroup has built-in context cancellation; semaphore limits concurrency on-demand.
- Local execution vs message queue: Both supported, auto-selected based on whether
executorFuncsormessageSenderexists.
Summary
The Agent system isn't "mysterious intelligent agents" — it's an engineering-driven task orchestration pipeline. Leader plans and orchestrates, Sub Agent executes, Dispatcher handles concurrency. Architecture wasn't pre-designed — it naturally emerged from the task decomposition requirement.