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 runtime;
20pub(crate) mod services;
21pub(crate) mod subsystems;
22
23pub(crate) use self::compaction::MemoryCompactionState;
24pub(crate) use self::extraction::MemoryExtractionState;
25pub(crate) use self::persistence::MemoryPersistenceState;
26pub(crate) use self::runtime::AgentRuntime;
27pub(crate) use self::services::Services;
28pub(crate) use self::subsystems::MemorySubsystemState;
29
30use std::collections::{HashMap, HashSet, VecDeque};
31use std::path::PathBuf;
32use std::sync::Arc;
33
34use parking_lot::RwLock;
35use std::time::{Duration, Instant};
36
37use tokio::sync::{Notify, mpsc, watch};
38use tokio::time::Interval;
39use tokio_util::sync::CancellationToken;
40use zeph_llm::any::AnyProvider;
41use zeph_llm::provider::Message;
42use zeph_llm::stt::SpeechToText;
43
44use crate::config::{ProviderEntry, SecurityConfig, SkillPromptMode, TimeoutConfig};
45use crate::config_watcher::ConfigEvent;
46use crate::context::EnvironmentContext;
47use crate::cost::CostTracker;
48use crate::file_watcher::FileChangedEvent;
49use crate::instructions::{InstructionBlock, InstructionEvent, InstructionReloadState};
50use crate::metrics::MetricsSnapshot;
51use crate::vault::Secret;
52use zeph_config;
53use zeph_memory::TokenCounter;
54use zeph_sanitizer::ContentSanitizer;
55use zeph_sanitizer::quarantine::QuarantinedSummarizer;
56use zeph_skills::matcher::SkillMatcherBackend;
57use zeph_skills::registry::SkillRegistry;
58use zeph_skills::watcher::SkillEvent;
59use zeroize::Zeroizing;
60
61use super::message_queue::QueuedMessage;
62
63/// Coordinator struct holding four concern-separated sub-structs for memory management.
64///
65/// Each sub-struct groups fields by a single concern:
66/// - [`persistence`](MemoryPersistenceState) — `SQLite` handles, conversation IDs, recall budgets
67/// - [`compaction`](MemoryCompactionState) — summarization thresholds, shutdown summary, digest
68/// - [`extraction`](MemoryExtractionState) — graph config, RPE router, semantic labels
69/// - [`subsystems`](MemorySubsystemState) — `TiMem`, `autoDream`, `MagicDocs`, microcompact
70#[derive(Default)]
71pub(crate) struct MemoryState {
72    /// `SQLite` handles, conversation IDs, recall budgets, and autosave policy.
73    pub(crate) persistence: MemoryPersistenceState,
74    /// Summarization thresholds, shutdown summary, digest config, and context strategy.
75    pub(crate) compaction: MemoryCompactionState,
76    /// Graph extraction config, RPE router, document config, and semantic label configs.
77    pub(crate) extraction: MemoryExtractionState,
78    /// `TiMem`, `autoDream`, `MagicDocs`, and microcompact subsystem state.
79    pub(crate) subsystems: MemorySubsystemState,
80}
81
82#[allow(clippy::struct_excessive_bools)]
83pub(crate) struct SkillState {
84    pub(crate) registry: Arc<RwLock<SkillRegistry>>,
85    /// Per-turn trust snapshot written by `prepare_context` after `build_skill_trust_map`.
86    /// Shared with `SkillInvokeExecutor` so it can resolve trust without hitting `SQLite`
87    /// on every tool call. Refreshed once per turn — stale by at most one turn.
88    /// Carries full `SkillTrustSnapshot` (level + `requires_trust_check` + `blake3_hash`) so
89    /// `SkillInvokeExecutor` can perform per-invocation re-hash when the flag is set.
90    pub(crate) trust_snapshot:
91        Arc<RwLock<HashMap<String, crate::skill_invoker::SkillTrustSnapshot>>>,
92    pub(crate) skill_paths: Vec<PathBuf>,
93    pub(crate) managed_dir: Option<PathBuf>,
94    pub(crate) trust_config: crate::config::TrustConfig,
95    pub(crate) matcher: Option<SkillMatcherBackend>,
96    pub(crate) max_active_skills: usize,
97    pub(crate) disambiguation_threshold: f32,
98    pub(crate) min_injection_score: f32,
99    pub(crate) embedding_model: String,
100    pub(crate) skill_reload_rx: Option<mpsc::Receiver<SkillEvent>>,
101    /// Resolves the current set of per-plugin skill dirs at reload time.
102    ///
103    /// Called inside `reload_skills()` so that plugins installed via `/plugins add` after
104    /// startup are discovered on the next watcher event without restarting the agent.
105    pub(crate) plugin_dirs_supplier: Option<Arc<dyn Fn() -> Vec<PathBuf> + Send + Sync>>,
106    pub(crate) active_skill_names: Vec<String>,
107    pub(crate) last_skills_prompt: String,
108    pub(crate) prompt_mode: SkillPromptMode,
109    /// Custom secrets available at runtime: key=hyphenated name, value=secret.
110    pub(crate) available_custom_secrets: HashMap<String, Secret>,
111    pub(crate) cosine_weight: f32,
112    pub(crate) hybrid_search: bool,
113    /// Linear blend weight for BM25 hybrid fusion: `fused = bm25_alpha * cosine + (1-bm25_alpha) * bm25_norm`.
114    /// Clamped to `[0.0, 1.0]` at config load. Default: `0.7`.
115    pub(crate) bm25_alpha: f32,
116    pub(crate) bm25_index: Option<zeph_skills::bm25::Bm25Index>,
117    pub(crate) two_stage_matching: bool,
118    /// Threshold for confusability warnings (0.0 = disabled).
119    pub(crate) confusability_threshold: f32,
120    /// `SkillOrchestra` RL routing head. `Some` when `rl_routing_enabled = true` and
121    /// weights are loaded or initialized. `None` when RL routing is disabled.
122    pub(crate) rl_head: Option<zeph_skills::rl_head::RoutingHead>,
123    /// Blend weight for RL routing: `final = (1-rl_weight)*cosine + rl_weight*rl_score`.
124    pub(crate) rl_weight: f32,
125    /// Skip RL blending for the first N updates (cold-start warmup).
126    pub(crate) rl_warmup_updates: u32,
127    /// Directory where `/skill create` writes generated skills.
128    /// Defaults to `managed_dir` if `None`.
129    pub(crate) generation_output_dir: Option<std::path::PathBuf>,
130    /// Provider name for query rewriting before skill matching. Empty = disabled.
131    pub(crate) query_rewrite_provider_name: String,
132    /// Provider name for `/skill create` generation. Empty = primary.
133    pub(crate) generation_provider_name: String,
134    /// Provider name for skill disambiguation LLM calls. Empty = primary.
135    pub(crate) disambiguate_provider_name: String,
136    /// Timeout in milliseconds for `/skill create` LLM generation. Default: 60 000.
137    pub(crate) generation_timeout_ms: u64,
138    /// Optional quality-gate evaluator for generated SKILL.md files (#3319).
139    ///
140    /// When `Some`, the evaluator is attached to every `SkillGenerator` instance so that
141    /// generated skills are scored before being written to disk.
142    pub(crate) skill_evaluator: Option<std::sync::Arc<zeph_skills::evaluator::SkillEvaluator>>,
143    /// Weights for the evaluator composite score — forwarded to `SkillGenerator::with_evaluator`.
144    pub(crate) eval_weights: zeph_skills::evaluator::EvaluationWeights,
145    /// Minimum composite score required to accept a generated skill (forwarded to the generator).
146    pub(crate) eval_threshold: f32,
147    /// Enable `GoSkills` group-structured skill injection.
148    pub(crate) group_structured: bool,
149    /// Inter-skill cosine similarity threshold for `GoSkills` grouping.
150    pub(crate) support_similarity_threshold: f32,
151    /// Whether Stage-2 LLM semantic compliance scan is enabled on `plugin add`.
152    pub(crate) semantic_scan: bool,
153    /// Provider name for the semantic scan LLM. Empty = use primary provider.
154    pub(crate) semantic_scan_provider: String,
155}
156
157pub(crate) struct McpState {
158    pub(crate) tools: Vec<zeph_mcp::McpTool>,
159    pub(crate) registry: Option<zeph_mcp::McpToolRegistry>,
160    pub(crate) manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
161    pub(crate) allowed_commands: Vec<String>,
162    pub(crate) max_dynamic: usize,
163    /// Receives elicitation requests from MCP server handlers during tool execution.
164    /// When `Some`, the agent loop must process these concurrently with tool result awaiting
165    /// to avoid deadlock (tool result waits for elicitation, elicitation waits for agent loop).
166    pub(crate) elicitation_rx: Option<tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>>,
167    /// Shared with `McpToolExecutor` so native `tool_use` sees the current tool list.
168    ///
169    /// Two methods write to this `RwLock` — ordering matters:
170    /// - `sync_executor_tools()`: writes the **full** `self.tools` set.
171    /// - `apply_pruned_tools()`: writes the **pruned** subset (used after pruning).
172    ///
173    /// Within a turn, `sync_executor_tools` must always run **before**
174    /// `apply_pruned_tools`.  The normal call order guarantees this: tool-list
175    /// change events call `sync_executor_tools` (inside `check_tool_refresh`,
176    /// `handle_mcp_add`, `handle_mcp_remove`), and pruning runs later inside
177    /// `rebuild_system_prompt`.  See also: `apply_pruned_tools`.
178    pub(crate) shared_tools: Option<Arc<RwLock<Vec<zeph_mcp::McpTool>>>>,
179    /// Receives full flattened tool list after any `tools/list_changed` notification.
180    pub(crate) tool_rx: Option<tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>>,
181    /// Per-server connection outcomes from the initial `connect_all()` call.
182    pub(crate) server_outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
183    /// Per-message cache for MCP tool pruning results (#2298).
184    ///
185    /// Reset at the start of each user turn and whenever the MCP tool list
186    /// changes (via `tools/list_changed`, `/mcp add`, or `/mcp remove`).
187    pub(crate) pruning_cache: zeph_mcp::PruningCache,
188    /// Dedicated provider for MCP tool pruning LLM calls.
189    ///
190    /// `None` means fall back to the agent's primary provider.
191    /// Resolved from `[[llm.providers]]` at build time using `pruning_provider`
192    /// from `ToolPruningConfig`.
193    pub(crate) pruning_provider: Option<zeph_llm::any::AnyProvider>,
194    /// Whether MCP tool pruning is enabled.  Mirrors `ToolPruningConfig::enabled`.
195    pub(crate) pruning_enabled: bool,
196    /// Pruning parameters snapshot.  Derived from `ToolPruningConfig` at build time.
197    pub(crate) pruning_params: zeph_mcp::PruningParams,
198    /// Pre-computed semantic tool index for embedding-based discovery (#2321).
199    ///
200    /// Built at connect time via `rebuild_semantic_index()`, rebuilt on tool list change.
201    /// `None` when strategy is not `Embedding` or when build failed (fallback to all tools).
202    pub(crate) semantic_index: Option<zeph_mcp::SemanticToolIndex>,
203    /// Active discovery strategy and parameters.  Derived from `ToolDiscoveryConfig`.
204    pub(crate) discovery_strategy: zeph_mcp::ToolDiscoveryStrategy,
205    /// Discovery parameters snapshot.  Derived from `ToolDiscoveryConfig` at build time.
206    pub(crate) discovery_params: zeph_mcp::DiscoveryParams,
207    /// Dedicated embedding provider for tool discovery.  `None` = fall back to the
208    /// agent's primary embedding provider.
209    pub(crate) discovery_provider: Option<zeph_llm::any::AnyProvider>,
210    /// When `true`, show a security warning before prompting for fields whose names
211    /// match sensitive patterns (password, token, secret, key, credential, etc.).
212    pub(crate) elicitation_warn_sensitive_fields: bool,
213    /// When `true`, semantic index and registry need to be rebuilt at the next opportunity.
214    ///
215    /// Set after `/mcp add` or `/mcp remove` when called via `AgentAccess::handle_mcp`,
216    /// which cannot call `rebuild_semantic_index` and `sync_mcp_registry` directly because
217    /// those are `async fn(&mut self)` and their futures are `!Send` (they hold `&mut Agent<C>`
218    /// across `.await`). The rebuild is deferred to `check_tool_refresh`, which runs at the
219    /// start of each turn without the `Box<dyn Future + Send>` constraint.
220    pub(crate) pending_semantic_rebuild: bool,
221}
222
223pub(crate) struct IndexState {
224    pub(crate) retriever: Option<std::sync::Arc<zeph_index::retriever::CodeRetriever>>,
225    pub(crate) repo_map_tokens: usize,
226    pub(crate) cached_repo_map: Option<(String, std::time::Instant)>,
227    pub(crate) repo_map_ttl: std::time::Duration,
228}
229
230/// Snapshot of adversarial policy gate configuration for status display.
231#[derive(Debug, Clone)]
232pub struct AdversarialPolicyInfo {
233    pub provider: String,
234    pub policy_count: usize,
235    pub fail_open: bool,
236    /// Effective policy-LLM call timeout in milliseconds — either the explicitly
237    /// configured `timeout_ms`, or the value auto-scaled for `provider`'s kind
238    /// (local vs cloud). Surfaced so operators can tell at a glance whether a slow
239    /// local `policy_provider` got a realistic budget (see #5870).
240    pub timeout_ms: u64,
241}
242
243#[allow(clippy::struct_excessive_bools)] // independent boolean flags; bitflags or enum would obscure semantics without reducing complexity
244pub(crate) struct RuntimeConfig {
245    pub(crate) security: SecurityConfig,
246    pub(crate) timeouts: TimeoutConfig,
247    pub(crate) model_name: String,
248    /// Configured name from `[[llm.providers]]` (the `name` field), set at startup and on
249    /// `/provider` switch. Falls back to the provider type string when empty.
250    pub(crate) active_provider_name: String,
251    pub(crate) permission_policy: zeph_tools::PermissionPolicy,
252    pub(crate) redact_credentials: bool,
253    pub(crate) rate_limiter: super::rate_limiter::ToolRateLimiter,
254    pub(crate) semantic_cache_enabled: bool,
255    pub(crate) semantic_cache_threshold: f32,
256    pub(crate) semantic_cache_max_candidates: u32,
257    /// Dependency config snapshot stored for per-turn boost parameters.
258    pub(crate) dependency_config: zeph_tools::DependencyConfig,
259    /// Adversarial policy gate runtime info for /status display.
260    pub(crate) adversarial_policy_info: Option<AdversarialPolicyInfo>,
261    /// Current spawn depth of this agent instance (0 = top-level, 1 = first sub-agent, etc.).
262    /// Used by `build_spawn_context()` to propagate depth to children.
263    pub(crate) spawn_depth: u32,
264    /// Inject `<budget>` XML into the volatile system prompt section (#2267).
265    pub(crate) budget_hint_enabled: bool,
266    /// Per-channel skill allowlist. Skills not matching the allowlist are excluded from the
267    /// prompt. An empty `allowed` list means all skills are permitted (default).
268    pub(crate) channel_skills: zeph_config::ChannelSkillsConfig,
269    /// Per-channel tool allowlist. `None` = no restriction. `Some` = only listed tools permitted.
270    /// Populated from the active channel's `allowed_tools` config at agent build time.
271    pub(crate) channel_tool_allowlist: Option<Vec<String>>,
272    /// Minimum allowed interval for `/loop` ticks (seconds). Sourced from `[cli.loop] min_interval_secs`.
273    pub(crate) loop_min_interval_secs: u64,
274    /// Runtime middleware layers for LLM calls and tool dispatch (#2286).
275    ///
276    /// Default: empty vec (zero-cost — loops never iterate).
277    pub(crate) layers: Vec<std::sync::Arc<dyn crate::runtime_layer::RuntimeLayer>>,
278    /// Background supervisor config snapshot for turn-boundary abort logic.
279    pub(crate) supervisor_config: crate::config::TaskSupervisorConfig,
280    /// Session recap config (#3064).
281    pub(crate) recap_config: zeph_config::RecapConfig,
282    /// ACP server configuration snapshot for `/acp` slash-command display.
283    pub(crate) acp_config: zeph_config::AcpConfig,
284    /// Set to `true` after the auto-recap is emitted at session resume (#3144).
285    ///
286    /// Used by `/recap` to skip a redundant LLM call when no new messages have
287    /// been added since the auto-recap was shown.
288    pub(crate) auto_recap_shown: bool,
289    /// Number of non-system messages present when the session was resumed (#3144).
290    ///
291    /// Combined with `auto_recap_shown` to detect whether the user has added new
292    /// messages after the auto-recap was shown.
293    pub(crate) msg_count_at_resume: usize,
294    /// Callback that spawns an external ACP sub-agent process by shell command (#3302).
295    ///
296    /// Injected by the binary crate when the `acp` feature is enabled.
297    /// `None` in bare / non-ACP mode; callers must degrade gracefully.
298    pub(crate) acp_subagent_spawn_fn: Option<zeph_subagent::AcpSubagentSpawnFn>,
299    /// Channel type string used as part of the `(channel_type, channel_id)` persistence key.
300    ///
301    /// Set at build time from the active I/O channel (e.g. `"cli"`, `"tui"`, `"telegram"`).
302    /// Empty when channel identity has not been configured (persistence is skipped).
303    pub(crate) channel_type: String,
304    /// Whether provider preference persistence is enabled for this session (#3308).
305    ///
306    /// Controlled by `[session] provider_persistence = true` (the default). When `false`,
307    /// the stored provider preference is never read or written.
308    pub(crate) provider_persistence_enabled: bool,
309    /// Whether per-session provider override params (e.g. `reasoning_effort`) should be
310    /// persisted alongside the provider name (#4654).
311    ///
312    /// Only meaningful when `provider_persistence_enabled` is also `true`.
313    pub(crate) persist_provider_overrides_enabled: bool,
314    /// Guards against re-persisting during `restore_channel_provider` (#4654, F1).
315    ///
316    /// Set to `true` immediately before calling `provider_switch_as_string` inside the restore
317    /// path, cleared on every branch after the call. While `true`, `persist_channel_provider`
318    /// returns early without writing anything.
319    pub(crate) restoring_provider: bool,
320    /// Goal lifecycle feature configuration.
321    pub(crate) goals: GoalRuntimeConfig,
322    /// Set from the CLI `--bare` flag (#5551).
323    ///
324    /// Bare mode skips skill loading, memory init, MCP connections, scheduler startup, and
325    /// filesystem watchers at startup; this flag lets shutdown-path subsystems (autoDream
326    /// consolidation, skill trace-extraction, shutdown summary, session digest) apply the
327    /// same gating instead of firing unconditional LLM calls at session end.
328    pub(crate) bare: bool,
329    /// Set from `config.cli.safe_mode` (`--safe-mode` / `ZEPH_SAFE_MODE`, #6031).
330    ///
331    /// Distinct from `bare`: safe mode disables ZEPH.md/CLAUDE.md/AGENTS.md discovery,
332    /// plugins, skills, hooks, and MCP servers for troubleshooting isolation, rather than
333    /// `bare`'s memory/tool-registry/background-task test-mode behavior. Read by
334    /// `check_cwd_changed` (#6032) to gate whether a `/cd`-triggered directory change
335    /// re-runs instruction discovery — a safe-mode session must never silently re-load
336    /// project instructions mid-session, which would defeat the flag.
337    pub(crate) safe_mode: bool,
338}
339
340/// Groups feedback detection subsystems: correction detector, judge detector, and LLM classifier.
341pub(crate) struct FeedbackState {
342    pub(crate) detector: zeph_agent_feedback::FeedbackDetector,
343    pub(crate) judge: Option<zeph_agent_feedback::JudgeDetector>,
344    /// LLM-backed zero-shot classifier for `DetectorMode::Model`.
345    /// When `Some`, `spawn_judge_correction_check` uses this instead of `JudgeDetector`.
346    pub(crate) llm_classifier: Option<zeph_llm::classifier::llm::LlmClassifier>,
347}
348
349/// Groups security-related subsystems (sanitizer, quarantine, exfiltration guard).
350pub(crate) struct SecurityState {
351    pub(crate) sanitizer: ContentSanitizer,
352    pub(crate) quarantine_summarizer: Option<QuarantinedSummarizer>,
353    /// Whether this agent session is serving an ACP client.
354    /// When `true` and `mcp_to_acp_boundary` is enabled, MCP tool results
355    /// receive unconditional quarantine and cross-boundary audit logging.
356    pub(crate) is_acp_session: bool,
357    pub(crate) exfiltration_guard: zeph_sanitizer::exfiltration::ExfiltrationGuard,
358    pub(crate) flagged_urls: HashSet<String>,
359    /// URLs explicitly provided by the user across all turns in this session.
360    /// Populated from raw user message text; cleared on `/clear`.
361    /// Shared with `UrlGroundingVerifier` to check `fetch`/`web_scrape` calls at dispatch time.
362    pub(crate) user_provided_urls: Arc<RwLock<HashSet<String>>>,
363    pub(crate) pii_filter: zeph_sanitizer::pii::PiiFilter,
364    /// NER classifier for PII detection (`classifiers.ner_model`). When `Some`, the PII path
365    /// runs both regex (`pii_filter`) and NER, then merges spans before redaction.
366    /// `None` when `classifiers` feature is disabled or `classifiers.enabled = false`.
367    #[cfg(feature = "classifiers")]
368    pub(crate) pii_ner_backend: Option<std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>>,
369    /// Per-call timeout for the NER PII classifier in milliseconds.
370    #[cfg(feature = "classifiers")]
371    pub(crate) pii_ner_timeout_ms: u64,
372    /// Maximum number of bytes passed to the NER PII classifier per call.
373    ///
374    /// Large tool outputs (e.g. `search_code`) can produce 150+ `DeBERTa` chunks and exceed
375    /// the per-call timeout. Input is truncated at a valid UTF-8 boundary before classification.
376    #[cfg(feature = "classifiers")]
377    pub(crate) pii_ner_max_chars: usize,
378    /// Circuit-breaker threshold: number of consecutive timeouts before NER is disabled.
379    /// `0` means the circuit breaker is disabled (NER is always attempted).
380    #[cfg(feature = "classifiers")]
381    pub(crate) pii_ner_circuit_breaker_threshold: u32,
382    /// Number of consecutive NER timeouts observed since the last successful call.
383    #[cfg(feature = "classifiers")]
384    pub(crate) pii_ner_consecutive_timeouts: u32,
385    /// Set to `true` when the circuit breaker trips. NER is skipped for the rest of the session.
386    #[cfg(feature = "classifiers")]
387    pub(crate) pii_ner_tripped: bool,
388    pub(crate) memory_validator: zeph_sanitizer::memory_validation::MemoryWriteValidator,
389    /// LLM-based prompt injection pre-screener (opt-in).
390    pub(crate) guardrail: Option<zeph_sanitizer::guardrail::GuardrailFilter>,
391    /// SONAR NLI entailment-based injection detection stage (opt-in, observe-only).
392    pub(crate) nli_sanitizer: Option<zeph_sanitizer::nli::NliSanitizer>,
393    /// PAAC secret placeholder masking registry (opt-in). Shared with the bootstrap layer so
394    /// vault-resolved secrets registered during config load are masked at the LLM boundary.
395    pub(crate) secret_registry: Option<Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
396    /// Post-LLM response verification layer.
397    pub(crate) response_verifier: zeph_sanitizer::response_verifier::ResponseVerifier,
398    /// Temporal causal IPI analyzer (opt-in, disabled when `None`).
399    pub(crate) causal_analyzer: Option<zeph_sanitizer::causal_ipi::TurnCausalAnalyzer>,
400    /// VIGIL pre-sanitizer gate. `None` for subagent sessions (subagents are exempt).
401    /// Set at agent build time for top-level agents; skipped for subagents (high FP rate).
402    pub(crate) vigil: Option<crate::agent::vigil::VigilGate>,
403    /// Cross-turn risk accumulator (spec 050 Phase 1).
404    ///
405    /// `advance_turn()` MUST be called once per turn, before `PolicyGateExecutor::check_policy`.
406    /// Never expose score, level, or alerts to any LLM-callable surface.
407    pub(crate) trajectory: crate::agent::trajectory::TrajectorySentinel,
408    /// Shared risk-level slot for `PolicyGateExecutor` (spec 050).
409    ///
410    /// Written by the agent loop after each turn's `sentinel.current_risk()` call.
411    /// `PolicyGateExecutor::check_policy` reads it to downgrade `Allow` at `Critical`.
412    /// `u8` encoding: 0=Calm, 1=Elevated, 2=High, 3=Critical.
413    pub(crate) trajectory_risk_slot: zeph_tools::TrajectoryRiskSlot,
414    /// Pending risk signals from executor layers (spec 050 §2).
415    ///
416    /// `PolicyGateExecutor` and `ScopedToolExecutor` push signal codes here.
417    /// `begin_turn()` drains this queue into `trajectory.record()`.
418    pub(crate) trajectory_signal_queue: zeph_tools::RiskSignalQueue,
419    /// Persistent safety stream + LLM pre-execution probe (spec 050 Phase 2).
420    ///
421    /// `None` when `security.shadow_sentinel.enabled = false` (default).
422    /// When `Some`, `begin_turn()` calls `advance_turn()` to reset the per-turn probe counter.
423    pub(crate) shadow_sentinel:
424        Option<std::sync::Arc<crate::agent::shadow_sentinel::ShadowSentinel>>,
425    /// Per-turn multi-step attack chain accumulator.
426    ///
427    /// `None` by default. When `Some`, `begin_turn()` calls `reset()` to clear per-turn state.
428    /// The same `Arc` must be passed to `ShellExecutor::with_risk_chain` at build time.
429    pub(crate) risk_chain_accumulator: Option<std::sync::Arc<zeph_tools::RiskChainAccumulator>>,
430    /// MAGE trajectory risk accumulator (spec 004-16).
431    ///
432    /// Per-session in-memory accumulator that ingests sanitizer audit signals with exponential
433    /// temporal decay and gates tool execution when cumulative risk exceeds `risk_threshold`.
434    /// Initialized as noop when `memory.shadow_memory.enabled = false` (default).
435    /// `begin_turn()` calls `advance_turn()` then ingests pending signal codes.
436    pub(crate) mage_accumulator: zeph_memory::shadow::TrajectoryRiskAccumulator,
437    /// Per-session append-only shadow memory for cross-turn goal-drift detection (spec 010-7).
438    ///
439    /// `None` when `security.causal_ipi.shadow_memory.enabled = false` (default).
440    /// When `Some`, `process_tool_result_batch` records a `ShadowEvent` after each tool batch,
441    /// then calls `goal_drift_score()` and emits a `GoalDrift` security event when alerted.
442    pub(crate) shadow_memory: Option<zeph_sanitizer::ShadowMemory>,
443    /// Handle into `TrustGateExecutor`'s MCP tool-id registry
444    /// (`crates/zeph-tools/src/trust_gate.rs`), used to force-deny all MCP-sourced tools when
445    /// the active skill trust is Quarantined.
446    ///
447    /// `None` when the caller didn't attach a handle (e.g. tests, or an executor tree built
448    /// without `apply_common_tool_gating`). When `Some`, `check_tool_refresh` keeps it in sync
449    /// with `self.services.mcp.tools` so MCP servers connected after startup (`/mcp add`,
450    /// `tools/list_changed`) are folded into the Quarantine-deny set — the handle is otherwise
451    /// only ever populated once, at startup, and goes stale (#5747).
452    pub(crate) mcp_tool_ids: Option<Arc<RwLock<HashSet<String>>>>,
453}
454
455/// Groups debug/diagnostics subsystems (dumper, trace collector, anomaly detector, logging config).
456pub(crate) struct DebugState {
457    pub(crate) debug_dumper: Option<crate::debug_dump::DebugDumper>,
458    pub(crate) dump_format: crate::debug_dump::DumpFormat,
459    pub(crate) trace_collector: Option<crate::debug_dump::trace::TracingCollector>,
460    /// Monotonically increasing counter for `process_user_message` calls.
461    /// Used to key spans in `trace_collector.active_iterations`.
462    pub(crate) iteration_counter: usize,
463    pub(crate) anomaly_detector: Option<zeph_tools::AnomalyDetector>,
464    /// Whether to emit `reasoning_amplification` warnings for quality failures from reasoning
465    /// models. Mirrors `AnomalyConfig::reasoning_model_warning`. Default: `true`.
466    pub(crate) reasoning_model_warning: bool,
467    pub(crate) logging_config: crate::config::LoggingConfig,
468    /// Base dump directory — stored so `/dump-format trace` can create a `TracingCollector` (CR-04).
469    pub(crate) dump_dir: Option<PathBuf>,
470    /// Service name for `TracingCollector` created via runtime format switch (CR-04).
471    pub(crate) trace_service_name: String,
472    /// Whether to redact in `TracingCollector` created via runtime format switch (CR-04).
473    pub(crate) trace_redact: bool,
474    /// User-defined resource attributes forwarded to `TracingCollector` (from `telemetry.trace_metadata`).
475    pub(crate) trace_metadata: std::collections::HashMap<String, String>,
476    /// Span ID of the currently executing iteration — used by LLM/tool span wiring (CR-01).
477    /// Set to `Some` at the start of `process_user_message`, cleared at end.
478    pub(crate) current_iteration_span_id: Option<[u8; 8]>,
479}
480
481/// Snapshot of the shell-level overlay baked in at startup.
482///
483/// Used in `reload_config` to detect when a hot-reload would produce a different shell
484/// restriction set than the one baked into the live `ShellExecutor` (M4 warn-on-divergence).
485#[derive(Debug, Clone, Default, PartialEq, Eq)]
486pub struct ShellOverlaySnapshot {
487    /// Sorted `blocked_commands` contributed by plugins.
488    pub blocked: Vec<String>,
489    /// Sorted `allowed_commands` after plugin intersection (empty if base was empty).
490    pub allowed: Vec<String>,
491}
492
493/// Runtime state for an active `/loop` session.
494///
495/// At most one loop is active at a time; `LifecycleState::user_loop` holds `Some` while
496/// the loop is running and `None` otherwise.
497pub(crate) struct LoopState {
498    /// The prompt text injected on each tick.
499    pub(crate) prompt: String,
500    /// Number of ticks fired so far.
501    pub(crate) iteration: u64,
502    /// Tick interval. `MissedTickBehavior::Skip` prevents burst catch-up.
503    pub(crate) interval: Interval,
504    /// Cancel handle. Dropped (and token cancelled) when loop is stopped.
505    pub(crate) cancel_tx: CancellationToken,
506}
507
508/// Groups agent lifecycle state: shutdown signaling, timing, and I/O notification channels.
509pub(crate) struct LifecycleState {
510    pub(crate) shutdown: watch::Receiver<bool>,
511    pub(crate) start_time: Instant,
512    pub(crate) cancel_signal: Arc<Notify>,
513    pub(crate) cancel_token: CancellationToken,
514    /// Handle to the cancel bridge task spawned each turn. Aborted before a new one is created
515    /// to prevent unbounded task accumulation across turns.
516    pub(crate) cancel_bridge_handle: Option<zeph_common::task_supervisor::BlockingHandle<()>>,
517    pub(crate) config_path: Option<PathBuf>,
518    pub(crate) config_reload_rx: Option<mpsc::Receiver<ConfigEvent>>,
519    /// Path to the plugins directory; used to re-apply overlays on hot-reload.
520    pub(crate) plugins_dir: PathBuf,
521    /// Shell overlay snapshot baked in at startup. Used to detect divergence on hot-reload.
522    pub(crate) startup_shell_overlay: ShellOverlaySnapshot,
523    /// Handle for live-rebuilding the `ShellExecutor`'s `blocked_commands` policy on hot-reload.
524    /// `None` when no `ShellExecutor` is in the executor chain (test harnesses, daemon-only modes).
525    pub(crate) shell_policy_handle: Option<zeph_tools::ShellPolicyHandle>,
526    pub(crate) warmup_ready: Option<watch::Receiver<bool>>,
527    pub(crate) update_notify_rx: Option<mpsc::Receiver<String>>,
528    pub(crate) custom_task_rx: Option<mpsc::Receiver<String>>,
529    /// Active `/loop` state. `None` when no loop is running.
530    pub(crate) user_loop: Option<LoopState>,
531    /// Last known process cwd. Compared after each tool call to detect changes.
532    pub(crate) last_known_cwd: PathBuf,
533    /// Receiver for file-change events from `FileChangeWatcher`. `None` when no paths configured.
534    pub(crate) file_changed_rx: Option<mpsc::Receiver<FileChangedEvent>>,
535    /// Keeps the `FileChangeWatcher` alive for the agent's lifetime. Dropping it aborts the watcher task.
536    pub(crate) file_watcher: Option<crate::file_watcher::FileChangeWatcher>,
537    /// Supervised background task manager. Owned by the agent; call `reap()` between turns
538    /// and `abort_all()` on shutdown.
539    pub(crate) supervisor: super::agent_supervisor::BackgroundSupervisor,
540    /// Ticks periodically so `Agent::next_event` refreshes `bg_enrichment_inflight` /
541    /// `bg_telemetry_inflight` (and reaps completed tasks) during idle time between turns, not
542    /// only at the top of the next turn. Background enrichment/telemetry tasks run *after* a
543    /// turn's response is sent (spawned from `persist_message`), so without this the TUI status
544    /// segment showing in-flight background work was invisible for the entire idle window (#6279).
545    ///
546    /// `None` until the first `Agent::next_event` call lazily constructs it: `tokio::time::interval`
547    /// requires an active Tokio runtime, but `LifecycleState::new()` is also called from plain
548    /// (non-`#[tokio::test]`) unit tests that construct an `Agent` outside any runtime.
549    pub(crate) bg_metrics_tick: Option<Interval>,
550    /// Per-turn completion notifier. `None` when `notifications.enabled = false`.
551    pub(crate) notifier: Option<crate::notifications::Notifier>,
552    /// Per-turn LLM request counter. Incremented by `process_response`; reset at turn start.
553    pub(crate) turn_llm_requests: u32,
554    /// Timestamp of the last turn that ended with `LlmError::NoProviders`.
555    ///
556    /// Used to gate `advance_context_lifecycle`: when all providers are down, context preparation
557    /// is skipped (degraded mode) until `no_providers_backoff_secs` has elapsed.
558    pub(crate) last_no_providers_at: Option<Instant>,
559    /// Completions from background shell runs waiting to be injected into the next turn.
560    ///
561    /// Drained at the top of `process_user_message_inner` after `supervisor.reap()`.
562    /// All pending completions and the real user message are merged into a **single**
563    /// user-role block to satisfy strict alternation requirements (Anthropic Messages API).
564    ///
565    /// Capacity is capped at `BACKGROUND_COMPLETION_BUFFER_CAP`. On overflow the oldest
566    /// entry is dropped and a placeholder is substituted so the LLM learns results were lost.
567    pub(crate) pending_background_completions:
568        VecDeque<zeph_tools::shell::background::BackgroundCompletion>,
569    /// Receiver end of the dedicated background-completion channel created alongside the
570    /// `ShellExecutor`. Polled at the top of each turn to drain completions into
571    /// `pending_background_completions`. `None` when no `ShellExecutor` is configured.
572    pub(crate) background_completion_rx:
573        Option<tokio::sync::mpsc::Receiver<zeph_tools::BackgroundCompletion>>,
574    /// Shared reference to the `ShellExecutor` used to query in-flight background run snapshots
575    /// for TUI metrics display. `None` when no `ShellExecutor` is wired (test harnesses, etc.).
576    pub(crate) shell_executor_handle: Option<std::sync::Arc<zeph_tools::ShellExecutor>>,
577    /// Session-level task supervisor, shared with bootstrap and TUI. Used to register
578    /// background agent tasks (cancel bridge, compaction, sidequest eviction) for
579    /// observability and graceful shutdown.
580    ///
581    /// Created with a fresh [`CancellationToken`] in `LifecycleState::new()` for test
582    /// harnesses; production code overwrites it via `Agent::with_task_supervisor`.
583    pub(crate) task_supervisor: Arc<zeph_common::TaskSupervisor>,
584}
585
586/// Minimal config snapshot needed to reconstruct a provider at runtime via `/provider <name>`.
587///
588/// Secrets are stored as plain strings because [`Secret`] intentionally does not implement
589/// `Clone`. They are re-wrapped in `Secret` when passed to `build_provider_for_switch`.
590///
591/// `Clone` so ACP/serve deps structs (built once per process) can hand each session its own
592/// owned copy via [`Agent::with_provider_pool`](crate::agent::Agent::with_provider_pool).
593#[derive(Clone, Default)]
594pub struct ProviderConfigSnapshot {
595    pub claude_api_key: Option<String>,
596    pub openai_api_key: Option<String>,
597    pub gemini_api_key: Option<String>,
598    pub compatible_api_keys: std::collections::HashMap<String, String>,
599    pub llm_request_timeout_secs: u64,
600    pub embedding_model: String,
601    pub gonka_private_key: Option<Zeroizing<String>>,
602    pub gonka_address: Option<String>,
603    pub cocoon_access_hash: Option<String>,
604}
605
606/// Groups provider-related state: alternate providers, runtime switching, and compaction flags.
607pub(crate) struct ProviderState {
608    pub(crate) summary_provider: Option<AnyProvider>,
609    /// Shared slot for runtime model switching; set by external caller (e.g. ACP).
610    pub(crate) provider_override: Option<Arc<RwLock<Option<AnyProvider>>>>,
611    pub(crate) judge_provider: Option<AnyProvider>,
612    /// Dedicated provider for compaction probe LLM calls. Falls back to `summary_provider`
613    /// (or primary) when `None`.
614    pub(crate) probe_provider: Option<AnyProvider>,
615    /// Dedicated provider for `compress_context` LLM calls (#2356).
616    /// Falls back to the primary provider when `None`.
617    pub(crate) compress_provider: Option<AnyProvider>,
618    pub(crate) cached_prompt_tokens: u64,
619    /// Whether the active provider has server-side compaction enabled (Claude compact-2026-01-12).
620    /// When true, client-side compaction is skipped.
621    pub(crate) server_compaction_active: bool,
622    pub(crate) stt: Option<Box<dyn SpeechToText>>,
623    /// Snapshot of `[[llm.providers]]` entries for runtime `/provider` switching.
624    pub(crate) provider_pool: Vec<ProviderEntry>,
625    /// Resolved secrets and timeout settings needed to reconstruct providers at runtime.
626    pub(crate) provider_config_snapshot: Option<ProviderConfigSnapshot>,
627}
628
629/// Groups metrics and cost tracking state.
630pub(crate) struct MetricsState {
631    pub(crate) metrics_tx: Option<watch::Sender<MetricsSnapshot>>,
632    pub(crate) cost_tracker: Option<CostTracker>,
633    pub(crate) token_counter: Arc<TokenCounter>,
634    /// Set to `true` when Claude extended context (`enable_extended_context = true`) is active.
635    /// Read from config at build time, not derived from provider internals.
636    pub(crate) extended_context: bool,
637    /// Shared classifier latency ring buffer. Populated by `ContentSanitizer` (injection, PII)
638    /// and `LlmClassifier` (feedback). `None` when classifiers are not configured.
639    pub(crate) classifier_metrics: Option<Arc<zeph_llm::ClassifierMetrics>>,
640    /// Rolling window of per-turn latency samples (last 10 turns).
641    pub(crate) timing_window: std::collections::VecDeque<crate::metrics::TurnTimings>,
642    /// Accumulator for the current turn's timings. Flushed at turn end via `flush_turn_timings`.
643    pub(crate) pending_timings: crate::metrics::TurnTimings,
644    /// Optional histogram recorder for per-event Prometheus observations.
645    /// `None` when the `prometheus` feature is disabled or metrics are not enabled.
646    pub(crate) histogram_recorder: Option<std::sync::Arc<dyn crate::metrics::HistogramRecorder>>,
647}
648
649/// Groups task orchestration and subagent state.
650#[derive(Default)]
651pub(crate) struct OrchestrationState {
652    /// Lookahead tool hints snapshot taken after the most recent scheduler tick.
653    ///
654    /// Populated by `run_scheduler_loop` after each `scheduler.tick()` call via
655    /// `zeph_orchestration::lookahead_tools`. Cleared when the scheduler loop exits.
656    /// Read by `prepare_context` in `assembly.rs` to pass PAACE hints to `FidelityScorer`.
657    pub(crate) cached_lookahead: Vec<zeph_common::PlannedToolHint>,
658    /// On `OrchestrationState` (not `ProviderState`) because this provider is used exclusively
659    /// by `LlmPlanner` during orchestration, not shared across subsystems.
660    pub(crate) planner_provider: Option<AnyProvider>,
661    /// Provider for `PlanVerifier` LLM calls. `None` falls back to `orchestrator_provider`
662    /// then the primary provider.
663    pub(crate) verify_provider: Option<AnyProvider>,
664    /// Provider for scheduling-tier LLM calls (aggregation, predicate evaluation, verification
665    /// fallback). `None` falls back to the primary provider.
666    /// Set from `config.orchestration.orchestrator_provider` at startup.
667    pub(crate) orchestrator_provider: Option<AnyProvider>,
668    /// Provider for predicate gate evaluation. `None` falls back to `orchestrator_provider`
669    /// then `verify_provider` then primary.
670    pub(crate) predicate_provider: Option<AnyProvider>,
671    /// Resolved ensemble members for ORCH-style deterministic verifier ensemble-merge
672    /// (spec `073-orch-ensemble-merge`). Each entry pairs the `[[llm.providers]]` name with
673    /// its resolved provider — kept as pairs (not a bare `Vec<AnyProvider>`) so a partial
674    /// bootstrap-time resolution failure can never desynchronize a ballot's `member` name
675    /// from the wrong config entry.
676    ///
677    /// Empty when `[orchestration.ensemble].enabled = false` (the default) or when no member
678    /// resolved successfully. `SchedulerAction::Verify` only takes the ensemble branch when
679    /// this is non-empty.
680    pub(crate) ensemble_members: Vec<(String, AnyProvider)>,
681    /// Graph waiting for `/plan confirm` before execution starts.
682    pub(crate) pending_graph: Option<zeph_orchestration::TaskGraph>,
683    /// Cancellation token for the currently executing plan. `None` when no plan is running.
684    /// Created fresh in `handle_plan_confirm()`, cancelled in `handle_plan_cancel()`.
685    ///
686    /// # Known limitation
687    ///
688    /// Token plumbing is ready; the delivery path requires the agent message loop to be
689    /// restructured so `/plan cancel` can be received while `run_scheduler_loop` holds
690    /// `&mut self`. See follow-up issue #1603 (SEC-M34-002).
691    pub(crate) plan_cancel_token: Option<CancellationToken>,
692    /// Manages spawned sub-agents.
693    pub(crate) subagent_manager: Option<zeph_subagent::SubAgentManager>,
694    pub(crate) subagent_config: crate::config::SubAgentConfig,
695    pub(crate) orchestration_config: crate::config::OrchestrationConfig,
696    /// Lazily initialized plan template cache. `None` until first use or when
697    /// memory (`SQLite`) is unavailable.
698    #[allow(dead_code)]
699    pub(crate) plan_cache: Option<zeph_orchestration::PlanCache>,
700    /// Goal embedding from the most recent `plan_with_cache()` call. Consumed by
701    /// `finalize_plan_execution()` to cache the completed plan template.
702    pub(crate) pending_goal_embedding: Option<Vec<f32>>,
703    /// `AdaptOrch` topology advisor — `None` when `[orchestration.adaptorch]` is disabled.
704    pub(crate) topology_advisor: Option<std::sync::Arc<zeph_orchestration::TopologyAdvisor>>,
705    /// Last `AdaptOrch` verdict; carried from `handle_plan_goal_as_string` to scheduler loop
706    /// for `record_outcome`.
707    #[allow(dead_code)] // read via .take() in plan.rs; clippy false positive
708    pub(crate) last_advisor_verdict: Option<zeph_orchestration::AdvisorVerdict>,
709    /// Task graph persistence handle. `None` when no `SemanticMemory` was
710    /// attached via `with_memory`, or when
711    /// `OrchestrationConfig::persistence_enabled` is `false`. When `Some`, the
712    /// scheduler loop snapshots the graph once per tick and `/plan resume <id>`
713    /// rehydrates from disk.
714    pub(crate) graph_persistence: Option<
715        zeph_orchestration::GraphPersistence<zeph_memory::store::graph_store::TaskGraphStore>,
716    >,
717    /// Named execution environment for the current orchestration task.
718    ///
719    /// Set by the scheduler when dispatching a `TaskNode` that has
720    /// `execution_environment: Some(name)`. Cleared between tasks. When `Some`,
721    /// `prepare_tool_dispatch` injects an [`ExecutionContext`] named `name` into
722    /// every `ToolCall` so that `ShellExecutor::resolve_context` uses the right env.
723    pub(crate) task_execution_env: Option<String>,
724
725    // ── P2 durable adapter (spec-064) ─────────────────────────────────────────
726    /// Durable config snapshot used by the P2 adapter in `plan.rs`.
727    ///
728    /// `None` when durable execution is disabled or the agent was built without a durable config
729    /// (e.g. unit tests). When `Some` and `durable.orchestration = true`, `/plan resume` restores
730    /// the replan budget from the journal instead of zeroing it.
731    pub(crate) durable_config: Option<zeph_config::DurableConfig>,
732    /// Resolved path to `durable.db` (the dedicated journal file for `LocalBackend`).
733    ///
734    /// Derived at build time from `memory.sqlite_path` sibling directory. `None` when the durable
735    /// adapter is not configured.
736    pub(crate) durable_db_url: Option<String>,
737    /// Shared durable backend for P2 budget snapshots.
738    ///
739    /// Lazily initialised by `plan.rs` on first journal call; shared across pause/resume cycles
740    /// for the same process lifetime.
741    // Accessed exclusively through ensure_durable_backend() in plan.rs; rustc's cross-module
742    // dead_code analysis does not follow the indirect Option method chains.
743    #[allow(dead_code)]
744    pub(crate) durable_backend: Option<std::sync::Arc<zeph_durable::DurableBackendEnum>>,
745    /// Writer handle for the shared P2 durable backend.
746    // Same as durable_backend: accessed through plan.rs ensure_durable_backend().
747    #[allow(dead_code)]
748    pub(crate) durable_writer: Option<zeph_durable::JournalWriterHandle>,
749    /// [`BlockingHandle`] for the background `JournalWriter` actor task, tracked by `TaskSupervisor`.
750    ///
751    /// Kept so the agent can abort the writer on shutdown rather than relying on process exit.
752    /// `None` until `ensure_durable_backend()` initialises the backend for the first time.
753    pub(crate) durable_writer_task: Option<zeph_common::task_supervisor::BlockingHandle<()>>,
754    /// Cipher for encrypting P2 budget snapshots. `None` when `encrypt_payload = false`.
755    pub(crate) durable_cipher: Option<std::sync::Arc<dyn zeph_durable::PayloadCipher>>,
756    /// Control-entry row HMAC key (INV-8) for the P2 durable backend. `None` for a single-user
757    /// local, non-shared database — the documented stance where control entries carry no HMAC.
758    pub(crate) durable_hmac_key: Option<[u8; 32]>,
759}
760
761/// Groups instruction hot-reload state.
762#[derive(Default)]
763pub(crate) struct InstructionState {
764    pub(crate) blocks: Vec<InstructionBlock>,
765    pub(crate) reload_rx: Option<mpsc::Receiver<InstructionEvent>>,
766    pub(crate) reload_state: Option<InstructionReloadState>,
767}
768
769/// Groups experiment feature state (gated behind `experiments` feature flag).
770pub(crate) struct ExperimentState {
771    pub(crate) config: crate::config::ExperimentConfig,
772    /// Cancellation token for a running experiment session. `Some` means an experiment is active.
773    pub(crate) cancel: Option<tokio_util::sync::CancellationToken>,
774    /// Handle for the background experiment task. Stored so shutdown can abort it if the
775    /// `CancellationToken` signal is not observed in time (e.g. the task is blocked on I/O).
776    pub(crate) handle: Option<zeph_common::task_supervisor::BlockingHandle<()>>,
777    /// Pre-built config snapshot used as the experiment baseline (agent path).
778    pub(crate) baseline: zeph_experiments::ConfigSnapshot,
779    /// Dedicated judge provider for evaluation. When `Some`, the evaluator uses this provider
780    /// instead of the agent's primary provider, eliminating self-judge bias.
781    pub(crate) eval_provider: Option<AnyProvider>,
782    /// Receives completion/error messages from the background experiment engine task.
783    /// Always present so the select! branch compiles unconditionally.
784    pub(crate) notify_rx: Option<tokio::sync::mpsc::Receiver<String>>,
785    /// Sender end paired with `experiment_notify_rx`. Cloned into the background task.
786    pub(crate) notify_tx: tokio::sync::mpsc::Sender<String>,
787}
788
789/// Groups context-compression feature state (gated behind `context-compression` feature flag).
790#[derive(Default)]
791pub(crate) struct CompressionState {
792    /// Cached task goal for TaskAware/MIG pruning. Set by `maybe_compact()`,
793    /// invalidated when the last user message hash changes.
794    pub(crate) current_task_goal: Option<String>,
795    /// Hash of the last user message when `current_task_goal` was populated.
796    pub(crate) task_goal_user_msg_hash: Option<u64>,
797    /// Pending background task for goal extraction. Spawned when the user message hash changes;
798    /// result applied at the start of the next Soft compaction (#1909).
799    pub(crate) pending_task_goal:
800        Option<zeph_common::task_supervisor::BlockingHandle<Option<String>>>,
801    /// Pending `SideQuest` eviction result from the background LLM call spawned last turn.
802    /// Applied at the START of the next turn before compaction (PERF-1 fix).
803    pub(crate) pending_sidequest_result:
804        Option<zeph_common::task_supervisor::BlockingHandle<Option<Vec<usize>>>>,
805    /// In-memory subgoal registry for `Subgoal`/`SubgoalMig` pruning strategies (#2022).
806    pub(crate) subgoal_registry: zeph_agent_context::SubgoalRegistry,
807    /// Pending background subgoal extraction task.
808    pub(crate) pending_subgoal: Option<
809        zeph_common::task_supervisor::BlockingHandle<
810            Option<zeph_agent_context::SubgoalExtractionResult>,
811        >,
812    >,
813    /// Hash of the last user message when subgoal extraction was scheduled.
814    pub(crate) subgoal_user_msg_hash: Option<u64>,
815    /// Shared typed-page state (#3630). `None` when `typed_pages.enabled = false`.
816    pub(crate) typed_pages_state: Option<Arc<zeph_context::typed_page::TypedPagesState>>,
817}
818
819/// Groups runtime tool filtering, dependency tracking, and iteration bookkeeping.
820#[derive(Default)]
821pub(crate) struct ToolState {
822    /// Dynamic tool schema filter: pre-computed tool embeddings for per-turn filtering (#2020).
823    pub(crate) tool_schema_filter: Option<zeph_tools::ToolSchemaFilter>,
824    /// Cached filtered tool IDs for the current user turn.
825    pub(crate) cached_filtered_tool_ids: Option<HashSet<String>>,
826    /// Tool dependency graph for sequential tool availability (#2024).
827    pub(crate) dependency_graph: Option<zeph_tools::ToolDependencyGraph>,
828    /// Always-on tool IDs, mirrored from the tool schema filter for dependency gate bypass.
829    pub(crate) dependency_always_on: HashSet<String>,
830    /// Tool IDs that completed successfully in the current session.
831    pub(crate) completed_tool_ids: HashSet<String>,
832    /// Current tool loop iteration index within the active user turn.
833    pub(crate) current_tool_iteration: usize,
834    /// PASTE pattern store for tool invocation history and prediction (#3642).
835    ///
836    /// `Some` only when `config.tools.speculative.mode` is `Pattern` or `Both`.
837    pub(crate) pattern_store: Option<Arc<crate::agent::speculative::paste::PatternStore>>,
838    /// Per-turn mapping from tool name to `(skill_name, skill_hash)`, populated at skill
839    /// activation and used by `observe()` to attribute tool completions to their owning skill.
840    pub(crate) tool_to_skill: HashMap<String, (String, String)>,
841    /// Last tool executed per skill in the current turn, keyed by skill name.
842    /// Used as `prev_tool` for PASTE pattern transition recording.
843    pub(crate) last_tool_per_skill: HashMap<String, String>,
844    /// `config.tools.shell.allowed_paths`, mirrored here so `AgentAccess::change_working_directory`
845    /// (#6032 SEC-2) can validate a `/cd` target against the same sandbox boundary
846    /// `FileExecutor`/`DiagnosticsExecutor`/`SetCwdExecutor` already enforce, via
847    /// `zeph_common::security::validate_path_within`. Empty means "no session has set it yet";
848    /// callers treat empty the same way `FileExecutor::new` does (default to `[cwd]`), not as
849    /// "allow every path".
850    pub(crate) allowed_paths: Vec<std::path::PathBuf>,
851}
852
853/// Groups per-session I/O and policy state.
854#[allow(clippy::struct_excessive_bools)] // runtime state — boolean flags are idiomatic here
855pub(crate) struct SessionState {
856    pub(crate) env_context: EnvironmentContext,
857    /// Timestamp of the last assistant message appended to context.
858    /// Used by time-based microcompact to compute session idle gap (#2699).
859    /// `None` before the first assistant response.
860    pub(crate) last_assistant_at: Option<Instant>,
861    pub(crate) response_cache: Option<std::sync::Arc<zeph_memory::ResponseCache>>,
862    /// Parent tool call ID when this agent runs as a subagent inside another agent session.
863    /// Propagated into every `LoopbackEvent::ToolStart` / `ToolOutput` so the IDE can build
864    /// a subagent hierarchy.
865    pub(crate) parent_tool_use_id: Option<String>,
866    /// Current-turn intent snapshot for VIGIL. `None` between turns.
867    ///
868    /// Set at the top of `process_user_message` (before any tool call) to the first 1024 chars
869    /// of the user message. Cleared at `end_turn`, on `/clear`, and on any turn-abort path.
870    /// Never shared across turns or propagated into subagents.
871    pub(crate) current_turn_intent: Option<String>,
872    /// Optional status channel for sending spinner/status messages to TUI or stderr.
873    pub(crate) status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
874    /// LSP context injection hooks. Fires after native tool execution, injects
875    /// diagnostics/hover notes as `Role::System` messages before the next LLM call.
876    pub(crate) lsp_hooks: Option<crate::lsp_hooks::LspHookRunner>,
877    /// Snapshot of the policy config for `/policy` command inspection.
878    pub(crate) policy_config: Option<zeph_tools::PolicyConfig>,
879    /// `CwdChanged` hook definitions extracted from `[hooks]` config.
880    pub(crate) hooks_config: HooksConfigSnapshot,
881    /// Whether the current turn originates from a Telegram guest query (`guest_message` update).
882    ///
883    /// When `true`, the agent prompt includes a brief guest-context annotation, and the response
884    /// is delivered via `answerGuestQuery` instead of `sendMessage`.
885    pub(crate) is_guest_context: bool,
886    /// Active durable execution context for the P1 agent-loop adapter (spec-064 §P1, #5452).
887    ///
888    /// `Some` when `[durable] enabled = true` and `agent_turns = true`. The context is opened
889    /// lazily by [`Agent::ensure_session_durable_ctx`](crate::agent::Agent::ensure_session_durable_ctx)
890    /// the first time a durable-gated call site runs (not eagerly in the builder chain, since the
891    /// real `TaskSupervisor` is only attached later via `with_task_supervisor`), keyed on the
892    /// session's `ConversationId` so every turn replays under the same execution. `None` when
893    /// durable execution is disabled (or construction failed and degraded) — in which case the
894    /// loop runs unmodified.
895    pub(crate) durable_ctx: Option<std::sync::Arc<zeph_durable::DurableContext>>,
896    /// Mirror of `[durable] subagent` config flag (spec-064 §P4, #5452), set unconditionally at
897    /// bootstrap via `AgentBuilder::with_durable_subagent` from `config.durable.subagent`.
898    ///
899    /// When `true` and `durable_ctx` is `Some`, sub-agent spawns are wrapped in a durable
900    /// promise so a resumed parent can replay the child result without re-running the child.
901    pub(crate) durable_subagent: bool,
902    /// Set to `true` for the duration of a turn whose LLM step was replayed from the journal.
903    ///
904    /// Used by `process_single_native_turn` to suppress re-printing already-emitted assistant
905    /// output (spec-064 §INV-001 §15 `RuntimeLayer` double-print suppression). Cleared at the
906    /// start of each turn.
907    pub(crate) durable_turn_replayed: bool,
908    /// `DurableConfig`/db url/cipher stashed cheaply (no I/O) by
909    /// `AgentBuilder::with_durable_agent_turns` when `[durable] enabled = true` and
910    /// `agent_turns = true`. Consumed by `ensure_session_durable_ctx` to lazily open the backend
911    /// and construct `durable_ctx` on the first durable-gated call. `None` when the P1 adapter is
912    /// not configured, in which case `durable_ctx` stays `None` forever (#5452 FR-002).
913    pub(crate) durable_agent_turns_config: Option<zeph_config::DurableConfig>,
914    /// Sibling companion to [`Self::durable_agent_turns_config`]: the `durable.db` connection
915    /// string resolved at bootstrap.
916    pub(crate) durable_agent_turns_db_url: Option<String>,
917    /// Sibling companion to [`Self::durable_agent_turns_config`]: `config.memory.sqlite_path`,
918    /// folded into the P1 `ExecutionId` derivation alongside `ConversationId` so distinct memory
919    /// databases never collide on execution identity even if they ever shared a journal
920    /// `db_url` (#5553).
921    pub(crate) durable_agent_turns_sqlite_path: Option<String>,
922    /// Sibling companion to [`Self::durable_agent_turns_config`]: the AEAD cipher to attach to
923    /// the backend, `None` when `encrypt_payload = false` (development mode only).
924    pub(crate) durable_agent_turns_cipher: Option<std::sync::Arc<dyn zeph_durable::PayloadCipher>>,
925    /// Sibling companion to [`Self::durable_agent_turns_config`]: the control-entry row HMAC key
926    /// (INV-8) to attach to the backend. `None` for a single-user local, non-shared database —
927    /// the documented stance where control entries carry no HMAC.
928    pub(crate) durable_agent_turns_hmac_key: Option<[u8; 32]>,
929    /// Set to `true` the first time `ensure_session_durable_ctx` runs (success or failure) so a
930    /// failed backend construction (missing vault key, disk error) is not retried on every turn.
931    /// Reset to `false` by `reset_durable_ctx_for_conversation_switch` (`/new`, `/conv resume`,
932    /// `/conv fork` — #5452 critic finding S1) so a conversation switch re-derives a fresh
933    /// execution keyed on the new `ConversationId` instead of leaving this latched forever.
934    pub(crate) durable_ctx_init_attempted: bool,
935    /// Writer handle for the P1 adapter's durable backend, flushed on shutdown by
936    /// `flush_durable_writer` (mirrors `services.orchestration.durable_writer` for the P2 adapter).
937    pub(crate) durable_writer: Option<zeph_durable::JournalWriterHandle>,
938    /// [`BlockingHandle`] for the P1 adapter's background `JournalWriter` actor task, aborted on
939    /// shutdown by `flush_durable_writer` (mirrors `services.orchestration.durable_writer_task`).
940    pub(crate) durable_writer_task: Option<zeph_common::task_supervisor::BlockingHandle<()>>,
941    /// Process-exclusivity lock on `durable_ctx`'s `ExecutionId` (INV-15, #6122), held for as long
942    /// as this session drives the execution. Dropping it (on shutdown or
943    /// `reset_durable_ctx_for_conversation_switch`) releases the lock so another process — or a
944    /// later conversation switch in this same process — can open the same `ExecutionId`. `None`
945    /// when `durable_ctx` is `None`, or when the backend could not derive a lock (`:memory:`,
946    /// Postgres — see [`zeph_durable::LocalBackend::open_execution_exclusive`]).
947    pub(crate) durable_execution_lock: Option<zeph_durable::ExecutionLock>,
948    /// When `true`, the system prompt volatile block includes the `CAVEMAN_DIRECTIVE` on every
949    /// turn, instructing the LLM to use ultra-compressed telegraphic output.
950    ///
951    /// Initialized from `config.caveman.default_on` in `builder.rs`. Toggled at runtime by
952    /// `/caveman [on|off]`. Preserved across `/new` (session resets do not clear style flags —
953    /// only process restart returns to `default_on`).
954    pub(crate) caveman_active: bool,
955    /// Durable JSONL event-log dual-writer for this conversation-session (spec-068, #5343).
956    ///
957    /// `Some` when `[session] enabled = true`; the session has minted a
958    /// [`zeph_common::SessionId`](zeph_common::SessionId) and opened its event log. `None` when
959    /// session persistence is disabled — in which case only the `SQLite` `messages` projection
960    /// is written (pre-#5343 behavior).
961    pub(crate) session_sink: Option<std::sync::Arc<zeph_agent_persistence::SessionSink>>,
962    /// `[session]` config snapshot (spec-068, #5343, D-9) — retained (not just consumed at
963    /// construction) so a mid-session `/conv resume`/`/conv fork` swap can locate `data_dir` to
964    /// replay a different session's event log and re-point [`Self::session_sink`] to it.
965    /// `None` when session persistence is disabled.
966    pub(crate) session_persistence_config: Option<zeph_config::SessionConfig>,
967}
968
969/// Extracted hook lists from `[hooks]` config, stored in `SessionState`.
970#[derive(Default)]
971pub(crate) struct HooksConfigSnapshot {
972    /// Hooks fired when working directory changes.
973    pub(crate) cwd_changed: Vec<zeph_config::HookDef>,
974    /// Hooks fired when a watched file changes.
975    pub(crate) file_changed_hooks: Vec<zeph_config::HookDef>,
976    /// Hooks fired when a tool execution is blocked by a `RuntimeLayer::before_tool` check.
977    pub(crate) permission_denied: Vec<zeph_config::HookDef>,
978    /// Hooks fired after each agent turn completes (#3327).
979    ///
980    /// Populated from `HooksConfig::turn_complete` at session construction. Shares the
981    /// `Notifier::should_fire` gate when a notifier is configured; fires on every completion
982    /// when no notifier is present.
983    pub(crate) turn_complete: Vec<zeph_config::HookDef>,
984    /// Hooks fired before each tool execution, matched by tool name pattern.
985    pub(crate) pre_tool_use: Vec<zeph_config::HookMatcher>,
986    /// Hooks fired after each tool execution completes, matched by tool name pattern.
987    pub(crate) post_tool_use: Vec<zeph_config::HookMatcher>,
988}
989
990// Groups message buffering and image staging state.
991pub(crate) struct MessageState {
992    pub(crate) messages: Vec<Message>,
993    // QueuedMessage is pub(super) in message_queue — same visibility as this struct; lint suppressed.
994    #[allow(private_interfaces)]
995    pub(crate) message_queue: VecDeque<QueuedMessage>,
996    /// Image parts staged by `/image` commands, attached to the next user message.
997    pub(crate) pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
998    /// DB row ID of the most recently persisted message. Set by `persist_message`;
999    /// consumed by `push_message` call sites to populate `metadata.db_id` on in-memory messages.
1000    pub(crate) last_persisted_message_id: Option<i64>,
1001    /// DB message IDs pending hide after deferred tool pair summarization.
1002    pub(crate) deferred_db_hide_ids: Vec<i64>,
1003    /// Summary texts pending insertion after deferred tool pair summarization.
1004    pub(crate) deferred_db_summaries: Vec<String>,
1005    /// Set by `AgentBuilder::with_preloaded_messages` (spec-068, #5343) when `messages` was
1006    /// seeded from a durable event-log replay rather than the default single system-prompt
1007    /// message `Agent::new` always seeds. Makes [`super::super::Agent::load_history`]'s
1008    /// `SQLite`-skip guard precise — `messages.is_empty()` is never true at that point in the
1009    /// normal flow (the system prompt is always present), so a plain emptiness check cannot
1010    /// distinguish "already hydrated from the log" from "not yet loaded."
1011    pub(crate) history_preloaded: bool,
1012}
1013
1014impl McpState {
1015    /// Write the **full** `self.tools` set to the shared executor `RwLock`.
1016    ///
1017    /// This is the first of two writers to `shared_tools`. Within a turn this method must run
1018    /// **before** `apply_pruned_tools`, which writes the pruned subset. The normal call order
1019    /// guarantees this: tool-list change events call this method, and pruning runs later inside
1020    /// `rebuild_system_prompt`. See also: `apply_pruned_tools`.
1021    pub(crate) fn sync_executor_tools(&self) {
1022        if let Some(ref shared) = self.shared_tools {
1023            shared.write().clone_from(&self.tools);
1024        }
1025    }
1026
1027    /// Write the **pruned** tool subset to the shared executor `RwLock`.
1028    ///
1029    /// Must only be called **after** `sync_executor_tools` has established the full tool set for
1030    /// the current turn. `self.tools` (the full set) is intentionally **not** modified.
1031    ///
1032    /// This method must **NOT** call `sync_executor_tools` internally — doing so would overwrite
1033    /// the pruned subset with the full set. See also: `sync_executor_tools`.
1034    pub(crate) fn apply_pruned_tools(&self, pruned: Vec<zeph_mcp::McpTool>) {
1035        debug_assert!(
1036            pruned.iter().all(|p| self
1037                .tools
1038                .iter()
1039                .any(|t| t.server_id == p.server_id && t.name == p.name)),
1040            "pruned set must be a subset of self.tools"
1041        );
1042        if let Some(ref shared) = self.shared_tools {
1043            *shared.write() = pruned;
1044        }
1045    }
1046
1047    #[cfg(test)]
1048    pub(crate) fn tool_count(&self) -> usize {
1049        self.tools.len()
1050    }
1051}
1052
1053impl IndexState {
1054    #[tracing::instrument(name = "core.index.fetch_code_rag", skip(self), fields(%query, token_budget))]
1055    pub(crate) async fn fetch_code_rag(
1056        &self,
1057        query: &str,
1058        token_budget: usize,
1059    ) -> Result<Option<String>, crate::agent::error::AgentError> {
1060        let Some(retriever) = &self.retriever else {
1061            return Ok(None);
1062        };
1063        if token_budget == 0 {
1064            return Ok(None);
1065        }
1066
1067        let result = retriever
1068            .retrieve(query, token_budget)
1069            .await
1070            .map_err(|e| crate::agent::error::AgentError::ContextError(format!("{e:#}")))?;
1071        let context_text = zeph_index::retriever::format_as_context(&result);
1072
1073        if context_text.is_empty() {
1074            Ok(None)
1075        } else {
1076            tracing::debug!(
1077                strategy = ?result.strategy,
1078                chunks = result.chunks.len(),
1079                tokens = result.total_tokens,
1080                "code context fetched"
1081            );
1082            Ok(Some(context_text))
1083        }
1084    }
1085}
1086
1087impl DebugState {
1088    pub(crate) fn start_iteration_span(&mut self, iteration_index: usize, text: &str) {
1089        if let Some(ref mut tc) = self.trace_collector {
1090            tc.begin_iteration(iteration_index, text);
1091            self.current_iteration_span_id = tc.current_iteration_span_id(iteration_index);
1092        }
1093    }
1094
1095    pub(crate) fn end_iteration_span(
1096        &mut self,
1097        iteration_index: usize,
1098        status: crate::debug_dump::trace::SpanStatus,
1099    ) {
1100        if let Some(ref mut tc) = self.trace_collector {
1101            tc.end_iteration(iteration_index, status);
1102        }
1103        self.current_iteration_span_id = None;
1104    }
1105
1106    pub(crate) fn switch_format(&mut self, new_format: crate::debug_dump::DumpFormat) {
1107        let was_trace = self.dump_format == crate::debug_dump::DumpFormat::Trace;
1108        let now_trace = new_format == crate::debug_dump::DumpFormat::Trace;
1109
1110        if now_trace
1111            && !was_trace
1112            && let Some(ref dump_dir) = self.dump_dir.clone()
1113        {
1114            let service_name = self.trace_service_name.clone();
1115            let redact = self.trace_redact;
1116            let trace_metadata = self.trace_metadata.clone();
1117            match crate::debug_dump::trace::TracingCollector::new(
1118                dump_dir.as_path(),
1119                &service_name,
1120                trace_metadata,
1121                redact,
1122                None,
1123            ) {
1124                Ok(collector) => {
1125                    self.trace_collector = Some(collector);
1126                }
1127                Err(e) => {
1128                    tracing::warn!(error = %e, "failed to create TracingCollector on format switch");
1129                }
1130            }
1131        }
1132        if was_trace
1133            && !now_trace
1134            && let Some(mut tc) = self.trace_collector.take()
1135        {
1136            // Fire-and-forget: this is a sync fn, and unlike the session-end call site
1137            // (`agent/mod.rs`) a subsequent format switch or session activity follows, so
1138            // there's a real concurrency benefit to not blocking on this write (#6107 critic S1).
1139            let _ = tc.finish();
1140        }
1141
1142        self.dump_format = new_format;
1143    }
1144
1145    pub(crate) fn write_chat_debug_dump(
1146        &self,
1147        dump_id: Option<u32>,
1148        result: &zeph_llm::provider::ChatResponse,
1149        pii_filter: &zeph_sanitizer::pii::PiiFilter,
1150    ) {
1151        let Some((d, id)) = self.debug_dumper.as_ref().zip(dump_id) else {
1152            return;
1153        };
1154        let raw = match result {
1155            zeph_llm::provider::ChatResponse::Text(t) => t.clone(),
1156            zeph_llm::provider::ChatResponse::ToolUse {
1157                text, tool_calls, ..
1158            } => {
1159                let calls = serde_json::to_string_pretty(tool_calls).unwrap_or_default();
1160                format!(
1161                    "{}\n\n---TOOL_CALLS---\n{calls}",
1162                    text.as_deref().unwrap_or("")
1163                )
1164            }
1165            _ => String::new(),
1166        };
1167        let text = if pii_filter.is_enabled() {
1168            pii_filter.scrub(&raw).into_owned()
1169        } else {
1170            raw
1171        };
1172        d.dump_response(id, &text);
1173    }
1174}
1175
1176impl Default for McpState {
1177    fn default() -> Self {
1178        Self {
1179            tools: Vec::new(),
1180            registry: None,
1181            manager: None,
1182            allowed_commands: Vec::new(),
1183            max_dynamic: 10,
1184            elicitation_rx: None,
1185            shared_tools: None,
1186            tool_rx: None,
1187            server_outcomes: Vec::new(),
1188            pruning_cache: zeph_mcp::PruningCache::new(),
1189            pruning_provider: None,
1190            pruning_enabled: false,
1191            pruning_params: zeph_mcp::PruningParams::default(),
1192            semantic_index: None,
1193            discovery_strategy: zeph_mcp::ToolDiscoveryStrategy::default(),
1194            discovery_params: zeph_mcp::DiscoveryParams::default(),
1195            discovery_provider: None,
1196            elicitation_warn_sensitive_fields: true,
1197            pending_semantic_rebuild: false,
1198        }
1199    }
1200}
1201
1202impl Default for IndexState {
1203    fn default() -> Self {
1204        Self {
1205            retriever: None,
1206            repo_map_tokens: 0,
1207            cached_repo_map: None,
1208            repo_map_ttl: std::time::Duration::from_mins(5),
1209        }
1210    }
1211}
1212
1213impl Default for DebugState {
1214    fn default() -> Self {
1215        Self {
1216            debug_dumper: None,
1217            dump_format: crate::debug_dump::DumpFormat::default(),
1218            trace_collector: None,
1219            iteration_counter: 0,
1220            anomaly_detector: None,
1221            reasoning_model_warning: true,
1222            logging_config: crate::config::LoggingConfig::default(),
1223            dump_dir: None,
1224            trace_service_name: String::new(),
1225            trace_redact: true,
1226            trace_metadata: std::collections::HashMap::new(),
1227            current_iteration_span_id: None,
1228        }
1229    }
1230}
1231
1232impl Default for FeedbackState {
1233    fn default() -> Self {
1234        Self {
1235            detector: zeph_agent_feedback::FeedbackDetector::new(0.6),
1236            judge: None,
1237            llm_classifier: None,
1238        }
1239    }
1240}
1241
1242/// Goal lifecycle feature configuration stored in `RuntimeConfig`.
1243#[derive(Debug, Clone)]
1244pub(crate) struct GoalRuntimeConfig {
1245    /// Whether goal tracking is enabled.
1246    pub(crate) enabled: bool,
1247    /// Maximum allowed length (in Unicode chars) of goal text at creation.
1248    pub(crate) max_text_chars: usize,
1249    /// Default token budget for new goals (`None` = unlimited).
1250    pub(crate) default_token_budget: Option<u64>,
1251    /// Whether to inject the active goal block into the volatile system prompt region.
1252    pub(crate) inject_into_system_prompt: bool,
1253    /// Whether autonomous multi-turn execution is permitted.
1254    pub(crate) autonomous_enabled: bool,
1255    /// Maximum turns per autonomous session.
1256    pub(crate) autonomous_max_turns: u32,
1257    /// Provider name for the supervisor LLM call (`None` = use main provider).
1258    pub(crate) supervisor_provider: Option<zeph_config::ProviderName>,
1259    /// Turns between supervisor verification checks.
1260    pub(crate) verify_interval: u32,
1261    /// Timeout for a single supervisor call in seconds.
1262    pub(crate) supervisor_timeout_secs: u64,
1263    /// Consecutive stuck-detection threshold before aborting.
1264    pub(crate) max_stuck_count: u32,
1265    /// Wall-clock timeout in seconds for a single autonomous LLM turn.
1266    pub(crate) autonomous_turn_timeout_secs: u64,
1267    /// Maximum consecutive supervisor verification failures before pausing the session.
1268    pub(crate) max_supervisor_fail_count: u32,
1269}
1270
1271impl Default for GoalRuntimeConfig {
1272    fn default() -> Self {
1273        Self {
1274            enabled: false,
1275            max_text_chars: 2000,
1276            default_token_budget: None,
1277            inject_into_system_prompt: true,
1278            autonomous_enabled: false,
1279            autonomous_max_turns: 20,
1280            supervisor_provider: None,
1281            verify_interval: 5,
1282            supervisor_timeout_secs: 30,
1283            max_stuck_count: 3,
1284            autonomous_turn_timeout_secs: 300,
1285            max_supervisor_fail_count: 3,
1286        }
1287    }
1288}
1289
1290impl Default for RuntimeConfig {
1291    fn default() -> Self {
1292        Self {
1293            security: SecurityConfig::default(),
1294            timeouts: TimeoutConfig::default(),
1295            model_name: String::new(),
1296            active_provider_name: String::new(),
1297            permission_policy: zeph_tools::PermissionPolicy::default(),
1298            redact_credentials: true,
1299            rate_limiter: super::rate_limiter::ToolRateLimiter::new(
1300                super::rate_limiter::RateLimitConfig::default(),
1301            ),
1302            semantic_cache_enabled: false,
1303            semantic_cache_threshold: 0.95,
1304            semantic_cache_max_candidates: 10,
1305            dependency_config: zeph_tools::DependencyConfig::default(),
1306            adversarial_policy_info: None,
1307            spawn_depth: 0,
1308            budget_hint_enabled: true,
1309            channel_skills: zeph_config::ChannelSkillsConfig::default(),
1310            channel_tool_allowlist: None,
1311            loop_min_interval_secs: 5,
1312            layers: Vec::new(),
1313            supervisor_config: crate::config::TaskSupervisorConfig::default(),
1314            recap_config: zeph_config::RecapConfig::default(),
1315            acp_config: zeph_config::AcpConfig::default(),
1316            auto_recap_shown: false,
1317            msg_count_at_resume: 0,
1318            acp_subagent_spawn_fn: None,
1319            channel_type: String::new(),
1320            provider_persistence_enabled: true,
1321            persist_provider_overrides_enabled: true,
1322            restoring_provider: false,
1323            goals: GoalRuntimeConfig::default(),
1324            bare: false,
1325            safe_mode: false,
1326        }
1327    }
1328}
1329
1330impl SessionState {
1331    pub(crate) fn new() -> Self {
1332        Self {
1333            env_context: EnvironmentContext::gather(""),
1334            last_assistant_at: None,
1335            response_cache: None,
1336            parent_tool_use_id: None,
1337            current_turn_intent: None,
1338            status_tx: None,
1339            lsp_hooks: None,
1340            policy_config: None,
1341            hooks_config: HooksConfigSnapshot::default(),
1342            is_guest_context: false,
1343            durable_ctx: None,
1344            durable_subagent: false,
1345            durable_turn_replayed: false,
1346            durable_agent_turns_config: None,
1347            durable_agent_turns_db_url: None,
1348            durable_agent_turns_sqlite_path: None,
1349            durable_agent_turns_cipher: None,
1350            durable_agent_turns_hmac_key: None,
1351            durable_ctx_init_attempted: false,
1352            durable_writer: None,
1353            durable_writer_task: None,
1354            durable_execution_lock: None,
1355            caveman_active: false,
1356            session_sink: None,
1357            session_persistence_config: None,
1358        }
1359    }
1360}
1361
1362impl SkillState {
1363    pub(crate) fn new(
1364        registry: Arc<RwLock<SkillRegistry>>,
1365        matcher: Option<SkillMatcherBackend>,
1366        max_active_skills: usize,
1367        last_skills_prompt: String,
1368    ) -> Self {
1369        Self {
1370            registry,
1371            trust_snapshot: Arc::new(RwLock::new(HashMap::new())),
1372            skill_paths: Vec::new(),
1373            managed_dir: None,
1374            trust_config: crate::config::TrustConfig::default(),
1375            matcher,
1376            max_active_skills,
1377            disambiguation_threshold: 0.20,
1378            min_injection_score: 0.20,
1379            embedding_model: String::new(),
1380            skill_reload_rx: None,
1381            plugin_dirs_supplier: None,
1382            active_skill_names: Vec::new(),
1383            last_skills_prompt,
1384            prompt_mode: crate::config::SkillPromptMode::Auto,
1385            available_custom_secrets: HashMap::new(),
1386            cosine_weight: 0.7,
1387            hybrid_search: true,
1388            bm25_alpha: 0.7,
1389            bm25_index: None,
1390            two_stage_matching: false,
1391            confusability_threshold: 0.0,
1392            rl_head: None,
1393            rl_weight: 0.3,
1394            rl_warmup_updates: 50,
1395            generation_output_dir: None,
1396            query_rewrite_provider_name: String::new(),
1397            generation_provider_name: String::new(),
1398            disambiguate_provider_name: String::new(),
1399            generation_timeout_ms: 60_000,
1400            skill_evaluator: None,
1401            eval_weights: zeph_skills::evaluator::EvaluationWeights::default(),
1402            eval_threshold: 0.60,
1403            group_structured: false,
1404            support_similarity_threshold: 0.50,
1405            semantic_scan: false,
1406            semantic_scan_provider: String::new(),
1407        }
1408    }
1409}
1410
1411/// Interval between periodic `bg_metrics_tick` refreshes (#6279).
1412///
1413/// Short enough that the TUI's background-work status segment feels live during idle time
1414/// between turns, long enough to be a negligible fraction of `BackgroundSupervisor::reap`'s cost.
1415pub(crate) const BG_METRICS_TICK_INTERVAL: Duration = Duration::from_secs(2);
1416
1417impl LifecycleState {
1418    pub(crate) fn new() -> Self {
1419        let (_tx, rx) = watch::channel(false);
1420        Self {
1421            shutdown: rx,
1422            start_time: Instant::now(),
1423            cancel_signal: Arc::new(tokio::sync::Notify::new()),
1424            cancel_token: tokio_util::sync::CancellationToken::new(),
1425            cancel_bridge_handle: None,
1426            config_path: None,
1427            config_reload_rx: None,
1428            plugins_dir: PathBuf::new(),
1429            startup_shell_overlay: ShellOverlaySnapshot::default(),
1430            shell_policy_handle: None,
1431            warmup_ready: None,
1432            update_notify_rx: None,
1433            custom_task_rx: None,
1434            user_loop: None,
1435            last_known_cwd: std::env::current_dir().unwrap_or_default(),
1436            file_changed_rx: None,
1437            file_watcher: None,
1438            supervisor: super::agent_supervisor::BackgroundSupervisor::new(
1439                &crate::config::TaskSupervisorConfig::default(),
1440                None,
1441            ),
1442            bg_metrics_tick: None,
1443            notifier: None,
1444            turn_llm_requests: 0,
1445            last_no_providers_at: None,
1446            pending_background_completions: VecDeque::new(),
1447            background_completion_rx: None,
1448            shell_executor_handle: None,
1449            task_supervisor: Arc::new(zeph_common::TaskSupervisor::new(
1450                tokio_util::sync::CancellationToken::new(),
1451            )),
1452        }
1453    }
1454}
1455
1456impl ProviderState {
1457    pub(crate) fn new(initial_prompt_tokens: u64) -> Self {
1458        Self {
1459            summary_provider: None,
1460            provider_override: None,
1461            judge_provider: None,
1462            probe_provider: None,
1463            compress_provider: None,
1464            cached_prompt_tokens: initial_prompt_tokens,
1465            server_compaction_active: false,
1466            stt: None,
1467            provider_pool: Vec::new(),
1468            provider_config_snapshot: None,
1469        }
1470    }
1471}
1472
1473impl MetricsState {
1474    pub(crate) fn new(token_counter: Arc<zeph_memory::TokenCounter>) -> Self {
1475        Self {
1476            metrics_tx: None,
1477            cost_tracker: None,
1478            token_counter,
1479            extended_context: false,
1480            classifier_metrics: None,
1481            timing_window: std::collections::VecDeque::new(),
1482            pending_timings: crate::metrics::TurnTimings::default(),
1483            histogram_recorder: None,
1484        }
1485    }
1486}
1487
1488impl ExperimentState {
1489    pub(crate) fn new() -> Self {
1490        let (notify_tx, notify_rx) = tokio::sync::mpsc::channel::<String>(4);
1491        Self {
1492            config: crate::config::ExperimentConfig::default(),
1493            cancel: None,
1494            handle: None,
1495            baseline: zeph_experiments::ConfigSnapshot::default(),
1496            eval_provider: None,
1497            notify_rx: Some(notify_rx),
1498            notify_tx,
1499        }
1500    }
1501}
1502
1503pub(super) mod security;
1504pub(super) mod skill;
1505
1506#[cfg(test)]
1507mod tests;