1use std::collections::{BTreeSet, HashMap};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use tokio::sync::{broadcast, mpsc, RwLock};
12use tokio_util::sync::CancellationToken;
13use tracing::Instrument;
14
15use bamboo_agent_core::tools::ToolExecutor;
16use bamboo_agent_core::{AgentError, AgentEvent, Session};
17use bamboo_domain::ReasoningEffort;
18use bamboo_llm::LLMProvider;
19
20use crate::runtime::config::{
21 AuxiliaryModelConfig, BashCompletionSink, BashResumeHook, DisabledFilterResolver, GoldConfig,
22 GuardianConfig, GuardianSpawner, ImageFallbackConfig,
23};
24use crate::runtime::execution::child_completion::ChildCompletion;
25use crate::runtime::execution::event_forwarder::HistoryCommitBarrier;
26use crate::runtime::execution::runner_lifecycle::{
27 finalize_rejected_runner_if_distinct, finalize_runner_exact, reserve_runner_core,
28 reserve_runner_for_registration, ReserveOutcome, RunnerReservation,
29};
30use crate::runtime::execution::runner_state::AgentRunner;
31use crate::runtime::model_roster::ModelRoster;
32use crate::runtime::Agent;
33use crate::runtime::{ExecuteRequest, ExecuteRequestBuilder};
34use crate::session_activation::{
35 SessionActivationRouter, SessionRunRegistration, SessionRunRegistrationError,
36};
37
38pub use crate::session_cache::SessionCache;
39
40enum SessionExecutionActivationOwnership {
41 Unrouted,
43 UnpublishedActivation(Arc<SessionActivationRouter>),
46 RegistrationPending(Arc<SessionActivationRouter>),
50 Registered(Box<SessionRunRegistration>),
53}
54
55pub struct SessionExecutionReservation {
61 session_id: String,
62 run_id: String,
63 cancel_token: CancellationToken,
64 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
65 activation: SessionExecutionActivationOwnership,
66 root_actor: Option<Box<RootActorExecution>>,
67 physical_admission: Option<super::root_execution_lifetime::RootRunAdmission>,
68 execution_persistence: Option<Arc<dyn bamboo_domain::RuntimeSessionPersistence>>,
69 persistence_bound: bool,
70 selected_input: bool,
71 armed: bool,
72}
73
74impl SessionExecutionReservation {
75 pub fn session_id(&self) -> &str {
76 &self.session_id
77 }
78
79 pub fn run_id(&self) -> &str {
80 &self.run_id
81 }
82
83 pub fn cancel_token(&self) -> &CancellationToken {
84 &self.cancel_token
85 }
86
87 pub async fn bind_root_actor(
90 &mut self,
91 agent: &Agent,
92 session: &Session,
93 ) -> std::io::Result<()> {
94 if self.root_actor.is_none() {
95 if self.session_id != session.id {
96 return Err(std::io::Error::new(
97 std::io::ErrorKind::InvalidInput,
98 "Root binding target mismatch",
99 ));
100 }
101 if let Some(binding) = agent
102 .persistence()
103 .bind_root_actor_execution(session, &self.run_id)
104 .await?
105 {
106 self.root_actor = Some(Box::new(RootActorExecution::start(binding)));
107 }
108 }
109 self.bind_execution_persistence(agent, session)
110 }
111
112 fn bind_execution_persistence(
113 &mut self,
114 agent: &Agent,
115 session: &Session,
116 ) -> std::io::Result<()> {
117 if self.session_id != session.id {
118 return Err(std::io::Error::other(
119 "execution persistence target mismatch",
120 ));
121 }
122 if self.persistence_bound {
123 return Ok(());
124 }
125 let persistence = self
126 .root_actor
127 .as_ref()
128 .map_or_else(|| agent.persistence().clone(), |owner| owner.persistence());
129 if let Some(inherited) = persistence
130 .inherited_child_wait()
131 .or_else(|| bamboo_domain::InheritedChildWait::capture(session))
132 {
133 inherited.validate_session(session)?;
134 self.execution_persistence = Some(persistence.bind_inherited_child_wait(inherited)?);
135 } else if self.root_actor.is_some() {
136 self.execution_persistence = Some(persistence);
137 }
138 self.persistence_bound = true;
139 Ok(())
140 }
141
142 pub fn execution_persistence(
143 &self,
144 ) -> Option<Arc<dyn bamboo_domain::RuntimeSessionPersistence>> {
145 self.execution_persistence.clone()
146 }
147
148 pub fn bind_root_inbox_receipt(
151 &mut self,
152 receipt: bamboo_domain::SessionInboxReceipt,
153 ) -> std::io::Result<()> {
154 if !self.selected_input || self.root_actor.is_none() || self.cancel_token.is_cancelled() {
155 return Err(std::io::Error::other(
156 "Root receipt requires a live bound reservation",
157 ));
158 }
159 let persistence = self
160 .execution_persistence
161 .as_ref()
162 .ok_or_else(|| std::io::Error::other("Root execution persistence unavailable"))?;
163 self.execution_persistence = Some(persistence.bind_root_inbox_receipt(receipt)?);
164 Ok(())
165 }
166
167 pub async fn retain_selected_run_output(&self) -> std::io::Result<bool> {
170 if !self.selected_input {
171 return Ok(false);
172 }
173 let owner = self
174 .root_actor_writer()
175 .ok_or_else(|| std::io::Error::other("Selected output requires its Root writer"))?;
176 let receipt = self
177 .execution_persistence
178 .as_ref()
179 .and_then(|p| p.root_inbox_receipt())
180 .cloned()
181 .ok_or_else(|| std::io::Error::other("Selected output requires its bound receipt"))?;
182 if owner.fence.actor_id != self.session_id
183 || owner.fence.run_id != self.run_id
184 || self.cancel_token.is_cancelled()
185 {
186 return Err(std::io::Error::other(
187 "Selected output reservation is not live",
188 ));
189 }
190 let runners = self.runners.read().await;
191 let runner = runners
192 .get(&self.session_id)
193 .filter(|r| r.run_id == self.run_id && !r.cancel_token.is_cancelled())
194 .ok_or_else(|| std::io::Error::other("Selected output runner is unavailable"))?;
195 runner
196 .visible_messages
197 .retain_selected_output(&owner, receipt)?;
198 Ok(true)
199 }
200
201 pub fn root_actor_writer(&self) -> Option<bamboo_domain::RootActorRuntimeWrite> {
202 self.root_actor
203 .as_ref()
204 .map(|owner| owner.binding.owner.clone())
205 }
206
207 pub fn queue_root_response_event(&mut self, event: AgentEvent) -> Option<AgentEvent> {
210 let Some(owner) = self.root_actor.as_mut() else {
211 return Some(event);
212 };
213 owner.response_events.push(event);
214 None
215 }
216
217 pub fn take_root_response_events(&mut self) -> Vec<AgentEvent> {
218 self.root_actor
219 .as_mut()
220 .map(|owner| std::mem::take(&mut owner.response_events))
221 .unwrap_or_default()
222 }
223
224 pub async fn finish_root_actor(&mut self, outcome: bamboo_domain::ActorActivationFinish) {
227 if let Some(owner) = self.root_actor.take() {
228 (*owner).finish(outcome).await;
229 }
230 }
231
232 pub(crate) fn from_activation_placeholder(
238 session_id: impl Into<String>,
239 reservation: RunnerReservation,
240 router: Arc<SessionActivationRouter>,
241 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
242 ) -> Self {
243 Self {
244 session_id: session_id.into(),
245 run_id: reservation.run_id,
246 cancel_token: reservation.cancel_token,
247 runners,
248 activation: SessionExecutionActivationOwnership::UnpublishedActivation(router),
249 root_actor: None,
250 physical_admission: reservation.physical_admission,
251 execution_persistence: None,
252 persistence_bound: false,
253 selected_input: false,
254 armed: true,
255 }
256 }
257
258 pub(crate) fn from_pending_registration(
262 session_id: impl Into<String>,
263 reservation: RunnerReservation,
264 router: Option<Arc<SessionActivationRouter>>,
265 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
266 ) -> Self {
267 let activation = match router {
268 Some(router) => SessionExecutionActivationOwnership::RegistrationPending(router),
269 None => SessionExecutionActivationOwnership::Unrouted,
270 };
271 Self {
272 session_id: session_id.into(),
273 run_id: reservation.run_id,
274 cancel_token: reservation.cancel_token,
275 runners,
276 activation,
277 root_actor: None,
278 physical_admission: reservation.physical_admission,
279 execution_persistence: None,
280 persistence_bound: false,
281 selected_input: false,
282 armed: true,
283 }
284 }
285
286 pub(crate) fn mark_activation_published(&mut self) {
289 let activation = std::mem::replace(
290 &mut self.activation,
291 SessionExecutionActivationOwnership::Unrouted,
292 );
293 self.activation = match activation {
294 SessionExecutionActivationOwnership::UnpublishedActivation(router) => {
295 SessionExecutionActivationOwnership::RegistrationPending(router)
296 }
297 other => other,
298 };
299 }
300
301 pub(crate) async fn rollback_unpublished_activation(mut self) {
306 self.armed = false;
307 self.cancel_token.cancel();
308 self.finish_root_actor(bamboo_domain::ActorActivationFinish::Cancelled)
309 .await;
310 let activation = std::mem::replace(
311 &mut self.activation,
312 SessionExecutionActivationOwnership::Unrouted,
313 );
314 debug_assert!(matches!(
315 activation,
316 SessionExecutionActivationOwnership::UnpublishedActivation(_)
317 ));
318 remove_runner_exact(&self.runners, &self.session_id, &self.run_id).await;
319 }
320
321 pub async fn ensure_registered(&mut self) -> Result<(), SessionRunRegistrationError> {
327 let router = match &self.activation {
328 SessionExecutionActivationOwnership::RegistrationPending(router) => router.clone(),
329 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
330 unreachable!("unpublished activation entered an execution adapter")
331 }
332 SessionExecutionActivationOwnership::Unrouted
333 | SessionExecutionActivationOwnership::Registered(_) => return Ok(()),
334 };
335 match register_reserved_activation(
336 router,
337 &self.runners,
338 &self.session_id,
339 &self.run_id,
340 self.selected_input,
341 )
342 .await
343 {
344 Ok(registration) => {
345 self.activation =
346 SessionExecutionActivationOwnership::Registered(Box::new(registration));
347 Ok(())
348 }
349 Err(error) => {
350 self.disarm_after_registration_rejection(error.existing_run_id());
355 Err(error)
356 }
357 }
358 }
359
360 pub async fn abandon(mut self) {
364 self.armed = false;
365 self.cancel_token.cancel();
366 if let Some(owner) = self.root_actor.take() {
367 (*owner)
368 .finish(bamboo_domain::ActorActivationFinish::Cancelled)
369 .await;
370 }
371 let activation = std::mem::replace(
372 &mut self.activation,
373 SessionExecutionActivationOwnership::Unrouted,
374 );
375 let runners = self.runners.clone();
376 let session_id = self.session_id.clone();
377 let run_id = self.run_id.clone();
378 let cleanup_session_id = session_id.clone();
379 let cleanup_run_id = run_id.clone();
380 let selected_input = self.selected_input;
381 let cleanup = tokio::spawn(async move {
382 cleanup_execution_reservation(
383 activation,
384 runners,
385 cleanup_session_id,
386 cleanup_run_id,
387 selected_input,
388 )
389 .await;
390 });
391 if let Err(error) = cleanup.await {
392 tracing::error!(
393 %session_id,
394 %run_id,
395 %error,
396 "detached execution-reservation cleanup failed"
397 );
398 }
399 }
400
401 fn disarm_after_registration_rejection(&mut self, existing_run_id: &str) {
402 if existing_run_id != self.run_id {
403 self.cancel_token.cancel();
404 }
405 self.armed = false;
406 }
407
408 pub(crate) fn matches_execution_target(
409 &self,
410 session_id: &str,
411 domain_session_id: &str,
412 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
413 ) -> bool {
414 self.session_id == session_id
415 && domain_session_id == session_id
416 && Arc::ptr_eq(&self.runners, runners)
417 }
418
419 pub(crate) fn disarm_for_execution(
420 &mut self,
421 ) -> (CancellationToken, Option<SessionRunRegistration>) {
422 self.armed = false;
423 let registration = match std::mem::replace(
424 &mut self.activation,
425 SessionExecutionActivationOwnership::Unrouted,
426 ) {
427 SessionExecutionActivationOwnership::Registered(registration) => Some(*registration),
428 SessionExecutionActivationOwnership::Unrouted => None,
429 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
430 unreachable!("unpublished activation cannot transfer to execution")
431 }
432 SessionExecutionActivationOwnership::RegistrationPending(_) => {
433 unreachable!("ensure_registered adopts every pending router registration")
434 }
435 };
436 (self.cancel_token.clone(), registration)
437 }
438}
439
440impl Drop for SessionExecutionReservation {
441 fn drop(&mut self) {
442 if !self.armed {
443 return;
444 }
445 self.armed = false;
446 self.cancel_token.cancel();
447 let activation = std::mem::replace(
448 &mut self.activation,
449 SessionExecutionActivationOwnership::Unrouted,
450 );
451 let runners = self.runners.clone();
452 let session_id = self.session_id.clone();
453 let run_id = self.run_id.clone();
454 let selected_input = self.selected_input;
455 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
456 runtime.spawn(async move {
457 cleanup_execution_reservation(
458 activation,
459 runners,
460 session_id,
461 run_id,
462 selected_input,
463 )
464 .await;
465 });
466 }
467 }
468}
469
470pub struct SessionPreparationReservation {
473 reservation: Option<SessionExecutionReservation>,
474}
475
476impl SessionPreparationReservation {
477 pub async fn bind_root_actor(
478 &mut self,
479 agent: &Agent,
480 session: &Session,
481 ) -> std::io::Result<()> {
482 self.reservation
483 .as_mut()
484 .expect("live preparation")
485 .bind_root_actor(agent, session)
486 .await
487 }
488
489 pub fn root_actor_writer(&self) -> Option<bamboo_domain::RootActorRuntimeWrite> {
490 self.reservation
491 .as_ref()
492 .and_then(SessionExecutionReservation::root_actor_writer)
493 }
494
495 pub async fn finish(mut self, outcome: bamboo_domain::ActorActivationFinish) {
496 let reservation = self.reservation.take().expect("live preparation");
497 let _ = Self::release(reservation, outcome).await;
498 }
499
500 pub async fn abandon(self) {
501 self.finish(bamboo_domain::ActorActivationFinish::Cancelled)
502 .await;
503 }
504
505 fn release(
506 mut reservation: SessionExecutionReservation,
507 outcome: bamboo_domain::ActorActivationFinish,
508 ) -> tokio::task::JoinHandle<()> {
509 tokio::spawn(async move {
512 reservation.finish_root_actor(outcome).await;
513 reservation.abandon().await;
514 })
515 }
516}
517
518impl Drop for SessionPreparationReservation {
519 fn drop(&mut self) {
520 if let Some(reservation) = self.reservation.take() {
521 if tokio::runtime::Handle::try_current().is_ok() {
522 drop(Self::release(
523 reservation,
524 bamboo_domain::ActorActivationFinish::Cancelled,
525 ));
526 }
527 }
528 }
529}
530
531pub async fn reserve_session_preparation(
534 agent: &Arc<Agent>,
535 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
536 session_id: &str,
537) -> std::io::Result<SessionPreparationReservation> {
538 let router = agent.activation_router().cloned().ok_or_else(|| {
539 std::io::Error::new(
540 std::io::ErrorKind::Unsupported,
541 "Preparation requires its activation router",
542 )
543 })?;
544 let busy = || {
545 std::io::Error::new(
546 std::io::ErrorKind::WouldBlock,
547 "Session has an active runner or logical owner",
548 )
549 };
550 if runners.read().await.get(session_id).is_some_and(|runner| {
551 runner.root_reset_admission_reserved()
552 || matches!(
553 runner.status,
554 super::runner_state::AgentStatus::Pending
555 | super::runner_state::AgentStatus::Running
556 )
557 }) {
558 return Err(busy());
559 }
560 let reservation = reserve_logical_session(router, runners, session_id, true)
563 .await
564 .map_err(|_| busy())?;
565 let preparation = SessionPreparationReservation {
566 reservation: Some(reservation),
567 };
568 if runners.read().await.get(session_id).is_some_and(|runner| {
569 runner.root_reset_admission_reserved()
570 || matches!(
571 runner.status,
572 super::runner_state::AgentStatus::Pending
573 | super::runner_state::AgentStatus::Running
574 )
575 }) {
576 preparation.abandon().await;
577 return Err(busy());
578 }
579 Ok(preparation)
580}
581
582async fn reserve_logical_session(
583 router: Arc<SessionActivationRouter>,
584 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
585 session_id: &str,
586 selected_input: bool,
587) -> Result<SessionExecutionReservation, SessionRunRegistrationError> {
588 let mut reservation = SessionExecutionReservation {
589 session_id: session_id.to_owned(),
590 run_id: uuid::Uuid::new_v4().to_string(),
591 cancel_token: CancellationToken::new(),
592 runners: runners.clone(),
593 activation: SessionExecutionActivationOwnership::Unrouted,
594 root_actor: None,
595 physical_admission: None,
596 execution_persistence: None,
597 persistence_bound: false,
598 selected_input: false,
599 armed: true,
600 };
601 let registration = register_reserved_activation(
602 router,
603 runners,
604 session_id,
605 &reservation.run_id,
606 selected_input,
607 )
608 .await?;
609 reservation.activation =
610 SessionExecutionActivationOwnership::Registered(Box::new(registration));
611 Ok(reservation)
612}
613
614pub enum SessionExecutionReserveOutcome {
616 Reserved(SessionExecutionReservation),
617 AlreadyRunning { run_id: String },
618}
619
620async fn register_reserved_activation(
621 router: Arc<SessionActivationRouter>,
622 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
623 session_id: &str,
624 run_id: &str,
625 selected_input: bool,
626) -> Result<SessionRunRegistration, SessionRunRegistrationError> {
627 let mut registration = match router.register_run(session_id, run_id).await {
628 Ok(registration) => registration,
629 Err(error) => {
630 let collision = Err(AgentError::LLM(error.to_string()));
631 finalize_rejected_runner_if_distinct(
632 runners,
633 session_id,
634 error.existing_run_id(),
635 run_id,
636 &collision,
637 )
638 .await;
639 return Err(error);
640 }
641 };
642 if selected_input {
643 registration.restrict_to_selected_input();
644 }
645 let cleanup_runners = runners.clone();
646 let cleanup_session_id = session_id.to_string();
647 let cleanup_run_id = run_id.to_string();
648 registration.set_abort_cleanup(move || async move {
649 let abandoned = Err(AgentError::Cancelled);
650 finalize_runner_exact(
651 &cleanup_runners,
652 &cleanup_session_id,
653 &cleanup_run_id,
654 &abandoned,
655 )
656 .await;
657 });
658 Ok(registration)
659}
660
661async fn cleanup_execution_reservation(
662 activation: SessionExecutionActivationOwnership,
663 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
664 session_id: String,
665 run_id: String,
666 selected_input: bool,
667) {
668 match activation {
669 SessionExecutionActivationOwnership::Registered(registration) => {
670 (*registration).abandon().await;
671 }
672 SessionExecutionActivationOwnership::RegistrationPending(router) => {
673 match register_reserved_activation(
674 router,
675 &runners,
676 &session_id,
677 &run_id,
678 selected_input,
679 )
680 .await
681 {
682 Ok(registration) => registration.abandon().await,
683 Err(error) => {
684 tracing::debug!(
685 %session_id,
686 %run_id,
687 existing_run_id = %error.existing_run_id(),
688 "abandoned activation placeholder was already adopted or superseded"
689 );
690 }
691 }
692 }
693 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
694 remove_runner_exact(&runners, &session_id, &run_id).await;
695 }
696 SessionExecutionActivationOwnership::Unrouted => {
697 let abandoned = Err(AgentError::Cancelled);
698 finalize_runner_exact(&runners, &session_id, &run_id, &abandoned).await;
699 }
700 }
701}
702
703async fn remove_runner_exact(
704 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
705 session_id: &str,
706 run_id: &str,
707) -> bool {
708 let mut runners = runners.write().await;
709 if runners
710 .get(session_id)
711 .is_some_and(|runner| runner.run_id == run_id)
712 {
713 super::runner_lifecycle::remove_runner_entry(&mut runners, session_id).await;
714 true
715 } else {
716 false
717 }
718}
719
720pub async fn reserve_session_execution(
728 agent: &Arc<Agent>,
729 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
730 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
731 session_id: &str,
732 event_sender: &broadcast::Sender<AgentEvent>,
733) -> SessionExecutionReserveOutcome {
734 reserve_session_execution_inner(agent, runners, senders, session_id, event_sender, false).await
735}
736
737pub async fn reserve_selected_input_execution(
740 agent: &Arc<Agent>,
741 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
742 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
743 session_id: &str,
744 event_sender: &broadcast::Sender<AgentEvent>,
745) -> SessionExecutionReserveOutcome {
746 reserve_session_execution_inner(agent, runners, senders, session_id, event_sender, true).await
747}
748
749async fn reserve_session_execution_inner(
750 agent: &Arc<Agent>,
751 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
752 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
753 session_id: &str,
754 event_sender: &broadcast::Sender<AgentEvent>,
755 selected_input: bool,
756) -> SessionExecutionReserveOutcome {
757 if let Some(router) = agent.activation_router().cloned() {
758 if let Some(run_id) = runners
759 .read()
760 .await
761 .get(session_id)
762 .filter(|runner| {
763 runner.root_reset_admission_reserved()
764 || matches!(runner.status, super::runner_state::AgentStatus::Running)
765 })
766 .map(|runner| runner.run_id.clone())
767 {
768 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
769 }
770 let mut reservation =
773 match reserve_logical_session(router, runners, session_id, selected_input).await {
774 Ok(reservation) => reservation,
775 Err(error) => {
776 return SessionExecutionReserveOutcome::AlreadyRunning {
777 run_id: error.existing_run_id().to_owned(),
778 }
779 }
780 };
781 let identity = RunnerReservation {
782 run_id: reservation.run_id.clone(),
783 cancel_token: reservation.cancel_token.clone(),
784 physical_admission: None,
785 };
786 match reserve_runner_for_registration(runners, senders, session_id, event_sender, identity)
787 .await
788 {
789 ReserveOutcome::Reserved(raw) => {
790 reservation.physical_admission = raw.physical_admission;
791 reservation.selected_input = selected_input;
792 return SessionExecutionReserveOutcome::Reserved(reservation);
793 }
794 ReserveOutcome::AlreadyRunning(run_id) => {
795 reservation.abandon().await;
796 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
797 }
798 }
799 }
800
801 let reservation = match reserve_runner_core(runners, senders, session_id, event_sender).await {
802 ReserveOutcome::AlreadyRunning(run_id) => {
803 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
804 }
805 ReserveOutcome::Reserved(reservation) => reservation,
806 };
807
808 let execution_reservation = SessionExecutionReservation {
809 session_id: session_id.to_string(),
810 run_id: reservation.run_id,
811 cancel_token: reservation.cancel_token,
812 runners: runners.clone(),
813 activation: SessionExecutionActivationOwnership::Unrouted,
814 root_actor: None,
815 physical_admission: reservation.physical_admission,
816 execution_persistence: None,
817 persistence_bound: false,
818 selected_input,
819 armed: true,
820 };
821 SessionExecutionReserveOutcome::Reserved(execution_reservation)
822}
823
824pub fn read_cached_session(cache: &SessionCache, id: &str) -> Option<bamboo_agent_core::Session> {
832 cache
833 .get(id)
834 .map(|e| e.value().clone())
835 .map(|a| a.read().clone())
836}
837
838const SKILL_CONTEXT_START_MARKER: &str = "<!-- BAMBOO_SKILL_CONTEXT_START -->";
839const TOOL_GUIDE_START_MARKER: &str = "<!-- BAMBOO_TOOL_GUIDE_START -->";
840const EXTERNAL_MEMORY_START_MARKER: &str = "<!-- BAMBOO_EXTERNAL_MEMORY_START -->";
841const TASK_LIST_START_MARKER: &str = "<!-- BAMBOO_TASK_LIST_START -->";
842
843pub struct SessionExecutionOutcome {
850 pub success: bool,
852 pub cancelled: bool,
854 pub error: Option<String>,
856}
857
858impl SessionExecutionOutcome {
859 fn from_result(result: &Result<(), AgentError>) -> Self {
860 match result {
861 Ok(()) => Self {
862 success: true,
863 cancelled: false,
864 error: None,
865 },
866 Err(error) => Self {
867 success: false,
868 cancelled: error.is_cancelled(),
869 error: Some(error.to_string()),
870 },
871 }
872 }
873}
874
875pub type SessionCompletionHook = Box<
885 dyn for<'a> FnOnce(
886 SessionExecutionOutcome,
887 &'a mut Session,
888 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
889 + Send,
890>;
891
892pub struct SessionExecutionArgs {
898 pub agent: Arc<Agent>,
900 pub session_id: String,
901 pub session: Session,
902 pub execution_reservation: SessionExecutionReservation,
905
906 pub tools_override: Option<Arc<dyn ToolExecutor>>,
908 pub provider_override: Option<Arc<dyn LLMProvider>>,
909 pub model_roster: ModelRoster,
913 pub reasoning_effort: Option<ReasoningEffort>,
914 pub reasoning_effort_source: String,
915 pub auxiliary_model_resolver:
916 Option<Arc<dyn Fn() -> crate::runtime::config::AuxiliaryModelConfig + Send + Sync>>,
917 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
921 pub disabled_tools: Option<BTreeSet<String>>,
922 pub disabled_skill_ids: Option<BTreeSet<String>>,
923 pub selected_skill_ids: Option<Vec<String>>,
924 pub selected_skill_mode: Option<String>,
925 pub mpsc_tx: mpsc::Sender<AgentEvent>,
926 pub history_commit_barrier: HistoryCommitBarrier,
929 pub image_fallback: Option<ImageFallbackConfig>,
930 pub gold_config: Option<GoldConfig>,
931 pub guardian_config: Option<GuardianConfig>,
933 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
936 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
938 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
940 pub app_data_dir: Option<std::path::PathBuf>,
941 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
944
945 pub runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
947 pub sessions_cache: SessionCache,
948
949 pub on_complete: Option<SessionCompletionHook>,
952
953 pub child_completion_handler: Option<Arc<dyn super::ChildCompletionHandler>>,
965}
966
967struct ExecuteRequestParams {
977 tools: Option<Arc<dyn ToolExecutor>>,
978 provider_override: Option<Arc<dyn LLMProvider>>,
979 model_roster: ModelRoster,
980 reasoning_effort: Option<ReasoningEffort>,
981 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
982 disabled_filter_resolver: Option<DisabledFilterResolver>,
983 disabled_tools: Option<BTreeSet<String>>,
984 disabled_skill_ids: Option<BTreeSet<String>>,
985 selected_skill_ids: Option<Vec<String>>,
986 selected_skill_mode: Option<String>,
987 image_fallback: Option<ImageFallbackConfig>,
988 gold_config: Option<GoldConfig>,
989 guardian_config: Option<GuardianConfig>,
990 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
991 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
992 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
993 app_data_dir: Option<std::path::PathBuf>,
994 run_budget: Option<bamboo_config::RunBudgetConfig>,
995}
996
997fn build_execute_request(
1004 initial_message: String,
1005 event_tx: mpsc::Sender<AgentEvent>,
1006 cancel_token: CancellationToken,
1007 params: ExecuteRequestParams,
1008) -> ExecuteRequest {
1009 let ExecuteRequestParams {
1010 tools,
1011 provider_override,
1012 model_roster,
1013 reasoning_effort,
1014 auxiliary_model_resolver,
1015 disabled_filter_resolver,
1016 disabled_tools,
1017 disabled_skill_ids,
1018 selected_skill_ids,
1019 selected_skill_mode,
1020 image_fallback,
1021 gold_config,
1022 guardian_config,
1023 guardian_spawner,
1024 bash_resume_hook,
1025 bash_completion_sink,
1026 app_data_dir,
1027 run_budget,
1028 } = params;
1029
1030 let mut builder = ExecuteRequestBuilder::new(initial_message, event_tx, cancel_token)
1031 .model_roster(model_roster)
1032 .gold_config(gold_config)
1033 .guardian_config(guardian_config)
1034 .guardian_spawner(guardian_spawner)
1035 .bash_resume_hook(bash_resume_hook)
1036 .bash_completion_sink(bash_completion_sink);
1037
1038 if let Some(run_budget) = run_budget {
1039 builder = builder.run_budget(run_budget);
1040 }
1041
1042 if let Some(tools) = tools {
1043 builder = builder.tools(tools);
1044 }
1045 if let Some(provider_override) = provider_override {
1046 builder = builder.provider_override(provider_override);
1047 }
1048 if let Some(reasoning_effort) = reasoning_effort {
1049 builder = builder.reasoning_effort(reasoning_effort);
1050 }
1051 if let Some(disabled_filter_resolver) = disabled_filter_resolver {
1052 builder = builder.disabled_filter_resolver(disabled_filter_resolver);
1053 }
1054 if let Some(auxiliary_model_resolver) = auxiliary_model_resolver {
1055 builder = builder.auxiliary_model_resolver(auxiliary_model_resolver);
1056 }
1057 if let Some(disabled_tools) = disabled_tools {
1058 builder = builder.disabled_tools(disabled_tools);
1059 }
1060 if let Some(disabled_skill_ids) = disabled_skill_ids {
1061 builder = builder.disabled_skill_ids(disabled_skill_ids);
1062 }
1063 if let Some(selected_skill_ids) = selected_skill_ids {
1064 builder = builder.selected_skill_ids(selected_skill_ids);
1065 }
1066 if let Some(selected_skill_mode) = selected_skill_mode {
1067 builder = builder.selected_skill_mode(selected_skill_mode);
1068 }
1069 if let Some(image_fallback) = image_fallback {
1070 builder = builder.image_fallback(image_fallback);
1071 }
1072 if let Some(app_data_dir) = app_data_dir {
1073 builder = builder.app_data_dir(app_data_dir);
1074 }
1075
1076 builder.build()
1077}
1078
1079pub fn spawn_session_execution(args: SessionExecutionArgs) {
1088 spawn_session_execution_with_inputs(args, None);
1089}
1090
1091pub fn spawn_session_execution_with_inputs(
1094 args: SessionExecutionArgs,
1095 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1096) {
1097 spawn_session_execution_with_upstream_retry(args, inputs, Default::default());
1098}
1099
1100pub fn spawn_session_execution_with_upstream_retry(
1103 args: SessionExecutionArgs,
1104 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1105 upstream_retry: bamboo_llm::retry::UpstreamRetry,
1106) {
1107 let span_session_id = args.session_id.clone();
1108 let session_span = tracing::info_span!("agent_execution", session_id = %span_session_id);
1109 let is_root = args.session.kind == bamboo_domain::SessionKind::Root;
1110 let outer = if is_root {
1111 match args
1112 .execution_reservation
1113 .physical_admission
1114 .as_ref()
1115 .map(|admission| admission.attach(&args.session))
1116 .transpose()
1117 {
1118 Ok(outer) => outer,
1119 Err(error) => {
1120 tokio::spawn(async move {
1123 let _ = args
1124 .mpsc_tx
1125 .send(AgentEvent::Error {
1126 message: error.to_string(),
1127 })
1128 .await;
1129 args.execution_reservation.abandon().await;
1130 });
1131 return;
1132 }
1133 }
1134 } else {
1135 None
1136 };
1137 let task_outer = outer.clone();
1138
1139 let task = tokio::spawn(
1140 async move {
1141 let SessionExecutionArgs {
1142 agent,
1143 session_id,
1144 mut session,
1145 mut execution_reservation,
1146 tools_override,
1147 provider_override,
1148 model_roster,
1149 reasoning_effort,
1150 reasoning_effort_source,
1151 auxiliary_model_resolver,
1152 disabled_filter_resolver,
1153 disabled_tools,
1154 disabled_skill_ids,
1155 selected_skill_ids,
1156 selected_skill_mode,
1157 mpsc_tx,
1158 mut history_commit_barrier,
1159 image_fallback,
1160 gold_config,
1161 guardian_config,
1162 guardian_spawner,
1163 bash_resume_hook,
1164 bash_completion_sink,
1165 app_data_dir,
1166 run_budget,
1167 runners,
1168 sessions_cache,
1169 on_complete,
1170 child_completion_handler,
1171 } = args;
1172
1173 if !execution_reservation.matches_execution_target(&session_id, &session.id, &runners) {
1174 tracing::error!(
1175 %session_id,
1176 domain_session_id = %session.id,
1177 reservation_session_id = %execution_reservation.session_id(),
1178 run_id = %execution_reservation.run_id(),
1179 same_runner_registry = Arc::ptr_eq(&execution_reservation.runners, &runners),
1180 "refusing mismatched session execution reservation"
1181 );
1182 execution_reservation.abandon().await;
1183 return;
1184 }
1185 if let Err(error) = execution_reservation.ensure_registered().await {
1186 tracing::warn!(
1187 %session_id,
1188 run_id = %execution_reservation.run_id(),
1189 %error,
1190 "session execution could not adopt its router activation owner"
1191 );
1192 return;
1193 }
1194 if execution_reservation.root_actor.is_none()
1195 && agent.persistence().root_actor_execution_required(&session)
1196 {
1197 let message =
1198 "Root execution route requires a bound Actor writer and event handoff";
1199 tracing::warn!(%session_id, "{message}");
1200 let _ = mpsc_tx
1201 .send(AgentEvent::Error {
1202 message: message.into(),
1203 })
1204 .await;
1205 execution_reservation.abandon().await;
1206 return;
1207 }
1208 if let Err(error) = execution_reservation.bind_execution_persistence(&agent, &session) {
1211 let _ = mpsc_tx
1212 .send(AgentEvent::Error {
1213 message: error.to_string(),
1214 })
1215 .await;
1216 execution_reservation.abandon().await;
1217 return;
1218 }
1219 let ownership = match &execution_reservation.activation {
1220 SessionExecutionActivationOwnership::Registered(run) => Some(run.ownership()),
1221 _ => None,
1222 };
1223 let admission_persistence = execution_reservation
1224 .execution_persistence()
1225 .unwrap_or_else(|| agent.persistence().clone());
1226 if let (Some(router), Some(ownership)) = (agent.activation_router(), ownership.as_ref())
1227 {
1228 if !router.bind_run_lifetime(ownership, &session).await {
1229 let _ = mpsc_tx
1230 .send(AgentEvent::Error {
1231 message: "registered run Session lifetime differs".into(),
1232 })
1233 .await;
1234 execution_reservation.abandon().await;
1235 return;
1236 }
1237 }
1238 if let Err(error) = crate::runtime::counted_loop::admit_registered(
1239 &mut session,
1240 ownership.as_ref(),
1241 agent.activation_router(),
1242 admission_persistence.as_ref(),
1243 )
1244 .await
1245 {
1246 let _ = mpsc_tx
1247 .send(AgentEvent::Error {
1248 message: error.to_string(),
1249 })
1250 .await;
1251 execution_reservation.abandon().await;
1252 return;
1253 }
1254 let mut root_actor = execution_reservation.root_actor.take().map(|owner| *owner);
1255 let root_actor_bound = root_actor.is_some();
1256 let persistence = execution_reservation
1257 .execution_persistence()
1258 .unwrap_or_else(|| agent.persistence().clone());
1259 let agent = Arc::new(agent.with_execution_persistence(persistence));
1260 let activation_run_id = execution_reservation.run_id().to_string();
1261 let prior_message_ids = session.messages.iter().map(|m| m.id.clone()).collect();
1262 let (cancel_token, mut activation_registration) =
1263 execution_reservation.disarm_for_execution();
1264
1265 let model = model_roster.model.clone().unwrap_or_default();
1269
1270 let initial_message = initial_user_message_for_session(&session);
1271 let selected_skill_ids =
1272 selected_skill_ids.or_else(|| selected_skill_ids_for_session(&session));
1273 let selected_skill_mode =
1274 selected_skill_mode.or_else(|| selected_skill_mode_for_session(&session));
1275
1276 tracing::info!(
1277 "[{}] Using resolved session model: {}, reasoning_effort={}, reasoning_source={}",
1278 session_id,
1279 model,
1280 reasoning_effort
1281 .map(ReasoningEffort::as_str)
1282 .unwrap_or("none"),
1283 reasoning_effort_source
1284 );
1285
1286 crate::session_app::execution_prep::prepare_session_for_execution(
1293 &mut session,
1294 None,
1295 Some(&model),
1296 );
1297
1298 let system_prompt = system_prompt_for_debug_log(&mut session);
1299 if let Some(prompt) = system_prompt.as_ref() {
1300 log_base_system_prompt_snapshot(&session_id, prompt);
1301 }
1302
1303 let inherited_child_wait = agent.persistence().inherited_child_wait();
1307 let execute_request = build_execute_request(
1308 initial_message,
1309 mpsc_tx.clone(),
1310 cancel_token,
1311 ExecuteRequestParams {
1312 tools: tools_override,
1313 provider_override,
1314 model_roster,
1315 reasoning_effort,
1316 auxiliary_model_resolver,
1317 disabled_filter_resolver,
1318 disabled_tools,
1319 disabled_skill_ids,
1320 selected_skill_ids,
1321 selected_skill_mode,
1322 image_fallback,
1323 gold_config,
1324 guardian_config,
1325 guardian_spawner,
1326 bash_resume_hook,
1327 bash_completion_sink,
1328 app_data_dir,
1329 run_budget,
1330 },
1331 );
1332
1333 let result = {
1340 use futures::FutureExt;
1341 match std::panic::AssertUnwindSafe(agent.execute_with_inputs_and_upstream_retry(
1342 &mut session,
1343 execute_request,
1344 inputs,
1345 upstream_retry,
1346 ))
1347 .catch_unwind()
1348 .await
1349 {
1350 Ok(result) => result,
1351 Err(panic) => {
1352 if let Some(outer) = &task_outer {
1353 outer
1354 .unconfirmed("Root loop unwound; physical effects are unconfirmed");
1355 }
1356 let message = panic
1357 .downcast_ref::<&str>()
1358 .map(|s| (*s).to_string())
1359 .or_else(|| panic.downcast_ref::<String>().cloned())
1360 .unwrap_or_else(|| "non-string panic payload".to_string());
1361 tracing::error!(
1362 "[{}] agent execution panicked; finalizing as terminal error: {}",
1363 session_id,
1364 message
1365 );
1366 Err(AgentError::LLM(format!(
1367 "agent execution panicked: {message}"
1368 )))
1369 }
1370 }
1371 };
1372
1373 if let Some(error_event) = terminal_error_event_for_result(&result) {
1375 let _ = mpsc_tx.send(error_event).await;
1376 }
1377
1378 let suspended_non_terminal = result.is_ok()
1395 && session
1396 .metadata
1397 .get("runtime.suspend_reason")
1398 .is_some_and(|reason| !reason.trim().is_empty());
1399 match &result {
1400 Ok(()) if suspended_non_terminal => {
1401 session.set_last_run_status("suspended");
1402 session.clear_last_run_error();
1403 }
1404 Ok(()) => {
1405 session.set_last_run_status("completed");
1406 session.clear_last_run_error();
1407 }
1408 Err(error) if error.is_cancelled() => {
1409 session.set_last_run_status("cancelled");
1410 session.set_last_run_error(error.to_string());
1411 }
1412 Err(error) => {
1413 session.set_last_run_status("error");
1414 session.set_last_run_error(error.to_string());
1415 }
1416 }
1417
1418 if let Some(on_complete) = on_complete {
1422 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
1423 }
1424
1425 let executed_admitted_generation = session
1429 .session_inbox_admission()
1430 .map_or(0, |state| state.last_admitted_sequence);
1431 let legacy_migration =
1432 crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
1433 &mut session,
1434 Some(agent.storage()),
1435 Some(agent.persistence()),
1436 agent.session_inbox(),
1437 )
1438 .await;
1439 let pending_boundary_generation = session
1440 .session_inbox_admission()
1441 .and_then(|state| state.pending_activation_generation());
1442 let pending_generation = match (
1443 pending_boundary_generation,
1444 legacy_migration.highest_generation,
1445 ) {
1446 (Some(left), Some(right)) => Some(left.max(right)),
1447 (left, right) => left.or(right),
1448 };
1449 if let (Some(generation), Some(router)) =
1450 (pending_generation, agent.activation_router())
1451 {
1452 let activation_ready = if let Some(inbox) = agent.session_inbox() {
1453 match inbox
1454 .mark_activation_eligible(
1455 &session_id,
1456 generation,
1457 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
1458 )
1459 .await
1460 {
1461 Ok(()) => true,
1462 Err(error) => {
1463 tracing::error!(
1464 session_id = %session_id,
1465 %error,
1466 "failed to persist unadmitted SessionInbox activation watermark"
1467 );
1468 false
1469 }
1470 }
1471 } else {
1472 false
1473 };
1474 if activation_ready {
1475 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
1476 router.as_ref(),
1477 &session_id,
1478 generation,
1479 )
1480 .await
1481 {
1482 tracing::error!(
1483 session_id = %session_id,
1484 %error,
1485 "failed to hand unadmitted SessionInbox generation to activation router"
1486 );
1487 }
1488 }
1489 }
1490
1491 if let Some(registration) = activation_registration.as_mut() {
1495 registration.begin_finalization().await;
1496 }
1497
1498 super::child_completion::ChildCompletionSource::prepare(
1502 &mut session,
1503 &activation_run_id,
1504 &prior_message_ids,
1505 );
1506 let saved = save_finalized_runtime_with_inherited_child_wait(
1507 agent.persistence().as_ref(),
1508 &mut session,
1509 inherited_child_wait
1510 .as_ref()
1511 .map(|inherited| inherited.wait()),
1512 )
1513 .await;
1514 let history_committed = saved.is_ok();
1515 let completion_source =
1516 super::child_completion::ChildCompletionSource::after_final_save(
1517 &session,
1518 history_committed,
1519 );
1520 let completed_at = chrono::Utc::now();
1521 if let Err(error) = &saved {
1522 if let Some(outer) = &task_outer {
1523 outer.unconfirmed(format!(
1524 "Root final persistence outcome unconfirmed: {error}"
1525 ));
1526 }
1527 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
1528 }
1529
1530 if history_committed
1536 && !history_commit_barrier
1537 .send_and_wait(&mpsc_tx, session_id.clone())
1538 .await
1539 {
1540 if let Some(outer) = &task_outer {
1541 outer.unconfirmed("Root history publication barrier unconfirmed");
1542 }
1543 tracing::warn!(
1544 session_id = %session_id,
1545 "history commit barrier could not be published before runner finalization"
1546 );
1547 }
1548
1549 finalize_runner_exact(&runners, &session_id, &activation_run_id, &result).await;
1555
1556 #[cfg(test)]
1557 if let Some(outer) = &task_outer {
1558 outer.pause_before_actor_finish().await;
1559 }
1560
1561 if let Some(owner) = root_actor.take() {
1562 let outcome = if result.as_ref().is_err_and(|error| error.is_cancelled()) {
1563 bamboo_domain::ActorActivationFinish::Cancelled
1564 } else if result.is_err() || !history_committed {
1565 bamboo_domain::ActorActivationFinish::Failed
1566 } else {
1567 bamboo_domain::ActorActivationFinish::Succeeded
1568 };
1569 let finished = owner.finish_observed(outcome).await;
1570 if let Err(error) = &finished {
1571 tracing::warn!(%session_id, %error, "Root Actor closure unconfirmed");
1572 }
1573 if let Some(outer) = &task_outer {
1574 outer.actor_finished(finished);
1575 }
1576 }
1577
1578 let finalization = if let Some(registration) = activation_registration.take() {
1579 registration.finish(executed_admitted_generation).await
1580 } else {
1581 Ok(None)
1582 };
1583 if let Err(error) = finalization {
1584 if let Some(outer) = &task_outer {
1585 outer.unconfirmed(format!(
1586 "Root registration finalization unconfirmed: {error}"
1587 ));
1588 }
1589 tracing::error!(
1593 session_id = %session_id,
1594 %error,
1595 "failed to activate successor for finalization-racing SessionInbox delivery"
1596 );
1597 }
1598
1599 let child_completion = child_completion_handler.filter(|_| {
1608 session.kind == bamboo_agent_core::SessionKind::Child
1609 && session.parent_session_id.is_some()
1610 });
1611 let parent_session_id = session.parent_session_id.clone();
1612 let child_status = session.last_run_status();
1613 let child_error = session.last_run_error();
1614
1615 publish_final_session_cache(&sessions_cache, session, root_actor_bound, &saved);
1616
1617 if let (Some(handler), Some(parent_session_id), Some(status)) =
1618 (child_completion, parent_session_id, child_status)
1619 {
1620 use futures::FutureExt;
1621 let completion = ChildCompletion {
1622 parent_session_id: parent_session_id.clone(),
1623 child_session_id: session_id.clone(),
1624 status,
1625 error: child_error,
1626 completed_at,
1627 source: completion_source,
1628 };
1629 if std::panic::AssertUnwindSafe(handler.on_child_completed(completion))
1630 .catch_unwind()
1631 .await
1632 .is_err()
1633 {
1634 tracing::error!(
1635 %parent_session_id,
1636 child_session_id = %session_id,
1637 "child completion handler panicked on resumed-child terminal"
1638 );
1639 }
1640 }
1641
1642 tracing::info!("[{}] Agent execution completed", session_id);
1643 if let Some(outer) = &task_outer {
1644 outer.tail_completed();
1645 }
1646 }
1647 .instrument(session_span),
1648 );
1649 if let Some(outer) = outer {
1650 outer.retain(task);
1651 } else if is_root {
1652 tokio::spawn(async move {
1655 let _ = task.await;
1656 });
1657 }
1658}
1659
1660async fn save_finalized_runtime_with_inherited_child_wait(
1662 persistence: &dyn bamboo_domain::RuntimeSessionPersistence,
1663 session: &mut Session,
1664 inherited_child_wait: Option<&bamboo_domain::session::runtime_state::WaitingForChildrenState>,
1665) -> std::io::Result<()> {
1666 if let Some(inherited) = inherited_child_wait.filter(|wait| {
1667 session
1668 .agent_runtime_state
1669 .as_ref()
1670 .and_then(|runtime| runtime.waiting_for_children.as_ref())
1671 == Some(*wait)
1672 }) {
1673 persistence
1674 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
1675 .await
1676 } else {
1677 persistence.save_finalized_runtime_session(session).await
1678 }
1679}
1680
1681fn publish_final_session_cache(
1682 cache: &SessionCache,
1683 session: Session,
1684 root_actor_bound: bool,
1685 saved: &std::io::Result<()>,
1686) {
1687 let authority_conflict = saved.as_ref().err().is_some_and(|error| {
1688 error
1689 .get_ref()
1690 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>())
1691 });
1692 if !authority_conflict && !root_actor_bound {
1695 cache.insert(
1696 session.id.clone(),
1697 Arc::new(crate::SessionSnapshot::new(session)),
1698 );
1699 }
1700}
1701
1702pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
1704 tracing::info!(
1705 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
1706 session_id,
1707 prompt.len(),
1708 prompt.contains(SKILL_CONTEXT_START_MARKER),
1709 prompt.contains(TOOL_GUIDE_START_MARKER),
1710 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
1711 prompt.contains(TASK_LIST_START_MARKER),
1712 );
1713
1714 tracing::debug!(
1715 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
1716 session_id
1717 );
1718 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
1719 tracing::debug!("[{}] -----------------------------------", session_id);
1720 tracing::debug!("[{}] {}", session_id, prompt);
1721 tracing::debug!(
1722 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
1723 session_id
1724 );
1725}
1726
1727pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
1729 match result {
1730 Ok(_) => None,
1731 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
1732 message: "Agent execution cancelled by user".to_string(),
1733 }),
1734 Err(error) => Some(AgentEvent::Error {
1735 message: error.to_string(),
1736 }),
1737 }
1738}
1739
1740fn system_prompt_for_session(session: &Session) -> Option<String> {
1743 session
1744 .messages
1745 .iter()
1746 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
1747 .map(|message| message.content.clone())
1748}
1749
1750fn system_prompt_for_debug_log(session: &mut Session) -> Option<String> {
1754 crate::runtime::runner::session_setup::migrate_legacy_workspace_prompt(session);
1755 system_prompt_for_session(session)
1756}
1757
1758fn initial_user_message_for_session(session: &Session) -> String {
1759 session
1760 .messages
1761 .last()
1762 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
1763 .map(|message| message.content.clone())
1764 .unwrap_or_default()
1765}
1766
1767fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
1768 session
1769 .metadata
1770 .get("selected_skill_ids")
1771 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
1772}
1773
1774fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
1775 let value = session
1776 .metadata
1777 .get("skill_mode")
1778 .or_else(|| session.metadata.get("mode"))?;
1779 let trimmed = value.trim();
1780 if trimmed.is_empty() {
1781 None
1782 } else {
1783 Some(trimmed.to_string())
1784 }
1785}
1786
1787pub(super) struct RootActorExecution {
1788 binding: bamboo_domain::RootActorExecutionBinding,
1789 stop_renewal: CancellationToken,
1790 renewal: Option<tokio::task::JoinHandle<()>>,
1791 response_events: Vec<AgentEvent>,
1792}
1793
1794impl RootActorExecution {
1795 pub(super) fn start(binding: bamboo_domain::RootActorExecutionBinding) -> Self {
1796 let stop_renewal = CancellationToken::new();
1797 let stop = stop_renewal.clone();
1798 let directory = binding.directory.clone();
1799 let fence = binding.owner.fence.clone();
1800 let duration = binding.lease_duration;
1801 let renewal = tokio::spawn(async move {
1802 let mut interval = tokio::time::interval(duration / 3);
1803 interval.tick().await;
1804 loop {
1805 tokio::select! {
1806 _ = stop.cancelled() => break,
1807 _ = interval.tick() => {
1808 let now = chrono::Utc::now();
1809 let expires = now + chrono::Duration::from_std(duration).expect("bounded Root lease");
1810 if let Err(error) = directory.renew_activation(&fence, now, expires).await {
1811 tracing::warn!(actor_id = %fence.actor_id, run_id = %fence.run_id,
1815 %error, "Root Actor renewal lost its execution authority");
1816 break;
1817 }
1818 }
1819 }
1820 }
1821 });
1822 Self {
1823 binding,
1824 stop_renewal,
1825 renewal: Some(renewal),
1826 response_events: Vec::new(),
1827 }
1828 }
1829
1830 pub(super) fn persistence(
1831 &self,
1832 ) -> std::sync::Arc<dyn bamboo_domain::RuntimeSessionPersistence> {
1833 self.binding.persistence.clone()
1834 }
1835
1836 pub(super) async fn finish(self, outcome: bamboo_domain::ActorActivationFinish) {
1837 if let Err(error) = self.finish_observed(outcome).await {
1838 tracing::warn!(%error, "Root Actor closure unconfirmed");
1839 }
1840 }
1841
1842 async fn finish_observed(
1843 mut self,
1844 outcome: bamboo_domain::ActorActivationFinish,
1845 ) -> std::io::Result<()> {
1846 self.stop_renewal.cancel();
1847 let renewal = match self.renewal.take() {
1848 Some(renewal) => renewal.await.map_err(std::io::Error::other),
1849 None => Ok(()),
1850 };
1851 let directory = self.binding.directory.clone();
1852 let fence = self.binding.owner.fence.clone();
1853 let finish = tokio::spawn(async move {
1854 directory
1855 .finish_activation(&fence, chrono::Utc::now(), outcome)
1856 .await
1857 .map(|_| ())
1858 .map_err(std::io::Error::other)
1859 });
1860 self.binding.disarm_abandonment();
1861 finish.await.map_err(std::io::Error::other)??;
1862 renewal
1863 }
1864}
1865
1866impl Drop for RootActorExecution {
1867 fn drop(&mut self) {
1868 self.stop_renewal.cancel();
1869 }
1872}
1873
1874#[cfg(test)]
1875#[path = "agent_spawn_peer_privacy_tests.rs"]
1876mod peer_privacy_tests;
1877
1878#[cfg(test)]
1879mod reservation_tests {
1880 use super::*;
1881 use crate::runtime::execution::runner_state::AgentStatus;
1882
1883 struct CompletionAtFinalSave {
1884 directory: std::path::PathBuf,
1885 persistence: bamboo_storage::LockedSessionStore,
1886 cleared: std::sync::atomic::AtomicBool,
1887 }
1888
1889 impl CompletionAtFinalSave {
1890 async fn clear_wait(&self, id: &str) {
1891 let mut process = std::process::Command::new(std::env::current_exe().unwrap())
1892 .args(["--exact", "runtime::execution::agent_spawn::reservation_tests::inherited_child_wait_adopts_independent_completion_at_final_save", "--nocapture"])
1893 .env("BAMBOO_FINAL_WAIT_COMPLETION_DIR", &self.directory)
1894 .env("BAMBOO_FINAL_WAIT_COMPLETION_ID", id)
1895 .spawn().unwrap();
1896 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
1897 loop {
1898 if let Some(status) = process.try_wait().unwrap() {
1899 assert!(status.success(), "completion process failed");
1900 break;
1901 }
1902 if std::time::Instant::now() >= deadline {
1903 let _ = process.kill();
1904 let _ = process.wait();
1905 panic!("bounded completion process deadline");
1906 }
1907 std::thread::sleep(std::time::Duration::from_millis(5));
1908 }
1909 self.cleared
1910 .store(true, std::sync::atomic::Ordering::SeqCst);
1911 }
1912 }
1913
1914 #[async_trait::async_trait]
1915 impl bamboo_domain::RuntimeSessionPersistence for CompletionAtFinalSave {
1916 async fn save_runtime_session(&self, session: &mut Session) -> std::io::Result<()> {
1917 self.clear_wait(&session.id).await;
1918 self.persistence.merge_save_finalized_runtime(session).await
1919 }
1920 async fn save_finalized_runtime_with_inherited_child_wait(
1921 &self,
1922 session: &mut Session,
1923 inherited: &bamboo_domain::session::runtime_state::WaitingForChildrenState,
1924 ) -> std::io::Result<()> {
1925 self.clear_wait(&session.id).await;
1926 self.persistence
1927 .merge_save_inherited_child_wait_and_publish(session, inherited, |_, _| {})
1928 .await
1929 }
1930 }
1931
1932 #[tokio::test]
1933 async fn inherited_child_wait_adopts_independent_completion_at_final_save() {
1934 use bamboo_agent_core::storage::Storage;
1935 use bamboo_domain::session::runtime_state::{
1936 AgentRuntimeState, AgentStatusState, ChildWaitPolicy, WaitingForChildrenState,
1937 };
1938 if let Some(directory) = std::env::var_os("BAMBOO_FINAL_WAIT_COMPLETION_DIR") {
1939 let remote = bamboo_storage::SessionStoreV2::new(directory.into())
1940 .await
1941 .unwrap();
1942 let id = std::env::var("BAMBOO_FINAL_WAIT_COMPLETION_ID").unwrap();
1943 let mut latest = remote.load_session(&id).await.unwrap().unwrap();
1944 let runtime = latest.agent_runtime_state.as_mut().unwrap();
1945 runtime.waiting_for_children = None;
1946 runtime.status = AgentStatusState::Idle;
1947 runtime.suspension = None;
1948 latest.metadata.remove("runtime.suspend_reason");
1949 remote.save_runtime_state(&latest).await.unwrap();
1950 return;
1951 }
1952 let temp = tempfile::tempdir().unwrap();
1953 let local = Arc::new(
1954 bamboo_storage::SessionStoreV2::new(temp.path().to_path_buf())
1955 .await
1956 .unwrap(),
1957 );
1958 let mut runner = Session::new("cross-host-final-wait", "model");
1962 let inherited = WaitingForChildrenState::for_children(
1963 vec!["child".into()],
1964 ChildWaitPolicy::All,
1965 chrono::Utc::now(),
1966 );
1967 let mut runtime = AgentRuntimeState::new("run");
1968 runtime.waiting_for_children = Some(inherited.clone());
1969 runtime.status = AgentStatusState::Suspended;
1970 runner.agent_runtime_state = Some(runtime);
1971 runner.metadata.insert(
1972 "runtime.suspend_reason".into(),
1973 "waiting_for_children".into(),
1974 );
1975 runner.set_last_run_status("suspended");
1976 local.save_session(&runner).await.unwrap();
1977 let persistence = CompletionAtFinalSave {
1978 directory: temp.path().to_path_buf(),
1979 persistence: bamboo_storage::LockedSessionStore::new(local.clone()),
1980 cleared: std::sync::atomic::AtomicBool::new(false),
1981 };
1982 save_finalized_runtime_with_inherited_child_wait(
1983 &persistence,
1984 &mut runner,
1985 Some(&inherited),
1986 )
1987 .await
1988 .unwrap();
1989 assert!(persistence
1990 .cleared
1991 .load(std::sync::atomic::Ordering::SeqCst));
1992 let durable = local.load_session(&runner.id).await.unwrap().unwrap();
1993 for session in [&runner, &durable] {
1994 let runtime = session.agent_runtime_state.as_ref().unwrap();
1995 assert!(
1996 runtime.waiting_for_children.is_none(),
1997 "independent completion must not be resurrected"
1998 );
1999 assert_eq!(runtime.status, AgentStatusState::Idle);
2000 assert!(runtime.suspension.is_none());
2001 assert!(!session.metadata.contains_key("runtime.suspend_reason"));
2002 assert_eq!(session.last_run_status().as_deref(), Some("completed"));
2003 }
2004 }
2005
2006 #[tokio::test]
2007 async fn inherited_child_wait_rejects_aba_and_missing_birth_without_cache_publication() {
2008 use bamboo_agent_core::storage::Storage;
2009 use bamboo_domain::session::runtime_state::{
2010 AgentRuntimeState, ChildWaitPolicy, WaitingForChildrenState,
2011 };
2012 use bamboo_storage::{LockedSessionStore, SessionStoreV2};
2013
2014 for missing in [false, true] {
2015 let temp = tempfile::tempdir().unwrap();
2016 let storage = Arc::new(
2017 SessionStoreV2::new(temp.path().to_path_buf())
2018 .await
2019 .unwrap(),
2020 );
2021 let persistence = LockedSessionStore::new(storage.clone());
2022 let cache: SessionCache = Arc::default();
2023 storage
2024 .save_session(&Session::new("root", "model"))
2025 .await
2026 .unwrap();
2027 let mut stale = Session::new("nested-child-parent", "model");
2028 stale.kind = bamboo_domain::SessionKind::Child;
2029 stale.parent_session_id = Some("root".into());
2030 stale.root_session_id = "root".into();
2031 stale.spawn_depth = 1;
2032 let wait = WaitingForChildrenState::for_children(
2033 vec!["grandchild".into()],
2034 ChildWaitPolicy::All,
2035 chrono::Utc::now(),
2036 );
2037 let mut runtime = AgentRuntimeState::new("old-run");
2038 runtime.waiting_for_children = Some(wait.clone());
2039 stale.agent_runtime_state = Some(runtime);
2040 stale.add_message(bamboo_agent_core::Message::assistant("STALE", None));
2041 storage.save_session(&stale).await.unwrap();
2042
2043 let mut replacement = stale.clone();
2044 replacement.created_at += chrono::Duration::seconds(1);
2045 replacement.messages.clear();
2046 replacement.add_message(bamboo_agent_core::Message::assistant("REPLACEMENT", None));
2047 replacement
2048 .agent_runtime_state
2049 .as_mut()
2050 .unwrap()
2051 .waiting_for_children = None;
2052 storage.delete_session(&stale.id).await.unwrap();
2053 if !missing {
2054 storage.save_session(&replacement).await.unwrap();
2055 }
2056 if !missing {
2057 cache.insert(
2058 replacement.id.clone(),
2059 Arc::new(crate::SessionSnapshot::new(replacement.clone())),
2060 );
2061 }
2062
2063 let saved = save_finalized_runtime_with_inherited_child_wait(
2064 &persistence,
2065 &mut stale,
2066 Some(&wait),
2067 )
2068 .await;
2069 assert!(saved.is_err(), "stale incarnation must not be saved");
2070 publish_final_session_cache(&cache, stale, false, &saved);
2073 if missing {
2074 assert!(
2075 read_cached_session(&cache, &replacement.id).is_none(),
2076 "deleted incarnation must not be resurrected in cache"
2077 );
2078 } else {
2079 let cached = read_cached_session(&cache, &replacement.id).unwrap();
2080 assert_eq!(
2081 cached.created_at, replacement.created_at,
2082 "ABA rejection must retain replacement cache birth"
2083 );
2084 assert_eq!(
2085 serde_json::to_value(&cached.messages).unwrap(),
2086 serde_json::to_value(&replacement.messages).unwrap(),
2087 "ABA rejection must retain replacement cache transcript"
2088 );
2089 }
2090 assert!(saved
2091 .as_ref()
2092 .unwrap_err()
2093 .get_ref()
2094 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>()));
2095 let durable = storage.load_session(&replacement.id).await.unwrap();
2096 if missing {
2097 assert!(durable.is_none());
2098 } else {
2099 let durable = durable.unwrap();
2100 assert_eq!(durable.created_at, replacement.created_at);
2101 assert_eq!(
2102 serde_json::to_value(&durable.messages).unwrap(),
2103 serde_json::to_value(&replacement.messages).unwrap()
2104 );
2105 }
2106 }
2107 }
2108
2109 #[test]
2110 fn debug_prompt_snapshot_migrates_legacy_host_paths_before_logging() {
2111 let legacy_project = format!(
2112 "{}\nProject ID: legacy-project\nProject path: /private/legacy-project\nProject home: /private/legacy-home\n{}",
2113 crate::runtime::context::PROJECT_CONTEXT_START_MARKER,
2114 crate::runtime::context::PROJECT_CONTEXT_END_MARKER,
2115 );
2116 let legacy_workspace =
2117 crate::runtime::context::build_workspace_prompt_context("/private/legacy-workspace")
2118 .expect("legacy workspace context");
2119 let legacy_instruction = format!(
2120 "{}\nSource: /private/legacy-workspace/AGENTS.md\nlegacy policy\n{}",
2121 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER,
2122 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_END_MARKER,
2123 );
2124 let mut session = Session::new("legacy-debug-log", "model");
2125 session.add_message(bamboo_agent_core::Message::system(format!(
2126 "Base prompt\n\n{legacy_project}\n\n{legacy_workspace}\n\n{legacy_instruction}"
2127 )));
2128
2129 let prompt = system_prompt_for_debug_log(&mut session).expect("normalized System");
2130
2131 assert_eq!(prompt, "Base prompt");
2132 assert_eq!(
2133 session.workspace_path_meta().as_deref(),
2134 Some("/private/legacy-workspace")
2135 );
2136 for private_path in [
2137 "/private/legacy-project",
2138 "/private/legacy-home",
2139 "/private/legacy-workspace",
2140 ] {
2141 assert!(!prompt.contains(private_path));
2142 }
2143 assert!(!prompt.contains(crate::runtime::context::PROJECT_CONTEXT_START_MARKER));
2144 assert!(!prompt.contains(crate::runtime::context::WORKSPACE_CONTEXT_START_MARKER));
2145 assert!(!prompt
2146 .contains(crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER));
2147 }
2148
2149 #[test]
2150 fn reservation_target_requires_domain_id_and_exact_runner_registry() {
2151 let runners = Arc::new(RwLock::new(HashMap::new()));
2152 let other_runners = Arc::new(RwLock::new(HashMap::new()));
2153 let mut reservation = SessionExecutionReservation {
2154 session_id: "session-a".to_string(),
2155 run_id: "run-a".to_string(),
2156 cancel_token: CancellationToken::new(),
2157 runners: runners.clone(),
2158 activation: SessionExecutionActivationOwnership::Unrouted,
2159 root_actor: None,
2160 physical_admission: None,
2161 execution_persistence: None,
2162 persistence_bound: false,
2163 selected_input: false,
2164 armed: true,
2165 };
2166
2167 assert!(reservation.matches_execution_target("session-a", "session-a", &runners));
2168 assert!(!reservation.matches_execution_target("session-b", "session-a", &runners));
2169 assert!(!reservation.matches_execution_target("session-a", "session-b", &runners));
2170 assert!(!reservation.matches_execution_target("session-a", "session-a", &other_runners));
2171 reservation.armed = false;
2172 }
2173
2174 #[tokio::test]
2175 async fn forced_same_run_registration_rejection_never_cancels_live_owner() {
2176 let runners = Arc::new(RwLock::new(HashMap::new()));
2177 let mut runner = AgentRunner::new();
2178 runner.status = AgentStatus::Running;
2179 let run_id = runner.run_id.clone();
2180 let live_cancel_token = runner.cancel_token.clone();
2181 runners.write().await.insert("same-run".to_string(), runner);
2182
2183 let router = SessionActivationRouter::new();
2184 let mut live_registration = router
2185 .register_run("same-run", &run_id)
2186 .await
2187 .expect("first registration owns the run");
2188 let mut rejected = SessionExecutionReservation {
2189 session_id: "same-run".to_string(),
2190 run_id: run_id.clone(),
2191 cancel_token: live_cancel_token.clone(),
2192 runners: runners.clone(),
2193 activation: SessionExecutionActivationOwnership::RegistrationPending(router.clone()),
2194 root_actor: None,
2195 physical_admission: None,
2196 execution_persistence: None,
2197 persistence_bound: false,
2198 selected_input: false,
2199 armed: true,
2200 };
2201
2202 let error = match rejected.ensure_registered().await {
2203 Ok(()) => panic!("a duplicate registration for the same run must be rejected"),
2204 Err(error) => error,
2205 };
2206 assert_eq!(error.existing_run_id(), run_id);
2207 drop(rejected);
2208
2209 assert!(!live_cancel_token.is_cancelled());
2210 assert!(matches!(
2211 runners
2212 .read()
2213 .await
2214 .get("same-run")
2215 .map(|runner| &runner.status),
2216 Some(AgentStatus::Running)
2217 ));
2218 assert!(router.owns_run("same-run", &run_id).await);
2219 live_registration.begin_finalization().await;
2220 assert_eq!(live_registration.finish(0).await.unwrap(), None);
2221 }
2222}