car-ffi-common 0.32.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for structured context eviction (arXiv 2606.11213 / 2606.22528).
//!
//! Stateless helper, binding-only — see `docs/proposals/context-eviction.md`.
//! Plans a deterministic, budget-bounded eviction over typed trajectory
//! episodes: shed persisted action results first, preserve user turns and the
//! active reasoning frontier, never evict pinned constraints.

use car_memgine::eviction::{plan_eviction, ContextEpisode};

/// Plan a context eviction. `episodes_json` is a JSON array of `ContextEpisode`
/// (`{ id, kind: "constraint" | "user_turn" | "agent_reasoning" | "action_result"
/// | "observation", tokens?, persisted?, pinned?, recency? }`); `budget` is the
/// token ceiling. Returns the `EvictionPlan` JSON `{ evicted: [..ids..],
/// retained_tokens, pinned_tokens, within_budget }`. `within_budget = false`
/// signals the caller must fall back to summarization.
pub fn plan(episodes_json: &str, budget: u64) -> Result<String, String> {
    let episodes: Vec<ContextEpisode> = crate::from_json("episodes", episodes_json)?;
    let plan = plan_eviction(&episodes, budget);
    crate::to_json(&plan)
}

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

    #[test]
    fn evicts_persisted_action_first() {
        let eps = r#"[
            {"id":"user","kind":"user_turn","tokens":30,"recency":5},
            {"id":"act","kind":"action_result","tokens":40,"persisted":true,"recency":2}
        ]"#;
        let out = plan(eps, 40).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["evicted"][0], "act");
        assert_eq!(v["within_budget"], true);
    }

    #[test]
    fn constraint_is_pinned() {
        let eps = r#"[{"id":"c","kind":"constraint","tokens":80}]"#;
        let out = plan(eps, 10).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["evicted"].as_array().unwrap().len(), 0);
        assert_eq!(v["within_budget"], false);
        assert_eq!(v["pinned_tokens"], 80);
    }

    #[test]
    fn invalid_json_errors() {
        assert!(plan("nope", 10).is_err());
    }
}