New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@hanzo/ai

Package Overview
Dependencies
Maintainers
9
Versions
40
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@hanzo/ai

TypeScript client for the Hanzo AI backend — agents, machines, chat completions, Anthropic messages, models, embeddings, speech, images, answer-engine search/deep-research, and portable chat/message threads. Headless root; optional React hooks at @hanzo/a

latest
Source
npmnpm
Version
0.7.0
Version published
Weekly downloads
0
Maintainers
9
Weekly downloads
 
Created
Source

@hanzo/ai

The TypeScript SDK for the Hanzo AI Cloud — agents, models, chat, messages, embeddings, speech, images, portable threads, and router feedback in one headless client.

Pure client + types. No UI, no DOM dependencies beyond the standard fetch / ReadableStream web APIs, and no React in the package root. This is the shared API layer behind hanzo.chat, hanzo.app, the Hanzo desktop app, and the hanzo-dev CLI. Components live in @hanzo/ui; the React hooks that connect them to this client are an optional @hanzo/ai/react entry.

It is the first-party TypeScript surface for the Hanzo AI Cloud (api.hanzo.ai):

  • Agents — /v1/agents, the one agent store every Hanzo surface reads and writes: create, configure, list, run, and read the run history.
  • Machines — /v1/agents/targets, the org's compute. An agent bound to one is a bot.
  • Sandboxes — /v1/sandbox, computers leased by the second. An agent needs no machine to run: lease, run, end.
  • Chat completions — /v1/chat/completions, streaming and not, with Enso's routing bounds and the model that served.
  • Kai — /v1/decisions, typed questions answered with calibrated probabilities instead of generated text.
  • Messages — /v1/messages, streaming and not.
  • Models — the catalog at /v1/models.
  • Embeddings — /v1/embeddings, a vector for one text or many.
  • Speech — /v1/audio/speech and /v1/audio/transcriptions, both directions.
  • Images — /v1/images/generations.
  • Sessions — /v1/agents/sessions, a run's durable record: list, tree, steer, and one live SSE feed.
  • Tools — /v1/tools, what an agent can call: skills, plugins, connected MCP servers, and the catalog of what could be connected.
  • Threads — /v1/agents/chat/conversations, the conversation store every chat surface reads, so a conversation started on one resumes on another.
  • People — /v1/iam/scim/v2/Users, the org's directory over SCIM.
  • Transports — /v1/transport, the networks an org has connected and what arrived on them.
  • Account — the signed-in IAM identity.

Every shape here is checked against the contract the backend publishes: pnpm check:spec reads the live OpenAPI at api.hanzo.ai/v1/openapi.json and fails on any drift, so the types have one home and it is the server's.

Request and response shapes are the familiar chat / messages formats, so bringing existing code across is mechanical.

Install

npm install @hanzo/ai

Usage

Auth is a Hanzo IAM access token (e.g. from @hanzo/iam) sent as Authorization: Bearer <token>.

In a signed-in app, hand it IAM and nothing else — @hanzo/iam owns every credential step, and the client takes a fresh token per request (refreshing when one expires), so token plumbing is never re-implemented per app:

import { createAiClient } from "@hanzo/ai";
import { IAM } from "@hanzo/iam";

const iam = new IAM({ serverUrl: "https://hanzo.id", clientId: "my-app" });
const ai = createAiClient({ auth: iam }); // baseUrl defaults to https://api.hanzo.ai

A signed-out session throws AuthError instead of sending an unauthenticated request. For a server or a test holding a raw bearer (an hk- key, say), pass it directly — and if both are given, IAM wins while it has a session:

const ai = createAiClient({
  token: process.env.HANZO_TOKEN!,
  // or, for rotating tokens:
  // getToken: async () => session.accessToken,
});

Chat completions

const res = await ai.chat.completions.create({
  model: "enso",
  messages: [{ role: "user", content: "Explain MoE routing in one sentence." }],
});
console.log(res.choices[0].message.content);

Streaming:

