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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Brute
|
|
2
|
+
class Orchestrator
|
|
3
|
+
class Turn
|
|
4
|
+
def initialize(env:, pending:)
|
|
5
|
+
@env = env
|
|
6
|
+
@pending = pending
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
@env.dig(:callbacks, :on_tool_call_start).then do |on_start|
|
|
11
|
+
on_start&.call(
|
|
12
|
+
@pending.map do |tool, _|
|
|
13
|
+
{
|
|
14
|
+
name: tool.name,
|
|
15
|
+
arguments: tool.arguments
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
execute_tool_calls.tap do |results|
|
|
22
|
+
errors.each do |_, err|
|
|
23
|
+
on_result = @env.dig(:callbacks, :on_tool_result)
|
|
24
|
+
on_result&.call(err.name, result_value(err))
|
|
25
|
+
results << err
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def errors = @pending.select { |_, err| err }
|
|
31
|
+
def executable = @pending.reject { |_, err| err }.map(&:first)
|
|
32
|
+
|
|
33
|
+
def execute_tool_calls
|
|
34
|
+
if executable.empty?
|
|
35
|
+
[]
|
|
36
|
+
else
|
|
37
|
+
# Questions block execution — they must complete before other tools
|
|
38
|
+
# run, since the LLM may need the answer to inform subsequent work.
|
|
39
|
+
# Execute any question tools first (sequentially), then dispatch
|
|
40
|
+
# the remaining tools concurrently.
|
|
41
|
+
questions, others = executable.partition { _1.name == "question" }
|
|
42
|
+
|
|
43
|
+
Array.new.tap do |results|
|
|
44
|
+
if questions.any?
|
|
45
|
+
results.concat(execute_sequential(questions))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if others.size <= 1
|
|
49
|
+
results.concat(execute_sequential(others))
|
|
50
|
+
else
|
|
51
|
+
results.concat(execute_parallel(others))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Run a single tool call synchronously.
|
|
58
|
+
def execute_sequential(functions)
|
|
59
|
+
on_result = @env.dig(:callbacks, :on_tool_result)
|
|
60
|
+
on_question = @env.dig(:callbacks, :on_question)
|
|
61
|
+
|
|
62
|
+
functions.map do |fn|
|
|
63
|
+
Thread.current[:on_question] = on_question
|
|
64
|
+
result = fn.call
|
|
65
|
+
on_result&.call(fn.name, result_value(result))
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Run all pending tool calls concurrently via Async::Barrier.
|
|
71
|
+
#
|
|
72
|
+
# Each tool runs in its own fiber. File-mutating tools are safe because
|
|
73
|
+
# they go through FileMutationQueue, whose Mutex is fiber-scheduler-aware
|
|
74
|
+
# in Ruby 3.4 — a fiber blocked on a per-file mutex yields to other
|
|
75
|
+
# fibers instead of blocking the thread.
|
|
76
|
+
#
|
|
77
|
+
# The barrier is stored in @barrier so abort! can cancel in-flight tools.
|
|
78
|
+
#
|
|
79
|
+
def execute_parallel(functions)
|
|
80
|
+
on_result = @env.dig(:callbacks, :on_tool_result)
|
|
81
|
+
on_question = @env.dig(:callbacks, :on_question)
|
|
82
|
+
|
|
83
|
+
Array.new(functions.size).tap do |results|
|
|
84
|
+
Async do
|
|
85
|
+
@barrier = Async::Barrier.new
|
|
86
|
+
|
|
87
|
+
functions.each_with_index do |fn, i|
|
|
88
|
+
@barrier.async do
|
|
89
|
+
Thread.current[:on_question] = on_question
|
|
90
|
+
results[i] = fn.call
|
|
91
|
+
r = results[i]
|
|
92
|
+
on_result&.call(r.name, result_value(r))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@barrier.wait
|
|
97
|
+
ensure
|
|
98
|
+
@barrier&.stop
|
|
99
|
+
@barrier = nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/brute/pipeline.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
|
# Rack-style middleware pipeline for LLM calls.
|
|
5
8
|
#
|
|
@@ -16,13 +19,19 @@ module Brute
|
|
|
16
19
|
# ## The env hash
|
|
17
20
|
#
|
|
18
21
|
# {
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# input:
|
|
22
|
-
# tools:
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
# provider: LLM::Provider, # the LLM provider
|
|
23
|
+
# model: String|nil, # model override
|
|
24
|
+
# input: <prompt/results>, # what to pass to LLM
|
|
25
|
+
# tools: [Tool, ...], # tool classes
|
|
26
|
+
# messages: [LLM::Message], # conversation history (Brute-owned)
|
|
27
|
+
# stream: AgentStream|nil, # streaming bridge
|
|
28
|
+
# params: {}, # extra LLM call params
|
|
29
|
+
# metadata: {}, # shared scratchpad for middleware state
|
|
30
|
+
# callbacks: {}, # :on_content, :on_tool_call_start, :on_tool_result
|
|
31
|
+
# tool_results: Array|nil, # tool results from previous iteration
|
|
32
|
+
# streaming: Boolean, # whether streaming is active
|
|
33
|
+
# should_exit: Hash|nil, # exit signal from middleware
|
|
34
|
+
# pending_functions: [LLM::Function], # tool calls from last LLM response
|
|
26
35
|
# }
|
|
27
36
|
#
|
|
28
37
|
# ## The response
|
|
@@ -80,137 +89,72 @@ module Brute
|
|
|
80
89
|
end
|
|
81
90
|
end
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
require_relative "../../spec/
|
|
85
|
-
|
|
86
|
-
RSpec.describe "Middleware Pipeline Integration" do
|
|
87
|
-
let(:provider) { MockProvider.new }
|
|
88
|
-
let(:log_output) { StringIO.new }
|
|
89
|
-
let(:logger) { Logger.new(log_output) }
|
|
90
|
-
let(:session) { double("session", save: nil) }
|
|
91
|
-
let(:compactor) { double("compactor", should_compact?: false) }
|
|
92
|
-
|
|
93
|
-
describe "full orchestrator pipeline" do
|
|
94
|
-
it "passes env through all middleware and returns the response" do
|
|
95
|
-
response = MockResponse.new(content: "integrated response")
|
|
96
|
-
allow(provider).to receive(:complete).and_return(response)
|
|
97
|
-
|
|
98
|
-
ctx = LLM::Context.new(provider, tools: [])
|
|
99
|
-
prompt = ctx.prompt { |p| p.system("sys"); p.user("hello") }
|
|
100
|
-
|
|
101
|
-
pipeline = Brute::Pipeline.new
|
|
102
|
-
pipeline.use(Brute::Middleware::Tracing, logger: logger)
|
|
103
|
-
pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 2)
|
|
104
|
-
pipeline.use(Brute::Middleware::SessionPersistence, session: session)
|
|
105
|
-
pipeline.use(Brute::Middleware::TokenTracking)
|
|
106
|
-
pipeline.use(Brute::Middleware::CompactionCheck, compactor: compactor, system_prompt: "sys", tools: [])
|
|
107
|
-
pipeline.use(Brute::Middleware::ToolErrorTracking)
|
|
108
|
-
pipeline.use(Brute::Middleware::DoomLoopDetection, threshold: 3)
|
|
109
|
-
pipeline.use(Brute::Middleware::ToolUseGuard)
|
|
110
|
-
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
111
|
-
|
|
112
|
-
env = {
|
|
113
|
-
context: ctx,
|
|
114
|
-
provider: provider,
|
|
115
|
-
input: prompt,
|
|
116
|
-
tools: [],
|
|
117
|
-
params: {},
|
|
118
|
-
metadata: {},
|
|
119
|
-
callbacks: {},
|
|
120
|
-
tool_results: nil,
|
|
121
|
-
streaming: false,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
result = pipeline.call(env)
|
|
125
|
-
|
|
126
|
-
expect(result).not_to be_nil
|
|
127
|
-
expect(env[:metadata][:timing]).to include(:llm_call_count, :total_llm_elapsed)
|
|
128
|
-
expect(env[:metadata][:tokens]).to include(:total_input, :total_output, :call_count)
|
|
129
|
-
expect(session).to have_received(:save)
|
|
130
|
-
expect(log_output.string).to include("LLM call #1")
|
|
131
|
-
end
|
|
132
|
-
end
|
|
92
|
+
test do
|
|
93
|
+
require_relative "../../spec/support/mock_provider"
|
|
94
|
+
require_relative "../../spec/support/mock_response"
|
|
133
95
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
96
|
+
def make_env(provider:, input:)
|
|
97
|
+
{ provider: provider, model: nil, input: input, tools: [], messages: [],
|
|
98
|
+
stream: nil, params: {}, metadata: {}, callbacks: {}, tool_results: nil,
|
|
99
|
+
streaming: false, should_exit: nil, pending_functions: [] }
|
|
100
|
+
end
|
|
138
101
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
102
|
+
it "full pipeline passes env through all middleware" do
|
|
103
|
+
provider = MockProvider.new
|
|
104
|
+
session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
|
|
105
|
+
compactor = Object.new
|
|
106
|
+
compactor.define_singleton_method(:should_compact?) { |_msgs, **_| false }
|
|
107
|
+
log_output = StringIO.new
|
|
108
|
+
|
|
109
|
+
pipeline = Brute::Pipeline.new
|
|
110
|
+
pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(log_output))
|
|
111
|
+
pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 2)
|
|
112
|
+
pipeline.use(Brute::Middleware::SessionPersistence, session: session)
|
|
113
|
+
pipeline.use(Brute::Middleware::TokenTracking)
|
|
114
|
+
pipeline.use(Brute::Middleware::CompactionCheck, compactor: compactor, system_prompt: "sys")
|
|
115
|
+
pipeline.use(Brute::Middleware::ToolErrorTracking)
|
|
116
|
+
pipeline.use(Brute::Middleware::DoomLoopDetection, threshold: 3)
|
|
117
|
+
pipeline.use(Brute::Middleware::ToolUseGuard)
|
|
118
|
+
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
119
|
+
|
|
120
|
+
env = make_env(provider: provider, input: "hello")
|
|
121
|
+
result = pipeline.call(env)
|
|
122
|
+
result.should.not.be.nil
|
|
123
|
+
end
|
|
144
124
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
pipeline = Brute::Pipeline.new
|
|
149
|
-
pipeline.use(Brute::Middleware::Tracing, logger: logger)
|
|
150
|
-
pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 0)
|
|
151
|
-
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
152
|
-
|
|
153
|
-
env = {
|
|
154
|
-
context: ctx,
|
|
155
|
-
provider: provider,
|
|
156
|
-
input: prompt,
|
|
157
|
-
tools: [],
|
|
158
|
-
params: {},
|
|
159
|
-
metadata: {},
|
|
160
|
-
callbacks: {},
|
|
161
|
-
tool_results: nil,
|
|
162
|
-
streaming: false,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
result = pipeline.call(env)
|
|
166
|
-
|
|
167
|
-
expect(result).not_to be_nil
|
|
168
|
-
expect(env[:metadata][:timing][:llm_call_count]).to eq(1)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
125
|
+
it "pipeline populates timing metadata" do
|
|
126
|
+
provider = MockProvider.new
|
|
127
|
+
session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
|
|
171
128
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
ctx = LLM::Context.new(provider, tools: [])
|
|
178
|
-
prompt = ctx.prompt { |p| p.system("sys"); p.user("hi") }
|
|
179
|
-
|
|
180
|
-
save_args = []
|
|
181
|
-
allow(session).to receive(:save) { |ctx_arg| save_args << ctx_arg }
|
|
182
|
-
|
|
183
|
-
pipeline = Brute::Pipeline.new
|
|
184
|
-
pipeline.use(Brute::Middleware::SessionPersistence, session: session)
|
|
185
|
-
pipeline.use(Brute::Middleware::TokenTracking)
|
|
186
|
-
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
187
|
-
|
|
188
|
-
env = {
|
|
189
|
-
context: ctx,
|
|
190
|
-
provider: provider,
|
|
191
|
-
input: prompt,
|
|
192
|
-
tools: [],
|
|
193
|
-
params: {},
|
|
194
|
-
metadata: {},
|
|
195
|
-
callbacks: {},
|
|
196
|
-
tool_results: nil,
|
|
197
|
-
streaming: false,
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
pipeline.call(env)
|
|
201
|
-
|
|
202
|
-
expect(env[:metadata][:tokens]).to include(:total_input)
|
|
203
|
-
expect(save_args.size).to eq(1)
|
|
204
|
-
end
|
|
205
|
-
end
|
|
129
|
+
pipeline = Brute::Pipeline.new
|
|
130
|
+
pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(StringIO.new))
|
|
131
|
+
pipeline.use(Brute::Middleware::SessionPersistence, session: session)
|
|
132
|
+
pipeline.use(Brute::Middleware::TokenTracking)
|
|
133
|
+
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
206
134
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
135
|
+
env = make_env(provider: provider, input: "hello")
|
|
136
|
+
pipeline.call(env)
|
|
137
|
+
env[:metadata][:timing][:llm_call_count].should == 1
|
|
138
|
+
end
|
|
211
139
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
end
|
|
140
|
+
it "pipeline populates token metadata" do
|
|
141
|
+
provider = MockProvider.new
|
|
142
|
+
session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
|
|
143
|
+
|
|
144
|
+
pipeline = Brute::Pipeline.new
|
|
145
|
+
pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(StringIO.new))
|
|
146
|
+
pipeline.use(Brute::Middleware::SessionPersistence, session: session)
|
|
147
|
+
pipeline.use(Brute::Middleware::TokenTracking)
|
|
148
|
+
pipeline.run(Brute::Middleware::LLMCall.new)
|
|
149
|
+
|
|
150
|
+
env = make_env(provider: provider, input: "hello")
|
|
151
|
+
pipeline.call(env)
|
|
152
|
+
env[:metadata][:tokens][:total_input].should.be > 0
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "raises when no terminal app is set" do
|
|
156
|
+
pipeline = Brute::Pipeline.new
|
|
157
|
+
pipeline.use(Brute::Middleware::TokenTracking)
|
|
158
|
+
lambda { pipeline.call({}) }.should.raise(RuntimeError)
|
|
215
159
|
end
|
|
216
160
|
end
|
|
@@ -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 Prompts
|
|
5
8
|
module BuildSwitch
|
|
@@ -18,35 +21,28 @@ module Brute
|
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
subject(:text) { described_class.call({}) }
|
|
26
|
-
|
|
27
|
-
it "returns a string" do
|
|
28
|
-
expect(text).to be_a(String)
|
|
29
|
-
end
|
|
24
|
+
test do
|
|
25
|
+
it "returns a string" do
|
|
26
|
+
Brute::Prompts::BuildSwitch.call({}).should.be.kind_of(String)
|
|
27
|
+
end
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
29
|
+
it "wraps content in system-reminder tags" do
|
|
30
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /system-reminder/
|
|
31
|
+
end
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
it "announces mode change from plan to build" do
|
|
34
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /plan to build/
|
|
35
|
+
end
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
it "states no longer in read-only mode" do
|
|
38
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /no longer in read-only mode/
|
|
39
|
+
end
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
it "permits tool use" do
|
|
42
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /permitted to make file changes/
|
|
43
|
+
end
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
45
|
+
it "ignores context (static content)" do
|
|
46
|
+
Brute::Prompts::BuildSwitch.call({ agent_switched: "build" }).should == Brute::Prompts::BuildSwitch.call({})
|
|
51
47
|
end
|
|
52
48
|
end
|
|
@@ -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 Prompts
|
|
5
8
|
module Environment
|
|
@@ -24,49 +27,42 @@ module Brute
|
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
test do
|
|
31
|
+
require "tmpdir"
|
|
32
|
+
require "fileutils"
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(text).to include("/some/path")
|
|
34
|
-
end
|
|
34
|
+
it "includes cwd from context" do
|
|
35
|
+
Brute::Prompts::Environment.call(cwd: "/some/path", model_name: "test").should =~ /\/some\/path/
|
|
36
|
+
end
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
38
|
+
it "includes model name from context" do
|
|
39
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "claude-sonnet").should =~ /claude-sonnet/
|
|
40
|
+
end
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect(text).to include("</env>")
|
|
45
|
-
end
|
|
42
|
+
it "wraps environment info in env tags" do
|
|
43
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /<env>/
|
|
44
|
+
end
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
46
|
+
it "detects git repo when .git exists" do
|
|
47
|
+
Dir.mktmpdir do |dir|
|
|
48
|
+
Dir.mkdir(File.join(dir, ".git"))
|
|
49
|
+
text = Brute::Prompts::Environment.call(cwd: dir, model_name: "test")
|
|
50
|
+
text.should =~ /Is directory a git repo: yes/
|
|
53
51
|
end
|
|
52
|
+
end
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
54
|
+
it "detects non-git directory" do
|
|
55
|
+
Dir.mktmpdir do |dir|
|
|
56
|
+
text = Brute::Prompts::Environment.call(cwd: dir, model_name: "test")
|
|
57
|
+
text.should =~ /Is directory a git repo: no/
|
|
60
58
|
end
|
|
59
|
+
end
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
61
|
+
it "includes the platform" do
|
|
62
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /#{RUBY_PLATFORM}/
|
|
63
|
+
end
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(text).to include(Dir.pwd)
|
|
70
|
-
end
|
|
65
|
+
it "defaults cwd to Dir.pwd when not provided" do
|
|
66
|
+
Brute::Prompts::Environment.call(model_name: "test").should =~ /#{Regexp.escape(Dir.pwd)}/
|
|
71
67
|
end
|
|
72
68
|
end
|
|
@@ -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 Prompts
|
|
5
8
|
module Identity
|
|
@@ -10,38 +13,28 @@ module Brute
|
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
it "returns provider-specific text for anthropic" do
|
|
18
|
-
text = described_class.call(provider_name: "anthropic")
|
|
19
|
-
expect(text).to be_a(String)
|
|
20
|
-
expect(text).not_to be_empty
|
|
21
|
-
end
|
|
16
|
+
test do
|
|
17
|
+
it "returns a string for anthropic" do
|
|
18
|
+
Brute::Prompts::Identity.call(provider_name: "anthropic").should.be.kind_of(String)
|
|
19
|
+
end
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expect(text).not_to be_empty
|
|
27
|
-
end
|
|
21
|
+
it "returns non-empty text for anthropic" do
|
|
22
|
+
Brute::Prompts::Identity.call(provider_name: "anthropic").should.not.be.empty
|
|
23
|
+
end
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
expect(text).not_to be_empty
|
|
33
|
-
end
|
|
25
|
+
it "returns non-empty text for openai" do
|
|
26
|
+
Brute::Prompts::Identity.call(provider_name: "openai").should.not.be.empty
|
|
27
|
+
end
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
it "falls back to default for unknown providers" do
|
|
30
|
+
default_text = Brute::Prompts::Identity.call(provider_name: "default")
|
|
31
|
+
unknown_text = Brute::Prompts::Identity.call(provider_name: "nonexistent_provider")
|
|
32
|
+
unknown_text.should == default_text
|
|
33
|
+
end
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
35
|
+
it "returns different text for different providers" do
|
|
36
|
+
anthropic = Brute::Prompts::Identity.call(provider_name: "anthropic")
|
|
37
|
+
openai = Brute::Prompts::Identity.call(provider_name: "openai")
|
|
38
|
+
(anthropic != openai).should.be.true
|
|
46
39
|
end
|
|
47
40
|
end
|
|
@@ -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 Prompts
|
|
5
8
|
module Instructions
|
|
@@ -17,26 +20,20 @@ module Brute
|
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
it "returns nil when custom_rules is nil" do
|
|
25
|
-
expect(described_class.call(custom_rules: nil)).to be_nil
|
|
26
|
-
end
|
|
23
|
+
test do
|
|
24
|
+
it "returns nil when custom_rules is nil" do
|
|
25
|
+
Brute::Prompts::Instructions.call(custom_rules: nil).should.be.nil
|
|
26
|
+
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
it "returns nil when custom_rules is empty" do
|
|
29
|
+
Brute::Prompts::Instructions.call(custom_rules: "").should.be.nil
|
|
30
|
+
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
it "returns nil when custom_rules is whitespace-only" do
|
|
33
|
+
Brute::Prompts::Instructions.call(custom_rules: " \n ").should.be.nil
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(text).to include("Project-Specific Rules")
|
|
39
|
-
expect(text).to include("Always use tabs.")
|
|
40
|
-
end
|
|
36
|
+
it "wraps custom_rules in a Project-Specific Rules header" do
|
|
37
|
+
Brute::Prompts::Instructions.call(custom_rules: "Always use tabs.").should =~ /Project-Specific Rules/
|
|
41
38
|
end
|
|
42
39
|
end
|
|
@@ -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 Prompts
|
|
5
8
|
module MaxSteps
|
|
@@ -29,34 +32,24 @@ module Brute
|
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
subject(:text) { described_class.call({}) }
|
|
37
|
-
|
|
38
|
-
it "returns a string" do
|
|
39
|
-
expect(text).to be_a(String)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "announces maximum steps reached" do
|
|
43
|
-
expect(text).to include("MAXIMUM STEPS REACHED")
|
|
44
|
-
end
|
|
35
|
+
test do
|
|
36
|
+
it "returns a string" do
|
|
37
|
+
Brute::Prompts::MaxSteps.call({}).should.be.kind_of(String)
|
|
38
|
+
end
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
it "announces maximum steps reached" do
|
|
41
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /MAXIMUM STEPS REACHED/
|
|
42
|
+
end
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
it "states tools are disabled" do
|
|
45
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /Tools are disabled/
|
|
46
|
+
end
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
it "requires a text-only response" do
|
|
49
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /text ONLY/
|
|
50
|
+
end
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
52
|
+
it "ignores context (static content)" do
|
|
53
|
+
Brute::Prompts::MaxSteps.call({ max_steps_reached: true }).should == Brute::Prompts::MaxSteps.call({})
|
|
61
54
|
end
|
|
62
55
|
end
|