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