use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{Notify, mpsc, watch};
use zeph_llm::any::AnyProvider;
use zeph_llm::provider::LlmProvider;
use super::Agent;
use super::session_config::{AgentSessionConfig, CONTEXT_BUDGET_RESERVE_RATIO};
use crate::channel::Channel;
use crate::config::{
CompressionConfig, LearningConfig, RoutingConfig, SecurityConfig, TimeoutConfig,
};
use crate::config_watcher::ConfigEvent;
use crate::context::ContextBudget;
use crate::cost::CostTracker;
use crate::instructions::{InstructionEvent, InstructionReloadState};
use crate::metrics::MetricsSnapshot;
use zeph_memory::semantic::SemanticMemory;
use zeph_skills::watcher::SkillEvent;
impl<C: Channel> Agent<C> {
#[cfg(feature = "policy-enforcer")]
#[must_use]
pub fn with_policy_config(mut self, config: zeph_tools::PolicyConfig) -> Self {
self.policy_config = Some(config);
self
}
#[must_use]
pub fn with_autosave_config(mut self, autosave_assistant: bool, min_length: usize) -> Self {
self.memory_state.autosave_assistant = autosave_assistant;
self.memory_state.autosave_min_length = min_length;
self
}
#[must_use]
pub fn with_tool_call_cutoff(mut self, cutoff: usize) -> Self {
self.memory_state.tool_call_cutoff = cutoff;
self
}
#[must_use]
pub fn with_shutdown_summary_config(
mut self,
enabled: bool,
min_messages: usize,
max_messages: usize,
timeout_secs: u64,
) -> Self {
self.memory_state.shutdown_summary = enabled;
self.memory_state.shutdown_summary_min_messages = min_messages;
self.memory_state.shutdown_summary_max_messages = max_messages;
self.memory_state.shutdown_summary_timeout_secs = timeout_secs;
self
}
#[must_use]
pub fn with_response_cache(
mut self,
cache: std::sync::Arc<zeph_memory::ResponseCache>,
) -> Self {
self.response_cache = Some(cache);
self
}
#[must_use]
pub fn with_parent_tool_use_id(mut self, id: impl Into<String>) -> Self {
self.parent_tool_use_id = Some(id.into());
self
}
#[must_use]
pub fn with_stt(mut self, stt: Box<dyn zeph_llm::stt::SpeechToText>) -> Self {
self.providers.stt = Some(stt);
self
}
#[must_use]
pub fn with_debug_dumper(mut self, dumper: crate::debug_dump::DebugDumper) -> Self {
self.debug_state.debug_dumper = Some(dumper);
self
}
#[must_use]
pub fn with_trace_collector(
mut self,
collector: crate::debug_dump::trace::TracingCollector,
) -> Self {
self.debug_state.trace_collector = Some(collector);
self
}
#[must_use]
pub fn with_trace_config(
mut self,
dump_dir: std::path::PathBuf,
service_name: impl Into<String>,
redact: bool,
) -> Self {
self.debug_state.dump_dir = Some(dump_dir);
self.debug_state.trace_service_name = service_name.into();
self.debug_state.trace_redact = redact;
self
}
#[cfg(feature = "lsp-context")]
#[must_use]
pub fn with_lsp_hooks(mut self, runner: crate::lsp_hooks::LspHookRunner) -> Self {
self.lsp_hooks = Some(runner);
self
}
#[must_use]
pub fn with_update_notifications(mut self, rx: mpsc::Receiver<String>) -> Self {
self.lifecycle.update_notify_rx = Some(rx);
self
}
#[must_use]
pub fn with_custom_task_rx(mut self, rx: mpsc::Receiver<String>) -> Self {
self.lifecycle.custom_task_rx = Some(rx);
self
}
#[must_use]
pub fn add_tool_executor(
mut self,
extra: impl zeph_tools::executor::ToolExecutor + 'static,
) -> Self {
let existing = Arc::clone(&self.tool_executor);
let combined = zeph_tools::CompositeExecutor::new(zeph_tools::DynExecutor(existing), extra);
self.tool_executor = Arc::new(combined);
self
}
#[must_use]
pub fn with_max_tool_iterations(mut self, max: usize) -> Self {
self.tool_orchestrator.max_iterations = max;
self
}
#[must_use]
pub fn with_max_tool_retries(mut self, max: usize) -> Self {
self.tool_orchestrator.max_tool_retries = max.min(5);
self
}
#[must_use]
pub fn with_max_retry_duration_secs(mut self, secs: u64) -> Self {
self.tool_orchestrator.max_retry_duration_secs = secs;
self
}
#[must_use]
pub fn with_tool_repeat_threshold(mut self, threshold: usize) -> Self {
self.tool_orchestrator.repeat_threshold = threshold;
self.tool_orchestrator.recent_tool_calls = VecDeque::with_capacity(2 * threshold.max(1));
self
}
#[must_use]
pub fn with_memory(
mut self,
memory: Arc<SemanticMemory>,
conversation_id: zeph_memory::ConversationId,
history_limit: u32,
recall_limit: usize,
summarization_threshold: usize,
) -> Self {
self.memory_state.memory = Some(memory);
self.memory_state.conversation_id = Some(conversation_id);
self.memory_state.history_limit = history_limit;
self.memory_state.recall_limit = recall_limit;
self.memory_state.summarization_threshold = summarization_threshold;
self.update_metrics(|m| {
m.qdrant_available = false;
m.sqlite_conversation_id = Some(conversation_id);
});
self
}
#[must_use]
pub fn with_embedding_model(mut self, model: String) -> Self {
self.skill_state.embedding_model = model;
self
}
#[must_use]
pub fn with_disambiguation_threshold(mut self, threshold: f32) -> Self {
self.skill_state.disambiguation_threshold = threshold;
self
}
#[must_use]
pub fn with_skill_prompt_mode(mut self, mode: crate::config::SkillPromptMode) -> Self {
self.skill_state.prompt_mode = mode;
self
}
#[must_use]
pub fn with_document_config(mut self, config: crate::config::DocumentConfig) -> Self {
self.memory_state.document_config = config;
self
}
#[must_use]
pub fn with_compression_guidelines_config(
mut self,
config: zeph_memory::CompressionGuidelinesConfig,
) -> Self {
self.memory_state.compression_guidelines_config = config;
self
}
#[must_use]
pub fn with_graph_config(mut self, config: crate::config::GraphConfig) -> Self {
if config.enabled {
tracing::warn!(
"graph-memory is enabled: extracted entities are stored without PII redaction. \
Do not use with sensitive personal data until redaction is implemented."
);
}
self.memory_state.graph_config = config;
self
}
#[must_use]
pub fn with_anomaly_detector(mut self, detector: zeph_tools::AnomalyDetector) -> Self {
self.debug_state.anomaly_detector = Some(detector);
self
}
#[must_use]
pub fn with_instruction_blocks(
mut self,
blocks: Vec<crate::instructions::InstructionBlock>,
) -> Self {
self.instruction_blocks = blocks;
self
}
#[must_use]
pub fn with_instruction_reload(
mut self,
rx: mpsc::Receiver<InstructionEvent>,
state: InstructionReloadState,
) -> Self {
self.instruction_reload_rx = Some(rx);
self.instruction_reload_state = Some(state);
self
}
#[must_use]
pub fn with_shutdown(mut self, rx: watch::Receiver<bool>) -> Self {
self.lifecycle.shutdown = rx;
self
}
#[must_use]
pub fn with_skill_reload(
mut self,
paths: Vec<PathBuf>,
rx: mpsc::Receiver<SkillEvent>,
) -> Self {
self.skill_state.skill_paths = paths;
self.skill_state.skill_reload_rx = Some(rx);
self
}
#[must_use]
pub fn with_managed_skills_dir(mut self, dir: PathBuf) -> Self {
self.skill_state.managed_dir = Some(dir);
self
}
#[must_use]
pub fn with_trust_config(mut self, config: crate::config::TrustConfig) -> Self {
self.skill_state.trust_config = config;
self
}
#[must_use]
pub fn with_config_reload(mut self, path: PathBuf, rx: mpsc::Receiver<ConfigEvent>) -> Self {
self.lifecycle.config_path = Some(path);
self.lifecycle.config_reload_rx = Some(rx);
self
}
#[must_use]
pub fn with_logging_config(mut self, logging: crate::config::LoggingConfig) -> Self {
self.debug_state.logging_config = logging;
self
}
#[must_use]
pub fn with_available_secrets(
mut self,
secrets: impl IntoIterator<Item = (String, crate::vault::Secret)>,
) -> Self {
self.skill_state.available_custom_secrets = secrets.into_iter().collect();
self
}
#[must_use]
pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
self.skill_state.hybrid_search = enabled;
if enabled {
let reg = self
.skill_state
.registry
.read()
.expect("registry read lock");
let all_meta = reg.all_meta();
let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
}
self
}
#[must_use]
pub fn with_learning(mut self, config: LearningConfig) -> Self {
if config.correction_detection {
self.feedback_detector = super::feedback_detector::FeedbackDetector::new(
config.correction_confidence_threshold,
);
if config.detector_mode == crate::config::DetectorMode::Judge {
self.judge_detector = Some(super::feedback_detector::JudgeDetector::new(
config.judge_adaptive_low,
config.judge_adaptive_high,
));
}
}
self.learning_engine.config = Some(config);
self
}
#[must_use]
pub fn with_judge_provider(mut self, provider: AnyProvider) -> Self {
self.providers.judge_provider = Some(provider);
self
}
#[must_use]
pub fn with_server_compaction(mut self, enabled: bool) -> Self {
self.providers.server_compaction_active = enabled;
self
}
#[must_use]
pub fn with_mcp(
mut self,
tools: Vec<zeph_mcp::McpTool>,
registry: Option<zeph_mcp::McpToolRegistry>,
manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
mcp_config: &crate::config::McpConfig,
) -> Self {
self.mcp.tools = tools;
self.mcp.registry = registry;
self.mcp.manager = manager;
self.mcp
.allowed_commands
.clone_from(&mcp_config.allowed_commands);
self.mcp.max_dynamic = mcp_config.max_dynamic_servers;
self
}
#[must_use]
pub fn with_mcp_shared_tools(
mut self,
shared: std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>,
) -> Self {
self.mcp.shared_tools = Some(shared);
self
}
#[must_use]
pub fn with_mcp_tool_rx(
mut self,
rx: tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>,
) -> Self {
self.mcp.tool_rx = Some(rx);
self
}
#[must_use]
pub fn with_security(mut self, security: SecurityConfig, timeouts: TimeoutConfig) -> Self {
self.security.sanitizer =
crate::sanitizer::ContentSanitizer::new(&security.content_isolation);
self.security.exfiltration_guard = crate::sanitizer::exfiltration::ExfiltrationGuard::new(
security.exfiltration_guard.clone(),
);
self.security.pii_filter =
crate::sanitizer::pii::PiiFilter::new(security.pii_filter.clone());
self.security.memory_validator =
crate::sanitizer::memory_validation::MemoryWriteValidator::new(
security.memory_validation.clone(),
);
self.rate_limiter =
crate::agent::rate_limiter::ToolRateLimiter::new(security.rate_limit.clone());
let mut verifiers: Vec<Box<dyn zeph_tools::PreExecutionVerifier>> = Vec::new();
if security.pre_execution_verify.enabled {
let dcfg = &security.pre_execution_verify.destructive_commands;
if dcfg.enabled {
verifiers.push(Box::new(zeph_tools::DestructiveCommandVerifier::new(dcfg)));
}
let icfg = &security.pre_execution_verify.injection_patterns;
if icfg.enabled {
verifiers.push(Box::new(zeph_tools::InjectionPatternVerifier::new(icfg)));
}
}
self.tool_orchestrator.pre_execution_verifiers = verifiers;
self.runtime.security = security;
self.runtime.timeouts = timeouts;
self
}
#[must_use]
pub fn with_redact_credentials(mut self, enabled: bool) -> Self {
self.runtime.redact_credentials = enabled;
self
}
#[must_use]
pub fn with_tool_summarization(mut self, enabled: bool) -> Self {
self.tool_orchestrator.summarize_tool_output_enabled = enabled;
self
}
#[must_use]
pub fn with_overflow_config(mut self, config: zeph_tools::OverflowConfig) -> Self {
self.tool_orchestrator.overflow_config = config;
self
}
#[must_use]
pub fn with_summary_provider(mut self, provider: AnyProvider) -> Self {
self.providers.summary_provider = Some(provider);
self
}
#[must_use]
pub fn with_quarantine_summarizer(
mut self,
qs: crate::sanitizer::quarantine::QuarantinedSummarizer,
) -> Self {
self.security.quarantine_summarizer = Some(qs);
self
}
#[cfg(feature = "guardrail")]
#[must_use]
pub fn with_guardrail(mut self, filter: crate::sanitizer::guardrail::GuardrailFilter) -> Self {
use crate::sanitizer::guardrail::GuardrailAction;
let warn_mode = filter.action() == GuardrailAction::Warn;
self.security.guardrail = Some(filter);
self.update_metrics(|m| {
m.guardrail_enabled = true;
m.guardrail_warn_mode = warn_mode;
});
self
}
pub(super) fn summary_or_primary_provider(&self) -> &AnyProvider {
self.providers
.summary_provider
.as_ref()
.unwrap_or(&self.provider)
}
pub(super) fn last_assistant_response(&self) -> String {
self.messages
.iter()
.rev()
.find(|m| m.role == zeph_llm::provider::Role::Assistant)
.map(|m| super::context::truncate_chars(&m.content, 500))
.unwrap_or_default()
}
#[must_use]
pub fn with_permission_policy(mut self, policy: zeph_tools::PermissionPolicy) -> Self {
self.runtime.permission_policy = policy;
self
}
#[must_use]
pub fn with_context_budget(
mut self,
budget_tokens: usize,
reserve_ratio: f32,
hard_compaction_threshold: f32,
compaction_preserve_tail: usize,
prune_protect_tokens: usize,
) -> Self {
if budget_tokens > 0 {
self.context_manager.budget = Some(ContextBudget::new(budget_tokens, reserve_ratio));
}
self.context_manager.hard_compaction_threshold = hard_compaction_threshold;
self.context_manager.compaction_preserve_tail = compaction_preserve_tail;
self.context_manager.prune_protect_tokens = prune_protect_tokens;
self
}
#[must_use]
pub fn with_soft_compaction_threshold(mut self, threshold: f32) -> Self {
self.context_manager.soft_compaction_threshold = threshold;
self
}
#[must_use]
pub fn with_compaction_cooldown(mut self, cooldown_turns: u8) -> Self {
self.context_manager.compaction_cooldown_turns = cooldown_turns;
self
}
#[must_use]
pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
self.context_manager.compression = compression;
self
}
#[must_use]
pub fn with_focus_config(mut self, config: crate::config::FocusConfig) -> Self {
self.focus = super::focus::FocusState::new(config);
self
}
#[must_use]
pub fn with_sidequest_config(mut self, config: crate::config::SidequestConfig) -> Self {
self.sidequest = super::sidequest::SidequestState::new(config);
self
}
#[must_use]
pub fn with_routing(mut self, routing: RoutingConfig) -> Self {
self.context_manager.routing = routing;
self
}
#[must_use]
pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
self.runtime.model_name = name.into();
self
}
#[must_use]
pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
let path = path.into();
self.env_context =
crate::context::EnvironmentContext::gather_for_dir(&self.runtime.model_name, &path);
self
}
#[must_use]
pub fn with_warmup_ready(mut self, rx: watch::Receiver<bool>) -> Self {
self.lifecycle.warmup_ready = Some(rx);
self
}
#[must_use]
pub fn with_cost_tracker(mut self, tracker: CostTracker) -> Self {
self.metrics.cost_tracker = Some(tracker);
self
}
#[must_use]
pub fn with_extended_context(mut self, enabled: bool) -> Self {
self.metrics.extended_context = enabled;
self
}
#[must_use]
pub fn with_repo_map(mut self, token_budget: usize, ttl_secs: u64) -> Self {
self.index.repo_map_tokens = token_budget;
self.index.repo_map_ttl = std::time::Duration::from_secs(ttl_secs);
self
}
#[must_use]
pub fn with_code_retriever(
mut self,
retriever: std::sync::Arc<zeph_index::retriever::CodeRetriever>,
) -> Self {
self.index.retriever = Some(retriever);
self
}
#[must_use]
pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
let provider_name = self.provider.name().to_string();
let model_name = self.runtime.model_name.clone();
let total_skills = self
.skill_state
.registry
.read()
.expect("registry read lock")
.all_meta()
.len();
let qdrant_available = false;
let conversation_id = self.memory_state.conversation_id;
let prompt_estimate = self
.messages
.first()
.map_or(0, |m| u64::try_from(m.content.len()).unwrap_or(0) / 4);
let mcp_tool_count = self.mcp.tools.len();
let mcp_server_count = self
.mcp
.tools
.iter()
.map(|t| &t.server_id)
.collect::<std::collections::HashSet<_>>()
.len();
let extended_context = self.metrics.extended_context;
tx.send_modify(|m| {
m.provider_name = provider_name;
m.model_name = model_name;
m.total_skills = total_skills;
m.qdrant_available = qdrant_available;
m.sqlite_conversation_id = conversation_id;
m.context_tokens = prompt_estimate;
m.prompt_tokens = prompt_estimate;
m.total_tokens = prompt_estimate;
m.mcp_tool_count = mcp_tool_count;
m.mcp_server_count = mcp_server_count;
m.extended_context = extended_context;
});
self.metrics.metrics_tx = Some(tx);
self
}
#[must_use]
pub fn cancel_signal(&self) -> Arc<Notify> {
Arc::clone(&self.lifecycle.cancel_signal)
}
#[must_use]
pub fn with_cancel_signal(mut self, signal: Arc<Notify>) -> Self {
self.lifecycle.cancel_signal = signal;
self
}
#[must_use]
pub fn with_subagent_manager(mut self, manager: crate::subagent::SubAgentManager) -> Self {
self.orchestration.subagent_manager = Some(manager);
self
}
#[must_use]
pub fn with_subagent_config(mut self, config: crate::config::SubAgentConfig) -> Self {
self.orchestration.subagent_config = config;
self
}
#[must_use]
pub fn with_orchestration_config(mut self, config: crate::config::OrchestrationConfig) -> Self {
self.orchestration.orchestration_config = config;
self
}
#[cfg(feature = "experiments")]
#[must_use]
pub fn with_experiment_config(mut self, config: crate::config::ExperimentConfig) -> Self {
self.experiment_config = config;
self
}
#[cfg(feature = "experiments")]
#[must_use]
pub fn with_experiment_baseline(
mut self,
baseline: crate::experiments::ConfigSnapshot,
) -> Self {
self.experiment_baseline = baseline;
self
}
#[must_use]
pub fn with_provider_override(
mut self,
slot: Arc<std::sync::RwLock<Option<AnyProvider>>>,
) -> Self {
self.providers.provider_override = Some(slot);
self
}
#[must_use]
pub fn apply_session_config(mut self, cfg: AgentSessionConfig) -> Self {
let AgentSessionConfig {
max_tool_iterations,
max_tool_retries,
max_retry_duration_secs,
tool_repeat_threshold,
tool_summarization,
tool_call_cutoff,
overflow_config,
permission_policy,
model_name,
embed_model,
budget_tokens,
soft_compaction_threshold,
hard_compaction_threshold,
compaction_preserve_tail,
compaction_cooldown_turns,
prune_protect_tokens,
redact_credentials,
security,
timeouts,
learning,
document_config,
graph_config,
anomaly_config,
orchestration_config,
debug_config: _debug_config,
server_compaction,
secrets,
} = cfg;
self = self
.with_max_tool_iterations(max_tool_iterations)
.with_max_tool_retries(max_tool_retries)
.with_max_retry_duration_secs(max_retry_duration_secs)
.with_tool_repeat_threshold(tool_repeat_threshold)
.with_model_name(model_name)
.with_embedding_model(embed_model)
.with_context_budget(
budget_tokens,
CONTEXT_BUDGET_RESERVE_RATIO,
hard_compaction_threshold,
compaction_preserve_tail,
prune_protect_tokens,
)
.with_soft_compaction_threshold(soft_compaction_threshold)
.with_compaction_cooldown(compaction_cooldown_turns)
.with_security(security, timeouts)
.with_redact_credentials(redact_credentials)
.with_tool_summarization(tool_summarization)
.with_overflow_config(overflow_config)
.with_permission_policy(permission_policy)
.with_learning(learning)
.with_tool_call_cutoff(tool_call_cutoff)
.with_available_secrets(
secrets
.iter()
.map(|(k, v)| (k.clone(), crate::vault::Secret::new(v.expose().to_owned()))),
)
.with_server_compaction(server_compaction)
.with_document_config(document_config)
.with_graph_config(graph_config)
.with_orchestration_config(orchestration_config);
if anomaly_config.enabled {
self = self.with_anomaly_detector(zeph_tools::AnomalyDetector::new(
anomaly_config.window_size,
anomaly_config.error_threshold,
anomaly_config.critical_threshold,
));
}
self
}
}
#[cfg(test)]
mod tests {
use super::super::agent_tests::{
MockChannel, MockToolExecutor, create_test_registry, mock_provider,
};
use super::*;
use crate::config::{CompressionStrategy, RoutingStrategy};
fn make_agent() -> Agent<MockChannel> {
Agent::new(
mock_provider(vec![]),
MockChannel::new(vec![]),
create_test_registry(),
None,
5,
MockToolExecutor::no_tools(),
)
}
#[test]
fn with_compression_sets_proactive_strategy() {
let compression = CompressionConfig {
strategy: CompressionStrategy::Proactive {
threshold_tokens: 50_000,
max_summary_tokens: 2_000,
},
model: String::new(),
pruning_strategy: crate::config::PruningStrategy::default(),
};
let agent = make_agent().with_compression(compression);
assert!(
matches!(
agent.context_manager.compression.strategy,
CompressionStrategy::Proactive {
threshold_tokens: 50_000,
max_summary_tokens: 2_000,
}
),
"expected Proactive strategy after with_compression"
);
}
#[test]
fn with_routing_sets_routing_config() {
let routing = RoutingConfig {
strategy: RoutingStrategy::Heuristic,
};
let agent = make_agent().with_routing(routing);
assert_eq!(
agent.context_manager.routing.strategy,
RoutingStrategy::Heuristic,
"routing strategy must be set by with_routing"
);
}
#[test]
fn default_compression_is_reactive() {
let agent = make_agent();
assert_eq!(
agent.context_manager.compression.strategy,
CompressionStrategy::Reactive,
"default compression strategy must be Reactive"
);
}
#[test]
fn default_routing_is_heuristic() {
let agent = make_agent();
assert_eq!(
agent.context_manager.routing.strategy,
RoutingStrategy::Heuristic,
"default routing strategy must be Heuristic"
);
}
#[test]
fn with_cancel_signal_replaces_internal_signal() {
let agent = Agent::new(
mock_provider(vec![]),
MockChannel::new(vec![]),
create_test_registry(),
None,
5,
MockToolExecutor::no_tools(),
);
let shared = Arc::new(Notify::new());
let agent = agent.with_cancel_signal(Arc::clone(&shared));
assert!(Arc::ptr_eq(&shared, &agent.cancel_signal()));
}
#[tokio::test]
async fn with_managed_skills_dir_enables_install_command() {
let provider = mock_provider(vec![]);
let channel = MockChannel::new(vec![]);
let registry = create_test_registry();
let executor = MockToolExecutor::no_tools();
let managed = tempfile::tempdir().unwrap();
let mut agent_no_dir = Agent::new(
mock_provider(vec![]),
MockChannel::new(vec![]),
create_test_registry(),
None,
5,
MockToolExecutor::no_tools(),
);
agent_no_dir
.handle_skill_command("install /some/path")
.await
.unwrap();
let sent_no_dir = agent_no_dir.channel.sent_messages();
assert!(
sent_no_dir.iter().any(|s| s.contains("not configured")),
"without managed dir: {sent_no_dir:?}"
);
let _ = (provider, channel, registry, executor);
let mut agent_with_dir = Agent::new(
mock_provider(vec![]),
MockChannel::new(vec![]),
create_test_registry(),
None,
5,
MockToolExecutor::no_tools(),
)
.with_managed_skills_dir(managed.path().to_path_buf());
agent_with_dir
.handle_skill_command("install /nonexistent/path")
.await
.unwrap();
let sent_with_dir = agent_with_dir.channel.sent_messages();
assert!(
!sent_with_dir.iter().any(|s| s.contains("not configured")),
"with managed dir should not say not configured: {sent_with_dir:?}"
);
assert!(
sent_with_dir.iter().any(|s| s.contains("Install failed")),
"with managed dir should fail due to bad path: {sent_with_dir:?}"
);
}
#[test]
fn default_graph_config_is_disabled() {
let agent = make_agent();
assert!(
!agent.memory_state.graph_config.enabled,
"graph_config must default to disabled"
);
}
#[test]
fn with_graph_config_enabled_sets_flag() {
let cfg = crate::config::GraphConfig {
enabled: true,
..Default::default()
};
let agent = make_agent().with_graph_config(cfg);
assert!(
agent.memory_state.graph_config.enabled,
"with_graph_config must set enabled flag"
);
}
#[test]
fn apply_session_config_wires_graph_orchestration_anomaly() {
use crate::config::Config;
let mut config = Config::default();
config.memory.graph.enabled = true;
config.orchestration.enabled = true;
config.orchestration.max_tasks = 42;
config.tools.anomaly.enabled = true;
config.tools.anomaly.window_size = 7;
let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
assert!(session_cfg.graph_config.enabled);
assert!(session_cfg.orchestration_config.enabled);
assert_eq!(session_cfg.orchestration_config.max_tasks, 42);
assert!(session_cfg.anomaly_config.enabled);
assert_eq!(session_cfg.anomaly_config.window_size, 7);
let agent = make_agent().apply_session_config(session_cfg);
assert!(
agent.memory_state.graph_config.enabled,
"apply_session_config must wire graph_config into agent"
);
assert!(
agent.orchestration.orchestration_config.enabled,
"apply_session_config must wire orchestration_config into agent"
);
assert_eq!(
agent.orchestration.orchestration_config.max_tasks, 42,
"orchestration max_tasks must match config"
);
assert!(
agent.debug_state.anomaly_detector.is_some(),
"apply_session_config must create anomaly_detector when enabled"
);
}
#[test]
fn apply_session_config_skips_anomaly_detector_when_disabled() {
use crate::config::Config;
let config = Config::default(); let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
assert!(!session_cfg.anomaly_config.enabled);
let agent = make_agent().apply_session_config(session_cfg);
assert!(
agent.debug_state.anomaly_detector.is_none(),
"apply_session_config must not create anomaly_detector when disabled"
);
}
}