Send documents
Attach a PDF to a turn and a document-capable model reads it alongside your text. The direct Anthropic, Gemini, and OpenAI providers encode the PDF as a native document part; AgentKit Cloud routes it to a tier whose model accepts documents. Where document input isn't available, the call fails loud rather than quietly dropping your PDF.
Attach documents to a message
send(_:documents:) takes an array of DocumentRef. Use the pdf factories:
raw bytes, or a file URL.
let contract = try Data(contentsOf: contractURL)
try await agent.send(
"Summarize the termination clauses.",
documents: [.pdf(contract, filename: "contract.pdf")]
)
try await agent.send(
"Compare these two contracts.",
documents: [.pdf(fileURL: oldContractURL), .pdf(fileURL: newContractURL)]
)
The turn appends one user message with a pinned content order: your text first, then the documents in argument order.
File URLs are read exactly once, at the send boundary — security-scoped, so a file vended by a document picker reads correctly — and the conversation stores bytes, so providers never touch your disk and a resumed conversation carries real content. The filename derives from the URL's last path component.
Only PDFs, within size limits
Before anything leaves the device, each document is validated in the relay's
order: the media type must be application/pdf, each document must be within
the route's per-document cap and the turn within its aggregate cap, and the
bytes must begin with the %PDF- signature. Any failure throws a typed error
and the conversation is untouched — no partial request leaves:
do {
try await agent.send("Read this.", documents: [.pdf(oversized)])
} catch AgentSessionError.documentTooLarge(let bytes, let limit) {
print("\(bytes) bytes exceeds the \(limit)-byte cap")
}
Each route advertises its own decoded-size caps — AgentKit Cloud mirrors the relay's documented 10 MB per-document / 20 MB per-turn limits, the direct providers their own — and every route is additionally bounded by a hard SDK ceiling, so a custom provider can never make the SDK read an unbounded file. The backend stays the final authority.
Where documents work
The direct Anthropic, Gemini, and OpenAI providers encode a PDF as that
provider's native document part. AgentKit Cloud routes it to a tier whose model
accepts documents. The on-device Apple provider does not build document send:
rather than silently degrading your PDF to a text placeholder, it reports
document input as unsupported and a send(_:documents:) call fails loud.
| Path | Document input |
|---|---|
| Anthropic / Gemini / OpenAI (direct) | the PDF rides as the provider's native document part |
| AgentKit Cloud — document-capable tier | the PDF rides the wire as a real document block |
| AgentKit Cloud — tier whose model can't accept documents | fails fast before upload (see below), else the relay rejects it cleanly |
| Apple on-device | not built — send(_:documents:) throws documentInputUnsupported |
documentInput = true means a route can encode a PDF, not that every model
accepts one — AgentKit model ids are opaque strings, so a model that can't read
PDFs surfaces the provider's own API error and your conversation is left intact
(the failed turn keeps your message but appends no reply).
Fail fast, before the bytes leave. A route counts as unsupported only when
it says so explicitly. For AgentKit Cloud that signal is a pre-request capability
probe: before a document send the SDK fetches the tier's current capabilities, and
if the probe says documents aren't accepted it throws documentInputUnsupported
before uploading a single byte. Until the probe resolves — or against a relay that
predates the capability endpoint — the capability is unknown and the send
dispatches: the backend stays the authority and returns a clean rejection if the
resolved model can't take the document. The streaming handshake still reports
capabilities in-band for the turn it dispatches, but the pre-upload gate is the
probe alone — a capability seen on an earlier stream never silently blocks a later
send.
If a document already sits in your conversation history and you continue the
turn on a route that can't represent it, the send throws
documentHistoryUnsupported rather than silently pruning the document.
One edge case worth knowing: a .fileURL document that enters history without
going through send() (seeded conversation history) degrades to a name-only
text descriptor on every wire — providers never read the filesystem, and a full
path never reaches a request.
Error handling
send(_:documents:) throws a typed AgentSessionError before any session
mutation:
| Error | When |
|---|---|
documentInputUnsupported |
a new document on this send, but the resolved route explicitly does not accept documents |
documentHistoryUnsupported |
history already carries a document the resolved route can't represent (a seeded conversation, or a continuation after the route resolved to no-documents) |
unsupportedDocumentMediaType(mimeType:) |
the media type is not application/pdf |
documentTooLarge(bytes:limit:) |
a document, or the turn's aggregate, exceeds the size cap |
documentNotPDF |
the bytes do not begin with %PDF- |
unreadableDocumentAttachment(url:detail:) |
a .fileURL document could not be read |
Next
- Send images — the image sibling of this page.
- AgentKit Cloud — how tiers route to document-capable models.
- Error reference — every typed error, including the document cases.