1use lash_sansio::PromptUsage;
10
11use crate::session_model::{
12 Message, SessionEventRecord, SessionPolicy, TokenUsage, plugin_message_to_message,
13};
14use crate::{PersistedTurnState, ToolCallRecord};
15
16use super::usage::TokenLedgerEntry;
17
18#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
20pub struct SessionStateEnvelope {
21 pub session_id: String,
22 #[serde(default)]
23 pub policy: SessionPolicy,
24 #[serde(default)]
25 pub session_graph: crate::SessionGraph,
26 #[serde(default)]
27 pub turn_index: usize,
28 #[serde(default)]
29 pub token_usage: TokenUsage,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub last_prompt_usage: Option<PromptUsage>,
32 #[serde(default)]
33 pub mode_turn_options: crate::ModeTurnOptions,
34}
35
36impl SessionStateEnvelope {
37 pub(crate) fn read_model(&self) -> crate::session_graph::SessionReadModel {
38 self.session_graph.read_model()
39 }
40
41 pub fn replace_active_read_state(
42 &mut self,
43 messages: &[Message],
44 tool_calls: &[ToolCallRecord],
45 ) {
46 self.session_graph
47 .replace_active_read_state(messages, tool_calls);
48 }
49
50 pub fn replace_active_tool_calls(&mut self, tool_calls: &[ToolCallRecord]) {
51 self.session_graph.replace_active_tool_calls(tool_calls);
52 }
53
54 pub fn append_active_read_delta(
55 &mut self,
56 messages: &[Message],
57 tool_calls: &[ToolCallRecord],
58 ) {
59 self.session_graph
60 .append_active_read_delta(messages, tool_calls);
61 }
62
63 pub fn read_view(&self) -> crate::SessionReadView {
64 crate::SessionReadView::from_exported_state(self)
65 }
66}
67
68impl Default for SessionStateEnvelope {
69 fn default() -> Self {
70 Self {
71 session_id: "root".to_string(),
72 policy: SessionPolicy::default(),
73 session_graph: crate::SessionGraph::default(),
74 turn_index: 0,
75 token_usage: TokenUsage::default(),
76 last_prompt_usage: None,
77 mode_turn_options: crate::ModeTurnOptions::default(),
78 }
79 }
80}
81
82#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
84pub struct PersistedSessionState {
85 pub session_id: String,
86 #[serde(default)]
87 pub policy: SessionPolicy,
88 #[serde(default)]
89 pub session_graph: crate::SessionGraph,
90 #[serde(default)]
91 pub turn_index: usize,
92 #[serde(default)]
93 pub token_usage: TokenUsage,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub last_prompt_usage: Option<PromptUsage>,
96 #[serde(default)]
97 pub mode_turn_options: crate::ModeTurnOptions,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub tool_state_ref: Option<crate::store::BlobRef>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub tool_state_generation: Option<u64>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub tool_state_snapshot: Option<crate::ToolState>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub plugin_snapshot_ref: Option<crate::store::BlobRef>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub plugin_snapshot_revision: Option<u64>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub plugin_snapshot: Option<crate::PluginSessionSnapshot>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub execution_state_ref: Option<crate::store::BlobRef>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub execution_state_snapshot: Option<Vec<u8>>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub token_ledger: Vec<TokenLedgerEntry>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub checkpoint_ref: Option<crate::store::BlobRef>,
122 #[serde(skip)]
126 pub head_revision: Option<u64>,
127 #[serde(skip)]
131 pub graph_replace_required: bool,
132}
133
134impl PersistedSessionState {
135 pub fn from_state(state: SessionStateEnvelope) -> Self {
136 Self {
137 session_id: state.session_id,
138 policy: state.policy,
139 session_graph: state.session_graph,
140 turn_index: state.turn_index,
141 token_usage: state.token_usage,
142 last_prompt_usage: state.last_prompt_usage,
143 mode_turn_options: state.mode_turn_options,
144 tool_state_ref: None,
145 tool_state_generation: None,
146 tool_state_snapshot: None,
147 plugin_snapshot_ref: None,
148 plugin_snapshot_revision: None,
149 plugin_snapshot: None,
150 execution_state_ref: None,
151 execution_state_snapshot: None,
152 token_ledger: Vec::new(),
153 checkpoint_ref: None,
154 head_revision: None,
155 graph_replace_required: false,
156 }
157 }
158
159 pub fn export_state(&self) -> SessionStateEnvelope {
160 SessionStateEnvelope {
161 session_id: self.session_id.clone(),
162 policy: self.policy.clone(),
163 session_graph: self.session_graph.clone(),
164 turn_index: self.turn_index,
165 token_usage: self.token_usage.clone(),
166 last_prompt_usage: self.last_prompt_usage.clone(),
167 mode_turn_options: self.mode_turn_options.clone(),
168 }
169 }
170
171 pub fn apply_exported_state(&mut self, state: &SessionStateEnvelope) {
172 self.session_id = state.session_id.clone();
173 self.policy = state.policy.clone();
174 self.session_graph = state.session_graph.clone();
175 self.turn_index = state.turn_index;
176 self.token_usage = state.token_usage.clone();
177 self.last_prompt_usage = state.last_prompt_usage.clone();
178 self.mode_turn_options = state.mode_turn_options.clone();
179 }
180
181 pub fn stamp_runtime_state(
182 &mut self,
183 tool_state: Option<&crate::ToolState>,
184 plugin_snapshot: Option<&crate::PluginSessionSnapshot>,
185 ) {
186 self.tool_state_snapshot = tool_state.cloned();
187 self.tool_state_generation = tool_state.map(|snapshot| snapshot.generation());
188 self.plugin_snapshot = plugin_snapshot.cloned();
189 }
190
191 pub fn usage_report(&self) -> super::usage::SessionUsageReport {
192 super::usage::SessionUsageReport::from_entries(&self.token_ledger)
193 }
194
195 pub(crate) fn read_model(&self) -> crate::session_graph::SessionReadModel {
196 self.session_graph.read_model()
197 }
198
199 pub fn replace_active_read_state(
200 &mut self,
201 messages: &[Message],
202 tool_calls: &[ToolCallRecord],
203 ) {
204 self.session_graph
205 .replace_active_read_state(messages, tool_calls);
206 self.graph_replace_required = false;
207 }
208
209 pub fn replace_active_tool_calls(&mut self, tool_calls: &[ToolCallRecord]) {
210 self.session_graph.replace_active_tool_calls(tool_calls);
211 self.graph_replace_required = false;
212 }
213
214 pub fn append_active_read_delta(
215 &mut self,
216 messages: &[Message],
217 tool_calls: &[ToolCallRecord],
218 ) {
219 self.session_graph
220 .append_active_read_delta(messages, tool_calls);
221 }
222
223 pub fn append_active_conversation_messages(&mut self, messages: &[Message]) {
224 self.session_graph
225 .append_active_conversation_messages(messages);
226 }
227
228 pub fn read_view(&self) -> crate::SessionReadView {
229 crate::SessionReadView::from_persisted_state(self)
230 }
231
232 pub fn session_graph(&self) -> &crate::SessionGraph {
233 &self.session_graph
234 }
235
236 pub fn policy(&self) -> &SessionPolicy {
237 &self.policy
238 }
239
240 pub fn turn_state(&self) -> PersistedTurnState {
241 PersistedTurnState {
242 turn_index: self.turn_index,
243 token_usage: self.token_usage.clone(),
244 last_prompt_usage: self.last_prompt_usage.clone(),
245 mode_turn_options: self.mode_turn_options.clone(),
246 }
247 }
248
249 pub fn token_ledger(&self) -> &[TokenLedgerEntry] {
250 &self.token_ledger
251 }
252
253 pub fn apply_persisted_commit_result(&mut self, result: crate::store::RuntimeCommitResult) {
254 self.head_revision = Some(result.head_revision);
255 self.checkpoint_ref = Some(result.checkpoint_ref);
256 self.tool_state_ref = result.manifest.tool_state_ref;
257 if let Some(snapshot) = self.tool_state_snapshot.as_ref() {
258 self.tool_state_generation = Some(snapshot.generation());
259 } else if self.tool_state_ref.is_none() {
260 self.tool_state_generation = None;
261 }
262 self.plugin_snapshot_ref = result.manifest.plugin_snapshot_ref;
263 self.plugin_snapshot_revision = result.manifest.plugin_snapshot_revision;
264 self.execution_state_ref = result.manifest.execution_state_ref;
265 self.graph_replace_required = false;
266 self.tool_state_snapshot = None;
267 self.plugin_snapshot = None;
268 self.execution_state_snapshot = None;
269 }
270
271 pub fn discard_runtime_snapshots(&mut self) {
272 self.tool_state_snapshot = None;
273 self.plugin_snapshot = None;
274 self.execution_state_snapshot = None;
275 }
276
277 pub fn set_execution_state_snapshot(&mut self, execution_state_snapshot: Option<Vec<u8>>) {
278 if execution_state_snapshot.is_none() {
279 self.execution_state_ref = None;
280 }
281 self.execution_state_snapshot = execution_state_snapshot;
282 }
283
284 pub fn execution_state_snapshot(&self) -> Option<&[u8]> {
285 self.execution_state_snapshot.as_deref()
286 }
287
288 pub fn refresh_plugin_snapshots(&mut self, plugins: &crate::PluginSession) {
289 let tool_registry = plugins.tool_registry();
290 let generation = tool_registry.generation();
291 if self.tool_state_ref.is_none() || self.tool_state_generation != Some(generation) {
292 let snapshot = tool_registry.export_state();
293 self.tool_state_generation = Some(snapshot.generation());
294 self.tool_state_snapshot = Some(snapshot);
295 }
296
297 let revision = plugins.snapshot_revision_fingerprint();
298 if self.plugin_snapshot_ref.is_none() || self.plugin_snapshot_revision != Some(revision) {
299 self.plugin_snapshot = plugins.snapshot().ok();
300 }
301 self.plugin_snapshot_revision = Some(revision);
302 }
303}
304
305impl Default for PersistedSessionState {
306 fn default() -> Self {
307 Self {
308 session_id: "root".to_string(),
309 policy: SessionPolicy::default(),
310 session_graph: crate::SessionGraph::default(),
311 turn_index: 0,
312 token_usage: TokenUsage::default(),
313 last_prompt_usage: None,
314 mode_turn_options: crate::ModeTurnOptions::default(),
315 tool_state_ref: None,
316 tool_state_generation: None,
317 tool_state_snapshot: None,
318 plugin_snapshot_ref: None,
319 plugin_snapshot_revision: None,
320 plugin_snapshot: None,
321 execution_state_ref: None,
322 execution_state_snapshot: None,
323 token_ledger: Vec::new(),
324 checkpoint_ref: None,
325 head_revision: None,
326 graph_replace_required: false,
327 }
328 }
329}
330
331pub(super) fn apply_persisted_session_config(
332 policy: &mut SessionPolicy,
333 config: &crate::PersistedSessionConfig,
334) {
335 if !config.configured_model.is_empty() {
336 policy.model = config.configured_model.clone();
337 }
338 if config.context_window > 0 {
339 policy.max_context_tokens = Some(config.context_window as usize);
340 }
341 policy.execution_mode = config.execution_mode.clone();
342 policy.standard_context_approach = config.standard_context_approach.clone();
343 policy.model_variant = config.model_variant.clone();
344}
345
346pub(super) fn apply_session_checkpoint(
347 state: &mut PersistedSessionState,
348 checkpoint: Option<crate::store::HydratedSessionCheckpoint>,
349) {
350 let Some(checkpoint) = checkpoint else {
351 state.tool_state_ref = None;
352 state.tool_state_generation = None;
353 state.tool_state_snapshot = None;
354 state.plugin_snapshot_ref = None;
355 state.plugin_snapshot_revision = None;
356 state.plugin_snapshot = None;
357 state.execution_state_ref = None;
358 state.execution_state_snapshot = None;
359 return;
360 };
361 state.turn_index = checkpoint.turn_state.turn_index;
362 state.token_usage = checkpoint.turn_state.token_usage;
363 state.last_prompt_usage = checkpoint.turn_state.last_prompt_usage;
364 state.mode_turn_options = checkpoint.turn_state.mode_turn_options;
365 state.tool_state_ref = checkpoint.tool_state_ref.clone();
366 state.tool_state_generation = checkpoint
367 .tool_state
368 .as_ref()
369 .map(|snapshot| snapshot.generation());
370 state.tool_state_snapshot = checkpoint.tool_state;
371 state.plugin_snapshot_ref = checkpoint.plugin_snapshot_ref.clone();
372 state.plugin_snapshot_revision = checkpoint.plugin_snapshot_revision;
373 state.plugin_snapshot = checkpoint.plugin_snapshot;
374 state.execution_state_ref = checkpoint.execution_state_ref.clone();
375 state.execution_state_snapshot = None;
376}
377
378pub(super) fn apply_session_head(
379 state: &mut PersistedSessionState,
380 head: &crate::store::SessionHead,
381) {
382 state.session_graph = head.graph.clone();
383 state.checkpoint_ref = head.checkpoint_ref.clone();
384 state.token_ledger = head.token_ledger.clone();
385 state.tool_state_ref = None;
386 state.tool_state_generation = None;
387 state.tool_state_snapshot = None;
388 state.plugin_snapshot_ref = None;
389 state.plugin_snapshot_revision = None;
390 state.plugin_snapshot = None;
391 state.execution_state_ref = None;
392 state.execution_state_snapshot = None;
393 state.head_revision = Some(head.head_revision);
394 state.graph_replace_required = false;
395 apply_persisted_session_config(&mut state.policy, &head.config);
396}
397
398pub(super) fn append_session_nodes_to_state(
399 state: &mut PersistedSessionState,
400 nodes: &[crate::SessionAppendNode],
401) -> Vec<String> {
402 let mut node_ids = Vec::with_capacity(nodes.len());
403 for node in nodes {
404 match node {
405 crate::SessionAppendNode::Message { message } => {
406 let message = plugin_message_to_message(message);
407 node_ids.push(
408 state
409 .session_graph
410 .append_event(SessionEventRecord::Conversation(
411 crate::session_model::ConversationRecord::from_message(message),
412 )),
413 );
414 }
415 crate::SessionAppendNode::Event { event } => {
416 node_ids.push(state.session_graph.append_event(event.clone()));
417 }
418 crate::SessionAppendNode::Plugin { plugin_type, body } => {
419 node_ids.push(
420 state
421 .session_graph
422 .append_plugin(plugin_type.clone(), body.clone()),
423 );
424 }
425 }
426 }
427 normalize_session_graph(state);
428 node_ids
429}
430
431pub(super) fn normalize_session_graph(state: &mut PersistedSessionState) {
440 if state.session_graph.heal_orphaned_leaf() {
441 state.graph_replace_required = true;
442 }
443}
444
445pub(super) fn apply_residency_on_load(
453 state: &mut PersistedSessionState,
454 residency: crate::Residency,
455) {
456 match residency {
457 crate::Residency::KeepAll => {}
458 crate::Residency::ActivePathOnly => {
459 state.session_graph = state.session_graph.fork_current_path();
460 }
461 }
462}