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(crate) sdk_skill_execution_host:
53 Option<Arc<dyn crate::runtime::config::SdkSkillExecutionHost>>,
54 pub(crate) workspace_execution_host:
55 Option<Arc<dyn crate::runtime::config::WorkspaceExecutionHost>>,
56 pub session_inbox: Option<Arc<dyn SessionInboxPort>>,
57 pub activation_router: Option<Arc<SessionActivationRouter>>,
58 pub session_messenger: Option<Arc<SessionMessenger>>,
59 pub attachment_reader: Arc<dyn AttachmentReader>,
60 pub skill_manager: Arc<SkillManager>,
61 pub project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
62 pub metrics_collector: MetricsCollector,
63 pub config: Arc<RwLock<Config>>,
64 pub(crate) model_limits_section:
65 Option<Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>>,
66 pub permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
68 pub permission_mode: PermissionMode,
70
71 pub provider: Arc<dyn LLMProvider>,
73
74 pub memory_store: bamboo_memory::memory_store::MemoryStore,
76
77 pub default_tools: Arc<dyn ToolExecutor>,
81
82 pub hook_runner: Arc<HookRunner>,
85}
86
87pub struct AgentRuntimeBuilder {
102 storage: Option<Arc<dyn Storage>>,
103 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
104 session_inbox: Option<Arc<dyn SessionInboxPort>>,
105 activation_router: Option<Arc<SessionActivationRouter>>,
106 session_messenger: Option<Arc<SessionMessenger>>,
107 attachment_reader: Option<Arc<dyn AttachmentReader>>,
108 skill_manager: Option<Arc<SkillManager>>,
109 project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
110 metrics_collector: Option<MetricsCollector>,
111 config: Option<Arc<RwLock<Config>>>,
112 model_limits_section:
113 Option<Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>>,
114 permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
115 permission_mode: PermissionMode,
116 provider: Option<Arc<dyn LLMProvider>>,
117 memory_store: bamboo_memory::memory_store::MemoryStore,
118 default_tools: Option<Arc<dyn ToolExecutor>>,
119 hook_runner: Arc<HookRunner>,
120}
121
122impl AgentRuntimeBuilder {
123 pub fn new() -> Self {
124 Self {
125 storage: None,
126 persistence: None,
127 session_inbox: None,
128 activation_router: None,
129 session_messenger: None,
130 attachment_reader: None,
131 skill_manager: None,
132 project_context_resolver: None,
133 metrics_collector: None,
134 config: None,
135 model_limits_section: None,
136 permission_config: None,
137 permission_mode: PermissionMode::Default,
138 provider: None,
139 memory_store: bamboo_memory::memory_store::MemoryStore::with_defaults(),
140 default_tools: None,
141 hook_runner: Arc::new(HookRunner::new()),
142 }
143 }
144
145 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
146 self.storage = Some(v);
147 self
148 }
149
150 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
151 self.persistence = Some(v);
152 self
153 }
154
155 pub fn session_inbox(mut self, v: Arc<dyn SessionInboxPort>) -> Self {
156 self.session_inbox = Some(v);
157 self
158 }
159
160 pub fn activation_router(mut self, v: Arc<SessionActivationRouter>) -> Self {
161 self.activation_router = Some(v);
162 self
163 }
164
165 pub fn session_messenger(mut self, v: Arc<SessionMessenger>) -> Self {
166 self.session_messenger = Some(v);
167 self
168 }
169
170 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
171 self.attachment_reader = Some(v);
172 self
173 }
174
175 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
176 self.skill_manager = Some(v);
177 self
178 }
179
180 pub fn project_context_resolver(
181 mut self,
182 v: Arc<crate::project_context::ProjectContextResolver>,
183 ) -> Self {
184 self.project_context_resolver = Some(v);
185 self
186 }
187
188 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
189 self.metrics_collector = Some(v);
190 self
191 }
192
193 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
194 self.config = Some(v);
195 self
196 }
197
198 pub fn model_limits_section(
202 mut self,
203 section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
204 ) -> Self {
205 self.model_limits_section = Some(section);
206 self
207 }
208
209 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
210 self.permission_config = Some(v);
211 self
212 }
213
214 pub fn permission_mode(mut self, v: PermissionMode) -> Self {
215 self.permission_mode = v;
216 self
217 }
218
219 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
220 self.provider = Some(v);
221 self
222 }
223
224 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
225 self.memory_store = v;
226 self
227 }
228
229 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
230 self.default_tools = Some(v);
231 self
232 }
233
234 pub fn hook_runner(mut self, v: Arc<HookRunner>) -> Self {
236 self.hook_runner = v;
237 self
238 }
239
240 pub fn build(self) -> Result<AgentRuntime, &'static str> {
241 if let (Some(router), Some(inbox)) = (&self.activation_router, &self.session_inbox) {
242 router.set_inbox(inbox.clone());
243 }
244 Ok(AgentRuntime {
245 inherited_child_wait_captured: false,
246 sdk_skill_execution_host: None,
247 workspace_execution_host: None,
248 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
249 persistence: self
250 .persistence
251 .ok_or_else(|| format_missing("persistence"))?,
252 session_inbox: self.session_inbox,
253 activation_router: self.activation_router,
254 session_messenger: self.session_messenger,
255 attachment_reader: self
256 .attachment_reader
257 .ok_or_else(|| format_missing("attachment_reader"))?,
258 skill_manager: self
259 .skill_manager
260 .ok_or_else(|| format_missing("skill_manager"))?,
261 project_context_resolver: self.project_context_resolver,
262 metrics_collector: self
263 .metrics_collector
264 .ok_or_else(|| format_missing("metrics_collector"))?,
265 config: self.config.ok_or_else(|| format_missing("config"))?,
266 model_limits_section: self.model_limits_section,
267 permission_config: self.permission_config,
268 permission_mode: self.permission_mode,
269 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
270 memory_store: self.memory_store,
271 default_tools: self
272 .default_tools
273 .ok_or_else(|| format_missing("default_tools"))?,
274 hook_runner: self.hook_runner,
275 })
276 }
277}
278
279fn format_missing(field: &str) -> &'static str {
280 match field {
283 "storage" => "AgentRuntimeBuilder: missing storage",
284 "persistence" => "AgentRuntimeBuilder: missing persistence",
285 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
286 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
287 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
288 "config" => "AgentRuntimeBuilder: missing config",
289 "provider" => "AgentRuntimeBuilder: missing provider",
290 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
291 _ => "AgentRuntimeBuilder: missing required field",
292 }
293}
294
295impl Default for AgentRuntimeBuilder {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301pub struct ExecuteRequest {
311 pub ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
313 pub initial_message: String,
315 pub event_tx: mpsc::Sender<AgentEvent>,
316 pub cancel_token: CancellationToken,
317
318 pub tools: Option<Arc<dyn ToolExecutor>>,
322 pub provider_override: Option<Arc<dyn LLMProvider>>,
325
326 pub model_roster: ModelRoster,
333 pub reasoning_effort: Option<ReasoningEffort>,
334 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
337 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
340 pub disabled_tools: Option<BTreeSet<String>>,
342 pub disabled_skill_ids: Option<BTreeSet<String>>,
344 pub selected_skill_ids: Option<Vec<String>>,
345 pub selected_skill_mode: Option<String>,
346 pub image_fallback: Option<ImageFallbackConfig>,
347 pub gold_config: Option<GoldConfig>,
348 pub guardian_config: Option<GuardianConfig>,
350 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
353 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
356 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
359 pub app_data_dir: Option<std::path::PathBuf>,
361 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
369 pub counted_loop: Option<super::counted_loop::CountedLoopConfig>,
372 pub loop_control: Option<super::loop_control::LoopControlConfig>,
374}
375
376pub struct ExecuteRequestBuilder {
392 ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
393 initial_message: String,
394 event_tx: mpsc::Sender<AgentEvent>,
395 cancel_token: CancellationToken,
396
397 tools: Option<Arc<dyn ToolExecutor>>,
398 provider_override: Option<Arc<dyn LLMProvider>>,
399 model: Option<String>,
403 provider_name: Option<String>,
404 provider_type: Option<String>,
405 fast_model: Option<String>,
406 fast_model_provider: Option<Arc<dyn LLMProvider>>,
407 background_model: Option<String>,
408 background_model_provider: Option<Arc<dyn LLMProvider>>,
409 summarization_model: Option<String>,
410 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
411 reasoning_effort: Option<ReasoningEffort>,
412 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
413 disabled_filter_resolver: Option<DisabledFilterResolver>,
414 disabled_tools: Option<BTreeSet<String>>,
415 disabled_skill_ids: Option<BTreeSet<String>>,
416 selected_skill_ids: Option<Vec<String>>,
417 selected_skill_mode: Option<String>,
418 image_fallback: Option<ImageFallbackConfig>,
419 gold_config: Option<GoldConfig>,
420 guardian_config: Option<GuardianConfig>,
421 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
422 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
423 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
424 app_data_dir: Option<std::path::PathBuf>,
425 run_budget: Option<bamboo_config::RunBudgetConfig>,
426 counted_loop: Option<super::counted_loop::CountedLoopConfig>,
427 loop_control: Option<super::loop_control::LoopControlConfig>,
428}
429
430impl ExecuteRequestBuilder {
431 pub fn new(
434 initial_message: impl Into<String>,
435 event_tx: mpsc::Sender<AgentEvent>,
436 cancel_token: CancellationToken,
437 ) -> Self {
438 Self {
439 initial_message: initial_message.into(),
440 ticket_worker_plan: None,
441 event_tx,
442 cancel_token,
443 tools: None,
444 provider_override: None,
445 model: None,
446 provider_name: None,
447 provider_type: None,
448 fast_model: None,
449 fast_model_provider: None,
450 background_model: None,
451 background_model_provider: None,
452 summarization_model: None,
453 summarization_model_provider: None,
454 reasoning_effort: None,
455 auxiliary_model_resolver: None,
456 disabled_filter_resolver: None,
457 disabled_tools: None,
458 disabled_skill_ids: None,
459 selected_skill_ids: None,
460 selected_skill_mode: None,
461 image_fallback: None,
462 gold_config: None,
463 guardian_config: None,
464 guardian_spawner: None,
465 bash_resume_hook: None,
466 bash_completion_sink: None,
467 app_data_dir: None,
468 run_budget: None,
469 counted_loop: None,
470 loop_control: None,
471 }
472 }
473
474 pub fn ticket_worker_plan(
476 mut self,
477 plan: Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>,
478 ) -> Self {
479 self.ticket_worker_plan = Some(plan);
480 self
481 }
482
483 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
485 self.tools = Some(v);
486 self
487 }
488
489 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
491 self.provider_override = Some(v);
492 self
493 }
494
495 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
501 self.fast_model = roster.fast_model();
502 self.fast_model_provider = roster.fast_model_provider();
503 self.background_model = roster.background_model();
504 self.background_model_provider = roster.background_model_provider();
505 self.summarization_model = roster.summarization_model();
506 self.summarization_model_provider = roster.summarization_model_provider();
507 self.model = roster.model;
508 self.provider_name = roster.provider_name;
509 self.provider_type = roster.provider_type;
510 self
511 }
512
513 pub fn model(mut self, v: impl Into<String>) -> Self {
515 self.model = Some(v.into());
516 self
517 }
518
519 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
521 self.provider_name = Some(v.into());
522 self
523 }
524
525 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
527 self.provider_type = Some(v.into());
528 self
529 }
530
531 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
533 self.fast_model = Some(v.into());
534 self
535 }
536
537 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
539 self.fast_model_provider = Some(v);
540 self
541 }
542
543 pub fn background_model(mut self, v: impl Into<String>) -> Self {
545 self.background_model = Some(v.into());
546 self
547 }
548
549 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
551 self.background_model_provider = Some(v);
552 self
553 }
554
555 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
557 self.summarization_model = Some(v.into());
558 self
559 }
560
561 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
563 self.summarization_model_provider = Some(v);
564 self
565 }
566
567 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
569 self.reasoning_effort = Some(v);
570 self
571 }
572
573 pub fn auxiliary_model_resolver(
575 mut self,
576 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
577 ) -> Self {
578 self.auxiliary_model_resolver = Some(v);
579 self
580 }
581
582 pub fn disabled_filter_resolver(mut self, v: DisabledFilterResolver) -> Self {
584 self.disabled_filter_resolver = Some(v);
585 self
586 }
587
588 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
590 self.disabled_tools = Some(v);
591 self
592 }
593
594 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
596 self.disabled_skill_ids = Some(v);
597 self
598 }
599
600 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
602 self.selected_skill_ids = Some(v);
603 self
604 }
605
606 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
608 self.selected_skill_mode = Some(v.into());
609 self
610 }
611
612 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
614 self.image_fallback = Some(v);
615 self
616 }
617
618 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
625 self.gold_config = v;
626 self
627 }
628
629 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
632 self.guardian_config = v;
633 self
634 }
635
636 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
639 self.guardian_spawner = v;
640 self
641 }
642
643 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
646 self.bash_resume_hook = v;
647 self
648 }
649
650 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
653 self.bash_completion_sink = v;
654 self
655 }
656
657 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
659 self.app_data_dir = Some(v);
660 self
661 }
662
663 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
668 self.run_budget = Some(v);
669 self
670 }
671
672 pub fn counted_loop(mut self, config: super::counted_loop::CountedLoopConfig) -> Self {
675 self.counted_loop = Some(config);
676 self
677 }
678
679 pub fn loop_control(mut self, config: super::loop_control::LoopControlConfig) -> Self {
682 self.loop_control = Some(config);
683 self
684 }
685
686 pub fn build(self) -> ExecuteRequest {
691 let model_roster = ModelRoster {
692 model: self.model,
693 provider_name: self.provider_name,
694 provider_type: self.provider_type,
695 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
696 background: RoleModel::from_parts(
697 self.background_model,
698 self.background_model_provider,
699 ),
700 summarization: RoleModel::from_parts(
701 self.summarization_model,
702 self.summarization_model_provider,
703 ),
704 };
705 ExecuteRequest {
706 ticket_worker_plan: self.ticket_worker_plan,
707 initial_message: self.initial_message,
708 event_tx: self.event_tx,
709 cancel_token: self.cancel_token,
710 tools: self.tools,
711 provider_override: self.provider_override,
712 model_roster,
713 reasoning_effort: self.reasoning_effort,
714 auxiliary_model_resolver: self.auxiliary_model_resolver,
715 disabled_filter_resolver: self.disabled_filter_resolver,
716 disabled_tools: self.disabled_tools,
717 disabled_skill_ids: self.disabled_skill_ids,
718 selected_skill_ids: self.selected_skill_ids,
719 selected_skill_mode: self.selected_skill_mode,
720 image_fallback: self.image_fallback,
721 gold_config: self.gold_config,
722 guardian_config: self.guardian_config,
723 guardian_spawner: self.guardian_spawner,
724 bash_resume_hook: self.bash_resume_hook,
725 bash_completion_sink: self.bash_completion_sink,
726 app_data_dir: self.app_data_dir,
727 run_budget: self.run_budget,
728 counted_loop: self.counted_loop,
729 loop_control: self.loop_control,
730 }
731 }
732}
733
734fn extract_system_prompt(session: &Session) -> Option<String> {
740 session
741 .messages
742 .iter()
743 .find(|m| matches!(m.role, Role::System))
744 .map(|m| m.content.clone())
745}
746
747pub(super) fn should_observe_completed_tool_trace(
748 result: &crate::runtime::runner::Result<()>,
749 final_checkpoint_succeeded: bool,
750 session: &Session,
751) -> bool {
752 final_checkpoint_succeeded
753 && result.is_ok()
754 && session
755 .agent_runtime_state
756 .as_ref()
757 .is_some_and(|state| matches!(state.status, bamboo_domain::AgentStatusState::Completed))
758 && !session.metadata.contains_key("runtime.suspend_reason")
759 && !session.metadata.contains_key("runtime.completion_reason")
760}
761
762impl AgentRuntime {
767 pub async fn execute(
772 &self,
773 session: &mut Session,
774 req: ExecuteRequest,
775 ) -> crate::runtime::runner::Result<()> {
776 self.execute_with_inputs(session, req, None).await
777 }
778
779 pub(crate) async fn execute_with_inputs(
780 &self,
781 session: &mut Session,
782 req: ExecuteRequest,
783 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
784 ) -> crate::runtime::runner::Result<()> {
785 struct WorkspaceFinish(Option<Arc<dyn crate::runtime::config::WorkspaceExecutionHost>>);
786 impl Drop for WorkspaceFinish {
787 fn drop(&mut self) {
788 if let Some(host) = &self.0 {
789 host.finish();
790 }
791 }
792 }
793 let _workspace_finish = WorkspaceFinish(self.workspace_execution_host.clone());
796 session
797 .check_execution_compatibility()
798 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
799 super::counted_loop::reject_ambiguous_reentry(session, self.persistence.as_ref()).await?;
802 if let Some(control) = req.loop_control.as_ref() {
803 control
804 .validate_admission(session, self.activation_router.as_ref())
805 .await?;
806 }
807 struct SdkFinish {
808 host: Option<Arc<dyn crate::runtime::config::SdkSkillExecutionHost>>,
809 session_id: String,
810 execution_id: String,
811 }
812 impl Drop for SdkFinish {
813 fn drop(&mut self) {
814 if let Some(host) = self.host.as_ref() {
815 host.finish(&self.session_id, &self.execution_id);
816 }
817 }
818 }
819 let _sdk_finish = SdkFinish {
820 host: self.sdk_skill_execution_host.clone(),
821 session_id: session.id.clone(),
822 execution_id: inputs
823 .as_ref()
824 .and_then(|inputs| inputs.sdk_execution_id())
825 .unwrap_or("")
826 .to_owned(),
827 };
828 let existing = self.persistence.inherited_child_wait();
829 if let Some(inherited) = existing.as_ref() {
830 inherited
831 .validate_session(session)
832 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
833 } else if !self.inherited_child_wait_captured {
834 let mut runtime = self.clone();
835 if let Some(inherited) = bamboo_domain::InheritedChildWait::capture(session) {
836 runtime.persistence = self
837 .persistence
838 .bind_inherited_child_wait(inherited)
839 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
840 }
841 runtime.inherited_child_wait_captured = true;
842 return runtime.execute_bound(session, req, inputs).await;
843 }
844 self.execute_bound(session, req, inputs).await
845 }
846
847 async fn execute_bound(
848 &self,
849 session: &mut Session,
850 req: ExecuteRequest,
851 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
852 ) -> crate::runtime::runner::Result<()> {
853 if self.persistence.root_actor_execution_required(session) {
854 return Err(bamboo_agent_core::AgentError::Tool(
855 "Root execution route requires a bound Actor writer and event handoff".into(),
856 ));
857 }
858 let session_activation_notifications = match self.activation_router.as_ref() {
859 Some(router) => Some(Arc::new(parking_lot::Mutex::new(
860 router.subscribe(&session.id).await,
861 ))),
862 None => None,
863 };
864 let guidance_active_run_id = match self.activation_router.as_ref() {
865 Some(router) => router.current_run_id(&session.id).await,
866 None => None,
867 };
868 if let Some(counted) = req.counted_loop.as_ref() {
869 if !counted
870 .owns_session(self.activation_router.as_ref(), session)
871 .await
872 {
873 return Err(bamboo_agent_core::AgentError::Budget(
874 "counted loop requires the caller's exact Session activation owner".into(),
875 ));
876 }
877 }
878 let system_prompt = extract_system_prompt(session);
879 let config = self.config.read().await;
880 let ExecuteRequest {
881 ticket_worker_plan,
882 initial_message,
883 event_tx,
884 cancel_token,
885 tools,
886 provider_override,
887 model_roster,
888 reasoning_effort,
889 auxiliary_model_resolver,
890 disabled_filter_resolver,
891 disabled_tools,
892 disabled_skill_ids,
893 selected_skill_ids,
894 selected_skill_mode,
895 image_fallback,
896 gold_config,
897 guardian_config,
898 guardian_spawner,
899 bash_resume_hook,
900 bash_completion_sink,
901 app_data_dir,
902 run_budget,
903 counted_loop,
904 loop_control,
905 } = req;
906 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
907 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
908 let configured_permission_mode = self
909 .permission_config
910 .as_ref()
911 .map(|config| config.mode())
912 .unwrap_or(self.permission_mode);
913 let active_plan_gate = session
914 .agent_runtime_state
915 .as_ref()
916 .is_some_and(|state| state.plan_mode.is_some());
917
918 let fast_model = model_roster.fast_model();
923 let fast_model_provider = model_roster.fast_model_provider();
924 let background_model = model_roster.background_model();
925 let background_model_provider = model_roster.background_model_provider();
926 let summarization_model = model_roster.summarization_model();
927 let summarization_model_provider = model_roster.summarization_model_provider();
928 let ModelRoster {
929 model,
930 provider_name,
931 provider_type,
932 ..
933 } = model_roster;
934 let active_provider =
935 provider_name.unwrap_or_else(|| config.effective_default_provider().to_string());
936 let hook_runner = inputs
937 .as_ref()
938 .and_then(|inputs| inputs.sdk_hook_runner())
939 .unwrap_or_else(|| {
940 Arc::new(
941 self.hook_runner
942 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir.clone()),
943 )
944 });
945
946 let mut effective_run_budget = config.run_budget.merged_with_override(run_budget.as_ref());
947 if let Some(control) = loop_control.as_ref() {
948 effective_run_budget.max_rounds = Some(
949 effective_run_budget
950 .max_rounds
951 .map_or(control.max_total_rounds, |cap| {
952 cap.min(control.max_total_rounds)
953 }),
954 );
955 }
956 let loop_config = AgentLoopConfig {
957 ticket_worker_plan: ticket_worker_plan.clone(),
958 guidance_active_run_id,
959 system_prompt,
960 legacy_model_limits: config.extra.get("model_limits").cloned(),
963 model_limits_section: self.model_limits_section.clone(),
964 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
965 selected_skill_ids,
966 selected_skill_mode,
967 initial_untrusted_inputs: inputs,
968 sdk_skill_execution_host: self.sdk_skill_execution_host.clone(),
969 workspace_execution_host: self.workspace_execution_host.clone(),
970 skill_manager: Some(self.skill_manager.clone()),
971 project_context_resolver: self.project_context_resolver.clone(),
972 skip_initial_user_message: true,
973 storage: Some(self.storage.clone()),
974 persistence: Some(self.persistence.clone()),
975 session_inbox: self.session_inbox.clone(),
976 session_activation_notifications,
977 attachment_reader: Some(self.attachment_reader.clone()),
978 metrics_collector: Some(self.metrics_collector.clone()),
979 model_name: model,
980 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
981 fast_model_provider,
982 auxiliary_evaluation_max_concurrency:
983 crate::runtime::config::normalize_auxiliary_evaluation_max_concurrency(
984 config
985 .extra
986 .get("auxiliary_evaluation_max_concurrency")
987 .and_then(serde_json::Value::as_u64),
988 ),
989 background_model_name: background_model
990 .or_else(|| config.get_memory_background_model()),
991 planning_model_name: config
992 .defaults
993 .as_ref()
994 .and_then(|d| d.planning.as_ref())
995 .map(|r| r.model.clone()),
996 search_model_name: config
997 .defaults
998 .as_ref()
999 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
1000 .map(|r| r.model.clone()),
1001 compression_instructions: None,
1002 summary_target_ratio: config
1003 .memory()
1004 .as_ref()
1005 .map(|memory| memory.summary_target_ratio)
1006 .unwrap_or(0.20),
1007 context_management: config.context_management.clone(),
1008 summary_safe_window_percent: config
1009 .memory()
1010 .as_ref()
1011 .map(|memory| memory.summary_safe_window_percent)
1012 .unwrap_or(80),
1013 summarization_model_name: summarization_model
1014 .or_else(|| config.get_task_summary_model()),
1015 background_model_provider,
1016 summarization_model_provider,
1017 provider_name: Some(active_provider.clone()),
1018 provider_type,
1019 reasoning_effort,
1020 auxiliary_model_resolver,
1021 disabled_filter_resolver,
1022 disabled_tools: {
1023 let mut merged = config.disabled_tool_references();
1024 if let Some(dt) = disabled_tools {
1025 merged.extend(dt);
1026 }
1027 merged
1028 },
1029 image_fallback,
1030 vision_model_resolver: Some(
1031 crate::runtime::runner::image_fallback::configured_vision_resolver(
1032 self.config.clone(),
1033 app_data_dir.clone(),
1034 active_provider,
1035 ),
1036 ),
1037 app_data_dir,
1038 memory_store: self.memory_store.clone(),
1039 prompt_memory_flags: config
1040 .memory()
1041 .as_ref()
1042 .map(PromptMemoryFlags::from)
1043 .unwrap_or_default(),
1044 features_dynamic_model_routing: config.features.dynamic_model_routing,
1045 features_final_evidence_check: config.features.final_evidence_check,
1046 permission_mode: Some(if active_plan_gate {
1047 PermissionMode::Plan
1048 } else {
1049 configured_permission_mode
1050 }),
1051 gold_config,
1052 guardian_config,
1053 guardian_spawner,
1054 bash_resume_hook,
1055 bash_completion_sink,
1056 hook_runner,
1057 mcp_tool_guidance: tools.tool_guidance(),
1061 run_budget: effective_run_budget,
1067 counted_loop,
1068 loop_control,
1069 stream_timeout: config.stream_timeout,
1070 ..Default::default()
1071 };
1072
1073 drop(config);
1074 #[cfg(test)]
1075 crate::runtime::tests::observe_untrusted_inputs(
1076 &session.id,
1077 loop_config.initial_untrusted_inputs(),
1078 );
1079
1080 if let Some(plan) = &ticket_worker_plan {
1081 session
1082 .agent_runtime_state
1083 .get_or_insert_with(Default::default)
1084 .run_id = plan.run_id().to_owned();
1085 plan.bind_session(session)
1086 .map_err(|e| bamboo_agent_core::AgentError::LLM(e.to_string()))?;
1087 }
1088
1089 let trace_message_start = session.messages.len();
1090 let session_end_runner = loop_config.hook_runner.clone();
1091 let session_end_event_tx = event_tx.clone();
1092 let sdk_mode = loop_config.sdk_skill_execution_host.is_some();
1093 let loop_future = Box::pin(run_agent_loop_with_config(
1094 session,
1095 initial_message,
1096 event_tx,
1097 llm,
1098 tools,
1099 cancel_token,
1100 loop_config,
1101 ));
1102 let result = if sdk_mode {
1103 crate::runtime::runner::session_setup::legacy_instruction::scope_sdk_runtime(
1104 loop_future,
1105 )
1106 .await
1107 } else {
1108 loop_future.await
1109 };
1110
1111 crate::runtime::hooks::run_session_end_hooks(
1112 &session_end_runner,
1113 &result,
1114 session,
1115 &session_end_event_tx,
1116 )
1117 .await;
1118
1119 let final_checkpoint_succeeded =
1131 match self.persistence.checkpoint_runtime_session(session).await {
1132 Ok(()) => true,
1133 Err(checkpoint_error) => {
1134 match &result {
1135 Ok(()) => tracing::warn!(
1136 session_id = %session.id,
1137 error = %checkpoint_error,
1138 "failed to checkpoint session transcript after successful execution"
1139 ),
1140 Err(execution_error) => tracing::warn!(
1141 session_id = %session.id,
1142 error = %checkpoint_error,
1143 execution_error = %execution_error,
1144 "failed to checkpoint session transcript after execution error"
1145 ),
1146 }
1147 false
1148 }
1149 };
1150
1151 if should_observe_completed_tool_trace(&result, final_checkpoint_succeeded, session) {
1152 if let Err(error) = self
1153 .skill_manager
1154 .observe_completed_tool_trace(session, trace_message_start)
1155 .await
1156 {
1157 tracing::warn!(
1158 session_id = %session.id,
1159 %error,
1160 "failed to observe completed tool trace for reuse draft discovery"
1161 );
1162 }
1163 }
1164
1165 result
1166 }
1167}