Skip to content

Add read_image tool PoC (Mistral backend)#900

Open
pavelpenev wants to merge 1 commit into
mistralai:mainfrom
pavelpenev:read-image-poc
Open

Add read_image tool PoC (Mistral backend)#900
pavelpenev wants to merge 1 commit into
mistralai:mainfrom
pavelpenev:read-image-poc

Conversation

@pavelpenev

Copy link
Copy Markdown

Give the agent a tool to read an image file and have the vision model "see" it in the next LLM turn. Currently the agent can only receive images via user attachment (@-mention, clipboard paste) — it has no tool to read an image itself.

How it works

Agent calls read_image("/path/to/img.png")
-> Tool reads file, snapshots to session attachments
-> get_result_images() reconstructs ImageAttachment from snapshot path
-> Pipeline threads ImageAttachment onto LLMMessage(role=Role.tool, images=[...])
-> Mistral backend renders images in the tool message API call
-> Vision model sees image + text in the next turn

The image persists in context across subsequent turns (not consumed) — the ReAct loop includes all message history, including tool results with images. _messages_for_backend already strips images for non-vision models, so no special handling is needed there.

Scope: Mistral backend only (PoC)

This is a proof-of-concept targeting the Mistral backend, which is a self-contained class that does not share code with the other backends' adapters. The pipeline changes (base.py, read_image.py, _loop.py, hooks.py, format.py) are backend-agnostic; other backends silently drop images on tool messages (no errors, the image is just ignored). This is offered as a base the maintainers can adopt or adapt.

Confirmed: the Mistral API accepts images in tool messages — a direct test with mistral-medium-3.5 sent a ToolMessage with [TextChunk, ImageURLChunk] and the model accurately described the image with no API errors.

Follow-up work (deferred, not in this PoC) would extend:

  • Anthropic (docs confirm tool-result images are supported)
  • Generic (OpenAI-compatible, untested)
  • OpenAI Responses (untested)
  • ReasoningAdapter (untested)

Provenance

This implementation was produced by evaluating two AI-generated drafts against a shared plan, then merging the strongest aspects of each: the snapshot-path data flow (which keeps the LLM message pointing at the stable persisted snapshot, so session resume and file relocation stay safe) combined with scratchpad display-suffix handling that matches the existing read_file tool convention.

Files changed (9)

  • vibe/core/tools/base.py — add get_result_images hook on BaseTool (non-abstract, default None; zero existing subclasses break)
  • vibe/core/tools/builtins/read_image.py — new tool
  • vibe/core/tools/builtins/prompts/read_image.md — tool prompt
  • vibe/core/agent_loop/_loop.py — extract images in _invoke_tool, accept in _handle_tool_response
  • vibe/core/agent_loop_hooks.py — thread images through _run_after_tool_and_finalize
  • vibe/core/llm/format.py — accept images in create_tool_response_message
  • vibe/core/llm/backend/mistral.py — render images on Role.tool messages (same pattern as the existing Role.user handler)
  • tests/core/tools/builtins/test_read_image.py — unit tests
  • tests/backend/test_image_mapping.py — backend mapping tests

Known limitations (PoC)

  • ToolResultEvent has no image field — the UI can't display a thumbnail of what read_image read. The model still sees the image.
  • Other backends silently drop images on tool messages.
  • image_path leaks into the result text dump — harmless but noisy.
  • No width/height in result — Pillow is not a project dependency.

Testing

  • uv run pytest tests/core/tools/builtins/test_read_image.py tests/backend/test_image_mapping.py (30 passed)
  • uv run ruff check --fix . && uv run ruff format .
  • uv run pyright (0 errors)
  • uv run pytest (full suite: no new failures; 2 pre-existing unrelated failures in tests/tools/test_grep.py and an e2e TUI test)

Give the agent a tool to read an image file and have the vision model
"see" it in the next LLM turn. Currently the agent can only receive
images via user attachment (@-mention, clipboard paste) — it has no
tool to read an image itself.

## How it works

Agent calls read_image("/path/to/img.png")
  -> Tool reads file, snapshots to session attachments
  -> get_result_images() reconstructs ImageAttachment from snapshot path
  -> Pipeline threads ImageAttachment onto LLMMessage(role=Role.tool, images=[...])
  -> Mistral backend renders images in the tool message API call
  -> Vision model sees image + text in the next turn

The image persists in context across subsequent turns (not consumed) —
the ReAct loop includes all message history, including tool results
with images. _messages_for_backend already strips images for
non-vision models, so no special handling is needed there.

## Scope: Mistral backend only (PoC)

This is a proof-of-concept targeting the Mistral backend, which is a
self-contained class that does not share code with the other backends'
adapters. The pipeline changes (base.py, read_image.py, _loop.py,
hooks.py, format.py) are backend-agnostic; other backends silently
drop images on tool messages (no errors, the image is just ignored).
This is offered as a base the maintainers can adopt or adapt.

Confirmed: the Mistral API accepts images in tool messages — a direct
test with mistral-medium-3.5 sent a ToolMessage with
[TextChunk, ImageURLChunk] and the model accurately described the
image with no API errors.

Follow-up work (deferred, not in this PoC) would extend:
- Anthropic (docs confirm tool-result images are supported)
- Generic (OpenAI-compatible, untested)
- OpenAI Responses (untested)
- ReasoningAdapter (untested)

## Provenance

This implementation was produced by evaluating two AI-generated
drafts against a shared plan, then merging the strongest aspects of
each: the snapshot-path data flow (which keeps the LLM message
pointing at the stable persisted snapshot, so session resume and
file relocation stay safe) combined with scratchpad display-suffix
handling that matches the existing read_file tool convention.

## Files changed (9)

- vibe/core/tools/base.py — add get_result_images hook on BaseTool
  (non-abstract, default None; zero existing subclasses break)
- vibe/core/tools/builtins/read_image.py — new tool
- vibe/core/tools/builtins/prompts/read_image.md — tool prompt
- vibe/core/agent_loop/_loop.py — extract images in _invoke_tool,
  accept in _handle_tool_response
- vibe/core/agent_loop_hooks.py — thread images through
  _run_after_tool_and_finalize
- vibe/core/llm/format.py — accept images in create_tool_response_message
- vibe/core/llm/backend/mistral.py — render images on Role.tool messages
  (same pattern as the existing Role.user handler)
- tests/core/tools/builtins/test_read_image.py — unit tests
- tests/backend/test_image_mapping.py — backend mapping tests

## Known limitations (PoC)

- ToolResultEvent has no image field — the UI can't display a
  thumbnail of what read_image read. The model still sees the image.
- Other backends silently drop images on tool messages.
- image_path leaks into the result text dump — harmless but noisy.
- No width/height in result — Pillow is not a project dependency.

## Testing

- uv run pytest tests/core/tools/builtins/test_read_image.py
  tests/backend/test_image_mapping.py  (30 passed)
- uv run ruff check --fix . && uv run ruff format .
- uv run pyright  (0 errors)
- uv run pytest  (full suite: no new failures; 2 pre-existing
  unrelated failures in tests/tools/test_grep.py and an e2e TUI test)
@pavelpenev
pavelpenev requested a review from a team as a code owner July 12, 2026 11:56
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