antigravity-sdk-rust 0.1.14

Rust SDK for Google Antigravity and Gemini to build autonomous, stateful, and secure AI agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# Conversation

Stateful session management for the Antigravity Rust SDK.

## Overview

`Conversation` is a stateful wrapper around a [`Connection`](./connections.md) that tracks
step history, accumulates token usage metadata, and processes stream chunks. It is the
primary interface for interacting with an active agent session — whether you want streaming
chunk-by-chunk output or a simple blocking call-and-response.

Most users access `Conversation` through `Agent<Started>`:

```rust,no_run
use antigravity_sdk_rust::agent::Agent;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let agent = Agent::builder()
        .allow_all()
        .build()
        .start().await?;

    // Get the active Conversation (Arc<Conversation>)
    let conversation = agent.conversation();

    // Use it for streaming or blocking chat
    let response = conversation.chat_to_completion("Hello!").await?;
    println!("{}", response.text);

    agent.stop().await
}
```

## Creating a Conversation

`Conversation` wraps an `AnyConnection` and an optional history size limit:

```rust,no_run
use antigravity_sdk_rust::conversation::Conversation;
use antigravity_sdk_rust::connection::AnyConnection;

// Created internally by Agent::start(), but can be constructed directly:
let conversation = Conversation::new(
    any_connection,     // AnyConnection (Local, Wasm, or Mock)
    Some(5000),         // max_history_size (None → default 10,000; Some(0) → unlimited)
);
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `conn` | `AnyConnection` || The underlying connection to the harness |
| `max_history_size` | `Option<usize>` | `Some(10_000)` | Max steps retained in memory. `None` uses 10,000. `Some(0)` disables trimming. |

## Key Methods

### Chatting

#### `chat(prompt)` — Streaming

Sends a prompt and returns a `BoxStream<StreamChunk>` for real-time processing:

```rust,no_run
use antigravity_sdk_rust::types::StreamChunk;
use futures_util::StreamExt;

let mut stream = conversation.chat("Explain monads").await?;

while let Some(chunk_res) = stream.next().await {
    match chunk_res? {
        StreamChunk::Thought { text, step_index } => {
            // Model's internal reasoning (thinking tokens)
            eprint!("{}", text);
        }
        StreamChunk::Text { text, step_index } => {
            // Response text fragments
            print!("{}", text);
        }
        StreamChunk::ToolCall(call) => {
            // The model requested a tool execution
            println!("[Tool: {} | Args: {}]", call.name, call.args);
        }
    }
}
```

**Signature:**
```rust,no_run
pub async fn chat(
    &self,
    prompt: &str,
) -> Result<BoxStream<'static, Result<StreamChunk, anyhow::Error>>, anyhow::Error>
```

#### `chat_to_completion(prompt)` — Blocking

Sends a prompt and waits for the complete response, accumulating all chunks internally:

```rust,no_run
let response = conversation.chat_to_completion("What is 2 + 2?").await?;

println!("Response: {}", response.text);
println!("Thinking: {}", response.thinking);
println!("Steps: {}", response.steps.len());
println!("Total tokens: {}", response.usage_metadata.total_token_count);
```

**Signature:**
```rust,no_run
pub async fn chat_to_completion(
    &self,
    prompt: &str,
) -> Result<ChatResponse, anyhow::Error>
```

### Sending & Receiving (Low-Level)

| Method | Signature | Description |
|--------|-----------|-------------|
| `send(prompt)` | `async fn send(&self, prompt: &str) -> Result<()>` | Sends a raw prompt and registers a turn boundary. Does **not** return a stream. |
| `receive_steps()` | `fn receive_steps(&self) -> BoxStream<'static, Result<Step>>` | Raw step-level stream. Inserts steps into history, tracks compaction, enforces history limits. |
| `receive_chunks()` | `fn receive_chunks(&self) -> BoxStream<'static, Result<StreamChunk>>` | Filters `receive_steps()` into high-level `StreamChunk` events. Only emits model→user content. |

#### `receive_chunks()` Filtering Logic

The chunk stream applies these rules:

1. **Thought chunks** — emitted when `source == Model`, `target == User`, and `thinking_delta` is non-empty
2. **Text chunks** — emitted when `source == Model`, `target == User`, and `content_delta` is non-empty
3. **ToolCall chunks** — emitted for each `ToolCall` in the step, **deduplicated by `call.id`** (empty IDs are never deduplicated)
4. **Environment-targeted steps** are silently filtered out (e.g., harness internal tool dispatches)

### Connection Access

