Concepts
The Agent Pipeline
Brute.agent returns a Brute::Turn::AgentPipeline — a subclass of Rack::Builder that is simultaneously the builder and the agent. You configure it by chaining, and run it with .start:
agent = Brute.agent # => AgentPipeline .use(Brute::Middleware::SystemPrompt) # => same pipeline (.use returns self) .run ->(env) { ... } # => same pipeline (.run returns self)
env = agent.start("what changed?") # runs one turn, returns the envA block form is equivalent (evaluated in the pipeline’s context):
agent = Brute.agent do use Brute::Middleware::SystemPrompt run ->(env) { ... }endThe env
Section titled “The env”.start seeds a plain Hash and sends it through the stack:
| Key | Value |
|---|---|
:messages |
the conversation log — an Array of Brute::Message with role-tagging sugar (Brute.log) |
:events |
an event sink (<<-able); defaults to a null sink — see Events |
:metadata |
a scratch Hash for middleware (timing, session ids, …) |
:current_iteration |
the tool-loop counter, starts at 1 |
:tools |
set by the ToolPipeline middleware on the way in |
.start accepts a String (becomes a role: :user message), a Brute::Message, a Hash (coerced into one), an Array (used as the log), or nothing (empty log — useful when SessionLog provides the history).
The terminal run proc
Section titled “The terminal run proc”The innermost app is the LLM call, and it is yours. Brute has no completion middleware and no LLM configuration — provider, model, and credentials all live in the proc, written with whatever library you like:
run do |env| # 1. convert env[:messages] to your library's format (transport.dump_all) # 2. make ONE completion, advertising env[:tools] # 3. append the response back as Brute::Message values (transport.wrap_each)endThe MessageTransport classes handle steps 1 and 3 for ruby_llm, llm.rb, openai and anthropic.
The proc does one completion per pass, not the whole loop — Loop::ToolResult re-invokes the stack while the model keeps calling tools, so Brute stays the turn manager.
Slash commands
Section titled “Slash commands”map registers a command against what the room just said. Whatever it is given becomes a check — a function of the newest message that answers true or false — and the block is a middleware, run before the rest of the stack:
agent = Brute.agent .map("/compact") { |env| env[:messages].clear } .map(/\Aplease compact/i) { |env| ... } .map(->(said) { said.length > 10_000 }) { |env| ... } .run ->(env) { ... }
agent.start("/compact keep the deploy notes")Three kinds of matcher, all normalised to a check at registration:
| Given | Becomes |
|---|---|
"/compact" |
/^\/compact.*/ — the command and whatever rides after it |
"compact" |
the same; a String without a leading slash grows one |
/\Aplease compact/i |
itself, wrapped in a function that evaluates it |
anything answering to #call |
itself |
Checks are tried in the order they were registered and the first to pass is the one that runs. Only a user message is offered to them — never the assistant, never an empty log.
start puts the registry into the turn as env[:commands], and Brute::Middleware::SlashCommands — which the builder puts at the head of every chain, registry or no registry — runs the match. A command’s block is a middleware, so what it leaves in env is what the rest of the chain works on.
This is Rack::Builder#map overridden. An agent routes on what was said rather than on a path, so there are no sub-builders and no URLMap.
Agents from .ru strings and files
Section titled “Agents from .ru strings and files”Because the pipeline is a Rack::Builder, an agent can be defined in rackup syntax and parsed at runtime:
agent = Brute::Turn::AgentPipeline.new_from_string(<<~RU, "(inline)") use Brute::Middleware::SystemPrompt run ->(env) { env[:messages].assistant("hi") }RU
# or from a file:agent = Brute::Turn::AgentPipeline.parse_file("agent.ru")agent.start("hello")This is also what lets an agent serve over HTTP — the same builder drops into a config.ru.
Pipelines everywhere
Section titled “Pipelines everywhere”AgentPipeline composes Brute::Turn::Pipeline, a thin Rack::Builder subclass whose use/run return self for chaining. The same class powers ToolPipeline (tools with middleware) and SubAgent (agents as tools) — one mental model for the whole framework.