kcode_kennedy_subagent_context/
lib.rs1#![forbid(unsafe_code)]
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use kcode_agent_runtime::StateUpdate;
8use kcode_dev_tools::{ManagedSourceKind, SourceSnapshot};
9use kcode_kweb_context::{Context as KwebContext, NodeDraft, ProjectionItem, StagedCreate};
10
11const KWEB_PREFIX: &str = "kweb:";
12const MANAGED_PREFIX: &str = "managed:";
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub struct StateChanges {
17 pub updates: Vec<StateUpdate>,
19}
20
21impl StateChanges {
22 pub fn is_empty(&self) -> bool {
24 self.updates.is_empty()
25 }
26
27 pub fn display_text(&self) -> String {
29 self.updates
30 .iter()
31 .filter_map(|update| update.text.as_deref())
32 .collect::<Vec<_>>()
33 .join("\n\n")
34 }
35
36 pub fn displayed_state_keys(&self) -> Vec<String> {
38 self.updates
39 .iter()
40 .filter(|update| update.text.is_some())
41 .map(|update| update.key.clone())
42 .collect()
43 }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct SourceState {
49 pub key: String,
51 pub text: String,
53 pub update: Option<StateUpdate>,
55}
56
57#[derive(Clone, Debug)]
59pub struct Context {
60 kweb: KwebContext,
61 states: BTreeMap<String, String>,
62 kweb_keys: BTreeSet<String>,
63 staged_node_ids: BTreeSet<String>,
64}
65
66impl Context {
67 pub fn new(root_node_ids: Vec<String>, load_fixed_connections: bool) -> anyhow::Result<Self> {
69 Ok(Self {
70 kweb: KwebContext::with_fixed_connections(root_node_ids, load_fixed_connections)
71 .map_err(anyhow::Error::new)?,
72 states: BTreeMap::new(),
73 kweb_keys: BTreeSet::new(),
74 staged_node_ids: BTreeSet::new(),
75 })
76 }
77
78 pub fn kweb(&self) -> &KwebContext {
80 &self.kweb
81 }
82
83 pub fn kweb_mut(&mut self) -> &mut KwebContext {
85 &mut self.kweb
86 }
87
88 pub fn include_staged_nodes(&mut self, ids: impl IntoIterator<Item = String>) {
90 self.staged_node_ids.extend(ids);
91 }
92
93 pub fn reconcile_kweb(
95 &mut self,
96 updates: &BTreeMap<String, NodeDraft>,
97 creates: &[StagedCreate],
98 ) -> anyhow::Result<StateChanges> {
99 let visible_updates = updates
100 .iter()
101 .filter(|(id, _)| self.kweb.contains_full_node(id))
102 .map(|(id, update)| (id.clone(), update.clone()))
103 .collect();
104 let visible_creates = creates
105 .iter()
106 .filter(|create| self.staged_node_ids.contains(&create.pending_id))
107 .cloned()
108 .collect::<Vec<_>>();
109 let projected = self
110 .kweb
111 .projection(&visible_updates, &visible_creates)
112 .map_err(anyhow::Error::new)?;
113 let next_keys = projected
114 .iter()
115 .map(|item| kweb_key(&item.key))
116 .collect::<BTreeSet<_>>();
117 let mut changes = StateChanges::default();
118
119 for key in self.kweb_keys.difference(&next_keys) {
120 self.states.remove(key);
121 changes.updates.push(StateUpdate {
122 key: key.clone(),
123 text: None,
124 });
125 }
126 for item in projected {
127 self.apply_kweb_item(item, &mut changes);
128 }
129 self.kweb_keys = next_keys;
130 Ok(changes)
131 }
132
133 pub fn apply_source_snapshot(&mut self, snapshot: SourceSnapshot) -> SourceState {
135 let state = source_state(&snapshot);
136 let update = (self.states.get(&state.key) != Some(&state.text)).then(|| StateUpdate {
137 key: state.key.clone(),
138 text: Some(state.text.clone()),
139 });
140 self.states.insert(state.key.clone(), state.text.clone());
141 SourceState { update, ..state }
142 }
143
144 pub fn source_state(&self, snapshot: &SourceSnapshot) -> SourceState {
146 let mut state = source_state(snapshot);
147 state.update = (self.states.get(&state.key) != Some(&state.text)).then(|| StateUpdate {
148 key: state.key.clone(),
149 text: Some(state.text.clone()),
150 });
151 state
152 }
153
154 pub fn source_is_open(&self, kind: ManagedSourceKind, name: &str) -> bool {
156 self.states.contains_key(&managed_key(kind, name))
157 }
158
159 fn apply_kweb_item(&mut self, item: ProjectionItem, changes: &mut StateChanges) {
160 let key = kweb_key(&item.key);
161 let text = format!("Current {}:\n{}", item.name, item.text);
162 if self.states.get(&key) != Some(&text) {
163 self.states.insert(key.clone(), text.clone());
164 changes.updates.push(StateUpdate {
165 key,
166 text: Some(text),
167 });
168 }
169 }
170}
171
172fn source_state(snapshot: &SourceSnapshot) -> SourceState {
173 SourceState {
174 key: managed_key(snapshot.kind, &snapshot.name),
175 text: format!(
176 "Current Managed {} {}:\n{}",
177 snapshot.kind.label(),
178 snapshot.name,
179 snapshot.text
180 ),
181 update: None,
182 }
183}
184
185fn kweb_key(logical: &str) -> String {
186 format!("{KWEB_PREFIX}{logical}")
187}
188
189fn managed_key(kind: ManagedSourceKind, name: &str) -> String {
190 let kind = match kind {
191 ManagedSourceKind::RustLibrary => "rust-library",
192 ManagedSourceKind::WebLibrary => "web-library",
193 ManagedSourceKind::RustBinary => "rust-binary",
194 };
195 format!("{MANAGED_PREFIX}{kind}:{name}")
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use kcode_kweb_context::Node;
202
203 fn node(description: &str) -> Node {
204 Node {
205 id: "AAAAAAAE".into(),
206 short_name: "Root".into(),
207 short_description: "Summary".into(),
208 long_description: description.into(),
209 owner: "self".into(),
210 fixed_connections: Vec::new(),
211 recent_connections: Vec::new(),
212 objects: Vec::new(),
213 last_modified_by: "test".into(),
214 last_modified_at: None,
215 }
216 }
217
218 #[test]
219 fn kweb_changes_reuse_stable_keys_without_boxes() {
220 let mut context = Context::new(vec!["AAAAAAAE".into()], false).unwrap();
221 context
222 .kweb_mut()
223 .apply_load(node("old"), Vec::new())
224 .unwrap();
225 let first = context.reconcile_kweb(&BTreeMap::new(), &[]).unwrap();
226 assert_eq!(first.updates[0].key, "kweb:AAAAAAAE");
227 assert!(first.display_text().contains("old"));
228
229 context.kweb_mut().refresh([node("new")]).unwrap();
230 let second = context.reconcile_kweb(&BTreeMap::new(), &[]).unwrap();
231 assert_eq!(second.updates.len(), 1);
232 assert_eq!(second.updates[0].key, "kweb:AAAAAAAE");
233 assert!(second.updates[0].text.as_deref().unwrap().contains("new"));
234 }
235
236 #[test]
237 fn managed_sources_are_isolated_by_kind_and_name() {
238 let mut context = Context::new(vec!["AAAAAAAE".into()], false).unwrap();
239 let snapshot = SourceSnapshot {
240 kind: ManagedSourceKind::RustLibrary,
241 name: "example".into(),
242 text: "source".into(),
243 };
244 let applied = context.apply_source_snapshot(snapshot.clone());
245 assert_eq!(applied.key, "managed:rust-library:example");
246 assert!(applied.update.is_some());
247 assert!(context.source_is_open(snapshot.kind, &snapshot.name));
248 assert!(context.apply_source_snapshot(snapshot).update.is_none());
249 assert!(!context.source_is_open(ManagedSourceKind::WebLibrary, "example"));
250 }
251
252 #[test]
253 fn kweb_projection_hides_unloaded_parent_plan_state() {
254 let mut context = Context::new(vec!["AAAAAAAE".into()], false).unwrap();
255 context
256 .kweb_mut()
257 .apply_load(node("loaded"), Vec::new())
258 .unwrap();
259 let unrelated_id = "AAAAAAAI".to_owned();
260 let updates = BTreeMap::from([(
261 unrelated_id,
262 NodeDraft {
263 short_name: "Hidden".into(),
264 short_description: "Hidden".into(),
265 long_description: "unrelated parent update".into(),
266 owner: "self".into(),
267 fixed_connections: Vec::new(),
268 recent_connections: Vec::new(),
269 objects: Vec::new(),
270 },
271 )]);
272 let hidden = StagedCreate {
273 pending_id: "pending:hidden".into(),
274 data: updates.values().next().unwrap().clone(),
275 };
276
277 let changes = context
278 .reconcile_kweb(&updates, std::slice::from_ref(&hidden))
279 .unwrap();
280 assert!(!changes.display_text().contains("unrelated parent update"));
281 assert!(!changes.display_text().contains("pending:hidden"));
282
283 context.include_staged_nodes([hidden.pending_id.clone()]);
284 let changes = context.reconcile_kweb(&updates, &[hidden]).unwrap();
285 assert!(changes.display_text().contains("pending:hidden"));
286 }
287}