# Tool calling

> The ToolsPlugin provides a registry for defining and executing tools that LLMs can invoke.

`ToolsPlugin` provides a `ToolRegistry` global resource that stores tool definitions and dispatches execution. Tools are async functions annotated with `#[tool]`, which generates the `Tool` trait implementation and a JSON schema automatically. The LLM sees these schemas as available functions it can call, and your systems execute the calls and return results.

```rust
use polaris_ai::system::server::Server;
use polaris_ai::tools::{tool, ToolError, ToolRegistry, ToolsPlugin};


#[tool]
/// Search for documents matching a query.
async fn search(
    /// The search query string.
    query: String,
    /// Maximum number of results to return.
    #[default(10)]
    limit: usize,
) -> Result<String, ToolError> {
    Ok(format!("Found results for: {query} (limit {limit})"))
}
```

The `#[tool]` macro turns this function into a struct that implements the `Tool` trait. The function’s doc comment becomes the tool’s description. Each parameter’s doc comment becomes that parameter’s description in the JSON schema. The macro generates the schema from the parameter types — `String`, `usize`, `bool`, and other standard types are supported out of the box.

## Optional parameters

Mark a parameter as optional with the `#[default(value)]` attribute. When the LLM omits that argument, the default value is used instead.

```rust
use polaris_ai::polaris_tools;
use polaris_ai::tools::{tool, ToolError};


#[tool]
/// Fetch a web page.
async fn fetch_url(
    /// The URL to fetch.
    url: String,
    /// Request timeout in seconds.
    #[default(30)]
    timeout_secs: u64,
    /// Whether to follow redirects.
    #[default(true)]
    follow_redirects: bool,
) -> Result<String, ToolError> {
    Ok(format!("Fetched {url}"))
}
```

In the generated JSON schema, `timeout_secs` and `follow_redirects` are not listed as required properties. The LLM can include them or leave them out.

## ToolRegistry

`ToolRegistry` is the central store for tools. It is created by `ToolsPlugin` during the server’s `build` phase and promoted to a `GlobalResource` during `ready`. Systems access it through `Res<ToolRegistry>`.

The registry provides four main operations:

* `register` adds a single tool. The tool’s name must be unique; a duplicate panics.
* `register_toolset` adds all tools from a `Toolset` implementation at once.
* `definitions` returns `Vec<ToolDefinition>` for all registered tools, which you pass to the LLM request.
* `execute` runs a tool by name with JSON arguments and returns a JSON result.

```rust
use polaris_ai::tools::ToolRegistry;


fn configure_registry(registry: &mut ToolRegistry) {
    registry.register(search());
    registry.register(fetch_url());
}
```

Calling `search()` returns the macro-generated struct instance. You pass it directly to `register`.

## Registering tools in a plugin

Tools belong in plugins. Your plugin declares a dependency on `ToolsPlugin`, then registers tools during `build`.

```rust
use polaris_ai::system::plugin::{Plugin, PluginId, Version};
use polaris_ai::system::server::Server;
use polaris_ai::tools::{tool, ToolError, ToolRegistry, ToolsPlugin};


#[tool]
/// Search for documents matching a query.
async fn search(
    /// The search query string.
    query: String,
    /// Maximum number of results to return.
    #[default(10)]
    limit: usize,
) -> Result<String, ToolError> {
    Ok(format!("Found results for: {query}"))
}


struct SearchPlugin;


impl Plugin for SearchPlugin {
    const ID: &'static str = "myapp::search";
    const VERSION: Version = Version::new(0, 1, 0);


    fn dependencies(&self) -> Vec<PluginId> {
        vec![PluginId::of::<ToolsPlugin>()]
    }


    fn build(&self, server: &mut Server) {
        let mut registry = server
            .get_resource_mut::<ToolRegistry>()
            .expect("ToolsPlugin must be added before SearchPlugin");
        registry.register(search());
    }
}
```

The dependency on `ToolsPlugin` ensures the `ToolRegistry` exists by the time `build` runs.

## Toolsets

When you have several related tools, group them into a `Toolset` using the `#[toolset]` macro on an impl block. Each method marked with `#[tool]` becomes an individual tool, and the struct’s fields are available as shared state.

```rust
use polaris_ai::polaris_tools;
use polaris_ai::tools::{tool, toolset, ToolError};
use std::path::PathBuf;


#[derive(Clone)]
struct FileTools {
    working_dir: PathBuf,
}


#[toolset]
impl FileTools {
    #[tool]
    /// List files in a directory.
    async fn list_files(
        &self,
        /// Directory path relative to the working directory.
        path: String,
    ) -> Result<String, ToolError> {
        let resolved = self.working_dir.join(&path);
        let mut entries = tokio::fs::read_dir(&resolved)
            .await
            .map_err(|e| ToolError::execution_error(e.to_string()))?;
        let mut files = Vec::new();
        while let Some(entry) = entries.next_entry().await.transpose() {
            if let Ok(entry) = entry {
                files.push(entry.file_name().to_string_lossy().to_string());
            }
        }
        Ok(files.join("\n"))
    }


    #[tool]
    /// Read the contents of a file.
    async fn read_file(
        &self,
        /// File path relative to the working directory.
        path: String,
    ) -> Result<String, ToolError> {
        let resolved = self.working_dir.join(&path);
        tokio::fs::read_to_string(&resolved)
            .await
            .map_err(|e| ToolError::execution_error(e.to_string()))
    }
}
```

Register a toolset with `register_toolset`:

```rust
use polaris_ai::tools::ToolRegistry;
use std::path::PathBuf;


fn register_file_tools(registry: &mut ToolRegistry) {
    registry.register_toolset(FileTools {
        working_dir: PathBuf::from("/tmp/sandbox"),
    });
}
```

The `Toolset` trait has one method — `fn tools(self) -> Vec<Box<dyn Tool>>` — which the macro generates. The generated tools share the toolset instance through an `Arc`.

## Wiring tools into LLM calls

To make tools available to the LLM, use the `with_registry` extension method on `LlmRequestBuilder`. This adds all registered tool definitions to the request so the model knows which functions it can call.

```rust
use polaris_ai::tools::LlmRequestBuilderExt;
use polaris_ai::models::llm::Llm;
use polaris_ai::tools::ToolRegistry;


async fn call_with_tools(llm: &Llm, registry: &ToolRegistry) {
    let response = llm
        .builder()
        .with_registry(registry)
        .system("You are a helpful assistant with access to tools.")
        .user("Find documents about Rust concurrency.")
        .generate()
        .await
        .expect("LLM generation failed");
}
```

You can also add tools individually with `with_tool` or from a toolset with `with_toolset`, but `with_registry` is the typical approach when you have a `ToolRegistry` available as a resource.

## Executing tool calls

When the LLM response contains tool calls, you execute them through the registry and feed the results back as messages for the next turn.

```rust
use polaris_ai::models::llm::{Llm, Message, ToolResultContent};
use polaris_ai::tools::ToolRegistry;
use polaris_ai::tools::LlmRequestBuilderExt;


async fn execute_tool_calls(
    llm: &Llm,
    registry: &ToolRegistry,
    messages: &mut Vec<Message>,
) {
    let response = llm
        .builder()
        .with_registry(registry)
        .system("You are a helpful assistant.")
        .messages(messages.clone())
        .generate()
        .await
        .expect("LLM generation failed");


    if response.has_tool_calls() {
        // Add the assistant's response (which contains the tool calls) to history
        messages.push(Message::assistant_from_blocks(response.content.clone()));


        for call in response.tool_calls() {
            let result = registry
                .execute(&call.function.name, &call.function.arguments)
                .await;


            let content = match result {
                Ok(value) => ToolResultContent::Text(value.to_string()),
                Err(err) => ToolResultContent::Text(format!("Error: {err}")),
            };


            messages.push(Message::tool_result(&call.id, content));
        }
    }
}
```

Each `ToolCall` in the response has an `id` (which you pass back in the tool result so the LLM can match results to calls) and a `function` field containing the `name` and `arguments`. The `registry.execute` method looks up the tool by name and runs it with the provided JSON arguments.

This execute-and-append pattern is the core of a tool-use loop. In a full agent, you would wrap this in a graph loop that continues calling the LLM until it responds without tool calls. See the [Building a ReAct agent](/docs/polaris/guides/building-a-react-agent.md) guide for a complete example.

## Per-call context and permissions

Use `ToolRegistry::execute_with(name, arguments, &context)` when a tool declares `#[context]` parameters. `execute` creates an empty `ToolContext`, so it cannot supply workspace, identity or permission state expected by such tools. Check the tool’s context requirements before invoking it.

The file example above demonstrates a toolset, not a filesystem sandbox: joining a path alone does not restrict traversal or symlinks. Use the workspace’s permission-aware shell/filesystem tools or enforce a boundary in your application before exposing file access to a model.

## The reason extension

The `LlmReasonExt` trait adds a `reason` method to `LlmRequestBuilder` as a convenience for single-shot tool-use calls. It behaves like `generate` but validates that at least one tool definition is present before making the call.

```rust
use polaris_ai::tools::{LlmRequestBuilderExt, LlmReasonExt};
use polaris_ai::models::llm::Llm;
use polaris_ai::tools::ToolRegistry;


async fn single_shot(llm: &Llm, registry: &ToolRegistry) {
    let response = llm
        .builder()
        .with_registry(registry)
        .user("What files are in the current directory?")
        .reason()
        .await
        .expect("reason call failed");


    if response.has_tool_calls() {
        // Handle tool calls
    } else {
        println!("{}", response.text());
    }
}
```

`reason` returns `ReasonError::NoTools` if you call it without adding any tool definitions, and `ReasonError::Generation` if the underlying LLM call fails.
