From 919a1755109b06a383d9e7b73c92953dbb74ae66 Mon Sep 17 00:00:00 2001 From: Lasmar Khalifa Date: Thu, 2 Jul 2026 12:13:07 -0400 Subject: [PATCH] add grep tool formatters --- .../pi/messages/tool_call_message.rb | 30 +++++++++++++ .../pi/messages/tool_result_message.rb | 24 ++++++++++ .../pi/messages/tool_call_message_test.rb | 45 +++++++++++++++++++ .../pi/messages/tool_result_message_test.rb | 44 ++++++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb b/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb index 96098fd0..b4423bb9 100644 --- a/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb +++ b/lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb @@ -139,6 +139,36 @@ def format_edit "EDIT #{path} (#{count} #{"edit".pluralize(count)})" end + # Formats a grep tool call. + # + # Input fields: + # :pattern (String) – regex/text to search for [required] + # :path (String) – file or directory to search [optional] + # :glob (String) – glob filter for matched files [optional] + # :limit (Integer) – max number of matches [optional] + # + # Output: 'GREP "" (glob: , limit: )' – + # is truncated to TRUNCATE_LIMIT chars; is shown only + # when present. The "glob:" and "limit:" details are each included only + # when present and joined with ", " inside a single trailing "(...)". + # + # Examples: + # GREP "def format" lib (glob: *.rb, limit: 50) + # GREP "TODO" (limit: 50) + # GREP "TODO" + # + #: () -> String + def format_grep + pattern, path, glob, limit = arguments.values_at(:pattern, :path, :glob, :limit) + base = "GREP \"#{truncate(pattern)}\"" + base = "#{base} #{path}" if path.present? + details = [ + ("glob: #{glob}" if glob.present?), + ("limit: #{limit}" if limit.present?), + ].compact + details.any? ? "#{base} (#{details.join(", ")})" : base + end + # Formats a tool call for which Roast has no dedicated formatter. # # Output: " : , ..." – the upcased tool name, then each diff --git a/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb b/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb index 3ffe6c85..cb99c5bd 100644 --- a/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb +++ b/lib/roast/cogs/agent/providers/pi/messages/tool_result_message.rb @@ -123,6 +123,30 @@ def format_edit ok_line(@input[:path]) end + # Formats a grep tool result. + # + # Content: matching lines, and possibly informational notes, one per + # line. + # + # Output: "GREP OK [ · NOTE ]" – counts + # lines that look like matches (a leading path or line-number prefix); + # any remaining lines are joined into a truncated NOTE, shown only when + # there are both matches and notes. + # + # Examples: + # GREP OK 3 matches + # GREP OK 1 match + # GREP OK 0 matches + # + #: () -> String + def format_grep + lines = content.to_s.lines.map(&:strip).reject(&:empty?) + matches, notes = lines.partition { |line| line.match?(%r{\A\S+/}) || line.match?(/\A(?:\S+:)?\d+:/) } + count = matches.length + note = "NOTE #{truncate(notes.join(" "))}" if matches.any? && notes.any? + ok_line("#{count} #{"match".pluralize(count)}", note) + end + # Formats a result for which Roast has no dedicated formatter. # # Content: the tool's output text. diff --git a/test/roast/cogs/agent/providers/pi/messages/tool_call_message_test.rb b/test/roast/cogs/agent/providers/pi/messages/tool_call_message_test.rb index fc57e80a..64c9aa39 100644 --- a/test/roast/cogs/agent/providers/pi/messages/tool_call_message_test.rb +++ b/test/roast/cogs/agent/providers/pi/messages/tool_call_message_test.rb @@ -109,6 +109,51 @@ class ToolCallMessageTest < ActiveSupport::TestCase assert_equal "EDIT lib/roast.rb (0 edits)", msg.format end + test "format renders GREP with the pattern, path, and glob" do + msg = ToolCallMessage.new( + id: "1", + name: "grep", + arguments: { pattern: "def format", path: "lib", glob: "*.rb" }, + ) + assert_equal 'GREP "def format" lib (glob: *.rb)', msg.format + end + + test "format renders GREP with the pattern and path but no glob" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "TODO", path: "lib/roast" }) + assert_equal 'GREP "TODO" lib/roast', msg.format + end + + test "format renders GREP with the pattern and glob but no path" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "TODO", glob: "*.rb" }) + assert_equal 'GREP "TODO" (glob: *.rb)', msg.format + end + + test "format renders GREP with only the pattern" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "TODO" }) + assert_equal 'GREP "TODO"', msg.format + end + + test "format renders GREP with the glob and limit joined in one parenthetical" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "TODO", path: "lib", glob: "*.rb", limit: 50 }) + assert_equal 'GREP "TODO" lib (glob: *.rb, limit: 50)', msg.format + end + + test "format renders GREP with the limit but no glob" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "TODO", limit: 50 }) + assert_equal 'GREP "TODO" (limit: 50)', msg.format + end + + test "format truncates a long grep pattern" do + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: "x" * 100 }) + assert_equal "GREP \"#{"x" * (ToolCallMessage::TRUNCATE_LIMIT - 3)}...\"", msg.format + end + + test "format truncates the grep pattern but not the path" do + long = "x" * 100 + msg = ToolCallMessage.new(id: "1", name: "grep", arguments: { pattern: long, path: long }) + assert_equal "GREP \"#{"x" * (ToolCallMessage::TRUNCATE_LIMIT - 3)}...\" #{long}", msg.format + end + test "format renders an unhandled tool as NAME key: value, ..." do msg = ToolCallMessage.new( id: "1", diff --git a/test/roast/cogs/agent/providers/pi/messages/tool_result_message_test.rb b/test/roast/cogs/agent/providers/pi/messages/tool_result_message_test.rb index e6c3aef7..35e76019 100644 --- a/test/roast/cogs/agent/providers/pi/messages/tool_result_message_test.rb +++ b/test/roast/cogs/agent/providers/pi/messages/tool_result_message_test.rb @@ -126,6 +126,50 @@ def setup assert_equal "EDIT OK", msg.format(@context) end + test "format summarizes grep output with a match count" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "grep", + content: "lib/roast.rb:1:class Roast\nlib/roast/version.rb:3:VERSION = \"1.0\"", + is_error: false, + ) + + assert_equal "GREP OK 2 matches", msg.format(@context) + end + + test "format pluralizes a single grep match" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "grep", + content: "lib/roast.rb:1:class Roast", + is_error: false, + ) + + assert_equal "GREP OK 1 match", msg.format(@context) + end + + test "format reports zero grep matches when there is no output" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "grep", + content: nil, + is_error: false, + ) + + assert_equal "GREP OK 0 matches", msg.format(@context) + end + + test "format appends a NOTE when grep output includes non-match lines" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "grep", + content: "lib/roast.rb:1:class Roast\nresults truncated", + is_error: false, + ) + + assert_equal "GREP OK 1 match · NOTE results truncated", msg.format(@context) + end + test "format renders NAME ERROR with the message for an error result" do msg = ToolResultMessage.new( tool_call_id: "1",