PAIPAI

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 input

This 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.

On this page