@zuno-ai/sdk
v0.2.2
Published
Embed [Zuno](https://github.com/iarmankhan/zuno) — a typed AI agent harness with sandboxed tool execution and AI SDK v6 native streaming — directly into your Node app. **No service to deploy, no database to provision.**
Readme
@zuno-ai/sdk
Embed Zuno — a typed AI agent harness with sandboxed tool execution and AI SDK v6 native streaming — directly into your Node app. No service to deploy, no database to provision.
import { createZuno } from "@zuno-ai/sdk";
import { anthropic } from "@ai-sdk/anthropic";
const zuno = await createZuno({ model: anthropic("claude-sonnet-4.6") });
const reply = await zuno.chat("Read package.json and tell me the project name.");
console.log(reply);That's the whole hello-world. No Postgres, no session ceremony, no schemas. Storage is in-memory by default; swap in Postgres when you need persistence.
Install
pnpm add @zuno-ai/sdk ai @ai-sdk/anthropicPick a model provider that matches your stack — Zuno doesn't bundle any:
pnpm add @ai-sdk/anthropic # Anthropic
pnpm add @ai-sdk/openai # OpenAI
pnpm add @ai-sdk/google-vertex # Anthropic via Vertex
# Vercel AI Gateway: `gateway()` ships with `ai`, no extra install.Continuity
zuno.chat() auto-creates an anonymous session on the first call and reuses it for subsequent calls — so a follow-up question continues the conversation:
await zuno.chat("My favourite colour is teal.");
await zuno.chat("What did I just say my favourite colour was?");
// → "Teal."
zuno.reset(); // start a fresh thread
await zuno.chat("Again, what is my favourite colour?");
// → "I don't have that information."Streaming
const stream = await zuno.stream("Walk me through what's in this folder.");
for await (const chunk of stream) {
// chunk is a v6 UIMessageChunk — drop into useChat / readUIMessageStream / AI Elements
process.stdout.write(chunk.type === "text-delta" ? chunk.delta : "");
}Adding a database (production)
The in-memory backend lives in the host process and disappears when it exits. For persistence, pass a Postgres URL:
const zuno = await createZuno({
model: anthropic("claude-sonnet-4.6"),
database: {
connectionString: process.env.DATABASE_URL!,
runMigrations: true, // creates Zuno tables on first start
},
});You'll also need pnpm add drizzle-orm pg — these are required by the Postgres backend.
Multi-tenant: one Postgres, many Zunos
All Zuno tables live in a configurable Postgres schema (default 'zuno'). Two Zuno installs against the same database stay fully isolated as long as they pick different schema names:
const tenantA = await createZuno({
model,
database: { connectionString: SHARED_DB_URL, schema: "zuno", runMigrations: true },
});
const tenantB = await createZuno({
model,
database: { connectionString: SHARED_DB_URL, schema: "zuno_app2", runMigrations: true },
});Each schema has its own __drizzle_migrations tracking table, so the two installs version independently. Schema names must match the Postgres unquoted-identifier shape ([a-zA-Z_][a-zA-Z0-9_]{0,62}).
Adding tools, skills, sandbox
By default chat() runs in direct-system sandbox mode — tool execution happens in the host process, scoped to process.cwd(). Pick a different sandbox for stronger isolation:
const zuno = await createZuno({
model: anthropic("claude-sonnet-4.6"),
sandbox: { kind: "local-docker", image: "node:22-alpine" },
// or: { kind: "daytona", apiKey: "..." }
});Skills, custom tools, and memory are similarly configurable. The full config:
| Field | Required | Default | Notes |
|---|---|---|---|
| model | yes | — | resolved AI SDK v6 LanguageModel |
| database | no | (in-memory) | when set, persists to Postgres |
| sandbox | no | { kind: 'direct-system' } | one of direct-system / local-docker / daytona |
| skills | no | { kind: 'noop' } | filesystem walks an org/user/repo skill tree |
| memory | no | { kind: 'builtin' } | storage-backed memory provider |
| systemPrompt | no | bundled | task-specific override |
| extraTools | no | [] | additional tools after the built-in + memory union |
| builtinAllowList | no | all | restrict registered built-ins by name |
Explicit sessions (multi-user apps)
chat() is for the simple single-thread case. When you're building a multi-user app, manage sessions explicitly:
const session = await zuno.createSession({
orgId,
actor: { kind: "user", userId },
});
if (!session.isOk()) throw session.error;
await zuno.storage.appendMessage({
sessionId: session.value.id,
role: "user",
parts: [{ type: "text", text: "Read README.md and summarize it." }],
});
const result = await zuno.runTurn({ sessionId: session.value.id });
const stream = zuno.streamChatTurn({ sessionId: session.value.id });Power-user subpath exports
When createZuno() is too high-level, drop down to the underlying packages via subpath:
import { createAgentLoop } from "@zuno-ai/sdk/agent-loop";
import { createPostgresStorage } from "@zuno-ai/sdk/storage";
import { createMemoryStorage } from "@zuno-ai/sdk/storage";
import { createFsSkillRepository } from "@zuno-ai/sdk/skills";
import { createBuiltinMemory } from "@zuno-ai/sdk/memory";
import { translateEvent, eventsToChunks } from "@zuno-ai/sdk/stream";Each subpath re-exports its underlying package; this lets a consumer take a single dep on @zuno-ai/sdk and avoid the workspace fan-out.
AI SDK v6 streaming in a Next.js route
streamChatTurn(...) returns a ReadableStream<UIMessageChunk> already in v6's UIMessage Stream Protocol shape:
// app/api/chat/route.ts
import { createZuno, type ZunoClient } from "@zuno-ai/sdk";
let zuno: ZunoClient;
async function getZuno() {
zuno ??= await createZuno({ /* … */ });
return zuno;
}
export async function POST(req: Request) {
const { messages, sessionId } = await req.json();
const z = await getZuno();
// ... append the latest user message, kick off the agent ...
const stream = z.streamChatTurn({ sessionId });
return new Response(stream, {
headers: {
"content-type": "text/event-stream",
"x-vercel-ai-ui-message-stream": "v1",
"cache-control": "no-cache",
},
});
}Browser side, plain DefaultChatTransport:
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
export function Chat() {
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
// ...
}Embedding vs. service deployment
The SDK is for embedding Zuno in a Node app (Hono, Fastify, Express, Next.js route handlers, plain scripts). If you instead want to run Zuno as a separate service with HTTP/SSE access (Slack bridge, CLI, external products), use the apps/api-gateway deployment target — it consumes the same internal packages over HTTP. The two paths are not mutually exclusive: a single Postgres instance can host both an embedded SDK and a deployed gateway.
