Skip to main content

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}