← ALL POSTS

Engineering · Oct 21, 2026 · 10 min read

How to Deploy AI Agents with Persistent State

IR
Isabella Reed
COO & Growth, camelAI
How to Deploy AI Agents with Persistent State

An agent that works on your laptop is a loop: call the model, run the tools it asks for, repeat. Deploying it means deciding where that loop runs, where its state lives, and what happens when a server goes away in the middle of a turn.

This guide covers what you take on when you deploy AI agents, then walks through a deployment on camelRun, our hosted AI agent runtime. The checklist at the end applies wherever you host.

camelRun hosts the agent loop and state. These examples run your application-side tools locally; production still needs a host for those tools. The guide shows where that boundary changes when you add workers.

About the examples. Code is taken or adapted from the camelRun documentation, and every name and option in it was checked against the SDK source (@camelai/run 0.22.1, camelai-run 0.18.1) on October 21, 2026. Step 1 contains complete scripts; Step 2 explains how to rerun them. Later snippets are fragments, labeled as such, with links to the full recipes. These examples have not been exercised against a live account.

What a production agent runtime has to do

Whether you build it or buy it, something has to handle these:

JobWhat goes wrong without it
Run the model and tool loopTurns that take minutes hit request timeouts
Store conversation historyAgents forget after a deploy
Give each conversation an identityUsers see each other's context
Survive restarts mid-turnHalf-finished turns and stuck spinners
Keep credentials server-sideSecrets leak into prompts and logs
Pause for a personApprovals hold a server thread for days
Bound spendOne runaway loop empties the budget

You have three broad options. Run the loop inside your web app, which is simplest and couples every agent to your deploys. Run it as your own service with a queue and a database, which works and is real infrastructure to own. Or use a hosted runtime that runs the loop and stores state while your app supplies tools and starts runs. The rest of this guide shows the third.

Step 1: Run your first agent

Get an API key from the console. The quickstart covers sign-up and credit. Install the SDK for your language only.

TypeScript (Node 22 or later, or Bun). Save as support.mts:

npm install @camelai/run
export CAMELAI_API_KEY=art_...
import { Agents, schema, tool } from "@camelai/run";

const agents = new Agents(); // reads CAMELAI_API_KEY

// Fixture data for the example, not a real order service.
const lookupOrder = tool({
  description: "Look up an order by id",
  input: schema.Object({ orderId: schema.String() }),
  execute: ({ orderId }) => ({ orderId, status: "shipped" }),
});

// The same key is the same agent, with its history, every time.
const agent = await agents.upsert("deploy-demo-ts", {
  instructions: "You help customers with their orders.",
  tools: { lookupOrder },
});

const run = await agent.run("Where is order 42?");
console.log(run.text);

await agents.close();
npx tsx support.mts

Python (3.11 or later). Save as support.py:

pip install camelai-run
export CAMELAI_API_KEY=art_...
import asyncio
from camelai_run import Agents, tool

@tool
def lookup_order(order_id: str) -> dict:
    """Look up an order by id"""
    return {"order_id": order_id, "status": "shipped"}  # fixture data

async def main():
    async with Agents() as agents:  # reads CAMELAI_API_KEY
        agent = await agents.upsert(
            "deploy-demo-py",
            instructions="You help customers with their orders.",
            tools=[lookup_order],
        )
        run = await agent.run("Where is order 42?")
        print(run.text)

asyncio.run(main())
python support.py

The two scripts use different keys on purpose, so they do not replace each other's tools. Neither names a model, so the agent uses your account's default. To pick one, find the models your account can use on the console's Models & keys page (or with GET /v1/models?available=true) and pass one's id as model.

Three things to notice. The loop runs in camelRun, not in this script. The tool runs in your process: tool credentials stay in the server-side function, so do not include them in tool arguments, return values, or logs. And the key is the agent's identity: run the script again and it is the same agent with the same history.

Step 2: Check what a restart does

Run the script a second time with a follow-up question, such as "What did I just ask you about?" The agent should answer from its earlier history, because the history was never in your process. For a stricter test that inspects the stored record, see the two-process restart test.

Two details decide how this behaves in production.

Use the durably recorded result as the source of truth. The event stream is for showing progress. After losing a process, recover the existing run using its saved id, a stable run idempotency key, or the result endpoint; a restarted process does not retain its old Promise. In practice, save your own id for each run before sending it and pass it as idempotencyKey. Calling run() again with the same text and key returns that run instead of starting another. The SDK reference also documents the lower-level agent.client.waitForRequest(id) (wait_for_request in Python), which waits for a run already sent, from any process.

A tool call in flight when your process dies is not sent again. The model is told the outcome is unknown. An unknown outcome means the tool may already have acted. Check the business record before repeating it, and enforce idempotency in the destination system. Each call carries context.idempotencyKey, but a newly requested call may carry a different key, so business-level deduplication still matters. Check run.toolErrors even when a run completes.

Step 3: Key agents by your own ids

Fragment: an identity pattern, not a continuation of the script above.

Give each user, ticket, or thread its own agent, using an id from your own records:

const agent = await agents.upsert(`thread-${thread.id}`, {
  definition: SUPPORT_DEFINITION,  // tools served by your backend, see step 4
  subject: thread.ownerId,         // whom the agent acts for
  context: { org: thread.orgId },  // claims your tools authorize against
});
const run = await agent.run(message, { user: currentUser.id });

