Skip to content

Add file_view tool: image, SVG, and PDF support for slots - #114

Open
mlund01 wants to merge 3 commits into
feature/file-inputsfrom
feature/file-view-media
Open

Add file_view tool: image, SVG, and PDF support for slots#114
mlund01 wants to merge 3 commits into
feature/file-inputsfrom
feature/file-view-media

Conversation

@mlund01

@mlund01 mlund01 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Stacked on #112 (file inputs). Base is feature/file-inputs so the diff is scoped to just the media work — retarget to main once #112 merges.

Summary

File inputs — and any slot (memory, scratchpad, packet, input) — can now surface images, SVGs, and PDFs to the model as vision/document content. Previously slots were text-only: file_read rejected binary, so an uploaded image or PDF materialized on disk but the agent couldn't perceive it.

A new file_view tool returns the file as a vision/document block:

  • Images (PNG/JPEG/GIF/WebP) → image block, passthrough
  • SVG → rasterized to PNG (oksvg/rasterx, pure-Go) → image block
  • PDF → native document block (new ContentTypeDocument)
  • Non-viewable files → text pointer to file_read

file_read now rejects binary on all slots (not just text-only ones) and points the agent to file_view.

Design

Tools return a plain string (and large strings get interceptor-truncated), so media can't ride the return value. A typed MediaTool channel (aitools/media.go, mirroring OutputSchemaTool) lets a tool hand back (text, []MediaBlock) out-of-band. The orchestrator and commander dispatch it, keep the small text summary as the tool_result, and route the media into the conversation via Session.AddToolResultMedia.

Attachment: media is merged into the same user turn as the tool results ([tool_result, image, document]). This satisfies Anthropic's strict user/assistant alternation (image/document blocks are valid after tool_result blocks in one user turn) while OpenAI and Gemini render it as a standard multimodal user message — so there's no fragile cross-message fold-back, association is positional, and each part persists as its own row.

New llm.ContentTypeDocument + DocumentBlock is rendered by Anthropic (NewDocumentBlock), OpenAI (input_file), and Gemini (inline_data). On providers without document support (Ollama) the agent still sees the text summary.

Persistence

document_data / document_media_type / document_filename columns added via migration 0004, threaded through SQLite + Postgres, so document blocks survive resume.

Dependencies

github.com/srwiley/oksvg + github.com/srwiley/rasterx (pure-Go, no cgo) for SVG rasterization. Note: oksvg renders an SVG subset — CSS/filters/gradients/embedded fonts may render blank; the tool falls back to a text pointer on parse failure.

Tests

go build, go vet, full go test ./... pass (19 packages). New coverage: SVG→PNG raster, file_view per type + magic-byte-beats-extension + text fallback, file_read binary pointer, orchestrator MediaTool dispatch, AddToolResultMedia merge + document Clone, migration 0004 checksums.

Verified live via MCP run_mission with base64 envelopes:

  • OpenAI agent: described an SVG (red square + blue circle) and read text from a PDF
  • Anthropic agent: described the same rasterized SVG (exercises the merged-turn path)

File inputs (and any slot) can now surface images, SVGs, and PDFs to the
model as vision/document content instead of being text-only.

- New file_view tool: returns images (PNG/JPEG/GIF/WebP) and PDFs as native
  vision/document blocks, and rasterizes SVG to PNG via oksvg/rasterx. Works
  on any slot (memory, scratchpad, packet, input).
- file_read now rejects binary content on all slots and points to file_view.
- Typed MediaTool channel in aitools: tools return media out-of-band so it
  bypasses the result interceptor. The orchestrator and commander route it
  into the conversation via Session.AddToolResultMedia, merged into the
  tool-results user turn (valid for Anthropic's strict alternation; rendered
  as a multimodal user message for OpenAI and Gemini).
- New llm ContentTypeDocument + DocumentBlock, rendered by Anthropic (document
  block inside the tool_result), OpenAI (input_file), and Gemini (inline_data).
- Persistence: document_* columns via migration 0004 so document blocks
  survive resume.
mlund01 added 2 commits June 23, 2026 22:42
macOS screenshot filenames embed a U+202F narrow no-break space, and models
routinely echo such names back with the whitespace normalized to a plain ASCII
space — so file_view/file_read would stat-miss the on-disk file ("no such file
or directory") even though file_list showed it.

resolveExistingSlotFile now falls back to a whitespace-insensitive match
against the slot directory when the exact path is missing, folding every Unicode
space rune to ASCII space for comparison. file_view and file_read use it.
Replace the read-side directory scan with a fix at the source: fold every
Unicode whitespace rune in a staged file's name to a plain ASCII space when it
is written to disk (both base64 uploads and copied-in paths). macOS embeds a
U+202F narrow no-break space in screenshot names; models echo such names back
from file_list with the whitespace normalized, so staging under the folded name
keeps the listing and the path the model later passes to file_read/file_view
byte-for-byte identical — no fuzzy matching needed.

Reverts resolveExistingSlotFile; file_view and file_read resolve exact paths
again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant