diff options
| author | Adam Malczewski <[email protected]> | 2026-06-06 23:43:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-06 23:43:43 +0900 |
| commit | fac44794432928d0341728642fd70eef87837da4 (patch) | |
| tree | 53445547f98bf5e798966efb339ce67cd8ebd20b /.dispatch/transport-contract.reference.md | |
| parent | f1409cd46d5a3cfb9002cbcdfd4ab947ac6846aa (diff) | |
| download | dispatch-web-fac44794432928d0341728642fd70eef87837da4.tar.gz dispatch-web-fac44794432928d0341728642fd70eef87837da4.zip | |
Slice 2 unblock: pin wire + transport-contract; mirror contracts
- pin @dispatch/wire + @dispatch/transport-contract (+ ui-contract) as file:
deps @0.1.0; overrides{} workaround for their workspace:* deps (bun can't
resolve workspace: from outside the monorepo)
- add fake-indexeddb (dev) for the upcoming IndexedDB adapter tests
- mirror wire + transport-contract into .dispatch/*.reference.md for headless
agents; point package-agent.md at all three references
- backend-handoff.md: convert to a living FE<->backend seam doc
Verified green: svelte-check 0/0, vitest 91, biome clean, build ok; contract
import smoke-test passes.
Diffstat (limited to '.dispatch/transport-contract.reference.md')
| -rw-r--r-- | .dispatch/transport-contract.reference.md | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md new file mode 100644 index 0000000..3a7a59c --- /dev/null +++ b/.dispatch/transport-contract.reference.md @@ -0,0 +1,125 @@ +# `@dispatch/transport-contract` — in-repo reference (read THIS, not node_modules) + +> MIRRORS the backend's `@dispatch/transport-contract` package source so headless FE agents can read +> the HTTP + WebSocket wire shapes WITHOUT following the `file:` dep symlink out of this repo (which +> hangs on a permission prompt). Your CODE still imports `@dispatch/transport-contract` normally — +> this file is for READING only. +> +> **Orchestrator:** SNAPSHOT of `[email protected]`. Regenerate whenever it changes. +> Depends on `@dispatch/wire` (see `wire.reference.md`) + `@dispatch/ui-contract` +> (see `ui-contract.reference.md`). + +## Endpoints (backend, confirmed live — CORS wildcard `*`, HTTP port 24203, WS port 24205) + +- `POST /chat` — body `ChatRequest` (JSON); response NDJSON stream, one `AgentEvent` per line; + resolved id also in `X-Conversation-Id` header. +- `GET /models` — `ModelsResponse`. +- `GET /conversations/:id?sinceSeq=<n>` — `ConversationHistoryResponse`: RAW, append-order, + seq-ordered slice with `seq > n` (NOT reconciled — dangling tool-calls returned as-is). + `latestSeq` = last chunk's `seq`, or the requested `sinceSeq` when caught up (empty `chunks`). +- WebSocket on :24205 — ONE path-agnostic socket multiplexes surface ops + (`@dispatch/ui-contract`) + chat ops (below). Open once, send `WsClientMessage`, receive + `WsServerMessage`. Live `AgentEvent` deltas carry `conversationId`+`turnId` but **no `seq`** + (seq lives only on `StoredChunk`, obtained via the `sinceSeq` sync after `turn-sealed`). +- DEFERRED (not built; do not depend on): `GET /conversations` (list), `POST /conversations/:id/cancel`. + +```ts +/** + * Transport contract — the typed description of Dispatch's client–server API + * (HTTP + WebSocket). Types-only (zero runtime). Each side owns its own + * (de)serialization — the contract is the SHAPES, not the codec. + * + * The WebSocket carries BOTH chat ops (here) and surface ops (in + * `@dispatch/ui-contract`) over one connection; the unified `WsClientMessage` / + * `WsServerMessage` unions below compose them. Chat ops are new, non-colliding + * `type` variants (`chat.*`) — the shipped surface protocol is unchanged. + */ + +import type { SurfaceClientMessage, SurfaceServerMessage } from "@dispatch/ui-contract"; +import type { AgentEvent, StoredChunk } from "@dispatch/wire"; + +export type { AgentEvent, StoredChunk } from "@dispatch/wire"; + +/** + * Request body for `POST /chat` (sent as JSON). + * + * The response is an NDJSON stream: one JSON-encoded `AgentEvent` per line. + * The resolved conversation id is also returned in the `X-Conversation-Id` + * response header (useful when `conversationId` was omitted). + */ +export interface ChatRequest { + /** The conversation to continue. Omit to start fresh — server mints an id (X-Conversation-Id). */ + readonly conversationId?: string; + /** The user's message text for this turn. */ + readonly message: string; + /** Model name in `<credentialName>/<model>` form (one of `GET /models`). Omit = server default. */ + readonly model?: string; + /** Working directory for this turn's tool execution. Defaults server-side. Not part of the prompt. */ + readonly cwd?: string; +} + +/** + * Response body for `GET /models` — the model catalog. Each entry is a model + * name in `<credentialName>/<model>` form (exactly `ChatRequest.model`). + */ +export interface ModelsResponse { + readonly models: readonly string[]; +} + +/** + * Response body for `GET /conversations/:id?sinceSeq=<n>` — the incremental + * read-side history endpoint a long-lived client uses to (re)hydrate cheaply. + * + * `chunks` is the RAW, append-order, seq-ordered slice with `seq > sinceSeq` + * (or the whole log when `sinceSeq` is omitted/0). NOT reconciled: a dangling + * tool-call is returned as-is. `latestSeq` is the `seq` of the LAST chunk, or — + * when the slice is empty (caught up) — the requested `sinceSeq` (0 for a full + * read of an empty conversation). After applying, the client's new cursor is + * always `latestSeq`; empty `chunks` means "nothing new past your cursor". + */ +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`). Chat `type`s are namespaced (`chat.*`) so they +// never collide with surface ones. + +/** + * Client → server: start or continue a turn over the WS connection. Same fields + * as the HTTP `ChatRequest`; 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, …). Fold these into the transcript + * exactly as 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 + chat ops. Discriminate on `type`. */ +export type WsClientMessage = SurfaceClientMessage | ChatSendMessage; + +/** Every server → client WS message: surface ops + chat ops. Discriminate on `type`. */ +export type WsServerMessage = SurfaceServerMessage | ChatDeltaMessage | ChatErrorMessage; +``` |
