Documentation
One object. One file. Build anything from a chatbot to a coding agent.
Quickstart
npm install oncellCreate agent.ts:
import { Agent, tools } from "oncell";
const agent = new Agent("support", {
instructions: `
You are a support agent for Acme Corp.
Search the knowledge base before answering.
`,
tools: {
searchDocs: {
description: "Search the knowledge base",
params: z.object({ query: z.string() }),
run: async ({ query }) => agent.files.search(query),
},
},
});
agent.chat(async ({ message }) => {
return agent.llm(message, { autoContext: true });
});
export default agent;Deploy and call:
import { OnCell } from "@oncell/sdk";
const oncell = new OnCell({ apiKey: "oncell_sk_..." });
// Deploy
await oncell.deploy("./agent.ts");
// Call
const reply = await oncell.agent("support").run("chat", {
message: "How do I reset my password?"
});
// Or stream
for await (const event of oncell.agent("support").stream("chat", {
message: "How do I reset my password?"
})) {
console.log(event.type, event.data);
}Primitives
Every agent gets these. No setup, no config, no separate services.
agent.llm()LLM call. Agentic loop when tools are provided. Always returns Result.agent.llm.step()Single LLM turn, raw messages in/out. The escape hatch.agent.memoryDurable KV. Auto-scoped per user in chat handlers. .forUser(id) elsewhere.agent.filesDurable filesystem (per-agent). Knowledge base, artifacts. .search() for RAG.agent.workspaceEphemeral filesystem (per-run). Shell cwd. Fresh each run. Concurrent runs never collide.agent.dbSQL via tagged templates. Injection impossible by construction.agent.secretsTyped, traced-as-redacted, never serialized into snapshots.agent.shell()Shell in gVisor sandbox. Array-args form for LLM-controlled input.agent.sleep()Durable sleep. Days, months. Zero cost while sleeping.agent.askHuman()Human-in-the-loop. Pause for approval. Durable — survives crashes and deploys.agent.spawn()Launch sub-agents. Awaited, durable, budget-cascading.agent.emit()Push progress events. Streamed to UI and trace.agent.memory
// Per-agent memory
await agent.memory.set("config", { theme: "dark" });
const config = await agent.memory.get("config");
// Per-user (inside chat handlers, auto-scoped)
const prefs = await agent.memory.get("prefs"); // this user's prefs
// Per-user (outside chat handlers, explicit)
await agent.memory.forUser("user_123").set("prefs", { lang: "ts" });agent.files (durable) vs agent.workspace (ephemeral)
agent.files is the knowledge base — durable, shared across runs. agent.workspace is the shell's cwd — fresh per run, garbage-collected after.
// Durable files: upload KB docs, search them in any run
await agent.files.write("kb/faq.md", content);
const docs = await agent.files.search("password reset");
// Workspace: ephemeral, per-run (coding agents clone here)
// agent.shell() runs in workspace by default
await agent.shell("git", ["clone", repoUrl, "."]);
// Two concurrent runs = two workspaces, no collisionagent.db
// Tagged templates only — injection impossible
await agent.db.sql"color:#5cdb7f">`INSERT INTO tickets(title, priority) VALUES(${title}, ${priority})`;
const rows = await agent.db.sql"color:#5cdb7f">`SELECT * FROM tickets WHERE status = ${"open"}`;agent.secrets
const token = agent.secrets.GITHUB_TOKEN; // typed, traced-as-redacted
// Never serialized into snapshots. On restore, re-injected by the host.
// Set via dashboard or: oncell secrets set GITHUB_TOKEN=ghp_... --env prodagent.llm()
One method, two modes. Without tools: a single LLM call. With tools: an agentic loop that runs until done.
Simple call
const answer = await agent.llm("Summarize this ticket");
console.log("color:#5cdb7f">`${answer}`); // toString() returns .textAgentic loop
const result = await agent.llm("Fix the failing tests", {
tools: [tools.workspace, tools.shell],
maxSteps: 50,
maxCost: 5.00, // halt if cumulative cost exceeds $5
});
// result.status: "completed" | "max_steps" | "max_cost" | "timeout"
// result.text: final output
// result.steps: number of tool iterations
// result.cost: dollars spent (includes spawned children)
// result.resumeToken: present if halted — continue from hereThe Result type
interface Result<T = unknown> {
text: string;
status: "completed" | "max_steps" | "max_cost" | "max_tokens" | "timeout";
steps: number;
cost: number;
usage: { inputTokens: number; outputTokens: number };
resumeToken?: string;
data?: T; // typed when output schema provided
toString(): string; // returns .text
}Resume after halt
if (result.status === "max_cost") {
const ok = await agent.askHuman({
question: "color:#5cdb7f">`Spent $${result.cost}. Approve $5 more?`,
});
if (ok.approved) {
const continued = await agent.llm.resume(result.resumeToken, { maxCost: 5.00 });
// resumes from exact checkpoint — no step re-executes or re-bills
}
}Structured output
const result = await agent.llm("Extract invoice details", {
output: z.object({
invoiceNumber: z.string(),
amount: z.number(),
dueDate: z.string(),
}),
});
result.data.amount; // number — typed, validatedEscape hatch: agent.llm.step()
One model turn. Raw messages in, one assistant message out. No loop, no tool execution — you control everything.
const turn = await agent.llm.step(messages, {
tools: myTools,
system: "Custom system prompt for this turn",
});
// turn.message, turn.toolCalls — your dispatch, your policiesTool Presets
Battle-tested, injection-safe, output-truncation tuned. Everyone writes the same five tools — the platform ships them.
import { Agent, tools } from "oncell";
const agent = new Agent("coder", {
instructions: "You are a coding agent.",
tools: [tools.workspace, tools.shell, tools.git],
});tools.workspaceread, write, list, glob, search — operates on agent.workspace (per-run). Path-traversal guarded.tools.shellLLM authors the command string. Sandbox is the guardrail. Output truncated with markers.tools.gitclone, status, diff, commit, push, checkout. Auth from agent.secrets.GITHUB_TOKEN.tools.websearch, fetch (readable extraction). Proxy-routed, memoized — safe on crash recovery.Compose and narrow:
tools: [
tools.workspace.pick("read", "search"), // read-only
tools.shell,
{ deploy: { description: "...", params: z.object({...}), confirm: true, run: ... } },
]Triggers
Same agent, different entry points.
agent.task() — HTTPS endpoint
agent.task("fix", async ({ repo, issue }: { repo: string; issue: string }) => {
await agent.emit("Cloning repository…");
await agent.shell("git", ["clone", "color:#5cdb7f">`https://github.com/${repo}.git`, "."]);
return agent.llm(issue, { tools: [tools.workspace, tools.shell], maxCost: 5.00 });
});agent.chat() — conversational + embeddable UI
Auto-streams text and tool-call events. History is conversational by default — the model sees the full thread. Customer screenshots arrive in files and are visible to the model.
agent.chat(async ({ message, files }) => {
return agent.llm(message, { autoContext: true });
});agent.schedule() — cron
agent.schedule("followup", "every monday 9am", async () => {
const open = await agent.db.sql"color:#5cdb7f">`SELECT * FROM tickets WHERE status = ${"open"} LIMIT ${20}`;
for (const ticket of open) {
const update = await agent.llm("color:#5cdb7f">`Status update for "${ticket.title}"`, { maxSteps: 3 });
await agent.db.sql"color:#5cdb7f">`UPDATE tickets SET last_update = ${update.text} WHERE id = ${ticket.id}`;
}
}, { maxCost: 20.00 }); // always budget schedulesagent.onWebhook() — external events
agent.onWebhook("/github", async ({ payload }) => {
if (payload.action === "opened") {
return agent.llm("color:#5cdb7f">`Review this PR: ${payload.pull_request.url}`, {
tools: [tools.web],
});
}
});User Scoping
Inside chat handlers: agent.memory, conversation history, uploaded files, and autoContext are scoped to the current user by default. Opt out with agent.global.*.
agent.chat(async ({ message }) => {
const prefs = await agent.memory.get("prefs"); // this user
const config = await agent.global.memory.get("config"); // cross-user, explicit
return agent.llm(message, { autoContext: true });
});
agent.task("admin", async ({ userId }) => {
// task = API endpoint, nothing auto-scoped
const prefs = await agent.memory.forUser(userId).get("prefs");
});askHuman in a scoped handler goes to the current user. In a task, specify the channel explicitly.
Durability
Every await agent.* is a checkpoint. If the process crashes, the agent resumes from the last completed step. No LLM call is paid twice. No side effect fires twice.
agent.task("process-refund", async ({ orderId, amount }) => {
const order = await agent.db.sql"color:#5cdb7f">`SELECT * FROM orders WHERE id = ${orderId}`;
// Agent parks here — can wait days. Process can crash, redeploy, move.
const ok = await agent.askHuman({
question: "color:#5cdb7f">`Refund $${amount} for order ${orderId}?`,
});
if (ok.approved) {
await agent.llm("Process the refund and notify the customer");
}
// Sleep 30 days, then follow up
await agent.sleep({ days: 30 });
await agent.llm("Check if the customer is satisfied");
});Tool confirmation
createTicket: {
description: "Create a support ticket",
params: z.object({ title: z.string(), priority: z.enum(["low", "high"]) }),
confirm: { channel: "dashboard" }, // staff approves, not the customer
run: async ({ title, priority }) => {
await agent.db.sql"color:#5cdb7f">`INSERT INTO tickets(title, priority) VALUES(${title}, ${priority})`;
return "created";
},
}
// On rejection: LLM receives { denied: true, reason } — adapts, doesn't crashExamples
Coding agent
import { Agent, tools } from "oncell";
const agent = new Agent("coder", {
instructions: "You are a coding agent. Read code, make changes, run tests, iterate.",
skills: {
debugging: { when: "Tests fail", guide: "Read error. Find file. Root cause. Fix. Re-test." },
},
tools: [tools.workspace, tools.shell, tools.git],
});
agent.task("fix", async ({ repo, issue }: { repo: string; issue: string }) => {
await agent.emit("Cloning…");
await agent.shell("git", ["clone", "color:#5cdb7f">`https://github.com/${repo}.git`, "."]);
return agent.llm(issue, { maxSteps: 50, maxCost: 5.00 });
});
export default agent;Research agent (multi-agent)
// researcher.ts
import { Agent, tools } from "oncell";
export const researcher = new Agent("researcher", {
instructions: "Research a topic thoroughly using web search.",
tools: [tools.web],
});
researcher.task("research", async ({ topic }: { topic: string }) => {
return researcher.llm(topic, { maxSteps: 30, maxCost: 2.00 });
});// planner.ts
import { Agent } from "oncell";
import { z } from "zod";
import { researcher } from "./researcher";
const planner = new Agent("planner", {
instructions: "Break questions into sub-questions. Delegate. Synthesize.",
tools: {
delegate: {
description: "Send a sub-question to a researcher",
params: z.object({ topic: z.string() }),
run: async ({ topic }) => planner.spawn(researcher, "research", { topic }),
},
},
});
planner.task("deep-research", async ({ question }: { question: string }) => {
return planner.llm(question, { maxCost: 10.00 });
// Parallel delegate calls run concurrently. Child spend cascades against $10.
});
export default planner;
// oncell deploy ./planner.ts deploys both (entry-graph rule)Client SDK
The unit is the agent handle. Tasks are run/stream. Chat is a conversation object.
import { OnCell } from "@oncell/sdk";
const oncell = new OnCell({ apiKey: process.env.ONCELL_API_KEY });
const coder = oncell.agent("coder");
// Run a task
const result = await coder.run("fix", { repo: "acme/api", issue: "Tests failing" });
// Stream a task
for await (const event of coder.stream("fix", { repo, issue })) {
// event.type: "emit" | "text" | "tool_call" | "tool_result" | "parked" | "done"
}
// Chat (conversation object owns continuity)
const convo = oncell.agent("support").chat({ user: "user_123" });
const reply = await convo.send("My invoice is wrong");
const reply2 = await convo.send("Yes, the March one", { files: [screenshot] });
// Chat streaming
for await (const event of convo.stream("Check my March invoice?")) {
// same event types as task streaming
}
// Resume a halted run
await coder.resume(result.resumeToken, { maxCost: 5.00 });
// Push files to agent's durable knowledge base
await oncell.agent("support").files.push("./kb/");
// Manage
await oncell.agents.list();