Daemon Client SDK
The @opta/daemon-client TypeScript package provides a typed client for the daemon HTTP API and WebSocket event stream, handling authentication, serialization, and reconnection automatically.
Daemon Client SDK
The daemon client is used by Code Desktop and can be used by any TypeScript application that needs to interact with the Opta daemon. It wraps the daemon v3 REST API and WebSocket endpoint with fully typed request and response objects.
Creating a Client
import { DaemonClient } from "@opta/daemon-client";
const client = new DaemonClient({
host: "127.0.0.1",
port: 9999,
token: "<bearer-token>",
});
// Verify connection
const health = await client.health();
console.log(health.status); // "ok"The token is read from the daemon's state.json file at ~/.config/opta/daemon/state.json. In browser environments, Code Desktop stores the token in localStorage after initial authentication.
Session Lifecycle
The daemon client supports the full session lifecycle: create, submit turns, poll for events, and close.
Create a Session
const session = await client.createSession({
mode: "chat", // "chat" or "do"
model: "qwen3-72b", // model to use for inference
});
console.log(session.id); // "sess-abc123"
console.log(session.status); // "active"Submit a Turn
const turn = await client.submitTurn(session.id, {
content: "Explain how unified memory works on Apple Silicon",
});
console.log(turn.id); // "turn-xyz789"After submitting a turn, the model begins inference. You can track progress through WebSocket events or by polling the session endpoint.
Poll Events
const events = await client.getEvents(session.id, {
afterSeq: 0, // get all events from the beginning
});
for (const event of events) {
console.log(event.event, event.seq);
// "turn.token" 1
// "turn.token" 2
// ...
// "turn.done" 42
}WebSocket Streaming
const ws = client.connectWebSocket();
ws.on("event", (envelope) => {
switch (envelope.event) {
case "turn.token":
// Streaming token: envelope.data.token
process.stdout.write(envelope.data.token);
break;
case "turn.tool_call":
// Tool invocation: envelope.data.toolName, envelope.data.args
console.log("Tool:", envelope.data.toolName);
break;
case "turn.tool_result":
// Tool result: envelope.data.toolName, envelope.data.result
console.log("Result:", envelope.data.result);
break;
case "turn.done":
// Turn completed: envelope.stats
console.log("\nStats:", envelope.stats);
break;
case "turn.error":
// Error during turn
console.error("Error:", envelope.data.message);
break;
case "session.cancelled":
// Session was cancelled
console.log("Session cancelled");
break;
}
});
ws.on("disconnect", () => {
console.log("WebSocket disconnected");
});Event Handling
The daemon emits several event types through the WebSocket connection:
| Event | Description | Stop Event |
|---|---|---|
| turn.token | Streaming token from model output | No |
| turn.thinking | Model reasoning/thinking output | No |
| turn.tool_call | Model invoked a tool | No |
| turn.tool_result | Tool execution result | No |
| turn.done | Turn completed successfully | Yes |
| turn.error | Error during inference or tool execution | Yes |
| session.cancelled | Session was cancelled by user | Yes |
Stop events (turn.done, turn.error, session.cancelled) indicate that no more events will be emitted for the current turn. Your UI should transition from a streaming state to a completed state when it receives a stop event.
Reconnection with afterSeq
Each event has a sequence number (seq). When reconnecting after a disconnection, pass the last received sequence number as afterSeq to avoid re-delivery of events you have already processed.
let lastSeq = 0;
ws.on("event", (envelope) => {
lastSeq = envelope.seq;
// ... handle event
});
ws.on("disconnect", () => {
// Reconnect with cursor to avoid re-delivery
const newWs = client.connectWebSocket({ afterSeq: lastSeq });
// ... reattach event handlers
});afterSeq cursor is essential for reliable reconnection. Without it, the client would receive duplicate events for everything that happened before the disconnection.