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, GoldConfig, GuardianConfig,
22 GuardianSpawner, ImageFallbackConfig,
23};
24use crate::runtime::execution::child_completion::ChildCompletion;
25use crate::runtime::execution::runner_lifecycle::{
26 finalize_rejected_runner_if_distinct, finalize_runner, finalize_runner_exact,
27 reserve_runner_core, ReserveOutcome, RunnerReservation,
28};
29use crate::runtime::execution::runner_state::AgentRunner;
30use crate::runtime::model_roster::ModelRoster;
31use crate::runtime::Agent;
32use crate::runtime::{ExecuteRequest, ExecuteRequestBuilder};
33use crate::session_activation::{
34 SessionActivationRouter, SessionRunRegistration, SessionRunRegistrationError,
35};
36
37pub type SessionCache = std::sync::Arc<
45 dashmap::DashMap<String, std::sync::Arc<parking_lot::RwLock<bamboo_agent_core::Session>>>,
46>;
47
48enum SessionExecutionActivationOwnership {
49 Unrouted,
51 UnpublishedActivation(Arc<SessionActivationRouter>),
54 RegistrationPending(Arc<SessionActivationRouter>),
59 Registered(SessionRunRegistration),
62}
63
64pub struct SessionExecutionReservation {
70 session_id: String,
71 run_id: String,
72 cancel_token: CancellationToken,
73 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
74 activation: SessionExecutionActivationOwnership,
75 armed: bool,
76}
77
78impl SessionExecutionReservation {
79 pub fn session_id(&self) -> &str {
80 &self.session_id
81 }
82
83 pub fn run_id(&self) -> &str {
84 &self.run_id
85 }
86
87 pub fn cancel_token(&self) -> &CancellationToken {
88 &self.cancel_token
89 }
90
91 pub(crate) fn from_activation_placeholder(
97 session_id: impl Into<String>,
98 reservation: RunnerReservation,
99 router: Arc<SessionActivationRouter>,
100 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
101 ) -> Self {
102 Self {
103 session_id: session_id.into(),
104 run_id: reservation.run_id,
105 cancel_token: reservation.cancel_token,
106 runners,
107 activation: SessionExecutionActivationOwnership::UnpublishedActivation(router),
108 armed: true,
109 }
110 }
111
112 pub(crate) fn from_pending_registration(
116 session_id: impl Into<String>,
117 reservation: RunnerReservation,
118 router: Option<Arc<SessionActivationRouter>>,
119 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
120 ) -> Self {
121 let activation = match router {
122 Some(router) => SessionExecutionActivationOwnership::RegistrationPending(router),
123 None => SessionExecutionActivationOwnership::Unrouted,
124 };
125 Self {
126 session_id: session_id.into(),
127 run_id: reservation.run_id,
128 cancel_token: reservation.cancel_token,
129 runners,
130 activation,
131 armed: true,
132 }
133 }
134
135 pub(crate) fn mark_activation_published(&mut self) {
138 let activation = std::mem::replace(
139 &mut self.activation,
140 SessionExecutionActivationOwnership::Unrouted,
141 );
142 self.activation = match activation {
143 SessionExecutionActivationOwnership::UnpublishedActivation(router) => {
144 SessionExecutionActivationOwnership::RegistrationPending(router)
145 }
146 other => other,
147 };
148 }
149
150 pub(crate) async fn rollback_unpublished_activation(mut self) {
155 self.armed = false;
156 self.cancel_token.cancel();
157 let activation = std::mem::replace(
158 &mut self.activation,
159 SessionExecutionActivationOwnership::Unrouted,
160 );
161 debug_assert!(matches!(
162 activation,
163 SessionExecutionActivationOwnership::UnpublishedActivation(_)
164 ));
165 remove_runner_exact(&self.runners, &self.session_id, &self.run_id).await;
166 }
167
168 pub async fn ensure_registered(&mut self) -> Result<(), SessionRunRegistrationError> {
174 let router = match &self.activation {
175 SessionExecutionActivationOwnership::RegistrationPending(router) => router.clone(),
176 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
177 unreachable!("unpublished activation entered an execution adapter")
178 }
179 SessionExecutionActivationOwnership::Unrouted
180 | SessionExecutionActivationOwnership::Registered(_) => return Ok(()),
181 };
182 match register_reserved_activation(router, &self.runners, &self.session_id, &self.run_id)
183 .await
184 {
185 Ok(registration) => {
186 self.activation = SessionExecutionActivationOwnership::Registered(registration);
187 Ok(())
188 }
189 Err(error) => {
190 self.disarm_after_registration_rejection(error.existing_run_id());
195 Err(error)
196 }
197 }
198 }
199
200 pub async fn abandon(mut self) {
204 self.armed = false;
205 self.cancel_token.cancel();
206 let activation = std::mem::replace(
207 &mut self.activation,
208 SessionExecutionActivationOwnership::Unrouted,
209 );
210 let runners = self.runners.clone();
211 let session_id = self.session_id.clone();
212 let run_id = self.run_id.clone();
213 let cleanup_session_id = session_id.clone();
214 let cleanup_run_id = run_id.clone();
215 let cleanup = tokio::spawn(async move {
216 cleanup_execution_reservation(activation, runners, cleanup_session_id, cleanup_run_id)
217 .await;
218 });
219 if let Err(error) = cleanup.await {
220 tracing::error!(
221 %session_id,
222 %run_id,
223 %error,
224 "detached execution-reservation cleanup failed"
225 );
226 }
227 }
228
229 fn disarm_after_registration_rejection(&mut self, existing_run_id: &str) {
230 if existing_run_id != self.run_id {
231 self.cancel_token.cancel();
232 }
233 self.armed = false;
234 }
235
236 pub(crate) fn matches_execution_target(
237 &self,
238 session_id: &str,
239 domain_session_id: &str,
240 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
241 ) -> bool {
242 self.session_id == session_id
243 && domain_session_id == session_id
244 && Arc::ptr_eq(&self.runners, runners)
245 }
246
247 pub(crate) fn disarm_for_execution(
248 &mut self,
249 ) -> (CancellationToken, Option<SessionRunRegistration>) {
250 self.armed = false;
251 let registration = match std::mem::replace(
252 &mut self.activation,
253 SessionExecutionActivationOwnership::Unrouted,
254 ) {
255 SessionExecutionActivationOwnership::Registered(registration) => Some(registration),
256 SessionExecutionActivationOwnership::Unrouted => None,
257 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
258 unreachable!("unpublished activation cannot transfer to execution")
259 }
260 SessionExecutionActivationOwnership::RegistrationPending(_) => {
261 unreachable!("ensure_registered adopts every pending router registration")
262 }
263 };
264 (self.cancel_token.clone(), registration)
265 }
266}
267
268impl Drop for SessionExecutionReservation {
269 fn drop(&mut self) {
270 if !self.armed {
271 return;
272 }
273 self.armed = false;
274 self.cancel_token.cancel();
275 let activation = std::mem::replace(
276 &mut self.activation,
277 SessionExecutionActivationOwnership::Unrouted,
278 );
279 let runners = self.runners.clone();
280 let session_id = self.session_id.clone();
281 let run_id = self.run_id.clone();
282 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
283 runtime.spawn(async move {
284 cleanup_execution_reservation(activation, runners, session_id, run_id).await;
285 });
286 }
287 }
288}
289
290pub enum SessionExecutionReserveOutcome {
292 Reserved(SessionExecutionReservation),
293 AlreadyRunning { run_id: String },
294}
295
296async fn register_reserved_activation(
297 router: Arc<SessionActivationRouter>,
298 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
299 session_id: &str,
300 run_id: &str,
301) -> Result<SessionRunRegistration, SessionRunRegistrationError> {
302 let mut registration = match router.register_run(session_id, run_id).await {
303 Ok(registration) => registration,
304 Err(error) => {
305 let collision = Err(AgentError::LLM(error.to_string()));
306 finalize_rejected_runner_if_distinct(
307 runners,
308 session_id,
309 error.existing_run_id(),
310 run_id,
311 &collision,
312 )
313 .await;
314 return Err(error);
315 }
316 };
317 let cleanup_runners = runners.clone();
318 let cleanup_session_id = session_id.to_string();
319 let cleanup_run_id = run_id.to_string();
320 registration.set_abort_cleanup(move || async move {
321 let abandoned = Err(AgentError::Cancelled);
322 finalize_runner_exact(
323 &cleanup_runners,
324 &cleanup_session_id,
325 &cleanup_run_id,
326 &abandoned,
327 )
328 .await;
329 });
330 Ok(registration)
331}
332
333async fn cleanup_execution_reservation(
334 activation: SessionExecutionActivationOwnership,
335 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
336 session_id: String,
337 run_id: String,
338) {
339 match activation {
340 SessionExecutionActivationOwnership::Registered(registration) => {
341 registration.abandon().await;
342 }
343 SessionExecutionActivationOwnership::RegistrationPending(router) => {
344 match register_reserved_activation(router, &runners, &session_id, &run_id).await {
345 Ok(registration) => registration.abandon().await,
346 Err(error) => {
347 tracing::debug!(
348 %session_id,
349 %run_id,
350 existing_run_id = %error.existing_run_id(),
351 "abandoned activation placeholder was already adopted or superseded"
352 );
353 }
354 }
355 }
356 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
357 remove_runner_exact(&runners, &session_id, &run_id).await;
358 }
359 SessionExecutionActivationOwnership::Unrouted => {
360 let abandoned = Err(AgentError::Cancelled);
361 finalize_runner_exact(&runners, &session_id, &run_id, &abandoned).await;
362 }
363 }
364}
365
366async fn remove_runner_exact(
367 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
368 session_id: &str,
369 run_id: &str,
370) -> bool {
371 let mut runners = runners.write().await;
372 if runners
373 .get(session_id)
374 .is_some_and(|runner| runner.run_id == run_id)
375 {
376 runners.remove(session_id);
377 true
378 } else {
379 false
380 }
381}
382
383pub async fn reserve_session_execution(
391 agent: &Arc<Agent>,
392 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
393 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
394 session_id: &str,
395 event_sender: &broadcast::Sender<AgentEvent>,
396) -> SessionExecutionReserveOutcome {
397 let reservation = match reserve_runner_core(runners, senders, session_id, event_sender).await {
398 ReserveOutcome::AlreadyRunning(run_id) => {
399 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
400 }
401 ReserveOutcome::Reserved(reservation) => reservation,
402 };
403
404 let mut execution_reservation = SessionExecutionReservation {
410 session_id: session_id.to_string(),
411 run_id: reservation.run_id,
412 cancel_token: reservation.cancel_token,
413 runners: runners.clone(),
414 activation: SessionExecutionActivationOwnership::Unrouted,
415 armed: true,
416 };
417
418 if let Some(router) = agent.activation_router().cloned() {
419 execution_reservation.activation =
420 SessionExecutionActivationOwnership::RegistrationPending(router);
421 if let Err(error) = execution_reservation.ensure_registered().await {
422 tracing::warn!(
423 %session_id,
424 attempted_run_id = %execution_reservation.run_id(),
425 existing_run_id = %error.existing_run_id(),
426 %error,
427 "runner reservation collided with an existing logical-session owner"
428 );
429 return SessionExecutionReserveOutcome::AlreadyRunning {
430 run_id: error.existing_run_id().to_string(),
431 };
432 }
433 }
434 SessionExecutionReserveOutcome::Reserved(execution_reservation)
435}
436
437pub fn read_cached_session(cache: &SessionCache, id: &str) -> Option<bamboo_agent_core::Session> {
445 cache
446 .get(id)
447 .map(|e| e.value().clone())
448 .map(|a| a.read().clone())
449}
450
451const SKILL_CONTEXT_START_MARKER: &str = "<!-- BAMBOO_SKILL_CONTEXT_START -->";
452const TOOL_GUIDE_START_MARKER: &str = "<!-- BAMBOO_TOOL_GUIDE_START -->";
453const EXTERNAL_MEMORY_START_MARKER: &str = "<!-- BAMBOO_EXTERNAL_MEMORY_START -->";
454const TASK_LIST_START_MARKER: &str = "<!-- BAMBOO_TASK_LIST_START -->";
455
456pub struct SessionExecutionOutcome {
463 pub success: bool,
465 pub cancelled: bool,
467 pub error: Option<String>,
469}
470
471impl SessionExecutionOutcome {
472 fn from_result(result: &Result<(), AgentError>) -> Self {
473 match result {
474 Ok(()) => Self {
475 success: true,
476 cancelled: false,
477 error: None,
478 },
479 Err(error) => Self {
480 success: false,
481 cancelled: error.is_cancelled(),
482 error: Some(error.to_string()),
483 },
484 }
485 }
486}
487
488pub type SessionCompletionHook = Box<
498 dyn for<'a> FnOnce(
499 SessionExecutionOutcome,
500 &'a mut Session,
501 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
502 + Send,
503>;
504
505pub struct SessionExecutionArgs {
511 pub agent: Arc<Agent>,
513 pub session_id: String,
514 pub session: Session,
515 pub execution_reservation: SessionExecutionReservation,
518
519 pub tools_override: Option<Arc<dyn ToolExecutor>>,
521 pub provider_override: Option<Arc<dyn LLMProvider>>,
522 pub model_roster: ModelRoster,
526 pub reasoning_effort: Option<ReasoningEffort>,
527 pub reasoning_effort_source: String,
528 pub auxiliary_model_resolver:
529 Option<Arc<dyn Fn() -> crate::runtime::config::AuxiliaryModelConfig + Send + Sync>>,
530 pub disabled_filter_resolver:
534 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
535 pub disabled_tools: Option<BTreeSet<String>>,
536 pub disabled_skill_ids: Option<BTreeSet<String>>,
537 pub selected_skill_ids: Option<Vec<String>>,
538 pub selected_skill_mode: Option<String>,
539 pub mpsc_tx: mpsc::Sender<AgentEvent>,
540 pub image_fallback: Option<ImageFallbackConfig>,
541 pub gold_config: Option<GoldConfig>,
542 pub guardian_config: Option<GuardianConfig>,
544 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
547 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
549 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
551 pub app_data_dir: Option<std::path::PathBuf>,
552 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
555
556 pub runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
558 pub sessions_cache: SessionCache,
559
560 pub on_complete: Option<SessionCompletionHook>,
563
564 pub child_completion_handler: Option<Arc<dyn super::ChildCompletionHandler>>,
576}
577
578struct ExecuteRequestParams {
588 tools: Option<Arc<dyn ToolExecutor>>,
589 provider_override: Option<Arc<dyn LLMProvider>>,
590 model_roster: ModelRoster,
591 reasoning_effort: Option<ReasoningEffort>,
592 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
593 disabled_filter_resolver:
594 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
595 disabled_tools: Option<BTreeSet<String>>,
596 disabled_skill_ids: Option<BTreeSet<String>>,
597 selected_skill_ids: Option<Vec<String>>,
598 selected_skill_mode: Option<String>,
599 image_fallback: Option<ImageFallbackConfig>,
600 gold_config: Option<GoldConfig>,
601 guardian_config: Option<GuardianConfig>,
602 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
603 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
604 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
605 app_data_dir: Option<std::path::PathBuf>,
606 run_budget: Option<bamboo_config::RunBudgetConfig>,
607}
608
609fn build_execute_request(
616 initial_message: String,
617 event_tx: mpsc::Sender<AgentEvent>,
618 cancel_token: CancellationToken,
619 params: ExecuteRequestParams,
620) -> ExecuteRequest {
621 let ExecuteRequestParams {
622 tools,
623 provider_override,
624 model_roster,
625 reasoning_effort,
626 auxiliary_model_resolver,
627 disabled_filter_resolver,
628 disabled_tools,
629 disabled_skill_ids,
630 selected_skill_ids,
631 selected_skill_mode,
632 image_fallback,
633 gold_config,
634 guardian_config,
635 guardian_spawner,
636 bash_resume_hook,
637 bash_completion_sink,
638 app_data_dir,
639 run_budget,
640 } = params;
641
642 let mut builder = ExecuteRequestBuilder::new(initial_message, event_tx, cancel_token)
643 .model_roster(model_roster)
644 .gold_config(gold_config)
645 .guardian_config(guardian_config)
646 .guardian_spawner(guardian_spawner)
647 .bash_resume_hook(bash_resume_hook)
648 .bash_completion_sink(bash_completion_sink);
649
650 if let Some(run_budget) = run_budget {
651 builder = builder.run_budget(run_budget);
652 }
653
654 if let Some(tools) = tools {
655 builder = builder.tools(tools);
656 }
657 if let Some(provider_override) = provider_override {
658 builder = builder.provider_override(provider_override);
659 }
660 if let Some(reasoning_effort) = reasoning_effort {
661 builder = builder.reasoning_effort(reasoning_effort);
662 }
663 if let Some(disabled_filter_resolver) = disabled_filter_resolver {
664 builder = builder.disabled_filter_resolver(disabled_filter_resolver);
665 }
666 if let Some(auxiliary_model_resolver) = auxiliary_model_resolver {
667 builder = builder.auxiliary_model_resolver(auxiliary_model_resolver);
668 }
669 if let Some(disabled_tools) = disabled_tools {
670 builder = builder.disabled_tools(disabled_tools);
671 }
672 if let Some(disabled_skill_ids) = disabled_skill_ids {
673 builder = builder.disabled_skill_ids(disabled_skill_ids);
674 }
675 if let Some(selected_skill_ids) = selected_skill_ids {
676 builder = builder.selected_skill_ids(selected_skill_ids);
677 }
678 if let Some(selected_skill_mode) = selected_skill_mode {
679 builder = builder.selected_skill_mode(selected_skill_mode);
680 }
681 if let Some(image_fallback) = image_fallback {
682 builder = builder.image_fallback(image_fallback);
683 }
684 if let Some(app_data_dir) = app_data_dir {
685 builder = builder.app_data_dir(app_data_dir);
686 }
687
688 builder.build()
689}
690
691pub fn spawn_session_execution(args: SessionExecutionArgs) {
700 let span_session_id = args.session_id.clone();
701 let session_span = tracing::info_span!("agent_execution", session_id = %span_session_id);
702
703 tokio::spawn(
704 async move {
705 let SessionExecutionArgs {
706 agent,
707 session_id,
708 mut session,
709 mut execution_reservation,
710 tools_override,
711 provider_override,
712 model_roster,
713 reasoning_effort,
714 reasoning_effort_source,
715 auxiliary_model_resolver,
716 disabled_filter_resolver,
717 disabled_tools,
718 disabled_skill_ids,
719 selected_skill_ids,
720 selected_skill_mode,
721 mpsc_tx,
722 image_fallback,
723 gold_config,
724 guardian_config,
725 guardian_spawner,
726 bash_resume_hook,
727 bash_completion_sink,
728 app_data_dir,
729 run_budget,
730 runners,
731 sessions_cache,
732 on_complete,
733 child_completion_handler,
734 } = args;
735
736 if !execution_reservation.matches_execution_target(&session_id, &session.id, &runners) {
737 tracing::error!(
738 %session_id,
739 domain_session_id = %session.id,
740 reservation_session_id = %execution_reservation.session_id(),
741 run_id = %execution_reservation.run_id(),
742 same_runner_registry = Arc::ptr_eq(&execution_reservation.runners, &runners),
743 "refusing mismatched session execution reservation"
744 );
745 execution_reservation.abandon().await;
746 return;
747 }
748 if let Err(error) = execution_reservation.ensure_registered().await {
749 tracing::warn!(
750 %session_id,
751 run_id = %execution_reservation.run_id(),
752 %error,
753 "session execution could not adopt its router activation owner"
754 );
755 return;
756 }
757 let (cancel_token, mut activation_registration) =
758 execution_reservation.disarm_for_execution();
759
760 let model = model_roster.model.clone().unwrap_or_default();
764
765 let initial_message = initial_user_message_for_session(&session);
766 let selected_skill_ids =
767 selected_skill_ids.or_else(|| selected_skill_ids_for_session(&session));
768 let selected_skill_mode =
769 selected_skill_mode.or_else(|| selected_skill_mode_for_session(&session));
770
771 tracing::info!(
772 "[{}] Using resolved session model: {}, reasoning_effort={}, reasoning_source={}",
773 session_id,
774 model,
775 reasoning_effort
776 .map(ReasoningEffort::as_str)
777 .unwrap_or("none"),
778 reasoning_effort_source
779 );
780
781 crate::session_app::execution_prep::prepare_session_for_execution(
788 &mut session,
789 None,
790 Some(&model),
791 );
792
793 let system_prompt = system_prompt_for_session(&session);
794 if let Some(prompt) = system_prompt.as_ref() {
795 log_base_system_prompt_snapshot(&session_id, prompt);
796 }
797
798 let execute_request = build_execute_request(
799 initial_message,
800 mpsc_tx.clone(),
801 cancel_token,
802 ExecuteRequestParams {
803 tools: tools_override,
804 provider_override,
805 model_roster,
806 reasoning_effort,
807 auxiliary_model_resolver,
808 disabled_filter_resolver,
809 disabled_tools,
810 disabled_skill_ids,
811 selected_skill_ids,
812 selected_skill_mode,
813 image_fallback,
814 gold_config,
815 guardian_config,
816 guardian_spawner,
817 bash_resume_hook,
818 bash_completion_sink,
819 app_data_dir,
820 run_budget,
821 },
822 );
823
824 let result = {
831 use futures::FutureExt;
832 match std::panic::AssertUnwindSafe(agent.execute(&mut session, execute_request))
833 .catch_unwind()
834 .await
835 {
836 Ok(result) => result,
837 Err(panic) => {
838 let message = panic
839 .downcast_ref::<&str>()
840 .map(|s| (*s).to_string())
841 .or_else(|| panic.downcast_ref::<String>().cloned())
842 .unwrap_or_else(|| "non-string panic payload".to_string());
843 tracing::error!(
844 "[{}] agent execution panicked; finalizing as terminal error: {}",
845 session_id,
846 message
847 );
848 Err(AgentError::LLM(format!(
849 "agent execution panicked: {message}"
850 )))
851 }
852 }
853 };
854
855 if let Some(error_event) = terminal_error_event_for_result(&result) {
857 let _ = mpsc_tx.send(error_event).await;
858 }
859
860 let suspended_non_terminal = result.is_ok()
877 && session
878 .metadata
879 .get("runtime.suspend_reason")
880 .is_some_and(|reason| !reason.trim().is_empty());
881 match &result {
882 Ok(()) if suspended_non_terminal => {
883 session.set_last_run_status("suspended");
884 session.clear_last_run_error();
885 }
886 Ok(()) => {
887 session.set_last_run_status("completed");
888 session.clear_last_run_error();
889 }
890 Err(error) if error.is_cancelled() => {
891 session.set_last_run_status("cancelled");
892 session.set_last_run_error(error.to_string());
893 }
894 Err(error) => {
895 session.set_last_run_status("error");
896 session.set_last_run_error(error.to_string());
897 }
898 }
899
900 if let Some(on_complete) = on_complete {
904 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
905 }
906
907 let executed_admitted_generation = session
911 .session_inbox_admission()
912 .map_or(0, |state| state.last_admitted_sequence);
913 let legacy_migration =
914 crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
915 &mut session,
916 Some(agent.storage()),
917 Some(agent.persistence()),
918 agent.session_inbox(),
919 )
920 .await;
921 let pending_boundary_generation = session
922 .session_inbox_admission()
923 .and_then(|state| state.pending_activation_generation());
924 let pending_generation = match (
925 pending_boundary_generation,
926 legacy_migration.highest_generation,
927 ) {
928 (Some(left), Some(right)) => Some(left.max(right)),
929 (left, right) => left.or(right),
930 };
931 if let (Some(generation), Some(router)) =
932 (pending_generation, agent.activation_router())
933 {
934 let activation_ready = if let Some(inbox) = agent.session_inbox() {
935 match inbox
936 .mark_activation_eligible(
937 &session_id,
938 generation,
939 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
940 )
941 .await
942 {
943 Ok(()) => true,
944 Err(error) => {
945 tracing::error!(
946 session_id = %session_id,
947 %error,
948 "failed to persist unadmitted SessionInbox activation watermark"
949 );
950 false
951 }
952 }
953 } else {
954 false
955 };
956 if activation_ready {
957 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
958 router.as_ref(),
959 &session_id,
960 generation,
961 )
962 .await
963 {
964 tracing::error!(
965 session_id = %session_id,
966 %error,
967 "failed to hand unadmitted SessionInbox generation to activation router"
968 );
969 }
970 }
971 }
972
973 if let Some(registration) = activation_registration.as_mut() {
977 registration.begin_finalization().await;
978 }
979
980 if let Err(error) = agent.persistence().save_runtime_session(&mut session).await {
984 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
985 }
986
987 finalize_runner(&runners, &session_id, &result).await;
993
994 let finalization = if let Some(registration) = activation_registration.take() {
995 registration.finish(executed_admitted_generation).await
996 } else {
997 Ok(None)
998 };
999 if let Err(error) = finalization {
1000 tracing::error!(
1004 session_id = %session_id,
1005 %error,
1006 "failed to activate successor for finalization-racing SessionInbox delivery"
1007 );
1008 }
1009
1010 let child_completion = child_completion_handler.filter(|_| {
1019 session.kind == bamboo_agent_core::SessionKind::Child
1020 && session.parent_session_id.is_some()
1021 });
1022 let parent_session_id = session.parent_session_id.clone();
1023 let child_status = session.last_run_status();
1024 let child_error = session.last_run_error();
1025
1026 sessions_cache.insert(
1028 session_id.clone(),
1029 Arc::new(parking_lot::RwLock::new(session)),
1030 );
1031
1032 if let (Some(handler), Some(parent_session_id), Some(status)) =
1033 (child_completion, parent_session_id, child_status)
1034 {
1035 use futures::FutureExt;
1036 let completion = ChildCompletion {
1037 parent_session_id: parent_session_id.clone(),
1038 child_session_id: session_id.clone(),
1039 status,
1040 error: child_error,
1041 completed_at: chrono::Utc::now(),
1042 };
1043 if std::panic::AssertUnwindSafe(handler.on_child_completed(completion))
1044 .catch_unwind()
1045 .await
1046 .is_err()
1047 {
1048 tracing::error!(
1049 %parent_session_id,
1050 child_session_id = %session_id,
1051 "child completion handler panicked on resumed-child terminal"
1052 );
1053 }
1054 }
1055
1056 tracing::info!("[{}] Agent execution completed", session_id);
1057 }
1058 .instrument(session_span),
1059 );
1060}
1061
1062pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
1064 tracing::info!(
1065 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
1066 session_id,
1067 prompt.len(),
1068 prompt.contains(SKILL_CONTEXT_START_MARKER),
1069 prompt.contains(TOOL_GUIDE_START_MARKER),
1070 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
1071 prompt.contains(TASK_LIST_START_MARKER),
1072 );
1073
1074 tracing::debug!(
1075 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
1076 session_id
1077 );
1078 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
1079 tracing::debug!("[{}] -----------------------------------", session_id);
1080 tracing::debug!("[{}] {}", session_id, prompt);
1081 tracing::debug!(
1082 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
1083 session_id
1084 );
1085}
1086
1087pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
1089 match result {
1090 Ok(_) => None,
1091 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
1092 message: "Agent execution cancelled by user".to_string(),
1093 }),
1094 Err(error) => Some(AgentEvent::Error {
1095 message: error.to_string(),
1096 }),
1097 }
1098}
1099
1100fn system_prompt_for_session(session: &Session) -> Option<String> {
1103 session
1104 .messages
1105 .iter()
1106 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
1107 .map(|message| message.content.clone())
1108}
1109
1110fn initial_user_message_for_session(session: &Session) -> String {
1111 session
1112 .messages
1113 .last()
1114 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
1115 .map(|message| message.content.clone())
1116 .unwrap_or_default()
1117}
1118
1119fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
1120 session
1121 .metadata
1122 .get("selected_skill_ids")
1123 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
1124}
1125
1126fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
1127 let value = session
1128 .metadata
1129 .get("skill_mode")
1130 .or_else(|| session.metadata.get("mode"))?;
1131 let trimmed = value.trim();
1132 if trimmed.is_empty() {
1133 None
1134 } else {
1135 Some(trimmed.to_string())
1136 }
1137}
1138
1139#[cfg(test)]
1140mod reservation_tests {
1141 use super::*;
1142 use crate::runtime::execution::runner_state::AgentStatus;
1143
1144 #[test]
1145 fn reservation_target_requires_domain_id_and_exact_runner_registry() {
1146 let runners = Arc::new(RwLock::new(HashMap::new()));
1147 let other_runners = Arc::new(RwLock::new(HashMap::new()));
1148 let mut reservation = SessionExecutionReservation {
1149 session_id: "session-a".to_string(),
1150 run_id: "run-a".to_string(),
1151 cancel_token: CancellationToken::new(),
1152 runners: runners.clone(),
1153 activation: SessionExecutionActivationOwnership::Unrouted,
1154 armed: true,
1155 };
1156
1157 assert!(reservation.matches_execution_target("session-a", "session-a", &runners));
1158 assert!(!reservation.matches_execution_target("session-b", "session-a", &runners));
1159 assert!(!reservation.matches_execution_target("session-a", "session-b", &runners));
1160 assert!(!reservation.matches_execution_target("session-a", "session-a", &other_runners));
1161 reservation.armed = false;
1162 }
1163
1164 #[tokio::test]
1165 async fn forced_same_run_registration_rejection_never_cancels_live_owner() {
1166 let runners = Arc::new(RwLock::new(HashMap::new()));
1167 let mut runner = AgentRunner::new();
1168 runner.status = AgentStatus::Running;
1169 let run_id = runner.run_id.clone();
1170 let live_cancel_token = runner.cancel_token.clone();
1171 runners.write().await.insert("same-run".to_string(), runner);
1172
1173 let router = SessionActivationRouter::new();
1174 let mut live_registration = router
1175 .register_run("same-run", &run_id)
1176 .await
1177 .expect("first registration owns the run");
1178 let mut rejected = SessionExecutionReservation {
1179 session_id: "same-run".to_string(),
1180 run_id: run_id.clone(),
1181 cancel_token: live_cancel_token.clone(),
1182 runners: runners.clone(),
1183 activation: SessionExecutionActivationOwnership::RegistrationPending(router.clone()),
1184 armed: true,
1185 };
1186
1187 let error = match rejected.ensure_registered().await {
1188 Ok(()) => panic!("a duplicate registration for the same run must be rejected"),
1189 Err(error) => error,
1190 };
1191 assert_eq!(error.existing_run_id(), run_id);
1192 drop(rejected);
1193
1194 assert!(!live_cancel_token.is_cancelled());
1195 assert!(matches!(
1196 runners
1197 .read()
1198 .await
1199 .get("same-run")
1200 .map(|runner| &runner.status),
1201 Some(AgentStatus::Running)
1202 ));
1203 assert!(router.owns_run("same-run", &run_id).await);
1204 live_registration.begin_finalization().await;
1205 assert_eq!(live_registration.finish(0).await.unwrap(), None);
1206 }
1207}