Architecture chapter 05

runtime/host

The async runtime is the host boundary around the pure turn machine. It owns residency, persistence, plugin hooks, provider calls, background work, usage accounting, and read projections while keeping CLI rendering and the app-facing lash API outside the core session model.

Runtime Shape

LashRuntime holds the live session registry, the managed-sessions map, the active handoff continuation table, the pending first-turn input queue, and a usage ledger. Sans-IO computes protocol effects, plugins contribute behavior, providers complete requests, and persistence records durable turn results.

Runtime composition
flowchart TD Host["CLI / lash / benchmark runner"] --> Runtime["LashRuntime"] Runtime --> Env["RuntimeEnvironment
providers, tools, tasks, traces"] Runtime --> Manager["RuntimeSessionManager"] Runtime --> Persistence["RuntimePersistence trait"] Persistence --> Sqlite["lash-sqlite-store
SQLite + blobs"] Manager --> Session["Session"] Session --> Machine["lash-sansio TurnMachine"] Session --> Plugins["SessionPlugin hooks"] Session --> ReadView["SessionReadView"] Machine --> Effects["Effect
LLM, tools, ExecCode, checkpoint"] Effects --> Env ReadView --> Projection["ChronologicalProjection
RLM history / UI / export"]

Residency And Sessions

The runtime can be interactive, app-owned, or benchmark-owned. The same session machinery supports active sessions, managed child sessions, parked runtimes, and resumable app hosts.

interactive

CLI-owned runtime

lash-cli composes providers, plugins, MCP servers, traces, and terminal rendering around a live LashRuntime.

app

App-owned sessions

lash exposes LashCore/LashSession for long-lived host applications. Hosts open sessions from ids and stores; parking/resume remains runtime plumbing behind the facade.

evaluated

Benchmark-owned runtime

Benchmark runners use the same runtime with controlled plugins, store paths, trace capture, and task-specific validation.

Host Capabilities

RuntimeSessionManager implements focused plugin host traits rather than one broad mutable runtime handle. Tool authors do not receive those bridge traits directly; ToolContext projects them into explicit methods such as session_model(), tool_catalog(), sessions(), tasks(), and direct_completion(...).

TraitRuntime responsibility
SessionSnapshotHost, ToolCatalogHostRead current state and tool catalogs through stable projections.
ToolStateHostRead and mutate per-session dynamic tool availability without exposing the inner registry.
SessionLifecycleHost, TurnHostCreate, fork, continue, and stream managed sessions.
SessionGraphHostAppend plugin-authored nodes to a session graph through a guarded contract.
TaskHost, MonitorHostRegister background tasks, monitors, async handles, wakeups, and cancellation.
TraceHostExpose trace sinks to hooks without exposing session internals. Human input is provided by host-owned tools such as the CLI ask implementation, not by a runtime prompt event.
DirectCompletionHostRun one-shot completions for helper tools such as llm_query and account for their usage.

Turn Outcomes And Handoffs

A turn finishes as one of three outcomes. App hosts usually call session.turn(input).stream(&sink), .run(), or .collect_with(&sink); those facade calls follow handoffs and return the final TurnResult. Raw stream_turn and stream_turn_following_handoffs are runtime internals for lower-level hosts.

OutcomeShapeMeaning
TurnOutcome::FinishedTurnFinish::AssistantMessage { text }, TurnFinish::SubmittedValue { value }, or TurnFinish::ToolValue { tool_name, value }The turn produced a final answer, either as plain assistant text or as a typed terminal value authored by RLM submit or a terminal tool control.
TurnOutcome::Handoff{ session_id }RLM continue_as opened a successor session with a fresh window. The runtime queues the packed { task, seed } as that session's first turn input.
TurnOutcome::StoppedTurnStop::{Cancelled, InvalidInput, MaxTurns, ToolFailure, ProviderError, PluginAbort, RuntimeError, SubmittedError{...}, ToolError{...}}The turn ended without a clean answer. SubmittedError and ToolError carry terminal errors authored by failing control paths, such as submit_error, so hosts can surface them.

Tool Result Projection

The built-in projector enforces budgeted views of tool output across three hook sites so the durable graph, the live model prompt, and rolled-up history each see consistent, bounded content.

Tool result projection hooks
flowchart LR Tools["Tool dispatch ToolResult"] --> BeforeState["BeforeState projector"] BeforeState --> Graph["SessionGraph storage"] Tools --> BeforeModel["BeforeModel projector"] BeforeModel --> Provider["next LlmRequest"] Tools --> BeforeHistory["BeforeHistory projector"] BeforeHistory --> History["chronological / rolling history"]

Defaults: ToolOutputBudgetMode::Bytes at 16 KiB and 400 lines, configurable via ToolOutputBudgetConfig. RLM piggybacks on the same projector for its print observations.

Persistence And Usage

Persistence is turn-scoped. Commits update the session graph, checkpoint blobs, session heads, and usage ledger together so read views and benchmark cost reports agree.

Commit and accounting path
flowchart LR Driver["RuntimeTurnDriver"] --> Pipeline["TurnCommitPipeline"] Pipeline --> Delta["RuntimeCommit
GraphCommitDelta"] Delta --> Store["lash-sqlite-store"] Store --> Head["session_head revision"] Store --> Blobs["checkpoint + attachment blobs"] Store --> Ledger["usage ledger
source + model + tokens"] Pipeline --> View["SessionReadView"] Ledger --> Reports["SessionUsageReport
diff_usage_reports"]