summaryrefslogtreecommitdiffhomepage
path: root/.dispatch/transport-contract.reference.md
blob: 3a7a59ce56b90b658ae7d8346c6da3e46be68f61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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;
```