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`,
40///   `logging_config`, `subagent`, `experiment`, `instruction`, `lsp_hooks`, `response_cache`,
41///   `cost_tracker`) — not used in ACP sessions; keeping them out avoids unused-field noise and
42///   prevents inadvertent ACP behavior changes. Neither `disambiguation_threshold` nor
43///   `trust_config` (#5920) belong in this category — both ARE used in ACP sessions, just wired
44///   via their own standalone builder call (`with_skill_matching_config` /
45///   `with_trust_config`) rather than through this bundle.
46/// - **Scheduler runtime objects** (`scheduler_executor`, broadcast senders) — runtime state,
47///   not config-derived values.
48#[derive(Clone)]
49#[allow(clippy::struct_excessive_bools)] // independent boolean flags; bitflags or enum would obscure semantics without reducing complexity
50pub struct AgentSessionConfig {
51    // Tool behavior
52    pub max_tool_iterations: usize,
53    pub max_tool_retries: usize,
54    pub max_retry_duration_secs: u64,
55    pub retry_base_ms: u64,
56    pub retry_max_ms: u64,
57    pub parameter_reformat_provider: String,
58    pub tool_repeat_threshold: usize,
59    pub tool_summarization: bool,
60    pub tool_call_cutoff: usize,
61    pub max_tool_calls_per_session: Option<u32>,
62    pub overflow_config: zeph_tools::OverflowConfig,
63    pub permission_policy: zeph_tools::PermissionPolicy,
64
65    // Model
66    pub model_name: String,
67    pub embed_model: String,
68
69    // Semantic cache
70    pub semantic_cache_enabled: bool,
71    pub semantic_cache_threshold: f32,
72    pub semantic_cache_max_candidates: u32,
73
74    // Memory / compaction
75    pub budget_tokens: usize,
76    pub soft_compaction_threshold: f32,
77    pub hard_compaction_threshold: f32,
78    pub compaction_preserve_tail: usize,
79    pub compaction_cooldown_turns: u8,
80    pub prune_protect_tokens: usize,
81    pub redact_credentials: bool,
82    /// Write-time memory-consent gate (issue #6490, `MemGhost`).
83    pub consent_gate: zeph_config::ConsentGateConfig,
84
85    // Security
86    pub security: SecurityConfig,
87    pub timeouts: TimeoutConfig,
88
89    // Feature configs
90    pub learning: LearningConfig,
91    pub document_config: DocumentConfig,
92    pub graph_config: GraphConfig,
93    pub persona_config: PersonaConfig,
94    pub trajectory_config: crate::config::TrajectoryConfig,
95    pub category_config: crate::config::CategoryConfig,
96    pub reasoning_config: zeph_config::ReasoningConfig,
97    pub memcot_config: zeph_config::MemCotConfig,
98    pub tree_config: crate::config::TreeConfig,
99    pub microcompact_config: crate::config::MicrocompactConfig,
100    pub autodream_config: crate::config::AutoDreamConfig,
101    pub magic_docs_config: crate::config::MagicDocsConfig,
102    /// Acon tool-result compression configuration (#4021).
103    pub acon_config: zeph_config::AconConfig,
104    /// ARC agent-initiated compaction configuration (#4020).
105    pub arc_config: zeph_config::ArcCompactionConfig,
106    pub anomaly_config: zeph_tools::AnomalyConfig,
107    pub result_cache_config: zeph_tools::ResultCacheConfig,
108    pub utility_config: zeph_tools::UtilityScoringConfig,
109    pub orchestration_config: OrchestrationConfig,
110    /// Cross-thread store config (spec-080, `[memory.store]`, GitHub #6363). Read by
111    /// `scheduler_loop.rs`'s Command-handoff produce-side seam to gate the feature and
112    /// bound written-value size — `orchestration_config.command` carries the sibling
113    /// `[orchestration.command]` section and is already threaded above.
114    pub store_config: zeph_config::CrossThreadStoreConfig,
115    pub debug_config: DebugConfig,
116    pub server_compaction: bool,
117
118    /// Inject `<budget>` XML into the volatile system prompt section (#2267).
119    pub budget_hint_enabled: bool,
120
121    /// Inject a `<current_time>` reminder into the volatile system prompt every N agent turns
122    /// (#6361).
123    pub time_reminder_enabled: bool,
124    /// Turn interval between `<current_time>` reminder injections (#6361).
125    pub time_reminder_interval_requests: u32,
126
127    /// Token budget for skill bodies injected into a sub-agent's one-shot system prompt
128    /// at spawn time (#6421). See `SkillsConfig::subagent_skill_token_budget`.
129    pub subagent_skill_token_budget: usize,
130
131    /// Session recap settings (#3064).
132    pub recap: zeph_config::RecapConfig,
133
134    /// Resume-visibility banner and `/history` bound settings (spec-068 §13, §18, #6420).
135    pub resume: zeph_config::ResumeConfig,
136
137    /// Minimum allowed `/loop` tick interval in seconds. From `[cli.loop] min_interval_secs`.
138    pub loop_min_interval_secs: u64,
139
140    /// Long-horizon goal lifecycle configuration.
141    pub goal_config: zeph_config::GoalConfig,
142
143    /// Custom secrets from config.
144    ///
145    /// Stored as `Arc` because `Secret` intentionally does not implement `Clone` —
146    /// the wrapper prevents accidental duplication. Iteration produces new `Secret`
147    /// values via `Secret::new(v.expose())` on the consumption side.
148    pub secrets: Arc<[(String, Secret)]>,
149
150    /// CAM fidelity scoring configuration (#4547). `None` → scoring disabled.
151    pub fidelity_config: Option<zeph_config::FidelityConfig>,
152
153    /// Global caps for MCP image passthrough (spec-072), used by the vision-tier
154    /// emission gate in `process_one_tool_result` to enforce `max_images_per_turn`.
155    pub mcp_media: zeph_config::McpMediaConfig,
156
157    /// `true` when at least one configured `[[mcp.servers]]` entry has
158    /// `media_passthrough = true` (spec-072 FR-011). Gates the static untrusted-image
159    /// caveat line added once at session-assembly time in `assemble_final_system_prompt`.
160    pub media_passthrough_note_enabled: bool,
161
162    /// Install-time plugin/skill name-similarity typosquat check config (spec-043, #5864).
163    /// Read by the `/plugins add` slash-command handler so it applies the same
164    /// `plugins.reputation` policy as `zeph plugin add` and the bootstrap auto-update path.
165    pub plugins_reputation: zeph_config::plugins::ReputationConfig,
166}
167
168impl AgentSessionConfig {
169    /// Build from a resolved [`Config`] snapshot and a pre-computed `budget_tokens`.
170    ///
171    /// `budget_tokens` is passed as a parameter because its computation (`auto_budget_tokens`)
172    /// depends on the active provider and must happen before `AgentSessionConfig` is constructed.
173    #[must_use]
174    pub fn from_config(config: &Config, budget_tokens: usize) -> Self {
175        Self {
176            max_tool_iterations: config.agent.max_tool_iterations,
177            max_tool_retries: config.tools.retry.max_attempts,
178            max_retry_duration_secs: config.tools.retry.budget_secs,
179            retry_base_ms: config.tools.retry.base_ms,
180            retry_max_ms: config.tools.retry.max_ms,
181            parameter_reformat_provider: config.tools.retry.parameter_reformat_provider.to_string(),
182            tool_repeat_threshold: config.agent.tool_repeat_threshold,
183            tool_summarization: config.tools.summarize_output,
184            tool_call_cutoff: config.memory.tool_call_cutoff,
185            max_tool_calls_per_session: config.tools.max_tool_calls_per_session,
186            overflow_config: config.tools.overflow.clone(),
187            permission_policy: zeph_tools::build_permission_policy(
188                &config.tools,
189                config.security.autonomy_level,
190            ),
191            model_name: config.llm.effective_model().to_owned(),
192            embed_model: config.llm.stable_skill_embedding_model(),
193            semantic_cache_enabled: config.llm.semantic_cache_enabled,
194            semantic_cache_threshold: config.llm.semantic_cache_threshold,
195            semantic_cache_max_candidates: config.llm.semantic_cache_max_candidates,
196            budget_tokens,
197            soft_compaction_threshold: config.memory.soft_compaction_threshold,
198            hard_compaction_threshold: config.memory.hard_compaction_threshold,
199            compaction_preserve_tail: config.memory.compaction_preserve_tail,
200            compaction_cooldown_turns: config.memory.compaction_cooldown_turns,
201            prune_protect_tokens: config.memory.prune_protect_tokens,
202            redact_credentials: config.memory.redact_credentials,
203            consent_gate: config.memory.consent_gate.clone(),
204            security: config.security.clone(),
205            timeouts: config.timeouts,
206            learning: config.skills.learning.clone(),
207            document_config: config.memory.documents.clone(),
208            graph_config: config.memory.graph.clone(),
209            persona_config: config.memory.persona.clone(),
210            trajectory_config: config.memory.trajectory.clone(),
211            category_config: config.memory.category.clone(),
212            reasoning_config: config.memory.reasoning.clone(),
213            memcot_config: config.memory.memcot.clone(),
214            tree_config: config.memory.tree.clone(),
215            microcompact_config: config.memory.microcompact.clone(),
216            autodream_config: config.memory.autodream.clone(),
217            magic_docs_config: config.magic_docs.clone(),
218            acon_config: config.memory.compression.acon.clone(),
219            arc_config: config.memory.compression.arc.clone(),
220            anomaly_config: config.tools.anomaly.clone(),
221            result_cache_config: config.tools.result_cache.clone(),
222            utility_config: config.tools.utility.clone(),
223            orchestration_config: config.orchestration.clone(),
224            store_config: config.memory.store.clone(),
225            debug_config: config.debug.clone(),
226            server_compaction: config.llm.providers.iter().any(|e| e.server_compaction),
227            budget_hint_enabled: config.agent.budget_hint_enabled,
228            time_reminder_enabled: config.agent.time_reminder_enabled,
229            time_reminder_interval_requests: config.agent.time_reminder_interval_requests,
230            subagent_skill_token_budget: config.skills.subagent_skill_token_budget.get(),
231            secrets: config
232                .secrets
233                .custom
234                .iter()
235                .map(|(k, v)| (k.clone(), Secret::new(v.expose().to_owned())))
236                .collect::<Vec<_>>()
237                .into(),
238            recap: config.session.recap.clone(),
239            resume: config.session.resume.clone(),
240            loop_min_interval_secs: config.cli.loop_.min_interval_secs,
241            goal_config: config.goals.clone(),
242            fidelity_config: {
243                let fc = config.memory.fidelity.clone();
244                if let Some(ref cfg) = fc
245                    && let Err(e) = cfg.validate()
246                {
247                    tracing::warn!("fidelity config invalid, scoring disabled: {e}");
248                }
249                fc
250            },
251            mcp_media: config.mcp.media.clone(),
252            media_passthrough_note_enabled: config.mcp.servers.iter().any(|s| s.media_passthrough),
253            plugins_reputation: config.plugins.reputation.clone(),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    #[allow(clippy::too_many_lines)]
264    fn from_config_maps_all_fields() {
265        let config = Config::default();
266        let budget = 100_000;
267        let sc = AgentSessionConfig::from_config(&config, budget);
268
269        assert_eq!(sc.max_tool_iterations, config.agent.max_tool_iterations);
270        assert_eq!(sc.max_tool_retries, config.tools.retry.max_attempts);
271        assert_eq!(sc.max_retry_duration_secs, config.tools.retry.budget_secs);
272        assert_eq!(sc.retry_base_ms, config.tools.retry.base_ms);
273        assert_eq!(sc.retry_max_ms, config.tools.retry.max_ms);
274        assert_eq!(
275            sc.parameter_reformat_provider,
276            config.tools.retry.parameter_reformat_provider.as_str()
277        );
278        assert_eq!(sc.tool_repeat_threshold, config.agent.tool_repeat_threshold);
279        assert_eq!(sc.tool_summarization, config.tools.summarize_output);
280        assert_eq!(sc.tool_call_cutoff, config.memory.tool_call_cutoff);
281        assert_eq!(sc.model_name, config.llm.effective_model());
282        assert_eq!(sc.embed_model, config.llm.stable_skill_embedding_model());
283        assert_eq!(sc.semantic_cache_enabled, config.llm.semantic_cache_enabled);
284        assert!(
285            (sc.semantic_cache_threshold - config.llm.semantic_cache_threshold).abs()
286                < f32::EPSILON
287        );
288        assert_eq!(
289            sc.semantic_cache_max_candidates,
290            config.llm.semantic_cache_max_candidates
291        );
292        assert_eq!(sc.budget_tokens, budget);
293        assert!(
294            (sc.soft_compaction_threshold - config.memory.soft_compaction_threshold).abs()
295                < f32::EPSILON
296        );
297        assert!(
298            (sc.hard_compaction_threshold - config.memory.hard_compaction_threshold).abs()
299                < f32::EPSILON
300        );
301        assert_eq!(
302            sc.compaction_preserve_tail,
303            config.memory.compaction_preserve_tail
304        );
305        assert_eq!(
306            sc.compaction_cooldown_turns,
307            config.memory.compaction_cooldown_turns
308        );
309        assert_eq!(sc.prune_protect_tokens, config.memory.prune_protect_tokens);
310        assert_eq!(sc.redact_credentials, config.memory.redact_credentials);
311        assert_eq!(sc.graph_config.enabled, config.memory.graph.enabled);
312        assert_eq!(
313            sc.orchestration_config.enabled,
314            config.orchestration.enabled
315        );
316        assert_eq!(
317            sc.orchestration_config.max_tasks,
318            config.orchestration.max_tasks
319        );
320        assert_eq!(sc.store_config.enabled, config.memory.store.enabled);
321        assert_eq!(
322            sc.store_config.max_value_bytes,
323            config.memory.store.max_value_bytes
324        );
325        assert_eq!(sc.anomaly_config.enabled, config.tools.anomaly.enabled);
326        assert_eq!(
327            sc.result_cache_config.enabled,
328            config.tools.result_cache.enabled
329        );
330        assert_eq!(
331            sc.result_cache_config.ttl_secs,
332            config.tools.result_cache.ttl_secs
333        );
334        assert_eq!(sc.debug_config.enabled, config.debug.enabled);
335        assert_eq!(
336            sc.document_config.rag_enabled,
337            config.memory.documents.rag_enabled
338        );
339        assert_eq!(
340            sc.overflow_config.threshold,
341            config.tools.overflow.threshold
342        );
343        assert_eq!(
344            sc.permission_policy.autonomy_level(),
345            config.security.autonomy_level
346        );
347        assert_eq!(sc.security.autonomy_level, config.security.autonomy_level);
348        assert_eq!(sc.timeouts.llm_seconds, config.timeouts.llm_seconds);
349        assert_eq!(sc.learning.enabled, config.skills.learning.enabled);
350        assert_eq!(
351            sc.server_compaction,
352            config.llm.providers.iter().any(|e| e.server_compaction)
353        );
354        assert_eq!(sc.secrets.len(), config.secrets.custom.len());
355        assert_eq!(sc.recap.on_resume, config.session.recap.on_resume);
356        assert_eq!(sc.recap.max_tokens, config.session.recap.max_tokens);
357        assert_eq!(sc.goal_config.enabled, config.goals.enabled);
358        assert_eq!(sc.goal_config.max_text_chars, config.goals.max_text_chars);
359        assert_eq!(
360            sc.fidelity_config.is_some(),
361            config.memory.fidelity.is_some()
362        );
363        assert_eq!(
364            sc.subagent_skill_token_budget,
365            config.skills.subagent_skill_token_budget.get()
366        );
367        assert_eq!(
368            sc.plugins_reputation.enabled,
369            config.plugins.reputation.enabled
370        );
371        assert!(
372            (sc.plugins_reputation.similarity_threshold
373                - config.plugins.reputation.similarity_threshold)
374                .abs()
375                < f32::EPSILON
376        );
377    }
378}