Skip to content

Commit d0e9fd0

Browse files
update docs
1 parent c6df512 commit d0e9fd0

3 files changed

Lines changed: 122 additions & 70 deletions

File tree

ARCHITECTURE.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ The Antigravity SDK orchestrates interactions between an LLM-based agent (runnin
1111
```mermaid
1212
graph TD
1313
A[Agent] --> B[Conversation]
14-
A --> C[LocalConnection]
14+
A --> C[LocalConnection / WasmConnection]
1515
A --> D[ToolRunner]
1616
A --> E[HookRunner]
17+
A --> I[TriggerRunner]
1718
C -->|Subprocess IPC / WebSocket| F[localharness]
19+
C -->|Network WebSocket| F
1820
D -->|Executes| G[Local Tools]
1921
E -->|Intercepts| H[User Hooks / Policies]
22+
I -->|Spawns| J[Background Triggers]
2023
```
2124

2225
---
@@ -25,9 +28,10 @@ graph TD
2528

2629
The SDK leverages several object-oriented and functional design patterns:
2730

28-
### 1. Connection & Strategy Pattern (`connection.rs`, `local.rs`)
29-
- **Connection Trait**: Defines an abstraction for communication. This allows swapping the local subprocess harness with other backends (e.g., remote or mock harnesses) in the future.
30-
- **LocalConnectionStrategy**: Acts as a builder/strategy to configure and initialize a `LocalConnection`.
31+
### 1. Connection & Strategy Pattern (`connection.rs`, `local.rs`, `wasm.rs`)
32+
- **Connection Trait**: Defines an abstraction for communication. This allows swapping the local subprocess harness with other backends (e.g., remote, mock, or WASM-based network harnesses) in the future.
33+
- **LocalConnectionStrategy**: Configures and initializes a `LocalConnection` by spawning a local helper subprocess (for native / non-WASM environments).
34+
- **WasmConnectionStrategy**: Configures and initializes a `WasmConnection` that connects to a remote or host-side `localharness` WebSocket server over TCP (enabled for `target_arch = "wasm32"` environments where subprocess spawning is not supported).
3135

3236
### 2. Observer Pattern (`hooks.rs`)
3337
- **Hook Trait**: Defines lifecycle hooks that users can register to observe and modify agent actions:
@@ -49,13 +53,20 @@ The SDK leverages several object-oriented and functional design patterns:
4953
- **Tool Trait**: Encapsulates specific capabilities (e.g., file edits, command execution, directory searching) into unified command units.
5054
- **ToolRunner**: Coordinates registration and execution of these command objects, mapping harness tool calls to their respective handlers.
5155

56+
### 5. Background Trigger Pattern (`triggers.rs`)
57+
- **Trigger Trait**: Defines asynchronous background tasks (such as status polling, listener intervals, etc.) that can interact with the connection session concurrently.
58+
- **TriggerRunner**: Coordinates and spawns registered triggers in separate tasks when the agent session starts.
59+
60+
### 6. Direct Gemini Client (`direct.rs`)
61+
- **GeminiDirectClient**: A transport-agnostic client that constructs REST request payloads and parses responses directly from the Gemini API. Bypasses the `localharness` and WebSocket loop entirely. It is helpful for environments where TCP/WebSocket or spawning subprocesses is not possible (e.g. lightweight WASI components).
62+
5263
---
5364

5465
## Component Details
5566

56-
### Connection Lifecycle
67+
### Connection Lifecycle (Native Subprocess)
5768

58-
The connection to `localharness` follows a strict handshake and upgrade protocol:
69+
The connection to `localharness` via subprocess follows a strict handshake and upgrade protocol:
5970

6071
```mermaid
6172
sequenceDiagram
@@ -79,6 +90,21 @@ sequenceDiagram
7990
3. **Upgrade**: The SDK initiates a WebSocket client connection to the harness server using the retrieved port and API key, upgrading communication to a structured bi-directional stream.
8091
4. **Disconnection**: When dropped, the subprocess is killed cleanly.
8192

93+
### Connection Lifecycle (WebAssembly Network Target)
94+
95+
For WebAssembly targets (`wasm32-wasip1`), the SDK connects to a running host-side harness over the network rather than spawning a subprocess:
96+
97+
```mermaid
98+
sequenceDiagram
99+
participant SDK as WASM Rust SDK
100+
participant Host as Host Machine (localharness)
101+
102+
SDK->>Host: Open TCP Connection & Upgrade to WebSocket (with API Key)
103+
Host->>SDK: Handshake Completed
104+
SDK->>Host: Send InitializeConversationEvent (HarnessConfig)
105+
Note over SDK,Host: Step execution loop active
106+
```
107+
82108
---
83109

84110
## Thread Safety & Concurrency

skills/google-antigravity-sdk-rust/examples/getting_started/custom_tool.md

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,47 @@ In the Rust SDK, custom tools are defined by implementing the `Tool` trait. You
99
```rust
1010
use antigravity_sdk_rust::agent::{Agent, AgentConfig};
1111
use antigravity_sdk_rust::policy;
12-
use antigravity_sdk_rust::tool::Tool;
12+
use antigravity_sdk_rust::tools::Tool;
1313
use antigravity_sdk_rust::types::GeminiConfig;
1414
use async_trait::async_trait;
15-
use serde_json::json;
15+
use serde_json::Value;
1616
use std::sync::Arc;
1717

1818
// 1. Define the custom tool struct
1919
pub struct WeatherTool;
2020

2121
#[async_trait]
2222
impl Tool for WeatherTool {
23-
fn declaration(&self) -> serde_json::Value {
24-
// Return JSON Schema outlining name, description, and parameters
25-
json!({
26-
"name": "get_current_temperature",
27-
"description": "Gets the current temperature for a given location.",
28-
"parameters": {
29-
"type": "OBJECT",
30-
"properties": {
31-
"location": {
32-
"type": "STRING",
33-
"description": "The city and state, e.g. 'San Francisco, CA'"
34-
}
35-
},
36-
"required": ["location"]
37-
}
38-
})
23+
fn name(&self) -> &str {
24+
"get_current_temperature"
25+
}
26+
27+
fn description(&self) -> &str {
28+
"Gets the current temperature for a given location."
29+
}
30+
31+
fn parameters_json_schema(&self) -> &str {
32+
r#"{
33+
"type": "object",
34+
"properties": {
35+
"location": {
36+
"type": "string",
37+
"description": "The city and state, e.g. 'San Francisco, CA'"
38+
}
39+
},
40+
"required": ["location"]
41+
}"#
3942
}
4043

41-
async fn call(&self, args: serde_json::Value) -> Result<serde_json::Value, anyhow::Error> {
42-
let location = args["location"]
43-
.as_str()
44+
async fn call(&self, args: Value) -> Result<Value, anyhow::Error> {
45+
let location = args
46+
.get("location")
47+
.and_then(Value::as_str)
4448
.ok_or_else(|| anyhow::anyhow!("Missing 'location' parameter"))?;
4549

4650
// In a real application, call an external weather API here
4751
let temperature = format!("The temperature in {} is 72°F.", location);
48-
Ok(json!({ "result": temperature }))
52+
Ok(Value::String(temperature))
4953
}
5054
}
5155

@@ -81,9 +85,9 @@ Here is how to implement a stateful fruit count tracker tool:
8185
```rust
8286
use antigravity_sdk_rust::agent::{Agent, AgentConfig};
8387
use antigravity_sdk_rust::policy;
84-
use antigravity_sdk_rust::tool::Tool;
88+
use antigravity_sdk_rust::tools::Tool;
8589
use async_trait::async_trait;
86-
use serde_json::json;
90+
use serde_json::Value;
8791
use std::collections::HashMap;
8892
use std::sync::{Arc, Mutex};
8993

@@ -102,44 +106,53 @@ impl FruitInventoryTool {
102106

103107
#[async_trait]
104108
impl Tool for FruitInventoryTool {
105-
fn declaration(&self) -> serde_json::Value {
106-
json!({
107-
"name": "record_fruit",
108-
"description": "Records the mention of fruits and updates the total count.",
109-
"parameters": {
110-
"type": "OBJECT",
111-
"properties": {
112-
"fruit_name": {
113-
"type": "STRING",
114-
"description": "The name of the fruit."
115-
},
116-
"count": {
117-
"type": "INTEGER",
118-
"description": "The number of fruits mentioned."
119-
}
109+
fn name(&self) -> &str {
110+
"record_fruit"
111+
}
112+
113+
fn description(&self) -> &str {
114+
"Records the mention of fruits and updates the total count."
115+
}
116+
117+
fn parameters_json_schema(&self) -> &str {
118+
r#"{
119+
"type": "object",
120+
"properties": {
121+
"fruit_name": {
122+
"type": "string",
123+
"description": "The name of the fruit."
120124
},
121-
"required": ["fruit_name", "count"]
122-
}
123-
})
125+
"count": {
126+
"type": "integer",
127+
"description": "The number of fruits mentioned."
128+
}
129+
},
130+
"required": ["fruit_name", "count"]
131+
}"#
124132
}
125133

126-
async fn call(&self, args: serde_json::Value) -> Result<serde_json::Value, anyhow::Error> {
127-
let fruit_name = args["fruit_name"]
128-
.as_str()
134+
async fn call(&self, args: Value) -> Result<Value, anyhow::Error> {
135+
let fruit_name = args
136+
.get("fruit_name")
137+
.and_then(Value::as_str)
129138
.ok_or_else(|| anyhow::anyhow!("Missing 'fruit_name'"))?;
130-
let count = args["count"]
131-
.as_i64()
139+
let count = args
140+
.get("count")
141+
.and_then(Value::as_i64)
132142
.ok_or_else(|| anyhow::anyhow!("Missing 'count'"))? as i32;
133143

134-
let mut inv = self.inventory.lock().unwrap();
144+
let mut inv = self
145+
.inventory
146+
.lock()
147+
.map_err(|e| anyhow::anyhow!("Mutex lock poisoned: {}", e))?;
135148
let entry = inv.entry(fruit_name.to_string()).or_insert(0);
136149
*entry += count;
137150

138151
let result_msg = format!(
139152
"Recorded {} {}(s). Total {} count is now {}.",
140153
count, fruit_name, fruit_name, *entry
141154
);
142-
Ok(json!({ "result": result_msg }))
155+
Ok(Value::String(result_msg))
143156
}
144157
}
145158

skills/google-antigravity-sdk-rust/examples/getting_started/policies.md

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,37 @@ use antigravity_sdk_rust::types::{GeminiConfig, ToolCall};
1313
use serde_json::Value;
1414
use std::sync::Arc;
1515

16-
// Predicate to intercept dangerous shell commands containing 'rm'
1716
fn block_rm_predicate(tool_call: &ToolCall) -> bool {
18-
tool_call.args.get("CommandLine")
17+
tool_call
18+
.args
19+
.get("CommandLine")
1920
.and_then(Value::as_str)
2021
.is_some_and(|cmd| cmd.contains("rm"))
2122
}
2223

23-
// Predicate to identify key files or production environments
2424
fn critical_file_predicate(tool_call: &ToolCall) -> bool {
25-
tool_call.args.get("TargetFile")
25+
tool_call
26+
.args
27+
.get("TargetFile")
28+
.or_else(|| tool_call.args.get("target_file"))
29+
.or_else(|| tool_call.args.get("path"))
2630
.and_then(Value::as_str)
27-
.is_some_and(|p| p.ends_with(".key") || p.contains("production"))
31+
.is_some_and(|p| {
32+
std::path::Path::new(p)
33+
.extension()
34+
.is_some_and(|ext| ext.eq_ignore_ascii_case("key"))
35+
|| p.contains("production")
36+
})
2837
}
2938

30-
// Handler simulation for 'AskUser' decision
3139
fn programmatic_approval_handler(tool_call: &ToolCall) -> bool {
32-
println!("[Policy Interceptor] Prompting for critical write: {}", tool_call.name);
33-
// Simulating user rejection
34-
false
40+
println!(
41+
"\n [ASK_USER Handler] Intercepted request for tool: {}",
42+
tool_call.name
43+
);
44+
println!(" [ASK_USER Handler] Target arguments: {}", tool_call.args);
45+
println!(" [ASK_USER Handler] Simulating user review... Decision: DENY.");
46+
false
3547
}
3648

3749
#[tokio::main]
@@ -42,28 +54,29 @@ async fn main() -> Result<(), anyhow::Error> {
4254
gemini_config.models.default.name = "gemini-3.5-flash".to_string();
4355
config.gemini_config = gemini_config;
4456

45-
// Define policy list (evaluated in reverse order)
57+
// Configure policies using the recommended "Deny by Default" posture.
4658
let policies = vec![
4759
// 1. Deny everything by default
4860
policy::deny_all(),
49-
// 2. Allow directory listings
61+
// 2. Allow listing directories
5062
policy::allow("LIST_DIR"),
51-
// 3. Deny commands containing 'rm', fallback to allow general commands
63+
// 3. Allow running commands, but block dangerous 'rm' commands
5264
Policy::new(
5365
"RUN_COMMAND".to_string(),
5466
Decision::Deny,
5567
Some(Arc::new(block_rm_predicate)),
5668
None,
5769
"block-rm".to_string(),
5870
),
71+
// Fallback: Allow general RUN_COMMAND calls if they don't match the rm block predicate
5972
policy::allow("RUN_COMMAND"),
60-
// 4. Prompt user before writes matching critical predicates, allow general writes
73+
// 4. Allow editing/creating files, but ask the user first if it's a critical file
6174
Policy::new(
6275
"WRITE_TO_FILE".to_string(),
6376
Decision::AskUser,
6477
Some(Arc::new(critical_file_predicate)),
6578
Some(Arc::new(programmatic_approval_handler)),
66-
"ask-critical-writes".to_string(),
79+
"ask-for-critical-writes".to_string(),
6780
),
6881
policy::allow("WRITE_TO_FILE"),
6982
];
@@ -79,7 +92,7 @@ async fn main() -> Result<(), anyhow::Error> {
7992
agent.chat("Delete all files using rm -rf.").await?;
8093

8194
// Blocked action - production.key write rejected by custom handler
82-
agent.chat("Create a new file named production.key with secret data.").await?;
95+
agent.chat("Create a new configuration file named production.key with content 'debug=true'.").await?;
8396

8497
agent.stop().await?;
8598
Ok(())

0 commit comments

Comments
 (0)