car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for the cost-aware knowledge cascade (U-Mem, arXiv 2602.22406).
//!
//! Stateless helper, binding-only — see
//! `docs/proposals/autonomous-memory-agents.md`. Decides U-Mem's *Evolve*
//! escalation: given the current confidence in a piece of knowledge and a policy
//! (ordered tiers self → tools → expert, each with a cost and a confidence it can
//! deliver, plus a target and a budget), returns which tier to run — or that the
//! answer is already confident enough, or that the budget is exhausted.
//!
//! The decision is pure: it never runs reflection, calls tools, or asks a human.
//! The caller maps the chosen tier onto CAR's machinery (`reflect()`, tool
//! execution, the HITL `ApprovalLedger`).

use car_memgine::cascade::{decide_cascade, CascadePolicy};

/// Decide the cost-aware cascade. `policy_json` is a [`CascadePolicy`] as JSON
/// (`{ confidence_target, budget, tiers: [{ tier, cost, expected_confidence }] }`
/// where `tier` is `"self_reflect" | "tool_verify" | "human_expert"`). Returns
/// the [`car_memgine::cascade::CascadeOutcome`] as JSON — one of
/// `{ decision: "already_confident", ... }`, `{ decision: "accept", tier, ... }`,
/// or `{ decision: "exhausted", best_tier, ... }`.
pub fn decide(current_confidence: f64, policy_json: &str) -> Result<String, String> {
    let policy: CascadePolicy = crate::from_json("policy", policy_json)?;
    let outcome = decide_cascade(current_confidence, &policy);
    crate::to_json(&outcome)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::Value;

    const POLICY: &str = r#"{
        "confidence_target": 0.8,
        "budget": 100.0,
        "tiers": [
            {"tier": "self_reflect", "cost": 1.0, "expected_confidence": 0.6},
            {"tier": "tool_verify", "cost": 5.0, "expected_confidence": 0.85},
            {"tier": "human_expert", "cost": 50.0, "expected_confidence": 0.99}
        ]
    }"#;

    #[test]
    fn escalates_to_tool_verify_for_mid_target() {
        let out = decide(0.2, POLICY).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["decision"], "accept");
        assert_eq!(v["tier"], "tool_verify");
        assert_eq!(v["cost_spent"], 6.0);
    }

    #[test]
    fn already_confident_round_trips() {
        let out = decide(0.95, POLICY).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["decision"], "already_confident");
    }

    #[test]
    fn invalid_json_errors() {
        assert!(decide(0.5, "not json").is_err());
    }
}