ryo_executor/decider/
context.rs1use super::action::ActionResult;
4use super::state::AgentState;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct DecisionContext {
12 pub agent_id: u32,
14
15 pub query: String,
17
18 pub phase: String,
20
21 pub tick: u64,
23
24 pub recent_results: Vec<ActionResult>,
26
27 pub state: AgentState,
29
30 pub active_files: Vec<String>,
32
33 pub remaining_work: Vec<String>,
35
36 #[serde(default)]
38 pub metadata: std::collections::HashMap<String, String>,
39}
40
41impl DecisionContext {
42 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 pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
53 self.phase = phase.into();
54 self
55 }
56
57 pub fn with_tick(mut self, tick: u64) -> Self {
59 self.tick = tick;
60 self
61 }
62
63 pub fn add_result(&mut self, result: ActionResult) {
65 self.recent_results.push(result);
66 if self.recent_results.len() > 10 {
68 self.recent_results.remove(0);
69 }
70 }
71
72 pub fn with_active_files(mut self, files: Vec<String>) -> Self {
74 self.active_files = files;
75 self
76 }
77
78 pub fn with_remaining_work(mut self, work: Vec<String>) -> Self {
80 self.remaining_work = work;
81 self
82 }
83
84 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 pub fn last_result(&self) -> Option<&ActionResult> {
92 self.recent_results.last()
93 }
94
95 pub fn last_succeeded(&self) -> bool {
97 self.last_result().map(|r| r.success).unwrap_or(true)
98 }
99
100 pub fn last_failed(&self) -> bool {
102 self.last_result().map(|r| !r.success).unwrap_or(false)
103 }
104
105 pub fn recent_failure_count(&self) -> usize {
107 self.recent_results.iter().filter(|r| !r.success).count()
108 }
109
110 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 pub fn has_remaining_work(&self) -> bool {
121 !self.remaining_work.is_empty()
122 }
123
124 pub fn next_work_item(&self) -> Option<&String> {
126 self.remaining_work.first()
127 }
128
129 pub fn query_contains(&self, keyword: &str) -> bool {
131 self.query.to_lowercase().contains(&keyword.to_lowercase())
132 }
133
134 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")); assert!(ctx.query_contains_any(&["rename", "delete", "add"]));
183 assert!(!ctx.query_contains_any(&["compile", "test"]));
184 }
185}