Skip to main content

zeph_core/agent/state/
mod.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Sub-struct definitions for the `Agent` struct.
5//!
6//! Each struct groups a related cluster of `Agent` fields.
7//! All types are `pub(crate)` — visible only within the `zeph-core` crate.
8//!
9//! `MemoryState` is decomposed into four concern-separated sub-structs, each in its own file:
10//!
11//! - [`MemoryPersistenceState`] — `SQLite` handles, conversation IDs, recall budgets, autosave
12//! - [`MemoryCompactionState`] — summarization thresholds, shutdown summary, digest, strategy
13//! - [`MemoryExtractionState`] — graph config, RPE router, document config, semantic labels
14//! - [`MemorySubsystemState`] — `TiMem`, `autoDream`, `MagicDocs`, microcompact
15
16pub(crate) mod compaction;
17pub(crate) mod extraction;
18pub(crate) mod persistence;
19pub(crate) mod subsystems;
20
21pub(crate) use self::compaction::MemoryCompactionState;
22pub(crate) use self::extraction::MemoryExtractionState;
23pub(crate) use self::persistence::MemoryPersistenceState;
24pub(crate) use self::subsystems::MemorySubsystemState;
25
26use std::collections::{HashMap, HashSet, VecDeque};
27use std::path::PathBuf;
28use std::sync::Arc;
29
30use parking_lot::RwLock;
31use std::time::Instant;
32
33use tokio::sync::{Notify, mpsc, watch};
34use tokio::task::JoinHandle;
35use tokio::time::Interval;
36use tokio_util::sync::CancellationToken;
37use zeph_llm::any::AnyProvider;
38use zeph_llm::provider::Message;
39use zeph_llm::stt::SpeechToText;
40
41use crate::config::{ProviderEntry, SecurityConfig, SkillPromptMode, TimeoutConfig};
42use crate::config_watcher::ConfigEvent;
43use crate::context::EnvironmentContext;
44use crate::cost::CostTracker;
45use crate::file_watcher::FileChangedEvent;
46use crate::instructions::{InstructionBlock, InstructionEvent, InstructionReloadState};
47use crate::metrics::MetricsSnapshot;
48use crate::vault::Secret;
49use zeph_config;
50use zeph_memory::TokenCounter;
51use zeph_sanitizer::ContentSanitizer;
52use zeph_sanitizer::quarantine::QuarantinedSummarizer;
53use zeph_skills::matcher::SkillMatcherBackend;
54use zeph_skills::registry::SkillRegistry;
55use zeph_skills::watcher::SkillEvent;
56
57use super::message_queue::QueuedMessage;
58
59/// Coordinator struct holding four concern-separated sub-structs for memory management.
60///
61/// Each sub-struct groups fields by a single concern:
62/// - [`persistence`](MemoryPersistenceState) — `SQLite` handles, conversation IDs, recall budgets
63/// - [`compaction`](MemoryCompactionState) — summarization thresholds, shutdown summary, digest
64/// - [`extraction`](MemoryExtractionState) — graph config, RPE router, semantic labels
65/// - [`subsystems`](MemorySubsystemState) — `TiMem`, `autoDream`, `MagicDocs`, microcompact
66#[derive(Default)]
67pub(crate) struct MemoryState {
68    /// `SQLite` handles, conversation IDs, recall budgets, and autosave policy.
69    pub(crate) persistence: MemoryPersistenceState,
70    /// Summarization thresholds, shutdown summary, digest config, and context strategy.
71    pub(crate) compaction: MemoryCompactionState,
72    /// Graph extraction config, RPE router, document config, and semantic label configs.
73    pub(crate) extraction: MemoryExtractionState,
74    /// `TiMem`, `autoDream`, `MagicDocs`, and microcompact subsystem state.
75    pub(crate) subsystems: MemorySubsystemState,
76}
77
78pub(crate) struct SkillState {
79    pub(crate) registry: Arc<RwLock<SkillRegistry>>,
80    /// Per-turn trust snapshot written by `prepare_context` after `build_skill_trust_map`.
81    /// Shared with `SkillInvokeExecutor` so it can resolve trust without hitting `SQLite`
82    /// on every tool call. Refreshed once per turn — stale by at most one turn.
83    pub(crate) trust_snapshot: Arc<RwLock<HashMap<String, zeph_common::SkillTrustLevel>>>,
84    pub(crate) skill_paths: Vec<PathBuf>,
85    pub(crate) managed_dir: Option<PathBuf>,
86    pub(crate) trust_config: crate::config::TrustConfig,
87    pub(crate) matcher: Option<SkillMatcherBackend>,
88    pub(crate) max_active_skills: usize,
89    pub(crate) disambiguation_threshold: f32,
90    pub(crate) min_injection_score: f32,
91    pub(crate) embedding_model: String,
92    pub(crate) skill_reload_rx: Option<mpsc::Receiver<SkillEvent>>,
93    /// Resolves the current set of per-plugin skill dirs at reload time.
94    ///
95    /// Called inside `reload_skills()` so that plugins installed via `/plugins add` after
96    /// startup are discovered on the next watcher event without restarting the agent.
97    pub(crate) plugin_dirs_supplier: Option<Arc<dyn Fn() -> Vec<PathBuf> + Send + Sync>>,
98    pub(crate) active_skill_names: Vec<String>,
99    pub(crate) last_skills_prompt: String,
100    pub(crate) prompt_mode: SkillPromptMode,
101    /// Custom secrets available at runtime: key=hyphenated name, value=secret.
102    pub(crate) available_custom_secrets: HashMap<String, Secret>,
103    pub(crate) cosine_weight: f32,
104    pub(crate) hybrid_search: bool,
105    pub(crate) bm25_index: Option<zeph_skills::bm25::Bm25Index>,
106    pub(crate) two_stage_matching: bool,
107    /// Threshold for confusability warnings (0.0 = disabled).
108    pub(crate) confusability_threshold: f32,
109    /// `SkillOrchestra` RL routing head. `Some` when `rl_routing_enabled = true` and
110    /// weights are loaded or initialized. `None` when RL routing is disabled.
111    pub(crate) rl_head: Option<zeph_skills::rl_head::RoutingHead>,
112    /// Blend weight for RL routing: `final = (1-rl_weight)*cosine + rl_weight*rl_score`.
113    pub(crate) rl_weight: f32,
114    /// Skip RL blending for the first N updates (cold-start warmup).
115    pub(crate) rl_warmup_updates: u32,
116    /// Directory where `/skill create` writes generated skills.
117    /// Defaults to `managed_dir` if `None`.
118    pub(crate) generation_output_dir: Option<std::path::PathBuf>,
119    /// Provider name for `/skill create` generation. Empty = primary.
120    pub(crate) generation_provider_name: String,
121    /// Optional quality-gate evaluator for generated SKILL.md files (#3319).
122    ///
123    /// When `Some`, the evaluator is attached to every `SkillGenerator` instance so that
124    /// generated skills are scored before being written to disk.
125    pub(crate) skill_evaluator: Option<std::sync::Arc<zeph_skills::evaluator::SkillEvaluator>>,
126    /// Weights for the evaluator composite score — forwarded to `SkillGenerator::with_evaluator`.
127    pub(crate) eval_weights: zeph_skills::evaluator::EvaluationWeights,
128    /// Minimum composite score required to accept a generated skill (forwarded to the generator).
129    pub(crate) eval_threshold: f32,
130}
131
132pub(crate) struct McpState {
133    pub(crate) tools: Vec<zeph_mcp::McpTool>,
134    pub(crate) registry: Option<zeph_mcp::McpToolRegistry>,
135    pub(crate) manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
136    pub(crate) allowed_commands: Vec<String>,
137    pub(crate) max_dynamic: usize,
138    /// Receives elicitation requests from MCP server handlers during tool execution.
139    /// When `Some`, the agent loop must process these concurrently with tool result awaiting
140    /// to avoid deadlock (tool result waits for elicitation, elicitation waits for agent loop).
141    pub(crate) elicitation_rx: Option<tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>>,
142    /// Shared with `McpToolExecutor` so native `tool_use` sees the current tool list.
143    ///
144    /// Two methods write to this `RwLock` — ordering matters:
145    /// - `sync_executor_tools()`: writes the **full** `self.tools` set.
146    /// - `apply_pruned_tools()`: writes the **pruned** subset (used after pruning).
147    ///
148    /// Within a turn, `sync_executor_tools` must always run **before**
149    /// `apply_pruned_tools`.  The normal call order guarantees this: tool-list
150    /// change events call `sync_executor_tools` (inside `check_tool_refresh`,
151    /// `handle_mcp_add`, `handle_mcp_remove`), and pruning runs later inside
152    /// `rebuild_system_prompt`.  See also: `apply_pruned_tools`.
153    pub(crate) shared_tools: Option<Arc<RwLock<Vec<zeph_mcp::McpTool>>>>,
154    /// Receives full flattened tool list after any `tools/list_changed` notification.
155    pub(crate) tool_rx: Option<tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>>,
156    /// Per-server connection outcomes from the initial `connect_all()` call.
157    pub(crate) server_outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
158    /// Per-message cache for MCP tool pruning results (#2298).
159    ///
160    /// Reset at the start of each user turn and whenever the MCP tool list
161    /// changes (via `tools/list_changed`, `/mcp add`, or `/mcp remove`).
162    pub(crate) pruning_cache: zeph_mcp::PruningCache,
163    /// Dedicated provider for MCP tool pruning LLM calls.
164    ///
165    /// `None` means fall back to the agent's primary provider.
166    /// Resolved from `[[llm.providers]]` at build time using `pruning_provider`
167    /// from `ToolPruningConfig`.
168    pub(crate) pruning_provider: Option<zeph_llm::any::AnyProvider>,
169    /// Whether MCP tool pruning is enabled.  Mirrors `ToolPruningConfig::enabled`.
170    pub(crate) pruning_enabled: bool,
171    /// Pruning parameters snapshot.  Derived from `ToolPruningConfig` at build time.
172    pub(crate) pruning_params: zeph_mcp::PruningParams,
173    /// Pre-computed semantic tool index for embedding-based discovery (#2321).
174    ///
175    /// Built at connect time via `rebuild_semantic_index()`, rebuilt on tool list change.
176    /// `None` when strategy is not `Embedding` or when build failed (fallback to all tools).
177    pub(crate) semantic_index: Option<zeph_mcp::SemanticToolIndex>,
178    /// Active discovery strategy and parameters.  Derived from `ToolDiscoveryConfig`.
179    pub(crate) discovery_strategy: zeph_mcp::ToolDiscoveryStrategy,
180    /// Discovery parameters snapshot.  Derived from `ToolDiscoveryConfig` at build time.
181    pub(crate) discovery_params: zeph_mcp::DiscoveryParams,
182    /// Dedicated embedding provider for tool discovery.  `None` = fall back to the
183    /// agent's primary embedding provider.
184    pub(crate) discovery_provider: Option<zeph_llm::any::AnyProvider>,
185    /// When `true`, show a security warning before prompting for fields whose names
186    /// match sensitive patterns (password, token, secret, key, credential, etc.).
187    pub(crate) elicitation_warn_sensitive_fields: bool,
188    /// When `true`, semantic index and registry need to be rebuilt at the next opportunity.
189    ///
190    /// Set after `/mcp add` or `/mcp remove` when called via `AgentAccess::handle_mcp`,
191    /// which cannot call `rebuild_semantic_index` and `sync_mcp_registry` directly because
192    /// those are `async fn(&mut self)` and their futures are `!Send` (they hold `&mut Agent<C>`
193    /// across `.await`). The rebuild is deferred to `check_tool_refresh`, which runs at the
194    /// start of each turn without the `Box<dyn Future + Send>` constraint.
195    pub(crate) pending_semantic_rebuild: bool,
196}
197
198pub(crate) struct IndexState {
199    pub(crate) retriever: Option<std::sync::Arc<zeph_index::retriever::CodeRetriever>>,
200    pub(crate) repo_map_tokens: usize,
201    pub(crate) cached_repo_map: Option<(String, std::time::Instant)>,
202    pub(crate) repo_map_ttl: std::time::Duration,
203}
204
205/// Snapshot of adversarial policy gate configuration for status display.
206#[derive(Debug, Clone)]
207pub struct AdversarialPolicyInfo {
208    pub provider: String,
209    pub policy_count: usize,
210    pub fail_open: bool,
211}
212
213#[allow(clippy::struct_excessive_bools)]
214pub(crate) struct RuntimeConfig {
215    pub(crate) security: SecurityConfig,
216    pub(crate) timeouts: TimeoutConfig,
217    pub(crate) model_name: String,
218    /// Configured name from `[[llm.providers]]` (the `name` field), set at startup and on
219    /// `/provider` switch. Falls back to the provider type string when empty.
220    pub(crate) active_provider_name: String,
221    pub(crate) permission_policy: zeph_tools::PermissionPolicy,
222    pub(crate) redact_credentials: bool,
223    pub(crate) rate_limiter: super::rate_limiter::ToolRateLimiter,
224    pub(crate) semantic_cache_enabled: bool,
225    pub(crate) semantic_cache_threshold: f32,
226    pub(crate) semantic_cache_max_candidates: u32,
227    /// Dependency config snapshot stored for per-turn boost parameters.
228    pub(crate) dependency_config: zeph_tools::DependencyConfig,
229    /// Adversarial policy gate runtime info for /status display.
230    pub(crate) adversarial_policy_info: Option<AdversarialPolicyInfo>,
231    /// Current spawn depth of this agent instance (0 = top-level, 1 = first sub-agent, etc.).
232    /// Used by `build_spawn_context()` to propagate depth to children.
233    pub(crate) spawn_depth: u32,
234    /// Inject `<budget>` XML into the volatile system prompt section (#2267).
235    pub(crate) budget_hint_enabled: bool,
236    /// Per-channel skill allowlist. Skills not matching the allowlist are excluded from the
237    /// prompt. An empty `allowed` list means all skills are permitted (default).
238    pub(crate) channel_skills: zeph_config::ChannelSkillsConfig,
239    /// Minimum allowed interval for `/loop` ticks (seconds). Sourced from `[cli.loop] min_interval_secs`.
240    pub(crate) loop_min_interval_secs: u64,
241    /// Runtime middleware layers for LLM calls and tool dispatch (#2286).
242    ///
243    /// Default: empty vec (zero-cost — loops never iterate).
244    pub(crate) layers: Vec<std::sync::Arc<dyn crate::runtime_layer::RuntimeLayer>>,
245    /// Background supervisor config snapshot for turn-boundary abort logic.
246    pub(crate) supervisor_config: crate::config::TaskSupervisorConfig,
247    /// Session recap config (#3064).
248    pub(crate) recap_config: zeph_config::RecapConfig,
249    /// ACP server configuration snapshot for `/acp` slash-command display.
250    pub(crate) acp_config: zeph_config::AcpConfig,
251    /// Set to `true` after the auto-recap is emitted at session resume (#3144).
252    ///
253    /// Used by `/recap` to skip a redundant LLM call when no new messages have
254    /// been added since the auto-recap was shown.
255    pub(crate) auto_recap_shown: bool,
256    /// Number of non-system messages present when the session was resumed (#3144).
257    ///
258    /// Combined with `auto_recap_shown` to detect whether the user has added new
259    /// messages after the auto-recap was shown.
260    pub(crate) msg_count_at_resume: usize,
261    /// Callback that spawns an external ACP sub-agent process by shell command (#3302).
262    ///
263    /// Injected by the binary crate when the `acp` feature is enabled.
264    /// `None` in bare / non-ACP mode; callers must degrade gracefully.
265    pub(crate) acp_subagent_spawn_fn: Option<zeph_subagent::AcpSubagentSpawnFn>,
266    /// Channel type string used as part of the `(channel_type, channel_id)` persistence key.
267    ///
268    /// Set at build time from the active I/O channel (e.g. `"cli"`, `"tui"`, `"telegram"`).
269    /// Empty when channel identity has not been configured (persistence is skipped).
270    pub(crate) channel_type: String,
271    /// Whether provider preference persistence is enabled for this session (#3308).
272    ///
273    /// Controlled by `[session] provider_persistence = true` (the default). When `false`,
274    /// the stored provider preference is never read or written.
275    pub(crate) provider_persistence_enabled: bool,
276}
277
278/// Groups feedback detection subsystems: correction detector, judge detector, and LLM classifier.
279pub(crate) struct FeedbackState {
280    pub(crate) detector: super::feedback_detector::FeedbackDetector,
281    pub(crate) judge: Option<super::feedback_detector::JudgeDetector>,
282    /// LLM-backed zero-shot classifier for `DetectorMode::Model`.
283    /// When `Some`, `spawn_judge_correction_check` uses this instead of `JudgeDetector`.
284    pub(crate) llm_classifier: Option<zeph_llm::classifier::llm::LlmClassifier>,
285}
286
287/// Groups security-related subsystems (sanitizer, quarantine, exfiltration guard).
288pub(crate) struct SecurityState {
289    pub(crate) sanitizer: ContentSanitizer,
290    pub(crate) quarantine_summarizer: Option<QuarantinedSummarizer>,
291    /// Whether this agent session is serving an ACP client.
292    /// When `true` and `mcp_to_acp_boundary` is enabled, MCP tool results
293    /// receive unconditional quarantine and cross-boundary audit logging.
294    pub(crate) is_acp_session: bool,
295    pub(crate) exfiltration_guard: zeph_sanitizer::exfiltration::ExfiltrationGuard,
296    pub(crate) flagged_urls: HashSet<String>,
297    /// URLs explicitly provided by the user across all turns in this session.
298    /// Populated from raw user message text; cleared on `/clear`.
299    /// Shared with `UrlGroundingVerifier` to check `fetch`/`web_scrape` calls at dispatch time.
300    pub(crate) user_provided_urls: Arc<RwLock<HashSet<String>>>,
301    pub(crate) pii_filter: zeph_sanitizer::pii::PiiFilter,
302    /// NER classifier for PII detection (`classifiers.ner_model`). When `Some`, the PII path
303    /// runs both regex (`pii_filter`) and NER, then merges spans before redaction.
304    /// `None` when `classifiers` feature is disabled or `classifiers.enabled = false`.
305    #[cfg(feature = "classifiers")]
306    pub(crate) pii_ner_backend: Option<std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>>,
307    /// Per-call timeout for the NER PII classifier in milliseconds.
308    #[cfg(feature = "classifiers")]
309    pub(crate) pii_ner_timeout_ms: u64,
310    /// Maximum number of bytes passed to the NER PII classifier per call.
311    ///
312    /// Large tool outputs (e.g. `search_code`) can produce 150+ `DeBERTa` chunks and exceed
313    /// the per-call timeout. Input is truncated at a valid UTF-8 boundary before classification.
314    #[cfg(feature = "classifiers")]
315    pub(crate) pii_ner_max_chars: usize,
316    /// Circuit-breaker threshold: number of consecutive timeouts before NER is disabled.
317    /// `0` means the circuit breaker is disabled (NER is always attempted).
318    #[cfg(feature = "classifiers")]
319    pub(crate) pii_ner_circuit_breaker_threshold: u32,
320    /// Number of consecutive NER timeouts observed since the last successful call.
321    #[cfg(feature = "classifiers")]
322    pub(crate) pii_ner_consecutive_timeouts: u32,
323    /// Set to `true` when the circuit breaker trips. NER is skipped for the rest of the session.
324    #[cfg(feature = "classifiers")]
325    pub(crate) pii_ner_tripped: bool,
326    pub(crate) memory_validator: zeph_sanitizer::memory_validation::MemoryWriteValidator,
327    /// LLM-based prompt injection pre-screener (opt-in).
328    pub(crate) guardrail: Option<zeph_sanitizer::guardrail::GuardrailFilter>,
329    /// Post-LLM response verification layer.
330    pub(crate) response_verifier: zeph_sanitizer::response_verifier::ResponseVerifier,
331    /// Temporal causal IPI analyzer (opt-in, disabled when `None`).
332    pub(crate) causal_analyzer: Option<zeph_sanitizer::causal_ipi::TurnCausalAnalyzer>,
333    /// VIGIL pre-sanitizer gate. `None` for subagent sessions (subagents are exempt).
334    /// Set at agent build time for top-level agents; skipped for subagents (high FP rate).
335    pub(crate) vigil: Option<crate::agent::vigil::VigilGate>,
336}
337
338/// Groups debug/diagnostics subsystems (dumper, trace collector, anomaly detector, logging config).
339pub(crate) struct DebugState {
340    pub(crate) debug_dumper: Option<crate::debug_dump::DebugDumper>,
341    pub(crate) dump_format: crate::debug_dump::DumpFormat,
342    pub(crate) trace_collector: Option<crate::debug_dump::trace::TracingCollector>,
343    /// Monotonically increasing counter for `process_user_message` calls.
344    /// Used to key spans in `trace_collector.active_iterations`.
345    pub(crate) iteration_counter: usize,
346    pub(crate) anomaly_detector: Option<zeph_tools::AnomalyDetector>,
347    /// Whether to emit `reasoning_amplification` warnings for quality failures from reasoning
348    /// models. Mirrors `AnomalyConfig::reasoning_model_warning`. Default: `true`.
349    pub(crate) reasoning_model_warning: bool,
350    pub(crate) logging_config: crate::config::LoggingConfig,
351    /// Base dump directory — stored so `/dump-format trace` can create a `TracingCollector` (CR-04).
352    pub(crate) dump_dir: Option<PathBuf>,
353    /// Service name for `TracingCollector` created via runtime format switch (CR-04).
354    pub(crate) trace_service_name: String,
355    /// Whether to redact in `TracingCollector` created via runtime format switch (CR-04).
356    pub(crate) trace_redact: bool,
357    /// Span ID of the currently executing iteration — used by LLM/tool span wiring (CR-01).
358    /// Set to `Some` at the start of `process_user_message`, cleared at end.
359    pub(crate) current_iteration_span_id: Option<[u8; 8]>,
360}
361
362/// Snapshot of the shell-level overlay baked in at startup.
363///
364/// Used in `reload_config` to detect when a hot-reload would produce a different shell
365/// restriction set than the one baked into the live `ShellExecutor` (M4 warn-on-divergence).
366#[derive(Debug, Clone, Default, PartialEq, Eq)]
367pub struct ShellOverlaySnapshot {
368    /// Sorted `blocked_commands` contributed by plugins.
369    pub blocked: Vec<String>,
370    /// Sorted `allowed_commands` after plugin intersection (empty if base was empty).
371    pub allowed: Vec<String>,
372}
373
374/// Runtime state for an active `/loop` session.
375///
376/// At most one loop is active at a time; `LifecycleState::user_loop` holds `Some` while
377/// the loop is running and `None` otherwise.
378pub(crate) struct LoopState {
379    /// The prompt text injected on each tick.
380    pub(crate) prompt: String,
381    /// Number of ticks fired so far.
382    pub(crate) iteration: u64,
383    /// Tick interval. `MissedTickBehavior::Skip` prevents burst catch-up.
384    pub(crate) interval: Interval,
385    /// Cancel handle. Dropped (and token cancelled) when loop is stopped.
386    pub(crate) cancel_tx: CancellationToken,
387}
388
389/// Groups agent lifecycle state: shutdown signaling, timing, and I/O notification channels.
390pub(crate) struct LifecycleState {
391    pub(crate) shutdown: watch::Receiver<bool>,
392    pub(crate) start_time: Instant,
393    pub(crate) cancel_signal: Arc<Notify>,
394    pub(crate) cancel_token: CancellationToken,
395    /// Handle to the cancel bridge task spawned each turn. Aborted before a new one is created
396    /// to prevent unbounded task accumulation across turns.
397    pub(crate) cancel_bridge_handle: Option<JoinHandle<()>>,
398    pub(crate) config_path: Option<PathBuf>,
399    pub(crate) config_reload_rx: Option<mpsc::Receiver<ConfigEvent>>,
400    /// Path to the plugins directory; used to re-apply overlays on hot-reload.
401    pub(crate) plugins_dir: PathBuf,
402    /// Shell overlay snapshot baked in at startup. Used to detect divergence on hot-reload.
403    pub(crate) startup_shell_overlay: ShellOverlaySnapshot,
404    /// Handle for live-rebuilding the `ShellExecutor`'s `blocked_commands` policy on hot-reload.
405    /// `None` when no `ShellExecutor` is in the executor chain (test harnesses, daemon-only modes).
406    pub(crate) shell_policy_handle: Option<zeph_tools::ShellPolicyHandle>,
407    pub(crate) warmup_ready: Option<watch::Receiver<bool>>,
408    pub(crate) update_notify_rx: Option<mpsc::Receiver<String>>,
409    pub(crate) custom_task_rx: Option<mpsc::Receiver<String>>,
410    /// Active `/loop` state. `None` when no loop is running.
411    pub(crate) user_loop: Option<LoopState>,
412    /// Last known process cwd. Compared after each tool call to detect changes.
413    pub(crate) last_known_cwd: PathBuf,
414    /// Receiver for file-change events from `FileChangeWatcher`. `None` when no paths configured.
415    pub(crate) file_changed_rx: Option<mpsc::Receiver<FileChangedEvent>>,
416    /// Keeps the `FileChangeWatcher` alive for the agent's lifetime. Dropping it aborts the watcher task.
417    pub(crate) file_watcher: Option<crate::file_watcher::FileChangeWatcher>,
418    /// Supervised background task manager. Owned by the agent; call `reap()` between turns
419    /// and `abort_all()` on shutdown.
420    pub(crate) supervisor: super::agent_supervisor::BackgroundSupervisor,
421    /// Per-turn completion notifier. `None` when `notifications.enabled = false`.
422    pub(crate) notifier: Option<crate::notifications::Notifier>,
423    /// Per-turn LLM request counter. Incremented by `process_response`; reset at turn start.
424    pub(crate) turn_llm_requests: u32,
425    /// Timestamp of the last turn that ended with `LlmError::NoProviders`.
426    ///
427    /// Used to gate `advance_context_lifecycle`: when all providers are down, context preparation
428    /// is skipped (degraded mode) until `no_providers_backoff_secs` has elapsed.
429    pub(crate) last_no_providers_at: Option<Instant>,
430    /// Completions from background shell runs waiting to be injected into the next turn.
431    ///
432    /// Drained at the top of `process_user_message_inner` after `supervisor.reap()`.
433    /// All pending completions and the real user message are merged into a **single**
434    /// user-role block to satisfy strict alternation requirements (Anthropic Messages API).
435    ///
436    /// Capacity is capped at `BACKGROUND_COMPLETION_BUFFER_CAP`. On overflow the oldest
437    /// entry is dropped and a placeholder is substituted so the LLM learns results were lost.
438    pub(crate) pending_background_completions:
439        VecDeque<zeph_tools::shell::background::BackgroundCompletion>,
440    /// Receiver end of the dedicated background-completion channel created alongside the
441    /// `ShellExecutor`. Polled at the top of each turn to drain completions into
442    /// `pending_background_completions`. `None` when no `ShellExecutor` is configured.
443    pub(crate) background_completion_rx:
444        Option<tokio::sync::mpsc::Receiver<zeph_tools::BackgroundCompletion>>,
445}
446
447/// Minimal config snapshot needed to reconstruct a provider at runtime via `/provider <name>`.
448///
449/// Secrets are stored as plain strings because [`Secret`] intentionally does not implement
450/// `Clone`. They are re-wrapped in `Secret` when passed to `build_provider_for_switch`.
451pub struct ProviderConfigSnapshot {
452    pub claude_api_key: Option<String>,
453    pub openai_api_key: Option<String>,
454    pub gemini_api_key: Option<String>,
455    pub compatible_api_keys: std::collections::HashMap<String, String>,
456    pub llm_request_timeout_secs: u64,
457    pub embedding_model: String,
458}
459
460/// Groups provider-related state: alternate providers, runtime switching, and compaction flags.
461pub(crate) struct ProviderState {
462    pub(crate) summary_provider: Option<AnyProvider>,
463    /// Shared slot for runtime model switching; set by external caller (e.g. ACP).
464    pub(crate) provider_override: Option<Arc<RwLock<Option<AnyProvider>>>>,
465    pub(crate) judge_provider: Option<AnyProvider>,
466    /// Dedicated provider for compaction probe LLM calls. Falls back to `summary_provider`
467    /// (or primary) when `None`.
468    pub(crate) probe_provider: Option<AnyProvider>,
469    /// Dedicated provider for `compress_context` LLM calls (#2356).
470    /// Falls back to the primary provider when `None`.
471    pub(crate) compress_provider: Option<AnyProvider>,
472    pub(crate) cached_prompt_tokens: u64,
473    /// Whether the active provider has server-side compaction enabled (Claude compact-2026-01-12).
474    /// When true, client-side compaction is skipped.
475    pub(crate) server_compaction_active: bool,
476    pub(crate) stt: Option<Box<dyn SpeechToText>>,
477    /// Snapshot of `[[llm.providers]]` entries for runtime `/provider` switching.
478    pub(crate) provider_pool: Vec<ProviderEntry>,
479    /// Resolved secrets and timeout settings needed to reconstruct providers at runtime.
480    pub(crate) provider_config_snapshot: Option<ProviderConfigSnapshot>,
481}
482
483/// Groups metrics and cost tracking state.
484pub(crate) struct MetricsState {
485    pub(crate) metrics_tx: Option<watch::Sender<MetricsSnapshot>>,
486    pub(crate) cost_tracker: Option<CostTracker>,
487    pub(crate) token_counter: Arc<TokenCounter>,
488    /// Set to `true` when Claude extended context (`enable_extended_context = true`) is active.
489    /// Read from config at build time, not derived from provider internals.
490    pub(crate) extended_context: bool,
491    /// Shared classifier latency ring buffer. Populated by `ContentSanitizer` (injection, PII)
492    /// and `LlmClassifier` (feedback). `None` when classifiers are not configured.
493    pub(crate) classifier_metrics: Option<Arc<zeph_llm::ClassifierMetrics>>,
494    /// Rolling window of per-turn latency samples (last 10 turns).
495    pub(crate) timing_window: std::collections::VecDeque<crate::metrics::TurnTimings>,
496    /// Accumulator for the current turn's timings. Flushed at turn end via `flush_turn_timings`.
497    pub(crate) pending_timings: crate::metrics::TurnTimings,
498    /// Optional histogram recorder for per-event Prometheus observations.
499    /// `None` when the `prometheus` feature is disabled or metrics are not enabled.
500    pub(crate) histogram_recorder: Option<std::sync::Arc<dyn crate::metrics::HistogramRecorder>>,
501}
502
503/// Groups task orchestration and subagent state.
504#[derive(Default)]
505pub(crate) struct OrchestrationState {
506    /// On `OrchestrationState` (not `ProviderState`) because this provider is used exclusively
507    /// by `LlmPlanner` during orchestration, not shared across subsystems.
508    pub(crate) planner_provider: Option<AnyProvider>,
509    /// Provider for `PlanVerifier` LLM calls. `None` falls back to the primary provider.
510    /// On `OrchestrationState` for the same reason as `planner_provider`.
511    pub(crate) verify_provider: Option<AnyProvider>,
512    /// Graph waiting for `/plan confirm` before execution starts.
513    pub(crate) pending_graph: Option<zeph_orchestration::TaskGraph>,
514    /// Cancellation token for the currently executing plan. `None` when no plan is running.
515    /// Created fresh in `handle_plan_confirm()`, cancelled in `handle_plan_cancel()`.
516    ///
517    /// # Known limitation
518    ///
519    /// Token plumbing is ready; the delivery path requires the agent message loop to be
520    /// restructured so `/plan cancel` can be received while `run_scheduler_loop` holds
521    /// `&mut self`. See follow-up issue #1603 (SEC-M34-002).
522    pub(crate) plan_cancel_token: Option<CancellationToken>,
523    /// Manages spawned sub-agents.
524    pub(crate) subagent_manager: Option<zeph_subagent::SubAgentManager>,
525    pub(crate) subagent_config: crate::config::SubAgentConfig,
526    pub(crate) orchestration_config: crate::config::OrchestrationConfig,
527    /// Lazily initialized plan template cache. `None` until first use or when
528    /// memory (`SQLite`) is unavailable.
529    #[allow(dead_code)]
530    pub(crate) plan_cache: Option<zeph_orchestration::PlanCache>,
531    /// Goal embedding from the most recent `plan_with_cache()` call. Consumed by
532    /// `finalize_plan_execution()` to cache the completed plan template.
533    pub(crate) pending_goal_embedding: Option<Vec<f32>>,
534    /// `AdaptOrch` topology advisor — `None` when `[orchestration.adaptorch]` is disabled.
535    pub(crate) topology_advisor: Option<std::sync::Arc<zeph_orchestration::TopologyAdvisor>>,
536    /// Last `AdaptOrch` verdict; carried from `handle_plan_goal_as_string` to scheduler loop
537    /// for `record_outcome`.
538    #[allow(dead_code)] // read via .take() in plan.rs; clippy false positive
539    pub(crate) last_advisor_verdict: Option<zeph_orchestration::AdvisorVerdict>,
540    /// Task graph persistence handle. `None` when no `SemanticMemory` was
541    /// attached via `with_memory`, or when
542    /// `OrchestrationConfig::persistence_enabled` is `false`. When `Some`, the
543    /// scheduler loop snapshots the graph once per tick and `/plan resume <id>`
544    /// rehydrates from disk.
545    pub(crate) graph_persistence: Option<
546        zeph_orchestration::GraphPersistence<zeph_memory::store::graph_store::TaskGraphStore>,
547    >,
548}
549
550/// Groups instruction hot-reload state.
551#[derive(Default)]
552pub(crate) struct InstructionState {
553    pub(crate) blocks: Vec<InstructionBlock>,
554    pub(crate) reload_rx: Option<mpsc::Receiver<InstructionEvent>>,
555    pub(crate) reload_state: Option<InstructionReloadState>,
556}
557
558/// Groups experiment feature state (gated behind `experiments` feature flag).
559pub(crate) struct ExperimentState {
560    pub(crate) config: crate::config::ExperimentConfig,
561    /// Cancellation token for a running experiment session. `Some` means an experiment is active.
562    pub(crate) cancel: Option<tokio_util::sync::CancellationToken>,
563    /// Pre-built config snapshot used as the experiment baseline (agent path).
564    pub(crate) baseline: zeph_experiments::ConfigSnapshot,
565    /// Dedicated judge provider for evaluation. When `Some`, the evaluator uses this provider
566    /// instead of the agent's primary provider, eliminating self-judge bias.
567    pub(crate) eval_provider: Option<AnyProvider>,
568    /// Receives completion/error messages from the background experiment engine task.
569    /// Always present so the select! branch compiles unconditionally.
570    pub(crate) notify_rx: Option<tokio::sync::mpsc::Receiver<String>>,
571    /// Sender end paired with `experiment_notify_rx`. Cloned into the background task.
572    pub(crate) notify_tx: tokio::sync::mpsc::Sender<String>,
573}
574
575/// Output of a background subgoal extraction LLM call.
576pub(crate) struct SubgoalExtractionResult {
577    /// Current subgoal the agent is working toward.
578    pub(crate) current: String,
579    /// Just-completed subgoal, if the LLM detected a transition (`COMPLETED:` non-NONE).
580    pub(crate) completed: Option<String>,
581}
582
583/// Groups context-compression feature state (gated behind `context-compression` feature flag).
584#[derive(Default)]
585pub(crate) struct CompressionState {
586    /// Cached task goal for TaskAware/MIG pruning. Set by `maybe_compact()`,
587    /// invalidated when the last user message hash changes.
588    pub(crate) current_task_goal: Option<String>,
589    /// Hash of the last user message when `current_task_goal` was populated.
590    pub(crate) task_goal_user_msg_hash: Option<u64>,
591    /// Pending background task for goal extraction. Spawned fire-and-forget when the user message
592    /// hash changes; result applied at the start of the next Soft compaction (#1909).
593    pub(crate) pending_task_goal: Option<tokio::task::JoinHandle<Option<String>>>,
594    /// Pending `SideQuest` eviction result from the background LLM call spawned last turn.
595    /// Applied at the START of the next turn before compaction (PERF-1 fix).
596    pub(crate) pending_sidequest_result: Option<tokio::task::JoinHandle<Option<Vec<usize>>>>,
597    /// In-memory subgoal registry for `Subgoal`/`SubgoalMig` pruning strategies (#2022).
598    pub(crate) subgoal_registry: crate::agent::compaction_strategy::SubgoalRegistry,
599    /// Pending background subgoal extraction task.
600    pub(crate) pending_subgoal: Option<tokio::task::JoinHandle<Option<SubgoalExtractionResult>>>,
601    /// Hash of the last user message when subgoal extraction was scheduled.
602    pub(crate) subgoal_user_msg_hash: Option<u64>,
603}
604
605/// Groups runtime tool filtering, dependency tracking, and iteration bookkeeping.
606#[derive(Default)]
607pub(crate) struct ToolState {
608    /// Dynamic tool schema filter: pre-computed tool embeddings for per-turn filtering (#2020).
609    pub(crate) tool_schema_filter: Option<zeph_tools::ToolSchemaFilter>,
610    /// Cached filtered tool IDs for the current user turn.
611    pub(crate) cached_filtered_tool_ids: Option<HashSet<String>>,
612    /// Tool dependency graph for sequential tool availability (#2024).
613    pub(crate) dependency_graph: Option<zeph_tools::ToolDependencyGraph>,
614    /// Always-on tool IDs, mirrored from the tool schema filter for dependency gate bypass.
615    pub(crate) dependency_always_on: HashSet<String>,
616    /// Tool IDs that completed successfully in the current session.
617    pub(crate) completed_tool_ids: HashSet<String>,
618    /// Current tool loop iteration index within the active user turn.
619    pub(crate) current_tool_iteration: usize,
620}
621
622/// Groups per-session I/O and policy state.
623pub(crate) struct SessionState {
624    pub(crate) env_context: EnvironmentContext,
625    /// Timestamp of the last assistant message appended to context.
626    /// Used by time-based microcompact to compute session idle gap (#2699).
627    /// `None` before the first assistant response.
628    pub(crate) last_assistant_at: Option<Instant>,
629    pub(crate) response_cache: Option<std::sync::Arc<zeph_memory::ResponseCache>>,
630    /// Parent tool call ID when this agent runs as a subagent inside another agent session.
631    /// Propagated into every `LoopbackEvent::ToolStart` / `ToolOutput` so the IDE can build
632    /// a subagent hierarchy.
633    pub(crate) parent_tool_use_id: Option<String>,
634    /// Current-turn intent snapshot for VIGIL. `None` between turns.
635    ///
636    /// Set at the top of `process_user_message` (before any tool call) to the first 1024 chars
637    /// of the user message. Cleared at `end_turn`, on `/clear`, and on any turn-abort path.
638    /// Never shared across turns or propagated into subagents.
639    pub(crate) current_turn_intent: Option<String>,
640    /// Optional status channel for sending spinner/status messages to TUI or stderr.
641    pub(crate) status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
642    /// LSP context injection hooks. Fires after native tool execution, injects
643    /// diagnostics/hover notes as `Role::System` messages before the next LLM call.
644    pub(crate) lsp_hooks: Option<crate::lsp_hooks::LspHookRunner>,
645    /// Snapshot of the policy config for `/policy` command inspection.
646    pub(crate) policy_config: Option<zeph_tools::PolicyConfig>,
647    /// `CwdChanged` hook definitions extracted from `[hooks]` config.
648    pub(crate) hooks_config: HooksConfigSnapshot,
649}
650
651/// Extracted hook lists from `[hooks]` config, stored in `SessionState`.
652#[derive(Default)]
653pub(crate) struct HooksConfigSnapshot {
654    /// Hooks fired when working directory changes.
655    pub(crate) cwd_changed: Vec<zeph_config::HookDef>,
656    /// Hooks fired when a watched file changes.
657    pub(crate) file_changed_hooks: Vec<zeph_config::HookDef>,
658    /// Hooks fired when a tool execution is blocked by a `RuntimeLayer::before_tool` check.
659    pub(crate) permission_denied: Vec<zeph_config::HookDef>,
660    /// Hooks fired after each agent turn completes (#3327).
661    ///
662    /// Populated from `HooksConfig::turn_complete` at session construction. Shares the
663    /// `Notifier::should_fire` gate when a notifier is configured; fires on every completion
664    /// when no notifier is present.
665    pub(crate) turn_complete: Vec<zeph_config::HookDef>,
666}
667
668// Groups message buffering and image staging state.
669pub(crate) struct MessageState {
670    pub(crate) messages: Vec<Message>,
671    // QueuedMessage is pub(super) in message_queue — same visibility as this struct; lint suppressed.
672    #[allow(private_interfaces)]
673    pub(crate) message_queue: VecDeque<QueuedMessage>,
674    /// Image parts staged by `/image` commands, attached to the next user message.
675    pub(crate) pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
676    /// DB row ID of the most recently persisted message. Set by `persist_message`;
677    /// consumed by `push_message` call sites to populate `metadata.db_id` on in-memory messages.
678    pub(crate) last_persisted_message_id: Option<i64>,
679    /// DB message IDs pending hide after deferred tool pair summarization.
680    pub(crate) deferred_db_hide_ids: Vec<i64>,
681    /// Summary texts pending insertion after deferred tool pair summarization.
682    pub(crate) deferred_db_summaries: Vec<String>,
683}
684
685impl McpState {
686    /// Write the **full** `self.tools` set to the shared executor `RwLock`.
687    ///
688    /// This is the first of two writers to `shared_tools`. Within a turn this method must run
689    /// **before** `apply_pruned_tools`, which writes the pruned subset. The normal call order
690    /// guarantees this: tool-list change events call this method, and pruning runs later inside
691    /// `rebuild_system_prompt`. See also: `apply_pruned_tools`.
692    pub(crate) fn sync_executor_tools(&self) {
693        if let Some(ref shared) = self.shared_tools {
694            shared.write().clone_from(&self.tools);
695        }
696    }
697
698    /// Write the **pruned** tool subset to the shared executor `RwLock`.
699    ///
700    /// Must only be called **after** `sync_executor_tools` has established the full tool set for
701    /// the current turn. `self.tools` (the full set) is intentionally **not** modified.
702    ///
703    /// This method must **NOT** call `sync_executor_tools` internally — doing so would overwrite
704    /// the pruned subset with the full set. See also: `sync_executor_tools`.
705    pub(crate) fn apply_pruned_tools(&self, pruned: Vec<zeph_mcp::McpTool>) {
706        debug_assert!(
707            pruned.iter().all(|p| self
708                .tools
709                .iter()
710                .any(|t| t.server_id == p.server_id && t.name == p.name)),
711            "pruned set must be a subset of self.tools"
712        );
713        if let Some(ref shared) = self.shared_tools {
714            *shared.write() = pruned;
715        }
716    }
717
718    #[cfg(test)]
719    pub(crate) fn tool_count(&self) -> usize {
720        self.tools.len()
721    }
722}
723
724impl IndexState {
725    #[tracing::instrument(name = "core.index.fetch_code_rag", skip(self), fields(%query, token_budget))]
726    pub(crate) async fn fetch_code_rag(
727        &self,
728        query: &str,
729        token_budget: usize,
730    ) -> Result<Option<String>, crate::agent::error::AgentError> {
731        let Some(retriever) = &self.retriever else {
732            return Ok(None);
733        };
734        if token_budget == 0 {
735            return Ok(None);
736        }
737
738        let result = retriever
739            .retrieve(query, token_budget)
740            .await
741            .map_err(|e| crate::agent::error::AgentError::Other(format!("{e:#}")))?;
742        let context_text = zeph_index::retriever::format_as_context(&result);
743
744        if context_text.is_empty() {
745            Ok(None)
746        } else {
747            tracing::debug!(
748                strategy = ?result.strategy,
749                chunks = result.chunks.len(),
750                tokens = result.total_tokens,
751                "code context fetched"
752            );
753            Ok(Some(context_text))
754        }
755    }
756
757    /// Return `Some(self)` when code indexing is enabled, `None` otherwise.
758    ///
759    /// Used by `prepare_context` to pass an optional `IndexAccess` reference to
760    /// `zeph_context::assembler::ContextAssembler::gather` without wrapping the whole state.
761    pub(crate) fn as_index_access(&self) -> Option<&dyn zeph_context::input::IndexAccess> {
762        if self.retriever.is_some() {
763            Some(self)
764        } else {
765            None
766        }
767    }
768}
769
770impl DebugState {
771    pub(crate) fn start_iteration_span(&mut self, iteration_index: usize, text: &str) {
772        if let Some(ref mut tc) = self.trace_collector {
773            tc.begin_iteration(iteration_index, text);
774            self.current_iteration_span_id = tc.current_iteration_span_id(iteration_index);
775        }
776    }
777
778    pub(crate) fn end_iteration_span(
779        &mut self,
780        iteration_index: usize,
781        status: crate::debug_dump::trace::SpanStatus,
782    ) {
783        if let Some(ref mut tc) = self.trace_collector {
784            tc.end_iteration(iteration_index, status);
785        }
786        self.current_iteration_span_id = None;
787    }
788
789    pub(crate) fn switch_format(&mut self, new_format: crate::debug_dump::DumpFormat) {
790        let was_trace = self.dump_format == crate::debug_dump::DumpFormat::Trace;
791        let now_trace = new_format == crate::debug_dump::DumpFormat::Trace;
792
793        if now_trace
794            && !was_trace
795            && let Some(ref dump_dir) = self.dump_dir.clone()
796        {
797            let service_name = self.trace_service_name.clone();
798            let redact = self.trace_redact;
799            match crate::debug_dump::trace::TracingCollector::new(
800                dump_dir.as_path(),
801                &service_name,
802                redact,
803                None,
804            ) {
805                Ok(collector) => {
806                    self.trace_collector = Some(collector);
807                }
808                Err(e) => {
809                    tracing::warn!(error = %e, "failed to create TracingCollector on format switch");
810                }
811            }
812        }
813        if was_trace
814            && !now_trace
815            && let Some(mut tc) = self.trace_collector.take()
816        {
817            tc.finish();
818        }
819
820        self.dump_format = new_format;
821    }
822
823    pub(crate) fn write_chat_debug_dump(
824        &self,
825        dump_id: Option<u32>,
826        result: &zeph_llm::provider::ChatResponse,
827        pii_filter: &zeph_sanitizer::pii::PiiFilter,
828    ) {
829        let Some((d, id)) = self.debug_dumper.as_ref().zip(dump_id) else {
830            return;
831        };
832        let raw = match result {
833            zeph_llm::provider::ChatResponse::Text(t) => t.clone(),
834            zeph_llm::provider::ChatResponse::ToolUse {
835                text, tool_calls, ..
836            } => {
837                let calls = serde_json::to_string_pretty(tool_calls).unwrap_or_default();
838                format!(
839                    "{}\n\n---TOOL_CALLS---\n{calls}",
840                    text.as_deref().unwrap_or("")
841                )
842            }
843        };
844        let text = if pii_filter.is_enabled() {
845            pii_filter.scrub(&raw).into_owned()
846        } else {
847            raw
848        };
849        d.dump_response(id, &text);
850    }
851}
852
853impl Default for McpState {
854    fn default() -> Self {
855        Self {
856            tools: Vec::new(),
857            registry: None,
858            manager: None,
859            allowed_commands: Vec::new(),
860            max_dynamic: 10,
861            elicitation_rx: None,
862            shared_tools: None,
863            tool_rx: None,
864            server_outcomes: Vec::new(),
865            pruning_cache: zeph_mcp::PruningCache::new(),
866            pruning_provider: None,
867            pruning_enabled: false,
868            pruning_params: zeph_mcp::PruningParams::default(),
869            semantic_index: None,
870            discovery_strategy: zeph_mcp::ToolDiscoveryStrategy::default(),
871            discovery_params: zeph_mcp::DiscoveryParams::default(),
872            discovery_provider: None,
873            elicitation_warn_sensitive_fields: true,
874            pending_semantic_rebuild: false,
875        }
876    }
877}
878
879impl Default for IndexState {
880    fn default() -> Self {
881        Self {
882            retriever: None,
883            repo_map_tokens: 0,
884            cached_repo_map: None,
885            repo_map_ttl: std::time::Duration::from_mins(5),
886        }
887    }
888}
889
890impl Default for DebugState {
891    fn default() -> Self {
892        Self {
893            debug_dumper: None,
894            dump_format: crate::debug_dump::DumpFormat::default(),
895            trace_collector: None,
896            iteration_counter: 0,
897            anomaly_detector: None,
898            reasoning_model_warning: true,
899            logging_config: crate::config::LoggingConfig::default(),
900            dump_dir: None,
901            trace_service_name: String::new(),
902            trace_redact: true,
903            current_iteration_span_id: None,
904        }
905    }
906}
907
908impl Default for FeedbackState {
909    fn default() -> Self {
910        Self {
911            detector: super::feedback_detector::FeedbackDetector::new(0.6),
912            judge: None,
913            llm_classifier: None,
914        }
915    }
916}
917
918impl Default for RuntimeConfig {
919    fn default() -> Self {
920        Self {
921            security: SecurityConfig::default(),
922            timeouts: TimeoutConfig::default(),
923            model_name: String::new(),
924            active_provider_name: String::new(),
925            permission_policy: zeph_tools::PermissionPolicy::default(),
926            redact_credentials: true,
927            rate_limiter: super::rate_limiter::ToolRateLimiter::new(
928                super::rate_limiter::RateLimitConfig::default(),
929            ),
930            semantic_cache_enabled: false,
931            semantic_cache_threshold: 0.95,
932            semantic_cache_max_candidates: 10,
933            dependency_config: zeph_tools::DependencyConfig::default(),
934            adversarial_policy_info: None,
935            spawn_depth: 0,
936            budget_hint_enabled: true,
937            channel_skills: zeph_config::ChannelSkillsConfig::default(),
938            loop_min_interval_secs: 5,
939            layers: Vec::new(),
940            supervisor_config: crate::config::TaskSupervisorConfig::default(),
941            recap_config: zeph_config::RecapConfig::default(),
942            acp_config: zeph_config::AcpConfig::default(),
943            auto_recap_shown: false,
944            msg_count_at_resume: 0,
945            acp_subagent_spawn_fn: None,
946            channel_type: String::new(),
947            provider_persistence_enabled: true,
948        }
949    }
950}
951
952impl SessionState {
953    pub(crate) fn new() -> Self {
954        Self {
955            env_context: EnvironmentContext::gather(""),
956            last_assistant_at: None,
957            response_cache: None,
958            parent_tool_use_id: None,
959            current_turn_intent: None,
960            status_tx: None,
961            lsp_hooks: None,
962            policy_config: None,
963            hooks_config: HooksConfigSnapshot::default(),
964        }
965    }
966}
967
968impl SkillState {
969    pub(crate) fn new(
970        registry: Arc<RwLock<SkillRegistry>>,
971        matcher: Option<SkillMatcherBackend>,
972        max_active_skills: usize,
973        last_skills_prompt: String,
974    ) -> Self {
975        Self {
976            registry,
977            trust_snapshot: Arc::new(RwLock::new(HashMap::new())),
978            skill_paths: Vec::new(),
979            managed_dir: None,
980            trust_config: crate::config::TrustConfig::default(),
981            matcher,
982            max_active_skills,
983            disambiguation_threshold: 0.20,
984            min_injection_score: 0.20,
985            embedding_model: String::new(),
986            skill_reload_rx: None,
987            plugin_dirs_supplier: None,
988            active_skill_names: Vec::new(),
989            last_skills_prompt,
990            prompt_mode: crate::config::SkillPromptMode::Auto,
991            available_custom_secrets: HashMap::new(),
992            cosine_weight: 0.7,
993            hybrid_search: false,
994            bm25_index: None,
995            two_stage_matching: false,
996            confusability_threshold: 0.0,
997            rl_head: None,
998            rl_weight: 0.3,
999            rl_warmup_updates: 50,
1000            generation_output_dir: None,
1001            generation_provider_name: String::new(),
1002            skill_evaluator: None,
1003            eval_weights: zeph_skills::evaluator::EvaluationWeights::default(),
1004            eval_threshold: 0.60,
1005        }
1006    }
1007}
1008
1009impl LifecycleState {
1010    pub(crate) fn new() -> Self {
1011        let (_tx, rx) = watch::channel(false);
1012        Self {
1013            shutdown: rx,
1014            start_time: Instant::now(),
1015            cancel_signal: Arc::new(tokio::sync::Notify::new()),
1016            cancel_token: tokio_util::sync::CancellationToken::new(),
1017            cancel_bridge_handle: None,
1018            config_path: None,
1019            config_reload_rx: None,
1020            plugins_dir: PathBuf::new(),
1021            startup_shell_overlay: ShellOverlaySnapshot::default(),
1022            shell_policy_handle: None,
1023            warmup_ready: None,
1024            update_notify_rx: None,
1025            custom_task_rx: None,
1026            user_loop: None,
1027            last_known_cwd: std::env::current_dir().unwrap_or_default(),
1028            file_changed_rx: None,
1029            file_watcher: None,
1030            supervisor: super::agent_supervisor::BackgroundSupervisor::new(
1031                &crate::config::TaskSupervisorConfig::default(),
1032                None,
1033            ),
1034            notifier: None,
1035            turn_llm_requests: 0,
1036            last_no_providers_at: None,
1037            pending_background_completions: VecDeque::new(),
1038            background_completion_rx: None,
1039        }
1040    }
1041}
1042
1043impl ProviderState {
1044    pub(crate) fn new(initial_prompt_tokens: u64) -> Self {
1045        Self {
1046            summary_provider: None,
1047            provider_override: None,
1048            judge_provider: None,
1049            probe_provider: None,
1050            compress_provider: None,
1051            cached_prompt_tokens: initial_prompt_tokens,
1052            server_compaction_active: false,
1053            stt: None,
1054            provider_pool: Vec::new(),
1055            provider_config_snapshot: None,
1056        }
1057    }
1058}
1059
1060impl MetricsState {
1061    pub(crate) fn new(token_counter: Arc<zeph_memory::TokenCounter>) -> Self {
1062        Self {
1063            metrics_tx: None,
1064            cost_tracker: None,
1065            token_counter,
1066            extended_context: false,
1067            classifier_metrics: None,
1068            timing_window: std::collections::VecDeque::new(),
1069            pending_timings: crate::metrics::TurnTimings::default(),
1070            histogram_recorder: None,
1071        }
1072    }
1073}
1074
1075impl ExperimentState {
1076    pub(crate) fn new() -> Self {
1077        let (notify_tx, notify_rx) = tokio::sync::mpsc::channel::<String>(4);
1078        Self {
1079            config: crate::config::ExperimentConfig::default(),
1080            cancel: None,
1081            baseline: zeph_experiments::ConfigSnapshot::default(),
1082            eval_provider: None,
1083            notify_rx: Some(notify_rx),
1084            notify_tx,
1085        }
1086    }
1087}
1088
1089pub(super) mod security;
1090pub(super) mod skill;
1091
1092#[cfg(test)]
1093mod tests;