brute 5.1.0 → 6.0.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/compaction/middleware/sliding_window.rb +1 -1
  3. data/lib/brute/compaction/middleware/strategy.rb +0 -2
  4. data/lib/brute/compaction/summarize.rb +30 -21
  5. data/lib/brute/completion/lang_chain.rb +34 -31
  6. data/lib/brute/completion/llmrb.rb +31 -29
  7. data/lib/brute/completion/open_router.rb +43 -40
  8. data/lib/brute/completion/ruby_llm.rb +28 -27
  9. data/lib/brute/contrib/otel.rb +81 -49
  10. data/lib/brute/eval/case.rb +7 -2
  11. data/lib/brute/eval/suite.rb +10 -5
  12. data/lib/brute/eval/transcript.rb +3 -3
  13. data/lib/brute/eval/world.rb +2 -2
  14. data/lib/brute/eval.rb +1 -1
  15. data/lib/brute/events/handler.rb +3 -1
  16. data/lib/brute/events/prefixed_terminal_output.rb +3 -1
  17. data/lib/brute/events/terminal_output_handler.rb +1 -1
  18. data/lib/brute/hooks.rb +88 -112
  19. data/lib/brute/message_transport/anthropic.rb +13 -3
  20. data/lib/brute/message_transport/llm.rb +8 -4
  21. data/lib/brute/message_transport/open_router.rb +30 -16
  22. data/lib/brute/message_transport/openai.rb +14 -7
  23. data/lib/brute/message_transport/ruby_llm.rb +21 -17
  24. data/lib/brute/message_transport/ruby_open_ai.rb +59 -57
  25. data/lib/brute/messages.rb +15 -8
  26. data/lib/brute/middleware/000_base.rb +5 -5
  27. data/lib/brute/middleware/002_session_log.rb +12 -4
  28. data/lib/brute/middleware/008_checkpoint.rb +26 -18
  29. data/lib/brute/middleware/010_max_iterations.rb +1 -1
  30. data/lib/brute/middleware/020_system_prompt.rb +1 -1
  31. data/lib/brute/middleware/040_default_compaction_pipeline.rb +49 -45
  32. data/lib/brute/middleware/070_default_tool_pipeline.rb +44 -53
  33. data/lib/brute/prompt_template.rb +19 -15
  34. data/lib/brute/prompts/base.rb +19 -10
  35. data/lib/brute/prompts/environment.rb +3 -1
  36. data/lib/brute/prompts/instructions.rb +9 -7
  37. data/lib/brute/prompts/skills.rb +5 -3
  38. data/lib/brute/rack/adapter.rb +40 -23
  39. data/lib/brute/skill.rb +136 -89
  40. data/lib/brute/system_prompt.rb +9 -9
  41. data/lib/brute/token_counter.rb +3 -1
  42. data/lib/brute/tool.rb +10 -6
  43. data/lib/brute/tools/adapter.rb +53 -43
  44. data/lib/brute/tools/fs/snapshot_store.rb +5 -1
  45. data/lib/brute/tools/fs_patch.rb +16 -8
  46. data/lib/brute/tools/fs_read.rb +107 -80
  47. data/lib/brute/tools/fs_remove.rb +6 -2
  48. data/lib/brute/tools/fs_search.rb +14 -4
  49. data/lib/brute/tools/fs_undo.rb +6 -2
  50. data/lib/brute/tools/fs_write.rb +5 -1
  51. data/lib/brute/tools/net_fetch.rb +6 -2
  52. data/lib/brute/tools/question.rb +42 -39
  53. data/lib/brute/tools/shell.rb +20 -5
  54. data/lib/brute/tools/skill_load.rb +44 -39
  55. data/lib/brute/tools/todo_write.rb +19 -15
  56. data/lib/brute/truncation.rb +68 -43
  57. data/lib/brute/turn/agent_pipeline.rb +23 -13
  58. data/lib/brute/turn/compaction_pipeline.rb +4 -3
  59. data/lib/brute/turn/pipeline.rb +87 -96
  60. data/lib/brute/turn/tool_pipeline.rb +4 -3
  61. data/lib/brute/usage_detection/llmrb.rb +18 -14
  62. data/lib/brute/usage_detection/open_router.rb +20 -17
  63. data/lib/brute/usage_detection/ruby_llm.rb +18 -14
  64. data/lib/brute/usage_detection/usage.rb +10 -1
  65. data/lib/brute/utils/diff.rb +18 -10
  66. data/lib/brute/version.rb +1 -1
  67. data/lib/brute.rb +10 -11
  68. data/lib/brute_cli/providers/shell.rb +32 -29
  69. data/lib/brute_cli/providers/shell_response.rb +20 -18
  70. metadata +30 -5
  71. data/lib/brute/middleware/040_compaction_check.rb +0 -73
  72. data/lib/brute/middleware/070_tool_pipeline.rb +0 -61
  73. data/lib/brute/middleware/event_handler.rb +0 -27
