1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
75pub enum ActionCategory {
76 #[default]
78 NodeExpand,
79 NodeStateChange,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Action {
86 pub name: String,
87 pub params: ActionParams,
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct ActionParams {
93 pub target: Option<String>,
95 pub args: HashMap<String, String>,
97 pub data: Vec<u8>,
99}
100
101#[derive(Debug, Clone)]
112pub enum ActionOutput {
113 Text(String),
115
116 Structured(serde_json::Value),
118
119 Binary(Vec<u8>),
121}
122
123impl ActionOutput {
124 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 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 pub fn text(&self) -> Option<&str> {
144 match self {
145 Self::Text(s) => Some(s),
146 _ => None,
147 }
148 }
149
150 pub fn structured(&self) -> Option<&serde_json::Value> {
152 match self {
153 Self::Structured(v) => Some(v),
154 _ => None,
155 }
156 }
157}
158
159#[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 pub discovered_targets: Vec<String>,
176}
177
178impl ActionResult {
179 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 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 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 pub fn success(output: impl Into<String>, duration: Duration) -> Self {
214 Self::success_text(output, duration)
215 }
216
217 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 pub fn with_discoveries(mut self, targets: Vec<String>) -> Self {
230 self.discovered_targets = targets;
231 self
232 }
233}
234
235#[derive(Debug, Clone)]
241pub struct ParamDef {
242 pub name: String,
244 pub description: String,
246 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#[derive(Debug, Clone)]
273pub struct ParamVariants {
274 pub key: String,
276 pub values: Vec<String>,
278}
279
280impl ParamVariants {
281 pub fn new(key: impl Into<String>, values: Vec<String>) -> Self {
283 Self {
284 key: key.into(),
285 values,
286 }
287 }
288}
289
290#[derive(Debug, Clone)]
292pub struct ActionDef {
293 pub name: String,
295 pub description: String,
297 pub category: ActionCategory,
299 pub groups: Vec<String>,
301 pub params: Vec<ParamDef>,
303 pub example: Option<String>,
305 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 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 pub fn category(mut self, category: ActionCategory) -> Self {
352 self.category = category;
353 self
354 }
355
356 pub fn node_expand(mut self) -> Self {
358 self.category = ActionCategory::NodeExpand;
359 self
360 }
361
362 pub fn node_state_change(mut self) -> Self {
364 self.category = ActionCategory::NodeStateChange;
365 self
366 }
367
368 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 pub fn group(mut self, group: impl Into<String>) -> Self {
380 self.groups.push(group.into());
381 self
382 }
383
384 pub fn param(mut self, param: ParamDef) -> Self {
386 self.params.push(param);
387 self
388 }
389
390 pub fn required_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
392 self.param(ParamDef::required(name, description))
393 }
394
395 pub fn optional_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
397 self.param(ParamDef::optional(name, description))
398 }
399
400 pub fn example(mut self, example: impl Into<String>) -> Self {
402 self.example = Some(example.into());
403 self
404 }
405
406 pub fn has_group(&self, group: &str) -> bool {
408 self.groups.iter().any(|g| g == group)
409 }
410
411 pub fn has_any_group(&self, groups: &[&str]) -> bool {
413 groups.iter().any(|g| self.has_group(g))
414 }
415}
416
417#[derive(Debug, Clone, Default)]
426pub struct ActionGroup {
427 pub name: String,
429 pub include_groups: Vec<String>,
431 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 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 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 pub fn matches(&self, action: &ActionDef) -> bool {
466 if self.exclude_groups.iter().any(|g| action.has_group(g)) {
468 return false;
469 }
470
471 if self.include_groups.is_empty() {
473 return true;
474 }
475
476 self.include_groups.iter().any(|g| action.has_group(g))
478 }
479}
480
481#[derive(Debug, Clone, Default)]
490pub struct ActionsConfig {
491 actions: HashMap<String, ActionDef>,
493 groups: HashMap<String, ActionGroup>,
495}
496
497impl ActionsConfig {
498 pub fn new() -> Self {
499 Self::default()
500 }
501
502 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 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 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 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 pub fn all_action_names(&self) -> Vec<String> {
542 self.actions.keys().cloned().collect()
543 }
544
545 pub fn all_actions(&self) -> impl Iterator<Item = &ActionDef> {
547 self.actions.values()
548 }
549
550 pub fn get(&self, name: &str) -> Option<&ActionDef> {
552 self.actions.get(name)
553 }
554
555 pub fn get_group(&self, name: &str) -> Option<&ActionGroup> {
557 self.groups.get(name)
558 }
559
560 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 self.actions
567 .values()
568 .filter(|a| a.has_group(group_name))
569 .collect()
570 }
571 }
572
573 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 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 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 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 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 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 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 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 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 for param in &def.params {
690 if param.required && !action.params.args.contains_key(¶m.name) {
691 return Err(ActionValidationError::MissingParam(param.name.clone()));
692 }
693 }
694
695 Ok(())
696 }
697}
698
699#[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#[derive(Debug)]
740pub struct ParamResolver<'a> {
741 action: &'a Action,
742}
743
744impl<'a> ParamResolver<'a> {
745 pub fn new(action: &'a Action) -> Self {
747 Self { action }
748 }
749
750 pub fn get(&self, key: &str) -> Option<&str> {
763 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 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 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 pub fn get_target_first(&self, key: &str) -> Option<&str> {
803 if let Some(ref target) = self.action.params.target {
805 if !target.is_empty() {
806 return Some(target.as_str());
807 }
808 }
809
810 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 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 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 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 pub fn action_name(&self) -> &str {
847 &self.action.name
848 }
849
850 pub fn action(&self) -> &Action {
852 self.action
853 }
854}
855
856#[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"), )
899 }
900
901 #[test]
902 fn test_by_group_direct() {
903 let cfg = sample_config();
904
905 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 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())); 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(resolver.arg("service"), Some("args-service"));
1107 assert_eq!(resolver.arg("unknown"), None);
1108 }
1109}