Skip to main content

objectiveai_cli_sdk/output/
handle.rs

1//! Destinations for [`super::Output::emit`].
2//!
3//! - [`Handle::Stdout`] — this process's stdout (with fatal-error
4//!   mirror to stderr).
5//! - [`Handle::Stdin`] — a child process's stdin, for programmatic
6//!   embedders that spawn a subprocess to consume the cli's output.
7//! - [`Handle::Collect`] — an in-memory shared `Vec`, for tests or
8//!   in-process embedders that want the events without a subprocess.
9
10use std::sync::Arc;
11
12use serde::Serialize;
13use tokio::process::ChildStdin;
14use tokio::sync::Mutex;
15
16use super::{Notification, Output};
17
18/// Destination for [`super::Output::emit`].
19///
20/// `Arc<tokio::sync::Mutex<_>>` on the non-unit variants lets the
21/// handle be cloned cheaply across the command tree's call chain and
22/// guarantees concurrent emits serialize correctly. `tokio::sync::Mutex`
23/// (not `std::sync::Mutex`) because we hold the guard across `.await`
24/// boundaries during async writes, which would deadlock with std's.
25#[derive(Clone)]
26pub enum Handle {
27    /// Write each line to this process's stdout; mirror fatal
28    /// `Output::Error` lines to stderr.
29    Stdout,
30    /// Write each line to a child process's stdin.
31    Stdin(Arc<Mutex<ChildStdin>>),
32    /// Push each emitted `Output<T>` (reserialized as `Output<Value>`
33    /// for a uniform storage type) into a shared vector.
34    Collect(Arc<Mutex<Vec<Output<serde_json::Value>>>>),
35}
36
37impl Default for Handle {
38    fn default() -> Self {
39        Handle::Stdout
40    }
41}
42
43impl Handle {
44    /// Emit `output` to this destination. Panics on write failure to
45    /// match `println!` semantics.
46    pub async fn emit<T: Serialize>(&self, output: &Output<T>) {
47        match self {
48            Handle::Stdout => {
49                let json = serde_json::to_string(output)
50                    .expect("Output<T> serializes when T: Serialize");
51                println!("{json}");
52                if matches!(output, Output::Error(e) if e.fatal) {
53                    eprintln!("{json}");
54                }
55            }
56            Handle::Stdin(stdin) => {
57                let json = serde_json::to_string(output)
58                    .expect("Output<T> serializes when T: Serialize");
59                use tokio::io::AsyncWriteExt;
60                let mut guard = stdin.lock().await;
61                guard
62                    .write_all(json.as_bytes())
63                    .await
64                    .expect("emit to child stdin failed");
65                guard
66                    .write_all(b"\n")
67                    .await
68                    .expect("emit to child stdin failed");
69            }
70            Handle::Collect(vec) => {
71                // Rebuild variant-wise as Output<Value> so storage is
72                // uniform without paying a full serialize → deserialize
73                // roundtrip.
74                let typed = match output {
75                    Output::Error(e) => Output::Error(e.clone()),
76                    Output::Notification(n) => Output::Notification(Notification {
77                        value: serde_json::to_value(&n.value)
78                            .expect("T serializes when T: Serialize"),
79                    }),
80                    Output::Begin => Output::Begin,
81                    Output::End => Output::End,
82                };
83                vec.lock().await.push(typed);
84            }
85        }
86    }
87}