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}
167
168impl ActionResult {
169 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 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 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 pub fn success(output: impl Into<String>, duration: Duration) -> Self {
201 Self::success_text(output, duration)
202 }
203
204 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#[derive(Debug, Clone)]
221pub struct ParamDef {
222 pub name: String,
224 pub description: String,
226 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#[derive(Debug, Clone)]
253pub struct ParamVariants {
254 pub key: String,
256 pub values: Vec<String>,
258}
259
260impl ParamVariants {
261 pub fn new(key: impl Into<String>, values: Vec<String>) -> Self {
263 Self {
264 key: key.into(),
265 values,
266 }
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct ActionDef {
273 pub name: String,
275 pub description: String,
277 pub category: ActionCategory,
279 pub groups: Vec<String>,
281 pub params: Vec<ParamDef>,
283 pub example: Option<String>,
285 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 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 pub fn category(mut self, category: ActionCategory) -> Self {
332 self.category = category;
333 self
334 }
335
336 pub fn node_expand(mut self) -> Self {
338 self.category = ActionCategory::NodeExpand;
339 self
340 }
341
342 pub fn node_state_change(mut self) -> Self {
344 self.category = ActionCategory::NodeStateChange;
345 self
346 }
347
348 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 pub fn group(mut self, group: impl Into<String>) -> Self {
360 self.groups.push(group.into());
361 self
362 }
363
364 pub fn param(mut self, param: ParamDef) -> Self {
366 self.params.push(param);
367 self
368 }
369
370 pub fn required_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
372 self.param(ParamDef::required(name, description))
373 }
374
375 pub fn optional_param(self, name: impl Into<String>, description: impl Into<String>) -> Self {
377 self.param(ParamDef::optional(name, description))
378 }
379
380 pub fn example(mut self, example: impl Into<String>) -> Self {
382 self.example = Some(example.into());
383 self
384 }
385
386 pub fn has_group(&self, group: &str) -> bool {
388 self.groups.iter().any(|g| g == group)
389 }
390
391 pub fn has_any_group(&self, groups: &[&str]) -> bool {
393 groups.iter().any(|g| self.has_group(g))
394 }
395}
396
397#[derive(Debug, Clone, Default)]
406pub struct ActionGroup {
407 pub name: String,
409 pub include_groups: Vec<String>,
411 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 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 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 pub fn matches(&self, action: &ActionDef) -> bool {
446 if self.exclude_groups.iter().any(|g| action.has_group(g)) {
448 return false;
449 }
450
451 if self.include_groups.is_empty() {
453 return true;
454 }
455
456 self.include_groups.iter().any(|g| action.has_group(g))
458 }
459}
460
461#[derive(Debug, Clone, Default)]
470pub struct ActionsConfig {
471 actions: HashMap<String, ActionDef>,
473 groups: HashMap<String, ActionGroup>,
475}
476
477impl ActionsConfig {
478 pub fn new() -> Self {
479 Self::default()
480 }
481
482 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 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 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 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 pub fn all_action_names(&self) -> Vec<String> {
522 self.actions.keys().cloned().collect()
523 }
524
525 pub fn all_actions(&self) -> impl Iterator<Item = &ActionDef> {
527 self.actions.values()
528 }
529
530 pub fn get(&self, name: &str) -> Option<&ActionDef> {
532 self.actions.get(name)
533 }
534
535 pub fn get_group(&self, name: &str) -> Option<&ActionGroup> {
537 self.groups.get(name)
538 }
539
540 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 self.actions
547 .values()
548 .filter(|a| a.has_group(group_name))
549 .collect()
550 }
551 }
552
553 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 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 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 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 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 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 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 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 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 for param in &def.params {
670 if param.required && !action.params.args.contains_key(¶m.name) {
671 return Err(ActionValidationError::MissingParam(param.name.clone()));
672 }
673 }
674
675 Ok(())
676 }
677}
678
679#[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#[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"), )
735 }
736
737 #[test]
738 fn test_by_group_direct() {
739 let cfg = sample_config();
740
741 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 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())); 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 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 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 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}