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
| Term | Meaning |
|---|---|
tenantId | Stable id for one customer’s data. You create it from your login system. |
| Volume | One SQLite file for that tenant. Memory, skills, workspace, chat logs, and audit live here. |
sessionId | Id for one chat conversation. |
createTenantHome | Convention 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):
| Source | Locked? |
|---|---|
agent/skills/* | Only if locked/pinned/bundled or .locked |
| Created at runtime | Never (approval still applies) |
Checklist
- Author
agent/and runcompileAgentin CI / predev - Mark company-owned skills with frontmatter or
.locked - Pass sandbox
secrets/allowedHostsat home creation - Enable
javascript/pythononsandboxif the agent should runjs-exec/python3 - Add product tools with
addTools(see Tools) - Do not give the agent the raw volume write handle for tools
See also: Sandbox · Security · Company envelope PRD.
What your app must do
- Authenticate the user (cookie, JWT, session, or similar).
- Map that user to a stable
tenantId. Never taketenantIdfrom the request body alone. - Create a
sessionIdfor 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:
| Field | What it is |
|---|---|
home.volume | The tenant SQLite filesystem (memory, skills, workspace, audit). |
home.transcripts | Chat history store used by session_search and the curator. |
home.bash | Guarded shell toolkit (bash, readFile, writeFile). |
home.openSession | Opens 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-sdkloads@tursodatabase/database, which loads a per-platform.nodepackage (@tursodatabase/database-linux-x64-gnuand similar).just-bashcan load optional native helpers for archive commands (@mongodb-js/zstd, andnode-liblzmawhen 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-tooloutputFileTracingIncludes["/*"]:./{agentsDir}/**/*env.AGENT_KIT_AGENTS_DIR: sameagentsDir(soloadAgent("chat")matches tracing)
Rules:
- Set
export const runtime = "nodejs"in the route or action file. The edge runtime cannot load native bindings or local SQLite files. - Do not add
@socialrobot-io/*packages toserverExternalPackageswhen you install them from npm. They ship plain JavaScript and bundle safely.examples/example-applists them undertranspilePackagesbecause the monorepo maps them to TypeScript source. That setting is workspace-only. - 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
- One volume file per tenant. Never open tenant A’s path for tenant B.
- Do not share one open volume across tenants.
- Leave write approval on unless you opt out for a local demo. Use
curator.autoApprovewhen only curator proposals should skip human review. - Prefer
home.openSession(sessionId)so transcript ownership is asserted for you.
| Detail | Fact |
|---|---|
| What is in the volume | Memory, skills, workspace files, transcripts, audit data |
| Who checks login | Your app. The kit trusts the tenantId you pass. |
| Audit trail | Prefer 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:
- Human review: show staged proposals in a UI or ops tool, then call
approvePendingWriteswhen a human accepts them. - 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 followwriteApproval. - Disable curator: set
curator: falseif 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.