use std::num::NonZeroU32;
use crate::plan::{Node, Step};
pub(crate) const WORKER_MEMBER: &str = "worker";
pub(crate) const ZERO_TURNS: &str =
"`max_turns: 0` lets the dispatch take no turn at all; omit it to run under the agent \
graph's own ceiling";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct NodeControls {
pub max_turns: Option<NonZeroU32>,
}
enum Control {
MaxTurns(NonZeroU32),
#[cfg(test)]
Unappliable(&'static str),
}
impl Control {
fn name(&self) -> &'static str {
match self {
Self::MaxTurns(_) => "max_turns",
#[cfg(test)]
Self::Unappliable(name) => name,
}
}
fn set(&self) -> Option<String> {
match self {
Self::MaxTurns(budget) => Some(format!("members.{WORKER_MEMBER}.max_turns={budget}")),
#[cfg(test)]
Self::Unappliable(_) => None,
}
}
}
impl NodeControls {
pub fn of_node(node: &Node) -> std::result::Result<Self, String> {
Ok(Self {
max_turns: turn_budget(node.max_turns)?,
})
}
pub fn of_step(step: &Step) -> std::result::Result<Self, String> {
Ok(Self {
max_turns: turn_budget(step.max_turns)?,
})
}
pub fn overrides(&self) -> std::result::Result<Vec<String>, String> {
rendered(self.declared())
}
fn declared(&self) -> Vec<Control> {
let Self { max_turns } = self;
let mut declared = Vec::new();
if let Some(budget) = max_turns {
declared.push(Control::MaxTurns(*budget));
}
declared
}
}
fn turn_budget(declared: Option<u32>) -> std::result::Result<Option<NonZeroU32>, String> {
match declared {
None => Ok(None),
Some(budget) => NonZeroU32::new(budget)
.map(Some)
.ok_or(ZERO_TURNS.to_string()),
}
}
fn rendered(declared: Vec<Control>) -> std::result::Result<Vec<String>, String> {
declared
.into_iter()
.map(|control| {
control.set().ok_or_else(|| {
format!(
"`{}` is a control this build accepts and cannot apply to a dispatch, \
so the dispatch would silently run under a default nobody asked for",
control.name()
)
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn node(max_turns: Option<u32>) -> Node {
Node {
id: "build".into(),
persona: Some("engineer".into()),
task: Some("## What\nship it".into()),
max_turns,
..Node::default()
}
}
fn step(max_turns: Option<u32>) -> Step {
Step {
id: "implement".into(),
persona: Some("engineer".into()),
task: Some("## What\nship it".into()),
max_turns,
..Step::default()
}
}
#[test]
fn a_declared_turn_budget_renders_as_the_workers_own_override() {
let controls = NodeControls::of_node(&node(Some(45))).expect("45 is a budget");
assert_eq!(
controls.overrides().expect("a budget is appliable"),
vec!["members.worker.max_turns=45".to_string()]
);
}
#[test]
fn a_node_that_declares_no_control_overrides_nothing() {
let controls = NodeControls::of_node(&node(None)).expect("nothing to convert");
assert_eq!(controls.max_turns, None);
assert_eq!(
controls.overrides().expect("nothing to apply"),
Vec::<String>::new(),
"a set nobody asked for would override the graph's own value"
);
}
#[test]
fn the_checked_conversion_keeps_a_positive_budget_and_refuses_zero() {
assert_eq!(
NodeControls::of_node(&node(Some(45)))
.expect("45 converts")
.max_turns,
NonZeroU32::new(45)
);
assert_eq!(
NodeControls::of_step(&step(Some(45)))
.expect("45 converts")
.max_turns,
NonZeroU32::new(45)
);
for refused in [
NodeControls::of_node(&node(Some(0))).expect_err("a node cannot run for no turns"),
NodeControls::of_step(&step(Some(0))).expect_err("a step cannot run for no turns"),
] {
assert!(refused.contains("no turn at all"), "{refused}");
assert!(
refused.contains("omit it"),
"the refusal does not say what to do instead: {refused}"
);
}
}
#[test]
fn a_control_with_nowhere_to_land_is_refused_by_name_rather_than_dropped() {
let refused = rendered(vec![Control::Unappliable("someday")])
.expect_err("a control with no override cannot be applied");
assert!(refused.contains("someday"), "{refused}");
assert!(
refused.contains("default nobody asked for"),
"the refusal does not say what the silence would have cost: {refused}"
);
}
}