Skip to main content

vv_agent/
interactive.rs

1//! Embedded, stateful sessions built on the public runner primitives.
2
3mod runtime_support;
4
5use std::collections::{BTreeMap, VecDeque};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard};
8
9use serde_json::Value;
10use thiserror::Error;
11use tokio::sync::broadcast;
12
13use crate::agent::Agent;
14use crate::approval::{ApprovalBroker, ApprovalError};
15use crate::events::RunEvent;
16use crate::result::RunResult;
17use crate::run_config::RunConfig;
18use crate::run_handle::{RunHandle, RunHandleController};
19use crate::runner::Runner;
20use crate::runtime::{
21    background_session_manager, AfterToolCallEvent, BackgroundSessionListener,
22    BackgroundSessionSubscription, BeforeLlmEvent, BeforeLlmPatch, BeforeToolCallEvent,
23    BeforeToolCallPatch, CancellationToken, RuntimeHook,
24};
25use crate::sessions::{MemorySession, Session, SessionItem};
26use crate::tools::ApprovalDecision;
27use crate::types::{AgentStatus, Message, Metadata, ToolExecutionResult, ToolResultStatus};
28
29use runtime_support::{
30    build_background_command_notification, lock_queue, lock_unpoisoned, normalized_prompt,
31    resolve_session, select_session_source, InteractiveRunHandleController, RunEventForwarder,
32    RunLifecycleGuard, SteeringRuntimeHook,
33};
34
35type SteeringQueue = Arc<Mutex<VecDeque<String>>>;
36
37#[derive(Debug, Error)]
38pub enum InteractiveSessionError {
39    #[error("{operation} prompt cannot be empty")]
40    EmptyPrompt { operation: &'static str },
41    #[error("interactive session `{session_id}` is already running; use steer() or follow_up()")]
42    AlreadyRunning { session_id: String },
43    #[error("interactive session `{session_id}` is closed")]
44    Closed { session_id: String },
45    #[error("interactive session `{session_id}` has no queued prompt")]
46    NoQueuedPrompt { session_id: String },
47    #[error("session id cannot be empty")]
48    EmptySessionId,
49    #[error("requested session id `{requested}` does not match storage session id `{actual}`")]
50    SessionIdMismatch { requested: String, actual: String },
51    #[error("session storage failed: {0}")]
52    Session(String),
53    #[error("runner failed for interactive session `{session_id}`: {error}")]
54    Run { session_id: String, error: String },
55    #[error("session query failed with status {status:?}: {reason}")]
56    QueryFailed { status: AgentStatus, reason: String },
57    #[error("interactive session event subscriber lagged by {missed} event(s)")]
58    EventGap { missed: u64 },
59    #[error("interactive session event stream is closed")]
60    EventStreamClosed,
61    #[error("no interactive session event available")]
62    EventStreamEmpty,
63}
64
65#[derive(Clone)]
66pub struct InteractiveSessionOptions {
67    pub session_id: Option<String>,
68    pub session: Option<Arc<dyn Session>>,
69    pub run_config: RunConfig,
70    pub shared_state: Metadata,
71    pub event_buffer_capacity: usize,
72}
73
74impl InteractiveSessionOptions {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
80        self.session_id = Some(session_id.into());
81        self
82    }
83
84    pub fn session(mut self, session: impl Session + 'static) -> Self {
85        self.session = Some(Arc::new(session));
86        self
87    }
88
89    pub fn session_arc(mut self, session: Arc<dyn Session>) -> Self {
90        self.session = Some(session);
91        self
92    }
93
94    pub fn run_config(mut self, run_config: RunConfig) -> Self {
95        self.run_config = run_config;
96        self
97    }
98
99    pub fn shared_state(mut self, shared_state: Metadata) -> Self {
100        self.shared_state = shared_state;
101        self
102    }
103
104    pub fn event_buffer_capacity(mut self, capacity: usize) -> Self {
105        self.event_buffer_capacity = capacity.max(1);
106        self
107    }
108}
109
110impl Default for InteractiveSessionOptions {
111    fn default() -> Self {
112        Self {
113            session_id: None,
114            session: None,
115            run_config: RunConfig::default(),
116            shared_state: Metadata::new(),
117            event_buffer_capacity: 256,
118        }
119    }
120}
121
122#[derive(Debug, Clone, PartialEq)]
123#[non_exhaustive]
124pub enum InteractiveSessionEvent {
125    RunStarted {
126        session_id: String,
127        prompt: String,
128        existing_messages: usize,
129    },
130    ActiveHandleChanged {
131        session_id: String,
132        active: bool,
133    },
134    RunEvent {
135        session_id: String,
136        event: Box<RunEvent>,
137    },
138    RunEventStreamError {
139        session_id: String,
140        error: String,
141    },
142    SteerQueued {
143        session_id: String,
144        prompt: String,
145    },
146    SteerDequeued {
147        session_id: String,
148        prompt: String,
149        cycle_index: Option<u32>,
150    },
151    FollowUpQueued {
152        session_id: String,
153        prompt: String,
154    },
155    FollowUpDequeued {
156        session_id: String,
157        prompt: String,
158    },
159    QueuesCleared {
160        session_id: String,
161    },
162    CancelRequested {
163        session_id: String,
164    },
165    MessagesReplaced {
166        session_id: String,
167        message_count: usize,
168    },
169    SharedStateReplaced {
170        session_id: String,
171    },
172    RunFinished {
173        session_id: String,
174        run_id: String,
175        status: AgentStatus,
176        final_output: Option<String>,
177    },
178    RunFailed {
179        session_id: String,
180        error: String,
181    },
182    RunAborted {
183        session_id: String,
184    },
185    SessionClosed {
186        session_id: String,
187        aborted: bool,
188    },
189    BackgroundCommandTerminal {
190        session_id: String,
191        background_session_id: String,
192        status: String,
193        notification_message: String,
194        queued_to_session: bool,
195        queued_to_running_session: bool,
196    },
197}
198
199pub struct InteractiveSessionSubscription {
200    receiver: broadcast::Receiver<InteractiveSessionEvent>,
201    closed: Arc<AtomicBool>,
202}
203
204#[derive(Clone)]
205pub struct InteractiveSessionState {
206    pub session_id: String,
207    pub running: bool,
208    pub closed: bool,
209    pub messages: Vec<Message>,
210    pub shared_state: Metadata,
211    pub latest_run: Option<RunResult>,
212    pub active_handle: Option<RunHandle>,
213    pub pending_steering: usize,
214    pub pending_follow_ups: usize,
215}
216
217#[derive(Clone)]
218pub struct InteractiveAgentClient {
219    runner: Runner,
220}
221
222impl InteractiveAgentClient {
223    pub fn new(runner: Runner) -> Self {
224        Self { runner }
225    }
226
227    pub fn runner(&self) -> &Runner {
228        &self.runner
229    }
230
231    pub async fn create_session(
232        &self,
233        agent: Agent,
234        options: InteractiveSessionOptions,
235    ) -> Result<InteractiveSession, InteractiveSessionError> {
236        InteractiveSession::create(self.runner.clone(), agent, options).await
237    }
238}
239
240#[derive(Clone)]
241pub struct InteractiveSession {
242    inner: Arc<InteractiveSessionInner>,
243}
244
245struct InteractiveSessionInner {
246    runner: Runner,
247    agent: Agent,
248    session_id: String,
249    session: Arc<dyn Session>,
250    approval_broker: ApprovalBroker,
251    run_config: RunConfig,
252    steering: SteeringQueue,
253    state: Mutex<InteractiveSessionData>,
254    operation_gate: tokio::sync::Mutex<()>,
255    events: broadcast::Sender<InteractiveSessionEvent>,
256    closed: Arc<AtomicBool>,
257    background_commands: Mutex<BTreeMap<String, Option<BackgroundSessionSubscription>>>,
258}
259
260struct InteractiveSessionData {
261    running: bool,
262    closed: bool,
263    messages: Vec<Message>,
264    shared_state: Metadata,
265    latest_run: Option<RunResult>,
266    active_cancellation_token: Option<CancellationToken>,
267    active_handle: Option<RunHandle>,
268    active_handle_controller: Option<u64>,
269    follow_ups: VecDeque<String>,
270}
271
272impl InteractiveSession {
273    async fn create(
274        runner: Runner,
275        agent: Agent,
276        options: InteractiveSessionOptions,
277    ) -> Result<Self, InteractiveSessionError> {
278        let InteractiveSessionOptions {
279            session_id: requested_session_id,
280            session: option_session,
281            mut run_config,
282            mut shared_state,
283            event_buffer_capacity,
284        } = options;
285        let configured_session = select_session_source(option_session, run_config.session.clone())?;
286        let (session_id, session) = resolve_session(requested_session_id, configured_session)?;
287        run_config.session = Some(session.clone());
288        let approval_broker = run_config.approval_broker.clone().unwrap_or_default();
289        run_config.approval_broker = Some(approval_broker.clone());
290        let messages = session
291            .get_items(None)
292            .await
293            .map_err(InteractiveSessionError::Session)?
294            .into_iter()
295            .map(|item| item.to_message())
296            .collect();
297        shared_state
298            .entry("todo_list".to_string())
299            .or_insert_with(|| serde_json::Value::Array(Vec::new()));
300        let (events, _) = broadcast::channel(event_buffer_capacity.max(1));
301        let closed = Arc::new(AtomicBool::new(false));
302
303        Ok(Self {
304            inner: Arc::new(InteractiveSessionInner {
305                runner,
306                agent,
307                session_id,
308                session,
309                approval_broker,
310                run_config,
311                steering: SteeringQueue::default(),
312                state: Mutex::new(InteractiveSessionData {
313                    running: false,
314                    closed: false,
315                    messages,
316                    shared_state,
317                    latest_run: None,
318                    active_cancellation_token: None,
319                    active_handle: None,
320                    active_handle_controller: None,
321                    follow_ups: VecDeque::new(),
322                }),
323                operation_gate: tokio::sync::Mutex::new(()),
324                events,
325                closed,
326                background_commands: Mutex::new(BTreeMap::new()),
327            }),
328        })
329    }
330
331    pub fn session_id(&self) -> &str {
332        &self.inner.session_id
333    }
334
335    pub fn agent_name(&self) -> &str {
336        self.inner.agent.name()
337    }
338
339    pub fn session(&self) -> Arc<dyn Session> {
340        self.inner.session.clone()
341    }
342
343    pub fn subscribe(&self) -> InteractiveSessionSubscription {
344        InteractiveSessionSubscription::new(
345            self.inner.events.subscribe(),
346            self.inner.closed.clone(),
347        )
348    }
349
350    pub fn messages(&self) -> Vec<Message> {
351        self.lock_state().messages.clone()
352    }
353
354    pub fn shared_state(&self) -> Metadata {
355        self.lock_state().shared_state.clone()
356    }
357
358    pub fn latest_run(&self) -> Option<RunResult> {
359        self.lock_state().latest_run.clone()
360    }
361
362    pub fn running(&self) -> bool {
363        self.lock_state().running
364    }
365
366    pub fn closed(&self) -> bool {
367        self.lock_state().closed
368    }
369
370    pub fn active_run_handle(&self) -> Option<RunHandle> {
371        self.lock_state().active_handle.clone()
372    }
373
374    pub fn state(&self) -> InteractiveSessionState {
375        let state = self.lock_state();
376        InteractiveSessionState {
377            session_id: self.inner.session_id.clone(),
378            running: state.running,
379            closed: state.closed,
380            messages: state.messages.clone(),
381            shared_state: state.shared_state.clone(),
382            latest_run: state.latest_run.clone(),
383            active_handle: state.active_handle.clone(),
384            pending_steering: lock_queue(&self.inner.steering).len(),
385            pending_follow_ups: state.follow_ups.len(),
386        }
387    }
388
389    pub fn close(&self) -> bool {
390        let (was_running, token, handle, controller) = {
391            let mut state = self.lock_state();
392            if state.closed {
393                return false;
394            }
395            state.closed = true;
396            let was_running = state.running;
397            let token = state.active_cancellation_token.take();
398            let handle = state.active_handle.take();
399            let controller = state.active_handle_controller.take();
400            state.follow_ups.clear();
401            (was_running, token, handle, controller)
402        };
403
404        lock_queue(&self.inner.steering).clear();
405        lock_unpoisoned(&self.inner.background_commands).clear();
406        if let Some(token) = token {
407            token.cancel_with_reason("interactive session closed");
408        }
409        if let Some(handle) = handle {
410            if let Some(controller) = controller {
411                handle.detach_controller(controller);
412            }
413            handle.cancel_with_reason("interactive session closed");
414            self.emit(InteractiveSessionEvent::ActiveHandleChanged {
415                session_id: self.inner.session_id.clone(),
416                active: false,
417            });
418        }
419        let _ = self
420            .inner
421            .approval_broker
422            .cancel_pending("interactive session closed");
423        self.emit(InteractiveSessionEvent::SessionClosed {
424            session_id: self.inner.session_id.clone(),
425            aborted: was_running,
426        });
427        self.inner.closed.store(true, Ordering::SeqCst);
428        true
429    }
430
431    pub fn steer(&self, prompt: impl Into<String>) -> Result<(), InteractiveSessionError> {
432        let prompt = normalized_prompt(prompt, "steer")?;
433        self.ensure_open()?;
434        lock_queue(&self.inner.steering).push_back(prompt.clone());
435        self.emit(InteractiveSessionEvent::SteerQueued {
436            session_id: self.inner.session_id.clone(),
437            prompt,
438        });
439        Ok(())
440    }
441
442    pub fn follow_up(&self, prompt: impl Into<String>) -> Result<(), InteractiveSessionError> {
443        let prompt = normalized_prompt(prompt, "follow_up")?;
444        self.ensure_open()?;
445        self.lock_state().follow_ups.push_back(prompt.clone());
446        self.emit(InteractiveSessionEvent::FollowUpQueued {
447            session_id: self.inner.session_id.clone(),
448            prompt,
449        });
450        Ok(())
451    }
452
453    pub fn clear_queues(&self) -> Result<(), InteractiveSessionError> {
454        self.ensure_open()?;
455        lock_queue(&self.inner.steering).clear();
456        self.lock_state().follow_ups.clear();
457        self.emit(InteractiveSessionEvent::QueuesCleared {
458            session_id: self.inner.session_id.clone(),
459        });
460        Ok(())
461    }
462
463    pub fn approve(
464        &self,
465        request_id: impl AsRef<str>,
466        decision: ApprovalDecision,
467    ) -> Result<(), ApprovalError> {
468        if self.closed() {
469            return Err(ApprovalError::new(format!(
470                "interactive session `{}` is closed",
471                self.inner.session_id
472            )));
473        }
474        self.inner.approval_broker.resolve(request_id, decision)
475    }
476
477    pub fn cancel(&self) -> bool {
478        let (token, handle) = {
479            let mut state = self.lock_state();
480            let Some(token) = state.active_cancellation_token.clone() else {
481                return false;
482            };
483            if !state.running || token.is_cancelled() {
484                return false;
485            }
486            state.follow_ups.clear();
487            (token, state.active_handle.clone())
488        };
489        lock_queue(&self.inner.steering).clear();
490        if let Some(handle) = handle {
491            handle.cancel();
492        }
493        token.cancel();
494        self.emit(InteractiveSessionEvent::CancelRequested {
495            session_id: self.inner.session_id.clone(),
496        });
497        true
498    }
499
500    pub async fn prompt(
501        &self,
502        prompt: impl Into<String>,
503    ) -> Result<RunResult, InteractiveSessionError> {
504        let prompt = normalized_prompt(prompt, "prompt")?;
505        self.ensure_open()?;
506        let _operation = self.try_operation()?;
507        self.run_prompt_chain(prompt, true).await
508    }
509
510    pub async fn prompt_once(
511        &self,
512        prompt: impl Into<String>,
513    ) -> Result<RunResult, InteractiveSessionError> {
514        let prompt = normalized_prompt(prompt, "prompt")?;
515        self.ensure_open()?;
516        let _operation = self.try_operation()?;
517        self.run_prompt_chain(prompt, false).await
518    }
519
520    pub async fn continue_run(
521        &self,
522        prompt: Option<&str>,
523    ) -> Result<RunResult, InteractiveSessionError> {
524        self.ensure_open()?;
525        let _operation = self.try_operation()?;
526        let prompt = match prompt.map(str::trim).filter(|value| !value.is_empty()) {
527            Some(prompt) => prompt.to_string(),
528            None => self.pop_queued_prompt()?,
529        };
530        self.run_prompt_chain(prompt, false).await
531    }
532
533    pub async fn query(
534        &self,
535        prompt: impl Into<String>,
536    ) -> Result<String, InteractiveSessionError> {
537        let result = self.prompt(prompt).await?;
538        if result.status() == AgentStatus::Completed {
539            return Ok(result.final_output().unwrap_or_default().to_string());
540        }
541        Err(InteractiveSessionError::QueryFailed {
542            status: result.status(),
543            reason: result
544                .result()
545                .error
546                .clone()
547                .or_else(|| result.result().wait_reason.clone())
548                .or_else(|| result.final_output().map(str::to_string))
549                .unwrap_or_else(|| "session query did not complete".to_string()),
550        })
551    }
552
553    pub async fn replace_messages(
554        &self,
555        messages: Vec<Message>,
556    ) -> Result<(), InteractiveSessionError> {
557        self.ensure_open()?;
558        let _operation = self.try_operation()?;
559        self.ensure_not_running()?;
560        let items = messages
561            .iter()
562            .filter_map(SessionItem::from_message)
563            .collect::<Vec<_>>();
564        self.inner
565            .session
566            .clear()
567            .await
568            .map_err(InteractiveSessionError::Session)?;
569        self.inner
570            .session
571            .add_items(items)
572            .await
573            .map_err(InteractiveSessionError::Session)?;
574        self.lock_state().messages = messages.clone();
575        self.emit(InteractiveSessionEvent::MessagesReplaced {
576            session_id: self.inner.session_id.clone(),
577            message_count: messages.len(),
578        });
579        Ok(())
580    }
581
582    pub async fn replace_shared_state(
583        &self,
584        mut shared_state: Metadata,
585    ) -> Result<(), InteractiveSessionError> {
586        self.ensure_open()?;
587        let _operation = self.try_operation()?;
588        self.ensure_not_running()?;
589        shared_state
590            .entry("todo_list".to_string())
591            .or_insert_with(|| serde_json::Value::Array(Vec::new()));
592        self.lock_state().shared_state = shared_state;
593        self.emit(InteractiveSessionEvent::SharedStateReplaced {
594            session_id: self.inner.session_id.clone(),
595        });
596        Ok(())
597    }
598
599    async fn run_prompt_chain(
600        &self,
601        first_prompt: String,
602        auto_follow_up: bool,
603    ) -> Result<RunResult, InteractiveSessionError> {
604        let mut lifecycle = RunLifecycleGuard::begin(self.inner.clone())?;
605        let mut prompt = first_prompt;
606        let outcome = loop {
607            match self.execute_once(prompt).await {
608                Ok(result) => {
609                    if auto_follow_up && result.status() == AgentStatus::Completed {
610                        if let Some(follow_up) = self.pop_follow_up() {
611                            prompt = follow_up;
612                            continue;
613                        }
614                    }
615                    break Ok(result);
616                }
617                Err(error) => break Err(error),
618            }
619        };
620        lifecycle.finish();
621        outcome
622    }
623
624    async fn execute_once(&self, prompt: String) -> Result<RunResult, InteractiveSessionError> {
625        let existing_messages = self.lock_state().messages.len();
626        self.emit(InteractiveSessionEvent::RunStarted {
627            session_id: self.inner.session_id.clone(),
628            prompt: prompt.clone(),
629            existing_messages,
630        });
631
632        let mut config = self.inner.run_config.clone();
633        self.inner
634            .approval_broker
635            .reset_cancelled()
636            .map_err(|error| self.run_error(error.to_string()))?;
637        config.session = Some(self.inner.session.clone());
638        config.cancellation_token = self.lock_state().active_cancellation_token.clone();
639        config.approval_broker = Some(self.inner.approval_broker.clone());
640        config
641            .initial_shared_state
642            .extend(self.lock_state().shared_state.clone());
643        config.metadata.insert(
644            "session_id".to_string(),
645            serde_json::Value::String(self.inner.session_id.clone()),
646        );
647        config.hooks.push(Arc::new(SteeringRuntimeHook {
648            queue: self.inner.steering.clone(),
649            session_id: self.inner.session_id.clone(),
650            events: self.inner.events.clone(),
651            inner: Arc::downgrade(&self.inner),
652        }));
653
654        let handle = match self
655            .inner
656            .runner
657            .start(&self.inner.agent, prompt, config)
658            .await
659        {
660            Ok(handle) => handle,
661            Err(error) => return Err(self.run_error(error)),
662        };
663        self.set_active_handle(Some(handle.clone()));
664        let mut event_forwarder = self.forward_run_events(handle.events());
665        let result = handle.result().await;
666        self.set_active_handle(None);
667        match event_forwarder.finish().await {
668            Ok(()) => {}
669            Err(error) => self.emit(InteractiveSessionEvent::RunEventStreamError {
670                session_id: self.inner.session_id.clone(),
671                error: format!("run event forwarding task failed: {error}"),
672            }),
673        }
674
675        if self.closed() {
676            return Err(InteractiveSessionError::Closed {
677                session_id: self.inner.session_id.clone(),
678            });
679        }
680
681        let result = match result {
682            Ok(result) => result,
683            Err(error) => return Err(self.run_error(error)),
684        };
685        self.sync_background_command_watchers(&result);
686        let messages = self
687            .inner
688            .session
689            .get_items(None)
690            .await
691            .map_err(InteractiveSessionError::Session)?
692            .into_iter()
693            .map(|item| item.to_message())
694            .collect::<Vec<_>>();
695        {
696            let mut state = self.lock_state();
697            state.messages = messages;
698            state.shared_state = result.result().shared_state.clone();
699            state.latest_run = Some(result.clone());
700        }
701        self.emit(InteractiveSessionEvent::RunFinished {
702            session_id: self.inner.session_id.clone(),
703            run_id: result.run_id().to_string(),
704            status: result.status(),
705            final_output: result.final_output().map(str::to_string),
706        });
707        Ok(result)
708    }
709
710    fn sync_background_command_watchers(&self, result: &RunResult) {
711        for cycle in &result.result().cycles {
712            for (index, tool_result) in cycle.tool_results.iter().enumerate() {
713                let tool_name = cycle
714                    .tool_calls
715                    .iter()
716                    .find(|call| call.id == tool_result.tool_call_id)
717                    .or_else(|| cycle.tool_calls.get(index))
718                    .map(|call| call.name.trim().to_ascii_lowercase());
719                if let Some(tool_name) = tool_name {
720                    self.sync_background_command_result(&tool_name, tool_result);
721                }
722            }
723        }
724    }
725
726    fn sync_background_command_result(&self, tool_name: &str, tool_result: &ToolExecutionResult) {
727        if !matches!(
728            tool_name.trim().to_ascii_lowercase().as_str(),
729            "bash" | "check_background_command"
730        ) {
731            return;
732        }
733        let Some(background_session_id) = tool_result
734            .metadata
735            .get("session_id")
736            .and_then(Value::as_str)
737            .map(str::trim)
738            .filter(|value| !value.is_empty())
739        else {
740            return;
741        };
742        let status = tool_result
743            .metadata
744            .get("status")
745            .and_then(Value::as_str)
746            .unwrap_or_default()
747            .trim()
748            .to_ascii_lowercase();
749        if status == "running" || tool_result.status == ToolResultStatus::Running {
750            self.subscribe_background_command(background_session_id);
751        } else if matches!(
752            status.as_str(),
753            "completed" | "failed" | "timeout" | "missing"
754        ) {
755            self.unsubscribe_background_command(background_session_id);
756        }
757    }
758
759    fn subscribe_background_command(&self, background_session_id: &str) {
760        let background_session_id = background_session_id.trim();
761        if background_session_id.is_empty() {
762            return;
763        }
764        {
765            let mut subscriptions = lock_unpoisoned(&self.inner.background_commands);
766            if subscriptions.contains_key(background_session_id) {
767                return;
768            }
769            subscriptions.insert(background_session_id.to_string(), None);
770        }
771
772        let weak_inner = Arc::downgrade(&self.inner);
773        let callback_session_id = background_session_id.to_string();
774        let listener: BackgroundSessionListener = Arc::new(move |payload| {
775            if let Some(inner) = weak_inner.upgrade() {
776                InteractiveSession::handle_background_command_terminal(
777                    &inner,
778                    &callback_session_id,
779                    payload,
780                );
781            }
782        });
783        let subscription = background_session_manager().subscribe(background_session_id, listener);
784        let mut subscriptions = lock_unpoisoned(&self.inner.background_commands);
785        if let Some(slot) = subscriptions.get_mut(background_session_id) {
786            *slot = Some(subscription);
787        }
788    }
789
790    fn unsubscribe_background_command(&self, background_session_id: &str) {
791        lock_unpoisoned(&self.inner.background_commands).remove(background_session_id.trim());
792    }
793
794    fn handle_background_command_terminal(
795        inner: &Arc<InteractiveSessionInner>,
796        background_session_id: &str,
797        payload: &Value,
798    ) {
799        if inner.closed.load(Ordering::SeqCst) {
800            return;
801        }
802        lock_unpoisoned(&inner.background_commands).remove(background_session_id);
803        let notification_message = build_background_command_notification(payload);
804        let state = lock_unpoisoned(&inner.state);
805        let running = state.running && !state.closed;
806        drop(state);
807        if running {
808            lock_queue(&inner.steering).push_back(notification_message.clone());
809            let _ = inner.events.send(InteractiveSessionEvent::SteerQueued {
810                session_id: inner.session_id.clone(),
811                prompt: notification_message.clone(),
812            });
813        }
814        let _ = inner
815            .events
816            .send(InteractiveSessionEvent::BackgroundCommandTerminal {
817                session_id: inner.session_id.clone(),
818                background_session_id: background_session_id.to_string(),
819                status: payload
820                    .get("status")
821                    .and_then(Value::as_str)
822                    .unwrap_or("terminal")
823                    .trim()
824                    .to_ascii_lowercase(),
825                notification_message,
826                queued_to_session: running,
827                queued_to_running_session: running,
828            });
829    }
830
831    fn forward_run_events(&self, mut stream: crate::runner::RunEventStream) -> RunEventForwarder {
832        let events = self.inner.events.clone();
833        let session_id = self.inner.session_id.clone();
834        let closed = self.inner.closed.clone();
835        RunEventForwarder::new(tokio::spawn(async move {
836            while let Some(event) = stream.next().await {
837                if closed.load(Ordering::SeqCst) {
838                    break;
839                }
840                match event {
841                    Ok(event) => {
842                        let event = if event.session_id().is_some() {
843                            event
844                        } else {
845                            event.with_session_id(session_id.clone())
846                        };
847                        let _ = events.send(InteractiveSessionEvent::RunEvent {
848                            session_id: session_id.clone(),
849                            event: Box::new(event),
850                        });
851                    }
852                    Err(error) => {
853                        let _ = events.send(InteractiveSessionEvent::RunEventStreamError {
854                            session_id: session_id.clone(),
855                            error,
856                        });
857                    }
858                }
859            }
860        }))
861    }
862
863    fn pop_follow_up(&self) -> Option<String> {
864        let prompt = self.lock_state().follow_ups.pop_front()?;
865        self.emit(InteractiveSessionEvent::FollowUpDequeued {
866            session_id: self.inner.session_id.clone(),
867            prompt: prompt.clone(),
868        });
869        Some(prompt)
870    }
871
872    fn pop_queued_prompt(&self) -> Result<String, InteractiveSessionError> {
873        if let Some(prompt) = lock_queue(&self.inner.steering).pop_front() {
874            self.emit(InteractiveSessionEvent::SteerDequeued {
875                session_id: self.inner.session_id.clone(),
876                prompt: prompt.clone(),
877                cycle_index: None,
878            });
879            return Ok(prompt);
880        }
881        self.pop_follow_up()
882            .ok_or_else(|| InteractiveSessionError::NoQueuedPrompt {
883                session_id: self.inner.session_id.clone(),
884            })
885    }
886
887    fn ensure_not_running(&self) -> Result<(), InteractiveSessionError> {
888        self.ensure_open()?;
889        if self.running() {
890            return Err(InteractiveSessionError::AlreadyRunning {
891                session_id: self.inner.session_id.clone(),
892            });
893        }
894        Ok(())
895    }
896
897    fn ensure_open(&self) -> Result<(), InteractiveSessionError> {
898        if self.closed() {
899            return Err(InteractiveSessionError::Closed {
900                session_id: self.inner.session_id.clone(),
901            });
902        }
903        Ok(())
904    }
905
906    fn try_operation(&self) -> Result<tokio::sync::MutexGuard<'_, ()>, InteractiveSessionError> {
907        self.inner
908            .operation_gate
909            .try_lock()
910            .map_err(|_| InteractiveSessionError::AlreadyRunning {
911                session_id: self.inner.session_id.clone(),
912            })
913    }
914
915    fn set_active_handle(&self, handle: Option<RunHandle>) {
916        let controller = handle.as_ref().map(|handle| {
917            handle.attach_controller(Arc::new(InteractiveRunHandleController {
918                inner: Arc::downgrade(&self.inner),
919                session_id: self.inner.session_id.clone(),
920            }))
921        });
922        let (previous_handle, previous_controller, rejected, changed) = {
923            let mut state = self.lock_state();
924            if state.closed && handle.is_some() {
925                (None, None, true, false)
926            } else {
927                let changed = state.active_handle.is_some() != handle.is_some();
928                let previous_handle = std::mem::replace(&mut state.active_handle, handle.clone());
929                let previous_controller =
930                    std::mem::replace(&mut state.active_handle_controller, controller);
931                (previous_handle, previous_controller, false, changed)
932            }
933        };
934        if rejected {
935            if let (Some(handle), Some(controller)) = (handle, controller) {
936                handle.detach_controller(controller);
937                handle.cancel_with_reason("interactive session closed");
938            }
939            return;
940        }
941        if let (Some(previous_handle), Some(previous_controller)) =
942            (previous_handle, previous_controller)
943        {
944            previous_handle.detach_controller(previous_controller);
945        }
946        if changed {
947            self.emit(InteractiveSessionEvent::ActiveHandleChanged {
948                session_id: self.inner.session_id.clone(),
949                active: handle.is_some(),
950            });
951        }
952    }
953
954    fn run_error(&self, error: String) -> InteractiveSessionError {
955        self.emit(InteractiveSessionEvent::RunFailed {
956            session_id: self.inner.session_id.clone(),
957            error: error.clone(),
958        });
959        InteractiveSessionError::Run {
960            session_id: self.inner.session_id.clone(),
961            error,
962        }
963    }
964
965    fn emit(&self, event: InteractiveSessionEvent) {
966        if self.inner.closed.load(Ordering::SeqCst)
967            && !matches!(&event, InteractiveSessionEvent::SessionClosed { .. })
968        {
969            return;
970        }
971        let _ = self.inner.events.send(event);
972    }
973
974    fn lock_state(&self) -> MutexGuard<'_, InteractiveSessionData> {
975        lock_unpoisoned(&self.inner.state)
976    }
977}
978
979pub async fn create_interactive_session(
980    runner: &Runner,
981    agent: Agent,
982    options: InteractiveSessionOptions,
983) -> Result<InteractiveSession, InteractiveSessionError> {
984    InteractiveSession::create(runner.clone(), agent, options).await
985}