swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Action Event - 行動イベントの定義
//!
//! Worker が実行した行動を記録するイベント構造体。
//! オンライン統計・学習・永続化の基盤となる。

use std::time::Duration;

use crate::types::{GroupId, LoraConfig, TaskId, WorkerId};

/// 行動イベント
///
/// Worker が実行した行動の完全な記録。
/// オンライン統計や学習パイプラインに使用される。
#[derive(Debug, Clone)]
pub struct ActionEvent {
    /// イベントID(ユニーク)
    pub id: ActionEventId,
    /// タスク ID(どのタスクの一部か)
    pub task_id: TaskId,
    /// グループ ID(同条件の複数試行をグループ化、Learn 用)
    pub group_id: Option<GroupId>,
    /// 発生した Tick
    pub tick: u64,
    /// 実行した Worker
    pub worker_id: WorkerId,
    /// アクション名
    pub action: String,
    /// ターゲット(オプション)
    pub target: Option<String>,
    /// 実行結果
    pub result: ActionEventResult,
    /// 実行時間
    pub duration: Duration,
    /// コンテキスト情報
    pub context: ActionContext,
}

/// イベントID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ActionEventId(pub u64);

impl ActionEventId {
    /// 新しいIDを生成(tick + worker_id + sequence でユニーク性を保証)
    pub fn new(tick: u64, worker_id: WorkerId, sequence: u32) -> Self {
        // tick: 40bit, worker_id: 16bit, sequence: 8bit = 64bit
        let id = (tick << 24) | ((worker_id.0 as u64) << 8) | (sequence as u64 & 0xFF);
        Self(id)
    }
}

/// 行動結果
#[derive(Debug, Clone)]
pub struct ActionEventResult {
    /// 成功/失敗
    pub success: bool,
    /// 出力(テキスト表現)
    pub output: Option<String>,
    /// エラーメッセージ
    pub error: Option<String>,
    /// 発見したノード数(探索の場合)
    pub discoveries: u32,
    /// KPI貢献度(目標関数への寄与、0.0〜1.0 または負値も可)
    ///
    /// Environment が「このアクションは目標にどれだけ近づいたか」を
    /// スコアとして返す。学習時の報酬関数として使用可能。
    pub kpi_contribution: Option<f64>,
}

impl ActionEventResult {
    pub fn success() -> Self {
        Self {
            success: true,
            output: None,
            error: None,
            discoveries: 0,
            kpi_contribution: None,
        }
    }

    pub fn success_with_output(output: impl Into<String>) -> Self {
        Self {
            success: true,
            output: Some(output.into()),
            error: None,
            discoveries: 0,
            kpi_contribution: None,
        }
    }

    pub fn failure(error: impl Into<String>) -> Self {
        Self {
            success: false,
            output: None,
            error: Some(error.into()),
            discoveries: 0,
            kpi_contribution: None,
        }
    }

    pub fn with_discoveries(mut self, count: u32) -> Self {
        self.discoveries = count;
        self
    }

    /// KPI貢献度を設定
    ///
    /// 目標関数への寄与を設定する。
    /// - 正値: 目標に近づいた
    /// - 0: 変化なし
    /// - 負値: 目標から遠ざかった
    pub fn with_kpi(mut self, contribution: f64) -> Self {
        self.kpi_contribution = Some(contribution);
        self
    }
}

/// 行動のコンテキスト情報
///
/// なぜその行動が選択されたかの情報を保持。
/// 学習時の特徴量として使用可能。
#[derive(Debug, Clone, Default)]
pub struct ActionContext {
    /// 選択に使用されたロジック(UCB1, Thompson, Greedy 等)
    pub selection_logic: Option<String>,
    /// 探索空間でのノードID
    pub exploration_node_id: Option<u64>,
    /// Guidance からの指示だったか
    pub from_guidance: bool,
    /// 前回の行動(シーケンス分析用)
    pub previous_action: Option<String>,
    /// 使用した LoRA アダプター設定
    pub lora: Option<LoraConfig>,
    /// 追加のメタデータ
    pub metadata: std::collections::HashMap<String, String>,
}

