camelAI Documentation

Guides

Agent files and shared volumes in camelRun

Attach files to messages, read what a camelRun agent wrote, share volumes between agents, and sign download and upload links.

View as Markdown · Docs index for coding agents

Every agent has files: its own workspace at /workspace by default, or the volumes you mount. Files you attach to a message land there, the model works on them with its file tools and with code, tools save their outputs there, and you read what the agent made by path. For a short tour of what an agent keeps, see AI agent memory on camelRun.

Attach files to a message

const run = await agent.run("What changed in Q3?", {
  files: ["./q3.pdf", screenshotBytes, new File([csv], "data.csv"), { path: "/workspace/notes.md" }],
});

A file is bytes, a Blob or File, {name, data, contentType?} (Python {"name", "data", "content_type"?}), a local path (from @camelai/run/node in TypeScript, or a str or Path in Python), or {path} for a file already in the agent's mounts. The SDKs upload each file first, to uploads/<run id>/<name>, then send the message referring to them by path, so a retried run never uploads twice.

  • At most 20 files per message, each at most 256 MiB.
  • Over REST, upload with PUT /v1/agents/{id}/uploads/{requestId}/{name}, then send POST /v1/agents/{id}/prompt {text, requestId, files: [{path}]}. The prompt also takes small files inline, {name, data: <base64>, contentType?}, up to 4 MiB in all.

What the model sees

The message carries a line per file, such as [File /workspace/uploads/r1/q3.pdf (application/pdf, 2.1 MB)], and the file itself where the model can take it:

Shown nativelyLimits
Images (PNG, JPEG, GIF, WebP)Models with image input5 MiB and 8,000 px a side each
PDFsAnthropic, Google, OpenAI and OpenRouter models with image input16 MiB and 100 pages each
Per model requestThe newest 100 images, up to 20 MiB as sent, and 24 MiB of PDFs shown. Older ones past that are named only

An image goes to the model at most 1,568 px a side. A larger one is scaled down in the request, keeping its shape (the file itself is unchanged), so a conversation with many images stays within every provider's limits. One that can't be scaled down is named, with why, instead.

Audio is transcribed when it is attached, and the model reads the transcript (see Voice and audio). Anything else is only named, and the model reads it with its file tools. The history keeps a reference to each file's content, never its bytes, so a file changed or deleted later still reads as it was when attached.

The agent's file tools

read, write, edit, ls, glob and grep work on mount paths such as /workspace/notes.md. read shows an image or PDF to a model that can view it. camelRun remembers which version of each file the agent last read or wrote, so a write or edit of a file someone else changed since then fails ("changed since you last read it; read it again") and the model reads again. The model never sees version numbers. Pass fileTools: false (in a definition, or when the agent is made) to leave the tools out.

Code in js_exec has fs over the same mounts:

js
const csv = await fs.readFile("/workspace/data.csv", { encoding: "utf8" });
await fs.writeFile("/workspace/out/chart.png", png, { contentType: "image/png" });
await fs.stat("/workspace/out/chart.png");  // {path, type, size, updatedAt, contentType}
await fs.list("/workspace/out");

One fs call moves at most about 700 KiB. Read larger files in windows with tools.read({ path, offset, encoding: "base64" }).

Files out

A run lists what it wrote in run.files ([{path, version, size, contentType}], up to 100). The files the model handed over with present_file (with a caption, up to 20) are in run.raw.presented, and each is also a file_presented event as soon as it is presented, with a signed download url.

const { data, contentType } = await agent.files.download("/workspace/out/chart.png");
const listing = await agent.files.list({ path: "/workspace/out" });
const link = await agent.files.link("/workspace/out/report.pdf", { expiresIn: 3600 });
await agent.files.upload("/workspace/in/config.json", JSON.stringify(config));

Tools from a definition's sources take the agent's files as {"$file": path}, and their file outputs are saved to the workspace. See Tools.

Volumes

A volume is a shared file tree. Without mounts, each agent gets its own workspace volume at /workspace. Mount the same volume in several agents to share files, read-only or read-write:

