swarm_engine_core/context/
resolver.rs1use crate::types::WorkerId;
25
26use super::store::{
27 ActionCandidate, ContextStore, ContextTarget, ContextView, ResolvedContext, WorkerContext,
28};
29use crate::agent::{ManagerId, WorkerScope};
30
31pub struct ContextResolver;
39
40impl ContextResolver {
41 pub fn resolve(store: &ContextStore, view: &ContextView) -> ResolvedContext {
43 match view {
44 ContextView::Global { manager_id } => Self::resolve_global(store, *manager_id),
45 ContextView::Local {
46 worker_id,
47 neighbor_ids,
48 } => Self::resolve_local(store, *worker_id, neighbor_ids),
49 ContextView::Custom {
50 name: _,
51 visible_worker_ids,
52 visible_manager_ids: _,
53 } => Self::resolve_custom(store, visible_worker_ids),
54 }
55 }
56
57 fn resolve_global(store: &ContextStore, manager_id: ManagerId) -> ResolvedContext {
61 let global = store.global.clone();
62
63 let visible_workers: Vec<WorkerContext> = store.workers.values().cloned().collect();
65
66 let escalations = store.escalations.clone();
68
69 let candidates = store
71 .actions
72 .as_ref()
73 .map(ActionCandidate::from_config)
74 .unwrap_or_default();
75
76 ResolvedContext::new(global, ContextTarget::Manager(manager_id))
77 .with_workers(visible_workers)
78 .with_escalations(escalations)
79 .with_candidates(candidates)
80 .with_metadata(store.metadata.clone())
81 }
82
83 fn resolve_local(
87 store: &ContextStore,
88 worker_id: WorkerId,
89 neighbor_ids: &[WorkerId],
90 ) -> ResolvedContext {
91 let global = store.global.clone();
92
93 let mut visible_worker_ids = vec![worker_id];
95 visible_worker_ids.extend(neighbor_ids.iter().copied());
96
97 let visible_workers: Vec<WorkerContext> = visible_worker_ids
98 .iter()
99 .filter_map(|id| store.workers.get(id).cloned())
100 .collect();
101
102 let escalations: Vec<_> = store
104 .escalations
105 .iter()
106 .filter(|(wid, _)| visible_worker_ids.contains(wid))
107 .cloned()
108 .collect();
109
110 let candidates = store
112 .actions
113 .as_ref()
114 .map(ActionCandidate::from_config)
115 .unwrap_or_default();
116
117 let mut metadata = std::collections::HashMap::new();
119 if let Some(task) = store.get("task") {
120 metadata.insert("task".to_string(), task.clone());
121 }
122 if let Some(hint) = store.get("hint") {
123 metadata.insert("hint".to_string(), hint.clone());
124 }
125
126 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
127 .with_workers(visible_workers)
128 .with_escalations(escalations)
129 .with_candidates(candidates)
130 .with_metadata(metadata)
131 }
132
133 fn resolve_custom(store: &ContextStore, visible_worker_ids: &[WorkerId]) -> ResolvedContext {
135 let global = store.global.clone();
136
137 let visible_workers: Vec<WorkerContext> = visible_worker_ids
138 .iter()
139 .filter_map(|id| store.workers.get(id).cloned())
140 .collect();
141
142 let escalations: Vec<_> = store
143 .escalations
144 .iter()
145 .filter(|(wid, _)| visible_worker_ids.contains(wid))
146 .cloned()
147 .collect();
148
149 let candidates = store
150 .actions
151 .as_ref()
152 .map(ActionCandidate::from_config)
153 .unwrap_or_default();
154
155 let target = visible_worker_ids
157 .first()
158 .map(|&id| ContextTarget::Worker(id))
159 .unwrap_or(ContextTarget::Worker(WorkerId(0)));
160
161 ResolvedContext::new(global, target)
162 .with_workers(visible_workers)
163 .with_escalations(escalations)
164 .with_candidates(candidates)
165 .with_metadata(store.metadata.clone())
166 }
167
168 pub fn resolve_with_scope(
196 store: &ContextStore,
197 worker_id: WorkerId,
198 scope: &WorkerScope,
199 ) -> ResolvedContext {
200 let global = store.global.clone();
201
202 let candidates = store
204 .actions
205 .as_ref()
206 .map(ActionCandidate::from_config)
207 .unwrap_or_default();
208
209 match scope {
210 WorkerScope::Idle => {
211 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
213 .with_candidates(candidates)
214 }
215
216 WorkerScope::Minimal => {
217 let self_output = store
219 .workers
220 .get(&worker_id)
221 .and_then(|w| w.metadata.get("last_output"))
222 .and_then(|v| v.as_str())
223 .map(String::from);
224
225 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
226 .with_self_last_output(self_output)
227 .with_candidates(candidates)
228 }
229
230 WorkerScope::SelfDetail => {
231 let self_ctx = store.workers.get(&worker_id).cloned();
233 let visible_workers = self_ctx.into_iter().collect();
234
235 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
236 .with_workers(visible_workers)
237 .with_candidates(candidates)
238 }
239
240 WorkerScope::WithTeamSummary => {
241 let visible_workers: Vec<WorkerContext> = store
243 .workers
244 .values()
245 .map(|w| {
246 WorkerContext {
248 id: w.id,
249 consecutive_failures: 0,
250 last_action: w.last_action.clone(),
251 last_success: w.last_success,
252 history_len: 0,
253 has_escalation: w.has_escalation,
254 candidates: Vec::new(),
255 metadata: std::collections::HashMap::new(),
256 }
257 })
258 .collect();
259
260 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
261 .with_workers(visible_workers)
262 .with_candidates(candidates)
263 }
264
265 WorkerScope::WithTeamDetail => {
266 let visible_workers: Vec<WorkerContext> = store.workers.values().cloned().collect();
268
269 let escalations = store.escalations.clone();
270
271 ResolvedContext::new(global, ContextTarget::Worker(worker_id))
272 .with_workers(visible_workers)
273 .with_escalations(escalations)
274 .with_candidates(candidates)
275 }
276 }
277 }
278}
279
280pub trait NeighborStrategy: Send + Sync {
288 fn neighbors(&self, worker_id: WorkerId, all_workers: &[WorkerId]) -> Vec<WorkerId>;
290}
291
292#[derive(Debug, Clone, Default)]
294pub struct AllVisibleStrategy;
295
296impl NeighborStrategy for AllVisibleStrategy {
297 fn neighbors(&self, _worker_id: WorkerId, all_workers: &[WorkerId]) -> Vec<WorkerId> {
298 all_workers.to_vec()
299 }
300}
301
302#[derive(Debug, Clone)]
304pub struct AdjacentStrategy {
305 pub radius: usize,
307}
308
309impl AdjacentStrategy {
310 pub fn new(radius: usize) -> Self {
311 Self { radius }
312 }
313}
314
315impl NeighborStrategy for AdjacentStrategy {
316 fn neighbors(&self, worker_id: WorkerId, all_workers: &[WorkerId]) -> Vec<WorkerId> {
317 let idx = worker_id.0;
318
319 all_workers
320 .iter()
321 .enumerate()
322 .filter(|(i, _)| {
323 let diff = (*i as isize - idx as isize).unsigned_abs();
324 diff > 0 && diff <= self.radius
325 })
326 .map(|(_, &id)| id)
327 .collect()
328 }
329}
330
331#[derive(Debug, Clone, Default)]
333pub struct EscalationOnlyStrategy;
334
335impl NeighborStrategy for EscalationOnlyStrategy {
336 fn neighbors(&self, _worker_id: WorkerId, _all_workers: &[WorkerId]) -> Vec<WorkerId> {
337 Vec::new()
340 }
341}
342
343#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::context::ManagerContext;
351
352 fn create_test_store() -> ContextStore {
353 ContextStore::new(10)
354 .with_worker(
355 WorkerContext::new(WorkerId(0))
356 .with_last_action("read:/a", true)
357 .with_candidates(vec!["read".into(), "grep".into()]),
358 )
359 .with_worker(
360 WorkerContext::new(WorkerId(1))
361 .with_last_action("grep:pattern", false)
362 .with_escalation(true),
363 )
364 .with_worker(WorkerContext::new(WorkerId(2)).with_failures(3))
365 .with_manager(ManagerContext::new(ManagerId(0)))
366 .insert("task", "Find the bug")
367 .insert("hint", "Check error logs")
368 }
369
370 #[test]
371 fn test_resolve_global() {
372 let store = create_test_store();
373 let view = ContextView::global(ManagerId(0));
374 let resolved = ContextResolver::resolve(&store, &view);
375
376 assert_eq!(resolved.visible_workers.len(), 3);
378 assert!(resolved.is_manager());
379 assert_eq!(resolved.global.tick, 10);
380 }
381
382 #[test]
383 fn test_resolve_local_no_neighbors() {
384 let store = create_test_store();
385 let view = ContextView::local(WorkerId(0));
386 let resolved = ContextResolver::resolve(&store, &view);
387
388 assert_eq!(resolved.visible_workers.len(), 1);
390 assert_eq!(resolved.visible_workers[0].id, WorkerId(0));
391 assert!(resolved.is_worker());
392 }
393
394 #[test]
395 fn test_resolve_local_with_neighbors() {
396 let store = create_test_store();
397 let view = ContextView::local_with_neighbors(WorkerId(0), vec![WorkerId(1)]);
398 let resolved = ContextResolver::resolve(&store, &view);
399
400 assert_eq!(resolved.visible_workers.len(), 2);
402 let ids: Vec<_> = resolved.visible_workers.iter().map(|w| w.id).collect();
403 assert!(ids.contains(&WorkerId(0)));
404 assert!(ids.contains(&WorkerId(1)));
405 }
406
407 #[test]
408 fn test_resolve_local_filters_escalations() {
409 let store = create_test_store()
410 .with_escalation(
411 WorkerId(1),
412 crate::state::Escalation::consecutive_failures(3, 5),
413 )
414 .with_escalation(
415 WorkerId(2),
416 crate::state::Escalation::consecutive_failures(5, 8),
417 );
418
419 let view = ContextView::local_with_neighbors(WorkerId(0), vec![WorkerId(1)]);
421 let resolved = ContextResolver::resolve(&store, &view);
422
423 assert_eq!(resolved.escalations.len(), 1);
425 assert_eq!(resolved.escalations[0].0, WorkerId(1));
426 }
427
428 #[test]
429 fn test_adjacent_strategy() {
430 let strategy = AdjacentStrategy::new(1);
431 let all = vec![WorkerId(0), WorkerId(1), WorkerId(2), WorkerId(3)];
432
433 let neighbors = strategy.neighbors(WorkerId(1), &all);
434 assert!(neighbors.contains(&WorkerId(0)));
435 assert!(neighbors.contains(&WorkerId(2)));
436 assert!(!neighbors.contains(&WorkerId(3)));
437 }
438}