@@ -12,17 +12,25 @@ module Brute
12
12
  old_lines = old_text.lines
13
13
  new_lines = new_text.lines
14
14
  diffs = ::Diff::LCS.diff(old_lines, new_lines)
15
- return '' if diffs.empty?
16
-
17
- output = +''
18
- file_length_difference = 0
19
- diffs.each do |piece|
20
- hunk = ::Diff::LCS::Hunk.new(old_lines, new_lines, piece, context, file_length_difference)
21
- file_length_difference = hunk.file_length_difference
22
- output << hunk.diff(:unified)
23
- output << "\n"
15
+ if diffs.empty?
16
+ ''
17
+ else
18
+ output = +''
19
+ file_length_difference = 0
20
+ diffs.each do |piece|
21
+ hunk = ::Diff::LCS::Hunk.new(
22
+ old_lines,
23
+ new_lines,
24
+ piece,
25
+ context,
26
+ file_length_difference,
27
+ )
28
+ file_length_difference = hunk.file_length_difference
29
+ output << hunk.diff(:unified)
30
+ output << "\n"
31
+ end
32
+ output
24
33
  end
25
- output
26
34
  end
27
35
  end
28
36
  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 = "5.1.0"
4
+ VERSION = "6.0.0"
5
5
  end
data/lib/brute.rb CHANGED
@@ -12,13 +12,13 @@ require_relative 'brute/version'
12
12
 
13
13
  module Brute
14
14
  LOGO = <<-LOGO
15
- .o8 .
16
- "888 .o8
17
- 888oooo. oooo d8b oooo oooo .o888oo .ooooo.
18
- d88' `88b `888""8P `888 `888 888 d88' `88b
19
- 888 888 888 888 888 888 888ooo888
20
- 888 888 888 888 888 888 . 888 .o
21
- `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
15
+ .o8 .
16
+ "888 .o8
17
+ 888oooo. oooo d8b oooo oooo .o888oo .ooooo.
18
+ d88' `88b `888""8P `888 `888 888 d88' `88b
19
+ 888 888 888 888 888 888 888ooo888
20
+ 888 888 888 888 888 888 . 888 .o
21
+ `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
22
22
  LOGO
23
23
 
24
24
  # NOTE: Brute owns no LLM configuration and no LLM library. All
@@ -68,7 +68,9 @@ module Brute
68
68
  #
69
69
  def self.load_agent(path = "agent.ru")
70
70
  path = File.expand_path(path)
71
- raise ArgumentError, "no such agent file: #{path}" unless File.file?(path)
71
+ unless File.file?(path)
72
+ raise ArgumentError, "no such agent file: #{path}"
73
+ end
72
74
 
73
75
  Brute::Turn::AgentPipeline.parse_file(path)
74
76
  end
@@ -132,11 +134,8 @@ require_relative "brute/middleware/010_max_iterations"
132
134
  require_relative "brute/middleware/020_system_prompt"
133
135
  require_relative "brute/middleware/025_skills"
134
136
  require_relative "brute/middleware/040_default_compaction_pipeline"
135
- require_relative "brute/middleware/040_compaction_check"
136
137
  require_relative "brute/middleware/060_questions"
137
138
  require_relative "brute/middleware/070_default_tool_pipeline"
138
- require_relative "brute/middleware/070_tool_pipeline"
139
- require_relative "brute/middleware/event_handler"
140
139
  require_relative "brute/middleware/user_queue"
