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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# Agent

High-level, batteries-included entry point for building AI agents.

## Overview

The `Agent` struct manages the full lifecycle of an agentic AI session — binary discovery, tool wiring, hook registration, policy enforcement, and harness communication. It is the primary construct you'll interact with when building applications on the Antigravity SDK.

```text
┌─────────────────────────────────────────────────────────────────┐
│                        Agent<Unstarted>                         │
│  ┌──────────────┐  ┌──────────┐  ┌─────────┐  ┌────────────┐  │
│  │ AgentConfig   │  │ToolRunner│  │HookRunner│  │PolicyEnforcer│ │
│  │  (model, key, │  │(custom   │  │(lifecycle│  │ (safety     │ │
│  │   policies…)  │  │  tools)  │  │  hooks)  │  │  policies)  │ │
│  └──────────────┘  └──────────┘  └─────────┘  └────────────┘  │
│                          │                                      │
│                    .start().await?                               │
│                          ▼                                      │
│                   Agent<Started>                                │
│            ┌──────────────────────┐                             │
│            │  Conversation (Arc)  │                             │
│            │  ┌────────────────┐  │                             │
│            │  │ Connection     │  │                             │
│            │  │ (WebSocket/IPC)│  │                             │
│            │  └────────────────┘  │                             │
│            └──────────────────────┘                             │
└─────────────────────────────────────────────────────────────────┘
```

**Key responsibilities:**

- **Binary discovery** — locates `localharness` automatically via env var, local install, `PATH`, or Python site-packages
- **Tool registration** — wires custom Rust `Tool` implementations into the model's callable toolset
- **Hook dispatch** — sequences lifecycle observer hooks (pre/post tool call, session start/end, etc.)
- **Policy enforcement** — compiles safety policies into a prioritized hook that gates tool execution
- **Connection management** — spawns the subprocess, upgrades to WebSocket, manages the conversation stream

---

## Builder Pattern (Typestate)

The agent uses a **compile-time typestate pattern** to guarantee that safety policies are always configured before an agent can be built. This eliminates an entire class of "forgot to set policies" runtime errors.

```text
Agent::builder()
AgentBuilder<NoPolicies>    ← .binary_path(), .api_key(), .tools(), .hooks(), etc.
    │── .allow_all()  ──────────►  AgentBuilder<HasPolicies>  ──► .build()
    │── .read_only()  ──────────►  AgentBuilder<HasPolicies>  ──► .build()
    │── .policies(…)  ──────────►  AgentBuilder<HasPolicies>  ──► .build()
    │── .policy(…)    ──────────►  AgentBuilder<HasPolicies>  ──► .build()
    └── .build_unchecked()  ────►  Agent<Unstarted>  (escape hatch, skips check)
```

### How it works

1. `Agent::builder()` returns `AgentBuilder<NoPolicies>`
2. Configuration methods (`.api_key()`, `.tools()`, `.hooks()`, etc.) return `Self`, preserving the current type-state
3. Policy-setting methods (`.allow_all()`, `.read_only()`, `.policies()`, `.policy()`) **consume** the builder and return `AgentBuilder<HasPolicies>`
4. `.build()` is only implemented on `AgentBuilder<HasPolicies>` — calling it on `NoPolicies` is a **compile error**
5. `.build_unchecked()` is available on any `AgentBuilder<P>` as an escape hatch

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

// ✅ Compiles — policies are set via .allow_all()
let agent = Agent::builder()
    .api_key("my-key")
    .allow_all()
    .build();

// ❌ Compile error — .build() is not available on AgentBuilder<NoPolicies>
// let agent = Agent::builder()
//     .api_key("my-key")
//     .build();  // error[E0599]: no method named `build` found

// ⚠️ Escape hatch — bypasses compile-time policy check
let agent = Agent::builder()
    .api_key("my-key")
    .build_unchecked();
```

### Type-state markers

```rust,no_run
// Marker types — you won't construct these directly
pub struct NoPolicies;   // Initial state, .build() is NOT available
pub struct HasPolicies;  // After setting policies, .build() IS available
```

> **Python SDK comparison:** The Python SDK performs this check at runtime during `agent.start()`, raising a `ValueError` if write tools are enabled without policies. The Rust SDK catches it at compile time, shifting the error left.

---

## AgentConfig Fields

`AgentConfig` is the resolved configuration struct that powers the `Agent`. While you'll typically use the builder, understanding the underlying fields is useful for advanced use cases.

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

// AgentConfig is Default — all fields start as None/empty
let config = AgentConfig::default();
```

