brute
A framework-agnostic coding agent for Ruby — Rack-style middleware pipelines for agent turns, a full set of coding tools, and session persistence. Bring your own LLM library.
Brute treats an agent turn the way Rack treats an HTTP request: an env hash flowing through a middleware stack toward a terminal app. The middleware handles the agentic machinery — the tool loop, session persistence, the system prompt, iteration guards. The terminal app is a proc you write with whatever LLM library you prefer: ruby_llm, llm.rb, the official openai or anthropic gems, or raw HTTP. Brute depends on none of them.
Quick start
require "brute"
require "ruby_llm"
agent = Brute.agent
.use(Brute::Middleware::SessionLog, path: "tmp/session.jsonl")
.use(Brute::Middleware::SystemPrompt)
.use(Brute::Middleware::Loop::ToolResult)
.use(Brute::Middleware::MaxIterations)
.use(Brute::Middleware::ToolPipeline, tools: Brute::Tools::ALL)
.run do |env|
# The LLM call — yours, with your library. A MessageTransport
# translates between Brute's message format and the library's.
response = provider.complete(
Brute::MessageTransport::RubyLLM.dump_all(env[:messages]),
tools: rubyllm_tools(env[:tools]), model: model,
)
Brute::MessageTransport::RubyLLM.wrap_each(response) { |m| env[:messages] << m }
end
agent.start("What files are in the current directory?")
The proc makes ONE completion per pass; the ToolPipeline middleware executes whatever tools the model called, and Loop::ToolResult sends the results back down the stack until the model answers with text. Brute is the turn manager — the LLM library is just a client.
Head to Getting Started for a complete runnable walkthrough.
What’s here
- Core Features — the agent pipeline (
Brute.agent,.use,.run,.start), the middleware catalog, Brute’s message format, the MessageTransport pattern with shipped transports for four LLM libraries, and the tool system. - Advanced — sub-agents (agents as tools), session persistence, skills, events, and serving agents over HTTP.
- Examples — runnable agents, including the same agent on all four supported LLM libraries.
Design principles
- No LLM dependency.
brute.gemspecnames no LLM library. The terminalrunproc owns provider, model, and credentials. - Plain data. The conversation log is an
ArrayofBrute::Message— an immutableDatavalue withrole,content,tool_calls,tool_call_id. Anything that duck-types those methods rides along. - Middleware all the way down. Turns are pipelines; tools can be pipelines; even the event stream is a stack of handlers. If you know Rack, you know Brute.