Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/roast/cogs/agent/providers/pi/messages/tool_call_message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<pattern>" <path> (glob: <glob>, limit: <limit>)' –
# <pattern> is truncated to TRUNCATE_LIMIT chars; <path> 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)}\""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern is truncated but path, glob, and limit are appended untruncated, and they stack: GREP "<45>" <full path> (glob: <full glob>, limit: <n>) can easily run 150+ chars, contradicting the "generally one line" goal stated in the foundation comments. If the one-line bound matters, truncate the composed result; otherwise align the comment wording.

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: "<NAME> <key>: <value>, ..." – the upcased tool name, then each
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n> <match|matches>[ · NOTE <notes>]" – <n> 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+:/) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Match/note heuristic is best-effort and will miscount. %r{\A\S+/} treats any line starting with a non-space run + / as a match, and \A(?:\S+:)?\d+: as a line-number hit. Real grep hits that don't carry a path/line prefix (e.g. grep -h, or matched content that doesn't fit either shape) get bucketed as notes and undercount; conversely a note containing an early / counts as a match. Since "3 matches" is shown to the user, a wrong count is a visible lie. It won't break anything, but please add a comment that this depends on Pi's grep output format and is intentionally approximate — so nobody later trusts the count as exact or "fixes" it by tightening the regex without knowing the contract.

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading