Session Structure and Run Loop Workflow in Reasonix: Managing Conversation State and Tool Execution
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, 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 ofprovider.Messagestoring chronological history including system prompts, user inputs, assistant responses, and tool interactionsrewriteVersion: An integer incremented each time the log is compacted (e.g., after a long run-loop)normalizedDirty: A boolean flag set whenLoadSessionrepairs 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 promptAdd(m provider.Message): Appends a new message while holding the write lockReplace(msgs []provider.Message): Swaps the entire log during compaction operations that rewrite the middle of the historySnapshot() []provider.Message: Returns a deep copy that other goroutines can read safelyHasContent() bool: Determines if the session contains anything beyond a system prompt, used to decide if the session should be persistedRewriteVersion()/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 and orchestrated through 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 (lines 29-50). This function wires together the Session, tool Registry, Provider, and event Sink:
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:
- Provider Invocation: Sends the current snapshot via
prov.Chat(ctx, sess.Snapshot(), opts) - Response Parsing: Inspects the returned
provider.Messagefor tool calls - Tool Detection: If
Role == RoleAssistantandToolCallsare present, pauses normal text handling - Final Answer Detection: If no tool calls exist, appends the message and terminates the loop
- Tool Execution: For each call, looks up the tool name in
tool.Registryusingregistry.Get(name)and executes viatool.Execute(ctx, args) - Result Recording: Creates a
RoleToolmessage 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
maxStepsbudget exhausts, raising aMaxStepsExceedederror
Practical Implementation Example
The following example demonstrates creating a Session, running an agent with tool capabilities, and extracting the final response:
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.RWMutexand versioning fields likerewriteVersiondefined ininternal/agent/session.go - RunSubAgentWithSession in
internal/agent/task.goserves 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.MessageforToolCallson each iteration and dispatching viatool.Registry - Tool results are recorded as
RoleToolmessages via theExecutemethod, 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
maxStepsbudget 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 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.
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 →