execution/runtime

Execution starts in the CLI bootstrap path, then branches into interactive TUI or autonomous --print. Inside a turn, the active mode decides whether model output becomes native tool calls or Lashlang code execution.

CLI Startup

The CLI resolves durable session state and runtime composition before entering the TUI or --print runner. App hosts use the lash facade and never enter bootstrap::run.

Command execution path
flowchart TD Args["lash-cli Args
--print, --export, trace flags, provider/model"] --> Main["main()"] Main --> Export{"--export?"} Export -->|yes| ExportRun["lash-export
session DB + trace JSONL"] Export -->|no| Bootstrap["bootstrap::run"] Bootstrap --> SessionBootstrap["SessionBootstrap
new/resume/fork"] Bootstrap --> Providers["CLI provider spec materialization"] Bootstrap --> Plugins["PluginHost + plugin factories"] Bootstrap --> Dynamic["McpPluginFactory + MCP servers"] Bootstrap --> Runtime["CliSessionOpener -> LashSession"] Runtime --> Mode{"Host mode"} Mode -->|--print| Auto["run_autonomous"] Mode -->|interactive| Terminal["Terminal::enter + run_app"]

Protocol Execution

Two builder entry points on LashCore: standard_builder() and rlm_builder(factory). Each pre-seeds one protocol plugin and preamble. Background work and AgentFrame switches are cross-cutting outcomes either protocol can coordinate.

standard

Native provider tool calls

The Standard protocol interprets provider output through StandardDriver and emits DriverAction::StartTools. The runtime dispatches registered native tools first, then tools contributed by registered ToolProviders.

rlm

Persistent Lashlang execution

The RLM protocol extracts closed lashlang fences from the model output and runs them through a persistent VM. Read-only host bindings (e.g., history) are projected into Lashlang scope through RlmProjectedBindings; lazy bindings carry ProjectionRefs that resolve immediately before execution. They read like ordinary names but cannot be reassigned. The prompt renderer uses the effective LashlangHostEnvironment, so disabled process and trigger abilities are not advertised and fail during linking if emitted anyway. continue_as is an RLM native control tool, llm_query is supplied by lash-llm-tools, and the stream_mask plugin hides the still-streaming fence from live UI until execution completes. An RLM block ending with continue_as { task, seed } finishes the low-level turn as TurnOutcome::AgentFrameSwitch { frame_id, task }, appends a fresh AgentFrame inside the same session, and preserves turn index, trace continuity, process-scope decisions, and projected seed refs. App hosts call session.turn(input).stream_to(&sink) or .run(); the facade follows AgentFrame switches and returns the final TurnResult.

Background work (subagents and async tool calls) is described in Background Work below. Either protocol can spawn handles when a process registry is installed; RLM additionally discovers visible runs via processes.list and resolves handles with await.

Lash API

The app-facing facade. Hides CLI bootstrap and runtime plumbing; leaves persistence, HTTP, auth, and frontend streaming to the host.

App-facing lash facade
flowchart LR App["host app
HTTP / DB / auth / UI"] --> Core["LashCore
shared runtime environment"] Core --> Builder["LashCoreBuilder
standard_builder() / rlm_builder(factory)
provider, model, tools, plugins"] Builder --> Advanced["runtime_host_config
plugin host / runtime config"] Advanced --> Env["runtime environment
plugins + stores + tracing"] App --> Session["core.session(id)
SessionBuilder"] Session --> Runtime["parked runtime handle
per chat/session"] Runtime --> Turn["turn(TurnInput).stream_to(&sink) / .run()"] Runtime --> Observe["observe()
SessionObservation + cursor"] Turn --> Result["TurnResult / TurnOutput
result (+ activities)"] Turn --> Events["TurnActivitySink
text, reasoning, tools, usage, terminal values"]
LashCore
Cloneable shared core: runtime env, default session policy, one protocol plugin, effect host, session store factory, attachment store, trace sink, residency, and termination policy. Use LashCore::standard_builder() or LashCore::rlm_builder(factory) for built-in protocols; use LashCore::builder() only with an explicit custom protocol plugin.
LashCore::rlm_builder(factory)
Sugar entry point that installs the RLM protocol and Lashlang process engine from a host-configured RlmProtocolPluginFactory (projection resolver, deferred tool resolver, and a required Lashlang artifact store), plus the default runtime plugin stack.
LashSession
One conversation. Parked/resumable handle exposing run(...), turn(...), read_view(), observe(), and admin(). Carries an optional parent session id.
TurnBuilder
Per-turn config: cancellation token, mode options, plugin input, RLM bindings. .stream_to(&sink) drives against a TurnActivitySink; .stream() returns a pull-style futures_util::Stream; .run() returns a TurnOutput with terminal result and ordered activities.
TurnInput
Text plus image refs. Hosts resolve UI syntax like @path before constructing the turn.
TurnActivity
Semantic activity records with stable ids and correlation ids. Raw session events are runtime/tracing internals.
SessionObservation
Current session read view plus opaque cursor. Recent session observation events can be replayed for reconnect; persistent hosts should use subscribe_and_recover, which yields recoverable live replay gaps and resubscribes from the latest cursor.
let core = lash::LashCore::standard_builder()
    .provider(provider)
    .model(
        lash::ModelSpec::from_token_limits("anthropic/claude-sonnet-4.6", None, 200_000, None)
            .expect("valid model metadata"),
    )
    .store_factory(store_factory)
    .effect_host(std::sync::Arc::new(
        lash::durability::InlineEffectHost::default(),
    ))
    .attachment_store(std::sync::Arc::new(
        lash::persistence::FileAttachmentStore::new(data_dir.join("attachments")),
    ))
    .build()?;

let session = core.session(chat_id).open().await?;
let result = session
    .turn(TurnInput::text(user_text))
    .stream_to(&events)
    .await?;

Example shape

examples/agent-service: opens a session per chat id, runs turns through the facade, streams browser reconnect rows from session observation DTOs, and keeps product chat/board state in the app database beside SessionStoreFactory. Walkthrough: Lash API.

Boundary rule

Above runtime internals, below an app framework. The facade should not import CLI/TUI vocabulary, own product chat storage, or dictate HTTP/frontend protocols.

Background Work

Subagents are sessions. Started async tool calls are explicit process handles backed by the installed process work driver and registry.

Subagent and process path
flowchart LR Rlm["RLM lashlang
await agents.spawn / process ops"] --> Provider["spawn_agent_tool_definition
+ lash_core process admins"] Provider --> Host["ToolContext sessions/processes
+ process events"] Host --> Manager["runtime session manager"] Manager --> Child["start_turn -> stream_turn"] Manager --> Registry["process driver
+ registry"] Registry --> Handles["processes.list
tool.*"] Handles --> Await["await / cancel / submit_error"] Child --> Parent["usage relay + final result"]

The public subagent entry is lash_subagents::spawn_agent_tool_definition; the per-session RlmSubagentToolsProvider is crate-private. Generic process admins expose processes.list and processes.cancel, while start <tool> is the single way to run any ordinary tool as a durable process. A child can fail terminally with await tools.submit_error({ reason: reason })? (defined in lash-subagents), which surfaces back to the parent through the spawn_agent result.

Host Rendering

Live events drive previews; settled output comes from the authoritative read view.

Interactive TUI

run_app drives event handling. Completed turns reconcile with finish_turn_from_read_view: live markdown lanes are cleared, then the current turn is replaced from SessionReadView. The CLI does not consume a final assistant-message stream event.

Export and trace

lash-export reads store state plus the full provider trace JSONL when available, then renders chronological projections, prompt snapshots, context percentages, cache-read percentages, cache-write counts, and usage bars. lash-trace-viewer reads JSONL traces and displays timeline, LLM calls, stream events, and raw records.

read on ·