Skip to main content

codetether_agent/session/
derive_policy.rs

1//! Selectable derivation policies for [`derive_with_policy`].
2//!
3//! [`derive_context`] in Phase A is a single fixed pipeline: clone +
4//! experimental + `enforce_on_messages` + pairing repair. Phase B
5//! parameterises the pipeline with a [`DerivePolicy`] so different
6//! research directions can share the same `&Session → DerivedContext`
7//! signature.
8//!
9//! ## Variants
10//!
11//! * [`DerivePolicy::Legacy`] — the Phase A behaviour. Current default
12//!   until the Pareto benchmark ([plan step 23]) demonstrates one of
13//!   the alternatives is dominant.
14//! * [`DerivePolicy::Reset`] — **Lu et al.** reset-to-(prompt, summary)
15//!   semantic from arXiv:2510.06727. When the token estimate exceeds
16//!   the threshold, compresses the prefix to a single summary message
17//!   and keeps only the most recent user turn. See
18//!   [`derive_with_policy`](super::context::derive_with_policy) for
19//!   the implementation.
20//! * [`DerivePolicy::Incremental`] *(reserved, Phase B)* — Liu et al.
21//!   scoring + hierarchical summary lookup.
22//! * [`DerivePolicy::OracleReplay`] *(reserved, Phase B)* — ClawVM
23//!   replay oracle with `h`-turn future-demand lookahead.
24//!
25//! [`derive_context`]: super::context::derive_context
26//! [`derive_with_policy`]: super::context::derive_with_policy
27//! [plan step 23]: crate::session::context
28//!
29//! ## Examples
30//!
31//! ```rust
32//! use codetether_agent::session::derive_policy::DerivePolicy;
33//!
34//! let legacy = DerivePolicy::Legacy;
35//! let reset = DerivePolicy::Reset { threshold_tokens: 16_000 };
36//!
37//! assert!(matches!(legacy, DerivePolicy::Legacy));
38//! assert!(matches!(reset, DerivePolicy::Reset { .. }));
39//! ```
40
41use serde::{Deserialize, Serialize};
42
43/// Per-session derivation strategy selector.
44///
45/// Defaults to [`DerivePolicy::Legacy`] so existing code paths that do
46/// not opt into a new policy get the Phase A behaviour unchanged.
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
48#[serde(tag = "policy", rename_all = "snake_case")]
49pub enum DerivePolicy {
50    /// Phase A clone + experimental + `enforce_on_messages` + pairing.
51    /// The historical pipeline.
52    Legacy,
53    /// Lu et al. (arXiv:2510.06727) reset-to-(prompt, summary).
54    ///
55    /// When the request's estimated token cost exceeds
56    /// `threshold_tokens`, replace everything older than the last user
57    /// turn with a single RLM-generated summary and discard the rest of
58    /// the tail. The derived context for the next provider call
59    /// contains at most `[summary, last_user_turn]`.
60    Reset {
61        /// Token budget that triggers the reset. Typically ~95 % of
62        /// the model's working context length.
63        threshold_tokens: usize,
64    },
65}
66
67impl DerivePolicy {
68    /// Human-readable short name for logs, journal entries, and
69    /// telemetry.
70    ///
71    /// # Examples
72    ///
73    /// ```rust
74    /// use codetether_agent::session::derive_policy::DerivePolicy;
75    ///
76    /// assert_eq!(DerivePolicy::Legacy.kind(), "legacy");
77    /// assert_eq!(
78    ///     DerivePolicy::Reset { threshold_tokens: 0 }.kind(),
79    ///     "reset",
80    /// );
81    /// ```
82    pub fn kind(&self) -> &'static str {
83        match self {
84            DerivePolicy::Legacy => "legacy",
85            DerivePolicy::Reset { .. } => "reset",
86        }
87    }
88}
89
90impl Default for DerivePolicy {
91    fn default() -> Self {
92        DerivePolicy::Legacy
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn default_is_legacy() {
102        assert!(matches!(DerivePolicy::default(), DerivePolicy::Legacy));
103    }
104
105    #[test]
106    fn reset_carries_threshold() {
107        let p = DerivePolicy::Reset {
108            threshold_tokens: 8192,
109        };
110        if let DerivePolicy::Reset { threshold_tokens } = p {
111            assert_eq!(threshold_tokens, 8192);
112        } else {
113            panic!("expected Reset");
114        }
115    }
116
117    #[test]
118    fn kind_is_snake_case_and_distinct() {
119        assert_eq!(DerivePolicy::Legacy.kind(), "legacy");
120        assert_eq!(
121            DerivePolicy::Reset {
122                threshold_tokens: 0
123            }
124            .kind(),
125            "reset"
126        );
127    }
128
129    #[test]
130    fn policy_round_trips_through_serde() {
131        let p = DerivePolicy::Reset {
132            threshold_tokens: 12_000,
133        };
134        let json = serde_json::to_string(&p).unwrap();
135        assert!(json.contains("\"policy\":\"reset\""));
136        let back: DerivePolicy = serde_json::from_str(&json).unwrap();
137        assert!(matches!(
138            back,
139            DerivePolicy::Reset {
140                threshold_tokens: 12_000
141            }
142        ));
143    }
144}