141
140
  require_relative "brute/prompt_template"
142
141
  require_relative "brute/prompts"
@@ -44,12 +44,13 @@ module BruteCLI
44
44
 
45
45
  # nil text means we received tool results (second call) —
46
46
  # return an empty assistant response so the agent loop exits.
47
- return ShellResponse.new(model: model, tools: tools) if text.nil?
48
-
49
- wrap = INTERPRETERS.fetch(model, INTERPRETERS["bash"])
50
- command = wrap.call(text)
51
-
52
- ShellResponse.new(command: command, model: model, tools: tools)
47
+ if text.nil?
48
+ ShellResponse.new(model: model, tools: tools)
49
+ else
50
+ wrap = INTERPRETERS.fetch(model, INTERPRETERS["bash"])
51
+ command = wrap.call(text)
52
+ ShellResponse.new(command: command, model: model, tools: tools)
53
+ end
53
54
  end
54
55
 
55
56
  # For the REPL model picker: provider.models.all.select(&:chat?)
@@ -61,38 +62,40 @@ module BruteCLI
61
62
 
62
63
  private
63
64
 
64
- # Extract the user's text from the messages array.
65
- # Returns nil when the messages contain tool results (the second
66
- # round-trip) so #complete knows to return an empty response.
67
- def extract_text(prompt)
68
- case prompt
69
- when String
70
- prompt
71
- when ::Array
72
- return nil if prompt.any? { |m| m.respond_to?(:role) && m.role == :tool }
73
-
74
- user_msg = prompt.reverse_each.find { |m| m.respond_to?(:role) && m.role == :user }
75
- user_msg&.content.to_s
76
- else
77
- prompt.to_s
65
+ # Extract the user's text from the messages array.
66
+ # Returns nil when the messages contain tool results (the second
67
+ # round-trip) so #complete knows to return an empty response.
68
+ def extract_text(prompt)
69
+ case prompt
70
+ when String
71
+ prompt
72
+ when ::Array
73
+ if prompt.any? { |m| m.respond_to?(:role) && m.role == :tool }
74
+ nil
75
+ else
76
+ user_msg = prompt.reverse_each.find { |m| m.respond_to?(:role) && m.role == :user }
77
+ user_msg&.content.to_s
78
+ end
79
+ else
80
+ prompt.to_s
81
+ end
78
82
  end
79
- end
80
83
 
81
84
  # ── ModelList ──────────────────────────────────────────────────
82
85
 
83
86
  # Minimal object that quacks like provider.models so the REPL's
84
87
  # fetch_models can call provider.models.all.select(&:chat?).
85
- class ModelList
86
- ModelEntry = Struct.new(:id, :chat?, keyword_init: true)
88
+ class ModelList
89
+ ModelEntry = Struct.new(:id, :chat?, keyword_init: true)
87
90
 
88
- def initialize(names)
89
- @entries = names.map { |n| ModelEntry.new(id: n, chat?: true) }
90
- end
91
+ def initialize(names)
92
+ @entries = names.map { |n| ModelEntry.new(id: n, chat?: true) }
93
+ end
91
94
 
92
- def all
93
- @entries
95
+ def all
96
+ @entries
97
+ end
94
98
  end
95
- end
96
99
  end
97
100
  end
98
101
  end
@@ -25,22 +25,24 @@ module BruteCLI
25
25
  end
26
26
 
27
27
  def messages
