Without React
The framework-agnostic core.
@inlayai/react is a thin layer over @inlayai/sdk, which has no framework
dependency. Use it directly for a CLI, a worker, a test, or another UI
framework.
// No React, no DOM. `createChat` is the framework-agnostic core the hooks are
// built on: the same wasm reducer, the same wire format, driven by callbacks
// instead of a subscription. The API origin defaults to production.
const chat = createChat({
agentId: process.env.INLAY_AGENT_ID!,
session,
tools: {
// Hand-wired handlers receive `unknown` args — the SDK will not pretend to
// know an arbitrary agent's tool schema. Narrow them yourself, or generate
// a typed client (see typed-clients.md) and get them for free.
ask_question: (args) => `answering: ${(args as { question: string }).question}`,
},
onSnapshot: (snapshot) => {
// `snapshot.rows` is the flat top-level feed — a subagent's inner work is
// dropped here. Read `chat.fullSnapshot` for the path-tagged version.
const latest = snapshot.rows.at(-1);
process.stdout.write(rowText(latest));
},
});
// send() rejects when the run fails — catch it, or the worker exits with an
// unhandled rejection instead of a readable error. See failure-handling.md.
try {
await chat.send("hello");
} catch (e) {
console.error("the run failed:", e);
}
// The same derived views the hooks expose are plain functions over the rows.
for (const turn of transcript(chat.snapshot.rows)) {
console.log(`${turn.type}: ${turn.content}`);
}What changes
- Callbacks instead of subscriptions.
onSnapshotfires on every update;onRunfires only when the topology moves. chat.snapshot/chat.fullSnapshotreplace the hooks. The first is the flat top-level feed, the second is path-tagged and includes inner-subgraph rows — the same split described in The timeline.- Derived views are plain functions.
transcript(rows),rowsAtPath,outputsNamed,rowText— the hooks call exactly these.
What does not change
The reducer. Both packages drive the same WebAssembly core over the same wire format, so a conversation folds identically in Node, in a browser, and in the server's own tests. There is no second implementation to disagree with.
Streaming text
Set smooth: false (or read the headless surface, where pacing is off by
default) to get raw deltas as they arrive. The paced reveal exists to make text
readable in a UI; a script that is parsing output wants the raw stream.
Pacing covers every streamed text surface, not just chat: assistant content,
reasoning, and streamed artifacts (a building output with a text
format). It is display smoothing, not data smoothing — rows slice on render,
so useBinding and headless readers always see the true value.
The connection
Every entry point — createChat, the createBilling / createRetrieval /
createOps facades, listConversations — takes the same bag:
{ session, baseUrl?, fetch?, retries? }. baseUrl defaults to production
(INLAY_PRODUCTION_URL); set it for a self-hosted or staging deployment.
If you give it a session provider rather than a fixed session, it refreshes and retries once on a 401, so long-running processes do not need their own token loop. It retries transient network failures with backoff (default 2), and never retries HTTP error responses — a 400 is an answer, not a hiccup.
Advanced: custom transports
The bag's transport field accepts a full custom Transport implementation
— the seam the test suite uses to drive the wasm core without a network, and
the escape hatch for a non-browser host whose fetch needs replacing
wholesale. HttpTransport is the built-in one; construct it directly if you
need to share one transport's session cache across facades by hand.