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: 1000,
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    fn emit_stats_snapshot(&self) {
770        use crate::events::{LearnStatsOutcome, LearningEvent};
771        use crate::util::epoch_millis;
772
773        let state = self.orchestrator.state();
774        let tick = state.shared.tick;
775        let total_actions = state.shared.stats.total_visits() as u64;
776
777        // LearnStats を JSON シリアライズ
778        let stats_json = if let Some(provider) = self.orchestrator.learned_provider() {
779            provider
780                .stats()
781                .map(|stats| serde_json::to_string(stats).unwrap_or_default())
782                .unwrap_or_default()
783        } else {
784            String::new()
785        };
786
787        // Session ID を生成(timestamp ベース)
788        let session_id = format!("{}", epoch_millis());
789
790        // 結果を判定(environment_done で成功判定)
791        let outcome = if state.shared.environment_done {
792            LearnStatsOutcome::Success { score: 1.0 }
793        } else {
794            LearnStatsOutcome::Timeout {
795                partial_score: None,
796            }
797        };
798
799        // Event を発行
800        let event = LearningEvent::learn_stats_snapshot(&self.config.scenario)
801            .session_id(session_id)
802            .stats_json(stats_json)
803            .total_ticks(tick)
804            .total_actions(total_actions);
805
806        let event = match outcome {
807            LearnStatsOutcome::Success { score } => event.success(score),
808            LearnStatsOutcome::Timeout { partial_score } => event.timeout(partial_score),
809            LearnStatsOutcome::Failure { reason } => event.failure(reason),
810        };
811
812        LearningEventChannel::global().emit(event.build());
813
814        tracing::debug!(
815            scenario = %self.config.scenario,
816            tick = tick,
817            total_actions = total_actions,
818            "LearnStatsSnapshot emitted"
819        );
820    }
821
822    /// Shutdown 送信チャンネルを取得(所有権を移動)
823    ///
824    /// 非同期 shutdown が不可能な環境(tokio runtime 内での block_on)で使用。
825    /// 取得後、`try_send(())` でシャットダウン信号を送信し、Drop で cleanup。
826    pub fn take_shutdown_tx(&mut self) -> Option<mpsc::Sender<()>> {
827        self.shutdown_tx.take()
828    }
829
830    /// Graceful shutdown
831    ///
832    /// LearningDaemon と Subscriber を停止し、完了を待つ。
833    /// 学習が有効な場合、shutdown 前に LearnStatsSnapshot Event を発行する。
834    pub async fn shutdown(self) {
835        // 学習が有効な場合、LearnStatsSnapshot を発行
836        if self.config.learning_enabled {
837            self.emit_stats_snapshot();
838        }
839
840        // Shutdown signal を送信
841        if let Some(tx) = self.shutdown_tx {
842            let _ = tx.send(()).await;
843        }
844
845        // Daemon の終了を待つ
846        if let Some(handle) = self.daemon_handle {
847            match handle.await {
848                Ok(Ok(())) => {
849                    tracing::debug!("LearningDaemon shutdown completed");
850                }
851                Ok(Err(e)) => {
852                    tracing::warn!("LearningDaemon error on shutdown: {}", e);
853                }
854                Err(e) => {
855                    tracing::warn!("LearningDaemon join error: {}", e);
856                }
857            }
858        }
859
860        // Subscriber の終了を待つ
861        for handle in self.subscriber_handles {
862            let _ = handle.await;
863        }
864
865        tracing::debug!("LearnableSwarm shutdown completed");
866    }
867
868    /// 同期的な shutdown(blocking)
869    pub fn shutdown_blocking(self) {
870        let runtime = self.runtime.clone();
871        runtime.block_on(self.shutdown());
872    }
873}
874
875// ============================================================================
876// TraceSubscriber Runner
877// ============================================================================
878
879/// TraceSubscriber を実行するタスク
880async fn run_trace_subscriber(
881    mut rx: tokio::sync::broadcast::Receiver<crate::events::ActionEvent>,
882    subscriber: Arc<dyn TraceSubscriber>,
883) {
884    while let Ok(event) = rx.recv().await {
885        subscriber.on_event(&event);
886    }
887    subscriber.finish();
888}
889
890// ============================================================================
891// Tests
892// ============================================================================
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::agent::GenericWorker;
898
899    fn make_test_runtime() -> tokio::runtime::Runtime {
900        tokio::runtime::Builder::new_current_thread()
901            .enable_all()
902            .build()
903            .unwrap()
904    }
905
906    #[test]
907    fn test_config_default() {
908        let config = LearnableSwarmConfig::default();
909        assert!(!config.learning_enabled);
910        assert!(config.scenario.is_empty());
911    }
912
913    #[test]
914    fn test_config_builder() {
915        let config = LearnableSwarmConfig::new("test-scenario")
916            .with_learning(true)
917            .data_dir("/tmp/test");
918
919        assert_eq!(config.scenario, "test-scenario");
920        assert!(config.learning_enabled);
921        assert_eq!(config.data_dir, PathBuf::from("/tmp/test"));
922    }
923
924    #[test]
925    fn test_builder_basic() {
926        let rt = make_test_runtime();
927
928        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
929            .scenario("test")
930            .add_worker(Box::new(GenericWorker::new(0)));
931
932        assert_eq!(builder.config.scenario, "test");
933        assert_eq!(builder.workers.len(), 1);
934    }
935
936    #[test]
937    fn test_builder_with_learning() {
938        let rt = make_test_runtime();
939
940        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
941            .scenario("test")
942            .with_learning(true)
943            .add_worker(Box::new(GenericWorker::new(0)));
944
945        assert!(builder.config.learning_enabled);
946    }
947
948    #[test]
949    fn test_builder_learning_without_scenario_fails() {
950        let rt = make_test_runtime();
951
952        let result = LearnableSwarmBuilder::new(rt.handle().clone())
953            .with_learning(true)
954            .add_worker(Box::new(GenericWorker::new(0)))
955            .build();
956
957        assert!(result.is_err());
958        if let Err(err) = result {
959            assert!(err.to_string().contains("scenario is required"));
960        }
961    }
962
963    #[test]
964    fn test_builder_learning_disabled_without_scenario_ok() {
965        let rt = make_test_runtime();
966
967        // learning_enabled=false の場合、scenario が空でも OK
968        let result = LearnableSwarmBuilder::new(rt.handle().clone())
969            .add_worker(Box::new(GenericWorker::new(0)))
970            .build();
971
972        assert!(result.is_ok());
973    }
974
975    #[test]
976    fn test_builder_with_scenario_profile() {
977        use crate::learn::learned_component::LearnedExploration;
978        use crate::learn::scenario_profile::{ScenarioProfile, ScenarioSource};
979
980        let rt = make_test_runtime();
981
982        // Create a profile with exploration data
983        let mut profile =
984            ScenarioProfile::new("test-profile", ScenarioSource::from_path("/test.toml"));
985        profile.exploration = Some(LearnedExploration::new(2.5, 0.4, 1.2));
986
987        let builder = LearnableSwarmBuilder::new(rt.handle().clone())
988            .with_scenario_profile(&profile)
989            .add_worker(Box::new(GenericWorker::new(0)));
990
991        // Scenario should be auto-set from profile
992        assert_eq!(builder.config.scenario, "test-profile");
993        // OfflineModel should be set
994        assert!(builder.offline_model.is_some());
995        let model = builder.offline_model.as_ref().unwrap();
996        assert_eq!(model.parameters.ucb1_c, 2.5);
997    }
998}