brute 1.0.1 → 2.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +74 -6
  3. data/lib/brute/events/handler.rb +69 -0
  4. data/lib/brute/events/prefixed_terminal_output.rb +72 -0
  5. data/lib/brute/events/terminal_output_handler.rb +68 -0
  6. data/lib/brute/middleware/001_otel_span.rb +77 -0
  7. data/lib/brute/middleware/003_tool_result_loop.rb +103 -0
  8. data/lib/brute/middleware/004_summarize.rb +139 -0
  9. data/lib/brute/middleware/005_tracing.rb +94 -0
  10. data/lib/brute/middleware/010_max_iterations.rb +73 -0
  11. data/lib/brute/middleware/015_otel_token_usage.rb +42 -0
  12. data/lib/brute/middleware/020_system_prompt.rb +128 -0
  13. data/lib/brute/middleware/040_compaction_check.rb +155 -0
  14. data/lib/brute/middleware/060_questions.rb +41 -0
  15. data/lib/brute/middleware/070_tool_call.rb +247 -0
  16. data/lib/brute/middleware/073_otel_tool_call.rb +49 -0
  17. data/lib/brute/middleware/075_otel_tool_results.rb +46 -0
  18. data/lib/brute/middleware/100_llm_call.rb +62 -0
  19. data/lib/brute/middleware/event_handler.rb +25 -0
  20. data/lib/brute/middleware/user_queue.rb +35 -0
  21. data/lib/brute/pipeline.rb +44 -107
  22. data/lib/brute/prompts/skills.rb +2 -2
  23. data/lib/brute/prompts.rb +23 -23
  24. data/lib/brute/providers/shell.rb +6 -19
  25. data/lib/brute/providers/shell_response.rb +22 -30
  26. data/lib/brute/session.rb +52 -0
  27. data/lib/brute/store/snapshot_store.rb +21 -37
  28. data/lib/brute/sub_agent.rb +106 -0
  29. data/lib/brute/system_prompt.rb +1 -83
  30. data/lib/brute/tool.rb +107 -0
  31. data/lib/brute/tools/delegate.rb +61 -70
  32. data/lib/brute/tools/fs_patch.rb +9 -7
  33. data/lib/brute/tools/fs_read.rb +233 -20
  34. data/lib/brute/tools/fs_remove.rb +8 -9
  35. data/lib/brute/tools/fs_search.rb +98 -16
  36. data/lib/brute/tools/fs_undo.rb +8 -8
  37. data/lib/brute/tools/fs_write.rb +7 -5
  38. data/lib/brute/tools/net_fetch.rb +8 -8
  39. data/lib/brute/tools/question.rb +36 -24
  40. data/lib/brute/tools/shell.rb +74 -16
  41. data/lib/brute/tools/todo_read.rb +8 -8
  42. data/lib/brute/tools/todo_write.rb +25 -18
  43. data/lib/brute/tools.rb +8 -12
  44. data/lib/brute/truncation.rb +219 -0
  45. data/lib/brute/version.rb +1 -1
  46. data/lib/brute.rb +82 -45
  47. metadata +59 -46
  48. data/lib/brute/loop/agent_stream.rb +0 -118
  49. data/lib/brute/loop/agent_turn.rb +0 -520
  50. data/lib/brute/loop/compactor.rb +0 -107
  51. data/lib/brute/loop/doom_loop.rb +0 -86
  52. data/lib/brute/loop/step.rb +0 -332
  53. data/lib/brute/loop/tool_call_step.rb +0 -90
  54. data/lib/brute/middleware/base.rb +0 -27
  55. data/lib/brute/middleware/compaction_check.rb +0 -106
  56. data/lib/brute/middleware/doom_loop_detection.rb +0 -136
  57. data/lib/brute/middleware/llm_call.rb +0 -128
  58. data/lib/brute/middleware/message_tracking.rb +0 -339
  59. data/lib/brute/middleware/otel/span.rb +0 -105
  60. data/lib/brute/middleware/otel/token_usage.rb +0 -68
  61. data/lib/brute/middleware/otel/tool_calls.rb +0 -68
  62. data/lib/brute/middleware/otel/tool_results.rb +0 -65
  63. data/lib/brute/middleware/otel.rb +0 -34
  64. data/lib/brute/middleware/reasoning_normalizer.rb +0 -192
  65. data/lib/brute/middleware/retry.rb +0 -157
  66. data/lib/brute/middleware/session_persistence.rb +0 -72
  67. data/lib/brute/middleware/token_tracking.rb +0 -124
  68. data/lib/brute/middleware/tool_error_tracking.rb +0 -179
  69. data/lib/brute/middleware/tool_use_guard.rb +0 -133
  70. data/lib/brute/middleware/tracing.rb +0 -124
  71. data/lib/brute/middleware.rb +0 -18
  72. data/lib/brute/orchestrator/turn.rb +0 -105
  73. data/lib/brute/patches/anthropic_tool_role.rb +0 -35
  74. data/lib/brute/patches/buffer_nil_guard.rb +0 -26
  75. data/lib/brute/providers/models_dev.rb +0 -111
  76. data/lib/brute/providers/ollama.rb +0 -135
  77. data/lib/brute/providers/opencode_go.rb +0 -43
  78. data/lib/brute/providers/opencode_zen.rb +0 -87
  79. data/lib/brute/providers.rb +0 -62
  80. data/lib/brute/queue/base_queue.rb +0 -222
  81. data/lib/brute/queue/parallel_queue.rb +0 -66
  82. data/lib/brute/queue/sequential_queue.rb +0 -63
  83. data/lib/brute/store/message_store.rb +0 -362
  84. data/lib/brute/store/session.rb +0 -106
  85. /data/lib/brute/{diff.rb → utils/diff.rb} +0 -0
