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
@@ -42,7 +42,7 @@ module Brute
42
42
  loaded = []
43
43
  File.foreach(@path) do |line|
44
44
  line = line.strip
45
- loaded << ::RubyLLM::Message.new(**JSON.parse(line, symbolize_names: true)) unless line.empty?
45
+ loaded << Brute::Message.new(**JSON.parse(line, symbolize_names: true)) unless line.empty?
46
46
  end
47
47
  messages.unshift(*loaded)
48
48
  end
@@ -36,7 +36,7 @@ module Brute
36
36
  saved_tools = env[:tools]
37
37
  env[:tools] = []
38
38
  env[:current_iteration] = 1
39
- env[:messages] << RubyLLM::Message.new(role: :user, content: @prompt)
39
+ env[:messages] << Brute::Message.new(role: :user, content: @prompt)
40
40
  @app.call(env)
41
41
  env[:tools] = saved_tools
42
42
 
@@ -59,9 +59,9 @@ describe "brute/middleware/004_summarize" do
59
59
  inner = ->(env) do
60
60
  call_count += 1
61
61
  if call_count == 1
62
- env[:messages] << RubyLLM::Message.new(role: :tool, content: "some result", tool_call_id: "tc1")
62
+ env[:messages] << Brute::Message.new(role: :tool, content: "some result", tool_call_id: "tc1")
63
63
  else
64
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "Here is my complete summary.")
64
+ env[:messages] << Brute::Message.new(role: :assistant, content: "Here is my complete summary.")
65
65
  end
66
66
  end
67
67
 
@@ -77,7 +77,7 @@ describe "brute/middleware/004_summarize" do
77
77
 
78
78
  it "restores tools after summary call" do
79
79
  inner = ->(env) {
80
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "done")
80
+ env[:messages] << Brute::Message.new(role: :assistant, content: "done")
81
81
  }
82
82
 
83
83
  mw = Brute::Middleware::Summarize.new(inner)
@@ -93,7 +93,7 @@ describe "brute/middleware/004_summarize" do
93
93
  captured_iteration = nil
94
94
  inner = ->(env) {
95
95
  captured_iteration = env[:current_iteration]
96
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "done")
96
+ env[:messages] << Brute::Message.new(role: :assistant, content: "done")
97
97
  }
98
98
 
99
99
  mw = Brute::Middleware::Summarize.new(inner)
@@ -111,7 +111,7 @@ describe "brute/middleware/004_summarize" do
111
111
  inner = ->(env) {
112
112
  call_count += 1
113
113
  messages_at_second_call = env[:messages].map(&:content) if call_count == 2
114
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "done")
114
+ env[:messages] << Brute::Message.new(role: :assistant, content: "done")
115
115
  }
116
116
 
117
117
  mw = Brute::Middleware::Summarize.new(inner)
@@ -128,7 +128,7 @@ describe "brute/middleware/004_summarize" do
128
128
  inner = ->(env) {
129
129
  call_count += 1
130
130
  messages_at_second_call = env[:messages].map(&:content) if call_count == 2
131
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "done")
131
+ env[:messages] << Brute::Message.new(role: :assistant, content: "done")
132
132
  }
133
133
 
134
134
  mw = Brute::Middleware::Summarize.new(inner, prompt: "Give me the TL;DR.")
@@ -108,9 +108,9 @@ describe "brute/middleware/006_loop" do
108
108
  inner = ->(env) do
109
109
  call_count += 1
110
110
  if call_count == 1
111
- env[:messages] << RubyLLM::Message.new(role: :tool, content: "result", tool_call_id: "tc1")
111
+ env[:messages] << Brute::Message.new(role: :tool, content: "result", tool_call_id: "tc1")
112
112
  else
113
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "done")
113
+ env[:messages] << Brute::Message.new(role: :assistant, content: "done")
114
114
  end
115
115
  end
116
116
 
@@ -128,7 +128,7 @@ describe "brute/middleware/006_loop" do
128
128
  call_count = 0
129
129
  inner = ->(env) do
130
130
  call_count += 1
131
- env[:messages] << RubyLLM::Message.new(role: :tool, content: "result", tool_call_id: "tc#{call_count}")
131
+ env[:messages] << Brute::Message.new(role: :tool, content: "result", tool_call_id: "tc#{call_count}")
132
132
  env[:should_exit] = { reason: "max" } if call_count >= 2
