brute 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/message_transport/anthropic.rb +143 -0
  3. data/lib/brute/message_transport/llm.rb +94 -0
  4. data/lib/brute/message_transport/openai.rb +113 -0
  5. data/lib/brute/message_transport/ruby_llm.rb +78 -0
  6. data/lib/brute/message_transport.rb +133 -0
  7. data/lib/brute/messages.rb +79 -6
  8. data/lib/brute/middleware/002_session_log.rb +1 -1
  9. data/lib/brute/middleware/004_summarize.rb +7 -7
  10. data/lib/brute/middleware/006_loop.rb +4 -4
  11. data/lib/brute/middleware/008_checkpoint.rb +268 -0
  12. data/lib/brute/middleware/010_max_iterations.rb +1 -1
  13. data/lib/brute/middleware/020_system_prompt.rb +1 -1
  14. data/lib/brute/middleware/040_compaction_check.rb +2 -2
  15. data/lib/brute/middleware/070_tool_pipeline.rb +29 -26
  16. data/lib/brute/tool.rb +124 -0
  17. data/lib/brute/tools/adapter.rb +22 -60
  18. data/lib/brute/tools/fs_patch.rb +1 -1
  19. data/lib/brute/tools/fs_read.rb +1 -1
  20. data/lib/brute/tools/fs_remove.rb +1 -1
  21. data/lib/brute/tools/fs_search.rb +1 -1
  22. data/lib/brute/tools/fs_undo.rb +1 -1
  23. data/lib/brute/tools/fs_write.rb +1 -1
  24. data/lib/brute/tools/net_fetch.rb +1 -1
  25. data/lib/brute/tools/question.rb +1 -1
  26. data/lib/brute/tools/shell.rb +1 -1
  27. data/lib/brute/tools/skill_load.rb +1 -1
  28. data/lib/brute/tools/sub_agent.rb +5 -22
  29. data/lib/brute/tools/todo_read.rb +1 -1
  30. data/lib/brute/tools/todo_write.rb +1 -1
  31. data/lib/brute/turn/agent_pipeline.rb +2 -1
  32. data/lib/brute/turn/pipeline.rb +1 -1
  33. data/lib/brute/turn/tool_pipeline.rb +2 -12
  34. data/lib/brute/version.rb +1 -1
  35. data/lib/brute.rb +17 -18
  36. data/lib/brute_cli/providers/shell_response.rb +6 -10
  37. metadata +23 -17
  38. data/lib/ruby_llm/message_transport.rb +0 -117
@@ -12,10 +12,9 @@ module Brute
12
12
  # This solves three problems:
13
13
  #
14
14
  # 1. Using any tools library — anything that quacks like a tool
15
- # (RubyLLM::Tool today, others via their own adapters) is wrapped
16
- # into the same interface.
17
- # 2. Avoiding tool libraries entirely Brute::Turn::ToolPipeline and
18
- # Tools::SubAgent work without inheriting from a library class.
15
+ # (#name plus #call or #execute) is wrapped into the same interface.
16
+ # 2. Avoiding tool libraries entirely — Brute::Tool, Brute::Turn::ToolPipeline
17
+ # and Tools::SubAgent work without inheriting from a library class.
19
18
  # 3. Quickly adding tools — a plain Hash with a proc is enough:
20
19
  #
21
20
  # Brute::Tools::Adapter.wrap(
@@ -32,8 +31,8 @@ module Brute
32
31
  # adapter.params # { key => { type:, desc:, required: } }
33
32
  # adapter.call(args) # execute with a (string- or symbol-keyed) Hash
34
33
  #
35
- # Completion middlewares convert adapters into whatever their LLM
36
- # library expects (e.g. #to_ruby_llm); ToolPipeline executes them via #call.
34
+ # The inline `run` proc converts adapters (via #to_h) into whatever its
35
+ # LLM library expects; ToolPipeline executes them via #call.
37
36
  #
38
37
  class Adapter
39
38
  attr_reader :name, :description, :params
@@ -46,7 +45,7 @@ module Brute
46
45
 
47
46
  case tool
48
47
  when Hash then from_hash(tool)
49
- when ::RubyLLM::Tool then from_ruby_llm(tool)
48
+ when ::Brute::Tool then from_brute_tool(tool)
50
49
  when Brute::Tools::SubAgent then new(
51
50
  name: tool.name,
52
51
  description: tool.description,
@@ -90,29 +89,21 @@ module Brute
90
89
  )