const stream = await ai.chat.completions.create({
  model: "enso",
  messages: [{ role: "user", content: "Write a haiku about Go." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}

Tool calling works by passing tools / tool_choice.

Messages

const msg = await ai.messages.create({
  model: "enso",
  max_tokens: 512,
  system: "You are concise.",
  messages: [{ role: "user", content: "Summarize the CAP theorem." }],
});

// Streaming yields SSE events:
const events = await ai.messages.create({ /* ...,*/ stream: true } as any);
for await (const ev of events) {
  if (ev.type === "content_block_delta") { /* ev.delta.text */ }
}

Models

const models = await ai.models.list(); // Model[]

Model names such as enso are Zen models — the Hanzo family. You address them directly; the cloud routes each request to the right expert.

Kai

Kai answers a bounded question with a distribution over its answers. Each answer comes back under the name its question was asked by:

import { createAiClient, routedModel } from "@hanzo/ai";

const ai = createAiClient({ token: process.env.HANZO_API_KEY! });

const { answers } = await ai.decisions.create({
  model: "kai",
  state: "My order arrived broken and I want my money back.",
  questions: {
    intent: {
      type: "choice",
      instructions: "What does the customer want?",
      criteria: { refund: "money back", replacement: "a new item", status: "where the order is" },
    },
  },
});
const { choice = "", probabilities = {} } = answers.intent;
console.log(choice, probabilities[choice]);

noul asks whether a statement holds (answers.<name>.noul is P(true)) and score picks a level (answers.<name>.score). A refusal — 402, 422, 429 — throws APIError carrying its status.

Enso

Send model: "auto" and Enso picks the model per request across what your org can serve; send an enso-* id for one managed name. maxCost (USD per 1,000 tokens, sent as X-Max-Cost) and maxLatencyMs (X-Max-Latency-Ms) bound the pick. When Enso chooses the model — always for auto, and for an enso-* id when its ladder moves the request — the X-Routed-Model response header names it, and onResponse hands you the response to read it from. When nothing was chosen the header is absent and the id you sent is the one that served:

let served: string | undefined;
const completion = await ai.chat.completions.create(
  { model: "enso-auto", messages: [{ role: "user", content: "Summarize this changelog in one line." }] },
  { maxCost: 0.01, onResponse: (res) => (served = routedModel(res.headers)) },
);
console.log(served ?? completion.model, completion.choices[0]?.message.content);

// Teach the router how it went, keyed by the response id you already hold.
const reward = await ai.feedback({ requestId: completion.id, signal: "accept" });
console.log(reward?.recorded); // true

Routing pattern

Kai decides how hard a request is; Enso serves it at that tier:

const prompt = "Prove that the square root of 2 is irrational.";
const { answers: route } = await ai.decisions.create({
  model: "kai",
  state: prompt,
  questions: {
    tier: {
      type: "choice",
      instructions: "Which model tier should answer this request?",
      criteria: {
        "enso-flash": "a quick lookup, a greeting, or a short rewrite",
        "enso-auto": "an ordinary task that needs some reasoning",
        "enso-ultra": "hard multi-step reasoning, math, or code",
      },
    },
  },
});
const answer = await ai.chat.completions.create({
  model: route.tier.choice ?? "enso-auto",
  messages: [{ role: "user", content: prompt }],
});
console.log(route.tier.choice, answer.choices[0]?.message.content);

Embeddings

import { embed, embedMany } from "@hanzo/ai";

const vector = await embed(ai, "the cat sat", { model: "zen-embed" });
const vectors = await embedMany(ai, ["the cat sat", "on the mat"], { model: "zen-embed" });

embedMany answers the vectors in the order the texts were asked, not the order the provider sent them: each row of the wire carries the input it belongs to, and pairing by array position is silently wrong whenever an upstream reorders.

Speech

import { speak, transcribe } from "@hanzo/ai";

const { audio, type } = await speak(ai, "hello there", { model: "zen-voice", voice: "alloy" });
const { text } = await transcribe(ai, file, { model: "whisper-1" });

speak answers the bytes and the media type the server labelled them with — a provider that cannot make the container you asked for substitutes its own, so the response is what a player must be told, never the request. transcribe takes a Blob/File or raw bytes, and asking for timestamps ("word" / "segment") selects the verbose body those timings ride in.

Images

import { generateImages } from "@hanzo/ai";

const [image] = await generateImages(ai, "a red bicycle", { model: "zen3-image" });
image.url; // or image.b64_json, per what the upstream answered

n is clamped to [1,10] by the server.

Agents

/v1/agents is the one agent store. Every Hanzo surface — chat, app, console, playground, desktop, the CLI — reads and writes these records; none keeps its own. An agent is a model, a system prompt and a set of tool names, scoped to your org by the token.

const agent = await ai.agents.create({
  name: "helper",
  model: "enso-flash",
  instructions: "Answer in exactly one short sentence.",
});

const run = await ai.agents.run("helper", "What is 2+2?");
run.status; // "ok" — or "error", with run.error saying why
run.output; // "2+2 equals 4."

await ai.agents.list();               // Agent[]
await ai.agents.get("helper");        // AgentDetail — prompt + recent runs
await ai.agents.update("helper", { instructions: "be terse" });
await ai.agents.delete("helper");

A ref is the agent_… id or the org-unique name — either resolves the same agent. A run that fails resolves as a run carrying status: "error"; read the status rather than assuming output.

The dashboard reads

Three reads and a fold. Enough that a surface renders an agents dashboard without writing a client or a rollup of its own.

await ai.agents.runs();                              // org-wide feed, newest first
await ai.agents.runs({ limit: 50, status: "error" }); // just the failures
await ai.agents.runs({ agent: "helper", limit: 20 }); // one agent's runs

await ai.agents.metrics({ range: "7D" }); // invocation histogram + resource rollup
await ai.agents.activity();               // the org's activity feed

status and agent cannot be combined, and that is a type error rather than a runtime one: the org-wide feed filters on status, the per-agent route does not (measured — it answers unfiltered), so the pair that would silently return the wrong rows is unrepresentable.

The folds are pure functions over records you already hold — no client, no network:

import { agentStats, runStats, healthBreakdown, deriveActivity } from "@hanzo/ai";

healthBreakdown(agents); // { active: 7, idle: 0, error: 0, draft: 0 }
agentStats(agents);      // …plus total and summed run count
runStats(runs);          // { total, ok, failed, successRate, avgLatencyMs, …tokens }

Two rollups, not one, on purpose. An agent row carries a status and a run count, so agentStats answers what your agents are; success rate, latency and tokens are recorded on runs, so they come from runStats. Every field is null when nothing measured it — never a zero, because "unmeasured" and "zero" are different facts and a dashboard must be able to tell them apart.

deriveActivity(agents) builds a thinner feed from the agents' own timestamps, for a surface that cannot reach activity().

Machines

An agent bound to a machine by computeRef is what a bot is. The list is every machine your org has, provisioned or dialed in via hanzo link.

const machines = await ai.machines.list(); // Machine[]

Sandboxes

A machine is a box the org keeps; a sandbox is one it borrows. Running an agent takes no machine at all — lease an exec, run, end, and nothing is provisioned and nothing survives.

const box = await ai.sandboxes.lease({ class: "exec" });
const ran = await ai.sandboxes.run({ id: box.id, command: "pytest -q" });
await ai.sandboxes.end({ id: box.id });

Pass session to run() and the output is appended to that agent session as it is produced, so every surface watching the session watches the command work instead of a blank pause.

A dev class takes a project, which attaches that project's disk and keeps the workdir across leases — one live sandbox per project, since the disk mounts one computer at a time. Resume it by id; a caller holding an id who omits it gets a second computer rather than a second view of the first:

const dev = await ai.sandboxes.lease({ class: "dev", project: "site" });
await ai.sandboxes.end({ id: dev.id });          // disk survives
await ai.sandboxes.lease({ id: dev.id });        // same computer, back

runtime (runc, gvisor, kata-clh, kata-fc) is a REQUEST. The fleet refuses what it cannot honour, so read Leased.runtime for what you got.

React

@hanzo/ai/react is hooks and a provider — no components, no DOM — so the same code runs in a web app, a desktop shell and a native surface.

const { sessions, reload } = useSessions({ status: "running", limit: 20 });
const { session } = useSession(selectedId);        // null id fetches nothing
const { threads } = useThreads();                   // a chat sidebar's Recents: pinned first
const { threads: away } = useThreads({ archived: true }); // the archived ones alone
const { people } = usePeople({ owner: "hanzo", service: false });
const { transports } = useTransports();
const { messages, channels, cursor } = useInbox({ limit: 50 });

Every one hands back reload, because these lists change under you — a steered session, a renamed thread — and a sidebar that cannot re-read one is showing the past.

Pass params as a plain object; the hooks destructure them into primitive dependencies, so a fresh { status: "running" } each render does not re-fetch forever.

@hanzo/ai/react is the agent-authoring surface every platform mounts. It is hooks and a provider — no components, no DOM — so the same code runs in a web app, in the desktop webview and in React Native. Bring your own components from @hanzo/ui. react is an optional peer dependency; importing the package root pulls in none of it.

import { AiProvider, useAgents, useAgent } from "@hanzo/ai/react";

<AiProvider auth={iam}>
  <Agents />
</AiProvider>;

function Agents() {
  const { agents, loading, create, remove } = useAgents();
  const { agent, runs, run, running } = useAgent(agents[0]?.name ?? null);
  // …render with @hanzo/ui
}

useModels() and useMachines() supply the choices an authoring form offers for model and computeRef.

Tools

Four sources answer here and they are not interchangeable — list() is everything an agent may dispatch, skills() and plugins() and mcp() are where those came from, and catalog() is what could be connected but is not.

const available = await ai.tools.list({ activated: true });
const connected = await ai.tools.mcp();
const page = await ai.tools.catalog({ q: "github", limit: 20 });  // + total/offset

Activation is a WHOLE SET, not a delta — a name absent from enabled is off afterwards, so read before you write:

const { enabled = [] } = await ai.tools.activation();
await ai.tools.activate([...enabled, "search"]);

Sessions

A session is a run's durable record, and a tree: a run that spawns another names it as parent, and every descendant carries the same rootSessionId.

const running = await ai.sessions.list({ status: "running", limit: 20 });
const detail = await ai.sessions.get(running[0].id);   // + children + last 50 turns
const tree = await ai.sessions.tree(running[0].id);
await ai.sessions.update(running[0].id, { title: "renamed" });

limit bounds the page and there is no cursor — the route publishes no continuation, so asking for more means a bigger limit, not a second page.

Steering goes through a queue rather than a signal, which is why a pause posted from any surface reaches a run already in flight:

await ai.sessions.steer(id, "pause");
await ai.sessions.steer(id, "message", { message: "try the other branch" });

// The agent's own side: drain between turns, then act.
const { commands, cursor } = await ai.sessions.control(id, { after: seen });

One live feed for the org, narrowed to a tree with root:

for await (const frame of ai.sessions.stream({ root: id })) {
  if (frame.kind === "session") redrawRow(frame.session);
  else appendTurn(frame.event);
}

Each record arrives inside a one-key envelope on the wire ({"session":{…}}) and is unwrapped for you, so frame.session is the session rather than the wrapper. The heartbeat is an SSE comment and never becomes a frame.

There is no per-session stream. /v1/agents/sessions/{id}/stream answers 404 and appears nowhere in the contract; watching one run is this feed with root set to it.

Threads

The conversation store every Hanzo chat surface reads.

const threads = await ai.threads.list();          // [{ id, title, updatedAt, pinned, archived }], pinned first
const detail = await ai.threads.get(threads[0].id);   // ThreadDetail | null
await ai.threads.update(threads[0].id, { pinned: true });   // or { title }, { archived: true }; your own only
await ai.threads.remove(threads[0].id);           // its turns and its links, for good
detail?.messages.forEach((m) => console.log(m.role, m.content));

get() answers null for a thread this caller does not have. That is load bearing: asked for an id belonging to another tenant the route answers 200 with an empty messages rather than a 404, so a client trusting the status renders somebody else's id as an empty conversation.

This replaced a client over /v1/ai/chats, which answers {"data":[]} under every addressing — owner, user and global — because nothing writes that table.

People

const humans = await ai.people.list({ owner: "hanzo", service: false });
const bots = await ai.people.list({ owner: "hanzo", service: true });

Read through SCIM rather than the plain IAM user route, which answers ~200 fields per user where a directory needs six. The photos data URI — measured at 54 KB for one user — is dropped rather than carried into a list.

The filter is asymmetric on purpose: a service account is exactly userType: "service-account", and a human is anything else, because userType is ABSENT on some rows and a positive test for "normal-user" drops them.

Transports

A transport is the network a message travels over (Slack, Discord, Teams, Telegram, WhatsApp, GitHub, Linear); the conversation it lives in is a channel. The client binds the reads: what is connected, what arrived, who may speak and who is waiting to pair.

const transports = await ai.transports.list();
const box = await ai.transports.inbox({ limit: 50 });
const next = await ai.transports.inbox({ since: box.cursor });   // cursor = last id
const allow = await ai.transports.allowlist({ transport: "slack" });
const waiting = await ai.transports.pairing();

channels() groups a page you already hold into conversations. It is arithmetic, not a request: a channel here is the messages sharing a (transport, channelId).

import { channels } from "@hanzo/ai";
for (const channel of channels(box.messages)) {
  console.log(channel.channelKind, channel.last.text);   // "dm" | "group" | "thread"
}

sender is the platform's own id (U031THJJ7F1 on Slack). The schema declares a senderUser beside it and every measured message omits it, so render sender as an opaque handle and treat a resolved name as a bonus.

Account

const me = await ai.account.get();

Feedback (content-free reward signals)

sendFeedback emits a router-training signal keyed ONLY on the response id (the chatcmpl-… / msg_… the routing ledger keys on). It is content-free by construction — the payload can only ever be { request_id, signal, rating? }; no prompt, response, tag, filename or code can transit it. It is fire-and-forget: it never throws, never blocks UX, and is a silent no-op on any failure. This is the single implementation shared by hanzo.chat, hanzo.app and world.hanzo.ai.

import { sendFeedback } from "@hanzo/ai";

// Cross-origin to api.hanzo.ai, with a bearer for per-org attribution:
sendFeedback({ requestId: "chatcmpl-abc", signal: "up" }, { token });

// A 1–3 rating — the ONLY signal that carries a value:
sendFeedback({ requestId: "chatcmpl-abc", signal: "rating", rating: 3 }, { token });

// Same-origin BFF proxy (cookie auth) — prefers navigator.sendBeacon:
sendFeedback({ requestId: id, signal: "regenerate" }, { baseUrl: "" });

On a client, ai.feedback(input) sends it under the client's own base URL, credential and fetch. Both forms return a promise you may ignore: it resolves to the gateway's answer { request_id, reward, recorded } when the signal went over fetch, and to null for a beacon, a no-op or a failure. It never rejects.

Signals: up · down · regenerate · switch · abandon · accept · revert · rating · dismiss. The input is a discriminated union, so it is a type error to attach a rating to any non-rating signal (dismiss included) and a rating value is required for signal: "rating".

Options: baseUrl (absolute = api.hanzo.ai, "" = same-origin BFF), token (static or lazy — forces fetch since a beacon can't set headers), credentials (default "include"), disabled (config opt-out; also honors a HANZO_FEEDBACK=0 env var), fetch, sendBeacon, dedupe (default true).

Errors

All failures throw a subclass of HanzoAIError:

  • APIError — non-2xx HTTP, or a CRUD envelope with status: "error". Carries .status and .body.
  • AuthError — no token was available for an authenticated request.
import { APIError } from "@hanzo/ai";

try {
  await ai.models.list();
} catch (err) {
  if (err instanceof APIError) console.error(err.status, err.message);
}

Advanced

createAiClient accepts:

optiontypedefault
baseUrlstringhttps://api.hanzo.ai
authIamAuth (the @hanzo/iam SDK)—
tokenstring—
getToken() => string | Promise<string>—
fetchtypeof fetchglobalThis.fetch
headersRecord<string, string>{}

The low-level transport (client.http) and the SSE helpers (parseSSE, streamChatCompletion, streamMessage) are exported for custom calls.

License

Apache-2.0

Hanzo — the Open AI Cloud

Open source · every language · on-chain settlement. hanzo.ai · docs.hanzo.ai

SDKs in every language — Python (flagship) · TypeScript · Go · Rust · C++ · Swift · Kotlin · umbrella

Keywords

hanzo

FAQs

Package last updated on 30 Oct 2026

Related posts