deepstrike_core/context/
snapshot.rs1use super::sections::{ContextSectionRegistry, SectionCachePolicy};
2use crate::types::capability::CapabilityManifest;
3use crate::types::message::Message;
4use crate::context::task_state::TaskState;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ContextPage {
10 pub id: String,
11 pub content: String,
12 pub token_count: u32,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ContextSnapshot {
18 pub turn: u32,
19 pub system_messages: Vec<Message>,
20 pub knowledge_messages: Vec<Message>,
21 pub history_messages: Vec<Message>,
22 pub task_state: TaskState,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ContextArchiveRef {
28 pub seq: u64,
29 pub archive_ref: String,
30 pub summary: String,
31 pub token_count: u32,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ContextGcPolicy {
37 pub target_tokens_ratio: f64,
38 pub auto_compact_ratio: f64,
39}
40
41#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
43pub enum ContextFault {
44 #[error("Prompt exceeds maximum token limit: budget={budget}, actual={actual}")]
45 PromptTooLong { budget: u32, actual: u32 },
46 #[error("Missing archive chunk {seq} for session {session_id}")]
47 MissingArchive { session_id: String, seq: u64 },
48 #[error("Invalid replay at turn {turn}: {reason}")]
49 InvalidReplay { turn: u32, reason: String },
50}
51
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ContextSnapshotHint {
60 pub static_prefix_hash: u64,
61 pub section_plan_hash: u64,
62 pub capability_manifest_hash: u64,
63 pub static_section_ids: Vec<String>,
64}
65
66impl ContextSnapshotHint {
67 pub fn from_parts(
68 sections: &ContextSectionRegistry,
69 capabilities: &CapabilityManifest,
70 ) -> Self {
71 let plan = sections.plan();
72 let mut section_plan_material = String::new();
73 for id in &plan.ids {
74 section_plan_material.push_str(id.as_str());
75 section_plan_material.push('\n');
76 }
77
78 let mut static_section_ids = Vec::new();
79 let mut static_prefix_material = String::new();
80 for section in sections.sections() {
81 if !section.enabled || section.cache_policy != SectionCachePolicy::Static {
82 continue;
83 }
84 static_section_ids.push(section.id.to_string());
85 static_prefix_material.push_str(section.id.as_str());
86 static_prefix_material.push('|');
87 static_prefix_material.push_str(§ion.priority.to_string());
88 static_prefix_material.push('|');
89 static_prefix_material.push_str(&format!("{:?}", section.partition));
90 static_prefix_material.push('\n');
91 }
92
93 Self {
94 static_prefix_hash: stable_hash(static_prefix_material.as_bytes()),
95 section_plan_hash: stable_hash(section_plan_material.as_bytes()),
96 capability_manifest_hash: stable_hash(capabilities.format_inventory().as_bytes()),
97 static_section_ids,
98 }
99 }
100}
101
102pub(crate) fn stable_hash(bytes: &[u8]) -> u64 {
106 let mut hash = 0xcbf29ce484222325u64;
107 for byte in bytes {
108 hash ^= u64::from(*byte);
109 hash = hash.wrapping_mul(0x100000001b3);
110 }
111 hash
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::context::sections::{
118 ContextSection, ContextSectionPartition, ContextSectionRegistry, SectionCachePolicy,
119 };
120 use crate::types::capability::{CapabilityKind, CapabilityManifest};
121
122 #[test]
123 fn dynamic_sections_do_not_change_static_prefix_hash() {
124 let mut base = ContextSectionRegistry::new();
125 base.upsert(
126 ContextSection::new("system.base", ContextSectionPartition::System, 100)
127 .with_cache_policy(SectionCachePolicy::Static),
128 );
129
130 let mut changed = base.clone();
131 changed.upsert(ContextSection::new(
132 "history.rolling",
133 ContextSectionPartition::History,
134 10,
135 ));
136
137 let manifest = CapabilityManifest::new();
138 let base_hint = ContextSnapshotHint::from_parts(&base, &manifest);
139 let changed_hint = ContextSnapshotHint::from_parts(&changed, &manifest);
140
141 assert_eq!(
142 base_hint.static_prefix_hash,
143 changed_hint.static_prefix_hash
144 );
145 assert_ne!(base_hint.section_plan_hash, changed_hint.section_plan_hash);
146 }
147
148 #[test]
149 fn capability_changes_alter_manifest_hash() {
150 let sections = ContextSectionRegistry::default_agent_sections();
151 let mut before = CapabilityManifest::new();
152 let mut after = CapabilityManifest::new();
153 after.add_marker(CapabilityKind::Agent, "verify", "verification agent");
154
155 assert_ne!(
156 ContextSnapshotHint::from_parts(§ions, &before).capability_manifest_hash,
157 ContextSnapshotHint::from_parts(§ions, &after).capability_manifest_hash
158 );
159
160 before.add_marker(CapabilityKind::Agent, "verify", "verification agent");
161 assert_eq!(
162 ContextSnapshotHint::from_parts(§ions, &before).capability_manifest_hash,
163 ContextSnapshotHint::from_parts(§ions, &after).capability_manifest_hash
164 );
165 }
166}