Skip to main content

phi_core/context/
config.rs

1use super::token::TokenCounter;
2use super::{BlockCompactionStrategy, CompactionStrategy};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6// ---------------------------------------------------------------------------
7// Compaction scope
8// ---------------------------------------------------------------------------
9
10/// Controls how many earlier loops are included in compaction and context loading.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum CompactionScope {
13    /// Compact a fixed number of earlier loops on the active chain.
14    FixedCount(usize),
15    /// Walk the chain backward, accumulating per-loop token estimates,
16    /// and stop when `max_context_tokens` would be exceeded.
17    ///
18    /// NOTE: The scope can include loops whose raw messages EXCEED
19    /// `max_context_tokens`. This is intentional — the compacted summaries
20    /// of those loops will fit in the window, even though the originals
21    /// did not. This enables richer context for summarisation strategies
22    /// (e.g. LLM summarisers) that compress large loops into compact
23    /// representations that then fit within the budget.
24    TokenBudget,
25}
26
27impl Default for CompactionScope {
28    fn default() -> Self {
29        Self::FixedCount(3)
30    }
31}
32
33// ---------------------------------------------------------------------------
34// Compaction configuration
35// ---------------------------------------------------------------------------
36
37/// Full compaction policy — controls both WHEN and HOW to compact.
38#[derive(Clone, Serialize, Deserialize)]
39pub struct CompactionConfig {
40    // ── WHEN to compact ──
41    /// Fraction of `max_context_tokens` below which headroom is measured.
42    /// Compaction fires when headroom drops below `compact_budget_threshold_pct`.
43    /// Default: 0.90 (90%).
44    pub compact_at_pct: f64,
45    /// Minimum remaining headroom fraction before compaction fires.
46    /// Default: 0.05 (5%). With defaults at 100k/4k: fires at ~81k tokens.
47    pub compact_budget_threshold_pct: f64,
48    /// Scope controlling how many earlier loops to compact and load.
49    /// Default: `FixedCount(3)`.
50    pub compaction_scope: CompactionScope,
51
52    // ── HOW to compact ──
53    /// Turns to keep verbatim from the start (most recent loop only). Default: 2.
54    pub keep_first_turns: usize,
55    /// Minimum turns to keep from the end (most recent loop only).
56    /// Extended to turn boundary so ToolCall/ToolResult pairs are never split.
57    /// Default: 10.
58    pub keep_recent_turns: usize,
59    /// Token budget for the summarised middle section. Default: 2_000.
60    ///
61    /// This is a budget, not a per-turn limit. Implementations of
62    /// `BlockCompactionStrategy::keep_compacted()` should aim to summarise
63    /// ALL turns in the range within this budget — e.g. by producing shorter
64    /// per-turn summaries or an LLM-generated holistic digest.
65    /// `DefaultBlockCompaction` is a basic implementation that generates
66    /// per-turn one-liners and drops remaining turns when the budget runs out.
67    pub max_summary_tokens: usize,
68    /// Max lines per tool output in the keep_recent section. Default: 50.
69    pub tool_output_max_lines: usize,
70
71    // ── Focus message ──
72    /// Optional focus message to guide compaction summarization.
73    /// When set, prepended to the compacted section to tell the model what to prioritize.
74    /// Example: "Focus on specification details, API contracts, and architectural decisions."
75    #[serde(default)]
76    pub focus_message: Option<String>,
77
78    // ── Strategy objects (G5 — moved from AgentLoopConfig) ──
79    /// Custom in-memory compaction strategy. When set, replaces `DefaultCompaction`.
80    /// Used when `AgentContext.session` is `None` (sub-agents, tests, sessionless runs).
81    #[serde(skip)]
82    pub in_memory_strategy: Option<Arc<dyn CompactionStrategy>>,
83    /// Block-based compaction strategy for Session-aware compaction.
84    /// When set, replaces `DefaultBlockCompaction`.
85    /// Used when `AgentContext.session` is `Some`.
86    #[serde(skip)]
87    pub block_strategy: Option<Arc<dyn BlockCompactionStrategy>>,
88}
89
90impl std::fmt::Debug for CompactionConfig {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("CompactionConfig")
93            .field("compact_at_pct", &self.compact_at_pct)
94            .field(
95                "compact_budget_threshold_pct",
96                &self.compact_budget_threshold_pct,
97            )
98            .field("compaction_scope", &self.compaction_scope)
99            .field("keep_first_turns", &self.keep_first_turns)
100            .field("keep_recent_turns", &self.keep_recent_turns)
101            .field("max_summary_tokens", &self.max_summary_tokens)
102            .field("tool_output_max_lines", &self.tool_output_max_lines)
103            .field("focus_message", &self.focus_message)
104            .field(
105                "in_memory_strategy",
106                &self.in_memory_strategy.as_ref().map(|_| "..."),
107            )
108            .field(
109                "block_strategy",
110                &self.block_strategy.as_ref().map(|_| "..."),
111            )
112            .finish()
113    }
114}
115
116impl Default for CompactionConfig {
117    fn default() -> Self {
118        Self {
119            compact_at_pct: 0.90,
120            compact_budget_threshold_pct: 0.05,
121            compaction_scope: CompactionScope::default(),
122            keep_first_turns: 2,
123            keep_recent_turns: 10,
124            max_summary_tokens: 2_000,
125            tool_output_max_lines: 50,
126            focus_message: None,
127            in_memory_strategy: None,
128            block_strategy: None,
129        }
130    }
131}
132
133// ---------------------------------------------------------------------------
134// Context configuration
135// ---------------------------------------------------------------------------
136
137/// Configuration for context management — model constraints + compaction policy.
138///
139/// `CompactionConfig` is a required field: if you set a context limit,
140/// compaction is always ready with sensible defaults. Compaction as a whole
141/// is disabled by setting `context_config: None` on `AgentLoopConfig`.
142#[derive(Clone, Serialize, Deserialize)]
143pub struct ContextConfig {
144    /// Maximum context tokens (the model's context window).
145    pub max_context_tokens: usize,
146    /// Tokens reserved for the system prompt.
147    pub system_prompt_tokens: usize,
148    /// Compaction policy — always present when context limits are set.
149    pub compaction: CompactionConfig,
150
151    /// Custom token counter. When `None`, uses `HeuristicTokenCounter` (chars/4).
152    /// Set to a custom `TokenCounter` for model-specific tokenization.
153    #[serde(skip)]
154    pub token_counter: Option<Arc<dyn TokenCounter>>,
155
156    // Legacy fields — kept for backward compatibility with existing configs.
157    // New code should use `compaction.*` instead.
158    #[serde(default)]
159    pub keep_recent: usize,
160    #[serde(default)]
161    pub keep_first: usize,
162    #[serde(default)]
163    pub tool_output_max_lines: usize,
164}
165
166impl std::fmt::Debug for ContextConfig {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("ContextConfig")
169            .field("max_context_tokens", &self.max_context_tokens)
170            .field("system_prompt_tokens", &self.system_prompt_tokens)
171            .field("compaction", &self.compaction)
172            .field("token_counter", &self.token_counter.as_ref().map(|_| "..."))
173            .finish()
174    }
175}
176
177impl ContextConfig {
178    /// Returns the configured token counter, or the default heuristic (chars/4).
179    pub fn counter(&self) -> &dyn TokenCounter {
180        self.token_counter
181            .as_deref()
182            .unwrap_or(&super::token::HeuristicTokenCounter)
183    }
184}
185
186impl Default for ContextConfig {
187    fn default() -> Self {
188        Self {
189            max_context_tokens: 100_000,
190            system_prompt_tokens: 4_000,
191            compaction: CompactionConfig::default(),
192            token_counter: None,
193            keep_recent: 10,
194            keep_first: 2,
195            tool_output_max_lines: 50,
196        }
197    }
198}