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

Source
npmnpm
Version
0.6.19
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.
  • 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.
  • Channels — /v1/channels, connected transports 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.

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
const { people } = usePeople({ owner: "hanzo", service: false });
const { channels } = useChannels();
const { messages, rooms, 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 }]
const detail = await ai.threads.get(threads[0].id);   // ThreadDetail | null
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.

Channels

Inbound only — there is a read side and no send side because the server publishes none.

const transports = await ai.channels.list();
const box = await ai.channels.inbox({ limit: 50 });
const next = await ai.channels.inbox({ since: box.cursor });   // cursor = last id

rooms() groups a page you already hold into conversations. It is arithmetic, not a request: the server has no rooms route, and a room is just the messages sharing a (channel, roomId).

import { rooms } from "@hanzo/ai";
for (const room of rooms(box.messages)) {
  console.log(room.roomKind, room.last.text);   // "dm" | "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: "" });

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 Sep 2026

Related posts