brute 3.0.0 → 3.0.1

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 (37) 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/010_max_iterations.rb +1 -1
  12. data/lib/brute/middleware/020_system_prompt.rb +1 -1
  13. data/lib/brute/middleware/040_compaction_check.rb +2 -2
  14. data/lib/brute/middleware/070_tool_pipeline.rb +29 -26
  15. data/lib/brute/tool.rb +124 -0
  16. data/lib/brute/tools/adapter.rb +22 -60
  17. data/lib/brute/tools/fs_patch.rb +1 -1
  18. data/lib/brute/tools/fs_read.rb +1 -1
  19. data/lib/brute/tools/fs_remove.rb +1 -1
  20. data/lib/brute/tools/fs_search.rb +1 -1
  21. data/lib/brute/tools/fs_undo.rb +1 -1
  22. data/lib/brute/tools/fs_write.rb +1 -1
  23. data/lib/brute/tools/net_fetch.rb +1 -1
  24. data/lib/brute/tools/question.rb +1 -1
  25. data/lib/brute/tools/shell.rb +1 -1
  26. data/lib/brute/tools/skill_load.rb +1 -1
  27. data/lib/brute/tools/sub_agent.rb +5 -22
  28. data/lib/brute/tools/todo_read.rb +1 -1
  29. data/lib/brute/tools/todo_write.rb +1 -1
  30. data/lib/brute/turn/agent_pipeline.rb +2 -1
  31. data/lib/brute/turn/pipeline.rb +1 -1
  32. data/lib/brute/turn/tool_pipeline.rb +2 -12
  33. data/lib/brute/version.rb +1 -1
  34. data/lib/brute.rb +17 -18
  35. data/lib/brute_cli/providers/shell_response.rb +6 -10
  36. metadata +22 -17
  37. data/lib/ruby_llm/message_transport.rb +0 -117
@@ -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.0.1"
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.0.1
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,6 +173,11 @@ 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
@@ -234,6 +239,7 @@ files:
234
239
  - lib/brute/rack/adapter.rb
235
240
  - lib/brute/skill.rb
236
241
  - lib/brute/system_prompt.rb
242
+ - lib/brute/tool.rb
237
243
  - lib/brute/tools.rb
238
244
  - lib/brute/tools/adapter.rb
239
245
  - lib/brute/tools/fs/file_mutation_queue.rb
@@ -260,7 +266,6 @@ files:
260
266
  - lib/brute/version.rb
261
267
  - lib/brute_cli/providers/shell.rb
262
268
  - lib/brute_cli/providers/shell_response.rb
263
- - lib/ruby_llm/message_transport.rb
264
269
  homepage: https://github.com/general-intelligence-systems/brute
265
270
  licenses:
266
271
  - MIT
@@ -282,5 +287,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
282
287
  requirements: []
283
288
  rubygems_version: 3.7.2
284
289
  specification_version: 4
285
- summary: A coding agent built on ruby_llm
290
+ summary: A framework-agnostic coding agent
286
291
  test_files: []
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "brute"
5
-
6
- module RubyLLM
7
- # Transports the result of an LLM call into Brute's message format.
8
- #
9
- # Calling an LLM is trivial — `RubyLLM.chat.ask "..."` — so Brute has no
10
- # "completion middleware". The terminal `run` of an agent pipeline is an
11
- # inline proc that makes the LLM call itself. The only wrinkle: RubyLLM hands
12
- # back its own objects (a Message, an array, a whole Chat transcript), while
13
- # the rest of the stack — the turn manager, event handlers, tool loop and
14
- # SessionLog persistence — works off `env[:messages]` (a plain message log;
15
- # see Brute.log).
16
- #
17
- # This makes NO LLM call and does NO appending. It just wraps what the proc
18
- # got back and yields each message in Brute's format; the proc appends:
19
- #
20
- # response = provider.complete(env[:messages], ...) # one Message
21
- # RubyLLM::MessageTransport.new(response).wrap_each do |message|
22
- # env[:messages] << message
23
- # end
24
- #
25
- # before = chat.messages.length # a Chat loop
26
- # chat.complete
27
- # RubyLLM::MessageTransport.new(chat.messages[before..]).wrap_each do |message|
28
- # env[:messages] << message
29
- # end
30
- class MessageTransport
31
- # Convenience: RubyLLM::MessageTransport.wrap_each(result) { |m| ... }
32
- def self.wrap_each(result, &block)
33
- new(result).wrap_each(&block)
34
- end
35
-
36
- def initialize(result)
37
- @result = result
38
- end
39
-
40
- # Yield each result message in Brute's format. Without a block, returns an
41
- # Enumerator. The caller decides what to do with each (typically append to
42
- # env[:messages]).
43
- def wrap_each
44
- return enum_for(:wrap_each) unless block_given?
45
-
46
- messages.each { |message| yield wrap(message) }
47
- end
48
-
49
- # The result normalized to a flat list of messages. A single Message, an
50
- # array, or anything transcript-shaped (a Chat responds to #messages).
51
- def messages
52
- case @result
53
- when ::RubyLLM::Message then [@result]
54
- when Array then @result.compact
55
- else @result.respond_to?(:messages) ? @result.messages : Array(@result)
56
- end
57
- end
58
-
59
- private
60
-
61
- # The Brute-format view of a single message. RubyLLM::Message already IS
62
- # Brute's message format, so this is the identity seam — the place to
63
- # normalize if the two formats ever diverge.
64
- def wrap(message)
65
- message
66
- end
67
- end
68
- end
69
-
70
- __END__
71
-
72
- describe "ruby_llm/message_transport" do
73
- require "brute/messages"
74
-
75
- it "wraps a single message" do
76
- message = RubyLLM::Message.new(role: :assistant, content: "hi")
77
- RubyLLM::MessageTransport.new(message).wrap_each.to_a.should == [message]
78
- end
79
-
80
- it "wraps an array of messages" do
81
- a = RubyLLM::Message.new(role: :assistant, content: "one")
82
- b = RubyLLM::Message.new(role: :tool, content: "two", tool_call_id: "tc1")
83
- RubyLLM::MessageTransport.new([a, b]).wrap_each.to_a.should == [a, b]
84
- end
85
-
86
- it "wraps a transcript-shaped object (a Chat)" do
87
- fake_chat = Class.new do
88
- attr_reader :messages
89
- def initialize(messages); @messages = messages; end
90
- end
91
- msgs = [RubyLLM::Message.new(role: :user, content: "hi"),
92
- RubyLLM::Message.new(role: :assistant, content: "hello")]
93
-
94
- RubyLLM::MessageTransport.new(fake_chat.new(msgs)).wrap_each.to_a.map(&:role).should == [:user, :assistant]
95
- end
96
-
97
- it "yields each message to the block for the caller to append" do
98
- session = Brute.log
99
- session.user("hello")
100
- response = RubyLLM::Message.new(role: :assistant, content: "hi there")
101
-
102
- RubyLLM::MessageTransport.new(response).wrap_each { |message| session << message }
103
-
104
- session.last.role.should == :assistant
105
- session.last.content.should == "hi there"
106
- end
107
-
108
- it "exposes a class-level wrap_each convenience" do
109
- session = Brute.log
110
- a = RubyLLM::Message.new(role: :assistant, content: "a")
111
- b = RubyLLM::Message.new(role: :assistant, content: "b")
112
-
113
- RubyLLM::MessageTransport.wrap_each([a, b]) { |message| session << message }
114
-
115
- session.map(&:content).should == ["a", "b"]
116
- end
117
- end