brute 0.4.1 → 1.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.
- checksums.yaml +4 -4
- data/lib/brute/agent.rb +14 -0
- data/lib/brute/diff.rb +18 -28
- data/lib/brute/loop/agent_stream.rb +118 -0
- data/lib/brute/loop/agent_turn.rb +520 -0
- data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
- data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
- data/lib/brute/loop/step.rb +332 -0
- data/lib/brute/loop/tool_call_step.rb +90 -0
- data/lib/brute/middleware/compaction_check.rb +60 -146
- data/lib/brute/middleware/doom_loop_detection.rb +95 -92
- data/lib/brute/middleware/llm_call.rb +78 -80
- data/lib/brute/middleware/message_tracking.rb +115 -162
- data/lib/brute/middleware/otel/span.rb +25 -106
- data/lib/brute/middleware/otel/token_usage.rb +29 -84
- data/lib/brute/middleware/otel/tool_calls.rb +23 -107
- data/lib/brute/middleware/otel/tool_results.rb +22 -86
- data/lib/brute/middleware/reasoning_normalizer.rb +78 -103
- data/lib/brute/middleware/retry.rb +95 -76
- data/lib/brute/middleware/session_persistence.rb +38 -37
- data/lib/brute/middleware/token_tracking.rb +64 -63
- data/lib/brute/middleware/tool_error_tracking.rb +108 -82
- data/lib/brute/middleware/tool_use_guard.rb +57 -90
- data/lib/brute/middleware/tracing.rb +53 -63
- data/lib/brute/middleware.rb +18 -0
- data/lib/brute/orchestrator/turn.rb +105 -0
- data/lib/brute/pipeline.rb +77 -133
- data/lib/brute/prompts/build_switch.rb +21 -25
- data/lib/brute/prompts/environment.rb +31 -35
- data/lib/brute/prompts/identity.rb +22 -29
- data/lib/brute/prompts/instructions.rb +15 -18
- data/lib/brute/prompts/max_steps.rb +18 -25
- data/lib/brute/prompts/plan_reminder.rb +18 -26
- data/lib/brute/prompts/skills.rb +8 -30
- data/lib/brute/prompts.rb +28 -0
- data/lib/brute/providers/ollama.rb +135 -0
- data/lib/brute/providers/shell.rb +2 -2
- data/lib/brute/providers/shell_response.rb +2 -2
- data/lib/brute/providers.rb +62 -0
- data/lib/brute/queue/base_queue.rb +222 -0
- data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
- data/lib/brute/queue/parallel_queue.rb +66 -0
- data/lib/brute/queue/sequential_queue.rb +63 -0
- data/lib/brute/store/message_store.rb +362 -0
- data/lib/brute/store/session.rb +106 -0
- data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
- data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
- data/lib/brute/system_prompt.rb +81 -194
- data/lib/brute/tools/delegate.rb +46 -116
- data/lib/brute/tools/fs_patch.rb +36 -37
- data/lib/brute/tools/fs_remove.rb +2 -2
- data/lib/brute/tools/fs_undo.rb +2 -2
- data/lib/brute/tools/fs_write.rb +29 -41
- data/lib/brute/tools/todo_read.rb +1 -1
- data/lib/brute/tools/todo_write.rb +1 -1
- data/lib/brute/tools.rb +31 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +40 -204
- metadata +31 -20
- data/lib/brute/agent_stream.rb +0 -181
- data/lib/brute/hooks.rb +0 -84
- data/lib/brute/message_store.rb +0 -463
- data/lib/brute/orchestrator.rb +0 -550
- data/lib/brute/session.rb +0 -161
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ac58cb8a6e1a4e7d08ba8f9de933e03f0d9d0889b91aee1126962d1adb3c419
|
|
4
|
+
data.tar.gz: ff083ed2d8ba6485468d664238ab9e52782d03d7fed353ae3ce5456dc86f3c55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 628b77901e56dc1641350fdf3a55467ea66fa77354f3a01b8caa66f1a3c2c0df3a37b0a3f9e03e417746d132208e8cc7a34ed2925b9b842dca05f405d7dd8b4b
|
|
7
|
+
data.tar.gz: ab80c56ce002184c9bf04ae8a197f6695e053103a7443440ea2a8f659f6731253263b263be4c6c795a4e50c4cf10ea63f891ce11f184ca6444dfe2e1e60283d9
|
data/lib/brute/agent.rb
ADDED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|