brute 0.4.1 → 1.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +14 -0
  3. data/lib/brute/diff.rb +18 -28
  4. data/lib/brute/loop/agent_stream.rb +118 -0
  5. data/lib/brute/loop/agent_turn.rb +520 -0
  6. data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
  7. data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
  8. data/lib/brute/loop/step.rb +332 -0
  9. data/lib/brute/loop/tool_call_step.rb +90 -0
  10. data/lib/brute/middleware/compaction_check.rb +60 -146
  11. data/lib/brute/middleware/doom_loop_detection.rb +95 -92
  12. data/lib/brute/middleware/llm_call.rb +78 -80
  13. data/lib/brute/middleware/message_tracking.rb +115 -162
  14. data/lib/brute/middleware/otel/span.rb +25 -106
  15. data/lib/brute/middleware/otel/token_usage.rb +29 -84
  16. data/lib/brute/middleware/otel/tool_calls.rb +23 -107
  17. data/lib/brute/middleware/otel/tool_results.rb +22 -86
  18. data/lib/brute/middleware/reasoning_normalizer.rb +78 -103
  19. data/lib/brute/middleware/retry.rb +95 -76
  20. data/lib/brute/middleware/session_persistence.rb +38 -37
  21. data/lib/brute/middleware/token_tracking.rb +64 -63
  22. data/lib/brute/middleware/tool_error_tracking.rb +108 -82
  23. data/lib/brute/middleware/tool_use_guard.rb +57 -90
  24. data/lib/brute/middleware/tracing.rb +53 -63
  25. data/lib/brute/middleware.rb +18 -0
  26. data/lib/brute/orchestrator/turn.rb +105 -0
  27. data/lib/brute/pipeline.rb +77 -133
  28. data/lib/brute/prompts/build_switch.rb +21 -25
  29. data/lib/brute/prompts/environment.rb +31 -35
  30. data/lib/brute/prompts/identity.rb +22 -29
  31. data/lib/brute/prompts/instructions.rb +15 -18
  32. data/lib/brute/prompts/max_steps.rb +18 -25
  33. data/lib/brute/prompts/plan_reminder.rb +18 -26
  34. data/lib/brute/prompts/skills.rb +8 -30
  35. data/lib/brute/prompts.rb +28 -0
  36. data/lib/brute/providers/ollama.rb +135 -0
  37. data/lib/brute/providers/shell.rb +2 -2
  38. data/lib/brute/providers/shell_response.rb +2 -2
  39. data/lib/brute/providers.rb +62 -0
  40. data/lib/brute/queue/base_queue.rb +222 -0
  41. data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
  42. data/lib/brute/queue/parallel_queue.rb +66 -0
  43. data/lib/brute/queue/sequential_queue.rb +63 -0
  44. data/lib/brute/store/message_store.rb +362 -0
  45. data/lib/brute/store/session.rb +106 -0
  46. data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
  47. data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
  48. data/lib/brute/system_prompt.rb +81 -194
  49. data/lib/brute/tools/delegate.rb +46 -116
  50. data/lib/brute/tools/fs_patch.rb +36 -37
  51. data/lib/brute/tools/fs_remove.rb +2 -2
  52. data/lib/brute/tools/fs_undo.rb +2 -2
  53. data/lib/brute/tools/fs_write.rb +29 -41
  54. data/lib/brute/tools/todo_read.rb +1 -1
  55. data/lib/brute/tools/todo_write.rb +1 -1
  56. data/lib/brute/tools.rb +31 -0
  57. data/lib/brute/version.rb +1 -1
  58. data/lib/brute.rb +40 -204
  59. metadata +31 -20
  60. data/lib/brute/agent_stream.rb +0 -181
  61. data/lib/brute/hooks.rb +0 -84
  62. data/lib/brute/message_store.rb +0 -463
  63. data/lib/brute/orchestrator.rb +0 -550
  64. data/lib/brute/session.rb +0 -161
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87cc119e0dc26d2499af1ceab1fe26c0ea2a6b9685c2acb9718a29d155959971
4
- data.tar.gz: 7887f2d6d2a3680cf660c93bbd64999f45e28afcf1f86fbd4fe52605fc0fae28
3
+ metadata.gz: a3594fba62fc4a71baaaf36b878eb1a3c02a3c06f7b0b2517b434556468bcde5
4
+ data.tar.gz: d8f74e82c95d7698c11ecbe5792f57ace4739a81f4d625d3cb729123eb0b7179
5
5
  SHA512:
