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

Creating a daemon 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

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

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

Poll for 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
}
Polling vs. WebSocket
Polling is simpler but less efficient. For real-time streaming, use the WebSocket connection described below. Polling is best for one-off event retrieval or when WebSocket is unavailable.

WebSocket Streaming

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:

EventDescriptionStop Event
turn.tokenStreaming token from model outputNo
turn.thinkingModel reasoning/thinking outputNo
turn.tool_callModel invoked a toolNo
turn.tool_resultTool execution resultNo
turn.doneTurn completed successfullyYes
turn.errorError during inference or tool executionYes
session.cancelledSession was cancelled by userYes

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.

Reconnection with cursor
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
});
Event cursor
The afterSeq cursor is essential for reliable reconnection. Without it, the client would receive duplicate events for everything that happened before the disconnection.