| Field | Type | Description |
|---|---|---|
| `binary_path` | `Option<String>` | Path to the `localharness` binary. If `None`, auto-discovered via the [binary discovery]#binary-discovery algorithm. |
| `gemini_config` | `GeminiConfig` | Model selection, API key, Vertex AI project/location, search grounding, URL context. |
| `capabilities` | `CapabilitiesConfig` | Tool enable/disable lists, compaction threshold, image model override, finish tool schema. |
| `system_instructions` | `Option<SystemInstructions>` | Either `Custom` (full text override) or `Appended` (sections added to default identity). |
| `save_dir` | `Option<String>` | Directory for persisting session state/logs. |
| `workspaces` | `Option<Vec<String>>` | Working directories the agent may access. Defaults to `cwd` if `None`. Used by `workspace_only` policies. |
| `skills_paths` | `Vec<String>` | Paths to folders containing custom skill modules. |
| `policies` | `Option<Vec<Policy>>` | Safety policies controlling tool execution (approve, deny, ask_user). |
| `hooks` | `Vec<Arc<dyn DynHook>>` | Lifecycle hooks — observe/intercept session start, tool calls, turns, errors, etc. |
| `triggers` | `Vec<Arc<dyn DynTrigger>>` | Background async workers spawned when the agent starts. |
| `tools` | `Vec<Arc<dyn DynTool>>` | Custom Rust tools registered for model invocation. |
| `mcp_servers` | `Vec<McpServerConfig>` | Model Context Protocol server configurations (stdio, SSE, or HTTP transports). |
| `conversation_id` | `Option<String>` | Assign or resume a specific conversation ID. |
| `app_data_dir` | `Option<String>` | Application data directory for cache/configs. Defaults to `$HOME/.gemini/antigravity`. |
| `response_schema` | `Option<String>` | JSON Schema string constraining the agent's final structured output. |

---

## Builder Methods

All builder methods follow the fluent chaining pattern. Methods that don't affect policy state preserve the current typestate (`Self`). Methods that set policies transition from `NoPolicies` → `HasPolicies`.

### Configuration Methods (available on any `AgentBuilder<P>`)

These methods return `Self` and can be called in any order, regardless of policy state.

| Method | Signature | Description |
|---|---|---|
| `.binary_path()` | `fn binary_path(self, path: impl Into<String>) -> Self` | Sets the path to the `localharness` binary. |
| `.gemini_config()` | `fn gemini_config(self, config: GeminiConfig) -> Self` | Sets the full Gemini configuration (model, API key, Vertex AI, search). |
| `.api_key()` | `fn api_key(self, key: impl Into<String>) -> Self` | Shorthand to set the API key on `gemini_config`. |
| `.default_model()` | `fn default_model(self, model: impl Into<String>) -> Self` | Shorthand to set the default model name (e.g. `"gemini-3.5-flash"`). |
| `.capabilities()` | `fn capabilities(self, caps: CapabilitiesConfig) -> Self` | Sets tool enable/disable lists and thresholds. |
| `.system_instructions()` | `fn system_instructions(self, si: SystemInstructions) -> Self` | Sets custom or appended system instructions. |
| `.save_dir()` | `fn save_dir(self, dir: impl Into<String>) -> Self` | Sets the session state log directory. |
| `.workspaces()` | `fn workspaces(self, ws: Vec<String>) -> Self` | Sets the allowed workspace directories. |
| `.skills_paths()` | `fn skills_paths(self, paths: Vec<String>) -> Self` | Sets custom skill module folder paths. |
| `.hooks()` | `fn hooks(self, hooks: Vec<Arc<dyn DynHook>>) -> Self` | Sets the full list of lifecycle hooks (replaces any existing). |
| `.hook()` | `fn hook(self, hook: Arc<dyn DynHook>) -> Self` | Appends a single lifecycle hook. |
| `.triggers()` | `fn triggers(self, triggers: Vec<Arc<dyn DynTrigger>>) -> Self` | Sets the full list of background triggers (replaces any existing). |
| `.trigger()` | `fn trigger(self, trigger: Arc<dyn DynTrigger>) -> Self` | Appends a single background trigger. |
| `.tools()` | `fn tools(self, tools: Vec<Arc<dyn DynTool>>) -> Self` | Sets the full list of custom tools (replaces any existing). |
| `.tool()` | `fn tool(self, tool: Arc<dyn DynTool>) -> Self` | Appends a single custom tool. |
| `.conversation_id()` | `fn conversation_id(self, id: impl Into<String>) -> Self` | Assigns or resumes a conversation ID. |
| `.app_data_dir()` | `fn app_data_dir(self, dir: impl Into<String>) -> Self` | Sets the application data directory. |
| `.response_schema()` | `fn response_schema(self, schema: impl Into<String>) -> Self` | Sets a JSON Schema for structured output. |
| `.mcp_server()` | `fn mcp_server(self, server: McpServerConfig) -> Self` | Appends a single MCP server configuration. |
| `.mcp_servers()` | `fn mcp_servers(self, servers: Vec<McpServerConfig>) -> Self` | Sets the full list of MCP server configurations (replaces any existing). |

