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