1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//! 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());
}
}