swarm-engine-eval 0.1.6

Evaluation framework for SwarmEngine
Documentation
//! Runtime Task Specification
//!
//! CLI の `run` コマンド用のタスク仕様。
//! EvalScenario から継承し、Task だけ上書き可能。
//!
//! # Example
//!
//! ```ignore
//! use swarm_engine_eval::runtime::RuntimeTaskSpec;
//! use swarm_engine_eval::scenario::EvalScenario;
//!
//! // シナリオファイルから読み込み
//! let scenario: EvalScenario = toml::from_str(&content)?;
//!
//! // RuntimeTaskSpec に変換し、タスクを上書き
//! let spec = RuntimeTaskSpec::from(scenario)
//!     .with_task("Fix the authentication bug in src/auth.rs");
//! ```

use crate::scenario::{
    AgentsConfig, AppConfigTemplate, EnvironmentConfig, EvalConditions, EvalScenario, LlmConfig,
    ManagerConfig, ScenarioActions, ScenarioMeta, TaskConfig,
};
use std::path::PathBuf;

/// Runtime Task Specification
///
/// CLI から直接タスクを指定して実行するための仕様。
/// EvalScenario をベースに、Task 部分のみ上書き可能。
#[derive(Debug, Clone)]
pub struct RuntimeTaskSpec {
    /// シナリオメタ情報
    pub meta: ScenarioMeta,

    /// タスク定義(上書き可能)
    pub task: TaskConfig,

    /// LLM 設定
    pub llm: LlmConfig,

    /// Manager 設定
    pub manager: ManagerConfig,

    /// アクション定義
    pub actions: ScenarioActions,

    /// アプリ設定
    pub app_config: AppConfigTemplate,

    /// 環境設定(上書き可能)
    pub environment: EnvironmentConfig,

    /// エージェント設定
    pub agents: AgentsConfig,

    /// 成功/失敗条件
    pub conditions: EvalConditions,

    /// 作業ディレクトリ(オプション)
    pub working_dir: Option<PathBuf>,
}

impl RuntimeTaskSpec {
    /// タスクの goal を上書き
    pub fn with_task(mut self, goal: impl Into<String>) -> Self {
        self.task.goal = goal.into();
        self
    }

    /// Environment の env_type を上書き
    pub fn with_env_type(mut self, env_type: impl Into<String>) -> Self {
        self.environment.env_type = env_type.into();
        self
    }

    /// 作業ディレクトリを設定
    pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.working_dir = Some(dir.into());
        self
    }

    /// max_ticks を上書き
    pub fn with_max_ticks(mut self, max_ticks: u64) -> Self {
        self.app_config.max_ticks = max_ticks;
        self
    }

    /// EvalScenario に変換(Orchestrator 実行用)
    ///
    /// RuntimeTaskSpec から EvalScenario を再構築する。
    /// 評価用の条件やマイルストーンは空になる。
    ///
    /// Note: `working_dir` が設定されている場合、`environment.params["working_dir"]` に伝播される。
    pub fn into_eval_scenario(mut self) -> EvalScenario {
        // Propagate working_dir to environment.params for DefaultEnvironment
        if let Some(ref dir) = self.working_dir {
            let params = self.environment.params.as_object_mut();
            if let Some(obj) = params {
                obj.insert(
                    "working_dir".to_string(),
                    serde_json::Value::String(dir.display().to_string()),
                );
            } else {
                // params が Object でない場合は新規作成
                let mut obj = serde_json::Map::new();
                obj.insert(
                    "working_dir".to_string(),
                    serde_json::Value::String(dir.display().to_string()),
                );
                self.environment.params = serde_json::Value::Object(obj);
            }
        }

        EvalScenario {
            meta: self.meta,
            task: self.task,
            llm: self.llm,
            manager: self.manager,
            batch_processor: Default::default(),
            dependency_graph: None,
            actions: self.actions,
            app_config: self.app_config,
            environment: self.environment,
            agents: self.agents,
            conditions: self.conditions,
            milestones: vec![],
            variants: vec![],
        }
    }

    /// 作業ディレクトリを取得(未設定の場合は cwd)
    pub fn resolved_working_dir(&self) -> PathBuf {
        self.working_dir
            .clone()
            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
    }
}

impl From<EvalScenario> for RuntimeTaskSpec {
    fn from(scenario: EvalScenario) -> Self {
        Self {
            meta: scenario.meta,
            task: scenario.task,
            llm: scenario.llm,
            manager: scenario.manager,
            actions: scenario.actions,
            app_config: scenario.app_config,
            environment: scenario.environment,
            agents: scenario.agents,
            conditions: scenario.conditions,
            working_dir: None,
        }
    }
}

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

    fn minimal_scenario() -> EvalScenario {
        let toml_str = r#"
            [meta]
            name = "Test"
            id = "test:minimal:v1"

            [task]
            goal = "Original goal"

            [llm]
            provider = "llama-server"
            model = "test-model"

            [app_config]
            max_ticks = 100

            [environment]
            env_type = "default"

            [agents]
            [[agents.workers]]
            id_pattern = "worker_{i}"
            count = 1

            [conditions]
            on_timeout = "fail"
        "#;
        toml::from_str(toml_str).unwrap()
    }

    #[test]
    fn test_from_eval_scenario() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario);

        assert_eq!(spec.task.goal, "Original goal");
        assert_eq!(spec.environment.env_type, "default");
    }

    #[test]
    fn test_with_task() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario).with_task("New custom goal");

        assert_eq!(spec.task.goal, "New custom goal");
    }

    #[test]
    fn test_with_env_type() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario).with_env_type("realworld");

        assert_eq!(spec.environment.env_type, "realworld");
    }

    #[test]
    fn test_into_eval_scenario() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario)
            .with_task("Modified goal")
            .with_max_ticks(50);

        let converted = spec.into_eval_scenario();

        assert_eq!(converted.task.goal, "Modified goal");
        assert_eq!(converted.app_config.max_ticks, 50);
        assert!(converted.milestones.is_empty());
        assert!(converted.variants.is_empty());
    }

    #[test]
    fn test_working_dir_propagates_to_env_params() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario).with_working_dir("/tmp/test_workspace");

        let converted = spec.into_eval_scenario();

        // working_dir should be propagated to environment.params
        let params = converted.environment.params.as_object().unwrap();
        assert_eq!(
            params.get("working_dir").and_then(|v| v.as_str()),
            Some("/tmp/test_workspace")
        );
    }

    #[test]
    fn test_working_dir_without_override() {
        let scenario = minimal_scenario();
        let spec = RuntimeTaskSpec::from(scenario);

        // No working_dir set
        assert!(spec.working_dir.is_none());

        let converted = spec.into_eval_scenario();

        // params should not have working_dir
        let params = converted.environment.params.as_object();
        assert!(params.is_none() || params.unwrap().get("working_dir").is_none());
    }
}