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:
stringintegernumber— floating-point valuesbooleanarray— passitems_type:for primitive items, or a block for an array of objectshash— pass a block to declare nested properties, or omit it for a free-form objectenum— passvalues:as a non-empty array of same-typed values (all strings, all integers, all floats, or all booleans). Mixed-type or empty value sets raiseArgumentErrorat 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
| Kwarg | Applies to | JSON Schema keyword |
|---|---|---|
min_length: / max_length: | string | minLength / maxLength |
pattern: (Regexp or String) | string | pattern |
format: | string | format |
min: / max: | integer, number | minimum / maximum |
min_items: / max_items: | array | minItems / 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) —#executereceivesactor_type:andactor_id:as separate keyword arguments.actor_typeis validated againsttypes:like any other enum.resolve: true— requirestypes:. At runtime,invokelooks up the class named byactor_type(restricted to thetypes:whitelist — never an arbitrary string) viaObject.const_get, calls.find(actor_id)on it, and passes the result as a singleactor:keyword argument instead. A record that isn't found raisesArgumentError, 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.