91
90
  end
92
91
 
93
- # A RubyLLM::Tool instance (the library's own arg normalization and
94
- # validation stays in play via tool.call). Tools declared with the
95
- # params(...) schema DSL keep their full JSON schema.
96
- def self.from_ruby_llm(tool)
97
- params = tool.parameters.each_with_object({}) do |(key, param), hash|
98
- hash[key.to_sym] = { type: param.type, desc: param.description, required: param.required }.compact
99
- end
100
-
92
+ # A Brute::Tool instance. Tools declared with the params({...}) schema
93
+ # DSL keep their full JSON schema.
94
+ def self.from_brute_tool(tool)
101
95
  new(
102
96
  name: tool.name.to_s,
103
97
  description: tool.description,
104
- params: params,
105
- schema: (tool.params_schema if tool.respond_to?(:params_schema)),
98
+ params: tool.params,
99
+ schema: tool.params_schema,
106
100
  handler: ->(**args) { tool.call(args) },
107
101
  original: tool,
108
102
  )
109
103
  end
110
104
 
111
- # Anything tool-shaped: needs #name and #call or #execute. Honors
112
- # #to_ruby_llm for backward compatibility with existing adapters.
105
+ # Anything tool-shaped: needs #name and #call or #execute.
113
106
  def self.from_duck_type(tool)
114
- return from_ruby_llm(tool.to_ruby_llm) if tool.respond_to?(:to_ruby_llm)
115
-
116
107
  unless tool.respond_to?(:name) && (tool.respond_to?(:call) || tool.respond_to?(:execute))
117
108
  raise ArgumentError, "don't know how to adapt #{tool.inspect} into a tool"
118
109
  end
@@ -136,7 +127,7 @@ module Brute
136
127
  @original = original
137
128
  end
138
129
 
139
- # The tool object this adapter wraps (RubyLLM::Tool, Brute::Turn::ToolPipeline,
130
+ # The tool object this adapter wraps (Brute::Tool, Brute::Turn::ToolPipeline,
140
131
  # SubAgent, Hash definition, ...).
141
132
  attr_reader :original
142
133
 
@@ -147,23 +138,8 @@ module Brute
147
138
  @handler.call(**args)
148
139
  end
149
140
 
150
- # Convert to a RubyLLM::Tool so ruby_llm-backed completion can hand
151
- # the tool to its providers. Returns the wrapped tool untouched when
152
- # it already is one.
153
- def to_ruby_llm
154
- return @original if @original.is_a?(::RubyLLM::Tool)
155
-
156
- adapter = self
157
- Class.new(::RubyLLM::Tool) do
158
- description adapter.description
159
- adapter.params.each { |key, opts| param key, **opts.slice(:type, :desc, :required) }
160
- define_method(:name) { adapter.name }
161
- define_method(:execute) { |**args| adapter.call(args) }
162
- end.new
163
- end
164
-
165
- # Library-neutral tool definition (JSON-Schema-ish), for completion
166
- # middlewares that talk to an HTTP API directly.
141
+ # Library-neutral tool definition (JSON-Schema-ish). The inline `run`
142
+ # proc reshapes this into whatever its LLM library expects.
167
143
  def to_h
168
144
  return { name: @name, description: @description, parameters: @schema.deep_symbolize_keys } if @schema
169
145
 
@@ -194,16 +170,16 @@ end
194
170
  __END__
195
171
 
196
172
  describe "brute/tools/adapter" do
197
- it "wraps a RubyLLM::Tool class" do
198
- klass = Class.new(::RubyLLM::Tool) do
173
+ it "wraps a Brute::Tool class" do
174
+ klass = Class.new(::Brute::Tool) do
199
175
  description "test tool"
200
176
  param :input, type: "string", desc: "the input"
201
- def name; "rl_tool"; end
177
+ def name; "brute_tool"; end
202
178
  def execute(input:); "got #{input}"; end
203
179
  end
204
180
 
205
181
  adapter = Brute::Tools::Adapter.wrap(klass)
206
- adapter.name.should == "rl_tool"
182
+ adapter.name.should == "brute_tool"
207
183
  adapter.description.should == "test tool"
208
184
  adapter.params[:input][:type].should == "string"
209
185
  adapter.call("input" => "x").should == "got x"
@@ -255,29 +231,15 @@ describe "brute/tools/adapter" do
255
231
  tools[:a].should.be.kind_of?(Brute::Tools::Adapter)
256
232
  end