6
- metadata.gz: 4b04baad572cd024f4e7cf0b6b42fb3b8794e773918e3d4ee21d751ec0a296e4047903fdf290c68dc54aa9faf0556363b216c3e8f1eb23ce11c6e646bd7f14ca
7
- data.tar.gz: c719e091120b55f5f0f93149c0502dc39cfdb984ae141a71260d1f1a8116824a6718e63639a092726bc83fe199c65b2cf1f98412c82991b13e06d297b488f092
6
+ metadata.gz: 861ab5262a21c876fa6592d1fc22612c39aada6e33a30e35ce81adb1bbbdfa978b9dab7b31ce7d653bf0f8e8ed09256a37d3d50bbe0024b889c5583e1fb690b6
7
+ data.tar.gz: '082b158a7deec18b8ba1fededb03e7b1c08d7e744b03da7840d9e4af4234bb86b89d9266a560cc81e9d13a7f3a4bbca4f831a343fbaa18052acc3381075b4b8e'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ class Agent
5
+ attr_reader :provider, :model, :tools, :system_prompt
6
+
7
+ def initialize(provider:, model:, tools: Brute::Tools::ALL, system_prompt: nil)
8
+ @provider = provider
9
+ @model = model
10
+ @tools = tools
11
+ @system_prompt = system_prompt
12
+ end
13
+ end
14
+ end
data/lib/brute/diff.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
3
5
  require 'diff/lcs'
4
6
  require 'diff/lcs/hunk'
5
7
 
@@ -25,36 +27,24 @@ module Brute
25
27
  end
26
28
  end
27
29
 
28
- if __FILE__ == $0
29
- require_relative "../../spec/spec_helper"
30
-
31
- RSpec.describe Brute::Diff do
32
- describe ".unified" do
33
- it "generates a unified diff for changed content" do
34
- old = "line1\nold\nline3\n"
35
- new_text = "line1\nnew\nline3\n"
36
- diff = described_class.unified(old, new_text)
37
- expect(diff).to include("-old")
38
- expect(diff).to include("+new")
39
- expect(diff).to include("@@")
40
- end
30
+ test do
31
+ it "generates a unified diff for changed content" do
32
+ Brute::Diff.unified("line1\nold\nline3\n", "line1\nnew\nline3\n").should =~ /\-old/
33
+ end
41
34
 
42
- it "returns empty string for identical content" do
43
- text = "same\ncontent\n"
44
- expect(described_class.unified(text, text)).to eq("")
45
- end
35
+ it "includes additions in diff" do
36
+ Brute::Diff.unified("line1\nold\nline3\n", "line1\nnew\nline3\n").should =~ /\+new/
37
+ end
46
38
 
47
- it "handles empty old content (new file)" do
48
- diff = described_class.unified("", "new\ncontent\n")
49
- expect(diff).to include("+new")
50
- expect(diff).to include("+content")
51
- end
39
+ it "returns empty string for identical content" do
40
+ Brute::Diff.unified("same\ncontent\n", "same\ncontent\n").should == ""
41
+ end
52
42
 
53
- it "handles empty new content (deleted file)" do
54
- diff = described_class.unified("old\ncontent\n", "")
55
- expect(diff).to include("-old")
56
- expect(diff).to include("-content")
57
- end
58
- end
43
+ it "handles empty old content (new file)" do
44
+ Brute::Diff.unified("", "new\ncontent\n").should =~ /\+new/
45
+ end
46
+
47
+ it "handles empty new content (deleted file)" do
48
+ Brute::Diff.unified("old\ncontent\n", "").should =~ /\-old/
59
49
  end
