How Reasonix's Dual-Model Architecture with Executor and Planner Sessions Functions
Reasonix implements a dual-agent design where two independent LLM agents—the executor and the planner—operate with isolated sessions, coordinated by a TurnOrchestrator that hands off control and merges plans into the executor's state.
Reasonix is an open-source framework that solves complex multi-step tasks by splitting reasoning across two specialized model instances. Understanding this dual-model architecture is key to customizing model selection, debugging session flow, and extending the system's planning capabilities. The implementation centers on the Controller and TurnOrchestrator types in internal/control/.
Core Architecture: Two Agents, Two Sessions
At the heart of Reasonix lies a Controller struct defined in internal/control/controller.go. This struct explicitly maintains two separate *agent.Agent pointers:
type Controller struct {
// Executor handles the main interaction loop with the LLM.
Executor *agent.Agent
// Planner is used for generating plans before execution.
Planner *agent.Agent
// Other fields omitted.
}
The constructor at lines 92-98 instantiates each agent with its own session via agent.NewSession():
func New(opts Options) *Controller {
executor := agent.New(opts.ExecutorProvider, opts.ToolRegistry, agent.NewSession("executor"), opts.ExecutorOptions, event.Discard)
planner := agent.New(opts.PlannerProvider, opts.ToolRegistry, agent.NewSession("planner"), opts.PlannerOptions, event.Discard)
return &Controller{
Executor: executor,
Planner: planner,
}
}
Key observation: Both agents share the same ToolRegistry and event sink, but their Session objects—created with distinct IDs "executor" and "planner"—keep conversation histories completely isolated.
Turn Orchestration: When and How the Planner Invokes
The TurnOrchestrator type in internal/control/turn_orchestrator.go implements the coordination logic. Its RunTurn method (lines 147-151) begins by appending user input to the executor's session:
func (c *TurnOrchestrator) RunTurn(ctx context.Context, input string) error {
// Add user input to executor session
c.executor.Session().Add(provider.Message{Role: provider.RoleUser, Content: input})
// ... orchestration logic continues
}
When the orchestrator determines that planning is required, it dynamically creates a planner agent (lines 163-168):
// When a plan is needed, the orchestrator creates a planner agent
planner := agent.New(plannerProvider, tool.NewRegistry(), agent.NewSession("planner"), agent.Options{}, event.Discard)
// Run planner turn
err := planner.Run(ctx, "plan request")
if err != nil {
return err
}
After successful execution, the planner's output is merged into the executor's state as structured todo items (lines 169-170):
// Merge planner output into executor session as a plan
c.executor.SeedTodoState([]evidence.TodoItem{{Content: "generated plan"}})
Progress Tracking and State Synchronization
The orchestrator monitors execution progress through the executor's host progress signature. The helper method executorProgress() (lines 154-159) enables state comparison across turns:
func (c *TurnOrchestrator) executorProgress() string {
if c.executor == nil {
return ""
}
return c.executor.HostProgressSignature()
}
This signature serves two purposes: detecting when the executor has stalled and determining when fresh planner intervention is warranted.
Model Flexibility: Different Providers per Role
The dual-session design enables heterogeneous model deployment—a critical optimization for cost and latency:
opts := reasonix.Options{
ExecutorProvider: fastCheapModel, // e.g., 7B parameter model for quick responses
PlannerProvider: capableModel, // e.g., 70B parameter model for complex reasoning
ToolRegistry: tool.NewRegistry(),
}
ctrl := reasonix.New(opts)
Since sessions are isolated, the planner can run an expensive reasoning model only when multistep planning is required, while the executor maintains conversational flow with a faster, cheaper alternative.
Event Source Differentiation
The internal/event/event.go file defines constants that distinguish executor versus planner activity during telemetry and billing. Lines 460-462 reference UsageSourceExecutor and UsageSourcePlanner, enabling granular cost attribution across the dual-model system.
Practical Implementation: Running a Complete Turn
package main
import (
"context"
"log"
"github.com/esengine/DeepSeek-Reasonix/internal/control"
"github.com/esengine/DeepSeek-Reasonix/internal/tool"
)
func main() {
// Configure controller with separate model providers
opts := control.Options{
ExecutorProvider: loadExecutorModel(),
PlannerProvider: loadPlannerModel(),
ToolRegistry: tool.NewRegistry(),
}
ctrl := control.New(opts)
// Execute turn that may trigger planning
userInput := "Analyze this dataset, generate visualizations, and email the report."
err := ctrl.RunTurn(context.Background(), userInput)
if err != nil {
log.Fatalf("turn failed: %v", err)
}
// Inspect isolated session states
executorMsgs := ctrl.Executor.Session().Snapshot()
plannerMsgs := ctrl.Planner.Session().Snapshot()
log.Printf("Executor messages: %d, Planner messages: %d",
len(executorMsgs), len(plannerMsgs))
}
Summary
- Dual-agent instantiation: The
Controllerconstructor ininternal/control/controller.gocreates separateExecutorandPlanneragents with distinct session IDs. - Session isolation: Each agent maintains independent conversation history through
agent.NewSession("executor")andagent.NewSession("planner"). - Dynamic orchestration:
TurnOrchestrator.RunTurn()routes input to the executor and conditionally spins up the planner for multistep reasoning. - State merging: Planner outputs integrate into executor state via
SeedTodoState(), preserving execution continuity. - Progress monitoring:
HostProgressSignature()enables cross-turn state detection and replanning triggers. - Provider flexibility: The architecture supports different LLM models per role, optimizing cost and capability.
Frequently Asked Questions
How does Reasonix keep executor and planner contexts from interfering?
Each agent receives its own Session object during construction. The session ID—either "executor" or "planner"—scopes all message history, tool results, and state mutations. The TurnOrchestrator explicitly copies only structured plan outputs (via SeedTodoState()) rather than full conversation context.
Can I use the same model provider for both executor and planner?
Yes. The Options struct accepts identical providers for both roles. However, the dual-session architecture still maintains isolation, so the planner's reasoning scratchpad never leaks into the executor's conversational context even with identical underlying models.
What triggers the planner to run during a turn?
The TurnOrchestrator evaluates the executor's HostProgressSignature() against the current task state. When the signature indicates insufficient progress—typically when a multistep task is detected or the executor's response lacks actionable structure—the orchestrator invokes the planner. The exact heuristic is implementation-defined in the orchestrator's decision logic.
Where is task state stored between turns?
Persistent state lives within the executor agent's session and its associated TodoState. The planner is ephemeral: created fresh for each planning invocation, then discarded after its output is merged. This design minimizes memory overhead for the planning role while maintaining durable execution context in the executor.
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 →