useStream supports three transport modes: the default SSE, WebSocket, and custom adapters. The option bag is a discriminated union — picking transport selects which branch the hook runs on.
It can talk to LangGraph Platform directly, or to anything that implements the Agent Streaming Protocol. The key question is whether you need a custom browser transport, or just a custom backend.
Runnable example: The
react-custom-transportapp in the streaming cookbook serves LangGraph protocol events from a local Hono server and reads a custom projection alongside the chat stream.
That example demonstrates the recommended custom-backend path:
HttpAgentServerAdapter.custom:<name> convention and read with useExtension.You can bring your own server without shipping a custom AgentServerAdapter in every app.
Use the lightest layer that solves the problem:
"sse" + fetch override — you are still talking to a LangGraph-compatible server and only need headers, auth, or request rewriting.HttpAgentServerAdapter — you have your own HTTP server, but it can expose POST /threads/:threadId/commands and POST /threads/:threadId/stream semantics.AgentServerAdapter — you are not using that HTTP/SSE shape at all, e.g. WebTransport, postMessage, in-memory fixtures, a multiplexed socket, or an application-specific event bus.Most production custom backends should start with HttpAgentServerAdapter.
import { useStream } from "@langchain/svelte";
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
// transport defaults to "sse"
});
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
transport: "websocket",
});
Provide a custom factory if you need to tweak the WebSocket instance (for example to attach sub-protocols):
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
transport: "websocket",
webSocketFactory: (url) => new WebSocket(url, ["custom-protocol"]),
});
Browsers do not let regular WebSocket connections attach arbitrary custom headers. Use "sse" + a fetch override when you need header-based auth:
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
transport: "sse",
fetch: async (input, init) => {
const headers = new Headers(init?.headers);
headers.set("x-api-key", "my-key");
return fetch(input, { ...init, headers });
},
});
Use HttpAgentServerAdapter when you want streaming transport semantics against a custom HTTP/SSE backend. Your server implements the Agent Streaming Protocol endpoints; the browser keeps using the stock adapter.
Mount the adapter directly on useStream:
import { useMemo } from "svelte";
import { useStream, HttpAgentServerAdapter } from "@langchain/svelte";
const transport = useMemo(
() =>
new HttpAgentServerAdapter({
apiUrl: window.location.origin,
threadId: "local",
paths: {
commands: "/api/threads/local/commands",
stream: "/api/threads/local/stream",
},
defaultHeaders: { Authorization: `Bearer ${token}` },
}),
[],
);
const stream = useStream({ transport });
Or share it through provideStream:
<script lang="ts">
import { useMemo } from "svelte";
import { HttpAgentServerAdapter, provideStream } from "@langchain/svelte";
const transport = useMemo(() => {
const threadId = "local";
return new HttpAgentServerAdapter({
apiUrl: window.location.origin,
threadId,
paths: {
commands: `/api/threads/${threadId}/commands`,
stream: `/api/threads/${threadId}/stream`,
},
});
}, []);
provideStream<GraphType>({ transport });
</script>
<Chat />
On the custom-adapter branch, assistantId is optional (defaults to "_") and server-only options (apiUrl, apiKey, fetch, webSocketFactory) are rejected at compile time. Memoize the adapter — recreating it on every render closes the old stream and opens a new one.
On the server, two protocol endpoints back the adapter:
app.post("/api/threads/:threadId/commands", handleCommands);
app.post("/api/threads/:threadId/stream", handleStream);
POST /commands receives protocol Command objects and returns a CommandResponse or ErrorResponse. POST /stream receives SubscribeParams and returns a filtered SSE stream.
async function handleCommands(ctx) {
const threadId = ctx.req.param("threadId") ?? "local";
const command = (await ctx.req.json()) as Command;
return ctx.json(await session(threadId).handleCommand(command));
}
async function handleStream(ctx) {
const threadId = ctx.req.param("threadId") ?? "local";
const params = (await ctx.req.json()) as SubscribeParams;
return new Response(session(threadId).stream(params), {
headers: { "cache-control": "no-cache", "content-type": "text/event-stream" },
});
}
For the SSE/HTTP transport, commands are normal JSON request/response messages. A minimal run.start handler:
async function handleCommand(command: Command): Promise<CommandResponse | ErrorResponse> {
if (command.method !== "run.start") {
return {
type: "error",
id: command.id,
error: "unknown_command",
message: `Unsupported command: ${command.method}`,
};
}
void startRun((command.params as { input?: unknown }).input);
return { type: "success", id: command.id, result: { run_id: crypto.randomUUID() } };
}
For a richer backend, this is where you would also handle interrupt resume commands, state commands, cancellation, auth checks, or application-specific validation.
Each call to /stream is an independent SSE connection. The request body is SubscribeParams:
type SubscribeParams = {
channels: Array<
| "values"
| "messages"
| "updates"
| "checkpoints"
| "tasks"
| "tools"
| "custom"
| "lifecycle"
| "input.requested"
| `custom:${string}`
>;
namespaces?: Array<string[]>;
depth?: number;
since?: number;
};
When a new stream opens, replay buffered events newer than since and matching the requested channels, namespaces, and depth. This lets selector subscriptions mount after a run has already started without forcing the graph to restart.
The server must apply the same filtering model the client asks for:
channels selects event concerns: messages, values, tools, lifecycle, tasks, custom, or named custom channels like custom:a2a.namespaces is a list of namespace prefixes. [] means the root graph; child arrays target subgraphs or nested agents.depth limits how far below a matched namespace prefix events are delivered.since replays only events after the last sequence number the client observed.The Agent Streaming Protocol reserves method: "custom" for extensions. Normalize non-protocol event methods into the custom envelope server-side:
function normalizeEvent(event: ProtocolEvent): ProtocolEvent {
if (PROTOCOL_METHODS.has(event.method)) return event;
return {
...event,
method: "custom",
params: {
...event.params,
data: { name: event.method, payload: event.params.data },
},
} as ProtocolEvent;
}
After normalization, useExtension(stream, "a2a") subscribes to custom:a2a and receives params.data.payload:
const stream = getStream<GraphType>();
const a2a = useExtension<A2AStreamEvent>(stream, "a2a");
HttpAgentServerAdapter expects SSE frames whose data: field is a JSON Agent Protocol message. Mirror event_id or seq into the SSE id: field:
function encodeSse(event: ProtocolEvent) {
const eventId = (event as { event_id?: string }).event_id;
const id = eventId ?? (typeof event.seq === "number" ? `${event.seq}` : "");
const idLine = id ? `id: ${id}\n` : "";
return new TextEncoder().encode(`${idLine}event: message\ndata: ${JSON.stringify(event)}\n\n`);
}
Keep the event object intact in data:. Do not split protocol fields between SSE event: names and JSON payloads unless you also provide a custom browser adapter that reverses that transformation.
Implement AgentServerAdapter yourself only when the stock HTTP/SSE adapter cannot describe your transport — in-memory tests, browser-native transports, a shared socket that multiplexes many logical threads, or an application bus that is not HTTP/SSE shaped.
interface AgentServerAdapter {
readonly threadId: string;
open(): Promise<void>;
close(): Promise<void>;
send(command: Command): Promise<CommandResponse | ErrorResponse | void>;
events(): AsyncIterable<Message>;
openEventStream?(params: SubscribeParams): EventStreamHandle;
getState?<S = unknown>(): Promise<{
values: S;
checkpoint?: { checkpoint_id?: string } | null;
} | null>;
getHistory?<S = unknown>(options?: {
limit?: number;
}): Promise<Array<{ values: S; checkpoint?: { checkpoint_id?: string } | null }>>;
}
If your backend can expose command and stream endpoints, prefer HttpAgentServerAdapter plus server-side protocol handling.
Typical use cases for a fully custom adapter:
The LangGraph Platform client is not constructed when a custom adapter is passed, so bundles that only use a custom adapter tree-shake the entire built-in SSE / WebSocket stack. This matters if you're embedding @langchain/svelte in a size-sensitive surface like an extension or widget.
HttpAgentServerAdapter.since so late subscribers catch up.namespaces and depth being applied server-side.custom instead of custom:<name>. custom receives all custom events; useExtension(stream, "a2a") subscribes to custom:a2a.new HttpAgentServerAdapter(...).