Skip to main content

swarm_engine_core/
actions.rs

1//! Actions 統一管理
2//!
3//! Orchestrator 内で使用される Action を Protocol として統一管理する。
4//! Group 機能により、コンテキストに応じた Action セットを取得できる。
5//!
6//! # 設計
7//!
8//! ```text
9//! ActionsConfig
10//! ├─ actions: HashMap<String, ActionDef>
11//! │   └─ "read_file" → ActionDef { groups: ["file_ops", "exploration"] }
12//! │   └─ "grep"      → ActionDef { groups: ["search", "exploration"] }
13//! │   └─ "write"     → ActionDef { groups: ["file_ops", "mutation"] }
14//! │
15//! └─ groups: HashMap<String, ActionGroup>
16//!     └─ "readonly"  → ActionGroup { include: ["exploration"], exclude: ["mutation"] }
17//!     └─ "all"       → ActionGroup { include: ["*"] }
18//! ```
19//!
20//! # 使用例
21//!
22//! ```ignore
23//! // 構築
24//! let cfg = ActionsConfig::new()
25//!     .action("read_file", ActionDef::new("ファイル読み込み").groups(["file_ops", "exploration"]))
26//!     .action("grep", ActionDef::new("パターン検索").groups(["search", "exploration"]))
27//!     .group("readonly", ActionGroup::include(["exploration"]).exclude(["mutation"]));
28//!
29//! // Extensions に登録
30//! let orc = OrchestratorBuilder::new()
31//!     .extension(cfg)
32//!     .build(runtime);
33//!
34//! // Manager から使用
35//! let cfg = state.shared.extensions.get::<ActionsConfig>()?;
36//! let candidates = cfg.candidates_for("readonly");  // → ["read_file", "grep"]
37//! ```
38
39use std::collections::HashMap;
40use std::time::Duration;
41
42use serde::{Deserialize, Serialize};
43
44// ============================================================================
45// Action Types
46// ============================================================================
47
48/// Action のカテゴリ(探索空間への影響で分類)
49///
50/// # カテゴリ説明
51///
52/// - **NodeExpand**: 新しい探索対象を発見する(例: Grep, List)
53///   - 成功時: 発見した対象を新しい Node として追加
54///   - 探索グラフが拡張される
55///
56/// - **NodeStateChange**: 既存 Node の状態を遷移させる(例: Read)
57///   - 成功時: 既存 Node の状態を Explored/Completed に更新
58///   - 探索グラフは拡張されない
59///
60/// # 例: ファイル探索タスク
61///
62/// ```text
63/// Grep("auth") [NodeExpand]
64///   → 発見: src/auth.rs → 新 Node 追加
65///
66/// Read("src/auth.rs") [NodeStateChange]
67///   → src/auth.rs Node の状態を Explored に更新
68///   → 目標ファイルなら Completed
69/// ```
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
71pub enum ActionCategory {
72    /// 新しい探索対象を発見する(Grep, List など)
73    #[default]
74    NodeExpand,
75    /// 既存 Node の状態を遷移させる(Read など)
76    NodeStateChange,
77}
78
79/// Action - Agent が実行する処理
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Action {
82    pub name: String,
83    pub params: ActionParams,
84}
85
86/// Action パラメータ
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct ActionParams {
89    /// ターゲット(ファイルパス等)
90    pub target: Option<String>,
91    /// 引数
92    pub args: HashMap<String, String>,
93    /// 生データ
94    pub data: Vec<u8>,
95}
96
97// ============================================================================
98// ActionOutput - 型付き出力表現
99// ============================================================================
100
101/// Action 出力の型付き表現
102///
103/// `Box<dyn Any>` の代わりに明示的な型を使用することで:
104/// - Clone 可能
105/// - パターンマッチで安全なアクセス
106/// - 下流処理が明確化
107#[derive(Debug, Clone)]
108pub enum ActionOutput {
109    /// プレーンテキスト出力(stdout、ファイル内容、メッセージ等)
110    Text(String),
111
112    /// 構造化データ出力(観測結果、クエリ結果等)
113    Structured(serde_json::Value),
114
115    /// バイナリ出力(画像、ファイル等、将来用)
116    Binary(Vec<u8>),
117}
118
119impl ActionOutput {
120    /// テキストとして取得(Structured は JSON 文字列化)
121    pub fn as_text(&self) -> String {
122        match self {
123            Self::Text(s) => s.clone(),
124            Self::Structured(v) => v.to_string(),
125            Self::Binary(b) => format!("<binary: {} bytes>", b.len()),
126        }
127    }
128
129    /// 構造化データとして取得(Text は parse 試行)
130    pub fn as_structured(&self) -> Option<serde_json::Value> {
131        match self {
132            Self::Text(s) => serde_json::from_str(s).ok(),
133            Self::Structured(v) => Some(v.clone()),
134            Self::Binary(_) => None,
135        }
136    }
137
138    /// テキスト参照を取得(Text の場合のみ)
139    pub fn text(&self) -> Option<&str> {
140        match self {
141            Self::Text(s) => Some(s),
142            _ => None,
143        }
144    }
145
146    /// 構造化データ参照を取得(Structured の場合のみ)
147    pub fn structured(&self) -> Option<&serde_json::Value> {
148        match self {
149            Self::Structured(v) => Some(v),
150            _ => None,
151        }
152    }
153}
154
155// ============================================================================
156// ActionResult
157// ============================================================================
158
159/// Action 実行結果
160#[derive(Debug, Clone)]
161pub struct ActionResult {
162    pub success: bool,
163    pub output: Option<ActionOutput>,
164    pub duration: Duration,
165    pub error: Option<String>,
166}
167
168impl ActionResult {
169    /// テキスト出力で成功
170    pub fn success_text(output: impl Into<String>, duration: Duration) -> Self {
171        Self {
172            success: true,
173            output: Some(ActionOutput::Text(output.into())),
174            duration,
175            error: None,
176        }
177    }
178
179    /// 構造化データ出力で成功
180    pub fn success_structured(output: serde_json::Value, duration: Duration) -> Self {
181        Self {
182            success: true,
183            output: Some(ActionOutput::Structured(output)),
184            duration,
185            error: None,
186        }
187    }
188
189    /// バイナリ出力で成功
190    pub fn success_binary(output: Vec<u8>, duration: Duration) -> Self {
191        Self {
192            success: true,
193            output: Some(ActionOutput::Binary(output)),
194            duration,
195            error: None,
196        }
197    }
198
199    /// 後方互換: 文字列出力で成功(success_text のエイリアス)
200    pub fn success(output: impl Into<String>, duration: Duration) -> Self {
201        Self::success_text(output, duration)
202    }
203
204    /// 失敗
205    pub fn failure(error: String, duration: Duration) -> Self {
206        Self {
207            success: false,
208            output: None,
209            duration,
210            error: Some(error),
211        }
212    }
213}
214
215// ============================================================================
216// ActionDef - Action 定義
217// ============================================================================
218
219/// パラメータ定義
220#[derive(Debug, Clone)]
221pub struct ParamDef {
222    /// パラメータ名
223    pub name: String,
224    /// 説明
225    pub description: String,
226    /// 必須かどうか
227    pub required: bool,
228}
229
230impl ParamDef {
231    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
232        Self {
233            name: name.into(),
234            description: description.into(),
235            required: true,
236        }
237    }
238
239    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
240        Self {
241            name: name.into(),
242            description: description.into(),
243            required: false,
244        }
245    }
246}
247
248/// パラメータバリアント定義
249///
250/// ExplorationSpace がアクションの後続ノードを展開する際、
251/// 指定されたパラメータ値のバリエーションを自動生成する。
252#[derive(Debug, Clone)]
253pub struct ParamVariants {
254    /// パラメータのキー名(例: "target", "direction")
255    pub key: String,
256    /// 取り得る値のリスト(例: ["north", "south", "east", "west"])
257    pub values: Vec<String>,
258}
259
260impl ParamVariants {
261    /// 新しい ParamVariants を作成
262    pub fn new(key: impl Into<String>, values: Vec<String>) -> Self {
263        Self {
264            key: key.into(),
265            values,
266        }
267    }
268}
269
270/// Action 定義
271#[derive(Debug, Clone)]
272pub struct ActionDef {
273    /// Action 名
274    pub name: String,
275    /// 説明(LLM プロンプト用)
276    pub description: String,
277    /// カテゴリ(探索空間への影響)
278    pub category: ActionCategory,
279    /// 所属 Group
280    pub groups: Vec<String>,
281    /// パラメータ定義
282    pub params: Vec<ParamDef>,
283    /// 出力例(LLM プロンプト用 JSON 形式)
284    pub example: Option<String>,
285    /// パラメータバリアント(ExplorationSpace で自動展開)
286    ///
287    /// 例: Move アクションで direction に対して ["north", "south", "east", "west"] を指定すると、
288    /// ExplorationSpace が後続ノード展開時に 4 つのバリアントを生成する。
289    ///
290    /// - `param_key`: パラメータ名(例: "target", "direction")
291    /// - `variants`: 取り得る値のリスト
292    pub param_variants: Option<ParamVariants>,
293}
294
295impl ActionDef {
296    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
297        Self {
298            name: name.into(),
299            description: description.into(),
300            category: ActionCategory::default(),
301            groups: Vec::new(),
302            params: Vec::new(),
303            example: None,
304            param_variants: None,
305        }
306    }
307
308    /// パラメータバリアントを設定
309    ///
310    /// ExplorationSpace が後続ノード展開時に、指定されたバリアントを自動生成する。
311    ///
312    /// # Example
313    ///
314    /// ```ignore
315    /// ActionDef::new("Move", "Move to adjacent cell")
316    ///     .param_variants("target", vec!["north", "south", "east", "west"])
317    /// ```
318    pub fn param_variants(
319        mut self,
320        key: impl Into<String>,
321        values: impl IntoIterator<Item = impl Into<String>>,
322    ) -> Self {
323        self.param_variants = Some(ParamVariants::new(
324            key,
325            values.into_iter().map(|v| v.into()).collect(),
326        ));
327        self
328    }
329
330    /// カテゴリを設定
331    pub fn category(mut self, category: ActionCategory) -> Self {
332        self.category = category;
333        self
334    }
335
336    /// NodeExpand カテゴリに設定(新しい探索対象を発見する Action)
337    pub fn node_expand(mut self) -> Self {
338        self.category = ActionCategory::NodeExpand;
339        self
340    }
341
342    /// NodeStateChange カテゴリに設定(既存 Node の状態を遷移させる Action)
343    pub fn node_state_change(mut self) -> Self {
344        self.category = ActionCategory::NodeStateChange;
345        self
346    }
347
348    /// Group を設定
349    pub fn groups<I, S>(mut self, groups: I) -> Self
350    where
351        I: IntoIterator<Item = S>,
352        S: Into<String>,
353    {
354        self.groups = groups.into_iter().map(|s| s.into()).collect();
355        self
356    }
357
358    /// Group を追加
359    pub fn group(mut self, group: impl Into<String>) -> Self {
360        self.groups.push(group.into());
361        self
362    }
363
364    /// パラメータを追加
365    pub fn param(mut self, param: ParamDef) -> Self {
366        self.params.push(param);
367        self
368    }
369
370    /// 必須パラメータを追加
371    pub fn required_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
372        self.param(ParamDef::required(name, description))
373    }
374
375    /// オプションパラメータを追加
376    pub fn optional_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
377        self.param(ParamDef::optional(name, description))
378    }
379
380    /// 出力例を設定(LLM プロンプト用 JSON 形式)
381    pub fn example(mut self, example: impl Into<String>) -> Self {
382        self.example = Some(example.into());
383        self
384    }
385
386    /// 指定した Group に所属しているか
387    pub fn has_group(&self, group: &str) -> bool {
388        self.groups.iter().any(|g| g == group)
389    }
390
391    /// 指定した Group のいずれかに所属しているか
392    pub fn has_any_group(&self, groups: &[&str]) -> bool {
393        groups.iter().any(|g| self.has_group(g))
394    }
395}
396
397// ============================================================================
398// ActionGroup - Action グループ(フィルタリング用)
399// ============================================================================
400
401/// Action グループ定義
402///
403/// include/exclude で Action をフィルタリングする。
404/// include が空の場合は全 Action が対象。
405#[derive(Debug, Clone, Default)]
406pub struct ActionGroup {
407    /// グループ名
408    pub name: String,
409    /// 含める Group(これらの Group を持つ Action を含む)
410    pub include_groups: Vec<String>,
411    /// 除外する Group(これらの Group を持つ Action を除外)
412    pub exclude_groups: Vec<String>,
413}
414
415impl ActionGroup {
416    pub fn new(name: impl Into<String>) -> Self {
417        Self {
418            name: name.into(),
419            include_groups: Vec::new(),
420            exclude_groups: Vec::new(),
421        }
422    }
423
424    /// 含める Group を設定
425    pub fn include<I, S>(mut self, groups: I) -> Self
426    where
427        I: IntoIterator<Item = S>,
428        S: Into<String>,
429    {
430        self.include_groups = groups.into_iter().map(|s| s.into()).collect();
431        self
432    }
433
434    /// 除外する Group を設定
435    pub fn exclude<I, S>(mut self, groups: I) -> Self
436    where
437        I: IntoIterator<Item = S>,
438        S: Into<String>,
439    {
440        self.exclude_groups = groups.into_iter().map(|s| s.into()).collect();
441        self
442    }
443
444    /// ActionDef がこの Group にマッチするか判定
445    pub fn matches(&self, action: &ActionDef) -> bool {
446        // exclude チェック(1つでも該当すれば除外)
447        if self.exclude_groups.iter().any(|g| action.has_group(g)) {
448            return false;
449        }
450
451        // include が空なら全て含む
452        if self.include_groups.is_empty() {
453            return true;
454        }
455
456        // include チェック(1つでも該当すれば含む)
457        self.include_groups.iter().any(|g| action.has_group(g))
458    }
459}
460
461// ============================================================================
462// ActionsConfig - Actions 統一管理
463// ============================================================================
464
465/// Actions 統一管理
466///
467/// Orchestrator 内で使用される Action を Protocol として統一管理する。
468/// Extensions に登録して Manager/Worker から参照する。
469#[derive(Debug, Clone, Default)]
470pub struct ActionsConfig {
471    /// Action 定義
472    actions: HashMap<String, ActionDef>,
473    /// Group 定義
474    groups: HashMap<String, ActionGroup>,
475}
476
477impl ActionsConfig {
478    pub fn new() -> Self {
479        Self::default()
480    }
481
482    /// Action を追加
483    pub fn action(mut self, name: impl Into<String>, def: ActionDef) -> Self {
484        let name = name.into();
485        let mut def = def;
486        def.name = name.clone();
487        self.actions.insert(name, def);
488        self
489    }
490
491    /// Action を追加(mutable)
492    pub fn add_action(&mut self, name: impl Into<String>, def: ActionDef) {
493        let name = name.into();
494        let mut def = def;
495        def.name = name.clone();
496        self.actions.insert(name, def);
497    }
498
499    /// Group を追加
500    pub fn group(mut self, name: impl Into<String>, group: ActionGroup) -> Self {
501        let name = name.into();
502        let mut group = group;
503        group.name = name.clone();
504        self.groups.insert(name, group);
505        self
506    }
507
508    /// Group を追加(mutable)
509    pub fn add_group(&mut self, name: impl Into<String>, group: ActionGroup) {
510        let name = name.into();
511        let mut group = group;
512        group.name = name.clone();
513        self.groups.insert(name, group);
514    }
515
516    // ========================================================================
517    // Query API
518    // ========================================================================
519
520    /// 全 Action 名を取得
521    pub fn all_action_names(&self) -> Vec<String> {
522        self.actions.keys().cloned().collect()
523    }
524
525    /// 全 Action 定義を取得
526    pub fn all_actions(&self) -> impl Iterator<Item = &ActionDef> {
527        self.actions.values()
528    }
529
530    /// Action 定義を取得
531    pub fn get(&self, name: &str) -> Option<&ActionDef> {
532        self.actions.get(name)
533    }
534
535    /// Group 定義を取得
536    pub fn get_group(&self, name: &str) -> Option<&ActionGroup> {
537        self.groups.get(name)
538    }
539
540    /// 指定 Group に所属する Action を取得
541    pub fn by_group(&self, group_name: &str) -> Vec<&ActionDef> {
542        if let Some(group) = self.groups.get(group_name) {
543            self.actions.values().filter(|a| group.matches(a)).collect()
544        } else {
545            // Group 定義がない場合は、直接その group を持つ Action を返す
546            self.actions
547                .values()
548                .filter(|a| a.has_group(group_name))
549                .collect()
550        }
551    }
552
553    /// 指定 Group の Action 名リストを取得(LLM candidates 用)
554    pub fn candidates_for(&self, group_name: &str) -> Vec<String> {
555        self.by_group(group_name)
556            .into_iter()
557            .map(|a| a.name.clone())
558            .collect()
559    }
560
561    /// 複数 Group のいずれかに所属する Action を取得
562    pub fn by_groups(&self, group_names: &[&str]) -> Vec<&ActionDef> {
563        self.actions
564            .values()
565            .filter(|a| group_names.iter().any(|g| a.has_group(g)))
566            .collect()
567    }
568
569    /// 複数 Group の Action 名リストを取得
570    pub fn candidates_by_groups(&self, group_names: &[&str]) -> Vec<String> {
571        self.by_groups(group_names)
572            .into_iter()
573            .map(|a| a.name.clone())
574            .collect()
575    }
576
577    /// NodeExpand カテゴリのアクション名を取得
578    ///
579    /// 探索系アクション(新しい探索対象を発見する)のみを返す。
580    /// 初期展開時に使用する。
581    pub fn node_expand_actions(&self) -> Vec<String> {
582        self.actions
583            .values()
584            .filter(|a| a.category == ActionCategory::NodeExpand)
585            .map(|a| a.name.clone())
586            .collect()
587    }
588
589    /// NodeStateChange カテゴリのアクション名を取得
590    pub fn node_state_change_actions(&self) -> Vec<String> {
591        self.actions
592            .values()
593            .filter(|a| a.category == ActionCategory::NodeStateChange)
594            .map(|a| a.name.clone())
595            .collect()
596    }
597
598    /// 指定アクションのパラメータバリアントを取得
599    ///
600    /// ExplorationSpace がノード展開時にバリアントを生成するために使用。
601    ///
602    /// # Returns
603    ///
604    /// - `Some((key, values))`: パラメータバリアントが定義されている場合
605    /// - `None`: 定義されていない場合
606    pub fn param_variants(&self, action_name: &str) -> Option<(&str, &[String])> {
607        self.actions
608            .get(action_name)
609            .and_then(|a| a.param_variants.as_ref())
610            .map(|pv| (pv.key.as_str(), pv.values.as_slice()))
611    }
612
613    // ========================================================================
614    // Action Builder
615    // ========================================================================
616
617    /// Action を構築
618    ///
619    /// DecisionResponse から Action を構築する際に使用。
620    /// 登録されていない Action 名の場合は None を返す。
621    pub fn build_action(
622        &self,
623        name: &str,
624        target: Option<String>,
625        args: HashMap<String, String>,
626    ) -> Option<Action> {
627        self.actions.get(name).map(|_def| Action {
628            name: name.to_string(),
629            params: ActionParams {
630                target,
631                args,
632                data: Vec::new(),
633            },
634        })
635    }
636
637    /// Action を構築(登録されていなくても作成)
638    ///
639    /// バリデーションなしで Action を作成する。
640    /// 動的な Action 名に対応する場合に使用。
641    pub fn build_action_unchecked(
642        &self,
643        name: impl Into<String>,
644        target: Option<String>,
645        args: HashMap<String, String>,
646    ) -> Action {
647        Action {
648            name: name.into(),
649            params: ActionParams {
650                target,
651                args,
652                data: Vec::new(),
653            },
654        }
655    }
656
657    // ========================================================================
658    // Validation
659    // ========================================================================
660
661    /// Action をバリデート
662    pub fn validate(&self, action: &Action) -> Result<(), ActionValidationError> {
663        let def = self
664            .actions
665            .get(&action.name)
666            .ok_or_else(|| ActionValidationError::UnknownAction(action.name.clone()))?;
667
668        // 必須パラメータチェック
669        for param in &def.params {
670            if param.required && !action.params.args.contains_key(&param.name) {
671                return Err(ActionValidationError::MissingParam(param.name.clone()));
672            }
673        }
674
675        Ok(())
676    }
677}
678
679/// Action バリデーションエラー
680#[derive(Debug, Clone, thiserror::Error)]
681pub enum ActionValidationError {
682    #[error("Unknown action: {0}")]
683    UnknownAction(String),
684
685    #[error("Missing required parameter: {0}")]
686    MissingParam(String),
687
688    #[error("Invalid parameter value: {0}")]
689    InvalidParam(String),
690}
691
692// ============================================================================
693// Tests
694// ============================================================================
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    fn sample_config() -> ActionsConfig {
701        ActionsConfig::new()
702            .action(
703                "read_file",
704                ActionDef::new("read_file", "ファイルを読み込む")
705                    .groups(["file_ops", "exploration"])
706                    .required_param("path", "ファイルパス"),
707            )
708            .action(
709                "grep",
710                ActionDef::new("grep", "パターン検索")
711                    .groups(["search", "exploration"])
712                    .required_param("pattern", "検索パターン"),
713            )
714            .action(
715                "write_file",
716                ActionDef::new("write_file", "ファイルを書き込む")
717                    .groups(["file_ops", "mutation"])
718                    .required_param("path", "ファイルパス")
719                    .required_param("content", "内容"),
720            )
721            .action(
722                "escalate",
723                ActionDef::new("escalate", "Manager に報告").groups(["control"]),
724            )
725            .group(
726                "readonly",
727                ActionGroup::new("readonly")
728                    .include(["exploration", "search"])
729                    .exclude(["mutation"]),
730            )
731            .group(
732                "all",
733                ActionGroup::new("all"), // include/exclude なし = 全部
734            )
735    }
736
737    #[test]
738    fn test_by_group_direct() {
739        let cfg = sample_config();
740
741        // 直接 group 名で取得
742        let file_ops = cfg.by_group("file_ops");
743        assert_eq!(file_ops.len(), 2);
744
745        let exploration = cfg.by_group("exploration");
746        assert_eq!(exploration.len(), 2);
747    }
748
749    #[test]
750    fn test_by_group_defined() {
751        let cfg = sample_config();
752
753        // 定義された Group で取得
754        let readonly = cfg.candidates_for("readonly");
755        assert!(readonly.contains(&"read_file".to_string()));
756        assert!(readonly.contains(&"grep".to_string()));
757        assert!(!readonly.contains(&"write_file".to_string())); // mutation なので除外
758
759        let all = cfg.candidates_for("all");
760        assert_eq!(all.len(), 4);
761    }
762
763    #[test]
764    fn test_build_action() {
765        let cfg = sample_config();
766
767        let action = cfg.build_action(
768            "read_file",
769            Some("/path/to/file".to_string()),
770            HashMap::new(),
771        );
772        assert!(action.is_some());
773        assert_eq!(action.unwrap().name, "read_file");
774
775        let unknown = cfg.build_action("unknown", None, HashMap::new());
776        assert!(unknown.is_none());
777    }
778
779    #[test]
780    fn test_validate() {
781        let cfg = sample_config();
782
783        // 有効な Action
784        let action = Action {
785            name: "read_file".to_string(),
786            params: ActionParams {
787                target: None,
788                args: [("path".to_string(), "/tmp/test".to_string())]
789                    .into_iter()
790                    .collect(),
791                data: Vec::new(),
792            },
793        };
794        assert!(cfg.validate(&action).is_ok());
795
796        // 必須パラメータ不足
797        let action_missing = Action {
798            name: "read_file".to_string(),
799            params: ActionParams::default(),
800        };
801        assert!(matches!(
802            cfg.validate(&action_missing),
803            Err(ActionValidationError::MissingParam(_))
804        ));
805
806        // 未知の Action
807        let unknown = Action {
808            name: "unknown".to_string(),
809            params: ActionParams::default(),
810        };
811        assert!(matches!(
812            cfg.validate(&unknown),
813            Err(ActionValidationError::UnknownAction(_))
814        ));
815    }
816}