Skip to main content

antigravity_codes/
client.rs

1//! The turn-oriented client.
2
3use std::collections::{HashMap, HashSet};
4
5use crate::error::{Error, Result};
6use crate::handlers::Handlers;
7use crate::process::HarnessOptions;
8use crate::protocol::{
9    InitializeConversationResponse, InputEvent, OutputEventEvent, StepUpdate, ToolConfirmation,
10    TrajectoryStateUpdate, TrajectoryStateUpdateState, UsageMetadata,
11};
12use crate::steps::{Step, StepAssembler};
13use crate::RawClient;
14
15/// A harness session that drives whole turns.
16///
17/// [`Client`] wraps [`RawClient`] with the three things every caller ends up
18/// writing otherwise: [`Step`] assembly from delta frames, replies to the
19/// harness's [tool, hook, policy, and question requests](crate::handlers), and
20/// turn-completion detection from trajectory state.
21///
22/// ```no_run
23/// use antigravity_codes::{Client, HarnessOptions, ModelBuilder};
24///
25/// # async fn run() -> antigravity_codes::Result<()> {
26/// let mut client = Client::launch(
27///     HarnessOptions::new()
28///         .workspace("/tmp/project")
29///         .model(ModelBuilder::gemini("gemini-flash-latest", std::env::var("GEMINI_API_KEY").unwrap())),
30/// )
31/// .await?;
32///
33/// let mut turn = client.send("What files are here?").await?;
34/// while let Some(step) = turn.next_step().await? {
35///     if let Some(text) = step.user_facing_text() {
36///         println!("{text}");
37///     }
38/// }
39///
40/// client.shutdown().await?;
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Debug)]
45pub struct Client {
46    raw: RawClient,
47    handlers: Handlers,
48    assembler: StepAssembler,
49    usage: Option<UsageMetadata>,
50    trajectory_usage: HashMap<String, UsageMetadata>,
51}
52
53impl Client {
54    /// Launches a harness with no client-side handlers registered.
55    pub async fn launch(options: HarnessOptions) -> Result<Self> {
56        Self::launch_with(options, Handlers::new()).await
57    }
58
59    /// Launches a harness with handlers for its callbacks.
60    pub async fn launch_with(options: HarnessOptions, handlers: Handlers) -> Result<Self> {
61        let raw = RawClient::launch(options).await?;
62        let mut assembler = StepAssembler::new(raw.cascade_id().map(str::to_string));
63
64        let initialize = raw.initialize_response().clone();
65        // A resumed session replays its transcript in the initialize reply.
66        // Folding it through the assembler now means step indices already seen
67        // do not look new when the conversation continues.
68        for update in initialize.history.iter().cloned() {
69            assembler.ingest(update);
70        }
71
72        Ok(Self {
73            usage: initialize.cumulative_usage.clone(),
74            trajectory_usage: initialize
75                .trajectory_usage
76                .iter()
77                .filter_map(|e| Some((e.trajectory_id.clone()?, e.usage.clone()?)))
78                .collect(),
79            raw,
80            handlers,
81            assembler,
82        })
83    }
84
85    /// The conversation id, which the harness calls a "cascade id".
86    ///
87    /// Pass it to [`HarnessOptions::cascade_id`] to resume this conversation in
88    /// a later process.
89    pub fn cascade_id(&self) -> Option<&str> {
90        self.raw.cascade_id()
91    }
92
93    /// The initialize reply, including any replayed history.
94    pub fn initialize_response(&self) -> &InitializeConversationResponse {
95        self.raw.initialize_response()
96    }
97
98    /// Cumulative token usage for the conversation, as last reported.
99    pub fn usage(&self) -> Option<&UsageMetadata> {
100        self.usage.as_ref()
101    }
102
103    /// Per-trajectory token usage, which separates subagent spend from the
104    /// main conversation's.
105    pub fn trajectory_usage(&self) -> &HashMap<String, UsageMetadata> {
106        &self.trajectory_usage
107    }
108
109    /// The underlying frame-level client, for anything this layer does not model.
110    pub fn raw(&mut self) -> &mut RawClient {
111        &mut self.raw
112    }
113
114    /// Sends a prompt and returns the turn it started.
115    pub async fn send(&mut self, prompt: impl Into<String>) -> Result<Turn<'_>> {
116        self.send_event(InputEvent::user(prompt)).await
117    }
118
119    /// Sends an arbitrary input frame and treats it as the start of a turn.
120    ///
121    /// Use this for multimodal input
122    /// ([`complex_user_input`](crate::protocol::InputEvent::complex_user_input))
123    /// or to fire an
124    /// [`automated_trigger`](crate::protocol::InputEvent::automated_trigger).
125    pub async fn send_event(&mut self, event: InputEvent) -> Result<Turn<'_>> {
126        self.raw.send(&event).await?;
127        Ok(Turn {
128            client: self,
129            finished: false,
130            failure: None,
131            answered: HashSet::new(),
132        })
133    }
134
135    /// Asks the harness to abandon the turn in flight.
136    ///
137    /// The turn does not end here — the harness finishes what it was doing and
138    /// reports `STATE_CANCELLED`, which the in-progress [`Turn`] observes.
139    pub async fn cancel(&mut self) -> Result<()> {
140        self.raw.send(&InputEvent::halt()).await
141    }
142
143    /// Ends the session cleanly and stops the process.
144    pub async fn shutdown(self) -> Result<()> {
145        self.raw.shutdown().await
146    }
147
148    fn record_usage(&mut self, update: crate::protocol::UsageUpdate) {
149        if let Some(total) = update.total {
150            self.usage = Some(total);
151        }
152        for entry in update.agents {
153            if let (Some(id), Some(usage)) = (entry.trajectory_id, entry.usage) {
154                self.trajectory_usage.insert(id, usage);
155            }
156        }
157    }
158}
159
160/// One turn of the conversation: everything between a prompt and the agent
161/// going idle again.
162///
163/// Steps stream out of [`Turn::next_step`]. While it is being polled, the turn
164/// also answers whatever the harness asks of the client — so a tool handler
165/// runs *inside* `next_step`, not on a background task. Tool calls are
166/// therefore executed one at a time, in arrival order.
167#[derive(Debug)]
168pub struct Turn<'a> {
169    client: &'a mut Client,
170    finished: bool,
171    failure: Option<String>,
172    /// `(trajectory_id, step_index, kind)` triples already answered, so a step
173    /// that the harness re-sends is not answered twice.
174    answered: HashSet<(String, u32, &'static str)>,
175}
176
177impl Turn<'_> {
178    /// The next step, or `None` once the agent has gone idle.
179    ///
180    /// If the turn failed, the error surfaces *after* the last step, so
181    /// whatever the agent said before failing is still delivered.
182    pub async fn next_step(&mut self) -> Result<Option<Step>> {
183        loop {
184            if self.finished {
185                return match self.failure.take() {
186                    Some(message) => Err(Error::Turn { message }),
187                    None => Ok(None),
188                };
189            }
190
191            let Some(event) = self.client.raw.next_event().await? else {
192                self.finished = true;
193                continue;
194            };
195
196            match event.into_event() {
197                Some(OutputEventEvent::StepUpdate(update)) => {
198                    self.answer_in_band_requests(&update).await?;
199                    return Ok(Some(self.client.assembler.ingest(update)));
200                }
201                Some(OutputEventEvent::ToolCall(call)) => {
202                    let response = self.client.handlers.call_tool(call).await;
203                    self.client
204                        .raw
205                        .send(&InputEvent::tool_response(response))
206                        .await?;
207                }
208                Some(OutputEventEvent::CallHookRequest(request)) => {
209                    let response = self.client.handlers.call_hook(request).await;
210                    self.client
211                        .raw
212                        .send(&InputEvent::hook_response(response))
213                        .await?;
214                }
215                Some(OutputEventEvent::PolicyDecisionRequest(request)) => {
216                    let response = self.client.handlers.call_policy(request).await;
217                    self.client
218                        .raw
219                        .send(&InputEvent::policy_response(response))
220                        .await?;
221                }
222                Some(OutputEventEvent::UsageUpdate(update)) => self.client.record_usage(update),
223                Some(OutputEventEvent::TrajectoryStateUpdate(update)) => {
224                    self.observe_trajectory(&update)
225                }
226                Some(OutputEventEvent::SessionEndResponse(_)) => self.finished = true,
227                // The initialize reply was consumed at launch; anything else is
228                // a frame from a harness newer than this crate, and ignoring it
229                // is the documented contract.
230                Some(OutputEventEvent::InitializeConversationResponse(_)) | None => {}
231            }
232        }
233    }
234
235    /// Drains the turn and returns everything the agent said to the user.
236    pub async fn collect_text(&mut self) -> Result<String> {
237        let mut out = String::new();
238        let mut seen = HashSet::new();
239        while let Some(step) = self.next_step().await? {
240            if step.is_final() && seen.insert(step.id()) {
241                if let Some(text) = step.user_facing_text() {
242                    out.push_str(text);
243                }
244            }
245        }
246        Ok(out)
247    }
248
249    /// True once the agent has gone idle.
250    pub fn is_finished(&self) -> bool {
251        self.finished
252    }
253
254    /// A step can carry a request that has to be answered before the harness
255    /// will move on: a question for the user, or a tool awaiting confirmation.
256    async fn answer_in_band_requests(&mut self, update: &StepUpdate) -> Result<()> {
257        let trajectory_id = update.trajectory_id.clone().unwrap_or_default();
258        let step_index = update.step_index.unwrap_or_default();
259
260        if let Some(request) = update.questions_request.clone() {
261            if self
262                .answered
263                .insert((trajectory_id.clone(), step_index, "questions"))
264            {
265                let mut response = self.client.handlers.call_questions(request).await;
266                response.trajectory_id = Some(trajectory_id.clone());
267                response.step_index = Some(step_index);
268                self.client
269                    .raw
270                    .send(&InputEvent {
271                        question_response: Some(response),
272                        ..Default::default()
273                    })
274                    .await?;
275            }
276        }
277
278        if update.tool_confirmation_request.is_some()
279            && self
280                .answered
281                .insert((trajectory_id.clone(), step_index, "confirm"))
282        {
283            let accepted = self.client.handlers.call_confirm(update.clone()).await;
284            self.client
285                .raw
286                .send(&InputEvent {
287                    tool_confirmation: Some(ToolConfirmation {
288                        trajectory_id: Some(trajectory_id),
289                        step_index: Some(step_index),
290                        accepted: Some(accepted),
291                    }),
292                    ..Default::default()
293                })
294                .await?;
295        }
296
297        Ok(())
298    }
299
300    /// Only the main trajectory ends the turn. Subagents idle and fail on their
301    /// own schedule while the conversation carries on around them.
302    fn observe_trajectory(&mut self, update: &TrajectoryStateUpdate) {
303        let id = update.trajectory_id.as_deref().unwrap_or_default();
304        let error = update.error.clone().filter(|e| !e.is_empty());
305
306        if !self.client.assembler.is_main(id) {
307            if let Some(error) = error {
308                log::info!("subagent trajectory {id} failed: {error}");
309            }
310            return;
311        }
312
313        match update.state {
314            Some(TrajectoryStateUpdateState::FullyIdle) => {
315                self.finished = true;
316                self.failure = error;
317            }
318            Some(TrajectoryStateUpdateState::Cancelled) => {
319                self.finished = true;
320                self.failure = Some(error.unwrap_or_else(|| "turn cancelled".into()));
321            }
322            _ => {}
323        }
324    }
325}