1use std::collections::HashMap;
40use std::time::Duration;
41
42use serde::{Deserialize, Serialize};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
71pub enum ActionCategory {
72 #[default]
74 NodeExpand,
75 NodeStateChange,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Action {
82 pub name: String,
83 pub params: ActionParams,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct ActionParams {
89 pub target: Option<String>,
91 pub args: HashMap<String, String>,
93 pub data: Vec<u8>,
95}
96
97#[derive(Debug, Clone)]
108pub enum ActionOutput {
109 Text(String),
111
112 Structured(serde_json::Value),
114
115 Binary(Vec<u8>),
117}
118
119impl ActionOutput {
120 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 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 pub fn text(&self) -> Option<&str> {
140 match self {
141 Self::Text(s) => Some(s),
142 _ => None,
143 }
144 }
145
146 pub fn structured(&self) -> Option<&serde_json::Value> {
148 match self {
149 Self::Structured(v) => Some(v),
150 _ => None,
151 }
152 }
153}
154
155#[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 pub discovered_targets: Vec<String>,
172}
173
174impl ActionResult {
175 pub fn success_text(output: impl Into<String>, duration: Duration) -> Self {
177 Self {
178 success: true,
179 output: Some(ActionOutput::Text(output.into())),
180 duration,
181 error: None,
182 discovered_targets: Vec::new(),
183 }
184 }
185
186 pub fn success_structured(output: serde_json::Value, duration: Duration) -> Self {
188 Self {
189 success: true,
190 output: Some(ActionOutput::Structured(output)),
191 duration,
192 error: None,
193 discovered_targets: Vec::new(),
194 }
195 }
196
197 pub fn success_binary(output: Vec<u8>, duration: Duration) -> Self {
199 Self {
200 success: true,
201 output: Some(ActionOutput::Binary(output)),
202 duration,
203 error: None,
204 discovered_targets: Vec::new(),
205 }
206 }
207
208 pub fn success(output: impl Into<String>, duration: Duration) -> Self {
210 Self::success_text(output, duration)
211 }
212
213 pub fn failure(error: String, duration: Duration) -> Self {
215 Self {
216 success: false,
217 output: None,
218 duration,
219 error: Some(error),
220 discovered_targets: Vec::new(),
221 }
222 }
223
224 pub fn with_discoveries(mut self, targets: Vec<String>) -> Self {
226 self.discovered_targets = targets;
227 self
228 }
229}
230
231#[derive(Debug, Clone)]
237pub struct ParamDef {
238 pub name: String,
240 pub description: String,
242 pub required: bool,
244}
245
246impl ParamDef {
247 pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
248 Self {
249 name: name.into(),
250 description: description.into(),
251 required: true,
252 }
253 }
254
255 pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
256 Self {
257 name: name.into(),
258 description: description.into(),
259 required: false,
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
269pub struct ParamVariants {
270 pub key: String,
272 pub values: Vec<String>,
274}
275
276impl ParamVariants {
277 pub fn new(key: impl Into<String>, values: Vec<String>) -> Self {
279 Self {
280 key: key.into(),
281 values,
282 }
283 }
284}
285
286#[derive(Debug, Clone)]
288pub struct ActionDef {
289 pub name: String,
291 pub description: String,
293 pub category: ActionCategory,
295 pub groups: Vec<String>,
297 pub params: Vec<ParamDef>,
299 pub example: Option<String>,
301 pub param_variants: Option<ParamVariants>,
309}
310
311impl ActionDef {
312 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
313 Self {
314 name: name.into(),
315 description: description.into(),
316 category: ActionCategory::default(),
317 groups: Vec::new(),
318 params: Vec::new(),
319 example: None,
320 param_variants: None,
321 }
322 }
323
324 pub fn param_variants(
335 mut self,
336 key: impl Into<String>,
337 values: impl IntoIterator<Item = impl Into<String>>,
338 ) -> Self {
339 self.param_variants = Some(ParamVariants::new(
340 key,
341 values.into_iter().map(|v| v.into()).collect(),
342 ));
343 self
344 }
345
346 pub fn category(mut self, category: ActionCategory) -> Self {
348 self.category = category;
349 self
350 }
351
352 pub fn node_expand(mut self) -> Self {
354 self.category = ActionCategory::NodeExpand;
355 self
356 }
357
358 pub fn node_state_change(mut self) -> Self {
360 self.category = ActionCategory::NodeStateChange;
361 self
362 }
363
364 pub fn groups<I, S>(mut self, groups: I) -> Self
366 where
367 I: IntoIterator<Item = S>,
368 S: Into<String>,
369 {
370 self.groups = groups.into_iter().map(|s| s.into()).collect();
371 self
372 }
373
374 pub fn group(mut self, group: impl Into<String>) -> Self {
376 self.groups.push(group.into());
377 self
378 }
379
380 pub fn param(mut self, param: ParamDef) -> Self {
382 self.params.push(param);
383 self
384 }
385
386 pub fn required_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
388 self.param(ParamDef::required(name, description))
389 }
390
391 pub fn optional_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
393 self.param(ParamDef::optional(name, description))
394 }
395
396 pub fn example(mut self, example: impl Into<String>) -> Self {
398 self.example = Some(example.into());
399 self
400 }
401
402 pub fn has_group(&self, group: &str) -> bool {
404 self.groups.iter().any(|g| g == group)
405 }
406
407 pub fn has_any_group(&self, groups: &[&str]) -> bool {
409 groups.iter().any(|g| self.has_group(g))
410 }
411}
412
413#[derive(Debug, Clone, Default)]
422pub struct ActionGroup {
423 pub name: String,
425 pub include_groups: Vec<String>,
427 pub exclude_groups: Vec<String>,
429}
430
431impl ActionGroup {
432 pub fn new(name: impl Into<String>) -> Self {
433 Self {
434 name: name.into(),
435 include_groups: Vec::new(),
436 exclude_groups: Vec::new(),
437 }
438 }
439
440 pub fn include<I, S>(mut self, groups: I) -> Self
442 where
443 I: IntoIterator<Item = S>,
444 S: Into<String>,
445 {
446 self.include_groups = groups.into_iter().map(|s| s.into()).collect();
447 self
448 }
449
450 pub fn exclude<I, S>(mut self, groups: I) -> Self
452 where
453 I: IntoIterator<Item = S>,
454 S: Into<String>,
455 {
456 self.exclude_groups = groups.into_iter().map(|s| s.into()).collect();
457 self
458 }
459
460 pub fn matches(&self, action: &ActionDef) -> bool {
462 if self.exclude_groups.iter().any(|g| action.has_group(g)) {
464 return false;
465 }
466
467 if self.include_groups.is_empty() {
469 return true;
470 }
471
472 self.include_groups.iter().any(|g| action.has_group(g))
474 }
475}
476
477#[derive(Debug, Clone, Default)]
486pub struct ActionsConfig {
487 actions: HashMap<String, ActionDef>,
489 groups: HashMap<String, ActionGroup>,
491}
492
493impl ActionsConfig {
494 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn action(mut self, name: impl Into<String>, def: ActionDef) -> Self {
500 let name = name.into();
501 let mut def = def;
502 def.name = name.clone();
503 self.actions.insert(name, def);
504 self
505 }
506
507 pub fn add_action(&mut self, name: impl Into<String>, def: ActionDef) {
509 let name = name.into();
510 let mut def = def;
511 def.name = name.clone();
512 self.actions.insert(name, def);
513 }
514
515 pub fn group(mut self, name: impl Into<String>, group: ActionGroup) -> Self {
517 let name = name.into();
518 let mut group = group;
519 group.name = name.clone();
520 self.groups.insert(name, group);
521 self
522 }
523
524 pub fn add_group(&mut self, name: impl Into<String>, group: ActionGroup) {
526 let name = name.into();
527 let mut group = group;
528 group.name = name.clone();
529 self.groups.insert(name, group);
530 }
531
532 pub fn all_action_names(&self) -> Vec<String> {
538 self.actions.keys().cloned().collect()
539 }
540
541 pub fn all_actions(&self) -> impl Iterator<Item = &ActionDef> {
543 self.actions.values()
544 }
545
546 pub fn get(&self, name: &str) -> Option<&ActionDef> {
548 self.actions.get(name)
549 }
550
551 pub fn get_group(&self, name: &str) -> Option<&ActionGroup> {
553 self.groups.get(name)
554 }
555
556 pub fn by_group(&self, group_name: &str) -> Vec<&ActionDef> {
558 if let Some(group) = self.groups.get(group_name) {
559 self.actions.values().filter(|a| group.matches(a)).collect()
560 } else {
561 self.actions
563 .values()
564 .filter(|a| a.has_group(group_name))
565 .collect()
566 }
567 }
568
569 pub fn candidates_for(&self, group_name: &str) -> Vec<String> {
571 self.by_group(group_name)
572 .into_iter()
573 .map(|a| a.name.clone())
574 .collect()
575 }
576
577 pub fn by_groups(&self, group_names: &[&str]) -> Vec<&ActionDef> {
579 self.actions
580 .values()
581 .filter(|a| group_names.iter().any(|g| a.has_group(g)))
582 .collect()
583 }
584
585 pub fn candidates_by_groups(&self, group_names: &[&str]) -> Vec<String> {
587 self.by_groups(group_names)
588 .into_iter()
589 .map(|a| a.name.clone())
590 .collect()
591 }
592
593 pub fn node_expand_actions(&self) -> Vec<String> {
598 self.actions
599 .values()
600 .filter(|a| a.category == ActionCategory::NodeExpand)
601 .map(|a| a.name.clone())
602 .collect()
603 }
604
605 pub fn node_state_change_actions(&self) -> Vec<String> {
607 self.actions
608 .values()
609 .filter(|a| a.category == ActionCategory::NodeStateChange)
610 .map(|a| a.name.clone())
611 .collect()
612 }
613
614 pub fn param_variants(&self, action_name: &str) -> Option<(&str, &[String])> {
623 self.actions
624 .get(action_name)
625 .and_then(|a| a.param_variants.as_ref())
626 .map(|pv| (pv.key.as_str(), pv.values.as_slice()))
627 }
628
629 pub fn build_action(
638 &self,
639 name: &str,
640 target: Option<String>,
641 args: HashMap<String, String>,
642 ) -> Option<Action> {
643 self.actions.get(name).map(|_def| Action {
644 name: name.to_string(),
645 params: ActionParams {
646 target,
647 args,
648 data: Vec::new(),
649 },
650 })
651 }
652
653 pub fn build_action_unchecked(
658 &self,
659 name: impl Into<String>,
660 target: Option<String>,
661 args: HashMap<String, String>,
662 ) -> Action {
663 Action {
664 name: name.into(),
665 params: ActionParams {
666 target,
667 args,
668 data: Vec::new(),
669 },
670 }
671 }
672
673 pub fn validate(&self, action: &Action) -> Result<(), ActionValidationError> {
679 let def = self
680 .actions
681 .get(&action.name)
682 .ok_or_else(|| ActionValidationError::UnknownAction(action.name.clone()))?;
683
684 for param in &def.params {
686 if param.required && !action.params.args.contains_key(¶m.name) {
687 return Err(ActionValidationError::MissingParam(param.name.clone()));
688 }
689 }
690
691 Ok(())
692 }
693}
694
695#[derive(Debug, Clone, thiserror::Error)]
697pub enum ActionValidationError {
698 #[error("Unknown action: {0}")]
699 UnknownAction(String),
700
701 #[error("Missing required parameter: {0}")]
702 MissingParam(String),
703
704 #[error("Invalid parameter value: {0}")]
705 InvalidParam(String),
706}
707
708#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn sample_config() -> ActionsConfig {
717 ActionsConfig::new()
718 .action(
719 "read_file",
720 ActionDef::new("read_file", "ファイルを読み込む")
721 .groups(["file_ops", "exploration"])
722 .required_param("path", "ファイルパス"),
723 )
724 .action(
725 "grep",
726 ActionDef::new("grep", "パターン検索")
727 .groups(["search", "exploration"])
728 .required_param("pattern", "検索パターン"),
729 )
730 .action(
731 "write_file",
732 ActionDef::new("write_file", "ファイルを書き込む")
733 .groups(["file_ops", "mutation"])
734 .required_param("path", "ファイルパス")
735 .required_param("content", "内容"),
736 )
737 .action(
738 "escalate",
739 ActionDef::new("escalate", "Manager に報告").groups(["control"]),
740 )
741 .group(
742 "readonly",
743 ActionGroup::new("readonly")
744 .include(["exploration", "search"])
745 .exclude(["mutation"]),
746 )
747 .group(
748 "all",
749 ActionGroup::new("all"), )
751 }
752
753 #[test]
754 fn test_by_group_direct() {
755 let cfg = sample_config();
756
757 let file_ops = cfg.by_group("file_ops");
759 assert_eq!(file_ops.len(), 2);
760
761 let exploration = cfg.by_group("exploration");
762 assert_eq!(exploration.len(), 2);
763 }
764
765 #[test]
766 fn test_by_group_defined() {
767 let cfg = sample_config();
768
769 let readonly = cfg.candidates_for("readonly");
771 assert!(readonly.contains(&"read_file".to_string()));
772 assert!(readonly.contains(&"grep".to_string()));
773 assert!(!readonly.contains(&"write_file".to_string())); let all = cfg.candidates_for("all");
776 assert_eq!(all.len(), 4);
777 }
778
779 #[test]
780 fn test_build_action() {
781 let cfg = sample_config();
782
783 let action = cfg.build_action(
784 "read_file",
785 Some("/path/to/file".to_string()),
786 HashMap::new(),
787 );
788 assert!(action.is_some());
789 assert_eq!(action.unwrap().name, "read_file");
790
791 let unknown = cfg.build_action("unknown", None, HashMap::new());
792 assert!(unknown.is_none());
793 }
794
795 #[test]
796 fn test_validate() {
797 let cfg = sample_config();
798
799 let action = Action {
801 name: "read_file".to_string(),
802 params: ActionParams {
803 target: None,
804 args: [("path".to_string(), "/tmp/test".to_string())]
805 .into_iter()
806 .collect(),
807 data: Vec::new(),
808 },
809 };
810 assert!(cfg.validate(&action).is_ok());
811
812 let action_missing = Action {
814 name: "read_file".to_string(),
815 params: ActionParams::default(),
816 };
817 assert!(matches!(
818 cfg.validate(&action_missing),
819 Err(ActionValidationError::MissingParam(_))
820 ));
821
822 let unknown = Action {
824 name: "unknown".to_string(),
825 params: ActionParams::default(),
826 };
827 assert!(matches!(
828 cfg.validate(&unknown),
829 Err(ActionValidationError::UnknownAction(_))
830 ));
831 }
832}