a3s_code_core/store/
session_snapshot.rs1use super::SessionData;
2use crate::run::RunRecord;
3use crate::subagent_task_tracker::SubagentTaskSnapshot;
4use crate::tools::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
5use crate::trace::TraceEvent;
6use crate::verification::VerificationReport;
7use anyhow::{bail, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10
11pub const SESSION_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SessionSnapshotV1 {
20 pub schema_version: u32,
21 pub session: SessionData,
22 #[serde(default)]
23 pub artifacts: Vec<ToolArtifact>,
24 #[serde(default)]
25 pub trace_events: Vec<TraceEvent>,
26 #[serde(default)]
27 pub run_records: Vec<RunRecord>,
28 #[serde(default)]
29 pub verification_reports: Vec<VerificationReport>,
30 #[serde(default)]
31 pub subagent_tasks: Vec<SubagentTaskSnapshot>,
32}
33
34impl SessionSnapshotV1 {
35 pub fn new(
36 session: SessionData,
37 artifacts: &ArtifactStore,
38 trace_events: Vec<TraceEvent>,
39 run_records: Vec<RunRecord>,
40 verification_reports: Vec<VerificationReport>,
41 subagent_tasks: Vec<SubagentTaskSnapshot>,
42 ) -> Self {
43 Self {
44 schema_version: SESSION_SNAPSHOT_SCHEMA_VERSION,
45 session,
46 artifacts: artifacts.artifacts(),
47 trace_events,
48 run_records,
49 verification_reports,
50 subagent_tasks,
51 }
52 }
53
54 pub fn session_only(session: SessionData) -> Self {
55 Self::new(
56 session,
57 &ArtifactStore::new(),
58 Vec::new(),
59 Vec::new(),
60 Vec::new(),
61 Vec::new(),
62 )
63 }
64
65 pub fn artifact_store(&self) -> ArtifactStore {
66 artifact_store_from(&self.artifacts)
67 }
68
69 pub(crate) fn artifact_store_requirements(&self) -> ArtifactStoreLimits {
70 artifact_store_requirements(&self.artifacts)
71 }
72
73 pub fn ensure_loadable(&self) -> Result<()> {
74 if self.schema_version != SESSION_SNAPSHOT_SCHEMA_VERSION {
75 bail!(
76 "unsupported session snapshot schema version {}; expected {}",
77 self.schema_version,
78 SESSION_SNAPSHOT_SCHEMA_VERSION
79 );
80 }
81 Ok(())
82 }
83
84 pub fn validate_invariants(&self) -> Result<()> {
91 let mut run_ids = HashSet::with_capacity(self.run_records.len());
92
93 for (run_index, record) in self.run_records.iter().enumerate() {
94 let run_id = &record.snapshot.id;
95 if !run_ids.insert(run_id.as_str()) {
96 bail!(
97 "session snapshot {:?} contains duplicate run id {:?} at run record {}",
98 self.session.id,
99 run_id,
100 run_index
101 );
102 }
103
104 if record.snapshot.session_id != self.session.id {
105 bail!(
106 "run {:?} at record {} belongs to session {:?}, but snapshot belongs to session {:?}",
107 run_id,
108 run_index,
109 record.snapshot.session_id,
110 self.session.id
111 );
112 }
113
114 let mut previous_sequence = None;
115 for (event_index, event) in record.events.iter().enumerate() {
116 if let Some(previous) = previous_sequence {
117 if event.sequence <= previous {
118 bail!(
119 "run {:?} event {} has sequence {}, which is not strictly greater than previous sequence {}",
120 run_id,
121 event_index,
122 event.sequence,
123 previous
124 );
125 }
126 }
127 previous_sequence = Some(event.sequence);
128 }
129
130 if let Some(max_sequence) = previous_sequence {
131 let minimum_event_count = max_sequence.checked_add(1).ok_or_else(|| {
132 anyhow::anyhow!(
133 "run {:?} retained event sequence {} cannot be represented by event_count",
134 run_id,
135 max_sequence
136 )
137 })?;
138 if record.snapshot.event_count < minimum_event_count {
139 bail!(
140 "run {:?} event_count {} does not cover retained event sequence {}; expected at least {}",
141 run_id,
142 record.snapshot.event_count,
143 max_sequence,
144 minimum_event_count
145 );
146 }
147 }
148 }
149
150 for (task_index, task) in self.subagent_tasks.iter().enumerate() {
151 if !task.parent_session_id.is_empty() && task.parent_session_id != self.session.id {
155 bail!(
156 "subagent task {:?} at record {} belongs to parent session {:?}, but snapshot belongs to session {:?}",
157 task.task_id,
158 task_index,
159 task.parent_session_id,
160 self.session.id
161 );
162 }
163 }
164
165 Ok(())
166 }
167
168 pub fn validate_for_session(&self, session_id: &str) -> Result<()> {
170 self.ensure_loadable()?;
171 if self.session.id != session_id {
172 bail!(
173 "requested session {:?}, but snapshot payload belongs to session {:?}",
174 session_id,
175 self.session.id
176 );
177 }
178 self.validate_invariants()
179 }
180}
181
182pub(super) fn artifact_store_from(artifacts: &[ToolArtifact]) -> ArtifactStore {
183 let defaults = ArtifactStoreLimits::default();
187 let requirements = artifact_store_requirements(artifacts);
188 let store = ArtifactStore::with_limits(ArtifactStoreLimits {
189 max_artifacts: defaults.max_artifacts.max(requirements.max_artifacts),
190 max_bytes: defaults.max_bytes.max(requirements.max_bytes),
191 });
192 for artifact in artifacts {
193 store.put(artifact.clone());
194 }
195 store
196}
197
198fn artifact_store_requirements(artifacts: &[ToolArtifact]) -> ArtifactStoreLimits {
199 ArtifactStoreLimits {
200 max_artifacts: artifacts.len(),
201 max_bytes: artifacts.iter().fold(0usize, |total, artifact| {
202 total.saturating_add(artifact.content.len())
203 }),
204 }
205}