Skip to main content

zeph_config/memory/
session.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Session, document, and semantic-memory configuration.
5//!
6//! Per-session history limits, document chunking/ingestion, the semantic-memory
7//! toggle surface, and context-strategy selection.
8
9use crate::defaults::default_true;
10use crate::providers::ProviderName;
11use serde::{Deserialize, Serialize};
12
13use super::default_embed_timeout_secs;
14
15fn default_max_history() -> usize {
16    100
17}
18
19fn default_title_max_chars() -> usize {
20    60
21}
22
23fn default_document_collection() -> String {
24    "zeph_documents".into()
25}
26
27fn default_document_chunk_size() -> usize {
28    1000
29}
30
31fn default_document_chunk_overlap() -> usize {
32    100
33}
34
35fn default_document_top_k() -> usize {
36    3
37}
38
39fn default_temporal_decay_half_life_days() -> u32 {
40    30
41}
42
43fn default_mmr_lambda() -> f32 {
44    0.7
45}
46
47fn default_semantic_enabled() -> bool {
48    true
49}
50
51fn default_recall_limit() -> usize {
52    5
53}
54
55fn default_vector_weight() -> f64 {
56    0.7
57}
58
59fn default_keyword_weight() -> f64 {
60    0.3
61}
62
63fn validate_importance_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
64where
65    D: serde::Deserializer<'de>,
66{
67    let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
68    if value.is_nan() || value.is_infinite() {
69        return Err(serde::de::Error::custom(
70            "importance_weight must be a finite number",
71        ));
72    }
73    if value < 0.0 {
74        return Err(serde::de::Error::custom(
75            "importance_weight must be non-negative",
76        ));
77    }
78    if value > 1.0 {
79        return Err(serde::de::Error::custom("importance_weight must be <= 1.0"));
80    }
81    Ok(value)
82}
83
84fn default_importance_weight() -> f64 {
85    0.15
86}
87
88/// Context assembly strategy (#2288).
89#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91#[non_exhaustive]
92pub enum ContextStrategy {
93    /// Full conversation history trimmed to budget, with memory augmentation.
94    /// This is the default and existing behavior.
95    #[default]
96    FullHistory,
97    /// Drop conversation history; assemble context from summaries, semantic recall,
98    /// cross-session memory, and session digest only.
99    MemoryFirst,
100    /// Start as `FullHistory`; switch to `MemoryFirst` when turn count exceeds
101    /// `crossover_turn_threshold`.
102    Adaptive,
103}
104
105/// Session list and auto-title configuration, nested under `[memory.sessions]` in TOML.
106#[derive(Debug, Clone, Deserialize, Serialize)]
107#[serde(default)]
108pub struct SessionsConfig {
109    /// Maximum number of sessions returned by list operations (0 = unlimited).
110    #[serde(default = "default_max_history")]
111    pub max_history: usize,
112    /// Maximum characters for auto-generated session titles.
113    #[serde(default = "default_title_max_chars")]
114    pub title_max_chars: usize,
115}
116
117impl Default for SessionsConfig {
118    fn default() -> Self {
119        Self {
120            max_history: default_max_history(),
121            title_max_chars: default_title_max_chars(),
122        }
123    }
124}
125
126/// Configuration for the document ingestion and RAG retrieval pipeline.
127#[derive(Debug, Clone, Deserialize, Serialize)]
128pub struct DocumentConfig {
129    #[serde(default = "default_document_collection")]
130    pub collection: String,
131    #[serde(default = "default_document_chunk_size")]
132    pub chunk_size: usize,
133    #[serde(default = "default_document_chunk_overlap")]
134    pub chunk_overlap: usize,
135    /// Number of document chunks to inject into agent context per turn.
136    #[serde(default = "default_document_top_k")]
137    pub top_k: usize,
138    /// Enable document RAG injection into agent context.
139    #[serde(default)]
140    pub rag_enabled: bool,
141}
142
143impl Default for DocumentConfig {
144    fn default() -> Self {
145        Self {
146            collection: default_document_collection(),
147            chunk_size: default_document_chunk_size(),
148            chunk_overlap: default_document_chunk_overlap(),
149            top_k: default_document_top_k(),
150            rag_enabled: false,
151        }
152    }
153}
154
155/// Semantic (vector) memory retrieval configuration, nested under `[memory.semantic]` in TOML.
156///
157/// Controls how memories are searched and ranked, including temporal decay, MMR diversity
158/// re-ranking, and hybrid BM25+vector weighting.
159///
160/// # Example (TOML)
161///
162/// ```toml
163/// [memory.semantic]
164/// enabled = true
165/// recall_limit = 5
166/// vector_weight = 0.7
167/// keyword_weight = 0.3
168/// mmr_lambda = 0.7
169/// ```
170#[derive(Debug, Deserialize, Serialize)]
171#[allow(clippy::struct_excessive_bools)] // config struct — boolean flags are idiomatic for TOML-deserialized configuration
172pub struct SemanticConfig {
173    /// Enable vector-based semantic recall. Default: `true`.
174    #[serde(default = "default_semantic_enabled")]
175    pub enabled: bool,
176    #[serde(default = "default_recall_limit")]
177    pub recall_limit: usize,
178    #[serde(default = "default_vector_weight")]
179    pub vector_weight: f64,
180    #[serde(default = "default_keyword_weight")]
181    pub keyword_weight: f64,
182    #[serde(default = "default_true")]
183    pub temporal_decay_enabled: bool,
184    #[serde(default = "default_temporal_decay_half_life_days")]
185    pub temporal_decay_half_life_days: u32,
186    #[serde(default = "default_true")]
187    pub mmr_enabled: bool,
188    #[serde(default = "default_mmr_lambda")]
189    pub mmr_lambda: f32,
190    #[serde(default = "default_true")]
191    pub importance_enabled: bool,
192    #[serde(
193        default = "default_importance_weight",
194        deserialize_with = "validate_importance_weight"
195    )]
196    pub importance_weight: f64,
197    /// Name of a `[[llm.providers]]` entry to use exclusively for embedding calls during
198    /// memory write and backfill operations. A dedicated provider prevents `embed_backfill`
199    /// from contending with the guardrail at the API server level (rate limits, Ollama
200    /// single-model lock). Falls back to the main agent provider when `None`.
201    #[serde(default)]
202    pub embedding_provider: Option<ProviderName>,
203    /// Timeout in seconds applied to every `embed()` call inside `zeph-memory`.
204    ///
205    /// Applies to all embedding call sites: admission control, quality gate, recall,
206    /// summarization, graph retrieval, consolidation, and tree consolidation.
207    /// Set to a higher value when using slow remote embedding providers.
208    /// Default: `5`.
209    #[serde(default = "default_embed_timeout_secs")]
210    pub embed_timeout_secs: u64,
211}
212
213impl Default for SemanticConfig {
214    fn default() -> Self {
215        Self {
216            enabled: default_semantic_enabled(),
217            recall_limit: default_recall_limit(),
218            vector_weight: default_vector_weight(),
219            keyword_weight: default_keyword_weight(),
220            temporal_decay_enabled: true,
221            temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
222            mmr_enabled: true,
223            mmr_lambda: default_mmr_lambda(),
224            importance_enabled: true,
225            importance_weight: default_importance_weight(),
226            embedding_provider: None,
227            embed_timeout_secs: default_embed_timeout_secs(),
228        }
229    }
230}