-
-
Notifications
You must be signed in to change notification settings - Fork 278
feat: WebSocket transport — full-duplex resumable streams #969
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlemTuzlak
wants to merge
24
commits into
main
Choose a base branch
from
feat/websocket-transport
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
db8b0c0
feat(ai): ws frame codec for websocket transport
AlemTuzlak 22f2d74
feat(ai): WebSocketLike surface + fake socket test double
AlemTuzlak cfbc42a
feat(ai): WsRunContext + synthetic per-turn request builder
AlemTuzlak 4683e31
feat(ai): toWebSocketStream conversation loop (non-durable happy path)
AlemTuzlak ee44d8f
fix(ai): widen WsRunContext.messages to drop unsound cast
AlemTuzlak df45ae6
feat(ai): durable ws pumping via shared durableStreamSource
AlemTuzlak 1832912
feat(ai): ws abort frame + close/idle/heartbeat lifecycle + malformed…
AlemTuzlak 9d9196f
feat(ai): resumeWebSocketStream read-only log replay
AlemTuzlak 6d51a4d
fix(ai): guard resumeWebSocketStream replay pump against unhandled re…
AlemTuzlak 3cbd47d
feat(ai): toWebSocketResponse/resumeWebSocketResponse CF wrappers + e…
AlemTuzlak a43820a
fix(ai): drop unused request param from resumeWebSocketResponse
AlemTuzlak f8802ab
feat(ai-client): webSocket() subscribe/send connection adapter
AlemTuzlak b62f4b2
fix(ai-client): memoize webSocket open-promise + cover bare-chunk/joi…
AlemTuzlak 229c7d9
fix(ai-client): silence eager open-promise rejection + test tidy
AlemTuzlak cdfe2bf
refactor(ai-client): extract createReconnectTracker from resumableStream
AlemTuzlak 0092106
feat(ai-client): auto-resume webSocket subscribe on drop via ?offset
AlemTuzlak 2721eb7
fix(ai-client): surface fatal ws drop to consumer instead of hanging …
AlemTuzlak 0b116f0
test(e2e): websocket transport delivery + reconnect-resume
AlemTuzlak 580f7b1
feat(examples): websocket chat route in ts-react-chat
AlemTuzlak b8513d7
feat(framework): re-export webSocket connection adapter from solid/vu…
AlemTuzlak 1522e6a
docs(resumable-streams): websocket transport
AlemTuzlak cc6cefa
chore: changeset for websocket transport
AlemTuzlak 1433ca3
fix(ai,ai-client): close resume ws on log exhaustion; guard idle-reap…
AlemTuzlak 2225a78
ci: apply automated fixes
autofix-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| '@tanstack/ai': minor | ||
| '@tanstack/ai-client': minor | ||
| '@tanstack/ai-react': minor | ||
| '@tanstack/ai-solid': minor | ||
| '@tanstack/ai-vue': minor | ||
| '@tanstack/ai-svelte': minor | ||
| '@tanstack/ai-angular': minor | ||
| --- | ||
|
|
||
| WebSocket transport: a full-duplex, resumable third transport alongside SSE and | ||
| NDJSON, reusing the same delivery-durability seam. | ||
|
|
||
| On the server, `@tanstack/ai` adds `toWebSocketStream(socket, request, { onRun, | ||
| durability, batch, heartbeatMs, idleTimeoutMs, debug })` — a portable core that | ||
| pumps a conversation over an already-accepted WHATWG `WebSocketLike` server | ||
| socket (Node via `ws`, Bun, etc.), and `toWebSocketResponse(request, { onRun, | ||
| … })`, a thin wrapper that upgrades via `WebSocketPair` and returns a 101 | ||
| `Response` on Cloudflare Workers/Durable Objects (it throws elsewhere, pointing | ||
| you at `toWebSocketStream`). Because one socket outlives many `chat()` turns | ||
| (client-tool resubmits, follow-up user messages), you pass an `onRun(ctx) => | ||
| AsyncIterable<StreamChunk>` factory instead of a prebuilt stream — the helper | ||
| calls it per inbound `RunAgentInput` frame. The socket is conversation-scoped: | ||
| it stays open across turns and closes on client close, an `{ type: 'abort', | ||
| runId }` control frame (which aborts only that turn), or the idle timeout, with a | ||
| periodic `{ type: 'ping' }` heartbeat. Durability is keyed per turn and reuses | ||
| the existing `durableStreamSource`, so server→client frames carry the same | ||
| `{ id, chunk }` envelope as NDJSON. `resumeWebSocketStream(socket, { adapter })` | ||
| and `resumeWebSocketResponse({ adapter })` replay a run read-only from the | ||
| durability log (no model call). | ||
|
|
||
| On the client, `webSocket(url, options)` (in `@tanstack/ai-client`, re-exported | ||
| from `@tanstack/ai-react`, `-solid`, `-vue`, `-svelte`, and `-angular`) is a | ||
| full-duplex `subscribe` + `send` connection adapter for `useChat`. `send()` | ||
| writes a `RunAgentInput` frame; `subscribe()` yields inbound chunks, ignores | ||
| heartbeats, unwraps durable envelopes, and auto-reconnects a dropped durable run | ||
| by reopening with `?runId=&offset=` (browsers can't set a `Last-Event-ID` | ||
| handshake header, so the offset rides in the URL). The reconnect bookkeeping | ||
| (offset de-dupe, no-progress ceiling → `StreamReconnectLimitError`) is shared | ||
| with the HTTP adapters via the new `createReconnectTracker`, and a fatal drop | ||
| surfaces to the consumer (`StreamReadError` / `StreamReconnectLimitError`) | ||
| instead of hanging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a server endpoint snippet for WebSockets.
As per coding guidelines, when a documentation page covers both server and client behavior, snippets for both halves (the server endpoint and the client consumption) must be included. The SSE section on this page includes a server snippet, but the new WebSockets section only provides the client
webSocketsnippet. Please add a brief example of the paired server endpoint (e.g., usingtoWebSocketStreamortoWebSocketResponse).🤖 Prompt for AI Agents
Source: Coding guidelines