Skip to main content

ryo_executor/decider/
context.rs

1//! DecisionContext: Information available for making decisions
2
3use super::action::ActionResult;
4use super::state::AgentState;
5use serde::{Deserialize, Serialize};
6
7/// Context for making a decision
8///
9/// Contains all the information a Decider needs to choose the next action.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct DecisionContext {
12    /// The agent's ID
13    pub agent_id: u32,
14
15    /// The original query/goal
16    pub query: String,
17
18    /// Current phase of execution
19    pub phase: String,
20
21    /// Current tick number
22    pub tick: u64,
23
24    /// Recent action results (for learning from past)
25    pub recent_results: Vec<ActionResult>,
26
27    /// Current agent state
28    pub state: AgentState,
29
30    /// Files currently being worked on
31    pub active_files: Vec<String>,
32
33    /// Remaining work items
34    pub remaining_work: Vec<String>,
35
36    /// Custom metadata
37    #[serde(default)]
38    pub metadata: std::collections::HashMap<String, String>,
39}
40
41impl DecisionContext {
42    /// Create a new context
43    pub fn new(agent_id: u32, query: impl Into<String>) -> Self {
44        Self {
45            agent_id,
46            query: query.into(),
47            ..Default::default()
48        }
49    }
50
51    /// Set the current phase
52    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
53        self.phase = phase.into();
54        self
55    }
56
57    /// Set the current tick
58    pub fn with_tick(mut self, tick: u64) -> Self {
59        self.tick = tick;
60        self
61    }
62
63    /// Add a recent result
64    pub fn add_result(&mut self, result: ActionResult) {
65        self.recent_results.push(result);
66        // Keep only last N results
67        if self.recent_results.len() > 10 {
68            self.recent_results.remove(0);
69        }
70    }
71
72    /// Set active files
73    pub fn with_active_files(mut self, files: Vec<String>) -> Self {
74        self.active_files = files;
75        self
76    }
77
78    /// Set remaining work
79    pub fn with_remaining_work(mut self, work: Vec<String>) -> Self {
80        self.remaining_work = work;
81        self
82    }
83
84    /// Add metadata
85    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
86        self.metadata.insert(key.into(), value.into());
87        self
88    }
89
90    /// Get the last result
91    pub fn last_result(&self) -> Option<&ActionResult> {
92        self.recent_results.last()
93    }
94
95    /// Check if the last action succeeded
96    pub fn last_succeeded(&self) -> bool {
97        self.last_result().map(|r| r.success).unwrap_or(true)
98    }
99
100    /// Check if the last action failed
101    pub fn last_failed(&self) -> bool {
102        self.last_result().map(|r| !r.success).unwrap_or(false)
103    }
104
105    /// Count recent failures
106    pub fn recent_failure_count(&self) -> usize {
107        self.recent_results.iter().filter(|r| !r.success).count()
108    }
109
110    /// Calculate recent success rate
111    pub fn recent_success_rate(&self) -> f64 {
112        if self.recent_results.is_empty() {
113            return 1.0;
114        }
115        let successes = self.recent_results.iter().filter(|r| r.success).count();
116        successes as f64 / self.recent_results.len() as f64
117    }
118
119    /// Check if there's remaining work
120    pub fn has_remaining_work(&self) -> bool {
121        !self.remaining_work.is_empty()
122    }
123
124    /// Get the next work item
125    pub fn next_work_item(&self) -> Option<&String> {
126        self.remaining_work.first()
127    }
128
129    /// Check if query contains a keyword (case-insensitive)
130    pub fn query_contains(&self, keyword: &str) -> bool {
131        self.query.to_lowercase().contains(&keyword.to_lowercase())
132    }
133
134    /// Check if query contains any of the keywords
135    pub fn query_contains_any(&self, keywords: &[&str]) -> bool {
136        let query_lower = self.query.to_lowercase();
137        keywords
138            .iter()
139            .any(|k| query_lower.contains(&k.to_lowercase()))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::decider::action::Action;
147
148    #[test]
149    fn test_context_creation() {
150        let ctx = DecisionContext::new(0, "rename foo to bar")
151            .with_phase("executing")
152            .with_tick(5);
153
154        assert_eq!(ctx.agent_id, 0);
155        assert_eq!(ctx.query, "rename foo to bar");
156        assert_eq!(ctx.phase, "executing");
157        assert_eq!(ctx.tick, 5);
158    }
159
160    #[test]
161    fn test_context_results() {
162        let mut ctx = DecisionContext::new(0, "test");
163
164        let success = super::super::action::ActionResult::success(Action::read("a.rs"));
165        let failure = super::super::action::ActionResult::failure(Action::read("b.rs"), "error");
166
167        ctx.add_result(success);
168        ctx.add_result(failure);
169
170        assert!(!ctx.last_succeeded());
171        assert!(ctx.last_failed());
172        assert_eq!(ctx.recent_failure_count(), 1);
173        assert_eq!(ctx.recent_success_rate(), 0.5);
174    }
175
176    #[test]
177    fn test_query_keywords() {
178        let ctx = DecisionContext::new(0, "Rename function foo to bar");
179
180        assert!(ctx.query_contains("rename"));
181        assert!(ctx.query_contains("RENAME")); // case insensitive
182        assert!(ctx.query_contains_any(&["rename", "delete", "add"]));
183        assert!(!ctx.query_contains_any(&["compile", "test"]));
184    }
185}