feat(host-core): let Read return an image the model can be shown - #765
zhangqingkun976 wants to merge 1 commit into
Conversation
214e7c0 to
7b5ffe4
Compare
|
Rebased onto The only conflicts were append-vs-append — the E2E scenario catalog and the decisions log, where main and this branch each added an entry — and both sides are kept. The identifier bands are still free: upstream's newest ADR is 0299 and its newest decisions-log entry is D607 (the plugin crash report, which is this repository's own fix for issue #747 built on my #756). |
7b5ffe4 to
d71d97e
Compare
|
Rebased onto Two things changed in the description, both about evidence:
Local gates on the rebased head: |
A session asked to look at an image on disk could only be told the file was binary content and had no text to read (issue vastsa#711), while the same app happily sent an image the user attached. The runtime already turned a host tool result shaped `{text, images: [{data, mimeType}]}` into a model-visible image block; no tool produced that shape. - `Read` now returns it for `.png`, `.jpg`, `.jpeg`, `.gif` and `.webp`. The block is attached only when the active model accepts images, and the text is kept either way, so a model without vision reports that it cannot see the image instead of inventing its contents. - The magic number decides the type and has to agree with the extension: a text file renamed `.png` is still `TOOL_BINARY_CONTENT`, because a request carrying a broken image fails as a whole. - An image above 3 MB raw (base64 inflates by four thirds, and the strictest per-image ceiling in use is 5 MB encoded) is refused with the new `TOOL_IMAGE_TOO_LARGE`, naming the size and the alternative, rather than being cut into an unreadable fragment. - The base64 payload never enters a persisted UI message or transcript record; the stored detail keeps `imageCount`, as the attachment rule already required. C:\Users\10470\.pi-desktop\scratch\cb9e2d41-8a55-4a18-899d-92ca2791ab51\commit-711-r2.txt
d71d97e to
c65b117
Compare
|
Rebased onto It applied cleanly, including the Local gates on the rebased head: One thing to know when reading the JS job: |
Fixes #711.
The report is exact: with an image-capable model, asking the agent to read an image from a path fails while attaching the same file to the prompt works. The reason is in the tool, not in the transport — and the transport half of this already existed, untested.
What was actually missing
Readrefuses any file whose extension is inBINARY_EXTENSIONS(png,webp,ico…) and then refuses anything the byte sniff calls binary, both asTOOL_BINARY_CONTENT("has no text to read"). So the model was told the file had no content, which is true of text and false of an image.The other half was already wired: a host tool result shaped
{text, images: [{data, mimeType}]}becomes[{type: "text"}, {type: "image"}…]in the runtime, gated on the model declaring image input (visionFromModelConfig). Nothing produced that shape, and the path had no test. pi's ownAgentToolResult.contentis(TextContent | ImageContent)[], so this needed no new plumbing.What the change does
shot.png(a real PNG, model declares image input)imageblock withimage/pngand standard base64notes.png(text renamed)TOOL_BINARY_CONTENT— a request carrying a broken image fails whole, so the magic number has to agree with the extensionhuge.png(4 MB)TOOL_IMAGE_TOO_LARGE, naming the size and the alternative.pdf,.zip, …TOOL_BINARY_CONTENTThe 3 MB raw bound exists because base64 inflates by four thirds and the strictest per-image ceiling this build talks to is 5 MB encoded. The base64 payload never reaches a persisted UI message or transcript record — the stored detail keeps
imageCount, which the existing attachment rule already required.Verification
read_returns_an_image_for_the_model_to_view,read_refuses_an_image_extension_whose_bytes_are_not_an_image,read_refuses_an_image_too_large_to_inline(the fixture is a real 1×1 PNG, decoded from base64 in the test);cargo test -p host-core --locked→ 552 passed, 4 failed, the four being the pre-existing Windows path-separator assertions inmcp_servers,user_skills,scheduled_rpcandscheduled_tools.d71d97edthe suite is 751 passed / 1 failed, the failure being the pre-existingnative-pi-sessioncase, andsrc/runtime.test.ts— 215 tests, the new bridge among them — passes in full.cargo fmt --all --check0;cargo clippy --workspace --all-targets --all-featuresexit 0; shared 78/78 test files (the new code inerrors.ts);pnpm lintclean;node scripts/check-architecture.mjspassed;pnpm docs:check(78 EN/zh pairs, 495 pages) exit 0.E2E: partial.
pnpm test:e2e(scripts/e2e-smoke.mjs, the headless protocol suite) → 23/23 passed, 2 skipped on this branch, and it does exercise the changed code:E2E-013-read-toolruns the realReadtool through the host binary built from this branch, across the RPC boundary. What is not run is the scenario written below —E2E-TOOL-read-returns-an-image-the-model-can-seeneeds a live provider request with a fixture image, and this repository's E2E set does not drive a real image read. Alternative validation: the host tests exercise the realReadtool throughexecute_tool, and the runtime tests exercise the real tool callback and assert on the exact content blocks pi-ai would send. Remaining risk: how a specific provider renders the image (and whether a relay strips image blocks) is not observed here.Docs
03-runtime/16-tool-result-limits.md§4/§6 and its acceptance list,03-runtime/08-error-codes.md(the new code plus the widened meaning of the old one),08-meta/decisions-log.mdD609, and the E2E scenario above — all mirrored todocs/zh-CN/….D609 was free when this was opened and still is: D606 (calibration), D607
(plugin crash) and D608 (tool-call ids) have since landed in
main, and#748/#756/#760 are closed. D604/D605 still belong to the open #721, so nothing
needs renumbering unless that one lands first.
Base and head
base 43a37373· headc65b1177· 11 files, +378 / −7. Rebased onto currentmain(24 commits further on);it applied cleanly, including the
convertToLlmseammainhas since rewritten.