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_persona_config(mut self, config: crate::config::PersonaConfig) -> Self {
327 self.memory_state.persona_config = config;
328 self
329 }
330
331 #[must_use]
332 pub fn with_graph_config(mut self, config: crate::config::GraphConfig) -> Self {
333 if config.enabled {
336 tracing::warn!(
337 "graph-memory is enabled: extracted entities are stored without PII redaction. \
338 Do not use with sensitive personal data until redaction is implemented."
339 );
340 }
341 if config.rpe.enabled {
343 self.memory_state.rpe_router = Some(std::sync::Mutex::new(
344 zeph_memory::RpeRouter::new(config.rpe.threshold, config.rpe.max_skip_turns),
345 ));
346 } else {
347 self.memory_state.rpe_router = None;
348 }
349 self.memory_state.graph_config = config;
350 self
351 }
352
353 #[must_use]
354 pub fn with_anomaly_detector(mut self, detector: zeph_tools::AnomalyDetector) -> Self {
355 self.debug_state.anomaly_detector = Some(detector);
356 self
357 }
358
359 #[must_use]
360 pub fn with_instruction_blocks(
361 mut self,
362 blocks: Vec<crate::instructions::InstructionBlock>,
363 ) -> Self {
364 self.instructions.blocks = blocks;
365 self
366 }
367
368 #[must_use]
369 pub fn with_instruction_reload(
370 mut self,
371 rx: mpsc::Receiver<InstructionEvent>,
372 state: InstructionReloadState,
373 ) -> Self {
374 self.instructions.reload_rx = Some(rx);
375 self.instructions.reload_state = Some(state);
376 self
377 }
378
379 #[must_use]
380 pub fn with_shutdown(mut self, rx: watch::Receiver<bool>) -> Self {
381 self.lifecycle.shutdown = rx;
382 self
383 }
384
385 #[must_use]
386 pub fn with_skill_reload(
387 mut self,
388 paths: Vec<PathBuf>,
389 rx: mpsc::Receiver<SkillEvent>,
390 ) -> Self {
391 self.skill_state.skill_paths = paths;
392 self.skill_state.skill_reload_rx = Some(rx);
393 self
394 }
395
396 #[must_use]
397 pub fn with_managed_skills_dir(mut self, dir: PathBuf) -> Self {
398 self.skill_state.managed_dir = Some(dir);
399 self
400 }
401
402 #[must_use]
403 pub fn with_trust_config(mut self, config: crate::config::TrustConfig) -> Self {
404 self.skill_state.trust_config = config;
405 self
406 }
407
408 #[must_use]
409 pub fn with_config_reload(mut self, path: PathBuf, rx: mpsc::Receiver<ConfigEvent>) -> Self {
410 self.lifecycle.config_path = Some(path);
411 self.lifecycle.config_reload_rx = Some(rx);
412 self
413 }
414
415 #[must_use]
416 pub fn with_logging_config(mut self, logging: crate::config::LoggingConfig) -> Self {
417 self.debug_state.logging_config = logging;
418 self
419 }
420
421 #[must_use]
422 pub fn with_available_secrets(
423 mut self,
424 secrets: impl IntoIterator<Item = (String, crate::vault::Secret)>,
425 ) -> Self {
426 self.skill_state.available_custom_secrets = secrets.into_iter().collect();
427 self
428 }
429
430 #[must_use]
434 pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
435 self.skill_state.hybrid_search = enabled;
436 if enabled {
437 let reg = self
438 .skill_state
439 .registry
440 .read()
441 .expect("registry read lock");
442 let all_meta = reg.all_meta();
443 let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
444 self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
445 }
446 self
447 }
448
449 #[must_use]
450 pub fn with_learning(mut self, config: LearningConfig) -> Self {
451 if config.correction_detection {
452 self.feedback.detector = super::feedback_detector::FeedbackDetector::new(
453 config.correction_confidence_threshold,
454 );
455 if config.detector_mode == crate::config::DetectorMode::Judge {
456 self.feedback.judge = Some(super::feedback_detector::JudgeDetector::new(
457 config.judge_adaptive_low,
458 config.judge_adaptive_high,
459 ));
460 }
461 }
462 self.learning_engine.config = Some(config);
463 self
464 }
465
466 #[must_use]
470 pub fn with_rl_routing(
471 mut self,
472 enabled: bool,
473 learning_rate: f32,
474 rl_weight: f32,
475 persist_interval: u32,
476 warmup_updates: u32,
477 ) -> Self {
478 self.learning_engine.rl_routing = Some(crate::agent::learning_engine::RlRoutingConfig {
479 enabled,
480 learning_rate,
481 persist_interval,
482 });
483 self.skill_state.rl_weight = rl_weight;
484 self.skill_state.rl_warmup_updates = warmup_updates;
485 self
486 }
487
488 #[must_use]
490 pub fn with_rl_head(mut self, head: zeph_skills::rl_head::RoutingHead) -> Self {
491 self.skill_state.rl_head = Some(head);
492 self
493 }
494
495 #[must_use]
501 pub fn with_llm_classifier(
502 mut self,
503 classifier: zeph_llm::classifier::llm::LlmClassifier,
504 ) -> Self {
505 #[cfg(feature = "classifiers")]
507 let classifier = if let Some(ref m) = self.metrics.classifier_metrics {
508 classifier.with_metrics(std::sync::Arc::clone(m))
509 } else {
510 classifier
511 };
512 self.feedback.llm_classifier = Some(classifier);
513 self
514 }
515
516 #[must_use]
517 pub fn with_judge_provider(mut self, provider: AnyProvider) -> Self {
518 self.providers.judge_provider = Some(provider);
519 self
520 }
521
522 #[must_use]
523 pub fn with_probe_provider(mut self, provider: AnyProvider) -> Self {
524 self.providers.probe_provider = Some(provider);
525 self
526 }
527
528 #[must_use]
532 pub fn with_compress_provider(mut self, provider: AnyProvider) -> Self {
533 self.providers.compress_provider = Some(provider);
534 self
535 }
536
537 #[must_use]
538 pub fn with_planner_provider(mut self, provider: AnyProvider) -> Self {
539 self.orchestration.planner_provider = Some(provider);
540 self
541 }
542
543 #[must_use]
547 pub fn with_verify_provider(mut self, provider: AnyProvider) -> Self {
548 self.orchestration.verify_provider = Some(provider);
549 self
550 }
551
552 #[must_use]
556 pub fn with_server_compaction(mut self, enabled: bool) -> Self {
557 self.providers.server_compaction_active = enabled;
558 self
559 }
560
561 #[must_use]
562 pub fn with_mcp(
563 mut self,
564 tools: Vec<zeph_mcp::McpTool>,
565 registry: Option<zeph_mcp::McpToolRegistry>,
566 manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
567 mcp_config: &crate::config::McpConfig,
568 ) -> Self {
569 self.mcp.tools = tools;
570 self.mcp.registry = registry;
571 self.mcp.manager = manager;
572 self.mcp
573 .allowed_commands
574 .clone_from(&mcp_config.allowed_commands);
575 self.mcp.max_dynamic = mcp_config.max_dynamic_servers;
576 self.mcp.elicitation_warn_sensitive_fields = mcp_config.elicitation_warn_sensitive_fields;
577 self
578 }
579
580 #[must_use]
581 pub fn with_mcp_server_outcomes(
582 mut self,
583 outcomes: Vec<zeph_mcp::ServerConnectOutcome>,
584 ) -> Self {
585 self.mcp.server_outcomes = outcomes;
586 self
587 }
588
589 #[must_use]
590 pub fn with_mcp_shared_tools(
591 mut self,
592 shared: std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>,
593 ) -> Self {
594 self.mcp.shared_tools = Some(shared);
595 self
596 }
597
598 #[must_use]
604 pub fn with_mcp_pruning(
605 mut self,
606 params: zeph_mcp::PruningParams,
607 enabled: bool,
608 pruning_provider: Option<zeph_llm::any::AnyProvider>,
609 ) -> Self {
610 self.mcp.pruning_params = params;
611 self.mcp.pruning_enabled = enabled;
612 self.mcp.pruning_provider = pruning_provider;
613 self
614 }
615
616 #[must_use]
621 pub fn with_mcp_discovery(
622 mut self,
623 strategy: zeph_mcp::ToolDiscoveryStrategy,
624 params: zeph_mcp::DiscoveryParams,
625 discovery_provider: Option<zeph_llm::any::AnyProvider>,
626 ) -> Self {
627 self.mcp.discovery_strategy = strategy;
628 self.mcp.discovery_params = params;
629 self.mcp.discovery_provider = discovery_provider;
630 self
631 }
632
633 #[must_use]
637 pub fn with_mcp_tool_rx(
638 mut self,
639 rx: tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>,
640 ) -> Self {
641 self.mcp.tool_rx = Some(rx);
642 self
643 }
644
645 #[must_use]
650 pub fn with_mcp_elicitation_rx(
651 mut self,
652 rx: tokio::sync::mpsc::Receiver<zeph_mcp::ElicitationEvent>,
653 ) -> Self {
654 self.mcp.elicitation_rx = Some(rx);
655 self
656 }
657
658 #[must_use]
659 pub fn with_security(mut self, security: SecurityConfig, timeouts: TimeoutConfig) -> Self {
660 self.security.sanitizer =
661 zeph_sanitizer::ContentSanitizer::new(&security.content_isolation);
662 self.security.exfiltration_guard = zeph_sanitizer::exfiltration::ExfiltrationGuard::new(
663 security.exfiltration_guard.clone(),
664 );
665 self.security.pii_filter = zeph_sanitizer::pii::PiiFilter::new(security.pii_filter.clone());
666 self.security.memory_validator =
667 zeph_sanitizer::memory_validation::MemoryWriteValidator::new(
668 security.memory_validation.clone(),
669 );
670 self.runtime.rate_limiter =
671 crate::agent::rate_limiter::ToolRateLimiter::new(security.rate_limit.clone());
672
673 let mut verifiers: Vec<Box<dyn zeph_tools::PreExecutionVerifier>> = Vec::new();
678 if security.pre_execution_verify.enabled {
679 let dcfg = &security.pre_execution_verify.destructive_commands;
680 if dcfg.enabled {
681 verifiers.push(Box::new(zeph_tools::DestructiveCommandVerifier::new(dcfg)));
682 }
683 let icfg = &security.pre_execution_verify.injection_patterns;
684 if icfg.enabled {
685 verifiers.push(Box::new(zeph_tools::InjectionPatternVerifier::new(icfg)));
686 }
687 let ucfg = &security.pre_execution_verify.url_grounding;
688 if ucfg.enabled {
689 verifiers.push(Box::new(zeph_tools::UrlGroundingVerifier::new(
690 ucfg,
691 std::sync::Arc::clone(&self.security.user_provided_urls),
692 )));
693 }
694 let fcfg = &security.pre_execution_verify.firewall;
695 if fcfg.enabled {
696 verifiers.push(Box::new(zeph_tools::FirewallVerifier::new(fcfg)));
697 }
698 }
699 self.tool_orchestrator.pre_execution_verifiers = verifiers;
700
701 self.security.response_verifier = zeph_sanitizer::response_verifier::ResponseVerifier::new(
702 security.response_verification.clone(),
703 );
704
705 self.runtime.security = security;
706 self.runtime.timeouts = timeouts;
707 self
708 }
709
710 #[must_use]
712 pub fn with_audit_logger(mut self, logger: std::sync::Arc<zeph_tools::AuditLogger>) -> Self {
713 self.tool_orchestrator.audit_logger = Some(logger);
714 self
715 }
716
717 #[must_use]
718 pub fn with_redact_credentials(mut self, enabled: bool) -> Self {
719 self.runtime.redact_credentials = enabled;
720 self
721 }
722
723 #[must_use]
724 pub fn with_budget_hint_enabled(mut self, enabled: bool) -> Self {
725 self.runtime.budget_hint_enabled = enabled;
726 self
727 }
728
729 #[must_use]
730 pub fn with_channel_skills(mut self, config: zeph_config::ChannelSkillsConfig) -> Self {
731 self.runtime.channel_skills = config;
732 self
733 }
734
735 #[must_use]
736 pub fn with_tool_summarization(mut self, enabled: bool) -> Self {
737 self.tool_orchestrator.summarize_tool_output_enabled = enabled;
738 self
739 }
740
741 #[must_use]
742 pub fn with_overflow_config(mut self, config: zeph_tools::OverflowConfig) -> Self {
743 self.tool_orchestrator.overflow_config = config;
744 self
745 }
746
747 #[must_use]
751 pub fn with_tafc_config(mut self, config: zeph_tools::TafcConfig) -> Self {
752 self.tool_orchestrator.tafc = config.validated();
753 self
754 }
755
756 #[must_use]
757 pub fn with_result_cache_config(mut self, config: &zeph_tools::ResultCacheConfig) -> Self {
758 self.tool_orchestrator.set_cache_config(config);
759 self
760 }
761
762 #[must_use]
763 pub fn with_summary_provider(mut self, provider: AnyProvider) -> Self {
764 self.providers.summary_provider = Some(provider);
765 self
766 }
767
768 #[must_use]
769 pub fn with_quarantine_summarizer(
770 mut self,
771 qs: zeph_sanitizer::quarantine::QuarantinedSummarizer,
772 ) -> Self {
773 self.security.quarantine_summarizer = Some(qs);
774 self
775 }
776
777 #[must_use]
781 pub fn with_acp_session(mut self, is_acp: bool) -> Self {
782 self.security.is_acp_session = is_acp;
783 self
784 }
785
786 #[cfg(feature = "classifiers")]
791 #[must_use]
792 pub fn with_injection_classifier(
793 mut self,
794 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
795 timeout_ms: u64,
796 threshold: f32,
797 threshold_soft: f32,
798 ) -> Self {
799 let old = std::mem::replace(
801 &mut self.security.sanitizer,
802 zeph_sanitizer::ContentSanitizer::new(
803 &zeph_sanitizer::ContentIsolationConfig::default(),
804 ),
805 );
806 self.security.sanitizer = old
807 .with_classifier(backend, timeout_ms, threshold)
808 .with_injection_threshold_soft(threshold_soft);
809 self
810 }
811
812 #[cfg(feature = "classifiers")]
817 #[must_use]
818 pub fn with_enforcement_mode(mut self, mode: zeph_config::InjectionEnforcementMode) -> Self {
819 let old = std::mem::replace(
820 &mut self.security.sanitizer,
821 zeph_sanitizer::ContentSanitizer::new(
822 &zeph_sanitizer::ContentIsolationConfig::default(),
823 ),
824 );
825 self.security.sanitizer = old.with_enforcement_mode(mode);
826 self
827 }
828
829 #[cfg(feature = "classifiers")]
831 #[must_use]
832 pub fn with_three_class_classifier(
833 mut self,
834 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
835 threshold: f32,
836 ) -> Self {
837 let old = std::mem::replace(
838 &mut self.security.sanitizer,
839 zeph_sanitizer::ContentSanitizer::new(
840 &zeph_sanitizer::ContentIsolationConfig::default(),
841 ),
842 );
843 self.security.sanitizer = old.with_three_class_backend(backend, threshold);
844 self
845 }
846
847 #[must_use]
851 pub fn with_causal_analyzer(
852 mut self,
853 analyzer: zeph_sanitizer::causal_ipi::TurnCausalAnalyzer,
854 ) -> Self {
855 self.security.causal_analyzer = Some(analyzer);
856 self
857 }
858
859 #[cfg(feature = "classifiers")]
863 #[must_use]
864 pub fn with_scan_user_input(mut self, value: bool) -> Self {
865 let old = std::mem::replace(
866 &mut self.security.sanitizer,
867 zeph_sanitizer::ContentSanitizer::new(
868 &zeph_sanitizer::ContentIsolationConfig::default(),
869 ),
870 );
871 self.security.sanitizer = old.with_scan_user_input(value);
872 self
873 }
874
875 #[cfg(feature = "classifiers")]
880 #[must_use]
881 pub fn with_pii_detector(
882 mut self,
883 detector: std::sync::Arc<dyn zeph_llm::classifier::PiiDetector>,
884 threshold: f32,
885 ) -> Self {
886 let old = std::mem::replace(
887 &mut self.security.sanitizer,
888 zeph_sanitizer::ContentSanitizer::new(
889 &zeph_sanitizer::ContentIsolationConfig::default(),
890 ),
891 );
892 self.security.sanitizer = old.with_pii_detector(detector, threshold);
893 self
894 }
895
896 #[cfg(feature = "classifiers")]
901 #[must_use]
902 pub fn with_pii_ner_allowlist(mut self, entries: Vec<String>) -> Self {
903 let old = std::mem::replace(
904 &mut self.security.sanitizer,
905 zeph_sanitizer::ContentSanitizer::new(
906 &zeph_sanitizer::ContentIsolationConfig::default(),
907 ),
908 );
909 self.security.sanitizer = old.with_pii_ner_allowlist(entries);
910 self
911 }
912
913 #[cfg(feature = "classifiers")]
918 #[must_use]
919 pub fn with_pii_ner_classifier(
920 mut self,
921 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
922 timeout_ms: u64,
923 max_chars: usize,
924 circuit_breaker_threshold: u32,
925 ) -> Self {
926 self.security.pii_ner_backend = Some(backend);
927 self.security.pii_ner_timeout_ms = timeout_ms;
928 self.security.pii_ner_max_chars = max_chars;
929 self.security.pii_ner_circuit_breaker_threshold = circuit_breaker_threshold;
930 self
931 }
932
933 #[cfg(feature = "classifiers")]
939 #[must_use]
940 pub fn with_classifier_metrics(
941 mut self,
942 metrics: std::sync::Arc<zeph_llm::ClassifierMetrics>,
943 ) -> Self {
944 let old = std::mem::replace(
946 &mut self.security.sanitizer,
947 zeph_sanitizer::ContentSanitizer::new(
948 &zeph_sanitizer::ContentIsolationConfig::default(),
949 ),
950 );
951 self.security.sanitizer = old.with_classifier_metrics(std::sync::Arc::clone(&metrics));
952 self.metrics.classifier_metrics = Some(metrics);
954 self
955 }
956 #[must_use]
957 pub fn with_guardrail(mut self, filter: zeph_sanitizer::guardrail::GuardrailFilter) -> Self {
958 use zeph_sanitizer::guardrail::GuardrailAction;
959 let warn_mode = filter.action() == GuardrailAction::Warn;
960 self.security.guardrail = Some(filter);
961 self.update_metrics(|m| {
962 m.guardrail_enabled = true;
963 m.guardrail_warn_mode = warn_mode;
964 });
965 self
966 }
967
968 pub(super) fn summary_or_primary_provider(&self) -> &AnyProvider {
969 self.providers
970 .summary_provider
971 .as_ref()
972 .unwrap_or(&self.provider)
973 }
974
975 pub(super) fn probe_or_summary_provider(&self) -> &AnyProvider {
976 self.providers
977 .probe_provider
978 .as_ref()
979 .or(self.providers.summary_provider.as_ref())
980 .unwrap_or(&self.provider)
981 }
982
983 pub(super) fn last_assistant_response(&self) -> String {
985 self.msg
986 .messages
987 .iter()
988 .rev()
989 .find(|m| m.role == zeph_llm::provider::Role::Assistant)
990 .map(|m| super::context::truncate_chars(&m.content, 500))
991 .unwrap_or_default()
992 }
993
994 #[must_use]
995 pub fn with_permission_policy(mut self, policy: zeph_tools::PermissionPolicy) -> Self {
996 self.runtime.permission_policy = policy;
997 self
998 }
999
1000 #[must_use]
1001 pub fn with_context_budget(
1002 mut self,
1003 budget_tokens: usize,
1004 reserve_ratio: f32,
1005 hard_compaction_threshold: f32,
1006 compaction_preserve_tail: usize,
1007 prune_protect_tokens: usize,
1008 ) -> Self {
1009 if budget_tokens == 0 {
1010 tracing::warn!("context budget is 0 — agent will have no token tracking");
1011 }
1012 if budget_tokens > 0 {
1013 self.context_manager.budget = Some(ContextBudget::new(budget_tokens, reserve_ratio));
1014 }
1015 self.context_manager.hard_compaction_threshold = hard_compaction_threshold;
1016 self.context_manager.compaction_preserve_tail = compaction_preserve_tail;
1017 self.context_manager.prune_protect_tokens = prune_protect_tokens;
1018 self
1019 }
1020
1021 #[must_use]
1022 pub fn with_soft_compaction_threshold(mut self, threshold: f32) -> Self {
1023 self.context_manager.soft_compaction_threshold = threshold;
1024 self
1025 }
1026
1027 #[must_use]
1032 pub fn with_compaction_cooldown(mut self, cooldown_turns: u8) -> Self {
1033 self.context_manager.compaction_cooldown_turns = cooldown_turns;
1034 self
1035 }
1036
1037 #[must_use]
1038 pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
1039 self.context_manager.compression = compression;
1040 self
1041 }
1042
1043 #[must_use]
1045 pub fn with_focus_config(mut self, config: crate::config::FocusConfig) -> Self {
1046 self.focus = super::focus::FocusState::new(config);
1047 self
1048 }
1049
1050 #[must_use]
1052 pub fn with_sidequest_config(mut self, config: crate::config::SidequestConfig) -> Self {
1053 self.sidequest = super::sidequest::SidequestState::new(config);
1054 self
1055 }
1056
1057 #[must_use]
1058 pub fn with_routing(mut self, routing: StoreRoutingConfig) -> Self {
1059 self.context_manager.routing = routing;
1060 self
1061 }
1062
1063 #[must_use]
1064 pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
1065 self.runtime.model_name = name.into();
1066 self
1067 }
1068
1069 #[must_use]
1074 pub fn with_active_provider_name(mut self, name: impl Into<String>) -> Self {
1075 self.runtime.active_provider_name = name.into();
1076 self
1077 }
1078
1079 #[must_use]
1080 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
1081 let path = path.into();
1082 self.session.env_context =
1083 crate::context::EnvironmentContext::gather_for_dir(&self.runtime.model_name, &path);
1084 self
1085 }
1086
1087 #[must_use]
1093 pub fn with_hooks_config(mut self, config: &zeph_config::HooksConfig) -> Self {
1094 self.session
1095 .hooks_config
1096 .cwd_changed
1097 .clone_from(&config.cwd_changed);
1098
1099 if let Some(ref fc) = config.file_changed {
1100 self.session
1101 .hooks_config
1102 .file_changed_hooks
1103 .clone_from(&fc.hooks);
1104
1105 if !fc.watch_paths.is_empty() {
1106 let (tx, rx) = tokio::sync::mpsc::channel(64);
1107 match crate::file_watcher::FileChangeWatcher::start(
1108 &fc.watch_paths,
1109 fc.debounce_ms,
1110 tx,
1111 ) {
1112 Ok(watcher) => {
1113 self.lifecycle.file_watcher = Some(watcher);
1114 self.lifecycle.file_changed_rx = Some(rx);
1115 tracing::info!(
1116 paths = ?fc.watch_paths,
1117 debounce_ms = fc.debounce_ms,
1118 "file change watcher started"
1119 );
1120 }
1121 Err(e) => {
1122 tracing::warn!(error = %e, "failed to start file change watcher");
1123 }
1124 }
1125 }
1126 }
1127
1128 let cwd_str = &self.session.env_context.working_dir;
1130 if !cwd_str.is_empty() {
1131 self.lifecycle.last_known_cwd = std::path::PathBuf::from(cwd_str);
1132 }
1133
1134 self
1135 }
1136
1137 #[must_use]
1138 pub fn with_warmup_ready(mut self, rx: watch::Receiver<bool>) -> Self {
1139 self.lifecycle.warmup_ready = Some(rx);
1140 self
1141 }
1142
1143 #[must_use]
1144 pub fn with_cost_tracker(mut self, tracker: CostTracker) -> Self {
1145 self.metrics.cost_tracker = Some(tracker);
1146 self
1147 }
1148
1149 #[must_use]
1150 pub fn with_extended_context(mut self, enabled: bool) -> Self {
1151 self.metrics.extended_context = enabled;
1152 self
1153 }
1154
1155 #[must_use]
1156 pub fn with_repo_map(mut self, token_budget: usize, ttl_secs: u64) -> Self {
1157 self.index.repo_map_tokens = token_budget;
1158 self.index.repo_map_ttl = std::time::Duration::from_secs(ttl_secs);
1159 self
1160 }
1161
1162 #[must_use]
1163 pub fn with_code_retriever(
1164 mut self,
1165 retriever: std::sync::Arc<zeph_index::retriever::CodeRetriever>,
1166 ) -> Self {
1167 self.index.retriever = Some(retriever);
1168 self
1169 }
1170
1171 #[must_use]
1178 pub fn with_index_mcp_server(self, project_root: impl Into<std::path::PathBuf>) -> Self {
1179 let server = zeph_index::IndexMcpServer::new(project_root);
1180 self.add_tool_executor(server)
1181 }
1182
1183 #[must_use]
1187 pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
1188 let provider_name = if self.runtime.active_provider_name.is_empty() {
1189 self.provider.name().to_owned()
1190 } else {
1191 self.runtime.active_provider_name.clone()
1192 };
1193 let model_name = self.runtime.model_name.clone();
1194 let total_skills = self
1195 .skill_state
1196 .registry
1197 .read()
1198 .expect("registry read lock")
1199 .all_meta()
1200 .len();
1201 let qdrant_available = false;
1202 let conversation_id = self.memory_state.conversation_id;
1203 let prompt_estimate = self
1204 .msg
1205 .messages
1206 .first()
1207 .map_or(0, |m| u64::try_from(m.content.len()).unwrap_or(0) / 4);
1208 let mcp_tool_count = self.mcp.tools.len();
1209 let mcp_server_count = if self.mcp.server_outcomes.is_empty() {
1210 self.mcp
1212 .tools
1213 .iter()
1214 .map(|t| &t.server_id)
1215 .collect::<std::collections::HashSet<_>>()
1216 .len()
1217 } else {
1218 self.mcp.server_outcomes.len()
1219 };
1220 let mcp_connected_count = if self.mcp.server_outcomes.is_empty() {
1221 mcp_server_count
1222 } else {
1223 self.mcp
1224 .server_outcomes
1225 .iter()
1226 .filter(|o| o.connected)
1227 .count()
1228 };
1229 let mcp_servers: Vec<crate::metrics::McpServerStatus> = self
1230 .mcp
1231 .server_outcomes
1232 .iter()
1233 .map(|o| crate::metrics::McpServerStatus {
1234 id: o.id.clone(),
1235 status: if o.connected {
1236 crate::metrics::McpServerConnectionStatus::Connected
1237 } else {
1238 crate::metrics::McpServerConnectionStatus::Failed
1239 },
1240 tool_count: o.tool_count,
1241 error: o.error.clone(),
1242 })
1243 .collect();
1244 let extended_context = self.metrics.extended_context;
1245 tx.send_modify(|m| {
1246 m.provider_name = provider_name;
1247 m.model_name = model_name;
1248 m.total_skills = total_skills;
1249 m.qdrant_available = qdrant_available;
1250 m.sqlite_conversation_id = conversation_id;
1251 m.context_tokens = prompt_estimate;
1252 m.prompt_tokens = prompt_estimate;
1253 m.total_tokens = prompt_estimate;
1254 m.mcp_tool_count = mcp_tool_count;
1255 m.mcp_server_count = mcp_server_count;
1256 m.mcp_connected_count = mcp_connected_count;
1257 m.mcp_servers = mcp_servers;
1258 m.extended_context = extended_context;
1259 });
1260 self.metrics.metrics_tx = Some(tx);
1261 self
1262 }
1263
1264 #[must_use]
1268 pub fn cancel_signal(&self) -> Arc<Notify> {
1269 Arc::clone(&self.lifecycle.cancel_signal)
1270 }
1271
1272 #[must_use]
1275 pub fn with_cancel_signal(mut self, signal: Arc<Notify>) -> Self {
1276 self.lifecycle.cancel_signal = signal;
1277 self
1278 }
1279
1280 #[must_use]
1281 pub fn with_subagent_manager(mut self, manager: crate::subagent::SubAgentManager) -> Self {
1282 self.orchestration.subagent_manager = Some(manager);
1283 self
1284 }
1285
1286 #[must_use]
1287 pub fn with_subagent_config(mut self, config: crate::config::SubAgentConfig) -> Self {
1288 self.orchestration.subagent_config = config;
1289 self
1290 }
1291
1292 #[must_use]
1293 pub fn with_orchestration_config(mut self, config: crate::config::OrchestrationConfig) -> Self {
1294 self.orchestration.orchestration_config = config;
1295 self
1296 }
1297
1298 #[must_use]
1300 pub fn with_experiment_config(mut self, config: crate::config::ExperimentConfig) -> Self {
1301 self.experiments.config = config;
1302 self
1303 }
1304
1305 #[must_use]
1311 pub fn with_experiment_baseline(
1312 mut self,
1313 baseline: crate::experiments::ConfigSnapshot,
1314 ) -> Self {
1315 self.experiments.baseline = baseline;
1316 self
1317 }
1318
1319 #[must_use]
1324 pub fn with_eval_provider(mut self, provider: AnyProvider) -> Self {
1325 self.experiments.eval_provider = Some(provider);
1326 self
1327 }
1328
1329 #[must_use]
1332 pub fn with_provider_override(
1333 mut self,
1334 slot: Arc<std::sync::RwLock<Option<AnyProvider>>>,
1335 ) -> Self {
1336 self.providers.provider_override = Some(slot);
1337 self
1338 }
1339
1340 #[must_use]
1342 pub fn with_tool_schema_filter(mut self, filter: zeph_tools::ToolSchemaFilter) -> Self {
1343 self.tool_schema_filter = Some(filter);
1344 self
1345 }
1346
1347 #[must_use]
1349 pub fn with_dependency_config(mut self, config: zeph_tools::DependencyConfig) -> Self {
1350 self.runtime.dependency_config = config;
1351 self
1352 }
1353
1354 #[must_use]
1359 pub fn with_tool_dependency_graph(
1360 mut self,
1361 graph: zeph_tools::ToolDependencyGraph,
1362 always_on: std::collections::HashSet<String>,
1363 ) -> Self {
1364 self.dependency_graph = Some(graph);
1365 self.dependency_always_on = always_on;
1366 self
1367 }
1368
1369 pub async fn maybe_init_tool_schema_filter(
1374 mut self,
1375 config: &crate::config::ToolFilterConfig,
1376 provider: &zeph_llm::any::AnyProvider,
1377 ) -> Self {
1378 use zeph_llm::provider::LlmProvider;
1379
1380 if !config.enabled {
1381 return self;
1382 }
1383
1384 let always_on_set: std::collections::HashSet<&str> =
1385 config.always_on.iter().map(String::as_str).collect();
1386 let defs = self.tool_executor.tool_definitions_erased();
1387 let filterable: Vec<&zeph_tools::registry::ToolDef> = defs
1388 .iter()
1389 .filter(|d| !always_on_set.contains(d.id.as_ref()))
1390 .collect();
1391
1392 if filterable.is_empty() {
1393 tracing::info!("tool schema filter: all tools are always-on, nothing to filter");
1394 return self;
1395 }
1396
1397 let mut embeddings = Vec::with_capacity(filterable.len());
1398 for def in &filterable {
1399 let text = format!("{}: {}", def.id, def.description);
1400 match provider.embed(&text).await {
1401 Ok(emb) => {
1402 embeddings.push(zeph_tools::ToolEmbedding {
1403 tool_id: def.id.to_string(),
1404 embedding: emb,
1405 });
1406 }
1407 Err(e) => {
1408 tracing::info!(
1409 provider = provider.name(),
1410 "tool schema filter disabled: embedding not supported \
1411 by provider ({e:#})"
1412 );
1413 return self;
1414 }
1415 }
1416 }
1417
1418 tracing::info!(
1419 tool_count = embeddings.len(),
1420 always_on = config.always_on.len(),
1421 top_k = config.top_k,
1422 "tool schema filter initialized"
1423 );
1424
1425 let filter = zeph_tools::ToolSchemaFilter::new(
1426 config.always_on.clone(),
1427 config.top_k,
1428 config.min_description_words,
1429 embeddings,
1430 );
1431 self.tool_schema_filter = Some(filter);
1432 self
1433 }
1434
1435 #[must_use]
1443 pub fn apply_session_config(mut self, cfg: AgentSessionConfig) -> Self {
1444 let AgentSessionConfig {
1445 max_tool_iterations,
1446 max_tool_retries,
1447 max_retry_duration_secs,
1448 retry_base_ms,
1449 retry_max_ms,
1450 parameter_reformat_provider,
1451 tool_repeat_threshold,
1452 tool_summarization,
1453 tool_call_cutoff,
1454 overflow_config,
1455 permission_policy,
1456 model_name,
1457 embed_model,
1458 semantic_cache_enabled,
1459 semantic_cache_threshold,
1460 semantic_cache_max_candidates,
1461 budget_tokens,
1462 soft_compaction_threshold,
1463 hard_compaction_threshold,
1464 compaction_preserve_tail,
1465 compaction_cooldown_turns,
1466 prune_protect_tokens,
1467 redact_credentials,
1468 security,
1469 timeouts,
1470 learning,
1471 document_config,
1472 graph_config,
1473 persona_config,
1474 anomaly_config,
1475 result_cache_config,
1476 utility_config,
1477 orchestration_config,
1478 debug_config: _debug_config,
1481 server_compaction,
1482 budget_hint_enabled,
1483 secrets,
1484 } = cfg;
1485
1486 self = self
1487 .with_max_tool_iterations(max_tool_iterations)
1488 .with_max_tool_retries(max_tool_retries)
1489 .with_max_retry_duration_secs(max_retry_duration_secs)
1490 .with_retry_backoff(retry_base_ms, retry_max_ms)
1491 .with_parameter_reformat_provider(parameter_reformat_provider)
1492 .with_tool_repeat_threshold(tool_repeat_threshold)
1493 .with_model_name(model_name)
1494 .with_embedding_model(embed_model)
1495 .with_context_budget(
1496 budget_tokens,
1497 CONTEXT_BUDGET_RESERVE_RATIO,
1498 hard_compaction_threshold,
1499 compaction_preserve_tail,
1500 prune_protect_tokens,
1501 )
1502 .with_soft_compaction_threshold(soft_compaction_threshold)
1503 .with_compaction_cooldown(compaction_cooldown_turns)
1504 .with_security(security, timeouts)
1505 .with_redact_credentials(redact_credentials)
1506 .with_tool_summarization(tool_summarization)
1507 .with_overflow_config(overflow_config)
1508 .with_permission_policy(permission_policy)
1509 .with_learning(learning)
1510 .with_tool_call_cutoff(tool_call_cutoff)
1511 .with_available_secrets(
1512 secrets
1513 .iter()
1514 .map(|(k, v)| (k.clone(), crate::vault::Secret::new(v.expose().to_owned()))),
1515 )
1516 .with_server_compaction(server_compaction)
1517 .with_document_config(document_config)
1518 .with_graph_config(graph_config)
1519 .with_persona_config(persona_config)
1520 .with_orchestration_config(orchestration_config)
1521 .with_budget_hint_enabled(budget_hint_enabled);
1522
1523 self.debug_state.reasoning_model_warning = anomaly_config.reasoning_model_warning;
1524 if anomaly_config.enabled {
1525 self = self.with_anomaly_detector(zeph_tools::AnomalyDetector::new(
1526 anomaly_config.window_size,
1527 anomaly_config.error_threshold,
1528 anomaly_config.critical_threshold,
1529 ));
1530 }
1531
1532 self.runtime.semantic_cache_enabled = semantic_cache_enabled;
1533 self.runtime.semantic_cache_threshold = semantic_cache_threshold;
1534 self.runtime.semantic_cache_max_candidates = semantic_cache_max_candidates;
1535 self = self.with_result_cache_config(&result_cache_config);
1536 self.tool_orchestrator.set_utility_config(utility_config);
1537
1538 self
1539 }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544 use super::super::agent_tests::{
1545 MockChannel, MockToolExecutor, create_test_registry, mock_provider,
1546 };
1547 use super::*;
1548 use crate::config::{CompressionStrategy, StoreRoutingConfig, StoreRoutingStrategy};
1549
1550 fn make_agent() -> Agent<MockChannel> {
1551 Agent::new(
1552 mock_provider(vec![]),
1553 MockChannel::new(vec![]),
1554 create_test_registry(),
1555 None,
1556 5,
1557 MockToolExecutor::no_tools(),
1558 )
1559 }
1560
1561 #[test]
1562 #[allow(clippy::default_trait_access)]
1563 fn with_compression_sets_proactive_strategy() {
1564 let compression = CompressionConfig {
1565 strategy: CompressionStrategy::Proactive {
1566 threshold_tokens: 50_000,
1567 max_summary_tokens: 2_000,
1568 },
1569 model: String::new(),
1570 pruning_strategy: crate::config::PruningStrategy::default(),
1571 probe: zeph_memory::CompactionProbeConfig::default(),
1572 compress_provider: zeph_config::ProviderName::default(),
1573 archive_tool_outputs: false,
1574 focus_scorer_provider: zeph_config::ProviderName::default(),
1575 high_density_budget: 0.7,
1576 low_density_budget: 0.3,
1577 predictor: Default::default(),
1578 };
1579 let agent = make_agent().with_compression(compression);
1580 assert!(
1581 matches!(
1582 agent.context_manager.compression.strategy,
1583 CompressionStrategy::Proactive {
1584 threshold_tokens: 50_000,
1585 max_summary_tokens: 2_000,
1586 }
1587 ),
1588 "expected Proactive strategy after with_compression"
1589 );
1590 }
1591
1592 #[test]
1593 fn with_routing_sets_routing_config() {
1594 let routing = StoreRoutingConfig {
1595 strategy: StoreRoutingStrategy::Heuristic,
1596 ..StoreRoutingConfig::default()
1597 };
1598 let agent = make_agent().with_routing(routing);
1599 assert_eq!(
1600 agent.context_manager.routing.strategy,
1601 StoreRoutingStrategy::Heuristic,
1602 "routing strategy must be set by with_routing"
1603 );
1604 }
1605
1606 #[test]
1607 fn default_compression_is_reactive() {
1608 let agent = make_agent();
1609 assert_eq!(
1610 agent.context_manager.compression.strategy,
1611 CompressionStrategy::Reactive,
1612 "default compression strategy must be Reactive"
1613 );
1614 }
1615
1616 #[test]
1617 fn default_routing_is_heuristic() {
1618 let agent = make_agent();
1619 assert_eq!(
1620 agent.context_manager.routing.strategy,
1621 StoreRoutingStrategy::Heuristic,
1622 "default routing strategy must be Heuristic"
1623 );
1624 }
1625
1626 #[test]
1627 fn with_cancel_signal_replaces_internal_signal() {
1628 let agent = Agent::new(
1629 mock_provider(vec![]),
1630 MockChannel::new(vec![]),
1631 create_test_registry(),
1632 None,
1633 5,
1634 MockToolExecutor::no_tools(),
1635 );
1636
1637 let shared = Arc::new(Notify::new());
1638 let agent = agent.with_cancel_signal(Arc::clone(&shared));
1639
1640 assert!(Arc::ptr_eq(&shared, &agent.cancel_signal()));
1642 }
1643
1644 #[tokio::test]
1649 async fn with_managed_skills_dir_enables_install_command() {
1650 let provider = mock_provider(vec![]);
1651 let channel = MockChannel::new(vec![]);
1652 let registry = create_test_registry();
1653 let executor = MockToolExecutor::no_tools();
1654 let managed = tempfile::tempdir().unwrap();
1655
1656 let mut agent_no_dir = Agent::new(
1657 mock_provider(vec![]),
1658 MockChannel::new(vec![]),
1659 create_test_registry(),
1660 None,
1661 5,
1662 MockToolExecutor::no_tools(),
1663 );
1664 agent_no_dir
1665 .handle_skill_command("install /some/path")
1666 .await
1667 .unwrap();
1668 let sent_no_dir = agent_no_dir.channel.sent_messages();
1669 assert!(
1670 sent_no_dir.iter().any(|s| s.contains("not configured")),
1671 "without managed dir: {sent_no_dir:?}"
1672 );
1673
1674 let _ = (provider, channel, registry, executor);
1675 let mut agent_with_dir = Agent::new(
1676 mock_provider(vec![]),
1677 MockChannel::new(vec![]),
1678 create_test_registry(),
1679 None,
1680 5,
1681 MockToolExecutor::no_tools(),
1682 )
1683 .with_managed_skills_dir(managed.path().to_path_buf());
1684
1685 agent_with_dir
1686 .handle_skill_command("install /nonexistent/path")
1687 .await
1688 .unwrap();
1689 let sent_with_dir = agent_with_dir.channel.sent_messages();
1690 assert!(
1691 !sent_with_dir.iter().any(|s| s.contains("not configured")),
1692 "with managed dir should not say not configured: {sent_with_dir:?}"
1693 );
1694 assert!(
1695 sent_with_dir.iter().any(|s| s.contains("Install failed")),
1696 "with managed dir should fail due to bad path: {sent_with_dir:?}"
1697 );
1698 }
1699
1700 #[test]
1701 fn default_graph_config_is_disabled() {
1702 let agent = make_agent();
1703 assert!(
1704 !agent.memory_state.graph_config.enabled,
1705 "graph_config must default to disabled"
1706 );
1707 }
1708
1709 #[test]
1710 fn with_graph_config_enabled_sets_flag() {
1711 let cfg = crate::config::GraphConfig {
1712 enabled: true,
1713 ..Default::default()
1714 };
1715 let agent = make_agent().with_graph_config(cfg);
1716 assert!(
1717 agent.memory_state.graph_config.enabled,
1718 "with_graph_config must set enabled flag"
1719 );
1720 }
1721
1722 #[test]
1728 fn apply_session_config_wires_graph_orchestration_anomaly() {
1729 use crate::config::Config;
1730
1731 let mut config = Config::default();
1732 config.memory.graph.enabled = true;
1733 config.orchestration.enabled = true;
1734 config.orchestration.max_tasks = 42;
1735 config.tools.anomaly.enabled = true;
1736 config.tools.anomaly.window_size = 7;
1737
1738 let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1739
1740 assert!(session_cfg.graph_config.enabled);
1742 assert!(session_cfg.orchestration_config.enabled);
1743 assert_eq!(session_cfg.orchestration_config.max_tasks, 42);
1744 assert!(session_cfg.anomaly_config.enabled);
1745 assert_eq!(session_cfg.anomaly_config.window_size, 7);
1746
1747 let agent = make_agent().apply_session_config(session_cfg);
1748
1749 assert!(
1751 agent.memory_state.graph_config.enabled,
1752 "apply_session_config must wire graph_config into agent"
1753 );
1754
1755 assert!(
1757 agent.orchestration.orchestration_config.enabled,
1758 "apply_session_config must wire orchestration_config into agent"
1759 );
1760 assert_eq!(
1761 agent.orchestration.orchestration_config.max_tasks, 42,
1762 "orchestration max_tasks must match config"
1763 );
1764
1765 assert!(
1767 agent.debug_state.anomaly_detector.is_some(),
1768 "apply_session_config must create anomaly_detector when enabled"
1769 );
1770 }
1771
1772 #[test]
1773 fn with_focus_config_propagates_to_focus_state() {
1774 let cfg = crate::config::FocusConfig {
1775 enabled: true,
1776 compression_interval: 7,
1777 ..Default::default()
1778 };
1779 let agent = make_agent().with_focus_config(cfg.clone());
1780 assert!(
1781 agent.focus.config.enabled,
1782 "with_focus_config must set enabled"
1783 );
1784 assert_eq!(
1785 agent.focus.config.compression_interval, 7,
1786 "with_focus_config must propagate compression_interval"
1787 );
1788 }
1789
1790 #[test]
1791 fn with_sidequest_config_propagates_to_sidequest_state() {
1792 let cfg = crate::config::SidequestConfig {
1793 enabled: true,
1794 interval_turns: 3,
1795 ..Default::default()
1796 };
1797 let agent = make_agent().with_sidequest_config(cfg.clone());
1798 assert!(
1799 agent.sidequest.config.enabled,
1800 "with_sidequest_config must set enabled"
1801 );
1802 assert_eq!(
1803 agent.sidequest.config.interval_turns, 3,
1804 "with_sidequest_config must propagate interval_turns"
1805 );
1806 }
1807
1808 #[test]
1810 fn apply_session_config_skips_anomaly_detector_when_disabled() {
1811 use crate::config::Config;
1812
1813 let mut config = Config::default();
1814 config.tools.anomaly.enabled = false; let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1816 assert!(!session_cfg.anomaly_config.enabled);
1817
1818 let agent = make_agent().apply_session_config(session_cfg);
1819 assert!(
1820 agent.debug_state.anomaly_detector.is_none(),
1821 "apply_session_config must not create anomaly_detector when disabled"
1822 );
1823 }
1824
1825 #[test]
1826 fn with_two_stage_matching_sets_flag() {
1827 let agent = make_agent().with_two_stage_matching(true);
1828 assert!(
1829 agent.skill_state.two_stage_matching,
1830 "with_two_stage_matching(true) must enable two_stage_matching"
1831 );
1832
1833 let agent = make_agent().with_two_stage_matching(false);
1834 assert!(
1835 !agent.skill_state.two_stage_matching,
1836 "with_two_stage_matching(false) must disable two_stage_matching"
1837 );
1838 }
1839
1840 #[test]
1841 fn with_confusability_threshold_sets_and_clamps() {
1842 let agent = make_agent().with_confusability_threshold(0.85);
1843 assert!(
1844 (agent.skill_state.confusability_threshold - 0.85).abs() < f32::EPSILON,
1845 "with_confusability_threshold must store the provided value"
1846 );
1847
1848 let agent = make_agent().with_confusability_threshold(1.5);
1849 assert!(
1850 (agent.skill_state.confusability_threshold - 1.0).abs() < f32::EPSILON,
1851 "with_confusability_threshold must clamp values above 1.0"
1852 );
1853
1854 let agent = make_agent().with_confusability_threshold(-0.1);
1855 assert!(
1856 agent.skill_state.confusability_threshold.abs() < f32::EPSILON,
1857 "with_confusability_threshold must clamp values below 0.0"
1858 );
1859 }
1860}