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