brute 2.0.6 → 3.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/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/messages.rb +47 -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 +14 -12
- data/lib/brute/middleware/006_loop.rb +158 -0
- data/lib/brute/middleware/010_max_iterations.rb +8 -6
- data/lib/brute/middleware/015_otel_token_usage.rb +3 -1
- data/lib/brute/middleware/020_system_prompt.rb +5 -3
- data/lib/brute/middleware/040_compaction_check.rb +3 -1
- data/lib/brute/middleware/060_questions.rb +3 -1
- data/lib/brute/middleware/{070_tool_call.rb → 070_tool_pipeline.rb} +31 -20
- 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/tools/adapter.rb +296 -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 +5 -3
- data/lib/brute/tools/fs_read.rb +4 -2
- data/lib/brute/tools/fs_remove.rb +2 -2
- data/lib/brute/tools/fs_search.rb +3 -1
- data/lib/brute/tools/fs_undo.rb +2 -2
- data/lib/brute/tools/fs_write.rb +5 -3
- data/lib/brute/tools/shell.rb +3 -1
- data/lib/brute/tools/skill_load.rb +156 -0
- data/lib/brute/tools/sub_agent.rb +118 -0
- data/lib/brute/tools/todo_list/store.rb +36 -0
- data/lib/brute/tools/todo_read.rb +1 -1
- data/lib/brute/tools/todo_write.rb +1 -1
- data/lib/brute/tools.rb +2 -1
- data/lib/brute/truncation.rb +3 -1
- data/lib/brute/turn/agent_pipeline.rb +168 -0
- data/lib/brute/turn/pipeline.rb +95 -0
- data/lib/brute/turn/tool_pipeline.rb +106 -0
- data/lib/brute/utils/diff.rb +3 -1
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +46 -71
- data/lib/{brute → brute_cli}/providers/shell.rb +4 -1
- data/lib/{brute → brute_cli}/providers/shell_response.rb +3 -5
- data/lib/ruby_llm/message_transport.rb +117 -0
- metadata +34 -30
- 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
- data/lib/brute/tool.rb +0 -107
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
5
|
+
|
|
6
|
+
module Brute
|
|
7
|
+
module Turn
|
|
8
|
+
class Pipeline < ::Rack::Builder
|
|
9
|
+
module Chainable
|
|
10
|
+
# Enables the following syntax:
|
|
11
|
+
#
|
|
12
|
+
# Brute.agent
|
|
13
|
+
# .use(UltraSecurity)
|
|
14
|
+
# .use(MaxProfit)
|
|
15
|
+
# .use(DontTellMom)
|
|
16
|
+
# .run -> (env) {
|
|
17
|
+
# RubyLLM.chat.ask("How to make money?")
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
def use(...) = tap { super }
|
|
21
|
+
def run(...) = tap { super }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
include Chainable
|
|
25
|
+
|
|
26
|
+
# Rack's `new_from_string` evaluates the script then returns `to_app` —
|
|
27
|
+
# the built callable. An agent, though, is the *builder*: you `.start` it,
|
|
28
|
+
# and it may be re-`use`d or served through the Rack adapter. So evaluate
|
|
29
|
+
# the script against a fresh builder and hand back the builder itself.
|
|
30
|
+
# This also backs `parse_file` (→ `load_file` → here), so
|
|
31
|
+
# `AgentPipeline.parse_file("agent.ru").start(prompt)` works as documented.
|
|
32
|
+
def self.new_from_string(builder_script, path = "(rackup)", **options)
|
|
33
|
+
builder = new(**options)
|
|
34
|
+
eval(builder_script, ::Rack::BUILDER_TOPLEVEL_BINDING.call(builder), path) # rubocop:disable Security/Eval
|
|
35
|
+
builder
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Brute's name for Rack's `to_app`: nest the middleware around the
|
|
39
|
+
# terminal app and return the runnable callable. Raises (via `to_app`)
|
|
40
|
+
# when `run` was never called.
|
|
41
|
+
alias_method :build, :to_app
|
|
42
|
+
|
|
43
|
+
# Default null sink for env[:events] — swallows anything pushed to it.
|
|
44
|
+
class NullSink
|
|
45
|
+
def <<(_event); self; end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
__END__
|
|
52
|
+
|
|
53
|
+
describe "brute/turn/pipeline" do
|
|
54
|
+
it "builds and calls a chain" do
|
|
55
|
+
seen = []
|
|
56
|
+
inc = Class.new do
|
|
57
|
+
def initialize(app, label:); @app = app; @label = label; end
|
|
58
|
+
def call(env); env[:trace] << @label; @app.call(env); env[:trace] << "#{@label}-after"; end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
pipeline = Brute::Turn::Pipeline.new do
|
|
62
|
+
use inc, label: "outer"
|
|
63
|
+
use inc, label: "inner"
|
|
64
|
+
run ->(env) { env[:trace] << "core" }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
env = { trace: [] }
|
|
68
|
+
pipeline.call(env)
|
|
69
|
+
env[:trace].should == ["outer", "inner", "core", "inner-after", "outer-after"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "raises when run was never called" do
|
|
73
|
+
lambda { Brute::Turn::Pipeline.new.call({}) }.should.raise(RuntimeError)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "accepts a callable as the terminal app" do
|
|
77
|
+
pipeline = Brute::Turn::Pipeline.new do
|
|
78
|
+
run ->(env) { env[:result] = 42 }
|
|
79
|
+
end
|
|
80
|
+
env = {}
|
|
81
|
+
pipeline.call(env)
|
|
82
|
+
env[:result].should == 42
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "run accepts a block terminal and stays chainable" do
|
|
86
|
+
pipeline = Brute::Turn::Pipeline.new.use(Class.new do
|
|
87
|
+
def initialize(app); @app = app; end
|
|
88
|
+
def call(env); env[:trace] << "mw"; @app.call(env); end
|
|
89
|
+
end).run { |env| env[:trace] << "core" }
|
|
90
|
+
|
|
91
|
+
env = { trace: [] }
|
|
92
|
+
pipeline.call(env)
|
|
93
|
+
env[:trace].should == ["mw", "core"]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
5
|
+
require "brute/turn/pipeline"
|
|
6
|
+
|
|
7
|
+
module Brute
|
|
8
|
+
module Turn
|
|
9
|
+
# A ToolPipeline runs a tool call through a middleware stack. Like
|
|
10
|
+
# AgentPipeline it *composes* a Pipeline rather than inheriting: the
|
|
11
|
+
# definition block is instance_eval'd into the internal Pipeline, so `use`
|
|
12
|
+
# / `run` inside it are the builder's methods. The tool's terminal app does
|
|
13
|
+
# the work; middleware wraps it with concerns like file mutation queueing,
|
|
14
|
+
# validation, logging.
|
|
15
|
+
#
|
|
16
|
+
# Coexists with Brute::Tools::* (which inherit from RubyLLM::Tool). Use a
|
|
17
|
+
# ToolPipeline when you want middleware; use RubyLLM::Tool subclasses for
|
|
18
|
+
# simple cases.
|
|
19
|
+
#
|
|
20
|
+
# read = Brute::Turn::ToolPipeline.new(
|
|
21
|
+
# name: "read",
|
|
22
|
+
# description: "Read a file's contents",
|
|
23
|
+
# params: { file_path: { type: "string", required: true } },
|
|
24
|
+
# ) do
|
|
25
|
+
# use Brute::Middleware::Tool::ValidateParams
|
|
26
|
+
# run ->(env) {
|
|
27
|
+
# env[:result] = File.read(File.expand_path(env[:arguments][:file_path]))
|
|
28
|
+
# }
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# read.call(file_path: "lib/brute.rb")
|
|
32
|
+
#
|
|
33
|
+
class ToolPipeline
|
|
34
|
+
attr_reader :name, :description, :params
|
|
35
|
+
|
|
36
|
+
def initialize(name:, description:, params: {}, &block)
|
|
37
|
+
@name = name.to_s
|
|
38
|
+
@description = description
|
|
39
|
+
@params = params
|
|
40
|
+
@pipeline = Pipeline.new
|
|
41
|
+
@pipeline.instance_eval(&block) if block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(events: Pipeline::NullSink.new, **arguments)
|
|
45
|
+
env = {
|
|
46
|
+
name: @name,
|
|
47
|
+
arguments: arguments,
|
|
48
|
+
result: nil,
|
|
49
|
+
events: events,
|
|
50
|
+
metadata: {},
|
|
51
|
+
}
|
|
52
|
+
@pipeline.call(env)
|
|
53
|
+
env[:result]
|
|
54
|
+
end
|
|
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
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
__END__
|
|
71
|
+
|
|
72
|
+
describe "brute/turn/tool_pipeline" do
|
|
73
|
+
it "exposes name, description, params" do
|
|
74
|
+
t = Brute::Turn::ToolPipeline.new(name: "echo", description: "echo input") do
|
|
75
|
+
run ->(env) { env[:result] = env[:arguments][:msg] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
t.name.should == "echo"
|
|
79
|
+
t.description.should == "echo input"
|
|
80
|
+
t.call(msg: "hi").should == "hi"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "passes arguments through env to the terminal app" do
|
|
84
|
+
captured = nil
|
|
85
|
+
t = Brute::Turn::ToolPipeline.new(name: "x", description: "x") do
|
|
86
|
+
run ->(env) { captured = env[:arguments]; env[:result] = nil }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
t.call(a: 1, b: 2)
|
|
90
|
+
captured.should == { a: 1, b: 2 }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "runs middleware around the terminal app (instance_eval'd block)" do
|
|
94
|
+
wrap = Class.new do
|
|
95
|
+
def initialize(app, tag:); @app = app; @tag = tag; end
|
|
96
|
+
def call(env); (env[:metadata][:log] ||= []) << "in-#{@tag}"; @app.call(env); env[:metadata][:log] << "out-#{@tag}"; end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
t = Brute::Turn::ToolPipeline.new(name: "x", description: "x") do
|
|
100
|
+
use wrap, tag: "a"
|
|
101
|
+
run ->(env) { env[:metadata][:log] << "core"; env[:result] = :ok }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
t.call(input: 1).should == :ok
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/brute/utils/diff.rb
CHANGED
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
|
|
5
|
+
require "ruby_llm"
|
|
4
6
|
require "rack"
|
|
5
7
|
require 'timeout'
|
|
6
8
|
require 'logger'
|
|
7
|
-
require 'scampi/kernel_ext'
|
|
8
9
|
require 'colorize_extended'
|
|
9
10
|
require 'active_support/all'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
# Cross-cutting concerns are implemented as Rack-style middleware in a
|
|
14
|
-
# Pipeline that wraps every LLM call:
|
|
15
|
-
#
|
|
16
|
-
# Tracing → Retry → Session → Tokens → Compaction → ToolErrors → DoomLoop → Reasoning → [LLM Call]
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
require 'brute/version'
|
|
12
|
+
require_relative 'brute/version'
|
|
20
13
|
|
|
21
14
|
module Brute
|
|
22
15
|
LOGO = <<-LOGO
|
|
@@ -29,76 +22,58 @@ module Brute
|
|
|
29
22
|
`Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
|
|
30
23
|
LOGO
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
config.azure_ai_auth_token = ENV['AZURE_AI_AUTH_TOKEN'] # this
|
|
43
|
-
|
|
44
|
-
# Bedrock
|
|
45
|
-
config.bedrock_api_key = ENV['AWS_ACCESS_KEY_ID']
|
|
46
|
-
config.bedrock_secret_key = ENV['AWS_SECRET_ACCESS_KEY']
|
|
47
|
-
config.bedrock_region = ENV['AWS_REGION'] # Required for Bedrock
|
|
48
|
-
config.bedrock_session_token = ENV['AWS_SESSION_TOKEN'] # For temporary credentials
|
|
49
|
-
|
|
50
|
-
# DeepSeek
|
|
51
|
-
config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
|
|
52
|
-
config.deepseek_api_base = ENV['DEEPSEEK_API_BASE'] # Available in v1.13.0+ (optional custom DeepSeek endpoint)
|
|
53
|
-
|
|
54
|
-
# Gemini
|
|
55
|
-
config.gemini_api_key = ENV['GEMINI_API_KEY']
|
|
56
|
-
config.gemini_api_base = ENV['GEMINI_API_BASE'] # Available in v1.9.0+ (optional API version override)
|
|
57
|
-
|
|
58
|
-
# GPUStack
|
|
59
|
-
config.gpustack_api_base = ENV['GPUSTACK_API_BASE']
|
|
60
|
-
config.gpustack_api_key = ENV['GPUSTACK_API_KEY']
|
|
61
|
-
|
|
62
|
-
# Mistral
|
|
63
|
-
config.mistral_api_key = ENV['MISTRAL_API_KEY']
|
|
64
|
-
|
|
65
|
-
# Ollama
|
|
66
|
-
config.ollama_api_base = 'http://localhost:11434/v1'
|
|
67
|
-
config.ollama_api_key = ENV['OLLAMA_API_KEY'] # Available in v1.13.0+ (optional for authenticated/remote Ollama endpoints)
|
|
68
|
-
|
|
69
|
-
# OpenAI
|
|
70
|
-
config.openai_api_key = ENV['OPENAI_API_KEY']
|
|
71
|
-
config.openai_api_base = ENV['OPENAI_API_BASE'] # Optional custom OpenAI-compatible endpoint
|
|
72
|
-
|
|
73
|
-
# OpenRouter
|
|
74
|
-
config.openrouter_api_key = ENV['OPENROUTER_API_KEY']
|
|
75
|
-
config.openrouter_api_base = ENV['OPENROUTER_API_BASE'] # Available in v1.13.0+ (optional custom OpenRouter endpoint)
|
|
76
|
-
|
|
77
|
-
# Perplexity
|
|
78
|
-
config.perplexity_api_key = ENV['PERPLEXITY_API_KEY']
|
|
79
|
-
|
|
80
|
-
# Vertex AI
|
|
81
|
-
config.vertexai_project_id = ENV['GOOGLE_CLOUD_PROJECT'] # Available in v1.7.0+
|
|
82
|
-
config.vertexai_location = ENV['GOOGLE_CLOUD_LOCATION']
|
|
83
|
-
config.vertexai_service_account_key = ENV['VERTEXAI_SERVICE_ACCOUNT_KEY'] # Optional: service account JSON key
|
|
84
|
-
|
|
85
|
-
# xAI
|
|
86
|
-
config.xai_api_key = ENV['XAI_API_KEY'] # Available in v1.11.0+
|
|
87
|
-
end
|
|
88
|
-
RubyLLM.config
|
|
89
|
-
end
|
|
90
|
-
end
|
|
25
|
+
# NOTE: Brute owns no LLM configuration. Calling an LLM is just
|
|
26
|
+
# `RubyLLM.chat.ask "..."`, and all provider/model/credential config lives
|
|
27
|
+
# in the pipeline's terminal `run` proc — typically via `RubyLLM.context`:
|
|
28
|
+
#
|
|
29
|
+
# run ->(env) do
|
|
30
|
+
# context = RubyLLM.context { |c| c.ollama_api_base = ENV["OLLAMA_API_BASE"] }
|
|
31
|
+
# ...
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# See examples/agents/01_basic_agent.rb.
|
|
91
35
|
|
|
92
36
|
def self.provider
|
|
93
37
|
@provider ||= :anthropic
|
|
94
38
|
end
|
|
39
|
+
|
|
40
|
+
# Start building an agent turn. Returns an AgentPipeline — a rack-style
|
|
41
|
+
# builder that is also the runnable Agent: chain `.use` for middleware and
|
|
42
|
+
# `.run` for the terminal LLM-call proc (both return the AgentPipeline), then
|
|
43
|
+
# invoke it with `#start`. It takes no config — LLM config lives in the `run`
|
|
44
|
+
# proc, tools go to the ToolPipeline middleware, the log to SessionLog.
|
|
45
|
+
#
|
|
46
|
+
# agent = Brute.agent
|
|
47
|
+
# .use(Brute::Middleware::SystemPrompt)
|
|
48
|
+
# .use(Brute::Middleware::ToolPipeline, tools: Brute::Tools::ALL)
|
|
49
|
+
# .run ->(env) { ... } # the LLM-call proc (provider/model/creds here)
|
|
50
|
+
#
|
|
51
|
+
# agent.start("what changed?")
|
|
52
|
+
#
|
|
53
|
+
# A block form is equivalent (evaluated in the AgentPipeline's context):
|
|
54
|
+
#
|
|
55
|
+
# Brute.agent do
|
|
56
|
+
# use Brute::Middleware::SystemPrompt
|
|
57
|
+
# run ->(env) { ... }
|
|
58
|
+
# end
|
|
59
|
+
def self.agent(&block)
|
|
60
|
+
Brute::Turn::AgentPipeline.new(&block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Adapt any Brute tools (hashes, Brute::Turn::ToolPipeline, SubAgent, RubyLLM::Tool …)
|
|
64
|
+
# into a { name_sym => RubyLLM::Tool } hash — the shape an inline `run`
|
|
65
|
+
# proc hands to RubyLLM.chat.with_tools or a provider #complete call.
|
|
66
|
+
def self.rubyllm_tools(tools)
|
|
67
|
+
Brute::Tools::Adapter.wrap_all(tools || []).transform_values(&:to_ruby_llm)
|
|
68
|
+
end
|
|
95
69
|
|
|
96
70
|
def self.provider=(p)
|
|
97
71
|
@provider = p.to_sym
|
|
98
72
|
end
|
|
99
73
|
end
|
|
100
74
|
|
|
101
|
-
Dir.glob("#{__dir__}/brute/**/*.rb").sort.each do |path|
|
|
75
|
+
Dir.glob("#{__dir__}/{brute,ruby_llm}/**/*.rb").sort.each do |path|
|
|
102
76
|
require path
|
|
103
77
|
end
|
|
104
78
|
|
|
79
|
+
# gangsta g-dogg bruh...
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
5
|
+
|
|
3
6
|
require "shellwords"
|
|
4
7
|
|
|
5
|
-
module
|
|
8
|
+
module BruteCLI
|
|
6
9
|
module Providers
|
|
7
10
|
# A pseudo-LLM provider that executes user input as code via the
|
|
8
11
|
# existing Brute::Tools::Shell tool.
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "brute"
|
|
6
|
-
end
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
7
5
|
|
|
8
6
|
require "securerandom"
|
|
9
7
|
|
|
10
|
-
module
|
|
8
|
+
module BruteCLI
|
|
11
9
|
module Providers
|
|
12
10
|
# Synthetic completion response returned by Brute::Providers::Shell.
|
|
13
11
|
#
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 1980-01-
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -51,20 +51,6 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: scampi
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
55
|
name: activesupport
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,6 +149,20 @@ dependencies:
|
|
|
163
149
|
- - "~>"
|
|
164
150
|
- !ruby/object:Gem::Version
|
|
165
151
|
version: '13.0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: scampi
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '1.0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '1.0'
|
|
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: []
|
|
@@ -170,26 +170,25 @@ extensions: []
|
|
|
170
170
|
extra_rdoc_files: []
|
|
171
171
|
files:
|
|
172
172
|
- lib/brute.rb
|
|
173
|
-
- lib/brute/agent.rb
|
|
174
173
|
- lib/brute/events/handler.rb
|
|
175
174
|
- lib/brute/events/prefixed_terminal_output.rb
|
|
176
175
|
- lib/brute/events/terminal_output_handler.rb
|
|
176
|
+
- lib/brute/messages.rb
|
|
177
177
|
- lib/brute/middleware/001_otel_span.rb
|
|
178
|
-
- lib/brute/middleware/
|
|
178
|
+
- lib/brute/middleware/002_session_log.rb
|
|
179
179
|
- lib/brute/middleware/004_summarize.rb
|
|
180
180
|
- lib/brute/middleware/005_tracing.rb
|
|
181
|
+
- lib/brute/middleware/006_loop.rb
|
|
181
182
|
- lib/brute/middleware/010_max_iterations.rb
|
|
182
183
|
- lib/brute/middleware/015_otel_token_usage.rb
|
|
183
184
|
- lib/brute/middleware/020_system_prompt.rb
|
|
184
185
|
- lib/brute/middleware/040_compaction_check.rb
|
|
185
186
|
- lib/brute/middleware/060_questions.rb
|
|
186
|
-
- lib/brute/middleware/
|
|
187
|
+
- lib/brute/middleware/070_tool_pipeline.rb
|
|
187
188
|
- lib/brute/middleware/073_otel_tool_call.rb
|
|
188
189
|
- lib/brute/middleware/075_otel_tool_results.rb
|
|
189
|
-
- lib/brute/middleware/100_llm_call.rb
|
|
190
190
|
- lib/brute/middleware/event_handler.rb
|
|
191
191
|
- lib/brute/middleware/user_queue.rb
|
|
192
|
-
- lib/brute/pipeline.rb
|
|
193
192
|
- lib/brute/prompts.rb
|
|
194
193
|
- lib/brute/prompts/autonomy.rb
|
|
195
194
|
- lib/brute/prompts/base.rb
|
|
@@ -232,17 +231,13 @@ files:
|
|
|
232
231
|
- lib/brute/prompts/text/tool_usage/google.txt
|
|
233
232
|
- lib/brute/prompts/tone_and_style.rb
|
|
234
233
|
- lib/brute/prompts/tool_usage.rb
|
|
235
|
-
- lib/brute/
|
|
236
|
-
- lib/brute/providers/shell_response.rb
|
|
237
|
-
- lib/brute/queue/file_mutation_queue.rb
|
|
238
|
-
- lib/brute/session.rb
|
|
234
|
+
- lib/brute/rack/adapter.rb
|
|
239
235
|
- lib/brute/skill.rb
|
|
240
|
-
- lib/brute/store/snapshot_store.rb
|
|
241
|
-
- lib/brute/store/todo_store.rb
|
|
242
|
-
- lib/brute/sub_agent.rb
|
|
243
236
|
- lib/brute/system_prompt.rb
|
|
244
|
-
- lib/brute/tool.rb
|
|
245
237
|
- lib/brute/tools.rb
|
|
238
|
+
- lib/brute/tools/adapter.rb
|
|
239
|
+
- lib/brute/tools/fs/file_mutation_queue.rb
|
|
240
|
+
- lib/brute/tools/fs/snapshot_store.rb
|
|
246
241
|
- lib/brute/tools/fs_patch.rb
|
|
247
242
|
- lib/brute/tools/fs_read.rb
|
|
248
243
|
- lib/brute/tools/fs_remove.rb
|
|
@@ -252,11 +247,20 @@ files:
|
|
|
252
247
|
- lib/brute/tools/net_fetch.rb
|
|
253
248
|
- lib/brute/tools/question.rb
|
|
254
249
|
- lib/brute/tools/shell.rb
|
|
250
|
+
- lib/brute/tools/skill_load.rb
|
|
251
|
+
- lib/brute/tools/sub_agent.rb
|
|
252
|
+
- lib/brute/tools/todo_list/store.rb
|
|
255
253
|
- lib/brute/tools/todo_read.rb
|
|
256
254
|
- lib/brute/tools/todo_write.rb
|
|
257
255
|
- lib/brute/truncation.rb
|
|
256
|
+
- lib/brute/turn/agent_pipeline.rb
|
|
257
|
+
- lib/brute/turn/pipeline.rb
|
|
258
|
+
- lib/brute/turn/tool_pipeline.rb
|
|
258
259
|
- lib/brute/utils/diff.rb
|
|
259
260
|
- lib/brute/version.rb
|
|
261
|
+
- lib/brute_cli/providers/shell.rb
|
|
262
|
+
- lib/brute_cli/providers/shell_response.rb
|
|
263
|
+
- lib/ruby_llm/message_transport.rb
|
|
260
264
|
homepage: https://github.com/general-intelligence-systems/brute
|
|
261
265
|
licenses:
|
|
262
266
|
- MIT
|
|
@@ -276,7 +280,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
276
280
|
- !ruby/object:Gem::Version
|
|
277
281
|
version: '0'
|
|
278
282
|
requirements: []
|
|
279
|
-
rubygems_version:
|
|
283
|
+
rubygems_version: 3.7.2
|
|
280
284
|
specification_version: 4
|
|
281
285
|
summary: A coding agent built on ruby_llm
|
|
282
286
|
test_files: []
|