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