awaken_runtime/agent/state/
context_throttle.rs1use crate::state::StateKey;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
7pub struct ThrottleEntry {
8 pub last_step: usize,
10 pub content_hash: u64,
12}
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ContextThrottleMap {
17 pub entries: HashMap<String, ThrottleEntry>,
18}
19
20pub enum ContextThrottleUpdate {
22 Injected {
24 key: String,
25 step: usize,
26 content_hash: u64,
27 },
28}
29
30pub struct ContextThrottleState;
34
35impl StateKey for ContextThrottleState {
36 const KEY: &'static str = "__runtime.context_throttle";
37
38 type Value = ContextThrottleMap;
39 type Update = ContextThrottleUpdate;
40
41 fn apply(value: &mut Self::Value, update: Self::Update) {
42 match update {
43 ContextThrottleUpdate::Injected {
44 key,
45 step,
46 content_hash,
47 } => {
48 value.entries.insert(
49 key,
50 ThrottleEntry {
51 last_step: step,
52 content_hash,
53 },
54 );
55 }
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn throttle_map_default_is_empty() {
66 let map = ContextThrottleMap::default();
67 assert!(map.entries.is_empty());
68 }
69
70 #[test]
71 fn injected_creates_entry() {
72 let mut map = ContextThrottleMap::default();
73 ContextThrottleState::apply(
74 &mut map,
75 ContextThrottleUpdate::Injected {
76 key: "reminder".into(),
77 step: 5,
78 content_hash: 12345,
79 },
80 );
81 assert_eq!(map.entries.len(), 1);
82 let entry = &map.entries["reminder"];
83 assert_eq!(entry.last_step, 5);
84 assert_eq!(entry.content_hash, 12345);
85 }
86
87 #[test]
88 fn injected_updates_existing_entry() {
89 let mut map = ContextThrottleMap::default();
90 ContextThrottleState::apply(
91 &mut map,
92 ContextThrottleUpdate::Injected {
93 key: "reminder".into(),
94 step: 1,
95 content_hash: 111,
96 },
97 );
98 ContextThrottleState::apply(
99 &mut map,
100 ContextThrottleUpdate::Injected {
101 key: "reminder".into(),
102 step: 5,
103 content_hash: 222,
104 },
105 );
106 assert_eq!(map.entries.len(), 1);
107 let entry = &map.entries["reminder"];
108 assert_eq!(entry.last_step, 5);
109 assert_eq!(entry.content_hash, 222);
110 }
111
112 #[test]
113 fn injected_multiple_keys_independent() {
114 let mut map = ContextThrottleMap::default();
115 ContextThrottleState::apply(
116 &mut map,
117 ContextThrottleUpdate::Injected {
118 key: "a".into(),
119 step: 1,
120 content_hash: 100,
121 },
122 );
123 ContextThrottleState::apply(
124 &mut map,
125 ContextThrottleUpdate::Injected {
126 key: "b".into(),
127 step: 2,
128 content_hash: 200,
129 },
130 );
131 assert_eq!(map.entries.len(), 2);
132 assert_eq!(map.entries["a"].last_step, 1);
133 assert_eq!(map.entries["b"].last_step, 2);
134 }
135
136 #[test]
137 fn throttle_entry_serde_roundtrip() {
138 let entry = ThrottleEntry {
139 last_step: 42,
140 content_hash: 987654321,
141 };
142 let json = serde_json::to_string(&entry).unwrap();
143 let parsed: ThrottleEntry = serde_json::from_str(&json).unwrap();
144 assert_eq!(parsed, entry);
145 }
146
147 #[test]
148 fn throttle_map_serde_roundtrip() {
149 let mut map = ContextThrottleMap::default();
150 ContextThrottleState::apply(
151 &mut map,
152 ContextThrottleUpdate::Injected {
153 key: "k1".into(),
154 step: 3,
155 content_hash: 456,
156 },
157 );
158 let json = serde_json::to_string(&map).unwrap();
159 let parsed: ContextThrottleMap = serde_json::from_str(&json).unwrap();
160 assert_eq!(parsed, map);
161 }
162
163 #[test]
164 fn content_hash_change_detection() {
165 let mut map = ContextThrottleMap::default();
166
167 ContextThrottleState::apply(
169 &mut map,
170 ContextThrottleUpdate::Injected {
171 key: "reminder".into(),
172 step: 1,
173 content_hash: 111,
174 },
175 );
176
177 let entry = &map.entries["reminder"];
179 let new_hash: u64 = 222;
180 let content_changed = entry.content_hash != new_hash;
181 assert!(content_changed, "different hash should indicate change");
182
183 let same_hash: u64 = 111;
185 let content_same = entry.content_hash == same_hash;
186 assert!(content_same, "same hash should indicate no change");
187 }
188
189 #[test]
190 fn cooldown_check_logic() {
191 let mut map = ContextThrottleMap::default();
192
193 ContextThrottleState::apply(
195 &mut map,
196 ContextThrottleUpdate::Injected {
197 key: "reminder".into(),
198 step: 1,
199 content_hash: 100,
200 },
201 );
202
203 let entry = &map.entries["reminder"];
204 let cooldown = 3usize;
205
206 assert!(2usize.saturating_sub(entry.last_step) < cooldown);
208
209 assert!(3usize.saturating_sub(entry.last_step) < cooldown);
211
212 assert!(4usize.saturating_sub(entry.last_step) >= cooldown);
214 }
215}