| Method | Return Type | Description |
|--------|-------------|-------------|
| `connection()` | `AnyConnection` | Returns the underlying connection (clone of `Arc`) |
| `conversation_id()` | `&str` | Session ID (delegates to `Connection::conversation_id()`) |
| `is_idle()` | `bool` | Whether the connection is idle |
| `disconnect()` | `async -> Result<()>` | Gracefully closes the connection |

## StreamChunk

The streaming fragment enum used by `chat()` and `receive_chunks()`:

```rust,no_run
use antigravity_sdk_rust::types::ToolCall;

#[derive(Debug, Clone)]
pub enum StreamChunk {
    /// Model's internal reasoning/thinking fragment.
    Thought {
        /// Index of the step that produced this chunk.
        step_index: u32,
        /// Thinking text delta.
        text: String,
    },

    /// Response text fragment directed at the user.
    Text {
        /// Index of the step that produced this chunk.
        step_index: u32,
        /// Text content delta.
        text: String,
    },

    /// A complete tool call requested by the model.
    ToolCall(ToolCall),
}
```

### ToolCall Structure

```rust,no_run
#[derive(Debug, Clone)]
pub struct ToolCall {
    /// Unique correlation ID for this call.
    pub id: String,
    /// Name of the tool to invoke.
    pub name: String,
    /// Arguments as a JSON value.
    pub args: serde_json::Value,
    /// Optional canonical filesystem path (for file-targeting tools).
    pub canonical_path: Option<String>,
}
```

## Streaming Example

The complete streaming pattern from `examples/streaming.rs`:

```rust,no_run
use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::types::StreamChunk;
use futures_util::StreamExt;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    dotenvy::dotenv().ok();

    let agent = Agent::builder()
        .default_model("gemini-3.5-flash")
        .allow_all()
        .build()
        .start().await?;

    let conversation = agent.conversation();

    let prompt = "Solve this riddle: I speak without a mouth \
        and hear without ears. What am I?";
    println!("User: {}\n", prompt);

    let mut stream = conversation.chat(prompt).await?;

    while let Some(chunk_res) = stream.next().await {
        match chunk_res? {
            StreamChunk::Thought { text, .. } => {
                // Print reasoning tokens as they arrive
                print!("{}", text);
                std::io::Write::flush(&mut std::io::stdout())?;
            }
            StreamChunk::Text { text, .. } => {
                // Print response text tokens as they arrive
                print!("{}", text);
                std::io::Write::flush(&mut std::io::stdout())?;
            }
            StreamChunk::ToolCall(call) => {
                println!(
                    "\n[Tool Call: {} with args: {}]",
                    call.name, call.args
                );
            }
        }
    }

    println!(); // Final newline
    agent.stop().await?;
    Ok(())
}
```

## State Management

`Conversation` maintains a `ConversationState` protected by an async `Mutex`:

```rust,no_run
pub struct ConversationState {
    /// All executed steps (prompts, tool calls, responses, compaction markers).
    pub steps: Vec<Step>,
    /// Step indices marking each user prompt turn boundary.
    pub turn_start_indices: Vec<usize>,
    /// Step indices where history compaction occurred.
    pub compaction_indices: Vec<usize>,
    /// Cumulative token usage across all turns.
    pub cumulative_usage: UsageMetadata,
    /// Token usage for the current active turn.
    pub turn_usage: Option<UsageMetadata>,
}
```

### State Query Methods

All state methods are `async` because they acquire the internal `Mutex`:

| Method | Return Type | Description |
|--------|-------------|-------------|
| `history()` | `Vec<Step>` | Clone of the full step history |
| `turn_count()` | `usize` | Number of user-initiated turns |
| `compaction_indices()` | `Vec<usize>` | Where the harness compacted history |
| `last_response()` | `String` | Text content of the last complete model response |
| `total_usage()` | `UsageMetadata` | Cumulative token usage for the entire session |
| `last_turn_usage()` | `Option<UsageMetadata>` | Token usage for the most recent turn |
| `clear_history()` | `()` | Resets all steps, turns, compactions, and usage stats |

### Usage Example

```rust,no_run
// After a chat interaction
let history = conversation.history().await;
println!("Total steps: {}", history.len());
println!("Turns completed: {}", conversation.turn_count().await);

let total = conversation.total_usage().await;
println!("Session tokens: {} prompt, {} generated, {} total",
    total.prompt_token_count,
    total.candidates_token_count,
    total.total_token_count,
);

if let Some(turn) = conversation.last_turn_usage().await {
    println!("Last turn: {} thinking tokens", turn.thoughts_token_count);
}

// Check if compaction happened
let compactions = conversation.compaction_indices().await;
if !compactions.is_empty() {
    println!("History was compacted at step indices: {:?}", compactions);
}
```