impl ActionContext {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_selection_logic(mut self, logic: impl Into<String>) -> Self {
        self.selection_logic = Some(logic.into());
        self
    }

    pub fn with_exploration_node(mut self, node_id: u64) -> Self {
        self.exploration_node_id = Some(node_id);
        self
    }

    pub fn with_guidance(mut self) -> Self {
        self.from_guidance = true;
        self
    }

    pub fn with_previous_action(mut self, action: impl Into<String>) -> Self {
        self.previous_action = Some(action.into());
        self
    }

    pub fn with_lora(mut self, lora: LoraConfig) -> Self {
        self.lora = Some(lora);
        self
    }

    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }
}

/// ActionEvent のビルダー
pub struct ActionEventBuilder {
    task_id: TaskId,
    group_id: Option<GroupId>,
    tick: u64,
    worker_id: WorkerId,
    sequence: u32,
    action: String,
    target: Option<String>,
    result: ActionEventResult,
    duration: Duration,
    context: ActionContext,
}

impl ActionEventBuilder {
    pub fn new(tick: u64, worker_id: WorkerId, action: impl Into<String>) -> Self {
        Self {
            task_id: TaskId::new(),
            group_id: None,
            tick,
            worker_id,
            sequence: 0,
            action: action.into(),
            target: None,
            result: ActionEventResult::success(),
            duration: Duration::ZERO,
            context: ActionContext::default(),
        }
    }

    /// タスク ID を設定
    pub fn task_id(mut self, task_id: TaskId) -> Self {
        self.task_id = task_id;
        self
    }

    /// グループ ID を設定
    pub fn group_id(mut self, group_id: GroupId) -> Self {
        self.group_id = Some(group_id);
        self
    }

    /// グループ ID を設定(Option)
    pub fn group_id_opt(mut self, group_id: Option<GroupId>) -> Self {
        self.group_id = group_id;
        self
    }

    pub fn sequence(mut self, seq: u32) -> Self {
        self.sequence = seq;
        self
    }

    pub fn target(mut self, target: impl Into<String>) -> Self {
        self.target = Some(target.into());
        self
    }

    pub fn result(mut self, result: ActionEventResult) -> Self {
        self.result = result;
        self
    }

    pub fn duration(mut self, duration: Duration) -> Self {
        self.duration = duration;
        self
    }

    pub fn context(mut self, context: ActionContext) -> Self {
        self.context = context;
        self
    }

    pub fn build(self) -> ActionEvent {
        ActionEvent {
            id: ActionEventId::new(self.tick, self.worker_id, self.sequence),
            task_id: self.task_id,
            group_id: self.group_id,
            tick: self.tick,
            worker_id: self.worker_id,
            action: self.action,
            target: self.target,
            result: self.result,
            duration: self.duration,
            context: self.context,
        }
    }
}

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

    #[test]
    fn test_action_event_id_uniqueness() {
        let id1 = ActionEventId::new(100, WorkerId(0), 0);
        let id2 = ActionEventId::new(100, WorkerId(0), 1);
        let id3 = ActionEventId::new(100, WorkerId(1), 0);
        let id4 = ActionEventId::new(101, WorkerId(0), 0);

        assert_ne!(id1, id2);
        assert_ne!(id1, id3);
        assert_ne!(id1, id4);
    }

    #[test]
    fn test_action_event_builder() {
        let event = ActionEventBuilder::new(10, WorkerId(1), "CheckStatus")
            .target("user-service")
            .result(ActionEventResult::success_with_output("Service is running"))
            .duration(Duration::from_millis(50))
            .context(
                ActionContext::new()
                    .with_selection_logic("UCB1")
                    .with_guidance(),
            )
            .build();

        assert_eq!(event.tick, 10);
        assert_eq!(event.worker_id, WorkerId(1));
        assert_eq!(event.action, "CheckStatus");
        assert_eq!(event.target, Some("user-service".to_string()));
        assert!(event.result.success);
        assert_eq!(event.duration, Duration::from_millis(50));
        assert_eq!(event.context.selection_logic, Some("UCB1".to_string()));
        assert!(event.context.from_guidance);
    }
}