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
|
@@ -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 Middleware
|
|
5
8
|
# Logs timing and token usage for every LLM call, and tracks cumulative
|
|
@@ -9,7 +12,7 @@ module Brute
|
|
|
9
12
|
# call. It also tracks total wall-clock time across all calls in a turn
|
|
10
13
|
# (including tool execution gaps between LLM calls).
|
|
11
14
|
#
|
|
12
|
-
# A new turn is detected when env[:tool_results] is nil (the
|
|
15
|
+
# A new turn is detected when env[:tool_results] is nil (the agent loop
|
|
13
16
|
# sets this on the first call of each run()).
|
|
14
17
|
#
|
|
15
18
|
# Stores in env[:metadata][:timing]:
|
|
@@ -36,7 +39,7 @@ module Brute
|
|
|
36
39
|
@total_llm_elapsed = 0.0
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
messages = env[:
|
|
42
|
+
messages = env[:messages]
|
|
40
43
|
@logger.debug("[brute] LLM call ##{@call_count} (#{messages.size} messages in context)")
|
|
41
44
|
|
|
42
45
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -61,3 +64,61 @@ module Brute
|
|
|
61
64
|
end
|
|
62
65
|
end
|
|
63
66
|
end
|
|
67
|
+
|
|
68
|
+
test do
|
|
69
|
+
require_relative "../../../spec/support/mock_provider"
|
|
70
|
+
require_relative "../../../spec/support/mock_response"
|
|
71
|
+
|
|
72
|
+
def build_env(**overrides)
|
|
73
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
74
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
75
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "passes the response through unchanged" do
|
|
79
|
+
response = MockResponse.new(content: "traced response")
|
|
80
|
+
inner_app = ->(_env) { response }
|
|
81
|
+
middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
|
|
82
|
+
result = middleware.call(build_env(tool_results: nil))
|
|
83
|
+
result.should == response
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "populates timing with llm_call_count" do
|
|
87
|
+
response = MockResponse.new(content: "traced response")
|
|
88
|
+
inner_app = ->(_env) { response }
|
|
89
|
+
middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
|
|
90
|
+
env = build_env(tool_results: nil)
|
|
91
|
+
middleware.call(env)
|
|
92
|
+
env[:metadata][:timing][:llm_call_count].should == 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "populates timing with non-negative last_call_elapsed" do
|
|
96
|
+
response = MockResponse.new(content: "traced response")
|
|
97
|
+
inner_app = ->(_env) { response }
|
|
98
|
+
middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
|
|
99
|
+
env = build_env(tool_results: nil)
|
|
100
|
+
middleware.call(env)
|
|
101
|
+
(env[:metadata][:timing][:last_call_elapsed] >= 0).should.be.true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "accumulates call count across multiple calls" do
|
|
105
|
+
response = MockResponse.new(content: "traced response")
|
|
106
|
+
inner_app = ->(_env) { response }
|
|
107
|
+
middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
|
|
108
|
+
env = build_env(tool_results: nil)
|
|
109
|
+
middleware.call(env)
|
|
110
|
+
env[:tool_results] = [["read", {}]]
|
|
111
|
+
middleware.call(env)
|
|
112
|
+
middleware.call(env)
|
|
113
|
+
env[:metadata][:timing][:llm_call_count].should == 3
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "logs LLM call and response messages" do
|
|
117
|
+
response = MockResponse.new(content: "traced response")
|
|
118
|
+
inner_app = ->(_env) { response }
|
|
119
|
+
log_output = StringIO.new
|
|
120
|
+
middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(log_output))
|
|
121
|
+
middleware.call(build_env(tool_results: nil))
|
|
122
|
+
log_output.string.should =~ /LLM call #1/
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require_relative 'middleware/base'
|
|
2
|
+
require_relative 'middleware/llm_call'
|
|
3
|
+
require_relative 'middleware/retry'
|
|
4
|
+
require_relative 'middleware/doom_loop_detection'
|
|
5
|
+
require_relative 'middleware/token_tracking'
|
|
6
|
+
require_relative 'middleware/compaction_check'
|
|
7
|
+
require_relative 'middleware/session_persistence'
|
|
8
|
+
require_relative 'middleware/message_tracking'
|
|
9
|
+
require_relative 'middleware/tracing'
|
|
10
|
+
require_relative 'middleware/tool_error_tracking'
|
|
11
|
+
require_relative 'middleware/reasoning_normalizer'
|
|
12
|
+
require_relative "middleware/tool_use_guard"
|
|
13
|
+
require_relative "middleware/otel"
|
|
14
|
+
|
|
15
|
+
module Brute
|
|
16
|
+
module Middleware
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -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
|
|
@@ -79,3 +88,73 @@ module Brute
|
|
|
79
88
|
end
|
|
80
89
|
end
|
|
81
90
|
end
|
|
91
|
+
|
|
92
|
+
test do
|
|
93
|
+
require_relative "../../spec/support/mock_provider"
|
|
94
|
+
require_relative "../../spec/support/mock_response"
|
|
95
|
+
|
|
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
|
|
101
|
+
|
|
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
|
|
124
|
+
|
|
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
|
|
128
|
+
|
|
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)
|
|
134
|
+
|
|
135
|
+
env = make_env(provider: provider, input: "hello")
|
|
136
|
+
pipeline.call(env)
|
|
137
|
+
env[:metadata][:timing][:llm_call_count].should == 1
|
|
138
|
+
end
|
|
139
|
+
|
|
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)
|
|
159
|
+
end
|
|
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
|
|
@@ -17,3 +20,29 @@ module Brute
|
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
end
|
|
23
|
+
|
|
24
|
+
test do
|
|
25
|
+
it "returns a string" do
|
|
26
|
+
Brute::Prompts::BuildSwitch.call({}).should.be.kind_of(String)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "wraps content in system-reminder tags" do
|
|
30
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /system-reminder/
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "announces mode change from plan to build" do
|
|
34
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /plan to build/
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "states no longer in read-only mode" do
|
|
38
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /no longer in read-only mode/
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "permits tool use" do
|
|
42
|
+
Brute::Prompts::BuildSwitch.call({}).should =~ /permitted to make file changes/
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "ignores context (static content)" do
|
|
46
|
+
Brute::Prompts::BuildSwitch.call({ agent_switched: "build" }).should == Brute::Prompts::BuildSwitch.call({})
|
|
47
|
+
end
|
|
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
|
|
@@ -23,3 +26,43 @@ module Brute
|
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
end
|
|
29
|
+
|
|
30
|
+
test do
|
|
31
|
+
require "tmpdir"
|
|
32
|
+
require "fileutils"
|
|
33
|
+
|
|
34
|
+
it "includes cwd from context" do
|
|
35
|
+
Brute::Prompts::Environment.call(cwd: "/some/path", model_name: "test").should =~ /\/some\/path/
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "includes model name from context" do
|
|
39
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "claude-sonnet").should =~ /claude-sonnet/
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "wraps environment info in env tags" do
|
|
43
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /<env>/
|
|
44
|
+
end
|
|
45
|
+
|
|
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/
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
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/
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "includes the platform" do
|
|
62
|
+
Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /#{RUBY_PLATFORM}/
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "defaults cwd to Dir.pwd when not provided" do
|
|
66
|
+
Brute::Prompts::Environment.call(model_name: "test").should =~ /#{Regexp.escape(Dir.pwd)}/
|
|
67
|
+
end
|
|
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
|
|
@@ -9,3 +12,29 @@ module Brute
|
|
|
9
12
|
end
|
|
10
13
|
end
|
|
11
14
|
end
|
|
15
|
+
|
|
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
|
|
20
|
+
|
|
21
|
+
it "returns non-empty text for anthropic" do
|
|
22
|
+
Brute::Prompts::Identity.call(provider_name: "anthropic").should.not.be.empty
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns non-empty text for openai" do
|
|
26
|
+
Brute::Prompts::Identity.call(provider_name: "openai").should.not.be.empty
|
|
27
|
+
end
|
|
28
|
+
|
|
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
|
|
34
|
+
|
|
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
|
|
39
|
+
end
|
|
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
|
|
@@ -16,3 +19,21 @@ module Brute
|
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
end
|
|
22
|
+
|
|
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
|
+
|
|
28
|
+
it "returns nil when custom_rules is empty" do
|
|
29
|
+
Brute::Prompts::Instructions.call(custom_rules: "").should.be.nil
|
|
30
|
+
end
|
|
31
|
+
|
|
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
|
+
|
|
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/
|
|
38
|
+
end
|
|
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
|
|
@@ -28,3 +31,25 @@ module Brute
|
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
test do
|
|
36
|
+
it "returns a string" do
|
|
37
|
+
Brute::Prompts::MaxSteps.call({}).should.be.kind_of(String)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "announces maximum steps reached" do
|
|
41
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /MAXIMUM STEPS REACHED/
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "states tools are disabled" do
|
|
45
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /Tools are disabled/
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "requires a text-only response" do
|
|
49
|
+
Brute::Prompts::MaxSteps.call({}).should =~ /text ONLY/
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "ignores context (static content)" do
|
|
53
|
+
Brute::Prompts::MaxSteps.call({ max_steps_reached: true }).should == Brute::Prompts::MaxSteps.call({})
|
|
54
|
+
end
|
|
55
|
+
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 PlanReminder
|
|
@@ -38,3 +41,25 @@ module Brute
|
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|
|
44
|
+
|
|
45
|
+
test do
|
|
46
|
+
it "returns a string" do
|
|
47
|
+
Brute::Prompts::PlanReminder.call({}).should.be.kind_of(String)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "wraps content in system-reminder tags" do
|
|
51
|
+
Brute::Prompts::PlanReminder.call({}).should =~ /system-reminder/
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "declares READ-ONLY mode" do
|
|
55
|
+
Brute::Prompts::PlanReminder.call({}).should =~ /READ-ONLY/
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "forbids file edits" do
|
|
59
|
+
Brute::Prompts::PlanReminder.call({}).should =~ /STRICTLY FORBIDDEN/
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "ignores context (static content)" do
|
|
63
|
+
Brute::Prompts::PlanReminder.call({ agent: "plan" }).should == Brute::Prompts::PlanReminder.call({})
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/brute/prompts/skills.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 Prompts
|
|
5
8
|
module Skills
|
|
@@ -20,3 +23,13 @@ module Brute
|
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
26
|
+
|
|
27
|
+
test do
|
|
28
|
+
require "tmpdir"
|
|
29
|
+
|
|
30
|
+
it "returns nil when no skills are found" do
|
|
31
|
+
Dir.mktmpdir do |dir|
|
|
32
|
+
Brute::Prompts::Skills.call(cwd: dir).should.be.nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative 'prompts/base'
|
|
2
|
+
require_relative 'prompts/identity'
|
|
3
|
+
require_relative 'prompts/tone_and_style'
|
|
4
|
+
require_relative 'prompts/objectivity'
|
|
5
|
+
require_relative 'prompts/task_management'
|
|
6
|
+
require_relative 'prompts/doing_tasks'
|
|
7
|
+
require_relative 'prompts/tool_usage'
|
|
8
|
+
require_relative 'prompts/conventions'
|
|
9
|
+
require_relative 'prompts/git_safety'
|
|
10
|
+
require_relative 'prompts/code_references'
|
|
11
|
+
require_relative 'prompts/environment'
|
|
12
|
+
require_relative 'prompts/instructions'
|
|
13
|
+
require_relative 'prompts/editing_approach'
|
|
14
|
+
require_relative 'prompts/autonomy'
|
|
15
|
+
require_relative 'prompts/editing_constraints'
|
|
16
|
+
require_relative 'prompts/frontend_tasks'
|
|
17
|
+
require_relative 'prompts/proactiveness'
|
|
18
|
+
require_relative 'prompts/code_style'
|
|
19
|
+
require_relative 'prompts/security_and_safety'
|
|
20
|
+
require_relative 'prompts/skills'
|
|
21
|
+
require_relative 'prompts/plan_reminder'
|
|
22
|
+
require_relative 'prompts/max_steps'
|
|
23
|
+
require_relative 'prompts/build_switch'
|
|
24
|
+
|
|
25
|
+
module Brute
|
|
26
|
+
module Prompts
|
|
27
|
+
end
|
|
28
|
+
end
|