257
233
 
258
- it "converts to a RubyLLM::Tool" do
259
- adapter = Brute::Tools::Adapter.wrap(
260
- name: "echo",
261
- description: "Echo",
262
- params: { msg: { type: "string", desc: "message", required: true } },
263
- execute: ->(msg:) { msg },
264
- )
265
-
266
- rl = adapter.to_ruby_llm
267
- rl.should.be.kind_of?(::RubyLLM::Tool)
268
- rl.name.should == "echo"
269
- rl.call("msg" => "hello").should == "hello"
270
- end
271
-
272
- it "returns the original when it already is a RubyLLM::Tool" do
273
- klass = Class.new(::RubyLLM::Tool) do
234
+ it "exposes the wrapped original" do
235
+ klass = Class.new(::Brute::Tool) do
274
236
  description "test tool"
275
237
  def name; "original"; end
276
238
  def execute; "ok"; end
277
239
  end
278
240
  instance = klass.new
279
241
 
280
- Brute::Tools::Adapter.wrap(instance).to_ruby_llm.should == instance
242
+ Brute::Tools::Adapter.wrap(instance).original.should == instance
281
243
  end
282
244
 
283
245
  it "produces a neutral JSON-schema-ish definition" do
@@ -6,7 +6,7 @@ require "brute/tools"
6
6
 
7
7
  module Brute
8
8
  module Tools
9
- class FSPatch < RubyLLM::Tool
9
+ class FSPatch < Brute::Tool
10
10
  description 'Replace a specific string in a file. The old_string must match exactly ' \
11
11
  '(including whitespace and indentation). Always read a file before patching it.'
12
12
 
@@ -28,7 +28,7 @@ module Brute
28
28
  # 8. Return a plain string instead of a Hash — avoids the .to_s repr
29
29
  # bloat when ToolPipeline coerces the result for the LLM message.
30
30
  #
31
- class FSRead < RubyLLM::Tool
31
+ class FSRead < Brute::Tool
32
32
  description "Read the contents of a file. Returns file content with line numbers. " \
33
33
  "Use start_line/end_line for partial reads of large files."
34
34
 
@@ -7,7 +7,7 @@ require "fileutils"
7
7
 
8
8
  module Brute
9
9
  module Tools
10
- class FSRemove < RubyLLM::Tool
10
+ class FSRemove < Brute::Tool
11
11
  description "Remove a file or empty directory."
12
12
 
13
13
  param :path, type: 'string', desc: "Path to the file or directory to remove", required: true
@@ -21,7 +21,7 @@ module Brute
21
21
  # 5. Return a plain string instead of a Hash.
22
22
  # 6. Align output cap with universal truncation (2000 lines / 50 KB).
23
23
  #
24
- class FSSearch < RubyLLM::Tool
24
+ class FSSearch < Brute::Tool
25
25
  description "Search file contents using ripgrep (regex), or find files by glob pattern. " \
26
26
  "Returns matching lines with file paths and line numbers."
27
27
 
@@ -6,7 +6,7 @@ require "brute/tools"
6
6
 
7
7
  module Brute
8
8
  module Tools
9
- class FSUndo < RubyLLM::Tool
9
+ class FSUndo < Brute::Tool
10
10
  description "Undo the last write or patch operation on a file, restoring it to " \
11
11
  "its previous state."
12
12
 
@@ -7,7 +7,7 @@ require 'fileutils'
7
7
 
8
8
  module Brute
9
9
  module Tools
10
- class FSWrite < RubyLLM::Tool
10
+ class FSWrite < Brute::Tool
11
11
  description "Write content to a file. Creates parent directories if they don't exist. " \
12
12
  'Use this for creating new files or completely replacing file contents.'
13
13
 
@@ -9,7 +9,7 @@ require "uri"
9
9
 
10
10
  module Brute
11
11
  module Tools
12
- class NetFetch < RubyLLM::Tool
12
+ class NetFetch < Brute::Tool
13
13
  description "Fetch content from a URL. Returns the response body as text."
14
14
 
15
15
  param :url, type: 'string', desc: "The URL to fetch", required: true
@@ -6,7 +6,7 @@ require "brute/tools"
6
6
 
7
7
  module Brute
8
8
  module Tools
9
- class Question < RubyLLM::Tool
9
+ class Question < Brute::Tool
10
10
  description "Ask the user questions during execution. Use this to gather preferences, " \
11
11
  "clarify ambiguous instructions, get decisions on implementation choices, or " \
