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::file_watcher::FileChangedEvent;
25use crate::instructions::{InstructionBlock, InstructionEvent, InstructionReloadState};
26use crate::metrics::MetricsSnapshot;
27use crate::vault::Secret;
28use zeph_config;
29use zeph_memory::TokenCounter;
30use zeph_memory::semantic::SemanticMemory;
31use zeph_sanitizer::ContentSanitizer;
32use zeph_sanitizer::quarantine::QuarantinedSummarizer;
33use zeph_skills::matcher::SkillMatcherBackend;
34use zeph_skills::registry::SkillRegistry;
35use zeph_skills::watcher::SkillEvent;
36
37use super::message_queue::QueuedMessage;
38
39pub(crate) struct MemoryState {
40    pub(crate) memory: Option<Arc<SemanticMemory>>,
41    pub(crate) conversation_id: Option<zeph_memory::ConversationId>,
42    pub(crate) history_limit: u32,
43    pub(crate) recall_limit: usize,
44    pub(crate) summarization_threshold: usize,
45    pub(crate) cross_session_score_threshold: f32,
46    pub(crate) autosave_assistant: bool,
47    pub(crate) autosave_min_length: usize,
48    pub(crate) tool_call_cutoff: usize,
49    pub(crate) unsummarized_count: usize,
50    pub(crate) document_config: crate::config::DocumentConfig,
51    pub(crate) graph_config: crate::config::GraphConfig,
52    pub(crate) compression_guidelines_config: zeph_memory::CompressionGuidelinesConfig,
53    pub(crate) shutdown_summary: bool,
54    pub(crate) shutdown_summary_min_messages: usize,
55    pub(crate) shutdown_summary_max_messages: usize,
56    pub(crate) shutdown_summary_timeout_secs: u64,
57    /// When `true`, hard compaction uses `AnchoredSummary` (structured JSON) instead of
58    /// free-form prose. Falls back to prose on any LLM or validation failure.
59    pub(crate) structured_summaries: bool,
60    /// Top-1 semantic recall score from the most recent `prepare_context` cycle.
61    /// Used by MAR (Memory-Augmented Routing) to bias the bandit toward cheap providers
62    /// when memory confidence is high. Reset to `None` at the start of each turn.
63    pub(crate) last_recall_confidence: Option<f32>,
64    /// Session digest configuration (#2289).
65    pub(crate) digest_config: crate::config::DigestConfig,
66    /// Cached session digest text and its token count, loaded at session start.
67    pub(crate) cached_session_digest: Option<(String, usize)>,
68    /// Context assembly strategy (#2288).
69    pub(crate) context_strategy: crate::config::ContextStrategy,
70    /// Turn threshold for `Adaptive` strategy crossover (#2288).
71    pub(crate) crossover_turn_threshold: u32,
72    /// D-MEM RPE router. `Some` when `graph_config.rpe.enabled = true`.
73    /// Protected by `std::sync::Mutex` for non-async access from `maybe_spawn_graph_extraction`.
74    pub(crate) rpe_router: Option<std::sync::Mutex<zeph_memory::RpeRouter>>,
75    /// Goal text for the current user turn, derived from raw user input (#2483).
76    /// Passed to A-MAC admission control to enable goal-conditioned write gating.
77    /// Reset at the start of each user turn. `None` only before the first user message.
78    pub(crate) goal_text: Option<String>,
79    /// Persona memory configuration (#2461).
80    pub(crate) persona_config: zeph_config::PersonaConfig,
81}
82
83pub(crate) struct SkillState {
84    pub(crate) registry: std::sync::Arc<std::sync::RwLock<SkillRegistry>>,
85    pub(crate) skill_paths: Vec<PathBuf>,
86    pub(crate) managed_dir: Option<PathBuf>,
87    pub(crate) trust_config: crate::config::TrustConfig,
88    pub(crate) matcher: Option<SkillMatcherBackend>,
89    pub(crate) max_active_skills: usize,
90    pub(crate) disambiguation_threshold: f32,
91    pub(crate) min_injection_score: f32,
92    pub(crate) embedding_model: String,
93    pub(crate) skill_reload_rx: Option<mpsc::Receiver<SkillEvent>>,
94    pub(crate) active_skill_names: Vec<String>,
95    pub(crate) last_skills_prompt: String,
96    pub(crate) prompt_mode: SkillPromptMode,
97    /// Custom secrets available at runtime: key=hyphenated name, value=secret.
98    pub(crate) available_custom_secrets: HashMap<String, Secret>,
99    pub(crate) cosine_weight: f32,
100    pub(crate) hybrid_search: bool,
101    pub(crate) bm25_index: Option<zeph_skills::bm25::Bm25Index>,
102    pub(crate) two_stage_matching: bool,
103    /// Threshold for confusability warnings (0.0 = disabled).
104    pub(crate) confusability_threshold: f32,
105    /// `SkillOrchestra` RL routing head. `Some` when `rl_routing_enabled = true` and
106    /// weights are loaded or initialized. `None` when RL routing is disabled.
107    pub(crate) rl_head: Option<zeph_skills::rl_head::RoutingHead>,
108    /// Blend weight for RL routing: `final = (1-rl_weight)*cosine + rl_weight*rl_score`.
109    pub(crate) rl_weight: f32,
110    /// Skip RL blending for the first N updates (cold-start warmup).
111    pub(crate) rl_warmup_updates: u32,
112    /// Directory where `/skill create` writes generated skills.
113    /// Defaults to `managed_dir` if `None`.
114    pub(crate) generation_output_dir: Option<std::path::PathBuf>,
115    /// Provider name for `/skill create` generation. Empty = primary.
116    pub(crate) generation_provider_name: String,
117}
118
119pub(crate) struct McpState {
120    pub(crate) tools: Vec<zeph_mcp::McpTool>,
121    pub(crate) registry: Option<zeph_mcp::McpToolRegistry>,
122    pub(crate) manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
123    pub(crate) allowed_commands: Vec<String>,
124    pub(crate) max_dynamic: usize,
125    /// Receives elicitation requests from MCP server handlers during tool execution.
126    /// When `Some`, the agent loop must process these concurrently with tool result awaiting
127    /// to avoid deadlock (tool result waits for elicitation, elicitation waits for agent loop).
128    pub(crate) elicitation_rx: Option<tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>>,
129    /// Shared with `McpToolExecutor` so native `tool_use` sees the current tool list.
130    ///
131    /// Two methods write to this `RwLock` — ordering matters:
132    /// - `sync_mcp_executor_tools()`: writes the **full** `self.mcp.tools` set.
133    /// - `apply_pruned_mcp_tools()`: writes the **pruned** subset (used after pruning).
134    ///
135    /// Within a turn, `sync_mcp_executor_tools` must always run **before**
136    /// `apply_pruned_mcp_tools`.  The normal call order guarantees this: tool-list
137    /// change events call `sync_mcp_executor_tools` (inside `check_tool_refresh`,
138    /// `handle_mcp_add`, `handle_mcp_remove`), and pruning runs later inside
139    /// `rebuild_system_prompt`.  See also: `apply_pruned_mcp_tools`.
140    pub(crate) shared_tools: Option<std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>>,
141    /// Receives full flattened tool list after any `tools/list_changed` notification.
142    pub(crate) tool_rx: Option<tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>>,
143    /// Per-server connection outcomes from the initial `connect_all()` call.
144    pub(crate) server_outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
145    /// Per-message cache for MCP tool pruning results (#2298).
146    ///
147    /// Reset at the start of each user turn and whenever the MCP tool list
148    /// changes (via `tools/list_changed`, `/mcp add`, or `/mcp remove`).
149    pub(crate) pruning_cache: zeph_mcp::PruningCache,
150    /// Dedicated provider for MCP tool pruning LLM calls.
151    ///
152    /// `None` means fall back to the agent's primary provider.
153    /// Resolved from `[[llm.providers]]` at build time using `pruning_provider`
154    /// from `ToolPruningConfig`.
155    pub(crate) pruning_provider: Option<zeph_llm::any::AnyProvider>,
156    /// Whether MCP tool pruning is enabled.  Mirrors `ToolPruningConfig::enabled`.
157    pub(crate) pruning_enabled: bool,
158    /// Pruning parameters snapshot.  Derived from `ToolPruningConfig` at build time.
159    pub(crate) pruning_params: zeph_mcp::PruningParams,
160    /// Pre-computed semantic tool index for embedding-based discovery (#2321).
161    ///
162    /// Built at connect time via `rebuild_semantic_index()`, rebuilt on tool list change.
163    /// `None` when strategy is not `Embedding` or when build failed (fallback to all tools).
164    pub(crate) semantic_index: Option<zeph_mcp::SemanticToolIndex>,
165    /// Active discovery strategy and parameters.  Derived from `ToolDiscoveryConfig`.
166    pub(crate) discovery_strategy: zeph_mcp::ToolDiscoveryStrategy,
167    /// Discovery parameters snapshot.  Derived from `ToolDiscoveryConfig` at build time.
168    pub(crate) discovery_params: zeph_mcp::DiscoveryParams,
169    /// Dedicated embedding provider for tool discovery.  `None` = fall back to the
170    /// agent's primary embedding provider.
171    pub(crate) discovery_provider: Option<zeph_llm::any::AnyProvider>,
172    /// When `true`, show a security warning before prompting for fields whose names
173    /// match sensitive patterns (password, token, secret, key, credential, etc.).
174    pub(crate) elicitation_warn_sensitive_fields: bool,
175}
176
177pub(crate) struct IndexState {
178    pub(crate) retriever: Option<std::sync::Arc<zeph_index::retriever::CodeRetriever>>,
179    pub(crate) repo_map_tokens: usize,
180    pub(crate) cached_repo_map: Option<(String, std::time::Instant)>,
181    pub(crate) repo_map_ttl: std::time::Duration,
182}
183
184/// Snapshot of adversarial policy gate configuration for status display.
185#[derive(Debug, Clone)]
186pub struct AdversarialPolicyInfo {
187    pub provider: String,
188    pub policy_count: usize,
189    pub fail_open: bool,
190}
191
192pub(crate) struct RuntimeConfig {
193    pub(crate) security: SecurityConfig,
194    pub(crate) timeouts: TimeoutConfig,
195    pub(crate) model_name: String,
196    /// Configured name from `[[llm.providers]]` (the `name` field), set at startup and on
197    /// `/provider` switch. Falls back to the provider type string when empty.
198    pub(crate) active_provider_name: String,
199    pub(crate) permission_policy: zeph_tools::PermissionPolicy,
200    pub(crate) redact_credentials: bool,
201    pub(crate) rate_limiter: super::rate_limiter::ToolRateLimiter,
202    pub(crate) semantic_cache_enabled: bool,
203    pub(crate) semantic_cache_threshold: f32,
204    pub(crate) semantic_cache_max_candidates: u32,
205    /// Dependency config snapshot stored for per-turn boost parameters.
206    pub(crate) dependency_config: zeph_tools::DependencyConfig,
207    /// Adversarial policy gate runtime info for /status display.
208    pub(crate) adversarial_policy_info: Option<AdversarialPolicyInfo>,
209    /// Current spawn depth of this agent instance (0 = top-level, 1 = first sub-agent, etc.).
210    /// Used by `build_spawn_context()` to propagate depth to children.
211    pub(crate) spawn_depth: u32,
212    /// Inject `<budget>` XML into the volatile system prompt section (#2267).
213    pub(crate) budget_hint_enabled: bool,
214    /// Per-channel skill allowlist. Skills not matching the allowlist are excluded from the
215    /// prompt. An empty `allowed` list means all skills are permitted (default).
216    pub(crate) channel_skills: zeph_config::ChannelSkillsConfig,
217}
218
219/// Groups feedback detection subsystems: correction detector, judge detector, and LLM classifier.
220pub(crate) struct FeedbackState {
221    pub(crate) detector: super::feedback_detector::FeedbackDetector,
222    pub(crate) judge: Option<super::feedback_detector::JudgeDetector>,
223    /// LLM-backed zero-shot classifier for `DetectorMode::Model`.
224    /// When `Some`, `spawn_judge_correction_check` uses this instead of `JudgeDetector`.
225    pub(crate) llm_classifier: Option<zeph_llm::classifier::llm::LlmClassifier>,
226}
227
228/// Groups security-related subsystems (sanitizer, quarantine, exfiltration guard).
229pub(crate) struct SecurityState {
230    pub(crate) sanitizer: ContentSanitizer,
231    pub(crate) quarantine_summarizer: Option<QuarantinedSummarizer>,
232    /// Whether this agent session is serving an ACP client.
233    /// When `true` and `mcp_to_acp_boundary` is enabled, MCP tool results
234    /// receive unconditional quarantine and cross-boundary audit logging.
235    pub(crate) is_acp_session: bool,
236    pub(crate) exfiltration_guard: zeph_sanitizer::exfiltration::ExfiltrationGuard,
237    pub(crate) flagged_urls: HashSet<String>,
238    /// URLs explicitly provided by the user across all turns in this session.
239    /// Populated from raw user message text; cleared on `/clear`.
240    /// Shared with `UrlGroundingVerifier` to check `fetch`/`web_scrape` calls at dispatch time.
241    pub(crate) user_provided_urls: Arc<RwLock<HashSet<String>>>,
242    pub(crate) pii_filter: zeph_sanitizer::pii::PiiFilter,
243    /// NER classifier for PII detection (`classifiers.ner_model`). When `Some`, the PII path
244    /// runs both regex (`pii_filter`) and NER, then merges spans before redaction.
245    /// `None` when `classifiers` feature is disabled or `classifiers.enabled = false`.
246    #[cfg(feature = "classifiers")]
247    pub(crate) pii_ner_backend: Option<std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>>,
248    /// Per-call timeout for the NER PII classifier in milliseconds.
249    #[cfg(feature = "classifiers")]
250    pub(crate) pii_ner_timeout_ms: u64,
251    /// Maximum number of bytes passed to the NER PII classifier per call.
252    ///
253    /// Large tool outputs (e.g. `search_code`) can produce 150+ `DeBERTa` chunks and exceed
254    /// the per-call timeout. Input is truncated at a valid UTF-8 boundary before classification.
255    #[cfg(feature = "classifiers")]
256    pub(crate) pii_ner_max_chars: usize,
257    /// Circuit-breaker threshold: number of consecutive timeouts before NER is disabled.
258    /// `0` means the circuit breaker is disabled (NER is always attempted).
259    #[cfg(feature = "classifiers")]
260    pub(crate) pii_ner_circuit_breaker_threshold: u32,
261    /// Number of consecutive NER timeouts observed since the last successful call.
262    #[cfg(feature = "classifiers")]
263    pub(crate) pii_ner_consecutive_timeouts: u32,
264    /// Set to `true` when the circuit breaker trips. NER is skipped for the rest of the session.
265    #[cfg(feature = "classifiers")]
266    pub(crate) pii_ner_tripped: bool,
267    pub(crate) memory_validator: zeph_sanitizer::memory_validation::MemoryWriteValidator,
268    /// LLM-based prompt injection pre-screener (opt-in).
269    pub(crate) guardrail: Option<zeph_sanitizer::guardrail::GuardrailFilter>,
270    /// Post-LLM response verification layer.
271    pub(crate) response_verifier: zeph_sanitizer::response_verifier::ResponseVerifier,
272    /// Temporal causal IPI analyzer (opt-in, disabled when `None`).
273    pub(crate) causal_analyzer: Option<zeph_sanitizer::causal_ipi::TurnCausalAnalyzer>,
274}
275
276/// Groups debug/diagnostics subsystems (dumper, trace collector, anomaly detector, logging config).
277pub(crate) struct DebugState {
278    pub(crate) debug_dumper: Option<crate::debug_dump::DebugDumper>,
279    pub(crate) dump_format: crate::debug_dump::DumpFormat,
280    pub(crate) trace_collector: Option<crate::debug_dump::trace::TracingCollector>,
281    /// Monotonically increasing counter for `process_user_message` calls.
282    /// Used to key spans in `trace_collector.active_iterations`.
283    pub(crate) iteration_counter: usize,
284    pub(crate) anomaly_detector: Option<zeph_tools::AnomalyDetector>,
285    /// Whether to emit `reasoning_amplification` warnings for quality failures from reasoning
286    /// models. Mirrors `AnomalyConfig::reasoning_model_warning`. Default: `true`.
287    pub(crate) reasoning_model_warning: bool,
288    pub(crate) logging_config: crate::config::LoggingConfig,
289    /// Base dump directory — stored so `/dump-format trace` can create a `TracingCollector` (CR-04).
290    pub(crate) dump_dir: Option<PathBuf>,
291    /// Service name for `TracingCollector` created via runtime format switch (CR-04).
292    pub(crate) trace_service_name: String,
293    /// Whether to redact in `TracingCollector` created via runtime format switch (CR-04).
294    pub(crate) trace_redact: bool,
295    /// Span ID of the currently executing iteration — used by LLM/tool span wiring (CR-01).
296    /// Set to `Some` at the start of `process_user_message`, cleared at end.
297    pub(crate) current_iteration_span_id: Option<[u8; 8]>,
298}
299
300/// Groups agent lifecycle state: shutdown signaling, timing, and I/O notification channels.
301pub(crate) struct LifecycleState {
302    pub(crate) shutdown: watch::Receiver<bool>,
303    pub(crate) start_time: Instant,
304    pub(crate) cancel_signal: Arc<Notify>,
305    pub(crate) cancel_token: CancellationToken,
306    pub(crate) config_path: Option<PathBuf>,
307    pub(crate) config_reload_rx: Option<mpsc::Receiver<ConfigEvent>>,
308    pub(crate) warmup_ready: Option<watch::Receiver<bool>>,
309    pub(crate) update_notify_rx: Option<mpsc::Receiver<String>>,
310    pub(crate) custom_task_rx: Option<mpsc::Receiver<String>>,
311    /// Last known process cwd. Compared after each tool call to detect changes.
312    pub(crate) last_known_cwd: PathBuf,
313    /// Receiver for file-change events from `FileChangeWatcher`. `None` when no paths configured.
314    pub(crate) file_changed_rx: Option<mpsc::Receiver<FileChangedEvent>>,
315    /// Keeps the `FileChangeWatcher` alive for the agent's lifetime. Dropping it aborts the watcher task.
316    pub(crate) file_watcher: Option<crate::file_watcher::FileChangeWatcher>,
317}
318
319/// Minimal config snapshot needed to reconstruct a provider at runtime via `/provider <name>`.
320///
321/// Secrets are stored as plain strings because [`Secret`] intentionally does not implement
322/// `Clone`. They are re-wrapped in `Secret` when passed to `build_provider_for_switch`.
323pub struct ProviderConfigSnapshot {
324    pub claude_api_key: Option<String>,
325    pub openai_api_key: Option<String>,
326    pub gemini_api_key: Option<String>,
327    pub compatible_api_keys: std::collections::HashMap<String, String>,
328    pub llm_request_timeout_secs: u64,
329    pub embedding_model: String,
330}
331
332/// Groups provider-related state: alternate providers, runtime switching, and compaction flags.
333pub(crate) struct ProviderState {
334    pub(crate) summary_provider: Option<AnyProvider>,
335    /// Shared slot for runtime model switching; set by external caller (e.g. ACP).
336    pub(crate) provider_override: Option<Arc<std::sync::RwLock<Option<AnyProvider>>>>,
337    pub(crate) judge_provider: Option<AnyProvider>,
338    /// Dedicated provider for compaction probe LLM calls. Falls back to `summary_provider`
339    /// (or primary) when `None`.
340    pub(crate) probe_provider: Option<AnyProvider>,
341    /// Dedicated provider for `compress_context` LLM calls (#2356).
342    /// Falls back to the primary provider when `None`.
343    pub(crate) compress_provider: Option<AnyProvider>,
344    pub(crate) cached_prompt_tokens: u64,
345    /// Whether the active provider has server-side compaction enabled (Claude compact-2026-01-12).
346    /// When true, client-side compaction is skipped.
347    pub(crate) server_compaction_active: bool,
348    pub(crate) stt: Option<Box<dyn SpeechToText>>,
349    /// Snapshot of `[[llm.providers]]` entries for runtime `/provider` switching.
350    pub(crate) provider_pool: Vec<ProviderEntry>,
351    /// Resolved secrets and timeout settings needed to reconstruct providers at runtime.
352    pub(crate) provider_config_snapshot: Option<ProviderConfigSnapshot>,
353}
354
355/// Groups metrics and cost tracking state.
356pub(crate) struct MetricsState {
357    pub(crate) metrics_tx: Option<watch::Sender<MetricsSnapshot>>,
358    pub(crate) cost_tracker: Option<CostTracker>,
359    pub(crate) token_counter: Arc<TokenCounter>,
360    /// Set to `true` when Claude extended context (`enable_extended_context = true`) is active.
361    /// Read from config at build time, not derived from provider internals.
362    pub(crate) extended_context: bool,
363    /// Shared classifier latency ring buffer. Populated by `ContentSanitizer` (injection, PII)
364    /// and `LlmClassifier` (feedback). `None` when classifiers are not configured.
365    pub(crate) classifier_metrics: Option<Arc<zeph_llm::ClassifierMetrics>>,
366}
367
368/// Groups task orchestration and subagent state.
369pub(crate) struct OrchestrationState {
370    /// On `OrchestrationState` (not `ProviderState`) because this provider is used exclusively
371    /// by `LlmPlanner` during orchestration, not shared across subsystems.
372    pub(crate) planner_provider: Option<AnyProvider>,
373    /// Provider for `PlanVerifier` LLM calls. `None` falls back to the primary provider.
374    /// On `OrchestrationState` for the same reason as `planner_provider`.
375    pub(crate) verify_provider: Option<AnyProvider>,
376    /// Graph waiting for `/plan confirm` before execution starts.
377    pub(crate) pending_graph: Option<crate::orchestration::TaskGraph>,
378    /// Cancellation token for the currently executing plan. `None` when no plan is running.
379    /// Created fresh in `handle_plan_confirm()`, cancelled in `handle_plan_cancel()`.
380    ///
381    /// # Known limitation
382    ///
383    /// Token plumbing is ready; the delivery path requires the agent message loop to be
384    /// restructured so `/plan cancel` can be received while `run_scheduler_loop` holds
385    /// `&mut self`. See follow-up issue #1603 (SEC-M34-002).
386    pub(crate) plan_cancel_token: Option<CancellationToken>,
387    /// Manages spawned sub-agents.
388    pub(crate) subagent_manager: Option<crate::subagent::SubAgentManager>,
389    pub(crate) subagent_config: crate::config::SubAgentConfig,
390    pub(crate) orchestration_config: crate::config::OrchestrationConfig,
391    /// Lazily initialized plan template cache. `None` until first use or when
392    /// memory (`SQLite`) is unavailable.
393    pub(crate) plan_cache: Option<crate::orchestration::PlanCache>,
394    /// Goal embedding from the most recent `plan_with_cache()` call. Consumed by
395    /// `finalize_plan_execution()` to cache the completed plan template.
396    pub(crate) pending_goal_embedding: Option<Vec<f32>>,
397}
398
399/// Groups instruction hot-reload state.
400pub(crate) struct InstructionState {
401    pub(crate) blocks: Vec<InstructionBlock>,
402    pub(crate) reload_rx: Option<mpsc::Receiver<InstructionEvent>>,
403    pub(crate) reload_state: Option<InstructionReloadState>,
404}
405
406/// Groups experiment feature state (gated behind `experiments` feature flag).
407pub(crate) struct ExperimentState {
408    pub(crate) config: crate::config::ExperimentConfig,
409    /// Cancellation token for a running experiment session. `Some` means an experiment is active.
410    pub(crate) cancel: Option<tokio_util::sync::CancellationToken>,
411    /// Pre-built config snapshot used as the experiment baseline (agent path).
412    pub(crate) baseline: crate::experiments::ConfigSnapshot,
413    /// Dedicated judge provider for evaluation. When `Some`, the evaluator uses this provider
414    /// instead of the agent's primary provider, eliminating self-judge bias.
415    pub(crate) eval_provider: Option<AnyProvider>,
416    /// Receives completion/error messages from the background experiment engine task.
417    /// Always present so the select! branch compiles unconditionally.
418    pub(crate) notify_rx: Option<tokio::sync::mpsc::Receiver<String>>,
419    /// Sender end paired with `experiment_notify_rx`. Cloned into the background task.
420    pub(crate) notify_tx: tokio::sync::mpsc::Sender<String>,
421}
422
423/// Output of a background subgoal extraction LLM call.
424pub(crate) struct SubgoalExtractionResult {
425    /// Current subgoal the agent is working toward.
426    pub(crate) current: String,
427    /// Just-completed subgoal, if the LLM detected a transition (`COMPLETED:` non-NONE).
428    pub(crate) completed: Option<String>,
429}
430
431/// Groups context-compression feature state (gated behind `context-compression` feature flag).
432pub(crate) struct CompressionState {
433    /// Cached task goal for TaskAware/MIG pruning. Set by `maybe_compact()`,
434    /// invalidated when the last user message hash changes.
435    pub(crate) current_task_goal: Option<String>,
436    /// Hash of the last user message when `current_task_goal` was populated.
437    pub(crate) task_goal_user_msg_hash: Option<u64>,
438    /// Pending background task for goal extraction. Spawned fire-and-forget when the user message
439    /// hash changes; result applied at the start of the next Soft compaction (#1909).
440    pub(crate) pending_task_goal: Option<tokio::task::JoinHandle<Option<String>>>,
441    /// Pending `SideQuest` eviction result from the background LLM call spawned last turn.
442    /// Applied at the START of the next turn before compaction (PERF-1 fix).
443    pub(crate) pending_sidequest_result: Option<tokio::task::JoinHandle<Option<Vec<usize>>>>,
444    /// In-memory subgoal registry for `Subgoal`/`SubgoalMig` pruning strategies (#2022).
445    pub(crate) subgoal_registry: crate::agent::compaction_strategy::SubgoalRegistry,
446    /// Pending background subgoal extraction task.
447    pub(crate) pending_subgoal: Option<tokio::task::JoinHandle<Option<SubgoalExtractionResult>>>,
448    /// Hash of the last user message when subgoal extraction was scheduled.
449    pub(crate) subgoal_user_msg_hash: Option<u64>,
450}
451
452/// Groups per-session I/O and policy state.
453pub(crate) struct SessionState {
454    pub(crate) env_context: EnvironmentContext,
455    pub(crate) response_cache: Option<std::sync::Arc<zeph_memory::ResponseCache>>,
456    /// Parent tool call ID when this agent runs as a subagent inside another agent session.
457    /// Propagated into every `LoopbackEvent::ToolStart` / `ToolOutput` so the IDE can build
458    /// a subagent hierarchy.
459    pub(crate) parent_tool_use_id: Option<String>,
460    /// Optional status channel for sending spinner/status messages to TUI or stderr.
461    pub(crate) status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
462    /// LSP context injection hooks. Fires after native tool execution, injects
463    /// diagnostics/hover notes as `Role::System` messages before the next LLM call.
464    pub(crate) lsp_hooks: Option<crate::lsp_hooks::LspHookRunner>,
465    /// Snapshot of the policy config for `/policy` command inspection.
466    pub(crate) policy_config: Option<zeph_tools::PolicyConfig>,
467    /// `CwdChanged` hook definitions extracted from `[hooks]` config.
468    pub(crate) hooks_config: HooksConfigSnapshot,
469}
470
471/// Extracted hook lists from `[hooks]` config, stored in `SessionState`.
472#[derive(Default)]
473pub(crate) struct HooksConfigSnapshot {
474    /// Hooks fired when working directory changes.
475    pub(crate) cwd_changed: Vec<zeph_config::HookDef>,
476    /// Hooks fired when a watched file changes.
477    pub(crate) file_changed_hooks: Vec<zeph_config::HookDef>,
478}
479
480// Groups message buffering and image staging state.
481pub(crate) struct MessageState {
482    pub(crate) messages: Vec<Message>,
483    // QueuedMessage is pub(super) in message_queue — same visibility as this struct; lint suppressed.
484    #[allow(private_interfaces)]
485    pub(crate) message_queue: VecDeque<QueuedMessage>,
486    /// Image parts staged by `/image` commands, attached to the next user message.
487    pub(crate) pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
488}
489
490#[cfg(test)]
491mod tests;