Tools

August 2, 2026 · View on GitHub

In-process tool execution for the Antigravity Rust SDK.

Overview

Tools are Rust functions exposed to the Gemini model for invocation. The SDK provides a trait-based system with JSON schema validation, automatic registration, and optional session-scoped context.

Tool Trait

Define a tool by implementing the Tool trait:

use antigravity_sdk_rust::tools::Tool;
use serde_json::Value;

struct WeatherTool;

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

    fn description(&self) -> &str {
        "Get the current weather for a city."
    }

    fn parameters_json_schema(&self) -> &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(|c| c.as_str()).unwrap_or("Unknown");
        Ok(serde_json::json!({
            "temperature": 22,
            "condition": "sunny",
            "city": city
        }))
    }
}

Full Trait Signature

pub trait Tool: Send + Sync {
    /// Unique name used in tool calls from the model.
    fn name(&self) -> &str;

    /// Human-readable description of what the tool does.
    fn description(&self) -> &str;

    /// JSON Schema string describing the expected arguments.
    fn parameters_json_schema(&self) -> &str;

    /// Execute the tool with the given arguments.
    async fn call(&self, args: Value) -> Result<Value, anyhow::Error>;

    /// Whether this tool requires a `ToolContext`. Default: `false`.
    fn needs_context(&self) -> bool { false }

    /// Execute with session context. Default: delegates to `call()`.
    async fn call_with_context(
        &self,
        args: Value,
        ctx: &ToolContext,
    ) -> Result<Value, anyhow::Error> {
        self.call(args).await
    }
}

DynTool (Object-Safe)

The Tool trait uses async fn, which isn't object-safe. The SDK provides DynTool — an object-safe wrapper trait that uses BoxFuture — with an automatic blanket implementation:

// Any T: Tool automatically implements DynTool
// You never need to implement DynTool manually
let tool: Arc<dyn DynTool> = Arc::new(WeatherTool);

ToolRunner

ToolRunner manages tool registration and dispatch:

use antigravity_sdk_rust::tools::ToolRunner;
use std::sync::Arc;

let runner = ToolRunner::new();

// Register tools
runner.register(Arc::new(WeatherTool)).await;

// Execute a single tool by name
let result = runner.execute("get_weather", serde_json::json!({"city": "Tokyo"})).await?;

// Batch-execute multiple tool calls
let results = runner.process_tool_calls(&tool_calls).await;

Tools are stored behind Arc<RwLock<Vec<Arc<dyn DynTool>>>> for concurrent access.

Context-Aware Tools

Tools can opt-in to receiving a ToolContext for session state and agent communication:

use antigravity_sdk_rust::tools::Tool;
use antigravity_sdk_rust::tool_context::ToolContext;
use serde_json::Value;

struct CounterTool;

