Skip to main content

lash_core/plugin/
protocol.rs

1//! Protocol-plugin traits and narrow session/runtime context wrappers.
2//!
3//! Protocol plugins register their implementations here; the runtime narrows
4//! what a protocol plugin can poke at so external crates don't need direct access to
5//! `Session` / `LashRuntime` internals.
6//!
7//! Split out of `plugin/mod.rs` for file size; `pub use` there keeps
8//! the outer module path.
9
10use std::collections::BTreeMap;
11use std::sync::Arc;
12
13use serde::de::DeserializeOwned;
14use serde::{Deserialize, Serialize};
15
16use super::{SessionAppendNode, SessionCreateRequest};
17use crate::runtime::RuntimeSessionState;
18use crate::{
19    ExecRequest, ExecResponse, LlmRequest, PromptUsage, RuntimeExecutionContext, SessionReadView,
20};
21
22/// Session-scoped plugin that initializes, restores, and extends protocol
23/// state across a session's lifecycle. External protocol crates implement
24/// this via context wrappers ([`ProtocolSessionContext`],
25/// [`ProtocolRuntimeContext`]) so they don't need direct access to
26/// `Session`/`LashRuntime` internals — the context narrows what a
27/// plugin can poke at to the capabilities any protocol reasonably needs.
28#[async_trait::async_trait]
29pub trait ProtocolSessionPlugin: Send + Sync {
30    async fn initialize_session(
31        &self,
32        _ctx: ProtocolSessionContext<'_>,
33    ) -> Result<(), crate::SessionError> {
34        Ok(())
35    }
36
37    async fn restore_session(
38        &self,
39        _ctx: ProtocolSessionContext<'_>,
40        _state: &RuntimeSessionState,
41    ) -> Result<(), crate::SessionError> {
42        Ok(())
43    }
44
45    async fn append_session_nodes(
46        &self,
47        _ctx: ProtocolSessionContext<'_>,
48        _nodes: &[SessionAppendNode],
49    ) -> Result<(), crate::SessionError> {
50        Ok(())
51    }
52
53    async fn apply_session_extension(
54        &self,
55        _extension: crate::ProtocolSessionExtensionHandle,
56    ) -> Result<(), crate::SessionError> {
57        Err(crate::SessionError::Protocol(
58            "protocol does not accept session extensions".to_string(),
59        ))
60    }
61
62    async fn validate_turn_extension(
63        &self,
64        _extension: &crate::ProtocolTurnExtensionHandle,
65    ) -> Result<(), crate::SessionError> {
66        Ok(())
67    }
68
69    fn configure_runtime_from_request(
70        &self,
71        _ctx: ProtocolRuntimeContext<'_>,
72        _request: &SessionCreateRequest,
73    ) -> Result<(), crate::SessionError> {
74        Ok(())
75    }
76
77    async fn before_llm_call(
78        &self,
79        _ctx: ProtocolBeforeLlmCallContext<'_>,
80        _request: &LlmRequest,
81    ) -> Result<Option<ProtocolLlmCallAction>, crate::PluginError> {
82        Ok(None)
83    }
84}
85
86/// Narrow wrapper around `Session` that protocol plugins use to
87/// initialize, restore, and extend their per-session state.
88///
89/// Exposes only generic per-session lifecycle capabilities. Protocol-local
90/// execution state is owned by the protocol plugin itself and is accessed
91/// through [`ProtocolSessionPlugin`] callbacks.
92/// Prevents protocol plugins from reaching into unrelated `Session`
93/// internals.
94pub struct ProtocolSessionContext<'a> {
95    session_id: &'a str,
96}
97
98impl<'a> ProtocolSessionContext<'a> {
99    pub(crate) fn new(_session: &'a mut crate::Session, session_id: &'a str) -> Self {
100        Self { session_id }
101    }
102
103    /// ID of the session being initialized/restored. Equivalent to the
104    /// `session_id` previously passed as a separate argument.
105    pub fn session_id(&self) -> &str {
106        self.session_id
107    }
108}
109
110pub struct ProtocolBeforeLlmCallContext<'run> {
111    pub session_id: String,
112    pub sessions: Arc<dyn crate::plugin::SessionStateService>,
113    pub session_graph: Arc<dyn crate::plugin::SessionGraphService>,
114    pub processes: Arc<dyn crate::ProcessService>,
115    pub state: SessionReadView,
116    pub latest_prompt_usage: Option<PromptUsage>,
117    pub(crate) direct_completions: crate::DirectCompletionClient<'run>,
118    pub(crate) process_parent_invocation: crate::RuntimeInvocation,
119    pub(crate) effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
120}
121
122impl ProtocolBeforeLlmCallContext<'_> {
123    pub async fn direct_llm_completion(
124        &self,
125        request: crate::LlmRequest,
126        usage_source: &str,
127    ) -> Result<crate::DirectLlmCompletion, crate::PluginError> {
128        self.direct_completions
129            .direct_llm_completion(request, usage_source)
130            .await
131    }
132
133    pub fn process_scope(&self) -> crate::ProcessOpScope<'_> {
134        crate::ProcessOpScope::new(self.effect_controller.scoped())
135            .with_parent_invocation(Some(self.process_parent_invocation.clone()))
136    }
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum ProtocolLlmCallAction {
141    SwitchAgentFrame { frame_id: String },
142}
143
144/// Narrow wrapper around `LashRuntime` that protocol plugins use when
145/// configuring the runtime from a fresh `SessionCreateRequest`.
146///
147/// Exposes only the runtime-level capabilities protocols need to set
148/// (termination contract, etc.) so plugins don't reach into unrelated
149/// runtime internals.
150pub struct ProtocolRuntimeContext<'a> {
151    runtime: &'a mut crate::runtime::LashRuntime,
152}
153
154impl<'a> ProtocolRuntimeContext<'a> {
155    pub(crate) fn new(runtime: &'a mut crate::runtime::LashRuntime) -> Self {
156        Self { runtime }
157    }
158
159    pub fn set_protocol_turn_options(&mut self, options: crate::ProtocolTurnOptions) {
160        self.runtime.set_protocol_turn_options(options);
161    }
162}
163
164#[async_trait::async_trait]
165pub trait CodeExecutorPlugin: Send + Sync {
166    async fn execute_code(
167        &self,
168        ctx: RuntimeExecutionContext<'_>,
169        request: ExecRequest,
170    ) -> Result<ExecResponse, crate::SessionError>;
171
172    fn execution_state_dirty(&self) -> bool {
173        false
174    }
175
176    async fn snapshot_execution_state(
177        &self,
178        _ctx: ProtocolSessionContext<'_>,
179    ) -> Result<Option<Vec<u8>>, crate::SessionError> {
180        Ok(None)
181    }
182
183    async fn restore_execution_state(
184        &self,
185        _ctx: ProtocolSessionContext<'_>,
186        _data: &[u8],
187    ) -> Result<(), crate::SessionError> {
188        Ok(())
189    }
190}
191
192pub trait AssistantProseProjectorPlugin: Send + Sync {
193    fn project_assistant_prose(&self, text: &str) -> String;
194}
195
196/// Singleton plugin slot that owns the `ProtocolDriverHandle` and
197/// associated preamble (prompt text, tool surface, sync/async flag)
198/// for this session. Plugin stack construction must install exactly one
199/// implementation.
200pub trait ProtocolDriverPlugin: Send + Sync {
201    /// Build the `TurnDriverPreamble` (driver handle + prompt text + tool
202    /// surface metadata) for a turn.
203    fn build_preamble(&self, input: crate::ProtocolBuildInput) -> crate::TurnDriverPreamble;
204}
205
206/// Plugin-owned options carried on a `SessionCreateRequest`.
207#[derive(Clone, Debug, Default, Serialize, Deserialize)]
208pub struct PluginOptions {
209    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
210    pub plugins: BTreeMap<String, serde_json::Value>,
211}
212
213impl PluginOptions {
214    pub fn empty() -> Self {
215        Self::default()
216    }
217
218    pub fn typed<T>(plugin_id: impl Into<String>, extras: T) -> Result<Self, serde_json::Error>
219    where
220        T: Serialize,
221    {
222        let mut options = Self::default();
223        options.insert_typed(plugin_id, extras)?;
224        Ok(options)
225    }
226
227    pub fn insert_typed<T>(
228        &mut self,
229        plugin_id: impl Into<String>,
230        extras: T,
231    ) -> Result<(), serde_json::Error>
232    where
233        T: Serialize,
234    {
235        self.plugins
236            .insert(plugin_id.into(), serde_json::to_value(extras)?);
237        Ok(())
238    }
239
240    pub fn decode<T>(&self, plugin_id: &str) -> Result<Option<T>, serde_json::Error>
241    where
242        T: DeserializeOwned,
243    {
244        self.plugins
245            .get(plugin_id)
246            .cloned()
247            .map(serde_json::from_value)
248            .transpose()
249    }
250}