# Session Structure and Run Loop Workflow in Reasonix: Managing Conversation State and Tool Execution

> Explore the Reasonix Session structure and Run loop workflow. Learn how it manages conversation state and tool execution for deterministic LLM inference and final assistant responses.

- Repository: [YHH/DeepSeek-Reasonix](https://github.com/esengine/DeepSeek-Reasonix)
- Tags: internals
- Published: 2026-06-27

---

**Reasonix implements a thread-safe Session struct to maintain immutable conversation logs while a deterministic Run loop alternates between LLM inference and tool execution until producing a final assistant response.**

The `esengine/DeepSeek-Reasonix` framework orchestrates multi-turn AI conversations through a carefully designed state management system. Understanding the **Session structure** and **Run loop workflow** is essential for developers building complex agentic applications that require persistent context and dynamic tool calling. This article examines the core implementation details found in the Reasonix source code, demonstrating how the framework handles concurrency, tool dispatch, and conversation compaction.

## Understanding the Session Structure in Reasonix

The `Session` type, defined in [`internal/agent/session.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/session.go), serves as the immutable conversation log that persists across multiple turns of the Run loop.

### Core Fields and Concurrency Control

The Session struct uses a `sync.RWMutex` named `mu` to guard concurrent access patterns. While the Run loop acts as the sole writer, UI components may read snapshots concurrently without blocking the active conversation.

The struct maintains four critical fields:

- `Messages`: A slice of `provider.Message` storing chronological history including system prompts, user inputs, assistant responses, and tool interactions
- `rewriteVersion`: An integer incremented each time the log is compacted (e.g., after a long run-loop)
- `normalizedDirty`: A boolean flag set when `LoadSession` repairs corrupted history such as empty tool-call names, dangling calls, or truncated arguments

### Message Management Methods

The Session exposes several concurrency-safe operations for manipulating conversation state:

- `NewSession(system string)`: Creates a fresh session optionally pre-populated with a system prompt
- `Add(m provider.Message)`: Appends a new message while holding the write lock
- `Replace(msgs []provider.Message)`: Swaps the entire log during compaction operations that rewrite the middle of the history
- `Snapshot() []provider.Message`: Returns a deep copy that other goroutines can read safely
- `HasContent() bool`: Determines if the session contains anything beyond a system prompt, used to decide if the session should be persisted
- `RewriteVersion()` / `IncrementRewrite()`: Expose and bump the compaction version counter

## The Run Loop Workflow for Processing Tool Calls

The Run loop, primarily implemented in [`internal/agent/agent.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/agent.go) and orchestrated through [`internal/agent/task.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/task.go), drives the turn-by-turn conversation flow that processes tool calls.

### Entry Point: RunSubAgentWithSession

Sub-agents enter the loop via `RunSubAgentWithSession` in [`internal/agent/task.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/task.go) (lines 29-50). This function wires together the Session, tool Registry, Provider, and event Sink:

```go
func RunSubAgentWithSession(
    ctx context.Context,
    prov provider.Provider,
    reg  *tool.Registry,
    sess *Session,
    prompt string,
    opts  Options,
    sink  event.Sink,
) (string, error) {
    if sess == nil { return "", fmt.Errorf("sub‑agent session is nil") }
    sub := New(prov, reg, sess, opts, sink)   // creates the agent instance
    if err := sub.Run(ctx, prompt); err != nil {
        return "", fmt.Errorf("sub‑agent: %w", err)
    }
    // Walk backwards to find the last non-empty assistant message
    for i := len(sess.Messages) - 1; i >= 0; i-- {
        m := sess.Messages[i]
        if m.Role == provider.RoleAssistant && strings.TrimSpace(m.Content) != "" {
            return m.Content, nil
        }
    }
    return "", fmt.Errorf("sub‑agent finished without producing a final answer")
}

```

### Turn-by-Turn Execution Flow

Inside `Agent.Run`, the loop follows a strict execution pattern:

1. **Provider Invocation**: Sends the current snapshot via `prov.Chat(ctx, sess.Snapshot(), opts)`
2. **Response Parsing**: Inspects the returned `provider.Message` for tool calls
3. **Tool Detection**: If `Role == RoleAssistant` and `ToolCalls` are present, pauses normal text handling
4. **Final Answer Detection**: If no tool calls exist, appends the message and terminates the loop
5. **Tool Execution**: For each call, looks up the tool name in `tool.Registry` using `registry.Get(name)` and executes via `tool.Execute(ctx, args)`
6. **Result Recording**: Creates a `RoleTool` message with the execution result and appends it to the Session

### Event Nesting and Sub-Agent Isolation

Tool-dispatch and tool-result events forward to the parent `event.Sink` through a `subSink`. This architecture allows front-ends to render nested visual hierarchies under the original task call while keeping the sub-agent's textual reasoning isolated. Only the final assistant answer returns to the parent model, ensuring clean separation between parent and sub-agent contexts.

### Compaction and Loop Termination

After each turn, the agent evaluates compaction triggers when the message count exceeds the `ContextWindow` limit. Compaction rewrites the middle of the message slice, increments `rewriteVersion`, and calls `Session.Replace(newMsgs)` to atomically swap the log.

The loop terminates when either:

- An assistant message arrives without `ToolCalls` (success case)
- The configurable `maxSteps` budget exhausts, raising a `MaxStepsExceeded` error

## Practical Implementation Example

The following example demonstrates creating a Session, running an agent with tool capabilities, and extracting the final response:

```go
package main

import (
    "context"
    "fmt"

    "reasonix/internal/agent"
    "reasonix/internal/provider"
    "reasonix/internal/tool"
)

func mockProvider() provider.Provider   { /* ... */ return nil }
func mockRegistry() *tool.Registry      { /* ... */ return nil }

func main() {
    // Initialize session with system prompt
    sess := agent.NewSession("You are a coding assistant.")
    
    // Configure dependencies
    prov := mockProvider()
    reg := mockRegistry()
    opts := agent.Options{MaxSteps: 10, ContextWindow: 4096}
    
    // Create agent instance
    ag := agent.New(prov, reg, sess, opts, agent.EventSink())
    
    // Execute run loop
    prompt := "List the Go source files in the current directory."
    if err := ag.Run(context.Background(), prompt); err != nil {
        panic(err)
    }
    
    // Extract final answer from session history
    msgs := sess.Snapshot()
    for i := len(msgs) - 1; i >= 0; i-- {
        if msgs[i].Role == provider.RoleAssistant && msgs[i].Content != "" {
            fmt.Println("Final answer:", msgs[i].Content)
            break
        }
    }
}

```

This produces a message sequence: system → user → assistant (tool call) → tool result → assistant (final answer).

## Summary

- The **Session structure** in Reasonix provides thread-safe, immutable conversation storage using `sync.RWMutex` and versioning fields like `rewriteVersion` defined in [`internal/agent/session.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/session.go)
- **RunSubAgentWithSession** in [`internal/agent/task.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/task.go) serves as the primary entry point for sub-agent execution, coordinating the Provider, Registry, and Session
- The **Run loop** alternates between LLM inference and tool execution, checking `provider.Message` for `ToolCalls` on each iteration and dispatching via `tool.Registry`
- Tool results are recorded as `RoleTool` messages via the `Execute` method, maintaining a complete chronological transcript
- Conversation **compaction** rewrites history mid-log when context limits approach, incrementing the rewrite version through `Session.Replace()`
- The loop terminates upon receiving a final assistant message without tool calls or when the `maxSteps` budget is exceeded

## Frequently Asked Questions

### What is the Session structure in Reasonix?

The Session structure is a concurrency-safe conversation log defined in [`internal/agent/session.go`](https://github.com/esengine/DeepSeek-Reasonix/blob/main/internal/agent/session.go) that stores chronological `provider.Message` slices. It features a `sync.RWMutex` named `mu` for thread safety, a `rewriteVersion` counter for compaction tracking, and methods like `Snapshot()` and `Replace()` for safe concurrent access and history rewriting during long-running conversations.

### How does the Run loop handle tool calls?

The Run loop, implemented in `Agent.Run`, detects tool calls by checking if the assistant message contains `ToolCalls`. For each call, it looks up the tool name in `tool.Registry`, executes the tool with parsed JSON arguments via `tool.Execute(ctx, args)`, and appends the result as a `RoleTool` message to the Session before continuing the conversation cycle.

### What happens when the conversation exceeds the context window?

When message counts approach the `ContextWindow` limit configured in `Options`, the Run loop triggers compaction. This process rewrites the middle portion of the message history, increments `rewriteVersion`, and calls `Session.Replace()` to swap the log atomically, preserving recent context while removing older messages to maintain token limits.

### How does Reasonix ensure thread safety during concurrent access?

Reasonix protects Session state using a `sync.RWMutex` field named `mu`. The Run loop acquires write locks during message appends via `Add()` and replacements via `Replace()`, while UI components acquire read locks via `Snapshot()` to obtain consistent copies of the conversation history without blocking the active agent processing.