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 crate::channel::Channel;
14use crate::config::{
15 CompressionConfig, LearningConfig, RoutingConfig, SecurityConfig, TimeoutConfig,
16};
17use crate::config_watcher::ConfigEvent;
18use crate::context::ContextBudget;
19use crate::cost::CostTracker;
20use crate::instructions::{InstructionEvent, InstructionReloadState};
21use crate::metrics::MetricsSnapshot;
22use zeph_memory::semantic::SemanticMemory;
23use zeph_skills::watcher::SkillEvent;
24
25impl<C: Channel> Agent<C> {
26 #[must_use]
27 pub fn with_autosave_config(mut self, autosave_assistant: bool, min_length: usize) -> Self {
28 self.memory_state.autosave_assistant = autosave_assistant;
29 self.memory_state.autosave_min_length = min_length;
30 self
31 }
32
33 #[must_use]
34 pub fn with_tool_call_cutoff(mut self, cutoff: usize) -> Self {
35 self.memory_state.tool_call_cutoff = cutoff;
36 self
37 }
38
39 #[must_use]
40 pub fn with_response_cache(
41 mut self,
42 cache: std::sync::Arc<zeph_memory::ResponseCache>,
43 ) -> Self {
44 self.response_cache = Some(cache);
45 self
46 }
47
48 #[must_use]
54 pub fn with_parent_tool_use_id(mut self, id: impl Into<String>) -> Self {
55 self.parent_tool_use_id = Some(id.into());
56 self
57 }
58
59 #[must_use]
60 pub fn with_stt(mut self, stt: Box<dyn zeph_llm::stt::SpeechToText>) -> Self {
61 self.providers.stt = Some(stt);
62 self
63 }
64
65 #[must_use]
67 pub fn with_debug_dumper(mut self, dumper: crate::debug_dump::DebugDumper) -> Self {
68 self.debug_state.debug_dumper = Some(dumper);
69 self
70 }
71
72 #[must_use]
74 pub fn with_trace_collector(
75 mut self,
76 collector: crate::debug_dump::trace::TracingCollector,
77 ) -> Self {
78 self.debug_state.trace_collector = Some(collector);
79 self
80 }
81
82 #[must_use]
84 pub fn with_trace_config(
85 mut self,
86 dump_dir: std::path::PathBuf,
87 service_name: impl Into<String>,
88 redact: bool,
89 ) -> Self {
90 self.debug_state.dump_dir = Some(dump_dir);
91 self.debug_state.trace_service_name = service_name.into();
92 self.debug_state.trace_redact = redact;
93 self
94 }
95
96 #[cfg(feature = "lsp-context")]
98 #[must_use]
99 pub fn with_lsp_hooks(mut self, runner: crate::lsp_hooks::LspHookRunner) -> Self {
100 self.lsp_hooks = Some(runner);
101 self
102 }
103
104 #[must_use]
105 pub fn with_update_notifications(mut self, rx: mpsc::Receiver<String>) -> Self {
106 self.lifecycle.update_notify_rx = Some(rx);
107 self
108 }
109
110 #[must_use]
111 pub fn with_custom_task_rx(mut self, rx: mpsc::Receiver<String>) -> Self {
112 self.lifecycle.custom_task_rx = Some(rx);
113 self
114 }
115
116 #[must_use]
118 pub fn add_tool_executor(
119 mut self,
120 extra: impl zeph_tools::executor::ToolExecutor + 'static,
121 ) -> Self {
122 let existing = Arc::clone(&self.tool_executor);
123 let combined = zeph_tools::CompositeExecutor::new(zeph_tools::DynExecutor(existing), extra);
124 self.tool_executor = Arc::new(combined);
125 self
126 }
127
128 #[must_use]
129 pub fn with_max_tool_iterations(mut self, max: usize) -> Self {
130 self.tool_orchestrator.max_iterations = max;
131 self
132 }
133
134 #[must_use]
136 pub fn with_max_tool_retries(mut self, max: usize) -> Self {
137 self.tool_orchestrator.max_tool_retries = max.min(5);
138 self
139 }
140
141 #[must_use]
143 pub fn with_max_retry_duration_secs(mut self, secs: u64) -> Self {
144 self.tool_orchestrator.max_retry_duration_secs = secs;
145 self
146 }
147
148 #[must_use]
151 pub fn with_tool_repeat_threshold(mut self, threshold: usize) -> Self {
152 self.tool_orchestrator.repeat_threshold = threshold;
153 self.tool_orchestrator.recent_tool_calls = VecDeque::with_capacity(2 * threshold.max(1));
154 self
155 }
156
157 #[must_use]
158 pub fn with_memory(
159 mut self,
160 memory: Arc<SemanticMemory>,
161 conversation_id: zeph_memory::ConversationId,
162 history_limit: u32,
163 recall_limit: usize,
164 summarization_threshold: usize,
165 ) -> Self {
166 self.memory_state.memory = Some(memory);
167 self.memory_state.conversation_id = Some(conversation_id);
168 self.memory_state.history_limit = history_limit;
169 self.memory_state.recall_limit = recall_limit;
170 self.memory_state.summarization_threshold = summarization_threshold;
171 self.update_metrics(|m| {
172 m.qdrant_available = false;
173 m.sqlite_conversation_id = Some(conversation_id);
174 });
175 self
176 }
177
178 #[must_use]
179 pub fn with_embedding_model(mut self, model: String) -> Self {
180 self.skill_state.embedding_model = model;
181 self
182 }
183
184 #[must_use]
185 pub fn with_disambiguation_threshold(mut self, threshold: f32) -> Self {
186 self.skill_state.disambiguation_threshold = threshold;
187 self
188 }
189
190 #[must_use]
191 pub fn with_skill_prompt_mode(mut self, mode: crate::config::SkillPromptMode) -> Self {
192 self.skill_state.prompt_mode = mode;
193 self
194 }
195
196 #[must_use]
197 pub fn with_document_config(mut self, config: crate::config::DocumentConfig) -> Self {
198 self.memory_state.document_config = config;
199 self
200 }
201
202 #[must_use]
203 pub fn with_compression_guidelines_config(
204 mut self,
205 config: zeph_memory::CompressionGuidelinesConfig,
206 ) -> Self {
207 self.memory_state.compression_guidelines_config = config;
208 self
209 }
210
211 #[must_use]
212 pub fn with_graph_config(mut self, config: crate::config::GraphConfig) -> Self {
213 if config.enabled {
216 tracing::warn!(
217 "graph-memory is enabled: extracted entities are stored without PII redaction. \
218 Do not use with sensitive personal data until redaction is implemented."
219 );
220 }
221 self.memory_state.graph_config = config;
222 self
223 }
224
225 #[must_use]
226 pub fn with_anomaly_detector(mut self, detector: zeph_tools::AnomalyDetector) -> Self {
227 self.debug_state.anomaly_detector = Some(detector);
228 self
229 }
230
231 #[must_use]
232 pub fn with_instruction_blocks(
233 mut self,
234 blocks: Vec<crate::instructions::InstructionBlock>,
235 ) -> Self {
236 self.instruction_blocks = blocks;
237 self
238 }
239
240 #[must_use]
241 pub fn with_instruction_reload(
242 mut self,
243 rx: mpsc::Receiver<InstructionEvent>,
244 state: InstructionReloadState,
245 ) -> Self {
246 self.instruction_reload_rx = Some(rx);
247 self.instruction_reload_state = Some(state);
248 self
249 }
250
251 #[must_use]
252 pub fn with_shutdown(mut self, rx: watch::Receiver<bool>) -> Self {
253 self.lifecycle.shutdown = rx;
254 self
255 }
256
257 #[must_use]
258 pub fn with_skill_reload(
259 mut self,
260 paths: Vec<PathBuf>,
261 rx: mpsc::Receiver<SkillEvent>,
262 ) -> Self {
263 self.skill_state.skill_paths = paths;
264 self.skill_state.skill_reload_rx = Some(rx);
265 self
266 }
267
268 #[must_use]
269 pub fn with_managed_skills_dir(mut self, dir: PathBuf) -> Self {
270 self.skill_state.managed_dir = Some(dir);
271 self
272 }
273
274 #[must_use]
275 pub fn with_trust_config(mut self, config: crate::config::TrustConfig) -> Self {
276 self.skill_state.trust_config = config;
277 self
278 }
279
280 #[must_use]
281 pub fn with_config_reload(mut self, path: PathBuf, rx: mpsc::Receiver<ConfigEvent>) -> Self {
282 self.lifecycle.config_path = Some(path);
283 self.lifecycle.config_reload_rx = Some(rx);
284 self
285 }
286
287 #[must_use]
288 pub fn with_logging_config(mut self, logging: crate::config::LoggingConfig) -> Self {
289 self.debug_state.logging_config = logging;
290 self
291 }
292
293 #[must_use]
294 pub fn with_available_secrets(
295 mut self,
296 secrets: impl IntoIterator<Item = (String, crate::vault::Secret)>,
297 ) -> Self {
298 self.skill_state.available_custom_secrets = secrets.into_iter().collect();
299 self
300 }
301
302 #[must_use]
306 pub fn with_hybrid_search(mut self, enabled: bool) -> Self {
307 self.skill_state.hybrid_search = enabled;
308 if enabled {
309 let reg = self
310 .skill_state
311 .registry
312 .read()
313 .expect("registry read lock");
314 let all_meta = reg.all_meta();
315 let descs: Vec<&str> = all_meta.iter().map(|m| m.description.as_str()).collect();
316 self.skill_state.bm25_index = Some(zeph_skills::bm25::Bm25Index::build(&descs));
317 }
318 self
319 }
320
321 #[must_use]
322 pub fn with_learning(mut self, config: LearningConfig) -> Self {
323 if config.correction_detection {
324 self.feedback_detector = super::feedback_detector::FeedbackDetector::new(
325 config.correction_confidence_threshold,
326 );
327 if config.detector_mode == crate::config::DetectorMode::Judge {
328 self.judge_detector = Some(super::feedback_detector::JudgeDetector::new(
329 config.judge_adaptive_low,
330 config.judge_adaptive_high,
331 ));
332 }
333 }
334 self.learning_engine.config = Some(config);
335 self
336 }
337
338 #[must_use]
339 pub fn with_judge_provider(mut self, provider: AnyProvider) -> Self {
340 self.providers.judge_provider = Some(provider);
341 self
342 }
343
344 #[must_use]
348 pub fn with_server_compaction(mut self, enabled: bool) -> Self {
349 self.providers.server_compaction_active = enabled;
350 self
351 }
352
353 #[must_use]
354 pub fn with_mcp(
355 mut self,
356 tools: Vec<zeph_mcp::McpTool>,
357 registry: Option<zeph_mcp::McpToolRegistry>,
358 manager: Option<std::sync::Arc<zeph_mcp::McpManager>>,
359 mcp_config: &crate::config::McpConfig,
360 ) -> Self {
361 self.mcp.tools = tools;
362 self.mcp.registry = registry;
363 self.mcp.manager = manager;
364 self.mcp
365 .allowed_commands
366 .clone_from(&mcp_config.allowed_commands);
367 self.mcp.max_dynamic = mcp_config.max_dynamic_servers;
368 self
369 }
370
371 #[must_use]
372 pub fn with_mcp_shared_tools(
373 mut self,
374 shared: std::sync::Arc<std::sync::RwLock<Vec<zeph_mcp::McpTool>>>,
375 ) -> Self {
376 self.mcp.shared_tools = Some(shared);
377 self
378 }
379
380 #[must_use]
384 pub fn with_mcp_tool_rx(
385 mut self,
386 rx: tokio::sync::watch::Receiver<Vec<zeph_mcp::McpTool>>,
387 ) -> Self {
388 self.mcp.tool_rx = Some(rx);
389 self
390 }
391
392 #[must_use]
393 pub fn with_security(mut self, security: SecurityConfig, timeouts: TimeoutConfig) -> Self {
394 self.security.sanitizer =
395 crate::sanitizer::ContentSanitizer::new(&security.content_isolation);
396 self.security.exfiltration_guard = crate::sanitizer::exfiltration::ExfiltrationGuard::new(
397 security.exfiltration_guard.clone(),
398 );
399 self.security.pii_filter =
400 crate::sanitizer::pii::PiiFilter::new(security.pii_filter.clone());
401 self.security.memory_validator =
402 crate::sanitizer::memory_validation::MemoryWriteValidator::new(
403 security.memory_validation.clone(),
404 );
405 self.rate_limiter =
406 crate::agent::rate_limiter::ToolRateLimiter::new(security.rate_limit.clone());
407 self.runtime.security = security;
408 self.runtime.timeouts = timeouts;
409 self
410 }
411
412 #[must_use]
413 pub fn with_redact_credentials(mut self, enabled: bool) -> Self {
414 self.runtime.redact_credentials = enabled;
415 self
416 }
417
418 #[must_use]
419 pub fn with_tool_summarization(mut self, enabled: bool) -> Self {
420 self.tool_orchestrator.summarize_tool_output_enabled = enabled;
421 self
422 }
423
424 #[must_use]
425 pub fn with_overflow_config(mut self, config: zeph_tools::OverflowConfig) -> Self {
426 self.tool_orchestrator.overflow_config = config;
427 self
428 }
429
430 #[must_use]
431 pub fn with_summary_provider(mut self, provider: AnyProvider) -> Self {
432 self.providers.summary_provider = Some(provider);
433 self
434 }
435
436 #[must_use]
437 pub fn with_quarantine_summarizer(
438 mut self,
439 qs: crate::sanitizer::quarantine::QuarantinedSummarizer,
440 ) -> Self {
441 self.security.quarantine_summarizer = Some(qs);
442 self
443 }
444
445 pub(super) fn summary_or_primary_provider(&self) -> &AnyProvider {
446 self.providers
447 .summary_provider
448 .as_ref()
449 .unwrap_or(&self.provider)
450 }
451
452 pub(super) fn last_assistant_response(&self) -> String {
454 self.messages
455 .iter()
456 .rev()
457 .find(|m| m.role == zeph_llm::provider::Role::Assistant)
458 .map(|m| super::context::truncate_chars(&m.content, 500))
459 .unwrap_or_default()
460 }
461
462 #[must_use]
463 pub fn with_permission_policy(mut self, policy: zeph_tools::PermissionPolicy) -> Self {
464 self.runtime.permission_policy = policy;
465 self
466 }
467
468 #[must_use]
469 pub fn with_context_budget(
470 mut self,
471 budget_tokens: usize,
472 reserve_ratio: f32,
473 hard_compaction_threshold: f32,
474 compaction_preserve_tail: usize,
475 prune_protect_tokens: usize,
476 ) -> Self {
477 if budget_tokens > 0 {
478 self.context_manager.budget = Some(ContextBudget::new(budget_tokens, reserve_ratio));
479 }
480 self.context_manager.hard_compaction_threshold = hard_compaction_threshold;
481 self.context_manager.compaction_preserve_tail = compaction_preserve_tail;
482 self.context_manager.prune_protect_tokens = prune_protect_tokens;
483 self
484 }
485
486 #[must_use]
487 pub fn with_soft_compaction_threshold(mut self, threshold: f32) -> Self {
488 self.context_manager.soft_compaction_threshold = threshold;
489 self
490 }
491
492 #[must_use]
497 pub fn with_compaction_cooldown(mut self, cooldown_turns: u8) -> Self {
498 self.context_manager.compaction_cooldown_turns = cooldown_turns;
499 self
500 }
501
502 #[must_use]
503 pub fn with_compression(mut self, compression: CompressionConfig) -> Self {
504 self.context_manager.compression = compression;
505 self
506 }
507
508 #[must_use]
509 pub fn with_routing(mut self, routing: RoutingConfig) -> Self {
510 self.context_manager.routing = routing;
511 self
512 }
513
514 #[must_use]
515 pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
516 self.runtime.model_name = name.into();
517 self
518 }
519
520 #[must_use]
521 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
522 let path = path.into();
523 self.env_context =
524 crate::context::EnvironmentContext::gather_for_dir(&self.runtime.model_name, &path);
525 self
526 }
527
528 #[must_use]
529 pub fn with_warmup_ready(mut self, rx: watch::Receiver<bool>) -> Self {
530 self.lifecycle.warmup_ready = Some(rx);
531 self
532 }
533
534 #[must_use]
535 pub fn with_cost_tracker(mut self, tracker: CostTracker) -> Self {
536 self.metrics.cost_tracker = Some(tracker);
537 self
538 }
539
540 #[must_use]
541 pub fn with_extended_context(mut self, enabled: bool) -> Self {
542 self.metrics.extended_context = enabled;
543 self
544 }
545
546 #[must_use]
547 pub fn with_repo_map(mut self, token_budget: usize, ttl_secs: u64) -> Self {
548 self.index.repo_map_tokens = token_budget;
549 self.index.repo_map_ttl = std::time::Duration::from_secs(ttl_secs);
550 self
551 }
552
553 #[must_use]
554 pub fn with_code_retriever(
555 mut self,
556 retriever: std::sync::Arc<zeph_index::retriever::CodeRetriever>,
557 ) -> Self {
558 self.index.retriever = Some(retriever);
559 self
560 }
561
562 #[must_use]
566 pub fn with_metrics(mut self, tx: watch::Sender<MetricsSnapshot>) -> Self {
567 let provider_name = self.provider.name().to_string();
568 let model_name = self.runtime.model_name.clone();
569 let total_skills = self
570 .skill_state
571 .registry
572 .read()
573 .expect("registry read lock")
574 .all_meta()
575 .len();
576 let qdrant_available = false;
577 let conversation_id = self.memory_state.conversation_id;
578 let prompt_estimate = self
579 .messages
580 .first()
581 .map_or(0, |m| u64::try_from(m.content.len()).unwrap_or(0) / 4);
582 let mcp_tool_count = self.mcp.tools.len();
583 let mcp_server_count = self
584 .mcp
585 .tools
586 .iter()
587 .map(|t| &t.server_id)
588 .collect::<std::collections::HashSet<_>>()
589 .len();
590 let extended_context = self.metrics.extended_context;
591 tx.send_modify(|m| {
592 m.provider_name = provider_name;
593 m.model_name = model_name;
594 m.total_skills = total_skills;
595 m.qdrant_available = qdrant_available;
596 m.sqlite_conversation_id = conversation_id;
597 m.context_tokens = prompt_estimate;
598 m.prompt_tokens = prompt_estimate;
599 m.total_tokens = prompt_estimate;
600 m.mcp_tool_count = mcp_tool_count;
601 m.mcp_server_count = mcp_server_count;
602 m.extended_context = extended_context;
603 });
604 self.metrics.metrics_tx = Some(tx);
605 self
606 }
607
608 #[must_use]
612 pub fn cancel_signal(&self) -> Arc<Notify> {
613 Arc::clone(&self.lifecycle.cancel_signal)
614 }
615
616 #[must_use]
619 pub fn with_cancel_signal(mut self, signal: Arc<Notify>) -> Self {
620 self.lifecycle.cancel_signal = signal;
621 self
622 }
623
624 #[must_use]
625 pub fn with_subagent_manager(mut self, manager: crate::subagent::SubAgentManager) -> Self {
626 self.orchestration.subagent_manager = Some(manager);
627 self
628 }
629
630 #[must_use]
631 pub fn with_subagent_config(mut self, config: crate::config::SubAgentConfig) -> Self {
632 self.orchestration.subagent_config = config;
633 self
634 }
635
636 #[must_use]
637 pub fn with_orchestration_config(mut self, config: crate::config::OrchestrationConfig) -> Self {
638 self.orchestration.orchestration_config = config;
639 self
640 }
641
642 #[cfg(feature = "experiments")]
644 #[must_use]
645 pub fn with_experiment_config(mut self, config: crate::config::ExperimentConfig) -> Self {
646 self.experiment_config = config;
647 self
648 }
649
650 #[cfg(feature = "experiments")]
656 #[must_use]
657 pub fn with_experiment_baseline(
658 mut self,
659 baseline: crate::experiments::ConfigSnapshot,
660 ) -> Self {
661 self.experiment_baseline = baseline;
662 self
663 }
664
665 #[must_use]
668 pub fn with_provider_override(
669 mut self,
670 slot: Arc<std::sync::RwLock<Option<AnyProvider>>>,
671 ) -> Self {
672 self.providers.provider_override = Some(slot);
673 self
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::super::agent_tests::{
680 MockChannel, MockToolExecutor, create_test_registry, mock_provider,
681 };
682 use super::*;
683 use crate::config::{CompressionStrategy, RoutingStrategy};
684
685 fn make_agent() -> Agent<MockChannel> {
686 Agent::new(
687 mock_provider(vec![]),
688 MockChannel::new(vec![]),
689 create_test_registry(),
690 None,
691 5,
692 MockToolExecutor::no_tools(),
693 )
694 }
695
696 #[test]
697 fn with_compression_sets_proactive_strategy() {
698 let compression = CompressionConfig {
699 strategy: CompressionStrategy::Proactive {
700 threshold_tokens: 50_000,
701 max_summary_tokens: 2_000,
702 },
703 model: String::new(),
704 };
705 let agent = make_agent().with_compression(compression);
706 assert!(
707 matches!(
708 agent.context_manager.compression.strategy,
709 CompressionStrategy::Proactive {
710 threshold_tokens: 50_000,
711 max_summary_tokens: 2_000,
712 }
713 ),
714 "expected Proactive strategy after with_compression"
715 );
716 }
717
718 #[test]
719 fn with_routing_sets_routing_config() {
720 let routing = RoutingConfig {
721 strategy: RoutingStrategy::Heuristic,
722 };
723 let agent = make_agent().with_routing(routing);
724 assert_eq!(
725 agent.context_manager.routing.strategy,
726 RoutingStrategy::Heuristic,
727 "routing strategy must be set by with_routing"
728 );
729 }
730
731 #[test]
732 fn default_compression_is_reactive() {
733 let agent = make_agent();
734 assert_eq!(
735 agent.context_manager.compression.strategy,
736 CompressionStrategy::Reactive,
737 "default compression strategy must be Reactive"
738 );
739 }
740
741 #[test]
742 fn default_routing_is_heuristic() {
743 let agent = make_agent();
744 assert_eq!(
745 agent.context_manager.routing.strategy,
746 RoutingStrategy::Heuristic,
747 "default routing strategy must be Heuristic"
748 );
749 }
750
751 #[test]
752 fn with_cancel_signal_replaces_internal_signal() {
753 let agent = Agent::new(
754 mock_provider(vec![]),
755 MockChannel::new(vec![]),
756 create_test_registry(),
757 None,
758 5,
759 MockToolExecutor::no_tools(),
760 );
761
762 let shared = Arc::new(Notify::new());
763 let agent = agent.with_cancel_signal(Arc::clone(&shared));
764
765 assert!(Arc::ptr_eq(&shared, &agent.cancel_signal()));
767 }
768
769 #[tokio::test]
774 async fn with_managed_skills_dir_enables_install_command() {
775 let provider = mock_provider(vec![]);
776 let channel = MockChannel::new(vec![]);
777 let registry = create_test_registry();
778 let executor = MockToolExecutor::no_tools();
779 let managed = tempfile::tempdir().unwrap();
780
781 let mut agent_no_dir = Agent::new(
782 mock_provider(vec![]),
783 MockChannel::new(vec![]),
784 create_test_registry(),
785 None,
786 5,
787 MockToolExecutor::no_tools(),
788 );
789 agent_no_dir
790 .handle_skill_command("install /some/path")
791 .await
792 .unwrap();
793 let sent_no_dir = agent_no_dir.channel.sent_messages();
794 assert!(
795 sent_no_dir.iter().any(|s| s.contains("not configured")),
796 "without managed dir: {sent_no_dir:?}"
797 );
798
799 let _ = (provider, channel, registry, executor);
800 let mut agent_with_dir = Agent::new(
801 mock_provider(vec![]),
802 MockChannel::new(vec![]),
803 create_test_registry(),
804 None,
805 5,
806 MockToolExecutor::no_tools(),
807 )
808 .with_managed_skills_dir(managed.path().to_path_buf());
809
810 agent_with_dir
811 .handle_skill_command("install /nonexistent/path")
812 .await
813 .unwrap();
814 let sent_with_dir = agent_with_dir.channel.sent_messages();
815 assert!(
816 !sent_with_dir.iter().any(|s| s.contains("not configured")),
817 "with managed dir should not say not configured: {sent_with_dir:?}"
818 );
819 assert!(
820 sent_with_dir.iter().any(|s| s.contains("Install failed")),
821 "with managed dir should fail due to bad path: {sent_with_dir:?}"
822 );
823 }
824
825 #[test]
826 fn default_graph_config_is_disabled() {
827 let agent = make_agent();
828 assert!(
829 !agent.memory_state.graph_config.enabled,
830 "graph_config must default to disabled"
831 );
832 }
833
834 #[test]
835 fn with_graph_config_enabled_sets_flag() {
836 let cfg = crate::config::GraphConfig {
837 enabled: true,
838 ..Default::default()
839 };
840 let agent = make_agent().with_graph_config(cfg);
841 assert!(
842 agent.memory_state.graph_config.enabled,
843 "with_graph_config must set enabled flag"
844 );
845 }
846}