Skip to content

TurnRuntimeState

File: brain/agent/turn_state.py


Purpose

TurnRuntimeState is the single mutable container for all state scoped to one agent turn. It is created at the start of run_turn(), passed as an explicit parameter to every collaborator that needs to read or write turn data, and discarded when the turn ends.

Because the harness instance itself holds no mutable per-turn state, concurrent or sequential turns cannot contaminate each other.


Construction

state = TurnRuntimeState(
    session_id="sess_abc123",
    budget=budget,          # TurnBudget — required
)

All other fields are initialised to safe empty defaults.


Fields

Token Accounting

state.input_tokens: int        # Total input tokens across all LLM calls
state.output_tokens: int       # Total output tokens
state.cache_read_tokens: int   # Provider cache read tokens
state.cache_creation_tokens: int

Updated via state.add_tokens(input_tokens=..., output_tokens=..., ...).

Citations

state.citations: CitationAccumulator
    .local: set[str]              # Vault anchor strings (e.g. "notes/paris.md")
    .web: list[dict[str, Any]]    # Web source dicts
    .rendered_paths: list[str]    # Paths to rendered web content files

Populated by harness._apply_outcome_to_state() after each tool call.

Tool Tracking

state.tool_trace: dict[str, ToolTraceEntry]   # trace_id → entry
state.tool_results: list[dict[str, Any]]      # Lightweight feed for ReflectionEngine
state.blocked_tool_names: set[str]            # Tools blocked for the rest of this turn

tool_trace is the thread-safe late-write gate. tool_results is written only from the main thread after the complete_trace() gate returns True.

Validation Repair

state.validation_repair_counts: dict[str, int]  # call_id → repair attempts

Tracks how many ToolArgValidator repair attempts have been made per call ID. Once the count reaches MAX_REPAIR_ATTEMPTS, the call is denied.

Turn Outcome

state.final_text: str      # The LLM's final text response
state.timed_out: bool      # True if the turn expired before producing a response
state.prompt_render_hash: str  # Per-turn render hash set by TurnPreparer
state.context_was_compacted: bool  # True if ContextCompactor ran this turn

Budget Reference

state.budget: TurnBudget   # The resource envelope for this turn

Thread-Safe APIs

Tool Trace Lifecycle

The trace is the mechanism that prevents late-arriving tool threads from corrupting the turn.

state.set_trace_running(trace_id, tool_name)
# Called by BoundedToolExecutor before submitting the tool thread.
# Creates a ToolTraceEntry(status="running").

state.mark_trace_timed_out(trace_id)
# Called by the harness when future.result(timeout=...) raises TimeoutError.
# Marks the entry status="timed_out".

ok = state.complete_trace(trace_id, latency_ms=1234, retryable=True)
# Called by the tool thread when execution finishes.
# If status was "timed_out" → marks as "timed_out_late", returns False.
# If status was "running"   → marks as "completed",    returns True.
# Callers must check the return value and suppress side effects if False.

Race condition handled:

Main thread                     Tool thread
────────────────────────────    ──────────────────────────────
set_trace_running()
submit(future)
                                ... executing tool ...
future.result(timeout=20s)
  → TimeoutError
mark_trace_timed_out()
return ToolTimeout outcome
                                complete_trace() → False  ← suppressed
                                (no event log write, no tool_results append)

Late-Write Gate

state.is_accepting_writes()  # → bool: False after close_writes()
state.close_writes()         # Called by harness after the loop exits

Any background thread that completes after close_writes() will find is_accepting_writes() == False and must skip all side effects.


Helper Types

CitationAccumulator

@dataclass
class CitationAccumulator:
    local: set[str]              # Vault anchors
    web: list[dict[str, Any]]    # Web source objects
    rendered_paths: list[str]    # Rendered web content file paths

Collected incrementally during the turn. Handed to TurnFinalizer.finalize() at the end.

ToolTraceEntry

@dataclass
class ToolTraceEntry:
    tool_name: str
    status: str    # "running" | "completed" | "timed_out" | "timed_out_late"
    trace_id: str
    latency_ms: int
    retryable: bool

Lightweight record used only for late-write suppression. Not persisted.


Status Progression

set_trace_running()
   "running"
       ↓               ↓
complete_trace()  mark_trace_timed_out()
(thread finished)   (main thread: timeout)
       ↓               ↓
  "completed"      "timed_out"
               complete_trace() called late
                 "timed_out_late"  ← side effects suppressed