Skip to content

Send hydrated image attachments to the agent as embedded bytes#327482

Open
ulugbekna wants to merge 2 commits into
mainfrom
ulugbekna/send-hydrated-images-to-agent
Open

Send hydrated image attachments to the agent as embedded bytes#327482
ulugbekna wants to merge 2 commits into
mainfrom
ulugbekna/send-hydrated-images-to-agent

Conversation

@ulugbekna

Copy link
Copy Markdown
Contributor

Problem

In the Agents window, a uri-only image attachment (no inline bytes) — e.g. a hydrated edit-and-resend image — reached the model as a MessageAttachmentKind.Resource path reference, not pixels. Downstream, a Resource image is never resolved to bytes before hitting the model:

  • Copilot (copilotAgentSession.ts _toSdkAttachment): Resource image → { type: 'file', path } (path only)
  • Codex (codexPromptResolver.ts): Resource@<path> text mention
  • Only EmbeddedResource carries actual bytes.

So the agent genuinely could not see those images. This is a follow-up to #327381 (which only restored the UI thumbnail preview) and addresses the model-perception half of #327329.

Fix

In _toImageAttachment, in the no-inline-bytes branch, when the conversion is model-facing, read the referenced file via a newly injected IFileService and emit an EmbeddedResource (base64 data, preserving range/_meta) so the model receives the real image. On any read failure it falls back to the existing Resource path reference (never throws). The original (non-rebased) refUri is read directly — the registered vscode-agent-host: file-system provider handles remote reads; rebasing remains only for the path-reference fallback.

Hydration is gated by a hydrateImageBytes flag so only model-facing conversions pay the I/O:

  • turn (_convertVariablesToAttachments) → hydrate
  • pending / queued / steering (_syncPendingMessages) → hydrate
  • draft input state (_inputStateToDraft) → stays a lightweight path reference (avoids file I/O + base64 bloat on every debounced draft sync)

The conversion caller chain (_convertVariableToAttachment, _variableEntriesToAttachments, _appendActiveEditorAttachments, _convertVariablesToAttachments, _syncPendingMessages, _inputStateToDraft) is now async.

Because _syncPendingMessages now awaits file I/O between reading its diff baseline and dispatching, per-session pending syncs are serialized and coalesced through a ThrottlerByKey, so concurrent fire-and-forget runs can't interleave and dispatch a stale same-id message (latest run wins). This was flagged by code review.

Scope / caveat

Helps Copilot and Codex. Claude still drops EmbeddedResource today (claudePromptResolver.ts) — a pre-existing limitation affecting all embedded images (including today's inline-bytes ones), out of scope here.

No max-images-per-request cap exists on the pre-existing inline-bytes path, so there is nothing to apply symmetrically.

Tests

Added regression tests in agentHostChatContribution.test.ts using an injected fake IFileService (TestFileService, no any/global stubs):

  1. uri-only image + file bytes → EmbeddedResource with base64 data and correct contentType
  2. IFileService read fails → falls back to MessageAttachmentKind.Resource
  3. inline-bytes image → EmbeddedResource, with an assertion that the file service is never read

Validation

  • npm run typecheck-client — pass
  • Targeted browser suite agentHostChatContribution.test.ts — 261 passing, 0 failing
  • Pre-commit hygiene hook — pass

Follow-up to #327381. Fixes #327329.

Copilot AI review requested due to automatic review settings July 25, 2026 19:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hydrates URI-only image attachments into embedded bytes so Copilot and Codex can process their pixels.

Changes:

  • Reads URI-backed images for model-facing conversions with resource fallback.
  • Serializes pending-message synchronization.
  • Adds image hydration regression tests.
Show a summary per file
File Description
agentHostSessionHandler.ts Adds image hydration and asynchronous pending synchronization.
agentHostChatContribution.test.ts Tests hydration, fallback, and inline-byte behavior.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Medium

for (const p of pending) {
const variables = p.request.variableData?.variables ?? [];
const messageAttachments = this._variableEntriesToAttachments(variables, sessionResource, p.request.message.text);
const messageAttachments = await this._variableEntriesToAttachments(variables, sessionResource, p.request.message.text, true);
try {
// Read the original (non-rebased) URI; the registered
// vscode-agent-host file system provider handles remote reads.
const content = await this._fileService.readFile(refUri);
ulugbekna and others added 2 commits July 25, 2026 22:02
Uri-only image attachments (e.g. hydrated edit-and-resend images) reached the model as a Resource path reference instead of pixels, so the Agents window agent could not actually see them. In _toImageAttachment's no-inline-bytes branch, read the referenced file via IFileService and emit an EmbeddedResource (base64) for model-facing conversions (turn + pending), falling back to the existing Resource path reference on any read failure. Draft input-state sync stays a lightweight path reference.

The attachment-conversion caller chain (_convertVariableToAttachment, _variableEntriesToAttachments, _appendActiveEditorAttachments, _convertVariablesToAttachments, _syncPendingMessages, _inputStateToDraft) is now async. Per-session pending-message syncs are serialized and coalesced through a ThrottlerByKey so the added hydration I/O cannot interleave concurrent runs and dispatch a stale same-id message.

Helps Copilot and Codex; Claude still drops EmbeddedResource today (pre-existing, out of scope).

Follow-up to #327381 for #327329.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c77566c8-d94f-4245-b965-75ea069af547
A uri-only image in a chat draft is not model-facing, so the draft path
keeps it as a lightweight MessageAttachmentKind.Resource reference and must
not read file bytes on every debounced draft sync (hydrateImageBytes defaults
to false). Locks in that behavior, which was previously untested.

Follow-up to #327381 for #327329.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c77566c8-d94f-4245-b965-75ea069af547
@ulugbekna
ulugbekna force-pushed the ulugbekna/send-hydrated-images-to-agent branch from 04ec1c2 to 84197b9 Compare July 25, 2026 20:03
try {
// Read the original (non-rebased) URI; the registered
// vscode-agent-host file system provider handles remote reads.
const content = await this._fileService.readFile(refUri);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked closely at attachments in a bit but I'm not sure this is the right approach- reading and serializing the image and inlining all content. If this is as file on disk, shouldn't we pass it around as a URI, and the agent host can read it if or when it needs to?

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.

Agents window: hydrated image attachments lose their preview after window reload / session resume

3 participants