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#[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}