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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! Configuration for the memgine engine.
/// Context assembly mode — controls which layers are included.
///
/// `Full` runs all layers including embedding-based scoring, skill lookup,
/// LLM summarization, and known-unknowns extraction.
///
/// `Fast` skips expensive operations for latency-sensitive paths (voice,
/// real-time). Keeps: identity, constraints, recent conversation (no
/// embedding flush), environment. Skips: skill lookup, embedding-based
/// fact scoring, known-unknowns extraction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContextMode {
#[default]
Full,
Fast,
}
#[derive(Debug, Clone, Copy)]
pub struct LayerBudget {
pub fraction: f64,
}
impl LayerBudget {
pub fn tokens(&self, total: usize) -> usize {
(total as f64 * self.fraction) as usize
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompactionThresholds {
pub soft: f64,
pub hard: f64,
}
impl Default for CompactionThresholds {
fn default() -> Self {
Self {
soft: 0.70,
hard: 0.95,
}
}
}
#[derive(Debug, Clone)]
pub struct MemgineConfig {
/// Fixed token budget for context assembly (used when no model context window is provided).
pub token_budget: usize,
pub layer1_budget: LayerBudget,
pub layer2_budget: LayerBudget,
pub layer3_budget: LayerBudget,
pub layer4_budget: LayerBudget,
pub thresholds: CompactionThresholds,
pub working_set_max: usize,
pub working_set_keep_recent: usize,
pub environment_max: usize,
/// Maximum skills to include in context (SkillRL K parameter, default 6).
pub max_skills_in_context: usize,
/// Evolution threshold — trigger evolution when domain success rate drops below this.
pub evolution_threshold: f64,
/// Validation-gated skill optimization (SkillOpt-inspired — see
/// `docs/solutions/gated-skill-optimization.md`).
///
/// `min_trial_samples`: a provisional candidate must accrue at least this
/// many real outcomes before the promotion gate evaluates it. Below this it
/// keeps trialing — promoting on fewer samples is statistical noise. While a
/// candidate is under this floor, `find_skill` serves IT in place of its
/// incumbent (trial-until-sampled), guaranteeing it accrues outcomes instead
/// of starving; once it reaches the floor, the incumbent is served again and
/// the gate decides on the next `consolidate()`.
pub min_trial_samples: usize,
/// `wilson_z`: z-score for the Wilson lower bounds compared by the promotion
/// gate. Default 1.2816 ≈ 90% one-sided confidence. Higher = more
/// conservative (slower, safer promotions). The gate compares the
/// candidate's Wilson lower bound against the INCUMBENT's Wilson lower bound
/// (like-for-like) — not against the incumbent's raw success ratio, which
/// would be an unbeatable 1.0 after any clean promotion.
pub wilson_z: f64,
/// `optimizer_model`: optional model override for the skill-optimization
/// inference calls (distillation, evolution, repair) and conversation
/// reflection (fact authoring) — SkillOpt's separate
/// `optimizer_model` vs `target_model`. Skill generation is a meta-task
/// where quality matters more than latency, so a deployment can point it at
/// a stronger model than the runtime default. `None` = use default routing
/// (no change). Settable via `.car/config.toml`.
pub optimizer_model: Option<String>,
/// `promotion_carry_forward`: fraction [0.0, 1.0] of the incumbent's
/// accumulated success/fail counts carried into a promoted candidate, on top
/// of the candidate's full trial record. SkillOpt avoids this question by
/// scoring candidate and incumbent on the same fixed held-out set; CAR uses
/// online outcomes, so a clean re-baseline (0.0) would let a few fresh fails
/// degrade a slot that was reliable for months. Default 0.5 discounts the
/// predecessor's history (it was *different* code) while preserving enough
/// evidence that the promoted slot keeps a stable measured quality. 0.0 =
/// re-baseline to the trial window; 1.0 = full carry.
pub promotion_carry_forward: f64,
/// Budget weight multiplier for code facts (denser content, needs more tokens).
pub code_budget_weight: f64,
/// Budget weight multiplier for structured data facts (compressible key-value).
pub structured_budget_weight: f64,
/// Number of most-recent conversation turns to always keep verbatim during compaction.
pub conversation_keep_recent: usize,
/// Maximum turns to group per summary batch during compaction.
pub compaction_batch_size: usize,
/// Number of conversation turns between speculative compaction runs.
/// After every N turns, background summaries are pre-computed so they're
/// ready when context fills up. Set to 0 to disable. Default: 10.
pub speculative_compaction_interval: usize,
/// Tokens reserved for the model's response output (default 4096).
pub response_reservation: usize,
/// Fraction of remaining context window to use for context assembly (default 0.40).
/// Only used when a model context window is provided to `effective_budget()`.
pub context_budget_fraction: f64,
/// Utility-aware retrieval weight (U-Mem, arXiv 2602.22406). When > 0, fact
/// scoring blends in each fact's learned utility posterior
/// ([`crate::graph::FactMetadata::utility_posterior`]) via an
/// upper-confidence bound, on top of semantic relevance — proven facts are
/// exploited, cold-start facts get an explore bonus. Default 0.0 keeps
/// retrieval ordering exactly as before (pure relevance + legacy boosts).
pub utility_weight: f64,
/// Exploration coefficient for the utility UCB blend (see `utility_weight`).
/// Multiplies the posterior's uncertainty term, so a higher value surfaces
/// untried facts more aggressively. Only consulted when `utility_weight > 0`.
/// Default 0.0 — pure exploitation (rank by posterior mean).
pub utility_exploration: f64,
}
impl Default for MemgineConfig {
fn default() -> Self {
Self {
token_budget: 8000,
layer1_budget: LayerBudget { fraction: 0.05 },
layer2_budget: LayerBudget { fraction: 0.50 },
layer3_budget: LayerBudget { fraction: 0.30 },
layer4_budget: LayerBudget { fraction: 0.15 },
thresholds: CompactionThresholds::default(),
working_set_max: 10,
working_set_keep_recent: 3,
environment_max: 5,
max_skills_in_context: 6,
evolution_threshold: 0.6,
min_trial_samples: 8,
wilson_z: 1.2816,
promotion_carry_forward: 0.5,
optimizer_model: None,
code_budget_weight: 1.5,
structured_budget_weight: 0.8,
conversation_keep_recent: 6,
compaction_batch_size: 8,
speculative_compaction_interval: 10,
response_reservation: 4096,
context_budget_fraction: 0.40,
utility_weight: 0.0,
utility_exploration: 0.0,
}
}
}
/// Max for `utility_weight` — the blend coefficient is documented `0..1`
/// (matches the stateless `utility_rank` FFI), so a weight above 1 would let
/// the utility term dwarf semantic relevance in `score_facts`.
pub const UTILITY_WEIGHT_MAX: f64 = 1.0;
/// Max for `utility_exploration` — the UCB uncertainty coefficient. Sane values
/// sit near `sqrt(2) ≈ 1.41` (UCB1); 4 leaves headroom for aggressive
/// exploration while still rejecting absurd / non-finite inputs.
pub const UTILITY_EXPLORATION_MAX: f64 = 4.0;
/// Clamp a utility knob to `[0, max]`, mapping non-finite (`NaN`/`±inf`) to 0.
/// `f64::clamp` propagates `NaN`, so the finiteness guard is load-bearing.
pub(crate) fn clamp_utility(x: f64, max: f64) -> f64 {
if x.is_finite() {
x.clamp(0.0, max)
} else {
0.0
}
}
impl MemgineConfig {
/// Set the utility-aware retrieval blend (U-Mem, arXiv 2602.22406),
/// sanitizing both inputs: `weight` to `[0, UTILITY_WEIGHT_MAX]` (0 = pure
/// relevance, ordering unchanged) and `exploration` to
/// `[0, UTILITY_EXPLORATION_MAX]`; non-finite inputs become 0. The single
/// clamp point shared by the runtime setter and the `.car/` config override.
pub fn set_utility_retrieval(&mut self, weight: f64, exploration: f64) {
self.utility_weight = clamp_utility(weight, UTILITY_WEIGHT_MAX);
self.utility_exploration = clamp_utility(exploration, UTILITY_EXPLORATION_MAX);
}
pub fn layer_tokens(&self, layer: u8) -> usize {
match layer {
1 => self.layer1_budget.tokens(self.token_budget),
2 => self.layer2_budget.tokens(self.token_budget),
3 => self.layer3_budget.tokens(self.token_budget),
4 => self.layer4_budget.tokens(self.token_budget),
_ => 0,
}
}
/// Compute the effective token budget for context assembly.
///
/// When `model_context_window` is provided, dynamically sizes the budget:
/// budget = (context_window - response_reservation) * context_budget_fraction
/// Clamped to a minimum of 2000 tokens to remain useful.
///
/// When `None`, falls back to the fixed `token_budget` (default 8000).
pub fn effective_budget(&self, model_context_window: Option<usize>) -> usize {
match model_context_window {
Some(ctx_window) if ctx_window > 0 => {
let remaining = ctx_window.saturating_sub(self.response_reservation);
let dynamic = (remaining as f64 * self.context_budget_fraction) as usize;
// Clamp: at least 2000 tokens, at most the remaining window
dynamic.max(2000).min(remaining)
}
_ => self.token_budget,
}
}
pub fn validate(&self) -> Result<(), String> {
let total = self.layer1_budget.fraction
+ self.layer2_budget.fraction
+ self.layer3_budget.fraction
+ self.layer4_budget.fraction;
if (total - 1.0).abs() > 0.01 {
return Err(format!("Layer fractions must sum to 1.0, got {}", total));
}
if self.thresholds.soft >= self.thresholds.hard {
return Err("Soft threshold must be < hard threshold".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effective_budget_none_uses_fixed() {
let config = MemgineConfig::default();
assert_eq!(config.effective_budget(None), 8000);
}
#[test]
fn effective_budget_large_model() {
let config = MemgineConfig::default();
// GPT-5.2: 272,000 context window
// (272000 - 4096) * 0.40 = 107,161
let budget = config.effective_budget(Some(272_000));
assert_eq!(budget, 107_161);
}
#[test]
fn effective_budget_small_model() {
let config = MemgineConfig::default();
// 8K model: (8000 - 4096) * 0.40 = 1561, clamped to 2000
let budget = config.effective_budget(Some(8_000));
assert_eq!(budget, 2000);
}
#[test]
fn effective_budget_zero_window() {
let config = MemgineConfig::default();
assert_eq!(config.effective_budget(Some(0)), 8000);
}
#[test]
fn effective_budget_medium_model() {
let config = MemgineConfig::default();
// 128K model: (128000 - 4096) * 0.40 = 49,561
let budget = config.effective_budget(Some(128_000));
assert_eq!(budget, 49_561);
}
}