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.
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.
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.
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.
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.
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.
The app-facing facade. Hides CLI bootstrap and runtime plumbing; leaves persistence, HTTP, auth, and frontend streaming to the host.
LashCoreLashCore::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)RlmProtocolPluginFactory (projection resolver, deferred tool resolver, and a required Lashlang artifact store), plus the default runtime plugin stack.LashSessionrun(...), turn(...), read_view(), observe(), and admin(). Carries an optional parent session id.TurnBuilder.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@path before constructing the turn.TurnActivitySessionObservationsubscribe_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?;
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.
Above runtime internals, below an app framework. The facade should not import CLI/TUI vocabulary, own product chat storage, or dictate HTTP/frontend protocols.
Subagents are sessions. Started async tool calls are explicit process handles backed by the installed process work driver and registry.
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.
Live events drive previews; settled output comes from the authoritative read view.
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.
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.