Turn Lifecycle
The runtime owns host effects; TurnMachine owns protocol state. This keeps provider calls, tools, code execution, persistence, and streaming outside the pure state machine. Hosts can seed read-only host values into a turn's lashlang scope through RlmProjectedBindings: per-turn, apply them via TurnInput::rlm_project(bindings) from lash_mode_rlm::RlmTurnInputExt; per-session, install rlm_session_projection_extension(bindings) as a session-level mode extension (see lash-mode-rlm/src/projected_bindings.rs).
Handoff Continuations
RLM's continue_as tool ends the current trajectory and queues a successor session with the packed task + seed as its first input. The low-level runtime exposes this as TurnOutcome::Handoff { session_id }. App hosts call TurnBuilder::stream, run, or collect_with; the lash facade drives handoff chains and returns the final turn result. The underlying runtime primitive is stream_turn_following_handoffs.
(seeded by continue_as)"] Pending --> Successor["activate successor RLM session
fresh window, inherited turn_index"] Successor --> Turn
Tool Result Projection
ToolOutputBudgetPluginFactory registers projectors at three hook sites so the same raw output can be persisted in full while the model and rolled-up history see budgeted views.
| Hook | Effect |
|---|---|
BeforeState | Last chance to shape a result before it is committed to the durable session graph; only string-typed JSON payloads are truncated, structured values pass through. |
BeforeModel | Trims content right before it is shown to the LLM in the next request, including non-string payloads rendered for display. |
BeforeHistory | Same string-only truncation as BeforeState, applied as the result rolls into compacted history projections. |
Limits are configured via ToolOutputBudgetConfig (ToolOutputBudgetMode::{Bytes, Tokens}; defaults: 16 KiB, 400 lines). RLM reuses this projector for its own print observations through project_observation_text.
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 (lash-export/src/tree.rs, html.rs).
SQLite + blobs + session_head"] --> State["PersistedSessionState"] State --> Graph["SessionGraph
nodes + leaf_node_id"] Graph --> ReadModel["internal SessionReadModel cache
active_events, messages, tool_calls"] 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 Benchmark 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 benchmark harnesses read deltas from that ledger.
llm_query, spawn_agent(explore), continue_as, list_async_handles"] Surface --> Submit["submit JSON schema validation"] Runtime --> Usage["SessionUsageReport
ledger by source + model"] Usage --> Deltas["diff_usage_reports
benchmark cost deltas"] Runtime --> Trace["provider JSONL trace
LLM started/completed"] Runtime --> Store["SQLite session DB"] Trace --> Export["lash-export prompt snapshots
context %, cached %, token bars"] Store --> Export
Important Flow Rules
These rules are the easiest ones to break when refactoring runtime and UI code together.
-
Normalize host input before prompt work.
Text, file refs, dir refs, and image refs become typed message parts and attachment references. -
Runtime satisfies effects; sans-IO applies responses.
Provider calls, tools, checkpoints, sleeps, and code execution are host responsibilities.
-
Commit policy stays turn-scoped.
TurnCommitPipelineowns graph deltas, checkpoint boundaries, and usage reconciliation. -
UI vocabulary stays in CLI crates.
UiTimeline, activity blocks, render blocks, and chrome surfaces do not belong inlash. -
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.
-
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.
-
Handoffs are not a separate channel.
A handoff is an outcome of the same turn path. App hosts get chaining through
TurnBuilder::stream,run, orcollect_with; lower-level runtime callers opt in withstream_turn_following_handoffs. Managed sessions, turn index, attachment store, and trace ids carry across the boundary automatically.