data/flow

Turn data starts as host input. It becomes normalized messages and attachment refs, passes through plugin context transforms, drives a sans-IO effect loop, commits to the session graph, and finally projects into UI and export views.

Turn Lifecycle

The runtime handles runtime effects; TurnMachine holds protocol state. Reply-producing nondeterministic work crosses RuntimeEffectController with invocation metadata before the response returns to the machine. Hosts can seed read-only projected values into a turn's lashlang scope through RlmProjectedBindings: inline bindings store values, lazy bindings store ProjectionRefs resolved by RLM immediately before execution.

Turn effect loop
sequenceDiagram participant Host as CLI / app host participant Runtime as LashRuntime participant EffectController as RuntimeEffectController participant Plugins as PluginSession participant Machine as TurnMachine participant Provider as ProviderHandle participant Tools as Tool dispatch participant Lang as RLM lashlang runtime participant Store as RuntimePersistence Host->>Runtime: session.turn(TurnInput).stream_to(&sink) Runtime->>Runtime: normalize InputItem text/image refs Runtime->>Plugins: prepare_turn + context transforms Runtime->>Plugins: build TurnDriverPreamble from ProtocolBuildInput(ToolCatalog + protocol context) Runtime->>Machine: build_turn(TurnDriverPreamble, ToolCatalog, PromptTemplate) loop poll effect Machine-->>Runtime: Effect alt LlmCall Runtime->>EffectController: llm_call(invocation, request) EffectController->>Provider: complete(LlmRequest) Provider-->>EffectController: stream events + LlmResponse EffectController-->>Runtime: Result<LlmResponse, LlmCallError> Runtime->>Machine: Response::LlmComplete else ToolCalls loop each PendingToolCall Runtime->>EffectController: tool_call(invocation, call) EffectController->>Tools: native/mode/provider tool Tools-->>EffectController: ToolResult converted to ToolCallOutput + ModelToolReturn EffectController-->>Runtime: CompletedToolCall end Runtime->>Plugins: project_tool_result -> ModelToolReturn Runtime->>Machine: Response::ToolResults else ExecCode Runtime->>EffectController: exec_code(invocation, code) EffectController->>Lang: execute lashlang (with resolved ProjectedBindings) Lang-->>EffectController: ExecResponse + observations EffectController-->>Runtime: Result<ExecResponse, String> Runtime->>Machine: Response::ExecResult else Progress / Done Runtime->>Store: graph delta + checkpoint + usage end end Runtime->>Plugins: finalize_turn Runtime-->>Host: AssembledTurn (TurnOutcome) + stream events

AgentFrameSwitch Continuations

RLM's continue_as tool ends the current trajectory and creates a fresh AgentFrame in the same session with the packed task + seed. The low-level runtime exposes this as TurnOutcome::AgentFrameSwitch { frame_id, task }. App hosts call TurnBuilder::stream_to, run, or pull-style stream; the lash facade drives AgentFrame chains and returns the final turn result. The underlying runtime primitive is stream_turn_with_agent_frames.

AgentFrameSwitch drive loop (runtime internal)
flowchart LR Host["host (CLI / app / lash facade)"] --> Follow["runtime AgentFrame switch follower"] Follow --> Turn["stream_turn"] Turn --> Outcome["TurnOutcome"] Outcome -- "Finished / Stopped" --> Done["final turn result"] Outcome -- "AgentFrameSwitch(frame_id)" --> Frame["current AgentFrame
task + seed"] Frame --> Turn

Tool Result Projection

ToolOutputBudgetPluginFactory registers a single exclusive tool-result projector. The full ToolCallOutput is persisted in the durable graph, while the projector derives the budgeted ModelToolReturn the model and rolled-up history see. Registering more than one projector is rejected at build time.

PayloadProjection
String valuesTruncated to the budget when over the byte/token limit or line count; the head/tail direction is chosen per tool.
Structured valuesRendered to text with their string leaves truncated recursively; non-string scalars and shape pass through.
AttachmentsPassed through untouched as ModelToolReturnPart::Attachment.
batch resultsEach inner call's value is projected before the aggregate is rendered.

Limits are configured via ToolOutputBudgetConfig (ToolOutputBudgetMode::{Bytes, Tokens}; defaults: 16 KiB, 400 lines). RLM budgets its own print observation history with a separate BudgetedJsonProjector built by print_history_projector.

Graph To UI Projection

Chronological rendering is a projection policy, not a storage policy. The graph remains durable and branchable; UI and exports choose readable order. lash-export walks an entire session tree: load_tree_from_paths assembles a LoadedSessionTree from the SQLite store and trace JSONL, and render_tree emits a multi-view HTML.

Read projection path
flowchart LR Store["Store
SQLite/Postgres state + blobs/attachments + session_head"] --> State["PersistedSessionRead"] State --> Graph["SessionGraph
nodes + leaf_node_id"] Graph --> ReadModel["internal SessionReadModel cache
active_events, messages"] ReadModel --> View["SessionReadView"] View --> Chrono["ChronologicalProjection"] Chrono --> Timeline["lash-cli UiTimeline"] Chrono --> Export["lash-export HTML/JSON"] Chrono --> RlmHistory["RLM history projection"] Timeline --> Render["render_block_into
lash-tui Frame"]

Usage And Trace Flow

Token accounting is part of the runtime data flow. Parent turns, subagents, and direct LLM helper calls all report usage into the session ledger, and host applications can read deltas from that ledger.

Usage, trace, and export path
flowchart TD Host["Host application"] --> Runtime["LashRuntime"] Runtime --> Surface["tools and mode controls
llm_query, spawn_agent, continue_as, process admins"] Surface --> Submit["finish JSON schema validation"] Runtime --> Usage["SessionUsageReport
ledger by source + model"] Usage --> Deltas["usage deltas
host reports"] Runtime --> Trace["provider JSONL trace
LLM started/completed"] Runtime --> Store["session store
SQLite or Postgres"] Trace --> Export["lash-export prompt snapshots
context %, cache-read %, token bars"] Store --> Export

Important Flow Rules

These rules are the easiest ones to break when refactoring runtime and UI code together.

  1. Normalize host input before prompt work.

    InputItem::Text and InputItem::ImageRef become typed message parts and attachment references; host file and dir mentions are resolved into text markers before they reach the runtime.

  2. Runtime satisfies effects; sans-IO applies responses.

    Provider calls, direct completions, tools, process admin, checkpoints, sleeps, execution-surface sync, and code execution cross RuntimeEffectController before responses return to TurnMachine.

  3. Commit policy stays turn-scoped.

    TurnBoundary applies graph deltas, checkpoint boundaries, and usage reconciliation.

  4. UI vocabulary stays in CLI crates.

    UiTimeline, activity blocks, render blocks, and chrome surfaces do not belong in lash.

  5. Usage is a ledger, not an exporter-only calculation.

    Runtime usage events carry source/model/token deltas; exporters add trace-derived prompt context detail when the JSONL trace is available.

  6. Tool output is projected before it is read.

    The state, model, and history all read through projector hooks. Skipping these, or projecting twice, breaks budget guarantees and the parity between persisted state and what the model actually saw.

  7. AgentFrame switches are not a separate channel.

    An AgentFrame switch is an outcome of the same turn path. App hosts get chaining through TurnBuilder::stream_to, run, or pull-style stream; lower-level runtime callers opt in with stream_turn_with_agent_frames. The session id, turn index, attachment store, and trace ids carry across the boundary automatically.

read on ·