1use std::process::ExitCode;
36use std::sync::{Arc, Mutex};
37
38use futures::FutureExt;
39use futures::future::BoxFuture;
40use tokio::sync::oneshot;
41
42use crate::cli::bootstrap::{AppMode, Dispatched};
43use crate::core::agent_session_runtime::AgentSessionRuntime;
44use crate::core::output_guard;
45
46pub trait ModeDispatch: Send + Sync {
56 fn run_interactive(
62 &self,
63 dispatched: Dispatched,
64 runtime: Arc<AgentSessionRuntime>,
65 ) -> BoxFuture<'_, Result<u8, String>>;
66
67 fn run_print(
74 &self,
75 dispatched: Dispatched,
76 runtime: Arc<AgentSessionRuntime>,
77 ) -> BoxFuture<'_, Result<u8, String>>;
78
79 fn run_rpc(
85 &self,
86 dispatched: Dispatched,
87 runtime: Arc<AgentSessionRuntime>,
88 ) -> BoxFuture<'_, Result<u8, String>>;
89}
90
91pub async fn run_mode_default(dispatched: Dispatched, handler: &dyn ModeDispatch) -> ExitCode {
98 run_mode_with_codes(dispatched, handler, SignalCodes::default()).await
99}
100
101#[derive(Clone, Copy, Debug)]
103pub struct SignalCodes {
104 pub sigterm: u8,
106 pub sighup: Option<u8>,
108}
109
110impl Default for SignalCodes {
111 fn default() -> Self {
112 Self {
113 sigterm: defaults::SIGTERM,
114 #[cfg(unix)]
115 sighup: Some(defaults::SIGHUP),
116 #[cfg(not(unix))]
117 sighup: None,
118 }
119 }
120}
121
122pub mod defaults {
124 pub const STDIN_EOF: u8 = 0;
126 pub const SIGTERM: u8 = 143;
128 pub const SIGHUP: u8 = 129;
130}
131
132pub async fn run_mode_with_codes(
135 dispatched: Dispatched,
136 handler: &dyn ModeDispatch,
137 codes: SignalCodes,
138) -> ExitCode {
139 let mode = dispatched.mode;
140 let runtime = dispatched.handle.runtime.clone();
141
142 let signal = SignalRelay::install(codes);
143 let signal_rx = signal.take_rx();
144
145 let mode_fut = match mode {
146 AppMode::Interactive => handler
147 .run_interactive(dispatched, Arc::clone(&runtime))
148 .boxed(),
149 AppMode::Print | AppMode::Json => {
150 handler.run_print(dispatched, Arc::clone(&runtime)).boxed()
151 }
152 AppMode::Rpc => handler.run_rpc(dispatched, Arc::clone(&runtime)).boxed(),
153 };
154
155 let result = tokio::select! {
156 biased;
157 code = async {
158 match signal_rx {
159 Some(rx) => rx.await.unwrap_or(defaults::STDIN_EOF),
160 None => defaults::STDIN_EOF,
161 }
162 } => Ok(code),
163 outcome = mode_fut => outcome,
164 };
165
166 signal.cancel().await;
167
168 if !mode.is_interactive() {
170 output_guard::restore_stdout();
171 }
172
173 runtime.dispose().await;
175
176 let exit_code = match result {
177 Ok(code) => code,
178 Err(message) => {
179 output_guard::ProductOutput::writeln(&format!("Error: {message}"));
180 1
181 }
182 };
183 ExitCode::from(exit_code)
184}
185
186struct SignalRelay {
189 sender: Mutex<Option<oneshot::Sender<u8>>>,
191 cancel: tokio_util::sync::CancellationToken,
193 receiver: Mutex<Option<oneshot::Receiver<u8>>>,
195}
196
197pub struct SignalRelayHandle {
200 relay: Arc<SignalRelay>,
201}
202
203impl SignalRelayHandle {
204 fn take_rx(&self) -> Option<oneshot::Receiver<u8>> {
207 self.relay
208 .receiver
209 .lock()
210 .unwrap_or_else(std::sync::PoisonError::into_inner)
211 .take()
212 }
213
214 async fn cancel(&self) {
216 self.relay.cancel.cancel();
217 tokio::task::yield_now().await;
219 }
220}
221
222impl SignalRelay {
223 fn install(codes: SignalCodes) -> SignalRelayHandle {
225 let (tx, rx) = oneshot::channel::<u8>();
226 let relay = Arc::new(SignalRelay {
227 sender: Mutex::new(Some(tx)),
228 cancel: tokio_util::sync::CancellationToken::new(),
229 receiver: Mutex::new(Some(rx)),
230 });
231
232 let int_relay = Arc::clone(&relay);
234 let int_cancel = relay.cancel.clone();
235 tokio::spawn(async move {
236 tokio::select! {
237 biased;
238 () = int_cancel.cancelled() => {}
239 res = tokio::signal::ctrl_c() => {
240 if let Ok(()) = res {
241 fire(&int_relay, codes.sigterm);
242 }
243 }
244 }
245 });
246
247 #[cfg(unix)]
248 {
249 use tokio::signal::unix::{SignalKind, signal};
250 if let Ok(mut stream) = signal(SignalKind::terminate()) {
252 let term_relay = Arc::clone(&relay);
253 let term_cancel = relay.cancel.clone();
254 let term_code = codes.sigterm;
255 tokio::spawn(async move {
256 tokio::select! {
257 biased;
258 () = term_cancel.cancelled() => {}
259 _ = stream.recv() => {
260 fire(&term_relay, term_code);
261 }
262 }
263 });
264 }
265 if let Some(hup_code) = codes.sighup
267 && let Ok(mut stream) = signal(SignalKind::hangup())
268 {
269 let hup_relay = Arc::clone(&relay);
270 let hup_cancel = relay.cancel.clone();
271 tokio::spawn(async move {
272 tokio::select! {
273 biased;
274 () = hup_cancel.cancelled() => {}
275 _ = stream.recv() => {
276 fire(&hup_relay, hup_code);
277 }
278 }
279 });
280 }
281 }
282
283 #[cfg(not(unix))]
284 {
285 let _ = codes;
286 }
287
288 SignalRelayHandle { relay }
289 }
290}
291
292fn fire(relay: &Arc<SignalRelay>, code: u8) {
294 let sender = {
295 let mut guard = relay
296 .sender
297 .lock()
298 .unwrap_or_else(std::sync::PoisonError::into_inner);
299 guard.take()
300 };
301 if let Some(tx) = sender {
302 let _ = tx.send(code);
303 }
304}
305
306use crate::core::agent_session::AgentSessionEvent;
311use crate::core::agent_session::prompt::PromptOptions;
312use crate::modes::print::{OutputGuardSink, PrintModeOptions, PrintOutput, run_print_mode};
313use tokio::sync::mpsc;
314
315pub async fn run_print_session(
331 dispatched: Dispatched,
332 runtime: Arc<AgentSessionRuntime>,
333) -> Result<u8, String> {
334 let print_output = if dispatched.mode.is_json() {
335 PrintOutput::Json
336 } else {
337 PrintOutput::Text
338 };
339 let session = runtime.session();
340
341 let _ = session
345 .bind_extensions(crate::core::agent_session::ExtensionBindings {
346 mode: Some(if print_output.is_json() {
347 crate::core::agent_session::ExtensionMode::Json
348 } else {
349 crate::core::agent_session::ExtensionMode::Print
350 }),
351 ..Default::default()
352 })
353 .await;
354
355 let header = if print_output.is_json() {
357 let sm = session.session_manager();
358 let sm_guard = sm.lock().await;
359 let sm = sm_guard;
360 sm.get_header().cloned()
361 } else {
362 None
363 };
364
365 let (event_tx, event_rx) = mpsc::unbounded_channel::<AgentSessionEvent>();
367 let unsubscribe = session.subscribe(move |event: &AgentSessionEvent| {
368 let _ = event_tx.send(event.clone());
369 });
370
371 let source_str = if print_output.is_json() {
372 "json"
373 } else {
374 "print"
375 };
376 let source_string = source_str.to_owned();
377 let initial_images = dispatched.initial_images.clone();
378 let initial_message = dispatched.initial_message.clone();
379 let remaining_messages = dispatched.remaining_messages.clone();
380 let session_for_prompts = Arc::clone(&session);
381
382 let prompt_driver = move || async move {
383 if let Some(initial) = initial_message.as_deref() {
384 let opts = PromptOptions {
385 images: initial_images.clone(),
386 source: Some(source_string.clone()),
387 ..PromptOptions::default()
388 };
389 if let Err(err) = session_for_prompts.prompt(initial, opts).await {
390 return Err(std::io::Error::other(format!("{err}")));
391 }
392 }
393 for msg in &remaining_messages {
394 let opts = PromptOptions {
395 source: Some(source_string.clone()),
396 ..PromptOptions::default()
397 };
398 if let Err(err) = session_for_prompts.prompt(msg, opts).await {
399 return Err(std::io::Error::other(format!("{err}")));
400 }
401 }
402 Ok(())
403 };
404
405 let options = PrintModeOptions {
406 mode: print_output,
407 messages: Vec::new(),
408 initial_message: dispatched.initial_message.clone(),
409 initial_images: dispatched.initial_images.clone(),
410 };
411
412 let event_stream = Box::pin(futures::stream::unfold(event_rx, |mut rx| async move {
415 rx.recv().await.map(|event| (event, rx))
416 }));
417
418 let exit_code = run_print_mode(
419 &options,
420 header.as_ref(),
421 event_stream,
422 prompt_driver,
423 unsubscribe,
424 &OutputGuardSink,
425 )
426 .await
427 .map_err(|e| format!("{e}"))?;
428
429 Ok(u8::try_from(exit_code).unwrap_or(1))
430}
431
432pub type ModeRunner = dyn Fn(Dispatched, Arc<AgentSessionRuntime>) -> BoxFuture<'static, Result<u8, String>>
434 + Send
435 + Sync;
436
437pub struct DefaultDispatcher {
443 pub rpc: Option<Arc<ModeRunner>>,
445 pub interactive: Option<Arc<ModeRunner>>,
447}
448
449impl DefaultDispatcher {
450 #[must_use]
452 pub fn new() -> Self {
453 Self {
454 rpc: None,
455 interactive: None,
456 }
457 }
458
459 #[must_use]
461 pub fn with_rpc<F>(mut self, f: F) -> Self
462 where
463 F: Fn(Dispatched, Arc<AgentSessionRuntime>) -> BoxFuture<'static, Result<u8, String>>
464 + Send
465 + Sync
466 + 'static,
467 {
468 self.rpc = Some(Arc::new(f));
469 self
470 }
471
472 #[must_use]
474 pub fn with_interactive<F>(mut self, f: F) -> Self
475 where
476 F: Fn(Dispatched, Arc<AgentSessionRuntime>) -> BoxFuture<'static, Result<u8, String>>
477 + Send
478 + Sync
479 + 'static,
480 {
481 self.interactive = Some(Arc::new(f));
482 self
483 }
484}
485
486impl Default for DefaultDispatcher {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492impl ModeDispatch for DefaultDispatcher {
493 fn run_interactive(
494 &self,
495 dispatched: Dispatched,
496 runtime: Arc<AgentSessionRuntime>,
497 ) -> BoxFuture<'_, Result<u8, String>> {
498 match &self.interactive {
499 Some(runner) => runner(dispatched, runtime),
500 None => Box::pin(async move {
501 Err("interactive mode requires a TTY terminal".to_owned())
504 }),
505 }
506 }
507
508 fn run_print(
509 &self,
510 dispatched: Dispatched,
511 runtime: Arc<AgentSessionRuntime>,
512 ) -> BoxFuture<'_, Result<u8, String>> {
513 Box::pin(async move { run_print_session(dispatched, runtime).await })
514 }
515
516 fn run_rpc(
517 &self,
518 dispatched: Dispatched,
519 runtime: Arc<AgentSessionRuntime>,
520 ) -> BoxFuture<'_, Result<u8, String>> {
521 match &self.rpc {
522 Some(runner) => runner(dispatched, runtime),
523 None => {
524 Box::pin(async move { Err("rpc mode requires the RPC server runner".to_owned()) })
525 }
526 }
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::sync::Mutex as StdMutex;
534
535 #[derive(Default)]
537 struct FakeDispatcher {
538 print_code: StdMutex<Option<u8>>,
539 calls: StdMutex<Vec<&'static str>>,
540 }
541
542 impl ModeDispatch for Arc<FakeDispatcher> {
543 fn run_interactive(
544 &self,
545 _dispatched: Dispatched,
546 _runtime: Arc<AgentSessionRuntime>,
547 ) -> BoxFuture<'_, Result<u8, String>> {
548 let result = self
549 .calls
550 .lock()
551 .map_err(|error| format!("record interactive call: {error}"))
552 .map(|mut calls| calls.push("interactive"));
553 Box::pin(async move {
554 result?;
555 Ok(0)
556 })
557 }
558 fn run_print(
559 &self,
560 _dispatched: Dispatched,
561 _runtime: Arc<AgentSessionRuntime>,
562 ) -> BoxFuture<'_, Result<u8, String>> {
563 let result = self
564 .calls
565 .lock()
566 .map_err(|error| format!("record print call: {error}"))
567 .and_then(|mut calls| {
568 calls.push("print");
569 self.print_code
570 .lock()
571 .map_err(|error| format!("read print exit code: {error}"))
572 .map(|code| code.unwrap_or(0))
573 });
574 Box::pin(async move { result })
575 }
576 fn run_rpc(
577 &self,
578 _dispatched: Dispatched,
579 _runtime: Arc<AgentSessionRuntime>,
580 ) -> BoxFuture<'_, Result<u8, String>> {
581 let result = self
582 .calls
583 .lock()
584 .map_err(|error| format!("record RPC call: {error}"))
585 .map(|mut calls| calls.push("rpc"));
586 Box::pin(async move {
587 result?;
588 Ok(0)
589 })
590 }
591 }
592
593 #[test]
594 fn defaults_match_reference() {
595 assert_eq!(defaults::STDIN_EOF, 0);
596 assert_eq!(defaults::SIGTERM, 143);
597 assert_eq!(defaults::SIGHUP, 129);
598 }
599
600 #[test]
601 fn signal_codes_default() {
602 let codes = SignalCodes::default();
603 assert_eq!(codes.sigterm, 143);
604 #[cfg(unix)]
605 assert_eq!(codes.sighup, Some(129));
606 }
607
608 #[test]
609 fn fire_is_first_wins() -> Result<(), String> {
610 let (tx, _rx) = oneshot::channel::<u8>();
611 let relay = Arc::new(SignalRelay {
612 sender: Mutex::new(Some(tx)),
613 cancel: tokio_util::sync::CancellationToken::new(),
614 receiver: Mutex::new(None),
615 });
616 fire(&relay, 143);
617 assert!(
619 relay
620 .sender
621 .lock()
622 .map_err(|error| format!("inspect signal sender: {error}"))?
623 .is_none()
624 );
625 fire(&relay, 129);
627 assert!(
628 relay
629 .sender
630 .lock()
631 .map_err(|error| format!("inspect signal sender after second fire: {error}"))?
632 .is_none()
633 );
634 Ok(())
635 }
636}