swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Record - 生イベントの抽象化
//!
//! ## 設計思想
//!
//! **全ての Record は Event から変換されなければならない。**
//!
//! ```text
//! [External Events]                    [Learn Domain Records (DTO)]
//! ActionEvent ─────────────────────▶ ActionRecord ──────────────┐
//! LlmDebugEvent ───────────────────▶ LlmCallRecord ─────────────┤
//! LearningEvent::DependencyGraph ──▶ DependencyGraphRecord ─────├──▶ Record ──▶ Episode
//! LearningEvent::StrategyAdvice ───▶ StrategyAdviceRecord ──────┤
//! LearningEvent::LearnStatsSnapshot ▶ LearnStatsRecord ─────────┘
//! ```
//!
//! ## EventSource trait
//!
//! 新しい Record を追加する際は [`EventSource`] trait を実装すること。
//! これにより `From<&Event>` 実装が強制される。
//!
//! ```ignore
//! // 各 record ファイルで実装
//! impl EventSource for MyRecord {
//!     type Event = MyEvent;
//! }
//!
//! impl From<&MyEvent> for MyRecord {
//!     fn from(event: &MyEvent) -> Self { ... }
//! }
//! ```
//!
//! ## Checklist (新規 Record 追加時)
//!
//! 1. `record/` に新しいファイルを作成
//! 2. Record 構造体を定義
//! 3. 対応する Event を定義(または既存 Event に variant 追加)
//! 4. `From<&Event> for *Record` を同ファイルに実装
//! 5. `EventSource` trait を実装
//! 6. `mod.rs` に `Record` enum variant を追加
//! 7. `mod.rs` に `From<&Event> for Record` のルーティングを追加
//! 8. `FromRecord` trait を実装

mod action;
mod dependency_graph;
mod learn_stats;
mod llm;
mod strategy_advice;
mod stream;

use serde::{Deserialize, Serialize};

use crate::events::{ActionEvent, LearningEvent};

pub use action::ActionRecord;
pub use dependency_graph::DependencyGraphRecord;
pub use learn_stats::LearnStatsRecord;
pub use llm::LlmCallRecord;
pub use strategy_advice::StrategyAdviceRecord;
pub use stream::RecordStream;

// ============================================================================
// Record
// ============================================================================

/// 生イベントから変換された Record
///
/// 外部 Event を Learn ドメイン内の DTO として保持する。
/// EpisodeContext は Record のリストを保持し、Episode 構築に使用される。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Record {
    /// ActionEvent から変換
    Action(ActionRecord),
    /// LlmDebugEvent から変換
    Llm(LlmCallRecord),
    /// DependencyGraph 推論の記録
    DependencyGraph(DependencyGraphRecord),
    /// LLM 戦略アドバイスの記録
    StrategyAdvice(StrategyAdviceRecord),
    /// LearnStats スナップショットの記録
    LearnStats(LearnStatsRecord),
}

impl Record {
    /// Action Record かどうか
    pub fn is_action(&self) -> bool {
        matches!(self, Self::Action(_))
    }

    /// Llm Record かどうか
    pub fn is_llm(&self) -> bool {
        matches!(self, Self::Llm(_))
    }

    /// DependencyGraph Record かどうか
    pub fn is_dependency_graph(&self) -> bool {
        matches!(self, Self::DependencyGraph(_))
    }

    /// StrategyAdvice Record かどうか
    pub fn is_strategy_advice(&self) -> bool {
        matches!(self, Self::StrategyAdvice(_))
    }

    /// LearnStats Record かどうか
    pub fn is_learn_stats(&self) -> bool {
        matches!(self, Self::LearnStats(_))
    }

    /// ActionRecord を取得
    pub fn as_action(&self) -> Option<&ActionRecord> {
        match self {
            Self::Action(r) => Some(r),
            _ => None,
        }
    }

    /// LlmCallRecord を取得
    pub fn as_llm(&self) -> Option<&LlmCallRecord> {
        match self {
            Self::Llm(r) => Some(r),
            _ => None,
        }
    }

    /// DependencyGraphRecord を取得
    pub fn as_dependency_graph(&self) -> Option<&DependencyGraphRecord> {
        match self {
            Self::DependencyGraph(r) => Some(r),
            _ => None,
        }
    }

    /// StrategyAdviceRecord を取得
    pub fn as_strategy_advice(&self) -> Option<&StrategyAdviceRecord> {
        match self {
            Self::StrategyAdvice(r) => Some(r),
            _ => None,
        }
    }

    /// LearnStatsRecord を取得
    pub fn as_learn_stats(&self) -> Option<&LearnStatsRecord> {
        match self {
            Self::LearnStats(r) => Some(r),
            _ => None,
        }
    }

