Skip to content

Let a builder that sees open a picture itself - #1183

Draft
youkaicountry wants to merge 46 commits into
storytold:mainfrom
youkaicountry:feature/r5.4-builder-opens-pictures
Draft

youkaicountry wants to merge 46 commits into
storytold:mainfrom
youkaicountry:feature/r5.4-builder-opens-pictures

Conversation

@youkaicountry

Copy link
Copy Markdown
Contributor

Stacked: on #1182 (shrinking large pictures) → #1179 (pictures to builders that see) → #1177 → #1161 → #1155. It reuses #1182's shrinking. Only the last two commits are this PR: the claim and Let a builder that sees open a picture itself. I'll rebase as those merge.

Why

With #1179 an API builder whose model sees is shown the pictures the run attaches: the original's with the first message, and each check's after it. It still couldn't look at anything else. build_file_read refused any binary file, so a screenshot it saved of its own build, or a seventh picture in reference/, was out of reach.

What changes

Engine (build_file_read).

  • A PNG, JPEG, GIF or WebP gets a typed answer instead of the binary-file refusal: {path, picture, media_type, bytes, note}. Here picture is the path as shown in the project.
  • Other binary files are still refused.
  • The picture sniffer moves from the ai crate to web_common::picture_media_type, and model::image_media_type calls it, so there's one copy.
  • The tool's description says how pictures are handled.

Agent loop.

  • LoopConfig::project says which folder the build tools' paths are relative to.
  • A tool result with a picture field (a typed field, not a tool name) marks the picture as opened.
  • After the step's results, a model that sees is shown each opened picture after a line naming it. Pictures are read with Shrink a picture too large to show a model #1182's pictures::load, so a big one is shrunk and named with its scale. A step shows at most six, and the rest are named so the model can open them next.
  • A model that doesn't see gets "[app/shot.png is a picture, but this model isn't shown pictures.]".
  • Agent apps are unchanged: they open pictures with their own Read tool.

Run. AiHub::start_builder takes the project folder (its only caller is the run). The sighted builder's note now says it can open other pictures with build_file_read.

Tests

  • Engine: build_file_read on a PNG (by a root-style path, /app/shot.png) answers with picture: "app/shot.png", media_type: "image/png" and no text, and other binary files are still refused.
  • Agent: a_picture_a_tool_opens_is_shown_after_its_result_to_a_model_that_sees. One step opens a picture, a missing picture and a non-picture result. A sighted model gets the three results, then Picture: app/shot.png and the image, then the missing one named as not shown. A blind model gets no image and the note. With the agent no longer noting opened pictures, it fails.
  • Totals: 557 engine, 528 ai, and every MCP, CLI and ui-egui test pass. clippy, fmt, layers, locks, secrets and i18n-coverage are clean, and cargo xtask tools leaves docs/tools.md as it was.

Not tested: the run giving the builder its project folder is one line that no test covers (run tests pass in their own conversation). It hasn't been run live yet.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@youkaicountry

Copy link
Copy Markdown
Contributor Author

Rebased onto main (2e7f276) with the stack under it. #1258 gave start_builder and start_loop the run's mode (deep); project now comes after it, and main's new tests that start a builder pass None for it.

@youkaicountry
youkaicountry force-pushed the feature/r5.4-builder-opens-pictures branch 2 times, most recently from 7afee70 to 674f00a Compare November 10, 2026 05:58
@youkaicountry
youkaicountry force-pushed the feature/r5.4-builder-opens-pictures branch 3 times, most recently from 54cb26f to 7a4a89a Compare November 10, 2026 19:01
youkaicountry and others added 17 commits November 10, 2026 19:02
Add the plan for local models that can do more than the lean defaults
assume: the context window read from the server, an Auto/Lean/Full tool
list per AI, honest prompts for builders that can't see pictures, and
pictures for models that can. The first line is claimed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A model on this computer or its network was budgeted at 24k tokens
whatever it could hold, and the real window was only learned from a
refusal, which never came once we trimmed first. Before a backend's first
request, and once more after it while the server hasn't said, the agent
now asks the server for the window it has loaded: the model list
(context_length, max_model_len, meta.n_ctx), llama-swap's upstream route
for a loaded model, llama.cpp's /props, LM Studio's loaded_context_length
and Ollama's /api/ps. Three quarters of it is a local model's budget; it
only lowers a paid service's. Each server is a WindowProbe, so a fork can
add one.

context_tokens gets a field in Settings -> AI (Auto by default) and
`ai context <name> <tokens|auto>` in the CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings

The gate skipped every service off this network but OpenRouter, while
its comment and the docs said they were asked through their model list.
OpenAI-compatible services now are (official OpenAI and Gemini, whose
lists say no window, still aren't), and Groq's `context_window` field is
read. Adds tests for the hub's set_context_tokens, `ai context` and the
Settings field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The llama-swap upstream route and Ollama 0.40.15 were asked live; note
Ollama's 4096-token default under the line about setting a server's size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget was a bare text box that wrapped away from its label and
didn't say what Auto meant. It is now a dropdown on its own line (Auto,
8k to 1M, Custom… for any number, so nothing is capped), and under it a
note worked out with the agent loop's own rule from the window the
server names: "Auto: 393,216 tokens of the server's 524,288", or
"196,608 will be used: the server holds 262,144". `ai context` shows
the same window and budget. Strings are in all 13 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault

