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 d0bb62ce..0e0aaad6 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 @@ -61,6 +61,39 @@ 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 " (:path truncated to TRUNCATE_LIMIT chars), with a + # line range appended when :offset and/or :limit is given: + # :limit set → " (lines )", start = :offset (default 1), + # end = start + :limit - 1 + # :offset only → " (from line )" (reads to end of file) + # 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 = truncate(path) + details = if limit + 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 05377281..adac0e26 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 @@ -74,6 +74,23 @@ 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 line count (pluralized). + # + # 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 dd56ac3e..584fe0d0 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,36 @@ 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 truncates a long read path" do + msg = ToolCallMessage.new(id: "1", name: "read", arguments: { path: "x" * 100 }) + assert_equal "READ #{"x" * (ToolCallMessage::TRUNCATE_LIMIT - 3)}...", 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 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 9635fc1e..24c92d0b 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",