module Truncation
Universal tool output truncation.
Every tool result passes through Truncation.truncate() before entering the LLM context. This is the primary guard against context window explosion — even if a tool has no internal limits, this module caps the output to a safe size.
Existing features (ref: opencode truncate.ts):
- Line + byte dual cap — truncate when output exceeds MAX_LINES (2000) or MAX_BYTES (50 KB), whichever is hit first.
- Head mode (default) — keep the first N lines / bytes. Used for most tool output where the beginning is most relevant.
- Tail mode — keep the last N lines / bytes. Used for shell output where errors and summaries appear at the end.
- Overflow to disk — when truncating, write the full text to a file under TRUNCATION_DIR (e.g. ~/.local/share/brute/tool-output/). Return a preview + hint pointing to the saved file.
- Hint message — when truncated, append a contextual hint: "Full output saved to: (path). Use Read with offset/limit to view specific sections."
- Configurable limits — allow overriding MAX_LINES / MAX_BYTES via per-call options.
- Retention cleanup — purge saved output files older than a configurable retention period from a truncation directory.
- Per-line truncation — truncate individual lines longer than MAX_LINE_LENGTH (2000 chars) with a suffix.
Definitions
def self.truncate(text, max_lines: MAX_LINES, max_bytes: MAX_BYTES, direction: :head, truncation_dir: nil)
Truncate text to fit within line and byte limits.
Returns the text unchanged if it fits. Otherwise returns a truncated preview with a hint message.
Signature
-
parameter
textString the tool output to truncate
-
parameter
max_linesInteger maximum number of lines to keep
-
parameter
max_bytesInteger maximum byte size to keep
-
parameter
directionSymbol which end to keep
-
parameter
truncation_dirString, nil directory to save full output when truncating
-
returns
String the (possibly truncated) text
Implementation
def self.truncate(text, max_lines: MAX_LINES, max_bytes: MAX_BYTES, direction: :head, truncation_dir: nil)
return text if text.nil? || text.empty?
# Per-line truncation first — cap individual lines
lines = text.lines.map { |line| truncate_line(line) }
text = lines.join
return text if lines.size <= max_lines && text.bytesize <= max_bytes
# Determine how many lines we can keep within both caps
kept = direction == :tail ? lines.last(max_lines) : lines.first(max_lines)
# Enforce byte cap
result_lines = []
bytes = 0
kept.each do |line|
break if bytes + line.bytesize > max_bytes
result_lines << line
bytes += line.bytesize
end
result = result_lines.join
total = lines.size
shown = result_lines.size
# Overflow to disk — save the full output so it can be inspected later
saved_path = save_to_disk(text, truncation_dir)
hint = "\n#{TRUNCATION_MARKER} showing #{shown} of #{total} lines]"
if saved_path
hint += "\nFull output saved to: #{saved_path}. Use Read with offset/limit to view specific sections."
end
result + hint
end
def self.already_truncated?(text)
Check whether text already contains a truncation marker.
Implementation
def self.already_truncated?(text)
text.include?(TRUNCATION_MARKER)
end
def self.truncate_line(line, max: MAX_LINE_LENGTH)
Truncate a single line if it exceeds MAX_LINE_LENGTH.
Implementation
def self.truncate_line(line, max: MAX_LINE_LENGTH)
return line if line.length <= max
line[0, max] + "... (line truncated to #{max} chars)\n"
end
def self.cleanup!(dir, retention_days: 7)
Purge files older than retention_days from the given directory.
Implementation
def self.cleanup!(dir, retention_days: 7)
return unless File.directory?(dir)
cutoff = Time.now - (retention_days * 86400)
Dir.glob(File.join(dir, "*")).each do |path|
File.delete(path) if File.file?(path) && File.mtime(path) < cutoff
end
end
def self.save_to_disk(text, truncation_dir)
Save text to a file in truncation_dir. Returns the file path, or nil.
Implementation
def self.save_to_disk(text, truncation_dir)
dir = truncation_dir || TRUNCATION_DIR
return nil unless dir
FileUtils.mkdir_p(dir)
path = File.join(dir, "tool_#{SecureRandom.hex(8)}.txt")
File.write(path, text)
path
rescue => _e
nil
end