Skip to main content

swarm_engine_core/context/
resolver.rs

1//! ContextResolver - スコープに応じたコンテキスト解決
2//!
3//! ContextStore + ContextView → ResolvedContext の変換を担当。
4//!
5//! # 使用例
6//!
7//! ```ignore
8//! let store = ContextStore::new(tick)
9//!     .with_worker(worker_ctx)
10//!     .with_manager(mgr_ctx);
11//!
12//! // Manager用(全体が見える)
13//! let mgr_view = ContextView::global(ManagerId(0));
14//! let mgr_context = ContextResolver::resolve(&store, &mgr_view);
15//!
16//! // Worker用(自分 + Neighbor のみ)
17//! let worker_view = ContextView::local_with_neighbors(
18//!     WorkerId(0),
19//!     vec![WorkerId(1)],
20//! );
21//! let worker_context = ContextResolver::resolve(&store, &worker_view);
22//! ```
23
24use crate::types::WorkerId;
25
26use super::store::{
27    ActionCandidate, ContextStore, ContextTarget, ContextView, ResolvedContext, WorkerContext,
28};
29use crate::agent::{ManagerId, WorkerScope};
30
31// ============================================================================
32// ContextResolver
33// ============================================================================
34
35/// コンテキスト解決器
36///
37/// ContextStore と ContextView から ResolvedContext を生成する。
38pub struct ContextResolver;
39
40impl ContextResolver {
41    /// ContextStore + View → ResolvedContext
42    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    /// Global View の解決(Manager用)
58    ///
59    /// 全ての Worker 情報と Escalation が見える。
60    fn resolve_global(store: &ContextStore, manager_id: ManagerId) -> ResolvedContext {
61        let global = store.global.clone();
62
63        // 全 Worker が見える
64        let visible_workers: Vec<WorkerContext> = store.workers.values().cloned().collect();
65
66        // 全 Escalation が見える
67        let escalations = store.escalations.clone();
68
69        // Action候補(全体から取得)
70        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    /// Local View の解決(Worker用)
84    ///
85    /// 自分自身と Neighbor の情報のみ見える。
86    fn resolve_local(
87        store: &ContextStore,
88        worker_id: WorkerId,
89        neighbor_ids: &[WorkerId],
90    ) -> ResolvedContext {
91        let global = store.global.clone();
92
93        // 自分 + Neighbor のみ見える
94        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        // 可視範囲の Escalation のみ
103        let escalations: Vec<_> = store
104            .escalations
105            .iter()
106            .filter(|(wid, _)| visible_worker_ids.contains(wid))
107            .cloned()
108            .collect();
109
110        // Action候補(全体から取得)
111        let candidates = store
112            .actions
113            .as_ref()
114            .map(ActionCandidate::from_config)
115            .unwrap_or_default();
116
117        // Worker用のメタデータ(グローバルから必要なものだけコピー)
118        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    /// Custom View の解決
134    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        // Custom の場合、最初の Worker を target とする(暫定)
156        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    // ========================================================================
169    // Scope-based Resolution (for Worker prompts)
170    // ========================================================================
171
172    /// WorkerScope に応じてコンテキストを解決(Worker 向け)
173    ///
174    /// Manager が Guidance で指定した Scope に基づいて、
175    /// Worker に渡す情報量を制御する。
176    ///
177    /// # Arguments
178    ///
179    /// * `store` - コンテキストストア
180    /// * `worker_id` - 対象 Worker ID
181    /// * `scope` - 情報スコープ
182    ///
183    /// # Example
184    ///
185    /// ```ignore
186    /// // Minimal: 自分の last_output のみ
187    /// let ctx = ContextResolver::resolve_with_scope(&store, worker_id, &WorkerScope::Minimal);
188    /// assert!(ctx.visible_workers.is_empty());
189    /// assert!(ctx.self_last_output.is_some());
190    ///
191    /// // SelfDetail: 自分の詳細情報
192    /// let ctx = ContextResolver::resolve_with_scope(&store, worker_id, &WorkerScope::SelfDetail);
193    /// assert_eq!(ctx.visible_workers.len(), 1);
194    /// ```
195    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        // Action 候補は常に取得
203        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                // Idle: 最小限のコンテキスト(candidates のみ)
212                ResolvedContext::new(global, ContextTarget::Worker(worker_id))
213                    .with_candidates(candidates)
214            }
215
216            WorkerScope::Minimal => {
217                // Minimal: 自分の last_output のみ
218                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                // SelfDetail: 自分の詳細情報(WorkerContext 全体)
232                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                // WithTeamSummary: 自分 + チームのサマリー(last_action のみ)
242                let visible_workers: Vec<WorkerContext> = store
243                    .workers
244                    .values()
245                    .map(|w| {
246                        // サマリー版: id, last_action, last_success のみ
247                        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                // WithTeamDetail: 自分 + チームの詳細(全情報)
267                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
280// ============================================================================
281// NeighborStrategy - Neighbor決定戦略
282// ============================================================================
283
284/// Neighbor決定戦略
285///
286/// Worker の可視範囲を決定する戦略。
287pub trait NeighborStrategy: Send + Sync {
288    /// 指定 Worker の Neighbor を決定
289    fn neighbors(&self, worker_id: WorkerId, all_workers: &[WorkerId]) -> Vec<WorkerId>;
290}
291
292/// 全員が見える戦略(デフォルト)
293#[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/// 隣接N件のみ見える戦略
303#[derive(Debug, Clone)]
304pub struct AdjacentStrategy {
305    /// 前後何件見えるか
306    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/// Escalation中のWorkerのみ見える戦略
332#[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        // 実際の実装では ContextStore を参照してEscalation中のWorkerを返す
338        // ここでは簡易版として空を返す
339        Vec::new()
340    }
341}
342
343// ============================================================================
344// Tests
345// ============================================================================
346
347#[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        // Manager は全 Worker が見える
377        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        // 自分のみ見える
389        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        // 自分 + Neighbor(1) が見える
401        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        // Worker 0 は Worker 1 のみ Neighbor
420        let view = ContextView::local_with_neighbors(WorkerId(0), vec![WorkerId(1)]);
421        let resolved = ContextResolver::resolve(&store, &view);
422
423        // Worker 1 の Escalation のみ見える(Worker 2 のは見えない)
424        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}