Skip to main content

swarm_engine_core/learn/record/
mod.rs

1//! Record - 生イベントの抽象化
2//!
3//! ## 設計思想
4//!
5//! ActionEvent と LlmDebugEvent を統一的に扱うための抽象化層。
6//! Episode は Record のコレクションから構築される。
7//!
8//! ```text
9//! ActionEvent ──┐
10//!               ├──▶ Record ──▶ Episode
11//! LlmDebugEvent ┘
12//! ```
13
14mod action;
15mod dependency_graph;
16mod llm;
17mod stream;
18
19use serde::{Deserialize, Serialize};
20
21use crate::events::ActionEvent;
22
23pub use action::ActionRecord;
24pub use dependency_graph::DependencyGraphRecord;
25pub use llm::LlmCallRecord;
26pub use stream::RecordStream;
27
28// ============================================================================
29// Record
30// ============================================================================
31
32/// 生イベントから変換された Record
33///
34/// ActionEvent と LlmDebugEvent を統一的に扱うための抽象化。
35/// EpisodeContext は Record のリストを保持する。
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum Record {
38    /// ActionEvent から変換
39    Action(ActionRecord),
40    /// LlmDebugEvent から変換
41    Llm(LlmCallRecord),
42    /// DependencyGraph 推論の記録
43    DependencyGraph(DependencyGraphRecord),
44}
45
46impl Record {
47    /// Action Record かどうか
48    pub fn is_action(&self) -> bool {
49        matches!(self, Self::Action(_))
50    }
51
52    /// Llm Record かどうか
53    pub fn is_llm(&self) -> bool {
54        matches!(self, Self::Llm(_))
55    }
56
57    /// DependencyGraph Record かどうか
58    pub fn is_dependency_graph(&self) -> bool {
59        matches!(self, Self::DependencyGraph(_))
60    }
61
62    /// ActionRecord を取得
63    pub fn as_action(&self) -> Option<&ActionRecord> {
64        match self {
65            Self::Action(r) => Some(r),
66            _ => None,
67        }
68    }
69
70    /// LlmCallRecord を取得
71    pub fn as_llm(&self) -> Option<&LlmCallRecord> {
72        match self {
73            Self::Llm(r) => Some(r),
74            _ => None,
75        }
76    }
77
78    /// DependencyGraphRecord を取得
79    pub fn as_dependency_graph(&self) -> Option<&DependencyGraphRecord> {
80        match self {
81            Self::DependencyGraph(r) => Some(r),
82            _ => None,
83        }
84    }
85
86    /// Worker ID を取得(両方のレコードタイプから)
87    pub fn worker_id(&self) -> Option<usize> {
88        match self {
89            Self::Action(r) => Some(r.worker_id),
90            Self::Llm(r) => r.worker_id,
91            Self::DependencyGraph(_) => None, // DependencyGraph は Worker に紐付かない
92        }
93    }
94
95    /// タイムスタンプを取得(ソート用)
96    pub fn timestamp_ms(&self) -> u64 {
97        match self {
98            Self::Action(r) => r.tick,
99            Self::Llm(r) => r.timestamp_ms,
100            Self::DependencyGraph(r) => r.timestamp_ms,
101        }
102    }
103}
104
105impl From<ActionRecord> for Record {
106    fn from(record: ActionRecord) -> Self {
107        Self::Action(record)
108    }
109}
110
111impl From<LlmCallRecord> for Record {
112    fn from(record: LlmCallRecord) -> Self {
113        Self::Llm(record)
114    }
115}
116
117impl From<DependencyGraphRecord> for Record {
118    fn from(record: DependencyGraphRecord) -> Self {
119        Self::DependencyGraph(record)
120    }
121}
122
123impl From<&ActionEvent> for Record {
124    fn from(event: &ActionEvent) -> Self {
125        Self::Action(ActionRecord::from(event))
126    }
127}
128
129// ============================================================================
130// FromRecord - 型安全なクエリのための Trait
131// ============================================================================
132
133/// Record から特定の型を抽出するための Trait
134///
135/// 新しい Record 種別を追加したら、この Trait を実装することで
136/// EpisodeContext::iter::<T>() でクエリ可能になる。
137pub trait FromRecord: Sized {
138    fn from_record(record: &Record) -> Option<&Self>;
139}
140
141impl FromRecord for ActionRecord {
142    fn from_record(record: &Record) -> Option<&Self> {
143        record.as_action()
144    }
145}
146
147impl FromRecord for LlmCallRecord {
148    fn from_record(record: &Record) -> Option<&Self> {
149        record.as_llm()
150    }
151}
152
153impl FromRecord for DependencyGraphRecord {
154    fn from_record(record: &Record) -> Option<&Self> {
155        record.as_dependency_graph()
156    }
157}
158
159// ============================================================================
160// Tests
161// ============================================================================
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_record_from_action_record() {
169        let action = ActionRecord::new(1, 0, "CheckStatus").success(true);
170        let record = Record::from(action);
171
172        assert!(record.is_action());
173        assert!(!record.is_llm());
174        assert_eq!(record.worker_id(), Some(0));
175    }
176
177    #[test]
178    fn test_record_from_llm_call_record() {
179        let llm = LlmCallRecord::new("decide", "qwen2.5")
180            .worker_id(1)
181            .prompt("test")
182            .response("ok");
183        let record = Record::from(llm);
184
185        assert!(!record.is_action());
186        assert!(record.is_llm());
187        assert_eq!(record.worker_id(), Some(1));
188    }
189
190    #[test]
191    fn test_record_stream_filtering() {
192        let records = vec![
193            Record::from(ActionRecord::new(1, 0, "A").success(true)),
194            Record::from(LlmCallRecord::new("decide", "model").worker_id(0)),
195            Record::from(ActionRecord::new(2, 1, "B").success(true)),
196        ];
197
198        let stream = RecordStream::new(&records);
199
200        assert_eq!(stream.actions().count(), 2);
201        assert_eq!(stream.llm_calls().count(), 1);
202        assert_eq!(stream.by_worker(0).count(), 2);
203        assert_eq!(stream.by_worker(1).count(), 1);
204    }
205}