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:
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:
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 SDK | camelRun |
|---|---|
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_output | agent.run(text), run.text |
Runner.run_streamed | agent.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=Model | run(text, output=Model), run.output (pip install "camelai-run[pydantic]"). See Structured output |
| Handoffs | Not 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 guardrails | Your code, before run(). With createAgentHandler, onSend can rewrite or refuse a message |
| Output guardrails | Your 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: WebSearchTool | builtins=["web_search"] |
CodeInterpreterTool | js_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 |
MCPServerStreamableHttp | mcpServers 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_turns | No 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
| LangGraph | camelRun |
|---|---|
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_id | The 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 tool | In 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_output | run(text, output=Model) |
Supervisor (create_supervisor) and subgraphs | A 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 StateGraph | Where 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 |
| LangSmith | Export 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.
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:
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.