Skip to main content

swarm_engine_core/actions/
mod.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
39mod env_spec;
40
41pub use env_spec::{ActionSpec, EnvironmentSpec, EnvironmentSpecRegistry, ParamSpec};
42
43use std::collections::HashMap;
44use std::time::Duration;
45
46use serde::{Deserialize, Serialize};
47
48// ============================================================================
49// Action Types
50// ============================================================================
51
52/// Action のカテゴリ(探索空間への影響で分類)
53///
54/// # カテゴリ説明
55///
56/// - **NodeExpand**: 新しい探索対象を発見する(例: Grep, List)
57///   - 成功時: 発見した対象を新しい Node として追加
58///   - 探索グラフが拡張される
59///
60/// - **NodeStateChange**: 既存 Node の状態を遷移させる(例: Read)
61///   - 成功時: 既存 Node の状態を Explored/Completed に更新
62///   - 探索グラフは拡張されない
63///
64/// # 例: ファイル探索タスク
65///
66/// ```text
67/// Grep("auth") [NodeExpand]
68///   → 発見: src/auth.rs → 新 Node 追加
69///
70/// Read("src/auth.rs") [NodeStateChange]
71///   → src/auth.rs Node の状態を Explored に更新
72///   → 目標ファイルなら Completed
73/// ```
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
75pub enum ActionCategory {
76    /// 新しい探索対象を発見する(Grep, List など)
77    #[default]
78    NodeExpand,
79    /// 既存 Node の状態を遷移させる(Read など)
80    NodeStateChange,
81}
82
83/// Action - Agent が実行する処理
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Action {
86    pub name: String,
87    pub params: ActionParams,
88}
89
90/// Action パラメータ
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct ActionParams {
93    /// ターゲット(ファイルパス等)
94    pub target: Option<String>,
95    /// 引数
96    pub args: HashMap<String, String>,
97    /// 生データ
98    pub data: Vec<u8>,
99}
100
101// ============================================================================
102// ActionOutput - 型付き出力表現
103// ============================================================================
104
105/// Action 出力の型付き表現
106///
107/// `Box<dyn Any>` の代わりに明示的な型を使用することで:
108/// - Clone 可能
109/// - パターンマッチで安全なアクセス
110/// - 下流処理が明確化
111#[derive(Debug, Clone)]
112pub enum ActionOutput {
113    /// プレーンテキスト出力(stdout、ファイル内容、メッセージ等)
114    Text(String),
115
116    /// 構造化データ出力(観測結果、クエリ結果等)
117    Structured(serde_json::Value),
118
119    /// バイナリ出力(画像、ファイル等、将来用)
120    Binary(Vec<u8>),
121}
122
123impl ActionOutput {
124    /// テキストとして取得(Structured は JSON 文字列化)
125    pub fn as_text(&self) -> String {
126        match self {
127            Self::Text(s) => s.clone(),
128            Self::Structured(v) => v.to_string(),
129            Self::Binary(b) => format!("<binary: {} bytes>", b.len()),
130        }
131    }
132
133    /// 構造化データとして取得(Text は parse 試行)
134    pub fn as_structured(&self) -> Option<serde_json::Value> {
135        match self {
136            Self::Text(s) => serde_json::from_str(s).ok(),
137            Self::Structured(v) => Some(v.clone()),
138            Self::Binary(_) => None,
139        }
140    }
141
142    /// テキスト参照を取得(Text の場合のみ)
143    pub fn text(&self) -> Option<&str> {
144        match self {
145            Self::Text(s) => Some(s),
146            _ => None,
147        }
148    }
149
150    /// 構造化データ参照を取得(Structured の場合のみ)
151    pub fn structured(&self) -> Option<&serde_json::Value> {
152        match self {
153            Self::Structured(v) => Some(v),
154            _ => None,
155        }
156    }
157}
158
159// ============================================================================
160// ActionResult
161// ============================================================================
162
163/// Action 実行結果
164#[derive(Debug, Clone)]
165pub struct ActionResult {
166    pub success: bool,
167    pub output: Option<ActionOutput>,
168    pub duration: Duration,
169    pub error: Option<String>,
170    /// 発見したターゲット(ExploMap で新しいノードとして展開される)
171    ///
172    /// Search 系アクションで複数の結果を返す場合に使用。
173    /// 例: Search が ["doc1", "doc2", "doc3"] を返すと、
174    /// ExploMap がそれぞれを新しいコンテキストとして展開する。
175    pub discovered_targets: Vec<String>,
176}
177
178impl ActionResult {
179    /// テキスト出力で成功
180    pub fn success_text(output: impl Into<String>, duration: Duration) -> Self {
181        Self {
182            success: true,
183            output: Some(ActionOutput::Text(output.into())),
184            duration,
185            error: None,
186            discovered_targets: Vec::new(),
187        }
188    }
189
190    /// 構造化データ出力で成功
191    pub fn success_structured(output: serde_json::Value, duration: Duration) -> Self {
192        Self {
193            success: true,
194            output: Some(ActionOutput::Structured(output)),
195            duration,
196            error: None,
197            discovered_targets: Vec::new(),
198        }
199    }
200
201    /// バイナリ出力で成功
202    pub fn success_binary(output: Vec<u8>, duration: Duration) -> Self {
203        Self {
204            success: true,
205            output: Some(ActionOutput::Binary(output)),
206            duration,
207            error: None,
208            discovered_targets: Vec::new(),
209        }
210    }
211
212    /// 後方互換: 文字列出力で成功(success_text のエイリアス)
213    pub fn success(output: impl Into<String>, duration: Duration) -> Self {
214        Self::success_text(output, duration)
215    }
216
217    /// 失敗
218    pub fn failure(error: String, duration: Duration) -> Self {
219        Self {
220            success: false,
221            output: None,
222            duration,
223            error: Some(error),
224            discovered_targets: Vec::new(),
225        }
226    }
227
228    /// 発見したターゲットを設定(Builder パターン)
229    pub fn with_discoveries(mut self, targets: Vec<String>) -> Self {
230        self.discovered_targets = targets;
231        self
232    }
233}
234
235// ============================================================================
236// ActionDef - Action 定義
237// ============================================================================
238
239/// パラメータ定義
240#[derive(Debug, Clone)]
241pub struct ParamDef {
242    /// パラメータ名
243    pub name: String,
244    /// 説明
245    pub description: String,
246    /// 必須かどうか
247    pub required: bool,
248}
249
250impl ParamDef {
251    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
252        Self {
253            name: name.into(),
254            description: description.into(),
255            required: true,
256        }
257    }
258
259    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
260        Self {
261            name: name.into(),
262            description: description.into(),
263            required: false,
264        }
265    }
266}
267
268/// パラメータバリアント定義
269///
270/// ExplorationSpace がアクションの後続ノードを展開する際、
271/// 指定されたパラメータ値のバリエーションを自動生成する。
272#[derive(Debug, Clone)]
273pub struct ParamVariants {
274    /// パラメータのキー名(例: "target", "direction")
275    pub key: String,
276    /// 取り得る値のリスト(例: ["north", "south", "east", "west"])
277    pub values: Vec<String>,
278}
279
280impl ParamVariants {
281    /// 新しい ParamVariants を作成
282    pub fn new(key: impl Into<String>, values: Vec<String>) -> Self {
283        Self {
284            key: key.into(),
285            values,
286        }
287    }
288}
289
290/// Action 定義
291#[derive(Debug, Clone)]
292pub struct ActionDef {
293    /// Action 名
294    pub name: String,
295    /// 説明(LLM プロンプト用)
296    pub description: String,
297    /// カテゴリ(探索空間への影響)
298    pub category: ActionCategory,
299    /// 所属 Group
300    pub groups: Vec<String>,
301    /// パラメータ定義
302    pub params: Vec<ParamDef>,
303    /// 出力例(LLM プロンプト用 JSON 形式)
304    pub example: Option<String>,
305    /// パラメータバリアント(ExplorationSpace で自動展開)
306    ///
307    /// 例: Move アクションで direction に対して ["north", "south", "east", "west"] を指定すると、
308    /// ExplorationSpace が後続ノード展開時に 4 つのバリアントを生成する。
309    ///
310    /// - `param_key`: パラメータ名(例: "target", "direction")
311    /// - `variants`: 取り得る値のリスト
312    pub param_variants: Option<ParamVariants>,
313}
314
315impl ActionDef {
316    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
317        Self {
318            name: name.into(),
319            description: description.into(),
320            category: ActionCategory::default(),
321            groups: Vec::new(),
322            params: Vec::new(),
323            example: None,
324            param_variants: None,
325        }
326    }
327
328    /// パラメータバリアントを設定
329    ///
330    /// ExplorationSpace が後続ノード展開時に、指定されたバリアントを自動生成する。
331    ///
332    /// # Example
333    ///
334    /// ```ignore
335    /// ActionDef::new("Move", "Move to adjacent cell")
336    ///     .param_variants("target", vec!["north", "south", "east", "west"])
337    /// ```
338    pub fn param_variants(
339        mut self,
340        key: impl Into<String>,
341        values: impl IntoIterator<Item = impl Into<String>>,
342    ) -> Self {
343        self.param_variants = Some(ParamVariants::new(
344            key,
345            values.into_iter().map(|v| v.into()).collect(),
346        ));
347        self
348    }
349
350    /// カテゴリを設定
351    pub fn category(mut self, category: ActionCategory) -> Self {
352        self.category = category;
353        self
354    }
355
356    /// NodeExpand カテゴリに設定(新しい探索対象を発見する Action)
357    pub fn node_expand(mut self) -> Self {
358        self.category = ActionCategory::NodeExpand;
359        self
360    }
361
362    /// NodeStateChange カテゴリに設定(既存 Node の状態を遷移させる Action)
363    pub fn node_state_change(mut self) -> Self {
364        self.category = ActionCategory::NodeStateChange;
365        self
366    }
367
368    /// Group を設定
369    pub fn groups<I, S>(mut self, groups: I) -> Self
370    where
371        I: IntoIterator<Item = S>,
372        S: Into<String>,
373    {
374        self.groups = groups.into_iter().map(|s| s.into()).collect();
375        self
376    }
377
378    /// Group を追加
379    pub fn group(mut self, group: impl Into<String>) -> Self {
380        self.groups.push(group.into());
381        self
382    }
383
384    /// パラメータを追加
385    pub fn param(mut self, param: ParamDef) -> Self {
386        self.params.push(param);
387        self
388    }
389
390    /// 必須パラメータを追加
391    pub fn required_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
392        self.param(ParamDef::required(name, description))
393    }
394
395    /// オプションパラメータを追加
396    pub fn optional_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
397        self.param(ParamDef::optional(name, description))
398    }
399
400    /// 出力例を設定(LLM プロンプト用 JSON 形式)
401    pub fn example(mut self, example: impl Into<String>) -> Self {
402        self.example = Some(example.into());
403        self
404    }
405
406    /// 指定した Group に所属しているか
407    pub fn has_group(&self, group: &str) -> bool {
408        self.groups.iter().any(|g| g == group)
409    }
410
411    /// 指定した Group のいずれかに所属しているか
412    pub fn has_any_group(&self, groups: &[&str]) -> bool {
413        groups.iter().any(|g| self.has_group(g))
414    }
415}
416
417// ============================================================================
418// ActionGroup - Action グループ(フィルタリング用)
419// ============================================================================
420
421/// Action グループ定義
422///
423/// include/exclude で Action をフィルタリングする。
424/// include が空の場合は全 Action が対象。
425#[derive(Debug, Clone, Default)]
426pub struct ActionGroup {
427    /// グループ名
428    pub name: String,
429    /// 含める Group(これらの Group を持つ Action を含む)
430    pub include_groups: Vec<String>,
431    /// 除外する Group(これらの Group を持つ Action を除外)
432    pub exclude_groups: Vec<String>,
433}
434
435impl ActionGroup {
436    pub fn new(name: impl Into<String>) -> Self {
437        Self {
438            name: name.into(),
439            include_groups: Vec::new(),
440            exclude_groups: Vec::new(),
441        }
442    }
443
444    /// 含める Group を設定
445    pub fn include<I, S>(mut self, groups: I) -> Self
446    where
447        I: IntoIterator<Item = S>,
448        S: Into<String>,
449    {
450        self.include_groups = groups.into_iter().map(|s| s.into()).collect();
451        self
452    }
453
454    /// 除外する Group を設定
455    pub fn exclude<I, S>(mut self, groups: I) -> Self
456    where
457        I: IntoIterator<Item = S>,
458        S: Into<String>,
459    {
460        self.exclude_groups = groups.into_iter().map(|s| s.into()).collect();
461        self
462    }
463
464    /// ActionDef がこの Group にマッチするか判定
465    pub fn matches(&self, action: &ActionDef) -> bool {
466        // exclude チェック(1つでも該当すれば除外)
467        if self.exclude_groups.iter().any(|g| action.has_group(g)) {
468            return false;
469        }
470
471        // include が空なら全て含む
472        if self.include_groups.is_empty() {
473            return true;
474        }
475
476        // include チェック(1つでも該当すれば含む)
477        self.include_groups.iter().any(|g| action.has_group(g))
478    }
479}
480
481// ============================================================================
482// ActionsConfig - Actions 統一管理
483// ============================================================================
484
485/// Actions 統一管理
486///
487/// Orchestrator 内で使用される Action を Protocol として統一管理する。
488/// Extensions に登録して Manager/Worker から参照する。
489#[derive(Debug, Clone, Default)]
490pub struct ActionsConfig {
491    /// Action 定義
492    actions: HashMap<String, ActionDef>,
493    /// Group 定義
494    groups: HashMap<String, ActionGroup>,
495}
496
497impl ActionsConfig {
498    pub fn new() -> Self {
499        Self::default()
500    }
501
502    /// Action を追加
503    pub fn action(mut self, name: impl Into<String>, def: ActionDef) -> Self {
504        let name = name.into();
505        let mut def = def;
506        def.name = name.clone();
507        self.actions.insert(name, def);
508        self
509    }
510
511    /// Action を追加(mutable)
512    pub fn add_action(&mut self, name: impl Into<String>, def: ActionDef) {
513        let name = name.into();
514        let mut def = def;
515        def.name = name.clone();
516        self.actions.insert(name, def);
517    }
518
519    /// Group を追加
520    pub fn group(mut self, name: impl Into<String>, group: ActionGroup) -> Self {
521        let name = name.into();
522        let mut group = group;
523        group.name = name.clone();
524        self.groups.insert(name, group);
525        self
526    }
527
528    /// Group を追加(mutable)
529    pub fn add_group(&mut self, name: impl Into<String>, group: ActionGroup) {
530        let name = name.into();
531        let mut group = group;
532        group.name = name.clone();
533        self.groups.insert(name, group);
534    }
535
536    // ========================================================================
537    // Query API
538    // ========================================================================
539
540    /// 全 Action 名を取得
541    pub fn all_action_names(&self) -> Vec<String> {
542        self.actions.keys().cloned().collect()
543    }
544
545    /// 全 Action 定義を取得
546    pub fn all_actions(&self) -> impl Iterator<Item = &ActionDef> {
547        self.actions.values()
548    }
549
550    /// Action 定義を取得
551    pub fn get(&self, name: &str) -> Option<&ActionDef> {
552        self.actions.get(name)
553    }
554
555    /// Group 定義を取得
556    pub fn get_group(&self, name: &str) -> Option<&ActionGroup> {
557        self.groups.get(name)
558    }
559
560    /// 指定 Group に所属する Action を取得
561    pub fn by_group(&self, group_name: &str) -> Vec<&ActionDef> {
562        if let Some(group) = self.groups.get(group_name) {
563            self.actions.values().filter(|a| group.matches(a)).collect()
564        } else {
565            // Group 定義がない場合は、直接その group を持つ Action を返す
566            self.actions
567                .values()
568                .filter(|a| a.has_group(group_name))
569                .collect()
570        }
571    }
572
573    /// 指定 Group の Action 名リストを取得(LLM candidates 用)
574    pub fn candidates_for(&self, group_name: &str) -> Vec<String> {
575        self.by_group(group_name)
576            .into_iter()
577            .map(|a| a.name.clone())
578            .collect()
579    }
580
581    /// 複数 Group のいずれかに所属する Action を取得
582    pub fn by_groups(&self, group_names: &[&str]) -> Vec<&ActionDef> {
583        self.actions
584            .values()
585            .filter(|a| group_names.iter().any(|g| a.has_group(g)))
586            .collect()
587    }
588
589    /// 複数 Group の Action 名リストを取得
590    pub fn candidates_by_groups(&self, group_names: &[&str]) -> Vec<String> {
591        self.by_groups(group_names)
592            .into_iter()
593            .map(|a| a.name.clone())
594            .collect()
595    }
596
597    /// NodeExpand カテゴリのアクション名を取得
598    ///
599    /// 探索系アクション(新しい探索対象を発見する)のみを返す。
600    /// 初期展開時に使用する。
601    pub fn node_expand_actions(&self) -> Vec<String> {
602        self.actions
603            .values()
604            .filter(|a| a.category == ActionCategory::NodeExpand)
605            .map(|a| a.name.clone())
606            .collect()
607    }
608
609    /// NodeStateChange カテゴリのアクション名を取得
610    pub fn node_state_change_actions(&self) -> Vec<String> {
611        self.actions
612            .values()
613            .filter(|a| a.category == ActionCategory::NodeStateChange)
614            .map(|a| a.name.clone())
615            .collect()
616    }
617
618    /// 指定アクションのパラメータバリアントを取得
619    ///
620    /// ExplorationSpace がノード展開時にバリアントを生成するために使用。
621    ///
622    /// # Returns
623    ///
624    /// - `Some((key, values))`: パラメータバリアントが定義されている場合
625    /// - `None`: 定義されていない場合
626    pub fn param_variants(&self, action_name: &str) -> Option<(&str, &[String])> {
627        self.actions
628            .get(action_name)
629            .and_then(|a| a.param_variants.as_ref())
630            .map(|pv| (pv.key.as_str(), pv.values.as_slice()))
631    }
632
633    // ========================================================================
634    // Action Builder
635    // ========================================================================
636
637    /// Action を構築
638    ///
639    /// DecisionResponse から Action を構築する際に使用。
640    /// 登録されていない Action 名の場合は None を返す。
641    pub fn build_action(
642        &self,
643        name: &str,
644        target: Option<String>,
645        args: HashMap<String, String>,
646    ) -> Option<Action> {
647        self.actions.get(name).map(|_def| Action {
648            name: name.to_string(),
649            params: ActionParams {
650                target,
651                args,
652                data: Vec::new(),
653            },
654        })
655    }
656
657    /// Action を構築(登録されていなくても作成)
658    ///
659    /// バリデーションなしで Action を作成する。
660    /// 動的な Action 名に対応する場合に使用。
661    pub fn build_action_unchecked(
662        &self,
663        name: impl Into<String>,
664        target: Option<String>,
665        args: HashMap<String, String>,
666    ) -> Action {
667        Action {
668            name: name.into(),
669            params: ActionParams {
670                target,
671                args,
672                data: Vec::new(),
673            },
674        }
675    }
676
677    // ========================================================================
678    // Validation
679    // ========================================================================
680
681    /// Action をバリデート
682    pub fn validate(&self, action: &Action) -> Result<(), ActionValidationError> {
683        let def = self
684            .actions
685            .get(&action.name)
686            .ok_or_else(|| ActionValidationError::UnknownAction(action.name.clone()))?;
687
688        // 必須パラメータチェック
689        for param in &def.params {
690            if param.required && !action.params.args.contains_key(&param.name) {
691                return Err(ActionValidationError::MissingParam(param.name.clone()));
692            }
693        }
694
695        Ok(())
696    }
697}
698
699/// Action バリデーションエラー
700#[derive(Debug, Clone, thiserror::Error)]
701pub enum ActionValidationError {
702    #[error("Unknown action: {0}")]
703    UnknownAction(String),
704
705    #[error("Missing required parameter: {0}")]
706    MissingParam(String),
707
708    #[error("Invalid parameter value: {0}")]
709    InvalidParam(String),
710}
711
712// ============================================================================
713// ParamResolver - パラメータ解決の共通化
714// ============================================================================
715
716/// Action パラメータ解決ヘルパー
717///
718/// Environment がアクションパラメータを取得する際の共通パターンを提供する。
719/// `args[key]` と `target` の優先順位を統一し、空文字列の扱いを正規化する。
720///
721/// # Example
722///
723/// ```ignore
724/// fn handle_read_logs(&self, action: &Action) -> WorkResult {
725///     let resolver = ParamResolver::new(action);
726///
727///     // 必須パラメータ: なければエラー
728///     let service = match resolver.require("service") {
729///         Ok(s) => s,
730///         Err(e) => return WorkResult::env_failure(e.to_string()),
731///     };
732///
733///     // オプショナルパラメータ
734///     let limit = resolver.get("limit");
735///
736///     // ...
737/// }
738/// ```
739#[derive(Debug)]
740pub struct ParamResolver<'a> {
741    action: &'a Action,
742}
743
744impl<'a> ParamResolver<'a> {
745    /// 新しい ParamResolver を作成
746    pub fn new(action: &'a Action) -> Self {
747        Self { action }
748    }
749
750    /// パラメータを取得(args[key] を優先、なければ target をフォールバック)
751    ///
752    /// 空文字列は None として扱う。
753    ///
754    /// # Arguments
755    ///
756    /// * `key` - パラメータ名(例: "service", "path")
757    ///
758    /// # Returns
759    ///
760    /// - `Some(&str)`: 値が存在し、空でない場合
761    /// - `None`: 値が存在しないか、空文字列の場合
762    pub fn get(&self, key: &str) -> Option<&str> {
763        // 1. args[key] を優先
764        if let Some(value) = self.action.params.args.get(key) {
765            if !value.is_empty() {
766                return Some(value.as_str());
767            }
768        }
769
770        // 2. target をフォールバック
771        if let Some(ref target) = self.action.params.target {
772            if !target.is_empty() {
773                return Some(target.as_str());
774            }
775        }
776
777        None
778    }
779
780    /// 必須パラメータを取得(なければエラー)
781    ///
782    /// # Arguments
783    ///
784    /// * `key` - パラメータ名
785    ///
786    /// # Returns
787    ///
788    /// - `Ok(&str)`: 値が存在する場合
789    /// - `Err(ActionValidationError::MissingParam)`: 値が存在しない場合
790    pub fn require(&self, key: &str) -> Result<&str, ActionValidationError> {
791        self.get(key)
792            .ok_or_else(|| ActionValidationError::MissingParam(key.to_string()))
793    }
794
795    /// target を優先して取得(なければ args[key] をフォールバック)
796    ///
797    /// LLM が `target` フィールドにパラメータを入れる場合に使用。
798    ///
799    /// # Arguments
800    ///
801    /// * `key` - フォールバック用のパラメータ名
802    pub fn get_target_first(&self, key: &str) -> Option<&str> {
803        // 1. target を優先
804        if let Some(ref target) = self.action.params.target {
805            if !target.is_empty() {
806                return Some(target.as_str());
807            }
808        }
809
810        // 2. args[key] をフォールバック
811        if let Some(value) = self.action.params.args.get(key) {
812            if !value.is_empty() {
813                return Some(value.as_str());
814            }
815        }
816
817        None
818    }
819
820    /// target を優先して必須パラメータを取得
821    pub fn require_target_first(&self, key: &str) -> Result<&str, ActionValidationError> {
822        self.get_target_first(key)
823            .ok_or_else(|| ActionValidationError::MissingParam(key.to_string()))
824    }
825
826    /// target のみを取得(args は参照しない)
827    pub fn target(&self) -> Option<&str> {
828        self.action
829            .params
830            .target
831            .as_deref()
832            .filter(|s| !s.is_empty())
833    }
834
835    /// 指定した key の args のみを取得(target は参照しない)
836    pub fn arg(&self, key: &str) -> Option<&str> {
837        self.action
838            .params
839            .args
840            .get(key)
841            .map(|s| s.as_str())
842            .filter(|s| !s.is_empty())
843    }
844
845    /// Action 名を取得
846    pub fn action_name(&self) -> &str {
847        &self.action.name
848    }
849
850    /// 元の Action への参照を取得
851    pub fn action(&self) -> &Action {
852        self.action
853    }
854}
855
856// ============================================================================
857// Tests
858// ============================================================================
859
860#[cfg(test)]
861mod tests {
862    use super::*;
863
864    fn sample_config() -> ActionsConfig {
865        ActionsConfig::new()
866            .action(
867                "read_file",
868                ActionDef::new("read_file", "ファイルを読み込む")
869                    .groups(["file_ops", "exploration"])
870                    .required_param("path", "ファイルパス"),
871            )
872            .action(
873                "grep",
874                ActionDef::new("grep", "パターン検索")
875                    .groups(["search", "exploration"])
876                    .required_param("pattern", "検索パターン"),
877            )
878            .action(
879                "write_file",
880                ActionDef::new("write_file", "ファイルを書き込む")
881                    .groups(["file_ops", "mutation"])
882                    .required_param("path", "ファイルパス")
883                    .required_param("content", "内容"),
884            )
885            .action(
886                "escalate",
887                ActionDef::new("escalate", "Manager に報告").groups(["control"]),
888            )
889            .group(
890                "readonly",
891                ActionGroup::new("readonly")
892                    .include(["exploration", "search"])
893                    .exclude(["mutation"]),
894            )
895            .group(
896                "all",
897                ActionGroup::new("all"), // include/exclude なし = 全部
898            )
899    }
900
901    #[test]
902    fn test_by_group_direct() {
903        let cfg = sample_config();
904
905        // 直接 group 名で取得
906        let file_ops = cfg.by_group("file_ops");
907        assert_eq!(file_ops.len(), 2);
908
909        let exploration = cfg.by_group("exploration");
910        assert_eq!(exploration.len(), 2);
911    }
912
913    #[test]
914    fn test_by_group_defined() {
915        let cfg = sample_config();
916
917        // 定義された Group で取得
918        let readonly = cfg.candidates_for("readonly");
919        assert!(readonly.contains(&"read_file".to_string()));
920        assert!(readonly.contains(&"grep".to_string()));
921        assert!(!readonly.contains(&"write_file".to_string())); // mutation なので除外
922
923        let all = cfg.candidates_for("all");
924        assert_eq!(all.len(), 4);
925    }
926
927    #[test]
928    fn test_build_action() {
929        let cfg = sample_config();
930
931        let action = cfg.build_action(
932            "read_file",
933            Some("/path/to/file".to_string()),
934            HashMap::new(),
935        );
936        assert!(action.is_some());
937        assert_eq!(action.unwrap().name, "read_file");
938
939        let unknown = cfg.build_action("unknown", None, HashMap::new());
940        assert!(unknown.is_none());
941    }
942
943    #[test]
944    fn test_validate() {
945        let cfg = sample_config();
946
947        // 有効な Action
948        let action = Action {
949            name: "read_file".to_string(),
950            params: ActionParams {
951                target: None,
952                args: [("path".to_string(), "/tmp/test".to_string())]
953                    .into_iter()
954                    .collect(),
955                data: Vec::new(),
956            },
957        };
958        assert!(cfg.validate(&action).is_ok());
959
960        // 必須パラメータ不足
961        let action_missing = Action {
962            name: "read_file".to_string(),
963            params: ActionParams::default(),
964        };
965        assert!(matches!(
966            cfg.validate(&action_missing),
967            Err(ActionValidationError::MissingParam(_))
968        ));
969
970        // 未知の Action
971        let unknown = Action {
972            name: "unknown".to_string(),
973            params: ActionParams::default(),
974        };
975        assert!(matches!(
976            cfg.validate(&unknown),
977            Err(ActionValidationError::UnknownAction(_))
978        ));
979    }
980
981    // ========================================================================
982    // ParamResolver Tests
983    // ========================================================================
984
985    fn make_action(name: &str, target: Option<&str>, args: Vec<(&str, &str)>) -> Action {
986        Action {
987            name: name.to_string(),
988            params: ActionParams {
989                target: target.map(|s| s.to_string()),
990                args: args
991                    .into_iter()
992                    .map(|(k, v)| (k.to_string(), v.to_string()))
993                    .collect(),
994                data: Vec::new(),
995            },
996        }
997    }
998
999    #[test]
1000    fn test_param_resolver_get_from_args() {
1001        // args に値がある場合
1002        let action = make_action("test", None, vec![("service", "user-service")]);
1003        let resolver = ParamResolver::new(&action);
1004
1005        assert_eq!(resolver.get("service"), Some("user-service"));
1006        assert_eq!(resolver.get("unknown"), None);
1007    }
1008
1009    #[test]
1010    fn test_param_resolver_get_fallback_to_target() {
1011        // args にない場合は target にフォールバック
1012        let action = make_action("test", Some("user-service"), vec![]);
1013        let resolver = ParamResolver::new(&action);
1014
1015        assert_eq!(resolver.get("service"), Some("user-service"));
1016    }
1017
1018    #[test]
1019    fn test_param_resolver_args_priority_over_target() {
1020        // args が target より優先
1021        let action = make_action(
1022            "test",
1023            Some("target-service"),
1024            vec![("service", "args-service")],
1025        );
1026        let resolver = ParamResolver::new(&action);
1027
1028        assert_eq!(resolver.get("service"), Some("args-service"));
1029    }
1030
1031    #[test]
1032    fn test_param_resolver_empty_string_is_none() {
1033        // 空文字列は None として扱う
1034        let action = make_action("test", Some(""), vec![("service", "")]);
1035        let resolver = ParamResolver::new(&action);
1036
1037        assert_eq!(resolver.get("service"), None);
1038    }
1039
1040    #[test]
1041    fn test_param_resolver_empty_args_fallback_to_target() {
1042        // args が空文字列の場合は target にフォールバック
1043        let action = make_action("test", Some("target-service"), vec![("service", "")]);
1044        let resolver = ParamResolver::new(&action);
1045
1046        assert_eq!(resolver.get("service"), Some("target-service"));
1047    }
1048
1049    #[test]
1050    fn test_param_resolver_require_success() {
1051        let action = make_action("test", Some("user-service"), vec![]);
1052        let resolver = ParamResolver::new(&action);
1053
1054        assert_eq!(resolver.require("service").unwrap(), "user-service");
1055    }
1056
1057    #[test]
1058    fn test_param_resolver_require_failure() {
1059        let action = make_action("test", None, vec![]);
1060        let resolver = ParamResolver::new(&action);
1061
1062        let result = resolver.require("service");
1063        assert!(matches!(
1064            result,
1065            Err(ActionValidationError::MissingParam(ref s)) if s == "service"
1066        ));
1067    }
1068
1069    #[test]
1070    fn test_param_resolver_get_target_first() {
1071        // target が優先される
1072        let action = make_action(
1073            "test",
1074            Some("target-service"),
1075            vec![("service", "args-service")],
1076        );
1077        let resolver = ParamResolver::new(&action);
1078
1079        assert_eq!(resolver.get_target_first("service"), Some("target-service"));
1080    }
1081
1082    #[test]
1083    fn test_param_resolver_get_target_first_fallback() {
1084        // target がない場合は args にフォールバック
1085        let action = make_action("test", None, vec![("service", "args-service")]);
1086        let resolver = ParamResolver::new(&action);
1087
1088        assert_eq!(resolver.get_target_first("service"), Some("args-service"));
1089    }
1090
1091    #[test]
1092    fn test_param_resolver_target_only() {
1093        let action = make_action("test", Some("my-target"), vec![("service", "args-service")]);
1094        let resolver = ParamResolver::new(&action);
1095
1096        // target() は args を参照しない
1097        assert_eq!(resolver.target(), Some("my-target"));
1098    }
1099
1100    #[test]
1101    fn test_param_resolver_arg_only() {
1102        let action = make_action("test", Some("my-target"), vec![("service", "args-service")]);
1103        let resolver = ParamResolver::new(&action);
1104
1105        // arg() は target を参照しない
1106        assert_eq!(resolver.arg("service"), Some("args-service"));
1107        assert_eq!(resolver.arg("unknown"), None);
1108    }
1109}