brute 1.0.0 → 2.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/agent.rb +72 -6
- data/lib/brute/events/handler.rb +69 -0
- data/lib/brute/events/prefixed_terminal_output.rb +72 -0
- data/lib/brute/events/terminal_output_handler.rb +68 -0
- data/lib/brute/middleware/001_otel_span.rb +77 -0
- data/lib/brute/middleware/003_tool_result_loop.rb +103 -0
- data/lib/brute/middleware/004_summarize.rb +139 -0
- data/lib/brute/middleware/005_tracing.rb +86 -0
- data/lib/brute/middleware/010_max_iterations.rb +73 -0
- data/lib/brute/middleware/015_otel_token_usage.rb +42 -0
- data/lib/brute/middleware/020_system_prompt.rb +128 -0
- data/lib/brute/middleware/040_compaction_check.rb +155 -0
- data/lib/brute/middleware/060_questions.rb +41 -0
- data/lib/brute/middleware/070_tool_call.rb +247 -0
- data/lib/brute/middleware/073_otel_tool_call.rb +49 -0
- data/lib/brute/middleware/075_otel_tool_results.rb +46 -0
- data/lib/brute/middleware/100_llm_call.rb +62 -0
- data/lib/brute/middleware/event_handler.rb +25 -0
- data/lib/brute/middleware/user_queue.rb +35 -0
- data/lib/brute/pipeline.rb +44 -107
- data/lib/brute/prompts/skills.rb +2 -2
- data/lib/brute/prompts.rb +23 -23
- data/lib/brute/providers/shell.rb +6 -19
- data/lib/brute/providers/shell_response.rb +22 -30
- data/lib/brute/session.rb +52 -0
- data/lib/brute/store/snapshot_store.rb +21 -37
- data/lib/brute/sub_agent.rb +106 -0
- data/lib/brute/system_prompt.rb +1 -83
- data/lib/brute/tool.rb +107 -0
- data/lib/brute/tools/delegate.rb +61 -70
- data/lib/brute/tools/fs_patch.rb +9 -7
- data/lib/brute/tools/fs_read.rb +233 -20
- data/lib/brute/tools/fs_remove.rb +8 -9
- data/lib/brute/tools/fs_search.rb +98 -16
- data/lib/brute/tools/fs_undo.rb +8 -8
- data/lib/brute/tools/fs_write.rb +7 -5
- data/lib/brute/tools/net_fetch.rb +8 -8
- data/lib/brute/tools/question.rb +36 -24
- data/lib/brute/tools/shell.rb +74 -16
- data/lib/brute/tools/todo_read.rb +8 -8
- data/lib/brute/tools/todo_write.rb +25 -18
- data/lib/brute/tools.rb +8 -12
- data/lib/brute/truncation.rb +219 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +82 -45
- metadata +59 -46
- data/lib/brute/loop/agent_stream.rb +0 -118
- data/lib/brute/loop/agent_turn.rb +0 -520
- data/lib/brute/loop/compactor.rb +0 -107
- data/lib/brute/loop/doom_loop.rb +0 -86
- data/lib/brute/loop/step.rb +0 -332
- data/lib/brute/loop/tool_call_step.rb +0 -90
- data/lib/brute/middleware/base.rb +0 -27
- data/lib/brute/middleware/compaction_check.rb +0 -106
- data/lib/brute/middleware/doom_loop_detection.rb +0 -136
- data/lib/brute/middleware/llm_call.rb +0 -128
- data/lib/brute/middleware/message_tracking.rb +0 -339
- data/lib/brute/middleware/otel/span.rb +0 -105
- data/lib/brute/middleware/otel/token_usage.rb +0 -68
- data/lib/brute/middleware/otel/tool_calls.rb +0 -68
- data/lib/brute/middleware/otel/tool_results.rb +0 -65
- data/lib/brute/middleware/otel.rb +0 -34
- data/lib/brute/middleware/reasoning_normalizer.rb +0 -192
- data/lib/brute/middleware/retry.rb +0 -157
- data/lib/brute/middleware/session_persistence.rb +0 -72
- data/lib/brute/middleware/token_tracking.rb +0 -124
- data/lib/brute/middleware/tool_error_tracking.rb +0 -179
- data/lib/brute/middleware/tool_use_guard.rb +0 -133
- data/lib/brute/middleware/tracing.rb +0 -124
- data/lib/brute/middleware.rb +0 -18
- data/lib/brute/orchestrator/turn.rb +0 -105
- data/lib/brute/patches/anthropic_tool_role.rb +0 -35
- data/lib/brute/patches/buffer_nil_guard.rb +0 -26
- data/lib/brute/providers/models_dev.rb +0 -111
- data/lib/brute/providers/ollama.rb +0 -135
- data/lib/brute/providers/opencode_go.rb +0 -43
- data/lib/brute/providers/opencode_zen.rb +0 -87
- data/lib/brute/providers.rb +0 -62
- data/lib/brute/queue/base_queue.rb +0 -222
- data/lib/brute/queue/parallel_queue.rb +0 -66
- data/lib/brute/queue/sequential_queue.rb +0 -63
- data/lib/brute/store/message_store.rb +0 -362
- data/lib/brute/store/session.rb +0 -106
- /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 '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
4
|
+
version: 2.0.0
|
|
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:
|
|
41
|
+
name: ruby_llm
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
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: '
|
|
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/
|
|
91
|
-
- lib/brute/
|
|
92
|
-
- lib/brute/
|
|
93
|
-
- lib/brute/
|
|
94
|
-
- lib/brute/
|
|
95
|
-
- lib/brute/
|
|
96
|
-
- lib/brute/
|
|
97
|
-
- lib/brute/middleware.rb
|
|
98
|
-
- lib/brute/middleware/
|
|
99
|
-
- lib/brute/middleware/
|
|
100
|
-
- lib/brute/middleware/
|
|
101
|
-
- lib/brute/middleware/
|
|
102
|
-
- lib/brute/middleware/
|
|
103
|
-
- lib/brute/middleware/
|
|
104
|
-
- lib/brute/middleware/
|
|
105
|
-
- lib/brute/middleware/
|
|
106
|
-
- lib/brute/middleware/
|
|
107
|
-
- lib/brute/middleware/
|
|
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/
|
|
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.
|
|
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
|
|
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
|