1use super::action::{Action, ActionKind};
6use super::context::DecisionContext;
7use super::state::AgentState;
8use std::fmt::Debug;
9
10pub trait Decider: Send + Sync + Debug {
18 fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action;
20
21 fn name(&self) -> &'static str;
23}
24
25#[derive(Debug, Clone, Default)]
34pub struct PlainDecider;
35
36impl Decider for PlainDecider {
37 fn decide(&self, context: &DecisionContext, _state: &AgentState) -> Action {
38 if let Some(target) = context.next_work_item() {
40 return Action::mutate("Execute", target.clone());
41 }
42
43 Action::done()
45 }
46
47 fn name(&self) -> &'static str {
48 "PlainDecider"
49 }
50}
51
52#[derive(Debug, Clone)]
60pub struct ParameterizedDecider {
61 pub error_weight: f64,
63 pub recency_weight: f64,
65}
66
67impl Default for ParameterizedDecider {
68 fn default() -> Self {
69 Self {
70 error_weight: 0.7,
71 recency_weight: 0.3,
72 }
73 }
74}
75
76impl ParameterizedDecider {
77 pub fn with_weights(error_weight: f64, recency_weight: f64) -> Self {
79 Self {
80 error_weight,
81 recency_weight,
82 }
83 }
84}
85
86impl Decider for ParameterizedDecider {
87 fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
88 if state.has_errors() && self.error_weight > 0.5 {
90 if let Some(errored_file) = state.most_errored_file() {
91 return Action::read(errored_file.to_string_lossy())
92 .with_reason(format!("fixing {} errors", state.error_count()));
93 }
94 }
95
96 if context.last_failed() {
98 let available = state.available_files(context.agent_id);
100 if let Some(file) = available.first() {
101 return Action::read(file.to_string_lossy())
102 .with_reason("trying different file after failure");
103 }
104 }
105
106 if context.recent_success_rate() < 0.3 && context.recent_results.len() >= 3 {
108 return Action::new(ActionKind::Escalate)
109 .with_reason("low success rate, requesting help");
110 }
111
112 if let Some(target) = context.next_work_item() {
114 return Action::mutate("Execute", target.clone());
115 }
116
117 let available = state.available_files(context.agent_id);
118 if let Some(file) = available.first() {
119 return Action::read(file.to_string_lossy())
120 .with_reason("investigating available file");
121 }
122
123 Action::done()
124 }
125
126 fn name(&self) -> &'static str {
127 "ParameterizedDecider"
128 }
129}
130
131#[derive(Debug, Clone)]
142pub struct MurmurationDecider {
143 pub separation_weight: f64,
145}
146
147impl Default for MurmurationDecider {
148 fn default() -> Self {
149 Self {
150 separation_weight: 1.0,
151 }
152 }
153}
154
155impl MurmurationDecider {
156 pub fn with_separation(separation_weight: f64) -> Self {
158 Self { separation_weight }
159 }
160}
161
162impl Decider for MurmurationDecider {
163 fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
164 let available = state.available_files(context.agent_id);
166
167 if let Some(file) = available.first() {
168 return Action::read(file.to_string_lossy()).with_reason(format!(
169 "agent #{} investigating (separation)",
170 context.agent_id
171 ));
172 }
173
174 if !state.uninvestigated_files().is_empty() {
176 return Action::rest("waiting for other agents");
178 }
179
180 if let Some(target) = context.next_work_item() {
182 return Action::mutate("Execute", target.clone())
183 .with_reason("cohesion: executing final work");
184 }
185
186 Action::done()
187 }
188
189 fn name(&self) -> &'static str {
190 "MurmurationDecider"
191 }
192}
193
194pub trait DecisionModifier: Send + Sync + Debug {
204 fn modify(&self, action: Action, context: &DecisionContext, state: &AgentState) -> Action;
208
209 fn name(&self) -> &'static str;
211}
212
213#[derive(Debug, Clone)]
221pub struct ErrorAwareModifier {
222 pub weight: f64,
224}
225
226impl ErrorAwareModifier {
227 pub fn new(weight: f64) -> Self {
228 Self { weight }
229 }
230}
231
232impl DecisionModifier for ErrorAwareModifier {
233 fn modify(&self, action: Action, _context: &DecisionContext, state: &AgentState) -> Action {
234 if self.weight > 0.5 && state.has_errors() {
236 if let Some(errored_file) = state.most_errored_file() {
237 return Action::read(errored_file.to_string_lossy()).with_reason(format!(
238 "error-aware: {} errors in file",
239 state.error_count()
240 ));
241 }
242 }
243 action
244 }
245
246 fn name(&self) -> &'static str {
247 "ErrorAwareModifier"
248 }
249}
250
251#[derive(Debug, Clone)]
255pub struct StallDetectionModifier {
256 pub threshold: u64,
258}
259
260impl StallDetectionModifier {
261 pub fn new(threshold: u64) -> Self {
262 Self { threshold }
263 }
264}
265
266impl DecisionModifier for StallDetectionModifier {
267 fn modify(&self, action: Action, context: &DecisionContext, state: &AgentState) -> Action {
268 if state.is_stalled(context.tick, self.threshold) {
269 return Action::new(ActionKind::Escalate).with_reason(format!(
270 "stall-detection: no progress for {} ticks",
271 self.threshold
272 ));
273 }
274 action
275 }
276
277 fn name(&self) -> &'static str {
278 "StallDetectionModifier"
279 }
280}
281
282#[derive(Debug, Clone)]
286pub struct SuccessRateModifier {
287 pub threshold: f64,
289 pub min_samples: usize,
291}
292
293impl SuccessRateModifier {
294 pub fn new(threshold: f64) -> Self {
295 Self {
296 threshold,
297 min_samples: 3,
298 }
299 }
300}
301
302impl DecisionModifier for SuccessRateModifier {
303 fn modify(&self, action: Action, context: &DecisionContext, _state: &AgentState) -> Action {
304 if context.recent_results.len() >= self.min_samples
305 && context.recent_success_rate() < self.threshold
306 {
307 return Action::new(ActionKind::Escalate).with_reason(format!(
308 "success-rate: {:.0}% below threshold",
309 context.recent_success_rate() * 100.0
310 ));
311 }
312 action
313 }
314
315 fn name(&self) -> &'static str {
316 "SuccessRateModifier"
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct RetryModifier {
323 pub max_retries: u32,
325}
326
327impl RetryModifier {
328 pub fn new(max_retries: u32) -> Self {
329 Self { max_retries }
330 }
331}
332
333impl DecisionModifier for RetryModifier {
334 fn modify(&self, action: Action, context: &DecisionContext, _state: &AgentState) -> Action {
335 if context.last_failed() {
337 let consecutive_failures = context
338 .recent_results
339 .iter()
340 .rev()
341 .take_while(|r| !r.success)
342 .count() as u32;
343
344 if consecutive_failures < self.max_retries {
345 if let Some(last) = context.last_result() {
347 return last
348 .action
349 .clone()
350 .with_reason(format!("retry: attempt {}", consecutive_failures + 1));
351 }
352 }
353 }
354 action
355 }
356
357 fn name(&self) -> &'static str {
358 "RetryModifier"
359 }
360}
361
362#[derive(Debug)]
380pub struct ComposableDecider {
381 base: Box<dyn Decider>,
382 modifiers: Vec<Box<dyn DecisionModifier>>,
383}
384
385impl ComposableDecider {
386 pub fn new(base: impl Decider + 'static) -> Self {
388 Self {
389 base: Box::new(base),
390 modifiers: Vec::new(),
391 }
392 }
393
394 pub fn with_modifier(mut self, modifier: impl DecisionModifier + 'static) -> Self {
396 self.modifiers.push(Box::new(modifier));
397 self
398 }
399
400 pub fn error_aware(self, weight: f64) -> Self {
402 self.with_modifier(ErrorAwareModifier::new(weight))
403 }
404
405 pub fn stall_detection(self, threshold: u64) -> Self {
407 self.with_modifier(StallDetectionModifier::new(threshold))
408 }
409
410 pub fn success_rate(self, threshold: f64) -> Self {
412 self.with_modifier(SuccessRateModifier::new(threshold))
413 }
414
415 pub fn with_retry(self, max_retries: u32) -> Self {
417 self.with_modifier(RetryModifier::new(max_retries))
418 }
419}
420
421impl Decider for ComposableDecider {
422 fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
423 let mut action = self.base.decide(context, state);
425
426 for modifier in &self.modifiers {
428 action = modifier.modify(action, context, state);
429 }
430
431 action
432 }
433
434 fn name(&self) -> &'static str {
435 "ComposableDecider"
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use std::path::PathBuf;
443
444 fn create_test_context() -> DecisionContext {
445 DecisionContext::new(0, "rename foo to bar")
446 .with_tick(1)
447 .with_remaining_work(vec!["task1".to_string(), "task2".to_string()])
448 }
449
450 fn create_test_state() -> AgentState {
451 let mut state = AgentState::new();
452 state.update_file(
453 PathBuf::from("a.rs"),
454 super::super::state::FileState::new(100, 2000),
455 );
456 state.update_file(
457 PathBuf::from("b.rs"),
458 super::super::state::FileState::new(50, 1000),
459 );
460 state
461 }
462
463 #[test]
464 fn test_plain_decider() {
465 let decider = PlainDecider;
466 let context = create_test_context();
467 let state = create_test_state();
468
469 let action = decider.decide(&context, &state);
470 assert_eq!(action.kind, ActionKind::Mutate);
471 assert_eq!(action.target, Some("task1".to_string()));
472 }
473
474 #[test]
475 fn test_murmuration_decider() {
476 let decider = MurmurationDecider::default();
477 let context = DecisionContext::new(0, "investigate").with_tick(1);
478 let state = create_test_state();
479
480 let action = decider.decide(&context, &state);
481 assert_eq!(action.kind, ActionKind::Read);
482 assert!(action.target.is_some());
483 }
484
485 #[test]
486 fn test_parameterized_decider_with_errors() {
487 let decider = ParameterizedDecider::default();
488 let context = DecisionContext::new(0, "fix errors");
489
490 let mut state = create_test_state();
491 state.add_error(super::super::state::ErrorInfo::new(
492 PathBuf::from("a.rs"),
493 10,
494 "error",
495 ));
496
497 let action = decider.decide(&context, &state);
498 assert_eq!(action.kind, ActionKind::Read);
499 assert!(action.target.unwrap().contains("a.rs"));
501 }
502
503 #[test]
504 fn test_composable_decider_basic() {
505 let decider = ComposableDecider::new(MurmurationDecider::default());
507
508 let context = DecisionContext::new(0, "investigate").with_tick(1);
509 let state = create_test_state();
510
511 let action = decider.decide(&context, &state);
512 assert_eq!(action.kind, ActionKind::Read);
513 }
514
515 #[test]
516 fn test_composable_decider_with_error_modifier() {
517 let decider = ComposableDecider::new(MurmurationDecider::default()).error_aware(0.8);
519
520 let context = DecisionContext::new(0, "investigate").with_tick(1);
521 let mut state = create_test_state();
522
523 state.add_error(super::super::state::ErrorInfo::new(
525 PathBuf::from("a.rs"),
526 10,
527 "compile error",
528 ));
529
530 let action = decider.decide(&context, &state);
531 assert_eq!(action.kind, ActionKind::Read);
533 assert!(action.target.unwrap().contains("a.rs"));
534 assert!(action.reason.unwrap().contains("error-aware"));
535 }
536
537 #[test]
538 fn test_composable_decider_stall_detection() {
539 let decider = ComposableDecider::new(PlainDecider).stall_detection(5);
540
541 let context = DecisionContext::new(0, "task")
542 .with_tick(10)
543 .with_remaining_work(vec!["task1".to_string()]);
544
545 let mut state = create_test_state();
546 state.last_progress_tick = 0;
548
549 let action = decider.decide(&context, &state);
550 assert_eq!(action.kind, ActionKind::Escalate);
552 assert!(action.reason.unwrap().contains("stall"));
553 }
554
555 #[test]
556 fn test_composable_decider_chain() {
557 let decider = ComposableDecider::new(MurmurationDecider::default())
559 .error_aware(0.7)
560 .stall_detection(10)
561 .with_retry(3);
562
563 let context = DecisionContext::new(0, "investigate").with_tick(1);
564 let state = create_test_state();
565
566 let action = decider.decide(&context, &state);
568 assert_eq!(action.kind, ActionKind::Read);
569 }
570
571 #[test]
572 fn test_success_rate_modifier() {
573 use super::super::action::ActionResult;
574
575 let decider = ComposableDecider::new(PlainDecider).success_rate(0.5);
576
577 let mut context =
579 DecisionContext::new(0, "task").with_remaining_work(vec!["task1".to_string()]);
580
581 for _ in 0..4 {
583 context.add_result(ActionResult::failure(Action::read("test.rs"), "error"));
584 }
585
586 let state = create_test_state();
587 let action = decider.decide(&context, &state);
588
589 assert_eq!(action.kind, ActionKind::Escalate);
591 assert!(action.reason.unwrap().contains("success-rate"));
592 }
593}