use serde_json::Value;
pub fn eval_condition(expr: &str, state: &Value) -> bool {
let expr = expr.trim();
if expr.is_empty() {
return false;
}
match find_first_op(expr) {
Some((op_pos, op)) => eval_comparison(expr, state, op_pos, op),
None => eval_truthiness(expr, state),
}
}
fn find_first_op(expr: &str) -> Option<(usize, &'static str)> {
let b = expr.as_bytes();
let len = b.len();
if len < 2 {
return None;
}
for i in 0..len - 1 {
match (b[i], b[i + 1]) {
(b'!', b'=') => return Some((i, "!=")),
(b'=', b'=') => return Some((i, "==")),
_ => {}
}
}
None
}
fn eval_comparison(expr: &str, state: &Value, op_pos: usize, op: &str) -> bool {
let lhs = expr[..op_pos].trim();
let rhs = expr[op_pos + 2..].trim();
let path = match lhs.strip_prefix("state.") {
Some(p) if !p.is_empty() => p,
_ => {
tracing::warn!(
"eval_condition: lhs `{}` does not start with `state.<path>`; returning false",
lhs
);
return false;
}
};
let rhs_val = match parse_literal(rhs) {
Some(v) => v,
None => {
tracing::warn!(
"eval_condition: RHS `{}` is not a valid literal \
(must be a quoted string, true, false, null, or a number); returning false",
rhs
);
return false;
}
};
let resolved = resolve_path(state, path);
let equal = values_equal(resolved, &rhs_val);
match op {
"==" => equal,
"!=" => !equal,
_ => false, }
}
fn eval_truthiness(expr: &str, state: &Value) -> bool {
let path = match expr.strip_prefix("state.") {
Some(p) if !p.is_empty() => p,
_ => {
tracing::warn!(
"eval_condition: expr `{}` does not start with `state.<path>`; returning false",
expr
);
return false;
}
};
is_truthy(resolve_path(state, path))
}
fn resolve_path<'a>(root: &'a Value, dotted_path: &str) -> Option<&'a Value> {
let mut current = root;
for segment in dotted_path.split('.') {
if segment.is_empty() {
return None;
}
match current {
Value::Object(map) => {
current = map.get(segment)?;
}
Value::Array(arr) => {
let idx: usize = segment.parse().ok()?;
current = arr.get(idx)?;
}
_ => return None,
}
}
Some(current)
}
fn is_truthy(v: Option<&Value>) -> bool {
match v {
None => false,
Some(Value::Null) => false,
Some(Value::Bool(b)) => *b,
Some(Value::Number(n)) => n.as_f64().is_some_and(|f| f != 0.0),
Some(Value::String(s)) => !s.is_empty(),
Some(Value::Array(a)) => !a.is_empty(),
Some(Value::Object(o)) => !o.is_empty(),
}
}
fn parse_literal(s: &str) -> Option<Value> {
serde_json::from_str::<Value>(s).ok()
}
fn values_equal(resolved: Option<&Value>, rhs: &Value) -> bool {
match resolved {
None => rhs == &Value::Null,
Some(v) => v == rhs,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn string_eq_match() {
assert!(eval_condition(
r#"state.last_model_finish_reason == "tool_calls""#,
&json!({"last_model_finish_reason": "tool_calls"}),
));
}
#[test]
fn string_eq_mismatch() {
assert!(!eval_condition(
r#"state.last_model_finish_reason == "tool_calls""#,
&json!({"last_model_finish_reason": "stop"}),
));
}
#[test]
fn string_eq_missing_key() {
assert!(!eval_condition(
r#"state.last_model_finish_reason == "tool_calls""#,
&json!({}),
));
}
#[test]
fn string_neq_true() {
assert!(eval_condition(r#"state.x != "a""#, &json!({"x": "b"}),));
}
#[test]
fn string_neq_false() {
assert!(!eval_condition(r#"state.x != "a""#, &json!({"x": "a"}),));
}
#[test]
fn truthiness_bool_true() {
assert!(eval_condition(
"state.__cost_exceeded__",
&json!({"__cost_exceeded__": true}),
));
}
#[test]
fn truthiness_bool_false() {
assert!(!eval_condition(
"state.__cost_exceeded__",
&json!({"__cost_exceeded__": false}),
));
}
#[test]
fn truthiness_absent_key() {
assert!(!eval_condition("state.__cost_exceeded__", &json!({})));
}
#[test]
fn truthiness_non_empty_string() {
assert!(eval_condition(
"state.__cost_exceeded__",
&json!({"__cost_exceeded__": "yes"}),
));
}
#[test]
fn truthiness_zero_is_falsy() {
assert!(!eval_condition("state.count", &json!({"count": 0})));
}
#[test]
fn truthiness_empty_string_is_falsy() {
assert!(!eval_condition("state.s", &json!({"s": ""})));
}
#[test]
fn truthiness_empty_array_is_falsy() {
assert!(!eval_condition("state.arr", &json!({"arr": []})));
}
#[test]
fn truthiness_empty_object_is_falsy() {
assert!(!eval_condition("state.obj", &json!({"obj": {}})));
}
#[test]
fn nested_bool_eq_true() {
assert!(eval_condition(
"state.__critic_0_verdict__.passed == true",
&json!({"__critic_0_verdict__": {"passed": true}}),
));
}
#[test]
fn nested_bool_eq_false_value() {
assert!(!eval_condition(
"state.__critic_0_verdict__.passed == true",
&json!({"__critic_0_verdict__": {"passed": false}}),
));
}
#[test]
fn nested_missing_parent() {
assert!(!eval_condition(
"state.__critic_0_verdict__.passed == true",
&json!({}),
));
}
#[test]
fn null_eq_missing_path() {
assert!(eval_condition("state.missing_key == null", &json!({})));
}
#[test]
fn null_eq_explicit_null() {
assert!(eval_condition("state.x == null", &json!({"x": null}),));
}
#[test]
fn null_eq_non_null_is_false() {
assert!(!eval_condition(
"state.x == null",
&json!({"x": "something"}),
));
}
#[test]
fn number_eq_true() {
assert!(eval_condition("state.count == 5", &json!({"count": 5})));
}
#[test]
fn number_eq_string_is_false() {
assert!(!eval_condition("state.count == 5", &json!({"count": "5"}),));
}
#[test]
fn bool_false_literal_match() {
assert!(eval_condition(
"state.flag == false",
&json!({"flag": false}),
));
}
#[test]
fn malformed_garbage() {
assert!(!eval_condition("garbage", &json!({})));
}
#[test]
fn malformed_lhs_not_state() {
assert!(!eval_condition(r#"x == "y""#, &json!({"x": "y"})));
}
#[test]
fn malformed_empty_expr() {
assert!(!eval_condition("", &json!({})));
}
#[test]
fn malformed_just_state_dot() {
assert!(!eval_condition("state.", &json!({})));
}
#[test]
fn malformed_whitespace_only() {
assert!(!eval_condition(" ", &json!({})));
}
#[test]
fn unquoted_rhs_bareword_fail_closed() {
assert!(!eval_condition(
"state.status == done",
&json!({"status": "done"}),
));
}
#[test]
fn quoted_rhs_string_still_works() {
assert!(eval_condition(
r#"state.status == "done""#,
&json!({"status": "done"}),
));
}
}