133
133
  end
134
134
 
@@ -144,7 +144,7 @@ describe "brute/middleware/006_loop" do
144
144
  call_count = 0
145
145
  inner = ->(env) do
146
146
  call_count += 1
147
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "hello")
147
+ env[:messages] << Brute::Message.new(role: :assistant, content: "hello")
148
148
  end
149
149
 
150
150
  mw = Brute::Middleware::Loop::ToolResult.new(inner)
@@ -22,7 +22,7 @@ module Brute
22
22
 
23
23
  def call(env)
24
24
  if max_iterations_reached?(env)
25
- env[:messages] << RubyLLM::Message.new(
25
+ env[:messages] << Brute::Message.new(
26
26
  role: :user,
27
27
  content: "Maximum iterations reached.",
28
28
  )
@@ -37,7 +37,7 @@ module Brute
37
37
  result = @system_prompt.prepare(ctx)
38
38
  unless result.empty?
39
39
  env[:messages].unshift(
40
- RubyLLM::Message.new(role: :system, content: result.to_s)
40
+ Brute::Message.new(role: :system, content: result.to_s)
41
41
  )
42
42
  end
43
43
  end
@@ -39,8 +39,8 @@ module Brute
39
39
  # }
40
40
  # # Replace the message history with the summary
41
41
  # env[:messages] = [
42
- # RubyLLM::Message.new(role: :system, content: @system_prompt),
43
- # RubyLLM::Message.new(role: :user, content: "[Previous conversation summary]\n\n#{summary_text}"),
42
+ # Brute::Message.new(role: :system, content: @system_prompt),
43
+ # Brute::Message.new(role: :user, content: "[Previous conversation summary]\n\n#{summary_text}"),
44
44
  # ]
45
45
  # end
46
46
  #end
@@ -81,13 +81,13 @@ module Brute
81
81
  Sync do
82
82
  barrier = Async::Barrier.new
83
83
 
84
- tools_to_run.each do |id, tool_call|
84
+ tools_to_run.each do |tool_call|
85
85
  barrier.async do
86
86
  tool = available_tools[tool_call.name.to_sym]
87
87
  result = tool.call(tool_call.arguments)
88
88
 
89
- # Coerce to String so RubyLLM::Message doesn't treat Hash results
90
- # (e.g. Shell's {stdout:, stderr:, exit_code:}) as attachments.
89
+ # Coerce to String so Hash results (e.g. Shell's
90
+ # {stdout:, stderr:, exit_code:}) serialize predictably.
91
91
  content = result.is_a?(String) ? result : result.to_s
92
92
 
93
93
  # Universal truncation safety net — skip if already truncated
@@ -95,13 +95,13 @@ module Brute
95
95
  content = Brute::Truncation.truncate(content)
96
96
  end
97
97
 
98
- results << [id, tool_call, content]
98
+ results << [tool_call, content]
99
99
  rescue => e
100
100
  # Capture the error as a tool result so the LLM can see it
101
101
  # and reason about the failure, rather than crashing the
102
102
  # entire middleware chain.
103
103
  env[:events] << { type: :error, data: { error: e, message: e.message } }
104
- results << [id, tool_call, "Error: #{e.class}: #{e.message}"]
104
+ results << [tool_call, "Error: #{e.class}: #{e.message}"]
105
105
  end
106
106
  end
107
107
 
@@ -112,12 +112,11 @@ module Brute
112
112
 
113
113
  # Append events and messages in the original tool_call order so the
114
114
  # LLM sees a deterministic sequence regardless of completion order.
115
- order = tools_to_run.keys
116
- results.sort_by! { |id, _, _| order.index(id) }
115
+ results.sort_by! { |tool_call, _| tools_to_run.index(tool_call) }
117
116
 
118
- results.each do |_id, tool_call, content|
117
+ results.each do |tool_call, content|
119
118
  env[:events] << { type: :tool_result, data: { name: tool_call.name, content: content } }
120
- env[:messages] << RubyLLM::Message.new(role: :tool, content: content, tool_call_id: tool_call.id)
119
+ env[:messages] << Brute::Message.new(role: :tool, content: content, tool_call_id: tool_call.id)
121
120
  end
122
121
  end
123
122
 
@@ -126,8 +125,14 @@ module Brute
126
125
 
127
126
  private
128
127
 
128
+ # The last message's pending tool calls as a flat list. Duck-typed:
129
+ # tool_calls may be an Array (Brute::ToolCall) or an id-keyed Hash
130
+ # (some libraries' native shape); each entry needs #id, #name and
131
+ # #arguments.
129
132
  def pending_tool_calls(message)
130
- message.tool_calls.to_h.reject { |_id, tc| tc.name == "question" }
133
+ calls = message.respond_to?(:tool_calls) ? message.tool_calls : nil
134
+ calls = calls.values if calls.respond_to?(:values)
135
+ Array(calls).reject { |tc| tc.name == "question" }
131
136
  end
132
137
 
133
138
  def resolve_tools(tools)
@@ -137,7 +142,7 @@ module Brute
137
142
  def on_tool_call_start_event(pending_tools)
138
143
  {
139
144
  type: :tool_call_start,
140
- data: pending_tools.map { |_id, tc|
145
+ data: pending_tools.map { |tc|
141
146
  {
142
147
  name: tc.name,
143
148
  call_id: tc.id,
@@ -158,7 +163,7 @@ describe "brute/middleware/070_tool_pipeline" do
158
163
 
159
164
  it "passes through when no tool calls pending" do
160
165
  inner = ->(env) {
161
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "hi")
166
+ env[:messages] << Brute::Message.new(role: :assistant, content: "hi")
162
167
  }
163
168
  mw = Brute::Middleware::ToolPipeline.new(inner, tools: [])
164
169
  env = {
@@ -185,7 +190,7 @@ describe "brute/middleware/070_tool_pipeline" do
185
190
 
186
191
  it "truncates large tool results via Truncation" do
187
192
  # A fake tool that returns a huge string
188
- big_tool = Class.new(RubyLLM::Tool) do
193
+ big_tool = Class.new(Brute::Tool) do
189
194
  description "test tool"
190
195
  param :input, type: "string", desc: "input"
191
196
  def name; "big_tool"; end
@@ -194,17 +199,16 @@ describe "brute/middleware/070_tool_pipeline" do
194
199
  end
195
200
  end
196
201
 
197
- call_id = "tc_1"
198
- tool_calls = {
199
- call_id => RubyLLM::ToolCall.new(
200
- id: call_id,
202
+ tool_calls = [
203
+ Brute::ToolCall.new(
204
+ id: "tc_1",
201
205
  name: "big_tool",
202
206
  arguments: { "input" => "go" },
203
207
  )
204
- }
208
+ ]
205
209
 
206
210
  inner = ->(env) {
207
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
211
+ env[:messages] << Brute::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
208
212
  }
209
213
  mw = Brute::Middleware::ToolPipeline.new(inner, tools: [big_tool])
210
214
  env = {
@@ -223,7 +227,7 @@ describe "brute/middleware/070_tool_pipeline" do
223
227
 
224
228
  it "does not double-truncate already-truncated output" do
225
229
  # A fake tool that returns output already containing the truncation marker
226
- pre_truncated_tool = Class.new(RubyLLM::Tool) do
230
+ pre_truncated_tool = Class.new(Brute::Tool) do
227
231
  description "test tool"
228
232
  param :input, type: "string", desc: "input"
229
233
  def name; "pre_truncated_tool"; end
@@ -232,17 +236,16 @@ describe "brute/middleware/070_tool_pipeline" do
232
236
  end
233
237
  end
234
238
 
235
- call_id = "tc_2"
236
- tool_calls = {
237
- call_id => RubyLLM::ToolCall.new(
238
- id: call_id,
239
+ tool_calls = [
240
+ Brute::ToolCall.new(
241
+ id: "tc_2",
239
242
  name: "pre_truncated_tool",
240
243
  arguments: { "input" => "go" },
241
244
  )
242
- }
245
+ ]
243
246
 
244
247
  inner = ->(env) {
245
- env[:messages] << RubyLLM::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
248
+ env[:messages] << Brute::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
246
249
  }
247
250
  mw = Brute::Middleware::ToolPipeline.new(inner, tools: [pre_truncated_tool])
248
251
  env = {
data/lib/brute/tool.rb ADDED
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "brute"
5
+
6
+ module Brute
7
+ # Base class for Brute's built-in tools — a tiny, framework-agnostic tool
8
+ # DSL. It intentionally mirrors the common tool-library shape (description +
9
+ # params) so tools read the same as they would in any LLM library, without
10
+ # depending on one:
11
+ #
12
+ # class Shell < Brute::Tool
13
+ # description "Execute a shell command"
14
+ # param :command, type: 'string', desc: "The command", required: true
15
+ #
16
+ # def name; "shell"; end
17
+ #
18
+ # def execute(command:)
19
+ # ...
20
+ # end
21
+ # end
22
+ #
23
+ # For tools whose arguments don't fit the flat param list, pass a raw JSON
24
+ # schema instead:
25
+ #
26
+ # params({ type: 'object', properties: { ... }, required: [...] })
27
+ #
28
+ # Instances expose the neutral interface Brute::Tools::Adapter understands:
29
+ # #name, #description, #params, #params_schema, #call.
30
+ class Tool
31
+ class << self
32
+ def description(text = nil)
33
+ return @description unless text
34
+
35
+ @description = text
36
+ end
37
+
38
+ # Declare one parameter: param :key, type:, desc:, required:
39
+ def param(name, type: "string", desc: nil, required: true, **opts)
40
+ param_definitions[name.to_sym] = { type: type, desc: desc, required: required, **opts }.compact
41
+ end
42
+
43
+ def param_definitions
44
+ @param_definitions ||= {}
45
+ end
46
+
47
+ # Raw JSON-schema override for complex argument shapes.
48
+ def params(schema = nil)
49
+ return @params_schema unless schema
50
+
51
+ @params_schema = schema.deep_symbolize_keys
52
+ end
53
+ end
54
+
55
+ # Tool name; subclasses usually override with an explicit short name.
56
+ def name
57
+ self.class.name.demodulize.underscore
58
+ end
59
+
60
+ def description
61
+ self.class.description.to_s
62
+ end
63
+
64
+ # { key => { type:, desc:, required: } }
65
+ def params
66
+ self.class.param_definitions
67
+ end
68
+
69
+ # The raw JSON schema, when declared via params({...}).
70
+ def params_schema
71
+ self.class.params
72
+ end
73
+
74
+ # Execute with a string- or symbol-keyed argument hash, as delivered
75
+ # by LLM providers.
76
+ def call(arguments = {})
77
+ execute(**arguments.to_h.transform_keys(&:to_sym))
78
+ end
79
+
80
+ def execute(**)
81
+ raise NotImplementedError, "#{self.class} must implement #execute"
82
+ end
83
+ end
84
+ end
85
+
86
+ __END__
87
+
88
+ describe "brute/tool" do
89
+ it "exposes description and params from the DSL" do
90
+ klass = Class.new(Brute::Tool) do
91
+ description "test tool"
92
+ param :input, type: "string", desc: "the input"
93
+ def name; "t"; end
94
+ def execute(input:); "got #{input}"; end
95
+ end
96
+
97
+ tool = klass.new
98
+ tool.description.should == "test tool"
99
+ tool.params[:input][:type].should == "string"
100
+ tool.params[:input][:required].should.be.true
101
+ end
102
+
103
+ it "calls execute with symbolized keys" do
104
+ klass = Class.new(Brute::Tool) do
105
+ description "echo"
106
+ param :msg, type: "string"
107
+ def name; "echo"; end
108
+ def execute(msg:); msg; end
109
+ end
110
+
111
+ klass.new.call("msg" => "hi").should == "hi"
112
+ end
113
+
114
+ it "accepts a raw JSON schema via params({...})" do
115
+ klass = Class.new(Brute::Tool) do
116
+ description "schema tool"
117
+ params({ type: "object", properties: { a: { type: "string" } }, required: %w[a] })
118
+ def name; "s"; end
119
+ def execute(a:); a; end
120
+ end
121
+
122
+ klass.new.params_schema[:properties][:a][:type].should == "string"
123
+ end
124
+ end
@@ -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 " \