brute 3.0.0 → 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/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 +79 -6
- data/lib/brute/middleware/002_session_log.rb +1 -1
- data/lib/brute/middleware/004_summarize.rb +7 -7
- data/lib/brute/middleware/006_loop.rb +4 -4
- data/lib/brute/middleware/010_max_iterations.rb +1 -1
- data/lib/brute/middleware/020_system_prompt.rb +1 -1
- data/lib/brute/middleware/040_compaction_check.rb +2 -2
- data/lib/brute/middleware/070_tool_pipeline.rb +29 -26
- data/lib/brute/tool.rb +124 -0
- data/lib/brute/tools/adapter.rb +22 -60
- data/lib/brute/tools/fs_patch.rb +1 -1
- data/lib/brute/tools/fs_read.rb +1 -1
- data/lib/brute/tools/fs_remove.rb +1 -1
- data/lib/brute/tools/fs_search.rb +1 -1
- data/lib/brute/tools/fs_undo.rb +1 -1
- data/lib/brute/tools/fs_write.rb +1 -1
- data/lib/brute/tools/net_fetch.rb +1 -1
- data/lib/brute/tools/question.rb +1 -1
- data/lib/brute/tools/shell.rb +1 -1
- data/lib/brute/tools/skill_load.rb +1 -1
- data/lib/brute/tools/sub_agent.rb +5 -22
- data/lib/brute/tools/todo_read.rb +1 -1
- data/lib/brute/tools/todo_write.rb +1 -1
- data/lib/brute/turn/agent_pipeline.rb +2 -1
- data/lib/brute/turn/pipeline.rb +1 -1
- data/lib/brute/turn/tool_pipeline.rb +2 -12
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +17 -18
- data/lib/brute_cli/providers/shell_response.rb +6 -10
- metadata +22 -17
- data/lib/ruby_llm/message_transport.rb +0 -117
|
@@ -23,14 +23,10 @@ module Brute
|
|
|
23
23
|
# use Brute::Middleware::MaxIterations, max_iterations: 10
|
|
24
24
|
# use Brute::Middleware::ToolPipeline, tools: [Brute::Tools::FSRead, Brute::Tools::FSSearch]
|
|
25
25
|
# run ->(env) do
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# tools: Brute.rubyllm_tools(env[:tools]),
|
|
31
|
-
# temperature: 0.7,
|
|
32
|
-
# model: model)
|
|
33
|
-
# RubyLLM::MessageTransport.new(response).wrap_each { |m| env[:messages] << m }
|
|
26
|
+
# # The LLM call, written with your library of choice. Convert
|
|
27
|
+
# # env[:messages] to its format, call it, and append the response
|
|
28
|
+
# # back as Brute::Message values (the MessageTransport pattern —
|
|
29
|
+
# # see examples/ruby_llm.rb, examples/openai.rb, ...).
|
|
34
30
|
# end
|
|
35
31
|
# end
|
|
36
32
|
#
|
|
@@ -63,20 +59,7 @@ module Brute
|
|
|
63
59
|
extract_result(session)
|
|
64
60
|
end
|
|
65
61
|
|
|
66
|
-
#
|
|
67
|
-
# sees this as a regular tool. ToolPipeline middleware should call
|
|
68
|
-
# `to_ruby_llm` when building the tools hash if a tool responds to it.
|
|
69
|
-
def to_ruby_llm
|
|
70
|
-
sub = self
|
|
71
|
-
Class.new(::RubyLLM::Tool) do
|
|
72
|
-
description sub.description
|
|
73
|
-
sub.params.each { |k, opts| param k, **opts }
|
|
74
|
-
define_method(:name) { sub.sub_agent_name }
|
|
75
|
-
define_method(:execute) { |**args| sub.execute(args) }
|
|
76
|
-
end.new
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Lets ToolPipeline treat SubAgents the same as RubyLLM::Tool instances
|
|
62
|
+
# Lets ToolPipeline treat SubAgents the same as any other tool
|
|
80
63
|
# without checking respond_to? everywhere.
|
|
81
64
|
def name
|
|
82
65
|
@sub_agent_name
|
|
@@ -6,7 +6,7 @@ require "brute/tools"
|
|
|
6
6
|
|
|
7
7
|
module Brute
|
|
8
8
|
module Tools
|
|
9
|
-
class TodoRead <
|
|
9
|
+
class TodoRead < Brute::Tool
|
|
10
10
|
description "Read the current todo list to check task status and progress."
|
|
11
11
|
param :_placeholder, type: 'string', desc: "Unused, pass any value", required: false
|
|
12
12
|
|
|
@@ -57,7 +57,8 @@ module Brute
|
|
|
57
57
|
case input
|
|
58
58
|
when nil then Brute.log
|
|
59
59
|
when ::String then Brute.log.tap { |log| log.user(input) }
|
|
60
|
-
when ::
|
|
60
|
+
when ::Brute::Message then Brute.log(input)
|
|
61
|
+
when ::Hash then Brute.log(Brute::Message.new(**input.transform_keys(&:to_sym)))
|
|
61
62
|
when ::Array then input.extend(Brute::Messages)
|
|
62
63
|
else Brute.log(input)
|
|
63
64
|
end
|
data/lib/brute/turn/pipeline.rb
CHANGED
|
@@ -13,8 +13,8 @@ module Brute
|
|
|
13
13
|
# the work; middleware wraps it with concerns like file mutation queueing,
|
|
14
14
|
# validation, logging.
|
|
15
15
|
#
|
|
16
|
-
# Coexists with Brute::Tools::* (which inherit from
|
|
17
|
-
# ToolPipeline when you want middleware; use
|
|
16
|
+
# Coexists with Brute::Tools::* (which inherit from Brute::Tool). Use a
|
|
17
|
+
# ToolPipeline when you want middleware; use Brute::Tool subclasses for
|
|
18
18
|
# simple cases.
|
|
19
19
|
#
|
|
20
20
|
# read = Brute::Turn::ToolPipeline.new(
|
|
@@ -53,16 +53,6 @@ module Brute
|
|
|
53
53
|
env[:result]
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Adapter so the LLM can call this tool through ruby_llm.
|
|
57
|
-
def to_ruby_llm
|
|
58
|
-
tool = self
|
|
59
|
-
Class.new(RubyLLM::Tool) do
|
|
60
|
-
description tool.description
|
|
61
|
-
tool.params.each { |k, opts| param k, **opts }
|
|
62
|
-
define_method(:name) { tool.name }
|
|
63
|
-
define_method(:execute) { |**args| tool.call(**args) }
|
|
64
|
-
end.new
|
|
65
|
-
end
|
|
66
56
|
end
|
|
67
57
|
end
|
|
68
58
|
end
|
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
|
|
5
|
-
require "ruby_llm"
|
|
6
5
|
require "rack"
|
|
7
6
|
require 'timeout'
|
|
8
7
|
require 'logger'
|
|
@@ -22,16 +21,14 @@ module Brute
|
|
|
22
21
|
`Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
|
|
23
22
|
LOGO
|
|
24
23
|
|
|
25
|
-
# NOTE: Brute owns no LLM configuration
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# See examples/agents/01_basic_agent.rb.
|
|
24
|
+
# NOTE: Brute owns no LLM configuration and no LLM library. All
|
|
25
|
+
# provider/model/credential config lives in the pipeline's terminal `run`
|
|
26
|
+
# proc, which the user writes with whatever LLM library they prefer
|
|
27
|
+
# (ruby_llm, llm.rb, openai, anthropic, raw HTTP, ...). The proc converts
|
|
28
|
+
# env[:messages] (Brute::Message values — see Brute.log) to the library's
|
|
29
|
+
# format, makes the call, and appends the response back as Brute::Message
|
|
30
|
+
# values — the MessageTransport pattern. See examples/ruby_llm.rb,
|
|
31
|
+
# examples/llm.rb, examples/openai.rb and examples/anthropic.rb.
|
|
35
32
|
|
|
36
33
|
def self.provider
|
|
37
34
|
@provider ||= :anthropic
|
|
@@ -60,19 +57,21 @@ module Brute
|
|
|
60
57
|
Brute::Turn::AgentPipeline.new(&block)
|
|
61
58
|
end
|
|
62
59
|
|
|
63
|
-
# Adapt any Brute tools (hashes, Brute::Turn::ToolPipeline,
|
|
64
|
-
# into a { name_sym =>
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
# Adapt any Brute tools (hashes, Brute::Tool, Brute::Turn::ToolPipeline,
|
|
61
|
+
# SubAgent …) into a { name_sym => Brute::Tools::Adapter } hash. Each
|
|
62
|
+
# adapter exposes #to_h — a neutral JSON-Schema-ish definition the inline
|
|
63
|
+
# `run` proc converts to whatever its LLM library expects.
|
|
64
|
+
def self.tools(tools)
|
|
65
|
+
Brute::Tools::Adapter.wrap_all(tools || [])
|
|
68
66
|
end
|
|
69
|
-
|
|
67
|
+
|
|
68
|
+
|
|
70
69
|
def self.provider=(p)
|
|
71
70
|
@provider = p.to_sym
|
|
72
71
|
end
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
Dir.glob("#{__dir__}/
|
|
74
|
+
Dir.glob("#{__dir__}/brute/**/*.rb").sort.each do |path|
|
|
76
75
|
require path
|
|
77
76
|
end
|
|
78
77
|
|
|
@@ -28,15 +28,15 @@ module BruteCLI
|
|
|
28
28
|
return [empty_assistant] if @command.nil?
|
|
29
29
|
|
|
30
30
|
call_id = "shell_#{SecureRandom.hex(8)}"
|
|
31
|
-
tool_calls =
|
|
32
|
-
|
|
31
|
+
tool_calls = [
|
|
32
|
+
Brute::ToolCall.new(
|
|
33
33
|
id: call_id,
|
|
34
34
|
name: "shell",
|
|
35
35
|
arguments: { "command" => @command },
|
|
36
36
|
)
|
|
37
|
-
|
|
37
|
+
]
|
|
38
38
|
|
|
39
|
-
[
|
|
39
|
+
[Brute::Message.new(
|
|
40
40
|
role: :assistant,
|
|
41
41
|
content: "",
|
|
42
42
|
tool_calls: tool_calls,
|
|
@@ -78,17 +78,13 @@ module BruteCLI
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def usage
|
|
81
|
-
|
|
82
|
-
input: 0,
|
|
83
|
-
output: 0,
|
|
84
|
-
reasoning: 0,
|
|
85
|
-
)
|
|
81
|
+
{ input: 0, output: 0, reasoning: 0 }
|
|
86
82
|
end
|
|
87
83
|
|
|
88
84
|
private
|
|
89
85
|
|
|
90
86
|
def empty_assistant
|
|
91
|
-
|
|
87
|
+
Brute::Message.new(role: :assistant, content: "")
|
|
92
88
|
end
|
|
93
89
|
end
|
|
94
90
|
end
|
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: 3.0.
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.5'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: ruby_llm
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: activesupport
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,6 +149,20 @@ dependencies:
|
|
|
163
149
|
- - "~>"
|
|
164
150
|
- !ruby/object:Gem::Version
|
|
165
151
|
version: '1.0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: lefthook
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.1'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.1'
|
|
166
166
|
description: Production-grade coding agent with tool execution, middleware pipeline,
|
|
167
167
|
context compaction, session persistence, and multi-provider LLM support.
|
|
168
168
|
executables: []
|
|
@@ -173,6 +173,11 @@ files:
|
|
|
173
173
|
- lib/brute/events/handler.rb
|
|
174
174
|
- lib/brute/events/prefixed_terminal_output.rb
|
|
175
175
|
- lib/brute/events/terminal_output_handler.rb
|
|
176
|
+
- lib/brute/message_transport.rb
|
|
177
|
+
- lib/brute/message_transport/anthropic.rb
|
|
178
|
+
- lib/brute/message_transport/llm.rb
|
|
179
|
+
- lib/brute/message_transport/openai.rb
|
|
180
|
+
- lib/brute/message_transport/ruby_llm.rb
|
|
176
181
|
- lib/brute/messages.rb
|
|
177
182
|
- lib/brute/middleware/001_otel_span.rb
|
|
178
183
|
- lib/brute/middleware/002_session_log.rb
|
|
@@ -234,6 +239,7 @@ files:
|
|
|
234
239
|
- lib/brute/rack/adapter.rb
|
|
235
240
|
- lib/brute/skill.rb
|
|
236
241
|
- lib/brute/system_prompt.rb
|
|
242
|
+
- lib/brute/tool.rb
|
|
237
243
|
- lib/brute/tools.rb
|
|
238
244
|
- lib/brute/tools/adapter.rb
|
|
239
245
|
- lib/brute/tools/fs/file_mutation_queue.rb
|
|
@@ -260,7 +266,6 @@ files:
|
|
|
260
266
|
- lib/brute/version.rb
|
|
261
267
|
- lib/brute_cli/providers/shell.rb
|
|
262
268
|
- lib/brute_cli/providers/shell_response.rb
|
|
263
|
-
- lib/ruby_llm/message_transport.rb
|
|
264
269
|
homepage: https://github.com/general-intelligence-systems/brute
|
|
265
270
|
licenses:
|
|
266
271
|
- MIT
|
|
@@ -282,5 +287,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
282
287
|
requirements: []
|
|
283
288
|
rubygems_version: 3.7.2
|
|
284
289
|
specification_version: 4
|
|
285
|
-
summary: A coding agent
|
|
290
|
+
summary: A framework-agnostic coding agent
|
|
286
291
|
test_files: []
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "brute"
|
|
5
|
-
|
|
6
|
-
module RubyLLM
|
|
7
|
-
# Transports the result of an LLM call into Brute's message format.
|
|
8
|
-
#
|
|
9
|
-
# Calling an LLM is trivial — `RubyLLM.chat.ask "..."` — so Brute has no
|
|
10
|
-
# "completion middleware". The terminal `run` of an agent pipeline is an
|
|
11
|
-
# inline proc that makes the LLM call itself. The only wrinkle: RubyLLM hands
|
|
12
|
-
# back its own objects (a Message, an array, a whole Chat transcript), while
|
|
13
|
-
# the rest of the stack — the turn manager, event handlers, tool loop and
|
|
14
|
-
# SessionLog persistence — works off `env[:messages]` (a plain message log;
|
|
15
|
-
# see Brute.log).
|
|
16
|
-
#
|
|
17
|
-
# This makes NO LLM call and does NO appending. It just wraps what the proc
|
|
18
|
-
# got back and yields each message in Brute's format; the proc appends:
|
|
19
|
-
#
|
|
20
|
-
# response = provider.complete(env[:messages], ...) # one Message
|
|
21
|
-
# RubyLLM::MessageTransport.new(response).wrap_each do |message|
|
|
22
|
-
# env[:messages] << message
|
|
23
|
-
# end
|
|
24
|
-
#
|
|
25
|
-
# before = chat.messages.length # a Chat loop
|
|
26
|
-
# chat.complete
|
|
27
|
-
# RubyLLM::MessageTransport.new(chat.messages[before..]).wrap_each do |message|
|
|
28
|
-
# env[:messages] << message
|
|
29
|
-
# end
|
|
30
|
-
class MessageTransport
|
|
31
|
-
# Convenience: RubyLLM::MessageTransport.wrap_each(result) { |m| ... }
|
|
32
|
-
def self.wrap_each(result, &block)
|
|
33
|
-
new(result).wrap_each(&block)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def initialize(result)
|
|
37
|
-
@result = result
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Yield each result message in Brute's format. Without a block, returns an
|
|
41
|
-
# Enumerator. The caller decides what to do with each (typically append to
|
|
42
|
-
# env[:messages]).
|
|
43
|
-
def wrap_each
|
|
44
|
-
return enum_for(:wrap_each) unless block_given?
|
|
45
|
-
|
|
46
|
-
messages.each { |message| yield wrap(message) }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# The result normalized to a flat list of messages. A single Message, an
|
|
50
|
-
# array, or anything transcript-shaped (a Chat responds to #messages).
|
|
51
|
-
def messages
|
|
52
|
-
case @result
|
|
53
|
-
when ::RubyLLM::Message then [@result]
|
|
54
|
-
when Array then @result.compact
|
|
55
|
-
else @result.respond_to?(:messages) ? @result.messages : Array(@result)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
# The Brute-format view of a single message. RubyLLM::Message already IS
|
|
62
|
-
# Brute's message format, so this is the identity seam — the place to
|
|
63
|
-
# normalize if the two formats ever diverge.
|
|
64
|
-
def wrap(message)
|
|
65
|
-
message
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
__END__
|
|
71
|
-
|
|
72
|
-
describe "ruby_llm/message_transport" do
|
|
73
|
-
require "brute/messages"
|
|
74
|
-
|
|
75
|
-
it "wraps a single message" do
|
|
76
|
-
message = RubyLLM::Message.new(role: :assistant, content: "hi")
|
|
77
|
-
RubyLLM::MessageTransport.new(message).wrap_each.to_a.should == [message]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
it "wraps an array of messages" do
|
|
81
|
-
a = RubyLLM::Message.new(role: :assistant, content: "one")
|
|
82
|
-
b = RubyLLM::Message.new(role: :tool, content: "two", tool_call_id: "tc1")
|
|
83
|
-
RubyLLM::MessageTransport.new([a, b]).wrap_each.to_a.should == [a, b]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
it "wraps a transcript-shaped object (a Chat)" do
|
|
87
|
-
fake_chat = Class.new do
|
|
88
|
-
attr_reader :messages
|
|
89
|
-
def initialize(messages); @messages = messages; end
|
|
90
|
-
end
|
|
91
|
-
msgs = [RubyLLM::Message.new(role: :user, content: "hi"),
|
|
92
|
-
RubyLLM::Message.new(role: :assistant, content: "hello")]
|
|
93
|
-
|
|
94
|
-
RubyLLM::MessageTransport.new(fake_chat.new(msgs)).wrap_each.to_a.map(&:role).should == [:user, :assistant]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "yields each message to the block for the caller to append" do
|
|
98
|
-
session = Brute.log
|
|
99
|
-
session.user("hello")
|
|
100
|
-
response = RubyLLM::Message.new(role: :assistant, content: "hi there")
|
|
101
|
-
|
|
102
|
-
RubyLLM::MessageTransport.new(response).wrap_each { |message| session << message }
|
|
103
|
-
|
|
104
|
-
session.last.role.should == :assistant
|
|
105
|
-
session.last.content.should == "hi there"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
it "exposes a class-level wrap_each convenience" do
|
|
109
|
-
session = Brute.log
|
|
110
|
-
a = RubyLLM::Message.new(role: :assistant, content: "a")
|
|
111
|
-
b = RubyLLM::Message.new(role: :assistant, content: "b")
|
|
112
|
-
|
|
113
|
-
RubyLLM::MessageTransport.wrap_each([a, b]) { |message| session << message }
|
|
114
|
-
|
|
115
|
-
session.map(&:content).should == ["a", "b"]
|
|
116
|
-
end
|
|
117
|
-
end
|