Skip to main content

lash_core/plugin/
mode.rs

1//! Mode-plugin traits and narrow session/runtime context wrappers.
2//!
3//! Execution modes (standard vs RLM) register their plugin
4//! implementations here; the runtime narrows what a mode plugin can
5//! poke at so external mode crates don't need direct access to
6//! `Session` / `LashRuntime` internals.
7//!
8//! Split out of `plugin/mod.rs` for file size; `pub use` there keeps
9//! the outer module path.
10
11use std::sync::Arc;
12
13use serde::de::DeserializeOwned;
14use serde::{Deserialize, Serialize};
15
16use super::{SessionAppendNode, SessionCreateRequest};
17use crate::runtime::PersistedSessionState;
18use crate::{
19    ExecRequest, ExecResponse, ExecutionMode, LlmRequest, ModeExecutionContext, PromptUsage,
20    SessionReadView, ToolContract, ToolManifest, ToolResult,
21};
22
23/// Session-scoped plugin that initializes, restores, and extends mode
24/// state across a session's lifecycle. External mode crates implement
25/// this via context wrappers ([`ModeSessionContext`],
26/// [`ModeRuntimeContext`]) so they don't need direct access to
27/// `Session`/`LashRuntime` internals — the context narrows what a
28/// plugin can poke at to the capabilities any execution mode
29/// reasonably needs.
30#[async_trait::async_trait]
31pub trait ModeSessionPlugin: Send + Sync {
32    async fn initialize_session(
33        &self,
34        _ctx: ModeSessionContext<'_>,
35    ) -> Result<(), crate::SessionError> {
36        Ok(())
37    }
38
39    async fn restore_session(
40        &self,
41        _ctx: ModeSessionContext<'_>,
42        _state: &PersistedSessionState,
43    ) -> Result<(), crate::SessionError> {
44        Ok(())
45    }
46
47    async fn append_session_nodes(
48        &self,
49        _ctx: ModeSessionContext<'_>,
50        _nodes: &[SessionAppendNode],
51    ) -> Result<(), crate::SessionError> {
52        Ok(())
53    }
54
55    async fn apply_session_extension(
56        &self,
57        _extension: crate::ModeSessionExtensionHandle,
58    ) -> Result<(), crate::SessionError> {
59        Err(crate::SessionError::Protocol(
60            "execution mode does not accept session extensions".to_string(),
61        ))
62    }
63
64    async fn validate_turn_extension(
65        &self,
66        _extension: &crate::ModeTurnExtensionHandle,
67    ) -> Result<(), crate::SessionError> {
68        Ok(())
69    }
70
71    async fn execute_code(
72        &self,
73        _ctx: ModeExecutionContext,
74        _request: ExecRequest,
75    ) -> Result<ExecResponse, crate::SessionError> {
76        Err(crate::SessionError::RlmUnavailable)
77    }
78
79    fn execution_state_dirty(&self) -> bool {
80        false
81    }
82
83    async fn snapshot_execution_state(
84        &self,
85        _ctx: ModeSessionContext<'_>,
86    ) -> Result<Option<Vec<u8>>, crate::SessionError> {
87        Ok(None)
88    }
89
90    async fn restore_execution_state(
91        &self,
92        _ctx: ModeSessionContext<'_>,
93        _data: &[u8],
94    ) -> Result<(), crate::SessionError> {
95        Ok(())
96    }
97
98    fn configure_runtime_from_request(
99        &self,
100        _ctx: ModeRuntimeContext<'_>,
101        _request: &SessionCreateRequest,
102    ) {
103    }
104
105    async fn before_llm_call(
106        &self,
107        _ctx: ModeBeforeLlmCallContext,
108        _request: &LlmRequest,
109    ) -> Result<Option<ModeLlmCallAction>, crate::PluginError> {
110        Ok(None)
111    }
112}
113
114/// Narrow wrapper around `Session` that mode plugins use to
115/// initialize, restore, and extend their per-session state.
116///
117/// Exposes only generic per-session lifecycle capabilities. Mode-local
118/// execution state is owned by the mode plugin itself and is accessed
119/// through [`ModeSessionPlugin`] callbacks.
120/// Prevents mode plugins from reaching into unrelated `Session`
121/// internals.
122pub struct ModeSessionContext<'a> {
123    session_id: &'a str,
124}
125
126impl<'a> ModeSessionContext<'a> {
127    pub(crate) fn new(_session: &'a mut crate::Session, session_id: &'a str) -> Self {
128        Self { session_id }
129    }
130
131    /// ID of the session being initialized/restored. Equivalent to the
132    /// `session_id` previously passed as a separate argument.
133    pub fn session_id(&self) -> &str {
134        self.session_id
135    }
136}
137
138pub struct ModeBeforeLlmCallContext {
139    pub session_id: String,
140    pub host: Arc<dyn crate::plugin::RuntimeSessionHost>,
141    pub state: SessionReadView,
142    pub latest_prompt_usage: Option<PromptUsage>,
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146pub enum ModeLlmCallAction {
147    Handoff { session_id: String },
148}
149
150/// Narrow wrapper around `LashRuntime` that mode plugins use when
151/// configuring the runtime from a fresh `SessionCreateRequest`.
152///
153/// Exposes only the runtime-level capabilities modes need to set
154/// (termination contract, etc.) so plugins don't reach into unrelated
155/// runtime internals.
156pub struct ModeRuntimeContext<'a> {
157    runtime: &'a mut crate::runtime::LashRuntime,
158}
159
160impl<'a> ModeRuntimeContext<'a> {
161    pub(crate) fn new(runtime: &'a mut crate::runtime::LashRuntime) -> Self {
162        Self { runtime }
163    }
164
165    pub fn set_mode_turn_options(&mut self, options: crate::ModeTurnOptions) {
166        self.runtime.set_mode_turn_options(options);
167    }
168}
169
170#[async_trait::async_trait]
171pub trait ModeNativeToolsPlugin: Send + Sync {
172    fn tool_manifests(&self) -> Vec<ToolManifest>;
173    fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
174        self.tool_manifests()
175            .into_iter()
176            .find(|manifest| manifest.name == name)
177    }
178    fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
179
180    async fn execute(
181        &self,
182        context: &crate::tool_dispatch::ToolDispatchContext,
183        name: &str,
184        args: &serde_json::Value,
185        progress: Option<&crate::ProgressSender>,
186    ) -> Option<ToolResult>;
187}
188
189/// Singleton plugin slot that owns the `ProtocolDriverHandle` and
190/// associated preamble (prompt text, tool surface, sync/async flag)
191/// for a given execution mode. Mode-specific crates
192/// (`lash-mode-standard`, `lash-mode-rlm`) register one implementation
193/// each; the runtime picks the one whose `mode_id` matches the session
194/// policy's execution mode, falling back to `build_mode_preamble`
195/// when no plugin claims the slot.
196pub trait ModeProtocolDriverPlugin: Send + Sync {
197    /// Execution-mode identifier this driver implements (e.g.
198    /// `"standard"`, `"rlm"`). Matched against
199    /// `ExecutionMode::plugin_id()` at preamble-build time.
200    fn mode_id(&self) -> &str;
201
202    /// Build the `ModePreamble` (driver handle + prompt text + tool
203    /// surface metadata) for a turn in this mode.
204    fn build_preamble(&self, input: crate::ModeBuildInput) -> crate::ModePreamble;
205}
206
207/// Mode-specific extras carried on a `SessionCreateRequest`.
208///
209/// Each variant matches an `ExecutionMode` value and carries the
210/// settings only that mode cares about. Adding a new mode means adding
211/// a new variant with its own struct — no mode-specific fields ever
212/// leak into the base request.
213#[derive(Clone, Debug, Serialize)]
214pub struct ModeExtras {
215    pub mode_id: ExecutionMode,
216    #[serde(default)]
217    pub payload: serde_json::Value,
218}
219
220impl Default for ModeExtras {
221    fn default() -> Self {
222        Self::empty(ExecutionMode::standard())
223    }
224}
225
226impl ModeExtras {
227    pub fn empty(mode_id: ExecutionMode) -> Self {
228        Self {
229            mode_id,
230            payload: serde_json::Value::Object(serde_json::Map::new()),
231        }
232    }
233
234    pub fn typed<T>(mode_id: ExecutionMode, extras: T) -> Result<Self, serde_json::Error>
235    where
236        T: Serialize,
237    {
238        Ok(Self {
239            mode_id,
240            payload: serde_json::to_value(extras)?,
241        })
242    }
243
244    pub fn decode<T>(&self, expected_mode: &ExecutionMode) -> Result<Option<T>, serde_json::Error>
245    where
246        T: DeserializeOwned,
247    {
248        if &self.mode_id != expected_mode {
249            return Ok(None);
250        }
251        serde_json::from_value(self.payload.clone()).map(Some)
252    }
253}
254
255impl<'de> Deserialize<'de> for ModeExtras {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: serde::Deserializer<'de>,
259    {
260        let value = serde_json::Value::deserialize(deserializer)?;
261        if let Some(object) = value.as_object() {
262            if let (Some(mode_id), Some(payload)) = (object.get("mode_id"), object.get("payload")) {
263                let mode_id = ExecutionMode::deserialize(mode_id.clone())
264                    .map_err(serde::de::Error::custom)?;
265                return Ok(Self {
266                    mode_id,
267                    payload: payload.clone(),
268                });
269            }
270            if let Some(mode) = object.get("mode").and_then(serde_json::Value::as_str) {
271                let mut payload = object.clone();
272                payload.remove("mode");
273                return Ok(Self {
274                    mode_id: ExecutionMode::new(mode),
275                    payload: serde_json::Value::Object(payload),
276                });
277            }
278        }
279        Err(serde::de::Error::custom("invalid mode extras payload"))
280    }
281}
282
283#[derive(Clone, Debug, Default, Serialize, Deserialize)]
284pub struct StandardCreateExtras {}