camelAI Documentation

Guides

Coming from the OpenAI Agents SDK or LangGraph

Sessions, handoffs, guardrails, tracing, structured output and the rest, mapped to camelRun, with what it does not have

View as Markdown · Docs index for coding agents

The concepts mostly carry over. The main difference is where the agent lives. In the OpenAI Agents SDK and LangGraph, the loop runs in your process, and its state lives in a session or checkpointer that you run. In camelRun the loop runs in camelRun, and so do the agent's history and files. Your process provides the tools and starts runs.

This page maps each concept. It also lists what camelRun does not have, so you know before porting.

The same agent, ported

OpenAI Agents SDK:

python
from agents import Agent, Runner, SQLiteSession, function_tool

@function_tool
def lookup_order(order_id: str) -> dict:
    """Look up an order by id."""
    return orders.get(order_id)

agent = Agent(name="Support", instructions="You help with orders.", tools=[lookup_order])
result = await Runner.run(agent, "Where is order 42?", session=SQLiteSession("user-123"))
print(result.final_output)

camelRun:

python
from camelai_run import Agents, tool

@tool
def lookup_order(order_id: str) -> dict:
    """Look up an order by id."""
    return orders.get(order_id)

async with Agents() as agents:  # reads CAMELAI_API_KEY
    agent = await agents.upsert("user-123", instructions="You help with orders.", tools=[lookup_order])
    run = await agent.run("Where is order 42?")
    print(run.text)

The key ("user-123") does the job of the session: the same key is the same agent, with its history, from any process. upsert creates the agent the first time and later brings it to the configuration you pass. Nothing is stored on your side.

Deploy topology: one process serves an agent's tools. In the OpenAI Agents SDK and LangGraph, every worker runs its own loop with its own tools. Here the loop is in the runtime, and tools passed to upsert(tools=…) are served by the one process holding the agent's connection. A straight port that upserts with tools in every gunicorn or uvicorn worker, or every Celery task, works with one worker and fails at two: AgentError: Another process serves this agent's tools… (APPLICATION_CONNECTED). With several workers, serve the tools over HTTPS instead, and make agents from a definition that names the server, with no tools: then any worker can run any agent, and deploys lose no call. See Served tools.

from camelai_run import serve_tools

# In your web app (pip install "camelai-run[server]"): every worker serves the same tools.
tools_app = serve_tools([lookup_order], runtime="https://run.camelai.com", tenant="acme")  # mount at /mcp

# Once, at deploy time: a definition naming the server.
definition = await agents.runtime.upsert_definition("support", name="Support", systemPrompt="You help with orders.",
    mcpServers=[{"name": "shop", "url": "https://app.example.com/mcp", "auth": {"type": "runtime"}}])

# In any worker or task: no tools here, so no worker holds them.
agent = await agents.upsert("user-123", definition=definition["id"])
run = await agent.run("Where is order 42?")

tenant is your account's id (GET /v1/me). One long-lived process that holds the tools, with every other process upserting with attach=False, works too; see Tools.

OpenAI Agents SDK

OpenAI Agents SDKcamelRun
Agent(name, instructions, tools, model)agents.upsert(key, instructions=, tools=, model=); models are provider/model ids from GET /v1/models
Runner.run(agent, input), result.final_outputagent.run(text), run.text
Runner.run_streamedagent.stream(text): text, tool calls and results as they happen, then done with the run
@function_tool@tool (TypeScript: tool({ input, execute })). Arguments are checked against the schema first. See Tools
Sessions (SQLiteSession, Redis…)The agent's key. History is durable and compacted when it gets long; agent.history() reads it
output_type=Modelrun(text, output=Model), run.output (pip install "camelai-run[pydantic]"). See Structured output
HandoffsNot supported: camelRun has no handoffs. Delegate instead, or route in your code; see Handoffs and multi-agent
agent.as_tool()The delegate built-in: delegate: { agents: ["researcher"] }. The sub-agent's answer is the call's result; several calls run in parallel. See Multi-agent
Input guardrailsYour code, before run(). With createAgentHandler, onSend can rewrite or refuse a message
Output guardrailsYour code, on run.text or run.output, before you use it
Tool approval (needs_approval), interruptions@tool(needs_approval=True); the run ends input_required and run.inputs[0].answer(True) resumes it, from any process, days later if need be. See Human input
RunContextWrapper (context for tools)context and subject on the agent, plus user on each run. Tools get them in context.identity, set by your code and never by the model. See Identity
Hosted tools: WebSearchToolbuiltins=["web_search"]
CodeInterpreterTooljs_exec, always on: JavaScript in a sandbox that can call the agent's tools. It has no Python
FileSearchTool (vector stores)No equivalent. Agents have files and file tools, not vector search
MCPServerStreamableHttpmcpServers in a definition: the runtime calls the server itself. A local MCP server object can be attached with fromMcpServer (TypeScript)
Tracing (trace(), the traces dashboard)OpenTelemetry trace export to your own backend (LangSmith, Langfuse, Honeycomb, Datadog, Tempo, any OTLP endpoint): a span per run, model call, tool call and wait, continuing your traceparent. See Observability. Also the event stream (on_event, stream()), run.completed and usage.recorded webhooks, and GET /v1/agents/:id/history
max_turnsNo turn cap. spend_limit={"usd": …} on a run, or on the agent, bounds what it spends
ModelSettings (temperature, top_p)Not settable. thinking_level is