data/lib/brute.rb CHANGED
@@ -1,66 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'llm'
3
+ require 'ruby_llm'
4
4
  require 'timeout'
5
5
  require 'logger'
6
6
  require 'scampi/kernel_ext'
7
+ require 'colorize_extended'
8
+ require 'active_support/all'
7
9
 
8
- # Brute — a coding agent built on llm.rb
10
+ # Brute — a coding agent built on ruby_llm
9
11
  #
10
12
  # Cross-cutting concerns are implemented as Rack-style middleware in a
11
13
  # Pipeline that wraps every LLM call:
12
14
  #
13
15
  # Tracing → Retry → Session → Tokens → Compaction → ToolErrors → DoomLoop → Reasoning → [LLM Call]
14
16
  #
15
- # Entry point:
16
- #
17
- # agent = Brute::Agent.new(provider:, model:, tools:, system_prompt:)
18
- # step = Brute::Loop::AgentTurn.perform(agent:, session:, pipeline:, input:)
19
- #
20
- require_relative 'brute/version'
17
+
18
+ require 'brute/version'
21
19
 
22
20
  module Brute
23
- def self.provider
24
- @provider ||= Brute::Providers.guess_from_env
21
+ LOGO = <<-LOGO
22
+ .o8 .
23
+ "888 .o8
24
+ 888oooo. oooo d8b oooo oooo .o888oo .ooooo.
25
+ d88' `88b `888""8P `888 `888 888 d88' `88b
26
+ 888 888 888 888 888 888 888ooo888
27
+ 888 888 888 888 888 888 . 888 .o
28
+ `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
29
+ LOGO
30
+
31
+ def self.config
32
+ @config ||= begin
33
+ RubyLLM.configure do |config|
34
+ # Anthropic
35
+ config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
36
+ config.anthropic_api_base = ENV['ANTHROPIC_API_BASE'] # Available in v1.13.0+ (optional custom Anthropic endpoint)
37
+
38
+ # Azure
39
+ config.azure_api_base = ENV['AZURE_API_BASE'] # Microsoft Foundry project endpoint
40
+ config.azure_api_key = ENV['AZURE_API_KEY'] # use this or
41
+ config.azure_ai_auth_token = ENV['AZURE_AI_AUTH_TOKEN'] # this
42
+
43
+ # Bedrock
44
+ config.bedrock_api_key = ENV['AWS_ACCESS_KEY_ID']
45
+ config.bedrock_secret_key = ENV['AWS_SECRET_ACCESS_KEY']
46
+ config.bedrock_region = ENV['AWS_REGION'] # Required for Bedrock
47
+ config.bedrock_session_token = ENV['AWS_SESSION_TOKEN'] # For temporary credentials
48
+
49
+ # DeepSeek
50
+ config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
51
+ config.deepseek_api_base = ENV['DEEPSEEK_API_BASE'] # Available in v1.13.0+ (optional custom DeepSeek endpoint)
52
+
53
+ # Gemini
54
+ config.gemini_api_key = ENV['GEMINI_API_KEY']
55
+ config.gemini_api_base = ENV['GEMINI_API_BASE'] # Available in v1.9.0+ (optional API version override)
56
+
57
+ # GPUStack
58
+ config.gpustack_api_base = ENV['GPUSTACK_API_BASE']
59
+ config.gpustack_api_key = ENV['GPUSTACK_API_KEY']
60
+
61
+ # Mistral
62
+ config.mistral_api_key = ENV['MISTRAL_API_KEY']
63
+
64
+ # Ollama
65
+ config.ollama_api_base = 'http://localhost:11434/v1'
66
+ config.ollama_api_key = ENV['OLLAMA_API_KEY'] # Available in v1.13.0+ (optional for authenticated/remote Ollama endpoints)
67
+
68
+ # OpenAI
69
+ config.openai_api_key = ENV['OPENAI_API_KEY']
70
+ config.openai_api_base = ENV['OPENAI_API_BASE'] # Optional custom OpenAI-compatible endpoint
71
+
72
+ # OpenRouter
73
+ config.openrouter_api_key = ENV['OPENROUTER_API_KEY']
74
+ config.openrouter_api_base = ENV['OPENROUTER_API_BASE'] # Available in v1.13.0+ (optional custom OpenRouter endpoint)
75
+
76
+ # Perplexity
77
+ config.perplexity_api_key = ENV['PERPLEXITY_API_KEY']
78
+
79
+ # Vertex AI
80
+ config.vertexai_project_id = ENV['GOOGLE_CLOUD_PROJECT'] # Available in v1.7.0+
81
+ config.vertexai_location = ENV['GOOGLE_CLOUD_LOCATION']
82
+ config.vertexai_service_account_key = ENV['VERTEXAI_SERVICE_ACCOUNT_KEY'] # Optional: service account JSON key
83
+
84
+ # xAI
85
+ config.xai_api_key = ENV['XAI_API_KEY'] # Available in v1.11.0+
86
+ end
87
+ RubyLLM.config
88
+ end
25
89
  end
