deepstrike_core/context/
renewal.rs1use serde::{Deserialize, Serialize};
2
3use super::config::ContextConfig;
4use super::partitions::ContextPartitions;
5use super::pressure::PressureMonitor;
6use super::token_engine::ContextTokenEngine;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ContractCheckResult {
10 pub criterion_id: String,
11 pub passed: bool,
12 pub evidence: Option<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct HandoffArtifact {
18 pub goal: String,
19 pub sprint: u32,
20 pub progress_summary: String,
21 pub open_tasks: Vec<String>,
22 pub context_snapshot: serde_json::Value,
23 #[serde(default)]
24 pub contract_status: Vec<ContractCheckResult>,
25 #[serde(default)]
26 pub drift_rate_24h: f64,
27 #[serde(default)]
28 pub blocked_on: Vec<String>,
29}
30
31pub struct RenewalPolicy {
32 pub renewal_threshold: f64,
33 pub carryover_ratio: f64,
34}
35
36impl RenewalPolicy {
37 pub fn from_config(config: &ContextConfig) -> Self {
38 Self {
39 renewal_threshold: config.renewal_threshold,
40 carryover_ratio: config.carryover_ratio,
41 }
42 }
43
44 pub fn should_renew(
45 &self,
46 monitor: &PressureMonitor,
47 partitions: &ContextPartitions,
48 engine: &ContextTokenEngine,
49 ) -> bool {
50 monitor.pressure(partitions, engine, None) > self.renewal_threshold
51 }
52
53 pub fn renew(
57 &self,
58 partitions: &ContextPartitions,
59 goal: &str,
60 sprint: u32,
61 max_tokens: u32,
62 ) -> (ContextPartitions, HandoffArtifact) {
63 let config = ContextConfig {
64 carryover_ratio: self.carryover_ratio,
65 renewal_threshold: self.renewal_threshold,
66 ..Default::default()
67 };
68 let mut renewed = ContextPartitions::new(&config);
69
70 for msg in &partitions.system.messages {
72 renewed.system.push(msg.clone(), msg.token_count.unwrap_or(0));
73 }
74 renewed.knowledge = partitions.knowledge.clone();
78
79 renewed.task_state = partitions.task_state.clone();
81 renewed.task_state.scratchpad.clear();
82 let carryover_budget = config.carryover_tokens(max_tokens);
86 let mut remaining = carryover_budget;
87 let mut carried: Vec<_> = partitions.history.messages.iter().rev()
88 .take_while(|msg| {
89 let t = msg.token_count.unwrap_or(0);
90 if t <= remaining { remaining = remaining.saturating_sub(t); true } else { false }
91 })
92 .cloned()
93 .collect();
94 carried.reverse();
95 for msg in carried {
96 let t = msg.token_count.unwrap_or(0);
97 renewed.history.push(msg, t);
98 }
99
100 let artifact = HandoffArtifact {
101 goal: goal.to_string(),
102 sprint,
103 progress_summary: partitions.task_state.progress.clone(),
104 open_tasks: partitions.task_state.open_steps(),
105 context_snapshot: serde_json::json!({
106 "history_len": partitions.history.messages.len(),
107 "knowledge_len": partitions.knowledge.len(),
108 }),
109 contract_status: Vec::new(),
110 drift_rate_24h: 0.0,
111 blocked_on: partitions.task_state.blocked_on.clone(),
112 };
113
114 (renewed, artifact)
115 }
116}
117
118impl Default for RenewalPolicy {
119 fn default() -> Self { Self::from_config(&ContextConfig::default()) }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::context::config::ContextConfig;
126 use crate::context::partitions::ContextPartitions;
127 use crate::context::task_state::TaskState;
128 use crate::types::message::Message;
129
130 fn make_policy(carryover_ratio: f64) -> RenewalPolicy {
131 RenewalPolicy::from_config(&ContextConfig { carryover_ratio, ..Default::default() })
132 }
133
134 #[test]
135 fn renewal_preserves_system_and_knowledge() {
136 let cfg = ContextConfig::default();
137 let mut ctx = ContextPartitions::new(&cfg);
138 ctx.system.push(Message::system("rules"), 10);
139 ctx.knowledge.push(Message::system("skill: debug"), 20);
140 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
141 assert_eq!(renewed.system.len(), 1);
142 assert_eq!(renewed.knowledge.len(), 1);
143 }
144
145 #[test]
146 fn renewal_clears_signals() {
147 let cfg = ContextConfig::default();
148 let mut ctx = ContextPartitions::new(&cfg);
149 ctx.signals.push("[ROLLBACK] failed".to_string());
150 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
151 assert!(renewed.signals.is_empty());
152 }
153
154 #[test]
155 fn carryover_respects_token_budget() {
156 let cfg = ContextConfig::default();
157 let mut ctx = ContextPartitions::new(&cfg);
158 for i in 0..10 {
159 ctx.history.push(Message::user(format!("msg {i}")), 100);
160 }
161 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
162 assert!(renewed.history.token_count <= 100);
163 }
164
165 #[test]
166 fn renewal_clears_task_scratchpad_keeps_goal() {
167 let cfg = ContextConfig::default();
168 let mut ctx = ContextPartitions::new(&cfg);
169 ctx.task_state = TaskState {
170 goal: "build".to_string(),
171 scratchpad: "temp data".to_string(),
172 ..Default::default()
173 };
174 let (renewed, artifact) = make_policy(0.05).renew(&ctx, "build", 0, 1_000);
175 assert_eq!(renewed.task_state.goal, "build");
176 assert!(renewed.task_state.scratchpad.is_empty());
177 assert_eq!(artifact.goal, "build");
178 }
179}