Skip to content

feat(agent): add file uploads (chat attachments + workspace files)#10

Merged
ThomasK33 merged 3 commits into
mainfrom
worktree-agent-file-uploads
Jun 19, 2026
Merged

feat(agent): add file uploads (chat attachments + workspace files)#10
ThomasK33 merged 3 commits into
mainfrom
worktree-agent-file-uploads

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

What

Adds file uploads to @coder/ai-sdk-agent, split by intent rather than by transport:

  • Chat attachments — content for the model to read (PDF, image, CSV, …). A native AI SDK file part dropped into a message is uploaded to chat-file storage transparently:

    await agent.generate({ messages: [{ role: "user", content: [
      { type: "text", text: "Summarize this." },
      { type: "file", data: await readFile("report.pdf"), mediaType: "application/pdf", filename: "report.pdf" },
    ]}]});

    Or pre-upload once and reuse across turns (accepts Blob/File/stream/bytes):

    const f = await agent.attach({ content: await openAsBlob("report.pdf"), mediaType: "application/pdf" });
    f.toFilePart(); // references by id — no re-upload

    Validated against the 10 MiB cap and the media-type allowlist up front, with actionable errors.

  • uploadToWorkspace() — material for the agent to operate on (zips, datasets, binaries — anything over the cap or off the allowlist). Writes bytes as-is via an injected WorkspaceFileStore adapter, keeping the agent core dependency-free. No unpacking — instruct the agent to unzip, or do it over your own connection.

How

  • CoderChatClient.uploadChatFile() / getChatFile() wrap POST/GET /api/experimental/chats/files (raw body, not multipart), mirroring codersdk.
  • FileContent = Blob | ReadableStream | Uint8Array | ArrayBuffer; Blob/File carry their own type/name/size, raw bytes are wrapped in a Blob for a clean body + Content-Length.
  • Transparent file parts are uploaded inside CoderLanguageModel before the chat exists (the endpoint needs only the org id); a pre-uploaded id carried in providerOptions.coder.fileId is referenced without re-uploading.
  • Removes the "file inputs not forwarded" limitation from the README and adds a Files section + examples/05-file-upload.ts.

Testing

  • Unit (fake client): resolveFileContent normalization, transparent upload + providerOptions reuse, attach()/toFilePart(), uploadToWorkspace() with/without adapter. 34 unit tests pass.
  • Live e2e against dev.coder.com (v2.34.3-devel): upload/download round-trip + model-reads-attachment. 5/5 e2e pass — the endpoint is wire-verified, not just source-derived.
  • typecheck ✓ · oxlint ✓ · oxfmt ✓ · build ✓ (ESM + DTS).

Notes

  • uploadToWorkspace() has no e2e (the suite is deliberately workspace-free, no provisioning); it's unit-tested with a fake adapter. A live test would need a running workspace + a real sandbox-session adapter.
  • No client-side extract-text/chunk strategy yet (needs a PDF lib; intentionally kept out of core).

🤖 Generated with Claude Code

ThomasK33 and others added 3 commits June 19, 2026 11:34
Two ways to get a file to the agent, split by intent:

- Chat attachments — content for the model to *read*. A native AI SDK `file`
  part dropped into a message is uploaded to chat-file storage transparently;
  `attach()` returns a reusable handle whose `toFilePart()` references it by id
  (no re-upload, via `providerOptions.coder.fileId`). Validated against the
  10 MiB cap and media-type allowlist up front, with actionable errors.
- `uploadToWorkspace()` — material for the agent to *operate on* (zips,
  datasets, binaries). Writes bytes as-is through an injected
  `WorkspaceFileStore` adapter, so the agent core stays dependency-free; no
  unpacking (instruct the agent to unzip, or do it over your own connection).

New `CoderChatClient.uploadChatFile()`/`getChatFile()` wrap POST/GET
`/api/experimental/chats/files`. `FileContent` accepts Blob/File/
ReadableStream/Uint8Array/ArrayBuffer; raw bytes are wrapped in a Blob for a
clean body + Content-Length. Removes the "file inputs not forwarded" limitation.

Tested: unit (fake client) + live e2e against dev.coder.com (upload/download
round-trip and model-reads-attachment).

Change-Id: I9dc522d4e877b8bea5247989c4676c439d53d378
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
- Parallelize per-message file-part uploads (order-preserving Promise.all)
  instead of awaiting each sequentially.
- Extract a shared `#json()` body parser in CoderChatClient; uploadChatFile no
  longer hand-rolls JSON parsing.
- Reuse `@ai-sdk/provider`'s `LanguageModelV3DataContent` instead of
  re-declaring the data union; collapse the Uint8Array/ArrayBuffer branches.
- Single-source the providerOptions namespace as `CODER_PROVIDER_OPTIONS`
  (+ `CoderFileProviderOptions` type), used by both writer and reader.
- Drop unused `MAX_CHAT_FILE_IDS` and `ChatFileMetadata` (dead code).

Change-Id: I96c7707277f8e421fabd7af1ee5993c969debcf9
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
- uploadChatFile now throws when a 2xx response carries no file id, instead of
  silently returning id "" (which emitted a `file_id: ""` reference to nothing).
- coderFileId requires a non-empty id, so a blank reused handle re-uploads
  rather than referencing nothing.
- Normalize the media type (strip `;` params, lowercase) before the allowlist
  check and Content-Type, so `text/plain; charset=utf-8` (common from a browser
  File) is accepted instead of rejected.
- Content-Disposition now uses RFC 6266 (sanitized ASCII `filename` + RFC 5987
  `filename*`): non-ASCII names (CJK, emoji, accents) no longer throw a
  ByteString error in fetch, and CR/LF can't break or inject the header.
- Guard the constructor against a `workspaceFiles` adapter bound to a different
  workspace than the chat.
- Run `resolveModelConfigId` and the file uploads concurrently (`Promise.all`).
- `base64ToBytes` returns the Buffer directly (no extra copy); `#send` reuses
  `#json` for its error body.

Adds hermetic CoderChatClient.uploadChatFile unit tests (mocked fetch).

Change-Id: Ifb493026f9ed5f4ab6fdf0c00a3170fe47240dbf
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit cfc5dd9 Jun 19, 2026
6 checks passed
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