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, RoutingConfig, SecurityConfig, TimeoutConfig,
18};
19use crate::config_watcher::ConfigEvent;
20use crate::context::ContextBudget;
21use crate::cost::CostTracker;
22use crate::instructions::{InstructionEvent, InstructionReloadState};
23use crate::metrics::MetricsSnapshot;
24use zeph_memory::semantic::SemanticMemory;
25use zeph_skills::watcher::SkillEvent;
26
27impl<C: Channel> Agent<C> {
28 #[must_use]
32 pub fn with_status_tx(mut self, tx: tokio::sync::mpsc::UnboundedSender<String>) -> Self {
33 self.session.status_tx = Some(tx);
34 self
35 }
36
37 #[cfg(feature = "policy-enforcer")]
39 #[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]
46 pub fn with_structured_summaries(mut self, enabled: bool) -> Self {
47 self.memory_state.structured_summaries = enabled;
48 self
49 }
50
51 #[must_use]
52 pub fn with_autosave_config(mut self, autosave_assistant: bool, min_length: usize) -> Self {
53 self.memory_state.autosave_assistant = autosave_assistant;
54 self.memory_state.autosave_min_length = min_length;
55 self
56 }
57
58 #[must_use]
59 pub fn with_tool_call_cutoff(mut self, cutoff: usize) -> Self {
60 self.memory_state.tool_call_cutoff = cutoff;
61 self
62 }
63
64 #[must_use]
65 pub fn with_shutdown_summary_config(
66 mut self,
67 enabled: bool,
68 min_messages: usize,
69 max_messages: usize,
70 timeout_secs: u64,
71 ) -> Self {
72 self.memory_state.shutdown_summary = enabled;
73 self.memory_state.shutdown_summary_min_messages = min_messages;
74 self.memory_state.shutdown_summary_max_messages = max_messages;
75 self.memory_state.shutdown_summary_timeout_secs = timeout_secs;
76 self
77 }
78
79 #[must_use]
80 pub fn with_response_cache(
81 mut self,
82 cache: std::sync::Arc<zeph_memory::ResponseCache>,
83 ) -> Self {
84 self.session.response_cache = Some(cache);
85 self
86 }
87
88 #[must_use]
94 pub fn with_parent_tool_use_id(mut self, id: impl Into<String>) -> Self {
95 self.session.parent_tool_use_id = Some(id.into());
96 self
97 }
98
99 #[must_use]
100 pub fn with_stt(mut self, stt: Box<dyn zeph_llm::stt::SpeechToText>) -> Self {
101 self.providers.stt = Some(stt);
102 self
103 }
104
105 #[must_use]
109 pub fn with_embedding_provider(mut self, provider: AnyProvider) -> Self {
110 self.embedding_provider = provider;
111 self
112 }
113
114 #[must_use]
116 pub fn with_provider_pool(
117 mut self,
118 pool: Vec<ProviderEntry>,
119 snapshot: ProviderConfigSnapshot,
120 ) -> Self {
121 self.providers.provider_pool = pool;
122 self.providers.provider_config_snapshot = Some(snapshot);
123 self
124 }
125
126 #[must_use]
128 pub fn with_debug_dumper(mut self, dumper: crate::debug_dump::DebugDumper) -> Self {
129 self.debug_state.debug_dumper = Some(dumper);
130 self
131 }
132
133 #[must_use]
135 pub fn with_trace_collector(
136 mut self,
137 collector: crate::debug_dump::trace::TracingCollector,
138 ) -> Self {
139 self.debug_state.trace_collector = Some(collector);
140 self
141 }
142
143 #[must_use]
145 pub fn with_trace_config(
146 mut self,
147 dump_dir: std::path::PathBuf,
148 service_name: impl Into<String>,
149 redact: bool,
150 ) -> Self {
151 self.debug_state.dump_dir = Some(dump_dir);
152 self.debug_state.trace_service_name = service_name.into();
153 self.debug_state.trace_redact = redact;
154 self
155 }
156
157 #[cfg(feature = "lsp-context")]
159 #[must_use]
160 pub fn with_lsp_hooks(mut self, runner: crate::lsp_hooks::LspHookRunner) -> Self {
161 self.session.lsp_hooks = Some(runner);
162 self
163 }
164
165 #[must_use]
166 pub fn with_update_notifications(mut self, rx: mpsc::Receiver<String>) -> Self {
167 self.lifecycle.update_notify_rx = Some(rx);
168 self
169 }
170
171 #[must_use]
172 pub fn with_custom_task_rx(mut self, rx: mpsc::Receiver<String>) -> Self {
173 self.lifecycle.custom_task_rx = Some(rx);
174 self
175 }
176
177 #[must_use]
179 pub fn add_tool_executor(
180 mut self,
181 extra: impl zeph_tools::executor::ToolExecutor + 'static,
182 ) -> Self {
183 let existing = Arc::clone(&self.tool_executor);
184 let combined = zeph_tools::CompositeExecutor::new(zeph_tools::DynExecutor(existing), extra);
185 self.tool_executor = Arc::new(combined);
186 self
187 }
188
189 #[must_use]
190 pub fn with_max_tool_iterations(mut self, max: usize) -> Self {
191 self.tool_orchestrator.max_iterations = max;
192 self
193 }
194
195 #[must_use]
197 pub fn with_max_tool_retries(mut self, max: usize) -> Self {
198 self.tool_orchestrator.max_tool_retries = max.min(5);
199 self
200 }
201
202 #[must_use]
204 pub fn with_max_retry_duration_secs(mut self, secs: u64) -> Self {
205 self.tool_orchestrator.max_retry_duration_secs = secs;
206 self
207 }
208
209 #[must_use]
211 pub fn with_parameter_reformat_provider(mut self, provider: impl Into<String>) -> Self {
212 self.tool_orchestrator.parameter_reformat_provider = provider.into();
213 self
214 }
215
216 #[must_use]
218 pub fn with_retry_backoff(mut self, base_ms: u64, max_ms: u64) -> Self {
219 self.tool_orchestrator.retry_base_ms = base_ms;
220 self.tool_orchestrator.retry_max_ms = max_ms;
221 self
222 }
223
224 #[must_use]
227 pub fn with_tool_repeat_threshold(mut self, threshold: usize) -> Self {
228 self.tool_orchestrator.repeat_threshold = threshold;
229 self.tool_orchestrator.recent_tool_calls = VecDeque::with_capacity(2 * threshold.max(1));
230 self
231 }
232
233 #[must_use]
234 pub fn with_memory(
235 mut self,
236 memory: Arc<SemanticMemory>,
237 conversation_id: zeph_memory::ConversationId,
238 history_limit: u32,
239 recall_limit: usize,
240 summarization_threshold: usize,
241 ) -> Self {
242 self.memory_state.memory = Some(memory);
243 self.memory_state.conversation_id = Some(conversation_id);
244 self.memory_state.history_limit = history_limit;
245 self.memory_state.recall_limit = recall_limit;
246 self.memory_state.summarization_threshold = summarization_threshold;
247 self.update_metrics(|m| {
248 m.qdrant_available = false;
249 m.sqlite_conversation_id = Some(conversation_id);
250 });
251 self
252 }
253
254 #[must_use]
255 pub fn with_embedding_model(mut self, model: String) -> Self {
256 self.skill_state.embedding_model = model;
257 self
258 }
259
260 #[must_use]
261 pub fn with_disambiguation_threshold(mut self, threshold: f32) -> Self {
262 self.skill_state.disambiguation_threshold = threshold;
263 self
264 }
265
266 #[must_use]
267 pub fn with_skill_prompt_mode(mut self, mode: crate::config::SkillPromptMode) -> Self {
268 self.skill_state.prompt_mode = mode;
269 self
270 }
271
272 #[must_use]
273 pub fn with_document_config(mut self, config: crate::config::DocumentConfig) -> Self {
274 self.memory_state.document_config = config;
275 self
276 }
277
278 #[must_use]
279 pub fn with_compression_guidelines_config(
280 mut self,
281 config: zeph_memory::CompressionGuidelinesConfig,
282 ) -> Self {
283 self.memory_state.compression_guidelines_config = config;
284 self
285 }
286
287 #[must_use]
288 pub fn with_graph_config(mut self, config: crate::config::GraphConfig) -> Self {
289 if config.enabled {
292 tracing::warn!(
293 "graph-memory is enabled: extracted entities are stored without PII redaction. \
294 Do not use with sensitive personal data until redaction is implemented."
295 );
296 }
297 self.memory_state.graph_config = config;
298 self
299 }
300
301 #[must_use]
302 pub fn with_anomaly_detector(mut self, detector: zeph_tools::AnomalyDetector) -> Self {
303 self.debug_state.anomaly_detector = Some(detector);
304 self
305 }
306
307 #[must_use]
308 pub fn with_instruction_blocks(
309 mut self,
310 blocks: Vec<crate::instructions::InstructionBlock>,
311 ) -> Self {
312 self.instructions.blocks = blocks;
313 self
314 }
315
316 #[must_use]
317 pub fn with_instruction_reload(
318 mut self,
319 rx: mpsc::Receiver<InstructionEvent>,
320 state: InstructionReloadState,
321 ) -> Self {
322 self.instructions.reload_rx = Some(rx);
323 self.instructions.reload_state = Some(state);
324 self
325 }
326
327 #[must_use]
328 pub fn with_shutdown(mut self, rx: watch::Receiver<bool>) -> Self {
329 self.lifecycle.shutdown = rx;
330 self
331 }
332
333 #[must_use]
334 pub fn with_skill_reload(
335 mut self,
336 paths: Vec<PathBuf>,
337 rx: mpsc::Receiver<SkillEvent>,
338 ) -> Self {
339 self.skill_state.skill_paths = paths;
340 self.skill_state.skill_reload_rx = Some(rx);
341 self
342 }
343
344 #[must_use]
345 pub fn with_managed_skills_dir(mut self, dir: PathBuf) -> Self {
346 self.skill_state.managed_dir = Some(dir);
347 self
348 }
349
350 #[must_use]
351 pub fn with_trust_config(mut self, config: crate::config::TrustConfig) -> Self {
352 self.skill_state.trust_config = config;
353 self
354 }
355
356 #[must_use]
357 pub fn with_config_reload(mut self, path: PathBuf, rx: mpsc::Receiver<ConfigEvent>) -> Self {
358 self.lifecycle.config_path = Some(path);
359 self.lifecycle.config_reload_rx = Some(rx);
360 self
361 }
362
363 #[must_use]
364 pub fn with_logging_config(mut self, logging: crate::config::LoggingConfig) -> Self {
365 self.debug_state.logging_config = logging;
366 self
367 }
368
369 #[must_use]
370 pub fn with_available_secrets(
371 mut self,
372 secrets: impl IntoIterator<Item = (String, crate::vault::Secret)>,
373 ) -> Self {
374 self.skill_state.available_custom_secrets = secrets.into_iter().collect();
375 self
376 }
377
378 #[must_use]
382 pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
383 self.skill_state.hybrid_search = enabled;
384 if enabled {
385 let reg = self
386 .skill_state
387 .registry
388 .read()
389 .expect("registry read lock");
390 let all_meta = reg.all_meta();
391 let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
392 self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
393 }
394 self
395 }
396
397 #[must_use]
398 pub fn with_learning(mut self, config: LearningConfig) -> Self {
399 if config.correction_detection {
400 self.feedback.detector = super::feedback_detector::FeedbackDetector::new(
401 config.correction_confidence_threshold,
402 );
403 if config.detector_mode == crate::config::DetectorMode::Judge {
404 self.feedback.judge = Some(super::feedback_detector::JudgeDetector::new(
405 config.judge_adaptive_low,
406 config.judge_adaptive_high,
407 ));
408 }
409 }
410 self.learning_engine.config = Some(config);
411 self
412 }
413
414 #[must_use]
420 pub fn with_llm_classifier(
421 mut self,
422 classifier: zeph_llm::classifier::llm::LlmClassifier,
423 ) -> Self {
424 self.feedback.llm_classifier = Some(classifier);
425 self
426 }
427
428 #[must_use]
429 pub fn with_judge_provider(mut self, provider: AnyProvider) -> Self {
430 self.providers.judge_provider = Some(provider);
431 self
432 }
433
434 #[must_use]
435 pub fn with_probe_provider(mut self, provider: AnyProvider) -> Self {
436 self.providers.probe_provider = Some(provider);
437 self
438 }
439
440 #[must_use]
441 pub fn with_planner_provider(mut self, provider: AnyProvider) -> Self {
442 self.orchestration.planner_provider = Some(provider);
443 self
444 }
445
446 #[must_use]
450 pub fn with_server_compaction(mut self, enabled: bool) -> Self {
451 self.providers.server_compaction_active = enabled;
452 self
453 }
454
455 #[must_use]
456 pub fn with_mcp(
457 mut self,
458 tools: Vec<zeph_mcp::McpTool>,
459 registry: Option<zeph_mcp::McpToolRegistry>,
460 manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
461 mcp_config: &crate::config::McpConfig,
462 ) -> Self {
463 self.mcp.tools = tools;
464 self.mcp.registry = registry;
465 self.mcp.manager = manager;
466 self.mcp
467 .allowed_commands
468 .clone_from(&mcp_config.allowed_commands);
469 self.mcp.max_dynamic = mcp_config.max_dynamic_servers;
470 self
471 }
472
473 #[must_use]
474 pub fn with_mcp_shared_tools(
475 mut self,
476 shared: std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>,
477 ) -> Self {
478 self.mcp.shared_tools = Some(shared);
479 self
480 }
481
482 #[must_use]
486 pub fn with_mcp_tool_rx(
487 mut self,
488 rx: tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>,
489 ) -> Self {
490 self.mcp.tool_rx = Some(rx);
491 self
492 }
493
494 #[must_use]
495 pub fn with_security(mut self, security: SecurityConfig, timeouts: TimeoutConfig) -> Self {
496 self.security.sanitizer =
497 zeph_sanitizer::ContentSanitizer::new(&security.content_isolation);
498 self.security.exfiltration_guard = zeph_sanitizer::exfiltration::ExfiltrationGuard::new(
499 security.exfiltration_guard.clone(),
500 );
501 self.security.pii_filter = zeph_sanitizer::pii::PiiFilter::new(security.pii_filter.clone());
502 self.security.memory_validator =
503 zeph_sanitizer::memory_validation::MemoryWriteValidator::new(
504 security.memory_validation.clone(),
505 );
506 self.runtime.rate_limiter =
507 crate::agent::rate_limiter::ToolRateLimiter::new(security.rate_limit.clone());
508
509 let mut verifiers: Vec<Box<dyn zeph_tools::PreExecutionVerifier>> = Vec::new();
514 if security.pre_execution_verify.enabled {
515 let dcfg = &security.pre_execution_verify.destructive_commands;
516 if dcfg.enabled {
517 verifiers.push(Box::new(zeph_tools::DestructiveCommandVerifier::new(dcfg)));
518 }
519 let icfg = &security.pre_execution_verify.injection_patterns;
520 if icfg.enabled {
521 verifiers.push(Box::new(zeph_tools::InjectionPatternVerifier::new(icfg)));
522 }
523 let ucfg = &security.pre_execution_verify.url_grounding;
524 if ucfg.enabled {
525 verifiers.push(Box::new(zeph_tools::UrlGroundingVerifier::new(
526 ucfg,
527 std::sync::Arc::clone(&self.security.user_provided_urls),
528 )));
529 }
530 }
531 self.tool_orchestrator.pre_execution_verifiers = verifiers;
532
533 self.security.response_verifier = zeph_sanitizer::response_verifier::ResponseVerifier::new(
534 security.response_verification.clone(),
535 );
536
537 self.runtime.security = security;
538 self.runtime.timeouts = timeouts;
539 self
540 }
541
542 #[must_use]
543 pub fn with_redact_credentials(mut self, enabled: bool) -> Self {
544 self.runtime.redact_credentials = enabled;
545 self
546 }
547
548 #[must_use]
549 pub fn with_tool_summarization(mut self, enabled: bool) -> Self {
550 self.tool_orchestrator.summarize_tool_output_enabled = enabled;
551 self
552 }
553
554 #[must_use]
555 pub fn with_overflow_config(mut self, config: zeph_tools::OverflowConfig) -> Self {
556 self.tool_orchestrator.overflow_config = config;
557 self
558 }
559
560 #[must_use]
564 pub fn with_tafc_config(mut self, config: zeph_tools::TafcConfig) -> Self {
565 self.tool_orchestrator.tafc = config.validated();
566 self
567 }
568
569 #[must_use]
570 pub fn with_result_cache_config(mut self, config: &zeph_tools::ResultCacheConfig) -> Self {
571 self.tool_orchestrator.set_cache_config(config);
572 self
573 }
574
575 #[must_use]
576 pub fn with_summary_provider(mut self, provider: AnyProvider) -> Self {
577 self.providers.summary_provider = Some(provider);
578 self
579 }
580
581 #[must_use]
582 pub fn with_quarantine_summarizer(
583 mut self,
584 qs: zeph_sanitizer::quarantine::QuarantinedSummarizer,
585 ) -> Self {
586 self.security.quarantine_summarizer = Some(qs);
587 self
588 }
589
590 #[cfg(feature = "classifiers")]
595 #[must_use]
596 pub fn with_injection_classifier(
597 mut self,
598 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
599 timeout_ms: u64,
600 threshold: f32,
601 ) -> Self {
602 let old = std::mem::replace(
604 &mut self.security.sanitizer,
605 zeph_sanitizer::ContentSanitizer::new(
606 &zeph_sanitizer::ContentIsolationConfig::default(),
607 ),
608 );
609 self.security.sanitizer = old.with_classifier(backend, timeout_ms, threshold);
610 self
611 }
612
613 #[cfg(feature = "classifiers")]
618 #[must_use]
619 pub fn with_pii_detector(
620 mut self,
621 detector: std::sync::Arc<dyn zeph_llm::classifier::PiiDetector>,
622 threshold: f32,
623 ) -> Self {
624 let old = std::mem::replace(
625 &mut self.security.sanitizer,
626 zeph_sanitizer::ContentSanitizer::new(
627 &zeph_sanitizer::ContentIsolationConfig::default(),
628 ),
629 );
630 self.security.sanitizer = old.with_pii_detector(detector, threshold);
631 self
632 }
633
634 #[cfg(feature = "classifiers")]
639 #[must_use]
640 pub fn with_pii_ner_classifier(
641 mut self,
642 backend: std::sync::Arc<dyn zeph_llm::classifier::ClassifierBackend>,
643 timeout_ms: u64,
644 ) -> Self {
645 self.security.pii_ner_backend = Some(backend);
646 self.security.pii_ner_timeout_ms = timeout_ms;
647 self
648 }
649
650 #[cfg(feature = "guardrail")]
651 #[must_use]
652 pub fn with_guardrail(mut self, filter: zeph_sanitizer::guardrail::GuardrailFilter) -> Self {
653 use zeph_sanitizer::guardrail::GuardrailAction;
654 let warn_mode = filter.action() == GuardrailAction::Warn;
655 self.security.guardrail = Some(filter);
656 self.update_metrics(|m| {
657 m.guardrail_enabled = true;
658 m.guardrail_warn_mode = warn_mode;
659 });
660 self
661 }
662
663 pub(super) fn summary_or_primary_provider(&self) -> &AnyProvider {
664 self.providers
665 .summary_provider
666 .as_ref()
667 .unwrap_or(&self.provider)
668 }
669
670 pub(super) fn probe_or_summary_provider(&self) -> &AnyProvider {
671 self.providers
672 .probe_provider
673 .as_ref()
674 .or(self.providers.summary_provider.as_ref())
675 .unwrap_or(&self.provider)
676 }
677
678 pub(super) fn last_assistant_response(&self) -> String {
680 self.msg
681 .messages
682 .iter()
683 .rev()
684 .find(|m| m.role == zeph_llm::provider::Role::Assistant)
685 .map(|m| super::context::truncate_chars(&m.content, 500))
686 .unwrap_or_default()
687 }
688
689 #[must_use]
690 pub fn with_permission_policy(mut self, policy: zeph_tools::PermissionPolicy) -> Self {
691 self.runtime.permission_policy = policy;
692 self
693 }
694
695 #[must_use]
696 pub fn with_context_budget(
697 mut self,
698 budget_tokens: usize,
699 reserve_ratio: f32,
700 hard_compaction_threshold: f32,
701 compaction_preserve_tail: usize,
702 prune_protect_tokens: usize,
703 ) -> Self {
704 if budget_tokens == 0 {
705 tracing::warn!("context budget is 0 — agent will have no token tracking");
706 }
707 if budget_tokens > 0 {
708 self.context_manager.budget = Some(ContextBudget::new(budget_tokens, reserve_ratio));
709 }
710 self.context_manager.hard_compaction_threshold = hard_compaction_threshold;
711 self.context_manager.compaction_preserve_tail = compaction_preserve_tail;
712 self.context_manager.prune_protect_tokens = prune_protect_tokens;
713 self
714 }
715
716 #[must_use]
717 pub fn with_soft_compaction_threshold(mut self, threshold: f32) -> Self {
718 self.context_manager.soft_compaction_threshold = threshold;
719 self
720 }
721
722 #[must_use]
727 pub fn with_compaction_cooldown(mut self, cooldown_turns: u8) -> Self {
728 self.context_manager.compaction_cooldown_turns = cooldown_turns;
729 self
730 }
731
732 #[must_use]
733 pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
734 self.context_manager.compression = compression;
735 self
736 }
737
738 #[must_use]
740 pub fn with_focus_config(mut self, config: crate::config::FocusConfig) -> Self {
741 self.focus = super::focus::FocusState::new(config);
742 self
743 }
744
745 #[must_use]
747 pub fn with_sidequest_config(mut self, config: crate::config::SidequestConfig) -> Self {
748 self.sidequest = super::sidequest::SidequestState::new(config);
749 self
750 }
751
752 #[must_use]
753 pub fn with_routing(mut self, routing: RoutingConfig) -> Self {
754 self.context_manager.routing = routing;
755 self
756 }
757
758 #[must_use]
759 pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
760 self.runtime.model_name = name.into();
761 self
762 }
763
764 #[must_use]
769 pub fn with_active_provider_name(mut self, name: impl Into<String>) -> Self {
770 self.runtime.active_provider_name = name.into();
771 self
772 }
773
774 #[must_use]
775 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
776 let path = path.into();
777 self.session.env_context =
778 crate::context::EnvironmentContext::gather_for_dir(&self.runtime.model_name, &path);
779 self
780 }
781
782 #[must_use]
783 pub fn with_warmup_ready(mut self, rx: watch::Receiver<bool>) -> Self {
784 self.lifecycle.warmup_ready = Some(rx);
785 self
786 }
787
788 #[must_use]
789 pub fn with_cost_tracker(mut self, tracker: CostTracker) -> Self {
790 self.metrics.cost_tracker = Some(tracker);
791 self
792 }
793
794 #[must_use]
795 pub fn with_extended_context(mut self, enabled: bool) -> Self {
796 self.metrics.extended_context = enabled;
797 self
798 }
799
800 #[must_use]
801 pub fn with_repo_map(mut self, token_budget: usize, ttl_secs: u64) -> Self {
802 self.index.repo_map_tokens = token_budget;
803 self.index.repo_map_ttl = std::time::Duration::from_secs(ttl_secs);
804 self
805 }
806
807 #[must_use]
808 pub fn with_code_retriever(
809 mut self,
810 retriever: std::sync::Arc<zeph_index::retriever::CodeRetriever>,
811 ) -> Self {
812 self.index.retriever = Some(retriever);
813 self
814 }
815
816 #[must_use]
820 pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
821 let provider_name = if self.runtime.active_provider_name.is_empty() {
822 self.provider.name().to_owned()
823 } else {
824 self.runtime.active_provider_name.clone()
825 };
826 let model_name = self.runtime.model_name.clone();
827 let total_skills = self
828 .skill_state
829 .registry
830 .read()
831 .expect("registry read lock")
832 .all_meta()
833 .len();
834 let qdrant_available = false;
835 let conversation_id = self.memory_state.conversation_id;
836 let prompt_estimate = self
837 .msg
838 .messages
839 .first()
840 .map_or(0, |m| u64::try_from(m.content.len()).unwrap_or(0) / 4);
841 let mcp_tool_count = self.mcp.tools.len();
842 let mcp_server_count = self
843 .mcp
844 .tools
845 .iter()
846 .map(|t| &t.server_id)
847 .collect::<std::collections::HashSet<_>>()
848 .len();
849 let extended_context = self.metrics.extended_context;
850 tx.send_modify(|m| {
851 m.provider_name = provider_name;
852 m.model_name = model_name;
853 m.total_skills = total_skills;
854 m.qdrant_available = qdrant_available;
855 m.sqlite_conversation_id = conversation_id;
856 m.context_tokens = prompt_estimate;
857 m.prompt_tokens = prompt_estimate;
858 m.total_tokens = prompt_estimate;
859 m.mcp_tool_count = mcp_tool_count;
860 m.mcp_server_count = mcp_server_count;
861 m.extended_context = extended_context;
862 });
863 self.metrics.metrics_tx = Some(tx);
864 self
865 }
866
867 #[must_use]
871 pub fn cancel_signal(&self) -> Arc<Notify> {
872 Arc::clone(&self.lifecycle.cancel_signal)
873 }
874
875 #[must_use]
878 pub fn with_cancel_signal(mut self, signal: Arc<Notify>) -> Self {
879 self.lifecycle.cancel_signal = signal;
880 self
881 }
882
883 #[must_use]
884 pub fn with_subagent_manager(mut self, manager: crate::subagent::SubAgentManager) -> Self {
885 self.orchestration.subagent_manager = Some(manager);
886 self
887 }
888
889 #[must_use]
890 pub fn with_subagent_config(mut self, config: crate::config::SubAgentConfig) -> Self {
891 self.orchestration.subagent_config = config;
892 self
893 }
894
895 #[must_use]
896 pub fn with_orchestration_config(mut self, config: crate::config::OrchestrationConfig) -> Self {
897 self.orchestration.orchestration_config = config;
898 self
899 }
900
901 #[cfg(feature = "experiments")]
903 #[must_use]
904 pub fn with_experiment_config(mut self, config: crate::config::ExperimentConfig) -> Self {
905 self.experiments.config = config;
906 self
907 }
908
909 #[cfg(feature = "experiments")]
915 #[must_use]
916 pub fn with_experiment_baseline(
917 mut self,
918 baseline: crate::experiments::ConfigSnapshot,
919 ) -> Self {
920 self.experiments.baseline = baseline;
921 self
922 }
923
924 #[cfg(feature = "experiments")]
929 #[must_use]
930 pub fn with_eval_provider(mut self, provider: AnyProvider) -> Self {
931 self.experiments.eval_provider = Some(provider);
932 self
933 }
934
935 #[must_use]
938 pub fn with_provider_override(
939 mut self,
940 slot: Arc<std::sync::RwLock<Option<AnyProvider>>>,
941 ) -> Self {
942 self.providers.provider_override = Some(slot);
943 self
944 }
945
946 #[must_use]
948 pub fn with_tool_schema_filter(mut self, filter: zeph_tools::ToolSchemaFilter) -> Self {
949 self.tool_schema_filter = Some(filter);
950 self
951 }
952
953 #[must_use]
955 pub fn with_dependency_config(mut self, config: zeph_tools::DependencyConfig) -> Self {
956 self.runtime.dependency_config = config;
957 self
958 }
959
960 #[must_use]
965 pub fn with_tool_dependency_graph(
966 mut self,
967 graph: zeph_tools::ToolDependencyGraph,
968 always_on: std::collections::HashSet<String>,
969 ) -> Self {
970 self.dependency_graph = Some(graph);
971 self.dependency_always_on = always_on;
972 self
973 }
974
975 pub async fn maybe_init_tool_schema_filter(
980 mut self,
981 config: &crate::config::ToolFilterConfig,
982 provider: &zeph_llm::any::AnyProvider,
983 ) -> Self {
984 use zeph_llm::provider::LlmProvider;
985
986 if !config.enabled {
987 return self;
988 }
989
990 let always_on_set: std::collections::HashSet<&str> =
991 config.always_on.iter().map(String::as_str).collect();
992 let defs = self.tool_executor.tool_definitions_erased();
993 let filterable: Vec<&zeph_tools::registry::ToolDef> = defs
994 .iter()
995 .filter(|d| !always_on_set.contains(d.id.as_ref()))
996 .collect();
997
998 if filterable.is_empty() {
999 tracing::info!("tool schema filter: all tools are always-on, nothing to filter");
1000 return self;
1001 }
1002
1003 let mut embeddings = Vec::with_capacity(filterable.len());
1004 for def in &filterable {
1005 let text = format!("{}: {}", def.id, def.description);
1006 match provider.embed(&text).await {
1007 Ok(emb) => {
1008 embeddings.push(zeph_tools::ToolEmbedding {
1009 tool_id: def.id.to_string(),
1010 embedding: emb,
1011 });
1012 }
1013 Err(e) => {
1014 tracing::info!(
1015 provider = provider.name(),
1016 "tool schema filter disabled: embedding not supported \
1017 by provider ({e:#})"
1018 );
1019 return self;
1020 }
1021 }
1022 }
1023
1024 tracing::info!(
1025 tool_count = embeddings.len(),
1026 always_on = config.always_on.len(),
1027 top_k = config.top_k,
1028 "tool schema filter initialized"
1029 );
1030
1031 let filter = zeph_tools::ToolSchemaFilter::new(
1032 config.always_on.clone(),
1033 config.top_k,
1034 config.min_description_words,
1035 embeddings,
1036 );
1037 self.tool_schema_filter = Some(filter);
1038 self
1039 }
1040
1041 #[must_use]
1049 pub fn apply_session_config(mut self, cfg: AgentSessionConfig) -> Self {
1050 let AgentSessionConfig {
1051 max_tool_iterations,
1052 max_tool_retries,
1053 max_retry_duration_secs,
1054 retry_base_ms,
1055 retry_max_ms,
1056 parameter_reformat_provider,
1057 tool_repeat_threshold,
1058 tool_summarization,
1059 tool_call_cutoff,
1060 overflow_config,
1061 permission_policy,
1062 model_name,
1063 embed_model,
1064 semantic_cache_enabled,
1065 semantic_cache_threshold,
1066 semantic_cache_max_candidates,
1067 budget_tokens,
1068 soft_compaction_threshold,
1069 hard_compaction_threshold,
1070 compaction_preserve_tail,
1071 compaction_cooldown_turns,
1072 prune_protect_tokens,
1073 redact_credentials,
1074 security,
1075 timeouts,
1076 learning,
1077 document_config,
1078 graph_config,
1079 anomaly_config,
1080 result_cache_config,
1081 orchestration_config,
1082 debug_config: _debug_config,
1085 server_compaction,
1086 secrets,
1087 } = cfg;
1088
1089 self = self
1090 .with_max_tool_iterations(max_tool_iterations)
1091 .with_max_tool_retries(max_tool_retries)
1092 .with_max_retry_duration_secs(max_retry_duration_secs)
1093 .with_retry_backoff(retry_base_ms, retry_max_ms)
1094 .with_parameter_reformat_provider(parameter_reformat_provider)
1095 .with_tool_repeat_threshold(tool_repeat_threshold)
1096 .with_model_name(model_name)
1097 .with_embedding_model(embed_model)
1098 .with_context_budget(
1099 budget_tokens,
1100 CONTEXT_BUDGET_RESERVE_RATIO,
1101 hard_compaction_threshold,
1102 compaction_preserve_tail,
1103 prune_protect_tokens,
1104 )
1105 .with_soft_compaction_threshold(soft_compaction_threshold)
1106 .with_compaction_cooldown(compaction_cooldown_turns)
1107 .with_security(security, timeouts)
1108 .with_redact_credentials(redact_credentials)
1109 .with_tool_summarization(tool_summarization)
1110 .with_overflow_config(overflow_config)
1111 .with_permission_policy(permission_policy)
1112 .with_learning(learning)
1113 .with_tool_call_cutoff(tool_call_cutoff)
1114 .with_available_secrets(
1115 secrets
1116 .iter()
1117 .map(|(k, v)| (k.clone(), crate::vault::Secret::new(v.expose().to_owned()))),
1118 )
1119 .with_server_compaction(server_compaction)
1120 .with_document_config(document_config)
1121 .with_graph_config(graph_config)
1122 .with_orchestration_config(orchestration_config);
1123
1124 if anomaly_config.enabled {
1125 self = self.with_anomaly_detector(zeph_tools::AnomalyDetector::new(
1126 anomaly_config.window_size,
1127 anomaly_config.error_threshold,
1128 anomaly_config.critical_threshold,
1129 ));
1130 }
1131
1132 self.runtime.semantic_cache_enabled = semantic_cache_enabled;
1133 self.runtime.semantic_cache_threshold = semantic_cache_threshold;
1134 self.runtime.semantic_cache_max_candidates = semantic_cache_max_candidates;
1135 self = self.with_result_cache_config(&result_cache_config);
1136
1137 self
1138 }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::super::agent_tests::{
1144 MockChannel, MockToolExecutor, create_test_registry, mock_provider,
1145 };
1146 use super::*;
1147 use crate::config::{CompressionStrategy, RoutingStrategy};
1148
1149 fn make_agent() -> Agent<MockChannel> {
1150 Agent::new(
1151 mock_provider(vec![]),
1152 MockChannel::new(vec![]),
1153 create_test_registry(),
1154 None,
1155 5,
1156 MockToolExecutor::no_tools(),
1157 )
1158 }
1159
1160 #[test]
1161 fn with_compression_sets_proactive_strategy() {
1162 let compression = CompressionConfig {
1163 strategy: CompressionStrategy::Proactive {
1164 threshold_tokens: 50_000,
1165 max_summary_tokens: 2_000,
1166 },
1167 model: String::new(),
1168 pruning_strategy: crate::config::PruningStrategy::default(),
1169 probe: Default::default(),
1170 };
1171 let agent = make_agent().with_compression(compression);
1172 assert!(
1173 matches!(
1174 agent.context_manager.compression.strategy,
1175 CompressionStrategy::Proactive {
1176 threshold_tokens: 50_000,
1177 max_summary_tokens: 2_000,
1178 }
1179 ),
1180 "expected Proactive strategy after with_compression"
1181 );
1182 }
1183
1184 #[test]
1185 fn with_routing_sets_routing_config() {
1186 let routing = RoutingConfig {
1187 strategy: RoutingStrategy::Heuristic,
1188 };
1189 let agent = make_agent().with_routing(routing);
1190 assert_eq!(
1191 agent.context_manager.routing.strategy,
1192 RoutingStrategy::Heuristic,
1193 "routing strategy must be set by with_routing"
1194 );
1195 }
1196
1197 #[test]
1198 fn default_compression_is_reactive() {
1199 let agent = make_agent();
1200 assert_eq!(
1201 agent.context_manager.compression.strategy,
1202 CompressionStrategy::Reactive,
1203 "default compression strategy must be Reactive"
1204 );
1205 }
1206
1207 #[test]
1208 fn default_routing_is_heuristic() {
1209 let agent = make_agent();
1210 assert_eq!(
1211 agent.context_manager.routing.strategy,
1212 RoutingStrategy::Heuristic,
1213 "default routing strategy must be Heuristic"
1214 );
1215 }
1216
1217 #[test]
1218 fn with_cancel_signal_replaces_internal_signal() {
1219 let agent = Agent::new(
1220 mock_provider(vec![]),
1221 MockChannel::new(vec![]),
1222 create_test_registry(),
1223 None,
1224 5,
1225 MockToolExecutor::no_tools(),
1226 );
1227
1228 let shared = Arc::new(Notify::new());
1229 let agent = agent.with_cancel_signal(Arc::clone(&shared));
1230
1231 assert!(Arc::ptr_eq(&shared, &agent.cancel_signal()));
1233 }
1234
1235 #[tokio::test]
1240 async fn with_managed_skills_dir_enables_install_command() {
1241 let provider = mock_provider(vec![]);
1242 let channel = MockChannel::new(vec![]);
1243 let registry = create_test_registry();
1244 let executor = MockToolExecutor::no_tools();
1245 let managed = tempfile::tempdir().unwrap();
1246
1247 let mut agent_no_dir = Agent::new(
1248 mock_provider(vec![]),
1249 MockChannel::new(vec![]),
1250 create_test_registry(),
1251 None,
1252 5,
1253 MockToolExecutor::no_tools(),
1254 );
1255 agent_no_dir
1256 .handle_skill_command("install /some/path")
1257 .await
1258 .unwrap();
1259 let sent_no_dir = agent_no_dir.channel.sent_messages();
1260 assert!(
1261 sent_no_dir.iter().any(|s| s.contains("not configured")),
1262 "without managed dir: {sent_no_dir:?}"
1263 );
1264
1265 let _ = (provider, channel, registry, executor);
1266 let mut agent_with_dir = Agent::new(
1267 mock_provider(vec![]),
1268 MockChannel::new(vec![]),
1269 create_test_registry(),
1270 None,
1271 5,
1272 MockToolExecutor::no_tools(),
1273 )
1274 .with_managed_skills_dir(managed.path().to_path_buf());
1275
1276 agent_with_dir
1277 .handle_skill_command("install /nonexistent/path")
1278 .await
1279 .unwrap();
1280 let sent_with_dir = agent_with_dir.channel.sent_messages();
1281 assert!(
1282 !sent_with_dir.iter().any(|s| s.contains("not configured")),
1283 "with managed dir should not say not configured: {sent_with_dir:?}"
1284 );
1285 assert!(
1286 sent_with_dir.iter().any(|s| s.contains("Install failed")),
1287 "with managed dir should fail due to bad path: {sent_with_dir:?}"
1288 );
1289 }
1290
1291 #[test]
1292 fn default_graph_config_is_disabled() {
1293 let agent = make_agent();
1294 assert!(
1295 !agent.memory_state.graph_config.enabled,
1296 "graph_config must default to disabled"
1297 );
1298 }
1299
1300 #[test]
1301 fn with_graph_config_enabled_sets_flag() {
1302 let cfg = crate::config::GraphConfig {
1303 enabled: true,
1304 ..Default::default()
1305 };
1306 let agent = make_agent().with_graph_config(cfg);
1307 assert!(
1308 agent.memory_state.graph_config.enabled,
1309 "with_graph_config must set enabled flag"
1310 );
1311 }
1312
1313 #[test]
1319 fn apply_session_config_wires_graph_orchestration_anomaly() {
1320 use crate::config::Config;
1321
1322 let mut config = Config::default();
1323 config.memory.graph.enabled = true;
1324 config.orchestration.enabled = true;
1325 config.orchestration.max_tasks = 42;
1326 config.tools.anomaly.enabled = true;
1327 config.tools.anomaly.window_size = 7;
1328
1329 let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1330
1331 assert!(session_cfg.graph_config.enabled);
1333 assert!(session_cfg.orchestration_config.enabled);
1334 assert_eq!(session_cfg.orchestration_config.max_tasks, 42);
1335 assert!(session_cfg.anomaly_config.enabled);
1336 assert_eq!(session_cfg.anomaly_config.window_size, 7);
1337
1338 let agent = make_agent().apply_session_config(session_cfg);
1339
1340 assert!(
1342 agent.memory_state.graph_config.enabled,
1343 "apply_session_config must wire graph_config into agent"
1344 );
1345
1346 assert!(
1348 agent.orchestration.orchestration_config.enabled,
1349 "apply_session_config must wire orchestration_config into agent"
1350 );
1351 assert_eq!(
1352 agent.orchestration.orchestration_config.max_tasks, 42,
1353 "orchestration max_tasks must match config"
1354 );
1355
1356 assert!(
1358 agent.debug_state.anomaly_detector.is_some(),
1359 "apply_session_config must create anomaly_detector when enabled"
1360 );
1361 }
1362
1363 #[test]
1364 fn with_focus_config_propagates_to_focus_state() {
1365 let cfg = crate::config::FocusConfig {
1366 enabled: true,
1367 compression_interval: 7,
1368 ..Default::default()
1369 };
1370 let agent = make_agent().with_focus_config(cfg.clone());
1371 assert!(
1372 agent.focus.config.enabled,
1373 "with_focus_config must set enabled"
1374 );
1375 assert_eq!(
1376 agent.focus.config.compression_interval, 7,
1377 "with_focus_config must propagate compression_interval"
1378 );
1379 }
1380
1381 #[test]
1382 fn with_sidequest_config_propagates_to_sidequest_state() {
1383 let cfg = crate::config::SidequestConfig {
1384 enabled: true,
1385 interval_turns: 3,
1386 ..Default::default()
1387 };
1388 let agent = make_agent().with_sidequest_config(cfg.clone());
1389 assert!(
1390 agent.sidequest.config.enabled,
1391 "with_sidequest_config must set enabled"
1392 );
1393 assert_eq!(
1394 agent.sidequest.config.interval_turns, 3,
1395 "with_sidequest_config must propagate interval_turns"
1396 );
1397 }
1398
1399 #[test]
1401 fn apply_session_config_skips_anomaly_detector_when_disabled() {
1402 use crate::config::Config;
1403
1404 let mut config = Config::default();
1405 config.tools.anomaly.enabled = false; let session_cfg = AgentSessionConfig::from_config(&config, 100_000);
1407 assert!(!session_cfg.anomaly_config.enabled);
1408
1409 let agent = make_agent().apply_session_config(session_cfg);
1410 assert!(
1411 agent.debug_state.anomaly_detector.is_none(),
1412 "apply_session_config must not create anomaly_detector when disabled"
1413 );
1414 }
1415}