swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! ライフサイクル管理
//!
//! - validate(): 設定検証
//! - ensure_exploration_space(): ExplorationSpace の自動生成
//! - should_terminate(): 終了判定

use std::time::Instant;

use tracing::{debug, info, warn};

use crate::actions::ActionDef;
use crate::error::SwarmError;
use crate::events::{ActionContext, ActionEventBuilder, ActionEventResult};
use crate::exploration::{ExplorationSpaceV2, NodeRules};
use crate::types::WorkerId;

use super::Orchestrator;

impl Orchestrator {
    /// Orchestrator の設定を検証
    ///
    /// run_task() を呼ぶ前にこのメソッドで設定を検証できます。
    /// run_task() 内でも同様のチェックが行われますが、事前に検証したい場合に使用します。
    ///
    /// # Errors
    ///
    /// - `SwarmError::NoWorkers`: Worker が設定されていない
    /// - `SwarmError::MissingDependencyGraph`: DependencyGraph のソースが設定されていない
    ///
    /// # Example
    ///
    /// ```ignore
    /// let orchestrator = OrchestratorBuilder::new()
    ///     .add_worker(worker)
    ///     .batch_invoker(invoker)
    ///     .build(runtime);
    ///
    /// // 事前検証
    /// orchestrator.validate()?;
    ///
    /// // タスク実行
    /// let result = orchestrator.run_task(task)?;
    /// ```
    pub fn validate(&self) -> Result<(), SwarmError> {
        // Worker チェック
        if self.workers.is_empty() {
            return Err(SwarmError::NoWorkers);
        }

        // ============================================================
        // DependencyGraph ソースチェック(自動判別対応)
        // ============================================================
        // DependencyGraph は必須だが、以下のいずれかがあれば**自動生成**される:
        //
        // 1. dependency_provider - 明示的プロバイダー(最優先)
        // 2. batch_invoker - LLM で plan_dependencies() を呼び出して生成
        // 3. Extensions に DependencyGraph が登録済み
        // 4. space_v2 が既に設定されている
        //
        // シナリオファイルで dependency_graph を明示的に設定する必要は通常ない。
        // batch_invoker があれば LLM が自動生成を試みる。
        // ============================================================
        let has_dependency_graph = self
            .state
            .shared
            .extensions
            .get::<crate::exploration::DependencyGraph>()
            .is_some();

        if !has_dependency_graph
            && self.space_v2.is_none()
            && self.dependency_provider.is_none()
            && self.batch_invoker.is_none()
        {
            return Err(SwarmError::MissingDependencyGraph {
                hint: "No DependencyGraph source configured. \
                       Use batch_invoker() with LLM that supports plan_dependencies(), \
                       or dependency_provider() with custom provider, \
                       or extension(DependencyGraph) for static graph."
                    .to_string(),
            });
        }

        Ok(())
    }

