Skip to content

Tools

Tools let an agent call Rust functions during its agentic loop. When the model decides to use a tool, the runtime executes the registered function, appends the result to the conversation, and continues the loop. The model calls tools by name; the runtime dispatches to the matching implementation.


The Tool Trait

Sync tools implement four methods.

use potato_type::tools::Tool;
use potato_type::TypeError;
use serde_json::{json, Value};

#[derive(Debug)]
struct WeatherTool;

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

    fn description(&self) -> &str {
        "Get the current temperature for a city in Celsius"
    }

    fn parameter_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name"
                }
            },
            "required": ["city"]
        })
    }

    fn execute(&self, args: Value) -> Result<Value, TypeError> {
        let city = args["city"]
            .as_str()
            .ok_or_else(|| TypeError::Error("missing city".into()))?;

        Ok(json!({ "city": city, "temperature_celsius": 22 }))
    }
}

parameter_schema is sent verbatim to the LLM as a JSON Schema. The model generates arguments matching that schema; execute receives those arguments as a serde_json::Value.

Return a JSON object with descriptive keys so the model can parse the output. Return Err(TypeError::Error(...)) to surface a tool failure as AgentError — this stops the loop.

Registering a sync tool

let agent = AgentBuilder::new()
    .provider(Provider::OpenAI)
    .model("gpt-4o")
    .with_tool(WeatherTool)         // accepts any type implementing Tool + 'static
    .build()
    .await?;

The AsyncTool Trait

For tools that call external services or do async I/O, implement AsyncTool.

use async_trait::async_trait;
use potato_type::tools::AsyncTool;
use potato_type::TypeError;
use serde_json::{json, Value};

#[derive(Debug)]
struct DatabaseLookupTool;

#[async_trait]
impl AsyncTool for DatabaseLookupTool {
    fn name(&self) -> &str {
        "lookup_record"
    }

    fn description(&self) -> &str {
        "Look up a record by ID in the application database"
    }

    fn parameter_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "id": { "type": "string", "description": "Record ID" }
            },
            "required": ["id"]
        })
    }

    async fn execute(&self, args: Value) -> Result<Value, TypeError> {
        let id = args["id"].as_str().unwrap_or_default();
        // async database call
        Ok(json!({ "id": id, "name": "example record", "status": "active" }))
    }
}

Registering an async tool

use std::sync::Arc;

let agent = AgentBuilder::new()
    .provider(Provider::OpenAI)
    .model("gpt-4o")
    .with_async_tool(Arc::new(DatabaseLookupTool))
    .build()
    .await?;

Dispatch order

The runtime checks async tools first, then sync tools. If you register the same tool name as both async and sync, the async variant takes precedence.


Sub-Agents as Tools

An AgentRunner can be wrapped as a tool, allowing one agent to call another as part of its tool-use loop. This is the primary mechanism for composing specialized agents.

use potato_agent::{AgentBuilder, AgentRunner, SessionState};
use potato_type::Provider;
use std::sync::Arc;

// Build a specialized sub-agent
let research_agent = AgentBuilder::new()
    .provider(Provider::OpenAI)
    .model("gpt-4o")
    .system_prompt("You are a research assistant. Return factual summaries.")
    .max_iterations(3)
    .build()
    .await?;

// Register it as a tool on a parent agent
let orchestrator = AgentBuilder::new()
    .provider(Provider::OpenAI)
    .model("gpt-4o")
    .system_prompt("You are an orchestrator. Delegate research to the research tool.")
    .with_sub_agent(
        "research",
        "Research a topic and return a factual summary",
        research_agent as Arc<dyn AgentRunner>,
    )
    .build()
    .await?;

The sub-agent tool has a fixed parameter schema: {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]}. The model passes its query as the input field. The tool runs the sub-agent's full agentic loop and returns the final text response as the tool result.

Sub-agent tool policy

You can restrict what sub-agents are allowed to call using AgentToolPolicy.

use potato_agent::tool_ext::AgentToolPolicy;
use std::collections::HashSet;

let policy = AgentToolPolicy {
    disallow_sub_agent_calls: true,   // sub-agent cannot call any AgentTools
    disallowed_agent_ids: HashSet::new(),
};

let orchestrator = AgentBuilder::new()
    .provider(Provider::OpenAI)
    .model("gpt-4o")
    .with_sub_agent_policy(
        "research",
        "Research a topic",
        research_agent as Arc<dyn AgentRunner>,
        policy,
    )
    .build()
    .await?;
Policy field Effect
disallow_sub_agent_calls: true Sub-agent cannot invoke any tools that are AgentTool instances
disallowed_agent_ids Sub-agent cannot call any agent in the blocklist by agent ID

Circular Call Prevention

The runtime tracks ancestor agent IDs in SessionState under the reserved key __ancestor_ids. Before dispatching a sub-agent tool, it checks whether the target agent's ID is already in the ancestor chain. If it is, AgentError::CircularAgentCall is returned.

This applies regardless of nesting depth. If A calls B calls C calls A, the third dispatch is rejected.

The __ancestor_ids key is system-reserved and is not merged back to the parent session when a child agent completes.


Tool Trait Reference

Tool (sync)

Method Signature Required
name fn name(&self) -> &str Yes
description fn description(&self) -> &str Yes
parameter_schema fn parameter_schema(&self) -> Value Yes
execute fn execute(&self, args: Value) -> Result<Value, TypeError> Yes

Implement Send + Sync + Debug in addition to the trait methods.

AsyncTool

Same as Tool but execute is async:

async fn execute(&self, args: Value) -> Result<Value, TypeError>

Register with Arc::new(my_tool) and .with_async_tool(...).