Class: Brute::Tools::SubAgent

Inherits:
Brute::Turn::AgentPipeline show all
Defined in:
lib/brute/tools/sub_agent.rb

Overview

A SubAgent is an Agent that exposes a tool-shaped facade so it can be dropped into another agent's tools list. The parent agent hands it to the LLM as a regular tool; when invoked, the SubAgent runs its own pipeline against a fresh Session built from the tool arguments, then returns the final assistant message as the tool result.

Usage:

researcher = Brute::Tools::SubAgent.new(
name:        "research",
description: "Delegate a research task to a read-only sub-agent.",
) do
use Brute::Middleware::SystemPrompt
use Brute::Middleware::Loop::ToolResult
use Brute::Middleware::MaxIterations, max_iterations: 10
use Brute::Middleware::ToolPipeline, tools: [Brute::Tools::FSRead, Brute::Tools::FSSearch]
run ->(env) do
  ctx = RubyLLM.context { |c| c.ollama_api_base = ENV["OLLAMA_API_BASE"] }
  model, provider = RubyLLM::Models.resolve(
    "llama3.2", provider: :ollama, assume_exists: true, config: ctx.config)
  response = provider.complete(env[:messages],
                               tools:       Brute.rubyllm_tools(env[:tools]),
                               temperature: 0.7,
                               model:       model)
  RubyLLM::MessageTransport.new(response).wrap_each { |m| env[:messages] << m }
end
end

# The SubAgent IS a tool — hand it to a parent agent's ToolPipeline:
main_agent = Brute.agent do
use Brute::Middleware::ToolPipeline, tools: [Brute::Tools::FSRead, researcher]
run ->(env) { ... }
end
main_agent.start("delegate some research")

Constant Summary collapse

DEFAULT_PARAMS =
{
  task: { type: "string", desc: "A clear, detailed description of the task", required: true },
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Brute::Turn::AgentPipeline

#generate_map, #map, #start

Methods inherited from Brute::Turn::Pipeline

new_from_string

Methods included from Brute::Turn::Pipeline::Chainable

#run, #use

Constructor Details

#initialize(name:, description:, params: DEFAULT_PARAMS, &block) ⇒ SubAgent

Returns a new instance of SubAgent.



51
52
53
54
55
56
# File 'lib/brute/tools/sub_agent.rb', line 51

def initialize(name:, description:, params: DEFAULT_PARAMS, &block)
  @sub_agent_name = name.to_s
  @description    = description
  @params         = params
  super(&block)
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



49
50
51
# File 'lib/brute/tools/sub_agent.rb', line 49

def description
  @description
end

#paramsObject (readonly)

Returns the value of attribute params.



49
50
51
# File 'lib/brute/tools/sub_agent.rb', line 49

def params
  @params
end

#sub_agent_nameObject (readonly)

Returns the value of attribute sub_agent_name.



49
50
51
# File 'lib/brute/tools/sub_agent.rb', line 49

def sub_agent_name
  @sub_agent_name
end

Instance Method Details

#execute(arguments) ⇒ Object

Tool-shaped entry point. Builds a session from arguments, runs the agent loop, returns the last assistant message as a string.



60
61
62
63
64
# File 'lib/brute/tools/sub_agent.rb', line 60

def execute(arguments)
  session = build_session(arguments)
  start(session)
  extract_result(session)
end

#nameObject

Lets ToolPipeline treat SubAgents the same as RubyLLM::Tool instances without checking respond_to? everywhere.



81
82
83
# File 'lib/brute/tools/sub_agent.rb', line 81

def name
  @sub_agent_name
end

#to_ruby_llmObject

Adapter so the parent agent's completion middleware (and ruby_llm) sees this as a regular tool. ToolPipeline middleware should call to_ruby_llm when building the tools hash if a tool responds to it.



69
70
71
72
73
74
75
76
77
# File 'lib/brute/tools/sub_agent.rb', line 69

def to_ruby_llm
  sub = self
  Class.new(::RubyLLM::Tool) do
    description sub.description
    sub.params.each { |k, opts| param k, **opts }
    define_method(:name) { sub.sub_agent_name }
    define_method(:execute) { |**args| sub.execute(args) }
  end.new
end