feat: pluggable client transport with a plain WebSocket option - #83
Merged
Merged
Conversation
UseAIClient now takes a UseAITransport instead of building its own Socket.IO socket. Two transports ship: SocketIOTransport keeps today's behaviour and stays the default, WebSocketTransport carries JSON frames over a plain WebSocket for servers that do not speak Socket.IO. The server serves both on one port. webSocketPath (default '/ws') adds a plain listener beside Socket.IO in both runtime adapters. ClientSession.socket narrows to ClientConnection, which a Socket.IO socket satisfies, so plugins are unchanged. UseAIProvider accepts a transport prop, read once so an inline object does not churn the connection. new UseAIClient(url) and a provider given only serverUrl behave exactly as before. The UseAIClient suite now runs table-driven over both transports. An integration test drives a real WebSocketTransport through a full run on both the Bun and Node adapters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mm-zacharydavison
marked this pull request as draft
September 3, 2026 16:09
…cket - UseAIProvider takes exactly one of serverUrl or transport, as a discriminated union on UseAIConfig. Context serverUrl becomes optional. - The server runs one listener: transport: 'socketio' | 'websocket', default 'socketio'. No webSocketPath. The plain listener serves at '/'. The Docker image reads TRANSPORT. - Downstream frames are bare AG-UI events. agents and config travel as AG-UI CUSTOM events, so the wire is AG-UI rather than a bespoke envelope. - WebSocketTransport reconnects through partysocket instead of its own loop. - Protocol doc rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UseAITransport gains a readonly url. Both bundled transports already take one. UseAIContextValue.serverUrl is a required string again, populated from the transport when the provider was given one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client: UseAITransport is now onEvent(AGUIEvent) + onConnectionChange.
The five named channels, the handler registry and the agents/config
special case in WebSocketTransport are gone. SocketIOTransport presents
its legacy agents/config events as AG-UI CUSTOM events, and
UseAIClient.handleEvent gains one CUSTOM branch. SocketIOTransport loses
options nothing used. The provider resolves its target once.
Server: ClientConnection is the whole per-connection boundary (id,
ipAddress, connected, emit, onMessage, onClose). SocketIOClientConnection
and WebSocketClientConnection own their protocol details; server.ts has
one acceptConnection. handleClientMessage takes the session it is given.
The agents payload is built once. Bun adapter returns {path, upgrade,
websocket} per listener and keeps the connection on ws.data. Binary
frames are ignored on both runtimes. Listener member types are named.
X-Forwarded-For parsing is one helper.
Tests share one FakeWebSocket, one socket.io mock and one waitUntil.
The negative transport test checks a raw socket close instead of
waiting out a 5s timeout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Simplification pass in 6970d43, net -271 lines on the branch.
|
mm-zacharydavison
requested review from
masudahiroto and
yuma2024
and removed request for
masudahiroto
September 4, 2026 10:40
mm-zacharydavison
marked this pull request as ready for review
September 4, 2026 10:40
masudahiroto
approved these changes
Sep 11, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
🧑🦱 @masudahiroto & @yuma2024 : I want to use
use-aifor Cordless UI, so we can also be users of the library and contribute / test it.But Cordless is a Go application, so cannot run Socket.IO for transport, so I added support for plain WebSocket transport, and an abstraction for transport.
Please check it.
Summary
UseAIClientbuilds its own Socket.IO socket today, so a Socket.IO server is the only server it can reach. This PR gives the client a transport seam and ships two transports.SocketIOTransportkeeps today's behaviour. It is the default.WebSocketTransportcarries AG-UI events as JSON text frames over a plain WebSocket. Use it to reach a server that does not serve Socket.IO. That server does not have to be Node.Nothing existing changes.
new UseAIClient(url)still works. AUseAIProvidergivenserverUrlstill connects over Socket.IO.Client
UseAITransporthas five members:connect,disconnect,send,on,connected.UseAIClienttakesstring | UseAITransport.UseAIConfigis a discriminated union: giveUseAIProviderexactly one ofserverUrlortransport. The provider readstransportonce, on the first render, so an inline object does not reconnect the client on every render.UseAIContextValue.serverUrlbecomes optional.WebSocketTransportreconnects throughpartysocket, with the same limits as Socket.IO: indefinite, one second doubling to ten.connect_errorstay insideSocketIOTransport. Both are specific to Socket.IO.Server
transport?: 'socketio' | 'websocket'(default'socketio'). The server runs one listener. With'websocket', it does not create a Socket.IO server; it accepts WebSocket upgrades at/.RuntimeListenerunion. Bun usesBun.serve's own upgrade. Node useswsinnoServermode.ClientSession.socketnarrows fromSockettoClientConnection(id,connected,emit). A Socket.IO socket satisfies it structurally, soFeedbackPluginandWorkflowsPluginare unchanged.TRANSPORT.Framing
Upstream, the client sends each
UseAIClientMessageas one frame, with nothing around it. Downstream, the server sends one AG-UI event per frame.agentsandconfigtravel as AG-UICUSTOMevents. The client ignores an event type or a custom name it does not know. Seedocs/websocket-protocol.md.Tests
client.test.tsruns table-driven over both transports: 64 tests, up from 29. Each harness drives its transport at the wire level.SocketIOTransport.test.tskeeps thesocket.io-clientmodule mock and coversconnect_error, theio()options and upgrade logging.WebSocketTransport.test.tscovers framing,CUSTOMrouting, unknown names, malformed frames,connected, reconnection, anddisconnect()stopping it.websocket-transport.integration.test.tsdrives a realWebSocketTransportagainst a realtransport: 'websocket'server through prompt → tool call → tool result →RUN_FINISHED, on both the Bun and Node adapters. It also checks that a default server refuses a plain WebSocket.bun run testandbun run buildpass.apps/examplepassessuggestions.e2ewith real API calls.tool-approval.e2eanddynamic-tool-registration.e2efail identically onmain, so they are pre-existing.Dependencies
packages/clientaddspartysocket.packages/serveraddswsas a dependency and@meetsmore-oss/use-ai-clientas a devDependency, so the integration test imports the real transport rather than a stub.Not in this PR
No version bump or release. The change is additive, so a minor bump fits when you release.
🤖 Generated with Claude Code