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