use super::action::{Action, ActionKind};
use super::context::DecisionContext;
use super::state::AgentState;
use std::fmt::Debug;
pub trait Decider: Send + Sync + Debug {
fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Default)]
pub struct PlainDecider;
impl Decider for PlainDecider {
fn decide(&self, context: &DecisionContext, _state: &AgentState) -> Action {
if let Some(target) = context.next_work_item() {
return Action::mutate("Execute", target.clone());
}
Action::done()
}
fn name(&self) -> &'static str {
"PlainDecider"
}
}
#[derive(Debug, Clone)]
pub struct ParameterizedDecider {
pub error_weight: f64,
pub recency_weight: f64,
}
impl Default for ParameterizedDecider {
fn default() -> Self {
Self {
error_weight: 0.7,
recency_weight: 0.3,
}
}
}
impl ParameterizedDecider {
pub fn with_weights(error_weight: f64, recency_weight: f64) -> Self {
Self {
error_weight,
recency_weight,
}
}
}
impl Decider for ParameterizedDecider {
fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
if state.has_errors() && self.error_weight > 0.5 {
if let Some(errored_file) = state.most_errored_file() {
return Action::read(errored_file.to_string_lossy())
.with_reason(format!("fixing {} errors", state.error_count()));
}
}
if context.last_failed() {
let available = state.available_files(context.agent_id);
if let Some(file) = available.first() {
return Action::read(file.to_string_lossy())
.with_reason("trying different file after failure");
}
}
if context.recent_success_rate() < 0.3 && context.recent_results.len() >= 3 {
return Action::new(ActionKind::Escalate)
.with_reason("low success rate, requesting help");
}
if let Some(target) = context.next_work_item() {
return Action::mutate("Execute", target.clone());
}
let available = state.available_files(context.agent_id);
if let Some(file) = available.first() {
return Action::read(file.to_string_lossy())
.with_reason("investigating available file");
}
Action::done()
}
fn name(&self) -> &'static str {
"ParameterizedDecider"
}
}
#[derive(Debug, Clone)]
pub struct MurmurationDecider {
pub separation_weight: f64,
}
impl Default for MurmurationDecider {
fn default() -> Self {
Self {
separation_weight: 1.0,
}
}
}
impl MurmurationDecider {
pub fn with_separation(separation_weight: f64) -> Self {
Self { separation_weight }
}
}
impl Decider for MurmurationDecider {
fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
let available = state.available_files(context.agent_id);
if let Some(file) = available.first() {
return Action::read(file.to_string_lossy()).with_reason(format!(
"agent #{} investigating (separation)",
context.agent_id
));
}
if !state.uninvestigated_files().is_empty() {
return Action::rest("waiting for other agents");
}
if let Some(target) = context.next_work_item() {
return Action::mutate("Execute", target.clone())
.with_reason("cohesion: executing final work");
}
Action::done()
}
fn name(&self) -> &'static str {
"MurmurationDecider"
}
}
pub trait DecisionModifier: Send + Sync + Debug {
fn modify(&self, action: Action, context: &DecisionContext, state: &AgentState) -> Action;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone)]
pub struct ErrorAwareModifier {
pub weight: f64,
}
impl ErrorAwareModifier {
pub fn new(weight: f64) -> Self {
Self { weight }
}
}
impl DecisionModifier for ErrorAwareModifier {
fn modify(&self, action: Action, _context: &DecisionContext, state: &AgentState) -> Action {
if self.weight > 0.5 && state.has_errors() {
if let Some(errored_file) = state.most_errored_file() {
return Action::read(errored_file.to_string_lossy()).with_reason(format!(
"error-aware: {} errors in file",
state.error_count()
));
}
}
action
}
fn name(&self) -> &'static str {
"ErrorAwareModifier"
}
}
#[derive(Debug, Clone)]
pub struct StallDetectionModifier {
pub threshold: u64,
}
impl StallDetectionModifier {
pub fn new(threshold: u64) -> Self {
Self { threshold }
}
}
impl DecisionModifier for StallDetectionModifier {
fn modify(&self, action: Action, context: &DecisionContext, state: &AgentState) -> Action {
if state.is_stalled(context.tick, self.threshold) {
return Action::new(ActionKind::Escalate).with_reason(format!(
"stall-detection: no progress for {} ticks",
self.threshold
));
}
action
}
fn name(&self) -> &'static str {
"StallDetectionModifier"
}
}
#[derive(Debug, Clone)]
pub struct SuccessRateModifier {
pub threshold: f64,
pub min_samples: usize,
}
impl SuccessRateModifier {
pub fn new(threshold: f64) -> Self {
Self {
threshold,
min_samples: 3,
}
}
}
impl DecisionModifier for SuccessRateModifier {
fn modify(&self, action: Action, context: &DecisionContext, _state: &AgentState) -> Action {
if context.recent_results.len() >= self.min_samples
&& context.recent_success_rate() < self.threshold
{
return Action::new(ActionKind::Escalate).with_reason(format!(
"success-rate: {:.0}% below threshold",
context.recent_success_rate() * 100.0
));
}
action
}
fn name(&self) -> &'static str {
"SuccessRateModifier"
}
}
#[derive(Debug, Clone)]
pub struct RetryModifier {
pub max_retries: u32,
}
impl RetryModifier {
pub fn new(max_retries: u32) -> Self {
Self { max_retries }
}
}
impl DecisionModifier for RetryModifier {
fn modify(&self, action: Action, context: &DecisionContext, _state: &AgentState) -> Action {
if context.last_failed() {
let consecutive_failures = context
.recent_results
.iter()
.rev()
.take_while(|r| !r.success)
.count() as u32;
if consecutive_failures < self.max_retries {
if let Some(last) = context.last_result() {
return last
.action
.clone()
.with_reason(format!("retry: attempt {}", consecutive_failures + 1));
}
}
}
action
}
fn name(&self) -> &'static str {
"RetryModifier"
}
}
#[derive(Debug)]
pub struct ComposableDecider {
base: Box<dyn Decider>,
modifiers: Vec<Box<dyn DecisionModifier>>,
}
impl ComposableDecider {
pub fn new(base: impl Decider + 'static) -> Self {
Self {
base: Box::new(base),
modifiers: Vec::new(),
}
}
pub fn with_modifier(mut self, modifier: impl DecisionModifier + 'static) -> Self {
self.modifiers.push(Box::new(modifier));
self
}
pub fn error_aware(self, weight: f64) -> Self {
self.with_modifier(ErrorAwareModifier::new(weight))
}
pub fn stall_detection(self, threshold: u64) -> Self {
self.with_modifier(StallDetectionModifier::new(threshold))
}
pub fn success_rate(self, threshold: f64) -> Self {
self.with_modifier(SuccessRateModifier::new(threshold))
}
pub fn with_retry(self, max_retries: u32) -> Self {
self.with_modifier(RetryModifier::new(max_retries))
}
}
impl Decider for ComposableDecider {
fn decide(&self, context: &DecisionContext, state: &AgentState) -> Action {
let mut action = self.base.decide(context, state);
for modifier in &self.modifiers {
action = modifier.modify(action, context, state);
}
action
}
fn name(&self) -> &'static str {
"ComposableDecider"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_context() -> DecisionContext {
DecisionContext::new(0, "rename foo to bar")
.with_tick(1)
.with_remaining_work(vec!["task1".to_string(), "task2".to_string()])
}
fn create_test_state() -> AgentState {
let mut state = AgentState::new();
state.update_file(
PathBuf::from("a.rs"),
super::super::state::FileState::new(100, 2000),
);
state.update_file(
PathBuf::from("b.rs"),
super::super::state::FileState::new(50, 1000),
);
state
}
#[test]
fn test_plain_decider() {
let decider = PlainDecider;
let context = create_test_context();
let state = create_test_state();
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Mutate);
assert_eq!(action.target, Some("task1".to_string()));
}
#[test]
fn test_murmuration_decider() {
let decider = MurmurationDecider::default();
let context = DecisionContext::new(0, "investigate").with_tick(1);
let state = create_test_state();
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Read);
assert!(action.target.is_some());
}
#[test]
fn test_parameterized_decider_with_errors() {
let decider = ParameterizedDecider::default();
let context = DecisionContext::new(0, "fix errors");
let mut state = create_test_state();
state.add_error(super::super::state::ErrorInfo::new(
PathBuf::from("a.rs"),
10,
"error",
));
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Read);
assert!(action.target.unwrap().contains("a.rs"));
}
#[test]
fn test_composable_decider_basic() {
let decider = ComposableDecider::new(MurmurationDecider::default());
let context = DecisionContext::new(0, "investigate").with_tick(1);
let state = create_test_state();
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Read);
}
#[test]
fn test_composable_decider_with_error_modifier() {
let decider = ComposableDecider::new(MurmurationDecider::default()).error_aware(0.8);
let context = DecisionContext::new(0, "investigate").with_tick(1);
let mut state = create_test_state();
state.add_error(super::super::state::ErrorInfo::new(
PathBuf::from("a.rs"),
10,
"compile error",
));
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Read);
assert!(action.target.unwrap().contains("a.rs"));
assert!(action.reason.unwrap().contains("error-aware"));
}
#[test]
fn test_composable_decider_stall_detection() {
let decider = ComposableDecider::new(PlainDecider).stall_detection(5);
let context = DecisionContext::new(0, "task")
.with_tick(10)
.with_remaining_work(vec!["task1".to_string()]);
let mut state = create_test_state();
state.last_progress_tick = 0;
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Escalate);
assert!(action.reason.unwrap().contains("stall"));
}
#[test]
fn test_composable_decider_chain() {
let decider = ComposableDecider::new(MurmurationDecider::default())
.error_aware(0.7)
.stall_detection(10)
.with_retry(3);
let context = DecisionContext::new(0, "investigate").with_tick(1);
let state = create_test_state();
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Read);
}
#[test]
fn test_success_rate_modifier() {
use super::super::action::ActionResult;
let decider = ComposableDecider::new(PlainDecider).success_rate(0.5);
let mut context =
DecisionContext::new(0, "task").with_remaining_work(vec!["task1".to_string()]);
for _ in 0..4 {
context.add_result(ActionResult::failure(Action::read("test.rs"), "error"));
}
let state = create_test_state();
let action = decider.decide(&context, &state);
assert_eq!(action.kind, ActionKind::Escalate);
assert!(action.reason.unwrap().contains("success-rate"));
}
}