Skip to main content

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}