1mod 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}