1use std::collections::{BTreeSet, HashMap};
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, ProviderRegistry};
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
38pub(crate) type AuxiliaryModelResolver = Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>;
39pub(crate) type AuxiliaryModelResolverFactory =
40 Arc<dyn Fn(&str) -> AuxiliaryModelResolver + Send + Sync>;
41
42fn select_auxiliary_model_resolver(
45 factory: Option<&AuxiliaryModelResolverFactory>,
46 roster_provider: Option<&str>,
47 fallback: Option<AuxiliaryModelResolver>,
48) -> Option<AuxiliaryModelResolver> {
49 factory
50 .map(|create| create(roster_provider.unwrap_or_default()))
51 .or(fallback)
52}
53
54#[cfg(test)]
55mod auxiliary_resolver_factory_tests {
56 use super::*;
57 use std::sync::Mutex;
58
59 #[test]
60 fn factory_receives_each_roster_key_and_sdk_fallback_is_unchanged() {
61 let keys = Arc::new(Mutex::new(Vec::new()));
62 let recorded = keys.clone();
63 let factory: AuxiliaryModelResolverFactory = Arc::new(move |key| {
64 recorded.lock().unwrap().push(key.to_string());
65 let name = key.to_string();
66 Arc::new(move || AuxiliaryModelConfig {
67 fast_model_name: Some(name.clone()),
68 ..Default::default()
69 })
70 });
71 let fallback: AuxiliaryModelResolver = Arc::new(|| AuxiliaryModelConfig {
72 fast_model_name: Some("snapshot".into()),
73 ..Default::default()
74 });
75 let a = select_auxiliary_model_resolver(
76 Some(&factory),
77 Some("account-a"),
78 Some(fallback.clone()),
79 )
80 .unwrap();
81 let b = select_auxiliary_model_resolver(
82 Some(&factory),
83 Some("account-b"),
84 Some(fallback.clone()),
85 )
86 .unwrap();
87 let missing = select_auxiliary_model_resolver(Some(&factory), None, None).unwrap();
88 assert_eq!(a().fast_model_name.as_deref(), Some("account-a"));
89 assert_eq!(b().fast_model_name.as_deref(), Some("account-b"));
90 assert_eq!(missing().fast_model_name.as_deref(), Some(""));
91 assert_eq!(*keys.lock().unwrap(), vec!["account-a", "account-b", ""]);
92 let unchanged =
93 select_auxiliary_model_resolver(None, Some("account-a"), Some(fallback.clone()))
94 .unwrap();
95 assert!(Arc::ptr_eq(&unchanged, &fallback));
96 assert!(select_auxiliary_model_resolver(None, None, None).is_none());
97 }
98}
99
100pub(crate) fn bind_child_gold_delegate(
105 config: &mut AgentLoopConfig,
106 selected: Option<&Arc<dyn LLMProvider>>,
107 main: &Arc<dyn LLMProvider>,
108 decorated_main: &Arc<dyn LLMProvider>,
109) -> crate::runtime::runner::Result<()> {
110 let Some(selected) = selected else {
111 return Ok(());
112 };
113 if !Arc::ptr_eq(selected, main) {
114 return Err(bamboo_agent_core::AgentError::LLM(
115 "Managed child-resume provider differs from its selected route".into(),
116 ));
117 }
118 let resolve = config.gold_target_resolver.take().ok_or_else(|| {
119 bamboo_agent_core::AgentError::LLM(
120 "Managed child-resume Gold route is missing its guard".into(),
121 )
122 })?;
123 let delegate = decorated_main.clone();
124 config.gold_target_resolver = Some(Arc::new(move |model, _untrusted_delegate| {
125 resolve(model, delegate.clone())
126 }));
127 Ok(())
128}
129
130#[derive(Clone)]
139pub struct AgentRuntime {
140 pub storage: Arc<dyn Storage>,
141 pub persistence: Arc<dyn RuntimeSessionPersistence>,
142 pub(crate) inherited_child_wait_captured: bool,
144 pub(crate) legacy_image_from_config: bool,
146 pub(crate) legacy_image_provider_source:
147 Option<Arc<bamboo_llm::provider_registry::LegacyVisionProviderSource>>,
148 pub(crate) sdk_skill_execution_host:
149 Option<Arc<dyn crate::runtime::config::SdkSkillExecutionHost>>,
150 pub(crate) workspace_execution_host:
151 Option<Arc<dyn crate::runtime::config::WorkspaceExecutionHost>>,
152 pub session_inbox: Option<Arc<dyn SessionInboxPort>>,
153 pub(crate) live_bootstrap_host: Option<Arc<dyn crate::runtime::config::LiveBootstrapHost>>,
154 pub(crate) gold_target_resolver: Option<crate::runtime::config::GoldTargetResolver>,
155 pub(crate) host_gold_origin_resolver: Option<crate::runtime::host_gold::HostGoldOriginResolver>,
156 pub(crate) child_resume_gold_selected: Option<Arc<dyn LLMProvider>>,
158 pub(crate) background_model_resolver: Option<crate::runtime::config::BackgroundModelResolver>,
159 pub(crate) auxiliary_model_resolver_factory: Option<AuxiliaryModelResolverFactory>,
160 pub(crate) resume_auxiliary_resolver: Option<crate::runtime::config::ResumeAuxiliaryResolver>,
161 pub activation_router: Option<Arc<SessionActivationRouter>>,
162 pub session_messenger: Option<Arc<SessionMessenger>>,
163 pub attachment_reader: Arc<dyn AttachmentReader>,
164 pub skill_manager: Arc<SkillManager>,
165 pub project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
166 pub metrics_collector: MetricsCollector,
167 pub config: Arc<RwLock<Config>>,
168 pub(crate) model_limits_section:
169 Option<Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>>,
170 pub permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
172 pub permission_mode: PermissionMode,
174
175 pub provider: Arc<dyn LLMProvider>,
177
178 pub memory_store: bamboo_memory::memory_store::MemoryStore,
180
181 pub default_tools: Arc<dyn ToolExecutor>,
185
186 pub hook_runner: Arc<HookRunner>,
189}
190
191pub struct AgentRuntimeBuilder {
206 storage: Option<Arc<dyn Storage>>,
207 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
208 session_inbox: Option<Arc<dyn SessionInboxPort>>,
209 activation_router: Option<Arc<SessionActivationRouter>>,
210 session_messenger: Option<Arc<SessionMessenger>>,
211 attachment_reader: Option<Arc<dyn AttachmentReader>>,
212 skill_manager: Option<Arc<SkillManager>>,
213 project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
214 metrics_collector: Option<MetricsCollector>,
215 config: Option<Arc<RwLock<Config>>>,
216 model_limits_section:
217 Option<Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>>,
218 permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
219 permission_mode: PermissionMode,
220 provider: Option<Arc<dyn LLMProvider>>,
221 memory_store: bamboo_memory::memory_store::MemoryStore,
222 default_tools: Option<Arc<dyn ToolExecutor>>,
223 hook_runner: Arc<HookRunner>,
224}
225
226impl AgentRuntimeBuilder {
227 pub fn new() -> Self {
228 Self {
229 storage: None,
230 persistence: None,
231 session_inbox: None,
232 activation_router: None,
233 session_messenger: None,
234 attachment_reader: None,
235 skill_manager: None,
236 project_context_resolver: None,
237 metrics_collector: None,
238 config: None,
239 model_limits_section: None,
240 permission_config: None,
241 permission_mode: PermissionMode::Default,
242 provider: None,
243 memory_store: bamboo_memory::memory_store::MemoryStore::with_defaults(),
244 default_tools: None,
245 hook_runner: Arc::new(HookRunner::new()),
246 }
247 }
248
249 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
250 self.storage = Some(v);
251 self
252 }
253
254 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
255 self.persistence = Some(v);
256 self
257 }
258
259 pub fn session_inbox(mut self, v: Arc<dyn SessionInboxPort>) -> Self {
260 self.session_inbox = Some(v);
261 self
262 }
263
264 pub fn activation_router(mut self, v: Arc<SessionActivationRouter>) -> Self {
265 self.activation_router = Some(v);
266 self
267 }
268
269 pub fn session_messenger(mut self, v: Arc<SessionMessenger>) -> Self {
270 self.session_messenger = Some(v);
271 self
272 }
273
274 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
275 self.attachment_reader = Some(v);
276 self
277 }
278
279 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
280 self.skill_manager = Some(v);
281 self
282 }
283
284 pub fn project_context_resolver(
285 mut self,
286 v: Arc<crate::project_context::ProjectContextResolver>,
287 ) -> Self {
288 self.project_context_resolver = Some(v);
289 self
290 }
291
292 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
293 self.metrics_collector = Some(v);
294 self
295 }
296
297 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
298 self.config = Some(v);
299 self
300 }
301
302 pub fn model_limits_section(
306 mut self,
307 section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
308 ) -> Self {
309 self.model_limits_section = Some(section);
310 self
311 }
312
313 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
314 self.permission_config = Some(v);
315 self
316 }
317
318 pub fn permission_mode(mut self, v: PermissionMode) -> Self {
319 self.permission_mode = v;
320 self
321 }
322
323 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
324 self.provider = Some(v);
325 self
326 }
327
328 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
329 self.memory_store = v;
330 self
331 }
332
333 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
334 self.default_tools = Some(v);
335 self
336 }
337
338 pub fn hook_runner(mut self, v: Arc<HookRunner>) -> Self {
340 self.hook_runner = v;
341 self
342 }
343
344 pub fn build(self) -> Result<AgentRuntime, &'static str> {
345 if let (Some(router), Some(inbox)) = (&self.activation_router, &self.session_inbox) {
346 router.set_inbox(inbox.clone());
347 }
348 Ok(AgentRuntime {
349 inherited_child_wait_captured: false,
350 legacy_image_from_config: false,
351 legacy_image_provider_source: None,
352 sdk_skill_execution_host: None,
353 workspace_execution_host: None,
354 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
355 persistence: self
356 .persistence
357 .ok_or_else(|| format_missing("persistence"))?,
358 session_inbox: self.session_inbox,
359 live_bootstrap_host: None,
360 gold_target_resolver: None,
361 host_gold_origin_resolver: None,
362 child_resume_gold_selected: None,
363 background_model_resolver: None,
364 auxiliary_model_resolver_factory: None,
365 resume_auxiliary_resolver: None,
366 activation_router: self.activation_router,
367 session_messenger: self.session_messenger,
368 attachment_reader: self
369 .attachment_reader
370 .ok_or_else(|| format_missing("attachment_reader"))?,
371 skill_manager: self
372 .skill_manager
373 .ok_or_else(|| format_missing("skill_manager"))?,
374 project_context_resolver: self.project_context_resolver,
375 metrics_collector: self
376 .metrics_collector
377 .ok_or_else(|| format_missing("metrics_collector"))?,
378 config: self.config.ok_or_else(|| format_missing("config"))?,
379 model_limits_section: self.model_limits_section,
380 permission_config: self.permission_config,
381 permission_mode: self.permission_mode,
382 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
383 memory_store: self.memory_store,
384 default_tools: self
385 .default_tools
386 .ok_or_else(|| format_missing("default_tools"))?,
387 hook_runner: self.hook_runner,
388 })
389 }
390}
391
392fn format_missing(field: &str) -> &'static str {
393 match field {
396 "storage" => "AgentRuntimeBuilder: missing storage",
397 "persistence" => "AgentRuntimeBuilder: missing persistence",
398 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
399 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
400 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
401 "config" => "AgentRuntimeBuilder: missing config",
402 "provider" => "AgentRuntimeBuilder: missing provider",
403 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
404 _ => "AgentRuntimeBuilder: missing required field",
405 }
406}
407
408impl Default for AgentRuntimeBuilder {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414pub struct ExecuteRequest {
424 pub ticket_token_budget: Option<Arc<crate::ticket_token_budget::NativeWorkTokenBudget>>,
426 pub ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
428 pub initial_message: String,
430 pub event_tx: mpsc::Sender<AgentEvent>,
431 pub cancel_token: CancellationToken,
432
433 pub tools: Option<Arc<dyn ToolExecutor>>,
437 pub provider_override: Option<Arc<dyn LLMProvider>>,
440
441 pub model_roster: ModelRoster,
448 pub reasoning_effort: Option<ReasoningEffort>,
449 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
452 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
455 pub disabled_tools: Option<BTreeSet<String>>,
457 pub disabled_skill_ids: Option<BTreeSet<String>>,
459 pub selected_skill_ids: Option<Vec<String>>,
460 pub selected_skill_mode: Option<String>,
461 pub image_fallback: Option<ImageFallbackConfig>,
462 pub gold_config: Option<GoldConfig>,
463 pub guardian_config: Option<GuardianConfig>,
465 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
468 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
471 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
474 pub app_data_dir: Option<std::path::PathBuf>,
476 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
484 pub counted_loop: Option<super::counted_loop::CountedLoopConfig>,
487 pub loop_control: Option<super::loop_control::LoopControlConfig>,
489}
490
491pub struct ExecuteRequestBuilder {
507 ticket_token_budget: Option<Arc<crate::ticket_token_budget::NativeWorkTokenBudget>>,
508 ticket_worker_plan: Option<Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>>,
509 initial_message: String,
510 event_tx: mpsc::Sender<AgentEvent>,
511 cancel_token: CancellationToken,
512
513 tools: Option<Arc<dyn ToolExecutor>>,
514 provider_override: Option<Arc<dyn LLMProvider>>,
515 model: Option<String>,
519 provider_name: Option<String>,
520 provider_type: Option<String>,
521 fast_model: Option<String>,
522 fast_model_provider: Option<Arc<dyn LLMProvider>>,
523 background_model: Option<String>,
524 background_model_provider: Option<Arc<dyn LLMProvider>>,
525 summarization_model: Option<String>,
526 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
527 reasoning_effort: Option<ReasoningEffort>,
528 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
529 disabled_filter_resolver: Option<DisabledFilterResolver>,
530 disabled_tools: Option<BTreeSet<String>>,
531 disabled_skill_ids: Option<BTreeSet<String>>,
532 selected_skill_ids: Option<Vec<String>>,
533 selected_skill_mode: Option<String>,
534 image_fallback: Option<ImageFallbackConfig>,
535 gold_config: Option<GoldConfig>,
536 guardian_config: Option<GuardianConfig>,
537 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
538 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
539 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
540 app_data_dir: Option<std::path::PathBuf>,
541 run_budget: Option<bamboo_config::RunBudgetConfig>,
542 counted_loop: Option<super::counted_loop::CountedLoopConfig>,
543 loop_control: Option<super::loop_control::LoopControlConfig>,
544}
545
546impl ExecuteRequestBuilder {
547 pub(crate) fn ticket_token_budget(
548 mut self,
549 budget: Arc<crate::ticket_token_budget::NativeWorkTokenBudget>,
550 ) -> Self {
551 self.ticket_token_budget = Some(budget);
552 self
553 }
554
555 pub fn new(
558 initial_message: impl Into<String>,
559 event_tx: mpsc::Sender<AgentEvent>,
560 cancel_token: CancellationToken,
561 ) -> Self {
562 Self {
563 initial_message: initial_message.into(),
564 ticket_worker_plan: None,
565 ticket_token_budget: None,
566 event_tx,
567 cancel_token,
568 tools: None,
569 provider_override: None,
570 model: None,
571 provider_name: None,
572 provider_type: None,
573 fast_model: None,
574 fast_model_provider: None,
575 background_model: None,
576 background_model_provider: None,
577 summarization_model: None,
578 summarization_model_provider: None,
579 reasoning_effort: None,
580 auxiliary_model_resolver: None,
581 disabled_filter_resolver: None,
582 disabled_tools: None,
583 disabled_skill_ids: None,
584 selected_skill_ids: None,
585 selected_skill_mode: None,
586 image_fallback: None,
587 gold_config: None,
588 guardian_config: None,
589 guardian_spawner: None,
590 bash_resume_hook: None,
591 bash_completion_sink: None,
592 app_data_dir: None,
593 run_budget: None,
594 counted_loop: None,
595 loop_control: None,
596 }
597 }
598
599 pub fn ticket_worker_plan(
601 mut self,
602 plan: Arc<dyn crate::ticket_worker_plan::WorkerLocalPlan>,
603 ) -> Self {
604 self.ticket_worker_plan = Some(plan);
605 self
606 }
607
608 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
610 self.tools = Some(v);
611 self
612 }
613
614 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
616 self.provider_override = Some(v);
617 self
618 }
619
620 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
626 self.fast_model = roster.fast_model();
627 self.fast_model_provider = roster.fast_model_provider();
628 self.background_model = roster.background_model();
629 self.background_model_provider = roster.background_model_provider();
630 self.summarization_model = roster.summarization_model();
631 self.summarization_model_provider = roster.summarization_model_provider();
632 self.model = roster.model;
633 self.provider_name = roster.provider_name;
634 self.provider_type = roster.provider_type;
635 self
636 }
637
638 pub fn model(mut self, v: impl Into<String>) -> Self {
640 self.model = Some(v.into());
641 self
642 }
643
644 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
646 self.provider_name = Some(v.into());
647 self
648 }
649
650 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
652 self.provider_type = Some(v.into());
653 self
654 }
655
656 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
658 self.fast_model = Some(v.into());
659 self
660 }
661
662 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
664 self.fast_model_provider = Some(v);
665 self
666 }
667
668 pub fn background_model(mut self, v: impl Into<String>) -> Self {
670 self.background_model = Some(v.into());
671 self
672 }
673
674 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
676 self.background_model_provider = Some(v);
677 self
678 }
679
680 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
682 self.summarization_model = Some(v.into());
683 self
684 }
685
686 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
688 self.summarization_model_provider = Some(v);
689 self
690 }
691
692 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
694 self.reasoning_effort = Some(v);
695 self
696 }
697
698 pub fn auxiliary_model_resolver(
700 mut self,
701 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
702 ) -> Self {
703 self.auxiliary_model_resolver = Some(v);
704 self
705 }
706
707 pub fn disabled_filter_resolver(mut self, v: DisabledFilterResolver) -> Self {
709 self.disabled_filter_resolver = Some(v);
710 self
711 }
712
713 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
715 self.disabled_tools = Some(v);
716 self
717 }
718
719 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
721 self.disabled_skill_ids = Some(v);
722 self
723 }
724
725 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
727 self.selected_skill_ids = Some(v);
728 self
729 }
730
731 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
733 self.selected_skill_mode = Some(v.into());
734 self
735 }
736
737 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
739 self.image_fallback = Some(v);
740 self
741 }
742
743 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
750 self.gold_config = v;
751 self
752 }
753
754 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
757 self.guardian_config = v;
758 self
759 }
760
761 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
764 self.guardian_spawner = v;
765 self
766 }
767
768 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
771 self.bash_resume_hook = v;
772 self
773 }
774
775 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
778 self.bash_completion_sink = v;
779 self
780 }
781
782 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
784 self.app_data_dir = Some(v);
785 self
786 }
787
788 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
793 self.run_budget = Some(v);
794 self
795 }
796
797 pub fn counted_loop(mut self, config: super::counted_loop::CountedLoopConfig) -> Self {
800 self.counted_loop = Some(config);
801 self
802 }
803
804 pub fn loop_control(mut self, config: super::loop_control::LoopControlConfig) -> Self {
807 self.loop_control = Some(config);
808 self
809 }
810
811 pub fn build(self) -> ExecuteRequest {
816 let model_roster = ModelRoster {
817 model: self.model,
818 provider_name: self.provider_name,
819 provider_type: self.provider_type,
820 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
821 background: RoleModel::from_parts(
822 self.background_model,
823 self.background_model_provider,
824 ),
825 summarization: RoleModel::from_parts(
826 self.summarization_model,
827 self.summarization_model_provider,
828 ),
829 };
830 ExecuteRequest {
831 ticket_worker_plan: self.ticket_worker_plan,
832 ticket_token_budget: self.ticket_token_budget,
833 initial_message: self.initial_message,
834 event_tx: self.event_tx,
835 cancel_token: self.cancel_token,
836 tools: self.tools,
837 provider_override: self.provider_override,
838 model_roster,
839 reasoning_effort: self.reasoning_effort,
840 auxiliary_model_resolver: self.auxiliary_model_resolver,
841 disabled_filter_resolver: self.disabled_filter_resolver,
842 disabled_tools: self.disabled_tools,
843 disabled_skill_ids: self.disabled_skill_ids,
844 selected_skill_ids: self.selected_skill_ids,
845 selected_skill_mode: self.selected_skill_mode,
846 image_fallback: self.image_fallback,
847 gold_config: self.gold_config,
848 guardian_config: self.guardian_config,
849 guardian_spawner: self.guardian_spawner,
850 bash_resume_hook: self.bash_resume_hook,
851 bash_completion_sink: self.bash_completion_sink,
852 app_data_dir: self.app_data_dir,
853 run_budget: self.run_budget,
854 counted_loop: self.counted_loop,
855 loop_control: self.loop_control,
856 }
857 }
858}
859
860fn extract_system_prompt(session: &Session) -> Option<String> {
866 session
867 .messages
868 .iter()
869 .find(|m| matches!(m.role, Role::System))
870 .map(|m| m.content.clone())
871}
872
873pub(super) fn should_observe_completed_tool_trace(
874 result: &crate::runtime::runner::Result<()>,
875 final_checkpoint_succeeded: bool,
876 session: &Session,
877) -> bool {
878 final_checkpoint_succeeded
879 && result.is_ok()
880 && session
881 .agent_runtime_state
882 .as_ref()
883 .is_some_and(|state| matches!(state.status, bamboo_domain::AgentStatusState::Completed))
884 && !session.metadata.contains_key("runtime.suspend_reason")
885 && !session.metadata.contains_key("runtime.completion_reason")
886}
887
888impl AgentRuntime {
893 pub async fn execute(
898 &self,
899 session: &mut Session,
900 req: ExecuteRequest,
901 ) -> crate::runtime::runner::Result<()> {
902 self.execute_with_inputs(session, req, None).await
903 }
904
905 pub async fn execute_with_upstream_retry(
907 &self,
908 session: &mut Session,
909 req: ExecuteRequest,
910 upstream_retry: bamboo_llm::retry::UpstreamRetry,
911 ) -> crate::runtime::runner::Result<()> {
912 self.execute_with_inputs_and_upstream_retry(session, req, None, upstream_retry)
913 .await
914 }
915
916 pub(crate) async fn execute_with_inputs(
917 &self,
918 session: &mut Session,
919 req: ExecuteRequest,
920 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
921 ) -> crate::runtime::runner::Result<()> {
922 self.execute_with_inputs_and_upstream_retry(
923 session,
924 req,
925 inputs,
926 bamboo_llm::retry::UpstreamRetry::Bounded,
927 )
928 .await
929 }
930
931 pub(crate) async fn execute_with_inputs_and_upstream_retry(
932 &self,
933 session: &mut Session,
934 req: ExecuteRequest,
935 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
936 upstream_retry: bamboo_llm::retry::UpstreamRetry,
937 ) -> crate::runtime::runner::Result<()> {
938 struct WorkspaceFinish(Option<Arc<dyn crate::runtime::config::WorkspaceExecutionHost>>);
939 impl Drop for WorkspaceFinish {
940 fn drop(&mut self) {
941 if let Some(host) = &self.0 {
942 host.finish();
943 }
944 }
945 }
946 let _workspace_finish = WorkspaceFinish(self.workspace_execution_host.clone());
949 session
950 .check_execution_compatibility()
951 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
952 super::counted_loop::reject_ambiguous_reentry(session, self.persistence.as_ref()).await?;
955 if let Some(control) = req.loop_control.as_ref() {
956 control
957 .validate_admission(session, self.activation_router.as_ref())
958 .await?;
959 }
960 struct SdkFinish {
961 host: Option<Arc<dyn crate::runtime::config::SdkSkillExecutionHost>>,
962 session_id: String,
963 execution_id: String,
964 }
965 impl Drop for SdkFinish {
966 fn drop(&mut self) {
967 if let Some(host) = self.host.as_ref() {
968 host.finish(&self.session_id, &self.execution_id);
969 }
970 }
971 }
972 let _sdk_finish = SdkFinish {
973 host: self.sdk_skill_execution_host.clone(),
974 session_id: session.id.clone(),
975 execution_id: inputs
976 .as_ref()
977 .and_then(|inputs| inputs.sdk_execution_id())
978 .unwrap_or("")
979 .to_owned(),
980 };
981 let existing = self.persistence.inherited_child_wait();
982 if let Some(inherited) = existing.as_ref() {
983 inherited
984 .validate_session(session)
985 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
986 } else if !self.inherited_child_wait_captured {
987 let mut runtime = self.clone();
988 if let Some(inherited) = bamboo_domain::InheritedChildWait::capture(session) {
989 runtime.persistence = self
990 .persistence
991 .bind_inherited_child_wait(inherited)
992 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
993 }
994 runtime.inherited_child_wait_captured = true;
995 return runtime
996 .execute_bound(session, req, inputs, upstream_retry)
997 .await;
998 }
999 self.execute_bound(session, req, inputs, upstream_retry)
1000 .await
1001 }
1002
1003 async fn execute_bound(
1004 &self,
1005 session: &mut Session,
1006 req: ExecuteRequest,
1007 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1008 upstream_retry: bamboo_llm::retry::UpstreamRetry,
1009 ) -> crate::runtime::runner::Result<()> {
1010 if self.persistence.root_actor_execution_required(session) {
1011 return Err(bamboo_agent_core::AgentError::Tool(
1012 "Root execution route requires a bound Actor writer and event handoff".into(),
1013 ));
1014 }
1015 let session_activation_notifications = match self.activation_router.as_ref() {
1016 Some(router) => Some(Arc::new(parking_lot::Mutex::new(
1017 router.subscribe(&session.id).await,
1018 ))),
1019 None => None,
1020 };
1021 let guidance_active_run_id = match self.activation_router.as_ref() {
1022 Some(router) => router.current_run_id(&session.id).await,
1023 None => None,
1024 };
1025 if let Some(counted) = req.counted_loop.as_ref() {
1026 if !counted
1027 .owns_session(self.activation_router.as_ref(), session)
1028 .await
1029 {
1030 return Err(bamboo_agent_core::AgentError::Budget(
1031 "counted loop requires the caller's exact Session activation owner".into(),
1032 ));
1033 }
1034 }
1035 let system_prompt = extract_system_prompt(session);
1036 let config = self.config.read().await;
1037 let ExecuteRequest {
1038 ticket_worker_plan,
1039 ticket_token_budget,
1040 initial_message,
1041 event_tx,
1042 cancel_token,
1043 tools,
1044 provider_override,
1045 model_roster,
1046 reasoning_effort,
1047 auxiliary_model_resolver,
1048 disabled_filter_resolver,
1049 disabled_tools,
1050 disabled_skill_ids,
1051 selected_skill_ids,
1052 selected_skill_mode,
1053 image_fallback,
1054 gold_config,
1055 guardian_config,
1056 guardian_spawner,
1057 bash_resume_hook,
1058 bash_completion_sink,
1059 app_data_dir,
1060 run_budget,
1061 counted_loop,
1062 loop_control,
1063 } = req;
1064 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
1065 let uses_default_provider = provider_override.is_none();
1066 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
1067 let configured_permission_mode = self
1068 .permission_config
1069 .as_ref()
1070 .map(|config| config.mode())
1071 .unwrap_or(self.permission_mode);
1072 let active_plan_gate = session
1073 .agent_runtime_state
1074 .as_ref()
1075 .is_some_and(|state| state.plan_mode.is_some());
1076
1077 let explicit_roles = [
1080 model_roster.fast.is_some(),
1081 model_roster.background.is_some(),
1082 model_roster.summarization.is_some(),
1083 ];
1084 let fast_model = model_roster.fast_model();
1085 let fast_model_provider = model_roster.fast_model_provider();
1086 let background_model = model_roster.background_model();
1087 let background_model_provider = model_roster.background_model_provider();
1088 let summarization_model = model_roster.summarization_model();
1089 let summarization_model_provider = model_roster.summarization_model_provider();
1090 let ModelRoster {
1091 model,
1092 provider_name,
1093 provider_type,
1094 ..
1095 } = model_roster;
1096 let auxiliary_model_resolver = select_auxiliary_model_resolver(
1097 self.auxiliary_model_resolver_factory.as_ref(),
1098 provider_name.as_deref(),
1099 auxiliary_model_resolver,
1100 );
1101 let active_provider =
1102 provider_name.unwrap_or_else(|| config.effective_default_provider().to_string());
1103 let auxiliary_model_resolver = auxiliary_model_resolver.or_else(|| {
1104 let registry = Arc::new(ProviderRegistry::new(
1108 HashMap::from([(active_provider.clone(), llm.clone())]),
1109 active_provider.clone(),
1110 ));
1111 let resolve = crate::model_areas::live_auxiliary_model_resolver(
1112 self.config.clone(),
1113 registry,
1114 active_provider.clone(),
1115 );
1116 let overrides = AuxiliaryModelConfig {
1117 fast_model_name: fast_model.clone().or_else(|| config.get_fast_model()),
1118 fast_model_provider: fast_model_provider.clone(),
1119 background_model_name: background_model
1120 .clone()
1121 .or_else(|| config.get_memory_background_model()),
1122 background_model_provider: background_model_provider.clone(),
1123 summarization_model_name: summarization_model
1124 .clone()
1125 .or_else(|| config.get_task_summary_model()),
1126 summarization_model_provider: summarization_model_provider.clone(),
1127 ..Default::default()
1128 };
1129 let resolver: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync> =
1130 Arc::new(move || {
1131 let mut current = resolve();
1132 if explicit_roles[0] {
1133 current.fast_model_name = overrides.fast_model_name.clone();
1134 current.fast_model_provider = overrides.fast_model_provider.clone();
1135 }
1136 if explicit_roles[1] {
1137 current.background_model_name = overrides.background_model_name.clone();
1138 current.background_model_provider =
1139 overrides.background_model_provider.clone();
1140 }
1141 if explicit_roles[2] {
1142 current.summarization_model_name =
1143 overrides.summarization_model_name.clone();
1144 current.summarization_model_provider =
1145 overrides.summarization_model_provider.clone();
1146 current.summarization_model_error = None;
1147 }
1148 current
1149 });
1150 Some(resolver)
1151 });
1152 let hook_runner = inputs
1153 .as_ref()
1154 .and_then(|inputs| inputs.sdk_hook_runner())
1155 .unwrap_or_else(|| {
1156 Arc::new(
1157 self.hook_runner
1158 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir.clone()),
1159 )
1160 });
1161
1162 let mut effective_run_budget = config.run_budget.merged_with_override(run_budget.as_ref());
1163 let legacy_vision_model_resolver = image_fallback
1164 .as_ref()
1165 .filter(|fallback| fallback.mode == crate::ImageFallbackMode::Vision)
1166 .map(|fallback| {
1167 crate::runtime::runner::image_fallback::legacy_vision_resolver(
1168 config.clone(),
1169 active_provider.clone(),
1170 llm.clone(),
1171 fallback.vision_model.clone(),
1172 app_data_dir.clone(),
1173 self.legacy_image_from_config,
1174 self.legacy_image_provider_source.clone(),
1175 )
1176 });
1177 if let Some(control) = loop_control.as_ref() {
1178 effective_run_budget.max_rounds = Some(
1179 effective_run_budget
1180 .max_rounds
1181 .map_or(control.max_total_rounds, |cap| {
1182 cap.min(control.max_total_rounds)
1183 }),
1184 );
1185 }
1186 let mut loop_config = AgentLoopConfig {
1187 ticket_worker_plan: ticket_worker_plan.clone(),
1188 guidance_active_run_id,
1189 system_prompt,
1190 legacy_model_limits: config.extra.get("model_limits").cloned(),
1193 model_limits_section: self.model_limits_section.clone(),
1194 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
1195 selected_skill_ids,
1196 selected_skill_mode,
1197 initial_untrusted_inputs: inputs,
1198 sdk_skill_execution_host: self.sdk_skill_execution_host.clone(),
1199 workspace_execution_host: self.workspace_execution_host.clone(),
1200 skill_manager: Some(self.skill_manager.clone()),
1201 project_context_resolver: self.project_context_resolver.clone(),
1202 skip_initial_user_message: true,
1203 storage: Some(self.storage.clone()),
1204 persistence: Some(self.persistence.clone()),
1205 session_inbox: self.session_inbox.clone(),
1206 live_bootstrap_host: self.live_bootstrap_host.clone(),
1207 gold_target_resolver: self.gold_target_resolver.clone(),
1208 session_activation_notifications,
1209 attachment_reader: Some(self.attachment_reader.clone()),
1210 metrics_collector: Some(self.metrics_collector.clone()),
1211 model_name: model,
1212 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
1213 fast_model_provider,
1214 auxiliary_evaluation_max_concurrency:
1215 crate::runtime::config::normalize_auxiliary_evaluation_max_concurrency(
1216 config
1217 .extra
1218 .get("auxiliary_evaluation_max_concurrency")
1219 .and_then(serde_json::Value::as_u64),
1220 ),
1221 background_model_name: background_model
1222 .or_else(|| config.get_memory_background_model()),
1223 planning_model_name: config
1224 .defaults
1225 .as_ref()
1226 .and_then(|d| d.planning.as_ref())
1227 .map(|r| r.model.clone()),
1228 search_model_name: config
1229 .defaults
1230 .as_ref()
1231 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
1232 .map(|r| r.model.clone()),
1233 compression_instructions: None,
1234 summary_target_ratio: config
1235 .memory()
1236 .as_ref()
1237 .map(|memory| memory.summary_target_ratio)
1238 .unwrap_or(0.20),
1239 context_management: config.context_management.clone(),
1240 summary_safe_window_percent: config
1241 .memory()
1242 .as_ref()
1243 .map(|memory| memory.summary_safe_window_percent)
1244 .unwrap_or(80),
1245 summarization_model_name: summarization_model
1246 .or_else(|| config.get_task_summary_model()),
1247 background_model_provider,
1248 summarization_model_provider,
1249 provider_name: Some(active_provider.clone()),
1250 provider_type,
1251 reasoning_effort,
1252 auxiliary_model_resolver,
1253 background_model_resolver: self.background_model_resolver.clone(),
1254 resume_auxiliary_resolver: self.resume_auxiliary_resolver.clone(),
1255 disabled_filter_resolver,
1256 disabled_tools: {
1257 let mut merged = config.disabled_tool_references();
1258 if let Some(dt) = disabled_tools {
1259 merged.extend(dt);
1260 }
1261 merged
1262 },
1263 image_fallback,
1264 legacy_vision_model_resolver,
1265 vision_model_resolver: Some(
1266 crate::runtime::runner::image_fallback::configured_vision_resolver(
1267 self.config.clone(),
1268 app_data_dir.clone(),
1269 active_provider,
1270 ),
1271 ),
1272 app_data_dir,
1273 memory_store: self.memory_store.clone(),
1274 prompt_memory_flags: config
1275 .memory()
1276 .as_ref()
1277 .map(PromptMemoryFlags::from)
1278 .unwrap_or_default(),
1279 features_dynamic_model_routing: config.features.dynamic_model_routing,
1280 features_final_evidence_check: config.features.final_evidence_check,
1281 permission_mode: Some(if active_plan_gate {
1282 PermissionMode::Plan
1283 } else {
1284 configured_permission_mode
1285 }),
1286 gold_config,
1287 guardian_config,
1288 guardian_spawner,
1289 bash_resume_hook,
1290 bash_completion_sink,
1291 hook_runner,
1292 mcp_tool_guidance: tools.tool_guidance(),
1296 run_budget: effective_run_budget,
1302 counted_loop,
1303 loop_control,
1304 stream_timeout: config.stream_timeout,
1305 upstream_retry,
1306 ..Default::default()
1307 };
1308
1309 drop(config);
1310 let undecorated_main = llm.clone();
1311 let llm = match ticket_token_budget.as_ref() {
1312 Some(budget) => budget.decorate(&mut loop_config, llm),
1313 None => llm,
1314 };
1315 if uses_default_provider
1319 && loop_config
1320 .gold_config
1321 .as_ref()
1322 .is_some_and(|gold| gold.enabled && gold.model_name.is_some())
1323 {
1324 if let Some(origin_resolver) = self.host_gold_origin_resolver.clone() {
1325 let tracker = crate::runtime::host_gold::HostGoldRun::new(origin_resolver);
1326 let source_tracker = tracker.clone();
1327 let live_config = self.config.clone();
1328 let original_main = undecorated_main.clone();
1329 let budget = ticket_token_budget.clone();
1330 let limits = Arc::new(AgentLoopConfig {
1331 app_data_dir: loop_config.app_data_dir.clone(),
1332 model_limits_section: loop_config.model_limits_section.clone(),
1333 legacy_model_limits: loop_config.legacy_model_limits.clone(),
1334 ..Default::default()
1335 });
1336 let inherited = loop_config.gold_target_resolver.take();
1337 loop_config.gold_target_resolver = Some(Arc::new(move |model, _mutable_main| {
1338 let receipt = match source_tracker.accepted() {
1339 Ok(receipt) => receipt,
1340 Err(reason) => {
1341 return Arc::new(crate::runtime::host_gold::UnprovenGold {
1342 reason,
1343 refused: std::sync::atomic::AtomicBool::new(false),
1344 }) as Arc<dyn LLMProvider>
1345 }
1346 };
1347 let fixed = match &receipt.origin {
1348 crate::runtime::host_gold::HostGoldOrigin::Injected => {
1349 bamboo_llm::provider::fixed_injected_provider_with_identity(
1350 receipt.selected.clone(),
1351 original_main.clone(),
1352 )
1353 }
1354 crate::runtime::host_gold::HostGoldOrigin::Managed { .. } => {
1355 bamboo_llm::provider::fixed_provider_with_identity(
1356 receipt.selected.clone(),
1357 original_main.clone(),
1358 )
1359 }
1360 };
1361 let delegate = match budget.as_ref() {
1362 Some(budget) => budget.wrap(fixed, &limits),
1363 None => fixed,
1364 };
1365 match receipt.origin {
1366 crate::runtime::host_gold::HostGoldOrigin::Injected => inherited
1367 .as_ref()
1368 .map(|policy| policy(model, delegate.clone()))
1369 .unwrap_or(delegate),
1370 crate::runtime::host_gold::HostGoldOrigin::Managed {
1371 provider_name,
1372 origin,
1373 } => {
1374 let selected = receipt.selected;
1375 let config = live_config.clone();
1376 let managed: crate::runtime::config::GoldTargetResolver =
1377 Arc::new(move |model, delegate| {
1378 bamboo_llm::gold_admission::guarded_gold_target_with_origin(
1379 config.clone(),
1380 origin.clone(),
1381 bamboo_domain::ProviderModelRef::new(
1382 provider_name.as_str(),
1383 model,
1384 ),
1385 selected.clone(),
1386 delegate,
1387 )
1388 });
1389 crate::runtime::agent::compose_child_gold_policy(
1390 inherited.clone(),
1391 managed,
1392 )(model, delegate)
1393 }
1394 }
1395 }));
1396 loop_config.host_gold_run = Some(tracker);
1397 }
1398 }
1399 bind_child_gold_delegate(
1400 &mut loop_config,
1401 self.child_resume_gold_selected.as_ref(),
1402 &undecorated_main,
1403 &llm,
1404 )?;
1405 #[cfg(test)]
1406 crate::runtime::tests::observe_untrusted_inputs(
1407 &session.id,
1408 loop_config.initial_untrusted_inputs(),
1409 );
1410
1411 if let Some(plan) = &ticket_worker_plan {
1412 session
1413 .agent_runtime_state
1414 .get_or_insert_with(Default::default)
1415 .run_id = plan.run_id().to_owned();
1416 plan.bind_session(session)
1417 .map_err(|e| bamboo_agent_core::AgentError::LLM(e.to_string()))?;
1418 }
1419
1420 let trace_message_start = session.messages.len();
1421 let session_end_runner = loop_config.hook_runner.clone();
1422 let session_end_event_tx = event_tx.clone();
1423 let sdk_mode = loop_config.sdk_skill_execution_host.is_some();
1424 let loop_future = Box::pin(run_agent_loop_with_config(
1425 session,
1426 initial_message,
1427 event_tx,
1428 llm,
1429 tools,
1430 cancel_token,
1431 loop_config,
1432 ));
1433 let result = if sdk_mode {
1434 crate::runtime::runner::session_setup::legacy_instruction::scope_sdk_runtime(
1435 loop_future,
1436 )
1437 .await
1438 } else {
1439 loop_future.await
1440 };
1441
1442 crate::runtime::hooks::run_session_end_hooks(
1443 &session_end_runner,
1444 &result,
1445 session,
1446 &session_end_event_tx,
1447 )
1448 .await;
1449
1450 if let Err(bamboo_agent_core::AgentError::StreamTimeout(timeout)) = &result {
1455 if let Some(diagnostic) = timeout.diagnostic(session) {
1456 if session.record_llm_request_diagnostic(diagnostic.clone()) {
1457 bamboo_llm::retry::record_stream_timeout_diagnostic(diagnostic);
1458 }
1459 }
1460 }
1461
1462 let final_checkpoint_succeeded =
1474 match self.persistence.checkpoint_runtime_session(session).await {
1475 Ok(()) => true,
1476 Err(checkpoint_error) => {
1477 match &result {
1478 Ok(()) => tracing::warn!(
1479 session_id = %session.id,
1480 error = %checkpoint_error,
1481 "failed to checkpoint session transcript after successful execution"
1482 ),
1483 Err(execution_error) => tracing::warn!(
1484 session_id = %session.id,
1485 error = %checkpoint_error,
1486 execution_error = %execution_error,
1487 "failed to checkpoint session transcript after execution error"
1488 ),
1489 }
1490 false
1491 }
1492 };
1493
1494 if should_observe_completed_tool_trace(&result, final_checkpoint_succeeded, session) {
1495 if let Err(error) = self
1496 .skill_manager
1497 .observe_completed_tool_trace(session, trace_message_start)
1498 .await
1499 {
1500 tracing::warn!(
1501 session_id = %session.id,
1502 %error,
1503 "failed to observe completed tool trace for reuse draft discovery"
1504 );
1505 }
1506 }
1507
1508 result
1509 }
1510}