26
90
 
91
+ def self.provider
92
+ @provider ||= :anthropic
93
+ end
94
+
27
95
  def self.provider=(p)
28
- @provider = p
96
+ @provider = p.to_sym
29
97
  end
30
98
  end
31
99
 
32
- require_relative 'brute/diff'
33
- require_relative 'brute/skill'
34
- require_relative 'brute/prompts'
35
- require_relative 'brute/system_prompt'
36
- require_relative 'brute/pipeline'
37
- require_relative 'brute/agent'
38
-
39
- # Brute::Store
40
- require_relative 'brute/store/snapshot_store'
41
- require_relative 'brute/store/todo_store'
42
- require_relative 'brute/store/message_store'
43
- require_relative 'brute/store/session'
44
-
45
- # Brute::Loop (before Queue — queue tests reference Loop::Step)
46
- require_relative 'brute/loop/doom_loop'
47
- require_relative 'brute/loop/compactor'
48
- require_relative 'brute/loop/agent_stream'
49
- require_relative 'brute/loop/step'
50
- require_relative 'brute/loop/tool_call_step'
51
-
52
- # Brute::Queue
53
- require_relative 'brute/queue/file_mutation_queue'
54
- require_relative 'brute/queue/base_queue'
55
- require_relative 'brute/queue/sequential_queue'
56
- require_relative 'brute/queue/parallel_queue'
57
-
58
- # Brute::Loop (agent_turn depends on Queue)
59
- require_relative 'brute/loop/agent_turn'
60
-
61
- require_relative 'brute/patches/anthropic_tool_role'
62
- require_relative 'brute/patches/buffer_nil_guard'
100
+ Dir.glob("#{__dir__}/brute/**/*.rb").sort.each do |path|
101
+ require path
102
+ end
63
103
 
64
- require_relative 'brute/middleware'
65
- require_relative 'brute/tools'
66
- require_relative 'brute/providers'
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: 1.0.1
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
@@ -38,19 +38,19 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.5'
40
40
  - !ruby/object:Gem::Dependency
41
- name: llm.rb
41
+ name: ruby_llm
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '4.11'
46
+ version: '0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '4.11'
53
+ version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: scampi
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: activesupport
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: colorize-extended
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: rake
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -87,34 +115,24 @@ extra_rdoc_files: []
87
115
  files:
88
116
  - lib/brute.rb
89
117
  - lib/brute/agent.rb
