brute 5.1.0 → 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 +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 +3 -3
- 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 +88 -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 +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 +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
|
@@ -39,8 +39,11 @@ module Brute
|
|
|
39
39
|
wire = message.tool_calls.map do |tc|
|
|
40
40
|
{ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.generate(tc.arguments) } }
|
|
41
41
|
end
|
|
42
|
-
::LLM::Message.new(
|
|
43
|
-
|
|
42
|
+
::LLM::Message.new(
|
|
43
|
+
:assistant,
|
|
44
|
+
message.content.to_s,
|
|
45
|
+
{ tool_calls: wire, original_tool_calls: wire },
|
|
46
|
+
)
|
|
44
47
|
else
|
|
45
48
|
::LLM::Message.new(:assistant, message.content)
|
|
46
49
|
end
|
|
@@ -53,8 +56,9 @@ module Brute
|
|
|
53
56
|
|
|
54
57
|
# LLM::Message (from an LLM::Response) -> Brute::Message.
|
|
55
58
|
def wrap(message)
|
|
56
|
-
tool_calls =
|
|
57
|
-
|
|
59
|
+
tool_calls = nil
|
|
60
|
+
if message.tool_call?
|
|
61
|
+
tool_calls = message.tool_calls.map do |tc|
|
|
58
62
|
Brute::ToolCall.new(id: tc.id, name: tc["name"], arguments: tc.arguments.to_h)
|
|
59
63
|
end
|
|
60
64
|
end
|
|
@@ -26,9 +26,11 @@ module Brute
|
|
|
26
26
|
# An OpenRouter::Response's messages (one per choice; in practice
|
|
27
27
|
# OpenRouter returns exactly one).
|
|
28
28
|
def messages
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if @result.respond_to?(:choices)
|
|
30
|
+
@result.choices.map { |choice| choice["message"] || choice[:message] }
|
|
31
|
+
else
|
|
32
|
+
super
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
@@ -36,14 +38,25 @@ module Brute
|
|
|
36
38
|
def wrap(message)
|
|
37
39
|
# Coerce string keys to symbol keys if necessary
|
|
38
40
|
hash = message.to_h.transform_keys(&:to_sym)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if hash.key?(:role)
|
|
42
|
+
hash[:role] = hash[:role].to_sym
|
|
43
|
+
end
|
|
44
|
+
if hash[:tool_calls]
|
|
45
|
+
hash[:tool_calls] = hash[:tool_calls].map { |tc| wrap_tool_call(tc) }
|
|
46
|
+
end
|
|
41
47
|
|
|
42
48
|
case hash
|
|
43
49
|
in { role: (:system | :user | :assistant | :tool) }
|
|
44
50
|
# Slice away provider extras (refusal, reasoning, model, ...)
|
|
45
51
|
# that Brute::Message doesn't know.
|
|
46
|
-
Brute::Message.new(
|
|
52
|
+
Brute::Message.new(
|
|
53
|
+
**hash.slice(
|
|
54
|
+
:role,
|
|
55
|
+
:content,
|
|
56
|
+
:tool_calls,
|
|
57
|
+
:tool_call_id,
|
|
58
|
+
),
|
|
59
|
+
)
|
|
47
60
|
else
|
|
48
61
|
raise "Unrecognised message format #{message.inspect}"
|
|
49
62
|
end
|
|
@@ -53,16 +66,17 @@ module Brute
|
|
|
53
66
|
# -> the flat { id:, name:, arguments: Hash } Brute::Message understands.
|
|
54
67
|
def wrap_tool_call(tool_call)
|
|
55
68
|
tc = tool_call.to_h.transform_keys(&:to_sym)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
if tc[:function]
|
|
70
|
+
function = tc[:function].to_h.transform_keys(&:to_sym)
|
|
71
|
+
arguments = function[:arguments].to_s
|
|
72
|
+
{
|
|
73
|
+
id: tc[:id],
|
|
74
|
+
name: function[:name],
|
|
75
|
+
arguments: JSON.parse(arguments.empty? ? "{}" : arguments),
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
tc
|
|
79
|
+
end
|
|
66
80
|
end
|
|
67
81
|
end
|
|
68
82
|
end
|
|
@@ -28,9 +28,14 @@ module Brute
|
|
|
28
28
|
{ role: "tool", tool_call_id: message.tool_call_id, content: message.content.to_s }
|
|
29
29
|
when :assistant
|
|
30
30
|
if message.tool_call?
|
|
31
|
+
content = message.content
|
|
32
|
+
if content.to_s.empty?
|
|
33
|
+
content = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
{
|
|
32
37
|
role: "assistant",
|
|
33
|
-
content:
|
|
38
|
+
content: content,
|
|
34
39
|
tool_calls: message.tool_calls.map { |tc|
|
|
35
40
|
{ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.generate(tc.arguments) } }
|
|
36
41
|
},
|
|
@@ -45,9 +50,11 @@ module Brute
|
|
|
45
50
|
|
|
46
51
|
# A chat completion response's messages (one per choice).
|
|
47
52
|
def messages
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
if @result.respond_to?(:choices)
|
|
54
|
+
@result.choices.map(&:message)
|
|
55
|
+
else
|
|
56
|
+
super
|
|
57
|
+
end
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
private
|
|
@@ -56,10 +63,10 @@ module Brute
|
|
|
56
63
|
# arguments arrive as a JSON string; parse them into a Hash.
|
|
57
64
|
def wrap(message)
|
|
58
65
|
tool_calls = message.tool_calls&.map do |tc|
|
|
59
|
-
|
|
60
|
-
JSON.parse(tc.function.arguments.to_s)
|
|
66
|
+
begin
|
|
67
|
+
arguments = JSON.parse(tc.function.arguments.to_s)
|
|
61
68
|
rescue JSON::ParserError
|
|
62
|
-
{}
|
|
69
|
+
arguments = {}
|
|
63
70
|
end
|
|
64
71
|
Brute::ToolCall.new(id: tc.id, name: tc.function.name, arguments: arguments)
|
|
65
72
|
end
|
|
@@ -24,32 +24,36 @@ module Brute
|
|
|
24
24
|
role: message.role,
|
|
25
25
|
content: message.content,
|
|
26
26
|
tool_calls: tool_calls,
|
|
27
|
-
tool_call_id: message.tool_call_id
|
|
27
|
+
tool_call_id: message.tool_call_id,
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
33
|
# RubyLLM::Message -> Brute::Message.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
def wrap(message)
|
|
35
|
+
raw_calls = message.tool_calls
|
|
36
|
+
if raw_calls.respond_to?(:values)
|
|
37
|
+
calls_list = raw_calls.values
|
|
38
|
+
else
|
|
39
|
+
calls_list = raw_calls
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
tool_calls = calls_list&.map do |tc|
|
|
43
|
+
Brute::ToolCall.new(
|
|
44
|
+
id: tc.id,
|
|
45
|
+
name: tc.name,
|
|
46
|
+
arguments: tc.arguments,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
Brute::Message.new(
|
|
51
|
+
role: message.role,
|
|
52
|
+
content: message.content&.to_s, # Preserves nil safely
|
|
53
|
+
tool_calls: tool_calls,
|
|
54
|
+
tool_call_id: message.tool_call_id,
|
|
43
55
|
)
|
|
44
56
|
end
|
|
45
|
-
|
|
46
|
-
Brute::Message.new(
|
|
47
|
-
role: message.role,
|
|
48
|
-
content: message.content&.to_s, # Preserves nil safely
|
|
49
|
-
tool_calls: tool_calls,
|
|
50
|
-
tool_call_id: message.tool_call_id
|
|
51
|
-
)
|
|
52
|
-
end
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
59
|
end
|
|
@@ -4,80 +4,82 @@ module Brute
|
|
|
4
4
|
|
|
5
5
|
# Brute::Message -> ruby-openai Hash payload
|
|
6
6
|
def self.dump(message)
|
|
7
|
-
|
|
8
|
-
role: message.role.to_s
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
{
|
|
8
|
+
role: message.role.to_s,
|
|
9
|
+
}.tap do |payload|
|
|
10
|
+
# Include content if present
|
|
11
|
+
if message.content
|
|
12
|
+
payload[:content] = message.content
|
|
13
|
+
end
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
# Include tool call ID for tool outputs
|
|
16
|
+
if message.tool_call_id
|
|
17
|
+
payload[:tool_call_id] = message.tool_call_id
|
|
18
|
+
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
# Convert Brute::ToolCall objects to ruby-openai nested tool call hashes
|
|
21
|
+
if message.tool_call?
|
|
22
|
+
payload[:tool_calls] = message.tool_calls.map do |tc|
|
|
23
|
+
{
|
|
24
|
+
id: tc.id,
|
|
25
|
+
type: "function",
|
|
26
|
+
function: {
|
|
27
|
+
name: tc.name,
|
|
28
|
+
arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json,
|
|
29
|
+
},
|
|
26
30
|
}
|
|
27
|
-
|
|
31
|
+
end
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
|
-
|
|
31
|
-
payload
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
38
|
# ruby-openai Hash (or API response choice message) -> Brute::Message
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
def wrap(message)
|
|
40
|
+
# Normalize keys to symbols for pattern matching
|
|
41
|
+
hash = message.to_h.transform_keys(&:to_sym)
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
case hash
|
|
44
|
+
# Branch 1: System, User, or Tool responses with text content
|
|
45
|
+
in { role: ("system" | "user" | "tool") => role }
|
|
46
|
+
Brute::Message.new(
|
|
47
|
+
role: role,
|
|
48
|
+
content: hash[:content],
|
|
49
|
+
tool_call_id: hash[:tool_call_id],
|
|
50
|
+
)
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
# Branch 2: Assistant tool calls request
|
|
53
|
+
in { role: "assistant", tool_calls: Array => raw_calls }
|
|
54
|
+
tool_calls = raw_calls.map do |tc|
|
|
55
|
+
# Handle both string and symbol keys within nested tool_call hashes
|
|
56
|
+
tc_hash = tc.to_h.transform_keys(&:to_sym)
|
|
57
|
+
fn_hash = (tc_hash[:function] || {}).transform_keys(&:to_sym)
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
Brute::ToolCall.new(
|
|
60
|
+
id: tc_hash[:id],
|
|
61
|
+
name: fn_hash[:name],
|
|
62
|
+
arguments: fn_hash[:arguments],
|
|
63
|
+
)
|
|
64
|
+
end
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
Brute::Message.new(
|
|
67
|
+
role: :assistant,
|
|
68
|
+
content: hash[:content],
|
|
69
|
+
tool_calls: tool_calls,
|
|
70
|
+
)
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
# Branch 3: Standard Assistant text message
|
|
73
|
+
in { role: "assistant" }
|
|
74
|
+
Brute::Message.new(
|
|
75
|
+
role: :assistant,
|
|
76
|
+
content: hash[:content],
|
|
77
|
+
)
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
else
|
|
80
|
+
raise "Unrecognised message format for ruby-openai: #{message.inspect}"
|
|
81
|
+
end
|
|
79
82
|
end
|
|
80
|
-
end
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
end
|
data/lib/brute/messages.rb
CHANGED
|
@@ -22,27 +22,34 @@ module Brute
|
|
|
22
22
|
# Brute::ToolCall.new(id: "tc1", name: "shell", arguments: { "command" => "ls" }),
|
|
23
23
|
# ])
|
|
24
24
|
# Brute::Message.new(role: :tool, content: "result", tool_call_id: "tc1")
|
|
25
|
-
Message = Data.define(
|
|
25
|
+
Message = Data.define(
|
|
26
|
+
:role,
|
|
27
|
+
:content,
|
|
28
|
+
:tool_calls,
|
|
29
|
+
:tool_call_id,
|
|
30
|
+
) do
|
|
26
31
|
def initialize(role:, content: nil, tool_calls: nil, tool_call_id: nil)
|
|
27
32
|
formatted_calls = tool_calls&.map do |tc|
|
|
28
33
|
tc.is_a?(ToolCall) ? tc : ToolCall.new(**tc.to_h.transform_keys(&:to_sym))
|
|
29
34
|
end
|
|
30
|
-
|
|
35
|
+
|
|
31
36
|
super(
|
|
32
|
-
role:
|
|
33
|
-
content:
|
|
34
|
-
tool_calls:
|
|
37
|
+
role: role.to_sym,
|
|
38
|
+
content: content,
|
|
39
|
+
tool_calls: formatted_calls,
|
|
35
40
|
tool_call_id: tool_call_id
|
|
36
41
|
)
|
|
37
42
|
end
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
def tool_call? = !tool_calls.nil? && !tool_calls.empty?
|
|
40
45
|
alias_method :has_tool_calls?, :tool_call?
|
|
41
|
-
|
|
46
|
+
|
|
42
47
|
# Clean, JSON-ready hash export dropping nil values
|
|
43
48
|
def to_h(...)
|
|
44
49
|
hash = super
|
|
45
|
-
|
|
50
|
+
if tool_calls
|
|
51
|
+
hash[:tool_calls] = tool_calls.map(&:to_h)
|
|
52
|
+
end
|
|
46
53
|
hash.compact
|
|
47
54
|
end
|
|
48
55
|
end
|
|
@@ -11,13 +11,13 @@ module Brute
|
|
|
11
11
|
#
|
|
12
12
|
# class Shout < Brute::Middleware::Base
|
|
13
13
|
# def call(env)
|
|
14
|
-
# emit(
|
|
14
|
+
# env.emit(MIDDLEWARE_START_EVENT, self)
|
|
15
15
|
# @app.call(env)
|
|
16
16
|
# end
|
|
17
17
|
# end
|
|
18
18
|
#
|
|
19
19
|
# Brute::Hooks is included, so the event names are first class in every
|
|
20
|
-
# subclass —
|
|
20
|
+
# subclass — MIDDLEWARE_START_EVENT, not Brute::Hooks::MIDDLEWARE_START_EVENT.
|
|
21
21
|
class Base
|
|
22
22
|
include Brute::Hooks
|
|
23
23
|
|
|
@@ -49,9 +49,9 @@ describe "brute/middleware/000_base" do
|
|
|
49
49
|
Brute::Middleware::Base.new(terminal, :extra, keyword: true).call({}).should == {}
|
|
50
50
|
|
|
51
51
|
# The event names resolve through the class itself, so a subclass writes
|
|
52
|
-
#
|
|
53
|
-
Brute::Middleware::Base.const_get(:
|
|
54
|
-
Brute::Middleware::SystemPrompt.const_get(:
|
|
52
|
+
# MIDDLEWARE_START_EVENT rather than Brute::Hooks::MIDDLEWARE_START_EVENT.
|
|
53
|
+
Brute::Middleware::Base.const_get(:MIDDLEWARE_START_EVENT).should == :middleware_start
|
|
54
|
+
Brute::Middleware::SystemPrompt.const_get(:TOOL_END_EVENT).should == :tool_end
|
|
55
55
|
|
|
56
56
|
# Every middleware in the chain descends from it.
|
|
57
57
|
Brute::Middleware::SystemPrompt.ancestors.should.include Brute::Middleware::Base
|
|
@@ -30,9 +30,13 @@ module Brute
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def call(env)
|
|
33
|
-
|
|
33
|
+
if @path && File.exist?(@path)
|
|
34
|
+
load_into(env[:messages])
|
|
35
|
+
end
|
|
34
36
|
@app.call(env)
|
|
35
|
-
|
|
37
|
+
if @path
|
|
38
|
+
persist(env[:messages])
|
|
39
|
+
end
|
|
36
40
|
env
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -42,7 +46,9 @@ module Brute
|
|
|
42
46
|
loaded = []
|
|
43
47
|
File.foreach(@path) do |line|
|
|
44
48
|
line = line.strip
|
|
45
|
-
|
|
49
|
+
unless line.empty?
|
|
50
|
+
loaded << Brute::Message.new(**JSON.parse(line, symbolize_names: true))
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
messages.unshift(*loaded)
|
|
48
54
|
end
|
|
@@ -51,7 +57,9 @@ module Brute
|
|
|
51
57
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
52
58
|
File.open(@path, "w") do |f|
|
|
53
59
|
messages.each do |message|
|
|
54
|
-
|
|
60
|
+
if message.role == :system
|
|
61
|
+
next
|
|
62
|
+
end
|
|
55
63
|
|
|
56
64
|
f.puts(JSON.generate(message.to_h))
|
|
57
65
|
end
|
|
@@ -41,7 +41,9 @@ module Brute
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def call(env)
|
|
44
|
-
|
|
44
|
+
unless env[:metadata][:checkpoint]
|
|
45
|
+
restore(env)
|
|
46
|
+
end
|
|
45
47
|
@app.call(env)
|
|
46
48
|
persist(env)
|
|
47
49
|
env
|
|
@@ -50,11 +52,15 @@ module Brute
|
|
|
50
52
|
# Parsed checkpoint records (symbol keys, messages as plain hashes),
|
|
51
53
|
# oldest first.
|
|
52
54
|
def self.list(path)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if path && File.exist?(path)
|
|
56
|
+
File.foreach(path).filter_map do |line|
|
|
57
|
+
line = line.strip
|
|
58
|
+
unless line.empty?
|
|
59
|
+
JSON.parse(line, symbolize_names: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
[]
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
|
|
@@ -62,19 +68,21 @@ module Brute
|
|
|
62
68
|
|
|
63
69
|
def restore(env)
|
|
64
70
|
env[:metadata][:checkpoint] = { path: @path }
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
if @resume
|
|
72
|
+
record = find_record
|
|
73
|
+
if record.nil?
|
|
74
|
+
unless @resume == :latest
|
|
75
|
+
raise KeyError, "no checkpoint #{@resume.inspect} in #{@path}"
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
history = record[:messages].map { |h| Brute::Message.new(**h) }
|
|
79
|
+
index = env[:messages].index { |m| m.role != :system } || env[:messages].size
|
|
80
|
+
env[:messages].insert(index, *history)
|
|
81
|
+
env[:metadata][:checkpoint][:id] = record[:id]
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
nil
|
|
72
85
|
end
|
|
73
|
-
|
|
74
|
-
history = record[:messages].map { |h| Brute::Message.new(**h) }
|
|
75
|
-
index = env[:messages].index { |m| m.role != :system } || env[:messages].size
|
|
76
|
-
env[:messages].insert(index, *history)
|
|
77
|
-
env[:metadata][:checkpoint][:id] = record[:id]
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
def find_record
|