camelAI Documentation

Start Here

Build a chat app

Put a streaming chat with each user's agent in your app: one server route and one component

Your users talk to their own agents in your app. Replies stream in as the model writes them, tool calls show as cards (or as your own components), and questions and approvals are answered in place. Your API key stays on your server.

Start a new app

bash
npm create @camelai/run-app my-app -- --api-key $CAMELAI_API_KEY
cd my-app && npm run dev

This creates a Next.js app with the chat, the server route it talks to, a tool drawn by its own component, and a demo sign-in for you to replace with your own. Pass --base-url to point it at a self-hosted runtime.

The demo sign-in treats every browser as a user. Deployed with NODE_ENV=production, it refuses everyone until you replace it in app/api/agent/route.ts (or set DEMO_AUTH=1).

Add it to an app you have

bash
npm install @camelai/run @camelai/run-react
1

Add the route

One POST route does everything the browser needs. It checks who the user is with your own sign-in, gives their browser a short-lived token to read their agent, and sends their messages and answers.

app/api/agent/route.ts
import { createAgentHandler } from "@camelai/run/server";

export const POST = createAgentHandler({
  // Reads CAMELAI_API_KEY from the environment, or pass apiKey.
  authorize: async request => {
    const user = await getUser(request); // your session check
    return user ? { userId: user.id, name: user.name } : null; // null: 401
  },
  agent: {
    instructions: "You help customers with their orders.",
    model: "anthropic/claude-sonnet-5-5",
  },
});

The handler is a standard fetch handler, so it runs anywhere:

ts
// Hono
app.post("/api/agent", c => handler(c.req.raw));

// Cloudflare Workers, Bun, Deno
export default { fetch: handler };

// Express or node:http, mounted before any body parser for this route
import { nodeListener } from "@camelai/run/node";
app.post("/api/agent", nodeListener(handler));
2

Add the chat

tsx
"use client";
import { AgentChat } from "@camelai/run-react/ui";
import "@camelai/run-react/styles.css";

export default function Support() {
  return <AgentChat endpoint="/api/agent" suggestions={["Where is my order?"]} />;
}

<AgentChat> fills its container, so give the container a height.

Route options

Option
authorize(request, { thread, action })Required. Return { userId, name?, agentKey? }, or null for a 401
agentHow the user's agent is made: { instructions, model, tools, definition, context, spendLimit, ... }, or a function of the user
onSend({ auth, text, data, request })Runs before each message. Return { text?, metadata? }, or throw new Response(...) to refuse it (quotas, moderation)
browserToken{ ttlSeconds, events, redact, url } for the tokens it mints. Default 15 minutes, with provider costs left out
allowedOriginsOther origins whose pages may call the route, with CORS
proxytrue: browsers also read their agent through this route and only ever talk to your origin. See below
linkAnyMountedPathtrue: users may download any file in the agent's mounts. By default, only files the agent presented or wrote to its own workspace
apiKey, urlDefault CAMELAI_API_KEY, and CAMELAI_BASE_URL or https://run.camelai.com

How it stays secure

  • The API key stays on your server. The browser gets a browser token that reads one agent's events, history, state and inputs for 15 minutes. It can't send, answer, stop, or read another agent. The chat renews it through your route.
  • Every write goes through your route, after authorize. The browser never names an agent: the route picks the user's agent from what authorize returned, by default one per user and thread.
  • Messages and answers are sent as the user (from: { id: userId, name }), never as whatever the browser claims.
  • Sends are idempotent. Each carries the id the browser gave the message, so a retry never sends twice.
  • Cross-site requests are refused. The route only takes application/json and refuses Sec-Fetch-Site: cross-site unless you list the origin.
  • Rendering is safe. Markdown is rendered as elements, never raw HTML. Links are only http(s) and mailto, and images the model links are shown as links unless you set allowImages.

Customize the UI

Replace any part of the prebuilt chat:

tsx
<AgentChat
  endpoint="/api/agent"
  tools={{ get_weather: WeatherCard }}             // your component per tool
  components={{ Markdown: MyMarkdown, EmptyState: Welcome }}
  labels={{ placeholder: "Ask about your order" }} // every string, for translation
  theme="system"                                   // or "light" / "dark"
/>

Theme it with CSS variables on .agent-chat, such as --agent-accent, --agent-radius, --agent-font and --agent-max-width. With shadcn/ui, also import @camelai/run-react/shadcn.css to use your theme's tokens.

Tools with their own UI

Give the agent tools in the route, and draw their calls with your own components in the browser:

import { schema, tool } from "@camelai/run";

agent: {
  tools: {
    get_weather: tool({
      description: "The weather in a city",
      input: schema.Object({ city: schema.String() }),
      execute: async ({ city }, { identity }) => weather.now(city), // identity.subject is the user
    }),
  },
},

A renderer gets args (partial while the model writes them), state (input_streaming, running, input_required, done or error), result, progress, and for a call that waits on the user, its input and answer(value).

Tools given in the route run in that server process, so it must keep running (next start, a container, a Node or Bun server). On serverless hosting, serve the tools over HTTP with serveTools and name them in a definition: agent: { definition: "def_..." }. See Served tools.

Threads

A thread is your name for one of the user's conversations. Each thread is its own agent, with its own history:

tsx
<AgentChat endpoint="/api/agent" thread={conversationId} />

The route receives it in authorize(request, { thread }). To let several people share one agent (a team's assistant), check access and return the key yourself:

ts
authorize: async (request, { thread }) => {
  const user = await getUser(request);
  if (!user) return null;
  if (thread && !(await isMember(user.id, thread))) return null;
  return { userId: user.id, name: user.name, agentKey: thread ? `team-${thread}` : undefined };
},

Everyone in a shared thread sees the same conversation live, and it runs one turn at a time. Put private work in per-user agents.

Read through your route

By default the browser reads its agent's stream straight from camelRun with its browser token. With proxy: true, your route passes those reads through too, so the browser only talks to your origin and the runtime can stay private:

app/api/agent/[[...path]]/route.ts
const handler = createAgentHandler({ authorize, agent, proxy: true });
export const GET = handler;
export const POST = handler;

Each open chat then holds a request on your server. Serverless platforms cut long requests at their time limit; the chat reconnects, and falls back to long polls of at most 25 seconds. Use the proxy when policy requires a single origin.

Troubleshooting