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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/events/handler.rb +3 -1
  3. data/lib/brute/events/prefixed_terminal_output.rb +3 -1
  4. data/lib/brute/events/terminal_output_handler.rb +3 -1
  5. data/lib/brute/messages.rb +47 -0
  6. data/lib/brute/middleware/001_otel_span.rb +4 -2
  7. data/lib/brute/middleware/002_session_log.rb +100 -0
  8. data/lib/brute/middleware/004_summarize.rb +14 -12
  9. data/lib/brute/middleware/006_loop.rb +158 -0
  10. data/lib/brute/middleware/010_max_iterations.rb +8 -6
  11. data/lib/brute/middleware/015_otel_token_usage.rb +3 -1
  12. data/lib/brute/middleware/020_system_prompt.rb +5 -3
  13. data/lib/brute/middleware/040_compaction_check.rb +3 -1
  14. data/lib/brute/middleware/060_questions.rb +3 -1
  15. data/lib/brute/middleware/{070_tool_call.rb → 070_tool_pipeline.rb} +31 -20
  16. data/lib/brute/middleware/073_otel_tool_call.rb +3 -1
  17. data/lib/brute/middleware/075_otel_tool_results.rb +3 -1
  18. data/lib/brute/middleware/event_handler.rb +3 -1
  19. data/lib/brute/middleware/user_queue.rb +3 -1
  20. data/lib/brute/prompts/autonomy.rb +3 -0
  21. data/lib/brute/prompts/base.rb +3 -0
  22. data/lib/brute/prompts/build_switch.rb +3 -1
  23. data/lib/brute/prompts/code_references.rb +3 -0
  24. data/lib/brute/prompts/code_style.rb +3 -0
  25. data/lib/brute/prompts/conventions.rb +3 -0
  26. data/lib/brute/prompts/doing_tasks.rb +3 -0
  27. data/lib/brute/prompts/editing_approach.rb +3 -0
  28. data/lib/brute/prompts/editing_constraints.rb +3 -0
  29. data/lib/brute/prompts/environment.rb +3 -1
  30. data/lib/brute/prompts/frontend_tasks.rb +3 -0
  31. data/lib/brute/prompts/git_safety.rb +3 -0
  32. data/lib/brute/prompts/identity.rb +3 -1
  33. data/lib/brute/prompts/instructions.rb +3 -1
  34. data/lib/brute/prompts/max_steps.rb +3 -1
  35. data/lib/brute/prompts/objectivity.rb +3 -0
  36. data/lib/brute/prompts/plan_reminder.rb +3 -1
  37. data/lib/brute/prompts/proactiveness.rb +3 -0
  38. data/lib/brute/prompts/security_and_safety.rb +3 -0
  39. data/lib/brute/prompts/skills.rb +6 -2
  40. data/lib/brute/prompts/task_management.rb +3 -0
  41. data/lib/brute/prompts/tone_and_style.rb +3 -0
  42. data/lib/brute/prompts/tool_usage.rb +3 -0
  43. data/lib/brute/prompts.rb +5 -0
  44. data/lib/brute/rack/adapter.rb +237 -0
  45. data/lib/brute/skill.rb +192 -15
  46. data/lib/brute/system_prompt.rb +3 -1
  47. data/lib/brute/tools/adapter.rb +296 -0
  48. data/lib/brute/tools/fs/file_mutation_queue.rb +107 -0
  49. data/lib/brute/tools/fs/snapshot_store.rb +41 -0
  50. data/lib/brute/tools/fs_patch.rb +5 -3
  51. data/lib/brute/tools/fs_read.rb +4 -2
  52. data/lib/brute/tools/fs_remove.rb +2 -2
  53. data/lib/brute/tools/fs_search.rb +3 -1
  54. data/lib/brute/tools/fs_undo.rb +2 -2
  55. data/lib/brute/tools/fs_write.rb +5 -3
  56. data/lib/brute/tools/shell.rb +3 -1
  57. data/lib/brute/tools/skill_load.rb +156 -0
  58. data/lib/brute/tools/sub_agent.rb +118 -0
  59. data/lib/brute/tools/todo_list/store.rb +36 -0
  60. data/lib/brute/tools/todo_read.rb +1 -1
  61. data/lib/brute/tools/todo_write.rb +1 -1
  62. data/lib/brute/tools.rb +2 -1
  63. data/lib/brute/truncation.rb +3 -1
  64. data/lib/brute/turn/agent_pipeline.rb +168 -0
  65. data/lib/brute/turn/pipeline.rb +95 -0
  66. data/lib/brute/turn/tool_pipeline.rb +106 -0
  67. data/lib/brute/utils/diff.rb +3 -1
  68. data/lib/brute/version.rb +1 -1
  69. data/lib/brute.rb +46 -71
  70. data/lib/{brute → brute_cli}/providers/shell.rb +4 -1
  71. data/lib/{brute → brute_cli}/providers/shell_response.rb +3 -5
  72. data/lib/ruby_llm/message_transport.rb +117 -0
  73. metadata +34 -30
  74. data/lib/brute/agent.rb +0 -82
  75. data/lib/brute/middleware/003_tool_result_loop.rb +0 -103
  76. data/lib/brute/middleware/100_llm_call.rb +0 -63
  77. data/lib/brute/pipeline.rb +0 -97
  78. data/lib/brute/queue/file_mutation_queue.rb +0 -102
  79. data/lib/brute/session.rb +0 -51
  80. data/lib/brute/store/snapshot_store.rb +0 -36
  81. data/lib/brute/store/todo_store.rb +0 -30
  82. data/lib/brute/sub_agent.rb +0 -106
  83. 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
@@ -27,7 +27,9 @@ module Brute
27
27
  end
28
28
  end
29
29
 
30
- test do
30
+ __END__
31
+
32
+ describe "brute/utils/diff" do
31
33
  it "generates a unified diff for changed content" do
32
34
  Brute::Diff.unified("line1\nold\nline3\n", "line1\nnew\nline3\n").should =~ /\-old/
33
35
  end
data/lib/brute/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brute
4
- VERSION = "2.0.6"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/brute.rb CHANGED
@@ -1,22 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm'
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
- # Brute — a coding agent built on ruby_llm
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
- def self.config
33
- @config ||= begin
34
- RubyLLM.configure do |config|
35
- # Anthropic
36
- config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
37
- config.anthropic_api_base = ENV['ANTHROPIC_API_BASE'] # Available in v1.13.0+ (optional custom Anthropic endpoint)
38
-
39
- # Azure
40
- config.azure_api_base = ENV['AZURE_API_BASE'] # Microsoft Foundry project endpoint
41
- config.azure_api_key = ENV['AZURE_API_KEY'] # use this or
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 Brute
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
- if __FILE__ == $0
4
- require "bundler/setup"
5
- require "brute"
6
- end
3
+ require "bundler/setup"
4
+ require "brute"
7
5
 
8
6
  require "securerandom"
9
7
 
10
- module Brute
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: 2.0.6
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-02 00:00:00.000000000 Z
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/003_tool_result_loop.rb
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/070_tool_call.rb
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/providers/shell.rb
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: 4.0.6
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: []