1use std::collections::VecDeque;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use tokio::sync::{Notify, mpsc, watch};
9use zeph_llm::any::AnyProvider;
10use zeph_llm::provider::LlmProvider;
11
12use super::Agent;
13use super::session_config::{AgentSessionConfig, CONTEXT_BUDGET_RESERVE_RATIO};
14use crate::agent::state::ProviderConfigSnapshot;
15use crate::channel::Channel;
16use crate::config::{
17 CompressionConfig, LearningConfig, ProviderEntry, SecurityConfig, StoreRoutingConfig,
18 TimeoutConfig,
19};
20use crate::config_watcher::ConfigEvent;
21use crate::context::ContextBudget;
22use crate::cost::CostTracker;
23use crate::instructions::{InstructionEvent, InstructionReloadState};
24use crate::metrics::MetricsSnapshot;
25use zeph_memory::semantic::SemanticMemory;
26use zeph_skills::watcher::SkillEvent;
27
28impl<C: Channel> Agent<C> {
29 #[must_use]
33 pub fn with_status_tx(mut self, tx: tokio::sync::mpsc::UnboundedSender<String>) -> Self {
34 self.session.status_tx = Some(tx);
35 self
36 }
37
38 #[cfg(feature = "policy-enforcer")]
40 #[must_use]
41 pub fn with_policy_config(mut self, config: zeph_tools::PolicyConfig) -> Self {
42 self.session.policy_config = Some(config);
43 self
44 }
45
46 #[cfg(feature = "policy-enforcer")]
48 #[must_use]
49 pub fn with_adversarial_policy_info(
50 mut self,
51 info: crate::agent::state::AdversarialPolicyInfo,
52 ) -> Self {
53 self.runtime.adversarial_policy_info = Some(info);
54 self
55 }
56
57 #[must_use]
58 pub fn with_structured_summaries(mut self, enabled: bool) -> Self {
59 self.memory_state.structured_summaries = enabled;
60 self
61 }
62
63 #[must_use]
64 pub fn with_autosave_config(mut self, autosave_assistant: bool, min_length: usize) -> Self {
65 self.memory_state.autosave_assistant = autosave_assistant;
66 self.memory_state.autosave_min_length = min_length;
67 self
68 }
69
70 #[must_use]
71 pub fn with_tool_call_cutoff(mut self, cutoff: usize) -> Self {
72 self.memory_state.tool_call_cutoff = cutoff;
73 self
74 }
75
76 #[must_use]
77 pub fn with_shutdown_summary_config(
78 mut self,
79 enabled: bool,
80 min_messages: usize,
81 max_messages: usize,
82 timeout_secs: u64,
83 ) -> Self {
84 self.memory_state.shutdown_summary = enabled;
85 self.memory_state.shutdown_summary_min_messages = min_messages;
86 self.memory_state.shutdown_summary_max_messages = max_messages;
87 self.memory_state.shutdown_summary_timeout_secs = timeout_secs;
88 self
89 }
90
91 #[must_use]
92 pub fn with_response_cache(
93 mut self,
94 cache: std::sync::Arc<zeph_memory::ResponseCache>,
95 ) -> Self {
96 self.session.response_cache = Some(cache);
97 self
98 }
99
100 #[must_use]
106 pub fn with_parent_tool_use_id(mut self, id: impl Into<String>) -> Self {
107 self.session.parent_tool_use_id = Some(id.into());
108 self
109 }
110
111 #[must_use]
112 pub fn with_stt(mut self, stt: Box<dyn zeph_llm::stt::SpeechToText>) -> Self {
113 self.providers.stt = Some(stt);
114 self
115 }
116
117 #[must_use]
121 pub fn with_embedding_provider(mut self, provider: AnyProvider) -> Self {
122 self.embedding_provider = provider;
123 self
124 }
125
126 #[must_use]
128 pub fn with_provider_pool(
129 mut self,
130 pool: Vec<ProviderEntry>,
131 snapshot: ProviderConfigSnapshot,
132 ) -> Self {
133 self.providers.provider_pool = pool;
134 self.providers.provider_config_snapshot = Some(snapshot);
135 self
136 }
137
138 #[must_use]
140 pub fn with_debug_dumper(mut self, dumper: crate::debug_dump::DebugDumper) -> Self {
141 self.debug_state.debug_dumper = Some(dumper);
142 self
143 }
144
145 #[must_use]
147 pub fn with_trace_collector(
148 mut self,
149 collector: crate::debug_dump::trace::TracingCollector,
150 ) -> Self {
151 self.debug_state.trace_collector = Some(collector);
152 self
153 }
154
155 #[must_use]
157 pub fn with_trace_config(
158 mut self,
159 dump_dir: std::path::PathBuf,
160 service_name: impl Into<String>,
161 redact: bool,
162 ) -> Self {
163 self.debug_state.dump_dir = Some(dump_dir);
164 self.debug_state.trace_service_name = service_name.into();
165 self.debug_state.trace_redact = redact;
166 self
167 }
168
169 #[cfg(feature = "lsp-context")]
171 #[must_use]
172 pub fn with_lsp_hooks(mut self, runner: crate::lsp_hooks::LspHookRunner) -> Self {
173 self.session.lsp_hooks = Some(runner);
174 self
175 }
176
177 #[must_use]
178 pub fn with_update_notifications(mut self, rx: mpsc::Receiver<String>) -> Self {
179 self.lifecycle.update_notify_rx = Some(rx);
180 self
181 }
182
183 #[must_use]
184 pub fn with_custom_task_rx(mut self, rx: mpsc::Receiver<String>) -> Self {
185 self.lifecycle.custom_task_rx = Some(rx);
186 self
187 }
188
189 #[must_use]
191 pub fn add_tool_executor(
192 mut self,
193 extra: impl zeph_tools::executor::ToolExecutor + 'static,
194 ) -> Self {
195 let existing = Arc::clone(&self.tool_executor);
196 let combined = zeph_tools::CompositeExecutor::new(zeph_tools::DynExecutor(existing), extra);
197 self.tool_executor = Arc::new(combined);
198 self
199 }
200
201 #[must_use]
202 pub fn with_max_tool_iterations(mut self, max: usize) -> Self {
203 self.tool_orchestrator.max_iterations = max;
204 self
205 }
206
207 #[must_use]
209 pub fn with_max_tool_retries(mut self, max: usize) -> Self {
210 self.tool_orchestrator.max_tool_retries = max.min(5);
211 self
212 }
213
214 #[must_use]
216 pub fn with_max_retry_duration_secs(mut self, secs: u64) -> Self {
217 self.tool_orchestrator.max_retry_duration_secs = secs;
218 self
219 }
220
221 #[must_use]
223 pub fn with_parameter_reformat_provider(mut self, provider: impl Into<String>) -> Self {
224 self.tool_orchestrator.parameter_reformat_provider = provider.into();
225 self
226 }
227
228 #[must_use]
230 pub fn with_retry_backoff(mut self, base_ms: u64, max_ms: u64) -> Self {
231 self.tool_orchestrator.retry_base_ms = base_ms;
232 self.tool_orchestrator.retry_max_ms = max_ms;
233 self
234 }
235
236 #[must_use]
239 pub fn with_tool_repeat_threshold(mut self, threshold: usize) -> Self {
240 self.tool_orchestrator.repeat_threshold = threshold;
241 self.tool_orchestrator.recent_tool_calls = VecDeque::with_capacity(2 * threshold.max(1));
242 self
243 }
244
245 #[must_use]
246 pub fn with_memory(
247 mut self,
248 memory: Arc<SemanticMemory>,
249 conversation_id: zeph_memory::ConversationId,
250 history_limit: u32,
251 recall_limit: usize,
252 summarization_threshold: usize,
253 ) -> Self {
254 self.memory_state.memory = Some(memory);
255 self.memory_state.conversation_id = Some(conversation_id);
256 self.memory_state.history_limit = history_limit;
257 self.memory_state.recall_limit = recall_limit;
258 self.memory_state.summarization_threshold = summarization_threshold;
259 self.update_metrics(|m| {
260 m.qdrant_available = false;
261 m.sqlite_conversation_id = Some(conversation_id);
262 });
263 self
264 }
265
266 #[must_use]
267 pub fn with_embedding_model(mut self, model: String) -> Self {
268 self.skill_state.embedding_model = model;
269 self
270 }
271
272 #[must_use]
273 pub fn with_disambiguation_threshold(mut self, threshold: f32) -> Self {
274 self.skill_state.disambiguation_threshold = threshold;
275 self
276 }
277
278 #[must_use]
279 pub fn with_two_stage_matching(mut self, enabled: bool) -> Self {
280 self.skill_state.two_stage_matching = enabled;
281 self
282 }
283
284 #[must_use]
285 pub fn with_confusability_threshold(mut self, threshold: f32) -> Self {
286 self.skill_state.confusability_threshold = threshold.clamp(0.0, 1.0);
287 self
288 }
289
290 #[must_use]
291 pub fn with_skill_prompt_mode(mut self, mode: crate::config::SkillPromptMode) -> Self {
292 self.skill_state.prompt_mode = mode;
293 self
294 }
295
296 #[must_use]
297 pub fn with_document_config(mut self, config: crate::config::DocumentConfig) -> Self {
298 self.memory_state.document_config = config;
299 self
300 }
301
302 #[must_use]
303 pub fn with_compression_guidelines_config(
304 mut self,
305 config: zeph_memory::CompressionGuidelinesConfig,
306 ) -> Self {
307 self.memory_state.compression_guidelines_config = config;
308 self
309 }
310
311 #[must_use]
312 pub fn with_digest_config(mut self, config: crate::config::DigestConfig) -> Self {
313 self.memory_state.digest_config = config;
314 self
315 }
316
317 #[must_use]
318 pub fn with_context_strategy(
319 mut self,
320 strategy: crate::config::ContextStrategy,
321 crossover_turn_threshold: u32,
322 ) -> Self {
323 self.memory_state.context_strategy = strategy;
324 self.memory_state.crossover_turn_threshold = crossover_turn_threshold;
325 self
326 }
327
328 #[must_use]
329 pub fn with_graph_config(mut self, config: crate::config::GraphConfig) -> Self {
330 if config.enabled {
333 tracing::warn!(
334 "graph-memory is enabled: extracted entities are stored without PII redaction. \
335 Do not use with sensitive personal data until redaction is implemented."
336 );
337 }
338 if config.rpe.enabled {
340 self.memory_state.rpe_router = Some(std::sync::Mutex::new(
341 zeph_memory::RpeRouter::new(config.rpe.threshold, config.rpe.max_skip_turns),
342 ));
343 } else {
344 self.memory_state.rpe_router = None;
345 }
346 self.memory_state.graph_config = config;
347 self
348 }
349
350 #[must_use]
351 pub fn with_anomaly_detector(mut self, detector: zeph_tools::AnomalyDetector) -> Self {
352 self.debug_state.anomaly_detector = Some(detector);
353 self
354 }
355
356 #[must_use]
357 pub fn with_instruction_blocks(
358 mut self,
359 blocks: Vec<crate::instructions::InstructionBlock>,
360 ) -> Self {
361 self.instructions.blocks = blocks;
362 self
363 }
364
365 #[must_use]
366 pub fn with_instruction_reload(
367 mut self,
368 rx: mpsc::Receiver<InstructionEvent>,
369 state: InstructionReloadState,
370 ) -> Self {
371 self.instructions.reload_rx = Some(rx);
372 self.instructions.reload_state = Some(state);
373 self
374 }
375
376 #[must_use]
377 pub fn with_shutdown(mut self, rx: watch::Receiver<bool>) -> Self {
378 self.lifecycle.shutdown = rx;
379 self
380 }
381
382 #[must_use]
383 pub fn with_skill_reload(
384 mut self,
385 paths: Vec<PathBuf>,
386 rx: mpsc::Receiver<SkillEvent>,
387 ) -> Self {
388 self.skill_state.skill_paths = paths;
389 self.skill_state.skill_reload_rx = Some(rx);
390 self
391 }
392
393 #[must_use]
394 pub fn with_managed_skills_dir(mut self, dir: PathBuf) -> Self {
395 self.skill_state.managed_dir = Some(dir);
396 self
397 }
398
399 #[must_use]
400 pub fn with_trust_config(mut self, config: crate::config::TrustConfig) -> Self {
401 self.skill_state.trust_config = config;
402 self
403 }
404
405 #[must_use]
406 pub fn with_config_reload(mut self, path: PathBuf, rx: mpsc::Receiver<ConfigEvent>) -> Self {
407 self.lifecycle.config_path = Some(path);
408 self.lifecycle.config_reload_rx = Some(rx);
409 self
410 }
411
412 #[must_use]
413 pub fn with_logging_config(mut self, logging: crate::config::LoggingConfig) -> Self {
414 self.debug_state.logging_config = logging;
415 self
416 }
417
418 #[must_use]
419 pub fn with_available_secrets(
420 mut self,
421 secrets: impl IntoIterator<Item = (String, crate::vault::Secret)>,
422 ) -> Self {
423 self.skill_state.available_custom_secrets = secrets.into_iter().collect();
424 self
425 }
426
427 #[must_use]
431 pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
432 self.skill_state.hybrid_search = enabled;
433 if enabled {
434 let reg = self
435 .skill_state
436 .registry
437 .read()
438 .expect("registry read lock");
439 let all_meta = reg.all_meta();
440 let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
441 self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
442 }
443 self
444 }
445
446 #[must_use]
447 pub fn with_learning(mut self, config: LearningConfig) -> Self {
448 if config.correction_detection {
449 self.feedback.detector = super::feedback_detector::FeedbackDetector::new(
450 config.correction_confidence_threshold,
451 );
452 if config.detector_mode == crate::config::DetectorMode::Judge {
453 self.feedback.judge = Some(super::feedback_detector::JudgeDetector::new(
454 config.judge_adaptive_low,
455 config.judge_adaptive_high,
456 ));
457 }
458 }
459 self.learning_engine.config = Some(config);
460 self
461 }
462
463 #[must_use]
469 pub fn with_llm_classifier(
470 mut self,
471 classifier: zeph_llm::classifier::llm::LlmClassifier,
472 ) -> Self {
473 #[cfg(feature = "classifiers")]
475 let classifier = if let Some(ref m) = self.metrics.classifier_metrics {
476 classifier.with_metrics(std::sync::Arc::clone(m))
477 } else {
478 classifier
479 };
480 self.feedback.llm_classifier = Some(classifier);
481 self
482 }
483
484 #[must_use]
485 pub fn with_judge_provider(mut self, provider: AnyProvider) -> Self {
486 self.providers.judge_provider = Some(provider);
487 self
488 }
489
490 #[must_use]
491 pub fn with_probe_provider(mut self, provider: AnyProvider) -> Self {
492 self.providers.probe_provider = Some(provider);
493 self
494 }
495
496 #[must_use]
500 #[cfg(feature = "context-compression")]
501 pub fn with_compress_provider(mut self, provider: AnyProvider) -> Self {
502 self.providers.compress_provider = Some(provider);
503 self
504 }
505
506 #[must_use]
507 pub fn with_planner_provider(mut self, provider: AnyProvider) -> Self {
508 self.orchestration.planner_provider = Some(provider);
509 self
510 }
511
512 #[must_use]
516 pub fn with_verify_provider(mut self, provider: AnyProvider) -> Self {
517 self.orchestration.verify_provider = Some(provider);
518 self
519 }
520
521 #[must_use]
525 pub fn with_server_compaction(mut self, enabled: bool) -> Self {
526 self.providers.server_compaction_active = enabled;
527 self
528 }
529
530 #[must_use]
531 pub fn with_mcp(
532 mut self,
533 tools: Vec<zeph_mcp::McpTool>,
534 registry: Option<zeph_mcp::McpToolRegistry>,
535 manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
536 mcp_config: &crate::config::McpConfig,
537 ) -> Self {
538 self.mcp.tools = tools;
539 self.mcp.registry = registry;
540 self.mcp.manager = manager;
541 self.mcp
542 .allowed_commands
543 .clone_from(&mcp_config.allowed_commands);
544 self.mcp.max_dynamic = mcp_config.max_dynamic_servers;
545 self.mcp.elicitation_warn_sensitive_fields = mcp_config.elicitation_warn_sensitive_fields;
546 self
547 }
548
549 #[must_use]
550 pub fn with_mcp_server_outcomes(
551 mut self,
552 outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
553 ) -> Self {
554 self.mcp.server_outcomes = outcomes;
555 self
556 }
557
558 #[must_use]
559 pub fn with_mcp_shared_tools(
560 mut self,
561 shared: std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>,
562 ) -> Self {
563 self.mcp.shared_tools = Some(shared);
564 self
565 }
566
567 #[must_use]
573 pub fn with_mcp_pruning(
574 mut self,
575 params: zeph_mcp::PruningParams,
576 enabled: bool,
577 pruning_provider: Option<zeph_llm::any::AnyProvider>,
578 ) -> Self {
579 self.mcp.pruning_params = params;
580 self.mcp.pruning_enabled = enabled;
581 self.mcp.pruning_provider = pruning_provider;
582 self
583 }
584
585 #[must_use]
590 pub fn with_mcp_discovery(
591 mut self,
592 strategy: zeph_mcp::ToolDiscoveryStrategy,
593 params: zeph_mcp::DiscoveryParams,
594 discovery_provider: Option<zeph_llm::any::AnyProvider>,
595 ) -> Self {
596 self.mcp.discovery_strategy = strategy;
597 self.mcp.discovery_params = params;
598 self.mcp.discovery_provider = discovery_provider;
599 self
600 }
601
602 #[must_use]
606 pub fn with_mcp_tool_rx(
607 mut self,
608 rx: tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>,
609 ) -> Self {
610 self.mcp.tool_rx = Some(rx);
611 self
612 }
613
614 #[must_use]
619 pub fn with_mcp_elicitation_rx(
620 mut self,
621 rx: tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>,
622 ) -> Self {
623 self.mcp.elicitation_rx = Some(rx);
624 self
625 }
626
627 #[must_use]
628 pub fn with_security(mut self, security: SecurityConfig, timeouts: TimeoutConfig) -> Self {
629 self.security.sanitizer =
630 zeph_sanitizer::ContentSanitizer::new(&security.content_isolation);
631 self.security.exfiltration_guard = zeph_sanitizer::exfiltration::ExfiltrationGuard::new(
632 security.exfiltration_guard.clone(),
633 );
634 self.security.pii_filter = zeph_sanitizer::pii::PiiFilter::new(security.pii_filter.clone());
635 self.security.memory_validator =
636 zeph_sanitizer::memory_validation::MemoryWriteValidator::new(
637 security.memory_validation.clone(),
638 );
639 self.runtime.rate_limiter =
640 crate::agent::rate_limiter::ToolRateLimiter::new(security.rate_limit.clone());
641
642 let mut verifiers: Vec<Box<dyn zeph_tools::PreExecutionVerifier>> = Vec::new();
647 if security.pre_execution_verify.enabled {
648 let dcfg = &security.pre_execution_verify.destructive_commands;
649 if dcfg.enabled {
650 verifiers.push(Box::new(zeph_tools::DestructiveCommandVerifier::new(dcfg)));
651 }
652 let icfg = &security.pre_execution_verify.injection_patterns;
653 if icfg.enabled {
654 verifiers.push(Box::new(zeph_tools::InjectionPatternVerifier::new(icfg)));
655 }
656 let ucfg = &security.pre_execution_verify.url_grounding;
657 if ucfg.enabled {
658 verifiers.push(Box::new(zeph_tools::UrlGroundingVerifier::new(
659 ucfg,
660 std::sync::Arc::clone(&self.security.user_provided_urls),
661 )));
662 }
663 let fcfg = &security.pre_execution_verify.firewall;
664 if fcfg.enabled {
665 verifiers.push(Box::new(zeph_tools::FirewallVerifier::new(fcfg)));
666 }
667 }
668 self.tool_orchestrator.pre_execution_verifiers = verifiers;
669
670 self.security.response_verifier = zeph_sanitizer::response_verifier::ResponseVerifier::new(
671 security.response_verification.clone(),
672 );
673
674 self.runtime.security = security;
675 self.runtime.timeouts = timeouts;
676 self
677 }
678
679 #[must_use]
681 pub fn with_audit_logger(mut self, logger: std::sync::Arc<zeph_tools::AuditLogger>) -> Self {
682 self.tool_orchestrator.audit_logger = Some(logger);
683 self
684 }
685
686 #[must_use]
687 pub fn with_redact_credentials(mut self, enabled: bool) -> Self {
688 self.runtime.redact_credentials = enabled;
689 self
690 }
691
692 #[must_use]
693 pub fn with_tool_summarization(mut self, enabled: bool) -> Self {
694 self.tool_orchestrator.summarize_tool_output_enabled = enabled;
695 self
696 }
697
698 #[must_use]
699 pub fn with_overflow_config(mut self, config: zeph_tools::OverflowConfig) -> Self {
700 self.tool_orchestrator.overflow_config = config;
701 self
702 }
703
704 #[must_use]
708 pub fn with_tafc_config(mut self, config: zeph_tools::TafcConfig) -> Self {
709 self.tool_orchestrator.tafc = config.validated();
710 self
711 }
712
713 #[must_use]
714 pub fn with_result_cache_config(mut self, config: &zeph_tools::ResultCacheConfig) -> Self {
715 self.tool_orchestrator.set_cache_config(config);
716 self
717 }
718
719 #[must_use]
720 pub fn with_summary_provider(mut self, provider: AnyProvider) -> Self {
721 self.providers.summary_provider = Some(provider);
722 self
723 }
724
725 #[must_use]
726 pub fn with_quarantine_summarizer(
727 mut self,
728 qs: zeph_sanitizer::quarantine::QuarantinedSummarizer,
729 ) -> Self {
730 self.security.quarantine_summarizer = Some(qs);
731 self
732 }
733
734 #[must_use]
738 pub fn with_acp_session(mut self, is_acp: bool) -> Self {
739 self.security.is_acp_session = is_acp;
740 self
741 }
742
743 #[cfg(feature = "classifiers")]
748 #[must_use]
749 pub fn with_injection_classifier(
750 mut self,
751 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
752 timeout_ms: u64,
753 threshold: f32,
754 threshold_soft: f32,
755 ) -> Self {
756 let old = std::mem::replace(
758 &mut self.security.sanitizer,
759 zeph_sanitizer::ContentSanitizer::new(
760 &zeph_sanitizer::ContentIsolationConfig::default(),
761 ),
762 );
763 self.security.sanitizer = old
764 .with_classifier(backend, timeout_ms, threshold)
765 .with_injection_threshold_soft(threshold_soft);
766 self
767 }
768
769 #[cfg(feature = "classifiers")]
774 #[must_use]
775 pub fn with_enforcement_mode(mut self, mode: zeph_config::InjectionEnforcementMode) -> Self {
776 let old = std::mem::replace(
777 &mut self.security.sanitizer,
778 zeph_sanitizer::ContentSanitizer::new(
779 &zeph_sanitizer::ContentIsolationConfig::default(),
780 ),
781 );
782 self.security.sanitizer = old.with_enforcement_mode(mode);
783 self
784 }
785
786 #[cfg(feature = "classifiers")]
788 #[must_use]
789 pub fn with_three_class_classifier(
790 mut self,
791 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
792 threshold: f32,
793 ) -> Self {
794 let old = std::mem::replace(
795 &mut self.security.sanitizer,
796 zeph_sanitizer::ContentSanitizer::new(
797 &zeph_sanitizer::ContentIsolationConfig::default(),
798 ),
799 );
800 self.security.sanitizer = old.with_three_class_backend(backend, threshold);
801 self
802 }
803
804 #[must_use]
808 pub fn with_causal_analyzer(
809 mut self,
810 analyzer: zeph_sanitizer::causal_ipi::TurnCausalAnalyzer,
811 ) -> Self {
812 self.security.causal_analyzer = Some(analyzer);
813 self
814 }
815
816 #[cfg(feature = "classifiers")]
820 #[must_use]
821 pub fn with_scan_user_input(mut self, value: bool) -> Self {
822 let old = std::mem::replace(
823 &mut self.security.sanitizer,
824 zeph_sanitizer::ContentSanitizer::new(
825 &zeph_sanitizer::ContentIsolationConfig::default(),
826 ),
827 );
828 self.security.sanitizer = old.with_scan_user_input(value);
829 self
830 }
831
832 #[cfg(feature = "classifiers")]
837 #[must_use]
838 pub fn with_pii_detector(
839 mut self,
840 detector: std::sync::Arc<dyn zeph_llm::classifier::PiiDetector>,
841 threshold: f32,
842 ) -> Self {
843 let old = std::mem::replace(
844 &mut self.security.sanitizer,
845 zeph_sanitizer::ContentSanitizer::new(
846 &zeph_sanitizer::ContentIsolationConfig::default(),
847 ),
848 );
849 self.security.sanitizer = old.with_pii_detector(detector, threshold);
850 self
851 }
852
853 #[cfg(feature = "classifiers")]
858 #[must_use]
859 pub fn with_pii_ner_allowlist(mut self, entries: Vec<String>) -> Self {
860 let old = std::mem::replace(
861 &mut self.security.sanitizer,
862 zeph_sanitizer::ContentSanitizer::new(
863 &zeph_sanitizer::ContentIsolationConfig::default(),
864 ),
865 );
866 self.security.sanitizer = old.with_pii_ner_allowlist(entries);
867 self
868 }
869
870 #[cfg(feature = "classifiers")]
875 #[must_use]
876 pub fn with_pii_ner_classifier(
877 mut self,
878 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
879 timeout_ms: u64,
880 max_chars: usize,
881 ) -> Self {
882 self.security.pii_ner_backend = Some(backend);
883 self.security.pii_ner_timeout_ms = timeout_ms;
884 self.security.pii_ner_max_chars = max_chars;
885 self
886 }
887
888 #[cfg(feature = "classifiers")]
894 #[must_use]
895 pub fn with_classifier_metrics(
896 mut self,
897 metrics: std::sync::Arc<zeph_llm::ClassifierMetrics>,
898 ) -> Self {
899 let old = std::mem::replace(
901 &mut self.security.sanitizer,
902 zeph_sanitizer::ContentSanitizer::new(
903 &zeph_sanitizer::ContentIsolationConfig::default(),
904 ),
905 );
906 self.security.sanitizer = old.with_classifier_metrics(std::sync::Arc::clone(&metrics));
907 self.metrics.classifier_metrics = Some(metrics);
909 self
910 }
911
912 #[cfg(feature = "guardrail")]
913 #[must_use]
914 pub fn with_guardrail(mut self, filter: zeph_sanitizer::guardrail::GuardrailFilter) -> Self {
915 use zeph_sanitizer::guardrail::GuardrailAction;
916 let warn_mode = filter.action() == GuardrailAction::Warn;
917 self.security.guardrail = Some(filter);
918 self.update_metrics(|m| {
919 m.guardrail_enabled = true;
920 m.guardrail_warn_mode = warn_mode;
921 });
922 self
923 }
924
925 pub(super) fn summary_or_primary_provider(&self) -> &AnyProvider {
926 self.providers
927 .summary_provider
928 .as_ref()
929 .unwrap_or(&self.provider)
930 }
931
932 pub(super) fn probe_or_summary_provider(&self) -> &AnyProvider {
933 self.providers
934 .probe_provider
935 .as_ref()
936 .or(self.providers.summary_provider.as_ref())
937 .unwrap_or(&self.provider)
938 }
939
940 pub(super) fn last_assistant_response(&self) -> String {
942 self.msg
943 .messages
944 .iter()
945 .rev()
946 .find(|m| m.role == zeph_llm::provider::Role::Assistant)
947 .map(|m| super::context::truncate_chars(&m.content, 500))
948 .unwrap_or_default()
949 }
950
951 #[must_use]
952 pub fn with_permission_policy(mut self, policy: zeph_tools::PermissionPolicy) -> Self {
953 self.runtime.permission_policy = policy;
954 self
955 }
956
957 #[must_use]
958 pub fn with_context_budget(
959 mut self,
960 budget_tokens: usize,
961 reserve_ratio: f32,
962 hard_compaction_threshold: f32,
963 compaction_preserve_tail: usize,
964 prune_protect_tokens: usize,
965 ) -> Self {
966 if budget_tokens == 0 {
967 tracing::warn!("context budget is 0 — agent will have no token tracking");
968 }
969 if budget_tokens > 0 {
970 self.context_manager.budget = Some(ContextBudget::new(budget_tokens, reserve_ratio));
971 }
972 self.context_manager.hard_compaction_threshold = hard_compaction_threshold;
973 self.context_manager.compaction_preserve_tail = compaction_preserve_tail;
974 self.context_manager.prune_protect_tokens = prune_protect_tokens;
975 self
976 }
977
978 #[must_use]
979 pub fn with_soft_compaction_threshold(mut self, threshold: f32) -> Self {
980 self.context_manager.soft_compaction_threshold = threshold;
981 self
982 }
983
984 #[must_use]
989 pub fn with_compaction_cooldown(mut self, cooldown_turns: u8) -> Self {
990 self.context_manager.compaction_cooldown_turns = cooldown_turns;
991 self
992 }
993
994 #[must_use]
995 pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
996 self.context_manager.compression = compression;
997 self
998 }
999
1000 #[must_use]
1002 pub fn with_focus_config(mut self, config: crate::config::FocusConfig) -> Self {
1003 self.focus = super::focus::FocusState::new(config);
1004 self
1005 }
1006
1007 #[must_use]
1009 pub fn with_sidequest_config(mut self, config: crate::config::SidequestConfig) -> Self {
1010 self.sidequest = super::sidequest::SidequestState::new(config);
1011 self
1012 }
1013
1014 #[must_use]
1015 pub fn with_routing(mut self, routing: StoreRoutingConfig) -> Self {
1016 self.context_manager.routing = routing;
1017 self
1018 }
1019
1020 #[must_use]
1021 pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
1022 self.runtime.model_name = name.into();
1023 self
1024 }
1025
1026 #[must_use]
1031 pub fn with_active_provider_name(mut self, name: impl Into<String>) -> Self {
1032 self.runtime.active_provider_name = name.into();
1033 self
1034 }
1035
1036 #[must_use]
1037 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
1038 let path = path.into();
1039 self.session.env_context =
1040 crate::context::EnvironmentContext::gather_for_dir(&self.runtime.model_name, &path);
1041 self
1042 }
1043
1044 #[must_use]
1050 pub fn with_hooks_config(mut self, config: &zeph_config::HooksConfig) -> Self {
1051 self.session
1052 .hooks_config
1053 .cwd_changed
1054 .clone_from(&config.cwd_changed);
1055
1056 if let Some(ref fc) = config.file_changed {
1057 self.session
1058 .hooks_config
1059 .file_changed_hooks
1060 .clone_from(&fc.hooks);
1061
1062 if !fc.watch_paths.is_empty() {
1063 let (tx, rx) = tokio::sync::mpsc::channel(64);
1064 match crate::file_watcher::FileChangeWatcher::start(
1065 &fc.watch_paths,
1066 fc.debounce_ms,
1067 tx,
1068 ) {
1069 Ok(watcher) => {
1070 self.lifecycle.file_watcher = Some(watcher);
1071 self.lifecycle.file_changed_rx = Some(rx);
1072 tracing::info!(
1073 paths = ?fc.watch_paths,
1074 debounce_ms = fc.debounce_ms,
1075 "file change watcher started"
1076 );
1077 }
1078 Err(e) => {
1079 tracing::warn!(error = %e, "failed to start file change watcher");
1080 }
1081 }
1082 }
1083 }
1084
1085 let cwd_str = &self.session.env_context.working_dir;
1087 if !cwd_str.is_empty() {
1088 self.lifecycle.last_known_cwd = std::path::PathBuf::from(cwd_str);
1089 }
1090
1091 self
1092 }
1093
1094 #[must_use]
1095 pub fn with_warmup_ready(mut self, rx: watch::Receiver<bool>) -> Self {
1096 self.lifecycle.warmup_ready = Some(rx);
1097 self
1098 }
1099
1100 #[must_use]
1101 pub fn with_cost_tracker(mut self, tracker: CostTracker) -> Self {
1102 self.metrics.cost_tracker = Some(tracker);
1103 self
1104 }
1105
1106 #[must_use]
1107 pub fn with_extended_context(mut self, enabled: bool) -> Self {
1108 self.metrics.extended_context = enabled;
1109 self
1110 }
1111
1112 #[must_use]
1113 pub fn with_repo_map(mut self, token_budget: usize, ttl_secs: u64) -> Self {
1114 self.index.repo_map_tokens = token_budget;
1115 self.index.repo_map_ttl = std::time::Duration::from_secs(ttl_secs);
1116 self
1117 }
1118
1119 #[must_use]
1120 pub fn with_code_retriever(
1121 mut self,
1122 retriever: std::sync::Arc<zeph_index::retriever::CodeRetriever>,
1123 ) -> Self {
1124 self.index.retriever = Some(retriever);
1125 self
1126 }
1127
1128 #[must_use]
1132 pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
1133 let provider_name = if self.runtime.active_provider_name.is_empty() {
1134 self.provider.name().to_owned()
1135 } else {
1136 self.runtime.active_provider_name.clone()
1137 };
1138 let model_name = self.runtime.model_name.clone();
1139 let total_skills = self
1140 .skill_state
1141 .registry
1142 .read()
1143 .expect("registry read lock")
1144 .all_meta()
1145 .len();
1146 let qdrant_available = false;
1147 let conversation_id = self.memory_state.conversation_id;
1148 let prompt_estimate = self
1149 .msg
1150 .messages
1151 .first()
1152 .map_or(0, |m| u64::try_from(m.content.len()).unwrap_or(0) / 4);
1153 let mcp_tool_count = self.mcp.tools.len();
1154 let mcp_server_count = if self.mcp.server_outcomes.is_empty() {
1155 self.mcp
1157 .tools
1158 .iter()
1159 .map(|t| &t.server_id)
1160 .collect::<std::collections::HashSet<_>>()
1161 .len()
1162 } else {
1163 self.mcp.server_outcomes.len()
1164 };
1165 let mcp_connected_count = if self.mcp.server_outcomes.is_empty() {
1166 mcp_server_count
1167 } else {
1168 self.mcp
1169 .server_outcomes
1170 .iter()
1171 .filter(|o| o.connected)
1172 .count()
1173 };
1174 let mcp_servers: Vec<crate::metrics::McpServerStatus> = self
1175 .mcp
1176 .server_outcomes
1177 .iter()
1178 .map(|o| crate::metrics::McpServerStatus {
1179 id: o.id.clone(),
1180 status: if o.connected {
1181 crate::metrics::McpServerConnectionStatus::Connected
1182 } else {
1183 crate::metrics::McpServerConnectionStatus::Failed
1184 },
1185 tool_count: o.tool_count,
1186 error: o.error.clone(),
1187 })
1188 .collect();
1189 let extended_context = self.metrics.extended_context;
1190 tx.send_modify(|m| {
1191 m.provider_name = provider_name;
1192 m.model_name = model_name;
1193 m.total_skills = total_skills;
1194 m.qdrant_available = qdrant_available;
1195 m.sqlite_conversation_id = conversation_id;
1196 m.context_tokens = prompt_estimate;
1197 m.prompt_tokens = prompt_estimate;
1198 m.total_tokens = prompt_estimate;
1199 m.mcp_tool_count = mcp_tool_count;
1200 m.mcp_server_count = mcp_server_count;
1201 m.mcp_connected_count = mcp_connected_count;
1202 m.mcp_servers = mcp_servers;
1203 m.extended_context = extended_context;
1204 });
1205 self.metrics.metrics_tx = Some(tx);
1206 self
1207 }
1208
1209 #[must_use]
1213 pub fn cancel_signal(&self) -> Arc<Notify> {
1214 Arc::clone(&self.lifecycle.cancel_signal)
1215 }
1216
1217 #[must_use]
1220 pub fn with_cancel_signal(mut self, signal: Arc<Notify>) -> Self {
1221 self.lifecycle.cancel_signal = signal;
1222 self
1223 }
1224
1225 #[must_use]
1226 pub fn with_subagent_manager(mut self, manager: crate::subagent::SubAgentManager) -> Self {
1227 self.orchestration.subagent_manager = Some(manager);
1228 self
1229 }
1230
1231 #[must_use]
1232 pub fn with_subagent_config(mut self, config: crate::config::SubAgentConfig) -> Self {
1233 self.orchestration.subagent_config = config;
1234 self
1235 }
1236
1237 #[must_use]
1238 pub fn with_orchestration_config(mut self, config: crate::config::OrchestrationConfig) -> Self {
1239 self.orchestration.orchestration_config = config;
1240 self
1241 }
1242
1243 #[cfg(feature = "experiments")]
1245 #[must_use]
1246 pub fn with_experiment_config(mut self, config: crate::config::ExperimentConfig) -> Self {
1247 self.experiments.config = config;
1248 self
1249 }
1250
1251 #[cfg(feature = "experiments")]
1257 #[must_use]
1258 pub fn with_experiment_baseline(
1259 mut self,
1260 baseline: crate::experiments::ConfigSnapshot,
1261 ) -> Self {
1262 self.experiments.baseline = baseline;
1263 self
1264 }
1265
1266 #[cfg(feature = "experiments")]
1271 #[must_use]
1272 pub fn with_eval_provider(mut self, provider: AnyProvider) -> Self {
1273 self.experiments.eval_provider = Some(provider);
1274 self
1275 }
1276
1277 #[must_use]
1280 pub fn with_provider_override(
1281 mut self,
1282 slot: Arc<std::sync::RwLock<Option<AnyProvider>>>,
1283 ) -> Self {
1284 self.providers.provider_override = Some(slot);
1285 self
1286 }
1287
1288 #[must_use]
1290 pub fn with_tool_schema_filter(mut self, filter: zeph_tools::ToolSchemaFilter) -> Self {
1291 self.tool_schema_filter = Some(filter);
1292 self
1293 }
1294
1295 #[must_use]
1297 pub fn with_dependency_config(mut self, config: zeph_tools::DependencyConfig) -> Self {
1298 self.runtime.dependency_config = config;
1299 self
1300 }
1301
1302 #[must_use]
1307 pub fn with_tool_dependency_graph(
1308 mut self,
1309 graph: zeph_tools::ToolDependencyGraph,
1310 always_on: std::collections::HashSet<String>,
1311 ) -> Self {
1312 self.dependency_graph = Some(graph);
1313 self.dependency_always_on = always_on;
1314 self
1315 }
1316
1317 pub async fn maybe_init_tool_schema_filter(
1322 mut self,
1323 config: &crate::config::ToolFilterConfig,
1324 provider: &zeph_llm::any::AnyProvider,
1325 ) -> Self {
1326 use zeph_llm::provider::LlmProvider;
1327
1328 if !config.enabled {
1329 return self;
1330 }
1331
1332 let always_on_set: std::collections::HashSet<&str> =
1333 config.always_on.iter().map(String::as_str).collect();
1334 let defs = self.tool_executor.tool_definitions_erased();
1335 let filterable: Vec<&zeph_tools::registry::ToolDef> = defs
1336 .iter()
1337 .filter(|d| !always_on_set.contains(d.id.as_ref()))
1338 .collect();
1339
1340 if filterable.is_empty() {
1341 tracing::info!("tool schema filter: all tools are always-on, nothing to filter");
1342 return self;
1343 }
1344
1345 let mut embeddings = Vec::with_capacity(filterable.len());
1346 for def in &filterable {
1347 let text = format!("{}: {}", def.id, def.description);
1348 match provider.embed(&text).await {
1349 Ok(emb) => {
1350 embeddings.push(zeph_tools::ToolEmbedding {
1351 tool_id: def.id.to_string(),
1352 embedding: emb,
1353 });
1354 }
1355 Err(e) => {
1356 tracing::info!(
1357 provider = provider.name(),
1358 "tool schema filter disabled: embedding not supported \
1359 by provider ({e:#})"
1360 );
1361 return self;
1362 }
1363 }
1364 }
1365
1366 tracing::info!(
1367 tool_count = embeddings.len(),
1368 always_on = config.always_on.len(),
1369 top_k = config.top_k,
1370 "tool schema filter initialized"
1371 );
1372
1373 let filter = zeph_tools::ToolSchemaFilter::new(
1374 config.always_on.clone(),
1375 config.top_k,
1376 config.min_description_words,
1377 embeddings,
1378 );
1379 self.tool_schema_filter = Some(filter);
1380 self
1381 }
1382
1383 #[must_use]
1391 pub fn apply_session_config(mut self, cfg: AgentSessionConfig) -> Self {
1392 let AgentSessionConfig {
1393 max_tool_iterations,
1394 max_tool_retries,
1395 max_retry_duration_secs,
1396 retry_base_ms,
1397 retry_max_ms,
1398 parameter_reformat_provider,
1399 tool_repeat_threshold,
1400 tool_summarization,
1401 tool_call_cutoff,
1402 overflow_config,
1403 permission_policy,
1404 model_name,
1405 embed_model,
1406 semantic_cache_enabled,
1407 semantic_cache_threshold,
1408 semantic_cache_max_candidates,
1409 budget_tokens,
1410 soft_compaction_threshold,
1411 hard_compaction_threshold,
1412 compaction_preserve_tail,
1413 compaction_cooldown_turns,
1414 prune_protect_tokens,
1415 redact_credentials,
1416 security,
1417 timeouts,
1418 learning,
1419 document_config,
1420 graph_config,
1421 anomaly_config,
1422 result_cache_config,
1423 utility_config,
1424 orchestration_config,
1425 debug_config: _debug_config,
1428 server_compaction,
1429 secrets,
1430 } = cfg;
1431
1432 self = self
1433 .with_max_tool_iterations(max_tool_iterations)
1434 .with_max_tool_retries(max_tool_retries)
1435 .with_max_retry_duration_secs(max_retry_duration_secs)
1436 .with_retry_backoff(retry_base_ms, retry_max_ms)
1437 .with_parameter_reformat_provider(parameter_reformat_provider)
1438 .with_tool_repeat_threshold(tool_repeat_threshold)
1439 .with_model_name(model_name)
1440 .with_embedding_model(embed_model)
1441 .with_context_budget(
1442 budget_tokens,
1443 CONTEXT_BUDGET_RESERVE_RATIO,
1444 hard_compaction_threshold,
1445 compaction_preserve_tail,
1446 prune_protect_tokens,
1447 )
1448 .with_soft_compaction_threshold(soft_compaction_threshold)
1449 .with_compaction_cooldown(compaction_cooldown_turns)
1450 .with_security(security, timeouts)
1451 .with_redact_credentials(redact_credentials)
1452 .with_tool_summarization(tool_summarization)
1453 .with_overflow_config(overflow_config)
1454 .with_permission_policy(permission_policy)
1455 .with_learning(learning)
1456 .with_tool_call_cutoff(tool_call_cutoff)
1457 .with_available_secrets(
1458 secrets
1459 .iter()
1460 .map(|(k, v)| (k.clone(), crate::vault::Secret::new(v.expose().to_owned()))),
1461 )
1462 .with_server_compaction(server_compaction)
1463 .with_document_config(document_config)
1464 .with_graph_config(graph_config)
1465 .with_orchestration_config(orchestration_config);
1466
1467 self.debug_state.reasoning_model_warning = anomaly_config.reasoning_model_warning;
1468 if anomaly_config.enabled {
1469 self = self.with_anomaly_detector(zeph_tools::AnomalyDetector::new(
1470 anomaly_config.window_size,
1471 anomaly_config.error_threshold,
1472 anomaly_config.critical_threshold,
1473 ));
1474 }
1475
1476 self.runtime.semantic_cache_enabled = semantic_cache_enabled;
1477 self.runtime.semantic_cache_threshold = semantic_cache_threshold;
1478 self.runtime.semantic_cache_max_candidates = semantic_cache_max_candidates;
1479 self = self.with_result_cache_config(&result_cache_config);
1480 self.tool_orchestrator.set_utility_config(utility_config);
1481
1482 self
1483 }
1484}
1485
1486#[cfg(test)]
1487mod tests {
1488 use super::super::agent_tests::{
1489 MockChannel, MockToolExecutor, create_test_registry, mock_provider,
1490 };
1491 use super::*;
1492 use crate::config::{CompressionStrategy, StoreRoutingConfig, StoreRoutingStrategy};
1493
1494 fn make_agent() -> Agent<MockChannel> {
1495 Agent::new(
1496 mock_provider(vec![]),
1497 MockChannel::new(vec![]),
1498 create_test_registry(),
1499 None,
1500 5,
1501 MockToolExecutor::no_tools(),
1502 )
1503 }
1504
1505 #[test]
1506 fn with_compression_sets_proactive_strategy() {
1507 let compression = CompressionConfig {
1508 strategy: CompressionStrategy::Proactive {
1509 threshold_tokens: 50_000,
1510 max_summary_tokens: 2_000,
1511 },
1512 model: String::new(),
1513 pruning_strategy: crate::config::PruningStrategy::default(),
1514 probe: zeph_memory::CompactionProbeConfig::default(),
1515 compress_provider: String::new(),
1516 archive_tool_outputs: false,
1517 };
1518 let agent = make_agent().with_compression(compression);
1519 assert!(
1520 matches!(
1521 agent.context_manager.compression.strategy,
1522 CompressionStrategy::Proactive {
1523 threshold_tokens: 50_000,
1524 max_summary_tokens: 2_000,
1525 }
1526 ),
1527 "expected Proactive strategy after with_compression"
1528 );
1529 }
1530
1531 #[test]
1532 fn with_routing_sets_routing_config() {
1533 let routing = StoreRoutingConfig {
1534 strategy: StoreRoutingStrategy::Heuristic,
1535 ..StoreRoutingConfig::default()
1536 };
1537 let agent = make_agent().with_routing(routing);
1538 assert_eq!(
1539 agent.context_manager.routing.strategy,
1540 StoreRoutingStrategy::Heuristic,
1541 "routing strategy must be set by with_routing"
1542 );
1543 }
1544
1545 #[test]
1546 fn default_compression_is_reactive() {
1547 let agent = make_agent();
1548 assert_eq!(
1549 agent.context_manager.compression.strategy,
1550 CompressionStrategy::Reactive,
1551 "default compression strategy must be Reactive"
1552 );
1553 }
1554
1555 #[test]
1556 fn default_routing_is_heuristic() {
1557 let agent = make_agent();
1558 assert_eq!(
1559 agent.context_manager.routing.strategy,
1560 StoreRoutingStrategy::Heuristic,
1561 "default routing strategy must be Heuristic"
1562 );
1563 }
1564
1565 #[test]
1566 fn with_cancel_signal_replaces_internal_signal() {
1567 let agent = Agent::new(
1568 mock_provider(vec![]),
1569 MockChannel::new(vec![]),
1570 create_test_registry(),
1571 None,
1572 5,
1573 MockToolExecutor::no_tools(),
1574 );
1575
1576 let shared = Arc::new(Notify::new());
1577 let agent = agent.with_cancel_signal(Arc::clone(&shared));
1578
1579 assert!(Arc::ptr_eq(&shared, &agent.cancel_signal()));
1581 }
1582
1583 #[tokio::test]
1588 async fn with_managed_skills_dir_enables_install_command() {
1589 let provider = mock_provider(vec![]);
1590 let channel = MockChannel::new(vec![]);
1591 let registry = create_test_registry();
1592 let executor = MockToolExecutor::no_tools();
1593 let managed = tempfile::tempdir().unwrap();
1594
1595 let mut agent_no_dir = Agent::new(
1596 mock_provider(vec![]),
1597 MockChannel::new(vec![]),
1598 create_test_registry(),
1599 None,
1600 5,
1601 MockToolExecutor::no_tools(),
1602 );
1603 agent_no_dir
1604 .handle_skill_command("install /some/path")
1605 .await
1606 .unwrap();
1607 let sent_no_dir = agent_no_dir.channel.sent_messages();
1608 assert!(
1609 sent_no_dir.iter().any(|s| s.contains("not configured")),
1610 "without managed dir: {sent_no_dir:?}"
1611 );
1612
1613 let _ = (provider, channel, registry, executor);
1614 let mut agent_with_dir = Agent::new(
1615 mock_provider(vec![]),
1616 MockChannel::new(vec![]),
1617 create_test_registry(),
1618 None,
1619 5,
1620 MockToolExecutor::no_tools(),
1621 )
1622 .with_managed_skills_dir(managed.path().to_path_buf());
1623
1624 agent_with_dir
1625 .handle_skill_command("install /nonexistent/path")
1626 .await
1627 .unwrap();
1628 let sent_with_dir = agent_with_dir.channel.sent_messages();
1629 assert!(
1630 !sent_with_dir.iter().any(|s| s.contains("not configured")),
1631 "with managed dir should not say not configured: {sent_with_dir:?}"
1632 );
1633 assert!(
1634 sent_with_dir.iter().any(|s| s.contains("Install failed")),
1635 "with managed dir should fail due to bad path: {sent_with_dir:?}"
1636 );
1637 }
1638
1639 #[test]
1640 fn default_graph_config_is_disabled() {
1641 let agent = make_agent();
1642 assert!(
1643 !agent.memory_state.graph_config.enabled,
1644 "graph_config must default to disabled"
1645 );
1646 }
1647
1648 #[test]
1649 fn with_graph_config_enabled_sets_flag() {
1650 let cfg = crate::config::GraphConfig {
1651 enabled: true,
1652 ..Default::default()
1653 };
1654 let agent = make_agent().with_graph_config(cfg);
1655 assert!(
1656 agent.memory_state.graph_config.enabled,
1657 "with_graph_config must set enabled flag"
1658 );
1659 }
1660
1661 #[test]
1667 fn apply_session_config_wires_graph_orchestration_anomaly() {
1668 use crate::config::Config;
1669
1670 let mut config = Config::default();
1671 config.memory.graph.enabled = true;
1672 config.orchestration.enabled = true;
1673 config.orchestration.max_tasks = 42;
1674 config.tools.anomaly.enabled = true;
1675 config.tools.anomaly.window_size = 7;
1676
1677 let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1678
1679 assert!(session_cfg.graph_config.enabled);
1681 assert!(session_cfg.orchestration_config.enabled);
1682 assert_eq!(session_cfg.orchestration_config.max_tasks, 42);
1683 assert!(session_cfg.anomaly_config.enabled);
1684 assert_eq!(session_cfg.anomaly_config.window_size, 7);
1685
1686 let agent = make_agent().apply_session_config(session_cfg);
1687
1688 assert!(
1690 agent.memory_state.graph_config.enabled,
1691 "apply_session_config must wire graph_config into agent"
1692 );
1693
1694 assert!(
1696 agent.orchestration.orchestration_config.enabled,
1697 "apply_session_config must wire orchestration_config into agent"
1698 );
1699 assert_eq!(
1700 agent.orchestration.orchestration_config.max_tasks, 42,
1701 "orchestration max_tasks must match config"
1702 );
1703
1704 assert!(
1706 agent.debug_state.anomaly_detector.is_some(),
1707 "apply_session_config must create anomaly_detector when enabled"
1708 );
1709 }
1710
1711 #[test]
1712 fn with_focus_config_propagates_to_focus_state() {
1713 let cfg = crate::config::FocusConfig {
1714 enabled: true,
1715 compression_interval: 7,
1716 ..Default::default()
1717 };
1718 let agent = make_agent().with_focus_config(cfg.clone());
1719 assert!(
1720 agent.focus.config.enabled,
1721 "with_focus_config must set enabled"
1722 );
1723 assert_eq!(
1724 agent.focus.config.compression_interval, 7,
1725 "with_focus_config must propagate compression_interval"
1726 );
1727 }
1728
1729 #[test]
1730 fn with_sidequest_config_propagates_to_sidequest_state() {
1731 let cfg = crate::config::SidequestConfig {
1732 enabled: true,
1733 interval_turns: 3,
1734 ..Default::default()
1735 };
1736 let agent = make_agent().with_sidequest_config(cfg.clone());
1737 assert!(
1738 agent.sidequest.config.enabled,
1739 "with_sidequest_config must set enabled"
1740 );
1741 assert_eq!(
1742 agent.sidequest.config.interval_turns, 3,
1743 "with_sidequest_config must propagate interval_turns"
1744 );
1745 }
1746
1747 #[test]
1749 fn apply_session_config_skips_anomaly_detector_when_disabled() {
1750 use crate::config::Config;
1751
1752 let mut config = Config::default();
1753 config.tools.anomaly.enabled = false; let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1755 assert!(!session_cfg.anomaly_config.enabled);
1756
1757 let agent = make_agent().apply_session_config(session_cfg);
1758 assert!(
1759 agent.debug_state.anomaly_detector.is_none(),
1760 "apply_session_config must not create anomaly_detector when disabled"
1761 );
1762 }
1763
1764 #[test]
1765 fn with_two_stage_matching_sets_flag() {
1766 let agent = make_agent().with_two_stage_matching(true);
1767 assert!(
1768 agent.skill_state.two_stage_matching,
1769 "with_two_stage_matching(true) must enable two_stage_matching"
1770 );
1771
1772 let agent = make_agent().with_two_stage_matching(false);
1773 assert!(
1774 !agent.skill_state.two_stage_matching,
1775 "with_two_stage_matching(false) must disable two_stage_matching"
1776 );
1777 }
1778
1779 #[test]
1780 fn with_confusability_threshold_sets_and_clamps() {
1781 let agent = make_agent().with_confusability_threshold(0.85);
1782 assert!(
1783 (agent.skill_state.confusability_threshold - 0.85).abs() < f32::EPSILON,
1784 "with_confusability_threshold must store the provided value"
1785 );
1786
1787 let agent = make_agent().with_confusability_threshold(1.5);
1788 assert!(
1789 (agent.skill_state.confusability_threshold - 1.0).abs() < f32::EPSILON,
1790 "with_confusability_threshold must clamp values above 1.0"
1791 );
1792
1793 let agent = make_agent().with_confusability_threshold(-0.1);
1794 assert!(
1795 agent.skill_state.confusability_threshold.abs() < f32::EPSILON,
1796 "with_confusability_threshold must clamp values below 0.0"
1797 );
1798 }
1799}