brute 2.0.6 → 3.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/events/handler.rb +3 -1
- data/lib/brute/events/prefixed_terminal_output.rb +3 -1
- data/lib/brute/events/terminal_output_handler.rb +3 -1
- data/lib/brute/message_transport/anthropic.rb +143 -0
- data/lib/brute/message_transport/llm.rb +94 -0
- data/lib/brute/message_transport/openai.rb +113 -0
- data/lib/brute/message_transport/ruby_llm.rb +78 -0
- data/lib/brute/message_transport.rb +133 -0
- data/lib/brute/messages.rb +120 -0
- data/lib/brute/middleware/001_otel_span.rb +4 -2
- data/lib/brute/middleware/002_session_log.rb +100 -0
- data/lib/brute/middleware/004_summarize.rb +21 -19
- data/lib/brute/middleware/006_loop.rb +158 -0
- data/lib/brute/middleware/010_max_iterations.rb +9 -7
- data/lib/brute/middleware/015_otel_token_usage.rb +3 -1
- data/lib/brute/middleware/020_system_prompt.rb +6 -4
- data/lib/brute/middleware/040_compaction_check.rb +5 -3
- data/lib/brute/middleware/060_questions.rb +3 -1
- data/lib/brute/middleware/{070_tool_call.rb → 070_tool_pipeline.rb} +60 -46
- data/lib/brute/middleware/073_otel_tool_call.rb +3 -1
- data/lib/brute/middleware/075_otel_tool_results.rb +3 -1
- data/lib/brute/middleware/event_handler.rb +3 -1
- data/lib/brute/middleware/user_queue.rb +3 -1
- data/lib/brute/prompts/autonomy.rb +3 -0
- data/lib/brute/prompts/base.rb +3 -0
- data/lib/brute/prompts/build_switch.rb +3 -1
- data/lib/brute/prompts/code_references.rb +3 -0
- data/lib/brute/prompts/code_style.rb +3 -0
- data/lib/brute/prompts/conventions.rb +3 -0
- data/lib/brute/prompts/doing_tasks.rb +3 -0
- data/lib/brute/prompts/editing_approach.rb +3 -0
- data/lib/brute/prompts/editing_constraints.rb +3 -0
- data/lib/brute/prompts/environment.rb +3 -1
- data/lib/brute/prompts/frontend_tasks.rb +3 -0
- data/lib/brute/prompts/git_safety.rb +3 -0
- data/lib/brute/prompts/identity.rb +3 -1
- data/lib/brute/prompts/instructions.rb +3 -1
- data/lib/brute/prompts/max_steps.rb +3 -1
- data/lib/brute/prompts/objectivity.rb +3 -0
- data/lib/brute/prompts/plan_reminder.rb +3 -1
- data/lib/brute/prompts/proactiveness.rb +3 -0
- data/lib/brute/prompts/security_and_safety.rb +3 -0
- data/lib/brute/prompts/skills.rb +6 -2
- data/lib/brute/prompts/task_management.rb +3 -0
- data/lib/brute/prompts/tone_and_style.rb +3 -0
- data/lib/brute/prompts/tool_usage.rb +3 -0
- data/lib/brute/prompts.rb +5 -0
- data/lib/brute/rack/adapter.rb +237 -0
- data/lib/brute/skill.rb +192 -15
- data/lib/brute/system_prompt.rb +3 -1
- data/lib/brute/tool.rb +92 -75
- data/lib/brute/tools/adapter.rb +258 -0
- data/lib/brute/tools/fs/file_mutation_queue.rb +107 -0
- data/lib/brute/tools/fs/snapshot_store.rb +41 -0
- data/lib/brute/tools/fs_patch.rb +6 -4
- data/lib/brute/tools/fs_read.rb +5 -3
- data/lib/brute/tools/fs_remove.rb +3 -3
- data/lib/brute/tools/fs_search.rb +4 -2
- data/lib/brute/tools/fs_undo.rb +3 -3
- data/lib/brute/tools/fs_write.rb +6 -4
- data/lib/brute/tools/net_fetch.rb +1 -1
- data/lib/brute/tools/question.rb +1 -1
- data/lib/brute/tools/shell.rb +4 -2
- data/lib/brute/tools/skill_load.rb +156 -0
- data/lib/brute/tools/sub_agent.rb +101 -0
- data/lib/brute/tools/todo_list/store.rb +36 -0
- data/lib/brute/tools/todo_read.rb +2 -2
- data/lib/brute/tools/todo_write.rb +2 -2
- data/lib/brute/tools.rb +2 -1
- data/lib/brute/truncation.rb +3 -1
- data/lib/brute/turn/agent_pipeline.rb +169 -0
- data/lib/brute/turn/pipeline.rb +95 -0
- data/lib/brute/turn/tool_pipeline.rb +96 -0
- data/lib/brute/utils/diff.rb +3 -1
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +45 -71
- data/lib/{brute → brute_cli}/providers/shell.rb +4 -1
- data/lib/{brute → brute_cli}/providers/shell_response.rb +9 -15
- metadata +52 -43
- data/lib/brute/agent.rb +0 -82
- data/lib/brute/middleware/003_tool_result_loop.rb +0 -103
- data/lib/brute/middleware/100_llm_call.rb +0 -63
- data/lib/brute/pipeline.rb +0 -97
- data/lib/brute/queue/file_mutation_queue.rb +0 -102
- data/lib/brute/session.rb +0 -51
- data/lib/brute/store/snapshot_store.rb +0 -36
- data/lib/brute/store/todo_store.rb +0 -30
- data/lib/brute/sub_agent.rb +0 -106
|
@@ -54,16 +54,21 @@ module Brute
|
|
|
54
54
|
# one fiber runs at a time within a Sync block). No mutex needed.
|
|
55
55
|
#
|
|
56
56
|
# - FileMutationQueue compatibility — tools that mutate files use
|
|
57
|
-
# Brute::
|
|
57
|
+
# Brute::Tools::FS::FileMutationQueue.serialize, which uses Ruby 3.4's
|
|
58
58
|
# fiber-scheduler-aware Mutex. Operations on the same file are serialized;
|
|
59
59
|
# operations on different files proceed in parallel.
|
|
60
60
|
#
|
|
61
|
-
class
|
|
62
|
-
def initialize(app)
|
|
63
|
-
@app
|
|
61
|
+
class ToolPipeline
|
|
62
|
+
def initialize(app, tools: [])
|
|
63
|
+
@app = app
|
|
64
|
+
@tools = tools
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
# in -> advertise the tools to the turn (env[:tools], which the LLM-call
|
|
68
|
+
# proc reads to tell the model what it can call)
|
|
69
|
+
# out <- execute the tool calls the model made and append :tool results
|
|
66
70
|
def call(env)
|
|
71
|
+
env[:tools] = @tools
|
|
67
72
|
@app.call(env)
|
|
68
73
|
|
|
69
74
|
tools_to_run = pending_tool_calls(env[:messages].last)
|
|
@@ -76,13 +81,13 @@ module Brute
|
|
|
76
81
|
Sync do
|
|
77
82
|
barrier = Async::Barrier.new
|
|
78
83
|
|
|
79
|
-
tools_to_run.each do |
|
|
84
|
+
tools_to_run.each do |tool_call|
|
|
80
85
|
barrier.async do
|
|
81
86
|
tool = available_tools[tool_call.name.to_sym]
|
|
82
87
|
result = tool.call(tool_call.arguments)
|
|
83
88
|
|
|
84
|
-
# Coerce to String so
|
|
85
|
-
#
|
|
89
|
+
# Coerce to String so Hash results (e.g. Shell's
|
|
90
|
+
# {stdout:, stderr:, exit_code:}) serialize predictably.
|
|
86
91
|
content = result.is_a?(String) ? result : result.to_s
|
|
87
92
|
|
|
88
93
|
# Universal truncation safety net — skip if already truncated
|
|
@@ -90,13 +95,13 @@ module Brute
|
|
|
90
95
|
content = Brute::Truncation.truncate(content)
|
|
91
96
|
end
|
|
92
97
|
|
|
93
|
-
results << [
|
|
98
|
+
results << [tool_call, content]
|
|
94
99
|
rescue => e
|
|
95
100
|
# Capture the error as a tool result so the LLM can see it
|
|
96
101
|
# and reason about the failure, rather than crashing the
|
|
97
102
|
# entire middleware chain.
|
|
98
103
|
env[:events] << { type: :error, data: { error: e, message: e.message } }
|
|
99
|
-
results << [
|
|
104
|
+
results << [tool_call, "Error: #{e.class}: #{e.message}"]
|
|
100
105
|
end
|
|
101
106
|
end
|
|
102
107
|
|
|
@@ -107,12 +112,11 @@ module Brute
|
|
|
107
112
|
|
|
108
113
|
# Append events and messages in the original tool_call order so the
|
|
109
114
|
# LLM sees a deterministic sequence regardless of completion order.
|
|
110
|
-
|
|
111
|
-
results.sort_by! { |id, _, _| order.index(id) }
|
|
115
|
+
results.sort_by! { |tool_call, _| tools_to_run.index(tool_call) }
|
|
112
116
|
|
|
113
|
-
results.each do |
|
|
117
|
+
results.each do |tool_call, content|
|
|
114
118
|
env[:events] << { type: :tool_result, data: { name: tool_call.name, content: content } }
|
|
115
|
-
env[:messages] <<
|
|
119
|
+
env[:messages] << Brute::Message.new(role: :tool, content: content, tool_call_id: tool_call.id)
|
|
116
120
|
end
|
|
117
121
|
end
|
|
118
122
|
|
|
@@ -121,22 +125,24 @@ module Brute
|
|
|
121
125
|
|
|
122
126
|
private
|
|
123
127
|
|
|
128
|
+
# The last message's pending tool calls as a flat list. Duck-typed:
|
|
129
|
+
# tool_calls may be an Array (Brute::ToolCall) or an id-keyed Hash
|
|
130
|
+
# (some libraries' native shape); each entry needs #id, #name and
|
|
131
|
+
# #arguments.
|
|
124
132
|
def pending_tool_calls(message)
|
|
125
|
-
message.tool_calls
|
|
133
|
+
calls = message.respond_to?(:tool_calls) ? message.tool_calls : nil
|
|
134
|
+
calls = calls.values if calls.respond_to?(:values)
|
|
135
|
+
Array(calls).reject { |tc| tc.name == "question" }
|
|
126
136
|
end
|
|
127
137
|
|
|
128
138
|
def resolve_tools(tools)
|
|
129
|
-
|
|
130
|
-
instance = tool.is_a?(Class) ? tool.new : tool
|
|
131
|
-
instance = instance.to_ruby_llm if instance.respond_to?(:to_ruby_llm)
|
|
132
|
-
hash[instance.name.to_sym] = instance
|
|
133
|
-
end
|
|
139
|
+
Brute::Tools::Adapter.wrap_all(tools)
|
|
134
140
|
end
|
|
135
141
|
|
|
136
142
|
def on_tool_call_start_event(pending_tools)
|
|
137
143
|
{
|
|
138
144
|
type: :tool_call_start,
|
|
139
|
-
data: pending_tools.map { |
|
|
145
|
+
data: pending_tools.map { |tc|
|
|
140
146
|
{
|
|
141
147
|
name: tc.name,
|
|
142
148
|
call_id: tc.id,
|
|
@@ -149,18 +155,19 @@ module Brute
|
|
|
149
155
|
end
|
|
150
156
|
end
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
__END__
|
|
159
|
+
|
|
160
|
+
describe "brute/middleware/070_tool_pipeline" do
|
|
161
|
+
require "brute/messages"
|
|
154
162
|
require "brute/truncation"
|
|
155
163
|
|
|
156
164
|
it "passes through when no tool calls pending" do
|
|
157
165
|
inner = ->(env) {
|
|
158
|
-
env[:messages] <<
|
|
166
|
+
env[:messages] << Brute::Message.new(role: :assistant, content: "hi")
|
|
159
167
|
}
|
|
160
|
-
mw = Brute::Middleware::
|
|
168
|
+
mw = Brute::Middleware::ToolPipeline.new(inner, tools: [])
|
|
161
169
|
env = {
|
|
162
|
-
messages: Brute
|
|
163
|
-
tools: [],
|
|
170
|
+
messages: Brute.log,
|
|
164
171
|
events: [],
|
|
165
172
|
}
|
|
166
173
|
env[:messages].user("hello")
|
|
@@ -168,11 +175,22 @@ test do
|
|
|
168
175
|
env[:messages].last.content.should == "hi"
|
|
169
176
|
end
|
|
170
177
|
|
|
178
|
+
it "advertises its tools on env[:tools] on the way in" do
|
|
179
|
+
seen = nil
|
|
180
|
+
inner = ->(env) { seen = env[:tools] }
|
|
181
|
+
tool = { name: "echo", description: "", execute: ->(**) { "ok" } }
|
|
182
|
+
mw = Brute::Middleware::ToolPipeline.new(inner, tools: [tool])
|
|
183
|
+
env = { messages: Brute.log, events: [] }
|
|
184
|
+
env[:messages].user("hi")
|
|
185
|
+
mw.call(env)
|
|
186
|
+
seen.should == [tool]
|
|
187
|
+
end
|
|
188
|
+
|
|
171
189
|
# --- Universal output truncation ---
|
|
172
190
|
|
|
173
191
|
it "truncates large tool results via Truncation" do
|
|
174
192
|
# A fake tool that returns a huge string
|
|
175
|
-
big_tool = Class.new(
|
|
193
|
+
big_tool = Class.new(Brute::Tool) do
|
|
176
194
|
description "test tool"
|
|
177
195
|
param :input, type: "string", desc: "input"
|
|
178
196
|
def name; "big_tool"; end
|
|
@@ -181,22 +199,20 @@ test do
|
|
|
181
199
|
end
|
|
182
200
|
end
|
|
183
201
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
id: call_id,
|
|
202
|
+
tool_calls = [
|
|
203
|
+
Brute::ToolCall.new(
|
|
204
|
+
id: "tc_1",
|
|
188
205
|
name: "big_tool",
|
|
189
206
|
arguments: { "input" => "go" },
|
|
190
207
|
)
|
|
191
|
-
|
|
208
|
+
]
|
|
192
209
|
|
|
193
210
|
inner = ->(env) {
|
|
194
|
-
env[:messages] <<
|
|
211
|
+
env[:messages] << Brute::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
|
|
195
212
|
}
|
|
196
|
-
mw = Brute::Middleware::
|
|
213
|
+
mw = Brute::Middleware::ToolPipeline.new(inner, tools: [big_tool])
|
|
197
214
|
env = {
|
|
198
|
-
messages: Brute
|
|
199
|
-
tools: [big_tool],
|
|
215
|
+
messages: Brute.log,
|
|
200
216
|
events: [],
|
|
201
217
|
}
|
|
202
218
|
env[:messages].user("hello")
|
|
@@ -211,7 +227,7 @@ test do
|
|
|
211
227
|
|
|
212
228
|
it "does not double-truncate already-truncated output" do
|
|
213
229
|
# A fake tool that returns output already containing the truncation marker
|
|
214
|
-
pre_truncated_tool = Class.new(
|
|
230
|
+
pre_truncated_tool = Class.new(Brute::Tool) do
|
|
215
231
|
description "test tool"
|
|
216
232
|
param :input, type: "string", desc: "input"
|
|
217
233
|
def name; "pre_truncated_tool"; end
|
|
@@ -220,22 +236,20 @@ test do
|
|
|
220
236
|
end
|
|
221
237
|
end
|
|
222
238
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
id: call_id,
|
|
239
|
+
tool_calls = [
|
|
240
|
+
Brute::ToolCall.new(
|
|
241
|
+
id: "tc_2",
|
|
227
242
|
name: "pre_truncated_tool",
|
|
228
243
|
arguments: { "input" => "go" },
|
|
229
244
|
)
|
|
230
|
-
|
|
245
|
+
]
|
|
231
246
|
|
|
232
247
|
inner = ->(env) {
|
|
233
|
-
env[:messages] <<
|
|
248
|
+
env[:messages] << Brute::Message.new(role: :assistant, content: "", tool_calls: tool_calls)
|
|
234
249
|
}
|
|
235
|
-
mw = Brute::Middleware::
|
|
250
|
+
mw = Brute::Middleware::ToolPipeline.new(inner, tools: [pre_truncated_tool])
|
|
236
251
|
env = {
|
|
237
|
-
messages: Brute
|
|
238
|
-
tools: [pre_truncated_tool],
|
|
252
|
+
messages: Brute.log,
|
|
239
253
|
events: [],
|
|
240
254
|
}
|
|
241
255
|
env[:messages].user("hello")
|
data/lib/brute/prompts/base.rb
CHANGED
data/lib/brute/prompts/skills.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Brute
|
|
|
15
15
|
|
|
16
16
|
<<~TXT
|
|
17
17
|
Skills provide specialized instructions and workflows for specific tasks.
|
|
18
|
-
|
|
18
|
+
Use the skill tool to load a skill when a task matches its description. The tool
|
|
19
|
+
returns the skill's full instructions plus a base directory whose bundled files
|
|
20
|
+
(scripts, references, assets) you can read or run by relative path.
|
|
19
21
|
|
|
20
22
|
#{listing}
|
|
21
23
|
TXT
|
|
@@ -24,7 +26,9 @@ module Brute
|
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
__END__
|
|
30
|
+
|
|
31
|
+
describe "brute/prompts/skills" do
|
|
28
32
|
require "tmpdir"
|
|
29
33
|
|
|
30
34
|
it "returns nil when no skills are found" do
|