brute 4.2.0 → 4.3.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/completion/open_router.rb +46 -0
- data/lib/brute/hooks.rb +7 -1
- data/lib/brute/middleware/002_session_log.rb +7 -13
- data/lib/brute/middleware/004_summarize.rb +15 -81
- data/lib/brute/middleware/006_loop.rb +50 -68
- data/lib/brute/middleware/010_max_iterations.rb +7 -22
- data/lib/brute/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8264f0e2d23e001a3c980d3edd0f88ac3ceb8a3f167359c56391e3fe9702e59
|
|
4
|
+
data.tar.gz: 7439466f57b0f3e18aae290a723f8cb4077970b200f1a8e0f7970fd7d863aee6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fa71cbfe00b6069e2bdb90872cff9fe2f88eb2f48c7ff614feb59ead8f03890739c4772c863b09edf27fd7c2c122ac486fc5c3d84f582f477527f5f69d2dee4
|
|
7
|
+
data.tar.gz: 866794dc4f87f5433508b1e883db27788b00bb57157211ea2942fe64ccaa46969c3cfdd36fe917b9717fe7bdc385f4fd9881b1a5ce47c9763e859d7e00fbf356
|
|
@@ -48,6 +48,25 @@ module Brute
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
env[:hooks]&.emit(:after_llm, env)
|
|
51
|
+
env
|
|
52
|
+
# A provider call that raises is reported through the hooks rather than
|
|
53
|
+
# up the stack: :llm_failure with the turn env, then one hook naming the
|
|
54
|
+
# kind of failure. The classes are looked up defensively — the provider
|
|
55
|
+
# gem is only a dependency of the app that uses this middleware.
|
|
56
|
+
rescue => error
|
|
57
|
+
env[:hooks]&.emit(:llm_failure, env)
|
|
58
|
+
|
|
59
|
+
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
60
|
+
env[:hooks]&.emit(:faraday_error, error)
|
|
61
|
+
|
|
62
|
+
elsif defined?(::OpenRouter::ServerError) && error.is_a?(::OpenRouter::ServerError)
|
|
63
|
+
env[:hooks]&.emit(:open_router_server_error, error)
|
|
64
|
+
|
|
65
|
+
else
|
|
66
|
+
env[:hooks]&.emit(:standard_error, error)
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
|
|
51
70
|
env
|
|
52
71
|
end
|
|
53
72
|
end
|
|
@@ -106,6 +125,33 @@ describe "brute/completion/open_router" do
|
|
|
106
125
|
env.key?(:metadata).should.be.false
|
|
107
126
|
end
|
|
108
127
|
|
|
128
|
+
it "reports a failed provider call through the hooks instead of raising" do
|
|
129
|
+
seen = []
|
|
130
|
+
hooks = Brute::Hooks.new
|
|
131
|
+
%i[llm_failure standard_error after_llm].each do |event|
|
|
132
|
+
hooks.on(event) { |payload| seen << [event, payload] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
boom = RuntimeError.new("no route to host")
|
|
136
|
+
fake_client = Object.new
|
|
137
|
+
fake_client.define_singleton_method(:complete) { |_messages, _options| raise boom }
|
|
138
|
+
original = OpenRouter::Client.method(:new)
|
|
139
|
+
OpenRouter::Client.define_singleton_method(:new) { |**_config| fake_client }
|
|
140
|
+
|
|
141
|
+
begin
|
|
142
|
+
env = { messages: Brute.log, hooks: hooks }
|
|
143
|
+
env[:messages].user("hi")
|
|
144
|
+
returned = Brute::Completion::OpenRouter.new(->(e) { e }).call(env)
|
|
145
|
+
|
|
146
|
+
returned.should.be.identical_to env
|
|
147
|
+
seen.map(&:first).should == [:llm_failure, :standard_error]
|
|
148
|
+
seen.first.last.should.be.identical_to env
|
|
149
|
+
seen.last.last.should.be.identical_to boom
|
|
150
|
+
ensure
|
|
151
|
+
OpenRouter::Client.define_singleton_method(:new, original)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
109
155
|
it "still answers to the deprecated Middleware::OpenRouter::Completion name" do
|
|
110
156
|
deprecated = Brute::Middleware::OpenRouter::Completion.new(->(e) { e })
|
|
111
157
|
deprecated.should.be.kind_of?(Brute::Completion::OpenRouter)
|
data/lib/brute/hooks.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Brute
|
|
|
17
17
|
# :turn_start, :turn_end → the turn env (AgentPipeline#start; turn_end
|
|
18
18
|
# fires from an ensure, so it also fires on error)
|
|
19
19
|
# :before_llm, :after_llm → the turn env, around every LLM call
|
|
20
|
+
# :llm_failure → the turn env, when the LLM call raises; the
|
|
21
|
+
# completion middleware then emits one of
|
|
22
|
+
# :faraday_error, :open_router_server_error or
|
|
23
|
+
# :standard_error with the exception itself
|
|
20
24
|
# :before_tool → call env {name:, arguments:, result:, events:,
|
|
21
25
|
# metadata:, turn_env:} — mutate :arguments to
|
|
22
26
|
# rewrite the call, or set :result (or return a
|
|
@@ -29,7 +33,9 @@ module Brute
|
|
|
29
33
|
# Exceptions propagate to the caller — layers that want fail-open semantics
|
|
30
34
|
# rescue in their own subscriber.
|
|
31
35
|
class Hooks
|
|
32
|
-
EVENTS = %i[turn_start turn_end before_llm after_llm
|
|
36
|
+
EVENTS = %i[turn_start turn_end before_llm after_llm llm_failure faraday_error
|
|
37
|
+
open_router_server_error standard_error before_tool approve_tool
|
|
38
|
+
after_tool].freeze
|
|
33
39
|
|
|
34
40
|
def initialize
|
|
35
41
|
@subscribers = Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -71,30 +71,24 @@ describe "brute/middleware/002_session_log" do
|
|
|
71
71
|
Dir.mktmpdir do |dir|
|
|
72
72
|
path = File.join(dir, "session.jsonl")
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Brute::Middleware::SessionLog.new(turn, path: path).call(
|
|
77
|
-
{ messages: Brute.log.tap { |l| l.user("hi") } }
|
|
78
|
-
)
|
|
74
|
+
Brute::Middleware::SessionLog.new(->(e) { e[:messages].assistant("first reply") }, path: path)
|
|
75
|
+
.call({ messages: Brute.log.tap { |l| l.user("hi") } })
|
|
79
76
|
|
|
80
|
-
File.exist?(path).should.be.true
|
|
81
|
-
|
|
82
|
-
# Turn 2: fresh log with a new user message; history is prepended.
|
|
83
77
|
env = { messages: Brute.log.tap { |l| l.user("again") } }
|
|
84
78
|
Brute::Middleware::SessionLog.new(->(e) { }, path: path).call(env)
|
|
85
79
|
|
|
80
|
+
File.exist?(path).should.be.true
|
|
86
81
|
env[:messages].map(&:content).should == ["hi", "first reply", "again"]
|
|
87
82
|
end
|
|
88
83
|
end
|
|
89
84
|
|
|
90
85
|
it "does not persist the system message" do
|
|
91
86
|
Dir.mktmpdir do |dir|
|
|
92
|
-
path = File.join(dir, "
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
path = File.join(dir, "session.jsonl")
|
|
88
|
+
Brute::Middleware::SessionLog.new(->(e) { }, path: path)
|
|
89
|
+
.call({ messages: Brute.log.tap { |l| l.system("secret prompt"); l.user("hi") } })
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
reloaded.any? { |line| line.include?("secret prompt") }.should.be.false
|
|
91
|
+
File.read(path).should.not.match(/secret prompt/)
|
|
98
92
|
end
|
|
99
93
|
end
|
|
100
94
|
end
|
|
@@ -51,91 +51,25 @@ __END__
|
|
|
51
51
|
describe "brute/middleware/004_summarize" do
|
|
52
52
|
require "brute/messages"
|
|
53
53
|
|
|
54
|
-
it "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# Fake inner app: first call simulates a tool loop ending with a tool message,
|
|
58
|
-
# second call (summary) produces an assistant message.
|
|
54
|
+
it "reruns the stack tool-free with a summary prompt, then restores the tools" do
|
|
55
|
+
seen = []
|
|
59
56
|
inner = ->(env) do
|
|
60
|
-
|
|
61
|
-
if call_count == 1
|
|
62
|
-
env[:messages] << Brute::Message.new(role: :tool, content: "some result", tool_call_id: "tc1")
|
|
63
|
-
else
|
|
64
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "Here is my complete summary.")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
mw = Brute::Middleware::Summarize.new(inner)
|
|
69
|
-
session = Brute.log
|
|
70
|
-
session.user("explore the codebase")
|
|
71
|
-
env = { messages: session, tools: [:some_tool], current_iteration: 5 }
|
|
72
|
-
mw.call(env)
|
|
73
|
-
|
|
74
|
-
env[:messages].last.role.should == :assistant
|
|
75
|
-
env[:messages].last.content.should =~ /summary/i
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "restores tools after summary call" do
|
|
79
|
-
inner = ->(env) {
|
|
80
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
mw = Brute::Middleware::Summarize.new(inner)
|
|
84
|
-
tools = [:read, :search]
|
|
85
|
-
env = { messages: Brute.log, tools: tools.dup, current_iteration: 1 }
|
|
86
|
-
env[:messages].user("hi")
|
|
87
|
-
mw.call(env)
|
|
88
|
-
|
|
89
|
-
env[:tools].should == tools
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
it "resets current_iteration for the summary call" do
|
|
93
|
-
captured_iteration = nil
|
|
94
|
-
inner = ->(env) {
|
|
95
|
-
captured_iteration = env[:current_iteration]
|
|
96
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
mw = Brute::Middleware::Summarize.new(inner)
|
|
100
|
-
env = { messages: Brute.log, tools: [], current_iteration: 99 }
|
|
101
|
-
env[:messages].user("hi")
|
|
102
|
-
mw.call(env)
|
|
103
|
-
|
|
104
|
-
# The second call (summary) should have iteration reset to 1
|
|
105
|
-
captured_iteration.should == 1
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
it "injects a summary prompt message" do
|
|
109
|
-
messages_at_second_call = nil
|
|
110
|
-
call_count = 0
|
|
111
|
-
inner = ->(env) {
|
|
112
|
-
call_count += 1
|
|
113
|
-
messages_at_second_call = env[:messages].map(&:content) if call_count == 2
|
|
57
|
+
seen << [env[:tools], env[:current_iteration], env[:messages].last.content]
|
|
114
58
|
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
mw = Brute::Middleware::Summarize.new(inner)
|
|
118
|
-
env = { messages: Brute.log, tools: [], current_iteration: 1 }
|
|
119
|
-
env[:messages].user("hi")
|
|
120
|
-
mw.call(env)
|
|
121
|
-
|
|
122
|
-
messages_at_second_call.last.should =~ /findings/i
|
|
123
|
-
end
|
|
59
|
+
end
|
|
124
60
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
inner = ->(env) {
|
|
129
|
-
call_count += 1
|
|
130
|
-
messages_at_second_call = env[:messages].map(&:content) if call_count == 2
|
|
131
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
132
|
-
}
|
|
61
|
+
env = { messages: Brute.log.tap { |l| l.user("explore the codebase") },
|
|
62
|
+
tools: [:read, :search], current_iteration: 5 }
|
|
63
|
+
Brute::Middleware::Summarize.new(inner).call(env)
|
|
133
64
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
65
|
+
seen.should == [
|
|
66
|
+
[[:read, :search], 5, "explore the codebase"],
|
|
67
|
+
[[], 1, Brute::Middleware::Summarize::DEFAULT_PROMPT],
|
|
68
|
+
]
|
|
69
|
+
env[:tools].should == [:read, :search]
|
|
138
70
|
|
|
139
|
-
|
|
71
|
+
seen.clear
|
|
72
|
+
Brute::Middleware::Summarize.new(inner, prompt: "Give me the TL;DR.").call(env)
|
|
73
|
+
seen.last.last.should == "Give me the TL;DR."
|
|
140
74
|
end
|
|
141
75
|
end
|
|
@@ -71,6 +71,24 @@ module Brute
|
|
|
71
71
|
super(app, CONDITION)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
|
+
|
|
75
|
+
# Keeps the turn alive while background jobs are running. Sit it
|
|
76
|
+
# OUTSIDE the tool loop: the inner loop ends when the model answers
|
|
77
|
+
# with text, and this one sends control back in for another pass while
|
|
78
|
+
# jobs are still running — which is how a finished job gets reported.
|
|
79
|
+
# Whatever spawns the jobs (say, a middleware running a subagent in
|
|
80
|
+
# the background) keeps env[:background_jobs] current.
|
|
81
|
+
#
|
|
82
|
+
# use Brute::Middleware::Loop::BackgroundJobs
|
|
83
|
+
# use Brute::Middleware::Loop::ToolResult
|
|
84
|
+
#
|
|
85
|
+
class BackgroundJobs < Loop
|
|
86
|
+
CONDITION = ->(env) { env[:background_jobs] }
|
|
87
|
+
|
|
88
|
+
def initialize(app)
|
|
89
|
+
super(app, CONDITION)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
74
92
|
end
|
|
75
93
|
end
|
|
76
94
|
end
|
|
@@ -80,90 +98,54 @@ __END__
|
|
|
80
98
|
describe "brute/middleware/006_loop" do
|
|
81
99
|
require "brute/messages"
|
|
82
100
|
|
|
83
|
-
it "runs the
|
|
84
|
-
|
|
85
|
-
mw = Brute::Middleware::Loop.new(->(_env) { calls += 1 }, ->(_env) { false })
|
|
86
|
-
mw.call({})
|
|
87
|
-
calls.should == 1
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it "loops while the condition is truthy" do
|
|
91
|
-
calls = 0
|
|
92
|
-
mw = Brute::Middleware::Loop.new(->(_env) { calls += 1 }, ->(_env) { calls < 3 })
|
|
93
|
-
mw.call({})
|
|
94
|
-
calls.should == 3
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "accepts a block condition and passes env to it" do
|
|
98
|
-
seen = []
|
|
99
|
-
mw = Brute::Middleware::Loop.new(->(env) { env[:n] += 1 }) { |env| seen << env[:n]; env[:n] < 2 }
|
|
101
|
+
it "runs the app until the condition turns false, passing it env" do
|
|
102
|
+
passes = []
|
|
100
103
|
env = { n: 0 }
|
|
101
|
-
|
|
102
|
-
env[:n].should == 2
|
|
103
|
-
seen.should == [1, 2]
|
|
104
|
-
end
|
|
104
|
+
returned = Brute::Middleware::Loop.new(->(e) { e[:n] += 1; passes << e[:n]; e }) { |e| e[:n] < 3 }.call(env)
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
env
|
|
108
|
-
Brute::Middleware::Loop.new(->(_e) {}, ->(_e) { false }).call(env).should == env
|
|
109
|
-
end
|
|
106
|
+
passes.should == [1, 2, 3]
|
|
107
|
+
returned.should.be.identical_to env
|
|
110
108
|
|
|
111
|
-
it "raises without a proc or block" do
|
|
112
109
|
lambda { Brute::Middleware::Loop.new(->(_e) {}) }.should.raise(ArgumentError)
|
|
113
110
|
end
|
|
114
111
|
|
|
115
112
|
# --- Loop::ToolResult (reimplements the old ToolResultLoop) ---
|
|
116
113
|
|
|
117
|
-
it "ToolResult
|
|
118
|
-
|
|
114
|
+
it "ToolResult reruns while the last message is a tool result, unless should_exit" do
|
|
115
|
+
calls = []
|
|
119
116
|
inner = ->(env) do
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
else
|
|
124
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
125
|
-
end
|
|
117
|
+
calls << env[:current_iteration]
|
|
118
|
+
env[:messages] << Brute::Message.new(role: :tool, content: "result", tool_call_id: "tc#{calls.size}")
|
|
119
|
+
env[:should_exit] = { reason: "max" } if calls.size >= 2
|
|
126
120
|
end
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
mw.call(env)
|
|
122
|
+
env = { messages: Brute.log.tap { |l| l.user("hi") }, current_iteration: 1 }
|
|
123
|
+
Brute::Middleware::Loop::ToolResult.new(inner).call(env)
|
|
124
|
+
calls.should == [1, 2]
|
|
132
125
|
|
|
133
|
-
|
|
134
|
-
env
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
it "ToolResult stops when should_exit is set" do
|
|
139
|
-
call_count = 0
|
|
140
|
-
inner = ->(env) do
|
|
141
|
-
call_count += 1
|
|
142
|
-
env[:messages] << Brute::Message.new(role: :tool, content: "result", tool_call_id: "tc#{call_count}")
|
|
143
|
-
env[:should_exit] = { reason: "max" } if call_count >= 2
|
|
126
|
+
calls.clear
|
|
127
|
+
text_only = ->(env) do
|
|
128
|
+
calls << env[:current_iteration]
|
|
129
|
+
env[:messages] << Brute::Message.new(role: :assistant, content: "done")
|
|
144
130
|
end
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
env[:
|
|
149
|
-
mw.call(env)
|
|
150
|
-
|
|
151
|
-
call_count.should == 2
|
|
131
|
+
env = { messages: Brute.log.tap { |l| l.user("hi") }, current_iteration: 1 }
|
|
132
|
+
Brute::Middleware::Loop::ToolResult.new(text_only).call(env)
|
|
133
|
+
calls.should == [1]
|
|
134
|
+
env[:current_iteration].should == 1
|
|
152
135
|
end
|
|
153
136
|
|
|
154
|
-
|
|
155
|
-
call_count = 0
|
|
156
|
-
inner = ->(env) do
|
|
157
|
-
call_count += 1
|
|
158
|
-
env[:messages] << Brute::Message.new(role: :assistant, content: "hello")
|
|
159
|
-
end
|
|
137
|
+
# --- Loop::BackgroundJobs ---
|
|
160
138
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
env[:
|
|
164
|
-
mw.call(env)
|
|
139
|
+
it "BackgroundJobs reruns while jobs are running" do
|
|
140
|
+
seen = []
|
|
141
|
+
inner = ->(env) { seen << env[:background_jobs]; env[:background_jobs] = seen.size < 3 }
|
|
165
142
|
|
|
166
|
-
|
|
167
|
-
|
|
143
|
+
Brute::Middleware::Loop::BackgroundJobs.new(inner).call({})
|
|
144
|
+
seen.should == [nil, true, true]
|
|
145
|
+
|
|
146
|
+
seen.clear
|
|
147
|
+
Brute::Middleware::Loop::BackgroundJobs.new(->(e) { seen << e[:background_jobs]; e[:background_jobs] = false })
|
|
148
|
+
.call({ background_jobs: true })
|
|
149
|
+
seen.should == [true]
|
|
168
150
|
end
|
|
169
151
|
end
|
|
@@ -45,30 +45,15 @@ __END__
|
|
|
45
45
|
describe "brute/middleware/010_max_iterations" do
|
|
46
46
|
require "brute/messages"
|
|
47
47
|
|
|
48
|
-
it "
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
mw = Brute::Middleware::MaxIterations.new(inner)
|
|
52
|
-
mw.call({ current_iteration: 1, messages: Brute.log })
|
|
53
|
-
called.should.be.true
|
|
54
|
-
end
|
|
48
|
+
it "lets the turn run under the max, then blocks the stack and injects a notice" do
|
|
49
|
+
ran = []
|
|
50
|
+
env = { messages: Brute.log.tap { |l| l.user("hi") }, current_iteration: 1 }
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
inner = ->(env) { called = true }
|
|
59
|
-
mw = Brute::Middleware::MaxIterations.new(inner, max_iterations: 0)
|
|
60
|
-
env = { current_iteration: 1, messages: Brute.log }
|
|
61
|
-
mw.call(env)
|
|
62
|
-
called.should.be.false
|
|
63
|
-
end
|
|
52
|
+
Brute::Middleware::MaxIterations.new(->(e) { ran << e[:current_iteration] }).call(env)
|
|
53
|
+
ran.should == [1]
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
mw = Brute::Middleware::MaxIterations.new(inner, max_iterations: 0)
|
|
68
|
-
session = Brute.log
|
|
69
|
-
session.user("hi")
|
|
70
|
-
env = { current_iteration: 1, messages: session }
|
|
71
|
-
mw.call(env)
|
|
55
|
+
Brute::Middleware::MaxIterations.new(->(e) { ran << e[:current_iteration] }, max_iterations: 0).call(env)
|
|
56
|
+
ran.should == [1]
|
|
72
57
|
env[:messages].last.role.should == :user
|
|
73
58
|
env[:messages].last.content.should =~ /Maximum iterations reached/
|
|
74
59
|
end
|
data/lib/brute/version.rb
CHANGED
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: 4.
|
|
4
|
+
version: 4.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
@@ -197,14 +197,14 @@ dependencies:
|
|
|
197
197
|
requirements:
|
|
198
198
|
- - "~>"
|
|
199
199
|
- !ruby/object:Gem::Version
|
|
200
|
-
version: '0.
|
|
200
|
+
version: '0.3'
|
|
201
201
|
type: :development
|
|
202
202
|
prerelease: false
|
|
203
203
|
version_requirements: !ruby/object:Gem::Requirement
|
|
204
204
|
requirements:
|
|
205
205
|
- - "~>"
|
|
206
206
|
- !ruby/object:Gem::Version
|
|
207
|
-
version: '0.
|
|
207
|
+
version: '0.3'
|
|
208
208
|
description: Production-grade coding agent with tool execution, middleware pipeline,
|
|
209
209
|
context compaction, session persistence, and multi-provider LLM support.
|
|
210
210
|
executables: []
|