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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/compaction/middleware/sliding_window.rb +1 -1
  3. data/lib/brute/compaction/middleware/strategy.rb +0 -2
  4. data/lib/brute/compaction/summarize.rb +30 -21
  5. data/lib/brute/completion/lang_chain.rb +34 -31
  6. data/lib/brute/completion/llmrb.rb +31 -29
  7. data/lib/brute/completion/open_router.rb +43 -40
  8. data/lib/brute/completion/ruby_llm.rb +28 -27
  9. data/lib/brute/contrib/otel.rb +81 -49
  10. data/lib/brute/eval/case.rb +7 -2
  11. data/lib/brute/eval/suite.rb +10 -5
  12. data/lib/brute/eval/transcript.rb +3 -3
  13. data/lib/brute/eval/world.rb +2 -2
  14. data/lib/brute/eval.rb +1 -1
  15. data/lib/brute/events/handler.rb +3 -1
  16. data/lib/brute/events/prefixed_terminal_output.rb +3 -1
  17. data/lib/brute/events/terminal_output_handler.rb +1 -1
  18. data/lib/brute/hooks.rb +88 -112
  19. data/lib/brute/message_transport/anthropic.rb +13 -3
  20. data/lib/brute/message_transport/llm.rb +8 -4
  21. data/lib/brute/message_transport/open_router.rb +30 -16
  22. data/lib/brute/message_transport/openai.rb +14 -7
  23. data/lib/brute/message_transport/ruby_llm.rb +21 -17
  24. data/lib/brute/message_transport/ruby_open_ai.rb +59 -57
  25. data/lib/brute/messages.rb +15 -8
  26. data/lib/brute/middleware/000_base.rb +5 -5
  27. data/lib/brute/middleware/002_session_log.rb +12 -4
  28. data/lib/brute/middleware/008_checkpoint.rb +26 -18
  29. data/lib/brute/middleware/010_max_iterations.rb +1 -1
  30. data/lib/brute/middleware/020_system_prompt.rb +1 -1
  31. data/lib/brute/middleware/040_default_compaction_pipeline.rb +49 -45
  32. data/lib/brute/middleware/070_default_tool_pipeline.rb +44 -53
  33. data/lib/brute/prompt_template.rb +19 -15
  34. data/lib/brute/prompts/base.rb +19 -10
  35. data/lib/brute/prompts/environment.rb +3 -1
  36. data/lib/brute/prompts/instructions.rb +9 -7
  37. data/lib/brute/prompts/skills.rb +5 -3
  38. data/lib/brute/rack/adapter.rb +40 -23
  39. data/lib/brute/skill.rb +136 -89
  40. data/lib/brute/system_prompt.rb +9 -9
  41. data/lib/brute/token_counter.rb +3 -1
  42. data/lib/brute/tool.rb +10 -6
  43. data/lib/brute/tools/adapter.rb +53 -43
  44. data/lib/brute/tools/fs/snapshot_store.rb +5 -1
  45. data/lib/brute/tools/fs_patch.rb +16 -8
  46. data/lib/brute/tools/fs_read.rb +107 -80
  47. data/lib/brute/tools/fs_remove.rb +6 -2
  48. data/lib/brute/tools/fs_search.rb +14 -4
  49. data/lib/brute/tools/fs_undo.rb +6 -2
  50. data/lib/brute/tools/fs_write.rb +5 -1
  51. data/lib/brute/tools/net_fetch.rb +6 -2
  52. data/lib/brute/tools/question.rb +42 -39
  53. data/lib/brute/tools/shell.rb +20 -5
  54. data/lib/brute/tools/skill_load.rb +44 -39
  55. data/lib/brute/tools/todo_write.rb +19 -15
  56. data/lib/brute/truncation.rb +68 -43
  57. data/lib/brute/turn/agent_pipeline.rb +23 -13
  58. data/lib/brute/turn/compaction_pipeline.rb +4 -3
  59. data/lib/brute/turn/pipeline.rb +87 -96
  60. data/lib/brute/turn/tool_pipeline.rb +4 -3
  61. data/lib/brute/usage_detection/llmrb.rb +18 -14
  62. data/lib/brute/usage_detection/open_router.rb +20 -17
  63. data/lib/brute/usage_detection/ruby_llm.rb +18 -14
  64. data/lib/brute/usage_detection/usage.rb +10 -1
  65. data/lib/brute/utils/diff.rb +18 -10
  66. data/lib/brute/version.rb +1 -1
  67. data/lib/brute.rb +10 -11
  68. data/lib/brute_cli/providers/shell.rb +32 -29
  69. data/lib/brute_cli/providers/shell_response.rb +20 -18
  70. metadata +30 -5
  71. data/lib/brute/middleware/040_compaction_check.rb +0 -73
  72. data/lib/brute/middleware/070_tool_pipeline.rb +0 -61
  73. 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(:assistant, message.content.to_s,
43
- { tool_calls: wire, original_tool_calls: wire })
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 = if message.tool_call?
57
- message.tool_calls.map do |tc|
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
- return @result.choices.map { |choice| choice["message"] || choice[:message] } if @result.respond_to?(:choices)
30
-
31
- super
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
- hash[:role] = hash[:role].to_sym if hash.key?(:role)
40
- hash[:tool_calls] = hash[:tool_calls].map { |tc| wrap_tool_call(tc) } if hash[:tool_calls]
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(**hash.slice(:role, :content, :tool_calls, :tool_call_id))
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
- return tc unless tc[:function] # already flat { id:, name:, arguments: }
57
-
58
- function = tc[:function].to_h.transform_keys(&:to_sym)
59
- arguments = function[:arguments].to_s
60
-
61
- {
62
- id: tc[:id],
63
- name: function[:name],
64
- arguments: JSON.parse(arguments.empty? ? "{}" : arguments),
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: (message.content unless message.content.to_s.empty?),
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
- return @result.choices.map(&:message) if @result.respond_to?(:choices)
49
-
50
- super
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
- arguments = begin
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
- def wrap(message)
35
- raw_calls = message.tool_calls
36
- calls_list = raw_calls.respond_to?(:values) ? raw_calls.values : raw_calls
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
- tool_calls = calls_list&.map do |tc|
39
- Brute::ToolCall.new(
40
- id: tc.id,
41
- name: tc.name,
42
- arguments: tc.arguments
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
- payload = {
8
- role: message.role.to_s
9
- }
10
-
11
- # Include content if present
12
- payload[:content] = message.content if message.content
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
- # Include tool call ID for tool outputs
15
- payload[:tool_call_id] = message.tool_call_id if message.tool_call_id
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
- # Convert Brute::ToolCall objects to ruby-openai nested tool call hashes
18
- if message.tool_call?
19
- payload[:tool_calls] = message.tool_calls.map do |tc|
20
- {
21
- id: tc.id,
22
- type: "function",
23
- function: {
24
- name: tc.name,
25
- arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json
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
- def wrap(message)
38
- # Normalize keys to symbols for pattern matching
39
- hash = message.to_h.transform_keys(&:to_sym)
39
+ def wrap(message)
40
+ # Normalize keys to symbols for pattern matching
41
+ hash = message.to_h.transform_keys(&:to_sym)
40
42
 
41
- case hash
42
- # Branch 1: System, User, or Tool responses with text content
43
- in { role: ("system" | "user" | "tool") => role }
44
- Brute::Message.new(
45
- role: role,
46
- content: hash[:content],
47
- tool_call_id: hash[:tool_call_id]
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
- # Branch 2: Assistant tool calls request
51
- in { role: "assistant", tool_calls: Array => raw_calls }
52
- tool_calls = raw_calls.map do |tc|
53
- # Handle both string and symbol keys within nested tool_call hashes
54
- tc_hash = tc.to_h.transform_keys(&:to_sym)
55
- fn_hash = (tc_hash[:function] || {}).transform_keys(&:to_sym)
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
- Brute::ToolCall.new(
58
- id: tc_hash[:id],
59
- name: fn_hash[:name],
60
- arguments: fn_hash[:arguments]
61
- )
62
- end
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
- Brute::Message.new(
65
- role: :assistant,
66
- content: hash[:content],
67
- tool_calls: tool_calls
68
- )
66
+ Brute::Message.new(
67
+ role: :assistant,
68
+ content: hash[:content],
69
+ tool_calls: tool_calls,
70
+ )
69
71
 
70
- # Branch 3: Standard Assistant text message
71
- in { role: "assistant" }
72
- Brute::Message.new(
73
- role: :assistant,
74
- content: hash[:content]
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
- else
78
- raise "Unrecognised message format for ruby-openai: #{message.inspect}"
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
@@ -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(:role, :content, :tool_calls, :tool_call_id) do
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: role.to_sym,
33
- content: content,
34
- tool_calls: formatted_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
- hash[:tool_calls] = tool_calls.map(&:to_h) if tool_calls
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(ENTER_EVENT, env, self)
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 — ENTER_EVENT, not Brute::Hooks::ENTER_EVENT.
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
- # ENTER_EVENT rather than Brute::Hooks::ENTER_EVENT.
53
- Brute::Middleware::Base.const_get(:ENTER_EVENT).should == :enter
54
- Brute::Middleware::SystemPrompt.const_get(:AFTER_TOOL_EVENT).should == :after_tool
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
- load_into(env[:messages]) if @path && File.exist?(@path)
33
+ if @path && File.exist?(@path)
34
+ load_into(env[:messages])
35
+ end
34
36
  @app.call(env)
35
- persist(env[:messages]) if @path
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
- loaded << Brute::Message.new(**JSON.parse(line, symbolize_names: true)) unless line.empty?
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
- next if message.role == :system
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
- restore(env) unless env[:metadata][:checkpoint]
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
- return [] unless path && File.exist?(path)
54
-
55
- File.foreach(path).filter_map do |line|
56
- line = line.strip
57
- JSON.parse(line, symbolize_names: true) unless line.empty?
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
- return unless @resume
66
-
67
- record = find_record
68
- if record.nil?
69
- return if @resume == :latest
70
-
71
- raise KeyError, "no checkpoint #{@resume.inspect} in #{@path}"
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
@@ -23,7 +23,7 @@ module Brute
23
23
  def call(env)
24
24
  if max_iterations_reached?(env)
25
25
  env[:messages] << Brute::Message.new(
26
- role: :user,
26
+ role: :user,
27
27
  content: "Maximum iterations reached.",
28
28
  )
29
29
  else
@@ -37,7 +37,7 @@ module Brute
37
37
  result = @system_prompt.prepare(ctx)
38
38
  unless result.empty?
39
39
  env[:messages].unshift(
40
- Brute::Message.new(role: :system, content: result.to_s)
40
+ Brute::Message.new(role: :system, content: result.to_s),
41
41
  )
42
42
  end
43
43
  end