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