1use std::sync::Arc;
7
8use bamboo_agent_core::Session;
9
10use crate::runtime::{AgentRuntime, AgentRuntimeBuilder, ExecuteRequest};
11use bamboo_domain::RuntimeSessionPersistence;
12
13#[derive(Clone)]
25pub struct Agent {
26 runtime: Arc<AgentRuntime>,
27}
28
29pub struct DirectExecutionLease {
35 target_session_id: String,
36 router: Option<Arc<crate::session_activation::SessionActivationRouter>>,
37 registration: Option<crate::session_activation::SessionRunRegistration>,
38}
39
40impl DirectExecutionLease {
41 pub async fn abandon(mut self) {
44 if let Some(registration) = self.registration.take() {
45 registration.abandon().await;
46 }
47 }
48}
49
50pub struct SdkInputSubmission {
53 session_id: String,
54 execution_id: String,
55 user: bamboo_agent_core::Message,
56 hook_runner: Arc<crate::runtime::hooks::HookRunner>,
57}
58
59impl SdkInputSubmission {
60 pub fn execution_id(&self) -> &str {
61 &self.execution_id
62 }
63 pub fn user(&self) -> &bamboo_agent_core::Message {
64 &self.user
65 }
66}
67
68impl Agent {
69 pub fn from_runtime(runtime: Arc<AgentRuntime>) -> Self {
71 Agent { runtime }
72 }
73
74 #[doc(hidden)]
77 pub fn with_execution_persistence(
78 &self,
79 persistence: Arc<dyn RuntimeSessionPersistence>,
80 ) -> Self {
81 let mut runtime = (*self.runtime).clone();
82 runtime.persistence = persistence;
83 runtime.inherited_child_wait_captured = true;
84 Self::from_runtime(Arc::new(runtime))
85 }
86
87 #[doc(hidden)]
89 pub fn with_sdk_skill_execution_host(
90 &self,
91 host: Arc<dyn crate::runtime::config::SdkSkillExecutionHost>,
92 ) -> Self {
93 self.with_skill_execution_host(host)
94 }
95
96 #[doc(hidden)]
98 pub fn with_skill_execution_host(
99 &self,
100 host: Arc<dyn crate::runtime::config::SkillExecutionHost>,
101 ) -> Self {
102 let mut runtime = (*self.runtime).clone();
103 runtime.sdk_skill_execution_host = Some(host);
104 Self::from_runtime(Arc::new(runtime))
105 }
106
107 #[doc(hidden)]
110 pub async fn precheck_sdk_user_input(
111 &self,
112 session: &mut Session,
113 user: &bamboo_agent_core::Message,
114 app_data_dir: Option<std::path::PathBuf>,
115 ) -> crate::runtime::runner::Result<SdkInputSubmission> {
116 session
117 .check_execution_compatibility()
118 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
119 if user.role != bamboo_agent_core::Role::User
120 || crate::runtime::config::UntrustedInputObservation::new(&user.id, None).is_none()
121 || session.messages.iter().any(|message| message.id == user.id)
122 {
123 return Err(bamboo_agent_core::AgentError::Tool(
124 "invalid new SDK User".into(),
125 ));
126 }
127 let hook_runner = {
128 let config = self.runtime.config.read().await;
129 Arc::new(
130 self.runtime
131 .hook_runner
132 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir),
133 )
134 };
135 session.metadata.remove("runtime.plugin_prompt_prechecked");
136 let prompt = hook_runner
137 .apply_portable_user_prompt(session, &user.content)
138 .await?;
139 let mut transformed = user.clone();
140 transformed.content = prompt.clone();
141 if let Some(parts) = transformed.content_parts.as_mut() {
142 if let Some(bamboo_domain::MessagePart::Text { text }) =
143 parts.iter_mut().find(|part| matches!(part, bamboo_domain::MessagePart::Text { text } if text == &user.content))
144 {
145 *text = prompt;
146 }
147 }
148 Ok(SdkInputSubmission {
149 session_id: session.id.clone(),
150 execution_id: crate::runtime::runner::round_prelude::new_execution_id(),
151 user: transformed,
152 hook_runner,
153 })
154 }
155
156 #[doc(hidden)]
159 pub fn append_sdk_user_input(
160 &self,
161 session: &mut Session,
162 user: bamboo_agent_core::Message,
163 receipt: Option<SdkInputSubmission>,
164 request: Option<bamboo_domain::SessionSkillRequest>,
165 ) -> crate::runtime::runner::Result<crate::runtime::config::UntrustedExecutionInputs> {
166 session
167 .check_execution_compatibility()
168 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
169 use crate::runtime::config::{
170 sdk_message_digest, sdk_message_header_digest, SdkPendingInput,
171 UntrustedExecutionInputs, UntrustedInputObservation,
172 };
173 let invalid = || bamboo_agent_core::AgentError::Tool("invalid SDK append receipt".into());
174 let header_digest = sdk_message_header_digest(&user);
175 let input_id = user.id.clone();
176 if let Some(receipt) = receipt.as_ref() {
177 if receipt.session_id != session.id
178 || receipt.user.role != bamboo_agent_core::Role::User
179 || user.role != bamboo_agent_core::Role::User
180 || sdk_message_header_digest(&receipt.user) != header_digest
181 || session
182 .messages
183 .iter()
184 .any(|message| message.id == input_id)
185 {
186 return Err(invalid());
187 }
188 }
189 let final_digest = receipt.as_ref().map(|_| sdk_message_digest(&user));
192 if receipt.is_some() {
193 crate::runtime::hooks::HookRunner::mark_user_prompt_prechecked(session, &user.content);
194 }
195 session.add_message(user);
196 let observation =
197 UntrustedInputObservation::new(&input_id, request.as_ref()).ok_or_else(invalid)?;
198 let carrier = UntrustedExecutionInputs::new(vec![observation]).ok_or_else(invalid)?;
199 let (execution_id, hook_runner) = receipt
200 .map(|receipt| (receipt.execution_id, Some(receipt.hook_runner)))
201 .unwrap_or_else(|| {
202 (
203 crate::runtime::runner::round_prelude::new_execution_id(),
204 None,
205 )
206 });
207 Ok(carrier.bind_sdk_append(
208 execution_id,
209 SdkPendingInput {
210 session_id: session.id.clone(),
211 input_id,
212 header_digest,
213 final_digest,
214 },
215 hook_runner,
216 ))
217 }
218
219 pub fn builder() -> AgentBuilder {
221 AgentBuilder::new()
222 }
223
224 pub async fn execute(
226 &self,
227 session: &mut Session,
228 req: ExecuteRequest,
229 ) -> crate::runtime::runner::Result<()> {
230 self.runtime.execute(session, req).await
231 }
232
233 pub async fn execute_with_inputs(
236 &self,
237 session: &mut Session,
238 req: ExecuteRequest,
239 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
240 ) -> crate::runtime::runner::Result<()> {
241 self.runtime.execute_with_inputs(session, req, inputs).await
242 }
243
244 pub async fn execute_direct(
255 &self,
256 session: &mut Session,
257 req: ExecuteRequest,
258 ) -> crate::runtime::runner::Result<()> {
259 session
260 .check_execution_compatibility()
261 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
262 let lease = self.begin_direct_execution(&session.id).await?;
263 self.check_direct_execution_admission(session, &lease)
264 .await?;
265 self.prepare_external_session_for_execution(session).await?;
266 self.execute_direct_registered(session, req, lease).await
267 }
268
269 pub fn check_direct_execution_admission<'a>(
273 &'a self,
274 session: &'a mut Session,
275 lease: &'a DirectExecutionLease,
276 ) -> impl std::future::Future<Output = crate::runtime::runner::Result<()>> + Send + 'a {
277 let matches_target = lease.target_session_id == session.id;
278 let ownership = lease.registration.as_ref().map(|run| run.ownership());
282 async move {
283 if !matches_target {
284 return Err(bamboo_agent_core::AgentError::LLM(
285 "direct execution lease target does not match session".into(),
286 ));
287 }
288 session
289 .check_execution_compatibility()
290 .map_err(|error| bamboo_agent_core::AgentError::Tool(error.to_string()))?;
291 super::counted_loop::admit_registered(
292 session,
293 ownership.as_ref(),
294 self.activation_router(),
295 self.persistence().as_ref(),
296 )
297 .await
298 }
299 }
300
301 pub async fn prepare_external_session_for_execution(
309 &self,
310 session: &mut Session,
311 ) -> crate::runtime::runner::Result<()> {
312 crate::session_app::execution_prep::prepare_external_session_for_execution(
313 session,
314 self.runtime.project_context_resolver.as_deref(),
315 )
316 .await
317 }
318
319 pub async fn prepare_external_project_assignment_read_only(
323 &self,
324 session: &mut Session,
325 ) -> crate::runtime::runner::Result<()> {
326 let resolver = self
327 .runtime
328 .project_context_resolver
329 .as_deref()
330 .ok_or_else(|| {
331 bamboo_agent_core::AgentError::ProjectContext(
332 "Project assignment requires a ProjectContextResolver".to_string(),
333 )
334 })?;
335 resolver
336 .refresh_session_prompt_read_only(session)
337 .await
338 .map(|_| ())
339 .map_err(|error| bamboo_agent_core::AgentError::ProjectContext(error.to_string()))
340 }
341
342 pub async fn begin_direct_execution(
345 &self,
346 target_session_id: &str,
347 ) -> crate::runtime::runner::Result<DirectExecutionLease> {
348 let Some(router) = self.activation_router().cloned() else {
349 return Ok(DirectExecutionLease {
350 target_session_id: target_session_id.to_string(),
351 router: None,
352 registration: None,
353 });
354 };
355 let run_id = format!("sdk-direct-{}", uuid::Uuid::new_v4());
356 let registration = router
357 .register_run(target_session_id, &run_id)
358 .await
359 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
360 Ok(DirectExecutionLease {
361 target_session_id: target_session_id.to_string(),
362 router: Some(router),
363 registration: Some(registration),
364 })
365 }
366
367 pub async fn execute_direct_registered(
370 &self,
371 session: &mut Session,
372 req: ExecuteRequest,
373 lease: DirectExecutionLease,
374 ) -> crate::runtime::runner::Result<()> {
375 self.execute_direct_registered_with_inputs(session, req, lease, None)
376 .await
377 }
378
379 pub async fn execute_direct_registered_with_inputs(
381 &self,
382 session: &mut Session,
383 req: ExecuteRequest,
384 lease: DirectExecutionLease,
385 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
386 ) -> crate::runtime::runner::Result<()> {
387 self.check_direct_execution_admission(session, &lease)
388 .await?;
389 let inherited = self.persistence().inherited_child_wait().or_else(|| {
390 (!self.runtime.inherited_child_wait_captured)
391 .then(|| bamboo_domain::InheritedChildWait::capture(session))
392 .flatten()
393 });
394 let agent = if let Some(inherited) = inherited {
395 inherited
396 .validate_session(session)
397 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
398 if self.persistence().inherited_child_wait().is_some() {
399 self.clone()
400 } else {
401 self.with_execution_persistence(
402 self.persistence()
403 .bind_inherited_child_wait(inherited)
404 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?,
405 )
406 }
407 } else {
408 self.with_execution_persistence(self.persistence().clone())
409 };
410 agent
411 .execute_direct_registered_bound(session, req, lease, inputs)
412 .await
413 }
414
415 async fn execute_direct_registered_bound(
416 &self,
417 session: &mut Session,
418 mut req: ExecuteRequest,
419 mut lease: DirectExecutionLease,
420 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
421 ) -> crate::runtime::runner::Result<()> {
422 if lease.target_session_id != session.id {
423 return Err(bamboo_agent_core::AgentError::LLM(format!(
424 "direct execution lease target {} does not match session {}",
425 lease.target_session_id, session.id
426 )));
427 }
428 let Some(router) = lease.router.take() else {
429 return self.execute_with_inputs(session, req, inputs).await;
430 };
431 let mut registration = lease.registration.take().ok_or_else(|| {
432 bamboo_agent_core::AgentError::LLM(
433 "direct execution lease is missing its router registration".to_string(),
434 )
435 })?;
436 if !router
437 .bind_run_lifetime(®istration.ownership(), session)
438 .await
439 {
440 return Err(bamboo_agent_core::AgentError::Tool(
441 "registered run Session lifetime differs".into(),
442 ));
443 }
444 if let Some(counted) = req.counted_loop.as_mut() {
445 counted.bind_owner(registration.ownership(), session);
446 }
447 if let Some(control) = req.loop_control.as_mut() {
448 control.bind_owner(registration.ownership(), session);
449 control.bind_transport_cleanup(
450 &mut registration,
451 self.storage().clone(),
452 self.session_inbox().cloned(),
453 self.persistence().root_actor_writer(),
454 );
455 }
456 let result = if req.loop_control.is_some() {
457 bamboo_agent_core::tools::observe_tool_suspension(false, async {
458 let token = bamboo_agent_core::tools::capture_suspension_restriction().ok_or_else(
459 || {
460 bamboo_agent_core::AgentError::Tool(
461 "direct loop_control execution is missing its host observation scope"
462 .into(),
463 )
464 },
465 )?;
466 registration.bind_tool_suspension_observation(token);
469 self.execute_with_inputs(session, req, inputs).await
470 })
471 .await
472 .0
473 } else {
474 self.execute_with_inputs(session, req, inputs).await
475 };
476 if registration.observed_forbidden_running() {
477 registration.quarantine().await;
482 return match result {
483 Err(error) => Err(error),
484 Ok(()) => Err(bamboo_agent_core::AgentError::Tool(
485 "suspension_disabled: detached Running outcome; stop unconfirmed".into(),
486 )),
487 };
488 }
489
490 let executed_admitted_generation = session
493 .session_inbox_admission()
494 .map_or(0, |state| state.last_admitted_sequence);
495 registration.begin_finalization().await;
496
497 let legacy_migration = crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
498 session,
499 Some(self.storage()),
500 Some(self.persistence()),
501 self.session_inbox(),
502 )
503 .await;
504 if let Some(generation) = legacy_migration.highest_generation {
505 session.session_inbox_admission_mut().observe(generation);
506 }
507 let pending_generation = session
508 .session_inbox_admission()
509 .and_then(|state| state.pending_activation_generation());
510 if let Some(generation) = pending_generation {
511 let activation_ready = if let Some(inbox) = self.session_inbox() {
512 match inbox
513 .mark_activation_eligible(
514 &session.id,
515 generation,
516 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
517 )
518 .await
519 {
520 Ok(()) => true,
521 Err(error) => {
522 tracing::error!(
523 session_id = %session.id,
524 %error,
525 "failed to persist direct SDK SessionInbox activation watermark"
526 );
527 false
528 }
529 }
530 } else {
531 false
532 };
533 if activation_ready {
534 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
535 router.as_ref(),
536 &session.id,
537 generation,
538 )
539 .await
540 {
541 tracing::error!(
542 session_id = %session.id,
543 %error,
544 "failed to hand direct SDK SessionInbox generation to activation router"
545 );
546 }
547 }
548 }
549
550 if let Err(error) = self.persistence().checkpoint_runtime_session(session).await {
553 tracing::warn!(
554 session_id = %session.id,
555 %error,
556 "failed to checkpoint direct SDK terminal SessionInbox state"
557 );
558 }
559 if let Err(error) = registration.finish(executed_admitted_generation).await {
560 tracing::error!(
561 session_id = %session.id,
562 %error,
563 "direct SDK SessionInbox finalization failed"
564 );
565 }
566
567 result
568 }
569
570 pub fn storage(&self) -> &Arc<dyn bamboo_agent_core::storage::Storage> {
572 &self.runtime.storage
573 }
574
575 pub fn persistence(&self) -> &Arc<dyn RuntimeSessionPersistence> {
577 &self.runtime.persistence
578 }
579
580 pub fn session_inbox(&self) -> Option<&Arc<dyn bamboo_domain::SessionInboxPort>> {
581 self.runtime.session_inbox.as_ref()
582 }
583
584 pub async fn admit_session_inbox_at_safe_boundary(
589 &self,
590 session: &mut bamboo_agent_core::Session,
591 ) -> usize {
592 crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
593 session,
594 Some(self.storage()),
595 Some(self.persistence()),
596 self.session_inbox(),
597 )
598 .await
599 .merged
600 }
601
602 pub async fn admit_session_inbox_at_safe_boundary_checked(
606 &self,
607 session: &mut bamboo_agent_core::Session,
608 ) -> Result<usize, bamboo_agent_core::AgentError> {
609 let refresh = crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
610 session,
611 Some(self.storage()),
612 Some(self.persistence()),
613 self.session_inbox(),
614 )
615 .await;
616 if let Some(error) = refresh.admission_error {
617 return Err(bamboo_agent_core::AgentError::Tool(error));
618 }
619 Ok(refresh.merged)
620 }
621
622 pub fn activation_router(
623 &self,
624 ) -> Option<&Arc<crate::session_activation::SessionActivationRouter>> {
625 self.runtime.activation_router.as_ref()
626 }
627
628 pub fn session_messenger(&self) -> Option<&Arc<crate::SessionMessenger>> {
629 self.runtime.session_messenger.as_ref()
630 }
631
632 pub fn default_tools(&self) -> &Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
642 &self.runtime.default_tools
643 }
644}
645
646pub struct AgentBuilder {
654 inner: AgentRuntimeBuilder,
655}
656
657impl AgentBuilder {
658 pub fn new() -> Self {
659 Self {
660 inner: AgentRuntimeBuilder::new(),
661 }
662 }
663
664 pub fn storage(mut self, v: Arc<dyn bamboo_agent_core::storage::Storage>) -> Self {
665 self.inner = self.inner.storage(v);
666 self
667 }
668
669 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
670 self.inner = self.inner.persistence(v);
671 self
672 }
673
674 pub fn session_inbox(mut self, v: Arc<dyn bamboo_domain::SessionInboxPort>) -> Self {
675 self.inner = self.inner.session_inbox(v);
676 self
677 }
678
679 pub fn activation_router(
680 mut self,
681 v: Arc<crate::session_activation::SessionActivationRouter>,
682 ) -> Self {
683 self.inner = self.inner.activation_router(v);
684 self
685 }
686
687 pub fn session_messenger(mut self, v: Arc<crate::SessionMessenger>) -> Self {
688 self.inner = self.inner.session_messenger(v);
689 self
690 }
691
692 pub fn attachment_reader(
693 mut self,
694 v: Arc<dyn bamboo_agent_core::storage::AttachmentReader>,
695 ) -> Self {
696 self.inner = self.inner.attachment_reader(v);
697 self
698 }
699
700 pub fn skill_manager(mut self, v: Arc<bamboo_skills::SkillManager>) -> Self {
701 self.inner = self.inner.skill_manager(v);
702 self
703 }
704
705 pub fn project_context_resolver(
706 mut self,
707 v: Arc<crate::project_context::ProjectContextResolver>,
708 ) -> Self {
709 self.inner = self.inner.project_context_resolver(v);
710 self
711 }
712
713 pub fn metrics_collector(mut self, v: bamboo_metrics::MetricsCollector) -> Self {
714 self.inner = self.inner.metrics_collector(v);
715 self
716 }
717
718 pub fn config(mut self, v: Arc<tokio::sync::RwLock<bamboo_llm::Config>>) -> Self {
719 self.inner = self.inner.config(v);
720 self
721 }
722
723 pub fn model_limits_section(
726 mut self,
727 section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
728 ) -> Self {
729 self.inner = self.inner.model_limits_section(section);
730 self
731 }
732
733 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
734 self.inner = self.inner.permission_config(v);
735 self
736 }
737
738 pub fn permission_mode(mut self, v: bamboo_domain::PermissionMode) -> Self {
739 self.inner = self.inner.permission_mode(v);
740 self
741 }
742
743 pub fn provider(mut self, v: Arc<dyn bamboo_llm::LLMProvider>) -> Self {
744 self.inner = self.inner.provider(v);
745 self
746 }
747
748 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
749 self.inner = self.inner.memory_store(v);
750 self
751 }
752
753 pub fn default_tools(mut self, v: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
754 self.inner = self.inner.default_tools(v);
755 self
756 }
757
758 pub fn hook_runner(mut self, v: Arc<crate::runtime::HookRunner>) -> Self {
760 self.inner = self.inner.hook_runner(v);
761 self
762 }
763
764 pub fn build(self) -> Result<Agent, &'static str> {
765 let runtime = self.inner.build()?;
766 Ok(Agent {
767 runtime: Arc::new(runtime),
768 })
769 }
770}
771
772impl Default for AgentBuilder {
773 fn default() -> Self {
774 Self::new()
775 }
776}