1use std::sync::Arc;
7
8use bamboo_agent_core::Session;
9
10use crate::runtime::{AgentRuntime, AgentRuntimeBuilder, ExecuteRequest};
11use bamboo_domain::RuntimeSessionPersistence;
12
13struct ChildGoldPolicyRefusal {
17 dispatched: std::sync::atomic::AtomicBool,
18 rejected: Option<Arc<dyn bamboo_llm::LLMProvider>>,
19}
20
21impl ChildGoldPolicyRefusal {
22 fn unverified_error() -> bamboo_llm::LLMError {
23 bamboo_llm::LLMError::Api(
24 "Existing Gold policy returned an unverified provider for managed child resume".into(),
25 )
26 }
27
28 fn error(&self) -> bamboo_llm::LLMError {
29 if self
30 .rejected
31 .as_ref()
32 .is_some_and(|provider| provider.local_dispatch_refused())
33 {
34 return stale_gold_policy_error();
35 }
36 self.rejected
37 .as_ref()
38 .and_then(|provider| provider.known_pre_dispatch_error())
39 .unwrap_or_else(Self::unverified_error)
40 }
41}
42
43fn stale_gold_policy_error() -> bamboo_llm::LLMError {
44 bamboo_llm::LLMError::Api(
45 "Inherited Gold target already refused a dispatch; a fresh target is required".into(),
46 )
47}
48
49#[async_trait::async_trait]
50impl bamboo_llm::LLMProvider for ChildGoldPolicyRefusal {
51 fn known_pre_dispatch_error(&self) -> Option<bamboo_llm::LLMError> {
52 Some(self.error())
53 }
54 fn queue_requires_current_route(&self) -> bool {
55 true
58 }
59 fn local_dispatch_refused(&self) -> bool {
60 self.dispatched.load(std::sync::atomic::Ordering::Acquire)
61 }
62 async fn chat_stream(
63 &self,
64 _: &[bamboo_agent_core::Message],
65 _: &[bamboo_agent_core::ToolSchema],
66 _: Option<u32>,
67 _: &str,
68 ) -> Result<bamboo_llm::LLMStream, bamboo_llm::LLMError> {
69 self.dispatched
70 .store(true, std::sync::atomic::Ordering::Release);
71 Err(self.error())
72 }
73}
74
75pub(crate) fn compose_child_gold_policy(
76 inherited: Option<crate::runtime::config::GoldTargetResolver>,
77 managed: crate::runtime::config::GoldTargetResolver,
78) -> crate::runtime::config::GoldTargetResolver {
79 let Some(inherited) = inherited else {
80 return managed;
81 };
82 Arc::new(move |model, delegate| {
83 let result = inherited(model, delegate.clone());
86 if !gold_policy_reaches_delegate(&result, &delegate) {
87 return Arc::new(ChildGoldPolicyRefusal {
88 dispatched: std::sync::atomic::AtomicBool::new(false),
89 rejected: None,
90 }) as Arc<dyn bamboo_llm::LLMProvider>;
91 }
92 if result.local_dispatch_refused() {
93 return Arc::new(ChildGoldPolicyRefusal {
94 dispatched: std::sync::atomic::AtomicBool::new(false),
95 rejected: Some(result),
96 }) as Arc<dyn bamboo_llm::LLMProvider>;
97 }
98 let allocation_identity = gold_policy_allocation_identity(&result, &delegate);
99 let policy: Arc<dyn bamboo_llm::LLMProvider> = Arc::new(ChildGoldPolicyProvider {
100 inherited: inherited.clone(),
101 delegate,
102 allocation_identity,
103 model: model.to_owned(),
104 dispatched: std::sync::atomic::AtomicBool::new(false),
105 });
106 managed(model, policy)
107 })
108}
109
110fn gold_policy_allocation_identity(
111 policy: &Arc<dyn bamboo_llm::LLMProvider>,
112 delegate: &Arc<dyn bamboo_llm::LLMProvider>,
113) -> usize {
114 policy
115 .resource_allocation_identity()
116 .or_else(|| delegate.resource_allocation_identity())
117 .unwrap_or(Arc::as_ptr(delegate) as *const () as usize)
118}
119
120fn gold_policy_reaches_delegate(
125 candidate: &Arc<dyn bamboo_llm::LLMProvider>,
126 delegate: &Arc<dyn bamboo_llm::LLMProvider>,
127) -> bool {
128 const MAX_WRAPPERS: usize = 32;
129 let mut seen = Vec::<Arc<dyn bamboo_llm::LLMProvider>>::new();
130 let mut current = candidate.clone();
131 for _ in 0..=MAX_WRAPPERS {
132 if Arc::ptr_eq(¤t, delegate) {
133 return true;
134 }
135 if seen.iter().any(|previous| Arc::ptr_eq(previous, ¤t)) {
136 return false;
137 }
138 seen.push(current.clone());
139 let Some(next) = current.fixed_gold_policy_delegate() else {
140 return false;
141 };
142 current = next;
143 }
144 false
145}
146
147struct ChildGoldPolicyProvider {
151 inherited: crate::runtime::config::GoldTargetResolver,
152 delegate: Arc<dyn bamboo_llm::LLMProvider>,
153 allocation_identity: usize,
154 model: String,
155 dispatched: std::sync::atomic::AtomicBool,
156}
157
158impl ChildGoldPolicyProvider {
159 fn select(
160 &self,
161 model: &str,
162 ) -> Result<Arc<dyn bamboo_llm::LLMProvider>, bamboo_llm::LLMError> {
163 if model != self.model {
164 return Err(bamboo_llm::LLMError::Api(
165 "Gold model changed after policy selection".into(),
166 ));
167 }
168 let current = (self.inherited)(model, self.delegate.clone());
169 if !gold_policy_reaches_delegate(¤t, &self.delegate) {
170 return Err(ChildGoldPolicyRefusal::unverified_error());
171 }
172 if current.local_dispatch_refused() {
176 return Err(stale_gold_policy_error());
177 }
178 if gold_policy_allocation_identity(¤t, &self.delegate) != self.allocation_identity {
182 return Err(bamboo_llm::LLMError::Api(
183 "Gold policy changed its allocation identity before dispatch".into(),
184 ));
185 }
186 Ok(current)
187 }
188
189 fn dispatch(
190 &self,
191 model: &str,
192 ) -> Result<Arc<dyn bamboo_llm::LLMProvider>, bamboo_llm::LLMError> {
193 let selected = self.select(model).and_then(|provider| {
194 if let Some(error) = provider.known_pre_dispatch_error() {
195 Err(error)
196 } else {
197 Ok(provider)
198 }
199 });
200 if selected.is_err() {
201 self.dispatched
202 .store(true, std::sync::atomic::Ordering::Release);
203 }
204 selected
205 }
206
207 fn note_delegate_refusal<T>(
208 &self,
209 provider: &Arc<dyn bamboo_llm::LLMProvider>,
210 result: &Result<T, bamboo_llm::LLMError>,
211 ) {
212 if result.is_err() && provider.local_dispatch_refused() {
213 self.dispatched
214 .store(true, std::sync::atomic::Ordering::Release);
215 }
216 }
217}
218
219#[async_trait::async_trait]
220impl bamboo_llm::LLMProvider for ChildGoldPolicyProvider {
221 fn fixed_gold_policy_delegate(&self) -> Option<Arc<dyn bamboo_llm::LLMProvider>> {
222 Some(self.delegate.clone())
225 }
226
227 fn known_pre_dispatch_error(&self) -> Option<bamboo_llm::LLMError> {
228 match self.select(&self.model) {
229 Ok(provider) => provider.known_pre_dispatch_error(),
230 Err(error) => Some(error),
231 }
232 }
233
234 fn queue_requires_current_route(&self) -> bool {
235 true
236 }
237
238 fn queued_route_identity(&self) -> bamboo_llm::provider::QueuedRouteIdentity {
239 bamboo_llm::provider::queued_identity_for(&self.delegate)
240 }
241
242 fn local_dispatch_refused(&self) -> bool {
243 self.dispatched.load(std::sync::atomic::Ordering::Acquire)
244 }
245
246 fn resource_allocation_identity(&self) -> Option<usize> {
247 Some(self.allocation_identity)
248 }
249
250 async fn supports_vision(&self, model: &str) -> bool {
251 match self.select(&self.model) {
252 Ok(provider) => provider.supports_vision(model).await,
253 Err(_) => false,
254 }
255 }
256
257 async fn vision_support_override(&self, model: &str) -> Option<bool> {
258 match self.select(&self.model) {
259 Ok(provider) => provider.vision_support_override(model).await,
260 Err(_) => Some(false),
261 }
262 }
263
264 async fn capability_loading_mode(
265 &self,
266 model: &str,
267 required: Option<&str>,
268 ) -> bamboo_domain::CapabilityLoadingMode {
269 match self.select(&self.model) {
270 Ok(provider) => provider.capability_loading_mode(model, required).await,
271 Err(_) => bamboo_domain::CapabilityLoadingMode::LegacyFullCatalog,
272 }
273 }
274
275 async fn provider_visible_tool_footprint(
276 &self,
277 ir: &bamboo_llm::PromptIR,
278 tools: &[bamboo_agent_core::ToolSchema],
279 model: &str,
280 required: Option<&str>,
281 ) -> Result<bamboo_llm::ProviderVisibleToolFootprint, bamboo_llm::LLMError> {
282 self.select(model)?
283 .provider_visible_tool_footprint(ir, tools, model, required)
284 .await
285 }
286
287 async fn chat_stream(
288 &self,
289 messages: &[bamboo_agent_core::Message],
290 tools: &[bamboo_agent_core::ToolSchema],
291 max: Option<u32>,
292 model: &str,
293 ) -> Result<bamboo_llm::LLMStream, bamboo_llm::LLMError> {
294 let provider = self.dispatch(model)?;
295 let result = provider.chat_stream(messages, tools, max, model).await;
296 self.note_delegate_refusal(&provider, &result);
297 result
298 }
299
300 async fn chat_stream_with_options(
301 &self,
302 messages: &[bamboo_agent_core::Message],
303 tools: &[bamboo_agent_core::ToolSchema],
304 max: Option<u32>,
305 model: &str,
306 options: Option<&bamboo_llm::LLMRequestOptions>,
307 ) -> Result<bamboo_llm::LLMStream, bamboo_llm::LLMError> {
308 let provider = self.dispatch(model)?;
309 let result = provider
310 .chat_stream_with_options(messages, tools, max, model, options)
311 .await;
312 self.note_delegate_refusal(&provider, &result);
313 result
314 }
315
316 async fn chat_stream_ir(
317 &self,
318 ir: &bamboo_llm::PromptIR,
319 tools: &[bamboo_agent_core::ToolSchema],
320 max: Option<u32>,
321 model: &str,
322 options: Option<&bamboo_llm::LLMRequestOptions>,
323 ) -> Result<bamboo_llm::LLMStream, bamboo_llm::LLMError> {
324 let provider = self.dispatch(model)?;
325 let result = provider
326 .chat_stream_ir(ir, tools, max, model, options)
327 .await;
328 self.note_delegate_refusal(&provider, &result);
329 result
330 }
331
332 async fn list_models(&self) -> Result<Vec<String>, bamboo_llm::LLMError> {
333 self.select(&self.model)?.list_models().await
334 }
335
336 async fn list_model_info(
337 &self,
338 ) -> Result<Vec<bamboo_llm::provider::ProviderModelInfo>, bamboo_llm::LLMError> {
339 self.select(&self.model)?.list_model_info().await
340 }
341}
342
343#[derive(Clone)]
355pub struct Agent {
356 runtime: Arc<AgentRuntime>,
357}
358
359pub struct DirectExecutionLease {
365 target_session_id: String,
366 router: Option<Arc<crate::session_activation::SessionActivationRouter>>,
367 registration: Option<crate::session_activation::SessionRunRegistration>,
368}
369
370impl DirectExecutionLease {
371 pub async fn abandon(mut self) {
374 if let Some(registration) = self.registration.take() {
375 registration.abandon().await;
376 }
377 }
378}
379
380pub struct SdkInputSubmission {
383 session_id: String,
384 execution_id: String,
385 user: bamboo_agent_core::Message,
386 hook_runner: Arc<crate::runtime::hooks::HookRunner>,
387}
388
389impl SdkInputSubmission {
390 pub fn execution_id(&self) -> &str {
391 &self.execution_id
392 }
393 pub fn user(&self) -> &bamboo_agent_core::Message {
394 &self.user
395 }
396}
397
398impl Agent {
399 pub async fn stage_context_compaction(
402 &self,
403 session: &mut Session,
404 registry: &Arc<bamboo_llm::ProviderRegistry>,
405 tools: &dyn bamboo_agent_core::tools::ToolExecutor,
406 app_data_dir: &std::path::Path,
407 ) -> crate::runtime::runner::Result<bool> {
408 use crate::model_config_helper::{
409 get_default_model_for_provider, get_reasoning_effort_for_provider, resolve_gold_config,
410 resolve_live_image_fallback, resolve_provider_routing_key, resolve_provider_type,
411 try_resolve_primary_model, GOLD_CONFIG_METADATA_KEY,
412 };
413 use crate::runtime::config::AgentLoopConfig;
414 use crate::runtime::runner::image_fallback;
415 use crate::session_app::execute::resolve_execution_model;
416 use crate::session_app::types::{ExecuteInput, ExecutionConfigSnapshot};
417 use bamboo_agent_core::AgentError;
418
419 let config = self.runtime.config.read().await.clone();
420 let default_provider =
421 resolve_provider_routing_key(&config, config.effective_default_provider(), registry);
422 let (target, model, _) = resolve_execution_model(
423 session,
424 &ExecuteInput::default(),
425 &ExecutionConfigSnapshot {
426 default_model: default_provider
427 .as_ref()
428 .ok()
429 .and_then(|key| get_default_model_for_provider(&config, key).ok()),
430 default_model_ref: config
431 .defaults
432 .as_ref()
433 .map(|defaults| defaults.chat.clone()),
434 provider_model_ref_enabled: config.features.provider_model_ref,
435 ..Default::default()
436 },
437 )
438 .map_err(|error| AgentError::LLM(error.to_string()))?;
439 let target = match target {
440 Some(target) => target,
441 None if session.metadata.contains_key("created_by_schedule_id") => {
442 return Err(AgentError::LLM(
443 "Scheduled session has no provider/model target".into(),
444 ));
445 }
446 None => bamboo_domain::ProviderModelRef::new(
447 default_provider.map_err(|error| AgentError::LLM(error.to_string()))?,
448 model
449 .ok_or_else(|| AgentError::LLM("A model is required for compaction".into()))?,
450 ),
451 };
452 let selected = try_resolve_primary_model(&config, registry, &target)
453 .map_err(|error| AgentError::LLM(error.to_string()))?;
454 let provider_name = &selected.target.provider;
455 let model = &selected.target.model;
456 let llm = &selected.provider;
457 let data_dir = Some(app_data_dir.to_path_buf());
458 let fallback = resolve_live_image_fallback(&config).map_err(AgentError::LLM)?;
459 let legacy_vision_model_resolver = fallback
460 .as_ref()
461 .filter(|fallback| fallback.mode == crate::ImageFallbackMode::Vision)
462 .map(|fallback| {
463 let source = bamboo_llm::provider_registry::LegacyVisionProviderSource::capture(
464 &config,
465 provider_name,
466 llm.clone(),
467 selected.managed_origin.clone(),
468 )
469 .ok()
470 .map(Arc::new);
471 image_fallback::legacy_vision_resolver(
472 config.clone(),
473 provider_name.clone(),
474 llm.clone(),
475 fallback.vision_model.clone(),
476 data_dir.clone(),
477 true,
478 source,
479 )
480 });
481 let memory = config.memory();
482 let loop_config = AgentLoopConfig {
483 legacy_model_limits: config.extra.get("model_limits").cloned(),
484 model_limits_section: self.runtime.model_limits_section.clone(),
485 attachment_reader: Some(self.runtime.attachment_reader.clone()),
486 model_name: Some(model.clone()),
487 provider_name: Some(provider_name.clone()),
488 provider_type: resolve_provider_type(&config, provider_name, registry),
489 reasoning_effort: session
490 .reasoning_effort
491 .or(target.reasoning_effort)
492 .or_else(|| get_reasoning_effort_for_provider(&config, provider_name)),
493 context_management: config.context_management.clone(),
494 gold_config: resolve_gold_config(
495 &config,
496 session
497 .metadata
498 .get(GOLD_CONFIG_METADATA_KEY)
499 .map(String::as_str),
500 ),
501 summary_target_ratio: memory.as_ref().map_or(0.20, |m| m.summary_target_ratio),
502 summary_safe_window_percent: memory
503 .as_ref()
504 .map_or(80, |m| m.summary_safe_window_percent),
505 auxiliary_model_resolver: Some(crate::model_areas::live_auxiliary_model_resolver(
506 self.runtime.config.clone(),
507 registry.clone(),
508 provider_name.clone(),
509 )),
510 disabled_tools: config.disabled_tool_references(),
511 disabled_skill_ids: config.disabled_skill_ids(),
512 image_fallback: fallback,
513 legacy_vision_model_resolver,
514 vision_model_resolver: Some(image_fallback::configured_vision_resolver(
515 self.runtime.config.clone(),
516 data_dir.clone(),
517 provider_name.clone(),
518 )),
519 hook_runner: Arc::new(
520 self.runtime
521 .hook_runner
522 .with_lifecycle_config(&config.lifecycle_hooks, data_dir.clone()),
523 ),
524 app_data_dir: data_dir,
525 ..Default::default()
526 };
527 let tools = crate::runtime::runner::session_setup::tool_schemas::resolve_available_tool_schemas_for_session(
528 &loop_config, tools, session,
529 );
530 crate::runtime::runner::round_lifecycle::stage_user_context_compaction(
531 session,
532 &loop_config,
533 model,
534 &tools,
535 llm,
536 )
537 .await
538 }
539
540 pub fn from_runtime(runtime: Arc<AgentRuntime>) -> Self {
542 Agent { runtime }
543 }
544
545 #[doc(hidden)]
547 pub fn legacy_injected_provider_snapshot(
548 &self,
549 ) -> Result<Option<Arc<dyn bamboo_llm::LLMProvider>>, bamboo_llm::LLMError> {
550 self.runtime.provider.legacy_injected_provider_snapshot()
551 }
552
553 #[doc(hidden)]
555 pub fn fixed_legacy_injected_provider(
556 &self,
557 expected: &Arc<dyn bamboo_llm::LLMProvider>,
558 ) -> Result<Option<Arc<dyn bamboo_llm::LLMProvider>>, bamboo_llm::LLMError> {
559 Ok(self
560 .runtime
561 .provider
562 .legacy_injected_provider_snapshot()?
563 .and_then(|current| {
564 Arc::ptr_eq(¤t, expected).then(|| {
565 bamboo_llm::provider::fixed_injected_provider_with_identity(
566 current,
567 self.runtime.provider.clone(),
568 )
569 })
570 }))
571 }
572
573 #[doc(hidden)]
576 pub fn with_execution_persistence(
577 &self,
578 persistence: Arc<dyn RuntimeSessionPersistence>,
579 ) -> Self {
580 let mut runtime = (*self.runtime).clone();
581 runtime.persistence = persistence;
582 runtime.inherited_child_wait_captured = true;
583 Self::from_runtime(Arc::new(runtime))
584 }
585
586 #[doc(hidden)]
589 pub fn with_config_derived_image_fallback(&self) -> Self {
590 let mut runtime = (*self.runtime).clone();
591 runtime.legacy_image_from_config = true;
592 Self::from_runtime(Arc::new(runtime))
593 }
594
595 #[doc(hidden)]
597 pub fn with_legacy_vision_provider_source(
598 &self,
599 source: Option<Arc<bamboo_llm::provider_registry::LegacyVisionProviderSource>>,
600 ) -> Self {
601 let mut runtime = (*self.runtime).clone();
602 runtime.legacy_image_provider_source = source;
603 Self::from_runtime(Arc::new(runtime))
604 }
605
606 #[doc(hidden)]
609 pub fn with_default_provider_adapter(
610 &self,
611 adapter: impl FnOnce(Arc<dyn bamboo_llm::LLMProvider>) -> Arc<dyn bamboo_llm::LLMProvider>,
612 ) -> Self {
613 let mut runtime = (*self.runtime).clone();
614 runtime.provider = adapter(runtime.provider.clone());
615 Self::from_runtime(Arc::new(runtime))
616 }
617
618 #[doc(hidden)]
621 pub fn with_live_bootstrap_host(
622 &self,
623 host: Arc<dyn crate::runtime::config::LiveBootstrapHost>,
624 ) -> Self {
625 let mut runtime = (*self.runtime).clone();
626 runtime.live_bootstrap_host = Some(host);
627 Self::from_runtime(Arc::new(runtime))
628 }
629
630 #[doc(hidden)]
632 pub fn with_background_model_resolver(
633 &self,
634 resolver: crate::runtime::config::BackgroundModelResolver,
635 ) -> Self {
636 let mut runtime = (*self.runtime).clone();
637 runtime.background_model_resolver = Some(resolver);
638 Self::from_runtime(Arc::new(runtime))
639 }
640
641 #[doc(hidden)]
643 pub fn with_auxiliary_model_resolver_factory(
644 &self,
645 factory: Arc<
646 dyn Fn(&str) -> Arc<dyn Fn() -> crate::AuxiliaryModelConfig + Send + Sync>
647 + Send
648 + Sync,
649 >,
650 ) -> Self {
651 let mut runtime = (*self.runtime).clone();
652 runtime.auxiliary_model_resolver_factory = Some(factory);
653 runtime.background_model_resolver = None;
657 runtime.resume_auxiliary_resolver = None;
658 Self::from_runtime(Arc::new(runtime))
659 }
660
661 #[doc(hidden)]
663 pub fn with_resume_auxiliary_resolver(
664 &self,
665 resolver: crate::runtime::config::ResumeAuxiliaryResolver,
666 ) -> Self {
667 let mut runtime = (*self.runtime).clone();
668 runtime.resume_auxiliary_resolver = Some(resolver);
669 Self::from_runtime(Arc::new(runtime))
670 }
671
672 #[doc(hidden)]
675 pub fn with_host_gold_origin_resolver(
676 &self,
677 resolver: crate::runtime::host_gold::HostGoldOriginResolver,
678 ) -> Self {
679 let mut runtime = (*self.runtime).clone();
680 runtime.host_gold_origin_resolver = Some(resolver);
681 Self::from_runtime(Arc::new(runtime))
682 }
683
684 #[doc(hidden)]
688 pub fn with_gold_target_resolver(
689 &self,
690 resolver: crate::runtime::config::GoldTargetResolver,
691 ) -> Self {
692 let mut runtime = (*self.runtime).clone();
693 runtime.gold_target_resolver = Some(resolver);
694 runtime.child_resume_gold_selected = None;
695 Self::from_runtime(Arc::new(runtime))
696 }
697
698 #[doc(hidden)]
700 pub fn with_composed_gold_target_resolver(
701 &self,
702 resolver: crate::runtime::config::GoldTargetResolver,
703 ) -> Self {
704 let mut runtime = (*self.runtime).clone();
705 runtime.gold_target_resolver = Some(compose_child_gold_policy(
706 runtime.gold_target_resolver.take(),
707 resolver,
708 ));
709 Self::from_runtime(Arc::new(runtime))
710 }
711
712 pub(crate) fn with_child_resume_gold_target_resolver(
715 &self,
716 selected: Arc<dyn bamboo_llm::LLMProvider>,
717 resolver: crate::runtime::config::GoldTargetResolver,
718 ) -> Self {
719 let mut runtime = (*self.runtime).clone();
720 runtime.gold_target_resolver = Some(compose_child_gold_policy(
721 runtime.gold_target_resolver.take(),
722 resolver,
723 ));
724 runtime.child_resume_gold_selected = Some(selected);
725 Self::from_runtime(Arc::new(runtime))
726 }
727
728 #[doc(hidden)]
730 pub fn with_sdk_skill_execution_host(
731 &self,
732 host: Arc<dyn crate::runtime::config::SdkSkillExecutionHost>,
733 ) -> Self {
734 self.with_skill_execution_host(host)
735 }
736
737 #[doc(hidden)]
739 pub fn with_skill_execution_host(
740 &self,
741 host: Arc<dyn crate::runtime::config::SkillExecutionHost>,
742 ) -> Self {
743 let mut runtime = (*self.runtime).clone();
744 runtime.sdk_skill_execution_host = Some(host);
745 Self::from_runtime(Arc::new(runtime))
746 }
747
748 #[doc(hidden)]
750 pub fn with_workspace_execution_host(
751 &self,
752 host: Arc<dyn crate::runtime::config::WorkspaceExecutionHost>,
753 ) -> Self {
754 let mut runtime = (*self.runtime).clone();
755 runtime.workspace_execution_host = Some(host);
756 Self::from_runtime(Arc::new(runtime))
757 }
758
759 #[doc(hidden)]
762 pub async fn precheck_sdk_user_input(
763 &self,
764 session: &mut Session,
765 user: &bamboo_agent_core::Message,
766 app_data_dir: Option<std::path::PathBuf>,
767 ) -> crate::runtime::runner::Result<SdkInputSubmission> {
768 session
769 .check_execution_compatibility()
770 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
771 if user.role != bamboo_agent_core::Role::User
772 || crate::runtime::config::UntrustedInputObservation::new(&user.id, None).is_none()
773 || session.messages.iter().any(|message| message.id == user.id)
774 {
775 return Err(bamboo_agent_core::AgentError::Tool(
776 "invalid new SDK User".into(),
777 ));
778 }
779 let hook_runner = {
780 let config = self.runtime.config.read().await;
781 Arc::new(
782 self.runtime
783 .hook_runner
784 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir),
785 )
786 };
787 session.metadata.remove("runtime.plugin_prompt_prechecked");
788 let prompt = hook_runner
789 .apply_portable_user_prompt(session, &user.content)
790 .await?;
791 let mut transformed = user.clone();
792 transformed.content = prompt.clone();
793 if let Some(parts) = transformed.content_parts.as_mut() {
794 if let Some(bamboo_domain::MessagePart::Text { text }) =
795 parts.iter_mut().find(|part| matches!(part, bamboo_domain::MessagePart::Text { text } if text == &user.content))
796 {
797 *text = prompt;
798 }
799 }
800 Ok(SdkInputSubmission {
801 session_id: session.id.clone(),
802 execution_id: crate::runtime::runner::round_prelude::new_execution_id(),
803 user: transformed,
804 hook_runner,
805 })
806 }
807
808 #[doc(hidden)]
811 pub fn append_sdk_user_input(
812 &self,
813 session: &mut Session,
814 user: bamboo_agent_core::Message,
815 receipt: Option<SdkInputSubmission>,
816 request: Option<bamboo_domain::SessionSkillRequest>,
817 ) -> crate::runtime::runner::Result<crate::runtime::config::UntrustedExecutionInputs> {
818 session
819 .check_execution_compatibility()
820 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
821 use crate::runtime::config::{
822 sdk_message_digest, sdk_message_header_digest, SdkPendingInput,
823 UntrustedExecutionInputs, UntrustedInputObservation,
824 };
825 let invalid = || bamboo_agent_core::AgentError::Tool("invalid SDK append receipt".into());
826 let header_digest = sdk_message_header_digest(&user);
827 let input_id = user.id.clone();
828 if let Some(receipt) = receipt.as_ref() {
829 if receipt.session_id != session.id
830 || receipt.user.role != bamboo_agent_core::Role::User
831 || user.role != bamboo_agent_core::Role::User
832 || sdk_message_header_digest(&receipt.user) != header_digest
833 || session
834 .messages
835 .iter()
836 .any(|message| message.id == input_id)
837 {
838 return Err(invalid());
839 }
840 }
841 let final_digest = receipt.as_ref().map(|_| sdk_message_digest(&user));
844 if receipt.is_some() {
845 crate::runtime::hooks::HookRunner::mark_user_prompt_prechecked(session, &user.content);
846 }
847 session.add_message(user);
848 let observation =
849 UntrustedInputObservation::new(&input_id, request.as_ref()).ok_or_else(invalid)?;
850 let carrier = UntrustedExecutionInputs::new(vec![observation]).ok_or_else(invalid)?;
851 let (execution_id, hook_runner) = receipt
852 .map(|receipt| (receipt.execution_id, Some(receipt.hook_runner)))
853 .unwrap_or_else(|| {
854 (
855 crate::runtime::runner::round_prelude::new_execution_id(),
856 None,
857 )
858 });
859 Ok(carrier.bind_sdk_append(
860 execution_id,
861 SdkPendingInput {
862 session_id: session.id.clone(),
863 input_id,
864 header_digest,
865 final_digest,
866 },
867 hook_runner,
868 ))
869 }
870
871 pub fn builder() -> AgentBuilder {
873 AgentBuilder::new()
874 }
875
876 pub async fn execute(
878 &self,
879 session: &mut Session,
880 req: ExecuteRequest,
881 ) -> crate::runtime::runner::Result<()> {
882 self.runtime.execute(session, req).await
883 }
884
885 pub async fn execute_with_upstream_retry(
887 &self,
888 session: &mut Session,
889 req: ExecuteRequest,
890 upstream_retry: bamboo_llm::retry::UpstreamRetry,
891 ) -> crate::runtime::runner::Result<()> {
892 self.runtime
893 .execute_with_upstream_retry(session, req, upstream_retry)
894 .await
895 }
896
897 pub async fn execute_with_inputs(
900 &self,
901 session: &mut Session,
902 req: ExecuteRequest,
903 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
904 ) -> crate::runtime::runner::Result<()> {
905 self.runtime.execute_with_inputs(session, req, inputs).await
906 }
907
908 pub(crate) async fn execute_with_inputs_and_upstream_retry(
909 &self,
910 session: &mut Session,
911 req: ExecuteRequest,
912 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
913 upstream_retry: bamboo_llm::retry::UpstreamRetry,
914 ) -> crate::runtime::runner::Result<()> {
915 self.runtime
916 .execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
917 .await
918 }
919
920 pub async fn execute_direct(
931 &self,
932 session: &mut Session,
933 req: ExecuteRequest,
934 ) -> crate::runtime::runner::Result<()> {
935 self.execute_direct_with_upstream_retry(
936 session,
937 req,
938 bamboo_llm::retry::UpstreamRetry::Bounded,
939 )
940 .await
941 }
942
943 pub async fn execute_direct_with_upstream_retry(
945 &self,
946 session: &mut Session,
947 req: ExecuteRequest,
948 upstream_retry: bamboo_llm::retry::UpstreamRetry,
949 ) -> crate::runtime::runner::Result<()> {
950 session
951 .check_execution_compatibility()
952 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
953 let lease = self.begin_direct_execution(&session.id).await?;
954 self.check_direct_execution_admission(session, &lease)
955 .await?;
956 self.prepare_external_session_for_execution(session).await?;
957 self.execute_direct_registered_with_inputs_and_upstream_retry(
958 session,
959 req,
960 lease,
961 None,
962 upstream_retry,
963 )
964 .await
965 }
966
967 pub fn check_direct_execution_admission<'a>(
971 &'a self,
972 session: &'a mut Session,
973 lease: &'a DirectExecutionLease,
974 ) -> impl std::future::Future<Output = crate::runtime::runner::Result<()>> + Send + 'a {
975 let matches_target = lease.target_session_id == session.id;
976 let ownership = lease.registration.as_ref().map(|run| run.ownership());
980 async move {
981 if !matches_target {
982 return Err(bamboo_agent_core::AgentError::LLM(
983 "direct execution lease target does not match session".into(),
984 ));
985 }
986 session
987 .check_execution_compatibility()
988 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
989 super::counted_loop::admit_registered(
990 session,
991 ownership.as_ref(),
992 self.activation_router(),
993 self.persistence().as_ref(),
994 )
995 .await
996 }
997 }
998
999 pub async fn prepare_external_session_for_execution(
1007 &self,
1008 session: &mut Session,
1009 ) -> crate::runtime::runner::Result<()> {
1010 crate::session_app::execution_prep::prepare_external_session_for_execution(
1011 session,
1012 self.runtime.project_context_resolver.as_deref(),
1013 )
1014 .await
1015 }
1016
1017 pub async fn prepare_external_project_assignment_read_only(
1021 &self,
1022 session: &mut Session,
1023 ) -> crate::runtime::runner::Result<()> {
1024 let resolver = self
1025 .runtime
1026 .project_context_resolver
1027 .as_deref()
1028 .ok_or_else(|| {
1029 bamboo_agent_core::AgentError::ProjectContext(
1030 "Project assignment requires a ProjectContextResolver".to_string(),
1031 )
1032 })?;
1033 resolver
1034 .refresh_session_prompt_read_only(session)
1035 .await
1036 .map(|_| ())
1037 .map_err(|error| bamboo_agent_core::AgentError::ProjectContext(error.to_string()))
1038 }
1039
1040 pub async fn begin_direct_execution(
1043 &self,
1044 target_session_id: &str,
1045 ) -> crate::runtime::runner::Result<DirectExecutionLease> {
1046 let Some(router) = self.activation_router().cloned() else {
1047 return Ok(DirectExecutionLease {
1048 target_session_id: target_session_id.to_string(),
1049 router: None,
1050 registration: None,
1051 });
1052 };
1053 let run_id = format!("sdk-direct-{}", uuid::Uuid::new_v4());
1054 let registration = router
1055 .register_run(target_session_id, &run_id)
1056 .await
1057 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
1058 Ok(DirectExecutionLease {
1059 target_session_id: target_session_id.to_string(),
1060 router: Some(router),
1061 registration: Some(registration),
1062 })
1063 }
1064
1065 pub async fn execute_direct_registered(
1068 &self,
1069 session: &mut Session,
1070 req: ExecuteRequest,
1071 lease: DirectExecutionLease,
1072 ) -> crate::runtime::runner::Result<()> {
1073 self.execute_direct_registered_with_inputs(session, req, lease, None)
1074 .await
1075 }
1076
1077 pub async fn execute_direct_registered_with_inputs(
1079 &self,
1080 session: &mut Session,
1081 req: ExecuteRequest,
1082 lease: DirectExecutionLease,
1083 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1084 ) -> crate::runtime::runner::Result<()> {
1085 self.execute_direct_registered_with_inputs_and_upstream_retry(
1086 session,
1087 req,
1088 lease,
1089 inputs,
1090 bamboo_llm::retry::UpstreamRetry::Bounded,
1091 )
1092 .await
1093 }
1094
1095 async fn execute_direct_registered_with_inputs_and_upstream_retry(
1096 &self,
1097 session: &mut Session,
1098 req: ExecuteRequest,
1099 lease: DirectExecutionLease,
1100 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1101 upstream_retry: bamboo_llm::retry::UpstreamRetry,
1102 ) -> crate::runtime::runner::Result<()> {
1103 self.check_direct_execution_admission(session, &lease)
1104 .await?;
1105 let inherited = self.persistence().inherited_child_wait().or_else(|| {
1106 (!self.runtime.inherited_child_wait_captured)
1107 .then(|| bamboo_domain::InheritedChildWait::capture(session))
1108 .flatten()
1109 });
1110 let agent = if let Some(inherited) = inherited {
1111 inherited
1112 .validate_session(session)
1113 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
1114 if self.persistence().inherited_child_wait().is_some() {
1115 self.clone()
1116 } else {
1117 self.with_execution_persistence(
1118 self.persistence()
1119 .bind_inherited_child_wait(inherited)
1120 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?,
1121 )
1122 }
1123 } else {
1124 self.with_execution_persistence(self.persistence().clone())
1125 };
1126 agent
1127 .execute_direct_registered_bound(session, req, lease, inputs, upstream_retry)
1128 .await
1129 }
1130
1131 async fn execute_direct_registered_bound(
1132 &self,
1133 session: &mut Session,
1134 mut req: ExecuteRequest,
1135 mut lease: DirectExecutionLease,
1136 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1137 upstream_retry: bamboo_llm::retry::UpstreamRetry,
1138 ) -> crate::runtime::runner::Result<()> {
1139 if lease.target_session_id != session.id {
1140 return Err(bamboo_agent_core::AgentError::LLM(format!(
1141 "direct execution lease target {} does not match session {}",
1142 lease.target_session_id, session.id
1143 )));
1144 }
1145 let Some(router) = lease.router.take() else {
1146 return self
1147 .execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1148 .await;
1149 };
1150 let mut registration = lease.registration.take().ok_or_else(|| {
1151 bamboo_agent_core::AgentError::LLM(
1152 "direct execution lease is missing its router registration".to_string(),
1153 )
1154 })?;
1155 if !router
1156 .bind_run_lifetime(®istration.ownership(), session)
1157 .await
1158 {
1159 return Err(bamboo_agent_core::AgentError::Tool(
1160 "registered run Session lifetime differs".into(),
1161 ));
1162 }
1163 if let Some(counted) = req.counted_loop.as_mut() {
1164 counted.bind_owner(registration.ownership(), session);
1165 }
1166 if let Some(control) = req.loop_control.as_mut() {
1167 control.bind_owner(registration.ownership(), session);
1168 control.bind_transport_cleanup(
1169 &mut registration,
1170 self.storage().clone(),
1171 self.session_inbox().cloned(),
1172 self.persistence().root_actor_writer(),
1173 );
1174 }
1175 let result = if req.loop_control.is_some() {
1176 bamboo_agent_core::tools::observe_tool_suspension(false, async {
1177 let token = bamboo_agent_core::tools::capture_suspension_restriction().ok_or_else(
1178 || {
1179 bamboo_agent_core::AgentError::Tool(
1180 "direct loop_control execution is missing its host observation scope"
1181 .into(),
1182 )
1183 },
1184 )?;
1185 registration.bind_tool_suspension_observation(token);
1188 self.execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1189 .await
1190 })
1191 .await
1192 .0
1193 } else {
1194 self.execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1195 .await
1196 };
1197 if registration.observed_forbidden_running() {
1198 registration.quarantine().await;
1203 return match result {
1204 Err(error) => Err(error),
1205 Ok(()) => Err(bamboo_agent_core::AgentError::Tool(
1206 "suspension_disabled: detached Running outcome; stop unconfirmed".into(),
1207 )),
1208 };
1209 }
1210
1211 let executed_admitted_generation = session
1214 .session_inbox_admission()
1215 .map_or(0, |state| state.last_admitted_sequence);
1216 registration.begin_finalization().await;
1217
1218 let legacy_migration = crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
1219 session,
1220 Some(self.storage()),
1221 Some(self.persistence()),
1222 self.session_inbox(),
1223 )
1224 .await;
1225 if let Some(generation) = legacy_migration.highest_generation {
1226 session.session_inbox_admission_mut().observe(generation);
1227 }
1228 let pending_generation = session
1229 .session_inbox_admission()
1230 .and_then(|state| state.pending_activation_generation());
1231 if let Some(generation) = pending_generation {
1232 let activation_ready = if let Some(inbox) = self.session_inbox() {
1233 match inbox
1234 .mark_activation_eligible(
1235 &session.id,
1236 generation,
1237 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
1238 )
1239 .await
1240 {
1241 Ok(()) => true,
1242 Err(error) => {
1243 tracing::error!(
1244 session_id = %session.id,
1245 %error,
1246 "failed to persist direct SDK SessionInbox activation watermark"
1247 );
1248 false
1249 }
1250 }
1251 } else {
1252 false
1253 };
1254 if activation_ready {
1255 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
1256 router.as_ref(),
1257 &session.id,
1258 generation,
1259 )
1260 .await
1261 {
1262 tracing::error!(
1263 session_id = %session.id,
1264 %error,
1265 "failed to hand direct SDK SessionInbox generation to activation router"
1266 );
1267 }
1268 }
1269 }
1270
1271 if let Err(error) = self.persistence().checkpoint_runtime_session(session).await {
1274 tracing::warn!(
1275 session_id = %session.id,
1276 %error,
1277 "failed to checkpoint direct SDK terminal SessionInbox state"
1278 );
1279 }
1280 if let Err(error) = registration.finish(executed_admitted_generation).await {
1281 tracing::error!(
1282 session_id = %session.id,
1283 %error,
1284 "direct SDK SessionInbox finalization failed"
1285 );
1286 }
1287
1288 result
1289 }
1290
1291 pub fn storage(&self) -> &Arc<dyn bamboo_agent_core::storage::Storage> {
1293 &self.runtime.storage
1294 }
1295
1296 pub fn persistence(&self) -> &Arc<dyn RuntimeSessionPersistence> {
1298 &self.runtime.persistence
1299 }
1300
1301 pub fn session_inbox(&self) -> Option<&Arc<dyn bamboo_domain::SessionInboxPort>> {
1302 self.runtime.session_inbox.as_ref()
1303 }
1304
1305 pub async fn admit_session_inbox_at_safe_boundary(
1310 &self,
1311 session: &mut bamboo_agent_core::Session,
1312 ) -> usize {
1313 crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
1314 session,
1315 Some(self.storage()),
1316 Some(self.persistence()),
1317 self.session_inbox(),
1318 )
1319 .await
1320 .merged
1321 }
1322
1323 pub async fn admit_session_inbox_at_safe_boundary_checked(
1327 &self,
1328 session: &mut bamboo_agent_core::Session,
1329 ) -> Result<usize, bamboo_agent_core::AgentError> {
1330 let refresh = crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
1331 session,
1332 Some(self.storage()),
1333 Some(self.persistence()),
1334 self.session_inbox(),
1335 )
1336 .await;
1337 if let Some(error) = refresh.admission_error {
1338 return Err(bamboo_agent_core::AgentError::Tool(error));
1339 }
1340 Ok(refresh.merged)
1341 }
1342
1343 pub fn activation_router(
1344 &self,
1345 ) -> Option<&Arc<crate::session_activation::SessionActivationRouter>> {
1346 self.runtime.activation_router.as_ref()
1347 }
1348
1349 pub fn session_messenger(&self) -> Option<&Arc<crate::SessionMessenger>> {
1350 self.runtime.session_messenger.as_ref()
1351 }
1352
1353 pub fn default_tools(&self) -> &Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
1363 &self.runtime.default_tools
1364 }
1365}
1366
1367pub struct AgentBuilder {
1375 inner: AgentRuntimeBuilder,
1376}
1377
1378impl AgentBuilder {
1379 pub fn new() -> Self {
1380 Self {
1381 inner: AgentRuntimeBuilder::new(),
1382 }
1383 }
1384
1385 pub fn storage(mut self, v: Arc<dyn bamboo_agent_core::storage::Storage>) -> Self {
1386 self.inner = self.inner.storage(v);
1387 self
1388 }
1389
1390 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
1391 self.inner = self.inner.persistence(v);
1392 self
1393 }
1394
1395 pub fn session_inbox(mut self, v: Arc<dyn bamboo_domain::SessionInboxPort>) -> Self {
1396 self.inner = self.inner.session_inbox(v);
1397 self
1398 }
1399
1400 pub fn activation_router(
1401 mut self,
1402 v: Arc<crate::session_activation::SessionActivationRouter>,
1403 ) -> Self {
1404 self.inner = self.inner.activation_router(v);
1405 self
1406 }
1407
1408 pub fn session_messenger(mut self, v: Arc<crate::SessionMessenger>) -> Self {
1409 self.inner = self.inner.session_messenger(v);
1410 self
1411 }
1412
1413 pub fn attachment_reader(
1414 mut self,
1415 v: Arc<dyn bamboo_agent_core::storage::AttachmentReader>,
1416 ) -> Self {
1417 self.inner = self.inner.attachment_reader(v);
1418 self
1419 }
1420
1421 pub fn skill_manager(mut self, v: Arc<bamboo_skills::SkillManager>) -> Self {
1422 self.inner = self.inner.skill_manager(v);
1423 self
1424 }
1425
1426 pub fn project_context_resolver(
1427 mut self,
1428 v: Arc<crate::project_context::ProjectContextResolver>,
1429 ) -> Self {
1430 self.inner = self.inner.project_context_resolver(v);
1431 self
1432 }
1433
1434 pub fn metrics_collector(mut self, v: bamboo_metrics::MetricsCollector) -> Self {
1435 self.inner = self.inner.metrics_collector(v);
1436 self
1437 }
1438
1439 pub fn config(mut self, v: Arc<tokio::sync::RwLock<bamboo_llm::Config>>) -> Self {
1440 self.inner = self.inner.config(v);
1441 self
1442 }
1443
1444 pub fn model_limits_section(
1447 mut self,
1448 section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
1449 ) -> Self {
1450 self.inner = self.inner.model_limits_section(section);
1451 self
1452 }
1453
1454 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
1455 self.inner = self.inner.permission_config(v);
1456 self
1457 }
1458
1459 pub fn permission_mode(mut self, v: bamboo_domain::PermissionMode) -> Self {
1460 self.inner = self.inner.permission_mode(v);
1461 self
1462 }
1463
1464 pub fn provider(mut self, v: Arc<dyn bamboo_llm::LLMProvider>) -> Self {
1465 self.inner = self.inner.provider(v);
1466 self
1467 }
1468
1469 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
1470 self.inner = self.inner.memory_store(v);
1471 self
1472 }
1473
1474 pub fn default_tools(mut self, v: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
1475 self.inner = self.inner.default_tools(v);
1476 self
1477 }
1478
1479 pub fn hook_runner(mut self, v: Arc<crate::runtime::HookRunner>) -> Self {
1481 self.inner = self.inner.hook_runner(v);
1482 self
1483 }
1484
1485 pub fn build(self) -> Result<Agent, &'static str> {
1486 let runtime = self.inner.build()?;
1487 Ok(Agent {
1488 runtime: Arc::new(runtime),
1489 })
1490 }
1491}
1492
1493impl Default for AgentBuilder {
1494 fn default() -> Self {
1495 Self::new()
1496 }
1497}
1498
1499#[cfg(test)]
1500mod auxiliary_factory_isolation_tests {
1501 use super::*;
1502 use bamboo_agent_core::storage::Storage;
1503 use bamboo_agent_core::{Message, ToolSchema};
1504 use bamboo_llm::{LLMError, LLMProvider, LLMStream};
1505
1506 struct UnusedProvider;
1507
1508 #[async_trait::async_trait]
1509 impl LLMProvider for UnusedProvider {
1510 async fn chat_stream(
1511 &self,
1512 _messages: &[Message],
1513 _tools: &[ToolSchema],
1514 _max_output_tokens: Option<u32>,
1515 _model: &str,
1516 ) -> Result<LLMStream, LLMError> {
1517 Err(LLMError::Api("provider must not be called".into()))
1518 }
1519 }
1520
1521 #[tokio::test]
1522 async fn factory_clone_discards_only_its_inherited_route_overrides() {
1523 let dir = tempfile::tempdir().unwrap();
1524 let store = Arc::new(
1525 bamboo_storage::SessionStoreV2::new(dir.path().join("sessions"))
1526 .await
1527 .unwrap(),
1528 );
1529 let storage: Arc<dyn Storage> = store.clone();
1530 let persistence: Arc<dyn RuntimeSessionPersistence> =
1531 Arc::new(bamboo_storage::LockedSessionStore::new(storage.clone()));
1532 let metrics = bamboo_metrics::MetricsCollector::spawn(
1533 Arc::new(bamboo_metrics::SqliteMetricsStorage::new(
1534 dir.path().join("metrics.db"),
1535 )),
1536 7,
1537 );
1538 let ordinary = Agent::builder()
1539 .storage(storage)
1540 .persistence(persistence)
1541 .attachment_reader(store)
1542 .skill_manager(Arc::new(bamboo_skills::SkillManager::new()))
1543 .metrics_collector(metrics)
1544 .config(Arc::new(tokio::sync::RwLock::new(
1545 bamboo_llm::Config::default(),
1546 )))
1547 .provider(Arc::new(UnusedProvider))
1548 .default_tools(Arc::new(bamboo_tools::BuiltinToolExecutor::new()))
1549 .build()
1550 .unwrap();
1551 let background: crate::runtime::config::BackgroundModelResolver = Arc::new(|_| None);
1552 let resume: crate::runtime::config::ResumeAuxiliaryResolver = Arc::new(|| (Ok(None), None));
1553 let bound = ordinary
1554 .with_background_model_resolver(background.clone())
1555 .with_resume_auxiliary_resolver(resume.clone());
1556 let sibling = bound.clone();
1557 let schedule = bound.with_auxiliary_model_resolver_factory(Arc::new(|key| {
1558 let key = key.to_owned();
1559 Arc::new(move || crate::AuxiliaryModelConfig {
1560 fast_model_name: Some(key.clone()),
1561 ..Default::default()
1562 })
1563 }));
1564
1565 assert!(ordinary.runtime.auxiliary_model_resolver_factory.is_none());
1566 assert!(ordinary.runtime.background_model_resolver.is_none());
1567 assert!(ordinary.runtime.resume_auxiliary_resolver.is_none());
1568 for agent in [&bound, &sibling] {
1569 assert!(agent.runtime.auxiliary_model_resolver_factory.is_none());
1570 assert!(Arc::ptr_eq(
1571 agent.runtime.background_model_resolver.as_ref().unwrap(),
1572 &background,
1573 ));
1574 assert!(Arc::ptr_eq(
1575 agent.runtime.resume_auxiliary_resolver.as_ref().unwrap(),
1576 &resume,
1577 ));
1578 }
1579 assert!(schedule.runtime.background_model_resolver.is_none());
1580 assert!(schedule.runtime.resume_auxiliary_resolver.is_none());
1581 let factory = schedule
1582 .runtime
1583 .auxiliary_model_resolver_factory
1584 .as_ref()
1585 .unwrap();
1586 assert_eq!(
1587 factory("run-key")().fast_model_name.as_deref(),
1588 Some("run-key")
1589 );
1590 }
1591}
1592
1593#[cfg(test)]
1594#[path = "child_resume_gold_policy_tests.rs"]
1595mod child_resume_gold_policy_tests;