zeph_config/memory/reasoning.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Reasoning, probing, and memory chain-of-thought configuration.
5//!
6//! Memory-augmented reasoning ([`ReasoningConfig`]), compaction probes, `MemCoT`
7//! semantic state, auto-dream consolidation, and magic-docs synthesis.
8
9use crate::providers::ProviderName;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// autoDream background memory consolidation configuration (#2697).
15///
16/// When `enabled = true`, a constrained consolidation subagent runs after
17/// a session ends if both `min_sessions` and `min_hours` gates pass.
18#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
19#[serde(default)]
20pub struct AutoDreamConfig {
21 /// Enable autoDream consolidation. Default: `false`.
22 pub enabled: bool,
23 /// Minimum number of sessions between consolidations. Default: `3`.
24 pub min_sessions: u32,
25 /// Minimum hours between consolidations. Default: `24`.
26 pub min_hours: u32,
27 /// Provider name from `[[llm.providers]]` for consolidation LLM calls.
28 /// Falls back to the primary provider when empty. Default: `""`.
29 pub consolidation_provider: ProviderName,
30 /// Maximum agent loop iterations for the consolidation subagent. Default: `8`.
31 pub max_iterations: u8,
32 /// LLM call timeout per `propose_merge_op` invocation, in seconds. Default: `30`.
33 #[serde(default = "default_autodream_llm_timeout_secs")]
34 pub llm_timeout_secs: u64,
35}
36
37impl Default for AutoDreamConfig {
38 fn default() -> Self {
39 Self {
40 enabled: false,
41 min_sessions: 3,
42 min_hours: 24,
43 consolidation_provider: ProviderName::default(),
44 max_iterations: 8,
45 llm_timeout_secs: default_autodream_llm_timeout_secs(),
46 }
47 }
48}
49
50fn default_autodream_llm_timeout_secs() -> u64 {
51 30
52}
53
54/// `MagicDocs` auto-maintained markdown configuration (#2702).
55///
56/// When `enabled = true`, files read via file tools that contain a `# MAGIC DOC:` header
57/// are registered and periodically updated by a constrained subagent.
58#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
59#[serde(default)]
60pub struct MagicDocsConfig {
61 /// Enable `MagicDocs` auto-maintenance. Default: `false`.
62 pub enabled: bool,
63 /// Minimum turns between updates for a given doc path. Default: `5`.
64 pub min_turns_between_updates: u32,
65 /// Provider name from `[[llm.providers]]` for doc update LLM calls.
66 /// Falls back to the primary provider when empty. Default: `""`.
67 pub update_provider: ProviderName,
68 /// Maximum agent loop iterations per doc update. Default: `4`.
69 pub max_iterations: u8,
70}
71
72impl Default for MagicDocsConfig {
73 fn default() -> Self {
74 Self {
75 enabled: false,
76 min_turns_between_updates: 5,
77 update_provider: ProviderName::default(),
78 max_iterations: 4,
79 }
80 }
81}
82
83/// `ReasoningBank`: distilled reasoning strategy memory configuration (#3342).
84///
85/// When `enabled = true`, each completed agent turn is evaluated by a self-judge LLM call.
86/// Successful and failed reasoning chains are compressed into short, generalizable strategy
87/// summaries. At context-build time, top-k strategies are retrieved by embedding similarity
88/// and injected into the prompt preamble.
89///
90/// All LLM work (self-judge, distillation) runs asynchronously — never on the turn thread.
91///
92/// # Example
93///
94/// ```toml
95/// [memory.reasoning]
96/// enabled = true
97/// extract_provider = "fast"
98/// distill_provider = "fast"
99/// top_k = 3
100/// store_limit = 1000
101/// ```
102#[derive(Debug, Clone, Deserialize, Serialize)]
103#[serde(default)]
104pub struct ReasoningConfig {
105 /// Enable the reasoning-bank pipeline. Default: `false`.
106 pub enabled: bool,
107 /// Provider name from `[[llm.providers]]` for the self-judge step.
108 /// Falls back to the primary provider when empty. Default: `""`.
109 pub extract_provider: ProviderName,
110 /// Provider name from `[[llm.providers]]` for the distillation step.
111 /// Falls back to the primary provider when empty. Default: `""`.
112 pub distill_provider: ProviderName,
113 /// Number of strategies retrieved per turn for context injection. Default: `3`.
114 pub top_k: usize,
115 /// Maximum stored strategies; oldest unused are evicted when limit is reached. Default: `1000`.
116 pub store_limit: usize,
117 /// Maximum number of recent messages passed to the self-judge LLM. Default: `6`.
118 pub max_messages: usize,
119 /// Per-message content truncation limit (chars) before building the judge transcript. Default: `2000`.
120 pub max_message_chars: usize,
121 /// Maximum token budget for injected reasoning strategies in context. Default: `500`.
122 pub context_budget_tokens: usize,
123 /// Minimum number of messages required before self-judge fires. Default: `2`.
124 pub min_messages: usize,
125 /// Timeout in seconds for the self-judge LLM call. Default: `30`.
126 pub extraction_timeout_secs: u64,
127 /// Timeout in seconds for the distillation LLM call. Default: `30`.
128 pub distill_timeout_secs: u64,
129 /// Maximum number of recent messages passed to the self-judge evaluator.
130 /// Narrowing to the last user+assistant pair improves classification accuracy.
131 /// Default: `2`.
132 pub self_judge_window: usize,
133 /// Minimum characters in the assistant response to trigger self-judge.
134 /// Short or trivial responses are skipped. Default: `50`.
135 pub min_assistant_chars: usize,
136}
137
138impl Default for ReasoningConfig {
139 fn default() -> Self {
140 Self {
141 enabled: false,
142 extract_provider: ProviderName::default(),
143 distill_provider: ProviderName::default(),
144 top_k: 3,
145 store_limit: 1000,
146 max_messages: 6,
147 max_message_chars: 2000,
148 context_budget_tokens: 500,
149 min_messages: 2,
150 extraction_timeout_secs: 30,
151 distill_timeout_secs: 30,
152 self_judge_window: 2,
153 min_assistant_chars: 50,
154 }
155 }
156}
157
158// ── Compaction probe config (moved from zeph-memory) ─────────────────────────
159
160/// Functional category of a compaction probe question.
161///
162/// `zeph-memory` re-exports this type from here.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
164#[serde(rename_all = "lowercase")]
165#[non_exhaustive]
166pub enum ProbeCategory {
167 /// Did specific facts survive? (file paths, function names, values, decisions)
168 Recall,
169 /// Does the agent know which files/tools/URLs it used?
170 Artifact,
171 /// Can it pick up mid-task? (current step, next steps, blockers, open questions)
172 Continuation,
173 /// Are past reasoning traces intact? (why X over Y, trade-offs, constraints)
174 Decision,
175}
176
177/// Configuration for the compaction probe.
178///
179/// `zeph-memory` re-exports this type from here.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(default)]
182pub struct CompactionProbeConfig {
183 /// Enable compaction probe validation. Default: `false`.
184 pub enabled: bool,
185 /// Provider name from `[[llm.providers]]` for probe LLM calls.
186 /// `None` (or `Some("")`) uses the summary provider.
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub probe_provider: Option<ProviderName>,
189 /// Minimum score to pass without warnings. Default: `0.6`.
190 pub threshold: f32,
191 /// Score below this triggers `HardFail` (block compaction). Default: `0.35`.
192 pub hard_fail_threshold: f32,
193 /// Maximum number of probe questions to generate. Default: `5`.
194 pub max_questions: usize,
195 /// Timeout for the entire probe (both LLM calls) in seconds. Default: `15`.
196 pub timeout_secs: u64,
197 /// Optional per-category weight multipliers for the overall score.
198 #[serde(default)]
199 pub category_weights: Option<HashMap<ProbeCategory, f32>>,
200}
201
202impl Default for CompactionProbeConfig {
203 fn default() -> Self {
204 Self {
205 enabled: false,
206 probe_provider: None,
207 threshold: 0.6,
208 hard_fail_threshold: 0.35,
209 max_questions: 5,
210 timeout_secs: 15,
211 category_weights: None,
212 }
213 }
214}
215
216// ── MemCoT semantic state config ─────────────────────────────────────────────
217
218/// `MemCoT` semantic-state distillation configuration.
219///
220/// When `enabled = true`, the agent maintains a short rolling "semantic state" buffer
221/// summarizing conceptual progress across turns. This buffer is injected into graph
222/// recall queries to improve retrieval relevance.
223///
224/// All LLM work (distillation) runs asynchronously — never on the turn thread.
225/// When `enabled = false`, this is a **complete no-op**: no allocation, no LLM calls.
226///
227/// # Config example
228///
229/// ```toml
230/// [memory.memcot]
231/// enabled = true
232/// distill_provider = "fast"
233/// distill_timeout_secs = 5
234/// min_assistant_chars = 200
235/// min_distill_interval_secs = 30
236/// max_distills_per_session = 50
237/// max_state_chars = 800
238/// recall_view = "head"
239/// ```
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(default)]
242pub struct MemCotConfig {
243 /// Enable the `MemCoT` semantic state pipeline. Default: `false`.
244 ///
245 /// When `false`, the accumulator is never allocated and no LLM calls are made.
246 pub enabled: bool,
247 /// Provider name from `[[llm.providers]]` for distillation.
248 ///
249 /// Must reference a **fast-tier** provider (e.g. `gpt-4o-mini`, `qwen3:8b`).
250 /// A startup warning is emitted when the resolved model does not look fast-tier.
251 /// Falls back to the primary provider when empty. Default: `""`.
252 pub distill_provider: ProviderName,
253 /// Timeout in seconds for each distillation LLM call. Default: `5`.
254 pub distill_timeout_secs: u64,
255 /// Minimum characters in the assistant response to trigger distillation.
256 /// Short or trivial replies are skipped. Default: `200`.
257 pub min_assistant_chars: usize,
258 /// Minimum elapsed seconds between successive distillation spawns. Default: `30`.
259 ///
260 /// Prevents runaway costs on long sessions with rapid turns.
261 /// Clearing `/new` resets this counter.
262 pub min_distill_interval_secs: u64,
263 /// Maximum distillation spawns per conversation session. Default: `50`.
264 ///
265 /// Once this cap is reached the accumulator stops distilling for the rest of the
266 /// session. Counter is reset when the user sends `/new`.
267 pub max_distills_per_session: u64,
268 /// Maximum characters for the semantic state buffer (UTF-8 char boundary truncation).
269 /// Default: `800`.
270 pub max_state_chars: usize,
271 /// Recall view applied when `MemCoT` is active. Default: `Head`.
272 ///
273 /// - `head`: standard retrieval, no enrichment (suitable for low-latency setups).
274 /// - `zoom_in`: adds source-message provenance to each returned fact.
275 /// - `zoom_out`: expands 1-hop neighbors per returned fact.
276 ///
277 /// TODO(F3): add a per-call override parameter on `recall_graph_view`.
278 pub recall_view: RecallViewConfig,
279 /// Maximum 1-hop neighbor facts per head fact in `zoom_out` view. Default: `3`.
280 pub zoom_out_neighbor_cap: usize,
281 /// Optional model name allowlist for the fast-tier soft validator (lowercase substring match).
282 /// Empty (default) → falls back to the built-in `FAST_TIER_MODEL_HINTS` list.
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
284 pub fast_tier_models: Vec<String>,
285}
286
287/// Recall view variant exposed in config.
288///
289/// Maps 1-to-1 to `zeph_memory::RecallView`.
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
291#[serde(rename_all = "snake_case")]
292#[non_exhaustive]
293pub enum RecallViewConfig {
294 /// Standard retrieval — no enrichment. Byte-identical to legacy behaviour.
295 #[default]
296 Head,
297 /// Adds source-message provenance to each returned fact.
298 ZoomIn,
299 /// Expands 1-hop neighbor facts per returned fact.
300 ZoomOut,
301}
302
303impl Default for MemCotConfig {
304 fn default() -> Self {
305 Self {
306 enabled: false,
307 distill_provider: ProviderName::default(),
308 distill_timeout_secs: 5,
309 min_assistant_chars: 200,
310 min_distill_interval_secs: 30,
311 max_distills_per_session: 50,
312 max_state_chars: 800,
313 recall_view: RecallViewConfig::Head,
314 zoom_out_neighbor_cap: 3,
315 fast_tier_models: Vec::new(),
316 }
317 }
318}