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 effacf9c..3491c081 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 @@ -32,24 +32,80 @@ def initialize(id:, name:, arguments:) def format return unless name - case name.to_s.downcase - when "bash" - "BASH #{arguments[:command]}" - when "read" - "READ #{arguments[:path]}" - when "edit" - "EDIT #{arguments[:path]}" - when "write" - "WRITE #{arguments[:path]}" - when "grep" - "GREP #{arguments[:pattern]} #{arguments[:path]}" - when "find" - "FIND #{arguments[:pattern]} #{arguments[:path]}" - when "ls" - "LS #{arguments[:path]}" - else - "TOOL [#{name}] #{arguments.inspect}" - end + format_method_name = "format_#{name.to_s.downcase}".to_sym + return send(format_method_name) if respond_to?(format_method_name, true) + + format_unknown + end + + TRUNCATE_LIMIT = 45 + + private + + #: () -> String + def format_bash + command = truncate(arguments[:command]) + command.empty? ? "BASH" : "BASH #{command}" + end + + #: () -> String + def format_read + path = arguments[:path] + path.to_s.empty? ? "READ" : "READ #{path}" + end + + #: () -> String + def format_write + path, content = arguments.values_at(:path, :content) + lines = content.to_s.lines + preview = truncate(lines.first.to_s.strip) + count = lines.length + line_label = count == 1 ? "line" : "lines" + "WRITE #{path} \"#{preview}\" (+#{count} #{line_label})" + end + + #: () -> String + def format_edit + path = arguments[:path] + edits = arguments[:edits] || [] + count = edits.length + edit_label = count == 1 ? "edit" : "edits" + "EDIT #{path} (#{count} #{edit_label})" + end + + #: () -> String + def format_grep + pattern, path, glob = arguments.values_at(:pattern, :path, :glob) + base = "GREP \"#{truncate(pattern)}\"" + base = "#{base} #{path}" if path.present? + glob.present? ? "#{base} (glob=#{glob})" : base + end + + #: () -> String + def format_find + pattern, path = arguments.values_at(:pattern, :path) + path.present? ? "FIND #{pattern} (in #{path})" : "FIND #{pattern}" + end + + #: () -> String + def format_ls + path = arguments[:path] + path.to_s.empty? ? "LS" : "LS #{path}" + end + + #: () -> String + def format_unknown + label = name.to_s.upcase + return label if arguments.empty? + + details = arguments.map { |key, value| "#{key}: #{truncate(value.inspect)}" }.join(", ") + "#{label} #{details}" + end + + #: (String?) -> String + def truncate(str) + s = str.to_s + s.length > TRUNCATE_LIMIT ? "#{s[0...TRUNCATE_LIMIT - 3]}..." : s end end end 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 ff2f59f1..0f74095d 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 @@ -30,23 +30,94 @@ def initialize(tool_call_id:, tool_name:, content:, is_error:) @tool_name = tool_name @content = content @is_error = is_error + @name = (tool_name || "unknown").to_s #: String + @input = {} #: Hash[Symbol, untyped] end #: (PiInvocation::Context) -> String? def format(context) - tool_call = context.tool_call(tool_call_id) - name = tool_name || tool_call&.name || "unknown" - status = is_error ? "ERROR" : "OK" - - # Truncate long tool results for progress display - c = content - display_content = if c && c.length > 200 - "#{c[0..197]}..." - else - c - end - - "#{name.upcase} #{status}#{display_content ? " #{display_content}" : ""}" + call = context.tool_call(tool_call_id) + @name = (tool_name || call&.name || "unknown").to_s + @input = call&.arguments || {} + + return error_line if is_error + + format_method_name = "format_#{@name.downcase}".to_sym + return send(format_method_name) if respond_to?(format_method_name, true) + + format_unknown + end + + TRUNCATE_LIMIT = 45 + + private + + #: () -> String + def format_bash + lines = content.to_s.lines + count = lines.length + ok_line("#{count} #{"line".pluralize(count)}") + end + + #: () -> String + def format_read + count = content.to_s.lines.length + ok_line("#{count} #{"line".pluralize(count)}") + end + + #: () -> String + def format_write + ok_line(@input[:path]) + end + + #: () -> String + def format_edit + ok_line(@input[:path]) + end + + #: () -> 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 + + #: () -> String + def format_find + count = content.to_s.lines.map(&:strip).reject(&:empty?).length + ok_line("#{count} #{"path".pluralize(count)}") + end + + #: () -> String + def format_ls + count = content.to_s.lines.map(&:strip).reject(&:empty?).length + ok_line("#{count} #{"entry".pluralize(count)}") + end + + #: () -> String + def format_unknown + preview = truncate(content.to_s.lines.first.to_s.strip) + ok_line(preview) + end + + #: (*String?) -> String + def ok_line(*parts) + summary = parts.select(&:present?).join(" ยท ") + prefix = "#{@name.upcase} OK" + summary.present? ? "#{prefix} #{summary}" : prefix + end + + #: () -> String + def error_line + "#{@name.upcase} ERROR #{content.to_s.strip}".strip + end + + #: (String?) -> String + def truncate(str) + s = str.to_s + s.length > TRUNCATE_LIMIT ? "#{s[0...TRUNCATE_LIMIT - 3]}..." : s end end end