Two-Model Collaboration in Reasonix: How the Coordinator Pattern Orchestrates Dual LLM Agents
Reasonix implements two-model collaboration by wiring a lightweight planner LLM and a high-capacity executor LLM into a single Coordinator that maintains separate sessions and orchestrates two-phase turns to minimize token costs while maximizing reasoning quality.
The DeepSeek-Reasonix repository supports both single-model and dual-model execution modes. When the planner_model configuration is set, the system instantiates a Coordinator that manages two distinct language models—a planner and an executor—enabling sophisticated task decomposition while preserving context isolation. This article explains the three architectural pillars of the Coordinator pattern: session separation, two-phase turns, and tooling restrictions.
What Is the Coordinator Pattern?
The Coordinator pattern transforms Reasonix from a single-agent architecture into a two-model collaboration system. Instead of one LLM handling both reasoning and execution, the Coordinator delegates high-level planning to a dedicated, often smaller model, while reserving the heavy lifting for a separate executor model.
This design addresses token economy and context stability. The planner operates on a low-frequency schedule with a cache-stable system prompt, while the executor handles high-frequency tool calls and file modifications. The pattern is implemented in internal/agent/coordinator.go, with assembly logic residing in internal/boot/boot.go.
Core Components
- Planner Model: Configured via
cfg.Agent.PlannerModel, this LLM reasons about what actions to take using read-only tools. - Executor Model: The primary LLM (often larger) that performs writes, bash commands, and MCP operations.
- Coordinator: The orchestration layer that manages session state, emits phase events, and formats hand-off messages between models.
How Two-Model Collaboration Works
The Coordinator pattern rests on three architectural constraints that prevent context pollution and optimize token usage.
Separate Sessions for Planner and Executor
Each model maintains its own Session instance—a cache-stable list of messages containing system prompts, memory blocks, and skill definitions. In internal/agent/coordinator.go (lines 43–50), the Coordinator stores these as plannerSess and executor fields.
This isolation ensures that the planner’s reasoning context never absorbs the executor’s verbose tool outputs. Consequently, the planner’s prefix remains cache-stable across many turns, reducing token costs for planning phases.
Two-Phase Turn Execution
Every user request flows through the Coordinator.Run method, which implements a strict two-phase protocol defined in internal/agent/coordinator.go (lines 86–104):
- Planning Phase: The Coordinator queries the planner with the
DefaultPlannerPrompt. If theshouldPlangate returns false (indicating a trivial turn), the request short-circuits directly to the executor. - Execution Phase: The planner’s output is wrapped into a hand-off message via
formatHandoffand passed to the executor, which then performs the actual file writes and tool calls.
Event emissions mark transitions between phases, allowing external observers to track whether the Coordinator is in planning or execution mode via event.Event structures defined in internal/event/event.go.
Read-Only Tooling for the Planner
To prevent the planner from accidentally modifying the workspace during its reasoning phase, Reasonix constructs a filtered PlannerToolRegistry in internal/boot/boot.go (lines 76–92). This registry contains only introspection tools—such as code-graph queries and search operations—while excluding write-capable tools like bash or file editors.
The executor receives the full tool registry, ensuring that all side-effecting operations originate from the higher-capacity model following an explicit plan.
Implementation Details
Bootstrapping the Coordinator
The Coordinator is instantiated during the boot sequence when cfg.Agent.PlannerModel is non-empty and token economy mode is disabled. The following excerpt from internal/boot/boot.go demonstrates the assembly:
if pm := cfg.Agent.PlannerModel; pm != "" && !tokenEconomy {
pe, ok := cfg.ResolveModel(pm)
if !ok {
return nil, fmt.Errorf("planner_model %q is not a configured provider", pm)
}
plannerProv, err := NewProviderWithProxy(pe, proxySpec)
if err != nil {
return nil, fmt.Errorf("planner %q: %w", pm, err)
}
plannerSess := agent.NewSession(agent.PlannerPromptWithContext(mem.Block()))
plannerTools := agent.PlannerToolRegistry(reg)
runner = agent.NewCoordinator(
plannerProv, plannerSess, pe.Price, plannerTools,
agent.Options{MaxSteps: cfg.Agent.PlannerMaxSteps},
executor, cfg.Agent.Temperature, sink,
control.NewPlannerGate(classifier),
)
label = entry.Model + " + planner " + pe.Model
}
The code creates distinct provider instances for both models, initializes the planner’s session with a context-aware system prompt, and injects the read-only tool registry.
Executing a Turn
The Run method orchestrates the hand-off logic:
func (c *Coordinator) Run(ctx context.Context, input string) error {
c.sink.Emit(event.Event{Kind: event.TurnStarted})
// 1️⃣ optional trivial‑turn shortcut
if c.shouldPlan != nil && !c.shouldPlan(input) {
c.sink.Emit(event.Event{Kind: event.Phase, Text: c.executor.prov.Name() + " · executing", Source: event.UsageSourceExecutor})
return c.executor.Run(ctx, input)
}
// 2️⃣ planner phase
c.sink.Emit(event.Event{Kind: event.Phase, Text: c.planner.Name() + " · planning", Source: event.UsageSourcePlanner})
plan, err := c.plan(ctx, input)
if err != nil {
return fmt.Errorf("planner: %w", err)
}
// 3️⃣ executor hand‑off
c.sink.Emit(event.Event{Kind: event.Phase, Text: c.executor.prov.Name() + " · executing", Source: event.UsageSourceExecutor})
if isNoOpPlan(plan) {
c.persistExecutorNoOp(ctx, input, plan)
c.sink.Emit(event.Event{Kind: event.Text, Text: plan})
return nil
}
return c.executor.Run(ctx, formatHandoff(input, plan, executorToolHandoffContext(c.executor)))
}
If the planner returns a no-op plan, the Coordinator emits the plan text directly without invoking the executor, further reducing token consumption.
Hand-Off Message Format
The formatHandoff function constructs the executor’s prompt by injecting the original task, the planner’s output, and optional tool context:
func formatHandoff(task, plan string, toolContext ...string) string {
toolBlock := ""
if len(toolContext) > 0 {
toolBlock = strings.TrimSpace(toolContext[0])
}
if toolBlock != "" {
toolBlock = "\n\nExecutor tool context:\n" + toolBlock
}
return fmt.Sprintf(`# %s
You are the executor now. Use your available tools to execute the task.
Original task:
%s
Planner output:
%s
%s
Executor instructions:
…`, executorHandoffMarker, task, plan, toolBlock)
}
This structured prompt ensures the executor understands its role transition while maintaining access to the planner’s reasoning trace.
Benefits of the Two-Model Architecture
- Token Efficiency: The planner’s cache-stable prefix avoids re-transmitting the entire conversation history, reducing API costs for long sessions.
- Safety: Read-only tooling prevents the planner from issuing destructive commands before the executor validates the plan.
- Modularity: Teams can swap planner models (e.g., switching to a cheaper endpoint for simple tasks) without modifying executor configuration, as implemented in the
cfg.ResolveModelresolution logic.
Summary
- Two-model collaboration in Reasonix uses a Coordinator to manage separate planner and executor LLMs.
- Session isolation in
internal/agent/coordinator.gokeeps each model’s context clean and cache-stable. - Two-phase turns route requests through a planning phase (read-only tools) before handing off to an execution phase (full tool access).
- Filtered registries in
internal/boot/boot.gorestrict the planner to introspection tools, preventing accidental workspace modifications. - Hand-off formatting ensures the executor receives structured context including the original task, planner output, and tool availability.
Frequently Asked Questions
What is the Coordinator pattern in Reasonix?
The Coordinator pattern is an architectural design in the DeepSeek-Reasonix repository that enables two LLMs—a planner and an executor—to collaborate on a single task. It manages separate conversation sessions, orchestrates the flow between planning and execution phases, and formats hand-off messages so the executor understands the planner’s intent without sharing polluted context.
How does the planner model differ from the executor model?
The planner model is typically a low-cost, low-frequency LLM configured via planner_model that uses read-only tools to generate concise action plans. The executor model is the primary, often higher-capacity LLM that performs actual file writes, bash commands, and tool executions based on the planner’s output. They communicate through the Coordinator’s Run method rather than sharing a single message history.
Why does the planner use read-only tools?
Reasonix filters the planner’s available tools through PlannerToolRegistry to include only non-mutating operations like search and code-graph queries. This restriction prevents the planner from accidentally modifying the workspace during its reasoning phase, ensuring that all side-effecting changes are deliberate actions taken by the executor after reviewing the plan.
How does Reasonix prevent context pollution between models?
Each model maintains an independent Session object (stored in plannerSess and executor fields within the Coordinator). Because the sessions are isolated, the executor’s verbose tool outputs and file diffs never appear in the planner’s message history, keeping the planner’s system prompt cache-stable and preventing the gradual degradation of reasoning quality that occurs in shared-context architectures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →