bruteSourceBruteToolsAdapter

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:

  1. Using any tools library — anything that quacks like a tool (RubyLLM::Tool today, others via their own adapters) is wrapped into the same interface.

  2. Avoiding tool libraries entirely — Brute::Tool pipelines and Tools::SubAgent work without inheriting from a library class.

  3. 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