### Policy Methods (transition `NoPolicies``HasPolicies`)

These methods **consume** the builder and return `AgentBuilder<HasPolicies>`, enabling `.build()`.

| Method | Signature | Description |
|---|---|---|
| `.policy()` | `fn policy(self, policy: Policy) -> AgentBuilder<HasPolicies>` | Appends a single policy and transitions to `HasPolicies`. |
| `.policies()` | `fn policies(self, policies: Vec<Policy>) -> AgentBuilder<HasPolicies>` | Sets the full policy list and transitions to `HasPolicies`. |
| `.allow_all()` | `fn allow_all(self) -> AgentBuilder<HasPolicies>` | Convenience: sets `policy::allow_all()` — approves all tool calls unconditionally. |
| `.read_only()` | `fn read_only(self) -> AgentBuilder<HasPolicies>` | Convenience: denies all tools except read-only ones (`FindFile`, `ListDir`, `ViewFile`, `SearchDir`). |

### Build Methods

| Method | Signature | Available On | Description |
|---|---|---|---|
| `.build()` | `fn build(self) -> Agent<Unstarted>` | `AgentBuilder<HasPolicies>` only | Constructs the agent. Compile error if policies are not set. |
| `.build_unchecked()` | `fn build_unchecked(self) -> Agent<Unstarted>` | Any `AgentBuilder<P>` | Escape hatch — builds without compile-time policy check. Runtime errors may still occur at `.start()` if write tools are enabled without policies. |

---

## Lifecycle: Unstarted → Started

The `Agent` is generic over its lifecycle state, using the `AgentLifecycle` trait:

```text
Agent<Unstarted>  ──  .start().await?  ──►  Agent<Started>  ──  .stop().await?
```

### `Agent<Unstarted>`

An agent that has been configured but not yet connected. Available methods:

```rust,no_run
use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::hooks::DynHook;
use antigravity_sdk_rust::tools::DynTool;
use antigravity_sdk_rust::triggers::DynTrigger;
use std::sync::Arc;

let mut agent = Agent::builder().allow_all().build();

// Additional registrations before starting
// agent.register_hook(hook);
// agent.register_tool(tool);
// agent.register_trigger(trigger)?;
```

| Method | Signature | Description |
|---|---|---|
| `Agent::new()` | `fn new(config: AgentConfig) -> Self` | Direct construction from config (prefer the builder). |
| `Agent::builder()` | `fn builder() -> AgentBuilder<NoPolicies>` | Returns a new builder. |
| `.register_hook()` | `fn register_hook(&mut self, hook: Arc<dyn DynHook>)` | Adds a hook after construction but before starting. |
| `.register_tool()` | `fn register_tool(&mut self, tool: Arc<dyn DynTool>)` | Adds a tool after construction but before starting. |
| `.register_trigger()` | `fn register_trigger(&mut self, trigger: Arc<dyn DynTrigger>) -> Result<()>` | Adds a trigger after construction but before starting. |
| `.start()` | `fn start(self) -> BoxFuture<'static, Result<Agent<Started>>>` | Resolves the binary, connects, registers tools/hooks/policies, starts triggers. Consumes `self`. |

### `Agent<Started>`

A running agent with an active connection. Available methods:

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

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

