Skip to main content

swarm_engine_core/learn/
facade.rs

1//! Learnable Swarm Facade - 学習機能付き Swarm の統合ファサード
2//!
3//! ## 設計思想
4//!
5//! **Orchestrator + LearningDaemon + Subscriber の統合を隠蔽し、
6//! Ready な Swarm を提供する。**
7//!
8//! ```text
9//! LearnableSwarmBuilder
10//!   ├── OrchestratorBuilder 設定
11//!   ├── LearningDaemon 起動
12//!   ├── ActionEventSubscriber 連動
13//!   └── LearningEventSubscriber 連動
14//!         │
15//!         ▼
16//!   LearnableSwarm (Ready to run)
17//!         │
18//!    ┌────┴────┐
19//!    ▼         ▼
20//! EvalRunner  run command / GUI
21//! ```
22//!
23//! ## 使用例
24//!
25//! ```ignore
26//! use swarm_engine_core::learn::facade::{LearnableSwarm, LearnableSwarmBuilder};
27//!
28//! let swarm = LearnableSwarmBuilder::new(runtime.handle().clone())
29//!     .scenario("troubleshooting")
30//!     .with_learning(true)
31//!     .with_offline_model(model)
32//!     .build()?;
33//!
34//! let result = swarm.run_task(task)?;
35//!
36//! // Graceful shutdown (Subscriber / Daemon 終了待ち)
37//! swarm.shutdown().await;
38//! ```
39//!
40//! ## 責務分離
41//!
42//! | レイヤー | 責務 |
43//! |----------|------|
44//! | `LearnableSwarm` | 統合・ライフサイクル管理 |
45//! | `Orchestrator` | Tick 駆動実行 |
46//! | `LearningDaemon` | 継続的学習 |
47//! | `Subscriber` | Event → Record 変換 |
48
49use std::path::PathBuf;
50use std::sync::Arc;
51use std::time::Duration;
52
53use tokio::runtime::Handle;
54use tokio::sync::mpsc;
55use tokio::task::JoinHandle;
56
57use crate::agent::{BatchInvoker, ManagerAgent, WorkerAgent};
58use crate::error::SwarmError;
59use crate::events::{ActionEventPublisher, LearningEventChannel, LifecycleHook, TraceSubscriber};
60use crate::exploration::{DependencyGraph, DependencyGraphProvider, NodeRules, OperatorProvider};
61use crate::extensions::Extensions;
62use crate::orchestrator::{Orchestrator, OrchestratorBuilder, SwarmConfig, SwarmResult};
63use crate::types::SwarmTask;
64
65use super::daemon::{
66    ActionEventSubscriber, DaemonConfig, DaemonError, EventSubscriberConfig, LearningDaemon,
67    LearningEventSubscriber,
68};
69use super::profile_adapter::profile_to_offline_model;
70use super::scenario_profile::ScenarioProfile;
71use super::snapshot::LearningStore;
72use super::trigger::{TrainTrigger, TriggerBuilder};
73use super::LearningSnapshot;
74use super::OfflineModel;
75
76// ============================================================================
77// Type Aliases
78// ============================================================================
79
80/// LearningDaemon の JoinHandle 型
81type DaemonHandle = JoinHandle<Result<(), DaemonError>>;
82
83/// Learning コンポーネントのセットアップ結果
84#[derive(Default)]
85struct LearningSetupResult {
86    /// LearningDaemon の JoinHandle
87    daemon_handle: Option<DaemonHandle>,
88    /// Subscriber の JoinHandle 群
89    subscriber_handles: Vec<JoinHandle<()>>,
90    /// Shutdown 送信チャンネル
91    shutdown_tx: Option<mpsc::Sender<()>>,
92}
93
94// ============================================================================
95// LearnableSwarmConfig
96// ============================================================================
97
98/// 学習機能付き Swarm の設定
99#[derive(Debug, Clone)]
100pub struct LearnableSwarmConfig {
101    /// シナリオ名(学習データのキー)
102    pub scenario: String,
103    /// 学習データディレクトリ
104    pub data_dir: PathBuf,
105    /// 学習機能の有効/無効
106    pub learning_enabled: bool,
107    /// Subscriber のバッチサイズ
108    pub subscriber_batch_size: usize,
109    /// Subscriber のフラッシュ間隔(ミリ秒)
110    pub subscriber_flush_interval_ms: u64,
111    /// Daemon のチェック間隔
112    pub daemon_check_interval: Duration,
113}
114
115impl Default for LearnableSwarmConfig {
116    fn default() -> Self {
117        Self {
118            scenario: String::new(),
119            data_dir: default_learning_dir(),
120            learning_enabled: false,
121            subscriber_batch_size: 100,
122            subscriber_flush_interval_ms: 100,
123            daemon_check_interval: Duration::from_secs(10),
124        }
125    }
126}
127
128impl LearnableSwarmConfig {
129    /// 新しい設定を作成
130    pub fn new(scenario: impl Into<String>) -> Self {
131        Self {
132            scenario: scenario.into(),
133            ..Default::default()
134        }
135    }
136
137    /// 学習を有効化
138    pub fn with_learning(mut self, enabled: bool) -> Self {
139        self.learning_enabled = enabled;
140        self
141    }
142
143    /// データディレクトリを設定
144    pub fn data_dir(mut self, path: impl Into<PathBuf>) -> Self {
145        self.data_dir = path.into();
146        self
147    }
148}
149
150fn default_learning_dir() -> PathBuf {
151    dirs::data_dir()
152        .unwrap_or_else(|| PathBuf::from("."))
153        .join("swarm-engine")
154        .join("learning")
155}
156
157// ============================================================================
158// LearnableSwarmBuilder
159// ============================================================================
160
161/// 学習機能付き Swarm のビルダー
162///
163/// Orchestrator + LearningDaemon + Subscriber を統合構築する。
164pub struct LearnableSwarmBuilder {
165    /// Tokio runtime handle
166    runtime: Handle,
167    /// 設定
168    config: LearnableSwarmConfig,
169    /// Swarm 設定
170    swarm_config: Option<SwarmConfig>,
171    /// Workers
172    workers: Vec<Box<dyn WorkerAgent>>,
173    /// Managers
174    managers: Vec<Box<dyn ManagerAgent>>,
175    /// BatchInvoker
176    batch_invoker: Option<Box<dyn BatchInvoker>>,
177    /// DependencyGraphProvider
178    dependency_provider: Option<Box<dyn DependencyGraphProvider>>,
179    /// OperatorProvider
180    operator_provider: Option<Box<dyn OperatorProvider<NodeRules>>>,
181    /// Extensions
182    extensions: Option<Extensions>,
183    /// DependencyGraph
184    dependency_graph: Option<DependencyGraph>,
185    /// Offline model
186    offline_model: Option<OfflineModel>,
187    /// Prior snapshot
188    prior_snapshot: Option<LearningSnapshot>,
189    /// LearningStore
190    learning_store: Option<LearningStore>,
191    /// Train trigger
192    train_trigger: Option<Arc<dyn TrainTrigger>>,
193    /// LifecycleHook
194    lifecycle_hook: Option<Box<dyn LifecycleHook>>,
195    /// Exploration 有効化
196    enable_exploration: bool,
197    /// Deferred error from builder methods (checked in build())
198    deferred_error: Option<SwarmError>,
199    /// Trace subscriber (ActionEvent のトレース出力)
200    trace_subscriber: Option<Arc<dyn TraceSubscriber>>,
201}
202
203impl LearnableSwarmBuilder {
204    /// 新しいビルダーを作成
205    pub fn new(runtime: Handle) -> Self {
206        Self {
207            runtime,
208            config: LearnableSwarmConfig::default(),
209            swarm_config: None,
210            workers: Vec::new(),
211            managers: Vec::new(),
212            batch_invoker: None,
213            dependency_provider: None,
214            operator_provider: None,
215            extensions: None,
216            dependency_graph: None,
217            offline_model: None,
218            prior_snapshot: None,
219            learning_store: None,
220            train_trigger: None,
221            lifecycle_hook: None,
222            enable_exploration: false,
223            deferred_error: None,
224            trace_subscriber: None,
225        }
226    }
227
228    /// シナリオ名を設定
229    pub fn scenario(mut self, name: impl Into<String>) -> Self {
230        self.config.scenario = name.into();
231        self
232    }
233
234    /// 学習機能を有効化
235    ///
236    /// Note: `LearningEventChannel` の有効化は `build()` 時に行われる。
237    pub fn with_learning(mut self, enabled: bool) -> Self {
238        self.config.learning_enabled = enabled;
239        self
240    }
241
242    /// データディレクトリを設定
243    pub fn data_dir(mut self, path: impl Into<PathBuf>) -> Self {
244        self.config.data_dir = path.into();
245        self
246    }
247
248    /// Swarm 設定を追加
249    pub fn swarm_config(mut self, config: SwarmConfig) -> Self {
250        self.swarm_config = Some(config);
251        self
252    }
253
254    /// Worker を追加
255    pub fn add_worker(mut self, worker: Box<dyn WorkerAgent>) -> Self {
256        self.workers.push(worker);
257        self
258    }
259
260    /// 複数の Workers を追加
261    pub fn workers(mut self, workers: Vec<Box<dyn WorkerAgent>>) -> Self {
262        self.workers = workers;
263        self
264    }
265
266    /// Manager を追加
267    pub fn add_manager(mut self, manager: Box<dyn ManagerAgent>) -> Self {
268        self.managers.push(manager);
269        self
270    }
271
272    /// 複数の Managers を追加
273    pub fn managers(mut self, managers: Vec<Box<dyn ManagerAgent>>) -> Self {
274        self.managers = managers;
275        self
276    }
277
278    /// BatchInvoker を設定
279    pub fn batch_invoker(mut self, invoker: Box<dyn BatchInvoker>) -> Self {
280        self.batch_invoker = Some(invoker);
281        self
282    }
283
284    /// DependencyGraphProvider を設定
285    pub fn dependency_provider(mut self, provider: Box<dyn DependencyGraphProvider>) -> Self {
286        self.dependency_provider = Some(provider);
287        self
288    }
289
290    /// OperatorProvider を設定
291    pub fn operator_provider(mut self, provider: Box<dyn OperatorProvider<NodeRules>>) -> Self {
292        self.operator_provider = Some(provider);
293        self
294    }
295
296    /// Extensions を設定
297    pub fn extensions(mut self, extensions: Extensions) -> Self {
298        self.extensions = Some(extensions);
299        self
300    }
301
302    /// DependencyGraph を設定
303    pub fn dependency_graph(mut self, graph: DependencyGraph) -> Self {
304        self.dependency_graph = Some(graph);
305        self
306    }
307
308    /// Offline model を設定
309    pub fn offline_model(mut self, model: OfflineModel) -> Self {
310        self.offline_model = Some(model);
311        self
312    }
313
314    /// ScenarioProfile から OfflineModel を生成して設定
315    ///
316    /// ScenarioProfile の各コンポーネント(LearnedExploration, LearnedStrategy, LearnedDepGraph)
317    /// を OfflineModel に変換し、Orchestrator に適用する。
318    ///
319    /// # Example
320    ///
321    /// ```ignore
322    /// use swarm_engine_core::learn::{LearnableSwarmBuilder, ScenarioProfile};
323    ///
324    /// let profile = load_profile("troubleshooting")?;
325    /// let swarm = LearnableSwarmBuilder::new(runtime.handle().clone())
326    ///     .with_scenario_profile(&profile)
327    ///     .build()?;
328    /// ```
329    pub fn with_scenario_profile(mut self, profile: &ScenarioProfile) -> Self {
330        let model = profile_to_offline_model(profile);
331        self.offline_model = Some(model);
332        // シナリオ名も自動設定
333        if self.config.scenario.is_empty() {
334            self.config.scenario = profile.id.0.clone();
335        }
336        self
337    }
338
339    /// Offline model への参照を取得(build 前に参照する場合)
340    pub fn offline_model_ref(&self) -> Option<&OfflineModel> {
341        self.offline_model.as_ref()
342    }
343
344    /// Prior snapshot を設定
345    pub fn prior_snapshot(mut self, snapshot: LearningSnapshot) -> Self {
346        self.prior_snapshot = Some(snapshot);
347        self
348    }
349
350    /// LearningStore を設定(ロードなし、store のみ設定)
351    pub fn learning_store(mut self, store: LearningStore) -> Self {
352        self.learning_store = Some(store);
353        self
354    }
355
356    /// 既存の LearningStore からシナリオデータを自動ロード
357    ///
358    /// prior_snapshot, offline_model, data_dir を自動設定し、learning を有効化する。
359    /// EvalRunner など、store を保持し続ける必要がある場合に使用。
360    ///
361    /// # Note
362    /// ロード時のクリティカルエラー(permission denied 等)は `build()` 時に返される。
363    /// `NotFound`(初回実行)は正常として扱われる。
364    pub fn with_learning_store(mut self, store: LearningStore) -> Self {
365        if let Err(e) = self.load_from_store(&store) {
366            self.deferred_error = Some(e);
367        }
368        self.config.data_dir = store.storage().base_dir().to_path_buf();
369        self.config.learning_enabled = true;
370        self.learning_store = Some(store);
371        self
372    }
373
374    /// Train trigger を設定
375    pub fn train_trigger(mut self, trigger: Arc<dyn TrainTrigger>) -> Self {
376        self.train_trigger = Some(trigger);
377        self
378    }
379
380    /// LifecycleHook を設定
381    pub fn lifecycle_hook(mut self, hook: Box<dyn LifecycleHook>) -> Self {
382        self.lifecycle_hook = Some(hook);
383        self
384    }
385
386    /// Exploration を有効化
387    pub fn enable_exploration(mut self, enabled: bool) -> Self {
388        self.enable_exploration = enabled;
389        self
390    }
391
392    /// TraceSubscriber を設定(ActionEvent のトレース出力)
393    ///
394    /// # Example
395    ///
396    /// ```ignore
397    /// use swarm_engine_core::events::{InMemoryTraceSubscriber, JsonlTraceSubscriber};
398    ///
399    /// // InMemory: 最後にまとめて出力
400    /// let trace = Arc::new(InMemoryTraceSubscriber::new());
401    /// builder.with_trace_subscriber(trace.clone());
402    ///
403    /// // Jsonl: リアルタイム出力
404    /// let trace = Arc::new(JsonlTraceSubscriber::new("trace.jsonl")?);
405    /// builder.with_trace_subscriber(trace);
406    /// ```
407    pub fn with_trace_subscriber(mut self, subscriber: Arc<dyn TraceSubscriber>) -> Self {
408        self.trace_subscriber = Some(subscriber);
409        self
410    }
411
412    /// LearningStore からシナリオデータを自動ロード(パス指定)
413    ///
414    /// prior_snapshot と offline_model を自動設定する。
415    ///
416    /// # Note
417    /// Store 作成失敗やロード時のクリティカルエラーは `build()` 時に返される。
418    pub fn with_learning_store_path(mut self, path: impl AsRef<std::path::Path>) -> Self {
419        match LearningStore::new(&path) {
420            Ok(store) => {
421                if let Err(e) = self.load_from_store(&store) {
422                    self.deferred_error = Some(e);
423                }
424                self.config.learning_enabled = true;
425                self.learning_store = Some(store);
426            }
427            Err(e) => {
428                // Store 作成失敗もクリティカル
429                self.deferred_error = Some(SwarmError::Config {
430                    message: format!(
431                        "Failed to create LearningStore at '{}': {}",
432                        path.as_ref().display(),
433                        e
434                    ),
435                });
436            }
437        }
438        self
439    }
440
441    /// LearningStore からシナリオデータをロード(内部ヘルパー)
442    ///
443    /// # Errors
444    /// - `NotFound`: 初回実行時(データなし)→ 正常、None を返す
445    /// - その他の IO エラー: クリティカル → Err を返す
446    fn load_from_store(&mut self, store: &LearningStore) -> Result<(), SwarmError> {
447        let scenario_key = &self.config.scenario;
448
449        // Prior snapshot をロード
450        match store.load_scenario(scenario_key) {
451            Ok(snapshot) => {
452                self.prior_snapshot = Some(snapshot);
453            }
454            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
455                // 初回実行時は存在しない → 正常
456                tracing::debug!(scenario = %scenario_key, "No prior snapshot found (first run)");
457            }
458            Err(e) => {
459                // Permission denied, corrupted data 等 → クリティカル
460                return Err(SwarmError::Config {
461                    message: format!(
462                        "Failed to load prior snapshot for '{}': {}",
463                        scenario_key, e
464                    ),
465                });
466            }
467        }
468
469        // Offline model をロード
470        match store.load_offline_model(scenario_key) {
471            Ok(model) => {
472                tracing::debug!(
473                    ucb1_c = model.parameters.ucb1_c,
474                    strategy = %model.strategy_config.initial_strategy,
475                    action_order = model.action_order.is_some(),
476                    "Offline model loaded"
477                );
478                self.offline_model = Some(model);
479            }
480            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
481                // 初回実行時は存在しない → 正常
482                tracing::debug!(scenario = %scenario_key, "No offline model found (first run)");
483            }
484            Err(e) => {
485                // Permission denied, corrupted data 等 → クリティカル
486                return Err(SwarmError::Config {
487                    message: format!("Failed to load offline model for '{}': {}", scenario_key, e),
488                });
489            }
490        }
491
492        Ok(())
493    }
494
495    /// LearnableSwarm を構築
496    pub fn build(mut self) -> Result<LearnableSwarm, SwarmError> {
497        // ========================================================================
498        // バリデーション
499        // ========================================================================
500        // Deferred error from builder methods (e.g., load_from_store failure)
501        if let Some(err) = self.deferred_error.take() {
502            return Err(err);
503        }
504
505        if self.config.learning_enabled && self.config.scenario.is_empty() {
506            return Err(SwarmError::Config {
507                message: "scenario is required when learning is enabled".into(),
508            });
509        }
510
511        // LearningEventChannel の有効化(ここで一度だけ行う)
512        if self.config.learning_enabled {
513            LearningEventChannel::global().enable();
514        }
515
516        let swarm_config = self.swarm_config.take().unwrap_or_default();
517
518        // ========================================================================
519        // Learning 関連の起動(learning_enabled の場合のみ)
520        // ========================================================================
521        // Note: Initial receiver is not needed; subscribers call action_publisher.subscribe()
522        let (action_publisher, _initial_rx) = ActionEventPublisher::new(1024);
523
524        // TraceSubscriber の起動(action_publisher を move する前に subscribe)
525        let mut subscriber_handles_trace = Vec::new();
526        if let Some(trace_subscriber) = self.trace_subscriber.take() {
527            let rx = action_publisher.subscribe();
528            let handle = self.runtime.spawn(async move {
529                run_trace_subscriber(rx, trace_subscriber).await;
530            });
531            subscriber_handles_trace.push(handle);
532        }
533
534        let LearningSetupResult {
535            daemon_handle,
536            mut subscriber_handles,
537            shutdown_tx,
538        } = if self.config.learning_enabled {
539            self.setup_learning_components(&action_publisher)?
540        } else {
541            LearningSetupResult::default()
542        };
543
544        // TraceSubscriber の handle を追加
545        subscriber_handles.extend(subscriber_handles_trace);
546
547        // ========================================================================
548        // OrchestratorBuilder を構築
549        // ========================================================================
550        // Note: Extensions は self からフィールドを take() するため、
551        //       workers/managers を move する前に呼ぶ必要がある
552        let extensions = self.build_extensions();
553
554        let mut orch_builder = OrchestratorBuilder::new()
555            .config(swarm_config)
556            .extensions(extensions);
557
558        // Workers
559        for worker in self.workers {
560            orch_builder = orch_builder.add_worker_boxed(worker);
561        }
562
563        // Managers
564        for manager in self.managers {
565            orch_builder = orch_builder.add_manager_boxed(manager);
566        }
567
568        // BatchInvoker
569        if let Some(invoker) = self.batch_invoker {
570            orch_builder = orch_builder.batch_invoker_boxed(invoker);
571        }
572
573        // DependencyGraphProvider
574        if let Some(provider) = self.dependency_provider {
575            orch_builder = orch_builder.dependency_provider_boxed(provider);
576        }
577
578        // OperatorProvider
579        if let Some(provider) = self.operator_provider {
580            orch_builder = orch_builder.operator_provider_boxed(provider);
581        }
582
583        // Exploration
584        if self.enable_exploration {
585            orch_builder = orch_builder.with_exploration();
586        }
587
588        // Offline model
589        if let Some(ref model) = self.offline_model {
590            orch_builder = orch_builder.with_offline_model(model.clone());
591        }
592
593        // LifecycleHook
594        if let Some(hook) = self.lifecycle_hook {
595            orch_builder = orch_builder.lifecycle_hook(hook);
596        }
597
598        // ActionEventPublisher を Orchestrator に渡す
599        orch_builder = orch_builder.action_collector(action_publisher);
600
601        // Orchestrator を構築
602        let orchestrator = orch_builder.build(self.runtime.clone());
603
604        Ok(LearnableSwarm {
605            orchestrator,
606            runtime: self.runtime,
607            config: self.config,
608            learning_store: self.learning_store,
609            offline_model: self.offline_model,
610            daemon_handle,
611            subscriber_handles,
612            shutdown_tx,
613        })
614    }
615
616    /// Extensions を構築(DependencyGraph, PriorSnapshot を統合)
617    fn build_extensions(&mut self) -> Extensions {
618        let mut ext = self.extensions.take().unwrap_or_default();
619
620        if let Some(graph) = self.dependency_graph.take() {
621            ext.insert(graph);
622        }
623        if let Some(snapshot) = self.prior_snapshot.take() {
624            ext.insert(snapshot);
625        }
626
627        ext
628    }
629
630    /// Learning 関連コンポーネントをセットアップ
631    fn setup_learning_components(
632        &self,
633        action_publisher: &ActionEventPublisher,
634    ) -> Result<LearningSetupResult, SwarmError> {
635        // LearningDaemon を作成
636        let daemon_config = DaemonConfig::new(&self.config.scenario)
637            .data_dir(&self.config.data_dir)
638            .check_interval(self.config.daemon_check_interval);
639
640        let trigger = self
641            .train_trigger
642            .clone()
643            .unwrap_or_else(|| TriggerBuilder::never());
644
645        let mut daemon =
646            LearningDaemon::new(daemon_config, trigger).map_err(|e| SwarmError::Config {
647                message: format!("Failed to create LearningDaemon: {}", e),
648            })?;
649
650        let record_tx = daemon.record_sender();
651        let shutdown_tx = daemon.shutdown_sender();
652
653        // Subscriber 設定
654        let sub_config = EventSubscriberConfig::new()
655            .batch_size(self.config.subscriber_batch_size)
656            .flush_interval_ms(self.config.subscriber_flush_interval_ms);
657
658        let mut subscriber_handles = Vec::new();
659
660        // ActionEventSubscriber 起動
661        let action_sub = ActionEventSubscriber::with_config(
662            action_publisher.subscribe(),
663            record_tx.clone(),
664            sub_config.clone(),
665        );
666        let action_handle = self.runtime.spawn(async move {
667            action_sub.run().await;
668        });
669        subscriber_handles.push(action_handle);
670
671        // LearningEventSubscriber 起動
672        let learning_channel = LearningEventChannel::global();
673        let learning_sub = LearningEventSubscriber::with_config(
674            learning_channel.subscribe(),
675            record_tx,
676            sub_config,
677        );
678        let learning_handle = self.runtime.spawn(async move {
679            learning_sub.run().await;
680        });
681        subscriber_handles.push(learning_handle);
682
683        // LearningDaemon を起動
684        let daemon_handle = self.runtime.spawn(async move { daemon.run().await });
685
686        Ok(LearningSetupResult {
687            daemon_handle: Some(daemon_handle),
688            subscriber_handles,
689            shutdown_tx: Some(shutdown_tx),
690        })
691    }
692}
693
694// ============================================================================
695// LearnableSwarm
696// ============================================================================
697
698/// 学習機能付き Swarm
699///
700/// Orchestrator + LearningDaemon + Subscriber を統合管理する。
701pub struct LearnableSwarm {
702    /// Orchestrator
703    orchestrator: Orchestrator,
704    /// Runtime handle
705    runtime: Handle,
706    /// 設定
707    config: LearnableSwarmConfig,
708    /// LearningStore
709    learning_store: Option<LearningStore>,
710    /// Offline model
711    offline_model: Option<OfflineModel>,
712    /// LearningDaemon の JoinHandle
713    daemon_handle: Option<DaemonHandle>,
714    /// Subscriber の JoinHandle
715    subscriber_handles: Vec<JoinHandle<()>>,
716    /// Shutdown 送信チャンネル
717    shutdown_tx: Option<mpsc::Sender<()>>,
718}
719
720impl LearnableSwarm {
721    /// タスクを実行
722    pub fn run_task(&mut self, task: SwarmTask) -> Result<SwarmResult, SwarmError> {
723        self.orchestrator.run_task(task)
724    }
725
726    /// メインループを実行(タスクなし)
727    pub fn run(&mut self) -> SwarmResult {
728        self.orchestrator.run()
729    }
730
731    /// Orchestrator への参照を取得
732    pub fn orchestrator(&self) -> &Orchestrator {
733        &self.orchestrator
734    }
735
736    /// Orchestrator への可変参照を取得
737    pub fn orchestrator_mut(&mut self) -> &mut Orchestrator {
738        &mut self.orchestrator
739    }
740
741    /// DependencyGraph への参照を取得
742    pub fn dependency_graph(&self) -> Option<&DependencyGraph> {
743        self.orchestrator.dependency_graph()
744    }
745
746    /// 設定を取得
747    pub fn config(&self) -> &LearnableSwarmConfig {
748        &self.config
749    }
750
751    /// LearningStore を取得
752    pub fn learning_store(&self) -> Option<&LearningStore> {
753        self.learning_store.as_ref()
754    }
755
756    /// Offline model を取得
757    pub fn offline_model(&self) -> Option<&OfflineModel> {
758        self.offline_model.as_ref()
759    }
760
761    /// 学習機能が有効かどうか
762    pub fn is_learning_enabled(&self) -> bool {
763        self.config.learning_enabled
764    }
765
766    /// LearnStatsSnapshot Event を発行
767    ///
768    /// セッション終了時に呼び出し、LearnStats をスナップショットとして記録する。
769    /// Learning が有効な場合、shutdown 前に必ず呼び出すこと。
770    pub fn emit_stats_snapshot(&self) {
771        use crate::events::{LearnStatsOutcome, LearningEvent};
772        use crate::util::epoch_millis;
773
774        let state = self.orchestrator.state();
775        let tick = state.shared.tick;
776        let total_actions = state.shared.stats.total_visits() as u64;
777
778        // LearnStats を JSON シリアライズ
779        let stats_json = if let Some(provider) = self.orchestrator.learned_provider() {
780            provider
781                .stats()
782                .map(|stats| serde_json::to_string(stats).unwrap_or_default())
783                .unwrap_or_default()
784        } else {
785            String::new()
786        };
787
788        // Session ID を生成(timestamp ベース)
789        let session_id = format!("{}", epoch_millis());
790
791        // 結果を判定(environment_done で成功判定)
792        let outcome = if state.shared.environment_done {
793            LearnStatsOutcome::Success { score: 1.0 }
794        } else {
795            LearnStatsOutcome::Timeout {
796                partial_score: None,
797            }
798        };
799
800        // Event を発行
801        let event = LearningEvent::learn_stats_snapshot(&self.config.scenario)
802            .session_id(session_id)
803            .stats_json(stats_json)
804            .total_ticks(tick)
805            .total_actions(total_actions);
806
807        let event = match outcome {
808            LearnStatsOutcome::Success { score } => event.success(score),
809            LearnStatsOutcome::Timeout { partial_score } => event.timeout(partial_score),
810            LearnStatsOutcome::Failure { reason } => event.failure(reason),
811        };
812
813        LearningEventChannel::global().emit(event.build());
814
815        tracing::debug!(
816            scenario = %self.config.scenario,
817            tick = tick,
818            total_actions = total_actions,
819            "LearnStatsSnapshot emitted"
820        );
821    }
822
823    /// Shutdown 送信チャンネルを取得(所有権を移動)
824    ///
825    /// 非同期 shutdown が不可能な環境(tokio runtime 内での block_on)で使用。
826    /// 取得後、`try_send(())` でシャットダウン信号を送信し、Drop で cleanup。
827    pub fn take_shutdown_tx(&mut self) -> Option<mpsc::Sender<()>> {
828        self.shutdown_tx.take()
829    }
830
831    /// Graceful shutdown
832    ///
833    /// LearningDaemon と Subscriber を停止し、完了を待つ。
834    /// 学習が有効な場合、shutdown 前に LearnStatsSnapshot Event を発行する。
835    pub async fn shutdown(self) {
836        // 学習が有効な場合、LearnStatsSnapshot を発行
837        if self.config.learning_enabled {
838            self.emit_stats_snapshot();
839        }
840
841        // Shutdown signal を送信
842        if let Some(tx) = self.shutdown_tx {
843            let _ = tx.send(()).await;
844        }
845
846        // Daemon の終了を待つ
847        if let Some(handle) = self.daemon_handle {
848            match handle.await {
849                Ok(Ok(())) => {
850                    tracing::debug!("LearningDaemon shutdown completed");
851                }
852                Ok(Err(e)) => {
853                    tracing::warn!("LearningDaemon error on shutdown: {}", e);
854                }
855                Err(e) => {
856                    tracing::warn!("LearningDaemon join error: {}", e);
857                }
858            }
859        }
860
861        // Subscriber の終了を待つ
862        for handle in self.subscriber_handles {
863            let _ = handle.await;
864        }
865
866        tracing::debug!("LearnableSwarm shutdown completed");
867    }
868
869    /// 同期的な shutdown(blocking)
870    pub fn shutdown_blocking(self) {
871        let runtime = self.runtime.clone();
872        runtime.block_on(self.shutdown());
873    }
874}
875
876// ============================================================================
877// TraceSubscriber Runner
878// ============================================================================
879
880/// TraceSubscriber を実行するタスク
881async fn run_trace_subscriber(
882    mut rx: tokio::sync::broadcast::Receiver<crate::events::ActionEvent>,
883    subscriber: Arc<dyn TraceSubscriber>,
884) {
885    while let Ok(event) = rx.recv().await {
886        subscriber.on_event(&event);
887    }
888    subscriber.finish();
889}
890
891// ============================================================================
892// Tests
893// ============================================================================
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898    use crate::agent::GenericWorker;
899
900    fn make_test_runtime() -> tokio::runtime::Runtime {
901        tokio::runtime::Builder::new_current_thread()
902            .enable_all()
903            .build()
904            .unwrap()
905    }
906
907    #[test]
908    fn test_config_default() {
909        let config = LearnableSwarmConfig::default();
910        assert!(!config.learning_enabled);
911        assert!(config.scenario.is_empty());
912    }
913
914    #[test]
915    fn test_config_builder() {
916        let config = LearnableSwarmConfig::new("test-scenario")
917            .with_learning(true)
918            .data_dir("/tmp/test");
919
920        assert_eq!(config.scenario, "test-scenario");
921        assert!(config.learning_enabled);
922        assert_eq!(config.data_dir, PathBuf::from("/tmp/test"));
923    }
924
925    #[test]
926    fn test_builder_basic() {
927        let rt = make_test_runtime();
928
929        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
930            .scenario("test")
931            .add_worker(Box::new(GenericWorker::new(0)));
932
933        assert_eq!(builder.config.scenario, "test");
934        assert_eq!(builder.workers.len(), 1);
935    }
936
937    #[test]
938    fn test_builder_with_learning() {
939        let rt = make_test_runtime();
940
941        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
942            .scenario("test")
943            .with_learning(true)
944            .add_worker(Box::new(GenericWorker::new(0)));
945
946        assert!(builder.config.learning_enabled);
947    }
948
949    #[test]
950    fn test_builder_learning_without_scenario_fails() {
951        let rt = make_test_runtime();
952
953        let result = LearnableSwarmBuilder::new(rt.handle().clone())
954            .with_learning(true)
955            .add_worker(Box::new(GenericWorker::new(0)))
956            .build();
957
958        assert!(result.is_err());
959        if let Err(err) = result {
960            assert!(err.to_string().contains("scenario is required"));
961        }
962    }
963
964    #[test]
965    fn test_builder_learning_disabled_without_scenario_ok() {
966        let rt = make_test_runtime();
967
968        // learning_enabled=false の場合、scenario が空でも OK
969        let result = LearnableSwarmBuilder::new(rt.handle().clone())
970            .add_worker(Box::new(GenericWorker::new(0)))
971            .build();
972
973        assert!(result.is_ok());
974    }
975
976    #[test]
977    fn test_builder_with_scenario_profile() {
978        use crate::learn::learned_component::LearnedExploration;
979        use crate::learn::scenario_profile::{ScenarioProfile, ScenarioSource};
980
981        let rt = make_test_runtime();
982
983        // Create a profile with exploration data
984        let mut profile =
985            ScenarioProfile::new("test-profile", ScenarioSource::from_path("/test.toml"));
986        profile.exploration = Some(LearnedExploration::new(2.5, 0.4, 1.2));
987
988        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
989            .with_scenario_profile(&profile)
990            .add_worker(Box::new(GenericWorker::new(0)));
991
992        // Scenario should be auto-set from profile
993        assert_eq!(builder.config.scenario, "test-profile");
994        // OfflineModel should be set
995        assert!(builder.offline_model.is_some());
996        let model = builder.offline_model.as_ref().unwrap();
997        assert_eq!(model.parameters.ucb1_c, 2.5);
998    }
999}