diff options
| author | Adam Malczewski <[email protected]> | 2026-06-06 21:13:58 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-06 21:13:58 +0900 |
| commit | fedf9c2695476e9ee6f95776b0244acfc37f022f (patch) | |
| tree | 313b2c39e1a7677f2d0997d74df8241986f3283a /packages/transport-contract/src | |
| parent | 61b6e24c7abb4eebf94da0a0498a68a1bb8ba92e (diff) | |
| download | dispatch-fedf9c2695476e9ee6f95776b0244acfc37f022f.tar.gz dispatch-fedf9c2695476e9ee6f95776b0244acfc37f022f.zip | |
feat(transport-ws,transport-contract): multiplex chat ops onto the surface WS
Add chat WS ops (chat.send / chat.delta / chat.error) + unified
WsClientMessage/WsServerMessage unions to @dispatch/transport-contract
(imports ui-contract; surface protocol unchanged — additive non-colliding
type variants, no channel wrapper). transport-ws drives
sessionOrchestrator.handleMessage, streaming each AgentEvent as chat.delta
over the same connection that carries surface ops; per-connection
AbortController cancels in-flight turns on socket close; error-isolated.
Verified live: one WS connection delivered the surface catalog AND a real
flash chat turn (chat.delta stream, reply 'Hello my friend').
Completes the FE Slice 2 backend prereqs. typecheck clean, 485 vitest + 80 bun,
biome clean.
Discovered (separate, pre-existing): runtime does not emit
turn-start/done/turn-sealed on either transport — needed for FE cache-commit;
tracked in tasks.md.
Diffstat (limited to 'packages/transport-contract/src')
| -rw-r--r-- | packages/transport-contract/src/index.ts | 65 |
1 files changed, 61 insertions, 4 deletions
diff --git a/packages/transport-contract/src/index.ts b/packages/transport-contract/src/index.ts index 9d8f6f4..cdddad6 100644 --- a/packages/transport-contract/src/index.ts +++ b/packages/transport-contract/src/index.ts @@ -1,18 +1,26 @@ /** - * Transport contract — the typed description of Dispatch's HTTP API. + * Transport contract — the typed description of Dispatch's client–server API + * (HTTP + WebSocket). * * This package is types-only (zero runtime). It is the single shared surface * every client imports to know how to talk to the backend — the CLI, the web - * frontend (in its own repo), any third-party client — and the transport-http - * server imports to know what it must accept and emit. + * frontend (in its own repo), any third-party client — and the transport-http / + * transport-ws servers import to know what they must accept and emit. * * Each side owns its OWN (de)serialization: there is deliberately no shared * parse/serialize helper here (isolation-over-DRY). The contract is the SHAPES, * not the codec. The streaming response payload is the kernel's `AgentEvent` * union, re-exported here so a client has one import for the whole wire. + * + * The WebSocket carries BOTH chat ops (defined here) and surface ops (defined in + * `@dispatch/ui-contract`) over one connection; the unified `WsClientMessage` / + * `WsServerMessage` unions below compose them. Chat ops are new, non-colliding + * `type` variants — there is no channel wrapper, so the shipped surface protocol + * is unchanged. */ -import type { StoredChunk } from "@dispatch/wire"; +import type { SurfaceClientMessage, SurfaceServerMessage } from "@dispatch/ui-contract"; +import type { AgentEvent, StoredChunk } from "@dispatch/wire"; export type { AgentEvent, StoredChunk } from "@dispatch/wire"; @@ -83,3 +91,52 @@ export interface ConversationHistoryResponse { readonly chunks: readonly StoredChunk[]; readonly latestSeq: number; } + +// ─── WebSocket chat ops ─────────────────────────────────────────────────────── +// The persistent WS connection multiplexes chat ops (below) with surface ops +// (`@dispatch/ui-contract`). The unified unions at the bottom compose both. Chat +// `type`s are namespaced (`chat.*`) so they never collide with surface ones. + +/** + * Client → server: start or continue a turn over the WS connection. Carries the + * same fields as the HTTP `ChatRequest` (so one shape drives both transports); + * omit `conversationId` to start fresh — the resolved id arrives on the streamed + * `AgentEvent`s (each carries `conversationId`). + */ +export interface ChatSendMessage extends ChatRequest { + readonly type: "chat.send"; +} + +/** + * Server → client: one `AgentEvent` from an in-flight turn (text-delta, + * tool-call, usage, done, turn-sealed, …). The client folds these into its + * transcript exactly as it folds the HTTP NDJSON stream — same events, different + * carrier. + */ +export interface ChatDeltaMessage { + readonly type: "chat.delta"; + readonly event: AgentEvent; +} + +/** + * Server → client: a chat-scoped TRANSPORT error — e.g. a malformed `chat.send` + * or a failure before a turn could start. (Errors DURING a turn arrive as a + * `TurnErrorEvent` inside a `chat.delta`.) + */ +export interface ChatErrorMessage { + readonly type: "chat.error"; + readonly conversationId?: string; + readonly message: string; +} + +/** + * Every client → server WS message: surface ops (`@dispatch/ui-contract`) + chat + * ops. A server discriminates on `type`. + */ +export type WsClientMessage = SurfaceClientMessage | ChatSendMessage; + +/** + * Every server → client WS message: surface ops (`@dispatch/ui-contract`) + chat + * ops. A client discriminates on `type`. + */ +export type WsServerMessage = SurfaceServerMessage | ChatDeltaMessage | ChatErrorMessage; |
