Skip to main content

Tools

Tools are how agents take action beyond generating text. Every tool is a small class that declares an input schema and implements #execute.

Defining a Tool​

Tools generated for an agent live under app/agents/<agent_name>/tools/ and are namespaced under <AgentClass>::Tools:

module ResearchAgent::Tools
class GetWeather < OmniAgent::Tool
description "Get current weather for a city"
tags :weather
metadata category: :utility

input do
string :city, description: "City name"
end

def execute(city:)
"Sunny in #{city}"
end
end
end
  • description: Sent to the provider so the model knows when to call the tool.
  • tags: Symbols used for filtering tools available to an agent.
  • metadata: Free-form hash for your own bookkeeping (not sent to the provider).
  • input: Declares the JSON-schema-style parameters the model must supply.
  • execute: Runs when the model calls the tool. Receives the parsed arguments as keyword args.

Input Schema DSL​

Use input do ... end to declare typed parameters:

input do
string :city, description: "City name"
integer :days, description: "Forecast length", required: false
number :confidence, min: 0.0, max: 1.0, required: false
boolean :metric, required: false
array :tags, items_type: "string"
hash :coordinates do
string :lat
string :lng
end
enum :unit, values: [ "celsius", "fahrenheit" ], description: "Temperature unit"
end

Supported field types:

  • string
  • integer
  • number — floating-point values
  • boolean
  • array — pass items_type: for primitive items, or a block for an array of objects
  • hash — pass a block to declare nested properties, or omit it for a free-form object
  • enum — pass values: as a non-empty array of same-typed values (all strings, all integers, all floats, or all booleans). Mixed-type or empty value sets raise ArgumentError at schema-definition time.
  • polymorphic — see Polymorphic References below.

All fields are required: true by default. Pass required: false to make a field optional.

invoke validates arguments against the schema before calling #execute: a missing required field, an enum value outside values:, a constraint violation, or a failing custom validator all raise ArgumentError rather than reaching your tool code.

Validations​

string, integer, number, and array accept optional constraint kwargs. These are emitted as standard JSON Schema keywords (so the model sees them too) and enforced again at runtime by invoke:

input do
string :name, min_length: 3, max_length: 40, pattern: /\A[a-z]+\z/i, format: "email"
integer :level, min: 1, max: 5
number :ratio, min: 0.0, max: 1.0
array :tags, items_type: "string", min_items: 1, max_items: 10
end
KwargApplies toJSON Schema keyword
min_length: / max_length:stringminLength / maxLength
pattern: (Regexp or String)stringpattern
format:stringformat
min: / max:integer, numberminimum / maximum
min_items: / max_items:arrayminItems / maxItems

Every field type also accepts validate:, a Proc for arbitrary runtime rules. It is never serialized into the schema sent to the provider — it only runs locally in invoke. Return false to raise a generic ArgumentError, or raise your own ArgumentError for a custom message:

input do
string :slug, validate: ->(v) { v == v.downcase or raise ArgumentError, "slug must be lowercase" }
end

Polymorphic References​

polymorphic :name declares a Rails-style polymorphic reference. It expands into two ordinary schema fields, name_type and name_id:

input do
polymorphic :actor,
types: [ "User", "Admin" ], # allowed actor_type values -> becomes an enum
description: "The actor performing the action",
id_type: :integer, # actor_id schema type, default "string"
resolve: true # fetch the actual record instead of raw type/id
end

Individual descriptions for each sub-field can be set with the block form instead of a shared description::

polymorphic :actor, types: [ "User", "Admin" ], resolve: true do
type values: [ "User", "Admin" ], description: "The actor's class name"
id type: :integer, description: "The actor's primary key"
end

Two runtime modes:

  • resolve: false (default) — #execute receives actor_type: and actor_id: as separate keyword arguments. actor_type is validated against types: like any other enum.
  • resolve: true — requires types:. At runtime, invoke looks up the class named by actor_type (restricted to the types: whitelist — never an arbitrary string) via Object.const_get, calls .find(actor_id) on it, and passes the result as a single actor: keyword argument instead. A record that isn't found raises ArgumentError, which the agent loop surfaces back to the model as a retryable tool error, just like an invalid enum value.

resolve: true without types: raises ArgumentError at schema-definition time.

Stopping Generation Early​

Sometimes a tool call should end the agent's run instead of looping back to the model — for example, a tool that hands off to a human or returns a final answer directly.

Declare it at the class level:

class Escalate < OmniAgent::Tool
stops_generation

def execute(reason:)
"Escalated: #{reason}"
end
end

Or trigger it conditionally from inside #execute:

def execute(reason:)
stop_generation! if reason == "urgent"
"Logged: #{reason}"
end

How Tools Are Discovered​

OmniAgent::Agent#available_tools looks up the <AgentClass>::Tools namespace and collects every constant that is a subclass of OmniAgent::Tool. There's no manual registration step — defining a tool class in the right namespace is enough for the agent to pick it up.