Files And Attachments
Upload immutable user files and send them as message parts.
Files are user-supplied inputs. PAI stores them as immutable attachment references in the transcript, then asks the configured file provider to resolve those references when the model needs bytes.
const uploaded = await client.files.upload({
body: file,
mediaType: file.type,
filename: file.name,
});
await thread.send({
text: "Describe this image.",
attachments: [uploaded],
});Uploading is its own step, and upload returns a value the shorthand accepts
directly. Keeping it separate is what makes per-file upload progress, removing
one attachment before sending, thumbnails, and knowing which file failed
possible — a send that uploaded for you could report none of them.
Only fileId crosses the wire. The descriptive fields are snapshotted from the
file provider when the part is persisted, so the transcript records what was
attached at the time and nothing a caller passes can contradict the provider.
Do not inline large file bytes into the message.
The upload client does not accept a userKey. The server derives the creator
key from trusted runtime identity and records it as provider-side provenance.
That key is not copied into the attachment part and does not grant file access;
the application-defined scope remains the authorization partition.
Attachment Parts
What you send and what gets persisted are different shapes. A send carries the reference:
type UserAttachmentPartInput = {
type: "attachment";
fileId: string;
};The runtime resolves that reference against the file provider and persists the provider's answer:
type PaiAttachmentPart = {
type: "attachment";
id: string;
fileId: string;
mediaType: string;
filename?: string;
byteSize?: number;
metadata?: JsonObject;
};The provider is the only writer of the descriptive fields, so they cannot
disagree with the stored file. Application annotation belongs on the file at
upload time — pass metadata to client.files.upload(...), where a provider's
beforeSave hook can validate or normalize it — and it is snapshotted onto
every part that references the file.
Rendering Attachments
Attachment parts appear in place inside PaiMessage.parts:
for (const message of state.messages) {
for (const part of message.parts) {
if (part.type === "attachment") {
renderAttachment(part);
}
}
}Read transcript metadata from the part, such as part.fileId and
part.filename. Access URLs are not stored in the message. Request one lazily
when a preview or download is needed.
Reading Or Linking Files
Use file helpers when the app needs preview or download behavior:
const body = await client.files.read({ fileId });
const preview = await client.files.url({
fileId,
intent: "view",
});
const download = await client.files.url({
fileId,
intent: "download",
});The provider decides whether it can return bytes, signed URLs, data URLs, or nothing. The transcript only depends on fileId.
Model Input
The runtime resolves attachments before the model call:
attachment message part
-> file provider reads immutable file metadata/body
-> model adapter converts it to provider-specific file inputThis keeps the transcript stable while allowing storage-specific implementations, such as filesystem, object storage, database blobs, or test fixtures.
What Not To Store
Do not use PAI attachments for app-owned resources that the agent edits over time. Those should live in your application data model and be exposed to the agent through normal tools.
Attachments are for immutable user inputs that are part of the conversation history.