brute 5.0.5 → 6.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/compaction/middleware/sliding_window.rb +170 -0
- data/lib/brute/compaction/middleware/strategy.rb +137 -0
- data/lib/brute/compaction/middleware/tool_results.rb +146 -0
- data/lib/brute/compaction/summarize.rb +349 -0
- data/lib/brute/compaction/transcript.rb +166 -0
- data/lib/brute/compaction.rb +57 -0
- 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 +83 -51
- data/lib/brute/env.rb +54 -0
- data/lib/brute/eval/case.rb +254 -0
- data/lib/brute/eval/suite.rb +181 -0
- data/lib/brute/eval/transcript.rb +147 -0
- data/lib/brute/eval/world.rb +106 -0
- data/lib/brute/eval.rb +48 -0
- 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 +89 -102
- 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 +6 -6
- data/lib/brute/middleware/002_session_log.rb +12 -4
- data/lib/brute/middleware/008_checkpoint.rb +27 -19
- 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 +356 -0
- data/lib/brute/middleware/{070_tool_pipeline.rb → 070_default_tool_pipeline.rb} +49 -58
- 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/approximate.rb +54 -0
- data/lib/brute/token_counter/tiktoken.rb +80 -0
- data/lib/brute/token_counter.rb +150 -0
- data/lib/brute/tool.rb +10 -6
- data/lib/brute/tools/adapter.rb +57 -47
- 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 +46 -41
- data/lib/brute/tools/sub_agent.rb +2 -2
- 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 +123 -0
- data/lib/brute/turn/pipeline.rb +87 -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 +25 -12
- data/lib/brute_cli/providers/shell.rb +32 -29
- data/lib/brute_cli/providers/shell_response.rb +20 -18
- metadata +48 -5
- data/lib/brute/middleware/040_compaction_check.rb +0 -157
- data/lib/brute/middleware/event_handler.rb +0 -27
data/lib/brute.rb
CHANGED
|
@@ -12,13 +12,13 @@ require_relative 'brute/version'
|
|
|
12
12
|
|
|
13
13
|
module Brute
|
|
14
14
|
LOGO = <<-LOGO
|
|
15
|
-
.o8 .
|
|
16
|
-
"888 .o8
|
|
17
|
-
888oooo. oooo d8b oooo oooo .o888oo .ooooo.
|
|
18
|
-
d88' `88b `888""8P `888 `888 888 d88' `88b
|
|
19
|
-
888 888 888 888 888 888 888ooo888
|
|
20
|
-
888 888 888 888 888 888 . 888 .o
|
|
21
|
-
`Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
|
|
15
|
+
.o8 .
|
|
16
|
+
"888 .o8
|
|
17
|
+
888oooo. oooo d8b oooo oooo .o888oo .ooooo.
|
|
18
|
+
d88' `88b `888""8P `888 `888 888 d88' `88b
|
|
19
|
+
888 888 888 888 888 888 888ooo888
|
|
20
|
+
888 888 888 888 888 888 . 888 .o
|
|
21
|
+
`Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
|
|
22
22
|
LOGO
|
|
23
23
|
|
|
24
24
|
# NOTE: Brute owns no LLM configuration and no LLM library. All
|
|
@@ -42,7 +42,7 @@ module Brute
|
|
|
42
42
|
#
|
|
43
43
|
# agent = Brute.agent
|
|
44
44
|
# .use(Brute::Middleware::SystemPrompt)
|
|
45
|
-
# .use(Brute::Middleware::
|
|
45
|
+
# .use(Brute::Middleware::DefaultToolPipeline, tools: Brute::Tools::ALL)
|
|
46
46
|
# .run ->(env) { ... } # the LLM-call proc (provider/model/creds here)
|
|
47
47
|
#
|
|
48
48
|
# agent.start("what changed?")
|
|
@@ -68,7 +68,9 @@ module Brute
|
|
|
68
68
|
#
|
|
69
69
|
def self.load_agent(path = "agent.ru")
|
|
70
70
|
path = File.expand_path(path)
|
|
71
|
-
|
|
71
|
+
unless File.file?(path)
|
|
72
|
+
raise ArgumentError, "no such agent file: #{path}"
|
|
73
|
+
end
|
|
72
74
|
|
|
73
75
|
Brute::Turn::AgentPipeline.parse_file(path)
|
|
74
76
|
end
|
|
@@ -93,7 +95,14 @@ end
|
|
|
93
95
|
require_relative "brute/version"
|
|
94
96
|
require_relative "brute/hooks"
|
|
95
97
|
require_relative "brute/messages"
|
|
98
|
+
require_relative "brute/env"
|
|
96
99
|
require_relative "brute/middleware/000_base"
|
|
100
|
+
require_relative "brute/compaction"
|
|
101
|
+
require_relative "brute/compaction/transcript"
|
|
102
|
+
require_relative "brute/compaction/middleware/strategy"
|
|
103
|
+
require_relative "brute/compaction/middleware/tool_results"
|
|
104
|
+
require_relative "brute/compaction/middleware/sliding_window"
|
|
105
|
+
require_relative "brute/compaction/summarize"
|
|
97
106
|
require_relative "brute/completion/open_router"
|
|
98
107
|
require_relative "brute/completion/lang_chain"
|
|
99
108
|
require_relative "brute/completion/llmrb"
|
|
@@ -124,10 +133,9 @@ require_relative "brute/middleware/008_checkpoint"
|
|
|
124
133
|
require_relative "brute/middleware/010_max_iterations"
|
|
125
134
|
require_relative "brute/middleware/020_system_prompt"
|
|
126
135
|
require_relative "brute/middleware/025_skills"
|
|
127
|
-
require_relative "brute/middleware/
|
|
136
|
+
require_relative "brute/middleware/040_default_compaction_pipeline"
|
|
128
137
|
require_relative "brute/middleware/060_questions"
|
|
129
|
-
require_relative "brute/middleware/
|
|
130
|
-
require_relative "brute/middleware/event_handler"
|
|
138
|
+
require_relative "brute/middleware/070_default_tool_pipeline"
|
|
131
139
|
require_relative "brute/middleware/user_queue"
|
|
132
140
|
require_relative "brute/prompt_template"
|
|
133
141
|
require_relative "brute/prompts"
|
|
@@ -157,6 +165,9 @@ require_relative "brute/prompts/tool_usage"
|
|
|
157
165
|
require_relative "brute/rack/adapter"
|
|
158
166
|
require_relative "brute/skill"
|
|
159
167
|
require_relative "brute/system_prompt"
|
|
168
|
+
require_relative "brute/token_counter"
|
|
169
|
+
require_relative "brute/token_counter/approximate"
|
|
170
|
+
require_relative "brute/token_counter/tiktoken"
|
|
160
171
|
require_relative "brute/tool"
|
|
161
172
|
require_relative "brute/tools"
|
|
162
173
|
require_relative "brute/tools/adapter"
|
|
@@ -179,7 +190,9 @@ require_relative "brute/tools/todo_write"
|
|
|
179
190
|
require_relative "brute/truncation"
|
|
180
191
|
require_relative "brute/turn/agent_pipeline"
|
|
181
192
|
require_relative "brute/turn/pipeline"
|
|
193
|
+
require_relative "brute/turn/compaction_pipeline"
|
|
182
194
|
require_relative "brute/turn/tool_pipeline"
|
|
195
|
+
require_relative "brute/eval"
|
|
183
196
|
require_relative "brute/utils/diff"
|
|
184
197
|
|
|
185
198
|
# gangsta g-dogg bruh...
|
|
@@ -44,12 +44,13 @@ module BruteCLI
|
|
|
44
44
|
|
|
45
45
|
# nil text means we received tool results (second call) —
|
|
46
46
|
# return an empty assistant response so the agent loop exits.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
if text.nil?
|
|
48
|
+
ShellResponse.new(model: model, tools: tools)
|
|
49
|
+
else
|
|
50
|
+
wrap = INTERPRETERS.fetch(model, INTERPRETERS["bash"])
|
|
51
|
+
command = wrap.call(text)
|
|
52
|
+
ShellResponse.new(command: command, model: model, tools: tools)
|
|
53
|
+
end
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
# For the REPL model picker: provider.models.all.select(&:chat?)
|
|
@@ -61,38 +62,40 @@ module BruteCLI
|
|
|
61
62
|
|
|
62
63
|
private
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
# Extract the user's text from the messages array.
|
|
66
|
+
# Returns nil when the messages contain tool results (the second
|
|
67
|
+
# round-trip) so #complete knows to return an empty response.
|
|
68
|
+
def extract_text(prompt)
|
|
69
|
+
case prompt
|
|
70
|
+
when String
|
|
71
|
+
prompt
|
|
72
|
+
when ::Array
|
|
73
|
+
if prompt.any? { |m| m.respond_to?(:role) && m.role == :tool }
|
|
74
|
+
nil
|
|
75
|
+
else
|
|
76
|
+
user_msg = prompt.reverse_each.find { |m| m.respond_to?(:role) && m.role == :user }
|
|
77
|
+
user_msg&.content.to_s
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
prompt.to_s
|
|
81
|
+
end
|
|
78
82
|
end
|
|
79
|
-
end
|
|
80
83
|
|
|
81
84
|
# ── ModelList ──────────────────────────────────────────────────
|
|
82
85
|
|
|
83
86
|
# Minimal object that quacks like provider.models so the REPL's
|
|
84
87
|
# fetch_models can call provider.models.all.select(&:chat?).
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
class ModelList
|
|
89
|
+
ModelEntry = Struct.new(:id, :chat?, keyword_init: true)
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
def initialize(names)
|
|
92
|
+
@entries = names.map { |n| ModelEntry.new(id: n, chat?: true) }
|
|
93
|
+
end
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
def all
|
|
96
|
+
@entries
|
|
97
|
+
end
|
|
94
98
|
end
|
|
95
|
-
end
|
|
96
99
|
end
|
|
97
100
|
end
|
|
98
101
|
end
|
|
@@ -25,22 +25,24 @@ module BruteCLI
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def messages
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
if @command.nil?
|
|
29
|
+
[empty_assistant]
|
|
30
|
+
else
|
|
31
|
+
call_id = "shell_#{SecureRandom.hex(8)}"
|
|
32
|
+
tool_calls = [
|
|
33
|
+
Brute::ToolCall.new(
|
|
34
|
+
id: call_id,
|
|
35
|
+
name: "shell",
|
|
36
|
+
arguments: { "command" => @command },
|
|
37
|
+
)
|
|
38
|
+
]
|
|
39
|
+
[Brute::Message.new(
|
|
40
|
+
role: :assistant,
|
|
41
|
+
content: "",
|
|
42
|
+
tool_calls: tool_calls,
|
|
36
43
|
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
[Brute::Message.new(
|
|
40
|
-
role: :assistant,
|
|
41
|
-
content: "",
|
|
42
|
-
tool_calls: tool_calls,
|
|
43
|
-
)]
|
|
44
|
+
]
|
|
45
|
+
end
|
|
44
46
|
end
|
|
45
47
|
alias_method :choices, :messages
|
|
46
48
|
|
|
@@ -83,9 +85,9 @@ module BruteCLI
|
|
|
83
85
|
|
|
84
86
|
private
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
def empty_assistant
|
|
89
|
+
Brute::Message.new(role: :assistant, content: "")
|
|
90
|
+
end
|
|
89
91
|
end
|
|
90
92
|
end
|
|
91
93
|
end
|
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: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
@@ -219,6 +219,20 @@ dependencies:
|
|
|
219
219
|
- - ">="
|
|
220
220
|
- !ruby/object:Gem::Version
|
|
221
221
|
version: '0'
|
|
222
|
+
- !ruby/object:Gem::Dependency
|
|
223
|
+
name: rubocop
|
|
224
|
+
requirement: !ruby/object:Gem::Requirement
|
|
225
|
+
requirements:
|
|
226
|
+
- - ">="
|
|
227
|
+
- !ruby/object:Gem::Version
|
|
228
|
+
version: '0'
|
|
229
|
+
type: :development
|
|
230
|
+
prerelease: false
|
|
231
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
232
|
+
requirements:
|
|
233
|
+
- - ">="
|
|
234
|
+
- !ruby/object:Gem::Version
|
|
235
|
+
version: '0'
|
|
222
236
|
- !ruby/object:Gem::Dependency
|
|
223
237
|
name: rake
|
|
224
238
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -233,6 +247,20 @@ dependencies:
|
|
|
233
247
|
- - "~>"
|
|
234
248
|
- !ruby/object:Gem::Version
|
|
235
249
|
version: '13.0'
|
|
250
|
+
- !ruby/object:Gem::Dependency
|
|
251
|
+
name: rdoc
|
|
252
|
+
requirement: !ruby/object:Gem::Requirement
|
|
253
|
+
requirements:
|
|
254
|
+
- - "~>"
|
|
255
|
+
- !ruby/object:Gem::Version
|
|
256
|
+
version: '8.0'
|
|
257
|
+
type: :development
|
|
258
|
+
prerelease: false
|
|
259
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
260
|
+
requirements:
|
|
261
|
+
- - "~>"
|
|
262
|
+
- !ruby/object:Gem::Version
|
|
263
|
+
version: '8.0'
|
|
236
264
|
- !ruby/object:Gem::Dependency
|
|
237
265
|
name: scampi
|
|
238
266
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -282,12 +310,24 @@ extensions: []
|
|
|
282
310
|
extra_rdoc_files: []
|
|
283
311
|
files:
|
|
284
312
|
- lib/brute.rb
|
|
313
|
+
- lib/brute/compaction.rb
|
|
314
|
+
- lib/brute/compaction/middleware/sliding_window.rb
|
|
315
|
+
- lib/brute/compaction/middleware/strategy.rb
|
|
316
|
+
- lib/brute/compaction/middleware/tool_results.rb
|
|
317
|
+
- lib/brute/compaction/summarize.rb
|
|
318
|
+
- lib/brute/compaction/transcript.rb
|
|
285
319
|
- lib/brute/completion/lang_chain.rb
|
|
286
320
|
- lib/brute/completion/llmrb.rb
|
|
287
321
|
- lib/brute/completion/open_router.rb
|
|
288
322
|
- lib/brute/completion/ruby_llm.rb
|
|
289
323
|
- lib/brute/contrib/log_file.rb
|
|
290
324
|
- lib/brute/contrib/otel.rb
|
|
325
|
+
- lib/brute/env.rb
|
|
326
|
+
- lib/brute/eval.rb
|
|
327
|
+
- lib/brute/eval/case.rb
|
|
328
|
+
- lib/brute/eval/suite.rb
|
|
329
|
+
- lib/brute/eval/transcript.rb
|
|
330
|
+
- lib/brute/eval/world.rb
|
|
291
331
|
- lib/brute/events/handler.rb
|
|
292
332
|
- lib/brute/events/prefixed_terminal_output.rb
|
|
293
333
|
- lib/brute/events/terminal_output_handler.rb
|
|
@@ -310,10 +350,9 @@ files:
|
|
|
310
350
|
- lib/brute/middleware/010_max_iterations.rb
|
|
311
351
|
- lib/brute/middleware/020_system_prompt.rb
|
|
312
352
|
- lib/brute/middleware/025_skills.rb
|
|
313
|
-
- lib/brute/middleware/
|
|
353
|
+
- lib/brute/middleware/040_default_compaction_pipeline.rb
|
|
314
354
|
- lib/brute/middleware/060_questions.rb
|
|
315
|
-
- lib/brute/middleware/
|
|
316
|
-
- lib/brute/middleware/event_handler.rb
|
|
355
|
+
- lib/brute/middleware/070_default_tool_pipeline.rb
|
|
317
356
|
- lib/brute/middleware/user_queue.rb
|
|
318
357
|
- lib/brute/prompt_template.rb
|
|
319
358
|
- lib/brute/prompts.rb
|
|
@@ -362,6 +401,9 @@ files:
|
|
|
362
401
|
- lib/brute/rack/adapter.rb
|
|
363
402
|
- lib/brute/skill.rb
|
|
364
403
|
- lib/brute/system_prompt.rb
|
|
404
|
+
- lib/brute/token_counter.rb
|
|
405
|
+
- lib/brute/token_counter/approximate.rb
|
|
406
|
+
- lib/brute/token_counter/tiktoken.rb
|
|
365
407
|
- lib/brute/tool.rb
|
|
366
408
|
- lib/brute/tools.rb
|
|
367
409
|
- lib/brute/tools/adapter.rb
|
|
@@ -383,6 +425,7 @@ files:
|
|
|
383
425
|
- lib/brute/tools/todo_write.rb
|
|
384
426
|
- lib/brute/truncation.rb
|
|
385
427
|
- lib/brute/turn/agent_pipeline.rb
|
|
428
|
+
- lib/brute/turn/compaction_pipeline.rb
|
|
386
429
|
- lib/brute/turn/pipeline.rb
|
|
387
430
|
- lib/brute/turn/tool_pipeline.rb
|
|
388
431
|
- lib/brute/usage_detection/lang_chain.rb
|
|
@@ -397,7 +440,7 @@ files:
|
|
|
397
440
|
- lib/brute_cli/providers/shell_response.rb
|
|
398
441
|
homepage: https://github.com/general-intelligence-systems/brute
|
|
399
442
|
licenses:
|
|
400
|
-
-
|
|
443
|
+
- Apache-2.0
|
|
401
444
|
metadata:
|
|
402
445
|
documentation_uri: https://general-intelligence-systems.github.io/brute/
|
|
403
446
|
rdoc_options: []
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "brute"
|
|
5
|
-
|
|
6
|
-
module Brute
|
|
7
|
-
module Middleware
|
|
8
|
-
# Checks context size after each LLM call and triggers compaction
|
|
9
|
-
# when thresholds are exceeded.
|
|
10
|
-
#
|
|
11
|
-
# It should add a compaction event to the logs with the context token
|
|
12
|
-
# total listed... this way a model that supports extra context can
|
|
13
|
-
# include the compaction as well as the previous messages...
|
|
14
|
-
#
|
|
15
|
-
# Or an LLM that doesn't support it can just use the messages
|
|
16
|
-
# that come after the compaction
|
|
17
|
-
#
|
|
18
|
-
class CompactionCheck < Brute::Middleware::Base
|
|
19
|
-
def initialize(app, compactor: nil, system_prompt:, **compactor_opts)
|
|
20
|
-
@app = app
|
|
21
|
-
@compactor = compactor
|
|
22
|
-
@compactor_opts = compactor_opts
|
|
23
|
-
@system_prompt = system_prompt
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def call(env)
|
|
27
|
-
#@compactor ||= Compactor.new(env[:provider], **@compactor_opts)
|
|
28
|
-
|
|
29
|
-
#messages = env[:messages]
|
|
30
|
-
#usage = env[:metadata].dig(:tokens, :last_call)
|
|
31
|
-
|
|
32
|
-
#if @compactor.should_compact?(messages, usage: usage)
|
|
33
|
-
# result = @compactor.compact(messages)
|
|
34
|
-
# if result
|
|
35
|
-
# summary_text, _recent = result
|
|
36
|
-
# env[:metadata][:compaction] = {
|
|
37
|
-
# messages_before: messages.size,
|
|
38
|
-
# timestamp: Time.now.iso8601,
|
|
39
|
-
# }
|
|
40
|
-
# # Replace the message history with the summary
|
|
41
|
-
# env[:messages] = [
|
|
42
|
-
# Brute::Message.new(role: :system, content: @system_prompt),
|
|
43
|
-
# Brute::Message.new(role: :user, content: "[Previous conversation summary]\n\n#{summary_text}"),
|
|
44
|
-
# ]
|
|
45
|
-
# end
|
|
46
|
-
#end
|
|
47
|
-
|
|
48
|
-
@app.call(env)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Context compaction service. When the conversation grows past configurable
|
|
52
|
-
# thresholds, older messages are summarized into a condensed form and the
|
|
53
|
-
# original messages are dropped, keeping the context window manageable.
|
|
54
|
-
class Compactor
|
|
55
|
-
DEFAULTS = {
|
|
56
|
-
token_threshold: 100_000, # Compact when estimated tokens exceed this
|
|
57
|
-
message_threshold: 200, # Compact when message count exceeds this
|
|
58
|
-
retention_window: 6, # Minimum recent messages to always keep
|
|
59
|
-
summary_model: nil, # Model for summarization (uses agent's model if nil)
|
|
60
|
-
}.freeze
|
|
61
|
-
|
|
62
|
-
attr_reader :config
|
|
63
|
-
|
|
64
|
-
def initialize(provider, **opts)
|
|
65
|
-
@provider = provider
|
|
66
|
-
@config = DEFAULTS.merge(opts)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Check whether compaction should run based on current context state.
|
|
70
|
-
def should_compact?(messages, usage: nil)
|
|
71
|
-
return true if messages.size > @config[:message_threshold]
|
|
72
|
-
return true if usage && (usage[:total] || 0) > @config[:token_threshold]
|
|
73
|
-
false
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Compact the message history by summarizing older messages.
|
|
77
|
-
#
|
|
78
|
-
# Returns [summary_message, kept_messages] — the caller rebuilds
|
|
79
|
-
# the context from these.
|
|
80
|
-
def compact(messages)
|
|
81
|
-
total = messages.size
|
|
82
|
-
keep_count = [@config[:retention_window], total].min
|
|
83
|
-
return nil if total <= keep_count
|
|
84
|
-
|
|
85
|
-
old_messages = messages[0...(total - keep_count)]
|
|
86
|
-
recent_messages = messages[(total - keep_count)..]
|
|
87
|
-
|
|
88
|
-
summary_text = summarize(old_messages)
|
|
89
|
-
|
|
90
|
-
[summary_text, recent_messages]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def summarize(messages)
|
|
96
|
-
# Build a condensed representation of the conversation for the summarizer
|
|
97
|
-
conversation_text = messages.map { |m|
|
|
98
|
-
role = if m.respond_to?(:role)
|
|
99
|
-
m.role.to_s
|
|
100
|
-
else
|
|
101
|
-
"unknown"
|
|
102
|
-
end
|
|
103
|
-
content = if m.respond_to?(:content)
|
|
104
|
-
m.content.to_s[0..1000]
|
|
105
|
-
else
|
|
106
|
-
m.to_s[0..1000]
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Include tool call info for assistant messages
|
|
110
|
-
tool_info = ""
|
|
111
|
-
if m.respond_to?(:functions) && m.functions&.any?
|
|
112
|
-
calls = m.functions.map { |f| "#{f.name}(#{f.arguments.to_s[0..200]})" }
|
|
113
|
-
tool_info = " [tools: #{calls.join(", ")}]"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
"#{role}:#{tool_info} #{content}"
|
|
117
|
-
}.join("\n---\n")
|
|
118
|
-
|
|
119
|
-
prompt = <<~PROMPT
|
|
120
|
-
Summarize this conversation history for context continuity. The summary will replace
|
|
121
|
-
these messages in the context window, so include everything the agent needs to continue
|
|
122
|
-
working effectively.
|
|
123
|
-
|
|
124
|
-
Structure your summary as:
|
|
125
|
-
## Goal
|
|
126
|
-
What the user asked for.
|
|
127
|
-
|
|
128
|
-
## Progress
|
|
129
|
-
- Files read, created, or modified (list paths)
|
|
130
|
-
- Commands executed and their outcomes
|
|
131
|
-
- Key decisions made
|
|
132
|
-
|
|
133
|
-
## Current State
|
|
134
|
-
Where things stand right now — what's done and what remains.
|
|
135
|
-
|
|
136
|
-
## Next Steps
|
|
137
|
-
What should happen next based on the conversation.
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
CONVERSATION:
|
|
141
|
-
#{conversation_text}
|
|
142
|
-
PROMPT
|
|
143
|
-
|
|
144
|
-
model = @config[:summary_model] || "claude-sonnet-4-20250514"
|
|
145
|
-
res = @provider.complete(prompt, model: model)
|
|
146
|
-
res.content
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
__END__
|
|
154
|
-
|
|
155
|
-
describe "brute/middleware/040_compaction_check" do
|
|
156
|
-
# not implemented
|
|
157
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'bundler/setup'
|
|
4
|
-
require 'brute'
|
|
5
|
-
|
|
6
|
-
module Brute
|
|
7
|
-
module Middleware
|
|
8
|
-
class EventHandler < Brute::Middleware::Base
|
|
9
|
-
def initialize(app, handler_class:, **opts)
|
|
10
|
-
@app = app
|
|
11
|
-
@handler_class = handler_class
|
|
12
|
-
@opts = opts
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(env)
|
|
16
|
-
env[:events] = @handler_class.new(env[:events], **@opts)
|
|
17
|
-
@app.call(env)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
__END__
|
|
24
|
-
|
|
25
|
-
describe "brute/middleware/event_handler" do
|
|
26
|
-
# not implemented
|
|
27
|
-
end
|