feat(engine): read_file returns images as real Blob content - #130
Conversation
engine/filetools.go's read_file tool now sniffs a file's magic bytes (never its extension) and returns a Text+Blob ToolResult for a recognized image (png/jpeg/gif/webp), matching the shape MCP already produces. Every transcoder's existing Blob handling and imageclamp pass apply with no new wiring. A 20MB pre-clamp cap on the file size read_file will read in full stops it from loading an unbounded file into memory; an over-cap image returns a clear text error, no Blob. A transcode-time degrade of an image Blob to text for a model with no vision capability is deliberately not implemented: no per-model vision signal exists anywhere in the codebase to gate it on. Filed as #129 instead of forcing an ugly seam. AGENTS.md documents both the shipped behavior and the deferred gap.
📦 Previous Review (superseded by newer review)Claude finished @andybons's task in 5m 32s —— View job Code review: read_file returns images as real Blob contentNice, tightly-scoped change. The core design is right: sniff by magic bytes (not extension), reuse MCP's existing The findings below are all low-severity robustness/efficiency items, not blockers. Findings (inline)
Note (not a defect in this PR)
Everything above is optional hardening; the PR is sound as-is. |
Address the automated and Opus review of the read_file image feature: - Single open/read pass (readImageIfDetected replaces the old sniff-then-reread split): fixes a double-open and dropped bytes on a non-regular file. - io.ReadFull for the magic-byte sniff instead of one Read, so a short read(2) on a pipe or FUSE mount can no longer misclassify a real image as text. - The 20MB cap now binds on an io.LimitReader over the read itself, not a pre-read os.Stat size a concurrently growing file could outrun (TOCTOU). - image.DecodeConfig must succeed before committing to the image path, so a corrupt or truncated file that only matches a magic-byte prefix falls back to a text read instead of shipping an unusable Blob. - read_file's tool description now mentions image support, so a model has a reason to reach for it on a screenshot. - Fixed "image/png image" double-word phrasing in the summary and error text. Also, per AGENTS.md's "verification drives the production entry point" rule, added a test that drives read_file through Session.runToolCalls (the real per-turn dispatch path) rather than only Tool.Run directly, and a test proving a truncated PNG falls back to text. AGENTS.md's new section is corrected to state plainly that only the Anthropic route puts a tool-result image on the wire today; openai/openaicompat replace it with a text omission note.
📦 Previous Review (superseded by newer review)Claude finished @andybons's task in 7m 11s —— View job Code reviewI read AGENTS.md and reviewed the diff on Prior review findings — all addressed on current HEAD ✅
Finding (posted inline)
Minor / by-design (not posted inline)
One skill-suggested finding I discarded as a false positiveA generated finding claimed a >20MB corrupt image bypasses the cap by falling through to the unbounded TestingTests follow the repo's TDD/red-verify conventions; the golden transcode test proves the Blob lands as a real wire Overall this is a clean, well-scoped change with thorough docs. The one inline note is a low-severity wording nit, not a blocker. |
…ad guard test Address round-2 review of the read_file image feature: - read_file's tool Description no longer implies every provider ships the image on the wire; it now says "where the current provider supports tool-result images" (only provider/anthropic does today). - Extract sniffMediaType(io.Reader) out of readImageIfDetected so a test can drive it with iotest.OneByteReader. Added TestSniffMediaTypeSurvivesShortReads and red-verified it against the actual io.ReadFull mechanism (reverted to a plain Read, confirmed the test fails, restored). An earlier claim that this mechanism had been red-verified was made before the test existed; this closes that gap for real. - Fixed a duplicated "bytes" in AGENTS.md's read_file section.
|
Round-2 fixes pushed at a670a20 (on top of 02647ff), addressing the
Correcting the record on the 20MB cap ( Filed #133 as the one
|
📦 Previous Review (superseded by newer review)Claude finished @andybons's task in 7m 2s —— View job Code review: read_file returns images as real Blob contentSolid, tightly-scoped change. The core is right: sniff by magic bytes via Two prior-round fixes verified: the Findings (inline)
Notes (design considerations, not defects)
The description-wording concern from the earlier review round already looks addressed — the tool description now says "where the current provider supports tool-result images." Everything above is optional hardening; the PR is sound as-is. |
readPathContent replaces readImageIfDetected: it opens the target path exactly once and reads it exactly once regardless of outcome. Before this, a non-image read (the overwhelmingly common case) did an extra open+512-byte-read+close for the image sniff and then a second, separate os.ReadFile for the actual text content. The text outcome now continues reading from the same handle right after the sniff (io.ReadAll(f)), so no bytes are lost on a non-regular file (a FIFO, a streaming mount) either. The image path's DecodeConfig-failure fallback (a magic-byte match that turns out not to be a real image) also now reads the true remainder through the same handle instead of silently truncating the text fallback at the image cap. AGENTS.md's read_file section is updated for the rename and now notes the summary-vs-clamped-image mismatch: imageclamp.Clamp can downscale or re-encode an image later, at transcode time, so the dimensions read_file reported when it read the file can drift from what the model is eventually shown. This is accepted, not fixed here.
|
Claude finished @andybons's task in 9m 49s —— View job Code review: read_file returns images as real Blob contentI read AGENTS.md and reviewed the diff on I found one substantive issue, posted inline. Finding (inline)The decode-failure text fallback is dead code, and both its comment and AGENTS.md "guard 3" mis-describe it (
Low severity (needs a corrupt/false-positive image), but the code and its load-bearing AGENTS.md prose disagree with the actual behavior. Suggested fix in the inline thread. Minor / by-design (not posted inline)
Overall: sound as-is. The inline finding is a doc/behavior mismatch worth tightening, not a blocker.
|
Summary
read_file(engine/filetools.go) sniffs a file's magic bytes viahttp.DetectContentType(first 512 bytes) — never its extension — andreturns a
Text+BlobToolResultfor a recognized image(
image/png,image/jpeg,image/gif,image/webp), matching theshape MCP's
mcpContentToPartsalready produces. Every transcoder'sexisting Blob handling and the
imageclampdimension/byte-size passapply with no new wiring.
readFileMaxImageBytes(20MB) bounds the file sizeread_filereadsin full once an image is detected, so it never slurps an unbounded
file into memory before
imageclamp.Clampever runs at transcodetime. An over-cap image returns a plain text error, no Blob.
(unbounded) text-read behavior.
provider/anthropic/transcode_test.go)proves this exact shape lands as a real wire
imagecontent block,bytes intact, on the one route that recurses into tool-result Blobs
at all.
No per-model vision-capability signal exists anywhere in the
codebase today (no embedded models.dev catalog, no
provider.Requestcapability flag), so building it now would mean inventing an ad hoc,
likely-wrong static model list. Filed as Degrade image Blobs to text for non-vision models at transcode time #129 instead.
Test plan
go build ./...go vet ./...go test -race ./...the three affirmative image tests failed for the expected reason
(no Blob / no over-cap enforcement) while the negative
extension-lie test still passed, then restored.
assertion and confirming failure, then restored.
🤖 Generated with Claude Code