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}