// Send a prompt and wait for the full response
let response = agent.chat("What is 2+2?").await?;
println!("Text: {}", response.text);
println!("Thinking: {}", response.thinking);
println!("Steps: {}", response.steps.len());

// Access the conversation for streaming or advanced use
let conversation = agent.conversation();

// Get the conversation ID
let id = agent.conversation_id();

// Shut down gracefully
agent.stop().await?;
# Ok(())
# }
```

| Method | Signature | Description |
|---|---|---|
| `.chat()` | `async fn chat(&self, prompt: &str) -> Result<ChatResponse>` | Sends a prompt and awaits the complete response. Returns text, thinking, steps, and usage metadata. |
| `.conversation()` | `fn conversation(&self) -> Arc<Conversation>` | Returns the active `Conversation` for streaming or direct access. |
| `.conversation_id()` | `fn conversation_id(&self) -> String` | Returns the conversation session ID. |
| `.stop()` | `async fn stop(&self) -> Result<()>` | Gracefully disconnects the harness and stops the session. |

### `ChatResponse`

The response from `.chat()` contains:

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

// ChatResponse {
//     text: String,              // Combined model text output
//     thinking: String,          // Combined reasoning/thinking text
//     steps: Vec<Step>,          // All intermediate execution steps
//     usage_metadata: UsageMetadata, // Token consumption stats
// }
```

---

## `.start()` Internals

When `.start()` is called, the following happens in order:

1. **Resolve binary path** — Uses the [binary discovery]#binary-discovery algorithm to find `localharness`
2. **Register hooks** — All configured hooks are added to the `HookRunner`
3. **Process capabilities** — Resolves enabled/disabled tool lists (mutually exclusive)
4. **Compile policies** — Builds `PolicyEnforcer` from configured policies; prepends `workspace_only` policies unless `allow_all()` was used
5. **Safety check** — Fails if write tools are enabled without any policies
6. **Register tools** — All custom tools are added to the `ToolRunner`
7. **Connect** — Spawns `localharness` subprocess, establishes WebSocket connection
8. **Start triggers** — Spawns background trigger tasks

### Errors

`.start()` returns `Err` if:

- The `localharness` binary cannot be found
- `enabled_tools` and `disabled_tools` are both set (mutually exclusive)
- Write tools are enabled but no policies are configured
- The subprocess or WebSocket connection fails

---

## Binary Discovery

When `binary_path` is not explicitly set, the SDK searches for `localharness` in the following order:

| Priority | Location | Description |
|---|---|---|
| 1 | `$ANTIGRAVITY_HARNESS_PATH` | Environment variable override |
| 2 | `./bin/localharness` | Local install relative to `cwd` (where `just install` places it) |
| 3 | `$PATH` lookup | Standard PATH search (e.g. via `pip install google-antigravity`) |
| 4 | Python site-packages | Fallback: queries `python3 -c "import site; ..."` and checks `google/antigravity/bin/localharness` |

If none are found, `.start()` returns an error with a message to specify `binary_path` explicitly.

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

# async fn example() -> Result<(), anyhow::Error> {
// Explicit path — skips discovery
let agent = Agent::builder()
    .binary_path("/usr/local/bin/localharness")
    .allow_all()
    .build()
    .start()
    .await?;

// Auto-discovery — checks env var, ./bin, PATH, site-packages
let agent = Agent::builder()
    .allow_all()
    .build()
    .start()
    .await?;
# Ok(())
# }
```

---

## Examples

### 1. Minimal Hello World

The simplest possible agent — auto-discovers the binary, uses default model, permits all tool calls:

```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?;

    let response = agent.chat("Say 'Hello World!'").await?;
    println!("Agent: {}", response.text);

    agent.stop().await?;
    Ok(())
}
```

### 2. Custom Model + API Key

Configure a specific model and API key, with an explicit binary path:

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

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

    let agent = Agent::builder()
        .binary_path(std::env::var("ANTIGRAVITY_HARNESS_PATH").unwrap())
        .api_key(std::env::var("GEMINI_API_KEY").unwrap())
        .default_model("gemini-3.5-flash")
        .allow_all()
        .build()
        .start()
        .await?;

    let response = agent.chat("Explain Rust's ownership model in 3 sentences.").await?;
    println!("{}", response.text);
    println!("Tokens used: {}", response.usage_metadata.total_token_count);

    agent.stop().await?;
    Ok(())
}
```