LangGraph

LangGraphcamelRun
create_react_agent(model, tools, prompt) (LangChain's create_agent)agents.upsert(key, model=, tools=, instructions=): the runtime runs the tool loop
Checkpointer and thread_idThe agent's key. One agent per thread: upsert(f"thread-{thread_id}")
graph.get_state(config)agent.history(), agent.history_page()
interrupt() in a node or toolIn a tool: await context.confirm(…), await context.ask(…). The model can also ask, with builtins=["ask_user"]
Command(resume=…)run.inputs[0].answer(value)
graph.stream(…, stream_mode="messages")agent.stream(text), or the agent's events
response_format, with_structured_outputrun(text, output=Model)
Supervisor (create_supervisor) and subgraphsA lead agent with the delegate built-in, each worker or subgraph a definition. See Multi-agent. A swarm (handoffs between agents) is not supported
A custom StateGraphWhere the model decides, delegate; where your code decides, a sequence of runs, branching on run.output. There is no graph DSL; see below
Send (map-reduce)Parallel delegate calls in one response, or asyncio.gather over runs of several agents
Long-term memory (Store)Files. Mount a shared volume in several agents
Time travel (get_state_history, replay from a checkpoint)Not supported. A conversation cannot be forked or rewound
LangSmithExport traces to LangSmith's OTLP endpoint (PUT /v1/telemetry, preset); include content to see messages

Handoffs and multi-agent

camelRun has no handoffs: an agent cannot pass its conversation to another agent, and there is no graph. Use one of these instead.

Delegate. The agent hands a task to a sub-agent and gets its answer back as the call's result, while the conversation stays with it: the Agents SDK's as_tool, or a LangGraph supervisor. The sub-agent is an agent of its own, made from a definition for each call, and the parent's limits cover it. See Multi-agent.

python
await agents.runtime.upsert_definition("billing-specialist", name="Billing specialist",
    description="Answers questions about invoices, refunds and charges", systemPrompt="You answer billing questions.")
support = await agents.upsert("support-user-123", instructions="You help customers.",
    delegate={"agents": ["billing-specialist"]})

Several delegate calls in one response run in parallel. To keep one specialist's history across calls, name an existing agent instead of a definition: {"agent": f"billing-{user_id}"}.

Route. Your code picks the agent for each message. A small agent with structured output can make the decision:

python
class Route(BaseModel):
    team: Literal["billing", "technical", "general"]

router = await agents.upsert("router", instructions="Pick the team that should answer this message.")
team = (await router.run(message, output=Route)).output.team
answer = await (await agents.upsert(f"{team}-user-123", instructions=PROMPTS[team])).run(message)

What does not carry over: in a handoff, the receiving agent takes over the conversation and sees its history. In camelRun each agent has its own history, so pass what the other agent needs in the task or the message.

Bringing conversations over

Existing conversations can continue in camelRun. Create the agent with its history in initialMessages, converted to the runtime's message format (user, assistant with tool calls, tool results). See Bringing in existing conversations.

What camelRun adds

  • Durable runs. A run survives your process restarting and the runtime's nodes being replaced. A run waiting on a person waits for days at no cost.
  • Tools anywhere. Tools run in your process (attached) or behind an HTTPS endpoint (served, for serverless and many workers), and the model can call many of them from code in js_exec. See Tools.
  • Channels and schedules. Slack, Telegram, Discord, GitHub and email can reach an agent directly, and agents can wake themselves. See Channels.