Skip to main content

lash_core/plugin/
runtime_host.rs

1use serde::{Deserialize, Serialize};
2
3use super::*;
4
5#[async_trait::async_trait]
6pub trait SessionStateService: Send + Sync {
7    async fn snapshot_current(&self) -> Result<SessionSnapshot, PluginError> {
8        Err(PluginError::Session(
9            "session snapshots are unavailable in this runtime".to_string(),
10        ))
11    }
12
13    async fn snapshot_session(&self, _session_id: &str) -> Result<SessionSnapshot, PluginError> {
14        Err(PluginError::Session(
15            "session lookup is unavailable in this runtime".to_string(),
16        ))
17    }
18
19    async fn tool_catalog(&self, _session_id: &str) -> Result<Vec<serde_json::Value>, PluginError> {
20        Err(PluginError::Session(
21            "tool catalogs are unavailable in this runtime".to_string(),
22        ))
23    }
24
25    async fn shared_tool_catalog(
26        &self,
27        session_id: &str,
28    ) -> Result<std::sync::Arc<Vec<serde_json::Value>>, PluginError> {
29        Ok(std::sync::Arc::new(self.tool_catalog(session_id).await?))
30    }
31
32    async fn tool_state(&self, _session_id: &str) -> Result<crate::ToolState, PluginError> {
33        Err(PluginError::Session(
34            "tool state is unavailable in this session".to_string(),
35        ))
36    }
37
38    async fn apply_tool_state(
39        &self,
40        _session_id: &str,
41        _snapshot: crate::ToolState,
42    ) -> Result<u64, PluginError> {
43        Err(PluginError::Session(
44            "tool state mutation is unavailable in this session".to_string(),
45        ))
46    }
47
48    async fn set_tools_availability(
49        &self,
50        session_id: &str,
51        tool_names: &[String],
52        availability: Option<crate::ToolAvailability>,
53    ) -> Result<u64, PluginError> {
54        let mut snapshot = self.tool_state(session_id).await?;
55        for name in tool_names {
56            let id = snapshot
57                .tool_manifests()
58                .into_iter()
59                .find(|manifest| manifest.name == *name)
60                .map(|manifest| manifest.id)
61                .ok_or_else(|| PluginError::Session(format!("unknown tool `{name}`")))?;
62            snapshot
63                .set_availability(&id, availability)
64                .map_err(|err| PluginError::Session(err.to_string()))?;
65        }
66        self.apply_tool_state(session_id, snapshot).await
67    }
68
69    async fn set_tool_availability(
70        &self,
71        session_id: &str,
72        tool_name: &str,
73        availability: Option<ToolAvailability>,
74    ) -> Result<u64, PluginError> {
75        let mut snapshot = self.tool_state(session_id).await?;
76        let id = snapshot
77            .tool_manifests()
78            .into_iter()
79            .find(|manifest| manifest.name == tool_name)
80            .map(|manifest| manifest.id)
81            .ok_or_else(|| PluginError::Session(format!("unknown tool `{tool_name}`")))?;
82        snapshot
83            .set_availability(&id, availability)
84            .map_err(|err| PluginError::Session(err.to_string()))?;
85        self.apply_tool_state(session_id, snapshot).await
86    }
87}
88
89#[async_trait::async_trait]
90pub trait SessionLifecycleService: Send + Sync {
91    async fn create_session(
92        &self,
93        _request: SessionCreateRequest,
94    ) -> Result<SessionHandle, PluginError> {
95        Err(PluginError::Session(
96            "session creation is unavailable in this runtime".to_string(),
97        ))
98    }
99
100    async fn close_session(&self, _session_id: &str) -> Result<(), PluginError> {
101        Err(PluginError::Session(
102            "session closing is unavailable in this runtime".to_string(),
103        ))
104    }
105
106    async fn start_turn(
107        &self,
108        _request: SessionTurnRequest<'_>,
109    ) -> Result<AssembledTurn, PluginError> {
110        Err(PluginError::Session(
111            "session execution is unavailable in this runtime".to_string(),
112        ))
113    }
114}
115
116#[async_trait::async_trait]
117pub trait SessionGraphService: Send + Sync {
118    async fn append_session_nodes(
119        &self,
120        _session_id: &str,
121        _request: AppendSessionNodesRequest,
122    ) -> Result<AppendSessionNodesResult, PluginError> {
123        Err(PluginError::Session(
124            "session graph mutation is unavailable in this session".to_string(),
125        ))
126    }
127
128    async fn emit_trace_event(
129        &self,
130        _context: lash_trace::TraceContext,
131        _event: lash_trace::TraceEvent,
132    ) -> Result<(), PluginError> {
133        Ok(())
134    }
135}
136
137/// Result of a single-shot direct LLM call.
138#[derive(Clone, Debug, Serialize, Deserialize)]
139pub struct DirectCompletion {
140    pub text: String,
141    pub usage: crate::TokenUsage,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
145pub struct DirectLlmCompletion {
146    pub response: crate::LlmResponse,
147    pub usage: crate::TokenUsage,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize)]
151pub struct SessionTurnInput {
152    pub session_id: String,
153    pub turn_id: String,
154    pub input: TurnInput,
155}
156
157pub struct SessionTurnRequest<'run> {
158    turn: SessionTurnInput,
159    scoped_effect_controller: crate::ScopedEffectController<'run>,
160}
161
162impl<'run> SessionTurnRequest<'run> {
163    pub fn new(
164        session_id: impl Into<String>,
165        turn_id: impl Into<String>,
166        mut input: TurnInput,
167        scoped_effect_controller: crate::ScopedEffectController<'run>,
168    ) -> Result<Self, PluginError> {
169        let session_id = session_id.into();
170        let turn_id = turn_id.into();
171        if turn_id.trim().is_empty() {
172            return Err(PluginError::Session(
173                "session turns require a non-empty stable turn id".to_string(),
174            ));
175        }
176        if scoped_effect_controller.turn_id() != Some(turn_id.as_str()) {
177            return Err(PluginError::Session(format!(
178                "session turn `{turn_id}` requires an effect turn scope with the same id"
179            )));
180        }
181        if scoped_effect_controller.execution_scope().session_id() != Some(session_id.as_str()) {
182            return Err(PluginError::Session(format!(
183                "session turn `{turn_id}` requires an execution scope for session `{session_id}`"
184            )));
185        }
186        if let Some(input_turn_id) = input.trace_turn_id.as_deref()
187            && input_turn_id != turn_id
188        {
189            return Err(PluginError::Session(format!(
190                "input trace_turn_id `{input_turn_id}` does not match turn id `{turn_id}`"
191            )));
192        }
193        input.trace_turn_id = Some(turn_id.clone());
194        Ok(Self {
195            turn: SessionTurnInput {
196                session_id,
197                turn_id,
198                input,
199            },
200            scoped_effect_controller,
201        })
202    }
203
204    pub fn session_id(&self) -> &str {
205        &self.turn.session_id
206    }
207
208    pub fn turn_id(&self) -> &str {
209        &self.turn.turn_id
210    }
211
212    pub fn input(&self) -> &TurnInput {
213        &self.turn.input
214    }
215
216    pub fn scoped_effect_controller(&self) -> &crate::ScopedEffectController<'run> {
217        &self.scoped_effect_controller
218    }
219
220    pub fn into_parts(self) -> (SessionTurnInput, crate::ScopedEffectController<'run>) {
221        (self.turn, self.scoped_effect_controller)
222    }
223}
224
225#[derive(Clone, Debug, Serialize, Deserialize)]
226pub struct AppendSessionNodesRequest {
227    pub nodes: Vec<SessionAppendNode>,
228    #[serde(default)]
229    pub requires_ancestor_node_id: Option<String>,
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize)]
233#[serde(tag = "status", rename_all = "snake_case")]
234pub enum AppendSessionNodesResult {
235    Appended {
236        node_ids: Vec<String>,
237        leaf_node_id: String,
238    },
239    StaleBranch {
240        current_leaf_node_id: Option<String>,
241    },
242}