Host an agent in your app

September 4, 2026 · View on GitHub

Use this guide when you wire agent-kit into a product: login, one disk file per customer (tenant), chat history, guarded shell, and a live model turn.

agent-kit is a library. It does not start a server, check cookies, or choose who may call you. Your app authenticates the user, maps them to a tenantId, then opens a tenant home.

This guide assumes one machine: one Node process and one SQLite file per tenant on local disk. Multi-machine hosting is not ready yet (roadmap).

Words used here

TermMeaning
tenantIdStable id for one customer’s data. You create it from your login system.
VolumeOne SQLite file for that tenant. Memory, skills, workspace, chat logs, and audit live here.
sessionIdId for one chat conversation.
createTenantHomeConvention entry: opens volume + transcripts + sandbox and caches per process.

Agent install

Author identity and skills under agent/. Compile once, import everywhere. Sessions already use a policy-wrapped FS (createAgentFs).

// scripts/compile-agent.mjs — run with: node scripts/compile-agent.mjs
import { compileAgent } from "@socialrobot-io/agent-kit-node";

await compileAgent({
  dir: "./agent",
  outFile: "./src/generated/agent.ts", // or .json
});

Wire that script into predev / prebuild in your app package.json.

import { createTenantHome } from "@socialrobot-io/agent-kit-node";
import { agent } from "./generated/agent";

const home = await createTenantHome({
  tenantId,
  agent,
  sandbox: {
    secrets: [process.env.TENANT_API_KEY!],
    allowedHosts: ["api.company.com"],
  },
});

const session = await home.openSession(sessionId);

Skill locking (see Skills & learning):

SourceLocked?
agent/skills/*Only if locked/pinned/bundled or .locked
Created at runtimeNever (approval still applies)

Checklist

  1. Author agent/ and run compileAgent in CI / predev
  2. Mark company-owned skills with frontmatter or .locked
  3. Pass sandbox secrets / allowedHosts at home creation
  4. Enable javascript / python on sandbox if the agent should run js-exec / python3
  5. Add product tools with addTools (see Tools)
  6. Do not give the agent the raw volume write handle for tools

See also: Sandbox · Security · Company envelope PRD.

What your app must do

  1. Authenticate the user (cookie, JWT, session, or similar).
  2. Map that user to a stable tenantId. Never take tenantId from the request body alone.
  3. Create a sessionId for each chat and keep it tied to that tenant.

The kit stores data under the tenantId you pass. It does not check whether that caller is allowed to use it.

Happy path

Install @socialrobot-io/agent-kit-node and its peer ai (Vercel AI SDK). Defaults:

  • volume at ./data/tenants/${tenantId}.db
  • transcripts + session_search
  • sandbox tools (bash, readFile, writeFile)
  • model anthropic/claude-sonnet-4-5
  • process cache so the same volume path reuses one home

Most apps want createAgentKit: one object that opens a tenant home (cached per process, bounded by the number of tenants) and a chat session on demand. Stateless by default — each kit.session(tenantId, sessionId) call opens a fresh session from disk; state lives in the volume + transcripts, not in memory. Set maxSessions to opt into a per-chat LRU cache for the perf win.

// lib/kit.ts
import { createAgentKit, loadAgent } from "@socialrobot-io/agent-kit-node";

export const kit = createAgentKit({
  agent: await loadAgent("chat"),
  // model defaults to anthropic/claude-sonnet-4-5
});

// in a route (stateless — fresh session each call):
const session = await kit.session(tenantId, sessionId);
const turn = await session.run([{ role: "user", content: "Summarize /workspace." }]);

Load agents with await loadAgent("chat"). That opens <app-root>/agents/chat by default. Under Next.js, wrap the config with withAgentKit from @socialrobot-io/agent-kit-next (see below): it sets the agents folder once for both file tracing and loadAgent.

Advanced: createTenantHome

When you need the pieces (custom volume path, transcript store, sandbox secrets inspected outside a turn), drop down to createTenantHome. The kit uses it internally.

import { createTenantHome } from "@socialrobot-io/agent-kit-node";
import { agent } from "./generated/agent";

const home = await createTenantHome({ tenantId, agent });
const session = await home.openSession(sessionId);

const turn = await session.run([
  { role: "user", content: "Summarize /workspace." },
]);

Common overrides

Override only what you need. The rest stays on convention. Both createAgentKit and createTenantHome accept these.

const kit = createAgentKit({
  agent: await loadAgent("chat"),
  dataDir: "/var/lib/agents", // or volumePath: "/data/acme.db"
  model: "anthropic/claude-sonnet-4-5", // or a ready LanguageModel
  interactiveApproval: true, // chat UI Approve applies writes
  workspaceFiles: { "README.md": "# hi\n" },
  sandbox: { allowedHosts: ["api.example.com"] }, // hostnames only; or sandbox: false
  // transcripts: false,
});

// per-chat overrides via the third argument:
const session = await kit.session(tenantId, sessionId, {
  addTools: [myTool],
  disableTools: ["skill_manage"],
});

What kit.home(tenantId) / createTenantHome returns:

FieldWhat it is
home.volumeThe tenant SQLite filesystem (memory, skills, workspace, audit).
home.transcriptsChat history store used by session_search and the curator.
home.bashGuarded shell toolkit (bash, readFile, writeFile).
home.openSessionOpens one chat with frozen memory for that sessionId.

Most apps only call openSession. Use the other fields when you persist messages yourself, inspect the volume, or call sandbox tools outside a turn.

A full streaming chat with the same shape lives in examples/example-app.

Next.js (App Router)

agent-kit works in App Router route handlers and server actions on the nodejs runtime. Turbopack and webpack cannot bundle the native bindings that agent-kit loads at runtime:

  • agentfs-sdk loads @tursodatabase/database, which loads a per-platform .node package (@tursodatabase/database-linux-x64-gnu and similar).
  • just-bash can load optional native helpers for archive commands (@mongodb-js/zstd, and node-liblzma when built on the host).

Keep these packages outside the bundle. When they are bundled, the route fails at module evaluation with Error: Cannot find native binding.

Use @socialrobot-io/agent-kit-next so you do not hand-tune tracing:

// next.config.ts
import type { NextConfig } from "next";
import { withAgentKit } from "@socialrobot-io/agent-kit-next";

const nextConfig: NextConfig = {};

// Default: agents/ next to app/
export default withAgentKit(nextConfig);

// Custom folder — still loadAgent("chat"):
// withAgentKit(nextConfig, { agentsDir: "src/agents" })

withAgentKit merges:

  • serverExternalPackages: agentfs-sdk, just-bash, bash-tool
  • outputFileTracingIncludes["/*"]: ./{agentsDir}/**/*
  • env.AGENT_KIT_AGENTS_DIR: same agentsDir (so loadAgent("chat") matches tracing)

