Tools

Tool registry, executors, three-tier classification, approval handlers.

Tools

Tools are the way Forge agents act on the world. Every tool is registered once, classified by tier, gated by capability, and approved per-call.

Registering a tool

use forge::prelude::*;

let registry = ToolRegistry::new();

registry.register(FnToolExecutor::new("get_weather", |input: serde_json::Value| async move {
    let city = input["city"].as_str().unwrap_or("Mountain View");
    let weather = fetch_weather(city).await?;
    Ok(serde_json::json!({ "temperature_f": weather.temp_f, "conditions": weather.conditions }))
}));

FnToolExecutor::new(name, handler) is the simplest path. For richer behavior, implement the ToolExecutor trait:

#[async_trait]
pub trait ToolExecutor: Send + Sync {
    fn name(&self) -> &str;
    fn definition(&self) -> ToolDefinition;
    async fn execute(&self, input: serde_json::Value) -> ForgeResult<serde_json::Value>;
}

ToolDefinition

What the model sees:

pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    pub input_schema: serde_json::Value,  // JSON Schema
    pub tier: ToolTier,
    pub required_scopes: Vec<String>,
}

The registry produces these from your registered executors. Pass them to the agent via AgentConfig::with_tool_registry(&registry).

Three tiers

pub enum ToolTier {
    Read,     // observes; safe by default in many policies
    Write,    // modifies state; requires explicit approval
    Execute,  // runs external code; requires explicit policy + scope
}

Each approval handler maps tiers to its own policy. The default handlers:

Handler Read Write Execute
AutoApprove grant grant grant
RejectAll reject reject reject
TierPolicy grant review review

AutoApprove is dev only.

Approval handlers

#[async_trait]
pub trait ApprovalHandler: Send + Sync {
    async fn approve(
        &self,
        tool_name: &str,
        input: &serde_json::Value,
        tier: ToolTier,
    ) -> ApprovalDecision;
}

pub enum ApprovalDecision {
    Granted,
    Rejected { reason: String },
    DeferredToHuman { request_id: String },
}

A custom handler for a chat product might post to a Slack channel and wait for a thumbs-up:

struct SlackApproval { client: SlackClient }

#[async_trait]
impl ApprovalHandler for SlackApproval {
    async fn approve(
        &self,
        tool_name: &str,
        input: &serde_json::Value,
        tier: ToolTier,
    ) -> ApprovalDecision {
        if matches!(tier, ToolTier::Read) {
            return ApprovalDecision::Granted;
        }
        let request_id = self.client.post_approval_request(tool_name, input).await?;
        ApprovalDecision::DeferredToHuman { request_id }
    }
}

DeferredToHuman parks the tool call until a submit_approval(request_id, decision) arrives via the harness adapter or another channel. The agent's loop pauses on that invocation; other invocations continue.

Execution path

When the model emits a tool call, Forge:

  1. Looks up the executor in the registry by name
  2. Checks the agent's ACT contains all required_scopes for the tool
  3. Calls the approval handler
  4. If granted, calls executor.execute(input)
  5. Records a ToolInvocationRecord (success / failed / timeout / rejected)
  6. Appends the result to the message list for the next iteration

Each step has a configurable timeout (StreamingLoopConfig::tool_timeout).

Built-in executors

The forge-tool crate ships:

  • FnToolExecutor — wrap an async closure
  • MockToolExecutor — for tests
  • BuilderExecutor — for tool definitions whose execution lives elsewhere (e.g., MCP, harness-rendered tools)

For HTTP-style tools, see forge-web which provides web-fetch primitives wired into the registry.

MCP tools

Tools served by an MCP server can be auto-registered:

use forge::mcp::{MCPClient, register_mcp_tools};

let mcp = MCPClient::connect("stdio:./my-mcp-server").await?;
register_mcp_tools(&mcp, &registry).await?;

See MCP integration.

Next