summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-contract/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-06 21:13:58 +0900
committerAdam Malczewski <[email protected]>2026-06-06 21:13:58 +0900
commitfedf9c2695476e9ee6f95776b0244acfc37f022f (patch)
tree313b2c39e1a7677f2d0997d74df8241986f3283a /packages/transport-contract/src
parent61b6e24c7abb4eebf94da0a0498a68a1bb8ba92e (diff)
downloaddispatch-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.ts65
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;