runtime/host

The async runtime is the effect controller boundary around the pure turn machine. It manages 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, managed child-session state, AgentFrame-following turn execution, process grants, pending user turn input, queued background work, and a usage ledger. Sans-IO computes protocol effects, RuntimeEffectController handles nondeterministic host work, plugins contribute behavior, providers complete requests, and persistence records committed session state plus final turn commit idempotency.

Runtime composition
flowchart TD Host["CLI / app host / embedded service"] --> Runtime["LashRuntime"] Runtime --> Env["RuntimeEnvironment
providers, tools, tasks, traces"] Runtime --> Manager["RuntimeSessionServices
host capability set"] Runtime --> Persistence["RuntimePersistence trait"] Persistence --> Stores["store implementations
SQLite, Postgres, or host-provided DB"] Runtime --> Attachments["AttachmentStore
local files, S3-compatible bytes, or host-provided"] Manager --> Session["Session"] Session --> Machine["lash-sansio TurnMachine"] Session --> Plugins["SessionPlugin hooks"] Session --> ReadView["SessionReadView"] Machine --> Effects["Effect
LLM, tools, ExecCode, checkpoint"] Effects --> EffectController["RuntimeEffectController
RuntimeInvocation + replay key"] EffectController --> Env ReadView --> Projection["ChronologicalProjection
RLM history / UI / export"]

Runtime Effect Controller

RuntimeTurnDriver only polls TurnMachine, builds a RuntimeEffectEnvelope, invokes the active ScopedEffectController, and feeds the returned response back. The default InlineEffectHost creates in-process scopes; durable adapters create handler-scoped controllers for stable ExecutionScope values.

Every invocation carries a RuntimeInvocation: session id, optional turn id/index, protocol iteration, subject, optional typed causal parent, stable replay.key, and ref-only request data. Cancellation, streams, provider trace sinks, and attachment byte hydration stay in the local executor or host workflow. The boundary covers turn LLM calls, direct/plugin LLM completions, individual tool calls, durable tool steps and waits, process admin, retry sleeps, RLM exec, checkpoints, and execution-surface sync. Process registry presence enables the generic process admin plane, while the process work driver owns execution and waits; protocol/language runtimes decide how that plane becomes authored process/lifecycle abilities.

RuntimeHostConfig::new, RuntimeEnvironmentBuilder::with_effect_host, EmbeddedRuntimeBuilder::with_effect_host, and the facade's AdvancedLashCoreBuilder::effect_host configure the deployment effect host. Durable workflow adapters pass a handler-scoped controller through the scoped turn entrypoints so the workflow controller and stable turn identity are explicit for that run. After a crash or worker move, the workflow handler reruns and the durable host replays effect outcomes before Lash retries the final commit. Restate is the first-party adapter; Temporal or another workflow engine can implement the same controller boundary.

Residency And Sessions

Lash can run behind the CLI or inside any long-lived host application. The same session machinery supports active sessions, managed child sessions, parked runtimes, and resumable hosts.

interactive

CLI host

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

application

Application 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.

embedded

Host services

Services can compose the same core with their own providers, plugins, stores, traces, and validation around product-specific session ids.

Host Capabilities

The internal RuntimeSessionServices capability set is projected through thin wrappers (RuntimeSessionStateService, RuntimeSessionLifecycleService, RuntimeSessionGraphService, RuntimeSessionProcessService) that implement the split host-boundary traits SessionStateService, SessionLifecycleService, SessionGraphService, and ProcessService with typed operations for plugin and tool needs. Tool authors do not receive the runtime handle directly; ToolContext projects host capabilities into explicit methods such as sessions() (whose ToolSessionAdmin exposes model() and tool_catalog()), processes(), direct_completions(), attachments(), and durable_effects(). Process starts enter through ProcessStartRequest, and intentional cancellation enters through the host-owned ProcessCancelAbility.

Host-service areaRuntime responsibility
Session snapshots and catalogsRead current state and tool catalogs through stable projections.
Tool stateRead and mutate per-session Tool Catalog membership without exposing the inner registry.
Session lifecycle and turnsCreate child sessions, fork sessions, append AgentFrames, and stream managed turns.
Session graph writesAppend plugin-authored nodes to a session graph through a guarded contract.
ProcessesRegister durable processes from typed start requests, grant process handles, deliver wakeups, and route user-facing cancellation through the configured cancel ability.
Trace accessExpose trace sinks to hooks without exposing session internals. Human input is provided by host tools such as the CLI ask implementation, not by a runtime prompt event.
Direct completionsRun one-shot completions for helper tools such as llm_query and account for their usage.

Turn Outcomes And AgentFrames

A turn finishes as one of three outcomes. App hosts usually call session.turn(input).stream_to(&sink) or .run(); durable handlers add .turn_id(...).effects(&controller). Those facade calls follow AgentFrame switches and return the final TurnResult. Raw stream_turn and stream_turn_with_agent_frames are runtime internals for lower-level hosts.

OutcomeShapeMeaning
TurnOutcome::FinishedTurnFinish::AssistantMessage { text }, TurnFinish::FinalValue { value }, or TurnFinish::ToolValue { tool_name, value }The turn produced a final answer. AssistantMessage is the sole terminal prose result; live prose is only AssistantProseDelta, and the runtime commit materializes the settled transcript message once. FinalValue and ToolValue are typed terminal values authored by RLM finish or terminal tool controls.
TurnOutcome::AgentFrameSwitch{ frame_id, task }RLM continue_as appended a new AgentFrame inside the same session with a fresh prompt window, explicit { task, seed } nodes, and frame-scoped execution state.
TurnOutcome::StoppedTurnStop::{Cancelled, Incomplete, 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

A single exclusive projector runs once per completed tool call. The durable graph keeps the full ToolCallOutput; the projector derives the budgeted ModelToolReturn that the live model prompt and rolled-up history see.

Tool result projection
flowchart LR Tools["Tool dispatch ToolCallOutput"] --> Graph["SessionGraph storage
(full ToolCallOutput)"] Tools --> Projector["tool-result projector"] Projector --> Model["ModelToolReturn
(budgeted view)"] Model --> Provider["next LlmRequest"] Model --> 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, usage ledger, and settled assistant prose together so read views and host reports agree. Modes describe outcomes; the shared runtime commit path handles final transcript materialization.

Commit and accounting path
flowchart LR Driver["RuntimeTurnDriver"] --> Pipeline["TurnBoundary"] 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"]
read on ·