90
- - lib/brute/diff.rb
91
- - lib/brute/loop/agent_stream.rb
92
- - lib/brute/loop/agent_turn.rb
93
- - lib/brute/loop/compactor.rb
94
- - lib/brute/loop/doom_loop.rb
95
- - lib/brute/loop/step.rb
96
- - lib/brute/loop/tool_call_step.rb
97
- - lib/brute/middleware.rb
98
- - lib/brute/middleware/base.rb
99
- - lib/brute/middleware/compaction_check.rb
100
- - lib/brute/middleware/doom_loop_detection.rb
101
- - lib/brute/middleware/llm_call.rb
102
- - lib/brute/middleware/message_tracking.rb
103
- - lib/brute/middleware/otel.rb
104
- - lib/brute/middleware/otel/span.rb
105
- - lib/brute/middleware/otel/token_usage.rb
106
- - lib/brute/middleware/otel/tool_calls.rb
107
- - lib/brute/middleware/otel/tool_results.rb
108
- - lib/brute/middleware/reasoning_normalizer.rb
109
- - lib/brute/middleware/retry.rb
110
- - lib/brute/middleware/session_persistence.rb
111
- - lib/brute/middleware/token_tracking.rb
112
- - lib/brute/middleware/tool_error_tracking.rb
113
- - lib/brute/middleware/tool_use_guard.rb
114
- - lib/brute/middleware/tracing.rb
115
- - lib/brute/orchestrator/turn.rb
116
- - lib/brute/patches/anthropic_tool_role.rb
117
- - lib/brute/patches/buffer_nil_guard.rb
118
+ - lib/brute/events/handler.rb
119
+ - lib/brute/events/prefixed_terminal_output.rb
120
+ - lib/brute/events/terminal_output_handler.rb
121
+ - lib/brute/middleware/001_otel_span.rb
122
+ - lib/brute/middleware/003_tool_result_loop.rb
123
+ - lib/brute/middleware/004_summarize.rb
124
+ - lib/brute/middleware/005_tracing.rb
125
+ - lib/brute/middleware/010_max_iterations.rb
126
+ - lib/brute/middleware/015_otel_token_usage.rb
127
+ - lib/brute/middleware/020_system_prompt.rb
128
+ - lib/brute/middleware/040_compaction_check.rb
129
+ - lib/brute/middleware/060_questions.rb
130
+ - lib/brute/middleware/070_tool_call.rb
131
+ - lib/brute/middleware/073_otel_tool_call.rb
132
+ - lib/brute/middleware/075_otel_tool_results.rb
133
+ - lib/brute/middleware/100_llm_call.rb
134
+ - lib/brute/middleware/event_handler.rb
135
+ - lib/brute/middleware/user_queue.rb
118
136
  - lib/brute/pipeline.rb
119
137
  - lib/brute/prompts.rb
120
138
  - lib/brute/prompts/autonomy.rb
@@ -158,23 +176,16 @@ files:
158
176
  - lib/brute/prompts/text/tool_usage/google.txt
159
177
  - lib/brute/prompts/tone_and_style.rb
160
178
  - lib/brute/prompts/tool_usage.rb
161
- - lib/brute/providers.rb
162
- - lib/brute/providers/models_dev.rb
163
- - lib/brute/providers/ollama.rb
164
- - lib/brute/providers/opencode_go.rb
165
- - lib/brute/providers/opencode_zen.rb
166
179
  - lib/brute/providers/shell.rb
167
180
  - lib/brute/providers/shell_response.rb
168
- - lib/brute/queue/base_queue.rb
169
181
  - lib/brute/queue/file_mutation_queue.rb
170
- - lib/brute/queue/parallel_queue.rb
171
- - lib/brute/queue/sequential_queue.rb
182
+ - lib/brute/session.rb
172
183
  - lib/brute/skill.rb
173
- - lib/brute/store/message_store.rb
174
- - lib/brute/store/session.rb
175
184
  - lib/brute/store/snapshot_store.rb
176
185
  - lib/brute/store/todo_store.rb
186
+ - lib/brute/sub_agent.rb
177
187
  - lib/brute/system_prompt.rb
188
+ - lib/brute/tool.rb
178
189
  - lib/brute/tools.rb
179
190
  - lib/brute/tools/delegate.rb
180
191
  - lib/brute/tools/fs_patch.rb
@@ -188,6 +199,8 @@ files:
188
199
  - lib/brute/tools/shell.rb
189
200
  - lib/brute/tools/todo_read.rb
190
201
  - lib/brute/tools/todo_write.rb
202
+ - lib/brute/truncation.rb
203
+ - lib/brute/utils/diff.rb
191
204
  - lib/brute/version.rb
192
205
  licenses:
193
206
  - MIT
@@ -199,7 +212,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
199
212
  requirements:
200
213
  - - ">="
201
214
  - !ruby/object:Gem::Version
202
- version: '3.2'
215
+ version: '3.4'
203
216
  required_rubygems_version: !ruby/object:Gem::Requirement
204
217
  requirements:
205
218
  - - ">="
@@ -208,5 +221,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
221
  requirements: []
209
222
  rubygems_version: 3.7.2
210
223
  specification_version: 4