### 3. Custom Tools + Hooks + Policies

Register custom tools with fine-grained safety policies — deny all built-in tools, allow only your custom ones:

```rust,no_run
use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::hooks::Hook;
use antigravity_sdk_rust::policy;
use antigravity_sdk_rust::tools::Tool;
use antigravity_sdk_rust::types::{
    ChatResponse, CustomSystemInstructions, HookResult, SystemInstructions, ToolCall,
};
use serde_json::Value;
use std::sync::Arc;

// ── Custom Tool ─────────────────────────────────────────────────────

struct WeatherTool;

impl Tool for WeatherTool {
    fn name(&self) -> &'static str {
        "get_weather"
    }

    fn description(&self) -> &'static str {
        "Returns the current weather for a given city."
    }

    fn parameters_json_schema(&self) -> &'static str {
        r#"{
            "type": "object",
            "properties": {
                "city": { "type": "string", "description": "City name" }
            },
            "required": ["city"]
        }"#
    }

    async fn call(&self, args: Value) -> Result<Value, anyhow::Error> {
        let city = args.get("city").and_then(Value::as_str).unwrap_or("unknown");
        Ok(Value::String(format!("Weather in {city}: 22°C, partly cloudy")))
    }
}

// ── Custom Hook ─────────────────────────────────────────────────────

struct AuditHook;

impl Hook for AuditHook {
    async fn pre_tool_call(&self, tool_call: &ToolCall) -> Result<HookResult, anyhow::Error> {
        println!("[AUDIT] Tool called: {} with args: {}", tool_call.name, tool_call.args);
        Ok(HookResult { allow: true, message: String::new() })
    }

    async fn post_turn(&self, response: &ChatResponse) -> Result<(), anyhow::Error> {
        println!("[AUDIT] Turn complete. Tokens: {}", response.usage_metadata.total_token_count);
        Ok(())
    }
}

// ── Main ────────────────────────────────────────────────────────────

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let agent = Agent::builder()
        .api_key("your-api-key")
        .default_model("gemini-3.5-flash")
        .system_instructions(SystemInstructions::Custom(CustomSystemInstructions {
            text: "You are a helpful weather assistant. Use get_weather to answer weather questions.".to_string(),
        }))
        .tool(Arc::new(WeatherTool))
        .hook(Arc::new(AuditHook))
        .policies(vec![
            policy::deny_all(),              // Deny everything by default
            policy::allow("get_weather"),     // Allow only our custom tool
        ])
        .build()
        .start()
        .await?;

    let response = agent.chat("What's the weather like in Tokyo?").await?;
    println!("Agent: {}", response.text);

    agent.stop().await?;
    Ok(())
}
```

### 4. Structured Output

Constrain the agent to return JSON matching a schema:

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

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let schema = r#"{
        "type": "object",
        "properties": {
            "name":    { "type": "string" },
            "capital": { "type": "string" },
            "population": { "type": "integer" }
        },
        "required": ["name", "capital", "population"]
    }"#;

    let agent = Agent::builder()
        .api_key("your-api-key")
        .default_model("gemini-3.5-flash")
        .response_schema(schema)
        .allow_all()
        .build()
        .start()
        .await?;

    let response = agent.chat("Tell me about Japan.").await?;
    println!("Raw text: {}", response.text);

    // The last Finish step contains the parsed structured output
    for step in &response.steps {
        if let Some(ref output) = step.structured_output {
            println!("Structured: {}", serde_json::to_string_pretty(output)?);
        }
    }

    agent.stop().await?;
    Ok(())
}
```

### 5. Streaming via `conversation()`

For token-by-token streaming, access the `Conversation` directly:

```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> {
    let agent = Agent::builder()
        .allow_all()
        .build()
        .start()
        .await?;

    let conversation = agent.conversation();

    // Send a prompt and get a streaming chunk iterator
    let mut stream = conversation.chat("Write a haiku about Rust.").await?;

    // Process chunks as they arrive
    while let Some(chunk_result) = stream.next().await {
        match chunk_result? {
            StreamChunk::Thought { text, .. } => {
                eprint!("[thinking] {}", text);
            }
            StreamChunk::Text { text, .. } => {
                print!("{}", text);
            }
            StreamChunk::ToolCall(call) => {
                println!("\n[tool] {} called with: {}", call.name, call.args);
            }
        }
    }
    println!();

    // Access conversation metadata
    println!("Total turns: {}", conversation.turn_count().await);
    println!("Total tokens: {}", conversation.total_usage().await.total_token_count);
    println!("History steps: {}", conversation.history().await.len());

    agent.stop().await?;
    Ok(())
}
```

### 6. Read-Only Agent

Restrict the agent to only read files — no writes, no commands:

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

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let agent = Agent::builder()
        .workspaces(vec!["/home/user/project".to_string()])
        .read_only()    // Only FindFile, ListDir, ViewFile, SearchDir
        .build()
        .start()
        .await?;

    let response = agent.chat("Summarize the README.md").await?;
    println!("{}", response.text);

    agent.stop().await?;
    Ok(())
}
```

