Client SDK
Call an agent from your backend, scripts, or any JavaScript runtime, and stream its events.
The client is the rivetkit/client package. It calls actions on the agent and gets the agent’s events over a connection. A browser connects with a short-lived token from your backend.
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
const client = createClient<typeof registry>();
const agent = client.agent.getOrCreate(["support", "customer-123"]);
const root = await agent.harness.root();
const { messages } = await agent.conversation.context(root.id);
console.log(`${messages.length} messages so far`);
const conn = agent.connect();
conn.onStatusChange((status) => console.log(`[connection ${status}]`));
// The text of the current answer printed so far.
let printed = "";
const unsubscribe = conn.on("pi.events", ({ events }) => {
for (const event of events) {
switch (event.type) {
case "message_update":
for (const change of event.changes) {
if (change.type === "text_delta") {
process.stdout.write(change.delta);
printed += change.delta;
}
}
break;
case "message_end": {
// A short answer can arrive whole here, with no text_delta before it.
const message = event.entry.model?.[0];
if (message?.role === "assistant") {
const text = message.content
.flatMap((block) => (block.type === "text" ? [block.text] : []))
.join("");
process.stdout.write(text.slice(printed.length));
}
printed = "";
break;
}
case "tool_execution_start":
console.log(`\n> ${event.toolName}`);
break;
case "auto_retry_start":
console.log(`\nRetrying (attempt ${event.attempt}): ${event.errorMessage}`);
break;
}
}
});
// Sends this conversation's events to this connection.
await conn.conversation.watchEvents(root.id);
const result = await conn.prompt("Clone https://github.com/honojs/hono and summarize its README.", {
requestId: crypto.randomUUID(),
});
if (result.status === "unanswered") console.error(`\nNo answer: ${result.reason}`);
unsubscribe();
await conn.dispose();
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
const extensions = createRegistry();
extensions.install(CodingTools);
const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider(),
});
export const registry = setup({ use: { agent } });
registry.start();
Handles and connections
- With no options,
createClient()readsRIVET_ENDPOINT,RIVET_NAMESPACE, andRIVET_TOKENfrom the environment, and defaults to a local Rivet atlocalhost:6420. - The
getOrCreatemethod returns a handle to the agent with that key, and creates the agent on first use. Actions called on the handle, likeconversation.context, are single requests. - An agent starts with one root conversation. The
harness.root()action returns its id, andpromptuses it unless you pass aconversationId. - Calling
connect()opens a live connection that can receive events. Actions called before it opens wait until it does. - A dropped connection reconnects on its own. The
onStatusChangecallback reportsconnecting,connected, anddisconnected, andidleafterdispose()closes the connection for good.
Events
Events go only to a connection that asks for them. Call conversation.watchEvents(id) on the connection. It returns a snapshot of the conversation, including any answer in progress, and then the connection receives pi.events with { conversationId, seq, events } as the agent works.
- Events from before the call are not sent again. Read them from the snapshot.
- A short answer can arrive whole in
message_end, with notext_deltabefore it, so print the rest of the text there. - The
seqnumber goes up by one for each batch. If a number is missing, callwatchEventsagain to get a new snapshot. - To stop listening, call the function that
onreturns, orconversation.unwatchEvents(id).
See Session Lifecycle for the event types.
Results and errors
- The
promptaction resolves when the run ends. The result is{ status: "done", text }with the answer, or{ status: "unanswered", reason }. - A model error doesn’t reject. The status is
"unanswered", andreasonexplains why. - Pass a
requestIdto make retries safe. A secondpromptwith the samerequestIdreturns the first one’s result and doesn’t run the agent again. - While a run is in progress, a new prompt waits as a follow-up. Pass
whenBusy: "steer"to send it into the current run, or"reject"to makepromptreject with theConversationBusycode. - The
promptaction rejects with anActorErrorwhen the action itself fails. For example, the run passes the ten-minute action timeout (action_timed_out). A timeout only stops the wait. The run keeps going until it finishes orconversation.abort(id)stops it. - To start a run without waiting for it, call
conversation.submit(id, { type: "input", content }). Follow its progress with events.
Browsers
A browser shouldn’t hold your Rivet credentials. Your backend checks who the user is, then mints a short-lived token that reaches only their agent.
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
async function fetchAgentToken(): Promise<{ agentId: string; token: string }> {
const response = await fetch("/agent-token", { method: "POST", cache: "no-store" });
if (!response.ok) throw new Error("Could not get an agent token.");
return (await response.json()) as { agentId: string; token: string };
}
const { agentId } = await fetchAgentToken();
const client = createClient<typeof registry>({
endpoint: "https://api.rivet.dev",
namespace: "production",
getToken: async () => (await fetchAgentToken()).token,
});
const conn = client.agent.getForId(agentId).connect();
const result = await conn.prompt("Clone https://github.com/honojs/hono and summarize its README.");
console.log(result.status === "done" ? result.text : `No answer: ${result.reason}`);
import { Hono } from "hono";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";
const admin = createClient<typeof registry>();
// Replace with your own auth, such as verifying a session cookie. Never trust a user id the client sends.
async function authenticateUser(request: Request): Promise<string | null> {
return request.headers.get("x-user-id");
}
const app = new Hono();
app.post("/agent-token", async (c) => {
const userId = await authenticateUser(c.req.raw);
if (!userId) return c.json({ error: "unauthorized" }, 401);
const agent = admin.agent.getOrCreate(["support", userId]);
const { token } = await agent.issueToken({ subject: userId, expiresIn: 900 });
return c.json({ agentId: await agent.resolve(), token }, 200, { "Cache-Control": "no-store" });
});
export default app;
- In
token.ts, your backend resolves the user’s agent and callsissueToken, which returns a token for that one agent that expires in 15 minutes. - In
browser.ts, the client connects withgetForId, andgetTokenfetches a new token whenever the old one expires, so the connection stays up.
See JWTs and Authentication.
The same actions work over HTTP. See the curl tab in the Quickstart.
See the client documentation in the Actors docs for everything rivetkit/client can do.