oncell

Documentation

One object. One file. Build anything from a chatbot to a coding agent.

Quickstart

npm install oncell

Create 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 collision

agent.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 prod

agent.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 .text

Agentic 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 here

The 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, validated

Escape 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 policies

Tool 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 schedules

agent.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 crash

Examples

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();

oncell · Dashboard · GitHub