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 let span_session_id = args.session_id.clone();
1098 let session_span = tracing::info_span!("agent_execution", session_id = %span_session_id);
1099 let is_root = args.session.kind == bamboo_domain::SessionKind::Root;
1100 let outer = if is_root {
1101 match args
1102 .execution_reservation
1103 .physical_admission
1104 .as_ref()
1105 .map(|admission| admission.attach(&args.session))
1106 .transpose()
1107 {
1108 Ok(outer) => outer,
1109 Err(error) => {
1110 tokio::spawn(async move {
1113 let _ = args
1114 .mpsc_tx
1115 .send(AgentEvent::Error {
1116 message: error.to_string(),
1117 })
1118 .await;
1119 args.execution_reservation.abandon().await;
1120 });
1121 return;
1122 }
1123 }
1124 } else {
1125 None
1126 };
1127 let task_outer = outer.clone();
1128
1129 let task = tokio::spawn(
1130 async move {
1131 let SessionExecutionArgs {
1132 agent,
1133 session_id,
1134 mut session,
1135 mut execution_reservation,
1136 tools_override,
1137 provider_override,
1138 model_roster,
1139 reasoning_effort,
1140 reasoning_effort_source,
1141 auxiliary_model_resolver,
1142 disabled_filter_resolver,
1143 disabled_tools,
1144 disabled_skill_ids,
1145 selected_skill_ids,
1146 selected_skill_mode,
1147 mpsc_tx,
1148 mut history_commit_barrier,
1149 image_fallback,
1150 gold_config,
1151 guardian_config,
1152 guardian_spawner,
1153 bash_resume_hook,
1154 bash_completion_sink,
1155 app_data_dir,
1156 run_budget,
1157 runners,
1158 sessions_cache,
1159 on_complete,
1160 child_completion_handler,
1161 } = args;
1162
1163 if !execution_reservation.matches_execution_target(&session_id, &session.id, &runners) {
1164 tracing::error!(
1165 %session_id,
1166 domain_session_id = %session.id,
1167 reservation_session_id = %execution_reservation.session_id(),
1168 run_id = %execution_reservation.run_id(),
1169 same_runner_registry = Arc::ptr_eq(&execution_reservation.runners, &runners),
1170 "refusing mismatched session execution reservation"
1171 );
1172 execution_reservation.abandon().await;
1173 return;
1174 }
1175 if let Err(error) = execution_reservation.ensure_registered().await {
1176 tracing::warn!(
1177 %session_id,
1178 run_id = %execution_reservation.run_id(),
1179 %error,
1180 "session execution could not adopt its router activation owner"
1181 );
1182 return;
1183 }
1184 if execution_reservation.root_actor.is_none()
1185 && agent.persistence().root_actor_execution_required(&session)
1186 {
1187 let message =
1188 "Root execution route requires a bound Actor writer and event handoff";
1189 tracing::warn!(%session_id, "{message}");
1190 let _ = mpsc_tx
1191 .send(AgentEvent::Error {
1192 message: message.into(),
1193 })
1194 .await;
1195 execution_reservation.abandon().await;
1196 return;
1197 }
1198 if let Err(error) = execution_reservation.bind_execution_persistence(&agent, &session) {
1201 let _ = mpsc_tx
1202 .send(AgentEvent::Error {
1203 message: error.to_string(),
1204 })
1205 .await;
1206 execution_reservation.abandon().await;
1207 return;
1208 }
1209 let ownership = match &execution_reservation.activation {
1210 SessionExecutionActivationOwnership::Registered(run) => Some(run.ownership()),
1211 _ => None,
1212 };
1213 let admission_persistence = execution_reservation
1214 .execution_persistence()
1215 .unwrap_or_else(|| agent.persistence().clone());
1216 if let (Some(router), Some(ownership)) = (agent.activation_router(), ownership.as_ref())
1217 {
1218 if !router.bind_run_lifetime(ownership, &session).await {
1219 let _ = mpsc_tx
1220 .send(AgentEvent::Error {
1221 message: "registered run Session lifetime differs".into(),
1222 })
1223 .await;
1224 execution_reservation.abandon().await;
1225 return;
1226 }
1227 }
1228 if let Err(error) = crate::runtime::counted_loop::admit_registered(
1229 &mut session,
1230 ownership.as_ref(),
1231 agent.activation_router(),
1232 admission_persistence.as_ref(),
1233 )
1234 .await
1235 {
1236 let _ = mpsc_tx
1237 .send(AgentEvent::Error {
1238 message: error.to_string(),
1239 })
1240 .await;
1241 execution_reservation.abandon().await;
1242 return;
1243 }
1244 let mut root_actor = execution_reservation.root_actor.take().map(|owner| *owner);
1245 let root_actor_bound = root_actor.is_some();
1246 let persistence = execution_reservation
1247 .execution_persistence()
1248 .unwrap_or_else(|| agent.persistence().clone());
1249 let agent = Arc::new(agent.with_execution_persistence(persistence));
1250 let activation_run_id = execution_reservation.run_id().to_string();
1251 let prior_message_ids = session.messages.iter().map(|m| m.id.clone()).collect();
1252 let (cancel_token, mut activation_registration) =
1253 execution_reservation.disarm_for_execution();
1254
1255 let model = model_roster.model.clone().unwrap_or_default();
1259
1260 let initial_message = initial_user_message_for_session(&session);
1261 let selected_skill_ids =
1262 selected_skill_ids.or_else(|| selected_skill_ids_for_session(&session));
1263 let selected_skill_mode =
1264 selected_skill_mode.or_else(|| selected_skill_mode_for_session(&session));
1265
1266 tracing::info!(
1267 "[{}] Using resolved session model: {}, reasoning_effort={}, reasoning_source={}",
1268 session_id,
1269 model,
1270 reasoning_effort
1271 .map(ReasoningEffort::as_str)
1272 .unwrap_or("none"),
1273 reasoning_effort_source
1274 );
1275
1276 crate::session_app::execution_prep::prepare_session_for_execution(
1283 &mut session,
1284 None,
1285 Some(&model),
1286 );
1287
1288 let system_prompt = system_prompt_for_debug_log(&mut session);
1289 if let Some(prompt) = system_prompt.as_ref() {
1290 log_base_system_prompt_snapshot(&session_id, prompt);
1291 }
1292
1293 let inherited_child_wait = agent.persistence().inherited_child_wait();
1297 let execute_request = build_execute_request(
1298 initial_message,
1299 mpsc_tx.clone(),
1300 cancel_token,
1301 ExecuteRequestParams {
1302 tools: tools_override,
1303 provider_override,
1304 model_roster,
1305 reasoning_effort,
1306 auxiliary_model_resolver,
1307 disabled_filter_resolver,
1308 disabled_tools,
1309 disabled_skill_ids,
1310 selected_skill_ids,
1311 selected_skill_mode,
1312 image_fallback,
1313 gold_config,
1314 guardian_config,
1315 guardian_spawner,
1316 bash_resume_hook,
1317 bash_completion_sink,
1318 app_data_dir,
1319 run_budget,
1320 },
1321 );
1322
1323 let result = {
1330 use futures::FutureExt;
1331 match std::panic::AssertUnwindSafe(agent.execute_with_inputs(
1332 &mut session,
1333 execute_request,
1334 inputs,
1335 ))
1336 .catch_unwind()
1337 .await
1338 {
1339 Ok(result) => result,
1340 Err(panic) => {
1341 if let Some(outer) = &task_outer {
1342 outer
1343 .unconfirmed("Root loop unwound; physical effects are unconfirmed");
1344 }
1345 let message = panic
1346 .downcast_ref::<&str>()
1347 .map(|s| (*s).to_string())
1348 .or_else(|| panic.downcast_ref::<String>().cloned())
1349 .unwrap_or_else(|| "non-string panic payload".to_string());
1350 tracing::error!(
1351 "[{}] agent execution panicked; finalizing as terminal error: {}",
1352 session_id,
1353 message
1354 );
1355 Err(AgentError::LLM(format!(
1356 "agent execution panicked: {message}"
1357 )))
1358 }
1359 }
1360 };
1361
1362 if let Some(error_event) = terminal_error_event_for_result(&result) {
1364 let _ = mpsc_tx.send(error_event).await;
1365 }
1366
1367 let suspended_non_terminal = result.is_ok()
1384 && session
1385 .metadata
1386 .get("runtime.suspend_reason")
1387 .is_some_and(|reason| !reason.trim().is_empty());
1388 match &result {
1389 Ok(()) if suspended_non_terminal => {
1390 session.set_last_run_status("suspended");
1391 session.clear_last_run_error();
1392 }
1393 Ok(()) => {
1394 session.set_last_run_status("completed");
1395 session.clear_last_run_error();
1396 }
1397 Err(error) if error.is_cancelled() => {
1398 session.set_last_run_status("cancelled");
1399 session.set_last_run_error(error.to_string());
1400 }
1401 Err(error) => {
1402 session.set_last_run_status("error");
1403 session.set_last_run_error(error.to_string());
1404 }
1405 }
1406
1407 if let Some(on_complete) = on_complete {
1411 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
1412 }
1413
1414 let executed_admitted_generation = session
1418 .session_inbox_admission()
1419 .map_or(0, |state| state.last_admitted_sequence);
1420 let legacy_migration =
1421 crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
1422 &mut session,
1423 Some(agent.storage()),
1424 Some(agent.persistence()),
1425 agent.session_inbox(),
1426 )
1427 .await;
1428 let pending_boundary_generation = session
1429 .session_inbox_admission()
1430 .and_then(|state| state.pending_activation_generation());
1431 let pending_generation = match (
1432 pending_boundary_generation,
1433 legacy_migration.highest_generation,
1434 ) {
1435 (Some(left), Some(right)) => Some(left.max(right)),
1436 (left, right) => left.or(right),
1437 };
1438 if let (Some(generation), Some(router)) =
1439 (pending_generation, agent.activation_router())
1440 {
1441 let activation_ready = if let Some(inbox) = agent.session_inbox() {
1442 match inbox
1443 .mark_activation_eligible(
1444 &session_id,
1445 generation,
1446 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
1447 )
1448 .await
1449 {
1450 Ok(()) => true,
1451 Err(error) => {
1452 tracing::error!(
1453 session_id = %session_id,
1454 %error,
1455 "failed to persist unadmitted SessionInbox activation watermark"
1456 );
1457 false
1458 }
1459 }
1460 } else {
1461 false
1462 };
1463 if activation_ready {
1464 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
1465 router.as_ref(),
1466 &session_id,
1467 generation,
1468 )
1469 .await
1470 {
1471 tracing::error!(
1472 session_id = %session_id,
1473 %error,
1474 "failed to hand unadmitted SessionInbox generation to activation router"
1475 );
1476 }
1477 }
1478 }
1479
1480 if let Some(registration) = activation_registration.as_mut() {
1484 registration.begin_finalization().await;
1485 }
1486
1487 super::child_completion::ChildCompletionSource::prepare(
1491 &mut session,
1492 &activation_run_id,
1493 &prior_message_ids,
1494 );
1495 let saved = save_finalized_runtime_with_inherited_child_wait(
1496 agent.persistence().as_ref(),
1497 &mut session,
1498 inherited_child_wait
1499 .as_ref()
1500 .map(|inherited| inherited.wait()),
1501 )
1502 .await;
1503 let history_committed = saved.is_ok();
1504 let completion_source =
1505 super::child_completion::ChildCompletionSource::after_final_save(
1506 &session,
1507 history_committed,
1508 );
1509 let completed_at = chrono::Utc::now();
1510 if let Err(error) = &saved {
1511 if let Some(outer) = &task_outer {
1512 outer.unconfirmed(format!(
1513 "Root final persistence outcome unconfirmed: {error}"
1514 ));
1515 }
1516 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
1517 }
1518
1519 if history_committed
1525 && !history_commit_barrier
1526 .send_and_wait(&mpsc_tx, session_id.clone())
1527 .await
1528 {
1529 if let Some(outer) = &task_outer {
1530 outer.unconfirmed("Root history publication barrier unconfirmed");
1531 }
1532 tracing::warn!(
1533 session_id = %session_id,
1534 "history commit barrier could not be published before runner finalization"
1535 );
1536 }
1537
1538 finalize_runner_exact(&runners, &session_id, &activation_run_id, &result).await;
1544
1545 #[cfg(test)]
1546 if let Some(outer) = &task_outer {
1547 outer.pause_before_actor_finish().await;
1548 }
1549
1550 if let Some(owner) = root_actor.take() {
1551 let outcome = if result.as_ref().is_err_and(|error| error.is_cancelled()) {
1552 bamboo_domain::ActorActivationFinish::Cancelled
1553 } else if result.is_err() || !history_committed {
1554 bamboo_domain::ActorActivationFinish::Failed
1555 } else {
1556 bamboo_domain::ActorActivationFinish::Succeeded
1557 };
1558 let finished = owner.finish_observed(outcome).await;
1559 if let Err(error) = &finished {
1560 tracing::warn!(%session_id, %error, "Root Actor closure unconfirmed");
1561 }
1562 if let Some(outer) = &task_outer {
1563 outer.actor_finished(finished);
1564 }
1565 }
1566
1567 let finalization = if let Some(registration) = activation_registration.take() {
1568 registration.finish(executed_admitted_generation).await
1569 } else {
1570 Ok(None)
1571 };
1572 if let Err(error) = finalization {
1573 if let Some(outer) = &task_outer {
1574 outer.unconfirmed(format!(
1575 "Root registration finalization unconfirmed: {error}"
1576 ));
1577 }
1578 tracing::error!(
1582 session_id = %session_id,
1583 %error,
1584 "failed to activate successor for finalization-racing SessionInbox delivery"
1585 );
1586 }
1587
1588 let child_completion = child_completion_handler.filter(|_| {
1597 session.kind == bamboo_agent_core::SessionKind::Child
1598 && session.parent_session_id.is_some()
1599 });
1600 let parent_session_id = session.parent_session_id.clone();
1601 let child_status = session.last_run_status();
1602 let child_error = session.last_run_error();
1603
1604 publish_final_session_cache(&sessions_cache, session, root_actor_bound, &saved);
1605
1606 if let (Some(handler), Some(parent_session_id), Some(status)) =
1607 (child_completion, parent_session_id, child_status)
1608 {
1609 use futures::FutureExt;
1610 let completion = ChildCompletion {
1611 parent_session_id: parent_session_id.clone(),
1612 child_session_id: session_id.clone(),
1613 status,
1614 error: child_error,
1615 completed_at,
1616 source: completion_source,
1617 };
1618 if std::panic::AssertUnwindSafe(handler.on_child_completed(completion))
1619 .catch_unwind()
1620 .await
1621 .is_err()
1622 {
1623 tracing::error!(
1624 %parent_session_id,
1625 child_session_id = %session_id,
1626 "child completion handler panicked on resumed-child terminal"
1627 );
1628 }
1629 }
1630
1631 tracing::info!("[{}] Agent execution completed", session_id);
1632 if let Some(outer) = &task_outer {
1633 outer.tail_completed();
1634 }
1635 }
1636 .instrument(session_span),
1637 );
1638 if let Some(outer) = outer {
1639 outer.retain(task);
1640 } else if is_root {
1641 tokio::spawn(async move {
1644 let _ = task.await;
1645 });
1646 }
1647}
1648
1649async fn save_finalized_runtime_with_inherited_child_wait(
1651 persistence: &dyn bamboo_domain::RuntimeSessionPersistence,
1652 session: &mut Session,
1653 inherited_child_wait: Option<&bamboo_domain::session::runtime_state::WaitingForChildrenState>,
1654) -> std::io::Result<()> {
1655 if let Some(inherited) = inherited_child_wait.filter(|wait| {
1656 session
1657 .agent_runtime_state
1658 .as_ref()
1659 .and_then(|runtime| runtime.waiting_for_children.as_ref())
1660 == Some(*wait)
1661 }) {
1662 persistence
1663 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
1664 .await
1665 } else {
1666 persistence.save_finalized_runtime_session(session).await
1667 }
1668}
1669
1670fn publish_final_session_cache(
1671 cache: &SessionCache,
1672 session: Session,
1673 root_actor_bound: bool,
1674 saved: &std::io::Result<()>,
1675) {
1676 let authority_conflict = saved.as_ref().err().is_some_and(|error| {
1677 error
1678 .get_ref()
1679 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>())
1680 });
1681 if !authority_conflict && !root_actor_bound {
1684 cache.insert(
1685 session.id.clone(),
1686 Arc::new(crate::SessionSnapshot::new(session)),
1687 );
1688 }
1689}
1690
1691pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
1693 tracing::info!(
1694 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
1695 session_id,
1696 prompt.len(),
1697 prompt.contains(SKILL_CONTEXT_START_MARKER),
1698 prompt.contains(TOOL_GUIDE_START_MARKER),
1699 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
1700 prompt.contains(TASK_LIST_START_MARKER),
1701 );
1702
1703 tracing::debug!(
1704 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
1705 session_id
1706 );
1707 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
1708 tracing::debug!("[{}] -----------------------------------", session_id);
1709 tracing::debug!("[{}] {}", session_id, prompt);
1710 tracing::debug!(
1711 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
1712 session_id
1713 );
1714}
1715
1716pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
1718 match result {
1719 Ok(_) => None,
1720 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
1721 message: "Agent execution cancelled by user".to_string(),
1722 }),
1723 Err(error) => Some(AgentEvent::Error {
1724 message: error.to_string(),
1725 }),
1726 }
1727}
1728
1729fn system_prompt_for_session(session: &Session) -> Option<String> {
1732 session
1733 .messages
1734 .iter()
1735 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
1736 .map(|message| message.content.clone())
1737}
1738
1739fn system_prompt_for_debug_log(session: &mut Session) -> Option<String> {
1743 crate::runtime::runner::session_setup::migrate_legacy_workspace_prompt(session);
1744 system_prompt_for_session(session)
1745}
1746
1747fn initial_user_message_for_session(session: &Session) -> String {
1748 session
1749 .messages
1750 .last()
1751 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
1752 .map(|message| message.content.clone())
1753 .unwrap_or_default()
1754}
1755
1756fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
1757 session
1758 .metadata
1759 .get("selected_skill_ids")
1760 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
1761}
1762
1763fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
1764 let value = session
1765 .metadata
1766 .get("skill_mode")
1767 .or_else(|| session.metadata.get("mode"))?;
1768 let trimmed = value.trim();
1769 if trimmed.is_empty() {
1770 None
1771 } else {
1772 Some(trimmed.to_string())
1773 }
1774}
1775
1776pub(super) struct RootActorExecution {
1777 binding: bamboo_domain::RootActorExecutionBinding,
1778 stop_renewal: CancellationToken,
1779 renewal: Option<tokio::task::JoinHandle<()>>,
1780 response_events: Vec<AgentEvent>,
1781}
1782
1783impl RootActorExecution {
1784 pub(super) fn start(binding: bamboo_domain::RootActorExecutionBinding) -> Self {
1785 let stop_renewal = CancellationToken::new();
1786 let stop = stop_renewal.clone();
1787 let directory = binding.directory.clone();
1788 let fence = binding.owner.fence.clone();
1789 let duration = binding.lease_duration;
1790 let renewal = tokio::spawn(async move {
1791 let mut interval = tokio::time::interval(duration / 3);
1792 interval.tick().await;
1793 loop {
1794 tokio::select! {
1795 _ = stop.cancelled() => break,
1796 _ = interval.tick() => {
1797 let now = chrono::Utc::now();
1798 let expires = now + chrono::Duration::from_std(duration).expect("bounded Root lease");
1799 if let Err(error) = directory.renew_activation(&fence, now, expires).await {
1800 tracing::warn!(actor_id = %fence.actor_id, run_id = %fence.run_id,
1804 %error, "Root Actor renewal lost its execution authority");
1805 break;
1806 }
1807 }
1808 }
1809 }
1810 });
1811 Self {
1812 binding,
1813 stop_renewal,
1814 renewal: Some(renewal),
1815 response_events: Vec::new(),
1816 }
1817 }
1818
1819 pub(super) fn persistence(
1820 &self,
1821 ) -> std::sync::Arc<dyn bamboo_domain::RuntimeSessionPersistence> {
1822 self.binding.persistence.clone()
1823 }
1824
1825 pub(super) async fn finish(self, outcome: bamboo_domain::ActorActivationFinish) {
1826 if let Err(error) = self.finish_observed(outcome).await {
1827 tracing::warn!(%error, "Root Actor closure unconfirmed");
1828 }
1829 }
1830
1831 async fn finish_observed(
1832 mut self,
1833 outcome: bamboo_domain::ActorActivationFinish,
1834 ) -> std::io::Result<()> {
1835 self.stop_renewal.cancel();
1836 let renewal = match self.renewal.take() {
1837 Some(renewal) => renewal.await.map_err(std::io::Error::other),
1838 None => Ok(()),
1839 };
1840 let directory = self.binding.directory.clone();
1841 let fence = self.binding.owner.fence.clone();
1842 let finish = tokio::spawn(async move {
1843 directory
1844 .finish_activation(&fence, chrono::Utc::now(), outcome)
1845 .await
1846 .map(|_| ())
1847 .map_err(std::io::Error::other)
1848 });
1849 self.binding.disarm_abandonment();
1850 finish.await.map_err(std::io::Error::other)??;
1851 renewal
1852 }
1853}
1854
1855impl Drop for RootActorExecution {
1856 fn drop(&mut self) {
1857 self.stop_renewal.cancel();
1858 }
1861}
1862
1863#[cfg(test)]
1864#[path = "agent_spawn_peer_privacy_tests.rs"]
1865mod peer_privacy_tests;
1866
1867#[cfg(test)]
1868mod reservation_tests {
1869 use super::*;
1870 use crate::runtime::execution::runner_state::AgentStatus;
1871
1872 struct CompletionAtFinalSave {
1873 directory: std::path::PathBuf,
1874 persistence: bamboo_storage::LockedSessionStore,
1875 cleared: std::sync::atomic::AtomicBool,
1876 }
1877
1878 impl CompletionAtFinalSave {
1879 async fn clear_wait(&self, id: &str) {
1880 let mut process = std::process::Command::new(std::env::current_exe().unwrap())
1881 .args(["--exact", "runtime::execution::agent_spawn::reservation_tests::inherited_child_wait_adopts_independent_completion_at_final_save", "--nocapture"])
1882 .env("BAMBOO_FINAL_WAIT_COMPLETION_DIR", &self.directory)
1883 .env("BAMBOO_FINAL_WAIT_COMPLETION_ID", id)
1884 .spawn().unwrap();
1885 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
1886 loop {
1887 if let Some(status) = process.try_wait().unwrap() {
1888 assert!(status.success(), "completion process failed");
1889 break;
1890 }
1891 if std::time::Instant::now() >= deadline {
1892 let _ = process.kill();
1893 let _ = process.wait();
1894 panic!("bounded completion process deadline");
1895 }
1896 std::thread::sleep(std::time::Duration::from_millis(5));
1897 }
1898 self.cleared
1899 .store(true, std::sync::atomic::Ordering::SeqCst);
1900 }
1901 }
1902
1903 #[async_trait::async_trait]
1904 impl bamboo_domain::RuntimeSessionPersistence for CompletionAtFinalSave {
1905 async fn save_runtime_session(&self, session: &mut Session) -> std::io::Result<()> {
1906 self.clear_wait(&session.id).await;
1907 self.persistence.merge_save_finalized_runtime(session).await
1908 }
1909 async fn save_finalized_runtime_with_inherited_child_wait(
1910 &self,
1911 session: &mut Session,
1912 inherited: &bamboo_domain::session::runtime_state::WaitingForChildrenState,
1913 ) -> std::io::Result<()> {
1914 self.clear_wait(&session.id).await;
1915 self.persistence
1916 .merge_save_inherited_child_wait_and_publish(session, inherited, |_, _| {})
1917 .await
1918 }
1919 }
1920
1921 #[tokio::test]
1922 async fn inherited_child_wait_adopts_independent_completion_at_final_save() {
1923 use bamboo_agent_core::storage::Storage;
1924 use bamboo_domain::session::runtime_state::{
1925 AgentRuntimeState, AgentStatusState, ChildWaitPolicy, WaitingForChildrenState,
1926 };
1927 if let Some(directory) = std::env::var_os("BAMBOO_FINAL_WAIT_COMPLETION_DIR") {
1928 let remote = bamboo_storage::SessionStoreV2::new(directory.into())
1929 .await
1930 .unwrap();
1931 let id = std::env::var("BAMBOO_FINAL_WAIT_COMPLETION_ID").unwrap();
1932 let mut latest = remote.load_session(&id).await.unwrap().unwrap();
1933 let runtime = latest.agent_runtime_state.as_mut().unwrap();
1934 runtime.waiting_for_children = None;
1935 runtime.status = AgentStatusState::Idle;
1936 runtime.suspension = None;
1937 latest.metadata.remove("runtime.suspend_reason");
1938 remote.save_runtime_state(&latest).await.unwrap();
1939 return;
1940 }
1941 let temp = tempfile::tempdir().unwrap();
1942 let local = Arc::new(
1943 bamboo_storage::SessionStoreV2::new(temp.path().to_path_buf())
1944 .await
1945 .unwrap(),
1946 );
1947 let mut runner = Session::new("cross-host-final-wait", "model");
1951 let inherited = WaitingForChildrenState::for_children(
1952 vec!["child".into()],
1953 ChildWaitPolicy::All,
1954 chrono::Utc::now(),
1955 );
1956 let mut runtime = AgentRuntimeState::new("run");
1957 runtime.waiting_for_children = Some(inherited.clone());
1958 runtime.status = AgentStatusState::Suspended;
1959 runner.agent_runtime_state = Some(runtime);
1960 runner.metadata.insert(
1961 "runtime.suspend_reason".into(),
1962 "waiting_for_children".into(),
1963 );
1964 runner.set_last_run_status("suspended");
1965 local.save_session(&runner).await.unwrap();
1966 let persistence = CompletionAtFinalSave {
1967 directory: temp.path().to_path_buf(),
1968 persistence: bamboo_storage::LockedSessionStore::new(local.clone()),
1969 cleared: std::sync::atomic::AtomicBool::new(false),
1970 };
1971 save_finalized_runtime_with_inherited_child_wait(
1972 &persistence,
1973 &mut runner,
1974 Some(&inherited),
1975 )
1976 .await
1977 .unwrap();
1978 assert!(persistence
1979 .cleared
1980 .load(std::sync::atomic::Ordering::SeqCst));
1981 let durable = local.load_session(&runner.id).await.unwrap().unwrap();
1982 for session in [&runner, &durable] {
1983 let runtime = session.agent_runtime_state.as_ref().unwrap();
1984 assert!(
1985 runtime.waiting_for_children.is_none(),
1986 "independent completion must not be resurrected"
1987 );
1988 assert_eq!(runtime.status, AgentStatusState::Idle);
1989 assert!(runtime.suspension.is_none());
1990 assert!(!session.metadata.contains_key("runtime.suspend_reason"));
1991 assert_eq!(session.last_run_status().as_deref(), Some("completed"));
1992 }
1993 }
1994
1995 #[tokio::test]
1996 async fn inherited_child_wait_rejects_aba_and_missing_birth_without_cache_publication() {
1997 use bamboo_agent_core::storage::Storage;
1998 use bamboo_domain::session::runtime_state::{
1999 AgentRuntimeState, ChildWaitPolicy, WaitingForChildrenState,
2000 };
2001 use bamboo_storage::{LockedSessionStore, SessionStoreV2};
2002
2003 for missing in [false, true] {
2004 let temp = tempfile::tempdir().unwrap();
2005 let storage = Arc::new(
2006 SessionStoreV2::new(temp.path().to_path_buf())
2007 .await
2008 .unwrap(),
2009 );
2010 let persistence = LockedSessionStore::new(storage.clone());
2011 let cache: SessionCache = Arc::default();
2012 storage
2013 .save_session(&Session::new("root", "model"))
2014 .await
2015 .unwrap();
2016 let mut stale = Session::new("nested-child-parent", "model");
2017 stale.kind = bamboo_domain::SessionKind::Child;
2018 stale.parent_session_id = Some("root".into());
2019 stale.root_session_id = "root".into();
2020 stale.spawn_depth = 1;
2021 let wait = WaitingForChildrenState::for_children(
2022 vec!["grandchild".into()],
2023 ChildWaitPolicy::All,
2024 chrono::Utc::now(),
2025 );
2026 let mut runtime = AgentRuntimeState::new("old-run");
2027 runtime.waiting_for_children = Some(wait.clone());
2028 stale.agent_runtime_state = Some(runtime);
2029 stale.add_message(bamboo_agent_core::Message::assistant("STALE", None));
2030 storage.save_session(&stale).await.unwrap();
2031
2032 let mut replacement = stale.clone();
2033 replacement.created_at += chrono::Duration::seconds(1);
2034 replacement.messages.clear();
2035 replacement.add_message(bamboo_agent_core::Message::assistant("REPLACEMENT", None));
2036 replacement
2037 .agent_runtime_state
2038 .as_mut()
2039 .unwrap()
2040 .waiting_for_children = None;
2041 storage.delete_session(&stale.id).await.unwrap();
2042 if !missing {
2043 storage.save_session(&replacement).await.unwrap();
2044 }
2045 if !missing {
2046 cache.insert(
2047 replacement.id.clone(),
2048 Arc::new(crate::SessionSnapshot::new(replacement.clone())),
2049 );
2050 }
2051
2052 let saved = save_finalized_runtime_with_inherited_child_wait(
2053 &persistence,
2054 &mut stale,
2055 Some(&wait),
2056 )
2057 .await;
2058 assert!(saved.is_err(), "stale incarnation must not be saved");
2059 publish_final_session_cache(&cache, stale, false, &saved);
2062 if missing {
2063 assert!(
2064 read_cached_session(&cache, &replacement.id).is_none(),
2065 "deleted incarnation must not be resurrected in cache"
2066 );
2067 } else {
2068 let cached = read_cached_session(&cache, &replacement.id).unwrap();
2069 assert_eq!(
2070 cached.created_at, replacement.created_at,
2071 "ABA rejection must retain replacement cache birth"
2072 );
2073 assert_eq!(
2074 serde_json::to_value(&cached.messages).unwrap(),
2075 serde_json::to_value(&replacement.messages).unwrap(),
2076 "ABA rejection must retain replacement cache transcript"
2077 );
2078 }
2079 assert!(saved
2080 .as_ref()
2081 .unwrap_err()
2082 .get_ref()
2083 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>()));
2084 let durable = storage.load_session(&replacement.id).await.unwrap();
2085 if missing {
2086 assert!(durable.is_none());
2087 } else {
2088 let durable = durable.unwrap();
2089 assert_eq!(durable.created_at, replacement.created_at);
2090 assert_eq!(
2091 serde_json::to_value(&durable.messages).unwrap(),
2092 serde_json::to_value(&replacement.messages).unwrap()
2093 );
2094 }
2095 }
2096 }
2097
2098 #[test]
2099 fn debug_prompt_snapshot_migrates_legacy_host_paths_before_logging() {
2100 let legacy_project = format!(
2101 "{}\nProject ID: legacy-project\nProject path: /private/legacy-project\nProject home: /private/legacy-home\n{}",
2102 crate::runtime::context::PROJECT_CONTEXT_START_MARKER,
2103 crate::runtime::context::PROJECT_CONTEXT_END_MARKER,
2104 );
2105 let legacy_workspace =
2106 crate::runtime::context::build_workspace_prompt_context("/private/legacy-workspace")
2107 .expect("legacy workspace context");
2108 let legacy_instruction = format!(
2109 "{}\nSource: /private/legacy-workspace/AGENTS.md\nlegacy policy\n{}",
2110 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER,
2111 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_END_MARKER,
2112 );
2113 let mut session = Session::new("legacy-debug-log", "model");
2114 session.add_message(bamboo_agent_core::Message::system(format!(
2115 "Base prompt\n\n{legacy_project}\n\n{legacy_workspace}\n\n{legacy_instruction}"
2116 )));
2117
2118 let prompt = system_prompt_for_debug_log(&mut session).expect("normalized System");
2119
2120 assert_eq!(prompt, "Base prompt");
2121 assert_eq!(
2122 session.workspace_path_meta().as_deref(),
2123 Some("/private/legacy-workspace")
2124 );
2125 for private_path in [
2126 "/private/legacy-project",
2127 "/private/legacy-home",
2128 "/private/legacy-workspace",
2129 ] {
2130 assert!(!prompt.contains(private_path));
2131 }
2132 assert!(!prompt.contains(crate::runtime::context::PROJECT_CONTEXT_START_MARKER));
2133 assert!(!prompt.contains(crate::runtime::context::WORKSPACE_CONTEXT_START_MARKER));
2134 assert!(!prompt
2135 .contains(crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER));
2136 }
2137
2138 #[test]
2139 fn reservation_target_requires_domain_id_and_exact_runner_registry() {
2140 let runners = Arc::new(RwLock::new(HashMap::new()));
2141 let other_runners = Arc::new(RwLock::new(HashMap::new()));
2142 let mut reservation = SessionExecutionReservation {
2143 session_id: "session-a".to_string(),
2144 run_id: "run-a".to_string(),
2145 cancel_token: CancellationToken::new(),
2146 runners: runners.clone(),
2147 activation: SessionExecutionActivationOwnership::Unrouted,
2148 root_actor: None,
2149 physical_admission: None,
2150 execution_persistence: None,
2151 persistence_bound: false,
2152 selected_input: false,
2153 armed: true,
2154 };
2155
2156 assert!(reservation.matches_execution_target("session-a", "session-a", &runners));
2157 assert!(!reservation.matches_execution_target("session-b", "session-a", &runners));
2158 assert!(!reservation.matches_execution_target("session-a", "session-b", &runners));
2159 assert!(!reservation.matches_execution_target("session-a", "session-a", &other_runners));
2160 reservation.armed = false;
2161 }
2162
2163 #[tokio::test]
2164 async fn forced_same_run_registration_rejection_never_cancels_live_owner() {
2165 let runners = Arc::new(RwLock::new(HashMap::new()));
2166 let mut runner = AgentRunner::new();
2167 runner.status = AgentStatus::Running;
2168 let run_id = runner.run_id.clone();
2169 let live_cancel_token = runner.cancel_token.clone();
2170 runners.write().await.insert("same-run".to_string(), runner);
2171
2172 let router = SessionActivationRouter::new();
2173 let mut live_registration = router
2174 .register_run("same-run", &run_id)
2175 .await
2176 .expect("first registration owns the run");
2177 let mut rejected = SessionExecutionReservation {
2178 session_id: "same-run".to_string(),
2179 run_id: run_id.clone(),
2180 cancel_token: live_cancel_token.clone(),
2181 runners: runners.clone(),
2182 activation: SessionExecutionActivationOwnership::RegistrationPending(router.clone()),
2183 root_actor: None,
2184 physical_admission: None,
2185 execution_persistence: None,
2186 persistence_bound: false,
2187 selected_input: false,
2188 armed: true,
2189 };
2190
2191 let error = match rejected.ensure_registered().await {
2192 Ok(()) => panic!("a duplicate registration for the same run must be rejected"),
2193 Err(error) => error,
2194 };
2195 assert_eq!(error.existing_run_id(), run_id);
2196 drop(rejected);
2197
2198 assert!(!live_cancel_token.is_cancelled());
2199 assert!(matches!(
2200 runners
2201 .read()
2202 .await
2203 .get("same-run")
2204 .map(|runner| &runner.status),
2205 Some(AgentStatus::Running)
2206 ));
2207 assert!(router.owns_run("same-run", &run_id).await);
2208 live_registration.begin_finalization().await;
2209 assert_eq!(live_registration.finish(0).await.unwrap(), None);
2210 }
2211}