use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::time::Instant;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum GoalCondition {
AllOf { conditions: Vec<GoalCondition> },
AnyOf { conditions: Vec<GoalCondition> },
ToolReceiptsGrounded,
PlanAchieved,
StateConsistent,
StatePredicate { key: String, equals: Value },
Command { id: String, expect_exit: i32 },
ModelJudge { id: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GoalInputs {
#[serde(default)]
pub receipts_grounded: Option<bool>,
#[serde(default)]
pub plan_achieved: Option<bool>,
#[serde(default)]
pub state_consistent: Option<bool>,
#[serde(default)]
pub state: HashMap<String, Value>,
#[serde(default)]
pub command_exits: HashMap<String, i32>,
#[serde(default)]
pub model_verdicts: HashMap<String, bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GoalVerdict {
pub met: bool,
pub grounded: bool,
pub reason: String,
}
pub fn evaluate_goal(condition: &GoalCondition, inputs: &GoalInputs) -> GoalVerdict {
let e = eval(condition, inputs);
GoalVerdict {
met: e.met,
grounded: if e.met { e.grounded } else { true },
reason: e.reason,
}
}
struct Eval {
met: bool,
grounded: bool,
reason: String,
}
fn eval(condition: &GoalCondition, inputs: &GoalInputs) -> Eval {
match condition {
GoalCondition::AllOf { conditions } => {
if conditions.is_empty() {
return Eval {
met: true,
grounded: true,
reason: "no conditions (vacuously met)".into(),
};
}
let mut all_grounded = true;
for c in conditions {
let r = eval(c, inputs);
if !r.met {
return Eval {
met: false,
grounded: true,
reason: r.reason,
};
}
all_grounded &= r.grounded;
}
Eval {
met: true,
grounded: all_grounded,
reason: format!("all {} conditions met", conditions.len()),
}
}
GoalCondition::AnyOf { conditions } => {
if conditions.is_empty() {
return Eval {
met: false,
grounded: true,
reason: "no conditions (vacuously unmet)".into(),
};
}
let mut met_grounded = false;
let mut met_any = false;
let mut first_met_reason = String::new();
let mut reasons = Vec::new();
for c in conditions {
let r = eval(c, inputs);
if r.met {
if !met_any {
first_met_reason = r.reason.clone();
}
met_any = true;
met_grounded |= r.grounded;
}
reasons.push(r.reason);
}
if met_any {
Eval {
met: true,
grounded: met_grounded,
reason: first_met_reason,
}
} else {
Eval {
met: false,
grounded: true,
reason: format!(
"none of {} conditions met: {}",
conditions.len(),
reasons.join("; ")
),
}
}
}
GoalCondition::ToolReceiptsGrounded => match inputs.receipts_grounded {
Some(true) => grounded_leaf("tool claims are grounded against the receipts"),
Some(false) => unmet("tool claims are NOT grounded — hallucinated tool use detected"),
None => unmet("tool-receipt grounding was not gathered"),
},
GoalCondition::PlanAchieved => match inputs.plan_achieved {
Some(true) => grounded_leaf("plan check reports the goal facts achieved"),
Some(false) => unmet("plan check reports the goal facts are not yet achieved"),
None => unmet("plan check was not gathered"),
},
GoalCondition::StateConsistent => match inputs.state_consistent {
Some(true) => grounded_leaf("shared state is transactionally consistent"),
Some(false) => unmet("shared state is inconsistent (unresolved conflict / drift)"),
None => unmet("state consistency was not gathered"),
},
GoalCondition::StatePredicate { key, equals } => match inputs.state.get(key) {
Some(v) if values_equal(v, equals) => {
grounded_leaf(&format!("state['{key}'] == {equals}"))
}
Some(v) => unmet(&format!("state['{key}'] is {v}, expected {equals}")),
None => unmet(&format!("state key '{key}' is not present")),
},
GoalCondition::Command { id, expect_exit } => match inputs.command_exits.get(id) {
Some(code) if code == expect_exit => {
grounded_leaf(&format!("command '{id}' exited {expect_exit}"))
}
Some(code) => unmet(&format!(
"command '{id}' exited {code}, expected {expect_exit}"
)),
None => unmet(&format!("command check '{id}' was not run")),
},
GoalCondition::ModelJudge { id } => match inputs.model_verdicts.get(id) {
Some(true) => Eval {
met: true,
grounded: false,
reason: format!("model judge '{id}' says met (UNGROUNDED — transcript-only)"),
},
Some(false) => unmet(&format!("model judge '{id}' says not yet met")),
None => unmet(&format!("model judge '{id}' returned no verdict")),
},
}
}
fn grounded_leaf(reason: &str) -> Eval {
Eval {
met: true,
grounded: true,
reason: reason.to_string(),
}
}
fn unmet(reason: &str) -> Eval {
Eval {
met: false,
grounded: true,
reason: reason.to_string(),
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => match (x.as_f64(), y.as_f64()) {
(Some(fx), Some(fy)) => fx == fy,
_ => x == y,
},
_ => a == b,
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GoalGovernor {
#[serde(default)]
pub max_turns: Option<u32>,
#[serde(default)]
pub max_cost_usd: Option<f64>,
#[serde(default)]
pub max_wall_secs: Option<u64>,
#[serde(default)]
pub no_progress_turns: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GoalRunState {
pub turns: u32,
pub cost_usd: f64,
pub elapsed_secs: u64,
pub turns_since_progress: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoalHalt {
TurnBudget,
CostBudget,
WallClock,
NoProgress,
Cancelled,
EvaluationTimeout,
}
impl GoalHalt {
pub fn as_str(&self) -> &'static str {
match self {
GoalHalt::TurnBudget => "turn budget exhausted",
GoalHalt::CostBudget => "cost budget exhausted",
GoalHalt::WallClock => "wall-clock budget exhausted",
GoalHalt::NoProgress => "no progress across the allowed iterations",
GoalHalt::Cancelled => "cancelled",
GoalHalt::EvaluationTimeout => "goal check did not complete within its bound",
}
}
}
pub fn governor_check(gov: &GoalGovernor, state: &GoalRunState) -> Option<GoalHalt> {
if let Some(max) = gov.max_turns {
if state.turns >= max {
return Some(GoalHalt::TurnBudget);
}
}
if let Some(max) = gov.max_cost_usd {
if state.cost_usd >= max {
return Some(GoalHalt::CostBudget);
}
}
if let Some(max) = gov.max_wall_secs {
if state.elapsed_secs >= max {
return Some(GoalHalt::WallClock);
}
}
if let Some(max) = gov.no_progress_turns {
if state.turns_since_progress >= max {
return Some(GoalHalt::NoProgress);
}
}
None
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalSpec {
pub goal: String,
pub condition: GoalCondition,
#[serde(default)]
pub governor: GoalGovernor,
}
#[derive(Debug, Clone, Default)]
pub struct IterationOutcome {
pub cost_usd: f64,
pub made_progress: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum GoalStatus {
Achieved,
Halted { halt: GoalHalt },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalRun {
pub status: GoalStatus,
pub iterations: u32,
pub grounded: bool,
pub cost_usd: f64,
pub last_reason: String,
pub evidence: Vec<GoalVerdict>,
}
pub fn anchor_directive(goal: &str, last_reason: &str) -> String {
if last_reason.is_empty() {
format!("Overall goal: {goal}")
} else {
format!("Overall goal: {goal}\n\nNot yet met — {last_reason}\nContinue working toward the goal.")
}
}
pub async fn run_goal_loop<Run, RunFut, Gather, GatherFut>(
spec: &GoalSpec,
mut run_iteration: Run,
mut gather_inputs: Gather,
) -> GoalRun
where
Run: FnMut(String) -> RunFut,
RunFut: Future<Output = IterationOutcome>,
Gather: FnMut() -> GatherFut,
GatherFut: Future<Output = GoalInputs>,
{
let start = Instant::now();
let mut run_state = GoalRunState::default();
let mut evidence: Vec<GoalVerdict> = Vec::new();
let mut last_reason = String::new();
loop {
run_state.elapsed_secs = start.elapsed().as_secs();
if let Some(halt) = governor_check(&spec.governor, &run_state) {
return GoalRun {
status: GoalStatus::Halted { halt },
iterations: run_state.turns,
grounded: evidence.last().map(|v| v.grounded).unwrap_or(true),
cost_usd: run_state.cost_usd,
last_reason: if last_reason.is_empty() {
halt.as_str().to_string()
} else {
format!("{} ({})", halt.as_str(), last_reason)
},
evidence,
};
}
let directive = anchor_directive(&spec.goal, &last_reason);
let outcome = run_iteration(directive).await;
run_state.turns += 1;
run_state.cost_usd += outcome.cost_usd;
if outcome.made_progress {
run_state.turns_since_progress = 0;
} else {
run_state.turns_since_progress += 1;
}
run_state.elapsed_secs = start.elapsed().as_secs();
let inputs = gather_inputs().await;
let verdict = evaluate_goal(&spec.condition, &inputs);
evidence.push(verdict.clone());
if verdict.met {
return GoalRun {
status: GoalStatus::Achieved,
iterations: run_state.turns,
grounded: verdict.grounded,
cost_usd: run_state.cost_usd,
last_reason: verdict.reason,
evidence,
};
}
last_reason = verdict.reason;
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::cell::Cell;
fn inputs() -> GoalInputs {
GoalInputs::default()
}
#[test]
fn receipts_grounded_leaf() {
let mut i = inputs();
i.receipts_grounded = Some(true);
let v = evaluate_goal(&GoalCondition::ToolReceiptsGrounded, &i);
assert!(v.met && v.grounded);
i.receipts_grounded = Some(false);
let v = evaluate_goal(&GoalCondition::ToolReceiptsGrounded, &i);
assert!(!v.met);
assert!(v.reason.contains("hallucinated"));
}
#[test]
fn missing_input_fails_closed() {
let v = evaluate_goal(&GoalCondition::ToolReceiptsGrounded, &inputs());
assert!(!v.met);
assert!(v.reason.contains("not gathered"));
}
#[test]
fn state_predicate_numeric_equality() {
let mut i = inputs();
i.state.insert("count".into(), json!(3));
let c = GoalCondition::StatePredicate {
key: "count".into(),
equals: json!(3.0),
};
assert!(evaluate_goal(&c, &i).met, "1 and 1.0 must compare equal");
let c2 = GoalCondition::StatePredicate {
key: "count".into(),
equals: json!(4),
};
let v = evaluate_goal(&c2, &i);
assert!(!v.met);
assert!(v.reason.contains("expected"));
}
#[test]
fn command_exit_code() {
let mut i = inputs();
i.command_exits.insert("tests".into(), 0);
let ok = GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
};
assert!(evaluate_goal(&ok, &i).met);
i.command_exits.insert("tests".into(), 1);
let v = evaluate_goal(&ok, &i);
assert!(!v.met);
assert!(v.reason.contains("exited 1"));
}
#[test]
fn all_of_requires_every_child() {
let mut i = inputs();
i.receipts_grounded = Some(true);
i.command_exits.insert("tests".into(), 0);
let c = GoalCondition::AllOf {
conditions: vec![
GoalCondition::ToolReceiptsGrounded,
GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
],
};
let v = evaluate_goal(&c, &i);
assert!(v.met && v.grounded);
assert!(v.reason.contains("all 2 conditions met"));
i.command_exits.insert("tests".into(), 1);
let v = evaluate_goal(&c, &i);
assert!(!v.met);
assert!(v.reason.contains("command 'tests' exited 1"));
}
#[test]
fn any_of_needs_one() {
let mut i = inputs();
i.command_exits.insert("a".into(), 1);
i.command_exits.insert("b".into(), 0);
let c = GoalCondition::AnyOf {
conditions: vec![
GoalCondition::Command {
id: "a".into(),
expect_exit: 0,
},
GoalCondition::Command {
id: "b".into(),
expect_exit: 0,
},
],
};
assert!(evaluate_goal(&c, &i).met);
}
#[test]
fn model_judge_makes_verdict_ungrounded() {
let mut i = inputs();
i.model_verdicts.insert("clarity".into(), true);
let v = evaluate_goal(
&GoalCondition::ModelJudge {
id: "clarity".into(),
},
&i,
);
assert!(v.met);
assert!(
!v.grounded,
"a ModelJudge-satisfied verdict must be ungrounded"
);
}
#[test]
fn any_of_grounded_when_a_deterministic_branch_satisfies() {
let mut i = inputs();
i.model_verdicts.insert("j".into(), true);
i.command_exits.insert("tests".into(), 0);
let c = GoalCondition::AnyOf {
conditions: vec![
GoalCondition::ModelJudge { id: "j".into() },
GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
],
};
let v = evaluate_goal(&c, &i);
assert!(v.met && v.grounded);
}
#[test]
fn any_of_ungrounded_when_only_model_judge_satisfies() {
let mut i = inputs();
i.model_verdicts.insert("j".into(), true);
i.command_exits.insert("tests".into(), 1); let c = GoalCondition::AnyOf {
conditions: vec![
GoalCondition::ModelJudge { id: "j".into() },
GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
],
};
let v = evaluate_goal(&c, &i);
assert!(v.met);
assert!(!v.grounded, "met only via ModelJudge -> ungrounded");
}
#[test]
fn governor_turn_budget() {
let gov = GoalGovernor {
max_turns: Some(3),
..Default::default()
};
let mut s = GoalRunState::default();
s.turns = 2;
assert!(governor_check(&gov, &s).is_none());
s.turns = 3;
assert_eq!(governor_check(&gov, &s), Some(GoalHalt::TurnBudget));
}
#[test]
fn governor_cost_and_wall_and_no_progress() {
let gov = GoalGovernor {
max_cost_usd: Some(1.0),
max_wall_secs: Some(60),
no_progress_turns: Some(2),
..Default::default()
};
let mut s = GoalRunState::default();
s.cost_usd = 1.5;
assert_eq!(governor_check(&gov, &s), Some(GoalHalt::CostBudget));
s.cost_usd = 0.0;
s.elapsed_secs = 61;
assert_eq!(governor_check(&gov, &s), Some(GoalHalt::WallClock));
s.elapsed_secs = 0;
s.turns_since_progress = 2;
assert_eq!(governor_check(&gov, &s), Some(GoalHalt::NoProgress));
}
#[test]
fn anchor_pins_goal_and_appends_reason() {
let d = anchor_directive("make tests pass", "");
assert_eq!(d, "Overall goal: make tests pass");
let d = anchor_directive("make tests pass", "command 'tests' exited 1");
assert!(d.starts_with("Overall goal: make tests pass"));
assert!(d.contains("Not yet met — command 'tests' exited 1"));
}
#[tokio::test]
async fn loop_converges_after_progress() {
let iters = Cell::new(0u32);
let spec = GoalSpec {
goal: "make tests pass".into(),
condition: GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
governor: GoalGovernor {
max_turns: Some(10),
..Default::default()
},
};
let run = |_directive: String| {
let n = iters.get() + 1;
iters.set(n);
async move {
IterationOutcome {
cost_usd: 0.01,
made_progress: true,
}
}
};
let gather = || {
let passing = iters.get() >= 3;
async move {
let mut i = GoalInputs::default();
i.command_exits
.insert("tests".into(), if passing { 0 } else { 1 });
i
}
};
let run = run_goal_loop(&spec, run, gather).await;
assert_eq!(run.status, GoalStatus::Achieved);
assert_eq!(run.iterations, 3);
assert!(run.grounded);
assert!((run.cost_usd - 0.03).abs() < 1e-9);
assert_eq!(run.evidence.len(), 3);
}
#[tokio::test]
async fn loop_halts_on_turn_budget_when_never_converges() {
let spec = GoalSpec {
goal: "impossible".into(),
condition: GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
governor: GoalGovernor {
max_turns: Some(4),
..Default::default()
},
};
let run = |_d: String| async {
IterationOutcome {
cost_usd: 0.0,
made_progress: true,
}
};
let gather = || async {
let mut i = GoalInputs::default();
i.command_exits.insert("tests".into(), 1); i
};
let run = run_goal_loop(&spec, run, gather).await;
assert_eq!(
run.status,
GoalStatus::Halted {
halt: GoalHalt::TurnBudget
}
);
assert_eq!(run.iterations, 4);
}
#[tokio::test]
async fn loop_halts_on_no_progress() {
let spec = GoalSpec {
goal: "stuck".into(),
condition: GoalCondition::Command {
id: "tests".into(),
expect_exit: 0,
},
governor: GoalGovernor {
max_turns: Some(100),
no_progress_turns: Some(2),
..Default::default()
},
};
let run = |_d: String| async {
IterationOutcome {
cost_usd: 0.0,
made_progress: false, }
};
let gather = || async {
let mut i = GoalInputs::default();
i.command_exits.insert("tests".into(), 1);
i
};
let run = run_goal_loop(&spec, run, gather).await;
assert_eq!(
run.status,
GoalStatus::Halted {
halt: GoalHalt::NoProgress
}
);
assert_eq!(run.iterations, 2);
}
#[tokio::test]
async fn loop_reports_ungrounded_when_completed_via_model_judge() {
let spec = GoalSpec {
goal: "reads clearly".into(),
condition: GoalCondition::ModelJudge {
id: "clarity".into(),
},
governor: GoalGovernor {
max_turns: Some(5),
..Default::default()
},
};
let run = |_d: String| async {
IterationOutcome {
cost_usd: 0.0,
made_progress: true,
}
};
let gather = || async {
let mut i = GoalInputs::default();
i.model_verdicts.insert("clarity".into(), true);
i
};
let run = run_goal_loop(&spec, run, gather).await;
assert_eq!(run.status, GoalStatus::Achieved);
assert!(
!run.grounded,
"a run that completed only via a model judge must report ungrounded"
);
}
#[test]
fn goal_condition_round_trips_through_json() {
let c = GoalCondition::AllOf {
conditions: vec![
GoalCondition::ToolReceiptsGrounded,
GoalCondition::StatePredicate {
key: "clean".into(),
equals: json!(true),
},
],
};
let s = serde_json::to_string(&c).unwrap();
let back: GoalCondition = serde_json::from_str(&s).unwrap();
assert_eq!(c, back);
}
}