The 150k cap for a paid service's Auto budget was applied before the person's own budget was compared with the window, so a 390k budget became 150k. Also escape a pipe that split a STATUS table row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
llama-swap starts a model for a /props?model= request too, so the llama.cpp probe swapped out the loaded model whenever the list gave no window for an unloaded one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Estonian (storytold#1205) merged after this branch was made, and every UI string needs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ollama and LM Studio name their window only once the model is loaded, and Settings kept the first answer for the whole session. Opening Settings now forgets answers without a window. The roadmap gets the ssh case. Both found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paced (from storytold#1469) wraps a backend with requests_per_minute, and it
didn't pass context_window on, so a paced backend's Auto budget never
saw the window its server names. It forwards it now, as Tunneled does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every builder on a local model got the lean list (storytold#417) and a system
prompt without the memory steps, whatever its window, so a model with a
262k or 512k window built with the same cut-down tools as a 4k one.

Each API backend now has a tool list: `tools` in [[ai.backends]],
Settings → AI "Tools for building", and `ai tools <name>`. Lean is the
building list, Full is every tool and the memory steps. Auto, the
default, is Full on a paid service, and on a local model Full when the
conversation's budget is FULL_TOOLS_BUDGET (49,152: a 64k window less
its quarter) or more, else Lean. With no window known a local model
stays lean, as before.

The loop now asks the server for its window before choosing, and
chooses again before each turn, rewriting the system prompt when the
list changes (Ollama names its window only once the model is loaded).
The choice is journaled as `tools`. Chats keep every tool.

Stacked on storytold#1155, which reads the server's window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the large-model half of the live-run item was run; the lean run on a small model gets its own open line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
youkaicountry and others added 29 commits November 10, 2026 19:02
Settings showed "(the lean list until its server says it holds more)" for a
local AI on Auto tools with no window yet, even when its saved context budget
was below the full list's: that budget stays lean whatever the server says.
`ServerWindow::lean_until_told` asks whether a server naming a large window
would change the choice, and the note follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vision = auto | yes | no on each API backend, and ai vision to show and set it. Auto asks the server: a model list's input modalities, llama-swap's upstream /props for a loaded model, llama.cpp's /props modalities, LM Studio's model type and Ollama's capabilities; a Claude model sees. Nothing said is treated as blind.

Carries storytold#1155's guard (asking llama-swap about an unloaded model only through its list), which the sight probe needs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sees_pictures read the model id as written, so anthropic/claude-… and anthropic.claude-… (gateway ids) said nothing and were treated as blind. It now strips them the way effort_for does, through one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paced didn't pass sees_pictures on, so under Vision Auto a backend with
requests_per_minute was taken as blind, even Claude. It forwards it now,
as Tunneled does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Block::Image: PNG, JPEG, GIF or WebP read from the bytes, up to 3.75 MB. OpenAI-style servers get image_url data URIs in the message's content parts, Anthropic an image block with a base64 source; messages without a picture are sent as before. The bytes stay in memory only: transcripts keep the picture's name and a resumed conversation sends a note instead. Pictures count as 1,600 tokens and old ones are trimmed to their note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
image_file reads one byte past the limit, so the size it read isn't the file's, and dividing it as an integer printed "3 MB, more than the 3.75 MB". Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ticked line now names the "Sees pictures" setting (vision: Auto, Yes or No) and drops the "what's left" sentence; the open line that asked for the setting and the attaching, both now done, goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A website check lists the start page at every width first; with each picture beside the original's, those filled the six a message carries, so a builder that sees was never shown the site's other pages, however unlike they were, and the first message never read capture/routes/. Pictures are now taken by page in turn (least alike first after a check), an original and the build's picture go together or not at all, and what didn't fit is counted in the feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Try again" after a check that couldn't run checks first, so its first message is the fix after that check. The original's first-message pictures were queued as well and went with it: the original's screenshot twice, and the build's pictures could be cut off at six. They now go only when a builder turn comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… starts

Settings and `ai vision` don't open an SSH tunnel just to ask a server
something, so for an AI on Auto reached over SSH they said "its server
doesn't say: not shown pictures". A build does ask through its own
tunnel, and its model may then be shown pictures, so that note could be
wrong. Settings now says it's asked when a build starts, and `ai vision`
gives `sees` as null with `over_ssh`; a set answer is shown as before.
BackendConfig::over_ssh is the one place that says a backend is reached
over SSH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rts ones left out, and that Auto is blind on OpenAI's and Gemini's own APIs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…starts (R5.4)

Settings doesn't ask a backend over SSH for its window (a tunnel would
be opened just for that), so the Auto budget's note said the server
doesn't say how much it holds, and the tool list's said the lean list
until it does. Both now say it's asked when a build starts, as the
Sees pictures line already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since storytold#1520 a run studies again when its capture is from the other
mode. The test checks in Clean room on a fixture marked Deep, so it
studied and the scripted model ran out of turns. The capture is now
Clean room's, as the run is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anthropic refuses a picture longer than 8000 px on either side, and a
full-page screenshot can be that tall while its PNG is well under 3.75 MB,
so fit passed it unchanged and the builder's request could be refused.
fit now reads the picture's size from its header (picture_dimensions, any
of the formats we take) and scales one over 8000 px down, keeping its
shape, as it does one over the byte limit. One that fits both is left
exactly as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@youkaicountry
youkaicountry force-pushed the feature/r5.4-builder-opens-pictures branch from 7a4a89a to 86b8e94 Compare November 11, 2026 00:09

This branch has not been deployed

No deployments
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