impl Tool for CounterTool {
    fn name(&self) -> &str { "counter" }
    fn description(&self) -> &str { "Increment and return a counter" }
    fn parameters_json_schema(&self) -> &str { r#"{"type":"object"}"# }

    fn needs_context(&self) -> bool { true } // Opt-in

    async fn call(&self, _args: Value) -> Result<Value, anyhow::Error> {
        // Never reached: a `needs_context` tool called without a context is an
        // error result, not a silent fallback.
        unreachable!()
    }

    async fn call_with_context(
        &self,
        _args: Value,
        ctx: &ToolContext,
    ) -> Result<Value, anyhow::Error> {
        let count: i32 = ctx.get_state("count").unwrap_or(0);
        ctx.set_state("count", count + 1);
        Ok(serde_json::json!({ "count": count + 1 }))
    }
}

ToolContext API

pub struct ToolContext {
    // Methods:
    fn conversation_id(&self) -> Option<String>;
    fn is_idle(&self) -> Option<bool>;
    async fn send(&self, message: &str) -> Result<()>;
    fn get_state<T: DeserializeOwned>(&self, key: &str) -> Option<T>;
    fn set_state<T: Serialize>(&self, key: &str, value: T);
    fn update_state<T, F>(&self, key: &str, transform: F); // atomic read-modify-write
}

The context holds a weak handle to the session — the connection owns the tool runner, so a strong one would be a cycle that never frees. The two Option-returning methods are None, and send errors, once the agent has stopped.

Agent::start() attaches the context. A needs_context tool invoked with none attached returns an error result rather than falling back to call(), which would run the tool in a subtly different mode.

Note: Tool state is independent of Hook state. They use separate stores.

Built-in Tools

The SDK provides these built-in tools (managed by the harness):

ToolDescription
CreateFileCreate a new file
EditFileEdit an existing file
FindFileSearch for files by name
ListDirList directory contents
RunCommandExecute shell commands
SearchDirSearch file contents
ViewFileRead file contents
StartSubagentLaunch sub-agents
GenerateImageGenerate images
FinishSignal task completion
AskQuestionPut a multiple-choice question to the user
SearchWebSearch the web (harness-side)
ReadUrlContentFetch and summarize a URL (harness-side)
GrepSearchGrep-based search

Read-Only Tools

BuiltinTools::read_only() returns: ListDir, SearchDir, FindFile, ViewFile, Finish — matching upstream's BuiltinTools.read_only(). Finish is included because an agent that cannot finish cannot terminate a turn or emit structured output.

Related sets: BuiltinTools::all_tools(), BuiltinTools::file_tools() (upstream's exact three file tools) and BuiltinTools::path_scoped_tools() (what workspace_only scopes — a deliberate superset, see policy scoping).

Agent Builder Integration

Register tools via the builder:

use antigravity_sdk_rust::agent::Agent;
use std::sync::Arc;

let agent = Agent::builder()
    .tool(Arc::new(WeatherTool))       // single tool
    .tools(vec![                        // multiple tools
        Arc::new(WeatherTool),
        Arc::new(CounterTool),
    ])
    .allow_all()
    .build();

Shared State Pattern

For tools that need shared mutable state, use Arc<Mutex<T>>:

use std::sync::{Arc, Mutex};
use std::collections::HashMap;

struct InventoryTool {
    db: Arc<Mutex<HashMap<String, i32>>>,
}

impl Tool for InventoryTool {
    fn name(&self) -> &str { "check_inventory" }
    fn description(&self) -> &str { "Check item inventory" }
    fn parameters_json_schema(&self) -> &str { r#"{"type":"object","properties":{"item":{"type":"string"}}}"# }

    async fn call(&self, args: Value) -> Result<Value, anyhow::Error> {
        let item = args["item"].as_str().unwrap_or("");
        let db = self.db.lock().unwrap();
        let count = db.get(item).copied().unwrap_or(0);
        Ok(serde_json::json!({ "item": item, "count": count }))
    }
}

Python SDK Comparison

PythonRust
@tool decorator or ToolWithSchemaimpl Tool for T trait
ToolRunner.register()ToolRunner::register()
ToolRunner.execute()ToolRunner::execute()
ToolContext with get_state/set_stateToolContext with get_state/set_state
Sync/async auto-detectionAll tools are async

Names must be unique

Registering two tools with the same name is an error, surfaced from Agent::start(). Silently replacing the first — the old behaviour — meant a collision between two modules' tools resolved to whichever registered last, and the model called something the caller never meant to expose.

Registration order is preserved, and it is the order the tool list reaches the model in.

A batch runs concurrently

When the model asks for several tools at once, they execute concurrently and the batch takes as long as its slowest member rather than the sum. Results come back in call order regardless of which finished first. Tools that share mutable state need their own synchronisation.

Failures on the wire

A tool that fails sends the harness both a payload and error_message. The field was never set before, so a failed call was recorded as a success whose output happened to mention an error.

ToolResult carries the failure twice on purpose: error is the message the model is shown, and exception is the same failure as a ToolExecutionError { message, tool_name, server_name } for hooks that route or count failures. server_name is None for built-ins and client-side Rust tools, and set for MCP tools — the name alone is ambiguous across servers.

Arguments are coerced to your schema

Models routinely send "3" where a schema says integer, or "true" for a boolean. Those are converted against the tool's own parameters_json_schema() before the tool sees them, including inside arrays and nested objects, and including a whole array or object that arrived as JSON text.

Only unambiguous conversions are made. "not a number" for an integer is passed through untouched, so a genuine type error still surfaces as one rather than being papered over.

Named subagents

AgentBuilder::subagent(...) declares a subagent the model can delegate to. Three rules are enforced when the config is built, matching upstream:

  • Capabilities default to the read-only built-ins. A subagent that inherited everything is not what "default" should mean.
  • START_SUBAGENT is dropped with a warning: the harness does not support a subagent spawning subagents.
  • Naming a client-side tool that is not registered on the main agent is an error, not a subagent that silently cannot call it.

enabled_tools and disabled_tools are mutually exclusive; setting both is a configuration error rather than a silent precedence rule.