class Adapter
Normalizes any tool shape into one neutral interface so the rest of Brute never has to care which tools library (if any) a tool was written with.
This solves three problems:
-
Using any tools library — anything that quacks like a tool (RubyLLM::Tool today, others via their own adapters) is wrapped into the same interface.
-
Avoiding tool libraries entirely — Brute::Tool pipelines and Tools::SubAgent work without inheriting from a library class.
-
Quickly adding tools — a plain Hash with a proc is enough:
Brute::Tools::Adapter.wrap( name: "echo", description: "Echo the input back", params:
msg: { type: "string", required: true}, execute: ->(msg:)msg, )
The neutral interface:
adapter.name # String
adapter.description # String
adapter.params # key => { type:, desc:, required: }
adapter.call(args) # execute with a (string- or symbol-keyed) Hash
Completion middlewares convert adapters into whatever their LLM library expects (e.g. #to_ruby_llm); ToolCall executes them via #call.
Definitions
def self.wrap(tool)
Wrap a single tool of any supported shape. Idempotent.
Implementation
def self.wrap(tool)
return tool if tool.is_a?(Adapter)
tool = tool.new if tool.is_a?(Class)
case tool
when Hash then from_hash(tool)
when ::RubyLLM::Tool then from_ruby_llm(tool)
when Brute::Tools::SubAgent then new(
name: tool.name,
description: tool.description,
params: tool.params,
handler: ->(**args) { tool.execute(args) },
original: tool,
)
when Brute::Tool then new(
name: tool.name,
description: tool.description,
params: tool.params,
handler: ->(**args) { tool.call(**args) },
original: tool,
)
else
from_duck_type(tool)
end
end
def self.wrap_all(tools)
Wrap a list of tools into a name_sym => adapter lookup hash —
the shape ToolCall and completion middlewares work with.
Implementation
def self.wrap_all(tools)
Array(tools).each_with_object({}) do |tool, hash|
adapter = wrap(tool)
hash[adapter.name.to_sym] = adapter
end
end
def self.from_hash(definition)
Quick inline tool: name:, description:, params:, execute:
Implementation
def self.from_hash(definition)
definition = definition.transform_keys(&:to_sym)
handler = definition.fetch(:execute) { definition[:handler] }
raise ArgumentError, "inline tool needs an :execute proc" unless handler.respond_to?(:call)
new(
name: definition.fetch(:name).to_s,
description: definition.fetch(:description, ""),
params: definition.fetch(:params, {}),
handler: ->(**args) { handler.call(**args) },
original: definition,
)
end
def self.from_ruby_llm(tool)
A RubyLLM::Tool instance (the library's own arg normalization and validation stays in play via tool.call). Tools declared with the params(...) schema DSL keep their full JSON schema.
Implementation
def self.from_ruby_llm(tool)
params = tool.parameters.each_with_object({}) do |(key, param), hash|
hash[key.to_sym] = { type: param.type, desc: param.description, required: param.required }.compact
end
new(
name: tool.name.to_s,
description: tool.description,
params: params,
schema: (tool.params_schema if tool.respond_to?(:params_schema)),
handler: ->(**args) { tool.call(args) },
original: tool,
)
end
def self.from_duck_type(tool)
Anything tool-shaped: needs #name and #call or #execute. Honors #to_ruby_llm for backward compatibility with existing adapters.
Implementation
def self.from_duck_type(tool)
return from_ruby_llm(tool.to_ruby_llm) if tool.respond_to?(:to_ruby_llm)
unless tool.respond_to?(:name) && (tool.respond_to?(:call) || tool.respond_to?(:execute))
raise ArgumentError, "don't know how to adapt #{tool.inspect} into a tool"
end
entry = tool.respond_to?(:execute) ? tool.method(:execute) : tool.method(:call)
new(
name: tool.name.to_s,
description: tool.respond_to?(:description) ? tool.description : "",
params: tool.respond_to?(:params) ? tool.params : {},
handler: ->(**args) { entry.call(**args) },
original: tool,
)
end
attr_reader :original
The tool object this adapter wraps (RubyLLM::Tool, Brute::Tool, SubAgent, Hash definition, ...).
def call(arguments = {})
Execute the tool. Accepts string- or symbol-keyed argument hashes, as delivered by LLM providers.
Implementation
def call(arguments = {})
args = arguments.to_h.transform_keys(&:to_sym)
@handler.call(**args)
end
def to_ruby_llm
Convert to a RubyLLM::Tool so ruby_llm-backed completion can hand the tool to its providers. Returns the wrapped tool untouched when it already is one.
Implementation
def to_ruby_llm
return @original if @original.is_a?(::RubyLLM::Tool)
adapter = self
Class.new(::RubyLLM::Tool) do
description adapter.description
adapter.params.each { |key, opts| param key, **opts.slice(:type, :desc, :required) }
define_method(:name) { adapter.name }
define_method(:execute) { |**args| adapter.call(args) }
end.new
end
def to_h
Library-neutral tool definition (JSON-Schema-ish), for completion middlewares that talk to an HTTP API directly.
Implementation
def to_h
return { name: @name, description: @description, parameters: @schema.deep_symbolize_keys } if @schema
properties = @params.transform_values do |opts|
{
type: opts[:type] || "string",
description: opts[:desc] || opts[:description],
items: opts[:items],
enum: opts[:enum],
}.compact
end
required = @params.select { |_k, opts| opts[:required] }.keys
{
name: @name,
description: @description,
parameters: {
type: "object",
properties: properties,
required: required.map(&:to_s),
},
}
end