use crate::Runtime;
use car_eventlog::tool_receipts::ToolClaim;
use car_eventlog::EventKind;
use car_ir::ActionProposal;
use car_verify::goal::{GoalCondition, GoalInputs};
use car_verify::plan_check::{check_plan, PlanCheckRequest};
use car_verify::transaction::check_transaction;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct GoalGather {
pub claims: Vec<ToolClaim>,
pub proposal_id: Option<String>,
pub plan: Option<PlanCheckRequest>,
pub transaction_proposal: Option<ActionProposal>,
pub state_keys: Vec<String>,
pub command_exits: HashMap<String, i32>,
pub model_verdicts: HashMap<String, bool>,
}
impl Runtime {
pub async fn gather_goal_inputs(&self, gather: &GoalGather) -> GoalInputs {
let receipts_grounded = Some(
self.verify_tool_receipts(&gather.claims, gather.proposal_id.as_deref())
.await
.grounded,
);
let plan_achieved = gather.plan.as_ref().map(|p| check_plan(p).valid);
let full = self.state.snapshot();
let state = if gather.state_keys.is_empty() {
full
} else {
gather
.state_keys
.iter()
.filter_map(|k| full.get(k).map(|v| (k.clone(), v.clone())))
.collect()
};
let state_consistent = gather.transaction_proposal.as_ref().map(|p| {
let (values, versions) = self.state.versioned_snapshot();
check_transaction(p, &versions, Some(&values)).consistent
});
GoalInputs {
receipts_grounded,
plan_achieved,
state_consistent,
state,
command_exits: gather.command_exits.clone(),
model_verdicts: gather.model_verdicts.clone(),
}
}
pub async fn record_goal_evaluated(
&self,
goal: &str,
condition: &GoalCondition,
iteration: u32,
met: bool,
grounded: bool,
reason: &str,
) {
let mut data = HashMap::new();
data.insert("goal".to_string(), serde_json::json!(goal));
data.insert(
"condition".to_string(),
serde_json::to_value(condition).unwrap_or(serde_json::Value::Null),
);
data.insert("iteration".to_string(), serde_json::json!(iteration));
data.insert("met".to_string(), serde_json::json!(met));
data.insert("grounded".to_string(), serde_json::json!(grounded));
data.insert("reason".to_string(), serde_json::json!(reason));
self.log
.lock()
.await
.append(EventKind::GoalEvaluated, None, None, data);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ToolExecutor;
use car_eventlog::tool_receipts::{ClaimKind, ToolClaim};
use car_ir::{Action, ActionProposal, ActionType, FailureBehavior};
use car_verify::goal::{evaluate_goal, GoalCondition};
use serde_json::Value;
use std::sync::Arc;
struct EchoExec;
#[async_trait::async_trait]
impl ToolExecutor for EchoExec {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
match tool {
"echo" => Ok(params.get("message").cloned().unwrap_or(Value::Null)),
_ => Err(format!("unknown tool: {tool}")),
}
}
}
fn tool_call(tool: &str, params: HashMap<String, Value>) -> Action {
Action {
id: uuid::Uuid::new_v4().simple().to_string()[..12].to_string(),
action_type: ActionType::ToolCall,
tool: Some(tool.to_string()),
parameters: params,
preconditions: vec![],
expected_effects: HashMap::new(),
state_dependencies: vec![],
read_set: vec![],
write_set: vec![],
assumptions: vec![],
invocation_mode: Default::default(),
idempotent: false,
max_retries: 3,
failure_behavior: FailureBehavior::Abort,
timeout_ms: None,
metadata: HashMap::new(),
}
}
fn state_write(key: &str, value: Value) -> Action {
Action {
id: uuid::Uuid::new_v4().simple().to_string()[..12].to_string(),
action_type: ActionType::StateWrite,
tool: None,
parameters: [
("key".to_string(), Value::from(key)),
("value".to_string(), value),
]
.into(),
preconditions: vec![],
expected_effects: HashMap::new(),
state_dependencies: vec![],
read_set: vec![],
write_set: vec![],
assumptions: vec![],
invocation_mode: Default::default(),
idempotent: false,
max_retries: 3,
failure_behavior: FailureBehavior::Abort,
timeout_ms: None,
metadata: HashMap::new(),
}
}
fn proposal(actions: Vec<Action>) -> ActionProposal {
ActionProposal {
id: "goal-test".to_string(),
source: "test".to_string(),
actions,
timestamp: chrono::Utc::now(),
context: HashMap::new(),
}
}
#[tokio::test]
async fn gather_reads_real_receipts_and_state() {
let rt = Runtime::new().with_executor(Arc::new(EchoExec));
rt.register_tool("echo").await;
let p = proposal(vec![
tool_call("echo", [("message".to_string(), Value::from("hi"))].into()),
state_write("tests_passing", Value::Bool(true)),
]);
let r = rt.execute(&p).await;
assert!(r.all_succeeded(), "setup proposal must succeed");
let gather = GoalGather {
claims: vec![ToolClaim {
kind: ClaimKind::Invoked,
tool: "echo".to_string(),
call_id: None,
count: None,
text: None,
}],
proposal_id: Some("goal-test".to_string()),
..Default::default()
};
let inputs = rt.gather_goal_inputs(&gather).await;
assert_eq!(inputs.receipts_grounded, Some(true));
assert_eq!(inputs.state.get("tests_passing"), Some(&Value::Bool(true)));
let cond = GoalCondition::AllOf {
conditions: vec![
GoalCondition::ToolReceiptsGrounded,
GoalCondition::StatePredicate {
key: "tests_passing".into(),
equals: Value::Bool(true),
},
],
};
let verdict = evaluate_goal(&cond, &inputs);
assert!(verdict.met && verdict.grounded, "{}", verdict.reason);
}
#[tokio::test]
async fn fabricated_claim_fails_the_receipts_condition() {
let rt = Runtime::new().with_executor(Arc::new(EchoExec));
rt.register_tool("echo").await;
let r = rt
.execute(&proposal(vec![tool_call(
"echo",
[("message".to_string(), Value::from("hi"))].into(),
)]))
.await;
assert!(r.all_succeeded());
let gather = GoalGather {
claims: vec![ToolClaim {
kind: ClaimKind::Invoked,
tool: "deploy".to_string(),
call_id: None,
count: None,
text: Some("I deployed it".to_string()),
}],
proposal_id: Some("goal-test".to_string()),
..Default::default()
};
let inputs = rt.gather_goal_inputs(&gather).await;
assert_eq!(inputs.receipts_grounded, Some(false));
let verdict = evaluate_goal(&GoalCondition::ToolReceiptsGrounded, &inputs);
assert!(
!verdict.met,
"a hallucinated tool claim must not satisfy the goal"
);
assert!(verdict.reason.contains("hallucinated"));
}
#[tokio::test]
async fn record_goal_evaluated_appends_auditable_event() {
let rt = Runtime::new();
let condition = GoalCondition::Command {
id: "tests".to_string(),
expect_exit: 0,
};
rt.record_goal_evaluated(
"make tests pass",
&condition,
2,
true,
true,
"command `test` exited 0",
)
.await;
let log = rt.log.lock().await;
let event = log.events().last().expect("goal event should be recorded");
assert_eq!(event.kind, car_eventlog::EventKind::GoalEvaluated);
assert_eq!(
event.data.get("goal"),
Some(&serde_json::json!("make tests pass"))
);
assert_eq!(
event.data.get("condition"),
Some(&serde_json::json!({"kind": "command", "id": "tests", "expect_exit": 0}))
);
assert_eq!(event.data.get("iteration"), Some(&serde_json::json!(2)));
assert_eq!(event.data.get("met"), Some(&serde_json::json!(true)));
assert_eq!(event.data.get("grounded"), Some(&serde_json::json!(true)));
assert_eq!(
event.data.get("reason"),
Some(&serde_json::json!("command `test` exited 0"))
);
}
}