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 fn from_runtime(runtime: Arc<AgentRuntime>) -> Self {
401 Agent { runtime }
402 }
403
404 #[doc(hidden)]
406 pub fn legacy_injected_provider_snapshot(
407 &self,
408 ) -> Result<Option<Arc<dyn bamboo_llm::LLMProvider>>, bamboo_llm::LLMError> {
409 self.runtime.provider.legacy_injected_provider_snapshot()
410 }
411
412 #[doc(hidden)]
414 pub fn fixed_legacy_injected_provider(
415 &self,
416 expected: &Arc<dyn bamboo_llm::LLMProvider>,
417 ) -> Result<Option<Arc<dyn bamboo_llm::LLMProvider>>, bamboo_llm::LLMError> {
418 Ok(self
419 .runtime
420 .provider
421 .legacy_injected_provider_snapshot()?
422 .and_then(|current| {
423 Arc::ptr_eq(¤t, expected).then(|| {
424 bamboo_llm::provider::fixed_injected_provider_with_identity(
425 current,
426 self.runtime.provider.clone(),
427 )
428 })
429 }))
430 }
431
432 #[doc(hidden)]
435 pub fn with_execution_persistence(
436 &self,
437 persistence: Arc<dyn RuntimeSessionPersistence>,
438 ) -> Self {
439 let mut runtime = (*self.runtime).clone();
440 runtime.persistence = persistence;
441 runtime.inherited_child_wait_captured = true;
442 Self::from_runtime(Arc::new(runtime))
443 }
444
445 #[doc(hidden)]
448 pub fn with_config_derived_image_fallback(&self) -> Self {
449 let mut runtime = (*self.runtime).clone();
450 runtime.legacy_image_from_config = true;
451 Self::from_runtime(Arc::new(runtime))
452 }
453
454 #[doc(hidden)]
456 pub fn with_legacy_vision_provider_source(
457 &self,
458 source: Option<Arc<bamboo_llm::provider_registry::LegacyVisionProviderSource>>,
459 ) -> Self {
460 let mut runtime = (*self.runtime).clone();
461 runtime.legacy_image_provider_source = source;
462 Self::from_runtime(Arc::new(runtime))
463 }
464
465 #[doc(hidden)]
468 pub fn with_default_provider_adapter(
469 &self,
470 adapter: impl FnOnce(Arc<dyn bamboo_llm::LLMProvider>) -> Arc<dyn bamboo_llm::LLMProvider>,
471 ) -> Self {
472 let mut runtime = (*self.runtime).clone();
473 runtime.provider = adapter(runtime.provider.clone());
474 Self::from_runtime(Arc::new(runtime))
475 }
476
477 #[doc(hidden)]
480 pub fn with_live_bootstrap_host(
481 &self,
482 host: Arc<dyn crate::runtime::config::LiveBootstrapHost>,
483 ) -> Self {
484 let mut runtime = (*self.runtime).clone();
485 runtime.live_bootstrap_host = Some(host);
486 Self::from_runtime(Arc::new(runtime))
487 }
488
489 #[doc(hidden)]
491 pub fn with_background_model_resolver(
492 &self,
493 resolver: crate::runtime::config::BackgroundModelResolver,
494 ) -> Self {
495 let mut runtime = (*self.runtime).clone();
496 runtime.background_model_resolver = Some(resolver);
497 Self::from_runtime(Arc::new(runtime))
498 }
499
500 #[doc(hidden)]
502 pub fn with_auxiliary_model_resolver_factory(
503 &self,
504 factory: Arc<
505 dyn Fn(&str) -> Arc<dyn Fn() -> crate::AuxiliaryModelConfig + Send + Sync>
506 + Send
507 + Sync,
508 >,
509 ) -> Self {
510 let mut runtime = (*self.runtime).clone();
511 runtime.auxiliary_model_resolver_factory = Some(factory);
512 runtime.background_model_resolver = None;
516 runtime.resume_auxiliary_resolver = None;
517 Self::from_runtime(Arc::new(runtime))
518 }
519
520 #[doc(hidden)]
522 pub fn with_resume_auxiliary_resolver(
523 &self,
524 resolver: crate::runtime::config::ResumeAuxiliaryResolver,
525 ) -> Self {
526 let mut runtime = (*self.runtime).clone();
527 runtime.resume_auxiliary_resolver = Some(resolver);
528 Self::from_runtime(Arc::new(runtime))
529 }
530
531 #[doc(hidden)]
534 pub fn with_host_gold_origin_resolver(
535 &self,
536 resolver: crate::runtime::host_gold::HostGoldOriginResolver,
537 ) -> Self {
538 let mut runtime = (*self.runtime).clone();
539 runtime.host_gold_origin_resolver = Some(resolver);
540 Self::from_runtime(Arc::new(runtime))
541 }
542
543 #[doc(hidden)]
547 pub fn with_gold_target_resolver(
548 &self,
549 resolver: crate::runtime::config::GoldTargetResolver,
550 ) -> Self {
551 let mut runtime = (*self.runtime).clone();
552 runtime.gold_target_resolver = Some(resolver);
553 runtime.child_resume_gold_selected = None;
554 Self::from_runtime(Arc::new(runtime))
555 }
556
557 #[doc(hidden)]
559 pub fn with_composed_gold_target_resolver(
560 &self,
561 resolver: crate::runtime::config::GoldTargetResolver,
562 ) -> Self {
563 let mut runtime = (*self.runtime).clone();
564 runtime.gold_target_resolver = Some(compose_child_gold_policy(
565 runtime.gold_target_resolver.take(),
566 resolver,
567 ));
568 Self::from_runtime(Arc::new(runtime))
569 }
570
571 pub(crate) fn with_child_resume_gold_target_resolver(
574 &self,
575 selected: Arc<dyn bamboo_llm::LLMProvider>,
576 resolver: crate::runtime::config::GoldTargetResolver,
577 ) -> Self {
578 let mut runtime = (*self.runtime).clone();
579 runtime.gold_target_resolver = Some(compose_child_gold_policy(
580 runtime.gold_target_resolver.take(),
581 resolver,
582 ));
583 runtime.child_resume_gold_selected = Some(selected);
584 Self::from_runtime(Arc::new(runtime))
585 }
586
587 #[doc(hidden)]
589 pub fn with_sdk_skill_execution_host(
590 &self,
591 host: Arc<dyn crate::runtime::config::SdkSkillExecutionHost>,
592 ) -> Self {
593 self.with_skill_execution_host(host)
594 }
595
596 #[doc(hidden)]
598 pub fn with_skill_execution_host(
599 &self,
600 host: Arc<dyn crate::runtime::config::SkillExecutionHost>,
601 ) -> Self {
602 let mut runtime = (*self.runtime).clone();
603 runtime.sdk_skill_execution_host = Some(host);
604 Self::from_runtime(Arc::new(runtime))
605 }
606
607 #[doc(hidden)]
609 pub fn with_workspace_execution_host(
610 &self,
611 host: Arc<dyn crate::runtime::config::WorkspaceExecutionHost>,
612 ) -> Self {
613 let mut runtime = (*self.runtime).clone();
614 runtime.workspace_execution_host = Some(host);
615 Self::from_runtime(Arc::new(runtime))
616 }
617
618 #[doc(hidden)]
621 pub async fn precheck_sdk_user_input(
622 &self,
623 session: &mut Session,
624 user: &bamboo_agent_core::Message,
625 app_data_dir: Option<std::path::PathBuf>,
626 ) -> crate::runtime::runner::Result<SdkInputSubmission> {
627 session
628 .check_execution_compatibility()
629 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
630 if user.role != bamboo_agent_core::Role::User
631 || crate::runtime::config::UntrustedInputObservation::new(&user.id, None).is_none()
632 || session.messages.iter().any(|message| message.id == user.id)
633 {
634 return Err(bamboo_agent_core::AgentError::Tool(
635 "invalid new SDK User".into(),
636 ));
637 }
638 let hook_runner = {
639 let config = self.runtime.config.read().await;
640 Arc::new(
641 self.runtime
642 .hook_runner
643 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir),
644 )
645 };
646 session.metadata.remove("runtime.plugin_prompt_prechecked");
647 let prompt = hook_runner
648 .apply_portable_user_prompt(session, &user.content)
649 .await?;
650 let mut transformed = user.clone();
651 transformed.content = prompt.clone();
652 if let Some(parts) = transformed.content_parts.as_mut() {
653 if let Some(bamboo_domain::MessagePart::Text { text }) =
654 parts.iter_mut().find(|part| matches!(part, bamboo_domain::MessagePart::Text { text } if text == &user.content))
655 {
656 *text = prompt;
657 }
658 }
659 Ok(SdkInputSubmission {
660 session_id: session.id.clone(),
661 execution_id: crate::runtime::runner::round_prelude::new_execution_id(),
662 user: transformed,
663 hook_runner,
664 })
665 }
666
667 #[doc(hidden)]
670 pub fn append_sdk_user_input(
671 &self,
672 session: &mut Session,
673 user: bamboo_agent_core::Message,
674 receipt: Option<SdkInputSubmission>,
675 request: Option<bamboo_domain::SessionSkillRequest>,
676 ) -> crate::runtime::runner::Result<crate::runtime::config::UntrustedExecutionInputs> {
677 session
678 .check_execution_compatibility()
679 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
680 use crate::runtime::config::{
681 sdk_message_digest, sdk_message_header_digest, SdkPendingInput,
682 UntrustedExecutionInputs, UntrustedInputObservation,
683 };
684 let invalid = || bamboo_agent_core::AgentError::Tool("invalid SDK append receipt".into());
685 let header_digest = sdk_message_header_digest(&user);
686 let input_id = user.id.clone();
687 if let Some(receipt) = receipt.as_ref() {
688 if receipt.session_id != session.id
689 || receipt.user.role != bamboo_agent_core::Role::User
690 || user.role != bamboo_agent_core::Role::User
691 || sdk_message_header_digest(&receipt.user) != header_digest
692 || session
693 .messages
694 .iter()
695 .any(|message| message.id == input_id)
696 {
697 return Err(invalid());
698 }
699 }
700 let final_digest = receipt.as_ref().map(|_| sdk_message_digest(&user));
703 if receipt.is_some() {
704 crate::runtime::hooks::HookRunner::mark_user_prompt_prechecked(session, &user.content);
705 }
706 session.add_message(user);
707 let observation =
708 UntrustedInputObservation::new(&input_id, request.as_ref()).ok_or_else(invalid)?;
709 let carrier = UntrustedExecutionInputs::new(vec![observation]).ok_or_else(invalid)?;
710 let (execution_id, hook_runner) = receipt
711 .map(|receipt| (receipt.execution_id, Some(receipt.hook_runner)))
712 .unwrap_or_else(|| {
713 (
714 crate::runtime::runner::round_prelude::new_execution_id(),
715 None,
716 )
717 });
718 Ok(carrier.bind_sdk_append(
719 execution_id,
720 SdkPendingInput {
721 session_id: session.id.clone(),
722 input_id,
723 header_digest,
724 final_digest,
725 },
726 hook_runner,
727 ))
728 }
729
730 pub fn builder() -> AgentBuilder {
732 AgentBuilder::new()
733 }
734
735 pub async fn execute(
737 &self,
738 session: &mut Session,
739 req: ExecuteRequest,
740 ) -> crate::runtime::runner::Result<()> {
741 self.runtime.execute(session, req).await
742 }
743
744 pub async fn execute_with_upstream_retry(
746 &self,
747 session: &mut Session,
748 req: ExecuteRequest,
749 upstream_retry: bamboo_llm::retry::UpstreamRetry,
750 ) -> crate::runtime::runner::Result<()> {
751 self.runtime
752 .execute_with_upstream_retry(session, req, upstream_retry)
753 .await
754 }
755
756 pub async fn execute_with_inputs(
759 &self,
760 session: &mut Session,
761 req: ExecuteRequest,
762 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
763 ) -> crate::runtime::runner::Result<()> {
764 self.runtime.execute_with_inputs(session, req, inputs).await
765 }
766
767 pub(crate) async fn execute_with_inputs_and_upstream_retry(
768 &self,
769 session: &mut Session,
770 req: ExecuteRequest,
771 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
772 upstream_retry: bamboo_llm::retry::UpstreamRetry,
773 ) -> crate::runtime::runner::Result<()> {
774 self.runtime
775 .execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
776 .await
777 }
778
779 pub async fn execute_direct(
790 &self,
791 session: &mut Session,
792 req: ExecuteRequest,
793 ) -> crate::runtime::runner::Result<()> {
794 self.execute_direct_with_upstream_retry(
795 session,
796 req,
797 bamboo_llm::retry::UpstreamRetry::Bounded,
798 )
799 .await
800 }
801
802 pub async fn execute_direct_with_upstream_retry(
804 &self,
805 session: &mut Session,
806 req: ExecuteRequest,
807 upstream_retry: bamboo_llm::retry::UpstreamRetry,
808 ) -> crate::runtime::runner::Result<()> {
809 session
810 .check_execution_compatibility()
811 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
812 let lease = self.begin_direct_execution(&session.id).await?;
813 self.check_direct_execution_admission(session, &lease)
814 .await?;
815 self.prepare_external_session_for_execution(session).await?;
816 self.execute_direct_registered_with_inputs_and_upstream_retry(
817 session,
818 req,
819 lease,
820 None,
821 upstream_retry,
822 )
823 .await
824 }
825
826 pub fn check_direct_execution_admission<'a>(
830 &'a self,
831 session: &'a mut Session,
832 lease: &'a DirectExecutionLease,
833 ) -> impl std::future::Future<Output = crate::runtime::runner::Result<()>> + Send + 'a {
834 let matches_target = lease.target_session_id == session.id;
835 let ownership = lease.registration.as_ref().map(|run| run.ownership());
839 async move {
840 if !matches_target {
841 return Err(bamboo_agent_core::AgentError::LLM(
842 "direct execution lease target does not match session".into(),
843 ));
844 }
845 session
846 .check_execution_compatibility()
847 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
848 super::counted_loop::admit_registered(
849 session,
850 ownership.as_ref(),
851 self.activation_router(),
852 self.persistence().as_ref(),
853 )
854 .await
855 }
856 }
857
858 pub async fn prepare_external_session_for_execution(
866 &self,
867 session: &mut Session,
868 ) -> crate::runtime::runner::Result<()> {
869 crate::session_app::execution_prep::prepare_external_session_for_execution(
870 session,
871 self.runtime.project_context_resolver.as_deref(),
872 )
873 .await
874 }
875
876 pub async fn prepare_external_project_assignment_read_only(
880 &self,
881 session: &mut Session,
882 ) -> crate::runtime::runner::Result<()> {
883 let resolver = self
884 .runtime
885 .project_context_resolver
886 .as_deref()
887 .ok_or_else(|| {
888 bamboo_agent_core::AgentError::ProjectContext(
889 "Project assignment requires a ProjectContextResolver".to_string(),
890 )
891 })?;
892 resolver
893 .refresh_session_prompt_read_only(session)
894 .await
895 .map(|_| ())
896 .map_err(|error| bamboo_agent_core::AgentError::ProjectContext(error.to_string()))
897 }
898
899 pub async fn begin_direct_execution(
902 &self,
903 target_session_id: &str,
904 ) -> crate::runtime::runner::Result<DirectExecutionLease> {
905 let Some(router) = self.activation_router().cloned() else {
906 return Ok(DirectExecutionLease {
907 target_session_id: target_session_id.to_string(),
908 router: None,
909 registration: None,
910 });
911 };
912 let run_id = format!("sdk-direct-{}", uuid::Uuid::new_v4());
913 let registration = router
914 .register_run(target_session_id, &run_id)
915 .await
916 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
917 Ok(DirectExecutionLease {
918 target_session_id: target_session_id.to_string(),
919 router: Some(router),
920 registration: Some(registration),
921 })
922 }
923
924 pub async fn execute_direct_registered(
927 &self,
928 session: &mut Session,
929 req: ExecuteRequest,
930 lease: DirectExecutionLease,
931 ) -> crate::runtime::runner::Result<()> {
932 self.execute_direct_registered_with_inputs(session, req, lease, None)
933 .await
934 }
935
936 pub async fn execute_direct_registered_with_inputs(
938 &self,
939 session: &mut Session,
940 req: ExecuteRequest,
941 lease: DirectExecutionLease,
942 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
943 ) -> crate::runtime::runner::Result<()> {
944 self.execute_direct_registered_with_inputs_and_upstream_retry(
945 session,
946 req,
947 lease,
948 inputs,
949 bamboo_llm::retry::UpstreamRetry::Bounded,
950 )
951 .await
952 }
953
954 async fn execute_direct_registered_with_inputs_and_upstream_retry(
955 &self,
956 session: &mut Session,
957 req: ExecuteRequest,
958 lease: DirectExecutionLease,
959 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
960 upstream_retry: bamboo_llm::retry::UpstreamRetry,
961 ) -> crate::runtime::runner::Result<()> {
962 self.check_direct_execution_admission(session, &lease)
963 .await?;
964 let inherited = self.persistence().inherited_child_wait().or_else(|| {
965 (!self.runtime.inherited_child_wait_captured)
966 .then(|| bamboo_domain::InheritedChildWait::capture(session))
967 .flatten()
968 });
969 let agent = if let Some(inherited) = inherited {
970 inherited
971 .validate_session(session)
972 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
973 if self.persistence().inherited_child_wait().is_some() {
974 self.clone()
975 } else {
976 self.with_execution_persistence(
977 self.persistence()
978 .bind_inherited_child_wait(inherited)
979 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?,
980 )
981 }
982 } else {
983 self.with_execution_persistence(self.persistence().clone())
984 };
985 agent
986 .execute_direct_registered_bound(session, req, lease, inputs, upstream_retry)
987 .await
988 }
989
990 async fn execute_direct_registered_bound(
991 &self,
992 session: &mut Session,
993 mut req: ExecuteRequest,
994 mut lease: DirectExecutionLease,
995 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
996 upstream_retry: bamboo_llm::retry::UpstreamRetry,
997 ) -> crate::runtime::runner::Result<()> {
998 if lease.target_session_id != session.id {
999 return Err(bamboo_agent_core::AgentError::LLM(format!(
1000 "direct execution lease target {} does not match session {}",
1001 lease.target_session_id, session.id
1002 )));
1003 }
1004 let Some(router) = lease.router.take() else {
1005 return self
1006 .execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1007 .await;
1008 };
1009 let mut registration = lease.registration.take().ok_or_else(|| {
1010 bamboo_agent_core::AgentError::LLM(
1011 "direct execution lease is missing its router registration".to_string(),
1012 )
1013 })?;
1014 if !router
1015 .bind_run_lifetime(®istration.ownership(), session)
1016 .await
1017 {
1018 return Err(bamboo_agent_core::AgentError::Tool(
1019 "registered run Session lifetime differs".into(),
1020 ));
1021 }
1022 if let Some(counted) = req.counted_loop.as_mut() {
1023 counted.bind_owner(registration.ownership(), session);
1024 }
1025 if let Some(control) = req.loop_control.as_mut() {
1026 control.bind_owner(registration.ownership(), session);
1027 control.bind_transport_cleanup(
1028 &mut registration,
1029 self.storage().clone(),
1030 self.session_inbox().cloned(),
1031 self.persistence().root_actor_writer(),
1032 );
1033 }
1034 let result = if req.loop_control.is_some() {
1035 bamboo_agent_core::tools::observe_tool_suspension(false, async {
1036 let token = bamboo_agent_core::tools::capture_suspension_restriction().ok_or_else(
1037 || {
1038 bamboo_agent_core::AgentError::Tool(
1039 "direct loop_control execution is missing its host observation scope"
1040 .into(),
1041 )
1042 },
1043 )?;
1044 registration.bind_tool_suspension_observation(token);
1047 self.execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1048 .await
1049 })
1050 .await
1051 .0
1052 } else {
1053 self.execute_with_inputs_and_upstream_retry(session, req, inputs, upstream_retry)
1054 .await
1055 };
1056 if registration.observed_forbidden_running() {
1057 registration.quarantine().await;
1062 return match result {
1063 Err(error) => Err(error),
1064 Ok(()) => Err(bamboo_agent_core::AgentError::Tool(
1065 "suspension_disabled: detached Running outcome; stop unconfirmed".into(),
1066 )),
1067 };
1068 }
1069
1070 let executed_admitted_generation = session
1073 .session_inbox_admission()
1074 .map_or(0, |state| state.last_admitted_sequence);
1075 registration.begin_finalization().await;
1076
1077 let legacy_migration = crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
1078 session,
1079 Some(self.storage()),
1080 Some(self.persistence()),
1081 self.session_inbox(),
1082 )
1083 .await;
1084 if let Some(generation) = legacy_migration.highest_generation {
1085 session.session_inbox_admission_mut().observe(generation);
1086 }
1087 let pending_generation = session
1088 .session_inbox_admission()
1089 .and_then(|state| state.pending_activation_generation());
1090 if let Some(generation) = pending_generation {
1091 let activation_ready = if let Some(inbox) = self.session_inbox() {
1092 match inbox
1093 .mark_activation_eligible(
1094 &session.id,
1095 generation,
1096 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
1097 )
1098 .await
1099 {
1100 Ok(()) => true,
1101 Err(error) => {
1102 tracing::error!(
1103 session_id = %session.id,
1104 %error,
1105 "failed to persist direct SDK SessionInbox activation watermark"
1106 );
1107 false
1108 }
1109 }
1110 } else {
1111 false
1112 };
1113 if activation_ready {
1114 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
1115 router.as_ref(),
1116 &session.id,
1117 generation,
1118 )
1119 .await
1120 {
1121 tracing::error!(
1122 session_id = %session.id,
1123 %error,
1124 "failed to hand direct SDK SessionInbox generation to activation router"
1125 );
1126 }
1127 }
1128 }
1129
1130 if let Err(error) = self.persistence().checkpoint_runtime_session(session).await {
1133 tracing::warn!(
1134 session_id = %session.id,
1135 %error,
1136 "failed to checkpoint direct SDK terminal SessionInbox state"
1137 );
1138 }
1139 if let Err(error) = registration.finish(executed_admitted_generation).await {
1140 tracing::error!(
1141 session_id = %session.id,
1142 %error,
1143 "direct SDK SessionInbox finalization failed"
1144 );
1145 }
1146
1147 result
1148 }
1149
1150 pub fn storage(&self) -> &Arc<dyn bamboo_agent_core::storage::Storage> {
1152 &self.runtime.storage
1153 }
1154
1155 pub fn persistence(&self) -> &Arc<dyn RuntimeSessionPersistence> {
1157 &self.runtime.persistence
1158 }
1159
1160 pub fn session_inbox(&self) -> Option<&Arc<dyn bamboo_domain::SessionInboxPort>> {
1161 self.runtime.session_inbox.as_ref()
1162 }
1163
1164 pub async fn admit_session_inbox_at_safe_boundary(
1169 &self,
1170 session: &mut bamboo_agent_core::Session,
1171 ) -> usize {
1172 crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
1173 session,
1174 Some(self.storage()),
1175 Some(self.persistence()),
1176 self.session_inbox(),
1177 )
1178 .await
1179 .merged
1180 }
1181
1182 pub async fn admit_session_inbox_at_safe_boundary_checked(
1186 &self,
1187 session: &mut bamboo_agent_core::Session,
1188 ) -> Result<usize, bamboo_agent_core::AgentError> {
1189 let refresh = crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
1190 session,
1191 Some(self.storage()),
1192 Some(self.persistence()),
1193 self.session_inbox(),
1194 )
1195 .await;
1196 if let Some(error) = refresh.admission_error {
1197 return Err(bamboo_agent_core::AgentError::Tool(error));
1198 }
1199 Ok(refresh.merged)
1200 }
1201
1202 pub fn activation_router(
1203 &self,
1204 ) -> Option<&Arc<crate::session_activation::SessionActivationRouter>> {
1205 self.runtime.activation_router.as_ref()
1206 }
1207
1208 pub fn session_messenger(&self) -> Option<&Arc<crate::SessionMessenger>> {
1209 self.runtime.session_messenger.as_ref()
1210 }
1211
1212 pub fn default_tools(&self) -> &Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
1222 &self.runtime.default_tools
1223 }
1224}
1225
1226pub struct AgentBuilder {
1234 inner: AgentRuntimeBuilder,
1235}
1236
1237impl AgentBuilder {
1238 pub fn new() -> Self {
1239 Self {
1240 inner: AgentRuntimeBuilder::new(),
1241 }
1242 }
1243
1244 pub fn storage(mut self, v: Arc<dyn bamboo_agent_core::storage::Storage>) -> Self {
1245 self.inner = self.inner.storage(v);
1246 self
1247 }
1248
1249 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
1250 self.inner = self.inner.persistence(v);
1251 self
1252 }
1253
1254 pub fn session_inbox(mut self, v: Arc<dyn bamboo_domain::SessionInboxPort>) -> Self {
1255 self.inner = self.inner.session_inbox(v);
1256 self
1257 }
1258
1259 pub fn activation_router(
1260 mut self,
1261 v: Arc<crate::session_activation::SessionActivationRouter>,
1262 ) -> Self {
1263 self.inner = self.inner.activation_router(v);
1264 self
1265 }
1266
1267 pub fn session_messenger(mut self, v: Arc<crate::SessionMessenger>) -> Self {
1268 self.inner = self.inner.session_messenger(v);
1269 self
1270 }
1271
1272 pub fn attachment_reader(
1273 mut self,
1274 v: Arc<dyn bamboo_agent_core::storage::AttachmentReader>,
1275 ) -> Self {
1276 self.inner = self.inner.attachment_reader(v);
1277 self
1278 }
1279
1280 pub fn skill_manager(mut self, v: Arc<bamboo_skills::SkillManager>) -> Self {
1281 self.inner = self.inner.skill_manager(v);
1282 self
1283 }
1284
1285 pub fn project_context_resolver(
1286 mut self,
1287 v: Arc<crate::project_context::ProjectContextResolver>,
1288 ) -> Self {
1289 self.inner = self.inner.project_context_resolver(v);
1290 self
1291 }
1292
1293 pub fn metrics_collector(mut self, v: bamboo_metrics::MetricsCollector) -> Self {
1294 self.inner = self.inner.metrics_collector(v);
1295 self
1296 }
1297
1298 pub fn config(mut self, v: Arc<tokio::sync::RwLock<bamboo_llm::Config>>) -> Self {
1299 self.inner = self.inner.config(v);
1300 self
1301 }
1302
1303 pub fn model_limits_section(
1306 mut self,
1307 section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
1308 ) -> Self {
1309 self.inner = self.inner.model_limits_section(section);
1310 self
1311 }
1312
1313 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
1314 self.inner = self.inner.permission_config(v);
1315 self
1316 }
1317
1318 pub fn permission_mode(mut self, v: bamboo_domain::PermissionMode) -> Self {
1319 self.inner = self.inner.permission_mode(v);
1320 self
1321 }
1322
1323 pub fn provider(mut self, v: Arc<dyn bamboo_llm::LLMProvider>) -> Self {
1324 self.inner = self.inner.provider(v);
1325 self
1326 }
1327
1328 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
1329 self.inner = self.inner.memory_store(v);
1330 self
1331 }
1332
1333 pub fn default_tools(mut self, v: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
1334 self.inner = self.inner.default_tools(v);
1335 self
1336 }
1337
1338 pub fn hook_runner(mut self, v: Arc<crate::runtime::HookRunner>) -> Self {
1340 self.inner = self.inner.hook_runner(v);
1341 self
1342 }
1343
1344 pub fn build(self) -> Result<Agent, &'static str> {
1345 let runtime = self.inner.build()?;
1346 Ok(Agent {
1347 runtime: Arc::new(runtime),
1348 })
1349 }
1350}
1351
1352impl Default for AgentBuilder {
1353 fn default() -> Self {
1354 Self::new()
1355 }
1356}
1357
1358#[cfg(test)]
1359mod auxiliary_factory_isolation_tests {
1360 use super::*;
1361 use bamboo_agent_core::storage::Storage;
1362 use bamboo_agent_core::{Message, ToolSchema};
1363 use bamboo_llm::{LLMError, LLMProvider, LLMStream};
1364
1365 struct UnusedProvider;
1366
1367 #[async_trait::async_trait]
1368 impl LLMProvider for UnusedProvider {
1369 async fn chat_stream(
1370 &self,
1371 _messages: &[Message],
1372 _tools: &[ToolSchema],
1373 _max_output_tokens: Option<u32>,
1374 _model: &str,
1375 ) -> Result<LLMStream, LLMError> {
1376 Err(LLMError::Api("provider must not be called".into()))
1377 }
1378 }
1379
1380 #[tokio::test]
1381 async fn factory_clone_discards_only_its_inherited_route_overrides() {
1382 let dir = tempfile::tempdir().unwrap();
1383 let store = Arc::new(
1384 bamboo_storage::SessionStoreV2::new(dir.path().join("sessions"))
1385 .await
1386 .unwrap(),
1387 );
1388 let storage: Arc<dyn Storage> = store.clone();
1389 let persistence: Arc<dyn RuntimeSessionPersistence> =
1390 Arc::new(bamboo_storage::LockedSessionStore::new(storage.clone()));
1391 let metrics = bamboo_metrics::MetricsCollector::spawn(
1392 Arc::new(bamboo_metrics::SqliteMetricsStorage::new(
1393 dir.path().join("metrics.db"),
1394 )),
1395 7,
1396 );
1397 let ordinary = Agent::builder()
1398 .storage(storage)
1399 .persistence(persistence)
1400 .attachment_reader(store)
1401 .skill_manager(Arc::new(bamboo_skills::SkillManager::new()))
1402 .metrics_collector(metrics)
1403 .config(Arc::new(tokio::sync::RwLock::new(
1404 bamboo_llm::Config::default(),
1405 )))
1406 .provider(Arc::new(UnusedProvider))
1407 .default_tools(Arc::new(bamboo_tools::BuiltinToolExecutor::new()))
1408 .build()
1409 .unwrap();
1410 let background: crate::runtime::config::BackgroundModelResolver = Arc::new(|_| None);
1411 let resume: crate::runtime::config::ResumeAuxiliaryResolver = Arc::new(|| (Ok(None), None));
1412 let bound = ordinary
1413 .with_background_model_resolver(background.clone())
1414 .with_resume_auxiliary_resolver(resume.clone());
1415 let sibling = bound.clone();
1416 let schedule = bound.with_auxiliary_model_resolver_factory(Arc::new(|key| {
1417 let key = key.to_owned();
1418 Arc::new(move || crate::AuxiliaryModelConfig {
1419 fast_model_name: Some(key.clone()),
1420 ..Default::default()
1421 })
1422 }));
1423
1424 assert!(ordinary.runtime.auxiliary_model_resolver_factory.is_none());
1425 assert!(ordinary.runtime.background_model_resolver.is_none());
1426 assert!(ordinary.runtime.resume_auxiliary_resolver.is_none());
1427 for agent in [&bound, &sibling] {
1428 assert!(agent.runtime.auxiliary_model_resolver_factory.is_none());
1429 assert!(Arc::ptr_eq(
1430 agent.runtime.background_model_resolver.as_ref().unwrap(),
1431 &background,
1432 ));
1433 assert!(Arc::ptr_eq(
1434 agent.runtime.resume_auxiliary_resolver.as_ref().unwrap(),
1435 &resume,
1436 ));
1437 }
1438 assert!(schedule.runtime.background_model_resolver.is_none());
1439 assert!(schedule.runtime.resume_auxiliary_resolver.is_none());
1440 let factory = schedule
1441 .runtime
1442 .auxiliary_model_resolver_factory
1443 .as_ref()
1444 .unwrap();
1445 assert_eq!(
1446 factory("run-key")().fast_model_name.as_deref(),
1447 Some("run-key")
1448 );
1449 }
1450}
1451
1452#[cfg(test)]
1453#[path = "child_resume_gold_policy_tests.rs"]
1454mod child_resume_gold_policy_tests;