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(super)` — visible only within the `agent` module.
8
9use std::collections::{HashMap, HashSet, VecDeque};
10use std::path::PathBuf;
11use std::sync::{Arc, RwLock};
12use std::time::Instant;
13
14use tokio::sync::{Notify, mpsc, watch};
15use tokio_util::sync::CancellationToken;
16use zeph_llm::any::AnyProvider;
17use zeph_llm::provider::Message;
18use zeph_llm::stt::SpeechToText;
19
20use crate::config::{ProviderEntry, SecurityConfig, SkillPromptMode, TimeoutConfig};
21use crate::config_watcher::ConfigEvent;
22use crate::context::EnvironmentContext;
23use crate::cost::CostTracker;
24use crate::instructions::{InstructionBlock, InstructionEvent, InstructionReloadState};
25use crate::metrics::MetricsSnapshot;
26use crate::vault::Secret;
27use zeph_memory::TokenCounter;
28use zeph_memory::semantic::SemanticMemory;
29use zeph_sanitizer::ContentSanitizer;
30use zeph_sanitizer::quarantine::QuarantinedSummarizer;
31use zeph_skills::matcher::SkillMatcherBackend;
32use zeph_skills::registry::SkillRegistry;
33use zeph_skills::watcher::SkillEvent;
34
35use super::message_queue::QueuedMessage;
36
37pub(crate) struct MemoryState {
38    pub(crate) memory: Option<Arc<SemanticMemory>>,
39    pub(crate) conversation_id: Option<zeph_memory::ConversationId>,
40    pub(crate) history_limit: u32,
41    pub(crate) recall_limit: usize,
42    pub(crate) summarization_threshold: usize,
43    pub(crate) cross_session_score_threshold: f32,
44    pub(crate) autosave_assistant: bool,
45    pub(crate) autosave_min_length: usize,
46    pub(crate) tool_call_cutoff: usize,
47    pub(crate) unsummarized_count: usize,
48    pub(crate) document_config: crate::config::DocumentConfig,
49    pub(crate) graph_config: crate::config::GraphConfig,
50    pub(crate) compression_guidelines_config: zeph_memory::CompressionGuidelinesConfig,
51    pub(crate) shutdown_summary: bool,
52    pub(crate) shutdown_summary_min_messages: usize,
53    pub(crate) shutdown_summary_max_messages: usize,
54    pub(crate) shutdown_summary_timeout_secs: u64,
55    /// When `true`, hard compaction uses `AnchoredSummary` (structured JSON) instead of
56    /// free-form prose. Falls back to prose on any LLM or validation failure.
57    pub(crate) structured_summaries: bool,
58}
59
60pub(crate) struct SkillState {
61    pub(crate) registry: std::sync::Arc<std::sync::RwLock<SkillRegistry>>,
62    pub(crate) skill_paths: Vec<PathBuf>,
63    pub(crate) managed_dir: Option<PathBuf>,
64    pub(crate) trust_config: crate::config::TrustConfig,
65    pub(crate) matcher: Option<SkillMatcherBackend>,
66    pub(crate) max_active_skills: usize,
67    pub(crate) disambiguation_threshold: f32,
68    pub(crate) embedding_model: String,
69    pub(crate) skill_reload_rx: Option<mpsc::Receiver<SkillEvent>>,
70    pub(crate) active_skill_names: Vec<String>,
71    pub(crate) last_skills_prompt: String,
72    pub(crate) prompt_mode: SkillPromptMode,
73    /// Custom secrets available at runtime: key=hyphenated name, value=secret.
74    pub(crate) available_custom_secrets: HashMap<String, Secret>,
75    pub(crate) cosine_weight: f32,
76    pub(crate) hybrid_search: bool,
77    pub(crate) bm25_index: Option<zeph_skills::bm25::Bm25Index>,
78}
79
80pub(crate) struct McpState {
81    pub(crate) tools: Vec<zeph_mcp::McpTool>,
82    pub(crate) registry: Option<zeph_mcp::McpToolRegistry>,
83    pub(crate) manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
84    pub(crate) allowed_commands: Vec<String>,
85    pub(crate) max_dynamic: usize,
86    /// Shared with `McpToolExecutor` so native `tool_use` sees the current tool list.
87    pub(crate) shared_tools: Option<std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>>,
88    /// Receives full flattened tool list after any `tools/list_changed` notification.
89    pub(crate) tool_rx: Option<tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>>,
90}
91
92pub(crate) struct IndexState {
93    pub(crate) retriever: Option<std::sync::Arc<zeph_index::retriever::CodeRetriever>>,
94    pub(crate) repo_map_tokens: usize,
95    pub(crate) cached_repo_map: Option<(String, std::time::Instant)>,
96    pub(crate) repo_map_ttl: std::time::Duration,
97}
98
99pub(crate) struct RuntimeConfig {
100    pub(crate) security: SecurityConfig,
101    pub(crate) timeouts: TimeoutConfig,
102    pub(crate) model_name: String,
103    /// Configured name from `[[llm.providers]]` (the `name` field), set at startup and on
104    /// `/provider` switch. Falls back to the provider type string when empty.
105    pub(crate) active_provider_name: String,
106    pub(crate) permission_policy: zeph_tools::PermissionPolicy,
107    pub(crate) redact_credentials: bool,
108    pub(crate) rate_limiter: super::rate_limiter::ToolRateLimiter,
109    pub(crate) semantic_cache_enabled: bool,
110    pub(crate) semantic_cache_threshold: f32,
111    pub(crate) semantic_cache_max_candidates: u32,
112    /// Dependency config snapshot stored for per-turn boost parameters.
113    pub(crate) dependency_config: zeph_tools::DependencyConfig,
114}
115
116/// Groups feedback detection subsystems: correction detector, judge detector, and LLM classifier.
117pub(crate) struct FeedbackState {
118    pub(crate) detector: super::feedback_detector::FeedbackDetector,
119    pub(crate) judge: Option<super::feedback_detector::JudgeDetector>,
120    /// LLM-backed zero-shot classifier for `DetectorMode::Model`.
121    /// When `Some`, `spawn_judge_correction_check` uses this instead of `JudgeDetector`.
122    pub(crate) llm_classifier: Option<zeph_llm::classifier::llm::LlmClassifier>,
123}
124
125/// Groups security-related subsystems (sanitizer, quarantine, exfiltration guard).
126pub(crate) struct SecurityState {
127    pub(crate) sanitizer: ContentSanitizer,
128    pub(crate) quarantine_summarizer: Option<QuarantinedSummarizer>,
129    pub(crate) exfiltration_guard: zeph_sanitizer::exfiltration::ExfiltrationGuard,
130    pub(crate) flagged_urls: HashSet<String>,
131    /// URLs explicitly provided by the user across all turns in this session.
132    /// Populated from raw user message text; cleared on `/clear`.
133    /// Shared with `UrlGroundingVerifier` to check `fetch`/`web_scrape` calls at dispatch time.
134    pub(crate) user_provided_urls: Arc<RwLock<HashSet<String>>>,
135    pub(crate) pii_filter: zeph_sanitizer::pii::PiiFilter,
136    /// NER classifier for PII detection (`classifiers.ner_model`). When `Some`, the PII path
137    /// runs both regex (`pii_filter`) and NER, then merges spans before redaction.
138    /// `None` when `classifiers` feature is disabled or `classifiers.enabled = false`.
139    #[cfg(feature = "classifiers")]
140    pub(crate) pii_ner_backend: Option<std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>>,
141    /// Per-call timeout for the NER PII classifier in milliseconds.
142    #[cfg(feature = "classifiers")]
143    pub(crate) pii_ner_timeout_ms: u64,
144    pub(crate) memory_validator: zeph_sanitizer::memory_validation::MemoryWriteValidator,
145    /// LLM-based prompt injection pre-screener (opt-in).
146    #[cfg(feature = "guardrail")]
147    pub(crate) guardrail: Option<zeph_sanitizer::guardrail::GuardrailFilter>,
148    /// Post-LLM response verification layer.
149    pub(crate) response_verifier: zeph_sanitizer::response_verifier::ResponseVerifier,
150}
151
152/// Groups debug/diagnostics subsystems (dumper, trace collector, anomaly detector, logging config).
153pub(crate) struct DebugState {
154    pub(crate) debug_dumper: Option<crate::debug_dump::DebugDumper>,
155    pub(crate) dump_format: crate::debug_dump::DumpFormat,
156    pub(crate) trace_collector: Option<crate::debug_dump::trace::TracingCollector>,
157    /// Monotonically increasing counter for `process_user_message` calls.
158    /// Used to key spans in `trace_collector.active_iterations`.
159    pub(crate) iteration_counter: usize,
160    pub(crate) anomaly_detector: Option<zeph_tools::AnomalyDetector>,
161    pub(crate) logging_config: crate::config::LoggingConfig,
162    /// Base dump directory — stored so `/dump-format trace` can create a `TracingCollector` (CR-04).
163    pub(crate) dump_dir: Option<PathBuf>,
164    /// Service name for `TracingCollector` created via runtime format switch (CR-04).
165    pub(crate) trace_service_name: String,
166    /// Whether to redact in `TracingCollector` created via runtime format switch (CR-04).
167    pub(crate) trace_redact: bool,
168    /// Span ID of the currently executing iteration — used by LLM/tool span wiring (CR-01).
169    /// Set to `Some` at the start of `process_user_message`, cleared at end.
170    pub(crate) current_iteration_span_id: Option<[u8; 8]>,
171}
172
173/// Groups agent lifecycle state: shutdown signaling, timing, and I/O notification channels.
174pub(crate) struct LifecycleState {
175    pub(crate) shutdown: watch::Receiver<bool>,
176    pub(crate) start_time: Instant,
177    pub(crate) cancel_signal: Arc<Notify>,
178    pub(crate) cancel_token: CancellationToken,
179    pub(crate) config_path: Option<PathBuf>,
180    pub(crate) config_reload_rx: Option<mpsc::Receiver<ConfigEvent>>,
181    pub(crate) warmup_ready: Option<watch::Receiver<bool>>,
182    pub(crate) update_notify_rx: Option<mpsc::Receiver<String>>,
183    pub(crate) custom_task_rx: Option<mpsc::Receiver<String>>,
184}
185
186/// Minimal config snapshot needed to reconstruct a provider at runtime via `/provider <name>`.
187///
188/// Secrets are stored as plain strings because [`Secret`] intentionally does not implement
189/// `Clone`. They are re-wrapped in `Secret` when passed to `build_provider_for_switch`.
190pub struct ProviderConfigSnapshot {
191    pub claude_api_key: Option<String>,
192    pub openai_api_key: Option<String>,
193    pub gemini_api_key: Option<String>,
194    pub compatible_api_keys: std::collections::HashMap<String, String>,
195    pub llm_request_timeout_secs: u64,
196    pub embedding_model: String,
197}
198
199/// Groups provider-related state: alternate providers, runtime switching, and compaction flags.
200pub(crate) struct ProviderState {
201    pub(crate) summary_provider: Option<AnyProvider>,
202    /// Shared slot for runtime model switching; set by external caller (e.g. ACP).
203    pub(crate) provider_override: Option<Arc<std::sync::RwLock<Option<AnyProvider>>>>,
204    pub(crate) judge_provider: Option<AnyProvider>,
205    /// Dedicated provider for compaction probe LLM calls. Falls back to `summary_provider`
206    /// (or primary) when `None`.
207    pub(crate) probe_provider: Option<AnyProvider>,
208    pub(crate) cached_prompt_tokens: u64,
209    /// Whether the active provider has server-side compaction enabled (Claude compact-2026-01-12).
210    /// When true, client-side compaction is skipped.
211    pub(crate) server_compaction_active: bool,
212    pub(crate) stt: Option<Box<dyn SpeechToText>>,
213    /// Snapshot of `[[llm.providers]]` entries for runtime `/provider` switching.
214    pub(crate) provider_pool: Vec<ProviderEntry>,
215    /// Resolved secrets and timeout settings needed to reconstruct providers at runtime.
216    pub(crate) provider_config_snapshot: Option<ProviderConfigSnapshot>,
217}
218
219/// Groups metrics and cost tracking state.
220pub(crate) struct MetricsState {
221    pub(crate) metrics_tx: Option<watch::Sender<MetricsSnapshot>>,
222    pub(crate) cost_tracker: Option<CostTracker>,
223    pub(crate) token_counter: Arc<TokenCounter>,
224    /// Set to `true` when Claude extended context (`enable_extended_context = true`) is active.
225    /// Read from config at build time, not derived from provider internals.
226    pub(crate) extended_context: bool,
227}
228
229/// Groups task orchestration and subagent state.
230pub(crate) struct OrchestrationState {
231    /// On `OrchestrationState` (not `ProviderState`) because this provider is used exclusively
232    /// by `LlmPlanner` during orchestration, not shared across subsystems.
233    pub(crate) planner_provider: Option<AnyProvider>,
234    /// Graph waiting for `/plan confirm` before execution starts.
235    pub(crate) pending_graph: Option<crate::orchestration::TaskGraph>,
236    /// Cancellation token for the currently executing plan. `None` when no plan is running.
237    /// Created fresh in `handle_plan_confirm()`, cancelled in `handle_plan_cancel()`.
238    ///
239    /// # Known limitation
240    ///
241    /// Token plumbing is ready; the delivery path requires the agent message loop to be
242    /// restructured so `/plan cancel` can be received while `run_scheduler_loop` holds
243    /// `&mut self`. See follow-up issue #1603 (SEC-M34-002).
244    pub(crate) plan_cancel_token: Option<CancellationToken>,
245    /// Manages spawned sub-agents.
246    pub(crate) subagent_manager: Option<crate::subagent::SubAgentManager>,
247    pub(crate) subagent_config: crate::config::SubAgentConfig,
248    pub(crate) orchestration_config: crate::config::OrchestrationConfig,
249    /// Lazily initialized plan template cache. `None` until first use or when
250    /// memory (`SQLite`) is unavailable.
251    pub(crate) plan_cache: Option<crate::orchestration::PlanCache>,
252    /// Goal embedding from the most recent `plan_with_cache()` call. Consumed by
253    /// `finalize_plan_execution()` to cache the completed plan template.
254    pub(crate) pending_goal_embedding: Option<Vec<f32>>,
255}
256
257/// Groups instruction hot-reload state.
258pub(crate) struct InstructionState {
259    pub(crate) blocks: Vec<InstructionBlock>,
260    pub(crate) reload_rx: Option<mpsc::Receiver<InstructionEvent>>,
261    pub(crate) reload_state: Option<InstructionReloadState>,
262}
263
264/// Groups experiment feature state (gated behind `experiments` feature flag).
265pub(crate) struct ExperimentState {
266    #[cfg(feature = "experiments")]
267    pub(crate) config: crate::config::ExperimentConfig,
268    /// Cancellation token for a running experiment session. `Some` means an experiment is active.
269    #[cfg(feature = "experiments")]
270    pub(crate) cancel: Option<tokio_util::sync::CancellationToken>,
271    /// Pre-built config snapshot used as the experiment baseline (agent path).
272    #[cfg(feature = "experiments")]
273    pub(crate) baseline: crate::experiments::ConfigSnapshot,
274    /// Dedicated judge provider for evaluation. When `Some`, the evaluator uses this provider
275    /// instead of the agent's primary provider, eliminating self-judge bias.
276    #[cfg(feature = "experiments")]
277    pub(crate) eval_provider: Option<AnyProvider>,
278    /// Receives completion/error messages from the background experiment engine task.
279    /// Always present so the select! branch compiles unconditionally.
280    pub(crate) notify_rx: Option<tokio::sync::mpsc::Receiver<String>>,
281    /// Sender end paired with `experiment_notify_rx`. Cloned into the background task.
282    #[cfg(feature = "experiments")]
283    pub(crate) notify_tx: tokio::sync::mpsc::Sender<String>,
284}
285
286/// Output of a background subgoal extraction LLM call.
287#[cfg(feature = "context-compression")]
288pub(crate) struct SubgoalExtractionResult {
289    /// Current subgoal the agent is working toward.
290    pub(crate) current: String,
291    /// Just-completed subgoal, if the LLM detected a transition (`COMPLETED:` non-NONE).
292    pub(crate) completed: Option<String>,
293}
294
295/// Groups context-compression feature state (gated behind `context-compression` feature flag).
296#[cfg(feature = "context-compression")]
297pub(crate) struct CompressionState {
298    /// Cached task goal for TaskAware/MIG pruning. Set by `maybe_compact()`,
299    /// invalidated when the last user message hash changes.
300    pub(crate) current_task_goal: Option<String>,
301    /// Hash of the last user message when `current_task_goal` was populated.
302    pub(crate) task_goal_user_msg_hash: Option<u64>,
303    /// Pending background task for goal extraction. Spawned fire-and-forget when the user message
304    /// hash changes; result applied at the start of the next Soft compaction (#1909).
305    pub(crate) pending_task_goal: Option<tokio::task::JoinHandle<Option<String>>>,
306    /// Pending `SideQuest` eviction result from the background LLM call spawned last turn.
307    /// Applied at the START of the next turn before compaction (PERF-1 fix).
308    pub(crate) pending_sidequest_result: Option<tokio::task::JoinHandle<Option<Vec<usize>>>>,
309    /// In-memory subgoal registry for `Subgoal`/`SubgoalMig` pruning strategies (#2022).
310    pub(crate) subgoal_registry: crate::agent::compaction_strategy::SubgoalRegistry,
311    /// Pending background subgoal extraction task.
312    pub(crate) pending_subgoal: Option<tokio::task::JoinHandle<Option<SubgoalExtractionResult>>>,
313    /// Hash of the last user message when subgoal extraction was scheduled.
314    pub(crate) subgoal_user_msg_hash: Option<u64>,
315}
316
317/// Groups per-session I/O and policy state.
318pub(crate) struct SessionState {
319    pub(crate) env_context: EnvironmentContext,
320    pub(crate) response_cache: Option<std::sync::Arc<zeph_memory::ResponseCache>>,
321    /// Parent tool call ID when this agent runs as a subagent inside another agent session.
322    /// Propagated into every `LoopbackEvent::ToolStart` / `ToolOutput` so the IDE can build
323    /// a subagent hierarchy.
324    pub(crate) parent_tool_use_id: Option<String>,
325    /// Optional status channel for sending spinner/status messages to TUI or stderr.
326    pub(crate) status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
327    /// LSP context injection hooks. Fires after native tool execution, injects
328    /// diagnostics/hover notes as `Role::System` messages before the next LLM call.
329    #[cfg(feature = "lsp-context")]
330    pub(crate) lsp_hooks: Option<crate::lsp_hooks::LspHookRunner>,
331    /// Snapshot of the policy config for `/policy` command inspection.
332    #[cfg(feature = "policy-enforcer")]
333    pub(crate) policy_config: Option<zeph_tools::PolicyConfig>,
334}
335
336// Groups message buffering and image staging state.
337pub(crate) struct MessageState {
338    pub(crate) messages: Vec<Message>,
339    // QueuedMessage is pub(super) in message_queue — same visibility as this struct; lint suppressed.
340    #[allow(private_interfaces)]
341    pub(crate) message_queue: VecDeque<QueuedMessage>,
342    /// Image parts staged by `/image` commands, attached to the next user message.
343    pub(crate) pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
344}
345
346#[cfg(test)]
347mod tests;