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_tool_membership(
52 &self,
53 session_id: &str,
54 tool_names: &[String],
55 present: bool,
56 ) -> Result<u64, PluginError> {
57 let mut snapshot = self.tool_state(session_id).await?;
58 for name in tool_names {
59 let id = snapshot
60 .iter()
61 .find(|(_, entry)| entry.manifest().name == *name)
62 .map(|(id, _)| id.clone())
63 .ok_or_else(|| PluginError::Session(format!("unknown tool `{name}`")))?;
64 snapshot
65 .set_membership(&id, present)
66 .map_err(|err| PluginError::Session(err.to_string()))?;
67 }
68 self.apply_tool_state(session_id, snapshot).await
69 }
70}
71
72#[async_trait::async_trait]
73pub trait SessionLifecycleService: Send + Sync {
74 async fn create_session(
75 &self,
76 _request: SessionCreateRequest,
77 ) -> Result<SessionHandle, PluginError> {
78 Err(PluginError::Session(
79 "session creation is unavailable in this runtime".to_string(),
80 ))
81 }
82
83 async fn close_session(&self, _session_id: &str) -> Result<(), PluginError> {
84 Err(PluginError::Session(
85 "session closing is unavailable in this runtime".to_string(),
86 ))
87 }
88
89 async fn start_turn(
90 &self,
91 _request: SessionTurnRequest<'_>,
92 ) -> Result<AssembledTurn, PluginError> {
93 Err(PluginError::Session(
94 "session execution is unavailable in this runtime".to_string(),
95 ))
96 }
97}
98
99#[async_trait::async_trait]
100pub trait SessionGraphService: Send + Sync {
101 async fn append_session_nodes(
102 &self,
103 _session_id: &str,
104 _request: AppendSessionNodesRequest,
105 ) -> Result<AppendSessionNodesResult, PluginError> {
106 Err(PluginError::Session(
107 "session graph mutation is unavailable in this session".to_string(),
108 ))
109 }
110
111 async fn emit_trace_event(
112 &self,
113 _context: lash_trace::TraceContext,
114 _event: lash_trace::TraceEvent,
115 ) -> Result<(), PluginError> {
116 Ok(())
117 }
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
122pub struct DirectCompletion {
123 pub text: String,
124 pub usage: crate::TokenUsage,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize)]
128pub struct DirectLlmCompletion {
129 pub response: crate::LlmResponse,
130 pub usage: crate::TokenUsage,
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize)]
134pub struct SessionTurnInput {
135 pub session_id: String,
136 pub turn_id: String,
137 pub input: TurnInput,
138}
139
140pub struct SessionTurnRequest<'run> {
141 turn: SessionTurnInput,
142 scoped_effect_controller: crate::ScopedEffectController<'run>,
143}
144
145impl<'run> SessionTurnRequest<'run> {
146 pub fn new(
147 session_id: impl Into<String>,
148 turn_id: impl Into<String>,
149 mut input: TurnInput,
150 scoped_effect_controller: crate::ScopedEffectController<'run>,
151 ) -> Result<Self, PluginError> {
152 let session_id = session_id.into();
153 let turn_id = turn_id.into();
154 if turn_id.trim().is_empty() {
155 return Err(PluginError::Session(
156 "session turns require a non-empty stable turn id".to_string(),
157 ));
158 }
159 if scoped_effect_controller.turn_id() != Some(turn_id.as_str()) {
160 return Err(PluginError::Session(format!(
161 "session turn `{turn_id}` requires an effect turn scope with the same id"
162 )));
163 }
164 if scoped_effect_controller.execution_scope().session_id() != Some(session_id.as_str()) {
165 return Err(PluginError::Session(format!(
166 "session turn `{turn_id}` requires an execution scope for session `{session_id}`"
167 )));
168 }
169 if let Some(input_turn_id) = input.trace_turn_id.as_deref()
170 && input_turn_id != turn_id
171 {
172 return Err(PluginError::Session(format!(
173 "input trace_turn_id `{input_turn_id}` does not match turn id `{turn_id}`"
174 )));
175 }
176 input.trace_turn_id = Some(turn_id.clone());
177 Ok(Self {
178 turn: SessionTurnInput {
179 session_id,
180 turn_id,
181 input,
182 },
183 scoped_effect_controller,
184 })
185 }
186
187 pub fn session_id(&self) -> &str {
188 &self.turn.session_id
189 }
190
191 pub fn turn_id(&self) -> &str {
192 &self.turn.turn_id
193 }
194
195 pub fn input(&self) -> &TurnInput {
196 &self.turn.input
197 }
198
199 pub fn scoped_effect_controller(&self) -> &crate::ScopedEffectController<'run> {
200 &self.scoped_effect_controller
201 }
202
203 pub fn into_parts(self) -> (SessionTurnInput, crate::ScopedEffectController<'run>) {
204 (self.turn, self.scoped_effect_controller)
205 }
206}
207
208#[derive(Clone, Debug, Serialize, Deserialize)]
209pub struct AppendSessionNodesRequest {
210 pub nodes: Vec<SessionAppendNode>,
211 #[serde(default)]
212 pub requires_ancestor_node_id: Option<String>,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize)]
216#[serde(tag = "status", rename_all = "snake_case")]
217pub enum AppendSessionNodesResult {
218 Appended {
219 node_ids: Vec<String>,
220 leaf_node_id: String,
221 },
222 StaleBranch {
223 current_leaf_node_id: Option<String>,
224 },
225}