Skip to main content

antigravity_codes/
client_raw.rs

1//! The unopinionated client: frames in, frames out.
2
3use futures_util::{SinkExt, StreamExt};
4use tokio_tungstenite::tungstenite::Message;
5
6use crate::error::{Error, Result};
7use crate::process::{Harness, HarnessOptions};
8use crate::protocol::{
9    InitializeConversationEvent, InitializeConversationResponse, InputEvent, OutputEvent,
10    OutputEventEvent,
11};
12use crate::ws;
13
14/// A harness session with no interpretation layered on top.
15///
16/// [`RawClient`] owns the process and the socket and does exactly three things:
17/// the handshake, the initialize exchange, and frame codec. Answering the
18/// harness's tool calls, hooks, and policy checks is the caller's job — see
19/// [`crate::Client`] for a version that does that for you.
20///
21/// ```no_run
22/// use antigravity_codes::{HarnessOptions, ModelBuilder, RawClient};
23/// use antigravity_codes::protocol::{InputEvent, OutputEventEvent};
24///
25/// # async fn run() -> antigravity_codes::Result<()> {
26/// let mut client = RawClient::launch(
27///     HarnessOptions::new()
28///         .workspace("/tmp/project")
29///         .model(ModelBuilder::gemini("gemini-flash-latest", "…")),
30/// )
31/// .await?;
32///
33/// client.send(&InputEvent::user("hello")).await?;
34/// while let Some(event) = client.next_event().await? {
35///     if let Some(OutputEventEvent::StepUpdate(step)) = event.into_event() {
36///         println!("{:?}", step.text_or_delta());
37///     }
38/// }
39/// # Ok(())
40/// # }
41/// ```
42#[derive(Debug)]
43pub struct RawClient {
44    harness: Harness,
45    socket: ws::Socket,
46    initialize: InitializeConversationResponse,
47    closed: bool,
48}
49
50impl RawClient {
51    /// Launches a harness, connects, and completes the initialize exchange.
52    pub async fn launch(options: HarnessOptions) -> Result<Self> {
53        let harness = Harness::launch(&options).await?;
54        let socket = ws::connect(harness.port(), harness.api_key()).await?;
55
56        let mut client = Self {
57            harness,
58            socket,
59            initialize: Default::default(),
60            closed: false,
61        };
62
63        let event = InitializeConversationEvent {
64            config: Some(options.config().clone()),
65        };
66        client.send_json(&event).await?;
67
68        // The harness answers the initialize event before anything else. If it
69        // instead drops the socket, the reason is on stderr — most often "no
70        // model configured", which it treats as fatal.
71        match client.next_event().await? {
72            Some(event) => match event.into_event() {
73                Some(OutputEventEvent::InitializeConversationResponse(response)) => {
74                    client.initialize = response;
75                }
76                other => {
77                    return Err(Error::HandshakeFailed {
78                        stderr: format!(
79                            "expected an initialize response, got {other:?}; harness stderr: {}",
80                            client.harness.stderr_after_exit().await
81                        ),
82                    })
83                }
84            },
85            None => {
86                return Err(Error::HandshakeFailed {
87                    stderr: format!(
88                        "harness closed the socket during initialize; stderr: {}",
89                        client.harness.stderr_after_exit().await
90                    ),
91                })
92            }
93        }
94
95        Ok(client)
96    }
97
98    /// The harness's reply to initialize: the conversation id, any replayed
99    /// history, and cumulative usage for a resumed session.
100    pub fn initialize_response(&self) -> &InitializeConversationResponse {
101        &self.initialize
102    }
103
104    /// The conversation id, which the harness calls a "cascade id".
105    pub fn cascade_id(&self) -> Option<&str> {
106        self.initialize.cascade_id.as_deref()
107    }
108
109    /// The running process, for its stderr tail and port.
110    pub fn harness(&self) -> &Harness {
111        &self.harness
112    }
113
114    /// Sends one frame.
115    pub async fn send(&mut self, event: &InputEvent) -> Result<()> {
116        self.send_json(event).await
117    }
118
119    async fn send_json<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
120        if self.closed {
121            return Err(Error::SessionClosed);
122        }
123        let payload = serde_json::to_string(value).map_err(|e| Error::decode(e, ""))?;
124        log::trace!("--> {payload}");
125        self.socket.send(Message::Text(payload)).await?;
126        Ok(())
127    }
128
129    /// Reads the next frame, or `None` once the harness has closed the socket.
130    ///
131    /// Non-text frames (pings, pongs, the close frame itself) are handled and
132    /// skipped rather than surfaced.
133    pub async fn next_event(&mut self) -> Result<Option<OutputEvent>> {
134        loop {
135            let message = match self.socket.next().await {
136                Some(Ok(message)) => message,
137                Some(Err(e)) => {
138                    self.closed = true;
139                    // A harness that dies mid-turn drops the TCP connection
140                    // without a close frame, which tungstenite reports as a
141                    // protocol violation. The useful diagnosis is on stderr.
142                    let stderr = self.harness.stderr_after_exit().await;
143                    if stderr.is_empty() {
144                        return Err(Error::from(e));
145                    }
146                    return Err(Error::HandshakeFailed { stderr });
147                }
148                None => {
149                    self.closed = true;
150                    return Ok(None);
151                }
152            };
153
154            match message {
155                Message::Text(text) => {
156                    log::trace!("<-- {text}");
157                    let event = serde_json::from_str::<OutputEvent>(&text)
158                        .map_err(|e| Error::decode(e, text))?;
159                    return Ok(Some(event));
160                }
161                Message::Close(_) => {
162                    self.closed = true;
163                    return Ok(None);
164                }
165                Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
166                    continue
167                }
168            }
169        }
170    }
171
172    /// True once the socket has closed in either direction.
173    pub fn is_closed(&self) -> bool {
174        self.closed
175    }
176
177    /// Asks the harness to end the session, waits for it to acknowledge, then
178    /// stops the process.
179    ///
180    /// The acknowledgement matters: the harness flushes conversation state to
181    /// its storage directory on the way out, so killing it early loses the
182    /// transcript a later `cascade_id` resume would have replayed.
183    pub async fn shutdown(mut self) -> Result<()> {
184        if !self.closed {
185            let _ = self.send(&InputEvent::session_end()).await;
186            loop {
187                match self.next_event().await {
188                    Ok(Some(event)) => {
189                        if event.session_end_response == Some(true) {
190                            break;
191                        }
192                    }
193                    Ok(None) => break,
194                    Err(_) => break,
195                }
196            }
197            let _ = self.socket.close(None).await;
198        }
199        self.harness.shutdown().await
200    }
201}