brute 0.4.0 → 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.
- checksums.yaml +4 -4
- data/lib/brute/agent.rb +14 -0
- data/lib/brute/diff.rb +24 -0
- 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 +70 -23
- data/lib/brute/middleware/doom_loop_detection.rb +110 -7
- data/lib/brute/middleware/llm_call.rb +88 -1
- data/lib/brute/middleware/message_tracking.rb +140 -10
- data/lib/brute/middleware/otel/span.rb +32 -2
- data/lib/brute/middleware/otel/token_usage.rb +38 -0
- data/lib/brute/middleware/otel/tool_calls.rb +30 -1
- data/lib/brute/middleware/otel/tool_results.rb +29 -1
- data/lib/brute/middleware/otel.rb +5 -0
- data/lib/brute/middleware/reasoning_normalizer.rb +94 -0
- data/lib/brute/middleware/retry.rb +113 -1
- data/lib/brute/middleware/session_persistence.rb +46 -3
- data/lib/brute/middleware/token_tracking.rb +78 -0
- data/lib/brute/middleware/tool_error_tracking.rb +128 -1
- data/lib/brute/middleware/tool_use_guard.rb +64 -28
- data/lib/brute/middleware/tracing.rb +63 -2
- data/lib/brute/middleware.rb +18 -0
- data/lib/brute/orchestrator/turn.rb +105 -0
- data/lib/brute/patches/buffer_nil_guard.rb +5 -0
- data/lib/brute/pipeline.rb +86 -7
- data/lib/brute/prompts/build_switch.rb +29 -0
- data/lib/brute/prompts/environment.rb +43 -0
- data/lib/brute/prompts/identity.rb +29 -0
- data/lib/brute/prompts/instructions.rb +21 -0
- data/lib/brute/prompts/max_steps.rb +25 -0
- data/lib/brute/prompts/plan_reminder.rb +25 -0
- data/lib/brute/prompts/skills.rb +13 -0
- data/lib/brute/prompts.rb +28 -0
- data/lib/brute/providers/ollama.rb +135 -0
- data/lib/brute/providers/opencode_go.rb +5 -0
- data/lib/brute/providers/opencode_zen.rb +7 -2
- data/lib/brute/providers/shell.rb +2 -2
- data/lib/brute/providers/shell_response.rb +7 -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/{message_store.rb → store/message_store.rb} +155 -62
- 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 +101 -0
- data/lib/brute/tools/delegate.rb +59 -0
- data/lib/brute/tools/fs_patch.rb +54 -2
- data/lib/brute/tools/fs_read.rb +5 -0
- data/lib/brute/tools/fs_remove.rb +7 -2
- data/lib/brute/tools/fs_search.rb +5 -0
- data/lib/brute/tools/fs_undo.rb +7 -2
- data/lib/brute/tools/fs_write.rb +40 -2
- data/lib/brute/tools/net_fetch.rb +5 -0
- data/lib/brute/tools/question.rb +5 -0
- data/lib/brute/tools/shell.rb +5 -0
- data/lib/brute/tools/todo_read.rb +6 -1
- data/lib/brute/tools/todo_write.rb +6 -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 -63
- data/lib/brute/hooks.rb +0 -84
- data/lib/brute/orchestrator.rb +0 -391
- data/lib/brute/session.rb +0 -161
data/lib/brute/tools/fs_patch.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
5
|
+
|
|
3
6
|
module Brute
|
|
4
7
|
module Tools
|
|
5
8
|
class FSPatch < LLM::Tool
|
|
@@ -14,13 +17,13 @@ module Brute
|
|
|
14
17
|
|
|
15
18
|
def call(file_path:, old_string:, new_string:, replace_all: false)
|
|
16
19
|
path = File.expand_path(file_path)
|
|
17
|
-
Brute::FileMutationQueue.serialize(path) do
|
|
20
|
+
Brute::Queue::FileMutationQueue.serialize(path) do
|
|
18
21
|
raise "File not found: #{path}" unless File.exist?(path)
|
|
19
22
|
|
|
20
23
|
original = File.read(path)
|
|
21
24
|
raise "old_string not found in #{path}" unless original.include?(old_string)
|
|
22
25
|
|
|
23
|
-
Brute::SnapshotStore.save(path)
|
|
26
|
+
Brute::Store::SnapshotStore.save(path)
|
|
24
27
|
|
|
25
28
|
updated = if replace_all
|
|
26
29
|
original.gsub(old_string, new_string)
|
|
@@ -37,3 +40,52 @@ module Brute
|
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
|
43
|
+
|
|
44
|
+
test do
|
|
45
|
+
require "tmpdir"
|
|
46
|
+
|
|
47
|
+
it "replaces old_string with new_string" do
|
|
48
|
+
Dir.mktmpdir do |dir|
|
|
49
|
+
path = File.join(dir, "test.rb")
|
|
50
|
+
File.write(path, "hello world\n")
|
|
51
|
+
result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "world", new_string: "ruby")
|
|
52
|
+
File.read(path).should == "hello ruby\n"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "returns a unified diff" do
|
|
57
|
+
Dir.mktmpdir do |dir|
|
|
58
|
+
path = File.join(dir, "test.rb")
|
|
59
|
+
File.write(path, "line1\nold line\nline3\n")
|
|
60
|
+
result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "old line", new_string: "new line")
|
|
61
|
+
result[:diff].should =~ /\-old line/
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "raises when file not found" do
|
|
66
|
+
Dir.mktmpdir do |dir|
|
|
67
|
+
lambda {
|
|
68
|
+
Brute::Tools::FSPatch.new.call(file_path: File.join(dir, "nope.rb"), old_string: "a", new_string: "b")
|
|
69
|
+
}.should.raise(RuntimeError)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "raises when old_string not found" do
|
|
74
|
+
Dir.mktmpdir do |dir|
|
|
75
|
+
path = File.join(dir, "test.rb")
|
|
76
|
+
File.write(path, "hello\n")
|
|
77
|
+
lambda {
|
|
78
|
+
Brute::Tools::FSPatch.new.call(file_path: path, old_string: "missing", new_string: "new")
|
|
79
|
+
}.should.raise(RuntimeError)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "supports replace_all" do
|
|
84
|
+
Dir.mktmpdir do |dir|
|
|
85
|
+
path = File.join(dir, "test.rb")
|
|
86
|
+
File.write(path, "aaa bbb aaa\n")
|
|
87
|
+
result = Brute::Tools::FSPatch.new.call(file_path: path, old_string: "aaa", new_string: "ccc", replace_all: true)
|
|
88
|
+
result[:replacements].should == 2
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/brute/tools/fs_read.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
if __FILE__ == $0
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "brute"
|
|
6
|
+
end
|
|
7
|
+
|
|
3
8
|
require "fileutils"
|
|
4
9
|
|
|
5
10
|
module Brute
|
|
@@ -12,10 +17,10 @@ module Brute
|
|
|
12
17
|
|
|
13
18
|
def call(path:)
|
|
14
19
|
target = File.expand_path(path)
|
|
15
|
-
Brute::FileMutationQueue.serialize(target) do
|
|
20
|
+
Brute::Queue::FileMutationQueue.serialize(target) do
|
|
16
21
|
raise "Path not found: #{target}" unless File.exist?(target)
|
|
17
22
|
|
|
18
|
-
Brute::SnapshotStore.save(target) if File.file?(target)
|
|
23
|
+
Brute::Store::SnapshotStore.save(target) if File.file?(target)
|
|
19
24
|
|
|
20
25
|
if File.directory?(target)
|
|
21
26
|
Dir.rmdir(target)
|
data/lib/brute/tools/fs_undo.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
if __FILE__ == $0
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "brute"
|
|
6
|
+
end
|
|
7
|
+
|
|
3
8
|
module Brute
|
|
4
9
|
module Tools
|
|
5
10
|
class FSUndo < LLM::Tool
|
|
@@ -11,8 +16,8 @@ module Brute
|
|
|
11
16
|
|
|
12
17
|
def call(path:)
|
|
13
18
|
target = File.expand_path(path)
|
|
14
|
-
Brute::FileMutationQueue.serialize(target) do
|
|
15
|
-
snapshot = Brute::SnapshotStore.pop(target)
|
|
19
|
+
Brute::Queue::FileMutationQueue.serialize(target) do
|
|
20
|
+
snapshot = Brute::Store::SnapshotStore.pop(target)
|
|
16
21
|
raise "No undo history available for: #{target}" unless snapshot
|
|
17
22
|
|
|
18
23
|
if snapshot == :did_not_exist
|
data/lib/brute/tools/fs_write.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
3
5
|
require 'fileutils'
|
|
4
6
|
|
|
5
7
|
module Brute
|
|
@@ -14,9 +16,9 @@ module Brute
|
|
|
14
16
|
|
|
15
17
|
def call(file_path:, content:)
|
|
16
18
|
path = File.expand_path(file_path)
|
|
17
|
-
Brute::FileMutationQueue.serialize(path) do
|
|
19
|
+
Brute::Queue::FileMutationQueue.serialize(path) do
|
|
18
20
|
old_content = File.exist?(path) ? File.read(path) : ''
|
|
19
|
-
Brute::SnapshotStore.save(path)
|
|
21
|
+
Brute::Store::SnapshotStore.save(path)
|
|
20
22
|
FileUtils.mkdir_p(File.dirname(path))
|
|
21
23
|
File.write(path, content)
|
|
22
24
|
diff = Brute::Diff.unified(old_content, content)
|
|
@@ -26,3 +28,39 @@ module Brute
|
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
31
|
+
|
|
32
|
+
test do
|
|
33
|
+
require "tmpdir"
|
|
34
|
+
|
|
35
|
+
it "writes content to a new file" do
|
|
36
|
+
Dir.mktmpdir do |dir|
|
|
37
|
+
path = File.join(dir, "new.rb")
|
|
38
|
+
Brute::Tools::FSWrite.new.call(file_path: path, content: "hello\n")
|
|
39
|
+
File.read(path).should == "hello\n"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "returns a diff for new files" do
|
|
44
|
+
Dir.mktmpdir do |dir|
|
|
45
|
+
path = File.join(dir, "new.rb")
|
|
46
|
+
result = Brute::Tools::FSWrite.new.call(file_path: path, content: "line1\nline2\n")
|
|
47
|
+
result[:diff].should =~ /\+line1/
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "creates parent directories" do
|
|
52
|
+
Dir.mktmpdir do |dir|
|
|
53
|
+
path = File.join(dir, "deep", "nested", "file.rb")
|
|
54
|
+
result = Brute::Tools::FSWrite.new.call(file_path: path, content: "nested\n")
|
|
55
|
+
result[:success].should.be.true
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns byte count" do
|
|
60
|
+
Dir.mktmpdir do |dir|
|
|
61
|
+
path = File.join(dir, "test.rb")
|
|
62
|
+
result = Brute::Tools::FSWrite.new.call(file_path: path, content: "hello")
|
|
63
|
+
result[:bytes].should == 5
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/brute/tools/question.rb
CHANGED
data/lib/brute/tools/shell.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
if __FILE__ == $0
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "brute"
|
|
6
|
+
end
|
|
7
|
+
|
|
3
8
|
module Brute
|
|
4
9
|
module Tools
|
|
5
10
|
class TodoRead < LLM::Tool
|
|
@@ -8,7 +13,7 @@ module Brute
|
|
|
8
13
|
param :_placeholder, String, "Unused, pass any value"
|
|
9
14
|
|
|
10
15
|
def call(_placeholder: nil)
|
|
11
|
-
{todos: Brute::TodoStore.all}
|
|
16
|
+
{todos: Brute::Store::TodoStore.all}
|
|
12
17
|
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
if __FILE__ == $0
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "brute"
|
|
6
|
+
end
|
|
7
|
+
|
|
3
8
|
module Brute
|
|
4
9
|
module Tools
|
|
5
10
|
class TodoWrite < LLM::Tool
|
|
@@ -24,7 +29,7 @@ module Brute
|
|
|
24
29
|
t = t.transform_keys(&:to_sym) if t.is_a?(Hash)
|
|
25
30
|
{id: t[:id], content: t[:content], status: t[:status]}
|
|
26
31
|
end
|
|
27
|
-
Brute::TodoStore.replace(items)
|
|
32
|
+
Brute::Store::TodoStore.replace(items)
|
|
28
33
|
{success: true, count: items.size}
|
|
29
34
|
end
|
|
30
35
|
end
|
data/lib/brute/tools.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative 'tools/fs_read'
|
|
2
|
+
require_relative 'tools/fs_write'
|
|
3
|
+
require_relative 'tools/fs_patch'
|
|
4
|
+
require_relative 'tools/fs_remove'
|
|
5
|
+
require_relative 'tools/fs_search'
|
|
6
|
+
require_relative 'tools/fs_undo'
|
|
7
|
+
require_relative 'tools/shell'
|
|
8
|
+
require_relative 'tools/net_fetch'
|
|
9
|
+
require_relative 'tools/todo_write'
|
|
10
|
+
require_relative 'tools/todo_read'
|
|
11
|
+
require_relative 'tools/delegate'
|
|
12
|
+
require_relative 'tools/question'
|
|
13
|
+
|
|
14
|
+
module Brute
|
|
15
|
+
module Tools
|
|
16
|
+
ALL = [
|
|
17
|
+
Tools::FSRead,
|
|
18
|
+
Tools::FSWrite,
|
|
19
|
+
Tools::FSPatch,
|
|
20
|
+
Tools::FSRemove,
|
|
21
|
+
Tools::FSSearch,
|
|
22
|
+
Tools::FSUndo,
|
|
23
|
+
Tools::Shell,
|
|
24
|
+
Tools::NetFetch,
|
|
25
|
+
Tools::TodoWrite,
|
|
26
|
+
Tools::TodoRead,
|
|
27
|
+
Tools::Delegate,
|
|
28
|
+
Tools::Question
|
|
29
|
+
].freeze
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'llm'
|
|
4
4
|
require 'timeout'
|
|
5
5
|
require 'logger'
|
|
6
|
+
require 'scampi/kernel_ext'
|
|
6
7
|
|
|
7
8
|
# Brute — a coding agent built on llm.rb
|
|
8
9
|
#
|
|
@@ -11,220 +12,55 @@ require 'logger'
|
|
|
11
12
|
#
|
|
12
13
|
# Tracing → Retry → Session → Tokens → Compaction → ToolErrors → DoomLoop → Reasoning → [LLM Call]
|
|
13
14
|
#
|
|
15
|
+
# Entry point:
|
|
16
|
+
#
|
|
17
|
+
# agent = Brute::Agent.new(provider:, model:, tools:, system_prompt:)
|
|
18
|
+
# step = Brute::Loop::AgentTurn.perform(agent:, session:, pipeline:, input:)
|
|
19
|
+
#
|
|
14
20
|
require_relative 'brute/version'
|
|
15
21
|
|
|
16
22
|
module Brute
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
def self.provider
|
|
24
|
+
@provider ||= Brute::Providers.guess_from_env
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.provider=(p)
|
|
28
|
+
@provider = p
|
|
29
|
+
end
|
|
20
30
|
end
|
|
21
31
|
|
|
22
|
-
# Infrastructure
|
|
23
32
|
require_relative 'brute/diff'
|
|
24
|
-
require_relative 'brute/snapshot_store'
|
|
25
|
-
require_relative 'brute/todo_store'
|
|
26
|
-
require_relative 'brute/file_mutation_queue'
|
|
27
|
-
require_relative 'brute/doom_loop'
|
|
28
|
-
require_relative 'brute/hooks'
|
|
29
|
-
require_relative 'brute/compactor'
|
|
30
|
-
require_relative 'brute/prompts/base'
|
|
31
|
-
require_relative 'brute/prompts/identity'
|
|
32
|
-
require_relative 'brute/prompts/tone_and_style'
|
|
33
|
-
require_relative 'brute/prompts/objectivity'
|
|
34
|
-
require_relative 'brute/prompts/task_management'
|
|
35
|
-
require_relative 'brute/prompts/doing_tasks'
|
|
36
|
-
require_relative 'brute/prompts/tool_usage'
|
|
37
|
-
require_relative 'brute/prompts/conventions'
|
|
38
|
-
require_relative 'brute/prompts/git_safety'
|
|
39
|
-
require_relative 'brute/prompts/code_references'
|
|
40
|
-
require_relative 'brute/prompts/environment'
|
|
41
|
-
require_relative 'brute/prompts/instructions'
|
|
42
|
-
require_relative 'brute/prompts/editing_approach'
|
|
43
|
-
require_relative 'brute/prompts/autonomy'
|
|
44
|
-
require_relative 'brute/prompts/editing_constraints'
|
|
45
|
-
require_relative 'brute/prompts/frontend_tasks'
|
|
46
|
-
require_relative 'brute/prompts/proactiveness'
|
|
47
|
-
require_relative 'brute/prompts/code_style'
|
|
48
|
-
require_relative 'brute/prompts/security_and_safety'
|
|
49
|
-
require_relative 'brute/prompts/skills'
|
|
50
|
-
require_relative 'brute/prompts/plan_reminder'
|
|
51
|
-
require_relative 'brute/prompts/max_steps'
|
|
52
|
-
require_relative 'brute/prompts/build_switch'
|
|
53
33
|
require_relative 'brute/skill'
|
|
34
|
+
require_relative 'brute/prompts'
|
|
54
35
|
require_relative 'brute/system_prompt'
|
|
55
|
-
require_relative 'brute/message_store'
|
|
56
|
-
require_relative 'brute/session'
|
|
57
36
|
require_relative 'brute/pipeline'
|
|
58
|
-
require_relative 'brute/
|
|
37
|
+
require_relative 'brute/agent'
|
|
38
|
+
|
|
39
|
+
# Brute::Store
|
|
40
|
+
require_relative 'brute/store/snapshot_store'
|
|
41
|
+
require_relative 'brute/store/todo_store'
|
|
42
|
+
require_relative 'brute/store/message_store'
|
|
43
|
+
require_relative 'brute/store/session'
|
|
44
|
+
|
|
45
|
+
# Brute::Loop (before Queue — queue tests reference Loop::Step)
|
|
46
|
+
require_relative 'brute/loop/doom_loop'
|
|
47
|
+
require_relative 'brute/loop/compactor'
|
|
48
|
+
require_relative 'brute/loop/agent_stream'
|
|
49
|
+
require_relative 'brute/loop/step'
|
|
50
|
+
require_relative 'brute/loop/tool_call_step'
|
|
51
|
+
|
|
52
|
+
# Brute::Queue
|
|
53
|
+
require_relative 'brute/queue/file_mutation_queue'
|
|
54
|
+
require_relative 'brute/queue/base_queue'
|
|
55
|
+
require_relative 'brute/queue/sequential_queue'
|
|
56
|
+
require_relative 'brute/queue/parallel_queue'
|
|
57
|
+
|
|
58
|
+
# Brute::Loop (agent_turn depends on Queue)
|
|
59
|
+
require_relative 'brute/loop/agent_turn'
|
|
59
60
|
|
|
60
|
-
# Provider patches
|
|
61
61
|
require_relative 'brute/patches/anthropic_tool_role'
|
|
62
62
|
require_relative 'brute/patches/buffer_nil_guard'
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
require_relative 'brute/
|
|
66
|
-
require_relative 'brute/
|
|
67
|
-
require_relative 'brute/middleware/retry'
|
|
68
|
-
require_relative 'brute/middleware/doom_loop_detection'
|
|
69
|
-
require_relative 'brute/middleware/token_tracking'
|
|
70
|
-
require_relative 'brute/middleware/compaction_check'
|
|
71
|
-
require_relative 'brute/middleware/session_persistence'
|
|
72
|
-
require_relative 'brute/middleware/message_tracking'
|
|
73
|
-
require_relative 'brute/middleware/tracing'
|
|
74
|
-
require_relative 'brute/middleware/tool_error_tracking'
|
|
75
|
-
require_relative 'brute/middleware/reasoning_normalizer'
|
|
76
|
-
require_relative "brute/middleware/tool_use_guard"
|
|
77
|
-
require_relative "brute/middleware/otel"
|
|
78
|
-
|
|
79
|
-
# Tools
|
|
80
|
-
require_relative 'brute/tools/fs_read'
|
|
81
|
-
require_relative 'brute/tools/fs_write'
|
|
82
|
-
require_relative 'brute/tools/fs_patch'
|
|
83
|
-
require_relative 'brute/tools/fs_remove'
|
|
84
|
-
require_relative 'brute/tools/fs_search'
|
|
85
|
-
require_relative 'brute/tools/fs_undo'
|
|
86
|
-
require_relative 'brute/tools/shell'
|
|
87
|
-
require_relative 'brute/tools/net_fetch'
|
|
88
|
-
require_relative 'brute/tools/todo_write'
|
|
89
|
-
require_relative 'brute/tools/todo_read'
|
|
90
|
-
require_relative 'brute/tools/delegate'
|
|
91
|
-
require_relative 'brute/tools/question'
|
|
92
|
-
|
|
93
|
-
# Providers
|
|
94
|
-
require_relative 'brute/providers/shell_response'
|
|
95
|
-
require_relative 'brute/providers/shell'
|
|
96
|
-
require_relative 'brute/providers/models_dev'
|
|
97
|
-
require_relative 'brute/providers/opencode_zen'
|
|
98
|
-
require_relative 'brute/providers/opencode_go'
|
|
99
|
-
|
|
100
|
-
# Orchestrator (depends on tools, middleware, and infrastructure)
|
|
101
|
-
require_relative 'brute/orchestrator'
|
|
102
|
-
|
|
103
|
-
module Brute
|
|
104
|
-
# The complete set of tools available to the agent.
|
|
105
|
-
TOOLS = [
|
|
106
|
-
Tools::FSRead,
|
|
107
|
-
Tools::FSWrite,
|
|
108
|
-
Tools::FSPatch,
|
|
109
|
-
Tools::FSRemove,
|
|
110
|
-
Tools::FSSearch,
|
|
111
|
-
Tools::FSUndo,
|
|
112
|
-
Tools::Shell,
|
|
113
|
-
Tools::NetFetch,
|
|
114
|
-
Tools::TodoWrite,
|
|
115
|
-
Tools::TodoRead,
|
|
116
|
-
Tools::Delegate,
|
|
117
|
-
Tools::Question
|
|
118
|
-
].freeze
|
|
119
|
-
|
|
120
|
-
# Default provider, resolved from environment.
|
|
121
|
-
def self.provider
|
|
122
|
-
@provider ||= resolve_provider
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def self.provider=(p)
|
|
126
|
-
@provider = p
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Create a new orchestrator with sensible defaults.
|
|
130
|
-
def self.agent(cwd: Dir.pwd, model: nil, tools: TOOLS, session: nil, reasoning: {}, agent_name: nil, **callbacks)
|
|
131
|
-
Orchestrator.new(
|
|
132
|
-
provider: provider,
|
|
133
|
-
model: model,
|
|
134
|
-
tools: tools,
|
|
135
|
-
cwd: cwd,
|
|
136
|
-
session: session,
|
|
137
|
-
reasoning: reasoning,
|
|
138
|
-
agent_name: agent_name,
|
|
139
|
-
**callbacks
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
PROVIDERS = {
|
|
144
|
-
'anthropic' => ->(key) { LLM.anthropic(key: key).tap { Patches::AnthropicToolRole.apply! } },
|
|
145
|
-
'openai' => ->(key) { LLM.openai(key: key) },
|
|
146
|
-
'google' => ->(key) { LLM.google(key: key) },
|
|
147
|
-
'deepseek' => ->(key) { LLM.deepseek(key: key) },
|
|
148
|
-
'ollama' => ->(key) { LLM.ollama(key: key) },
|
|
149
|
-
'xai' => ->(key) { LLM.xai(key: key) },
|
|
150
|
-
'opencode_zen' => ->(key) { LLM::OpencodeZen.new(key: key) },
|
|
151
|
-
'opencode_go' => ->(key) { LLM::OpencodeGo.new(key: key) },
|
|
152
|
-
'shell' => ->(_key) { Providers::Shell.new },
|
|
153
|
-
}.freeze
|
|
154
|
-
|
|
155
|
-
# List provider names that have API keys configured in the environment.
|
|
156
|
-
# The shell provider is always available (no key needed).
|
|
157
|
-
def self.configured_providers
|
|
158
|
-
PROVIDERS.keys.select { |name| api_key_for(name) }
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Build a provider instance for the given name using available API keys.
|
|
162
|
-
# Returns nil if no key is found.
|
|
163
|
-
def self.provider_for(name)
|
|
164
|
-
key = api_key_for(name)
|
|
165
|
-
return nil unless key
|
|
166
|
-
|
|
167
|
-
factory = PROVIDERS[name]
|
|
168
|
-
return nil unless factory
|
|
169
|
-
|
|
170
|
-
factory.call(key)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Look up the API key for a given provider name.
|
|
174
|
-
def self.api_key_for(name)
|
|
175
|
-
# Shell provider needs no key.
|
|
176
|
-
return "none" if name == "shell"
|
|
177
|
-
|
|
178
|
-
# OpenCode providers: check OPENCODE_API_KEY, fall back to "public" for anonymous access.
|
|
179
|
-
if name == "opencode_zen" || name == "opencode_go"
|
|
180
|
-
return ENV["OPENCODE_API_KEY"] || "public"
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Explicit generic key always works
|
|
184
|
-
return ENV["LLM_API_KEY"] if ENV["LLM_API_KEY"]
|
|
185
|
-
|
|
186
|
-
case name
|
|
187
|
-
when "anthropic" then ENV["ANTHROPIC_API_KEY"]
|
|
188
|
-
when "openai" then ENV["OPENAI_API_KEY"]
|
|
189
|
-
when "google" then ENV["GOOGLE_API_KEY"]
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Resolve the LLM provider from environment variables.
|
|
194
|
-
#
|
|
195
|
-
# Checks in order:
|
|
196
|
-
# 1. LLM_API_KEY + LLM_PROVIDER (explicit)
|
|
197
|
-
# 2. ANTHROPIC_API_KEY (implicit: provider = anthropic)
|
|
198
|
-
# 3. OPENAI_API_KEY (implicit: provider = openai)
|
|
199
|
-
# 4. GOOGLE_API_KEY (implicit: provider = google)
|
|
200
|
-
# 5. OPENCODE_API_KEY (implicit: provider = opencode_zen)
|
|
201
|
-
#
|
|
202
|
-
# Returns nil if no key is found. Error is deferred to Orchestrator#run.
|
|
203
|
-
def self.resolve_provider
|
|
204
|
-
if ENV['LLM_API_KEY']
|
|
205
|
-
key = ENV['LLM_API_KEY']
|
|
206
|
-
name = ENV.fetch('LLM_PROVIDER', 'anthropic').downcase
|
|
207
|
-
elsif ENV['ANTHROPIC_API_KEY']
|
|
208
|
-
key = ENV['ANTHROPIC_API_KEY']
|
|
209
|
-
name = 'anthropic'
|
|
210
|
-
elsif ENV['OPENAI_API_KEY']
|
|
211
|
-
key = ENV['OPENAI_API_KEY']
|
|
212
|
-
name = 'openai'
|
|
213
|
-
elsif ENV['GOOGLE_API_KEY']
|
|
214
|
-
key = ENV['GOOGLE_API_KEY']
|
|
215
|
-
name = 'google'
|
|
216
|
-
elsif ENV['OPENCODE_API_KEY']
|
|
217
|
-
key = ENV['OPENCODE_API_KEY']
|
|
218
|
-
name = 'opencode_zen'
|
|
219
|
-
else
|
|
220
|
-
return nil
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
factory = PROVIDERS[name]
|
|
224
|
-
raise "Unknown LLM provider: #{name}. Available: #{PROVIDERS.keys.join(', ')}" unless factory
|
|
225
|
-
|
|
226
|
-
factory.call(key)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
private_class_method :resolve_provider
|
|
230
|
-
end
|
|
64
|
+
require_relative 'brute/middleware'
|
|
65
|
+
require_relative 'brute/tools'
|
|
66
|
+
require_relative 'brute/providers'
|