pub(crate) mod compaction;
pub(crate) mod extraction;
pub(crate) mod persistence;
pub(crate) mod subsystems;
pub(crate) use self::compaction::MemoryCompactionState;
pub(crate) use self::extraction::MemoryExtractionState;
pub(crate) use self::persistence::MemoryPersistenceState;
pub(crate) use self::subsystems::MemorySubsystemState;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use std::time::Instant;
use tokio::sync::{Notify, mpsc, watch};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use zeph_llm::any::AnyProvider;
use zeph_llm::provider::Message;
use zeph_llm::stt::SpeechToText;
use crate::config::{ProviderEntry, SecurityConfig, SkillPromptMode, TimeoutConfig};
use crate::config_watcher::ConfigEvent;
use crate::context::EnvironmentContext;
use crate::cost::CostTracker;
use crate::file_watcher::FileChangedEvent;
use crate::instructions::{InstructionBlock, InstructionEvent, InstructionReloadState};
use crate::metrics::MetricsSnapshot;
use crate::vault::Secret;
use zeph_config;
use zeph_memory::TokenCounter;
use zeph_sanitizer::ContentSanitizer;
use zeph_sanitizer::quarantine::QuarantinedSummarizer;
use zeph_skills::matcher::SkillMatcherBackend;
use zeph_skills::registry::SkillRegistry;
use zeph_skills::watcher::SkillEvent;
use super::message_queue::QueuedMessage;
#[derive(Default)]
pub(crate) struct MemoryState {
pub(crate) persistence: MemoryPersistenceState,
pub(crate) compaction: MemoryCompactionState,
pub(crate) extraction: MemoryExtractionState,
pub(crate) subsystems: MemorySubsystemState,
}
pub(crate) struct SkillState {
pub(crate) registry: Arc<RwLock<SkillRegistry>>,
pub(crate) skill_paths: Vec<PathBuf>,
pub(crate) managed_dir: Option<PathBuf>,
pub(crate) trust_config: crate::config::TrustConfig,
pub(crate) matcher: Option<SkillMatcherBackend>,
pub(crate) max_active_skills: usize,
pub(crate) disambiguation_threshold: f32,
pub(crate) min_injection_score: f32,
pub(crate) embedding_model: String,
pub(crate) skill_reload_rx: Option<mpsc::Receiver<SkillEvent>>,
pub(crate) active_skill_names: Vec<String>,
pub(crate) last_skills_prompt: String,
pub(crate) prompt_mode: SkillPromptMode,
pub(crate) available_custom_secrets: HashMap<String, Secret>,
pub(crate) cosine_weight: f32,
pub(crate) hybrid_search: bool,
pub(crate) bm25_index: Option<zeph_skills::bm25::Bm25Index>,
pub(crate) two_stage_matching: bool,
pub(crate) confusability_threshold: f32,
pub(crate) rl_head: Option<zeph_skills::rl_head::RoutingHead>,
pub(crate) rl_weight: f32,
pub(crate) rl_warmup_updates: u32,
pub(crate) generation_output_dir: Option<std::path::PathBuf>,
pub(crate) generation_provider_name: String,
}
pub(crate) struct McpState {
pub(crate) tools: Vec<zeph_mcp::McpTool>,
pub(crate) registry: Option<zeph_mcp::McpToolRegistry>,
pub(crate) manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
pub(crate) allowed_commands: Vec<String>,
pub(crate) max_dynamic: usize,
pub(crate) elicitation_rx: Option<tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>>,
pub(crate) shared_tools: Option<Arc<RwLock<Vec<zeph_mcp::McpTool>>>>,
pub(crate) tool_rx: Option<tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>>,
pub(crate) server_outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
pub(crate) pruning_cache: zeph_mcp::PruningCache,
pub(crate) pruning_provider: Option<zeph_llm::any::AnyProvider>,
pub(crate) pruning_enabled: bool,
pub(crate) pruning_params: zeph_mcp::PruningParams,
pub(crate) semantic_index: Option<zeph_mcp::SemanticToolIndex>,
pub(crate) discovery_strategy: zeph_mcp::ToolDiscoveryStrategy,
pub(crate) discovery_params: zeph_mcp::DiscoveryParams,
pub(crate) discovery_provider: Option<zeph_llm::any::AnyProvider>,
pub(crate) elicitation_warn_sensitive_fields: bool,
pub(crate) pending_semantic_rebuild: bool,
}
pub(crate) struct IndexState {
pub(crate) retriever: Option<std::sync::Arc<zeph_index::retriever::CodeRetriever>>,
pub(crate) repo_map_tokens: usize,
pub(crate) cached_repo_map: Option<(String, std::time::Instant)>,
pub(crate) repo_map_ttl: std::time::Duration,
}
#[derive(Debug, Clone)]
pub struct AdversarialPolicyInfo {
pub provider: String,
pub policy_count: usize,
pub fail_open: bool,
}
pub(crate) struct RuntimeConfig {
pub(crate) security: SecurityConfig,
pub(crate) timeouts: TimeoutConfig,
pub(crate) model_name: String,
pub(crate) active_provider_name: String,
pub(crate) permission_policy: zeph_tools::PermissionPolicy,
pub(crate) redact_credentials: bool,
pub(crate) rate_limiter: super::rate_limiter::ToolRateLimiter,
pub(crate) semantic_cache_enabled: bool,
pub(crate) semantic_cache_threshold: f32,
pub(crate) semantic_cache_max_candidates: u32,
pub(crate) dependency_config: zeph_tools::DependencyConfig,
pub(crate) adversarial_policy_info: Option<AdversarialPolicyInfo>,
pub(crate) spawn_depth: u32,
pub(crate) budget_hint_enabled: bool,
pub(crate) channel_skills: zeph_config::ChannelSkillsConfig,
pub(crate) layers: Vec<std::sync::Arc<dyn crate::runtime_layer::RuntimeLayer>>,
pub(crate) supervisor_config: crate::config::TaskSupervisorConfig,
}
pub(crate) struct FeedbackState {
pub(crate) detector: super::feedback_detector::FeedbackDetector,
pub(crate) judge: Option<super::feedback_detector::JudgeDetector>,
pub(crate) llm_classifier: Option<zeph_llm::classifier::llm::LlmClassifier>,
}
pub(crate) struct SecurityState {
pub(crate) sanitizer: ContentSanitizer,
pub(crate) quarantine_summarizer: Option<QuarantinedSummarizer>,
pub(crate) is_acp_session: bool,
pub(crate) exfiltration_guard: zeph_sanitizer::exfiltration::ExfiltrationGuard,
pub(crate) flagged_urls: HashSet<String>,
pub(crate) user_provided_urls: Arc<RwLock<HashSet<String>>>,
pub(crate) pii_filter: zeph_sanitizer::pii::PiiFilter,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_backend: Option<std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>>,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_timeout_ms: u64,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_max_chars: usize,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_circuit_breaker_threshold: u32,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_consecutive_timeouts: u32,
#[cfg(feature = "classifiers")]
pub(crate) pii_ner_tripped: bool,
pub(crate) memory_validator: zeph_sanitizer::memory_validation::MemoryWriteValidator,
pub(crate) guardrail: Option<zeph_sanitizer::guardrail::GuardrailFilter>,
pub(crate) response_verifier: zeph_sanitizer::response_verifier::ResponseVerifier,
pub(crate) causal_analyzer: Option<zeph_sanitizer::causal_ipi::TurnCausalAnalyzer>,
}
pub(crate) struct DebugState {
pub(crate) debug_dumper: Option<crate::debug_dump::DebugDumper>,
pub(crate) dump_format: crate::debug_dump::DumpFormat,
pub(crate) trace_collector: Option<crate::debug_dump::trace::TracingCollector>,
pub(crate) iteration_counter: usize,
pub(crate) anomaly_detector: Option<zeph_tools::AnomalyDetector>,
pub(crate) reasoning_model_warning: bool,
pub(crate) logging_config: crate::config::LoggingConfig,
pub(crate) dump_dir: Option<PathBuf>,
pub(crate) trace_service_name: String,
pub(crate) trace_redact: bool,
pub(crate) current_iteration_span_id: Option<[u8; 8]>,
}
pub(crate) struct LifecycleState {
pub(crate) shutdown: watch::Receiver<bool>,
pub(crate) start_time: Instant,
pub(crate) cancel_signal: Arc<Notify>,
pub(crate) cancel_token: CancellationToken,
pub(crate) cancel_bridge_handle: Option<JoinHandle<()>>,
pub(crate) config_path: Option<PathBuf>,
pub(crate) config_reload_rx: Option<mpsc::Receiver<ConfigEvent>>,
pub(crate) warmup_ready: Option<watch::Receiver<bool>>,
pub(crate) update_notify_rx: Option<mpsc::Receiver<String>>,
pub(crate) custom_task_rx: Option<mpsc::Receiver<String>>,
pub(crate) last_known_cwd: PathBuf,
pub(crate) file_changed_rx: Option<mpsc::Receiver<FileChangedEvent>>,
pub(crate) file_watcher: Option<crate::file_watcher::FileChangeWatcher>,
pub(crate) supervisor: super::supervisor::BackgroundSupervisor,
}
pub struct ProviderConfigSnapshot {
pub claude_api_key: Option<String>,
pub openai_api_key: Option<String>,
pub gemini_api_key: Option<String>,
pub compatible_api_keys: std::collections::HashMap<String, String>,
pub llm_request_timeout_secs: u64,
pub embedding_model: String,
}
pub(crate) struct ProviderState {
pub(crate) summary_provider: Option<AnyProvider>,
pub(crate) provider_override: Option<Arc<RwLock<Option<AnyProvider>>>>,
pub(crate) judge_provider: Option<AnyProvider>,
pub(crate) probe_provider: Option<AnyProvider>,
pub(crate) compress_provider: Option<AnyProvider>,
pub(crate) cached_prompt_tokens: u64,
pub(crate) server_compaction_active: bool,
pub(crate) stt: Option<Box<dyn SpeechToText>>,
pub(crate) provider_pool: Vec<ProviderEntry>,
pub(crate) provider_config_snapshot: Option<ProviderConfigSnapshot>,
}
pub(crate) struct MetricsState {
pub(crate) metrics_tx: Option<watch::Sender<MetricsSnapshot>>,
pub(crate) cost_tracker: Option<CostTracker>,
pub(crate) token_counter: Arc<TokenCounter>,
pub(crate) extended_context: bool,
pub(crate) classifier_metrics: Option<Arc<zeph_llm::ClassifierMetrics>>,
pub(crate) timing_window: std::collections::VecDeque<crate::metrics::TurnTimings>,
pub(crate) pending_timings: crate::metrics::TurnTimings,
pub(crate) histogram_recorder: Option<std::sync::Arc<dyn crate::metrics::HistogramRecorder>>,
}
#[derive(Default)]
pub(crate) struct OrchestrationState {
pub(crate) planner_provider: Option<AnyProvider>,
pub(crate) verify_provider: Option<AnyProvider>,
pub(crate) pending_graph: Option<zeph_orchestration::TaskGraph>,
pub(crate) plan_cancel_token: Option<CancellationToken>,
pub(crate) subagent_manager: Option<zeph_subagent::SubAgentManager>,
pub(crate) subagent_config: crate::config::SubAgentConfig,
pub(crate) orchestration_config: crate::config::OrchestrationConfig,
#[allow(dead_code)]
pub(crate) plan_cache: Option<zeph_orchestration::PlanCache>,
pub(crate) pending_goal_embedding: Option<Vec<f32>>,
}
#[derive(Default)]
pub(crate) struct InstructionState {
pub(crate) blocks: Vec<InstructionBlock>,
pub(crate) reload_rx: Option<mpsc::Receiver<InstructionEvent>>,
pub(crate) reload_state: Option<InstructionReloadState>,
}
pub(crate) struct ExperimentState {
pub(crate) config: crate::config::ExperimentConfig,
pub(crate) cancel: Option<tokio_util::sync::CancellationToken>,
pub(crate) baseline: zeph_experiments::ConfigSnapshot,
pub(crate) eval_provider: Option<AnyProvider>,
pub(crate) notify_rx: Option<tokio::sync::mpsc::Receiver<String>>,
pub(crate) notify_tx: tokio::sync::mpsc::Sender<String>,
}
pub(crate) struct SubgoalExtractionResult {
pub(crate) current: String,
pub(crate) completed: Option<String>,
}
#[derive(Default)]
pub(crate) struct CompressionState {
pub(crate) current_task_goal: Option<String>,
pub(crate) task_goal_user_msg_hash: Option<u64>,
pub(crate) pending_task_goal: Option<tokio::task::JoinHandle<Option<String>>>,
pub(crate) pending_sidequest_result: Option<tokio::task::JoinHandle<Option<Vec<usize>>>>,
pub(crate) subgoal_registry: crate::agent::compaction_strategy::SubgoalRegistry,
pub(crate) pending_subgoal: Option<tokio::task::JoinHandle<Option<SubgoalExtractionResult>>>,
pub(crate) subgoal_user_msg_hash: Option<u64>,
}
#[derive(Default)]
pub(crate) struct ToolState {
pub(crate) tool_schema_filter: Option<zeph_tools::ToolSchemaFilter>,
pub(crate) cached_filtered_tool_ids: Option<HashSet<String>>,
pub(crate) dependency_graph: Option<zeph_tools::ToolDependencyGraph>,
pub(crate) dependency_always_on: HashSet<String>,
pub(crate) completed_tool_ids: HashSet<String>,
pub(crate) current_tool_iteration: usize,
}
pub(crate) struct SessionState {
pub(crate) env_context: EnvironmentContext,
pub(crate) last_assistant_at: Option<Instant>,
pub(crate) response_cache: Option<std::sync::Arc<zeph_memory::ResponseCache>>,
pub(crate) parent_tool_use_id: Option<String>,
pub(crate) status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
pub(crate) lsp_hooks: Option<crate::lsp_hooks::LspHookRunner>,
pub(crate) policy_config: Option<zeph_tools::PolicyConfig>,
pub(crate) hooks_config: HooksConfigSnapshot,
}
#[derive(Default)]
pub(crate) struct HooksConfigSnapshot {
pub(crate) cwd_changed: Vec<zeph_config::HookDef>,
pub(crate) file_changed_hooks: Vec<zeph_config::HookDef>,
}
pub(crate) struct MessageState {
pub(crate) messages: Vec<Message>,
#[allow(private_interfaces)]
pub(crate) message_queue: VecDeque<QueuedMessage>,
pub(crate) pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
pub(crate) last_persisted_message_id: Option<i64>,
pub(crate) deferred_db_hide_ids: Vec<i64>,
pub(crate) deferred_db_summaries: Vec<String>,
}
impl McpState {
pub(crate) fn sync_executor_tools(&self) {
if let Some(ref shared) = self.shared_tools {
shared.write().clone_from(&self.tools);
}
}
pub(crate) fn apply_pruned_tools(&self, pruned: Vec<zeph_mcp::McpTool>) {
debug_assert!(
pruned.iter().all(|p| self
.tools
.iter()
.any(|t| t.server_id == p.server_id && t.name == p.name)),
"pruned set must be a subset of self.tools"
);
if let Some(ref shared) = self.shared_tools {
*shared.write() = pruned;
}
}
#[cfg(test)]
pub(crate) fn tool_count(&self) -> usize {
self.tools.len()
}
}
impl IndexState {
pub(crate) async fn fetch_code_rag(
&self,
query: &str,
token_budget: usize,
) -> Result<Option<String>, crate::agent::error::AgentError> {
let Some(retriever) = &self.retriever else {
return Ok(None);
};
if token_budget == 0 {
return Ok(None);
}
let result = retriever
.retrieve(query, token_budget)
.await
.map_err(|e| crate::agent::error::AgentError::Other(format!("{e:#}")))?;
let context_text = zeph_index::retriever::format_as_context(&result);
if context_text.is_empty() {
Ok(None)
} else {
tracing::debug!(
strategy = ?result.strategy,
chunks = result.chunks.len(),
tokens = result.total_tokens,
"code context fetched"
);
Ok(Some(context_text))
}
}
pub(crate) fn as_index_access(&self) -> Option<&dyn zeph_context::input::IndexAccess> {
if self.retriever.is_some() {
Some(self)
} else {
None
}
}
}
impl DebugState {
pub(crate) fn start_iteration_span(&mut self, iteration_index: usize, text: &str) {
if let Some(ref mut tc) = self.trace_collector {
tc.begin_iteration(iteration_index, text);
self.current_iteration_span_id = tc.current_iteration_span_id(iteration_index);
}
}
pub(crate) fn end_iteration_span(
&mut self,
iteration_index: usize,
status: crate::debug_dump::trace::SpanStatus,
) {
if let Some(ref mut tc) = self.trace_collector {
tc.end_iteration(iteration_index, status);
}
self.current_iteration_span_id = None;
}
pub(crate) fn switch_format(&mut self, new_format: crate::debug_dump::DumpFormat) {
let was_trace = self.dump_format == crate::debug_dump::DumpFormat::Trace;
let now_trace = new_format == crate::debug_dump::DumpFormat::Trace;
if now_trace
&& !was_trace
&& let Some(ref dump_dir) = self.dump_dir.clone()
{
let service_name = self.trace_service_name.clone();
let redact = self.trace_redact;
match crate::debug_dump::trace::TracingCollector::new(
dump_dir.as_path(),
&service_name,
redact,
None,
) {
Ok(collector) => {
self.trace_collector = Some(collector);
}
Err(e) => {
tracing::warn!(error = %e, "failed to create TracingCollector on format switch");
}
}
}
if was_trace
&& !now_trace
&& let Some(mut tc) = self.trace_collector.take()
{
tc.finish();
}
self.dump_format = new_format;
}
pub(crate) fn write_chat_debug_dump(
&self,
dump_id: Option<u32>,
result: &zeph_llm::provider::ChatResponse,
pii_filter: &zeph_sanitizer::pii::PiiFilter,
) {
let Some((d, id)) = self.debug_dumper.as_ref().zip(dump_id) else {
return;
};
let raw = match result {
zeph_llm::provider::ChatResponse::Text(t) => t.clone(),
zeph_llm::provider::ChatResponse::ToolUse {
text, tool_calls, ..
} => {
let calls = serde_json::to_string_pretty(tool_calls).unwrap_or_default();
format!(
"{}\n\n---TOOL_CALLS---\n{calls}",
text.as_deref().unwrap_or("")
)
}
};
let text = if pii_filter.is_enabled() {
pii_filter.scrub(&raw).into_owned()
} else {
raw
};
d.dump_response(id, &text);
}
}
impl Default for McpState {
fn default() -> Self {
Self {
tools: Vec::new(),
registry: None,
manager: None,
allowed_commands: Vec::new(),
max_dynamic: 10,
elicitation_rx: None,
shared_tools: None,
tool_rx: None,
server_outcomes: Vec::new(),
pruning_cache: zeph_mcp::PruningCache::new(),
pruning_provider: None,
pruning_enabled: false,
pruning_params: zeph_mcp::PruningParams::default(),
semantic_index: None,
discovery_strategy: zeph_mcp::ToolDiscoveryStrategy::default(),
discovery_params: zeph_mcp::DiscoveryParams::default(),
discovery_provider: None,
elicitation_warn_sensitive_fields: true,
pending_semantic_rebuild: false,
}
}
}
impl Default for IndexState {
fn default() -> Self {
Self {
retriever: None,
repo_map_tokens: 0,
cached_repo_map: None,
repo_map_ttl: std::time::Duration::from_secs(300),
}
}
}
impl Default for DebugState {
fn default() -> Self {
Self {
debug_dumper: None,
dump_format: crate::debug_dump::DumpFormat::default(),
trace_collector: None,
iteration_counter: 0,
anomaly_detector: None,
reasoning_model_warning: true,
logging_config: crate::config::LoggingConfig::default(),
dump_dir: None,
trace_service_name: String::new(),
trace_redact: true,
current_iteration_span_id: None,
}
}
}
impl Default for FeedbackState {
fn default() -> Self {
Self {
detector: super::feedback_detector::FeedbackDetector::new(0.6),
judge: None,
llm_classifier: None,
}
}
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
security: SecurityConfig::default(),
timeouts: TimeoutConfig::default(),
model_name: String::new(),
active_provider_name: String::new(),
permission_policy: zeph_tools::PermissionPolicy::default(),
redact_credentials: true,
rate_limiter: super::rate_limiter::ToolRateLimiter::new(
super::rate_limiter::RateLimitConfig::default(),
),
semantic_cache_enabled: false,
semantic_cache_threshold: 0.95,
semantic_cache_max_candidates: 10,
dependency_config: zeph_tools::DependencyConfig::default(),
adversarial_policy_info: None,
spawn_depth: 0,
budget_hint_enabled: true,
channel_skills: zeph_config::ChannelSkillsConfig::default(),
layers: Vec::new(),
supervisor_config: crate::config::TaskSupervisorConfig::default(),
}
}
}
impl SessionState {
pub(crate) fn new() -> Self {
Self {
env_context: EnvironmentContext::gather(""),
last_assistant_at: None,
response_cache: None,
parent_tool_use_id: None,
status_tx: None,
lsp_hooks: None,
policy_config: None,
hooks_config: HooksConfigSnapshot::default(),
}
}
}
impl SkillState {
pub(crate) fn new(
registry: Arc<RwLock<SkillRegistry>>,
matcher: Option<SkillMatcherBackend>,
max_active_skills: usize,
last_skills_prompt: String,
) -> Self {
Self {
registry,
skill_paths: Vec::new(),
managed_dir: None,
trust_config: crate::config::TrustConfig::default(),
matcher,
max_active_skills,
disambiguation_threshold: 0.20,
min_injection_score: 0.20,
embedding_model: String::new(),
skill_reload_rx: None,
active_skill_names: Vec::new(),
last_skills_prompt,
prompt_mode: crate::config::SkillPromptMode::Auto,
available_custom_secrets: HashMap::new(),
cosine_weight: 0.7,
hybrid_search: false,
bm25_index: None,
two_stage_matching: false,
confusability_threshold: 0.0,
rl_head: None,
rl_weight: 0.3,
rl_warmup_updates: 50,
generation_output_dir: None,
generation_provider_name: String::new(),
}
}
}
impl LifecycleState {
pub(crate) fn new() -> Self {
let (_tx, rx) = watch::channel(false);
Self {
shutdown: rx,
start_time: Instant::now(),
cancel_signal: Arc::new(tokio::sync::Notify::new()),
cancel_token: tokio_util::sync::CancellationToken::new(),
cancel_bridge_handle: None,
config_path: None,
config_reload_rx: None,
warmup_ready: None,
update_notify_rx: None,
custom_task_rx: None,
last_known_cwd: std::env::current_dir().unwrap_or_default(),
file_changed_rx: None,
file_watcher: None,
supervisor: super::supervisor::BackgroundSupervisor::new(
&crate::config::TaskSupervisorConfig::default(),
None,
),
}
}
}
impl ProviderState {
pub(crate) fn new(initial_prompt_tokens: u64) -> Self {
Self {
summary_provider: None,
provider_override: None,
judge_provider: None,
probe_provider: None,
compress_provider: None,
cached_prompt_tokens: initial_prompt_tokens,
server_compaction_active: false,
stt: None,
provider_pool: Vec::new(),
provider_config_snapshot: None,
}
}
}
impl MetricsState {
pub(crate) fn new(token_counter: Arc<zeph_memory::TokenCounter>) -> Self {
Self {
metrics_tx: None,
cost_tracker: None,
token_counter,
extended_context: false,
classifier_metrics: None,
timing_window: std::collections::VecDeque::new(),
pending_timings: crate::metrics::TurnTimings::default(),
histogram_recorder: None,
}
}
}
impl ExperimentState {
pub(crate) fn new() -> Self {
let (notify_tx, notify_rx) = tokio::sync::mpsc::channel::<String>(4);
Self {
config: crate::config::ExperimentConfig::default(),
cancel: None,
baseline: zeph_experiments::ConfigSnapshot::default(),
eval_provider: None,
notify_rx: Some(notify_rx),
notify_tx,
}
}
}
pub(super) mod security;
pub(super) mod skill;
#[cfg(test)]
mod tests;