### History Auto-Trimming

When `max_history_size > 0` and the step count exceeds the limit, older steps are
drained from the front. Turn and compaction indices are adjusted accordingly:

```rust,no_run
// Only keep the last 100 steps in memory
let conversation = Conversation::new(any_connection, Some(100));

// Disable auto-trimming entirely
let conversation = Conversation::new(any_connection, Some(0));

// Use the default (10,000 steps)
let conversation = Conversation::new(any_connection, None);
```

## ChatResponse

The complete result returned by `chat_to_completion()`:

```rust,no_run
#[derive(Debug, Clone)]
pub struct ChatResponse {
    /// Combined final response text.
    pub text: String,
    /// Combined model reasoning/thinking text.
    pub thinking: String,
    /// All steps executed during this turn.
    pub steps: Vec<Step>,
    /// Cumulative token usage metrics.
    pub usage_metadata: UsageMetadata,
}
```

### UsageMetadata

```rust,no_run
#[derive(Debug, Clone, Default)]
pub struct UsageMetadata {
    /// Tokens included in the request prompt.
    pub prompt_token_count: i32,
    /// Tokens generated in model candidates.
    pub candidates_token_count: i32,
    /// Total combined tokens.
    pub total_token_count: i32,
    /// Cache-hit content tokens.
    pub cached_content_token_count: i32,
    /// Tokens consumed during reasoning/thinking.
    pub thoughts_token_count: i32,
}
```

## Step Model

Each event in the trajectory is represented as a `Step`:

```rust,no_run
#[derive(Debug, Clone)]
pub struct Step {
    pub id: String,                              // Unique step ID
    pub step_index: u32,                         // Position in trajectory
    pub r#type: StepType,                        // TextResponse, ToolCall, Compaction, Finish, etc.
    pub source: StepSource,                      // System, User, Model
    pub target: StepTarget,                      // User, Environment
    pub status: StepStatus,                      // Active, Done, Error, TerminalError, etc.
    pub content: String,                         // Full accumulated text
    pub content_delta: String,                   // Incremental text delta
    pub thinking: String,                        // Full accumulated thinking
    pub thinking_delta: String,                  // Incremental thinking delta
    pub tool_calls: Vec<ToolCall>,               // Tool calls in this step
    pub error: String,                           // Error message (if any)
    pub is_complete_response: Option<bool>,       // True for final model response
    pub structured_output: Option<serde_json::Value>, // Parsed structured output
    pub usage_metadata: Option<UsageMetadata>,   // Per-step token usage
    pub cascade_id: String,                      // Execution cascade group
    pub trajectory_id: String,                   // Sub-agent trajectory group
    pub http_code: u32,                          // HTTP status code (if applicable)
}
```

### Step Enums

| Enum | Variants |
|------|----------|
| `StepType` | `TextResponse`, `ToolCall`, `SystemMessage`, `Compaction`, `Finish`, `Unknown` |
| `StepSource` | `System`, `User`, `Model`, `Unknown` |
| `StepTarget` | `User`, `Environment`, `Unspecified`, `Unknown` |
| `StepStatus` | `Active`, `Done`, `WaitingForUser`, `Error`, `Canceled`, `TerminalError`, `Unknown` |

## Python SDK Comparison

| Concept | Python SDK | Rust SDK |
|---------|-----------|----------|
| Conversation class | `Conversation` | `Conversation` (same name) |
| Streaming | `async for chunk in conversation.chat(prompt)` | `conversation.chat(prompt).await?.next().await` |
| Blocking | `conversation.chat_to_completion(prompt)` | `conversation.chat_to_completion(prompt).await?` |
| History | `conversation.history` (property) | `conversation.history().await` (async method) |
| Turn count | `conversation.turn_count` | `conversation.turn_count().await` |
| Token usage | `conversation.total_usage` | `conversation.total_usage().await` |
| State mutex | GIL + threading lock | `tokio::sync::Mutex<ConversationState>` |
| Stream type | `AsyncIterator[StreamChunk]` | `BoxStream<'static, Result<StreamChunk>>` |
| Max history | `max_history_size` param | `max_history_size: Option<usize>` (default 10,000) |

> **Key difference:** In the Rust SDK, all state-querying methods are `async` because the
> internal state is protected by a `tokio::sync::Mutex`. In Python, these are synchronous
> properties protected by the GIL.