12
12
  "offer choices about direction. Users can always select \"Other\" to provide " \
@@ -22,7 +22,7 @@ module Brute
22
22
  # the LLM (defaults to 5 minutes).
23
23
  # 5. Return a plain string instead of a Hash.
24
24
  #
25
- class Shell < RubyLLM::Tool
25
+ class Shell < Brute::Tool
26
26
  description "Execute a shell command and return stdout, stderr, and exit code. " \
27
27
  "Use for git operations, running tests, installing packages, etc."
28
28
 
@@ -28,7 +28,7 @@ module Brute
28
28
  # tool with the matching cwd: Brute::Tools::SkillLoad.new(cwd: __dir__).
29
29
  #
30
30
  # Reference: opencode's tool/skill.ts.
31
- class SkillLoad < RubyLLM::Tool
31
+ class SkillLoad < Brute::Tool
32
32
  description "Load a specialized skill when the task at hand matches one of the " \
33
33
  "available skills listed in the system context. This injects the skill's " \
34
34
  "full instructions into the conversation, plus the skill's base directory " \
@@ -23,14 +23,10 @@ module Brute
23
23
  # use Brute::Middleware::MaxIterations, max_iterations: 10
24
24
  # use Brute::Middleware::ToolPipeline, tools: [Brute::Tools::FSRead, Brute::Tools::FSSearch]
25
25
  # run ->(env) do
26
- # ctx = RubyLLM.context { |c| c.ollama_api_base = ENV["OLLAMA_API_BASE"] }
27
- # model, provider = RubyLLM::Models.resolve(
28
- # "llama3.2", provider: :ollama, assume_exists: true, config: ctx.config)
29
- # response = provider.complete(env[:messages],
30
- # tools: Brute.rubyllm_tools(env[:tools]),
31
- # temperature: 0.7,
32
- # model: model)
33
- # RubyLLM::MessageTransport.new(response).wrap_each { |m| env[:messages] << m }
26
+ # # The LLM call, written with your library of choice. Convert
27
+ # # env[:messages] to its format, call it, and append the response
28
+ # # back as Brute::Message values (the MessageTransport pattern —
29
+ # # see examples/ruby_llm.rb, examples/openai.rb, ...).
34
30
  # end
35
31
  # end
36
32
  #
@@ -63,20 +59,7 @@ module Brute
63
59
  extract_result(session)
64
60
  end
65
61
 
66
- # Adapter so the parent agent's completion middleware (and ruby_llm)
67
- # sees this as a regular tool. ToolPipeline middleware should call
68
- # `to_ruby_llm` when building the tools hash if a tool responds to it.
69
- def to_ruby_llm
70
- sub = self
71
- Class.new(::RubyLLM::Tool) do
72
- description sub.description
73
- sub.params.each { |k, opts| param k, **opts }
74
- define_method(:name) { sub.sub_agent_name }
75
- define_method(:execute) { |**args| sub.execute(args) }
76
- end.new
77
- end
78
-
79
- # Lets ToolPipeline treat SubAgents the same as RubyLLM::Tool instances
62
+ # Lets ToolPipeline treat SubAgents the same as any other tool
80
63
  # without checking respond_to? everywhere.
81
64
  def name
82
65
  @sub_agent_name
@@ -6,7 +6,7 @@ require "brute/tools"
6
6
 
7
7
  module Brute
8
8
  module Tools
9
- class TodoRead < RubyLLM::Tool
9
+ class TodoRead < Brute::Tool
10
10
  description "Read the current todo list to check task status and progress."
11
11
  param :_placeholder, type: 'string', desc: "Unused, pass any value", required: false
12
12
 
@@ -6,7 +6,7 @@ require "brute/tools"
6
6
 
7
7
  module Brute
8
8
  module Tools
9
- class TodoWrite < RubyLLM::Tool
9
+ class TodoWrite < Brute::Tool
10
10
  description "Create or update the todo list. Send the complete list each time — " \
11
11
  "this replaces the existing list entirely."
12
12
 
@@ -57,7 +57,8 @@ module Brute
57
57
  case input
58
58
  when nil then Brute.log
59
59
  when ::String then Brute.log.tap { |log| log.user(input) }
60
- when ::RubyLLM::Message then Brute.log(input)
60
+ when ::Brute::Message then Brute.log(input)
61
+ when ::Hash then Brute.log(Brute::Message.new(**input.transform_keys(&:to_sym)))
61
62
  when ::Array then input.extend(Brute::Messages)
