brute 5.1.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/brute/compaction/middleware/sliding_window.rb +1 -1
- data/lib/brute/compaction/middleware/strategy.rb +0 -2
- data/lib/brute/compaction/summarize.rb +30 -21
- data/lib/brute/completion/lang_chain.rb +34 -31
- data/lib/brute/completion/llmrb.rb +31 -29
- data/lib/brute/completion/open_router.rb +43 -40
- data/lib/brute/completion/ruby_llm.rb +28 -27
- data/lib/brute/contrib/otel.rb +81 -49
- data/lib/brute/eval/case.rb +7 -2
- data/lib/brute/eval/suite.rb +10 -5
- data/lib/brute/eval/transcript.rb +4 -4
- data/lib/brute/eval/world.rb +2 -2
- data/lib/brute/eval.rb +1 -1
- 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 +1 -1
- data/lib/brute/hooks.rb +91 -112
- data/lib/brute/message_transport/anthropic.rb +13 -3
- data/lib/brute/message_transport/llm.rb +8 -4
- data/lib/brute/message_transport/open_router.rb +30 -16
- data/lib/brute/message_transport/openai.rb +14 -7
- data/lib/brute/message_transport/ruby_llm.rb +21 -17
- data/lib/brute/message_transport/ruby_open_ai.rb +59 -57
- data/lib/brute/messages.rb +15 -8
- data/lib/brute/middleware/000_base.rb +5 -5
- data/lib/brute/middleware/002_session_log.rb +12 -4
- data/lib/brute/middleware/008_checkpoint.rb +26 -18
- data/lib/brute/middleware/010_max_iterations.rb +1 -1
- data/lib/brute/middleware/020_system_prompt.rb +1 -1
- data/lib/brute/middleware/040_default_compaction_pipeline.rb +49 -45
- data/lib/brute/middleware/070_default_tool_pipeline.rb +44 -53
- data/lib/brute/prompt_template.rb +19 -15
- data/lib/brute/prompts/base.rb +19 -10
- data/lib/brute/prompts/environment.rb +3 -1
- data/lib/brute/prompts/instructions.rb +9 -7
- data/lib/brute/prompts/skills.rb +5 -3
- data/lib/brute/rack/adapter.rb +40 -23
- data/lib/brute/skill.rb +136 -89
- data/lib/brute/system_prompt.rb +9 -9
- data/lib/brute/token_counter.rb +3 -1
- data/lib/brute/tool.rb +10 -6
- data/lib/brute/tools/adapter.rb +53 -43
- data/lib/brute/tools/fs/snapshot_store.rb +5 -1
- data/lib/brute/tools/fs_patch.rb +16 -8
- data/lib/brute/tools/fs_read.rb +107 -80
- data/lib/brute/tools/fs_remove.rb +6 -2
- data/lib/brute/tools/fs_search.rb +14 -4
- data/lib/brute/tools/fs_undo.rb +6 -2
- data/lib/brute/tools/fs_write.rb +5 -1
- data/lib/brute/tools/net_fetch.rb +6 -2
- data/lib/brute/tools/question.rb +42 -39
- data/lib/brute/tools/shell.rb +20 -5
- data/lib/brute/tools/skill_load.rb +44 -39
- data/lib/brute/tools/todo_write.rb +19 -15
- data/lib/brute/truncation.rb +68 -43
- data/lib/brute/turn/agent_pipeline.rb +23 -13
- data/lib/brute/turn/compaction_pipeline.rb +4 -3
- data/lib/brute/turn/pipeline.rb +93 -96
- data/lib/brute/turn/tool_pipeline.rb +4 -3
- data/lib/brute/usage_detection/llmrb.rb +18 -14
- data/lib/brute/usage_detection/open_router.rb +20 -17
- data/lib/brute/usage_detection/ruby_llm.rb +18 -14
- data/lib/brute/usage_detection/usage.rb +10 -1
- data/lib/brute/utils/diff.rb +18 -10
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +10 -11
- data/lib/brute_cli/providers/shell.rb +32 -29
- data/lib/brute_cli/providers/shell_response.rb +20 -18
- metadata +30 -5
- data/lib/brute/middleware/040_compaction_check.rb +0 -73
- data/lib/brute/middleware/070_tool_pipeline.rb +0 -61
- data/lib/brute/middleware/event_handler.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43f7aeaf57214c89c53faa449bc8f61e5be4cc03d193e9ea9595c09256a8c716
|
|
4
|
+
data.tar.gz: 38149dc7a1be90c6ecbe73a815afa4ac6e8bfbb3e7432bcf5666a7a054bfb158
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01436d756163f52f35759f5ef3365c81493aceb50c74a692bc6bbb25a8122a10fe0b7d64be71b10c042cad71ae01eb1f3331abce19dfec97c854532ff67b61a4
|
|
7
|
+
data.tar.gz: c9a7efb6df3bbac0b816ead1dd10b54b57327ca5c1453000fcaf9a670a847b3fe0c1d1cf4ee783b9b13ea75385c92d70883e214db4de661bea3e0f3f3b577363
|
|
@@ -33,7 +33,7 @@ module Brute
|
|
|
33
33
|
# it anyway; declining here says why.
|
|
34
34
|
if dropped.any? && !dropped.all? { |index| Brute::Compaction::Transcript.marked?(messages[index], strategy) }
|
|
35
35
|
note = Brute::Message.new(
|
|
36
|
-
role:
|
|
36
|
+
role: :user,
|
|
37
37
|
content: Brute::Compaction::Transcript.mark(strategy, format(NOTE, dropped.length)),
|
|
38
38
|
)
|
|
39
39
|
[*kept[...note_index], note, *kept[note_index..]]
|
|
@@ -70,7 +70,6 @@ module Brute
|
|
|
70
70
|
if after < before
|
|
71
71
|
env[:conversation] = rewritten
|
|
72
72
|
env[:applied] << { strategy: strategy, before: before, after: after }
|
|
73
|
-
env[:events] << { type: :compacted, data: env[:applied].last }
|
|
74
73
|
end
|
|
75
74
|
end
|
|
76
75
|
end
|
|
@@ -113,7 +112,6 @@ describe "brute/compaction/middleware/strategy" do
|
|
|
113
112
|
env[:conversation].map { |m| m.content[0, 1] }.should == ["a", "b"]
|
|
114
113
|
asked.should == ["halves"]
|
|
115
114
|
env[:applied].should == [{ strategy: "halves", before: 4_022, after: 2_011 }]
|
|
116
|
-
env[:events].should == [{ type: :compacted, data: { strategy: "halves", before: 4_022, after: 2_011 } }]
|
|
117
115
|
|
|
118
116
|
# A rewrite that saved nothing is refused and the ladder carries on past
|
|
119
117
|
# it, so the strategy itself never has to check.
|
|
@@ -113,22 +113,21 @@ module Brute
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def summarise(env, indices)
|
|
116
|
-
asked = { messages: prompt(env[:conversation], indices), metadata: {}
|
|
116
|
+
asked = { messages: prompt(env[:conversation], indices), metadata: {} }
|
|
117
117
|
@generator.call(asked)
|
|
118
118
|
answer = asked[:messages].last&.content.to_s.strip
|
|
119
119
|
|
|
120
120
|
unless answer.empty?
|
|
121
121
|
answer
|
|
122
122
|
end
|
|
123
|
-
rescue
|
|
124
|
-
env[:events] << { type: :error, data: { error: error, message: error.message } }
|
|
123
|
+
rescue
|
|
125
124
|
nil
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
def swap(env, indices, text)
|
|
129
128
|
before = tokens(env[:conversation])
|
|
130
129
|
summary = Brute::Message.new(
|
|
131
|
-
role:
|
|
130
|
+
role: :user,
|
|
132
131
|
content: Brute::Compaction::Transcript.mark(STRATEGY, text),
|
|
133
132
|
)
|
|
134
133
|
compacted = splice(env[:conversation], indices, summary)
|
|
@@ -137,7 +136,6 @@ module Brute
|
|
|
137
136
|
if after < before
|
|
138
137
|
env[:conversation] = compacted
|
|
139
138
|
env[:applied] << { strategy: STRATEGY, before: before, after: after }
|
|
140
|
-
env[:events] << { type: :compacted, data: env[:applied].last }
|
|
141
139
|
true
|
|
142
140
|
else
|
|
143
141
|
false
|
|
@@ -177,27 +175,34 @@ module Brute
|
|
|
177
175
|
conversation = env[:conversation]
|
|
178
176
|
system_end = Brute::Compaction::Transcript.system_end(conversation)
|
|
179
177
|
task = Brute::Compaction::Transcript.task_index(conversation)
|
|
178
|
+
target = env[:target]
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
# Everything before the current task: what tiers 1 and 2 are allowed
|
|
181
|
+
# to give up, and the only thing either of them needs to be told.
|
|
182
|
+
history = system_end...(task || system_end)
|
|
183
|
+
|
|
184
|
+
t = turns(conversation, history, target)
|
|
185
|
+
h = history_summaries(conversation, history, target)
|
|
186
|
+
s = steps(conversation, target)
|
|
187
|
+
ts = task_summaries(conversation, task, target)
|
|
188
|
+
|
|
189
|
+
t || h || s || ts
|
|
185
190
|
end
|
|
186
191
|
|
|
187
192
|
# Tier 1. Whole historical turns, oldest first.
|
|
188
|
-
def turns(conversation,
|
|
193
|
+
def turns(conversation, history, target)
|
|
189
194
|
groups = Brute::Compaction::Transcript.turns(
|
|
190
195
|
conversation,
|
|
191
|
-
from:
|
|
192
|
-
to:
|
|
196
|
+
from: history.begin,
|
|
197
|
+
to: history.end,
|
|
193
198
|
).map(&:to_a)
|
|
194
199
|
|
|
195
200
|
enough(conversation, groups, target)
|
|
196
201
|
end
|
|
197
202
|
|
|
198
203
|
# Tier 2. History is nothing but summaries, so combine the oldest.
|
|
199
|
-
def history_summaries(conversation,
|
|
200
|
-
combine(conversation, summaries(conversation,
|
|
204
|
+
def history_summaries(conversation, history, target)
|
|
205
|
+
combine(conversation, summaries(conversation, history), target)
|
|
201
206
|
end
|
|
202
207
|
|
|
203
208
|
# Tier 3. The current task's own oldest steps, keeping the newest.
|
|
@@ -212,12 +217,17 @@ module Brute
|
|
|
212
217
|
|
|
213
218
|
# Tier 4. No step may go, so combine the task's own summaries.
|
|
214
219
|
def task_summaries(conversation, task, target)
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
if task.nil?
|
|
221
|
+
start = 0
|
|
222
|
+
else
|
|
223
|
+
start = task + 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
combine(conversation, summaries(conversation, start...conversation.length), target)
|
|
217
227
|
end
|
|
218
228
|
|
|
219
|
-
def summaries(conversation,
|
|
220
|
-
|
|
229
|
+
def summaries(conversation, range)
|
|
230
|
+
range.select do |index|
|
|
221
231
|
Brute::Compaction::Transcript.marked?(conversation[index], STRATEGY)
|
|
222
232
|
end
|
|
223
233
|
end
|
|
@@ -326,11 +336,10 @@ describe "brute/compaction/summarize" do
|
|
|
326
336
|
windy = ->(env) { env[:messages] << said(:assistant, "w" * 90_000) }
|
|
327
337
|
summarised(history, 3_000, windy, keep_steps: 1)[:applied].should == []
|
|
328
338
|
|
|
329
|
-
# A generator that raises ends the loop rather than the turn
|
|
330
|
-
env = { conversation: history, target: 3_000, applied: []
|
|
339
|
+
# A generator that raises ends the loop rather than the turn.
|
|
340
|
+
env = { conversation: history, target: 3_000, applied: [] }
|
|
331
341
|
Brute::Compaction::Summarize.new(->(_e) { raise IOError, "the summariser is down" }).call(env)
|
|
332
342
|
env[:applied].should == []
|
|
333
|
-
env[:events].map { |e| [e[:type], e[:data][:message]] }.should == [[:error, "the summariser is down"]]
|
|
334
343
|
|
|
335
344
|
# And one that works after the tiers are exhausted still stops.
|
|
336
345
|
once = ->(e) { e[:messages] << said(:assistant, "small") }
|
|
@@ -42,31 +42,32 @@ module Brute
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def call(env)
|
|
45
|
-
|
|
45
|
+
env.emit_trace do |env|
|
|
46
|
+
env.emit(LLM_START_EVENT)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
response = nil
|
|
49
|
+
env.emit(LLM_DURATION_EVENT) { response = @llm.chat(**params(env)) }
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
if (usage = Brute::MessageTransport::LangChain.usage_metrics(response))
|
|
52
|
+
(env[:metadata] ||= {})[:last_llm_usage] = usage
|
|
53
|
+
end
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# langchainrb speaks the OpenAI-style wire format both ways.
|
|
56
|
+
Brute::MessageTransport::LangChain.wrap_each(reply(response)) do |message|
|
|
57
|
+
env[:messages] << message
|
|
58
|
+
end
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
emit(LLM_FAILURE_EVENT, env)
|
|
60
|
+
env.emit(LLM_END_EVENT)
|
|
61
|
+
rescue => error
|
|
62
|
+
env.emit(LLM_FAILURE_EVENT)
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
65
|
+
env.emit(FARADAY_ERROR_EVENT, error)
|
|
66
|
+
else
|
|
67
|
+
env.emit(STANDARD_ERROR_EVENT, error)
|
|
68
|
+
end
|
|
69
69
|
|
|
70
|
+
end
|
|
70
71
|
env
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -75,18 +76,20 @@ module Brute
|
|
|
75
76
|
attr_reader :options
|
|
76
77
|
|
|
77
78
|
def params(env)
|
|
78
|
-
|
|
79
|
+
{
|
|
79
80
|
messages: Brute::MessageTransport::LangChain.dump_all(env[:messages]),
|
|
80
81
|
temperature: temperature(env),
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
}.tap do |params|
|
|
83
|
+
tools = tool_definitions(env)
|
|
84
|
+
if tools.any?
|
|
85
|
+
params[:tools] = tools
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
model = option(env, :model)
|
|
89
|
+
if model
|
|
90
|
+
params[:model] = model
|
|
91
|
+
end
|
|
92
|
+
end
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
def reply(response)
|
|
@@ -143,8 +146,8 @@ describe "brute/completion/lang_chain" do
|
|
|
143
146
|
seen = []
|
|
144
147
|
pipeline = Brute::Turn::Pipeline.new
|
|
145
148
|
pipeline.run Brute::Completion::LangChain.new(llm: llm, model: "gpt-4o-mini")
|
|
146
|
-
pipeline.on(Brute::Hooks::
|
|
147
|
-
pipeline.on(Brute::Hooks::
|
|
149
|
+
pipeline.on(Brute::Hooks::LLM_START_EVENT) { |_env| seen << :before }
|
|
150
|
+
pipeline.on(Brute::Hooks::LLM_END_EVENT) { |_env| seen << :after }
|
|
148
151
|
pipeline.call(env)
|
|
149
152
|
|
|
150
153
|
env[:messages].last.role.should == :assistant
|
|
@@ -44,34 +44,35 @@ module Brute
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def call(env)
|
|
47
|
-
|
|
47
|
+
env.emit_trace do |env|
|
|
48
|
+
env.emit(LLM_START_EVENT)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
# llm.rb takes the last message as the prompt and the rest as history.
|
|
51
|
+
messages = Brute::MessageTransport::LLM.dump_all(env[:messages])
|
|
52
|
+
prompt = messages.pop
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
response = nil
|
|
55
|
+
env.emit(LLM_DURATION_EVENT) { response = client(env).complete(prompt, **params(env, messages)) }
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (usage = Brute::MessageTransport::LLM.usage_metrics(response))
|
|
58
|
+
(env[:metadata] ||= {})[:last_llm_usage] = usage
|
|
59
|
+
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
Brute::MessageTransport::LLM.wrap_each(response) do |message|
|
|
62
|
+
env[:messages] << message
|
|
63
|
+
end
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
emit(LLM_FAILURE_EVENT, env)
|
|
65
|
+
env.emit(LLM_END_EVENT)
|
|
66
|
+
rescue => error
|
|
67
|
+
env.emit(LLM_FAILURE_EVENT)
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
70
|
+
env.emit(FARADAY_ERROR_EVENT, error)
|
|
71
|
+
else
|
|
72
|
+
env.emit(STANDARD_ERROR_EVENT, error)
|
|
73
|
+
end
|
|
74
74
|
|
|
75
|
+
end
|
|
75
76
|
env
|
|
76
77
|
end
|
|
77
78
|
|
|
@@ -80,16 +81,17 @@ module Brute
|
|
|
80
81
|
attr_reader :options
|
|
81
82
|
|
|
82
83
|
def params(env, history)
|
|
83
|
-
|
|
84
|
+
{
|
|
84
85
|
role: nil,
|
|
85
86
|
model: option(env, :model),
|
|
86
87
|
messages: history,
|
|
87
88
|
temperature: temperature(env),
|
|
88
|
-
}.compact
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
}.compact.tap do |params|
|
|
90
|
+
functions = functions(env)
|
|
91
|
+
if functions.any?
|
|
92
|
+
params[:tools] = functions
|
|
93
|
+
end
|
|
94
|
+
end
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def client(env)
|
|
@@ -154,8 +156,8 @@ describe "brute/completion/llmrb" do
|
|
|
154
156
|
seen = []
|
|
155
157
|
pipeline = Brute::Turn::Pipeline.new
|
|
156
158
|
pipeline.run Brute::Completion::LLMrb.new(client: client, temperature: 0.1)
|
|
157
|
-
pipeline.on(Brute::Hooks::
|
|
158
|
-
pipeline.on(Brute::Hooks::
|
|
159
|
+
pipeline.on(Brute::Hooks::LLM_START_EVENT) { |_env| seen << :before }
|
|
160
|
+
pipeline.on(Brute::Hooks::LLM_END_EVENT) { |_env| seen << :after }
|
|
159
161
|
pipeline.call(env)
|
|
160
162
|
|
|
161
163
|
env[:messages].last.role.should == :assistant
|
|
@@ -43,52 +43,53 @@ module Brute
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def call(env)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
46
|
+
env.emit_trace do |env|
|
|
47
|
+
env.emit(LLM_START_EVENT)
|
|
48
|
+
|
|
49
|
+
messages = Brute::MessageTransport::OpenRouter.dump_all(env[:messages])
|
|
50
|
+
|
|
51
|
+
::OpenRouter::Client.new(**@config).then do |client|
|
|
52
|
+
response = nil
|
|
53
|
+
env.emit(LLM_DURATION_EVENT) { response = client.complete(messages, options(env)) }
|
|
54
|
+
|
|
55
|
+
response.then do |response|
|
|
56
|
+
|
|
57
|
+
# Expose the provider's usage for downstream accounting
|
|
58
|
+
# (goal budgets, autonomous limits, compaction thresholds, usage
|
|
59
|
+
# attribution) — additive metadata only, normalised so a reader
|
|
60
|
+
# does not have to know which provider answered.
|
|
61
|
+
if (usage = Brute::MessageTransport::OpenRouter.usage_metrics(response))
|
|
62
|
+
(env[:metadata] ||= {})[:last_llm_usage] = usage
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# OpenRouter in fact only returns a single message...
|
|
66
|
+
# https://github.com/estiens/open_router_enhanced/blob/main/lib/open_router/response.rb
|
|
67
|
+
Brute::MessageTransport::OpenRouter.wrap_each(response) do |message|
|
|
68
|
+
env[:messages] << message
|
|
69
|
+
end
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
|
-
end
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
emit(LLM_FAILURE_EVENT, env)
|
|
73
|
+
env.emit(LLM_END_EVENT)
|
|
74
|
+
# A provider call that raises is reported through the hooks rather than
|
|
75
|
+
# up the stack: :llm_failure with the turn env, then one hook naming the
|
|
76
|
+
# kind of failure. The classes are looked up defensively — the provider
|
|
77
|
+
# gem is only a dependency of the app that uses this middleware.
|
|
78
|
+
rescue => error
|
|
79
|
+
env.emit(LLM_FAILURE_EVENT)
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
82
|
+
env.emit(FARADAY_ERROR_EVENT, error)
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
elsif defined?(::OpenRouter::ServerError) && error.is_a?(::OpenRouter::ServerError)
|
|
85
|
+
env.emit(OPEN_ROUTER_SERVER_ERROR_EVENT, error)
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
else
|
|
88
|
+
env.emit(STANDARD_ERROR_EVENT, error)
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
end
|
|
91
91
|
|
|
92
|
+
end
|
|
92
93
|
env
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -100,7 +101,9 @@ module Brute
|
|
|
100
101
|
|
|
101
102
|
unless @options.tools?
|
|
102
103
|
tools = tool_definitions(env)
|
|
103
|
-
|
|
104
|
+
if tools.any?
|
|
105
|
+
overrides[:tools] = tools
|
|
106
|
+
end
|
|
104
107
|
end
|
|
105
108
|
|
|
106
109
|
if !@model_given && (model = env[:model])
|
|
@@ -266,7 +269,7 @@ describe "brute/completion/open_router" do
|
|
|
266
269
|
env[:messages].user("hi")
|
|
267
270
|
pipeline = Brute::Turn::Pipeline.new
|
|
268
271
|
pipeline.run Brute::Completion::OpenRouter.new
|
|
269
|
-
%i[llm_failure standard_error
|
|
272
|
+
%i[llm_failure standard_error llm_end].each do |event|
|
|
270
273
|
pipeline.on(event) { |hook_env, extra| seen << [event, hook_env, extra] }
|
|
271
274
|
end
|
|
272
275
|
returned = pipeline.call(env)
|
|
@@ -59,35 +59,36 @@ module Brute
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def call(env)
|
|
62
|
-
|
|
62
|
+
env.emit_trace do |env|
|
|
63
|
+
env.emit(LLM_START_EVENT)
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
messages = Brute::MessageTransport::RubyLLM.dump_all(env[:messages])
|
|
66
|
+
response = nil
|
|
67
|
+
env.emit(LLM_DURATION_EVENT) { response = complete(env, messages) }
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
unless response.nil?
|
|
70
|
+
if (usage = Brute::MessageTransport::RubyLLM.usage_metrics(response))
|
|
71
|
+
(env[:metadata] ||= {})[:last_llm_usage] = usage
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Brute::MessageTransport::RubyLLM.wrap_each(response) do |message|
|
|
75
|
+
env[:messages] << message
|
|
76
|
+
end
|
|
71
77
|
end
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
env.emit(LLM_END_EVENT)
|
|
80
|
+
# A provider call that raises is reported through the hooks rather than
|
|
81
|
+
# up the stack, the same way the OpenRouter completion does it.
|
|
82
|
+
rescue => error
|
|
83
|
+
env.emit(LLM_FAILURE_EVENT)
|
|
84
|
+
|
|
85
|
+
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
86
|
+
env.emit(FARADAY_ERROR_EVENT, error)
|
|
87
|
+
else
|
|
88
|
+
env.emit(STANDARD_ERROR_EVENT, error)
|
|
75
89
|
end
|
|
76
|
-
end
|
|
77
90
|
|
|
78
|
-
emit(AFTER_LLM_EVENT, env)
|
|
79
|
-
env
|
|
80
|
-
# A provider call that raises is reported through the hooks rather than
|
|
81
|
-
# up the stack, the same way the OpenRouter completion does it.
|
|
82
|
-
rescue => error
|
|
83
|
-
emit(LLM_FAILURE_EVENT, env)
|
|
84
|
-
|
|
85
|
-
if defined?(::Faraday::Error) && error.is_a?(::Faraday::Error)
|
|
86
|
-
emit(FARADAY_ERROR_EVENT, env, error)
|
|
87
|
-
else
|
|
88
|
-
emit(STANDARD_ERROR_EVENT, env, error)
|
|
89
91
|
end
|
|
90
|
-
|
|
91
92
|
env
|
|
92
93
|
end
|
|
93
94
|
|
|
@@ -104,7 +105,7 @@ module Brute
|
|
|
104
105
|
|
|
105
106
|
provider_client = client(option(env, :provider))
|
|
106
107
|
|
|
107
|
-
# Streaming reports through
|
|
108
|
+
# Streaming reports through :content and :reasoning chunk by chunk; the final
|
|
108
109
|
# message still comes back for the log.
|
|
109
110
|
if streaming?(env)
|
|
110
111
|
provider_client.complete(messages, **kwargs) do |chunk|
|
|
@@ -117,11 +118,11 @@ module Brute
|
|
|
117
118
|
|
|
118
119
|
def stream(env, chunk)
|
|
119
120
|
if chunk.content && !chunk.content.to_s.empty?
|
|
120
|
-
env
|
|
121
|
+
env.emit(CONTENT_EVENT, chunk.content.to_s)
|
|
121
122
|
end
|
|
122
123
|
|
|
123
124
|
if chunk.respond_to?(:thinking) && chunk.thinking.respond_to?(:text) && chunk.thinking&.text
|
|
124
|
-
env
|
|
125
|
+
env.emit(REASONING_EVENT, chunk.thinking.text)
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
|
|
@@ -214,8 +215,8 @@ describe "brute/completion/ruby_llm" do
|
|
|
214
215
|
|
|
215
216
|
seen = []
|
|
216
217
|
pipeline = running.call(Brute::Completion::RubyLLM.new(client: client, model: "use-this-model", temperature: 0.1))
|
|
217
|
-
pipeline.on(Brute::Hooks::
|
|
218
|
-
pipeline.on(Brute::Hooks::
|
|
218
|
+
pipeline.on(Brute::Hooks::LLM_START_EVENT) { |_env| seen << :before }
|
|
219
|
+
pipeline.on(Brute::Hooks::LLM_END_EVENT) { |_env| seen << :after }
|
|
219
220
|
pipeline.call(env)
|
|
220
221
|
|
|
221
222
|
env[:messages].last.role.should == :assistant
|