From e3989e85f10796b85bb4255579992e91a2d4d064 Mon Sep 17 00:00:00 2001 From: Lasmar Khalifa Date: Thu, 2 Jul 2026 11:43:13 -0400 Subject: [PATCH] add read tool formatters --- .../pi/messages/tool_call_message.rb | 34 ++++++++++++++++++ .../pi/messages/tool_result_message.rb | 20 +++++++++++ .../pi/messages/tool_call_message_test.rb | 35 +++++++++++++++++++ .../pi/messages/tool_result_message_test.rb | 33 +++++++++++++++++ 4 files changed, 122 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 689e6844..a9414aa5 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 @@ -63,6 +63,40 @@ def format_bash command.empty? ? "BASH" : "BASH #{command}" end + # Formats a read tool call. + # + # Input fields: + # :path (String) – file to read [required] + # :offset (Integer) – 1-indexed first line [optional] + # :limit (Integer) – maximum number of lines [optional] + # + # Output: "READ ", with a line range appended when :offset and/or + # :limit is given: + # :limit >= 1 → " (lines )", start = :offset (default 1), + # end = start + :limit - 1 + # :offset only → " (from line )" (reads to end of file); also the + # fallback when :limit <= 0, which has no sensible range + # With neither, the bare "READ ". A missing path renders "READ". + # + # Examples: + # READ lib/roast.rb (lines 30–80) + # READ lib/roast.rb (from line 30) + # READ lib/roast.rb + # + #: () -> String + def format_read + path, offset, limit = arguments.values_at(:path, :offset, :limit) + path = path.to_s + details = if limit&.positive? + offset ||= 1 + "lines #{offset}–#{offset + limit - 1}" + elsif offset + "from line #{offset}" + end + label = path.empty? ? "READ" : "READ #{path}" + details ? "#{label} (#{details})" : label + 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 55d6b5bc..3af2ac8d 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 @@ -76,6 +76,26 @@ def format_bash ok_line("#{count} #{"line".pluralize(count)}", preview) end + # Formats a read tool result. + # + # Content: the file's text. + # + # Output: "READ OK " – is the file's line count + # (pluralized). Blank lines are counted: this is the file's own length, so + # it deliberately differs from entry-listing tools (find/ls), which drop + # blanks because a blank isn't an entry. + # + # Examples: + # READ OK 42 lines + # READ OK 1 line + # READ OK 0 lines + # + #: () -> String + def format_read + count = content.to_s.lines.length + ok_line("#{count} #{"line".pluralize(count)}") + 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 f429c3aa..2cd7a4c5 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 @@ -26,6 +26,41 @@ class ToolCallMessageTest < ActiveSupport::TestCase assert_equal "BASH #{"x" * (ToolCallMessage::TRUNCATE_LIMIT - 3)}...", msg.format end + test "format renders READ with the path" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb" }) + assert_equal "READ lib/roast.rb", msg.format + end + + test "format renders a bare READ when the path is missing" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: {}) + assert_equal "READ", msg.format + end + + test "format renders READ with a closed line range when offset and limit are set" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb", offset: 30, limit: 51 }) + assert_equal "READ lib/roast.rb (lines 30–80)", msg.format + end + + test "format defaults the read offset to 1 when only limit is given" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb", limit: 50 }) + assert_equal "READ lib/roast.rb (lines 1–50)", msg.format + end + + test "format renders an open-ended read range from offset when limit is absent" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb", offset: 30 }) + assert_equal "READ lib/roast.rb (from line 30)", msg.format + end + + test "format falls back to the open-ended range when read limit is non-positive" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb", offset: 30, limit: 0 }) + assert_equal "READ lib/roast.rb (from line 30)", msg.format + end + + test "format omits the range entirely when read limit is non-positive and no offset is given" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "lib/roast.rb", limit: 0 }) + assert_equal "READ lib/roast.rb", 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 85ad549d..ec1715ce 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 @@ -43,6 +43,39 @@ def setup assert_equal "BASH OK 0 lines", msg.format(@context) end + test "format summarizes read output with a line count" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "read", + content: "line one\nline two\nline three", + is_error: false, + ) + + assert_equal "READ OK 3 lines", msg.format(@context) + end + + test "format pluralizes a single line of read output" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "read", + content: "just one line", + is_error: false, + ) + + assert_equal "READ OK 1 line", msg.format(@context) + end + + test "format reports zero lines when the file is empty" do + msg = ToolResultMessage.new( + tool_call_id: "1", + tool_name: "read", + content: nil, + is_error: false, + ) + + assert_equal "READ OK 0 lines", msg.format(@context) + end + test "format renders NAME ERROR with the message for an error result" do msg = ToolResultMessage.new( tool_call_id: "1",