Tools
Tools extend agents with callable functions that LLMs can trigger during generation. ActiveAgent provides a unified interface across providers while highlighting provider-specific capabilities.
Quick Start
Define a method in your agent and register it as a tool:
class WeatherAgent < ApplicationAgent
generate_with :openai, model: "gpt-4o"
def weather_update
prompt(
input: "What's the weather in Boston?",
tools: [ {
name: "get_weather",
description: "Get current weather for a location",
parameters: {
type: "object",
properties: {
location: { type: "string", description: "City and state" }
},
required: [ "location" ]
}
} ]
)
end
def get_weather(location:)
{ location: location, temperature: "72°F", conditions: "sunny" }
end
endresponse = WeatherAgent.weather_update.generate_nowThe LLM calls get_weather automatically when it needs weather data, and uses the result to generate its response.
Provider Support Matrix
| Provider | Functions | Server-side Tools | Notes |
|---|---|---|---|
| OpenAI | 🟩 | 🟩 | Server-side tools require Responses API |
| Anthropic | 🟩 | 🟩 | Full support for built-in tools |
| OpenRouter | 🟩 | ❌ | Model-dependent capabilities |
| Ollama | 🟩 | ❌ | Model-dependent capabilities |
| RubyLLM | 🟩 | ❌ | Depends on underlying provider/model |
| Mock | 🟦 | ❌ | Accepted but not enforced |
For MCP (Model Context Protocol) support, see the MCP documentation.
Functions
Functions are callable methods in your agent that LLMs can trigger with appropriate parameters. All providers support the common format described above.
Basic Function Registration
Using the common format, register functions by passing tool definitions to the tools parameter:
class WeatherAgent < ApplicationAgent
generate_with :anthropic, model: "claude-haiku-4-5"
def weather_update
prompt(
message: "What's the weather in San Francisco?",
max_tokens: 1024,
tools: [ {
name: "get_weather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA"
}
},
required: [ "location" ]
}
} ]
)
end
def get_weather(location:)
{ location: location, temperature: "72°F", conditions: "sunny" }
end
endclass WeatherAgent < ApplicationAgent
generate_with :ollama, model: "qwen3:latest"
def weather_update
prompt(
message: "What's the weather in Boston?",
tools: [ {
name: "get_current_weather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA"
},
unit: {
type: "string",
enum: [ "celsius", "fahrenheit" ]
}
},
required: [ "location" ]
}
} ]
)
end
def get_current_weather(location:, unit: "fahrenheit")
{ location: location, unit: unit, temperature: "22" }
end
endclass WeatherAgent < ApplicationAgent
generate_with :openai, model: "gpt-4o"
def weather_update
prompt(
input: "What's the weather in Boston?",
tools: [ {
name: "get_current_weather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA"
},
unit: {
type: "string",
enum: [ "celsius", "fahrenheit" ]
}
},
required: [ "location" ]
}
} ]
)
end
def get_current_weather(location:, unit: "fahrenheit")
{ location: location, unit: unit, temperature: "22", conditions: "sunny" }
end
endclass WeatherAgent < ApplicationAgent
generate_with :openrouter, model: "google/gemini-2.0-flash-001"
def weather_update
prompt(
message: "What's the weather in Boston?",
tools: [ {
name: "get_current_weather",
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA"
},
unit: {
type: "string",
enum: [ "celsius", "fahrenheit" ]
}
},
required: [ "location" ]
}
} ]
)
end
def get_current_weather(location:, unit: "fahrenheit")
{ location: location, unit: unit, temperature: "22" }
end
endWhen the LLM decides to call a tool, ActiveAgent routes the call to your agent method and returns the result automatically.
Common Tools Format (Recommended)
ActiveAgent supports a universal common format for tool definitions that works seamlessly across all providers. This format eliminates the need to learn provider-specific syntax and makes your code portable.
Format Specification
{
name: "function_name", # Required: function name to call
description: "What it does", # Required: clear description for LLM
parameters: { # Required: JSON Schema for parameters
type: "object",
properties: {
param_name: {
type: "string",
description: "Parameter description"
}
},
required: ["param_name"]
}
}Cross-Provider Example
The same tool definition works everywhere:
# Define once, use with any provider
module WeatherTool
extend ActiveSupport::Concern
WEATHER_TOOL = {
name: "get_weather",
description: "Get current weather for a location",
parameters: {
type: "object",
properties: {
location: { type: "string", description: "City and state" },
unit: { type: "string", enum: [ "celsius", "fahrenheit" ] }
},
required: [ "location" ]
}
}
def get_current_weather(location:, unit: "fahrenheit")
{ location: location, unit: unit, temperature: "22" }
end
endclass AnthropicAgent < ApplicationAgent
include WeatherTool
generate_with :anthropic, model: "claude-haiku-4-5"
def check_weather
prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ])
end
endclass OllamaAgent < ApplicationAgent
include WeatherTool
generate_with :ollama, model: "qwen3:latest"
def check_weather
prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ])
end
endclass OpenAIAgent < ApplicationAgent
include WeatherTool
generate_with :openai, model: "gpt-4o"
def check_weather
prompt(input: "What's the weather?", tools: [ WEATHER_TOOL ])
end
endclass OpenRouterAgent < ApplicationAgent
include WeatherTool
generate_with :openrouter, model: "google/gemini-2.0-flash-001"
def check_weather
prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ])
end
endAlternative: input_schema Key
You can also use input_schema instead of parameters - both work identically:
{
name: "get_weather",
description: "Get current weather",
input_schema: { # Alternative to 'parameters'
type: "object",
properties: { ... }
}
}ActiveAgent automatically converts between common format and each provider's native format behind the scenes.
Tool Choice Control
Control when and which tools the LLM uses with the tool_choice parameter:
# Auto (default) - Let the model decide whether to use tools
prompt(message: "...", tools: tools, tool_choice: "auto")
# Required - Force the model to use at least one tool
prompt(message: "...", tools: tools, tool_choice: "required")
# None - Prevent tool usage entirely
prompt(message: "...", tools: tools, tool_choice: "none")
# Specific tool - Force a particular tool (common format)
prompt(message: "...", tools: tools, tool_choice: { name: "get_weather" })ActiveAgent automatically maps these common values to provider-specific formats:
- OpenAI:
"auto","required","none", or{type: "function", function: {name: "..."}} - Anthropic:
{type: :auto},{type: :any},{type: :tool, name: "..."} - OpenRouter:
"auto","any"(equivalent to "required") - Ollama: Model-dependent tool choice support
Server-Side Tools (Provider-Specific)
Some providers offer built-in tools that run on their servers, providing capabilities like web search and code execution without custom implementation.
OpenAI Built-in Tools
OpenAI's Responses API provides several built-in tools (requires GPT-5, GPT-4.1, o3, etc.) including Web Search for current information, File Search for querying vector stores, and other tools like image generation, code interpreter, and computer use. For complete details and examples, see OpenAI's tools documentation and the OpenAI Provider documentation.
Anthropic Built-in Tools
Anthropic provides web access and specialized capabilities including Web Search for real-time information, Web Fetch (Beta) for specific URLs, Extended Thinking to show reasoning processes, and Computer Use (Beta) for interface interaction. For complete details and examples, see Anthropic's tool use documentation.
Troubleshooting
Tool Not Being Called
If the LLM doesn't call your function when expected, improve the tool description or use tool_choice: "required" to force tool usage.
Invalid Parameters
If the LLM passes unexpected parameters, add detailed parameter descriptions with enum for restricted choices and mark required parameters explicitly.
Bounded Reads Over a Model (Schema Tools)
An agent given only generic tools has nothing to call when asked about the application's own records, so it answers from the prompt and invents the rest. ActiveAgent::SchemaTools closes that gap with a fixed, enumerable roster generated from one model — find_<records>, count_<records>, get_<record> — over the columns you declare and through the scope you name:
# app/agent_tools/reservation_tools.rb
class ReservationTools < ActiveAgent::SchemaTools
model Reservation
filterable :status, :guest_id, :arrives_on # the only columns a filter may name
returns :id, :status, :guest_id, :arrives_on # the only columns ever read back
scope_by_policy # ReservationPolicy::Scope.new(actor, Reservation).resolve
end
ReservationTools.tool_names
# => ["find_reservations", "count_reservations", "get_reservation"]
ReservationTools.call("find_reservations", actor: current_user, status: "held")
# => { results: [...], count: 3, truncated: false }Three properties make this safe to hand to a model:
- Allowlists reject, never drop. A filter on an undeclared column comes back as
{ error: ... }the model can act on, rather than silently answering a broader question — and rather than letting a model readpassword_digestone character at a time through row counts. - Results are capped (25 by default, 100 at most) with a
truncatedflag, so afind_*with no filters cannot select a table into the prompt. - The scope is yours.
scope { |actor| ... }receives whatever your host passes asactor:and returns the relation to read through;scope_by_policyresolves<Model>Policy::Scopeby name. Omit both and the tools read unscoped — a deliberate choice, not a default.
The tool definitions are ordinary common-format tools, so an agent offers them with prompt(tools: ReservationTools.tool_definitions) and answers each call with ReservationTools.call(name, actor: current_user, **arguments). The dashboard engine discovers every class under app/agent_tools and offers each generated tool in the agent editor.
Start from the generator rather than a blank file:
bin/rails generate active_agent:schema_tools ReservationIt writes the class above with every column listed, commented out, with its type, exposing nothing beyond id until you move a column into filterable or returns. Which columns an agent may see is a judgement about exposure, not a fact about the table, so that decision stays a review step; columns that look like secrets are left off the list altogether.
Delegating to Another Agent
When the work behind a tool is itself an AI task — summarizing, classifying, translating — reach for delegation instead of a plain function. A delegated sub-agent keeps its own instructions, templates and model, and runs under a declared schema, a cost/latency budget, and a swappable backend:
class TriageAgent < ApplicationAgent
delegation_budget max_calls: 6, max_duration: 45
delegate_to TicketClassifierAgent, budget: { max_calls: 1, timeout: 10 }
endDefining Schema Tools at Runtime
A schema tools class does not have to be a file. ActiveAgent::SchemaTools.define builds the same bounded roster from a declaration held anywhere — a table your dashboard edits, a configuration object, a test:
tools = ActiveAgent::SchemaTools.define(Reservation,
filterable: %i[status guest_id],
returns: %i[id status guest_id arrives_on],
policy: true) # ReservationPolicy::Scope, as scope_by_policy would
tools.name # => "ReservationTools"
tools.tool_names # => ["find_reservations", "count_reservations", "get_reservation"]The class is registered under its model, and defining the same model again replaces the earlier class rather than adding one, so a registry that is rebuilt on every change holds one class per model. undefine(Reservation) drops it. The dashboard engine discovers registry entries alongside the files under app/agent_tools, and a runtime definition supersedes a file for the same model.
The allowlist is the same judgement wherever the declaration lives: a definition that can be edited without a deploy deserves the same review a file would get, and nothing secret-shaped belongs in it.
Related Documentation
- Delegation - Expose another agent to your model as a tool
- MCP (Model Context Protocol) - Connect to external services via MCP
- Tools, Streaming & Caching (SolidAgent) - Declarative tool schemas, live tool status, cached results
- Agents - Understand the agent lifecycle and callbacks
- Generation - Execute tool-enabled generations
- Messages - Learn about conversation structure
- Streaming - Use tools with streaming responses
- Configuration - Configure tool behavior across environments
- OpenAI Provider - OpenAI-specific tool features
- Anthropic Provider - Anthropic-specific capabilities