# Quickstart In five minutes you will have an agent that runs in the hosted runtime, calls a tool in your own process, and answers you. The runtime keeps the model loop, the agent's history and its files; your tools stay in your code, with your credentials. **With a coding agent.** Paste this into Claude Code, Codex, Cursor or any coding agent. It asks you what the agent should do, installs the SDK, finds your API key without you pasting it into chat, and runs your first agent: ```text Read https://run.camelai.com/SKILL.md and set up camelRun in this project. Ask me what the agent should do. ``` **Try it with no API key.** Connect your coding agent to the hosted MCP server. Signing in with GitHub or Google creates your account: ```sh claude mcp add --transport http camelrun https://run.camelai.com/mcp # then /mcp in Claude Code codex mcp add camelrun --url https://run.camelai.com/mcp && codex mcp login camelrun ``` Then ask it to "use camelrun to build me my first agent". See the [CLI and MCP server](https://run.camelai.com/docs/reference/cli.md). **Credit.** GitHub accounts older than 30 days start with free credit. Everyone else (Google sign-ins, newer GitHub accounts) unlocks the same credit by verifying a card on the console's **Billing** page. The card is not charged. To build it into your app yourself, read on. ## 1. Get an API key Sign in to the console at with GitHub or Google. Under **API keys** (), create a key, and export it: ```sh export CAMELAI_API_KEY=art_... ``` The key can create and control every agent in your account. Keep it on your server and never ship it to a browser. ## 2. Install TypeScript (Node 22 or later, or Bun): ```sh npm install @camelai/run ``` Python (3.11 or later): ```sh pip install camelai-run ``` ## 3. Run an agent TypeScript, `quickstart.mts` (`.mts` makes it an ES module, which top-level `await` needs; in a project with `"type": "module"`, `.ts` works too): ```ts import { Agents, schema, tool } from "@camelai/run"; const agents = new Agents(); // reads CAMELAI_API_KEY // A tool is an ordinary function: it runs here, in your process. const weather = tool({ description: "Today's weather in a city", input: schema.Object({ city: schema.String() }), execute: ({ city }) => ({ city, forecast: "sunny", highC: 24 }), }); // The same key is the same agent, with its history, every time you run this. const agent = await agents.upsert("quickstart", { instructions: "You are a concise assistant.", tools: { weather }, }); const run = await agent.run("Should I bring an umbrella in Lisbon today?"); console.log(run.text); await agents.close(); ``` ```sh npx tsx quickstart.mts ``` Python, `quickstart.py`. The Python SDK is async; synchronous code (a script, a Django view, a Celery task) uses `camelai_run.sync`, which has the same names without `await` (see [the synchronous client](https://run.camelai.com/docs/reference/sdk.md#the-synchronous-client-python)): ```python import asyncio from camelai_run import Agents, tool @tool def weather(city: str) -> dict: """Today's weather in a city""" return {"city": city, "forecast": "sunny", "highC": 24} async def main(): async with Agents() as agents: # reads CAMELAI_API_KEY agent = await agents.upsert("quickstart", instructions="You are a concise assistant.", tools=[weather]) run = await agent.run("Should I bring an umbrella in Lisbon today?") print(run.text) asyncio.run(main()) ``` ```sh python quickstart.py ``` curl (the agent has no tools of yours here: tools that run in your process need an SDK, or a server of your own, see [Tools](https://run.camelai.com/docs/guides/tools.md)): ```sh BASE=https://run.camelai.com; AUTH="Authorization: Bearer $CAMELAI_API_KEY" # The Idempotency-Key is the agent's key: the same key is the same agent. AGENT=$(curl -s $BASE/v1/agents -H "$AUTH" -H "Content-Type: application/json" -H "Idempotency-Key: quickstart" \ -d '{"systemPrompt": "You are a concise assistant."}' | jq -r .id) REQ=$(curl -s $BASE/v1/agents/$AGENT/prompt -H "$AUTH" -H "Content-Type: application/json" \ -d '{"text": "Write a haiku about durable agents."}' | jq -r .id) # ?wait=25 answers as soon as the run ends (or after 25 s, still running: ask again). until RUN=$(curl -s "$BASE/v1/agents/$AGENT/requests/$REQ?wait=25" -H "$AUTH") && jq -e '.state == "completed"' <<< "$RUN" > /dev/null; do :; done jq -r 'if .status == "failed" then "failed: \(.error)" else .outcome.result.reply end' <<< "$RUN" ``` The agent names no model, so it uses your account's default: the first of the platform's default models your account can use. To choose one (optional), list the models you can use, and pass one as `model`: ```sh npx -y @camelai/camelrun models --available # or, without Node: curl -s "https://run.camelai.com/v1/models?available=true" -H "Authorization: Bearer $CAMELAI_API_KEY" | jq -r '.[].id' ``` ```ts const agent = await agents.upsert("quickstart", { model: "", instructions: "…" }); ``` ```python agent = await agents.upsert("quickstart", model="", instructions="…") ``` A model your account can't use fails the run with code `model_key_missing`, and an error that says which key to set: pick one from that list, or add the provider's key under **Models & keys**. ## 4. Stream it `run()` waits for the whole run. To show it as it happens, `stream()` yields its text as the model writes it, each tool call and result, and last the run: ```ts for await (const part of agent.stream("And tomorrow?")) { if (part.type === "text") process.stdout.write(part.text); if (part.type === "tool_call") console.log(`\n[${part.name}(${JSON.stringify(part.arguments)})]`); if (part.type === "done") console.log(`\n(${part.run.status})`); } ``` ```python stream = agent.stream("And tomorrow?") async for part in stream: if part.type == "text": print(part.text, end="", flush=True) elif part.type == "tool_call": print(f"\n[{part.name}({part.arguments})]") elif part.type == "done": print(f"\n({part.run.status})") run = await stream.result() # the same run as "done" has, also after a stream that broke off ``` The run's result is the truth; the stream is for display. A stream that breaks never loses the run: `run()` (or `stream.result()`) still resolves with it. ## What just happened - `upsert("quickstart", …)` made a keyed agent, or found the one made last time and brought it to this configuration. It lives until you delete it, so running the file again continues the same conversation. See [Concepts](https://run.camelai.com/docs/concepts.md). - Because the agent has `tools`, this process served them: the runtime called `weather` here, over the connection the SDK holds. One process at a time serves an agent's tools, and only while it runs. Serverless functions, several web workers or task workers, and multi-user backends serve tools over HTTP instead, which also keeps them working through deploys; see [Several processes, workers and deploys](https://run.camelai.com/docs/guides/tools.md#several-processes-workers-and-deploys). - `run()` resolved with a `Run`: `status` (`completed`, `input_required` or `failed`), `text`, `inputs`, `error`, `toolCalls` (Python `tool_calls`: each tool it called), `toolErrors` (Python `tool_errors`). A failed run throws a `RunError` unless you pass `throwOnError: false` (`throw_on_error=False`). A `completed` run can still list tool errors, e.g. `connection_lost` when the process serving its tools went away mid-call: check them. - `agents.close()` let the process exit. The agent stays in the runtime. One answer per input, with nothing to remember (classify, extract, vote)? Skip the agent: `await agents.run({ instructions, input, output })` is one call, and keeps nothing between runs. See [Stateless runs](https://run.camelai.com/docs/guides/stateless-runs.md). Next: [Concepts](https://run.camelai.com/docs/concepts.md), then the guide for what you are building: [a chat in your app](https://run.camelai.com/docs/frontend.md) (`npm create @camelai/run-app`), [tools](https://run.camelai.com/docs/guides/tools.md), [asking people](https://run.camelai.com/docs/guides/human-input.md), [showing an agent in a browser](https://run.camelai.com/docs/guides/browser.md), [files](https://run.camelai.com/docs/guides/files.md), [agents for many users](https://run.camelai.com/docs/guides/multi-user.md), [webhooks](https://run.camelai.com/docs/guides/webhooks.md). Before you ship, read the [production checklist](https://run.camelai.com/docs/production.md). --- # Concepts ## Agents are durable and keyed An agent is a conversation that lasts: its history, its files, its configuration (model, instructions, tools) and its pending work live in the runtime, not in your process. Most agents are asleep at any moment and cost nothing; a message wakes one on whichever node has room, and it picks up where it was. You name an agent with a **key**, your own id for it: `support-triage`, `user-123`, `thread-9f2c`. Keys are 1 to 80 letters, digits, `_` and `-`. ```ts const agent = await agents.upsert("user-123", { model, instructions, tools }); ``` - The same key is the same agent, from any process, for as long as you keep it. A keyed agent lives until you delete it (`agent.delete()`, `DELETE /v1/agents/{id}`). Only your API key can delete an agent: its own token gets a 401, since that token lives in your application's process (and in browsers through it), and deleting purges the agent's history and workspace. - `upsert` makes the agent when there is none, and otherwise brings it to the configuration you pass: a changed model, instructions or tool list is applied between its turns, and its history carries over. Who the agent acts for (`subject`, `context`), its definition and its mounts are fixed when it is made; changing them is a 409 that names the field (an upsert may change the mounts with `remount: true`). Delete it and upsert again to start over. - A deleted key makes a fresh agent the next time, with a new id and token. - Make an agent once per user, conversation or job, and reuse it. Creating agents is cheap and limited only against abuse (600 a minute; an upsert that changes nothing does not count). The limit to design around is **busy agents**: how many may run at once, 20 on free credit and more with each [usage tier](https://run.camelai.com/docs/reference/limits.md#usage-tiers). Runs are limited to 240 a minute on free credit (600 once you buy credit). See [Rate limits](https://run.camelai.com/docs/reference/limits.md#rate-limits). - Over REST, the key is the `Idempotency-Key` of `POST /v1/agents`. Every agent also has an **id** (`client_…`), which is safe to log and store, and a **token**, which lets its holder run it and nothing else (not delete it, nor change who it acts for, its tools' sources or its limits). The SDKs keep the token out of logs; you rarely need it, because upserting by key gives you the agent again. If a token leaks, `POST /v1/agents/{id}/credentials/rotate` gives the agent a new one: the old one stops working at once and connections made with it close, and `GET /v1/agents/{id}/credentials` (the SDKs' `agents.get`) gives the new one from then on. Agents made without a key are scratch agents: they expire after a day unless you set `ttlSeconds`. `idleTtlSeconds` instead makes an agent live that long from its latest run, so one in use is kept and one left alone expires (an agent for a chat that should go once nobody has written in a week, say). ## Runs A message to an agent starts a **run**: the model loop, with every tool call it makes, until the model answers or something stops it. One run at a time: a message sent while a run is going waits for it (`whileRunning: "queue"`, the default) or joins it (`whileRunning: "steer"`: the running turn reads it after its current step). A steer is answered at once (`agent.steer()` returns a receipt: `accepted`, `taken` or `queued`), and its request completes as the turn reads it, so a turn fed messages constantly never fills the agent's queue. ```ts const run = await agent.run("Summarize ticket 123", { user: "alice" }); ``` A `Run` has: | field | | | --- | --- | | `id` | the run's id; pass `idempotencyKey` to choose it, and sending the same key again returns the same run instead of starting another | | `status` | `completed`, `input_required` (it waits on a person: see `inputs`), or `failed` | | `text` | the final reply | | `output` | with `output: schema`, the answer in that shape ([Structured output](https://run.camelai.com/docs/guides/structured-output.md)) | | `inputs` | what it waits on, each with `answer()` | | `error` | `{code, message, uncertain?}` when it failed | | `toolErrors` | tool calls that did not complete (timed out, connection lost, no process serving the tools); the model was told and carried on | | `toolCalls` | every tool call the run made (the first 100), calls from `js_exec`'s code included: `{tool, toolCallId, innerCallId?, ok, code?, agentId?}`. A `delegate` call names its sub-agent (`agentId`). Arguments and results are in history | | `files` | files the run wrote | | `usage` | what its model calls used, where the runtime reports it; `subagentCostUsd`, what its sub-agents spent; `imageCostUsd`, what its `generate_image` calls cost | ### Runs without the history `history: "none"` (Python `history="none"`) runs a message as a new conversation would: the model sees the agent's instructions and tools as they are now, and this message, not the messages before it. One agent can then answer many independent questions without making an agent for each (creates are [rate limited](https://run.camelai.com/docs/reference/limits.md#rate-limits); runs far less). ```ts const run = await agent.run("Is a hot dog a sandwich?", { history: "none", output: Verdict }); ``` The run is still recorded in the agent's history, its user message marked `history: "none"`, for audit and for the console. Runs that do not say `none` see it as they see any other message. Such a run is never compacted after, since it read nothing to compact; a later run with the whole history compacts first if it must. It is for a prompt that starts its own turn, not `whileRunning: "steer"`. Resumed after a person answers it, or continued on another node, the run stays without the history. `run()` has no timeout: runs can take minutes, and a run waiting on a person can wait for days. Pass an `AbortSignal` (Python: `timeout=`) to stop waiting; the run itself goes on, and `agent.abort()` stops it: the running turn ends, and the runs queued behind it are cancelled (code `cancelled`), so nothing runs after the stop (`abort({ queued: "keep" })` stops the running turn only). A failed run throws a `RunError` carrying the run, unless you pass `throwOnError: false`. ### Stateless runs When each answer should stand alone (classify, extract, vote, judge), there is no agent to keep: `agents.run({ instructions, input, output })` (REST: `POST /v1/runs`) takes a configuration and an input and returns the result, with nothing carried over and no agent made. It is as durable as an agent's run and counts against the same limits. See [Stateless runs](https://run.camelai.com/docs/guides/stateless-runs.md). ### Long conversations An agent's history can outgrow its model's context window, so the runtime **compacts** it: older messages are summarized, and the model sees the summary followed by the recent messages. History keeps every message (`/history`); only what the model is sent changes. Compaction runs in the background. Once the context comes within a margin of its limit (32k tokens, or 15% of a smaller window, before the window less a reserve for the reply), the summary is made after the run ends, or between model requests, while the agent goes on: the next run starts at once with the whole context, and the summary takes over when it is written. Only a run whose context would not fit waits, for the summary being made or, if none is, for one of its own. A provider that rejects a request as too long also gets a summary and the request again. Summaries are billed like model responses (`kind: "compaction"` in usage) and count against spend limits; one made between runs belongs to no run. An agent compacts one summary at a time. ## Forking A **fork** is a new agent made from an existing one: the same configuration (model, instructions, definition, tools, built-ins, who it acts for, key scope, spend and run limits), a copy of its history, and a copy of its workspace files. From then on each is its own: a turn, a compaction or a file written in one never shows in the other. Fork to try another direction without losing the first, to branch a conversation where a user edits an earlier message, or to start many agents from one prepared one. ```ts const branch = await agent.fork({ key: "thread-9f2c-b" }); const earlier = await agent.fork({ atMessage: 4 }); // history through message 4 ``` - **Where it ends.** By default the copy ends with the agent's last finished turn: never inside a turn that is running or waiting on a person, so forking a busy agent is safe. `atMessage` ends it earlier: a history index (the `index` of a [history page](https://run.camelai.com/docs/reference/sdk.md#history-in-pages) entry) keeps that message and the tool results that answer it; a run's request id keeps that run's whole turn. A message in a turn that has not ended is a 409 `FORK_POINT_RUNNING`. - **Compaction.** The copy keeps the source's summaries, so the fork's model sees what the source's would at that point: the summary, then the messages after it. History (`/history`) has every message. - **Files.** The fork's workspace (at the source's path, `/workspace` by default) is a [fork of the source's volume](https://run.camelai.com/docs/guides/files.md#volumes) as it is when you fork, not as it was at `atMessage`. Other volumes the source mounts are mounted as they are, shared. Files the history refers to (attachments) stay the fork's, whatever becomes of the source. - **Not copied:** schedules, channel bindings, pending inputs, runs and their events, and what the source has spent. - **Its own:** `subject` and `context` (who it acts for, and its tool servers' context, fixed once it is made as at a create), `systemPromptAppend` and `modelHeaders` may be given for the fork instead of the source's: a fork for another conversation names that conversation. - **Keys.** A fork made with a `key` is a keyed agent: it lives until deleted, and forking again with the same key returns the same fork (another agent's key is a 409). Without one it is a scratch agent that lives a day, unless you set `ttlSeconds`. - `forkedFrom: {agentId, atMessage}` on the fork (`GET /v1/agents/{id}`, the console) says where it came from: `atMessage` is the index of the last message it began with. A fork is an agent like any other: it counts toward your agents and their storage, and against the `agent_creates` rate limit (`atMessage: 0` included). To run one message without the history, send it with `history: "none"` instead. The copy costs only storage; its history and files share stored content with the source. Over REST it is `POST /v1/agents/{id}/fork` with `{key?, name?, atMessage?, ttlSeconds?, subject?, context?, systemPromptAppend?, modelHeaders?}` (the key may be the `Idempotency-Key` header instead); in the console, **Fork** on the agent's page, or **Fork from here** under a reply. Browser tokens cannot fork. ## The result is the truth; events are for display An agent's **events** stream every step: text as the model writes it, tool calls and their progress, inputs asked and answered. Use them to show the agent working. Do not use them to decide what happened. - The run's result (`run()`, the webhook `run.completed`, or `GET /v1/agents/:id/requests/:id`) is recorded durably and always arrives, even if your process restarts or the connection drops. - The stream is replayed from memory on reconnect, as far as it goes. Where it cannot replay (a restarted node, a long disconnection), the SDK gets a snapshot of the running turn instead; streamed deltas in between are gone. So reply to your user from the run, and render the stream as progress. See the [event reference](https://run.camelai.com/docs/reference/events.md). ## Tools: where they run An agent's tools come from four kinds of place. Pick by where your code runs: | Your situation | Use | How | | --- | --- | --- | | A long-lived server or worker, and the tools touch its state | **Attached tools** | `tools` on `upsert`; the SDK holds a connection and the runtime calls your functions over it. One process serves an agent's tools at a time; closing it on SIGTERM finishes its calls, so deploys lose none | | Serverless functions, several instances, or one backend serving many users' agents | **Served tools** | `serveTools(tools)` on an HTTPS endpoint of yours, named in a **definition** with `auth: { type: "runtime" }` (or in the agent's own `mcpServers`); the runtime calls it with a signed token saying who each call is for. Safe through your deploys and restarts | | A third-party API | **OpenAPI** or **MCP** sources in a definition | the runtime calls the API itself, with credentials it stores sealed | | Web search, fetching pages, scheduling, asking the user | **Built-ins**, on the agent or in its definition | `builtins: ["web_search", "web_fetch", "schedule", "ask_user"]` | | Other agents doing tasks for this one | The **`delegate`** built-in, with the agents it may reach | `delegate: { agents: ["researcher"] }`. See [Multi-agent](https://run.camelai.com/docs/guides/multi-agent.md) | The same `tool({...})` definitions work attached and served, so you can start attached and move to served without changing a tool (see [Several processes, workers and deploys](https://run.camelai.com/docs/guides/tools.md#several-processes-workers-and-deploys)). Every agent also has file tools over its [files](https://run.camelai.com/docs/guides/files.md), and `js_exec`, a sandbox where the model writes code that calls all of its tools. See [Tools](https://run.camelai.com/docs/guides/tools.md). ## Processes and connections A process that has an agent's tools **attaches** to it: it holds the agent's event stream and answers its tool calls. One process at a time does, so a tool call never lands in the wrong process. A second process that tries gets `APPLICATION_CONNECTED`, unless it passes `takeover: true`; the one it replaces hears `APPLICATION_REPLACED` and goes on without serving the tools. Any number of processes can **run** an agent without serving its tools: an agent upserted without local tools, or with `attach: false`, follows the agent's stream read-only. That is how serverless functions, webhook handlers and second services run agents whose tools are served elsewhere. Such a handle connects lazily: it holds the stream only while `agent.stream()` reads a run (or an `onEvent` handler listens), and `agent.run()` asks for its outcome instead, so a server holding many agents holds no idle connections. A run of an agent with attached tools while no process serves them is refused up front, with `APPLICATION_NOT_CONNECTED` (after a few seconds' grace for a process reconnecting), rather than running without them. Pass `allowDisconnected: true` to run anyway: calls to those tools then fail, and the run lists them in `toolErrors` with code `not_connected`. Schedules, channels and resumed runs are never refused, so agents that must work with nobody attached should use served tools. A process that connects with tools other than those the agent was last given (it was restarted with changed code) declares its own, between the agent's turns. `upsert` declares them anyway. ## People in the loop A run can stop and wait for a person: the model asks a question (`ask_user`), a tool needs approval before it runs, or a tool asks for a form or a setup step. The run ends with `status: "input_required"` and its `inputs`; the agent sleeps, for days if need be; answering the last input resumes it. ```ts if (run.status === "input_required") run = await run.inputs[0].answer(true, { from: "alice" }); ``` See [Human input](https://run.camelai.com/docs/guides/human-input.md). ## Idempotency Everything that changes something can be retried safely: - `upsert` by key, and `run` / `POST /v1/agents/:id/prompt` by `idempotencyKey` (the request id): the same key returns the same agent or run. - Every other POST takes an `Idempotency-Key` header: a retry with the same key, path and body gets the first answer again (for a day); the same key with a different body is a 409. An answer with a secret shown once (an API token, including a new tenant's from `POST /v1/tenants`, a signing secret, a browser token, a rotated agent token or a signed link) is never stored: a retry of a key that succeeded is a 409, not the secret again. - Every tool call carries `context.idempotencyKey`, the same for every attempt of that call: a retry after a lost connection, or the call run again after a person answered. Key your side effects by it. (`context.callId` is new each attempt.) A tool call whose answer was lost (the connection dropped, or its deadline passed) is never sent again: the model is told its outcome is unknown, so it can check before repeating anything. ## Waking later An agent can be woken by a schedule (`agent.client.schedule({ text, inSeconds | at, everySeconds })`, or the `schedule` built-in for the agent to set its own), by a [channel](https://run.camelai.com/docs/guides/channels.md) (Slack, Telegram, Discord), or by your own code from a [webhook](https://run.camelai.com/docs/guides/webhooks.md). A repeating schedule's wake-ups are turns of the agent, so by default each one reads all the history before it, and a daily agent's context grows every day. `history: "none"` on a text schedule wakes it with only the system prompt and the text, as a prompt's `history: "none"` does: the runs are still recorded, but no wake-up pays to read the ones before it. Write the text to say everything the run needs. A wake-up the runtime cannot deliver (the agent is busy at its account's limit, say) is tried again 1, 5 and 15 minutes later, then given up; a refusal a retry cannot change, like an agent or account at its spend limit (402), is given up at once. A repeating schedule then moves to its next occurrence, and lists the failure: `failures` (occurrences given up in a row) and `lastError`, until a delivery succeeds. A one-off schedule given up is removed. ## Definitions A **definition** is a reusable configuration: model, instructions, tool sources (MCP servers, OpenAPI specs, built-ins), limits and mounts. Make agents from one (`definition: "def_…"` on `upsert`), and roll a change out to all of them with `apply: "all"`. See [Definitions](https://run.camelai.com/docs/guides/definitions.md). ## Several agents Agents can work together, inside the runtime. With the `delegate` built-in, an agent hands a task to a **sub-agent** (an agent of its own, made from a definition for the task, or an existing one) and gets its answer back as the call's result; several at once run in parallel. A sub-agent survives a lost node, and counts against its parent's limits. There are no handoffs: the conversation stays with the agent that has it. See [Multi-agent](https://run.camelai.com/docs/guides/multi-agent.md). --- # Pricing and credit camelRun is prepaid: you buy credit, and usage draws it down. There is no subscription and no per-seat or per-agent fee. `GET /v1/billing` (the console's **Billing** page) shows your balance, this month's spending by kind, and the rates below as your account is charged them. | What | Price | | --- | --- | | Model calls on the platform's keys | The provider's own reported cost, else the model catalog's price (`GET /v1/models`, `cost` per million tokens). No markup. OpenRouter models add what funding OpenRouter credit costs: ×1.055 | | Model calls on your own key (or a key scope's) | Nothing from camelRun: the provider bills you | | Agent time | $0.01 per hour an agent spends in a run (model calls and tool execution), metered continuously. An idle agent costs nothing | | Storage | $0.10 per GB-month of what your agents and volumes keep (history, files, snapshots), charged daily | | `web_search` on the platform's keys | Per search, by the provider that answered: Exa $0.007, Brave $0.005, Parallel $0.001 | | `web_fetch` page rendering on the platform's key | $0.00083 per page Firecrawl renders | | [Transcription](https://run.camelai.com/docs/guides/voice.md) on the platform's OpenAI key | $0.0045 per minute of audio (`gpt-transcribe`), billed per second | | [Images](https://run.camelai.com/docs/guides/images.md) on the platform's OpenAI key | Per token as OpenAI bills `gpt-image-2.5-flare`: $5 per million text tokens in, $8 per million image tokens in, $30 per million image tokens out. A 1024x1024 image is $0.0059 at `low`, $0.0132 at `medium`, $0.0527 at `high` | ## Credit - **Starting credit: $5**, once. GitHub accounts older than 30 days get it at sign-up. Everyone else unlocks it by verifying a card on the **Billing** page; the card is not charged. - **Buying credit:** $5 to $1,000 at a time, with a 5.5% fee on the purchase. Auto top-up can buy more when your balance runs low. - **Before your first purchase:** at most $1 of model tokens and agent time an hour, and 1 GB stored. After it, 100 GB stored, and the [usage tiers](https://run.camelai.com/docs/reference/limits.md#usage-tiers) raise how many agents may be busy at once as your purchases add up. - At zero balance, new runs and file writes get 402 `INSUFFICIENT_CREDIT`, and a running turn ends after the response that spent the last credit. Nothing is deleted: add credit and send the next message. ## Spending less - Use your own provider keys (`PUT /v1/providers/:provider/key`): model calls then cost you only what the provider charges, and camelRun charges agent time and storage. See [Models and keys](https://run.camelai.com/docs/guides/models-and-keys.md). - Bound an agent or a run with a [spend limit](https://run.camelai.com/docs/guides/models-and-keys.md#spend-limits), and see where credit goes with `GET /v1/usage` or the `usage.recorded` [webhook](https://run.camelai.com/docs/guides/webhooks.md). A self-hosted runtime charges nothing unless its operator sets up billing; see [Self-hosting](https://run.camelai.com/docs/operations/self-host.md). --- # Projects A project is something an agent builds for your users, a bot, a site, a report, that your application then runs or serves. The agent works on the files with its file tools; your application checks them and publishes versions. The pattern: 1. **A volume per project**, made once per key (`projects.create`). 2. **An agent that works in it**, with the volume mounted read-write and its own workspace beside it (`project.mount`). 3. **A publish step in your code**: snapshot the volume, read every file at that snapshot, check them, and store them where your application keeps what it serves (`project.publish`). The snapshot is the version: what you checked is exactly what you stored, whatever the agent writes meanwhile. 4. **A `publish` tool the model calls** when its work is ready (`publishTool`), which finds the project from the call's signed identity and tells the model what to fix when the check fails. ## Making a project ```ts import { Agents } from "@camelai/run"; const agents = new Agents(); const project = await agents.runtime.projects.create({ key: `bot-${bot.id}`, template: { "bot.ts": starterCode, "README.md": "# My bot\n" }, }); ``` - `key` names the project within your account: the first create makes its volume, and every later one returns the same volume, for as long as it lives (`POST /v1/volumes {key}`; `existing: true` on the later ones). Store `project.id` if you like, or create by key every time. - `template` (path to text or bytes) seeds a new project. It never overwrites a file that is there; a project with no files is seeded again, so a create cut off half-way finishes on its retry. - A deleted project's key makes no other (a 409): pick a new key. ## The agent that builds it ```ts const builder = await agents.upsert(`builder-${bot.id}`, { definition: "bot-builder", // its prompt and tools, including your publish tool subject: owner.id, context: { bot: bot.id }, ...project.mount("/bot"), }); ``` `project.mount(path)` mounts the project's volume read-write at `path`, first, so relative paths are the project's, keeps the agent's own workspace at `/workspace` beside it (uploads, tool outputs and scratch files go there, not into the project), and turns file tools on. To move an existing agent onto a project, add `remount: true` to its upsert. ## Publishing ```ts const result = await project.publish({ prefix: "/", // only these files (default: all) validate: files => check(files), // [] when nothing is wrong store: async (files, version) => saveRelease(bot.id, files, version.id), keep: 20, // published versions kept (default 20) }); if (!result.ok) console.log(formatProblems(result.problems)); ``` - `files` are `{path, size, contentType, sha256, text | data}`: `text` for UTF-8 files, base64 `data` for any other (`fileBytes(file)` gives the bytes). - `validate` returns problems, `{path?, line?, message}`. Any problem keeps the version from being published, and its snapshot is deleted. It may return `{problems, data}` instead: `data`, whatever the check worked out (a bundle, its manifest), reaches `store` as `checked` and comes back in the result (`result.checked`, and `published`'s argument in `publishTool`), so `store` does not work it out again. - `store` runs once per version. It gets the version (`{id, seq, name, createdAt}`) and `{project, identity}` (the identity when `publishTool` published it); keep `version.id` to read that version again with `project.files({ version })`. - `project.versions()` lists the published versions, oldest first. A volume keeps at most 100 snapshots, so older versions beyond `keep` are deleted. - `idempotencyKey` publishes once per key: a retried call gets the version the first one made (its check runs again only for its `data`). - `project.archive({ version?, path? })` gives a version's files (or the project as it is) as a tar.gz stream, for a build. - `publish({ pin: true, labels })` pins the version: kept until `project.unpin(version)`, past `keep` (pinned versions do not count against it), up to 10,000 of them. Pin releases your application runs, so their source is always there; `project.pin(version, labels?)` pins one later. `project.versions({ labels })` finds versions by label, e.g. `{ release: "v12" }`. - `project.restore(version)` puts the project back as a version had it, in place: the agent working in it sees the files change. The version stays published; publish again to make the restored files a new version. ## The publish tool Give the model a tool that publishes, served by your application with [`serveTools`](https://run.camelai.com/docs/guides/tools.md#served-tools-over-http-for-serverless-and-many-users) and named in the agent's definition with `auth: { type: "runtime" }`: ```ts import { publishTool } from "@camelai/run"; import { serveTools } from "@camelai/run/server"; export default { fetch: serveTools({ publish: publishTool({ // Who the call is for comes from the runtime's signed token, never from the model. project: identity => agents.runtime.projects.get(volumeOfBot(identity.context.bot)), validate: files => check(files), store: (files, version, { identity }) => saveRelease(String(identity!.context.bot), files, version.id), }), }, { runtime: "https://run.camelai.com", tenant: "acme" }), }; ``` An agent whose project changes during its life (one builder per user that moves its `/bot` mount from bot to bot with `remount: true`) has no project in its identity: its `context` was fixed when it was made. Look up what it has mounted now instead, from the agent the call came from: ```ts project: async identity => { const mount = (await agents.runtime.mounts(identity.agent)).find(entry => entry.path === "/bot"); if (!mount) throw new Error("No bot is open in this builder"); return agents.runtime.projects.get(mount.volumeId); }, ``` Check that the volume is one of the user's projects (a key or row of yours names it) before you publish it: the mount was set by your code, but the lookup is what decides which project a publish reaches. It takes no arguments. On success the model gets `{published: true, version}`; otherwise the call fails with the problems, one a line (`/bot.ts:12: …`), so the model fixes them and publishes again. A retried call publishes once: the runtime sends each call's idempotency key. A client that sends none gets a new version for every call. ## In Python `camelai_run.projects` has the same helper, async: ```python from camelai_run.projects import Projects, publish_tool projects = Projects(agents.runtime) project = await projects.create(f"bot-{bot.id}", template={"bot.py": starter}) builder = await agents.upsert(f"builder-{bot.id}", definition="bot-builder", subject=owner.id, context={"bot": bot.id}, **project.mount("/bot")) result = await project.publish(validate=check, store=lambda files, version, about: save_release(bot.id, files, version["id"])) app = serve_tools([publish_tool(project=lambda identity: projects.get(volume_of_bot(identity.context["bot"])), validate=check, store=save)], runtime=RUNTIME, tenant="acme") ``` `publish_tool` works in `serve_tools` (ASGI) and in `camelai_run.sync.serve_tools` (WSGI: Django, Flask). Files are dicts with `"text"` or base64 `"data"` (`file_bytes(file)` gives the bytes). ## Security - **Derive the project from identity.** `project(identity)` reads who the call is for from the runtime's signed token (`subject`, `context`), which you set when you made the agent and the model cannot change. Never take a project id, a path or a version from the model's arguments. - **Check what you read, not what the model says.** `publish` reads the files server-side from a snapshot; the model never hands you content. Treat every file as untrusted input: validate it, and run it only where your application runs untrusted code. - **Keep the agent to its project.** Give the builder the project's volume and its own workspace, nothing else of yours. Its own token can read and write its mounts; your API key, which reads every project, stays on your server. - **Store versions, serve versions.** Serve what `store` kept for a published version, never the live volume, which the agent may be changing. ## From the REST API | | | | --- | --- | | make or find a project's volume | `POST /v1/volumes {name, key}` | | mount it beside the workspace | `mounts: [{volumeId, path: "/bot", mode: "rw"}, {workspace: true}]` on `POST /v1/agents` (`remount: true` on an upsert to change them) | | take a version | `POST /v1/volumes/:id/snapshots {name}` | | read every file of it at once | `GET /v1/volumes/:id/files?content=true&snapshot=snap_…` | | list and delete versions | `GET /v1/volumes/:id/snapshots`, `DELETE /v1/volumes/:id/snapshots/:snapshot` | | restore a version in place | `POST /v1/volumes/:id/restore {snapshot}` | | pin a version, label it | `PATCH /v1/volumes/:id/snapshots/:snapshot {pinned, labels}`, or `pinned`/`labels` on the create | | bring in a version kept elsewhere | `POST /v1/volumes/:id/snapshots {name, files, pinned, labels}` | | download a version as a tar.gz | `GET /v1/volumes/:id/archive?snapshot=snap_…` | --- # Tools An agent's tools are how it acts. This guide covers where they run, how to write them so a retry never does harm, and how the runtime tells a tool who a call is for. For the short version of where tools should run, see the table in [Concepts](https://run.camelai.com/docs/concepts.md#tools-where-they-run). ## Writing a tool ```ts import { schema, tool } from "@camelai/run"; const refund = tool({ description: "Refund an order in full", input: schema.Object({ orderId: schema.String() }), timeoutMs: 60_000, execute: async ({ orderId }, context) => { context.progress("Contacting the payment provider"); return payments.refund(orderId, { idempotencyKey: context.idempotencyKey, signal: context.signal }); }, }); ``` ```python from camelai_run import ToolContext, tool @tool(timeout=60) async def refund(order_id: str, context: ToolContext) -> dict: """Refund an order in full""" context.progress("Contacting the payment provider") return await payments.refund(order_id, idempotency_key=context.idempotency_key) ``` - **Input.** TypeScript takes a [TypeBox](https://github.com/sinclairzx81/typebox) schema (`schema` is TypeBox's `Type`), and infers the arguments' type from it. Python derives the schema from the annotations (`str`, `int`, `float`, `bool`, `list`, `dict`) and the description from the docstring. Arguments are checked against the schema before your function runs. - **Result.** Return any JSON value; `undefined`/`None` is sent as `null`. The model reads it as JSON text, and code in `js_exec` gets the value. Throw (raise) to tell the model the call failed: it sees your message. A result is at most 1 MiB of JSON. The model reads at most 32,000 characters of a tool's result; a longer one is saved whole (up to 700 KB) to `/workspace/tool-results/.txt`, where it can read the rest. - **Python functions** may be `async` or plain; a plain one runs in a thread, so it never blocks the connection. - **`context.idempotencyKey`** is the same for every attempt of one call: a retry after a lost connection, or the call run again once a person answered. Pass it to anything with side effects. `context.callId` is new on each attempt. - **Deadline.** A call may go 15 seconds without answering (`timeoutMs`, `@tool(timeout=)`: 1 s to 20 minutes). Each `context.progress(...)` restarts the wait, up to 20 minutes in all, and people watching the agent see the progress. A call cut off is reported to the model as "outcome unknown" (it may still finish in your process): make the side effect idempotent and the model can check and retry safely. - **`context.signal`** aborts when the runtime cancels the call (the run was aborted, or the deadline passed). Cancellation is not rollback. - **`context.identity`** says who the call is for; see [Identity](https://run.camelai.com/docs/guides/tools.md#identity-who-a-call-is-for). - **`needsApproval: true`** (`needs_approval=True`) makes a person approve each call first, and `context.confirm/ask/requireUrl` ask from inside the tool; see [Human input](https://run.camelai.com/docs/guides/human-input.md). - **`exposure`** (`@tool(exposure=...)` in Python): `"direct"` (the model calls it as a tool), `"codemode"` (only from code in `js_exec`, as `tools.(args)`), or `"both"`. By default a source of up to 10 tools is `both` and a larger one `codemode`, so big catalogs do not crowd the model's context. **So by default the model can call your tools from `js_exec`**, your attached and served tools included, and it may well batch them in code. See [Keeping tools out of js_exec](https://run.camelai.com/docs/guides/tools.md#keeping-tools-out-of-js_exec). ### Keeping tools out of js_exec Code the model writes in `js_exec` can call every tool whose exposure is `codemode` or `both`. That is what makes large catalogs cheap (the model loops, filters and calls several tools in one step), but it also means a tool your app expected to see called on its own may be called from code instead: its call is then inside `js_exec`'s, its arguments do not stream as `toolcall_delta` events, and a page that renders a tool's arguments as they are written sees nothing until the code has run. To keep a tool out of `js_exec`, make it `direct`: | Where the tool comes from | Set | |---|---| | Your process (`tools` in `upsert`) | `exposure: "direct"` on the tool; Python `@tool(exposure="direct")` | | A served or MCP server named in a definition | `"exposure": "direct"` on the server entry (every tool), or `_meta["agent-runtime/exposure"]` on a tool in its `tools/list` (that tool; it beats the server's) | | An OpenAPI source | `"exposure": "direct"` on the source | A tool with `needsApproval` is always direct. The console's agent page shows each tool's exposure (**Tools the model gets**), and `GET /v1/agents/:id` its `toolSources`. `js_exec` itself stays: it still runs code, with `fs` and `fetch`, and the tools left in it. ## Attached tools: in your process Pass `tools` to `upsert` and this process serves them to the agent: ```ts const agent = await agents.upsert("ops", { model, instructions, tools: { refund } }); ``` The SDK holds the agent's event stream; the runtime sends each tool call over it and the SDK posts the result back. Your tools run with your process's state and credentials, and nothing is exposed on the network. The trade-offs: - One process at a time serves an agent's tools. Another process that upserts with the same tools gets `APPLICATION_CONNECTED` (pass `takeover: true` to replace the first, or `attach: false` to run the agent without serving them). - A run while no process serves the tools is refused with `APPLICATION_NOT_CONNECTED`, unless you pass `allowDisconnected: true` (`allow_disconnected=True`): then calls to them fail, listed in the run's `toolErrors` with code `not_connected`. Runs a schedule or a channel starts are never refused and may find nobody connected: use served tools for those agents. - If the connection drops while a call runs, its outcome is unknown to the runtime, and the model is told so; the call is never sent again. An existing MCP server (from `@modelcontextprotocol/sdk`) can be attached as is: `upsert(key, { mcp: await fromMcpServer(server) })` from `@camelai/run/mcp`. ### Several processes, workers and deploys Every process that upserts an agent with `tools` tries to serve them, and only one can. Under uvicorn or gunicorn with several workers, Celery, or any fleet of instances, the first to connect serves the tools and the others fail with `APPLICATION_CONNECTED` (or take them from each other, with `takeover`). Pick one of these: - **Served tools (recommended for fleets).** Serve the tools over HTTP from your web app (`serveTools` / `serve_tools`, below) and name the server in a definition. Every process, web worker and task then upserts the agent from the definition with no `tools`, and any of them can run it. Each call is an HTTP request of its own to whichever instance is up, so a rolling deploy that drains its connections loses no call. - **One tool process.** Run one long-lived process that upserts the agent with its `tools`. Everywhere else, upsert it with the same `tools` and `attach: false` (`attach=False`), which runs the agent without serving them. #### Deploying a tool process Close the agents when the process is told to stop, and a deploy loses no call: ```ts process.once("SIGTERM", () => void agents.close().then(() => process.exit(0))); ``` ```python from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app): yield await agents.close() # uvicorn runs this on SIGTERM app = FastAPI(lifespan=lifespan) ``` `close()` tells the runtime this process takes no new calls, finishes the calls it is running (for up to 25 seconds: `close({ drainMs })`, `close(drain=)` in seconds), then disconnects. Meanwhile new calls go to the next process: in a rolling deploy, start it with `takeover: true` (`takeover=True`), which takes the tools at once while the old process finishes its calls; in a stop-then-start deploy, it connects without a takeover, since a closing process holds the tools no longer, and calls wait a few seconds for it. A takeover alone (without `close()`) also lets the replaced process answer the calls it has, for up to 30 seconds. What a deploy can still lose: a call that outlives the drain, and every call running when a process crashes or is killed without `close()`. The model is then told, in words it can pass on, that the tool's server disconnected during the call and it may or may not have taken effect; the run lists it in `toolErrors` (Python `tool_errors`) with code `connection_lost`. Key side effects by `context.idempotencyKey` so that checking and retrying is safe. #### When the tool process dies mid-run The run lives in the runtime, not in your process, so it goes on without it: - **The call that was running** fails as `connection_lost`, as above. The model is told, and usually checks or tries again. - **A call while no process serves the tools** waits 3 seconds for one to connect, then fails as `not_connected` (it did not run). - **The restarted process serves the run's next calls.** Once it upserts the agent with its `tools`, it serves every call that comes, including calls of runs a process before it started. A run is not tied to the process that sent it. - **The `run()` the dead process awaited is gone, but its run is not.** Give each run an id of yours (`idempotencyKey`), saved before you send it, e.g. the job or the order it is for. After the restart, `run()` with the same text and the same key answers with that run instead of starting another: its result if it has ended, or once it ends if not. Without an SDK, `GET /v1/agents/:id/requests/?wait=25` answers the same, and `GET /v1/agents/:id/state` lists the agent's runs. Check `toolErrors` even when the run's `status` is `completed`: a run whose call was cut off often ends normally, after the model worked around it. ```ts // On startup (and after a crash): serve the tools, then collect the job's run. const agent = await agents.upsert("ops", { model, instructions, tools: { refund } }); const run = await agent.run(`Refund order ${job.orderId}`, { idempotencyKey: job.id }); const lost = run.toolErrors.filter(error => error.code === "connection_lost"); if (lost.length) console.warn(`check whether ${lost.map(error => error.tool).join(", ")} took effect`); console.log(run.status, run.text, run.toolCalls.map(call => call.tool)); ``` ```python # On startup (and after a crash): serve the tools, then collect the job's run. agent = await agents.upsert("ops", model=model, instructions=instructions, tools=[refund]) run = await agent.run(f"Refund order {job.order_id}", idempotency_key=job.id) lost = [error for error in run.tool_errors if error["code"] == "connection_lost"] if lost: print("check whether", ", ".join(error["tool"] for error in lost), "took effect") print(run.status, run.text, [call["tool"] for call in run.tool_calls]) ``` ## Served tools: over HTTP, for serverless and many users A server of yours answers tool calls over HTTP (MCP's Streamable HTTP), and a definition tells the runtime to call it. Any number of instances can serve, nothing needs to stay connected, and the runtime signs a token for each call saying which agent it is for, whom that agent acts for, and who is acting. ```ts import { serveTools } from "@camelai/run/server"; // A fetch handler: Cloudflare Workers, Bun and Deno serve it as is. // tenant: your tenant's id (GET /v1/me, or `await agents.runtime.me()`): tokens for other tenants' agents are refused. export default { fetch: serveTools({ refund }, { runtime: "https://run.camelai.com", tenant: "acme" }) }; ``` On Node, wrap it: `createServer(nodeListener(handler, { origin: "https://tools.example.com" }))` with `nodeListener` from `@camelai/run/node`. Tokens are checked against the URL the runtime called, so set `origin` to your public URL. Without it, the URL is the server's own (its socket's scheme and the Host header), which is wrong behind a load balancer or proxy that ends TLS. `trustProxy: true` reads `X-Forwarded-Proto` and `X-Forwarded-Host` instead, but only set it where the proxy overwrites those headers: otherwise any client could choose the URL tokens are checked against. ```python from camelai_run import serve_tools app = serve_tools([refund], runtime="https://run.camelai.com", tenant="acme") # ASGI: uvicorn, or mount in FastAPI ``` Behind a proxy, run uvicorn with `--proxy-headers` (and `--forwarded-allow-ips` naming the proxy), or pass `audience="https://tools.example.com/mcp"`. To offer each agent its own tools (an admin's, a plan's), pass a function of the caller's identity instead of the tools; a tool it is not given is refused if called. A `ToolServer` of your own gets the same as `listTools(context)`: `{identity, origin, signal}`. ```ts serveTools(identity => identity.context.role === "admin" ? { refund, ban } : { refund }, options); ``` ```python serve_tools(lambda identity: [refund, ban] if identity.context.get("role") == "admin" else [refund], runtime=..., tenant=...) ``` ### A server that moves By default a token's audience is the server's URL, so a server that moves (a new domain or path) refuses the tokens of agents still calling the old URL. Two ways to move without that: - Give the server a name that does not change: `audience: "urn:camelrun::"` in the definition's server entry (your tenant's id, as `GET /v1/me` shows it, then a name of letters, digits and `._~:-`), and the same `audience` to `serveTools`. Tokens name it wherever the server is. Only your tenant's sources can name an audience in your tenant's namespace. - Accept both addresses while agents move: `audience` takes a list, e.g. `["https://old.example.com/mcp", "https://new.example.com/mcp"]`. Agents keep the definition revision they were made from, so after changing the server's `url`, apply the new revision to them (`apply: "all"`, or `applyOnUpdate: true` on the definition; see [Definitions](https://run.camelai.com/docs/guides/definitions.md)). ```ts // Definition: { name: "shop", url: "https://tools.example.com/mcp", auth: { type: "runtime" }, audience: "urn:camelrun:acme:shop" } export default { fetch: serveTools({ refund }, { runtime: "https://run.camelai.com", tenant: "acme", audience: "urn:camelrun:acme:shop" }) }; ``` For Django, Flask or any WSGI server, `camelai_run.sync.serve_tools` takes the same arguments and is a WSGI app. Plain functions run in the request's thread, with its database connection; async ones run in an event loop of the request's own. Mount it at the path the definition names: ```python # Flask from werkzeug.middleware.dispatcher import DispatcherMiddleware from camelai_run.sync import serve_tools app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/mcp": serve_tools([refund], runtime="https://run.camelai.com", tenant="acme")}) ``` ```python # Django: wsgi.py from django.core.wsgi import get_wsgi_application from camelai_run.sync import serve_tools django_app = get_wsgi_application() tools_app = serve_tools([refund], runtime="https://run.camelai.com", tenant="acme") def application(environ, start_response): path = environ.get("PATH_INFO", "") if path == "/mcp" or path.startswith("/.well-known/oauth-protected-resource"): return tools_app(environ, start_response) return django_app(environ, start_response) ``` Behind a proxy, pass `audience="https://tools.example.com/mcp"`: the URL a WSGI app sees is the server's own. `camelai_run.sync.verify_runtime_token` checks a token in a view of your own. (`pip install "camelai-run[server]"` for the token checks.) Then name the server in a definition, and make agents from it: ```ts const definition = await agents.runtime.upsertDefinition("support", { name: "Support", mcpServers: [{ name: "shop", url: "https://tools.example.com/mcp", auth: { type: "runtime" } }], }); const agent = await agents.upsert(`user-${user.id}`, { definition: definition.id, subject: user.id, context: { org: user.orgId } }); ``` Every request without a valid token for your server gets a 401: the signature (the runtime's published Ed25519 keys), the issuer, the **tenant** (yours), the audience (your server's URL as the runtime calls it) and the expiry are checked, and nothing per user is stored anywhere. `tenant` is required, and it matters: an identity means something only within your own tenant. Any tenant can make agents, give them any `subject` and `context`, and point them at your server's URL; the runtime signs their tokens too. Only the tenant check tells your agents from theirs. Pass your tenant's id (or a list, if several of your tenants share the server). To test your authorization without a runtime, `testRuntime()` (`@camelai/run/testing`; `TestRuntime()` in Python, and `camelai_run.sync.TestRuntime()` for a WSGI app) signs tokens with a key of its own: ```ts const rt = await testRuntime(); const result = await rt.callTool(serveTools(tools, rt.options), "https://app.test/mcp", "refund", { orderId: "o1" }, { subject: "alice" }); ``` Built with the MCP SDK or Cloudflare's `createMcpHandler` instead? Verify with `runtimeAuth(request, { runtime, tenant })`, pass the result as the request's `auth`, and read `runtimeIdentity(extra)` in a handler. `verifyRuntimeToken(token, { runtime, tenant, audience })` checks a token on its own. `testRuntime()`'s tokens name tenant `test`, which `rt.options` passes. ## Identity: who a call is for Every tool call carries `context.identity`, from the signed token for served tools and from the call itself for attached ones, so a tool reads it the same way either way: | field | | | --- | --- | | `user` | whom to authorize as: the turn's actor, else the agent's subject | | `subject` | whom the agent acts for: `subject` given when it was made (its id if none) | | `actor` | who is acting in this turn: the run's `user` (`from.id`), or an `actor` given with the message | | `context` | claims given when the agent was made (`context: { org, workspace }`) | | `tenant`, `agent`, `definition` | whose agent it is, and which (`definition` only for an agent made from one) | | `origin` | where the turn came from: a channel, its conversation and sender | | `approval` | the person's approval, for a call that needed one | `subject` and `context` are set with your API key when the agent is made, and the agent's own token cannot change them; the model cannot touch any of it. So, once the token is checked to be from your own tenant (served tools), a tool can trust `identity`, and should never take a user id from the model's arguments. Another tenant's agents can claim any `subject`: `identity` is only meaningful within your tenant. See [Many users](https://run.camelai.com/docs/guides/multi-user.md). The token itself (for servers that verify it by hand, e.g. with `jose`): ```json { "iss": "https://agents.camelai.dev", "aud": "https://tools.example.com/mcp", "sub": "u_123", "tenant": "acme", "agent": "client_…", "definition": "def_…", "ctx": { "org": "acme" }, "act": "u_456", "origin": { … }, "req": "run_…", "tcid": "toolu_…", "par": "client_…", "root": "client_…", "iat": 1790000000, "exp": 1790000120, "jti": "…" } ``` ```ts const jwks = createRemoteJWKSet(new URL("https://run.camelai.com/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { issuer: "https://agents.camelai.dev", audience: "https://tools.example.com/mcp", algorithms: ["EdDSA"] }); // Any tenant's agent can get a token for your server's URL: accept only your own tenant's. if (payload.tenant !== "acme") throw new Error("Not our tenant's agent"); ``` - `tenant` must be your own (your tenant id, as `GET /v1/me` shows it): the signature, issuer and audience prove only that the runtime issued the token to some agent for your server's URL, and anyone can make an agent. - `iss` is the hosted runtime's first address, `https://agents.camelai.dev`, at either of its URLs (a self-hosted runtime's is its `AGENT_ISSUER`, else its `AGENT_PUBLIC_URL`). The SDKs expect it for either hosted URL. - `aud` is the server's URL, or the source's `audience`: another URL on the server's origin, or a name in your tenant's namespace, `urn:camelrun::` (see [A server that moves](https://run.camelai.com/docs/guides/tools.md#a-server-that-moves)). The runtime mints no token for another origin's URL or another tenant's name, so a token cannot be replayed against a server that is not yours. Tokens live two minutes, and each request gets its own `jti`. - `act` and `origin` are absent outside a turn (listing a server's tools as an agent starts), and so are `req`, the run's request id, and `tcid`, the model's tool call (`identity.requestId` and `toolCallId` in the SDKs). - `par` and `root` are set only in a [sub-agent](https://run.camelai.com/docs/guides/multi-agent.md)'s run: the agent that started it and the first agent of its chain, from the run's chain as the runtime signed it (`identity.parentAgentId` and `rootAgentId` in the SDKs). Authorize a sub-agent's call by `root` when only the first agent's user is yours. - The keys are at `/.well-known/jwks.json` (cache them for minutes); `/.well-known/oauth-authorization-server` names the issuer for MCP clients. ## Definitions' sources A [definition](https://run.camelai.com/docs/guides/definitions.md) lists tool sources the runtime calls itself. An agent without a definition can also have [MCP servers of its own](https://run.camelai.com/docs/guides/tools.md#an-agents-own-mcp-servers), without credentials. ### MCP servers ```json {"name": "Support", "mcpServers": [{ "name": "kb", "url": "https://mcp.example.com/mcp", "auth": {"type": "bearer", "token": "…"}, "headers": {"X-Team": "support"}, "allowTools": ["search", "fetch_article"], "exposure": "both", "timeoutMs": 30000 }]} ``` - Streamable HTTP, falling back to the older SSE transport when a server answers its POST with 400, 404 or 405. - `auth` is `{"type": "bearer", "token"}` or `{"type": "runtime"}` (signed identity tokens, above). `headers` and `auth` are stored sealed; the API returns only `headerNames` and `auth.type`. Updating a server without them keeps its stored credentials, unless its URL moved to another origin. - `fileArguments` (`"on"` or `"off"`) says whether its tools are sent the agent's files: on by default with `auth: {"type": "runtime"}` only. See [Files in tool calls](https://run.camelai.com/docs/guides/tools.md#files-in-tool-calls). - Tools reach the model as `__`, filtered by `allowTools` and `denyTools`. A server can set a tool's own exposure in `tools/list` with `_meta["agent-runtime/exposure"]`, and its deadline with `_meta["agent-runtime/timeoutMs"]`. - Each call has a deadline (`timeoutMs`, default 60 s, at most 20 minutes). It limits silence: each progress notification restarts it, up to 20 minutes in all. A call from `js_exec` also ends with its execution (60 s at most), so expose long tools directly. - Text reaches the model as it is; images, audio, blobs and text resources over 64 KiB are saved to the agent's workspace and reach it as files. `isError` becomes a tool error; `structuredContent` is what code gets. - The runtime lists each server when you save the definition: one that refuses its credentials is a 400 saying so, and the answer's `toolSources` shows what each server offers (`status: listed`, or `error` with why). A server that cannot be reached when an agent starts contributes no tools that run; the run's `sourceErrors` lists it. - Tool lists are cached for five minutes and dropped on `notifications/tools/list_changed`; a running agent takes a changed list at its next start. - Progress a server reports for a call (`notifications/progress`) reaches the agent's events as a `tool_execution_update`, at most one per call every 250 ms. Each call carries `_meta` for the server: `agent-runtime/idempotencyKey` (stable across attempts), `agent-runtime/callId`, `agent-runtime/toolCallId` (and `agent-runtime/innerCallId` for a call from code), `agent-runtime/actor`, `agent-runtime/origin`, and `camelrun/files` when files are sent. ### OpenAPI specs ```json {"name": "Support", "openApi": [{ "name": "shop", "spec": "https://api.example.com/openapi.json", "auth": {"type": "bearer", "token": "…"}, "allowTools": ["listOrders", "getOrder", "refundOrder"] }]} ``` Every operation of an OpenAPI 3 spec (JSON or YAML) is a tool: - Named `__` (or `___`). Its input is the operation's path, query and header parameters by name, plus `body` for the request body (JSON, or form-encoded with nested values in brackets, as Stripe reads them). A `multipart/form-data` body's file fields, and a binary body, take `{"$file": path}` and stream the file, when the source's `fileArguments` is on (as for MCP servers). - The spec is fetched and checked when the definition is saved, and its operations (after `allowTools`/`denyTools`, at most 1024) are stored with it: an agent's tools never change under it. Save the definition again to take a spec's changes. `spec` may also be the document itself. - Requests go to `baseUrl` (default the spec's first server), with no redirects, `timeoutMs` (default 30 s, at most 20 minutes), and each call's stable key as an `Idempotency-Key` header. A 2xx answer is the result (JSON, text, or a file saved to the workspace); any other status is a tool error quoting the method, path, status and the start of the body. ## An agent's own MCP servers An agent made without a definition, and a [stateless run](https://run.camelai.com/docs/guides/stateless-runs.md), can list MCP servers itself, with no definition: ```ts const agent = await agents.upsert("support", { instructions: "…", mcpServers: [{ name: "kb", url: "https://mcp.example.com/mcp", auth: { type: "runtime" } }], }); ``` ```bash curl -X POST "$BASE/v1/agents" -H "Authorization: Bearer $KEY" -H "Idempotency-Key: support" \ -d '{"mcpServers": [{"name": "kb", "url": "https://mcp.example.com/mcp", "auth": {"type": "runtime"}}]}' ``` - They take no credentials: `auth` is `{"type": "runtime"}` or left out, and there are no `headers`. A server with a token or headers is a 400 saying to use a definition. - Why: a definition seals its credentials once, for every agent made from it. Credentials on each agent or run would be secrets stored, copied and rotated per agent. Identity tokens need none: the runtime signs one for each request, naming the agent, its `subject` and `context` (and no `definition`). - Everything else is as in a definition: `allowTools`, `denyTools`, `exposure`, `timeoutMs`, `audience`, `approval`, and the same outbound checks. - They are not listed when saved. A server that cannot be listed when the agent starts shows in the run's `sourceErrors`. - `PATCH /v1/agents/:id/configuration` with `mcpServers` replaces the list; `null` or `[]` removes it. An upsert sets the whole list, so an upsert without `mcpServers` leaves none. They count toward the upsert's `configHash`. - Only your API key changes them, never the agent's own token. An agent made from a definition has its definition's servers and refuses its own (400). - Only MCP servers: OpenAPI specs are fetched and stored when saved, so they stay in definitions. ## Built-ins Tools the runtime answers itself, given to an agent as `builtins` when it is made or upserted (`agents.upsert("researcher", { builtins: ["web_search", "web_fetch"] })`, or `PATCH /v1/agents/:id/configuration`), or by the definition it is made from (whose builtins it then has): `"builtins": ["web_fetch", "web_search", "schedule", "ask_user", "delegate", "generate_image", "agents"]`. - `web_fetch` (`{url, maxCharacters?}`) reads a public page as text (HTML reduced to readable text, 20,000 characters by default, at most 100,000), or saves any other content (a PDF, an image) to the workspace. Pages that are only a JavaScript shell are rendered when a Firecrawl key is available. - `web_search` (`{query, count?, freshness?}`) returns `{query, provider, results: [{title, url, date?, content | snippet}]}`, trying search providers in order (Exa, Brave, Parallel; a definition can pin its own with `"webSearch": {"providers": ["brave"]}`). Your own key for a provider (`PUT /v1/providers//key`) is used first; otherwise the platform's, charged per search on prepaid credit. Saving a definition or an agent with `web_search` when none of its providers has a key answers with `warnings` saying which keys would set it up, and the console shows them; a search then fails with the same text ("Web search isn't available for this account: add an Exa, Brave or Parallel key under Models & keys"). - `schedule`, `list_schedules`, `cancel_schedule` let the agent manage its own wake-ups (100 per agent, at most a year ahead, repeats at least a minute apart). - `ask_user` lets the model ask the user 1 to 4 multiple-choice questions; the run waits for the answer. See [Human input](https://run.camelai.com/docs/guides/human-input.md). - `delegate` (with `delegate: { agents }`) hands a task to a sub-agent and returns its answer. See [Multi-agent](https://run.camelai.com/docs/guides/multi-agent.md). - `generate_image` (`{prompt, images?, size?, quality?, format?, background?}`) makes an image, or edits images from the agent's files (`images`: their paths), saves it to the workspace under `tool-outputs/generate_image/` and shows it to the model; the model presents it to the user with `present_file`. It uses OpenAI's `gpt-image-2.5-flare` on your OpenAI key (or a key scope's), else the platform's, charged per token on prepaid credit, and counts against the agent's and the run's spend limits. Saving a definition or an agent with it and no OpenAI key answers with a warning. See [Images](https://run.camelai.com/docs/guides/images.md#in-an-agent). - `agents` (with the same settings) starts sub-agents in the background (`spawn_agent`, `wait_agent`, `list_agents`); each one's answer arrives as a notification. See [Background sub-agents](https://run.camelai.com/docs/guides/multi-agent.md#background-sub-agents). ## Seeing an agent's tools `GET /v1/agents/:id` returns `toolSources` (SDK: `agents.runtime.toolSources(agentId)`, Python `tool_sources`): every source in order of precedence (the channel's, the attached application's, file tools, then the definition's), each with its `status` and `tools`, and for each tool why the model does not get it, if it does not (an earlier source has its name, or the catalog is full). `?schemas=true` adds input schemas; `?refresh=true` lists every MCP server now, which is how to check one before the agent runs. The console shows the same on an agent's Configuration tab. ## Tools from code: `js_exec` The model can write JavaScript that calls any of its tools, in a sandbox (a bare V8 isolate in a process of its own) that can reach nothing else: `await tools.shop__getOrder({ id })`, `Promise.all` for parallel calls, `fs` over its files, `tools.search(query)` to find tools in a large catalog, `tools.describe(name)` for a schema. Code runs for at most 60 seconds, 2 s of CPU and 256 tool calls; see [Limits](https://run.camelai.com/docs/reference/limits.md). `agent.client.execute(code)` runs code yourself, outside the model's history, which is handy for testing tools. ### Without code: `codeMode: false` An agent made with `codeMode: false` (Python `code_mode=False`, in a definition too) has no `js_exec`. The model calls every tool directly, whatever its `exposure`, and the runtime's part of the system prompt covers only the tools it has, with nothing about code. With `fileTools: false` as well and no tools of any source, it has no tools at all (not even `present_file`), and the system prompt is your instructions plus a short note on who sent each message: for an agent that answers in a line, a classifier or a judge, a request goes from about 2,000 input tokens to a few hundred. ```ts const voice = await agents.upsert("voice-12", { instructions, codeMode: false, fileTools: false }); ``` Set it when the agent is made. Changed later by an upsert, the agent's tools change at once, while a conversation already under way keeps the runtime text it began with (its prompt's cached prefix). ## Tool search Code finds tools with `tools.search(query)` (or `tools.search(query, { namespace, limit })`): the best matches as `{name, description, input}`, most relevant first (20 by default, at most 128). `tools.namespaces()` lists the sources with their tool counts. Only tool names enter the sandbox, so a catalog of thousands of tools costs a script nothing until it asks. Ranking is by keywords, and, on the hosted runtime, also by meaning (embeddings, then a model that drops tools that cannot do what is asked); searches ranked by meaning are billed at cost. ## Files in tool calls Tools of a definition's sources take and return files without their bytes passing through the model. The model names a file by its path, as `{"$file": "/workspace/report.pdf"}`, and the runtime sends it the way the tool's schema asks. ### Marking a parameter A string parameter takes a file when it is marked as MCP's file inputs proposal (SEP-2631, a draft) marks one: ```json {"type": "object", "properties": { "attachment": {"type": "string", "format": "uri", "x-mcp-file": {"accept": ["application/pdf", "image/*"], "maxSize": 10485760, "transferModes": ["url", "inline"]}}, "site": {"type": "string", "format": "uri", "x-mcp-file": {}, "x-camelrun-directory": true} }} ``` - `accept`: the MIME types it takes (`image/*` and `*/*` work). Others are a tool error. - `maxSize`: the most bytes it takes. A larger file is a tool error. - `transferModes`: `url` (the default), `inline`, or both. - `x-camelrun-directory: true`: the parameter takes a directory. Unmarked parameters take files too, as before: a base64 field (`contentEncoding: base64`, OpenAPI's `format: byte` or `binary`) gets the content (up to 4 MiB), and a `format: uri` field, or one named like a URL (`source_url`), gets a URL. An OpenAPI multipart or binary body streams the file. The model sees `{"$file": path}` offered beside the field, with what the tool takes. ### What the tool receives - **A URL**, `https:///v1/files//`. It answers `GET` (with `Range`) with the file, and nothing else. - **Inline**, when `transferModes` has `inline` and the file is at most 4 MiB: a `data:;base64,…` URI. With `inline` alone, a larger file is a tool error. - **A directory**: the URL of a manifest: ```json {"snapshot": "snap_…", "root": "/workspace/site", "files": [{"path": "index.html", "uri": "https://…/v1/files/…/index.html", "name": "index.html", "mimeType": "text/html", "size": 1840, "digest": {"algorithm": "sha-256", "value": "9f86d0…"}}], "archive": {"uri": "https://…/v1/files/…/site.tar.gz", "mimeType": "application/gzip"}} ``` Each file has its own URL, and `archive` is a tar.gz of them all, with paths relative to the directory. A directory has at most 1,000 files and 256 MiB. An MCP call also carries `_meta["camelrun/files"]`: each file sent as a URI, by its argument's JSON pointer, as SEP-2631 describes a file: ```json {"/attachment": {"uri": "https://…", "name": "report.pdf", "mimeType": "application/pdf", "size": 48213, "digest": {"algorithm": "sha-256", "value": "e3b0c4…"}}} ``` `digest` is the sha-256 of the bytes, in hex; files over 64 MiB have none. OpenAPI calls have no `_meta`. ### URL lifetime, binding and versions - A URL lasts 5 minutes: fetch the file while the call runs. - It is bound to one call. Its token names the tenant, the agent, the call (its idempotency key), the tool and the file. - A file's URL serves the version the call named. Once the file changes, it answers 410 ("changed since the call"), so a tool never gets a mix of versions. - A directory is sent as a snapshot made for the call. Its files read as they were then, whatever the agent writes meanwhile. The snapshot is deleted after the call, once its URLs have expired; it is not listed with the volume's snapshots and does not count toward them. - An expired or altered URL answers 403. ### Verifying a URL A tool can check that a URL came from camelRun, for the agent it expects: ```ts import { verifyFileUrl } from "@camelai/run/server"; const file = await verifyFileUrl(args.attachment, { runtime: "https://run.camelai.com", tenant: "acme", agent: expectedAgent }); // { tenant, agent, call, tool, volume, path, agentPath, kind: "file" | "manifest" | "archive", version | snapshot, exp } ``` ```python from camelai_run import verify_file_url file = await verify_file_url(arguments["attachment"], runtime="https://run.camelai.com", tenant="acme") ``` It checks that the URL is at the runtime and that its token is signed by the runtime's keys (`/.well-known/jwks.json`, the identity tokens' keys) for files, and has not expired. `camelai_run.sync.verify_file_url` is the synchronous one; `testRuntime().fileUrl()` (Python `TestRuntime().file_url()`) makes one to test with. ### Which sources get files A source's `fileArguments` says whether it is sent files: `"on"` or `"off"`. - By default it is on for sources with `auth: {"type": "runtime"}`: your own servers, which check who is calling. - It is off for every other source. Its tools are not offered `{"$file": …}`, a `$file` argument is a tool error, and nothing about files reaches it. A model talked into it could otherwise send any file the agent can read to another party's server. - Set `"fileArguments": "on"` on a third-party server you trust with the agent's files. ```json {"mcpServers": [{"name": "docs", "url": "https://mcp.example.com/mcp", "fileArguments": "on"}]} ``` ### Files a tool returns - Images, audio, blobs and long text resources are saved to `/workspace/tool-outputs///`, and the model gets a reference to each (shown natively when it is an image or PDF it can view). So are binary OpenAPI responses and `web_fetch` downloads. - For sources with `fileArguments` on, a `resource_link` to an `https:` or `data:` URI is saved too. It is fetched through the outbound guard, at most 64 MiB. The transcript keeps the file's path, never the URL. - One call may save 64 MiB, and one run 256 MiB. ## Outbound calls Every request to a URL you or the model chose (MCP servers, OpenAPI specs and APIs, `web_fetch`) goes through one guard: `https://` only, no credentials in URLs, and no private, loopback, link-local or otherwise internal address, checked on every connection (so DNS rebinding cannot slip past). MCP servers and API calls get no redirects; where redirects are followed (`web_fetch`, spec downloads) each hop is checked, and credentials never go to another origin. A self-hosted runtime's operator can allow exact origins of their own (`AGENT_OUTBOUND_ALLOW_ORIGINS`) for MCP servers, APIs and model providers on their network; `web_fetch` never reaches them. --- # Human input A run can stop and wait for a person: the model asks a question, a tool needs approval before it runs, or a tool asks for a form or a setup step. The call that needs the person stays open and the run **suspends**: it ends with `status: "input_required"` and the `inputs` it waits on, the agent goes to sleep (nothing is billed while it waits), and once the last input is answered, minutes or days later and from any process, the turn resumes. The model sees an ordinary tool call and result, told who answered and how long it took. ```ts let run = await agent.run("Delete the staging database", { user: "alice" }); while (run.status === "input_required") { const input = run.inputs[0]; console.log(input.kind, input.message); // "approval", "Allow db__drop to run?" run = await input.answer(true, { from: "alice" }); // resolves with the resumed run } console.log(run.text); ``` ```python run = await agent.run("Delete the staging database", user="alice") while run.status == "input_required": run = await run.inputs[0].answer(True, from_="alice") print(run.text) ``` ## Where inputs come from - **Approvals.** A tool with `needsApproval: true` (`@tool(needs_approval=True)`), or a function of its arguments deciding per call, is approved before each call. A definition's sources take an approval policy: `"approval": {"default": "never" | "always" | "destructive", "tools": {"delete_repo": "always"}}` (`destructive`: MCP tools annotated `destructiveHint`, OpenAPI operations other than GET, HEAD and OPTIONS; OpenAPI also takes `"methods": ["POST", "DELETE"]`). The person sees the real call: the tool, its source and its arguments. An approved call runs with exactly those arguments, and carries the approval (`context.identity.approval`), so a tool can require it; a declined call never runs. - **Questions.** The `ask_user` built-in (`"builtins": ["ask_user"]` on the agent or its definition) lets the model ask 1 to 4 multiple-choice questions, with free text allowed where it says so. - **The tool itself.** `context.confirm(message)`, `context.ask(message, schema)` (a flat form) and `context.requireUrl(url, message)` (a step on an https page: connect an account, sign something). An ask ends the call; once the person answers, the runtime calls the tool again with the same arguments and the ask returns the answer. **Everything before an ask runs again then**, so ask first and act after, and key side effects by `context.idempotencyKey`, which is the same on both calls. ```ts const deleteApp = tool({ description: "Delete an app", input: schema.Object({ app: schema.String() }), execute: async ({ app }, context) => { if (!await context.confirm(`Delete ${app}? Its URL stops working.`)) return { cancelled: true }; return apps.delete(app, { idempotencyKey: context.idempotencyKey }); }, }); ``` Code in `js_exec` cannot wait for days: a call from code that would ask fails with "needs the user's input: call it directly". Tools that ask are therefore always offered to the model directly. ## Answering `input.answer(value, { from })` takes a plain value by the input's `kind`: | kind | value | | --- | --- | | `approval` | `true` to approve, `false` to decline | | `question` | the chosen option's label (or labels, or free text where allowed); for several questions, `{ "": answer, … }` | | `form` | the fields, as an object (checked against the form's schema); a confirmation (`context.confirm`, a form without fields): `true` or `false` | | `url` | `true` once the person has done what the page asks, else `false` | `input.decline()` declines any input. Answering resolves with the resumed run, or, when the run waits on other inputs too, with the run still `input_required` listing those. Answering the same way twice is safe; a different answer to an input already settled is a 409 that says how it settled. Inputs outlive your process: answer later from anywhere. - `agent.pendingInputs()` (Python `pending_inputs()`) lists an agent's waiting inputs, each with `answer()`. - `agents.runtime.inbox("pending")` lists the inputs waiting across all your agents, for an approvals queue. - Over REST: `GET /v1/agents/:id/inputs?state=pending`, and `POST /v1/agents/:id/inputs/:inputId {action: "accept" | "decline" | "cancel", content?, from?}`. A question's `content` is `{answers: {"": "