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(crate) inherited_child_wait_captured: bool,
52 pub session_inbox: Option<Arc<dyn SessionInboxPort>>,
53 pub activation_router: Option<Arc<SessionActivationRouter>>,
54 pub session_messenger: Option<Arc<SessionMessenger>>,
55 pub attachment_reader: Arc<dyn AttachmentReader>,
56 pub skill_manager: Arc<SkillManager>,
57 pub project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
58 pub metrics_collector: MetricsCollector,
59 pub config: Arc<RwLock<Config>>,
60 pub permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
62 pub permission_mode: PermissionMode,
64
65 pub provider: Arc<dyn LLMProvider>,
67
68 pub memory_store: bamboo_memory::memory_store::MemoryStore,
70
71 pub default_tools: Arc<dyn ToolExecutor>,
75
76 pub hook_runner: Arc<HookRunner>,
79}
80
81pub struct AgentRuntimeBuilder {
96 storage: Option<Arc<dyn Storage>>,
97 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
98 session_inbox: Option<Arc<dyn SessionInboxPort>>,
99 activation_router: Option<Arc<SessionActivationRouter>>,
100 session_messenger: Option<Arc<SessionMessenger>>,
101 attachment_reader: Option<Arc<dyn AttachmentReader>>,
102 skill_manager: Option<Arc<SkillManager>>,
103 project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
104 metrics_collector: Option<MetricsCollector>,
105 config: Option<Arc<RwLock<Config>>>,
106 permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
107 permission_mode: PermissionMode,
108 provider: Option<Arc<dyn LLMProvider>>,
109 memory_store: bamboo_memory::memory_store::MemoryStore,
110 default_tools: Option<Arc<dyn ToolExecutor>>,
111 hook_runner: Arc<HookRunner>,
112}
113
114impl AgentRuntimeBuilder {
115 pub fn new() -> Self {
116 Self {
117 storage: None,
118 persistence: None,
119 session_inbox: None,
120 activation_router: None,
121 session_messenger: None,
122 attachment_reader: None,
123 skill_manager: None,
124 project_context_resolver: None,
125 metrics_collector: None,
126 config: None,
127 permission_config: None,
128 permission_mode: PermissionMode::Default,
129 provider: None,
130 memory_store: bamboo_memory::memory_store::MemoryStore::with_defaults(),
131 default_tools: None,
132 hook_runner: Arc::new(HookRunner::new()),
133 }
134 }
135
136 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
137 self.storage = Some(v);
138 self
139 }
140
141 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
142 self.persistence = Some(v);
143 self
144 }
145
146 pub fn session_inbox(mut self, v: Arc<dyn SessionInboxPort>) -> Self {
147 self.session_inbox = Some(v);
148 self
149 }
150
151 pub fn activation_router(mut self, v: Arc<SessionActivationRouter>) -> Self {
152 self.activation_router = Some(v);
153 self
154 }
155
156 pub fn session_messenger(mut self, v: Arc<SessionMessenger>) -> Self {
157 self.session_messenger = Some(v);
158 self
159 }
160
161 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
162 self.attachment_reader = Some(v);
163 self
164 }
165
166 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
167 self.skill_manager = Some(v);
168 self
169 }
170
171 pub fn project_context_resolver(
172 mut self,
173 v: Arc<crate::project_context::ProjectContextResolver>,
174 ) -> Self {
175 self.project_context_resolver = Some(v);
176 self
177 }
178
179 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
180 self.metrics_collector = Some(v);
181 self
182 }
183
184 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
185 self.config = Some(v);
186 self
187 }
188
189 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
190 self.permission_config = Some(v);
191 self
192 }
193
194 pub fn permission_mode(mut self, v: PermissionMode) -> Self {
195 self.permission_mode = v;
196 self
197 }
198
199 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
200 self.provider = Some(v);
201 self
202 }
203
204 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
205 self.memory_store = v;
206 self
207 }
208
209 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
210 self.default_tools = Some(v);
211 self
212 }
213
214 pub fn hook_runner(mut self, v: Arc<HookRunner>) -> Self {
216 self.hook_runner = v;
217 self
218 }
219
220 pub fn build(self) -> Result<AgentRuntime, &'static str> {
221 if let (Some(router), Some(inbox)) = (&self.activation_router, &self.session_inbox) {
222 router.set_inbox(inbox.clone());
223 }
224 Ok(AgentRuntime {
225 inherited_child_wait_captured: false,
226 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
227 persistence: self
228 .persistence
229 .ok_or_else(|| format_missing("persistence"))?,
230 session_inbox: self.session_inbox,
231 activation_router: self.activation_router,
232 session_messenger: self.session_messenger,
233 attachment_reader: self
234 .attachment_reader
235 .ok_or_else(|| format_missing("attachment_reader"))?,
236 skill_manager: self
237 .skill_manager
238 .ok_or_else(|| format_missing("skill_manager"))?,
239 project_context_resolver: self.project_context_resolver,
240 metrics_collector: self
241 .metrics_collector
242 .ok_or_else(|| format_missing("metrics_collector"))?,
243 config: self.config.ok_or_else(|| format_missing("config"))?,
244 permission_config: self.permission_config,
245 permission_mode: self.permission_mode,
246 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
247 memory_store: self.memory_store,
248 default_tools: self
249 .default_tools
250 .ok_or_else(|| format_missing("default_tools"))?,
251 hook_runner: self.hook_runner,
252 })
253 }
254}
255
256fn format_missing(field: &str) -> &'static str {
257 match field {
260 "storage" => "AgentRuntimeBuilder: missing storage",
261 "persistence" => "AgentRuntimeBuilder: missing persistence",
262 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
263 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
264 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
265 "config" => "AgentRuntimeBuilder: missing config",
266 "provider" => "AgentRuntimeBuilder: missing provider",
267 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
268 _ => "AgentRuntimeBuilder: missing required field",
269 }
270}
271
272impl Default for AgentRuntimeBuilder {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278pub struct ExecuteRequest {
288 pub ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
290 pub initial_message: String,
292 pub event_tx: mpsc::Sender<AgentEvent>,
293 pub cancel_token: CancellationToken,
294
295 pub tools: Option<Arc<dyn ToolExecutor>>,
299 pub provider_override: Option<Arc<dyn LLMProvider>>,
302
303 pub model_roster: ModelRoster,
310 pub reasoning_effort: Option<ReasoningEffort>,
311 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
314 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
317 pub disabled_tools: Option<BTreeSet<String>>,
319 pub disabled_skill_ids: Option<BTreeSet<String>>,
321 pub selected_skill_ids: Option<Vec<String>>,
322 pub selected_skill_mode: Option<String>,
323 pub image_fallback: Option<ImageFallbackConfig>,
324 pub gold_config: Option<GoldConfig>,
325 pub guardian_config: Option<GuardianConfig>,
327 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
330 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
333 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
336 pub app_data_dir: Option<std::path::PathBuf>,
338 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
346}
347
348pub struct ExecuteRequestBuilder {
364 ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
365 initial_message: String,
366 event_tx: mpsc::Sender<AgentEvent>,
367 cancel_token: CancellationToken,
368
369 tools: Option<Arc<dyn ToolExecutor>>,
370 provider_override: Option<Arc<dyn LLMProvider>>,
371 model: Option<String>,
375 provider_name: Option<String>,
376 provider_type: Option<String>,
377 fast_model: Option<String>,
378 fast_model_provider: Option<Arc<dyn LLMProvider>>,
379 background_model: Option<String>,
380 background_model_provider: Option<Arc<dyn LLMProvider>>,
381 summarization_model: Option<String>,
382 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
383 reasoning_effort: Option<ReasoningEffort>,
384 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
385 disabled_filter_resolver: Option<DisabledFilterResolver>,
386 disabled_tools: Option<BTreeSet<String>>,
387 disabled_skill_ids: Option<BTreeSet<String>>,
388 selected_skill_ids: Option<Vec<String>>,
389 selected_skill_mode: Option<String>,
390 image_fallback: Option<ImageFallbackConfig>,
391 gold_config: Option<GoldConfig>,
392 guardian_config: Option<GuardianConfig>,
393 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
394 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
395 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
396 app_data_dir: Option<std::path::PathBuf>,
397 run_budget: Option<bamboo_config::RunBudgetConfig>,
398}
399
400impl ExecuteRequestBuilder {
401 pub fn new(
404 initial_message: impl Into<String>,
405 event_tx: mpsc::Sender<AgentEvent>,
406 cancel_token: CancellationToken,
407 ) -> Self {
408 Self {
409 initial_message: initial_message.into(),
410 ticket_worker_plan: None,
411 event_tx,
412 cancel_token,
413 tools: None,
414 provider_override: None,
415 model: None,
416 provider_name: None,
417 provider_type: None,
418 fast_model: None,
419 fast_model_provider: None,
420 background_model: None,
421 background_model_provider: None,
422 summarization_model: None,
423 summarization_model_provider: None,
424 reasoning_effort: None,
425 auxiliary_model_resolver: None,
426 disabled_filter_resolver: None,
427 disabled_tools: None,
428 disabled_skill_ids: None,
429 selected_skill_ids: None,
430 selected_skill_mode: None,
431 image_fallback: None,
432 gold_config: None,
433 guardian_config: None,
434 guardian_spawner: None,
435 bash_resume_hook: None,
436 bash_completion_sink: None,
437 app_data_dir: None,
438 run_budget: None,
439 }
440 }
441
442 pub fn ticket_worker_plan(
444 mut self,
445 plan: Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>,
446 ) -> Self {
447 self.ticket_worker_plan = Some(plan);
448 self
449 }
450
451 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
453 self.tools = Some(v);
454 self
455 }
456
457 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
459 self.provider_override = Some(v);
460 self
461 }
462
463 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
469 self.fast_model = roster.fast_model();
470 self.fast_model_provider = roster.fast_model_provider();
471 self.background_model = roster.background_model();
472 self.background_model_provider = roster.background_model_provider();
473 self.summarization_model = roster.summarization_model();
474 self.summarization_model_provider = roster.summarization_model_provider();
475 self.model = roster.model;
476 self.provider_name = roster.provider_name;
477 self.provider_type = roster.provider_type;
478 self
479 }
480
481 pub fn model(mut self, v: impl Into<String>) -> Self {
483 self.model = Some(v.into());
484 self
485 }
486
487 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
489 self.provider_name = Some(v.into());
490 self
491 }
492
493 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
495 self.provider_type = Some(v.into());
496 self
497 }
498
499 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
501 self.fast_model = Some(v.into());
502 self
503 }
504
505 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
507 self.fast_model_provider = Some(v);
508 self
509 }
510
511 pub fn background_model(mut self, v: impl Into<String>) -> Self {
513 self.background_model = Some(v.into());
514 self
515 }
516
517 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
519 self.background_model_provider = Some(v);
520 self
521 }
522
523 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
525 self.summarization_model = Some(v.into());
526 self
527 }
528
529 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
531 self.summarization_model_provider = Some(v);
532 self
533 }
534
535 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
537 self.reasoning_effort = Some(v);
538 self
539 }
540
541 pub fn auxiliary_model_resolver(
543 mut self,
544 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
545 ) -> Self {
546 self.auxiliary_model_resolver = Some(v);
547 self
548 }
549
550 pub fn disabled_filter_resolver(mut self, v: DisabledFilterResolver) -> Self {
552 self.disabled_filter_resolver = Some(v);
553 self
554 }
555
556 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
558 self.disabled_tools = Some(v);
559 self
560 }
561
562 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
564 self.disabled_skill_ids = Some(v);
565 self
566 }
567
568 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
570 self.selected_skill_ids = Some(v);
571 self
572 }
573
574 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
576 self.selected_skill_mode = Some(v.into());
577 self
578 }
579
580 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
582 self.image_fallback = Some(v);
583 self
584 }
585
586 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
593 self.gold_config = v;
594 self
595 }
596
597 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
600 self.guardian_config = v;
601 self
602 }
603
604 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
607 self.guardian_spawner = v;
608 self
609 }
610
611 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
614 self.bash_resume_hook = v;
615 self
616 }
617
618 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
621 self.bash_completion_sink = v;
622 self
623 }
624
625 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
627 self.app_data_dir = Some(v);
628 self
629 }
630
631 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
636 self.run_budget = Some(v);
637 self
638 }
639
640 pub fn build(self) -> ExecuteRequest {
645 let model_roster = ModelRoster {
646 model: self.model,
647 provider_name: self.provider_name,
648 provider_type: self.provider_type,
649 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
650 background: RoleModel::from_parts(
651 self.background_model,
652 self.background_model_provider,
653 ),
654 summarization: RoleModel::from_parts(
655 self.summarization_model,
656 self.summarization_model_provider,
657 ),
658 };
659 ExecuteRequest {
660 ticket_worker_plan: self.ticket_worker_plan,
661 initial_message: self.initial_message,
662 event_tx: self.event_tx,
663 cancel_token: self.cancel_token,
664 tools: self.tools,
665 provider_override: self.provider_override,
666 model_roster,
667 reasoning_effort: self.reasoning_effort,
668 auxiliary_model_resolver: self.auxiliary_model_resolver,
669 disabled_filter_resolver: self.disabled_filter_resolver,
670 disabled_tools: self.disabled_tools,
671 disabled_skill_ids: self.disabled_skill_ids,
672 selected_skill_ids: self.selected_skill_ids,
673 selected_skill_mode: self.selected_skill_mode,
674 image_fallback: self.image_fallback,
675 gold_config: self.gold_config,
676 guardian_config: self.guardian_config,
677 guardian_spawner: self.guardian_spawner,
678 bash_resume_hook: self.bash_resume_hook,
679 bash_completion_sink: self.bash_completion_sink,
680 app_data_dir: self.app_data_dir,
681 run_budget: self.run_budget,
682 }
683 }
684}
685
686fn extract_system_prompt(session: &Session) -> Option<String> {
692 session
693 .messages
694 .iter()
695 .find(|m| matches!(m.role, Role::System))
696 .map(|m| m.content.clone())
697}
698
699pub(super) fn should_observe_completed_tool_trace(
700 result: &crate::runtime::runner::Result<()>,
701 final_checkpoint_succeeded: bool,
702 session: &Session,
703) -> bool {
704 final_checkpoint_succeeded
705 && result.is_ok()
706 && session
707 .agent_runtime_state
708 .as_ref()
709 .is_some_and(|state| matches!(state.status, bamboo_domain::AgentStatusState::Completed))
710 && !session.metadata.contains_key("runtime.suspend_reason")
711 && !session.metadata.contains_key("runtime.completion_reason")
712}
713
714impl AgentRuntime {
719 pub async fn execute(
724 &self,
725 session: &mut Session,
726 req: ExecuteRequest,
727 ) -> crate::runtime::runner::Result<()> {
728 let existing = self.persistence.inherited_child_wait();
729 if let Some(inherited) = existing.as_ref() {
730 inherited
731 .validate_session(session)
732 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
733 } else if !self.inherited_child_wait_captured {
734 let mut runtime = self.clone();
735 if let Some(inherited) = bamboo_domain::InheritedChildWait::capture(session) {
736 runtime.persistence = self
737 .persistence
738 .bind_inherited_child_wait(inherited)
739 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
740 }
741 runtime.inherited_child_wait_captured = true;
742 return runtime.execute_bound(session, req).await;
743 }
744 self.execute_bound(session, req).await
745 }
746
747 async fn execute_bound(
748 &self,
749 session: &mut Session,
750 req: ExecuteRequest,
751 ) -> crate::runtime::runner::Result<()> {
752 if self.persistence.root_actor_execution_required(session) {
753 return Err(bamboo_agent_core::AgentError::Tool(
754 "Root execution route requires a bound Actor writer and event handoff".into(),
755 ));
756 }
757 let session_activation_notifications = match self.activation_router.as_ref() {
758 Some(router) => Some(Arc::new(parking_lot::Mutex::new(
759 router.subscribe(&session.id).await,
760 ))),
761 None => None,
762 };
763 let guidance_active_run_id = match self.activation_router.as_ref() {
764 Some(router) => router.current_run_id(&session.id).await,
765 None => None,
766 };
767 let system_prompt = extract_system_prompt(session);
768 let config = self.config.read().await;
769 let ExecuteRequest {
770 ticket_worker_plan,
771 initial_message,
772 event_tx,
773 cancel_token,
774 tools,
775 provider_override,
776 model_roster,
777 reasoning_effort,
778 auxiliary_model_resolver,
779 disabled_filter_resolver,
780 disabled_tools,
781 disabled_skill_ids,
782 selected_skill_ids,
783 selected_skill_mode,
784 image_fallback,
785 gold_config,
786 guardian_config,
787 guardian_spawner,
788 bash_resume_hook,
789 bash_completion_sink,
790 app_data_dir,
791 run_budget,
792 } = req;
793 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
794 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
795 let configured_permission_mode = self
796 .permission_config
797 .as_ref()
798 .map(|config| config.mode())
799 .unwrap_or(self.permission_mode);
800 let active_plan_gate = session
801 .agent_runtime_state
802 .as_ref()
803 .is_some_and(|state| state.plan_mode.is_some());
804
805 let fast_model = model_roster.fast_model();
810 let fast_model_provider = model_roster.fast_model_provider();
811 let background_model = model_roster.background_model();
812 let background_model_provider = model_roster.background_model_provider();
813 let summarization_model = model_roster.summarization_model();
814 let summarization_model_provider = model_roster.summarization_model_provider();
815 let ModelRoster {
816 model,
817 provider_name,
818 provider_type,
819 ..
820 } = model_roster;
821 let hook_runner = Arc::new(
822 self.hook_runner
823 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir.clone()),
824 );
825
826 let loop_config = AgentLoopConfig {
827 ticket_worker_plan: ticket_worker_plan.clone(),
828 guidance_active_run_id,
829 system_prompt,
830 legacy_model_limits: config.extra.get("model_limits").cloned(),
833 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
834 selected_skill_ids,
835 selected_skill_mode,
836 skill_manager: Some(self.skill_manager.clone()),
837 project_context_resolver: self.project_context_resolver.clone(),
838 skip_initial_user_message: true,
839 storage: Some(self.storage.clone()),
840 persistence: Some(self.persistence.clone()),
841 session_inbox: self.session_inbox.clone(),
842 session_activation_notifications,
843 attachment_reader: Some(self.attachment_reader.clone()),
844 metrics_collector: Some(self.metrics_collector.clone()),
845 model_name: model,
846 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
847 fast_model_provider,
848 auxiliary_evaluation_max_concurrency:
849 crate::runtime::config::normalize_auxiliary_evaluation_max_concurrency(
850 config
851 .extra
852 .get("auxiliary_evaluation_max_concurrency")
853 .and_then(serde_json::Value::as_u64),
854 ),
855 background_model_name: background_model
856 .or_else(|| config.get_memory_background_model()),
857 planning_model_name: config
858 .defaults
859 .as_ref()
860 .and_then(|d| d.planning.as_ref())
861 .map(|r| r.model.clone()),
862 search_model_name: config
863 .defaults
864 .as_ref()
865 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
866 .map(|r| r.model.clone()),
867 compression_instructions: None,
868 summary_target_ratio: config
869 .memory()
870 .as_ref()
871 .map(|memory| memory.summary_target_ratio)
872 .unwrap_or(0.20),
873 context_management: config.context_management.clone(),
874 summary_safe_window_percent: config
875 .memory()
876 .as_ref()
877 .map(|memory| memory.summary_safe_window_percent)
878 .unwrap_or(80),
879 summarization_model_name: summarization_model
880 .or_else(|| config.get_task_summary_model()),
881 background_model_provider,
882 summarization_model_provider,
883 provider_name: Some(
884 provider_name.unwrap_or_else(|| config.effective_default_provider().to_string()),
885 ),
886 provider_type,
887 reasoning_effort,
888 auxiliary_model_resolver,
889 disabled_filter_resolver,
890 disabled_tools: {
891 let mut merged = config.disabled_tool_references();
892 if let Some(dt) = disabled_tools {
893 merged.extend(dt);
894 }
895 merged
896 },
897 image_fallback,
898 app_data_dir,
899 memory_store: self.memory_store.clone(),
900 prompt_memory_flags: config
901 .memory()
902 .as_ref()
903 .map(PromptMemoryFlags::from)
904 .unwrap_or_default(),
905 features_dynamic_model_routing: config.features.dynamic_model_routing,
906 permission_mode: Some(if active_plan_gate {
907 PermissionMode::Plan
908 } else {
909 configured_permission_mode
910 }),
911 gold_config,
912 guardian_config,
913 guardian_spawner,
914 bash_resume_hook,
915 bash_completion_sink,
916 hook_runner,
917 mcp_tool_guidance: tools.tool_guidance(),
921 run_budget: config.run_budget.merged_with_override(run_budget.as_ref()),
927 stream_timeout: config.stream_timeout,
928 ..Default::default()
929 };
930
931 drop(config);
932
933 if let Some(plan) = &ticket_worker_plan {
934 session
935 .agent_runtime_state
936 .get_or_insert_with(Default::default)
937 .run_id = plan.run_id().to_owned();
938 plan.bind_session(session)
939 .map_err(|e| bamboo_agent_core::AgentError::LLM(e.to_string()))?;
940 }
941
942 let trace_message_start = session.messages.len();
943 let session_end_runner = loop_config.hook_runner.clone();
944 let session_end_event_tx = event_tx.clone();
945 let result = Box::pin(run_agent_loop_with_config(
946 session,
947 initial_message,
948 event_tx,
949 llm,
950 tools,
951 cancel_token,
952 loop_config,
953 ))
954 .await;
955
956 crate::runtime::hooks::run_session_end_hooks(
957 &session_end_runner,
958 &result,
959 session,
960 &session_end_event_tx,
961 )
962 .await;
963
964 let final_checkpoint_succeeded =
976 match self.persistence.checkpoint_runtime_session(session).await {
977 Ok(()) => true,
978 Err(checkpoint_error) => {
979 match &result {
980 Ok(()) => tracing::warn!(
981 session_id = %session.id,
982 error = %checkpoint_error,
983 "failed to checkpoint session transcript after successful execution"
984 ),
985 Err(execution_error) => tracing::warn!(
986 session_id = %session.id,
987 error = %checkpoint_error,
988 execution_error = %execution_error,
989 "failed to checkpoint session transcript after execution error"
990 ),
991 }
992 false
993 }
994 };
995
996 if should_observe_completed_tool_trace(&result, final_checkpoint_succeeded, session) {
997 if let Err(error) = self
998 .skill_manager
999 .observe_completed_tool_trace(session, trace_message_start)
1000 .await
1001 {
1002 tracing::warn!(
1003 session_id = %session.id,
1004 %error,
1005 "failed to observe completed tool trace for reuse draft discovery"
1006 );
1007 }
1008 }
1009
1010 result
1011 }
1012}