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 for msg in &partitions.knowledge.messages {
75 renewed.knowledge.push(msg.clone(), msg.token_count.unwrap_or(0));
76 }
77
78 renewed.task_state = partitions.task_state.clone();
80 renewed.task_state.scratchpad.clear();
81 let carryover_budget = config.carryover_tokens(max_tokens);
85 let mut remaining = carryover_budget;
86 let mut carried: Vec<_> = partitions.history.messages.iter().rev()
87 .take_while(|msg| {
88 let t = msg.token_count.unwrap_or(0);
89 if t <= remaining { remaining = remaining.saturating_sub(t); true } else { false }
90 })
91 .cloned()
92 .collect();
93 carried.reverse();
94 for msg in carried {
95 let t = msg.token_count.unwrap_or(0);
96 renewed.history.push(msg, t);
97 }
98
99 let artifact = HandoffArtifact {
100 goal: goal.to_string(),
101 sprint,
102 progress_summary: partitions.task_state.progress.clone(),
103 open_tasks: partitions.task_state.open_steps(),
104 context_snapshot: serde_json::json!({
105 "history_len": partitions.history.messages.len(),
106 "knowledge_len": partitions.knowledge.messages.len(),
107 }),
108 contract_status: Vec::new(),
109 drift_rate_24h: 0.0,
110 blocked_on: partitions.task_state.blocked_on.clone(),
111 };
112
113 (renewed, artifact)
114 }
115}
116
117impl Default for RenewalPolicy {
118 fn default() -> Self { Self::from_config(&ContextConfig::default()) }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::context::config::ContextConfig;
125 use crate::context::partitions::ContextPartitions;
126 use crate::context::task_state::TaskState;
127 use crate::types::message::Message;
128
129 fn make_policy(carryover_ratio: f64) -> RenewalPolicy {
130 RenewalPolicy::from_config(&ContextConfig { carryover_ratio, ..Default::default() })
131 }
132
133 #[test]
134 fn renewal_preserves_system_and_knowledge() {
135 let cfg = ContextConfig::default();
136 let mut ctx = ContextPartitions::new(&cfg);
137 ctx.system.push(Message::system("rules"), 10);
138 ctx.knowledge.push(Message::system("skill: debug"), 20);
139 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
140 assert_eq!(renewed.system.len(), 1);
141 assert_eq!(renewed.knowledge.len(), 1);
142 }
143
144 #[test]
145 fn renewal_clears_signals() {
146 let cfg = ContextConfig::default();
147 let mut ctx = ContextPartitions::new(&cfg);
148 ctx.signals.push("[ROLLBACK] failed".to_string());
149 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
150 assert!(renewed.signals.is_empty());
151 }
152
153 #[test]
154 fn carryover_respects_token_budget() {
155 let cfg = ContextConfig::default();
156 let mut ctx = ContextPartitions::new(&cfg);
157 for i in 0..10 {
158 ctx.history.push(Message::user(format!("msg {i}")), 100);
159 }
160 let (renewed, _) = make_policy(0.05).renew(&ctx, "goal", 0, 1_000);
161 assert!(renewed.history.token_count <= 100);
162 }
163
164 #[test]
165 fn renewal_clears_task_scratchpad_keeps_goal() {
166 let cfg = ContextConfig::default();
167 let mut ctx = ContextPartitions::new(&cfg);
168 ctx.task_state = TaskState {
169 goal: "build".to_string(),
170 scratchpad: "temp data".to_string(),
171 ..Default::default()
172 };
173 let (renewed, artifact) = make_policy(0.05).renew(&ctx, "build", 0, 1_000);
174 assert_eq!(renewed.task_state.goal, "build");
175 assert!(renewed.task_state.scratchpad.is_empty());
176 assert_eq!(artifact.goal, "build");
177 }
178}