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 the active-task tail, anchored at the latest substantive
18//!   user turn rather than short follow-ups like "continue". See
19//!   [`derive_with_policy`](super::context::derive_with_policy) for
20//!   the implementation.
21//! * [`DerivePolicy::Incremental`] — Liu et al. select-then-pack
22//!   relevance scoring against a per-(message) [`RelevanceMeta`]
23//!   sidecar. Step-18-driven hierarchical summary lookup is wired
24//!   in a later commit; until then the policy returns the selected
25//!   tail without summary fillers.
26//! * [`DerivePolicy::OracleReplay`] *(reserved, Phase B)* — ClawVM
27//!   replay oracle with `h`-turn future-demand lookahead.
28//!
29//! [`derive_context`]: super::context::derive_context
30//! [`derive_with_policy`]: super::context::derive_with_policy
31//! [plan step 23]: crate::session::context
32//!
33//! ## Examples
34//!
35//! ```rust
36//! use codetether_agent::session::derive_policy::DerivePolicy;
37//!
38//! let legacy = DerivePolicy::Legacy;
39//! let reset = DerivePolicy::Reset { threshold_tokens: 16_000 };
40//!
41//! assert!(matches!(legacy, DerivePolicy::Legacy));
42//! assert!(matches!(reset, DerivePolicy::Reset { .. }));
43//! ```
44
45use serde::{Deserialize, Serialize};
46
47/// Per-session derivation strategy selector.
48///
49/// Defaults to [`DerivePolicy::Legacy`] so existing code paths that do
50/// not opt into a new policy get the Phase A behaviour unchanged.
51#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
52#[serde(tag = "policy", rename_all = "snake_case")]
53pub enum DerivePolicy {
54    /// Phase A clone + experimental + `enforce_on_messages` + pairing.
55    /// The historical pipeline.
56    Legacy,
57    /// Lu et al. (arXiv:2510.06727) reset-to-(prompt, summary).
58    ///
59    /// When the request's estimated token cost exceeds
60    /// `threshold_tokens`, replace everything older than the last user
61    /// turn with a single RLM-generated summary. The derived context for
62    /// the next provider call contains `[summary, active_task_tail...]`
63    /// so short continuation turns do not drop the task-defining prompt.
64    Reset {
65        /// Token budget that triggers the reset. Typically ~95 % of
66        /// the model's working context length.
67        threshold_tokens: usize,
68    },
69    /// Liu et al. (arXiv:2512.22087) incremental select-then-pack
70    /// derivation.
71    ///
72    /// Score every entry against the latest user turn's
73    /// [`RelevanceMeta`](crate::session::relevance::RelevanceMeta) and
74    /// greedy-pack the highest-scoring set into `budget_tokens`. The
75    /// recent window is always retained; older entries are kept only
76    /// when their relevance to the active task is high.
77    Incremental {
78        /// Per-call working-context budget. Older entries are dropped
79        /// once the selected set would exceed this estimate.
80        budget_tokens: usize,
81    },
82    /// ClawVM replay oracle (Phase B step 22). Evaluation-only.
83    ///
84    /// Given a recorded trace, picks representations with `h`-turn
85    /// future-demand lookahead to minimise fault count. Not suitable
86    /// for production — only for benchmarking online vs oracle gap.
87    OracleReplay {
88        /// Lookahead horizon in turns. ClawVM uses h=3.
89        lookahead: usize,
90    },
91}
92
93impl DerivePolicy {
94    /// Human-readable short name for logs, journal entries, and
95    /// telemetry.
96    ///
97    /// # Examples
98    ///
99    /// ```rust
100    /// use codetether_agent::session::derive_policy::DerivePolicy;
101    ///
102    /// assert_eq!(DerivePolicy::Legacy.kind(), "legacy");
103    /// assert_eq!(
104    ///     DerivePolicy::Reset { threshold_tokens: 0 }.kind(),
105    ///     "reset",
106    /// );
107    /// ```
108    pub fn kind(&self) -> &'static str {
109        match self {
110            DerivePolicy::Legacy => "legacy",
111            DerivePolicy::Reset { .. } => "reset",
112            DerivePolicy::Incremental { .. } => "incremental",
113            DerivePolicy::OracleReplay { .. } => "oracle_replay",
114        }
115    }
116}
117
118impl Default for DerivePolicy {
119    fn default() -> Self {
120        DerivePolicy::Legacy
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_is_legacy() {
130        assert!(matches!(DerivePolicy::default(), DerivePolicy::Legacy));
131    }
132
133    #[test]
134    fn reset_carries_threshold() {
135        let p = DerivePolicy::Reset {
136            threshold_tokens: 8192,
137        };
138        if let DerivePolicy::Reset { threshold_tokens } = p {
139            assert_eq!(threshold_tokens, 8192);
140        } else {
141            panic!("expected Reset");
142        }
143    }
144
145    #[test]
146    fn kind_is_snake_case_and_distinct() {
147        assert_eq!(DerivePolicy::Legacy.kind(), "legacy");
148        assert_eq!(
149            DerivePolicy::Reset {
150                threshold_tokens: 0
151            }
152            .kind(),
153            "reset"
154        );
155        assert_eq!(
156            DerivePolicy::Incremental { budget_tokens: 0 }.kind(),
157            "incremental"
158        );
159    }
160
161    #[test]
162    fn incremental_carries_budget() {
163        let p = DerivePolicy::Incremental {
164            budget_tokens: 16_384,
165        };
166        if let DerivePolicy::Incremental { budget_tokens } = p {
167            assert_eq!(budget_tokens, 16_384);
168        } else {
169            panic!("expected Incremental");
170        }
171    }
172
173    #[test]
174    fn policy_round_trips_through_serde() {
175        let p = DerivePolicy::Reset {
176            threshold_tokens: 12_000,
177        };
178        let json = serde_json::to_string(&p).unwrap();
179        assert!(json.contains("\"policy\":\"reset\""));
180        let back: DerivePolicy = serde_json::from_str(&json).unwrap();
181        assert!(matches!(
182            back,
183            DerivePolicy::Reset {
184                threshold_tokens: 12_000
185            }
186        ));
187    }
188}