# How Reasonix's Dual-Model Architecture with Executor and Planner Sessions Functions

> Discover how Reasonix's dual-model architecture works with separate executor and planner LLM agents coordinated by a TurnOrchestrator for efficient plan execution and state merging.

- Repository: [YHH/DeepSeek-Reasonix](https://github.com/esengine/DeepSeek-Reasonix)
- Tags: architecture
- Published: 2026-08-11

---

**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`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/control/controller.go). This struct explicitly maintains two separate `*agent.Agent` pointers:

```go
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()`:

```go
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`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/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:

```go
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):

```go
// 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):

```go
// 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:

```go
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:

```go
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`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/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

```go
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 `Controller` constructor in [`internal/control/controller.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/control/controller.go) creates separate `Executor` and `Planner` agents with distinct session IDs.
- **Session isolation:** Each agent maintains independent conversation history through `agent.NewSession("executor")` and `agent.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.