Skip to main content

ryo_executor/decider/
action.rs

1//! Action: What an agent decides to do
2//!
3//! Actions are the output of a Decider's decision-making process.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// An action that an agent can take
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Action {
12    /// The kind of action
13    pub kind: ActionKind,
14
15    /// Target of the action (file path, symbol name, etc.)
16    pub target: Option<String>,
17
18    /// Additional arguments
19    pub args: HashMap<String, String>,
20
21    /// Reason for this action (for logging/debugging)
22    pub reason: Option<String>,
23}
24
25impl Action {
26    /// Create a new action
27    pub fn new(kind: ActionKind) -> Self {
28        Self {
29            kind,
30            target: None,
31            args: HashMap::new(),
32            reason: None,
33        }
34    }
35
36    /// Set the target
37    pub fn with_target(mut self, target: impl Into<String>) -> Self {
38        self.target = Some(target.into());
39        self
40    }
41
42    /// Add an argument
43    pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
44        self.args.insert(key.into(), value.into());
45        self
46    }
47
48    /// Set the reason
49    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
50        self.reason = Some(reason.into());
51        self
52    }
53
54    /// Create a read action
55    pub fn read(path: impl Into<String>) -> Self {
56        Self::new(ActionKind::Read).with_target(path)
57    }
58
59    /// Create a grep action
60    pub fn grep(pattern: impl Into<String>) -> Self {
61        Self::new(ActionKind::Grep).with_arg("pattern", pattern)
62    }
63
64    /// Create a glob action
65    pub fn glob(pattern: impl Into<String>) -> Self {
66        Self::new(ActionKind::Glob).with_arg("pattern", pattern)
67    }
68
69    /// Create a mutation action
70    pub fn mutate(mutation_type: impl Into<String>, target: impl Into<String>) -> Self {
71        Self::new(ActionKind::Mutate)
72            .with_arg("mutation_type", mutation_type)
73            .with_target(target)
74    }
75
76    /// Create a rest action (agent has nothing to do)
77    pub fn rest(reason: impl Into<String>) -> Self {
78        Self::new(ActionKind::Rest).with_reason(reason)
79    }
80
81    /// Create a done action (agent completed its task)
82    pub fn done() -> Self {
83        Self::new(ActionKind::Done)
84    }
85
86    /// Check if this is a terminal action
87    pub fn is_terminal(&self) -> bool {
88        matches!(self.kind, ActionKind::Done | ActionKind::Rest)
89    }
90}
91
92/// The kind of action
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum ActionKind {
95    // === Investigation ===
96    /// Read a file
97    Read,
98    /// Search with grep pattern
99    Grep,
100    /// Search with glob pattern
101    Glob,
102    /// List directory
103    List,
104
105    // === Mutation ===
106    /// Apply a mutation
107    Mutate,
108    /// Apply a batch of mutations
109    MutateBatch,
110
111    // === Control ===
112    /// Rest (nothing to do right now)
113    Rest,
114    /// Done (completed task)
115    Done,
116    /// Escalate to higher-level decision maker
117    Escalate,
118}
119
120impl ActionKind {
121    /// Get the name of this action kind
122    pub fn as_str(&self) -> &'static str {
123        match self {
124            Self::Read => "read",
125            Self::Grep => "grep",
126            Self::Glob => "glob",
127            Self::List => "list",
128            Self::Mutate => "mutate",
129            Self::MutateBatch => "mutate_batch",
130            Self::Rest => "rest",
131            Self::Done => "done",
132            Self::Escalate => "escalate",
133        }
134    }
135
136    /// Check if this is an investigation action
137    pub fn is_investigation(&self) -> bool {
138        matches!(self, Self::Read | Self::Grep | Self::Glob | Self::List)
139    }
140
141    /// Check if this is a mutation action
142    pub fn is_mutation(&self) -> bool {
143        matches!(self, Self::Mutate | Self::MutateBatch)
144    }
145}
146
147/// Result of executing an action
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ActionResult {
150    /// The action that was executed
151    pub action: Action,
152
153    /// Whether the action succeeded
154    pub success: bool,
155
156    /// Output/result of the action
157    pub output: Option<String>,
158
159    /// Error message if failed
160    pub error: Option<String>,
161
162    /// Duration in microseconds
163    pub duration_us: u64,
164
165    /// Files affected
166    pub affected_files: Vec<PathBuf>,
167
168    /// Changes made (for mutations)
169    pub changes: usize,
170}
171
172impl ActionResult {
173    /// Create a successful result
174    pub fn success(action: Action) -> Self {
175        Self {
176            action,
177            success: true,
178            output: None,
179            error: None,
180            duration_us: 0,
181            affected_files: Vec::new(),
182            changes: 0,
183        }
184    }
185
186    /// Create a failed result
187    pub fn failure(action: Action, error: impl Into<String>) -> Self {
188        Self {
189            action,
190            success: false,
191            output: None,
192            error: Some(error.into()),
193            duration_us: 0,
194            affected_files: Vec::new(),
195            changes: 0,
196        }
197    }
198
199    /// Set the output
200    pub fn with_output(mut self, output: impl Into<String>) -> Self {
201        self.output = Some(output.into());
202        self
203    }
204
205    /// Set the duration
206    pub fn with_duration(mut self, duration_us: u64) -> Self {
207        self.duration_us = duration_us;
208        self
209    }
210
211    /// Set affected files
212    pub fn with_affected_files(mut self, files: Vec<PathBuf>) -> Self {
213        self.affected_files = files;
214        self
215    }
216
217    /// Set changes count
218    pub fn with_changes(mut self, changes: usize) -> Self {
219        self.changes = changes;
220        self
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_action_creation() {
230        let action = Action::read("src/lib.rs");
231        assert_eq!(action.kind, ActionKind::Read);
232        assert_eq!(action.target, Some("src/lib.rs".to_string()));
233
234        let action = Action::grep("TODO").with_target("src/");
235        assert_eq!(action.kind, ActionKind::Grep);
236        assert_eq!(action.args.get("pattern"), Some(&"TODO".to_string()));
237
238        let action = Action::mutate("Rename", "old_fn");
239        assert_eq!(action.kind, ActionKind::Mutate);
240        assert_eq!(action.target, Some("old_fn".to_string()));
241    }
242
243    #[test]
244    fn test_action_result() {
245        let action = Action::read("test.rs");
246        let result = ActionResult::success(action.clone())
247            .with_output("file contents")
248            .with_duration(100);
249
250        assert!(result.success);
251        assert_eq!(result.output, Some("file contents".to_string()));
252        assert_eq!(result.duration_us, 100);
253
254        let result = ActionResult::failure(action, "File not found");
255        assert!(!result.success);
256        assert_eq!(result.error, Some("File not found".to_string()));
257    }
258}