Class: Ask::Tools::Read
- Inherits:
-
Ask::Tool
- Object
- Ask::Tool
- Ask::Tools::Read
- Defined in:
- lib/ask/tools/shell/read.rb
Overview
Read file contents with line numbers, or list directory contents.
Engineered for token budgets. Three ceilings stop the three shapes of hostile file — the long file (line window), the wide file (byte budget), the minified bundle (per-line clamp):
max_lines = 2000 lines — the window
byte_budget = 128 KB — chars of output returned
max_line_chars = 2000 — per-line clamp
Truncation is a fact, not an error: reads that stop short return ok with a precomputed resume offset, so the model never does pagination arithmetic and never treats a fact about the world as a failure.
The other decisions that make a read cheap instead of expensive:
- strict offset/limit repair (never silently mangle "2abc" into 2)
- device blocklist — /dev/zero would hang a read forever
- filename repair: NFD/NFC, narrow NBSP, curly quotes, did-you-mean
- a self-expiring dedup stub for unchanged re-reads (consumed on use,
complete reads only, kill-switchable)
- a partial-view ledger that Write consults before overwriting
Constant Summary collapse
- DEFAULT_MAX_LINES =
2000- DEFAULT_BYTE_BUDGET =
128_000- DEFAULT_MAX_LINE_CHARS =
2000- DEVICE_PATHS =
Device files that never end or block forever — refused by name before any I/O, so a read can never hang on them.
%w[ /dev/zero /dev/random /dev/urandom /dev/stdin /dev/stdout /dev/stderr ].freeze
- MIME_TYPES =
{ ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".webp" => "image/webp", ".svg" => "image/svg+xml", ".pdf" => "application/pdf", ".zip" => "application/zip", ".gz" => "application/gzip", ".mp3" => "audio/mpeg", ".mp4" => "video/mp4" }.freeze
Instance Attribute Summary collapse
-
#byte_budget ⇒ Object
Returns the value of attribute byte_budget.
-
#dedup_enabled ⇒ Object
readonly
Returns the value of attribute dedup_enabled.
-
#max_line_chars ⇒ Object
Returns the value of attribute max_line_chars.
-
#max_lines ⇒ Object
Returns the value of attribute max_lines.
Instance Method Summary collapse
- #execute(path:, offset: nil, limit: nil) ⇒ Object
-
#initialize ⇒ Read
constructor
A new instance of Read.
Constructor Details
#initialize ⇒ Read
Returns a new instance of Read.
58 59 60 61 62 63 64 65 |
# File 'lib/ask/tools/shell/read.rb', line 58 def initialize super @max_lines = DEFAULT_MAX_LINES @byte_budget = (ENV["ASK_TOOLS_SHELL_READ_BYTE_BUDGET"] || DEFAULT_BYTE_BUDGET).to_i @max_line_chars = DEFAULT_MAX_LINE_CHARS @dedup_enabled = ENV["ASK_TOOLS_SHELL_READ_NO_CACHE"] != "1" @dedup = {} end |
Instance Attribute Details
#byte_budget ⇒ Object
Returns the value of attribute byte_budget.
55 56 57 |
# File 'lib/ask/tools/shell/read.rb', line 55 def byte_budget @byte_budget end |
#dedup_enabled ⇒ Object (readonly)
Returns the value of attribute dedup_enabled.
55 56 57 |
# File 'lib/ask/tools/shell/read.rb', line 55 def dedup_enabled @dedup_enabled end |
#max_line_chars ⇒ Object
Returns the value of attribute max_line_chars.
55 56 57 |
# File 'lib/ask/tools/shell/read.rb', line 55 def max_line_chars @max_line_chars end |
#max_lines ⇒ Object
Returns the value of attribute max_lines.
55 56 57 |
# File 'lib/ask/tools/shell/read.rb', line 55 def max_lines @max_lines end |
Instance Method Details
#execute(path:, offset: nil, limit: nil) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/ask/tools/shell/read.rb', line 67 def execute(path:, offset: nil, limit: nil) path = File.(path) if device_path?(path) return Ask::Result.error(message: "Refusing to read device file: #{path} (can block forever).") end unless File.exist?(path) return Ask::Result.error(message: (path)) end if File.directory?(path) return directory_listing(path) end unless File.file?(path) return Ask::Result.error(message: "Not a file: #{path}") end offset = coerce_int("offset", offset) return Ask::Result.error(message: offset) if offset.is_a?(String) limit = coerce_int("limit", limit) return Ask::Result.error(message: limit) if limit.is_a?(String) offset ||= 0 limit ||= @max_lines return Ask::Result.error(message: "Invalid offset: #{offset} (must be >= 0).") if offset.negative? return Ask::Result.error(message: "Invalid limit: #{limit} (must be >= 1).") if limit < 1 special = sniff(path) return special if special read = read_lines(path, offset, limit) partial_view = read[:more] || read[:clamped].positive? if @dedup_enabled && !partial_view && read[:lines].any? key = [path, File.mtime(path).to_f, File.size(path), offset, limit] if @dedup.key?(key) @dedup.delete(key) # self-expiring: one stub, then real content again return Ask::Result.ok( data: "File unchanged since last read — content is already in context.", metadata: { dedup: true } ) end @dedup[key] = true end Shell::FileLedger.record(path, partial: partial_view, lines_seen: [offset, offset + read[:lines].size]) data, resume_offset = format_output(path, offset, read) = { total_lines: read[:more] || (read[:lines].empty? && read[:saw_any]) ? nil : offset + read[:lines].size, start_line: read[:lines].empty? ? nil : offset + 1, end_line: offset + read[:lines].size, truncated: read[:more], partial_view: partial_view, clamped_lines: read[:clamped], resume_offset: resume_offset } .delete(:resume_offset) unless read[:more] Ask::Result.ok(data: data, metadata: ) end |