1use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use tokio::sync::{mpsc, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use bamboo_agent_core::storage::{AttachmentReader, Storage};
16use bamboo_agent_core::tools::ToolExecutor;
17use bamboo_agent_core::{AgentEvent, Role, Session};
18use bamboo_config::PermissionMode;
19use bamboo_domain::ReasoningEffort;
20use bamboo_llm::Config;
21use bamboo_llm::LLMProvider;
22use bamboo_metrics::MetricsCollector;
23use bamboo_skills::SkillManager;
24
25use crate::runtime::config::{
26 AgentLoopConfig, AuxiliaryModelConfig, BashCompletionSink, BashResumeHook,
27 DisabledFilterResolver, GoldConfig, GuardianConfig, GuardianSpawner, ImageFallbackConfig,
28 PromptMemoryFlags,
29};
30use crate::runtime::hooks::HookRunner;
31use crate::runtime::model_roster::{ModelRoster, RoleModel};
32use crate::runtime::runner::run_agent_loop_with_config;
33use bamboo_domain::{RuntimeSessionPersistence, SessionInboxPort};
34
35use crate::session_activation::SessionActivationRouter;
36use crate::session_messaging::SessionMessenger;
37
38#[derive(Clone)]
47pub struct AgentRuntime {
48 pub storage: Arc<dyn Storage>,
49 pub persistence: Arc<dyn RuntimeSessionPersistence>,
50 pub session_inbox: Option<Arc<dyn SessionInboxPort>>,
51 pub activation_router: Option<Arc<SessionActivationRouter>>,
52 pub session_messenger: Option<Arc<SessionMessenger>>,
53 pub attachment_reader: Arc<dyn AttachmentReader>,
54 pub skill_manager: Arc<SkillManager>,
55 pub project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
56 pub metrics_collector: MetricsCollector,
57 pub config: Arc<RwLock<Config>>,
58 pub permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
60 pub permission_mode: PermissionMode,
62
63 pub provider: Arc<dyn LLMProvider>,
65
66 pub memory_store: bamboo_memory::memory_store::MemoryStore,
68
69 pub default_tools: Arc<dyn ToolExecutor>,
73
74 pub hook_runner: Arc<HookRunner>,
77}
78
79pub struct AgentRuntimeBuilder {
94 storage: Option<Arc<dyn Storage>>,
95 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
96 session_inbox: Option<Arc<dyn SessionInboxPort>>,
97 activation_router: Option<Arc<SessionActivationRouter>>,
98 session_messenger: Option<Arc<SessionMessenger>>,
99 attachment_reader: Option<Arc<dyn AttachmentReader>>,
100 skill_manager: Option<Arc<SkillManager>>,
101 project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
102 metrics_collector: Option<MetricsCollector>,
103 config: Option<Arc<RwLock<Config>>>,
104 permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
105 permission_mode: PermissionMode,
106 provider: Option<Arc<dyn LLMProvider>>,
107 memory_store: bamboo_memory::memory_store::MemoryStore,
108 default_tools: Option<Arc<dyn ToolExecutor>>,
109 hook_runner: Arc<HookRunner>,
110}
111
112impl AgentRuntimeBuilder {
113 pub fn new() -> Self {
114 Self {
115 storage: None,
116 persistence: None,
117 session_inbox: None,
118 activation_router: None,
119 session_messenger: None,
120 attachment_reader: None,
121 skill_manager: None,
122 project_context_resolver: None,
123 metrics_collector: None,
124 config: None,
125 permission_config: None,
126 permission_mode: PermissionMode::Default,
127 provider: None,
128 memory_store: bamboo_memory::memory_store::MemoryStore::with_defaults(),
129 default_tools: None,
130 hook_runner: Arc::new(HookRunner::new()),
131 }
132 }
133
134 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
135 self.storage = Some(v);
136 self
137 }
138
139 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
140 self.persistence = Some(v);
141 self
142 }
143
144 pub fn session_inbox(mut self, v: Arc<dyn SessionInboxPort>) -> Self {
145 self.session_inbox = Some(v);
146 self
147 }
148
149 pub fn activation_router(mut self, v: Arc<SessionActivationRouter>) -> Self {
150 self.activation_router = Some(v);
151 self
152 }
153
154 pub fn session_messenger(mut self, v: Arc<SessionMessenger>) -> Self {
155 self.session_messenger = Some(v);
156 self
157 }
158
159 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
160 self.attachment_reader = Some(v);
161 self
162 }
163
164 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
165 self.skill_manager = Some(v);
166 self
167 }
168
169 pub fn project_context_resolver(
170 mut self,
171 v: Arc<crate::project_context::ProjectContextResolver>,
172 ) -> Self {
173 self.project_context_resolver = Some(v);
174 self
175 }
176
177 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
178 self.metrics_collector = Some(v);
179 self
180 }
181
182 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
183 self.config = Some(v);
184 self
185 }
186
187 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
188 self.permission_config = Some(v);
189 self
190 }
191
192 pub fn permission_mode(mut self, v: PermissionMode) -> Self {
193 self.permission_mode = v;
194 self
195 }
196
197 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
198 self.provider = Some(v);
199 self
200 }
201
202 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
203 self.memory_store = v;
204 self
205 }
206
207 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
208 self.default_tools = Some(v);
209 self
210 }
211
212 pub fn hook_runner(mut self, v: Arc<HookRunner>) -> Self {
214 self.hook_runner = v;
215 self
216 }
217
218 pub fn build(self) -> Result<AgentRuntime, &'static str> {
219 if let (Some(router), Some(inbox)) = (&self.activation_router, &self.session_inbox) {
220 router.set_inbox(inbox.clone());
221 }
222 Ok(AgentRuntime {
223 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
224 persistence: self
225 .persistence
226 .ok_or_else(|| format_missing("persistence"))?,
227 session_inbox: self.session_inbox,
228 activation_router: self.activation_router,
229 session_messenger: self.session_messenger,
230 attachment_reader: self
231 .attachment_reader
232 .ok_or_else(|| format_missing("attachment_reader"))?,
233 skill_manager: self
234 .skill_manager
235 .ok_or_else(|| format_missing("skill_manager"))?,
236 project_context_resolver: self.project_context_resolver,
237 metrics_collector: self
238 .metrics_collector
239 .ok_or_else(|| format_missing("metrics_collector"))?,
240 config: self.config.ok_or_else(|| format_missing("config"))?,
241 permission_config: self.permission_config,
242 permission_mode: self.permission_mode,
243 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
244 memory_store: self.memory_store,
245 default_tools: self
246 .default_tools
247 .ok_or_else(|| format_missing("default_tools"))?,
248 hook_runner: self.hook_runner,
249 })
250 }
251}
252
253fn format_missing(field: &str) -> &'static str {
254 match field {
257 "storage" => "AgentRuntimeBuilder: missing storage",
258 "persistence" => "AgentRuntimeBuilder: missing persistence",
259 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
260 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
261 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
262 "config" => "AgentRuntimeBuilder: missing config",
263 "provider" => "AgentRuntimeBuilder: missing provider",
264 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
265 _ => "AgentRuntimeBuilder: missing required field",
266 }
267}
268
269impl Default for AgentRuntimeBuilder {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275pub struct ExecuteRequest {
285 pub initial_message: String,
287 pub event_tx: mpsc::Sender<AgentEvent>,
288 pub cancel_token: CancellationToken,
289
290 pub tools: Option<Arc<dyn ToolExecutor>>,
294 pub provider_override: Option<Arc<dyn LLMProvider>>,
297
298 pub model_roster: ModelRoster,
305 pub reasoning_effort: Option<ReasoningEffort>,
306 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
309 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
312 pub disabled_tools: Option<BTreeSet<String>>,
314 pub disabled_skill_ids: Option<BTreeSet<String>>,
316 pub selected_skill_ids: Option<Vec<String>>,
317 pub selected_skill_mode: Option<String>,
318 pub image_fallback: Option<ImageFallbackConfig>,
319 pub gold_config: Option<GoldConfig>,
320 pub guardian_config: Option<GuardianConfig>,
322 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
325 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
328 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
331 pub app_data_dir: Option<std::path::PathBuf>,
333 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
341}
342
343pub struct ExecuteRequestBuilder {
359 initial_message: String,
360 event_tx: mpsc::Sender<AgentEvent>,
361 cancel_token: CancellationToken,
362
363 tools: Option<Arc<dyn ToolExecutor>>,
364 provider_override: Option<Arc<dyn LLMProvider>>,
365 model: Option<String>,
369 provider_name: Option<String>,
370 provider_type: Option<String>,
371 fast_model: Option<String>,
372 fast_model_provider: Option<Arc<dyn LLMProvider>>,
373 background_model: Option<String>,
374 background_model_provider: Option<Arc<dyn LLMProvider>>,
375 summarization_model: Option<String>,
376 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
377 reasoning_effort: Option<ReasoningEffort>,
378 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
379 disabled_filter_resolver: Option<DisabledFilterResolver>,
380 disabled_tools: Option<BTreeSet<String>>,
381 disabled_skill_ids: Option<BTreeSet<String>>,
382 selected_skill_ids: Option<Vec<String>>,
383 selected_skill_mode: Option<String>,
384 image_fallback: Option<ImageFallbackConfig>,
385 gold_config: Option<GoldConfig>,
386 guardian_config: Option<GuardianConfig>,
387 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
388 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
389 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
390 app_data_dir: Option<std::path::PathBuf>,
391 run_budget: Option<bamboo_config::RunBudgetConfig>,
392}
393
394impl ExecuteRequestBuilder {
395 pub fn new(
398 initial_message: impl Into<String>,
399 event_tx: mpsc::Sender<AgentEvent>,
400 cancel_token: CancellationToken,
401 ) -> Self {
402 Self {
403 initial_message: initial_message.into(),
404 event_tx,
405 cancel_token,
406 tools: None,
407 provider_override: None,
408 model: None,
409 provider_name: None,
410 provider_type: None,
411 fast_model: None,
412 fast_model_provider: None,
413 background_model: None,
414 background_model_provider: None,
415 summarization_model: None,
416 summarization_model_provider: None,
417 reasoning_effort: None,
418 auxiliary_model_resolver: None,
419 disabled_filter_resolver: None,
420 disabled_tools: None,
421 disabled_skill_ids: None,
422 selected_skill_ids: None,
423 selected_skill_mode: None,
424 image_fallback: None,
425 gold_config: None,
426 guardian_config: None,
427 guardian_spawner: None,
428 bash_resume_hook: None,
429 bash_completion_sink: None,
430 app_data_dir: None,
431 run_budget: None,
432 }
433 }
434
435 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
437 self.tools = Some(v);
438 self
439 }
440
441 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
443 self.provider_override = Some(v);
444 self
445 }
446
447 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
453 self.fast_model = roster.fast_model();
454 self.fast_model_provider = roster.fast_model_provider();
455 self.background_model = roster.background_model();
456 self.background_model_provider = roster.background_model_provider();
457 self.summarization_model = roster.summarization_model();
458 self.summarization_model_provider = roster.summarization_model_provider();
459 self.model = roster.model;
460 self.provider_name = roster.provider_name;
461 self.provider_type = roster.provider_type;
462 self
463 }
464
465 pub fn model(mut self, v: impl Into<String>) -> Self {
467 self.model = Some(v.into());
468 self
469 }
470
471 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
473 self.provider_name = Some(v.into());
474 self
475 }
476
477 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
479 self.provider_type = Some(v.into());
480 self
481 }
482
483 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
485 self.fast_model = Some(v.into());
486 self
487 }
488
489 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
491 self.fast_model_provider = Some(v);
492 self
493 }
494
495 pub fn background_model(mut self, v: impl Into<String>) -> Self {
497 self.background_model = Some(v.into());
498 self
499 }
500
501 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
503 self.background_model_provider = Some(v);
504 self
505 }
506
507 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
509 self.summarization_model = Some(v.into());
510 self
511 }
512
513 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
515 self.summarization_model_provider = Some(v);
516 self
517 }
518
519 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
521 self.reasoning_effort = Some(v);
522 self
523 }
524
525 pub fn auxiliary_model_resolver(
527 mut self,
528 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
529 ) -> Self {
530 self.auxiliary_model_resolver = Some(v);
531 self
532 }
533
534 pub fn disabled_filter_resolver(mut self, v: DisabledFilterResolver) -> Self {
536 self.disabled_filter_resolver = Some(v);
537 self
538 }
539
540 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
542 self.disabled_tools = Some(v);
543 self
544 }
545
546 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
548 self.disabled_skill_ids = Some(v);
549 self
550 }
551
552 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
554 self.selected_skill_ids = Some(v);
555 self
556 }
557
558 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
560 self.selected_skill_mode = Some(v.into());
561 self
562 }
563
564 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
566 self.image_fallback = Some(v);
567 self
568 }
569
570 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
577 self.gold_config = v;
578 self
579 }
580
581 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
584 self.guardian_config = v;
585 self
586 }
587
588 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
591 self.guardian_spawner = v;
592 self
593 }
594
595 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
598 self.bash_resume_hook = v;
599 self
600 }
601
602 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
605 self.bash_completion_sink = v;
606 self
607 }
608
609 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
611 self.app_data_dir = Some(v);
612 self
613 }
614
615 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
620 self.run_budget = Some(v);
621 self
622 }
623
624 pub fn build(self) -> ExecuteRequest {
629 let model_roster = ModelRoster {
630 model: self.model,
631 provider_name: self.provider_name,
632 provider_type: self.provider_type,
633 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
634 background: RoleModel::from_parts(
635 self.background_model,
636 self.background_model_provider,
637 ),
638 summarization: RoleModel::from_parts(
639 self.summarization_model,
640 self.summarization_model_provider,
641 ),
642 };
643 ExecuteRequest {
644 initial_message: self.initial_message,
645 event_tx: self.event_tx,
646 cancel_token: self.cancel_token,
647 tools: self.tools,
648 provider_override: self.provider_override,
649 model_roster,
650 reasoning_effort: self.reasoning_effort,
651 auxiliary_model_resolver: self.auxiliary_model_resolver,
652 disabled_filter_resolver: self.disabled_filter_resolver,
653 disabled_tools: self.disabled_tools,
654 disabled_skill_ids: self.disabled_skill_ids,
655 selected_skill_ids: self.selected_skill_ids,
656 selected_skill_mode: self.selected_skill_mode,
657 image_fallback: self.image_fallback,
658 gold_config: self.gold_config,
659 guardian_config: self.guardian_config,
660 guardian_spawner: self.guardian_spawner,
661 bash_resume_hook: self.bash_resume_hook,
662 bash_completion_sink: self.bash_completion_sink,
663 app_data_dir: self.app_data_dir,
664 run_budget: self.run_budget,
665 }
666 }
667}
668
669fn extract_system_prompt(session: &Session) -> Option<String> {
675 session
676 .messages
677 .iter()
678 .find(|m| matches!(m.role, Role::System))
679 .map(|m| m.content.clone())
680}
681
682pub(super) fn should_observe_completed_tool_trace(
683 result: &crate::runtime::runner::Result<()>,
684 final_checkpoint_succeeded: bool,
685 session: &Session,
686) -> bool {
687 final_checkpoint_succeeded
688 && result.is_ok()
689 && session
690 .agent_runtime_state
691 .as_ref()
692 .is_some_and(|state| matches!(state.status, bamboo_domain::AgentStatusState::Completed))
693 && !session.metadata.contains_key("runtime.suspend_reason")
694 && !session.metadata.contains_key("runtime.completion_reason")
695}
696
697impl AgentRuntime {
702 pub async fn execute(
707 &self,
708 session: &mut Session,
709 req: ExecuteRequest,
710 ) -> crate::runtime::runner::Result<()> {
711 let session_activation_notifications = match self.activation_router.as_ref() {
712 Some(router) => Some(Arc::new(parking_lot::Mutex::new(
713 router.subscribe(&session.id).await,
714 ))),
715 None => None,
716 };
717 let guidance_active_run_id = match self.activation_router.as_ref() {
718 Some(router) => router.current_run_id(&session.id).await,
719 None => None,
720 };
721 let system_prompt = extract_system_prompt(session);
722 let config = self.config.read().await;
723 let ExecuteRequest {
724 initial_message,
725 event_tx,
726 cancel_token,
727 tools,
728 provider_override,
729 model_roster,
730 reasoning_effort,
731 auxiliary_model_resolver,
732 disabled_filter_resolver,
733 disabled_tools,
734 disabled_skill_ids,
735 selected_skill_ids,
736 selected_skill_mode,
737 image_fallback,
738 gold_config,
739 guardian_config,
740 guardian_spawner,
741 bash_resume_hook,
742 bash_completion_sink,
743 app_data_dir,
744 run_budget,
745 } = req;
746 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
747 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
748 let configured_permission_mode = self
749 .permission_config
750 .as_ref()
751 .map(|config| config.mode())
752 .unwrap_or(self.permission_mode);
753 let active_plan_gate = session
754 .agent_runtime_state
755 .as_ref()
756 .is_some_and(|state| state.plan_mode.is_some());
757
758 let fast_model = model_roster.fast_model();
763 let fast_model_provider = model_roster.fast_model_provider();
764 let background_model = model_roster.background_model();
765 let background_model_provider = model_roster.background_model_provider();
766 let summarization_model = model_roster.summarization_model();
767 let summarization_model_provider = model_roster.summarization_model_provider();
768 let ModelRoster {
769 model,
770 provider_name,
771 provider_type,
772 ..
773 } = model_roster;
774 let hook_runner = Arc::new(
775 self.hook_runner
776 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir.clone()),
777 );
778
779 let loop_config = AgentLoopConfig {
780 guidance_active_run_id,
781 max_rounds: 200,
782 system_prompt,
783 legacy_model_limits: config.extra.get("model_limits").cloned(),
786 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
787 selected_skill_ids,
788 selected_skill_mode,
789 skill_manager: Some(self.skill_manager.clone()),
790 project_context_resolver: self.project_context_resolver.clone(),
791 skip_initial_user_message: true,
792 storage: Some(self.storage.clone()),
793 persistence: Some(self.persistence.clone()),
794 session_inbox: self.session_inbox.clone(),
795 session_activation_notifications,
796 attachment_reader: Some(self.attachment_reader.clone()),
797 metrics_collector: Some(self.metrics_collector.clone()),
798 model_name: model,
799 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
800 fast_model_provider,
801 auxiliary_evaluation_max_concurrency:
802 crate::runtime::config::normalize_auxiliary_evaluation_max_concurrency(
803 config
804 .extra
805 .get("auxiliary_evaluation_max_concurrency")
806 .and_then(serde_json::Value::as_u64),
807 ),
808 background_model_name: background_model
809 .or_else(|| config.get_memory_background_model()),
810 planning_model_name: config
811 .defaults
812 .as_ref()
813 .and_then(|d| d.planning.as_ref())
814 .map(|r| r.model.clone()),
815 search_model_name: config
816 .defaults
817 .as_ref()
818 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
819 .map(|r| r.model.clone()),
820 compression_instructions: None,
821 summary_target_ratio: config
822 .memory()
823 .as_ref()
824 .map(|memory| memory.summary_target_ratio)
825 .unwrap_or(0.20),
826 context_management: config.context_management.clone(),
827 summary_safe_window_percent: config
828 .memory()
829 .as_ref()
830 .map(|memory| memory.summary_safe_window_percent)
831 .unwrap_or(80),
832 summarization_model_name: summarization_model
833 .or_else(|| config.get_task_summary_model()),
834 background_model_provider,
835 summarization_model_provider,
836 provider_name: Some(
837 provider_name.unwrap_or_else(|| config.effective_default_provider().to_string()),
838 ),
839 provider_type,
840 reasoning_effort,
841 auxiliary_model_resolver,
842 disabled_filter_resolver,
843 disabled_tools: {
844 let mut merged = config.disabled_tool_references();
845 if let Some(dt) = disabled_tools {
846 merged.extend(dt);
847 }
848 merged
849 },
850 image_fallback,
851 app_data_dir,
852 memory_store: self.memory_store.clone(),
853 prompt_memory_flags: config
854 .memory()
855 .as_ref()
856 .map(PromptMemoryFlags::from)
857 .unwrap_or_default(),
858 features_dynamic_model_routing: config.features.dynamic_model_routing,
859 permission_mode: Some(if active_plan_gate {
860 PermissionMode::Plan
861 } else {
862 configured_permission_mode
863 }),
864 gold_config,
865 guardian_config,
866 guardian_spawner,
867 bash_resume_hook,
868 bash_completion_sink,
869 hook_runner,
870 mcp_tool_guidance: tools.tool_guidance(),
874 run_budget: config.run_budget.merged_with_override(run_budget.as_ref()),
880 stream_timeout: config.stream_timeout,
881 ..Default::default()
882 };
883
884 drop(config);
885
886 let trace_message_start = session.messages.len();
887 let session_end_runner = loop_config.hook_runner.clone();
888 let session_end_event_tx = event_tx.clone();
889 let result = run_agent_loop_with_config(
890 session,
891 initial_message,
892 event_tx,
893 llm,
894 tools,
895 cancel_token,
896 loop_config,
897 )
898 .await;
899
900 crate::runtime::hooks::run_session_end_hooks(
901 &session_end_runner,
902 &result,
903 session,
904 &session_end_event_tx,
905 )
906 .await;
907
908 let final_checkpoint_succeeded =
920 match self.persistence.checkpoint_runtime_session(session).await {
921 Ok(()) => true,
922 Err(checkpoint_error) => {
923 match &result {
924 Ok(()) => tracing::warn!(
925 session_id = %session.id,
926 error = %checkpoint_error,
927 "failed to checkpoint session transcript after successful execution"
928 ),
929 Err(execution_error) => tracing::warn!(
930 session_id = %session.id,
931 error = %checkpoint_error,
932 execution_error = %execution_error,
933 "failed to checkpoint session transcript after execution error"
934 ),
935 }
936 false
937 }
938 };
939
940 if should_observe_completed_tool_trace(&result, final_checkpoint_succeeded, session) {
941 if let Err(error) = self
942 .skill_manager
943 .observe_completed_tool_trace(session, trace_message_start)
944 .await
945 {
946 tracing::warn!(
947 session_id = %session.id,
948 %error,
949 "failed to observe completed tool trace for reuse draft discovery"
950 );
951 }
952 }
953
954 result
955 }
956}