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}