Your tools receive subject, context, and the acting user as identity. Authorize every tool call from that identity, never from ids in the model's arguments. Your backend must also check that the caller owns the conversation before it upserts the agent. See Agents for many users.

Step 4: Choose where tools run before you add a second worker

This step often surprises a first deployment.

Tools passed to upsert are attached: the SDK holds a connection and camelRun calls your functions over it. One process serves an agent's tools at a time. That suits one long-lived server. With two web workers, or serverless functions, a second process gets an APPLICATION_CONNECTED error.

For several instances, serve the tools over HTTPS and name the server in a definition.

Architecture sketch. acme and the example.com URL are placeholders, and this is not deployable as written. Follow the served tools recipe for a complete version.

import { serveTools } from "@camelai/run/server";

// tenant: your own tenant id, from GET /v1/me
export default {
  fetch: serveTools({ lookupOrder }, { runtime: "https://run.camelai.com", tenant: "acme" }),
};
const definition = await agents.runtime.upsertDefinition("support", {
  name: "Support",
  mcpServers: [{ name: "shop", url: "https://tools.example.com/mcp", auth: { type: "runtime" } }],
});

// A new key: an agent's definition is fixed when it is created.
const agent = await agents.upsert(`user-${user.id}`, {
  definition: definition.id,
  subject: user.id,
  context: { org: user.orgId },
});

Each call arrives with a signed token saying which agent it is for and who is acting. Set tenant to your own tenant. The docs are explicit that this check is what tells your agents from another account's, so signature verification alone is not enough.

Your deploymentUse
One long-lived server or workerAttached tools
Several instances or serverlessServed tools
A third-party APIAn OpenAPI or MCP source in a definition
Agents woken by schedules, channels, or webhooksServed tools

The same tool definitions work attached and served, so you can start attached and move later.

A streaming chat app follows the same rule. Tools in its route need a server that keeps running, such as next start or a container. On serverless hosting, serve them from a route of their own and name them in a definition.

Step 5: Approvals that outlive the request

A tool marked needsApproval: true (@tool(needs_approval=True) in Python) does not block a request while someone decides. The run ends with status: "input_required", the agent sleeps, and answering resumes it from any process, minutes or days later.

Show each pending input to an authenticated, authorized person. When they answer, reload the pending inputs with agent.pendingInputs() and submit their actual answer with their identity. Answering the final input resumes the run. Do not hard-code true in an automatic request handler.

Inputs expire after 7 days by default. If a tool asks from inside its own code with context.confirm or context.ask, everything before the ask runs again once it is answered, so ask first and act after. The full flow is in Human input.

Step 6: Put a ceiling on model spend

Set spendLimit: { usd } on agents that act for untrusted users. It caps what the agent may spend on model calls from then on, not agent time or storage. An agent at its limit gets a 402 for new runs.

upsert applies the limit only when it creates the agent. To change it later, send PATCH /v1/agents/{id}/configuration with a new spendLimit. A new value starts its counter again from zero, so patching a full allowance before every request is not a lasting cap. To give one run what is left of a budget you track, pass it as that run's own limit: agent.run(message, { spendLimit: { usd: remaining } }). See spend limits. The usage.recorded webhook reports each model response's cost with the agent's subject, so you can meter per customer.

What it costs

camelRun is prepaid: you buy credit, and usage draws it down. Budget for agent time, storage, model tokens and web search. As of October 21, 2026:

  • Agent time: $0.01 per hour an agent spends in a run. Idle and sleeping agents cost only storage.
  • Storage: $0.10 per GB-month for history and files, including for sleeping agents.
  • Model tokens: $0 from camelRun on your own provider keys (your provider bills you), or the provider's price, with no markup on tokens, on camelRun's keys.
  • Web search: $0.001 to $0.007 per search.

There's no subscription, no seats and no per-agent fee. The agent-hour rate does not include model tokens. The pricing page has the live rate table.

Limits to know before you commit

  • The built-in code sandbox runs JavaScript only. It has no shell, no package installation, and no Python interpreter. If your agent must execute Python it writes, plan a separate execution service.
  • There is no graph DSL and no built-in vector retrieval. The migration guide lists what does not carry over from LangGraph and the OpenAI Agents SDK.
  • If you would rather operate it yourself, camelRun is open source under the AGPL and self-hosts with Docker and Postgres.

AI agent deployment checklist

  1. API keys live only on your servers, never in a browser or repository.
  2. Agents are keyed by your own ids, and deleted when the user or thread is.
  3. Your backend checks conversation ownership, and tools authorize from identity.
  4. Served tools verify the runtime's token with your own tenant.
  5. Every tool with side effects is idempotent in the destination system.
  6. Tool hosting matches your deployment: attached for one process, served for many.
  7. You reply to users from the run's result, and retries reuse a stable run id.
  8. You handle RunError and read run.toolErrors.
  9. Processes with attached tools close their agents on shutdown, including on SIGTERM.
  10. Runs that wait on people reach someone authorized to answer.
  11. Untrusted users' agents have a model spend limit.
  12. Webhooks for failed runs and requested inputs are signature-checked and deduplicated.
  13. You handle 429 and 503 by waiting for Retry-After (the SDKs already retry them).
  14. You have run the restart test yourself.

The full version is the production checklist.

Deploy your first agent

Start with the quickstart. Then read How to Give AI Agents Memory That Survives Restarts for how history, files, and compaction behave over long conversations.

CAMELRUN · HOSTED AGENT RUNTIME

Run your AI agents on camelRun.