plugin/runtime

The cheap-build contract, snapshots and durability, wiring a plugin into LashCore, and reference examples in the workspace.

The Cheap-Build Contract

PluginFactory::build() is on the hot path. It runs for every fresh session, every subagent spawn, every fork, every compaction child. Specifically, do not:

Put expensive state on the factory struct (wrap it in Arc), clone the Arc into the SessionPlugin at build time, and clone again into hook closures. The factory lives once per core; sessions are cheap shells.

Snapshots And Durability

A session that uses a SessionStoreFactory (see Persistence) survives process restarts: runtime and plugin snapshots are persisted during committed turn/runtime writes and rehydrated when the session opens again. Plugins participate by implementing two optional SessionPlugin methods:

fn snapshot(&self, _writer: &mut dyn SnapshotWriter) -> Result<PluginSnapshotMeta, PluginError> { /* default: empty meta */ }
fn restore(&self, _meta: &PluginSnapshotMeta, _reader: &dyn SnapshotReader) -> Result<(), PluginError> { Ok(()) }

snapshot() serialises whatever session-local state the plugin holds (counters, accumulated context, pending state machines) and the bytes go into the same checkpoint blob the runtime uses for its own state, persisted through the configured session store. restore() is called on a fresh session plugin after register() when the runtime opens an existing session id. Treat the byte format as private to your plugin; version it inline if you anticipate schema drift.

Plugins that hold no per-session state at all (most prompt contributors, observers, pure tool providers) can skip both methods; the defaults are correct. Plugins that hold mutable per-session state (counters, plan documents, ongoing observations) must implement snapshot/restore to remain consistent across restart; without them, the session resumes with the runtime graph intact but the plugin's view reset to factory defaults.

Snapshot bytes live in the session store, so they share its durability story: every mutating session path runs under the session execution lease, and every commit is one atomic store write that verifies the lease fence plus the head CAS backstop. There is no separate flush of plugin state; your snapshot lands in the same atomic write as the turn that produced it.

Wiring It In

Pass your factory to LashCoreBuilder::plugin(...). It's an Arc<dyn PluginFactory> so the same factory instance can be shared across cores if needed.

use std::sync::Arc;

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"),
    )
    .effect_host(Arc::new(lash::durability::InlineEffectHost::default()))
    .attachment_store(Arc::new(lash::persistence::InMemoryAttachmentStore::new()))
    .plugin(Arc::new(UpdatePlanPluginFactory) as Arc<dyn PluginFactory>)
    .build()?;

For tools that aren't part of a plugin (no state, no lifecycle hooks), you can skip the factory and pass a ToolProvider directly to .tools(Arc::new(MyTools)). Both forms compose: provider-style tools live alongside plugin-registered tools on the same surface.

Reference Examples In The Workspace

If you want a plugin to model your work on, these are good starting points:

Tool + state + runtime event

lash-plugin-plan-mode: single tool with session-local state, semantic runtime-event emission on tool success, root-session-only gating. Good template for "I want to expose one stateful tool and let hosts choose the presentation."

Prompt contribution + factory state

lash-cli's PromptContextPluginFactory: captures an instruction source on the factory, projects it into prompts via reg.prompt().contribute(...). Good template for "I want to inject something into the system prompt."

Per-core connection pool

lash-plugin-mcp: factory keeps an Arc<McpConnectionPool> shared across every session, plus runtime attach_server / detach_server methods. Good template for "I'm wrapping an external service with one persistent connection."

read on ·