Skip to main content

zeph_core/agent/
session_config.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::sync::Arc;
5
6use crate::config::{
7    Config, DebugConfig, DocumentConfig, GraphConfig, LearningConfig, OrchestrationConfig,
8    PersonaConfig, SecurityConfig, TimeoutConfig,
9};
10use crate::vault::Secret;
11
12/// Reserve ratio for `with_context_budget`: fraction of budget reserved for LLM reply.
13///
14/// Extracted from the hardcoded `0.20` literal used in both `spawn_acp_agent` and `runner.rs`.
15pub const CONTEXT_BUDGET_RESERVE_RATIO: f32 = 0.20;
16
17/// All config-derived fields needed to configure an `Agent` session.
18///
19/// This is the single source of truth for config → agent wiring.
20/// Adding a new config field requires exactly three changes:
21///
22/// 1. Add the field here.
23/// 2. Map it in [`AgentSessionConfig::from_config`].
24/// 3. Apply it in [`super::Agent::apply_session_config`] (destructure triggers a compile error if
25///    you forget step 3 — see the S4 note in the critic handoff).
26///
27/// ## What is NOT here
28///
29/// - **Shared runtime objects** (`provider`, `registry`, `memory`, `mcp_manager`, etc.) — these
30///   are expensive to create and shared across sessions; they stay in `SharedAgentDeps`.
31/// - **ACP-specific fields** (`acp_max_sessions`, bearer token, etc.) — transport-level, not
32///   agent-level.
33/// - **Optional runtime providers** (`summary_provider`, `judge_provider`,
34///   `quarantine_provider`) — these contain HTTP client pools (`AnyProvider`) that carry runtime
35///   state; callers wire them separately via `with_summary_provider` / `with_judge_provider` /
36///   `apply_quarantine_provider`.
37/// - **`mcp_config`** — passed alongside runtime MCP objects in `with_mcp()`; separating it
38///   from `mcp_tools` / `mcp_manager` would make the call site awkward.
39/// - **Runner-only fields** (`compression`, `routing`, `autosave`, `hybrid_search`, `trust_config`,
40///   `disambiguation_threshold`, `logging_config`, `subagent`, `experiment`, `instruction`,
41///   `lsp_hooks`, `response_cache`, `cost_tracker`) — not used in ACP sessions; keeping them out
42///   avoids unused-field noise and prevents inadvertent ACP behavior changes.
43/// - **Scheduler runtime objects** (`scheduler_executor`, broadcast senders) — runtime state,
44///   not config-derived values.
45#[derive(Clone)]
46#[allow(clippy::struct_excessive_bools)] // independent boolean flags; bitflags or enum would obscure semantics without reducing complexity
47pub struct AgentSessionConfig {
48    // Tool behavior
49    pub max_tool_iterations: usize,
50    pub max_tool_retries: usize,
51    pub max_retry_duration_secs: u64,
52    pub retry_base_ms: u64,
53    pub retry_max_ms: u64,
54    pub parameter_reformat_provider: String,
55    pub tool_repeat_threshold: usize,
56    pub tool_summarization: bool,
57    pub tool_call_cutoff: usize,
58    pub max_tool_calls_per_session: Option<u32>,
59    pub overflow_config: zeph_tools::OverflowConfig,
60    pub permission_policy: zeph_tools::PermissionPolicy,
61
62    // Model
63    pub model_name: String,
64    pub embed_model: String,
65
66    // Semantic cache
67    pub semantic_cache_enabled: bool,
68    pub semantic_cache_threshold: f32,
69    pub semantic_cache_max_candidates: u32,
70
71    // Memory / compaction
72    pub budget_tokens: usize,
73    pub soft_compaction_threshold: f32,
74    pub hard_compaction_threshold: f32,
75    pub compaction_preserve_tail: usize,
76    pub compaction_cooldown_turns: u8,
77    pub prune_protect_tokens: usize,
78    pub redact_credentials: bool,
79
80    // Security
81    pub security: SecurityConfig,
82    pub timeouts: TimeoutConfig,
83
84    // Feature configs
85    pub learning: LearningConfig,
86    pub document_config: DocumentConfig,
87    pub graph_config: GraphConfig,
88    pub persona_config: PersonaConfig,
89    pub trajectory_config: crate::config::TrajectoryConfig,
90    pub category_config: crate::config::CategoryConfig,
91    pub reasoning_config: zeph_config::ReasoningConfig,
92    pub memcot_config: zeph_config::MemCotConfig,
93    pub tree_config: crate::config::TreeConfig,
94    pub microcompact_config: crate::config::MicrocompactConfig,
95    pub autodream_config: crate::config::AutoDreamConfig,
96    pub magic_docs_config: crate::config::MagicDocsConfig,
97    /// Acon tool-result compression configuration (#4021).
98    pub acon_config: zeph_config::AconConfig,
99    /// ARC agent-initiated compaction configuration (#4020).
100    pub arc_config: zeph_config::ArcCompactionConfig,
101    pub anomaly_config: zeph_tools::AnomalyConfig,
102    pub result_cache_config: zeph_tools::ResultCacheConfig,
103    pub utility_config: zeph_tools::UtilityScoringConfig,
104    pub orchestration_config: OrchestrationConfig,
105    pub debug_config: DebugConfig,
106    pub server_compaction: bool,
107
108    /// Inject `<budget>` XML into the volatile system prompt section (#2267).
109    pub budget_hint_enabled: bool,
110
111    /// Session recap settings (#3064).
112    pub recap: zeph_config::RecapConfig,
113
114    /// Minimum allowed `/loop` tick interval in seconds. From `[cli.loop] min_interval_secs`.
115    pub loop_min_interval_secs: u64,
116
117    /// Long-horizon goal lifecycle configuration.
118    pub goal_config: zeph_config::GoalConfig,
119
120    /// Custom secrets from config.
121    ///
122    /// Stored as `Arc` because `Secret` intentionally does not implement `Clone` —
123    /// the wrapper prevents accidental duplication. Iteration produces new `Secret`
124    /// values via `Secret::new(v.expose())` on the consumption side.
125    pub secrets: Arc<[(String, Secret)]>,
126
127    /// CAM fidelity scoring configuration (#4547). `None` → scoring disabled.
128    pub fidelity_config: Option<zeph_config::FidelityConfig>,
129}
130
131impl AgentSessionConfig {
132    /// Build from a resolved [`Config`] snapshot and a pre-computed `budget_tokens`.
133    ///
134    /// `budget_tokens` is passed as a parameter because its computation (`auto_budget_tokens`)
135    /// depends on the active provider and must happen before `AgentSessionConfig` is constructed.
136    #[must_use]
137    pub fn from_config(config: &Config, budget_tokens: usize) -> Self {
138        Self {
139            max_tool_iterations: config.agent.max_tool_iterations,
140            max_tool_retries: config.tools.retry.max_attempts,
141            max_retry_duration_secs: config.tools.retry.budget_secs,
142            retry_base_ms: config.tools.retry.base_ms,
143            retry_max_ms: config.tools.retry.max_ms,
144            parameter_reformat_provider: config.tools.retry.parameter_reformat_provider.to_string(),
145            tool_repeat_threshold: config.agent.tool_repeat_threshold,
146            tool_summarization: config.tools.summarize_output,
147            tool_call_cutoff: config.memory.tool_call_cutoff,
148            max_tool_calls_per_session: config.tools.max_tool_calls_per_session,
149            overflow_config: config.tools.overflow.clone(),
150            permission_policy: zeph_tools::build_permission_policy(
151                &config.tools,
152                config.security.autonomy_level,
153            ),
154            model_name: config.llm.effective_model().to_owned(),
155            embed_model: config.llm.stable_skill_embedding_model(),
156            semantic_cache_enabled: config.llm.semantic_cache_enabled,
157            semantic_cache_threshold: config.llm.semantic_cache_threshold,
158            semantic_cache_max_candidates: config.llm.semantic_cache_max_candidates,
159            budget_tokens,
160            soft_compaction_threshold: config.memory.soft_compaction_threshold,
161            hard_compaction_threshold: config.memory.hard_compaction_threshold,
162            compaction_preserve_tail: config.memory.compaction_preserve_tail,
163            compaction_cooldown_turns: config.memory.compaction_cooldown_turns,
164            prune_protect_tokens: config.memory.prune_protect_tokens,
165            redact_credentials: config.memory.redact_credentials,
166            security: config.security.clone(),
167            timeouts: config.timeouts,
168            learning: config.skills.learning.clone(),
169            document_config: config.memory.documents.clone(),
170            graph_config: config.memory.graph.clone(),
171            persona_config: config.memory.persona.clone(),
172            trajectory_config: config.memory.trajectory.clone(),
173            category_config: config.memory.category.clone(),
174            reasoning_config: config.memory.reasoning.clone(),
175            memcot_config: config.memory.memcot.clone(),
176            tree_config: config.memory.tree.clone(),
177            microcompact_config: config.memory.microcompact.clone(),
178            autodream_config: config.memory.autodream.clone(),
179            magic_docs_config: config.magic_docs.clone(),
180            acon_config: config.memory.compression.acon.clone(),
181            arc_config: config.memory.compression.arc.clone(),
182            anomaly_config: config.tools.anomaly.clone(),
183            result_cache_config: config.tools.result_cache.clone(),
184            utility_config: config.tools.utility.clone(),
185            orchestration_config: config.orchestration.clone(),
186            debug_config: config.debug.clone(),
187            server_compaction: config.llm.providers.iter().any(|e| e.server_compaction),
188            budget_hint_enabled: config.agent.budget_hint_enabled,
189            secrets: config
190                .secrets
191                .custom
192                .iter()
193                .map(|(k, v)| (k.clone(), Secret::new(v.expose().to_owned())))
194                .collect::<Vec<_>>()
195                .into(),
196            recap: config.session.recap.clone(),
197            loop_min_interval_secs: config.cli.loop_.min_interval_secs,
198            goal_config: config.goals.clone(),
199            fidelity_config: {
200                let fc = config.memory.fidelity.clone();
201                if let Some(ref cfg) = fc
202                    && let Err(e) = cfg.validate()
203                {
204                    tracing::warn!("fidelity config invalid, scoring disabled: {e}");
205                }
206                fc
207            },
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn from_config_maps_all_fields() {
218        let config = Config::default();
219        let budget = 100_000;
220        let sc = AgentSessionConfig::from_config(&config, budget);
221
222        assert_eq!(sc.max_tool_iterations, config.agent.max_tool_iterations);
223        assert_eq!(sc.max_tool_retries, config.tools.retry.max_attempts);
224        assert_eq!(sc.max_retry_duration_secs, config.tools.retry.budget_secs);
225        assert_eq!(sc.retry_base_ms, config.tools.retry.base_ms);
226        assert_eq!(sc.retry_max_ms, config.tools.retry.max_ms);
227        assert_eq!(
228            sc.parameter_reformat_provider,
229            config.tools.retry.parameter_reformat_provider.as_str()
230        );
231        assert_eq!(sc.tool_repeat_threshold, config.agent.tool_repeat_threshold);
232        assert_eq!(sc.tool_summarization, config.tools.summarize_output);
233        assert_eq!(sc.tool_call_cutoff, config.memory.tool_call_cutoff);
234        assert_eq!(sc.model_name, config.llm.effective_model());
235        assert_eq!(sc.embed_model, config.llm.stable_skill_embedding_model());
236        assert_eq!(sc.semantic_cache_enabled, config.llm.semantic_cache_enabled);
237        assert!(
238            (sc.semantic_cache_threshold - config.llm.semantic_cache_threshold).abs()
239                < f32::EPSILON
240        );
241        assert_eq!(
242            sc.semantic_cache_max_candidates,
243            config.llm.semantic_cache_max_candidates
244        );
245        assert_eq!(sc.budget_tokens, budget);
246        assert!(
247            (sc.soft_compaction_threshold - config.memory.soft_compaction_threshold).abs()
248                < f32::EPSILON
249        );
250        assert!(
251            (sc.hard_compaction_threshold - config.memory.hard_compaction_threshold).abs()
252                < f32::EPSILON
253        );
254        assert_eq!(
255            sc.compaction_preserve_tail,
256            config.memory.compaction_preserve_tail
257        );
258        assert_eq!(
259            sc.compaction_cooldown_turns,
260            config.memory.compaction_cooldown_turns
261        );
262        assert_eq!(sc.prune_protect_tokens, config.memory.prune_protect_tokens);
263        assert_eq!(sc.redact_credentials, config.memory.redact_credentials);
264        assert_eq!(sc.graph_config.enabled, config.memory.graph.enabled);
265        assert_eq!(
266            sc.orchestration_config.enabled,
267            config.orchestration.enabled
268        );
269        assert_eq!(
270            sc.orchestration_config.max_tasks,
271            config.orchestration.max_tasks
272        );
273        assert_eq!(sc.anomaly_config.enabled, config.tools.anomaly.enabled);
274        assert_eq!(
275            sc.result_cache_config.enabled,
276            config.tools.result_cache.enabled
277        );
278        assert_eq!(
279            sc.result_cache_config.ttl_secs,
280            config.tools.result_cache.ttl_secs
281        );
282        assert_eq!(sc.debug_config.enabled, config.debug.enabled);
283        assert_eq!(
284            sc.document_config.rag_enabled,
285            config.memory.documents.rag_enabled
286        );
287        assert_eq!(
288            sc.overflow_config.threshold,
289            config.tools.overflow.threshold
290        );
291        assert_eq!(
292            sc.permission_policy.autonomy_level(),
293            config.security.autonomy_level
294        );
295        assert_eq!(sc.security.autonomy_level, config.security.autonomy_level);
296        assert_eq!(sc.timeouts.llm_seconds, config.timeouts.llm_seconds);
297        assert_eq!(sc.learning.enabled, config.skills.learning.enabled);
298        assert_eq!(
299            sc.server_compaction,
300            config.llm.providers.iter().any(|e| e.server_compaction)
301        );
302        assert_eq!(sc.secrets.len(), config.secrets.custom.len());
303        assert_eq!(sc.recap.on_resume, config.session.recap.on_resume);
304        assert_eq!(sc.recap.max_tokens, config.session.recap.max_tokens);
305        assert_eq!(sc.goal_config.enabled, config.goals.enabled);
306        assert_eq!(sc.goal_config.max_text_chars, config.goals.max_text_chars);
307        assert_eq!(
308            sc.fidelity_config.is_some(),
309            config.memory.fidelity.is_some()
310        );
311    }
312}