agentvet 0.1.0

Validate LLM-generated tool args before execution. Throws a structured ToolArgError with LLM-friendly retry hints when the model hallucinates wrong types or missing fields.
Documentation
use agentvet::Validator;
use serde_json::json;

fn schema() -> serde_json::Value {
    json!({
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "units": {"type": "string", "enum": ["c", "f"]},
            "limit": {"type": "integer", "minimum": 1, "maximum": 100}
        },
        "required": ["city"],
        "additionalProperties": false
    })
}

#[test]
fn valid_args_pass() {
    let v = Validator::from_schema(&schema()).unwrap();
    v.validate(&json!({"city": "SFO", "units": "c"})).unwrap();
}

#[test]
fn missing_required_field_fails_with_hint() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v.validate(&json!({"units": "c"})).unwrap_err();
    assert!(!err.issues.is_empty());
    let hint = err.for_llm();
    assert!(hint.starts_with("Tool call rejected"));
    assert!(hint.contains("city"));
}

#[test]
fn wrong_type_fails() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v.validate(&json!({"city": 42})).unwrap_err();
    let hint = err.for_llm();
    assert!(hint.contains("city") || hint.contains("string") || hint.contains("type"));
}

#[test]
fn enum_violation_fails() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v.validate(&json!({"city": "SFO", "units": "kelvin"})).unwrap_err();
    let hint = err.for_llm();
    assert!(hint.contains("units") || hint.contains("kelvin") || hint.contains("enum"));
}

#[test]
fn additional_properties_rejected() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v.validate(&json!({"city": "SFO", "wat": "extra"})).unwrap_err();
    assert!(!err.issues.is_empty());
}

#[test]
fn integer_bounds_enforced() {
    let v = Validator::from_schema(&schema()).unwrap();
    v.validate(&json!({"city": "SFO", "limit": 200})).unwrap_err();
    v.validate(&json!({"city": "SFO", "limit": 0})).unwrap_err();
    v.validate(&json!({"city": "SFO", "limit": 50})).unwrap();
}

#[test]
fn malformed_schema_errors_at_compile() {
    // `type` must be a string or array of strings; not 42.
    let bad = json!({"type": 42});
    let res = Validator::from_schema(&bad);
    assert!(res.is_err());
}

#[test]
fn for_llm_format_is_useful() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v
        .validate(&json!({"units": "kelvin", "limit": 999}))
        .unwrap_err();
    let hint = err.for_llm();
    let lines: Vec<&str> = hint.lines().collect();
    // First line is the header; the rest are bullets.
    assert_eq!(lines[0], "Tool call rejected. Fix and try again:");
    assert!(lines.len() >= 2);
    assert!(lines[1].starts_with("  - "));
}

#[test]
fn is_valid_short_circuits() {
    let v = Validator::from_schema(&schema()).unwrap();
    assert!(v.is_valid(&json!({"city": "SFO"})));
    assert!(!v.is_valid(&json!({})));
}

#[test]
fn empty_object_when_required_field_missing() {
    let v = Validator::from_schema(&schema()).unwrap();
    let err = v.validate(&json!({})).unwrap_err();
    assert!(err.for_llm().contains("city"));
}