28
- return [empty_assistant] if @command.nil?
29
-
30
- call_id = "shell_#{SecureRandom.hex(8)}"
31
- tool_calls = [
32
- Brute::ToolCall.new(
33
- id: call_id,
34
- name: "shell",
35
- arguments: { "command" => @command },
28
+ if @command.nil?
29
+ [empty_assistant]
30
+ else
31
+ call_id = "shell_#{SecureRandom.hex(8)}"
32
+ tool_calls = [
33
+ Brute::ToolCall.new(
34
+ id: call_id,
35
+ name: "shell",
36
+ arguments: { "command" => @command },
37
+ )
38
+ ]
39
+ [Brute::Message.new(
40
+ role: :assistant,
41
+ content: "",
42
+ tool_calls: tool_calls,
36
43
  )
37
- ]
38
-
39
- [Brute::Message.new(
40
- role: :assistant,
41
- content: "",
42
- tool_calls: tool_calls,
43
- )]
44
+ ]
45
+ end
44
46
  end
45
47
  alias_method :choices, :messages
46
48
 
@@ -83,9 +85,9 @@ module BruteCLI
83
85
 
84
86
  private
85
87
 
86
- def empty_assistant
87
- Brute::Message.new(role: :assistant, content: "")
88
- end
88
+ def empty_assistant
89
+ Brute::Message.new(role: :assistant, content: "")
90
+ end
89
91
  end
90
92
  end
91
93
  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: 5.1.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
@@ -219,6 +219,20 @@ dependencies:
219
219
  - - ">="
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: rubocop
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
222
236
  - !ruby/object:Gem::Dependency
223
237
  name: rake
224
238
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +247,20 @@ dependencies:
233
247
  - - "~>"
234
248
  - !ruby/object:Gem::Version
235
249
  version: '13.0'
250
+ - !ruby/object:Gem::Dependency
251
+ name: rdoc
252
+ requirement: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - "~>"
255
+ - !ruby/object:Gem::Version
256
+ version: '8.0'
257
+ type: :development
258
+ prerelease: false
259
+ version_requirements: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '8.0'
236
264
  - !ruby/object:Gem::Dependency
237
265
  name: scampi
238
266
  requirement: !ruby/object:Gem::Requirement
@@ -322,12 +350,9 @@ files:
322
350
  - lib/brute/middleware/010_max_iterations.rb
323
351
  - lib/brute/middleware/020_system_prompt.rb
324
352
  - lib/brute/middleware/025_skills.rb
325
- - lib/brute/middleware/040_compaction_check.rb
326
353
  - lib/brute/middleware/040_default_compaction_pipeline.rb
327
354
  - lib/brute/middleware/060_questions.rb
328
355
  - lib/brute/middleware/070_default_tool_pipeline.rb
329
- - lib/brute/middleware/070_tool_pipeline.rb
330
- - lib/brute/middleware/event_handler.rb
331
356
  - lib/brute/middleware/user_queue.rb
332
357
  - lib/brute/prompt_template.rb
333
358
  - lib/brute/prompts.rb
@@ -415,7 +440,7 @@ files:
415
440
  - lib/brute_cli/providers/shell_response.rb
416
441
  homepage: https://github.com/general-intelligence-systems/brute
417
442
  licenses:
418
- - MIT
443
+ - Apache-2.0
419
444
  metadata:
420
445
  documentation_uri: https://general-intelligence-systems.github.io/brute/
421
446
  rdoc_options: []
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "brute"
5
- require "gem_kit"
6
-
7
- module Brute
8
- module Middleware
9
- # The old compaction trigger, kept working while it is deprecated.
10
- # DefaultCompactionPipeline is the layer that does the job.
11
- #
12
- # It passes the turn through and compacts nothing, which is what it always
13
- # did -- and it is deliberately not a subclass of its replacement, because
14
- # inheriting would make a layer that did nothing suddenly start giving up
15
- # context. A deprecation warns about a name; it does not change what the
16
- # code under that name does.
17
- #
18
- # Whatever it was configured with is accepted and ignored, so existing
19
- # `use` lines keep parsing until they are moved over:
20
- #
21
- # use Brute::Middleware::DefaultCompactionPipeline,
22
- # window: 200_000,
23
- # summariser: Brute::Completion::OpenRouter.new(config: { access_token: key })
24
- #
25
- class CompactionCheck < Brute::Middleware::Base
26
- extend GemKit::Deprecate
27
- superseded_by "Brute::Middleware::DefaultCompactionPipeline", "6.0"
28
-
29
- def initialize(app, **_options)
30
- @app = app
31
- end
32
-
33
- def call(env) = @app.call(env)
34
- end
35
- end
36
- end
37
-
38
- __END__
39
-
40
- describe "brute/middleware/040_compaction_check" do
41
- it "passes the turn through as it always did, and says what to use instead" do
42
- warned = []
43
- original = GemKit::Deprecate.method(:warn)
44
- GemKit::Deprecate.define_singleton_method(:warn) { |message| warned << message }
45
-
46
- begin
47
- # Whatever it was configured with is taken and ignored, so an existing
48
- # `use` line keeps parsing.
49
- layer = Brute::Middleware::CompactionCheck.new(
50
- ->(env) { env[:messages].assistant("answered") },
51
- system_prompt: "you are helpful",
52
- token_threshold: 100
53
- )
54
-
55
- env = { messages: Brute.log }
56
- layer.call(env)
57
- env[:messages].last.content.should == "answered"
58
- ensure
59
- GemKit::Deprecate.define_singleton_method(:warn, original)
60
- end
61
-
62
- # Not a subclass of its replacement: a layer that compacted nothing must
63
- # not start compacting because its name was deprecated.
64
- Brute::Middleware::CompactionCheck.ancestors
65
- .should.not.include Brute::Middleware::DefaultCompactionPipeline
66
-
67
- warned.first.should.include "Brute::Middleware::DefaultCompactionPipeline"
68
-
69
- declared = GemKit::Deprecate.registry.find { |entry| entry.name == "Brute::Middleware::CompactionCheck" }
70
- declared.replacement.should == "Brute::Middleware::DefaultCompactionPipeline"
71
- declared.removed_in.should == Gem::Version.new("6.0")
72
- end
73
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "brute"
5
- require "gem_kit"
6
-
7
- module Brute
8
- module Middleware
9
- # The old name for DefaultToolPipeline, kept working while it is
10
- # deprecated. The middleware is one particular wiring of tool dispatch and
11
- # the name now says so, leaving Brute::Turn::ToolPipeline as the mechanism
12
- # to compose when that wiring is not what you want.
13
- #
14
- # use Brute::Middleware::DefaultToolPipeline, tools: tools
15
- #
16
- class ToolPipeline < DefaultToolPipeline
17
- extend GemKit::Deprecate
18
- superseded_by "Brute::Middleware::DefaultToolPipeline", "6.0"
19
- end
20
- end
21
- end
22
-
23
- __END__
24
-
25
- describe "brute/middleware/070_tool_pipeline" do
26
- it "still dispatches tools under the old name, and says what to use instead" do
27
- warned = []
28
- original = GemKit::Deprecate.method(:warn)
29
- GemKit::Deprecate.define_singleton_method(:warn) { |message| warned << message }
30
-
31
- begin
32
- tool = Brute::Turn::ToolPipeline.new(name: "echo", description: "echo") do
33
- run ->(env) { env[:result] = "echoed" }
34
- end
35
-
36
- agent = Brute.agent
37
- .use(Brute::Middleware::ToolPipeline, tools: [tool])
38
- .run(
39
- ->(env) {
40
- env[:messages] << Brute::Message.new(
41
- role: :assistant,
42
- content: "",
43
- tool_calls: [{ id: "1", name: "echo", arguments: {} }]
44
- )
45
- }
46
- )
47
-
48
- agent.start("go")[:messages].last.content.should == "echoed"
49
- ensure
50
- GemKit::Deprecate.define_singleton_method(:warn, original)
51
- end
52
-
53
- Brute::Middleware::ToolPipeline.ancestors.should.include Brute::Middleware::DefaultToolPipeline
54
- warned.first.should.include "Brute::Middleware::DefaultToolPipeline"
55
- warned.first.should.include "6.0"
56
-
57
- declared = GemKit::Deprecate.registry.find { |entry| entry.name == "Brute::Middleware::ToolPipeline" }
58
- declared.replacement.should == "Brute::Middleware::DefaultToolPipeline"
59
- declared.removed_in.should == Gem::Version.new("6.0")
60
- end
61
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- require 'brute'
5
-
6
- module Brute
7
- module Middleware
8
- class EventHandler < Brute::Middleware::Base
9
- def initialize(app, handler_class:, **opts)
10
- @app = app
11
- @handler_class = handler_class
12
- @opts = opts
13
- end
14
-
15
- def call(env)
16
- env[:events] = @handler_class.new(env[:events], **@opts)
17
- @app.call(env)
18
- end
19
- end
20
- end
21
- end
22
-
23
- __END__
24
-
25
- describe "brute/middleware/event_handler" do
26
- # not implemented
27
- end