bamboo_engine/session_app/resume.rs
1//! Resume execution use case.
2//!
3//! Provides the application-layer logic for resuming agent execution on an
4//! existing session (e.g. after a user responds to a pending question).
5//! The server layer implements `ResumeExecutionPort` to supply the
6//! infrastructure operations.
7
8use async_trait::async_trait;
9use bamboo_agent_core::AgentEvent;
10use bamboo_domain::Session;
11use tokio::sync::broadcast;
12
13use super::execute::{consume_pending_clarification_resume, has_pending_user_message};
14use super::types::{ResumeConfigSnapshot, ResumeOutcome};
15use crate::execution::{SessionExecutionReservation, SessionExecutionReserveOutcome};
16
17// ---------------------------------------------------------------------------
18// Port trait
19// ---------------------------------------------------------------------------
20
21/// Adapter trait for resume execution infrastructure.
22///
23/// Implementations bridge the use case to server-specific concerns
24/// (storage, runner lifecycle, agent spawning).
25#[async_trait]
26pub trait ResumeExecutionPort: Send + Sync {
27 /// Load a session by ID. Returns `None` if not found.
28 async fn load_session(&self, session_id: &str) -> Option<Session>;
29
30 /// Persist a session and update any caches.
31 ///
32 /// Implementations may merge concurrent UI edits to title/pinned/title_version
33 /// from disk back into `session` (which is why this takes `&mut`).
34 async fn save_and_cache_session(&self, session: &mut Session);
35
36 /// Reserve the shared runner/router ownership for the given session.
37 async fn reserve_session_execution(
38 &self,
39 session_id: &str,
40 event_sender: &broadcast::Sender<AgentEvent>,
41 ) -> SessionExecutionReserveOutcome;
42
43 /// Get or create the long-lived broadcast sender for session events.
44 async fn get_or_create_event_sender(&self, session_id: &str) -> broadcast::Sender<AgentEvent>;
45
46 /// Spawn the resume execution loop in the background.
47 ///
48 /// The adapter creates the mpsc channel, spawns the event forwarder,
49 /// and calls the server's agent execution spawner.
50 async fn spawn_resume_execution(&self, request: ResumeSpawnRequest);
51}
52
53// ---------------------------------------------------------------------------
54// Value types
55// ---------------------------------------------------------------------------
56
57/// Request captured for the adapter to spawn a resumed agent execution.
58///
59/// This bundles everything the server-side spawner needs, keeping the
60/// application layer free of `AppState` and server-specific types.
61pub struct ResumeSpawnRequest {
62 pub session_id: String,
63 pub session: Session,
64 pub execution_reservation: SessionExecutionReservation,
65 pub event_sender: broadcast::Sender<AgentEvent>,
66 pub config: ResumeConfigSnapshot,
67}
68
69// ---------------------------------------------------------------------------
70// Use case
71// ---------------------------------------------------------------------------
72
73/// Resume agent execution on an existing session.
74///
75/// Returns the outcome of the resume attempt:
76/// - `Started` — execution spawned successfully
77/// - `AlreadyRunning` — a runner is already active
78/// - `Completed` — no pending user message
79/// - `NotFound` — session not found
80pub async fn resume_session_execution(
81 port: &dyn ResumeExecutionPort,
82 session_id: &str,
83 config: ResumeConfigSnapshot,
84) -> ResumeOutcome {
85 // Load session.
86 let Some(mut session) = port.load_session(session_id).await else {
87 return ResumeOutcome::NotFound;
88 };
89
90 if !has_pending_user_message(&session) {
91 return ResumeOutcome::Completed;
92 }
93
94 // Reserve runner slot.
95 let event_sender = port.get_or_create_event_sender(session_id).await;
96 let reservation = match port
97 .reserve_session_execution(session_id, &event_sender)
98 .await
99 {
100 SessionExecutionReserveOutcome::Reserved(reservation) => reservation,
101 SessionExecutionReserveOutcome::AlreadyRunning { run_id } => {
102 return ResumeOutcome::AlreadyRunning { run_id };
103 }
104 };
105 let run_id = reservation.run_id().to_string();
106
107 consume_pending_clarification_resume(&mut session);
108 port.save_and_cache_session(&mut session).await;
109
110 port.spawn_resume_execution(ResumeSpawnRequest {
111 session_id: session_id.to_string(),
112 session,
113 execution_reservation: reservation,
114 event_sender,
115 config,
116 })
117 .await;
118
119 ResumeOutcome::Started { run_id }
120}