    /// Worker ID を取得(紐付くレコードのみ)
    pub fn worker_id(&self) -> Option<usize> {
        match self {
            Self::Action(r) => Some(r.worker_id),
            Self::Llm(r) => r.worker_id,
            // 以下は Worker に紐付かない
            Self::DependencyGraph(_) => None,
            Self::StrategyAdvice(_) => None,
            Self::LearnStats(_) => None,
        }
    }

    /// タイムスタンプを取得(ソート用)
    pub fn timestamp_ms(&self) -> u64 {
        match self {
            Self::Action(r) => r.tick,
            Self::Llm(r) => r.timestamp_ms,
            Self::DependencyGraph(r) => r.timestamp_ms,
            Self::StrategyAdvice(r) => r.timestamp_ms,
            Self::LearnStats(r) => r.timestamp_ms,
        }
    }
}

impl From<ActionRecord> for Record {
    fn from(record: ActionRecord) -> Self {
        Self::Action(record)
    }
}

impl From<LlmCallRecord> for Record {
    fn from(record: LlmCallRecord) -> Self {
        Self::Llm(record)
    }
}

impl From<DependencyGraphRecord> for Record {
    fn from(record: DependencyGraphRecord) -> Self {
        Self::DependencyGraph(record)
    }
}

impl From<StrategyAdviceRecord> for Record {
    fn from(record: StrategyAdviceRecord) -> Self {
        Self::StrategyAdvice(record)
    }
}

impl From<LearnStatsRecord> for Record {
    fn from(record: LearnStatsRecord) -> Self {
        Self::LearnStats(record)
    }
}

impl From<&ActionEvent> for Record {
    fn from(event: &ActionEvent) -> Self {
        Self::Action(ActionRecord::from(event))
    }
}

impl From<&LearningEvent> for Record {
    fn from(event: &LearningEvent) -> Self {
        match event {
            LearningEvent::StrategyAdvice { .. } => {
                Self::StrategyAdvice(StrategyAdviceRecord::from(event))
            }
            LearningEvent::DependencyGraphInference { .. } => {
                Self::DependencyGraph(DependencyGraphRecord::from(event))
            }
            LearningEvent::LearnStatsSnapshot { .. } => {
                Self::LearnStats(LearnStatsRecord::from(event))
            }
        }
    }
}

// ============================================================================
// FromRecord - 型安全なクエリのための Trait
// ============================================================================

/// Record から特定の型を抽出するための Trait
///
/// 新しい Record 種別を追加したら、この Trait を実装することで
/// EpisodeContext::iter::<T>() でクエリ可能になる。
pub trait FromRecord: Sized {
    fn from_record(record: &Record) -> Option<&Self>;
}

impl FromRecord for ActionRecord {
    fn from_record(record: &Record) -> Option<&Self> {
        record.as_action()
    }
}

impl FromRecord for LlmCallRecord {
    fn from_record(record: &Record) -> Option<&Self> {
        record.as_llm()
    }
}

impl FromRecord for DependencyGraphRecord {
    fn from_record(record: &Record) -> Option<&Self> {
        record.as_dependency_graph()
    }
}

impl FromRecord for StrategyAdviceRecord {
    fn from_record(record: &Record) -> Option<&Self> {
        record.as_strategy_advice()
    }
}

impl FromRecord for LearnStatsRecord {
    fn from_record(record: &Record) -> Option<&Self> {
        record.as_learn_stats()
    }
}

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

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

    #[test]
    fn test_record_from_action_record() {
        let action = ActionRecord::new(1, 0, "CheckStatus").success(true);
        let record = Record::from(action);

        assert!(record.is_action());
        assert!(!record.is_llm());
        assert_eq!(record.worker_id(), Some(0));
    }

    #[test]
    fn test_record_from_llm_call_record() {
        let llm = LlmCallRecord::new("decide", "qwen2.5")
            .worker_id(1)
            .prompt("test")
            .response("ok");
        let record = Record::from(llm);

        assert!(!record.is_action());
        assert!(record.is_llm());
        assert_eq!(record.worker_id(), Some(1));
    }

    #[test]
    fn test_record_stream_filtering() {
        let records = vec![
            Record::from(ActionRecord::new(1, 0, "A").success(true)),
            Record::from(LlmCallRecord::new("decide", "model").worker_id(0)),
            Record::from(ActionRecord::new(2, 1, "B").success(true)),
        ];

        let stream = RecordStream::new(&records);

        assert_eq!(stream.actions().count(), 2);
        assert_eq!(stream.llm_calls().count(), 1);
        assert_eq!(stream.by_worker(0).count(), 2);
        assert_eq!(stream.by_worker(1).count(), 1);
    }
}