brute 0.1.5 → 0.1.7
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/diff.rb +26 -0
- data/lib/brute/middleware/tool_error_tracking.rb +14 -8
- data/lib/brute/middleware/tool_use_guard.rb +54 -0
- data/lib/brute/middleware/tracing.rb +35 -6
- data/lib/brute/orchestrator.rb +3 -0
- data/lib/brute/tools/fs_patch.rb +14 -12
- data/lib/brute/tools/fs_write.rb +8 -6
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +60 -59
- metadata +30 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f0f32487b029541fdb462f5f4958a95e4727150911b51f6d8ab457b875187d8
|
|
4
|
+
data.tar.gz: f162d75e227b4270e4a56dba42fe3cdddd23f9492adeff5cd1f345cbbb811961
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9aa172f042960dc5c9ec3250cf27781614077be7db114edc65d4ce72e178dfcab9ed9a9918d6583ed190d36cc54e865d8118bea9ac9814064a12a3f2b7ac8627
|
|
7
|
+
data.tar.gz: c0cc5addf257b161cc06385417a24833bf9f99a73038040abc67c3caef75ab0a0b9e3d357364b0fa0d194ca3b259555b80f09bf805fc12d17be1e4386d8b061b
|
data/lib/brute/diff.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'diff/lcs'
|
|
4
|
+
require 'diff/lcs/hunk'
|
|
5
|
+
|
|
6
|
+
module Brute
|
|
7
|
+
module Diff
|
|
8
|
+
# Generate a unified diff string from two texts.
|
|
9
|
+
def self.unified(old_text, new_text, context: 3)
|
|
10
|
+
old_lines = old_text.lines
|
|
11
|
+
new_lines = new_text.lines
|
|
12
|
+
diffs = ::Diff::LCS.diff(old_lines, new_lines)
|
|
13
|
+
return '' if diffs.empty?
|
|
14
|
+
|
|
15
|
+
output = +''
|
|
16
|
+
file_length_difference = 0
|
|
17
|
+
diffs.each do |piece|
|
|
18
|
+
hunk = ::Diff::LCS::Hunk.new(old_lines, new_lines, piece, context, file_length_difference)
|
|
19
|
+
file_length_difference = hunk.file_length_difference
|
|
20
|
+
output << hunk.diff(:unified)
|
|
21
|
+
output << "\n"
|
|
22
|
+
end
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Brute
|
|
4
4
|
module Middleware
|
|
5
|
-
# Tracks per-tool error counts
|
|
6
|
-
# the error ceiling is reached.
|
|
5
|
+
# Tracks per-tool error counts and total tool call count across LLM
|
|
6
|
+
# calls, and signals when the error ceiling is reached.
|
|
7
7
|
#
|
|
8
8
|
# This middleware doesn't execute tools itself — it inspects the tool
|
|
9
9
|
# results that were sent as input to the LLM call (env[:tool_results])
|
|
10
|
-
# and counts failures.
|
|
10
|
+
# and counts failures and totals.
|
|
11
11
|
#
|
|
12
12
|
# When any tool exceeds max_failures, it sets env[:metadata][:tool_error_limit_reached]
|
|
13
13
|
# so the orchestrator can decide to stop.
|
|
14
14
|
#
|
|
15
|
+
# Also stores env[:metadata][:tool_calls] with the cumulative number of
|
|
16
|
+
# tool invocations in the current session.
|
|
17
|
+
#
|
|
15
18
|
class ToolErrorTracking < Base
|
|
16
19
|
DEFAULT_MAX_FAILURES = 3
|
|
17
20
|
|
|
@@ -19,27 +22,30 @@ module Brute
|
|
|
19
22
|
super(app)
|
|
20
23
|
@max_failures = max_failures
|
|
21
24
|
@errors = Hash.new(0) # tool_name → count
|
|
25
|
+
@total_tool_calls = 0
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def call(env)
|
|
25
|
-
# PRE: count errors from tool results that are about to be sent
|
|
29
|
+
# PRE: count errors and totals from tool results that are about to be sent
|
|
26
30
|
if (results = env[:tool_results])
|
|
31
|
+
@total_tool_calls += results.size
|
|
32
|
+
|
|
27
33
|
results.each do |name, result|
|
|
28
|
-
if result.is_a?(Hash) && result[:error]
|
|
29
|
-
@errors[name] += 1
|
|
30
|
-
end
|
|
34
|
+
@errors[name] += 1 if result.is_a?(Hash) && result[:error]
|
|
31
35
|
end
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
env[:metadata][:tool_calls] = @total_tool_calls
|
|
34
39
|
env[:metadata][:tool_errors] = @errors.dup
|
|
35
40
|
env[:metadata][:tool_error_limit_reached] = @errors.any? { |_, c| c >= @max_failures }
|
|
36
41
|
|
|
37
42
|
@app.call(env)
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
# Reset
|
|
45
|
+
# Reset counts (e.g., between user turns).
|
|
41
46
|
def reset!
|
|
42
47
|
@errors.clear
|
|
48
|
+
@total_tool_calls = 0
|
|
43
49
|
end
|
|
44
50
|
end
|
|
45
51
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Middleware
|
|
5
|
+
# Guards against tool-only LLM responses where the assistant message
|
|
6
|
+
# is dropped from the context buffer.
|
|
7
|
+
#
|
|
8
|
+
# When the LLM responds with only tool_use blocks (no text), llm.rb's
|
|
9
|
+
# response adapter produces empty choices. Context#talk appends nil,
|
|
10
|
+
# BufferNilGuard strips it, and the assistant message carrying tool_use
|
|
11
|
+
# blocks is lost. This causes "unexpected tool_use_id" on the next call
|
|
12
|
+
# because tool_result references a tool_use that's missing from the buffer.
|
|
13
|
+
#
|
|
14
|
+
# This middleware runs post-call and injects a synthetic assistant message
|
|
15
|
+
# when tool calls exist but no assistant message was recorded.
|
|
16
|
+
class ToolUseGuard
|
|
17
|
+
def initialize(app)
|
|
18
|
+
@app = app
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(env)
|
|
22
|
+
response = @app.call(env)
|
|
23
|
+
|
|
24
|
+
ctx = env[:context]
|
|
25
|
+
functions = ctx.functions
|
|
26
|
+
|
|
27
|
+
# If there are pending tool calls, ensure the buffer has an assistant
|
|
28
|
+
# message with tool_use blocks.
|
|
29
|
+
if functions && !functions.empty?
|
|
30
|
+
messages = ctx.messages.to_a
|
|
31
|
+
last_assistant = messages.reverse.find { |m| m.role.to_s == "assistant" }
|
|
32
|
+
|
|
33
|
+
unless last_assistant&.tool_call?
|
|
34
|
+
# Build a synthetic assistant message with the tool_use data
|
|
35
|
+
tool_calls = functions.map do |fn|
|
|
36
|
+
LLM::Object.from(id: fn.id, name: fn.name, arguments: fn.arguments)
|
|
37
|
+
end
|
|
38
|
+
original_tool_calls = functions.map do |fn|
|
|
39
|
+
{ "type" => "tool_use", "id" => fn.id, "name" => fn.name, "input" => fn.arguments || {} }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
synthetic = LLM::Message.new(:assistant, "", {
|
|
43
|
+
tool_calls: tool_calls,
|
|
44
|
+
original_tool_calls: original_tool_calls,
|
|
45
|
+
})
|
|
46
|
+
ctx.messages.concat([synthetic])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
response
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -2,31 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
module Brute
|
|
4
4
|
module Middleware
|
|
5
|
-
# Logs timing and token usage for every LLM call
|
|
5
|
+
# Logs timing and token usage for every LLM call, and tracks cumulative
|
|
6
|
+
# timing data in env[:metadata][:timing].
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# As the outermost middleware, it sees the full pipeline elapsed time per
|
|
9
|
+
# call. It also tracks total wall-clock time across all calls in a turn
|
|
10
|
+
# (including tool execution gaps between LLM calls).
|
|
11
|
+
#
|
|
12
|
+
# A new turn is detected when env[:tool_results] is nil (the orchestrator
|
|
13
|
+
# sets this on the first call of each run()).
|
|
14
|
+
#
|
|
15
|
+
# Stores in env[:metadata][:timing]:
|
|
16
|
+
# total_elapsed: wall-clock since the turn began (includes tool gaps)
|
|
17
|
+
# total_llm_elapsed: cumulative time spent inside LLM calls only
|
|
18
|
+
# llm_call_count: number of LLM calls so far
|
|
19
|
+
# last_call_elapsed: duration of the most recent LLM call
|
|
10
20
|
#
|
|
11
21
|
class Tracing < Base
|
|
12
22
|
def initialize(app, logger:)
|
|
13
23
|
super(app)
|
|
14
24
|
@logger = logger
|
|
15
25
|
@call_count = 0
|
|
26
|
+
@total_llm_elapsed = 0.0
|
|
27
|
+
@turn_start = nil
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def call(env)
|
|
19
31
|
@call_count += 1
|
|
32
|
+
|
|
33
|
+
# Detect new turn: tool_results is nil on the first pipeline call
|
|
34
|
+
if env[:tool_results].nil?
|
|
35
|
+
@turn_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
36
|
+
@total_llm_elapsed = 0.0
|
|
37
|
+
end
|
|
38
|
+
|
|
20
39
|
messages = env[:context].messages.to_a
|
|
21
40
|
@logger.debug("[brute] LLM call ##{@call_count} (#{messages.size} messages in context)")
|
|
22
41
|
|
|
23
42
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
43
|
response = @app.call(env)
|
|
25
|
-
|
|
44
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
|
+
elapsed = now - start
|
|
46
|
+
|
|
47
|
+
@total_llm_elapsed += elapsed
|
|
26
48
|
|
|
27
|
-
tokens = response.respond_to?(:usage) ? response.usage&.total_tokens :
|
|
49
|
+
tokens = response.respond_to?(:usage) ? response.usage&.total_tokens : '?'
|
|
28
50
|
@logger.info("[brute] LLM response ##{@call_count}: #{tokens} tokens, #{elapsed.round(2)}s")
|
|
29
51
|
|
|
52
|
+
env[:metadata][:timing] = {
|
|
53
|
+
total_elapsed: now - (@turn_start || start),
|
|
54
|
+
total_llm_elapsed: @total_llm_elapsed,
|
|
55
|
+
llm_call_count: @call_count,
|
|
56
|
+
last_call_elapsed: elapsed
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
response
|
|
31
60
|
end
|
|
32
61
|
end
|
data/lib/brute/orchestrator.rb
CHANGED
|
@@ -181,6 +181,9 @@ module Brute
|
|
|
181
181
|
# Handle reasoning params and model-switch normalization (pre-call)
|
|
182
182
|
use Middleware::ReasoningNormalizer, **reasoning unless reasoning.empty?
|
|
183
183
|
|
|
184
|
+
# Guard against tool-only responses dropping the assistant message
|
|
185
|
+
use Middleware::ToolUseGuard
|
|
186
|
+
|
|
184
187
|
# Innermost: the actual LLM call
|
|
185
188
|
run Middleware::LLMCall.new
|
|
186
189
|
end
|
data/lib/brute/tools/fs_patch.rb
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
module Brute
|
|
4
4
|
module Tools
|
|
5
5
|
class FSPatch < LLM::Tool
|
|
6
|
-
name
|
|
7
|
-
description
|
|
8
|
-
|
|
6
|
+
name 'patch'
|
|
7
|
+
description 'Replace a specific string in a file. The old_string must match exactly ' \
|
|
8
|
+
'(including whitespace and indentation). Always read a file before patching it.'
|
|
9
9
|
|
|
10
|
-
param :file_path, String,
|
|
11
|
-
param :old_string, String,
|
|
12
|
-
param :new_string, String,
|
|
13
|
-
param :replace_all, Boolean,
|
|
10
|
+
param :file_path, String, 'Path to the file to patch', required: true
|
|
11
|
+
param :old_string, String, 'The exact text to find and replace', required: true
|
|
12
|
+
param :new_string, String, 'The replacement text', required: true
|
|
13
|
+
param :replace_all, Boolean, 'Replace all occurrences (default: false)'
|
|
14
14
|
|
|
15
15
|
def call(file_path:, old_string:, new_string:, replace_all: false)
|
|
16
16
|
path = File.expand_path(file_path)
|
|
@@ -23,13 +23,15 @@ module Brute
|
|
|
23
23
|
Brute::SnapshotStore.save(path)
|
|
24
24
|
|
|
25
25
|
updated = if replace_all
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
original.gsub(old_string, new_string)
|
|
27
|
+
else
|
|
28
|
+
original.sub(old_string, new_string)
|
|
29
|
+
end
|
|
30
30
|
|
|
31
31
|
File.write(path, updated)
|
|
32
|
-
|
|
32
|
+
diff = Brute::Diff.unified(original, updated)
|
|
33
|
+
count = replace_all ? original.scan(old_string).size : 1
|
|
34
|
+
{ success: true, file_path: path, replacements: count, diff: diff }
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
end
|
data/lib/brute/tools/fs_write.rb
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'fileutils'
|
|
4
4
|
|
|
5
5
|
module Brute
|
|
6
6
|
module Tools
|
|
7
7
|
class FSWrite < LLM::Tool
|
|
8
|
-
name
|
|
8
|
+
name 'write'
|
|
9
9
|
description "Write content to a file. Creates parent directories if they don't exist. " \
|
|
10
|
-
|
|
10
|
+
'Use this for creating new files or completely replacing file contents.'
|
|
11
11
|
|
|
12
|
-
param :file_path, String,
|
|
13
|
-
param :content, String,
|
|
12
|
+
param :file_path, String, 'Path to the file to write', required: true
|
|
13
|
+
param :content, String, 'The full content to write to the file', required: true
|
|
14
14
|
|
|
15
15
|
def call(file_path:, content:)
|
|
16
16
|
path = File.expand_path(file_path)
|
|
17
17
|
Brute::FileMutationQueue.serialize(path) do
|
|
18
|
+
old_content = File.exist?(path) ? File.read(path) : ''
|
|
18
19
|
Brute::SnapshotStore.save(path)
|
|
19
20
|
FileUtils.mkdir_p(File.dirname(path))
|
|
20
21
|
File.write(path, content)
|
|
21
|
-
|
|
22
|
+
diff = Brute::Diff.unified(old_content, content)
|
|
23
|
+
{ success: true, file_path: path, bytes: content.bytesize, diff: diff }
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require 'llm'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require 'logger'
|
|
6
6
|
|
|
7
7
|
# Brute — a coding agent built on llm.rb
|
|
8
8
|
#
|
|
@@ -11,7 +11,7 @@ require "logger"
|
|
|
11
11
|
#
|
|
12
12
|
# Tracing → Retry → Session → Tokens → Compaction → ToolErrors → DoomLoop → Reasoning → [LLM Call]
|
|
13
13
|
#
|
|
14
|
-
require_relative
|
|
14
|
+
require_relative 'brute/version'
|
|
15
15
|
|
|
16
16
|
module Brute
|
|
17
17
|
module Tools; end
|
|
@@ -20,51 +20,52 @@ module Brute
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Infrastructure
|
|
23
|
-
require_relative
|
|
24
|
-
require_relative
|
|
25
|
-
require_relative
|
|
26
|
-
require_relative
|
|
27
|
-
require_relative
|
|
28
|
-
require_relative
|
|
29
|
-
require_relative
|
|
30
|
-
require_relative
|
|
31
|
-
require_relative
|
|
32
|
-
require_relative
|
|
23
|
+
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/system_prompt'
|
|
31
|
+
require_relative 'brute/session'
|
|
32
|
+
require_relative 'brute/pipeline'
|
|
33
|
+
require_relative 'brute/agent_stream'
|
|
33
34
|
|
|
34
35
|
# Provider patches
|
|
35
|
-
require_relative
|
|
36
|
-
require_relative
|
|
36
|
+
require_relative 'brute/patches/anthropic_tool_role'
|
|
37
|
+
require_relative 'brute/patches/buffer_nil_guard'
|
|
37
38
|
|
|
38
39
|
# Middleware (Rack-style)
|
|
39
|
-
require_relative
|
|
40
|
-
require_relative
|
|
41
|
-
require_relative
|
|
42
|
-
require_relative
|
|
43
|
-
require_relative
|
|
44
|
-
require_relative
|
|
45
|
-
require_relative
|
|
46
|
-
require_relative
|
|
47
|
-
require_relative
|
|
48
|
-
require_relative
|
|
40
|
+
require_relative 'brute/middleware/base'
|
|
41
|
+
require_relative 'brute/middleware/llm_call'
|
|
42
|
+
require_relative 'brute/middleware/retry'
|
|
43
|
+
require_relative 'brute/middleware/doom_loop_detection'
|
|
44
|
+
require_relative 'brute/middleware/token_tracking'
|
|
45
|
+
require_relative 'brute/middleware/compaction_check'
|
|
46
|
+
require_relative 'brute/middleware/session_persistence'
|
|
47
|
+
require_relative 'brute/middleware/tracing'
|
|
48
|
+
require_relative 'brute/middleware/tool_error_tracking'
|
|
49
|
+
require_relative 'brute/middleware/reasoning_normalizer'
|
|
50
|
+
require_relative "brute/middleware/tool_use_guard"
|
|
49
51
|
|
|
50
52
|
# Tools
|
|
51
|
-
require_relative
|
|
52
|
-
require_relative
|
|
53
|
-
require_relative
|
|
54
|
-
require_relative
|
|
55
|
-
require_relative
|
|
56
|
-
require_relative
|
|
57
|
-
require_relative
|
|
58
|
-
require_relative
|
|
59
|
-
require_relative
|
|
60
|
-
require_relative
|
|
61
|
-
require_relative
|
|
53
|
+
require_relative 'brute/tools/fs_read'
|
|
54
|
+
require_relative 'brute/tools/fs_write'
|
|
55
|
+
require_relative 'brute/tools/fs_patch'
|
|
56
|
+
require_relative 'brute/tools/fs_remove'
|
|
57
|
+
require_relative 'brute/tools/fs_search'
|
|
58
|
+
require_relative 'brute/tools/fs_undo'
|
|
59
|
+
require_relative 'brute/tools/shell'
|
|
60
|
+
require_relative 'brute/tools/net_fetch'
|
|
61
|
+
require_relative 'brute/tools/todo_write'
|
|
62
|
+
require_relative 'brute/tools/todo_read'
|
|
63
|
+
require_relative 'brute/tools/delegate'
|
|
62
64
|
|
|
63
65
|
# Orchestrator (depends on tools, middleware, and infrastructure)
|
|
64
|
-
require_relative
|
|
66
|
+
require_relative 'brute/orchestrator'
|
|
65
67
|
|
|
66
68
|
module Brute
|
|
67
|
-
|
|
68
69
|
# The complete set of tools available to the agent.
|
|
69
70
|
TOOLS = [
|
|
70
71
|
Tools::FSRead,
|
|
@@ -77,7 +78,7 @@ module Brute
|
|
|
77
78
|
Tools::NetFetch,
|
|
78
79
|
Tools::TodoWrite,
|
|
79
80
|
Tools::TodoRead,
|
|
80
|
-
Tools::Delegate
|
|
81
|
+
Tools::Delegate
|
|
81
82
|
].freeze
|
|
82
83
|
|
|
83
84
|
# Default provider, resolved from environment.
|
|
@@ -102,12 +103,12 @@ module Brute
|
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
PROVIDERS = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
'anthropic' => ->(key) { LLM.anthropic(key: key).tap { Patches::AnthropicToolRole.apply! } },
|
|
107
|
+
'openai' => ->(key) { LLM.openai(key: key) },
|
|
108
|
+
'google' => ->(key) { LLM.google(key: key) },
|
|
109
|
+
'deepseek' => ->(key) { LLM.deepseek(key: key) },
|
|
110
|
+
'ollama' => ->(key) { LLM.ollama(key: key) },
|
|
111
|
+
'xai' => ->(key) { LLM.xai(key: key) }
|
|
111
112
|
}.freeze
|
|
112
113
|
|
|
113
114
|
# Resolve the LLM provider from environment variables.
|
|
@@ -120,24 +121,24 @@ module Brute
|
|
|
120
121
|
#
|
|
121
122
|
# Returns nil if no key is found. Error is deferred to Orchestrator#run.
|
|
122
123
|
def self.resolve_provider
|
|
123
|
-
if ENV[
|
|
124
|
-
key = ENV[
|
|
125
|
-
name = ENV.fetch(
|
|
126
|
-
elsif ENV[
|
|
127
|
-
key = ENV[
|
|
128
|
-
name =
|
|
129
|
-
elsif ENV[
|
|
130
|
-
key = ENV[
|
|
131
|
-
name =
|
|
132
|
-
elsif ENV[
|
|
133
|
-
key = ENV[
|
|
134
|
-
name =
|
|
124
|
+
if ENV['LLM_API_KEY']
|
|
125
|
+
key = ENV['LLM_API_KEY']
|
|
126
|
+
name = ENV.fetch('LLM_PROVIDER', 'anthropic').downcase
|
|
127
|
+
elsif ENV['ANTHROPIC_API_KEY']
|
|
128
|
+
key = ENV['ANTHROPIC_API_KEY']
|
|
129
|
+
name = 'anthropic'
|
|
130
|
+
elsif ENV['OPENAI_API_KEY']
|
|
131
|
+
key = ENV['OPENAI_API_KEY']
|
|
132
|
+
name = 'openai'
|
|
133
|
+
elsif ENV['GOOGLE_API_KEY']
|
|
134
|
+
key = ENV['GOOGLE_API_KEY']
|
|
135
|
+
name = 'google'
|
|
135
136
|
else
|
|
136
137
|
return nil
|
|
137
138
|
end
|
|
138
139
|
|
|
139
140
|
factory = PROVIDERS[name]
|
|
140
|
-
raise "Unknown LLM provider: #{name}. Available: #{PROVIDERS.keys.join(
|
|
141
|
+
raise "Unknown LLM provider: #{name}. Available: #{PROVIDERS.keys.join(', ')}" unless factory
|
|
141
142
|
|
|
142
143
|
factory.call(key)
|
|
143
144
|
end
|
metadata
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 1980-01-
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: async
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '2.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '2.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: diff-lcs
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '1.5'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '1.5'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: llm.rb
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
47
|
-
type: :
|
|
46
|
+
version: '4.11'
|
|
47
|
+
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
53
|
+
version: '4.11'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: rake
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.13'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.13'
|
|
68
82
|
description: Production-grade coding agent with tool execution, middleware pipeline,
|
|
69
83
|
context compaction, session persistence, and multi-provider LLM support.
|
|
70
84
|
executables: []
|
|
@@ -74,6 +88,7 @@ files:
|
|
|
74
88
|
- lib/brute.rb
|
|
75
89
|
- lib/brute/agent_stream.rb
|
|
76
90
|
- lib/brute/compactor.rb
|
|
91
|
+
- lib/brute/diff.rb
|
|
77
92
|
- lib/brute/doom_loop.rb
|
|
78
93
|
- lib/brute/file_mutation_queue.rb
|
|
79
94
|
- lib/brute/hooks.rb
|
|
@@ -86,6 +101,7 @@ files:
|
|
|
86
101
|
- lib/brute/middleware/session_persistence.rb
|
|
87
102
|
- lib/brute/middleware/token_tracking.rb
|
|
88
103
|
- lib/brute/middleware/tool_error_tracking.rb
|
|
104
|
+
- lib/brute/middleware/tool_use_guard.rb
|
|
89
105
|
- lib/brute/middleware/tracing.rb
|
|
90
106
|
- lib/brute/orchestrator.rb
|
|
91
107
|
- lib/brute/patches/anthropic_tool_role.rb
|