    /// ExplorationSpaceV2 がなければ DependencyGraph を自動生成
    ///
    /// ## 自動判別の優先順位
    ///
    /// 以下の順序で DependencyGraph を取得を試みる:
    ///
    /// 1. **DependencyGraphProvider** - 明示的に設定されたプロバイダー
    /// 2. **BatchInvoker.plan_dependencies()** - LLM による自動生成
    /// 3. エラー(DependencyGraph は必須)
    ///
    /// ## シナリオでの明示的設定は不要
    ///
    /// シナリオファイル(TOML)で `dependency_graph` を設定しなくても、
    /// `batch_invoker` が設定されていれば LLM が自動生成を試みる。
    ///
    /// ## 失敗するケース
    ///
    /// - LLM がタスクに適した依存関係を推論できない場合
    /// - LLM のレスポンスパースに失敗した場合
    /// - 生成されたグラフのバリデーションに失敗した場合
    ///
    /// これらの場合はシナリオファイルで明示的に設定する必要がある。
    ///
    /// # Errors
    ///
    /// - `SwarmError::MissingDependencyGraph`: DependencyGraph を生成できなかった
    pub(super) fn ensure_exploration_space(
        &mut self,
        task_goal: &str,
        actions: &[ActionDef],
    ) -> Result<(), SwarmError> {
        // 既に space_v2 がある場合は OK
        if self.space_v2.is_some() {
            return Ok(());
        }

        // アクションがない場合もスキップ(後続で別のエラーになる)
        if actions.is_empty() {
            debug!("No available actions, skipping ExplorationSpaceV2 generation");
            return Ok(());
        }

        // DependencyGraphProvider 用にアクション名リストを作成
        let action_names: Vec<String> = actions.iter().map(|a| a.name.clone()).collect();

        // 1. DependencyGraphProvider があれば使用
        if let Some(provider) = &self.dependency_provider {
            info!(
                task = %task_goal,
                actions = ?action_names,
                "Auto-generating DependencyGraph via Provider"
            );

            let start_time = Instant::now();
            let graph_opt = provider.provide_graph(task_goal, &action_names);
            let elapsed = start_time.elapsed();

            // 学習記録: dependency_provider イベント
            let (result, success) = if graph_opt.is_some() {
                (ActionEventResult::success(), true)
            } else {
                (ActionEventResult::failure("provider_returned_none"), false)
            };
            let event = ActionEventBuilder::new(0, WorkerId::MANAGER, "dependency_provider")
                .duration(elapsed)
                .result(result)
                .context(
                    ActionContext::new()
                        .with_metadata("task", task_goal.to_string())
                        .with_metadata("action_count", action_names.len().to_string()),
                )
                .build();
            self.state.shared.stats.record(&event);
            if let Some(ref collector) = self.action_collector {
                collector.record(event);
            }

            if let Some(graph) = graph_opt {
                info!(
                    start = ?graph.start_actions(),
                    terminal = ?graph.terminal_actions(),
                    edges = graph.edges().len(),
                    "DependencyGraph auto-generated via Provider"
                );

                // V2: NodeRules に変換して OperatorProvider 経由で Operator を作成
                let rules: NodeRules = (&graph).into();
                let operator = self.operator_provider.provide(rules, None);
                self.space_v2 =
                    Some(ExplorationSpaceV2::new(operator).with_dependency_graph(graph));
                return Ok(());
            } else if !success {
                warn!("DependencyGraphProvider returned None");
            }
        }

        // 2. BatchInvoker があればフォールバックとして使用
        if let Some(invoker) = &self.batch_invoker {
            // DependencyGraphProvider から SelectResult を取得(ヒントとして使用)
            let select_hint = self
                .dependency_provider
                .as_ref()
                .and_then(|p| p.select(task_goal, &action_names));

            info!(
                task = %task_goal,
                actions = ?action_names,
                has_hint = select_hint.is_some(),
                "Auto-generating DependencyGraph via BatchInvoker fallback"
            );

            let start_time = Instant::now();
            let graph_opt = invoker.plan_dependencies(task_goal, actions, select_hint.as_ref());
            let elapsed = start_time.elapsed();

            // 学習記録: llm_plan_deps イベント
            let (result, success) = if graph_opt.is_some() {
                (ActionEventResult::success(), true)
            } else {
                (ActionEventResult::failure("llm_returned_none"), false)
            };
            let event = ActionEventBuilder::new(0, WorkerId::MANAGER, "llm_plan_deps")
                .duration(elapsed)
                .result(result)
                .context(
                    ActionContext::new()
                        .with_metadata("task", task_goal.to_string())
                        .with_metadata("action_count", action_names.len().to_string())
                        .with_metadata("invoker", invoker.name().to_string()),
                )
                .build();
            self.state.shared.stats.record(&event);
            if let Some(ref collector) = self.action_collector {
                collector.record(event);
            }

            if let Some(graph) = graph_opt {
                info!(
                    start = ?graph.start_actions(),
                    terminal = ?graph.terminal_actions(),
                    edges = graph.edges().len(),
                    "DependencyGraph auto-generated via BatchInvoker"
                );

                // V2: NodeRules に変換して OperatorProvider 経由で Operator を作成
                let rules: NodeRules = (&graph).into();
                let operator = self.operator_provider.provide(rules, None);
                self.space_v2 =
                    Some(ExplorationSpaceV2::new(operator).with_dependency_graph(graph));
                return Ok(());
            } else if !success {
                warn!("BatchInvoker.plan_dependencies() returned None");
            }
        }

        // どちらも失敗した場合はエラー
        Err(SwarmError::MissingDependencyGraph {
            hint: format!(
                "Failed to generate DependencyGraph for task '{}' with actions {:?}. \
                 Ensure your LLM backend supports plan_dependencies(), \
                 or provide a custom DependencyGraphProvider, \
                 or register a static DependencyGraph via extension().",
                task_goal, action_names
            ),
        })
    }

    /// 終了判定
    ///
    /// TerminationJudge に委譲。ExplorationSpace の完了も judge 経由で判定。
    pub(super) fn should_terminate(&mut self) -> bool {
        // Check ExplorationSpaceV2 and notify judge if exhausted
        if let Some(ref space_v2) = self.space_v2 {
            if space_v2.is_complete()
                && !self
                    .termination_judge
                    .completion_state()
                    .is_exploration_done()
            {
                let exhausted = space_v2.is_exhausted();
                if space_v2.has_completed() {
                    info!("ExplorationSpaceV2: marked as completed, notifying judge");
                } else {
                    info!("ExplorationSpaceV2: exhausted (Strategy判定), notifying judge");
                }
                self.termination_judge
                    .notify_exploration_complete(exhausted);
            }
        }

        // Delegate to TerminationJudge
        self.termination_judge.should_terminate()
    }
}