Rules:

  1. Set export const runtime = "nodejs" in the route or action file. The edge runtime cannot load native bindings or local SQLite files.
  2. Do not add @socialrobot-io/* packages to serverExternalPackages when you install them from npm. They ship plain JavaScript and bundle safely. examples/example-app lists them under transpilePackages because the monorepo maps them to TypeScript source. That setting is workspace-only.
  3. If the error names a different native package, pass it through withAgentKit(config, { serverExternalPackages: ["better-sqlite3"] }).

The same Cannot find native binding error in plain Node (no bundler) means the platform package is missing from node_modules. See the npm note in Getting started.

Reference wiring: examples/example-app.

Rules you must keep

  1. One volume file per tenant. Never open tenant A’s path for tenant B.
  2. Do not share one open volume across tenants.
  3. Leave write approval on unless you opt out for a local demo. Use curator.autoApprove when only curator proposals should skip human review.
  4. Prefer home.openSession(sessionId) so transcript ownership is asserted for you.
DetailFact
What is in the volumeMemory, skills, workspace files, transcripts, audit data
Who checks loginYour app. The kit trusts the tenantId you pass.
Audit trailPrefer AgentFS timeline or SQL. The kit also records blocked shell commands.

Optional AgentFS overlay mode exists. The default home is the volume itself. Do not mix modes by accident.

After each turn (curator)

createTenantHome().openSession runs the curator after every completed turn (Hermes-style). It does not block the user reply. Proposals stage under pending/ when write approval is on and curator.autoApprove is off.

Toggle with agent config:

defineAgent({
  model: "anthropic/claude-sonnet-4-5",
  config: {
    // curator: false,
    // curator: { mode: "memory" | "skills" | "combined" },
    // curator: { autoApprove: true }, // apply curator proposals; no pending UI
  },
});

Default is on (curator: true, mode combined, autoApprove false). Pick one host posture:

  1. Human review: show staged proposals in a UI or ops tool, then call approvePendingWrites when a human accepts them.
  2. Trust curator: set curator: { autoApprove: true } when end users are not suited to accept or discard suggestions. No pending UI for curator output. In-chat agent writes still follow writeApproval.
  3. Disable curator: set curator: false if you do not want the learning loop.

Approved or auto-applied content shows up in a new session (the open chat keeps its frozen memory snapshot).

Bare openAgentSession (without createTenantHome) does not auto-run the curator. Call runBackgroundReview yourself in that case.

Details: Skills & learning.

Next