211
- summary: A coding agent built on llm.rb
224
+ summary: A coding agent built on ruby_llm
212
225
  test_files: []
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "brute"
5
-
6
- module Brute
7
- module Loop
8
- # Bridges llm.rb's streaming callbacks to the host application.
9
- #
10
- # Text and reasoning chunks fire immediately as the LLM generates them.
11
- # Tool calls are collected but NOT executed — execution is deferred to the
12
- # agent loop after the stream completes. This ensures text is never
13
- # concurrent with tool execution.
14
- #
15
- # After the stream finishes, the agent loop reads +pending_tools+ to
16
- # dispatch all tool calls concurrently, then fires +on_tool_call_start+
17
- # once with the full batch.
18
- #
19
- class AgentStream < LLM::Stream
20
- # Tool call metadata recorded during streaming, used by ToolUseGuard
21
- # when ctx.functions is empty (nil-choice bug in llm.rb).
22
- attr_reader :pending_tool_calls
23
-
24
- # Deferred tool/error pairs: [(LLM::Function, error_or_nil), ...]
25
- # The agent loop reads these after the stream completes.
26
- attr_reader :pending_tools
27
-
28
- def initialize(on_content: nil, on_reasoning: nil, on_question: nil)
29
- @on_content = on_content
30
- @on_reasoning = on_reasoning
31
- @on_question = on_question
32
- @pending_tool_calls = []
33
- @pending_tools = []
34
- end
35
-
36
- # The on_question callback, needed by the agent loop to set
37
- # thread/fiber-locals before tool execution.
38
- attr_reader :on_question
39
-
40
- def on_content(text)
41
- @on_content&.call(text)
42
- end
43
-
44
- def on_reasoning_content(text)
45
- @on_reasoning&.call(text)
46
- end
47
-
48
- # Called by llm.rb per tool as it arrives during streaming.
49
- # Records only — no execution, no threads, no queue pushes.
50
- def on_tool_call(tool, error)
51
- @pending_tool_calls << { id: tool.id, name: tool.name, arguments: tool.arguments }
52
- @pending_tools << [tool, error]
53
- end
54
-
55
- # Clear only the tool call metadata (used by ToolUseGuard after it
56
- # has consumed the data for synthetic message injection).
57
- def clear_pending_tool_calls!
58
- @pending_tool_calls.clear
59
- end
60
-
61
- # Clear the deferred execution queue after the agent loop has
62
- # consumed and dispatched all tool calls.
63
- def clear_pending_tools!
64
- @pending_tools.clear
65
- end
66
- end
67
- end
68
- end
69
-
70
- test do
71
- FakeTool = Struct.new(:id, :name, :arguments, keyword_init: true)
72
-
73
- it "records tool in pending_tools" do
74
- stream = Brute::Loop::AgentStream.new
75
- tool = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
76
- stream.on_tool_call(tool, nil)
77
- stream.pending_tools.size.should == 1
78
- end
79
-
80
- it "records tool call metadata" do
81
- stream = Brute::Loop::AgentStream.new
82
- tool = FakeTool.new(id: "toolu_abc", name: "read", arguments: { "file_path" => "test.rb" })
83
- stream.on_tool_call(tool, nil)
84
- stream.pending_tool_calls.first[:id].should == "toolu_abc"
85
- end
86
-
87
- it "records multiple tool calls" do
88
- stream = Brute::Loop::AgentStream.new
89
- t1 = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
90
- t2 = FakeTool.new(id: "toolu_2", name: "write", arguments: {})
91
- stream.on_tool_call(t1, nil)
92
- stream.on_tool_call(t2, nil)
93
- stream.pending_tool_calls.size.should == 2
94
- end
95
-
96
- it "clears pending tool calls and tools" do
97
- stream = Brute::Loop::AgentStream.new
98
- tool = FakeTool.new(id: "toolu_1", name: "read", arguments: {})
99
- stream.on_tool_call(tool, nil)
100
- stream.clear_pending_tool_calls!
101
- stream.clear_pending_tools!
102
- stream.pending_tool_calls.should.be.empty
103
- end
104
-
105
- it "fires the content callback" do
106
- received = nil
107
- stream = Brute::Loop::AgentStream.new(on_content: ->(text) { received = text })
108
- stream.on_content("hello")
109
- received.should == "hello"
110
- end
111
-
112
- it "fires the reasoning callback" do
113
- received = nil
114
- stream = Brute::Loop::AgentStream.new(on_reasoning: ->(text) { received = text })
115
- stream.on_reasoning_content("thinking...")
116
- received.should == "thinking..."
117
- end
118
- end