62
63
  else Brute.log(input)
63
64
  end
@@ -14,7 +14,7 @@ module Brute
14
14
  # .use(MaxProfit)
15
15
  # .use(DontTellMom)
16
16
  # .run -> (env) {
17
- # RubyLLM.chat.ask("How to make money?")
17
+ # your_llm_library.complete("How to make money?")
18
18
  # }
19
19
  #
20
20
  def use(...) = tap { super }
@@ -13,8 +13,8 @@ module Brute
13
13
  # the work; middleware wraps it with concerns like file mutation queueing,
14
14
  # validation, logging.
15
15
  #
16
- # Coexists with Brute::Tools::* (which inherit from RubyLLM::Tool). Use a
17
- # ToolPipeline when you want middleware; use RubyLLM::Tool subclasses for
16
+ # Coexists with Brute::Tools::* (which inherit from Brute::Tool). Use a
17
+ # ToolPipeline when you want middleware; use Brute::Tool subclasses for
18
18
  # simple cases.
19
19
  #
20
20
  # read = Brute::Turn::ToolPipeline.new(
@@ -53,16 +53,6 @@ module Brute
53
53
  env[:result]
54
54
  end
55
55
 
56
- # Adapter so the LLM can call this tool through ruby_llm.
57
- def to_ruby_llm
58
- tool = self
59
- Class.new(RubyLLM::Tool) do
60
- description tool.description
61
- tool.params.each { |k, opts| param k, **opts }
62
- define_method(:name) { tool.name }
63
- define_method(:execute) { |**args| tool.call(**args) }
64
- end.new
65
- end
66
56
  end
67
57
  end
68
58
  end
data/lib/brute/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brute
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/brute.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "bundler/setup"
4
4
 
5
- require "ruby_llm"
6
5
  require "rack"
7
6
  require 'timeout'
8
7
  require 'logger'
