swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Analyzer - SwarmState から TaskContext を生成
//!
//! # 設計
//!
//! ```text
//! SwarmState → [Analyzer] → TaskContext
//!//!                  ├─ 自動処理(メイン)
//!                  └─ 軽量LLM Single呼び出し(オプション)
//! ```
//!
//! Analyzer は SwarmState を分析して TaskContext を生成する。
//! デフォルト実装 (DefaultAnalyzer) は自動処理のみ。
//! カスタム実装で軽量LLMを使った分析を追加可能。

use std::collections::{HashMap, HashSet};

use crate::actions::ActionsConfig;
use crate::orchestrator::SwarmConfig;
use crate::state::SwarmState;
use crate::types::{SwarmTask, WorkerId};

use crate::context::{TaskContext, WorkerSummary};

// ============================================================================
// Analyzer Trait
// ============================================================================

/// 状況分析器: SwarmState → TaskContext
///
/// # 責務
///
/// - SwarmState から Worker の状態を抽出
/// - 成功率・進捗を計算
/// - Escalation を収集
/// - 利用可能なアクションを取得
/// - オプションで軽量LLMを使った追加分析
///
/// # 実装例
///
/// ```ignore
/// struct MyAnalyzer;
///
/// impl Analyzer for MyAnalyzer {
///     fn analyze(&self, state: &SwarmState) -> TaskContext {
///         let mut ctx = DefaultAnalyzer::new().analyze(state);
///
///         // 軽量LLMで追加分析
///         let summary = call_lightweight_llm(&ctx);
///         ctx.set("llm_summary", summary);
///
///         ctx
///     }
/// }
/// ```
pub trait Analyzer: Send + Sync {
    /// SwarmState を分析して TaskContext を生成
    fn analyze(&self, state: &SwarmState) -> TaskContext;

    /// 名前(デバッグ/ログ用)
    fn name(&self) -> &str {
        "Analyzer"
    }
}

// ============================================================================
// DefaultAnalyzer
// ============================================================================

/// デフォルトの Analyzer 実装
///
/// SwarmState から自動的に TaskContext を生成する。
/// 軽量LLM呼び出しは行わない(純粋な自動処理)。
#[derive(Debug, Clone, Default)]
pub struct DefaultAnalyzer {
    /// 名前
    name: String,
}

impl DefaultAnalyzer {
    pub fn new() -> Self {
        Self {
            name: "DefaultAnalyzer".to_string(),
        }
    }

    /// 名前を設定
    pub fn with_name(mut self, name: impl Into<String>) -> Self {
        self.name = name.into();
        self
    }

    /// 成功率を計算
    fn calculate_success_rate(state: &SwarmState) -> f64 {
        state.shared.stats.success_rate()
    }

    /// 進捗を計算(仮実装:成功アクション数 / 最大Tick で近似)
    fn calculate_progress(state: &SwarmState, max_ticks: u64) -> f64 {
        if max_ticks == 0 {
            // max_ticks が設定されていない場合は成功率で代用
            return Self::calculate_success_rate(state);
        }

        (state.shared.tick as f64 / max_ticks as f64).min(1.0)
    }
}

impl Analyzer for DefaultAnalyzer {
    fn analyze(&self, state: &SwarmState) -> TaskContext {
        let tick = state.shared.tick;
        let mut workers = HashMap::new();
        let mut escalations = Vec::new();

        // 各 Worker の状態を収集
        for (idx, ctx) in state.workers.iter().enumerate() {
            let worker_id = WorkerId(idx);
            let has_escalation = ctx.escalation.is_some();

            // Escalation 収集
            if let Some(esc) = &ctx.escalation {
                escalations.push((worker_id, esc.clone()));
            }

            // 最新履歴を取得
            let last_entry = ctx.history.latest();
            let (last_action, last_success) = last_entry
                .map(|e| (Some(e.action_name.clone()), Some(e.success)))
                .unwrap_or((None, None));

            // WorkerSummary を構築
            let summary = WorkerSummary {
                id: worker_id,
                consecutive_failures: ctx.consecutive_failures,
                last_action,
                last_success,
                last_output: ctx.last_output.clone(),
                history_len: ctx.history.len(),
                has_escalation,
            };

            workers.insert(worker_id, summary);
        }

        // 成功率・進捗を計算
        let success_rate = Self::calculate_success_rate(state);
        let max_ticks = state
            .shared
            .extensions
            .get::<SwarmConfig>()
            .map(|c| c.max_ticks)
            .unwrap_or(0);
        let progress = Self::calculate_progress(state, max_ticks);

        // ActionsConfig を取得
        let available_actions = state.shared.extensions.get::<ActionsConfig>().cloned();

        // メタデータを構築
        let mut metadata = HashMap::new();

        // SwarmTask から情報を抽出
        if let Some(task) = state.shared.extensions.get::<SwarmTask>() {
            metadata.insert(
                "task".to_string(),
                serde_json::Value::String(task.goal.clone()),
            );

            // SwarmTask の context をコピー(オブジェクトの場合)
            if let Some(obj) = task.context.as_object() {
                for (key, value) in obj {
                    metadata.insert(format!("task_{}", key), value.clone());
                }
            }
        }

        TaskContext {
            tick,
            workers,
            success_rate,
            progress,
            escalations,
            available_actions,
            v2_guidances: None,                 // Orchestrator が後から設定
            excluded_actions: Vec::new(),       // Orchestrator が後から設定
            previous_guidances: HashMap::new(), // Orchestrator が後から設定
            done_workers: HashSet::new(),       // RuntimeState から設定される
            metadata,
        }
    }

    fn name(&self) -> &str {
        &self.name
    }
}

// ============================================================================
// Blanket Implementation for Box
// ============================================================================

impl Analyzer for Box<dyn Analyzer> {
    fn analyze(&self, state: &SwarmState) -> TaskContext {
        (**self).analyze(state)
    }

    fn name(&self) -> &str {
        (**self).name()
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_analyzer_empty_state() {
        let analyzer = DefaultAnalyzer::new();
        let state = SwarmState::new(3);

        let ctx = analyzer.analyze(&state);

        assert_eq!(ctx.tick, 0);
        assert_eq!(ctx.workers.len(), 3);
        // アクションがない場合、success_rate は 1.0(失敗していない)
        assert_eq!(ctx.success_rate, 1.0);
        assert!(!ctx.has_escalations());
    }

    #[test]
    fn test_default_analyzer_with_name() {
        let analyzer = DefaultAnalyzer::new().with_name("TestAnalyzer");
        assert_eq!(analyzer.name(), "TestAnalyzer");
    }
}