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 observer edges, 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, independent direct/plugin LLM completions, atomic tool attempts, process admin, controller-level retry sleeps, RLM exec, checkpoints, and execution-surface sync. Direct completions and retry delays inside opaque tool code belong to the attempt. 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, and the facade's LashCoreBuilder::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.

Durable 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

A terminal host 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. Leaf tools receive controller-free AttemptContext reads and return typed intents; durable owner-bound process bodies receive InternalProcessContext, while authored orchestration receives OrchestrationContext. Hosts act outside a turn through the session-and-scope-bound ToolIntentIngress.

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. The caller's optional requires_ancestor_node_id precondition is a branch-liveness check, not a compare-and-swap on the session head: a head that merely advanced is accepted and the new nodes re-parent onto the current leaf, while a base that has left the active path is refused with nothing written.
ProcessesRegister durable processes from typed start requests, record session observers, deliver wakeups, and route user-facing cancellation through the canonical process service.
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 and account for their usage. Inside an atomic tool attempt, the provider call executes locally under that attempt rather than becoming a nested journal entry.

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 TurnReport. 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_key, 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
GraphAppend"] 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 ·