@@ -22,16 +21,14 @@ module Brute
22
21
  `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
23
22
  LOGO
24
23
 
25
- # NOTE: Brute owns no LLM configuration. Calling an LLM is just
26
- # `RubyLLM.chat.ask "..."`, and all provider/model/credential config lives
27
- # in the pipeline's terminal `run` proc typically via `RubyLLM.context`:
28
- #
29
- # run ->(env) do
30
- # context = RubyLLM.context { |c| c.ollama_api_base = ENV["OLLAMA_API_BASE"] }
31
- # ...
32
- # end
33
- #
34
- # See examples/agents/01_basic_agent.rb.
24
+ # NOTE: Brute owns no LLM configuration and no LLM library. All
25
+ # provider/model/credential config lives in the pipeline's terminal `run`
26
+ # proc, which the user writes with whatever LLM library they prefer
27
+ # (ruby_llm, llm.rb, openai, anthropic, raw HTTP, ...). The proc converts
28
+ # env[:messages] (Brute::Message values — see Brute.log) to the library's
29
+ # format, makes the call, and appends the response back as Brute::Message
30
+ # values — the MessageTransport pattern. See examples/ruby_llm.rb,
31
+ # examples/llm.rb, examples/openai.rb and examples/anthropic.rb.
35
32
 
36
33
  def self.provider
37
34
  @provider ||= :anthropic
@@ -60,19 +57,21 @@ module Brute
60
57
  Brute::Turn::AgentPipeline.new(&block)
61
58
  end
62
59
 
63
- # Adapt any Brute tools (hashes, Brute::Turn::ToolPipeline, SubAgent, RubyLLM::Tool …)
64
- # into a { name_sym => RubyLLM::Tool } hash — the shape an inline `run`
65
- # proc hands to RubyLLM.chat.with_tools or a provider #complete call.
66
- def self.rubyllm_tools(tools)
67
- Brute::Tools::Adapter.wrap_all(tools || []).transform_values(&:to_ruby_llm)
60
+ # Adapt any Brute tools (hashes, Brute::Tool, Brute::Turn::ToolPipeline,
61
+ # SubAgent …) into a { name_sym => Brute::Tools::Adapter } hash. Each
62
+ # adapter exposes #to_h a neutral JSON-Schema-ish definition the inline
63
+ # `run` proc converts to whatever its LLM library expects.
64
+ def self.tools(tools)
65
+ Brute::Tools::Adapter.wrap_all(tools || [])
68
66
  end
69
-
67
+
68
+
70
69
  def self.provider=(p)
71
70
  @provider = p.to_sym
72
71
  end
73
72
  end
74
73
 
75
- Dir.glob("#{__dir__}/{brute,ruby_llm}/**/*.rb").sort.each do |path|
74
+ Dir.glob("#{__dir__}/brute/**/*.rb").sort.each do |path|
76
75
  require path
77
76
  end
78
77
 
@@ -28,15 +28,15 @@ module BruteCLI
28
28
  return [empty_assistant] if @command.nil?
29
29
 
30
30
  call_id = "shell_#{SecureRandom.hex(8)}"
31
- tool_calls = {
32
- call_id => RubyLLM::ToolCall.new(
31
+ tool_calls = [
32
+ Brute::ToolCall.new(
33
33
  id: call_id,
34
34
  name: "shell",
35
35
  arguments: { "command" => @command },
36
36
  )
37
- }
37
+ ]
38
38
 
39
- [RubyLLM::Message.new(
39
+ [Brute::Message.new(
40
40
  role: :assistant,
41
41
  content: "",
42
42
  tool_calls: tool_calls,
@@ -78,17 +78,13 @@ module BruteCLI
78
78
  end
79
79
 
80
80
  def usage
81
- RubyLLM::Tokens.new(
82
- input: 0,
83
- output: 0,
84
- reasoning: 0,
85
- )
81
+ { input: 0, output: 0, reasoning: 0 }
86
82
  end
87
83
 
88
84
  private
89
85
 
90
86
  def empty_assistant
91
- RubyLLM::Message.new(role: :assistant, content: "")
87
+ Brute::Message.new(role: :assistant, content: "")
92
88
  end
93
89
  end
94
90
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brute
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
@@ -37,20 +37,6 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.5'
40
- - !ruby/object:Gem::Dependency
41
- name: ruby_llm
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: activesupport
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -163,6 +149,20 @@ dependencies:
163
149
  - - "~>"
164
150
  - !ruby/object:Gem::Version
165
151
  version: '1.0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: lefthook
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '2.1'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '2.1'
166
166
  description: Production-grade coding agent with tool execution, middleware pipeline,
167
167
  context compaction, session persistence, and multi-provider LLM support.
168
168
  executables: []
@@ -173,12 +173,18 @@ files:
173
173
  - lib/brute/events/handler.rb
174
174
  - lib/brute/events/prefixed_terminal_output.rb
175
175
  - lib/brute/events/terminal_output_handler.rb
176
+ - lib/brute/message_transport.rb
177
+ - lib/brute/message_transport/anthropic.rb
178
+ - lib/brute/message_transport/llm.rb
179
+ - lib/brute/message_transport/openai.rb
180
+ - lib/brute/message_transport/ruby_llm.rb
176
181
  - lib/brute/messages.rb
177
182
  - lib/brute/middleware/001_otel_span.rb
178
183
  - lib/brute/middleware/002_session_log.rb
179
184
  - lib/brute/middleware/004_summarize.rb
180
185
  - lib/brute/middleware/005_tracing.rb
181
186
  - lib/brute/middleware/006_loop.rb
187
+ - lib/brute/middleware/008_checkpoint.rb
182
188
  - lib/brute/middleware/010_max_iterations.rb
183
189
  - lib/brute/middleware/015_otel_token_usage.rb
184
190
  - lib/brute/middleware/020_system_prompt.rb
@@ -234,6 +240,7 @@ files:
234
240
  - lib/brute/rack/adapter.rb
235
241
  - lib/brute/skill.rb
236
242
  - lib/brute/system_prompt.rb
243
+ - lib/brute/tool.rb
237
244
  - lib/brute/tools.rb
238
245
  - lib/brute/tools/adapter.rb
239
246
  - lib/brute/tools/fs/file_mutation_queue.rb
@@ -260,7 +267,6 @@ files:
260
267
  - lib/brute/version.rb
261
268
  - lib/brute_cli/providers/shell.rb
262
269
  - lib/brute_cli/providers/shell_response.rb
263
- - lib/ruby_llm/message_transport.rb
264
270
  homepage: https://github.com/general-intelligence-systems/brute
265
271
  licenses:
266
272
  - MIT
@@ -282,5 +288,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
282
288
  requirements: []
283
289
  rubygems_version: 3.7.2
284
290
  specification_version: 4
285
- summary: A coding agent built on ruby_llm
291
+ summary: A framework-agnostic coding agent
286
292
  test_files: []