Skip to main content

swarm_engine_core/events/
action.rs

1//! Action Event - 行動イベントの定義
2//!
3//! Worker が実行した行動を記録するイベント構造体。
4//! オンライン統計・学習・永続化の基盤となる。
5
6use std::time::Duration;
7
8use crate::types::{LoraConfig, WorkerId};
9
10/// 行動イベント
11///
12/// Worker が実行した行動の完全な記録。
13/// オンライン統計や学習パイプラインに使用される。
14#[derive(Debug, Clone)]
15pub struct ActionEvent {
16    /// イベントID(ユニーク)
17    pub id: ActionEventId,
18    /// 発生した Tick
19    pub tick: u64,
20    /// 実行した Worker
21    pub worker_id: WorkerId,
22    /// アクション名
23    pub action: String,
24    /// ターゲット(オプション)
25    pub target: Option<String>,
26    /// 実行結果
27    pub result: ActionEventResult,
28    /// 実行時間
29    pub duration: Duration,
30    /// コンテキスト情報
31    pub context: ActionContext,
32}
33
34/// イベントID
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct ActionEventId(pub u64);
37
38impl ActionEventId {
39    /// 新しいIDを生成(tick + worker_id + sequence でユニーク性を保証)
40    pub fn new(tick: u64, worker_id: WorkerId, sequence: u32) -> Self {
41        // tick: 40bit, worker_id: 16bit, sequence: 8bit = 64bit
42        let id = (tick << 24) | ((worker_id.0 as u64) << 8) | (sequence as u64 & 0xFF);
43        Self(id)
44    }
45}
46
47/// 行動結果
48#[derive(Debug, Clone)]
49pub struct ActionEventResult {
50    /// 成功/失敗
51    pub success: bool,
52    /// 出力(テキスト表現)
53    pub output: Option<String>,
54    /// エラーメッセージ
55    pub error: Option<String>,
56    /// 発見したノード数(探索の場合)
57    pub discoveries: u32,
58    /// KPI貢献度(目標関数への寄与、0.0〜1.0 または負値も可)
59    ///
60    /// Environment が「このアクションは目標にどれだけ近づいたか」を
61    /// スコアとして返す。学習時の報酬関数として使用可能。
62    pub kpi_contribution: Option<f64>,
63}
64
65impl ActionEventResult {
66    pub fn success() -> Self {
67        Self {
68            success: true,
69            output: None,
70            error: None,
71            discoveries: 0,
72            kpi_contribution: None,
73        }
74    }
75
76    pub fn success_with_output(output: impl Into<String>) -> Self {
77        Self {
78            success: true,
79            output: Some(output.into()),
80            error: None,
81            discoveries: 0,
82            kpi_contribution: None,
83        }
84    }
85
86    pub fn failure(error: impl Into<String>) -> Self {
87        Self {
88            success: false,
89            output: None,
90            error: Some(error.into()),
91            discoveries: 0,
92            kpi_contribution: None,
93        }
94    }
95
96    pub fn with_discoveries(mut self, count: u32) -> Self {
97        self.discoveries = count;
98        self
99    }
100
101    /// KPI貢献度を設定
102    ///
103    /// 目標関数への寄与を設定する。
104    /// - 正値: 目標に近づいた
105    /// - 0: 変化なし
106    /// - 負値: 目標から遠ざかった
107    pub fn with_kpi(mut self, contribution: f64) -> Self {
108        self.kpi_contribution = Some(contribution);
109        self
110    }
111}
112
113/// 行動のコンテキスト情報
114///
115/// なぜその行動が選択されたかの情報を保持。
116/// 学習時の特徴量として使用可能。
117#[derive(Debug, Clone, Default)]
118pub struct ActionContext {
119    /// 選択に使用されたロジック(UCB1, Thompson, Greedy 等)
120    pub selection_logic: Option<String>,
121    /// 探索空間でのノードID
122    pub exploration_node_id: Option<u64>,
123    /// Guidance からの指示だったか
124    pub from_guidance: bool,
125    /// 前回の行動(シーケンス分析用)
126    pub previous_action: Option<String>,
127    /// 使用した LoRA アダプター設定
128    pub lora: Option<LoraConfig>,
129    /// 追加のメタデータ
130    pub metadata: std::collections::HashMap<String, String>,
131}
132
133impl ActionContext {
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    pub fn with_selection_logic(mut self, logic: impl Into<String>) -> Self {
139        self.selection_logic = Some(logic.into());
140        self
141    }
142
143    pub fn with_exploration_node(mut self, node_id: u64) -> Self {
144        self.exploration_node_id = Some(node_id);
145        self
146    }
147
148    pub fn with_guidance(mut self) -> Self {
149        self.from_guidance = true;
150        self
151    }
152
153    pub fn with_previous_action(mut self, action: impl Into<String>) -> Self {
154        self.previous_action = Some(action.into());
155        self
156    }
157
158    pub fn with_lora(mut self, lora: LoraConfig) -> Self {
159        self.lora = Some(lora);
160        self
161    }
162
163    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
164        self.metadata.insert(key.into(), value.into());
165        self
166    }
167}
168
169/// ActionEvent のビルダー
170pub struct ActionEventBuilder {
171    tick: u64,
172    worker_id: WorkerId,
173    sequence: u32,
174    action: String,
175    target: Option<String>,
176    result: ActionEventResult,
177    duration: Duration,
178    context: ActionContext,
179}
180
181impl ActionEventBuilder {
182    pub fn new(tick: u64, worker_id: WorkerId, action: impl Into<String>) -> Self {
183        Self {
184            tick,
185            worker_id,
186            sequence: 0,
187            action: action.into(),
188            target: None,
189            result: ActionEventResult::success(),
190            duration: Duration::ZERO,
191            context: ActionContext::default(),
192        }
193    }
194
195    pub fn sequence(mut self, seq: u32) -> Self {
196        self.sequence = seq;
197        self
198    }
199
200    pub fn target(mut self, target: impl Into<String>) -> Self {
201        self.target = Some(target.into());
202        self
203    }
204
205    pub fn result(mut self, result: ActionEventResult) -> Self {
206        self.result = result;
207        self
208    }
209
210    pub fn duration(mut self, duration: Duration) -> Self {
211        self.duration = duration;
212        self
213    }
214
215    pub fn context(mut self, context: ActionContext) -> Self {
216        self.context = context;
217        self
218    }
219
220    pub fn build(self) -> ActionEvent {
221        ActionEvent {
222            id: ActionEventId::new(self.tick, self.worker_id, self.sequence),
223            tick: self.tick,
224            worker_id: self.worker_id,
225            action: self.action,
226            target: self.target,
227            result: self.result,
228            duration: self.duration,
229            context: self.context,
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_action_event_id_uniqueness() {
240        let id1 = ActionEventId::new(100, WorkerId(0), 0);
241        let id2 = ActionEventId::new(100, WorkerId(0), 1);
242        let id3 = ActionEventId::new(100, WorkerId(1), 0);
243        let id4 = ActionEventId::new(101, WorkerId(0), 0);
244
245        assert_ne!(id1, id2);
246        assert_ne!(id1, id3);
247        assert_ne!(id1, id4);
248    }
249
250    #[test]
251    fn test_action_event_builder() {
252        let event = ActionEventBuilder::new(10, WorkerId(1), "CheckStatus")
253            .target("user-service")
254            .result(ActionEventResult::success_with_output("Service is running"))
255            .duration(Duration::from_millis(50))
256            .context(
257                ActionContext::new()
258                    .with_selection_logic("UCB1")
259                    .with_guidance(),
260            )
261            .build();
262
263        assert_eq!(event.tick, 10);
264        assert_eq!(event.worker_id, WorkerId(1));
265        assert_eq!(event.action, "CheckStatus");
266        assert_eq!(event.target, Some("user-service".to_string()));
267        assert!(event.result.success);
268        assert_eq!(event.duration, Duration::from_millis(50));
269        assert_eq!(event.context.selection_logic, Some("UCB1".to_string()));
270        assert!(event.context.from_guidance);
271    }
272}