1use lash_sansio::PromptUsage;
7
8use crate::session_model::{Message, SessionPolicy, TokenUsage, plugin_message_to_message};
9use crate::{PersistedTurnState, SessionSnapshot};
10
11use super::usage::TokenLedgerEntry;
12
13#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
19pub struct RuntimeSessionState {
20 pub session_id: String,
21 #[serde(default)]
22 pub policy: SessionPolicy,
23 #[serde(default)]
24 pub agent_frames: Vec<crate::AgentFrameRecord>,
25 #[serde(default, skip_serializing_if = "String::is_empty")]
26 pub current_agent_frame_id: crate::AgentFrameId,
27 #[serde(default)]
28 pub session_graph: crate::SessionGraph,
29 #[serde(default)]
30 pub turn_index: usize,
31 #[serde(default)]
32 pub token_usage: TokenUsage,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub last_prompt_usage: Option<PromptUsage>,
35 #[serde(default)]
36 pub protocol_turn_options: crate::ProtocolTurnOptions,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub tool_state_ref: Option<crate::store::BlobRef>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub tool_state_generation: Option<u64>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub tool_state_snapshot: Option<crate::ToolState>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub plugin_snapshot_ref: Option<crate::store::BlobRef>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub plugin_snapshot_revision: Option<u64>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub plugin_snapshot: Option<crate::PluginSessionSnapshot>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub execution_state_ref: Option<crate::store::BlobRef>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub execution_state_snapshot: Option<Vec<u8>>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub token_ledger: Vec<TokenLedgerEntry>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub checkpoint_ref: Option<crate::store::BlobRef>,
61 #[serde(skip)]
65 pub head_revision: Option<u64>,
66 #[serde(skip)]
70 pub graph_replace_required: bool,
71}
72
73impl RuntimeSessionState {
74 pub fn from_snapshot(snapshot: SessionSnapshot) -> Self {
75 let mut state = Self {
76 session_id: snapshot.session_id,
77 policy: snapshot.policy,
78 agent_frames: snapshot.agent_frames,
79 current_agent_frame_id: snapshot.current_agent_frame_id,
80 session_graph: snapshot.session_graph,
81 turn_index: snapshot.turn_index,
82 token_usage: snapshot.token_usage,
83 last_prompt_usage: snapshot.last_prompt_usage,
84 protocol_turn_options: snapshot.protocol_turn_options,
85 tool_state_ref: snapshot.tool_state_ref,
86 tool_state_generation: snapshot.tool_state_generation,
87 tool_state_snapshot: None,
88 plugin_snapshot_ref: snapshot.plugin_snapshot_ref,
89 plugin_snapshot_revision: snapshot.plugin_snapshot_revision,
90 plugin_snapshot: None,
91 execution_state_ref: snapshot.execution_state_ref,
92 execution_state_snapshot: None,
93 token_ledger: snapshot.token_ledger,
94 checkpoint_ref: snapshot.checkpoint_ref,
95 head_revision: None,
96 graph_replace_required: false,
97 };
98 for frame in &mut state.agent_frames {
99 frame.execution_state_snapshot = None;
100 }
101 state.ensure_agent_frame_initialized();
102 state
103 }
104
105 pub fn to_snapshot(&self) -> SessionSnapshot {
106 let mut agent_frames = self.agent_frames.clone();
107 for frame in &mut agent_frames {
108 frame.execution_state_snapshot = None;
109 }
110 SessionSnapshot {
111 session_id: self.session_id.clone(),
112 policy: self.policy.clone(),
113 agent_frames,
114 current_agent_frame_id: self.current_agent_frame_id.clone(),
115 session_graph: self.session_graph.clone(),
116 turn_index: self.turn_index,
117 token_usage: self.token_usage.clone(),
118 last_prompt_usage: self.last_prompt_usage.clone(),
119 protocol_turn_options: self.protocol_turn_options.clone(),
120 tool_state_ref: self.tool_state_ref.clone(),
121 tool_state_generation: self.tool_state_generation,
122 plugin_snapshot_ref: self.plugin_snapshot_ref.clone(),
123 plugin_snapshot_revision: self.plugin_snapshot_revision,
124 execution_state_ref: self.execution_state_ref.clone(),
125 token_ledger: self.token_ledger.clone(),
126 checkpoint_ref: self.checkpoint_ref.clone(),
127 }
128 }
129
130 pub fn apply_snapshot(&mut self, snapshot: &SessionSnapshot) {
131 self.session_id = snapshot.session_id.clone();
132 self.policy = snapshot.policy.clone();
133 self.agent_frames = snapshot.agent_frames.clone();
134 self.current_agent_frame_id = snapshot.current_agent_frame_id.clone();
135 self.ensure_agent_frame_initialized();
136 self.session_graph = snapshot.session_graph.clone();
137 self.turn_index = snapshot.turn_index;
138 self.token_usage = snapshot.token_usage.clone();
139 self.last_prompt_usage = snapshot.last_prompt_usage.clone();
140 self.protocol_turn_options = snapshot.protocol_turn_options.clone();
141 self.tool_state_ref = snapshot.tool_state_ref.clone();
142 self.tool_state_generation = snapshot.tool_state_generation;
143 self.plugin_snapshot_ref = snapshot.plugin_snapshot_ref.clone();
144 self.plugin_snapshot_revision = snapshot.plugin_snapshot_revision;
145 self.execution_state_ref = snapshot.execution_state_ref.clone();
146 self.token_ledger = snapshot.token_ledger.clone();
147 self.checkpoint_ref = snapshot.checkpoint_ref.clone();
148 }
149
150 pub fn stamp_runtime_state(
151 &mut self,
152 tool_state: Option<&crate::ToolState>,
153 plugin_snapshot: Option<&crate::PluginSessionSnapshot>,
154 ) {
155 self.tool_state_snapshot = tool_state.cloned();
156 self.tool_state_generation = tool_state.map(|snapshot| snapshot.generation());
157 self.plugin_snapshot = plugin_snapshot.cloned();
158 }
159
160 pub fn usage_report(&self) -> super::usage::SessionUsageReport {
161 super::usage::SessionUsageReport::from_entries(&self.token_ledger)
162 }
163
164 pub(crate) fn read_model(&self) -> crate::session_graph::SessionReadModel {
165 self.session_graph.read_model_for_agent_frame(
166 &self.current_agent_frame_id,
167 self.current_agent_frame_is_initial(),
168 )
169 }
170
171 pub fn replace_active_read_state(&mut self, messages: &[Message]) {
172 self.session_graph
173 .replace_active_read_state_for_agent_frame(&self.current_agent_frame_id, messages);
174 self.graph_replace_required = false;
175 }
176
177 pub fn append_active_read_delta(&mut self, messages: &[Message]) {
178 self.session_graph
179 .append_active_read_delta_for_agent_frame(&self.current_agent_frame_id, messages);
180 }
181
182 pub fn append_active_conversation_messages(&mut self, messages: &[Message]) {
183 self.session_graph
184 .append_active_conversation_messages_for_agent_frame(
185 &self.current_agent_frame_id,
186 messages,
187 );
188 }
189
190 pub(crate) fn append_active_conversation_messages_with_clock(
191 &mut self,
192 messages: &[Message],
193 clock: &dyn crate::Clock,
194 ) {
195 self.session_graph
196 .append_active_conversation_messages_for_agent_frame_at(
197 &self.current_agent_frame_id,
198 messages,
199 clock.timestamp_rfc3339(),
200 );
201 }
202
203 pub fn read_view(&self) -> crate::SessionReadView {
204 crate::SessionReadView::from_persisted_state(self)
205 }
206
207 pub fn session_graph(&self) -> &crate::SessionGraph {
208 &self.session_graph
209 }
210
211 pub fn policy(&self) -> &SessionPolicy {
212 self.effective_policy()
213 }
214
215 pub fn turn_state(&self) -> PersistedTurnState {
216 PersistedTurnState {
217 turn_index: self.turn_index,
218 token_usage: self.token_usage.clone(),
219 last_prompt_usage: self.last_prompt_usage.clone(),
220 protocol_turn_options: self.protocol_turn_options.clone(),
221 }
222 }
223
224 pub fn token_ledger(&self) -> &[TokenLedgerEntry] {
225 &self.token_ledger
226 }
227
228 pub fn apply_persisted_commit_result(&mut self, result: crate::store::RuntimeCommitResult) {
229 self.head_revision = Some(result.head_revision);
230 self.checkpoint_ref = Some(result.checkpoint_ref);
231 self.tool_state_ref = result.manifest.tool_state_ref;
232 if let Some(snapshot) = self.tool_state_snapshot.as_ref() {
233 self.tool_state_generation = Some(snapshot.generation());
234 } else if self.tool_state_ref.is_none() {
235 self.tool_state_generation = None;
236 }
237 self.plugin_snapshot_ref = result.manifest.plugin_snapshot_ref;
238 self.plugin_snapshot_revision = result.manifest.plugin_snapshot_revision;
239 self.execution_state_ref = result.manifest.execution_state_ref;
240 let execution_state_ref = self.execution_state_ref.clone();
241 if let Some(frame) = self.current_agent_frame_mut() {
242 frame.execution_state_ref = execution_state_ref;
243 frame.execution_state_snapshot = None;
244 }
245 self.graph_replace_required = false;
246 self.tool_state_snapshot = None;
247 self.plugin_snapshot = None;
248 self.execution_state_snapshot = None;
249 if let Some(frame) = self.current_agent_frame_mut() {
250 frame.execution_state_snapshot = None;
251 }
252 }
253
254 pub fn discard_runtime_snapshots(&mut self) {
255 self.tool_state_snapshot = None;
256 self.plugin_snapshot = None;
257 self.execution_state_snapshot = None;
258 if let Some(frame) = self.current_agent_frame_mut() {
259 frame.execution_state_snapshot = None;
260 }
261 }
262
263 pub fn set_execution_state_snapshot(&mut self, execution_state_snapshot: Option<Vec<u8>>) {
264 if execution_state_snapshot.is_none() {
265 self.execution_state_ref = None;
266 }
267 self.execution_state_snapshot = execution_state_snapshot.clone();
268 if let Some(frame) = self.current_agent_frame_mut() {
269 if execution_state_snapshot.is_none() {
270 frame.execution_state_ref = None;
271 }
272 frame.execution_state_snapshot = execution_state_snapshot;
273 }
274 }
275
276 pub fn execution_state_snapshot(&self) -> Option<&[u8]> {
277 self.current_agent_frame()
278 .and_then(|frame| frame.execution_state_snapshot.as_deref())
279 .or(self.execution_state_snapshot.as_deref())
280 }
281
282 pub fn refresh_plugin_snapshots(&mut self, plugins: &crate::PluginSession) {
283 let tool_registry = plugins.tool_registry();
284 let generation = tool_registry.generation();
285 if self.tool_state_ref.is_none() || self.tool_state_generation != Some(generation) {
286 let snapshot = tool_registry.export_state();
287 self.tool_state_generation = Some(snapshot.generation());
288 self.tool_state_snapshot = Some(snapshot);
289 }
290
291 let revision = plugins.snapshot_revision_fingerprint();
292 if self.plugin_snapshot_ref.is_none() || self.plugin_snapshot_revision != Some(revision) {
293 store_plugin_snapshot(&mut self.plugin_snapshot, plugins.snapshot());
294 }
295 self.plugin_snapshot_revision = Some(revision);
296 }
297}
298
299pub(crate) fn store_plugin_snapshot(
307 target: &mut Option<crate::PluginSessionSnapshot>,
308 captured: Result<crate::PluginSessionSnapshot, crate::PluginError>,
309) {
310 match captured {
311 Ok(snapshot) => *target = Some(snapshot),
312 Err(err) => tracing::warn!(
313 error = %err,
314 "failed to capture plugin snapshot; retaining the prior snapshot",
315 ),
316 }
317}
318
319impl RuntimeSessionState {
320 pub fn current_agent_frame(&self) -> Option<&crate::AgentFrameRecord> {
321 self.agent_frames
322 .iter()
323 .find(|frame| frame.frame_id == self.current_agent_frame_id)
324 }
325
326 pub fn current_agent_frame_mut(&mut self) -> Option<&mut crate::AgentFrameRecord> {
327 let current_agent_frame_id = self.current_agent_frame_id.clone();
328 self.agent_frames
329 .iter_mut()
330 .find(|frame| frame.frame_id == current_agent_frame_id)
331 }
332
333 pub fn effective_policy(&self) -> &SessionPolicy {
334 self.current_agent_frame()
335 .map(|frame| &frame.assignment.policy)
336 .unwrap_or(&self.policy)
337 }
338
339 pub fn process_execution_env_spec(
340 &self,
341 fallback_policy: &SessionPolicy,
342 ) -> crate::ProcessExecutionEnvSpec {
343 self.current_agent_frame()
344 .map(|frame| {
345 crate::ProcessExecutionEnvSpec::new(
346 frame.assignment.plugin_options.clone(),
347 frame.assignment.policy.clone(),
348 )
349 })
350 .unwrap_or_else(|| {
351 crate::ProcessExecutionEnvSpec::new(
352 crate::PluginOptions::default(),
353 fallback_policy.clone(),
354 )
355 })
356 }
357
358 pub fn effective_protocol_turn_options(&self) -> &crate::ProtocolTurnOptions {
359 self.current_agent_frame()
360 .map(|frame| &frame.protocol_turn_options)
361 .unwrap_or(&self.protocol_turn_options)
362 }
363
364 pub fn ensure_agent_frame_initialized(&mut self) {
365 self.ensure_agent_frame_initialized_with_clock(&crate::SystemClock);
366 }
367
368 pub fn ensure_agent_frame_initialized_with_clock(&mut self, clock: &dyn crate::Clock) {
369 if self.current_agent_frame_id.is_empty() {
370 self.current_agent_frame_id = default_agent_frame_id(&self.session_id);
371 }
372 if self
373 .agent_frames
374 .iter()
375 .any(|frame| frame.frame_id == self.current_agent_frame_id)
376 {
377 return;
378 }
379 let mut frame = default_agent_frame_with_clock(&self.session_id, &self.policy, clock);
380 frame.frame_id = self.current_agent_frame_id.clone();
381 frame.protocol_turn_options = self.protocol_turn_options.clone();
382 frame.execution_state_ref = self.execution_state_ref.clone();
383 frame.execution_state_snapshot = self.execution_state_snapshot.clone();
384 self.agent_frames.push(frame);
385 }
386
387 pub fn reset_initial_agent_frame(
388 &mut self,
389 assignment: crate::AgentFrameAssignment,
390 protocol_turn_options: crate::ProtocolTurnOptions,
391 ) {
392 self.reset_initial_agent_frame_with_clock(
393 assignment,
394 protocol_turn_options,
395 &crate::SystemClock,
396 );
397 }
398
399 pub fn reset_initial_agent_frame_with_clock(
400 &mut self,
401 assignment: crate::AgentFrameAssignment,
402 protocol_turn_options: crate::ProtocolTurnOptions,
403 clock: &dyn crate::Clock,
404 ) {
405 let frame_id = default_agent_frame_id(&self.session_id);
406 self.policy = assignment.policy.clone();
407 self.protocol_turn_options = protocol_turn_options.clone();
408 self.current_agent_frame_id = frame_id.clone();
409 self.agent_frames = vec![crate::AgentFrameRecord::new_at(
410 frame_id,
411 self.session_id.clone(),
412 None,
413 crate::AgentFrameReason::initial(),
414 None,
415 assignment,
416 protocol_turn_options,
417 clock.timestamp_rfc3339(),
418 )];
419 }
420
421 pub fn append_agent_frame(&mut self, mut frame: crate::AgentFrameRecord) {
422 let previous_frame_id = self.current_agent_frame_id.clone();
423 for existing in &mut self.agent_frames {
424 if existing.frame_id == previous_frame_id {
425 existing.status = crate::AgentFrameStatus::Superseded;
426 }
427 }
428 if frame.previous_frame_id.is_none() && !previous_frame_id.is_empty() {
429 frame.previous_frame_id = Some(previous_frame_id);
430 }
431 frame.status = crate::AgentFrameStatus::Active;
432 self.policy = frame.assignment.policy.clone();
433 self.protocol_turn_options = frame.protocol_turn_options.clone();
434 self.current_agent_frame_id = frame.frame_id.clone();
435 self.execution_state_ref = frame.execution_state_ref.clone();
436 self.execution_state_snapshot = frame.execution_state_snapshot.clone();
437 self.agent_frames.push(frame);
438 }
439
440 fn current_agent_frame_is_initial(&self) -> bool {
441 self.current_agent_frame()
442 .map(|frame| frame.previous_frame_id.is_none())
443 .unwrap_or(true)
444 }
445}
446
447impl Default for RuntimeSessionState {
448 fn default() -> Self {
449 Self {
450 session_id: "root".to_string(),
451 policy: SessionPolicy::default(),
452 agent_frames: default_agent_frames("root", &SessionPolicy::default()),
453 current_agent_frame_id: default_agent_frame_id("root"),
454 session_graph: crate::SessionGraph::default(),
455 turn_index: 0,
456 token_usage: TokenUsage::default(),
457 last_prompt_usage: None,
458 protocol_turn_options: crate::ProtocolTurnOptions::default(),
459 tool_state_ref: None,
460 tool_state_generation: None,
461 tool_state_snapshot: None,
462 plugin_snapshot_ref: None,
463 plugin_snapshot_revision: None,
464 plugin_snapshot: None,
465 execution_state_ref: None,
466 execution_state_snapshot: None,
467 token_ledger: Vec::new(),
468 checkpoint_ref: None,
469 head_revision: None,
470 graph_replace_required: false,
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use std::sync::{Arc, Mutex};
479
480 struct DynamicSnapshotTools {
481 names: Arc<Mutex<Vec<String>>>,
482 }
483
484 #[async_trait::async_trait]
485 impl crate::ToolProvider for DynamicSnapshotTools {
486 fn tool_manifests(&self) -> Vec<crate::ToolManifest> {
487 self.names
488 .lock()
489 .expect("dynamic snapshot names")
490 .iter()
491 .map(|name| {
492 crate::ToolDefinition::raw(
493 format!("tool:{name}"),
494 name,
495 "dynamic snapshot tool",
496 crate::ToolDefinition::default_input_schema(),
497 serde_json::json!({}),
498 )
499 .manifest()
500 })
501 .collect()
502 }
503
504 fn resolve_contract(&self, name: &str) -> Option<Arc<crate::ToolContract>> {
505 self.names
506 .lock()
507 .expect("dynamic snapshot names")
508 .iter()
509 .any(|candidate| candidate == name)
510 .then(|| {
511 Arc::new(
512 crate::ToolDefinition::raw(
513 format!("tool:{name}"),
514 name,
515 "dynamic snapshot tool",
516 crate::ToolDefinition::default_input_schema(),
517 serde_json::json!({}),
518 )
519 .contract(),
520 )
521 })
522 }
523
524 async fn execute(&self, _call: crate::ToolCall<'_>) -> crate::ToolResult {
525 crate::ToolResult::ok(serde_json::json!("ok"))
526 }
527 }
528
529 #[test]
530 fn session_snapshot_serialization_excludes_runtime_only_fields_and_round_trips() {
531 let mut state = RuntimeSessionState {
532 session_id: "snapshot-test".to_string(),
533 policy: SessionPolicy {
534 provider_id: "mock".to_string(),
535 ..SessionPolicy::default()
536 },
537 tool_state_snapshot: Some(crate::ToolState::default()),
538 plugin_snapshot: Some(crate::PluginSessionSnapshot::default()),
539 execution_state_snapshot: Some(vec![1, 2, 3]),
540 head_revision: Some(42),
541 graph_replace_required: true,
542 ..RuntimeSessionState::default()
543 };
544 state.ensure_agent_frame_initialized();
545 if let Some(frame) = state.current_agent_frame_mut() {
546 frame.execution_state_snapshot = Some(vec![4, 5, 6]);
547 }
548
549 let value = serde_json::to_value(state.to_snapshot()).expect("serialize snapshot");
550
551 for runtime_key in [
552 "head_revision",
553 "graph_replace_required",
554 "tool_state_snapshot",
555 "plugin_snapshot",
556 "execution_state_snapshot",
557 ] {
558 assert!(
559 value.get(runtime_key).is_none(),
560 "snapshot unexpectedly exposed {runtime_key}"
561 );
562 }
563 assert!(
564 value["agent_frames"]
565 .as_array()
566 .expect("agent frames")
567 .iter()
568 .all(|frame| frame.get("execution_state_snapshot").is_none())
569 );
570
571 let snapshot: SessionSnapshot = serde_json::from_value(value).expect("round-trip snapshot");
572 let hydrated = RuntimeSessionState::from_snapshot(snapshot);
573
574 assert_eq!(hydrated.session_id, "snapshot-test");
575 assert_eq!(hydrated.policy.recorded_provider_id(), "mock");
576 assert!(hydrated.head_revision.is_none());
577 assert!(!hydrated.graph_replace_required);
578 assert!(hydrated.tool_state_snapshot.is_none());
579 assert!(hydrated.plugin_snapshot.is_none());
580 assert!(hydrated.execution_state_snapshot.is_none());
581 assert!(
582 hydrated
583 .agent_frames
584 .iter()
585 .all(|frame| frame.execution_state_snapshot.is_none())
586 );
587 }
588
589 #[test]
590 fn reconciled_generation_forces_next_plugin_snapshot_export() {
591 let names = Arc::new(Mutex::new(vec!["dynamic_one".to_string()]));
592 let tools: Arc<dyn crate::ToolProvider> = Arc::new(DynamicSnapshotTools {
593 names: Arc::clone(&names),
594 });
595 let plugins = crate::runtime::tests::helpers::plugin_session_with_tools("root", tools);
596 let snapshot = plugins.tool_registry().export_state();
597 let persisted_generation = snapshot.generation();
598 let mut state = RuntimeSessionState {
599 tool_state_ref: Some("persisted-tool-state".to_string().into()),
600 tool_state_generation: Some(persisted_generation),
601 ..RuntimeSessionState::default()
602 };
603
604 names
605 .lock()
606 .expect("dynamic snapshot names")
607 .push("dynamic_two".to_string());
608 let report = plugins
609 .tool_registry()
610 .restore_state(snapshot)
611 .expect("live surface restore");
612 assert_eq!(report.generation, persisted_generation + 1);
613
614 state.refresh_plugin_snapshots(&plugins);
615 let refreshed = state
616 .tool_state_snapshot
617 .as_ref()
618 .expect("generation change re-exports the tool snapshot");
619 assert_eq!(refreshed.generation(), report.generation);
620 assert!(refreshed.contains(&crate::ToolId::from("tool:dynamic_two")));
621 }
622}
623
624pub(super) fn apply_persisted_session_config(
625 policy: &mut SessionPolicy,
626 config: &crate::PersistedSessionConfig,
627) {
628 policy.model = config.model.clone();
629 policy.provider_id = config.provider_id.clone();
630}
631
632pub(super) fn apply_session_checkpoint(
633 state: &mut RuntimeSessionState,
634 checkpoint: Option<crate::store::HydratedSessionCheckpoint>,
635) {
636 let Some(checkpoint) = checkpoint else {
637 state.tool_state_ref = None;
638 state.tool_state_generation = None;
639 state.tool_state_snapshot = None;
640 state.plugin_snapshot_ref = None;
641 state.plugin_snapshot_revision = None;
642 state.plugin_snapshot = None;
643 state.execution_state_ref = None;
644 state.execution_state_snapshot = None;
645 state.ensure_agent_frame_initialized();
646 return;
647 };
648 state.turn_index = checkpoint.turn_state.turn_index;
649 state.token_usage = checkpoint.turn_state.token_usage;
650 state.last_prompt_usage = checkpoint.turn_state.last_prompt_usage;
651 state.protocol_turn_options = checkpoint.turn_state.protocol_turn_options;
652 state.tool_state_ref = checkpoint.tool_state_ref.clone();
653 state.tool_state_generation = checkpoint
654 .tool_state
655 .as_ref()
656 .map(|snapshot| snapshot.generation());
657 state.tool_state_snapshot = checkpoint.tool_state;
658 state.plugin_snapshot_ref = checkpoint.plugin_snapshot_ref.clone();
659 state.plugin_snapshot_revision = checkpoint.plugin_snapshot_revision;
660 state.plugin_snapshot = checkpoint.plugin_snapshot;
661 state.execution_state_ref = checkpoint.execution_state_ref.clone();
662 state.execution_state_snapshot = None;
663 state.ensure_agent_frame_initialized();
664 if let Some(frame) = state.current_agent_frame_mut() {
665 frame.execution_state_ref = checkpoint.execution_state_ref.clone();
666 frame.execution_state_snapshot = checkpoint.execution_state;
667 }
668}
669
670pub(super) fn apply_session_head(
671 state: &mut RuntimeSessionState,
672 head: &crate::store::SessionHead,
673) {
674 state.session_graph = head.graph.clone();
675 state.agent_frames = head.agent_frames.clone();
676 state.current_agent_frame_id = head.current_agent_frame_id.clone();
677 state.checkpoint_ref = head.checkpoint_ref.clone();
678 state.token_ledger = head.token_ledger.clone();
679 state.tool_state_ref = None;
680 state.tool_state_generation = None;
681 state.tool_state_snapshot = None;
682 state.plugin_snapshot_ref = None;
683 state.plugin_snapshot_revision = None;
684 state.plugin_snapshot = None;
685 state.execution_state_ref = None;
686 state.execution_state_snapshot = None;
687 state.ensure_agent_frame_initialized();
688 state.head_revision = Some(head.head_revision);
689 state.graph_replace_required = false;
690 apply_persisted_session_config(&mut state.policy, &head.config);
691}
692
693pub(super) fn append_session_nodes_to_state_with_clock(
694 state: &mut RuntimeSessionState,
695 nodes: &[crate::SessionAppendNode],
696 clock: &dyn crate::Clock,
697) -> Vec<String> {
698 let drafts = nodes
699 .iter()
700 .map(session_append_node_draft)
701 .collect::<Vec<_>>();
702 state.ensure_agent_frame_initialized_with_clock(clock);
703 let node_ids = state.session_graph.append_node_drafts_for_agent_frame_at(
704 &state.current_agent_frame_id,
705 drafts,
706 clock.timestamp_rfc3339(),
707 );
708 normalize_session_graph(state);
709 node_ids
710}
711
712pub(super) fn open_agent_frame_in_state_with_clock(
713 state: &mut RuntimeSessionState,
714 request: crate::OpenAgentFrameRequest,
715 clock: &dyn crate::Clock,
716) -> crate::OpenAgentFrameResult {
717 state.ensure_agent_frame_initialized_with_clock(clock);
718 if request.frame_id.trim().is_empty() || state.current_agent_frame_id == request.frame_id {
719 return crate::OpenAgentFrameResult {
720 frame_id: state.current_agent_frame_id.clone(),
721 opened: false,
722 initial_node_ids: Vec::new(),
723 };
724 }
725
726 let previous = state.current_agent_frame().cloned();
727 let assignment = previous
728 .as_ref()
729 .map(|frame| frame.assignment.clone())
730 .unwrap_or_else(|| crate::AgentFrameAssignment::from_policy(state.policy.clone()));
731 let protocol_turn_options = previous
732 .as_ref()
733 .map(|frame| frame.protocol_turn_options.clone())
734 .unwrap_or_else(|| state.protocol_turn_options.clone());
735 let previous_frame_id = previous.map(|frame| frame.frame_id);
736 state.append_agent_frame(crate::AgentFrameRecord::new_at(
737 request.frame_id.clone(),
738 state.session_id.clone(),
739 previous_frame_id,
740 request.reason,
741 request.caused_by,
742 assignment,
743 protocol_turn_options,
744 clock.timestamp_rfc3339(),
745 ));
746
747 let initial_node_ids =
748 append_session_nodes_to_state_with_clock(state, &request.initial_nodes, clock);
749 if !initial_node_ids.is_empty() {
750 state.graph_replace_required = true;
751 }
752 crate::OpenAgentFrameResult {
753 frame_id: state.current_agent_frame_id.clone(),
754 opened: true,
755 initial_node_ids,
756 }
757}
758
759fn session_append_node_draft(
760 node: &crate::SessionAppendNode,
761) -> crate::session_graph::SessionNodeDraft {
762 match node {
763 crate::SessionAppendNode::Message { message, caused_by } => {
764 crate::session_graph::SessionNodeDraft::message(plugin_message_to_message(message))
765 .with_caused_by(caused_by.clone())
766 }
767 crate::SessionAppendNode::ProtocolEvent { event, caused_by } => {
768 crate::session_graph::SessionNodeDraft::protocol_event(event.clone())
769 .with_caused_by(caused_by.clone())
770 }
771 crate::SessionAppendNode::Plugin {
772 plugin_type,
773 body,
774 caused_by,
775 } => crate::session_graph::SessionNodeDraft::plugin(plugin_type.clone(), body.clone())
776 .with_caused_by(caused_by.clone()),
777 }
778}
779
780fn default_agent_frame_id(session_id: &str) -> crate::AgentFrameId {
781 format!("{session_id}:frame:initial")
782}
783
784fn default_agent_frames(session_id: &str, policy: &SessionPolicy) -> Vec<crate::AgentFrameRecord> {
785 vec![default_agent_frame(session_id, policy)]
786}
787
788fn default_agent_frame(session_id: &str, policy: &SessionPolicy) -> crate::AgentFrameRecord {
789 default_agent_frame_with_clock(session_id, policy, &crate::SystemClock)
790}
791
792fn default_agent_frame_with_clock(
793 session_id: &str,
794 policy: &SessionPolicy,
795 clock: &dyn crate::Clock,
796) -> crate::AgentFrameRecord {
797 crate::AgentFrameRecord::new_at(
798 default_agent_frame_id(session_id),
799 session_id.to_string(),
800 None,
801 crate::AgentFrameReason::initial(),
802 None,
803 crate::AgentFrameAssignment::from_policy(policy.clone()),
804 crate::ProtocolTurnOptions::default(),
805 clock.timestamp_rfc3339(),
806 )
807}
808
809pub(super) fn normalize_session_graph(state: &mut RuntimeSessionState) {
818 if state.session_graph.heal_orphaned_leaf() {
819 state.graph_replace_required = true;
820 }
821}
822
823pub(super) fn apply_residency_on_load(
831 state: &mut RuntimeSessionState,
832 residency: crate::Residency,
833) {
834 match residency {
835 crate::Residency::KeepAll => {}
836 crate::Residency::ActivePathOnly => {
837 state.session_graph = state.session_graph.fork_current_path();
838 }
839 }
840}
841
842#[cfg(test)]
843mod plugin_snapshot_tests {
844 use super::store_plugin_snapshot;
845 use crate::{PluginError, PluginSessionSnapshot};
846
847 #[test]
848 fn ok_capture_overwrites_target() {
849 let mut target = None;
850 store_plugin_snapshot(&mut target, Ok(PluginSessionSnapshot::default()));
851 assert!(target.is_some(), "a successful capture must be stored");
852 }
853
854 #[test]
855 fn failed_capture_retains_prior_snapshot() {
856 let prior = PluginSessionSnapshot::default();
861 let mut target = Some(prior);
862 store_plugin_snapshot(
863 &mut target,
864 Err(PluginError::Snapshot("capture failed".to_string())),
865 );
866 assert!(
867 target.is_some(),
868 "a failed capture must retain the prior snapshot, not erase it"
869 );
870 }
871}
872
873#[cfg(test)]
874mod residency_tests {
875 use super::apply_residency_on_load;
876 use crate::{
877 Message, MessageRole, Part, PartKind, PruneState, Residency, RuntimeSessionState,
878 shared_parts,
879 };
880
881 fn text_message(id: &str, content: &str) -> Message {
882 Message {
883 id: id.to_string(),
884 role: MessageRole::User,
885 parts: shared_parts(vec![Part {
886 id: format!("{id}.p0"),
887 kind: PartKind::Text,
888 content: content.to_string(),
889 attachment: None,
890 tool_call_id: None,
891 tool_name: None,
892 tool_replay: None,
893 prune_state: PruneState::Intact,
894 reasoning_meta: None,
895 response_meta: None,
896 }]),
897 origin: None,
898 }
899 }
900
901 fn branching_state() -> (RuntimeSessionState, String, String) {
904 let mut state = RuntimeSessionState::default();
905 state.append_active_conversation_messages(&[text_message("root", "root")]);
906 let root = state.session_graph.leaf_node_id.clone();
907 state.append_active_conversation_messages(&[text_message("inactive", "inactive branch")]);
908 let inactive_node = state
909 .session_graph
910 .leaf_node_id
911 .clone()
912 .expect("inactive node");
913 state.session_graph.branch_to(root);
914 state.append_active_conversation_messages(&[text_message("active", "active branch")]);
915 let active_node = state
916 .session_graph
917 .leaf_node_id
918 .clone()
919 .expect("active node");
920 (state, inactive_node, active_node)
921 }
922
923 #[test]
924 fn active_path_only_trims_orphan_branches_on_load() {
925 let (mut state, inactive_node, active_node) = branching_state();
929 assert!(
930 state.session_graph.find_node(&inactive_node).is_some(),
931 "the inactive branch is resident before trimming"
932 );
933 apply_residency_on_load(&mut state, Residency::ActivePathOnly);
934 assert!(
935 state.session_graph.find_node(&inactive_node).is_none(),
936 "ActivePathOnly must drop the orphaned inactive branch on rebuild"
937 );
938 assert!(
939 state.session_graph.find_node(&active_node).is_some(),
940 "the active path must be retained"
941 );
942 }
943
944 #[test]
945 fn keep_all_retains_orphan_branches_on_load() {
946 let (mut state, inactive_node, _active_node) = branching_state();
947 apply_residency_on_load(&mut state, Residency::KeepAll);
948 assert!(
949 state.session_graph.find_node(&inactive_node).is_some(),
950 "KeepAll must retain the full resident graph"
951 );
952 }
953}