swarm_engine_core/agent/manager.rs
1//! ManagerAgent - 観察・判断・指示を行う上位Agent
2//!
3//! # 型の正規定義
4//!
5//! 以下の型はこのモジュールで正規定義されています:
6//! - `ManagerId`
7//! - `BatchDecisionRequest`
8//! - `WorkerDecisionRequest`
9//! - `ManagementDecision`
10//! - `ManagementStrategy`
11//!
12//! これらは `batch.rs` からも re-export されます。
13//!
14//! # 設計変更(v2)
15//!
16//! ManagerAgent は TaskContext を受け取り、Request を生成する。
17//! SwarmState からの分析は Analyzer が担当。
18//!
19//! ```text
20//! SwarmState → [Analyzer] → TaskContext → [Manager] → BatchDecisionRequest
21//! ↓
22//! [BatchInvoker] → Response
23//! ↓
24//! [Manager.finalize] → ManagementDecision
25//! ```
26
27use std::collections::HashMap;
28
29use crate::types::{LoraConfig, WorkerId};
30
31use super::batch::DecisionResponse;
32use super::worker::Guidance;
33use crate::context::TaskContext;
34
35// ============================================================================
36// Management Strategy
37// ============================================================================
38
39/// ManagerAgent の起動タイミング戦略
40#[derive(Debug, Clone)]
41pub enum ManagementStrategy {
42 /// 毎 Tick 起動(LLM 不要なフロー向け)
43 ///
44 /// V2 ExplorationSpace など、LLM 呼び出しなしで Guidance を生成できる場合に使用。
45 /// Worker がアクションを完了したら即座に次のノードを割り当てられる。
46 EveryTick,
47
48 /// N Tick ごとに ManagerAgent を起動
49 FixedInterval { interval: u64 },
50
51 /// 全 WorkerAgent の完了を待って起動
52 CompletionBased {
53 /// 最大待機Tick(タイムアウト)
54 max_wait_ticks: u64,
55 },
56
57 /// ハイブリッド: 完了待ちだが、最大N Tickで強制起動
58 Hybrid {
59 preferred_interval: u64,
60 force_after_ticks: u64,
61 },
62
63 /// Escalation ベース: 定期 + Escalation 発生時に即時起動
64 EscalationBased {
65 /// 定期起動間隔
66 interval: u64,
67 /// Escalation 発生時に即時起動するか
68 immediate_on_escalation: bool,
69 },
70}
71
72impl Default for ManagementStrategy {
73 fn default() -> Self {
74 ManagementStrategy::FixedInterval { interval: 10 }
75 }
76}
77
78// ============================================================================
79// Manager ID
80// ============================================================================
81
82/// Manager ID
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub struct ManagerId(pub usize);
85
86// ============================================================================
87// Batch Request
88// ============================================================================
89
90/// Batch リクエスト(1 Manager が 1 Tick で送る全リクエスト)
91///
92/// Manager が各 Worker に対して判断を求めるリクエストをまとめたもの。
93/// ContextStore → ContextResolver → ResolvedContext の流れで生成される。
94#[derive(Debug, Clone)]
95pub struct BatchDecisionRequest {
96 /// 発行元 Manager ID
97 pub manager_id: ManagerId,
98 /// 各 Worker への判断リクエスト
99 pub requests: Vec<WorkerDecisionRequest>,
100}
101
102use crate::context::ResolvedContext;
103
104/// 個別 Worker への判断リクエスト
105///
106/// Worker が判断に必要な情報を含む。
107/// `context` は構造化データとして保持し、文字列化は LLM 層で行う。
108#[derive(Debug, Clone)]
109pub struct WorkerDecisionRequest {
110 /// 対象 Worker ID
111 pub worker_id: WorkerId,
112 /// タスク目標(query)
113 pub query: String,
114 /// Worker 固有のコンテキスト(構造化データ)
115 pub context: ResolvedContext,
116 /// LoRA アダプター設定(オプション)
117 ///
118 /// 指定すると、このリクエストに対して特定の LoRA アダプターを適用する。
119 /// llama.cpp の per-request LoRA 機能を使用。
120 pub lora: Option<LoraConfig>,
121}
122
123// ============================================================================
124// Management Decision
125// ============================================================================
126
127/// 非同期タスクリクエスト
128#[derive(Debug, Clone, Default)]
129pub struct AsyncTaskRequest {
130 pub task_type: String,
131 pub params: HashMap<String, String>,
132}
133
134/// ManagerAgent の決定結果
135#[derive(Default, Clone)]
136pub struct ManagementDecision {
137 /// WorkerAgent への Guidance(Worker ID -> Guidance)
138 pub guidances: HashMap<WorkerId, Guidance>,
139 /// 戦略の動的変更(オプション)
140 pub strategy_update: Option<ManagementStrategy>,
141 /// 非同期タスクの発行(重いLLM呼び出し等)
142 pub async_tasks: Vec<AsyncTaskRequest>,
143}
144
145impl ManagementDecision {
146 /// 単一 Worker への Guidance を設定
147 pub fn with_guidance(mut self, worker_id: WorkerId, guidance: Guidance) -> Self {
148 self.guidances.insert(worker_id, guidance);
149 self
150 }
151
152 /// 全 Worker に同じ Guidance を設定
153 pub fn broadcast_guidance(mut self, worker_ids: &[WorkerId], guidance: Guidance) -> Self {
154 for &id in worker_ids {
155 self.guidances.insert(id, guidance.clone());
156 }
157 self
158 }
159}
160
161// ============================================================================
162// ManagerAgent Trait
163// ============================================================================
164
165/// 観察・判断・指示を行う上位Agent(Batch 対応)
166///
167/// # 設計
168///
169/// ManagerAgent は TaskContext を受け取り、Request を生成する。
170/// SwarmState からの分析は Analyzer が担当。
171///
172/// ```text
173/// ┌─────────────────────────────────────────────────────────────┐
174/// │ Tick N │
175/// │ │
176/// │ SwarmState → [Analyzer] → TaskContext │
177/// │ ↓ │
178/// │ Manager[0].prepare(ctx) ─→ N Requests ─┐ │
179/// │ Manager[1].prepare(ctx) ─→ N Requests ─┼─→ Batch │
180/// │ ... │ │
181/// │ Manager[M].prepare(ctx) ─→ N Requests ─┘ │
182/// │ │
183/// │ ↓ LLM Batch Call (~1秒) │
184/// │ │
185/// │ M×N Responses → 各 Manager.finalize(ctx, responses) │
186/// │ ↓ │
187/// │ Worker Instructions 配布 │
188/// └─────────────────────────────────────────────────────────────┘
189/// ```
190pub trait ManagerAgent: Send + Sync {
191 /// TaskContext を見て Batch リクエストを生成(軽量、同期)
192 ///
193 /// どの Worker に何を聞くかを決定する。
194 /// Orchestrator がこれらを集約して LLM Batch Call を実行する。
195 fn prepare(&self, context: &TaskContext) -> BatchDecisionRequest;
196
197 /// Batch 結果受取後: 指示を生成(軽量、同期)
198 ///
199 /// LLM Batch Call の結果を受けて、Worker への具体的な指示を生成する。
200 fn finalize(
201 &self,
202 context: &TaskContext,
203 responses: Vec<(WorkerId, DecisionResponse)>,
204 ) -> ManagementDecision;
205
206 /// Manager ID を取得
207 fn id(&self) -> ManagerId;
208
209 /// 名前を取得
210 fn name(&self) -> &str;
211}
212
213/// Blanket implementation for Box<dyn ManagerAgent>
214impl ManagerAgent for Box<dyn ManagerAgent> {
215 fn prepare(&self, context: &TaskContext) -> BatchDecisionRequest {
216 (**self).prepare(context)
217 }
218
219 fn finalize(
220 &self,
221 context: &TaskContext,
222 responses: Vec<(WorkerId, DecisionResponse)>,
223 ) -> ManagementDecision {
224 (**self).finalize(context, responses)
225 }
226
227 fn id(&self) -> ManagerId {
228 (**self).id()
229 }
230
231 fn name(&self) -> &str {
232 (**self).name()
233 }
234}