60
50
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "brute"
5
+
6
+ module Brute
7
+ module Loop
8
+ # Bridges llm.rb's streaming callbacks to the host application.
9
+ #
10
+ # Text and reasoning chunks fire immediately as the LLM generates them.
11
+ # Tool calls are collected but NOT executed — execution is deferred to the
12
+ # agent loop after the stream completes. This ensures text is never
13
+ # concurrent with tool execution.
14
+ #
15
+ # After the stream finishes, the agent loop reads +pending_tools+ to
16
+ # dispatch all tool calls concurrently, then fires +on_tool_call_start+
17
+ # once with the full batch.
18
+ #
19
+ class AgentStream < LLM::Stream
20
+ # Tool call metadata recorded during streaming, used by ToolUseGuard
21
+ # when ctx.functions is empty (nil-choice bug in llm.rb).
22
+ attr_reader :pending_tool_calls
23
+
24
+ # Deferred tool/error pairs: [(LLM::Function, error_or_nil), ...]
25
+ # The agent loop reads these after the stream completes.
26
+ attr_reader :pending_tools
27
+
28
+ def initialize(on_content: nil, on_reasoning: nil, on_question: nil)
29
+ @on_content = on_content
30
+ @on_reasoning = on_reasoning
31
+ @on_question = on_question
32
+ @pending_tool_calls = []
33
+ @pending_tools = []
34
+ end
35
+
36
+ # The on_question callback, needed by the agent loop to set
37
+ # thread/fiber-locals before tool execution.
38
+ attr_reader :on_question
39
+
40
+ def on_content(text)
41
+ @on_content&.call(text)
42
+ end
43
+
44
+ def on_reasoning_content(text)
45
+ @on_reasoning&.call(text)
46
+ end
47
+
48
+ # Called by llm.rb per tool as it arrives during streaming.
49
+ # Records only — no execution, no threads, no queue pushes.
50
+ def on_tool_call(tool, error)
51
+ @pending_tool_calls << { id: tool.id, name: tool.name, arguments: tool.arguments }
52
+ @pending_tools << [tool, error]
53
+ end
54
+
55
+ # Clear only the tool call metadata (used by ToolUseGuard after it
56
+ # has consumed the data for synthetic message injection).
57
+ def clear_pending_tool_calls!
58
+ @pending_tool_calls.clear
59
+ end
60
+
61
+ # Clear the deferred execution queue after the agent loop has
62
+ # consumed and dispatched all tool calls.
63
+ def clear_pending_tools!
64
+ @pending_tools.clear
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ test do
71
+ FakeTool = Struct.new(:id, :name, :arguments, keyword_init: true)
72
+
73
+ it "records tool in pending_tools" do
74
+ stream = Brute::Loop::AgentStream.new
75
+ tool = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
76
+ stream.on_tool_call(tool, nil)
77
+ stream.pending_tools.size.should == 1
78
+ end
79
+
80
+ it "records tool call metadata" do
81
+ stream = Brute::Loop::AgentStream.new
82
+ tool = FakeTool.new(id: "toolu_abc", name: "read", arguments: { "file_path" => "test.rb" })
83
+ stream.on_tool_call(tool, nil)
84
+ stream.pending_tool_calls.first[:id].should == "toolu_abc"
85
+ end
86
+
87
+ it "records multiple tool calls" do
88
+ stream = Brute::Loop::AgentStream.new
89
+ t1 = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
90
+ t2 = FakeTool.new(id: "toolu_2", name: "write", arguments: {})
91
+ stream.on_tool_call(t1, nil)
92
+ stream.on_tool_call(t2, nil)
93
+ stream.pending_tool_calls.size.should == 2
94
+ end
95
+
96
+ it "clears pending tool calls and tools" do
97
+ stream = Brute::Loop::AgentStream.new
98
+ tool = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
99
+ stream.on_tool_call(tool, nil)
100
+ stream.clear_pending_tool_calls!
101
+ stream.clear_pending_tools!
102
+ stream.pending_tool_calls.should.be.empty
103
+ end
104
+
105
+ it "fires the content callback" do
106
+ received = nil
107
+ stream = Brute::Loop::AgentStream.new(on_content: ->(text) { received = text })
108
+ stream.on_content("hello")
109
+ received.should == "hello"
110
+ end
111
+
112
+ it "fires the reasoning callback" do
113
+ received = nil
114
+ stream = Brute::Loop::AgentStream.new(on_reasoning: ->(text) { received = text })
115
+ stream.on_reasoning_content("thinking...")
116
+ received.should == "thinking..."
117
+ end
118
+ end