ts
const docs = await agents.runtime.createVolume({ name: "shared docs" });
await agents.runtime.volume(docs.id).write("handbook.md", handbook);
const agent = await agents.upsert("support", {
  mounts: [{ volumeId: docs.id, path: "/docs", mode: "ro" }],
  // ...
});
  • Mounts are {volumeId, path, mode: "ro" | "rw", subpath?, notify?}, at most 16 per agent. notify prompts the agent (about a second after, coalesced) when others change files under the mount. Only your own volumes can be mounted. An agent's mounts are set when it is made: an upsert with other mounts is a 409, unless it sends remount: true, which sets them between its turns.
  • PUT /v1/agents/{id}/mounts replaces them during a turn too. The change applies at the agent's next tool call; a call under way keeps the mounts it began with. A removed volume is refused, a read-only one is read-only, and one added or swapped in is there at its path.
  • The agent's own workspace stays at /workspace, after the mounts you give. {workspace: true} among them puts it at that position, {workspace: false} leaves it out, and a mount you give at /workspace takes its place. The first mount is where relative paths resolve. {workspace: true, path: "/scratch"} mounts the workspace elsewhere, for example beside a project volume: [{volumeId: project, path: "/workspace", mode: "rw"}, {workspace: true, path: "/scratch"}].
  • Attachments are saved under uploads/<request>/ in the agent's own workspace, tool file outputs under tool-outputs/, and the model keeps scratch files in tmp/. Without the workspace, they go to a read-write /workspace, else the first read-write mount; an agent with none can't take attachments or tool files.
  • An agent's own workspace is deleted with the agent. Volumes it only mounted stay.
  • createVolume({ name }, { idempotencyKey }) (Python create_volume(name=…, idempotency_key=…)) makes a volume once per key, for a day: a retry, or another process, gets the same volume.
  • volume.write(path, data, { version }) writes only if nobody changed the file since that version (0 means it must not exist yet). Volumes also have read, list, remove and changes(since, { prefix? }), and runtime.volumes(ids) reads up to 50 volumes' seq (and files and bytes) in one request, to see which changed.
  • volume.readAll({ prefix?, glob?, snapshot? }) (Python read_all) reads every matching file with its contents in one request, as the volume was at one seq: {seq, files: [{path, size, version, contentType, sha256, text | data}]}. Use it to check or publish what an agent wrote. At most 1,000 files and 16 MiB a read. REST: GET /v1/volumes/{id}/files?content=true&prefix=….
  • volume.archive({ snapshot?, path?, glob? }) (GET /v1/volumes/{id}/archive) streams the files as a tar.gz, at most 10,000 files and 1 GiB.
  • A volume suits up to about 100,000 files. Uploads may take 15 minutes.

For an agent that builds something your application publishes (a bot, a site, a report), a volume per project plus a publish step built on snapshots is the pattern: see Projects.

Snapshots

  • volume.snapshot() and volume.fork({ snapshot }) copy metadata only: a fork shares its source's content and diverges independently. read, list and readAll take { snapshot } to read a snapshot as it was.
  • volume.restore(snapshot) (POST /v1/volumes/{id}/restore {snapshot}) makes the volume as the snapshot was, in place, in one write that agents mounting it see. It answers {snapshot, seq, written, removed}, and the snapshot stays.
  • A volume keeps 100 snapshots. A pinned one (snapshot({ pinned: true }), or updateSnapshot(id, { pinned })) is kept until unpinned and counts against 10,000 pinned snapshots instead. Deleting it takes unpinning first, or deleteSnapshot(id, { force: true }).
  • Snapshots take labels, your own string map (16 at most), and snapshots({ labels }) (?label=key:value) lists those that have them.
  • volume.snapshot({ files }) makes a snapshot of the files you give (path to text or bytes; 1,000 files and 16 MiB at most) instead of the volume, which it leaves untouched: for bringing in versions kept elsewhere. Its answer has each file's sha256.
  • Deleting a volume deletes all its snapshots, pinned ones too.

Deleted files, volumes and snapshots stop being stored and billed about a day after nothing refers to them. A file an agent was sent or shown stays until the agent is deleted, since its history refers to it. Storage is billed per GB-month: see Pricing.

agent.files.link(path, options) or volume.link(path, options) returns {url, expiresAt}: a URL that downloads (GET) or uploads (PUT, with maxBytes and contentType) that one file without a token, so a browser or another service moves the bytes directly. Links last 15 minutes by default and at most 24 hours, and can't be revoked sooner. Downloads take Range.

From your server, POST /v1/agents/{id}/links {path, method?, expiresIn?, maxBytes?, contentType?} signs a link by the path the agent sees (/workspace/report.pdf), and POST /v1/volumes/{id}/links by a volume's own path. Both answer {url, method, path, expiresAt}.