Token Streaming
Session replies are streamed token-by-token from the model provider through the control-plane to the client. The streaming contract is a sequence of three named SSE events on GET /v1/sessions/{id}/events.
step_started / step_completed / step_failed events, not token deltas.The three events
A single assistant turn produces events in this order:
message_delta
Emitted for each token chunk during streaming. Not emitted for tool-bearing turns (see limitation below).
{
"sessionId": "sess_abc123",
"turnId": "tur_xyz789", // stable across all events in this turn
"seq": 1, // monotone per-turn counter; starts at 1
"delta": "Hello" // text fragment; one or more tokens
}message_completed
Emitted exactly once per turn, after all deltas (or immediately for tool-bearing turns that do not stream). Contains the full assembled text and usage metadata.
{
"sessionId": "sess_abc123",
"turnId": "tur_xyz789",
"text": "Hello, how can I help you today?",
"usage": {
"tokensIn": 42,
"tokensOut": 12,
"costUsd": 0.000018
}
}message_error
Emitted when the LLM call fails mid-stream after at least one delta has been sent. Clients should discard any partial response accumulated so far for this turnId.
{
"sessionId": "sess_abc123",
"turnId": "tur_xyz789",
"error": "provider error — partial response discarded"
}The turnId is stable across all three event types for the same assistant turn. Clients can use it to correlate deltas to their final message_completed, and to discard in-progress buffers when a message_error arrives.
message_completed without any preceding message_delta events.SDK: sessions.streamMessage()
The TypeScript SDK exposes token streaming as an async iterator. It handles sequence reordering, unknown event filtering, and legacy server compatibility automatically.
import { LanternClient } from "@lantern/sdk";
const client = new LanternClient({ apiKey: "..." });
for await (const event of client.sessions.streamMessage(sessionId, {
content: "Summarise the last quarter in 3 bullets.",
})) {
if (event.type === "delta") {
process.stdout.write(event.delta); // stream tokens as they arrive
} else if (event.type === "completed") {
console.log("\nTotal cost:", event.usage.costUsd);
}
}The iterator yields a discriminated-union type SessionStreamEvent:
type SessionStreamEvent =
| { type: "delta"; delta: string }
| { type: "completed"; text: string; usage: { tokensIn: number; tokensOut: number; costUsd: number } };Out-of-order seq values are buffered and yielded in order. A message_error from the server throws a typed MessageStreamError. Unknown event kinds are silently ignored so old and new server versions are both handled.
SSE transport
Named SSE events are delivered on the existing session event stream:
GET /v1/sessions/{id}/events
Authorization: Bearer <token>
# Server-sent events:
event: message_delta
data: {"sessionId":"...","turnId":"...","seq":1,"delta":"Hello"}
event: message_delta
data: {"sessionId":"...","turnId":"...","seq":2,"delta":", world"}
event: message_completed
data: {"sessionId":"...","turnId":"...","text":"Hello, world","usage":{...}}Legacy clients that subscribe to agent.message events continue to receive them — the new named events are additive. The _event key in the payload tells the session event bus which SSE event name to emit.
Provider streaming
The control-plane calls the model provider in streaming mode via streamWithFailover. Each chunk from the provider triggers an onDelta callback that publishes a message_delta to the Redis session channel, which the SSE handler fans out to connected clients. The Idempotency-Key header is set on the provider request (invariant #8) so a crash-retry dedups at the provider.
Provider failover: if the primary provider fails before any deltas have been sent, the failover chain retries on the next configured provider. If the primary fails after deltas have been sent, a message_error is emitted and the partial response is discarded.
Dashboard and webchat
The dashboard session chat and the embedded webchat widget both consume these events. The webchat widget — served at /widget.js — renders token-by-token as deltas arrive and flips to the final text on message_completed.