### 7. Multi-Turn Conversation

The agent maintains state across turns within a single session:

```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?;

    let r1 = agent.chat("My name is Alice.").await?;
    println!("Agent: {}", r1.text);

    let r2 = agent.chat("What's my name?").await?;
    println!("Agent: {}", r2.text);  // Should reference "Alice"

    let r3 = agent.chat("Summarize our conversation.").await?;
    println!("Agent: {}", r3.text);

    agent.stop().await?;
    Ok(())
}
```

### 8. Advanced: Vertex AI + Full GeminiConfig

Use Vertex AI backend with custom model configuration:

```rust,no_run
use antigravity_sdk_rust::agent::Agent;
use antigravity_sdk_rust::types::{
    GeminiConfig, GenerationConfig, ModelConfig, ModelEntry, ThinkingLevel,
};

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let gemini_config = GeminiConfig {
        vertex: true,
        project: Some("my-gcp-project".to_string()),
        location: Some("us-central1".to_string()),
        models: ModelConfig {
            default: ModelEntry {
                name: "gemini-3.5-flash".to_string(),
                api_key: None,
                generation: GenerationConfig {
                    thinking_level: Some(ThinkingLevel::High),
                },
            },
            ..Default::default()
        },
        enable_google_search: Some(true),
        enable_url_context: Some(true),
        ..Default::default()
    };

    let agent = Agent::builder()
        .gemini_config(gemini_config)
        .allow_all()
        .build()
        .start()
        .await?;

    let response = agent.chat("Explain quantum computing.").await?;
    println!("{}", response.text);
    if !response.thinking.is_empty() {
        println!("\n--- Thinking ---\n{}", response.thinking);
    }

    agent.stop().await?;
    Ok(())
}
```

---

## Comparison with Python SDK

| Feature | Python SDK | Rust SDK |
|---|---|---|
| Policy enforcement | Runtime `ValueError` during `agent.connect()` | Compile-time typestate — `.build()` unavailable without policies |
| Tool trait | `Tool` base class with `call(args)` | `Tool` trait with `name()`, `description()`, `parameters_json_schema()`, `call(args)` |
| Hook trait | `Hook` base class, overridable methods | `Hook` trait with default async no-ops |
| Builder pattern | `Agent(model=..., tools=[...], ...)` constructor kwargs | Typestate builder: `Agent::builder().model().tools().allow_all().build()` |
| Streaming | `async for chunk in conversation.chat(prompt)` | `conversation.chat(prompt).await?``StreamExt::next()` on `BoxStream` |
| Lifecycle states | Implicit (`agent.connect()` / `agent.close()`) | Typestate: `Agent<Unstarted>` / `Agent<Started>` — method availability enforced at compile time |
| Async runtime | `asyncio` | `tokio` |
| Object safety | Not applicable (duck typing) | `DynTool` / `DynHook` / `DynTrigger` object-safe wrappers via blanket impls |

---

## Related Types

For deeper dives into the types referenced here, see:

- **[`Conversation`]conversation.md** — Stateful session wrapper with streaming and history
- **[`Tool`]tools.md** — Custom tool trait and registration
- **[`Hook`]hooks.md** — Lifecycle event hooks
- **[`Policy`]policy.md** — Safety policy system
- **[`Trigger`]triggers.md** — Background async workers
- **[`GeminiConfig`]types.md** — Model and API configuration
- **[`McpServerConfig`]types.md** — MCP server configuration (stdio, SSE, HTTP)