SDK

Build an agent app

A chat app in the framework you already use, backed by a Nyte host on your machine.

Create the project

pnpm create vite chat --template react-ts
cd chat
pnpm add @nyte-ai/sdk @nyte-ai/host @nyte-ai/plugin @nyte-ai/schema

Add the files

server.ts goes at the project root and runs the agent: the model login, the sessions and the tools. src/App.tsx replaces the template's app with the chat, and src/api.ts connects it to the host. useSyncExternalStore reads the session, so React re-renders whenever the conversation changes.

import process from "node:process";
import { createInterface } from "node:readline/promises";
import { sqliteStore } from "@nyte-ai/host";
import { asId } from "@nyte-ai/schema";
import { sdk } from "@nyte-ai/sdk";
import type { Host } from "@nyte-ai/sdk";

const store = sqliteStore({ path: "./agent.db" });
const host = await sdk.open({ store });

const { providers } = await host.models.list();

if (providers.find((provider) => provider.id === "openai-codex")?.credential === "missing") {
  await signIn(host);
}

const chosen = await host.models.setDefault({ provider: "openai-codex", id: "gpt-6-luna" });

if (chosen.kind !== "set") throw new Error("That model isn't in your account's catalog.");

const listener = await host.listen({
  // No port: the OS picks a free one, and the line below prints it.
  transport: { kind: "http" },
  auth: {
    kind: "custom",
    // Runs on every request and says who is calling. This one lets everyone in as the same user.
    authorize: () => ({
      // Principal ids are branded strings; asId turns "you" into one.
      principal: asId<"principal">("you"),
      // participant can start and use sessions. Owner calls, such as model sign-in, are refused.
      authority: new Set(["participant"]),
    }),
  },
  // The pages allowed to call the host: the address your dev server prints.
  origins: ["http://localhost:5173"],
});

console.log("Listening on", listener.address);

process.once("SIGINT", async () => {
  await host.close();
  await store.close();
});

async function signIn(host: Host) {
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
  const login = host.models.login({ provider: "openai-codex", method: { kind: "oauth" } });

  for await (const step of login.steps()) {
    if (step.kind === "open") console.log("Sign in at", step.url, step.code ?? "");
    if (step.kind === "paste") await login.answer(await prompt.question(`${step.message} `));
    if (step.kind === "choose") {
      for (const option of step.options) console.log(`${option.id}: ${option.label}`);
      await login.answer(await prompt.question(`${step.message} `));
    }
    if (step.kind === "failed") throw new Error(step.reason);
  }

  prompt.close();
}

Run it

The host needs Node 26.4 or later:

node server.ts

The first run signs you in to your OpenAI account in the terminal. The login is saved in agent.db, encrypted with the key in ~/.nyte/secret.key, so later runs skip it. Copy the address it prints into api.ts; the OS picks a new port each time the host starts.

pnpm dev

Then start your dev server in a second terminal. Vite prints http://localhost:5173, the origin server.ts already allows.

Open that address and send a message. The answer streams in as the host writes it. Before you put this online, replace authorize with your own sign-in; see Before you deploy.

This chat draws your messages and the text answers. Tool calls and other parts are skipped here; Render a session shows how to draw them.

Or a terminal app

The store works outside a browser too. Run this beside the host, with your question as the argument:

tui.ts
import process from "node:process";
import { isPart, text } from "@nyte-ai/plugin/core";
import { sdk } from "@nyte-ai/sdk/client";

const connection = await sdk.connect({
  // The address `node server.ts` printed.
  url: "http://127.0.0.1:PORT",
  credentials: { kind: "custom", headers: () => ({}) },
});
const { session } = await connection.sessions.start({ content: process.argv[2] ?? "Hello" });

session.subscribe(() => {
  const answer = session.current()?.messages.findLast((message) => isPart(message, text));

  if (answer === undefined || !isPart(answer, text)) return;
  console.clear();
  console.log(answer.state.markdown);
});
node tui.ts "What does this project do?"

The folder it works in

The agent works in the folder you start the host from, and its tools run there without asking first. It isn't a sandbox. Pass workspace: { kind: "local", path } to sdk.open to pick another folder.

The folder's own instructions and extensions, such as AGENTS.md, CLAUDE.md, .nyte/skills, .agents/skills, .nyte/prompts and .nyte/plugins, load only once you trust the folder:

const added = await host.workspaces.add({ kind: "local", path: process.cwd() });

if (added.kind === "added" || added.kind === "exists") {
  await host.workspace(added.workspace).update({ projectResources: true });
}

The decision is saved for the folder and the folders below it. To have tool calls ask first, run host.settings.apply("permissions", "ask"). The desktop app and the terminal show those requests; this chat doesn't, so leave it at the default while you build.

Before you deploy

  • Replace authorize with your own sign-in. It receives each Request, so it can read your session cookie and return that user as the principal, or { deny: "unauthorized" }.
  • Serve the host over HTTPS, and list your site's exact origin in origins.
  • To give each device its own revocable credential instead, use pairing.

The host is a long-running process. A serverless function can't be one, because it stops when the request ends; for serverless, start from the Vercel template below.

Deploy it

TemplateWhat runs
CloudflareOne host in a SQLite-backed Durable Object, chat only
VercelA Nitro app over PostgreSQL, with tools in a sandbox you supply

Extend it

Plugins change what the agent can do and how clients draw it. A core plugin adds tools, parts, commands, settings and hooks, and loads in the host. A client plugin adds renderers, commands, settings and keybindings, and loads in the app. Give the agent a tool is a complete core plugin.