diff options
Diffstat (limited to '.dispatch/wire.reference.md')
| -rw-r--r-- | .dispatch/wire.reference.md | 213 |
1 files changed, 102 insertions, 111 deletions
diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md index 44b0fe7..b7003ec 100644 --- a/.dispatch/wire.reference.md +++ b/.dispatch/wire.reference.md @@ -4,98 +4,13 @@ > types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission > prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only. > -> **Orchestrator:** SNAPSHOT of `[email protected]` (compaction). Regenerate -> whenever `@dispatch/wire` changes. +> **Orchestrator:** SNAPSHOT of `[email protected]` (workspaces). Regenerate whenever `@dispatch/wire` changes. > -> **2026-06-22 delta (compaction handoff — package bumped `0.10.0` → `0.11.0`, ADDITIVE):** -> adds `CompactionResult` — the result of a compaction operation (`summary`, `messagesSummarized`, -> `messagesKept`). The summary text is the model's output; the FE doesn't render it directly (it -> becomes the conversation's first system message after compaction). -> -> **2026-06-22 delta (conversation lifecycle handoff — package bumped `0.9.0` → `0.10.0`, ADDITIVE):** -> adds `ConversationStatus` (`"active" | "idle" | "closed"`) — the per-conversation lifecycle -> status. `ConversationMeta` gains a `status` field. `active` = a turn is generating; `idle` = -> exists, not generating; `closed` = dismissed (hidden from the tab bar). Transitions are -> backend-owned: `idle → active` on turn start, `active → idle` on turn settle, `→ closed` on -> `POST /conversations/:id/close`. Pushed to all WS clients via `conversation.statusChanged` -> (see `[email protected]`). -> -> **2026-06-21 delta (conversation.open handoff — package bumped `0.8.0` → `0.9.0`, ADDITIVE):** -> adds `ConversationMeta` — metadata for a conversation (id, title, createdAt, lastActivityAt), -> returned by `GET /conversations` (the list endpoint, see `[email protected]`). -> -> **2026-06-21 delta (message-queue + steering handoff — package bumped `0.7.0` → `0.8.0`, ADDITIVE):** -> adds the per-conversation **message queue** + **steering** feature. While a turn is GENERATING, -> a client enqueues a user message (via the `chat.queue` WS op or `POST /conversations/:id/queue`, -> see `[email protected]`); it is delivered mid-turn as **steering** — injected at the next -> tool-result boundary so the model sees it alongside the tool results and can adjust course. If the -> turn ends with a non-empty queue (no tool call fired), the queue is carried into a NEW turn as its -> opening prompt (no `steering` event — the new turn's `user-message` covers it). -> -> Adds: -> - **`QueuedMessage`** (`{ id, text, queuedAt }`) — a message held in the queue (stable id for UI -> keying + dedup). -> - **`QueuePayload`** (`{ messages: QueuedMessage[] }`) — the payload of the message-queue -> extension's per-conversation `custom` surface field (`rendererId: "message-queue"`). Carried on -> the SURFACE channel (NOT the chat stream) — the queue is control/state. Empty `messages` = empty -> queue. See `transport-contract.reference.md` for the surface + the enqueue op. -> - **`TurnSteeringEvent`** (`{ type: "steering"; conversationId; turnId; text }`) — a NEW -> `AgentEvent` union member, emitted on the chat stream when the kernel drains a non-empty queue -> at a tool-result boundary. Render `text` as a USER bubble in the transcript (positioned after -> the tool-result it followed); the queue surface separately clears on drain. One event per drain; -> `text` is the combined text of all drained messages. Late-join safe (buffered into the in-flight -> turn's event buffer, mirroring `user-message`). Carry-to-new-turn does NOT emit `steering`. -> ADDITIVE to the union — if you have an exhaustive `AgentEvent` switch, add a `steering` case. -> -> **2026-06-12 delta (reasoning-effort handoff — package bumped `0.6.1` → `0.7.0`, ADDITIVE):** -> adds the **`ReasoningEffort`** type — the per-request thinking-depth ladder -> `"low" | "medium" | "high" | "xhigh" | "max"`. Provider-agnostic; the Anthropic provider maps -> levels to extended-thinking token budgets (low 4096 · medium 10240 · high 16384 · xhigh 32768 · -> max 65536); providers without a thinking knob ignore it. Resolution is SERVER-owned (do not -> re-implement): per-turn `ChatRequest.reasoningEffort` override → persisted per-conversation value -> (`GET`/`PUT /conversations/:id/reasoning-effort`, see `[email protected]`) → default -> `"high"`. Higher levels mean longer runs of `reasoning-delta` events before the first text delta. -> See the `ReasoningEffort` definition below. -> -> **2026-06-12 delta (CR-5 history windowing — package bumped `0.6.0` → `0.6.1`, DOC-ONLY):** the -> per-conversation `seq` numbering is now a WRITTEN CONTRACTUAL GUARANTEE on `StoredChunk`: -> **1-based, monotonic, gap-free** — a conversation's first chunk is always `seq === 1` and -> numbering never skips. A client holding only a windowed suffix of the log derives "older chunks -> exist server-side" purely from `oldestLoaded.seq > 1` (no `earliestSeq`/`hasOlder` field exists). -> -> **2026-06-12 delta (CR-3 user-message handoff — package bumped `0.5.0` → `0.6.0`, ADDITIVE):** adds a -> new `AgentEvent` union member `TurnInputEvent` (`{ type: "user-message"; conversationId; turnId; text }`) -> that surfaces the turn's USER prompt INTO the outward event stream. Emitted ONCE as the FIRST event of -> every turn (before `turn-start`), so it is buffered + replayed to every subscriber — live AND late-join -> — and rides `chat.delta`/NDJSON like any other event. Fixes CR-3 (a pure watcher couldn't see the prompt -> until seal). The sender still echoes its own prompt optimistically, so consumers DE-DUP against that -> (by text); a pure watcher renders it directly. Persistence/metrics unchanged. See `TurnInputEvent` below. -> -> **2026-06-12 delta (context-size handoff — package bumped `0.4.0` → `0.5.0`):** adds an OPTIONAL -> `contextSize?: number` to BOTH `TurnDoneEvent` (live `done`) and `TurnMetrics` (persisted) — the -> turn's FINAL step `inputTokens + outputTokens` (current context occupancy), NOT the aggregate -> `usage` (which overcounts multi-step turns). The two carriers are equal for the same turn. Current -> value = the LATEST turn's `contextSize`; `undefined` ⇒ render "unknown", never `0`. See the field -> doc-comments on `TurnMetrics`/`TurnDoneEvent` below. -> -> **0.3.0 changes (token + timing metrics):** -> - **Live per-step/per-turn telemetry on the event stream** (transient — NOT persisted): -> `TurnUsageEvent` gained an OPTIONAL `stepId?` (attribute tokens per step). A NEW -> `TurnStepCompleteEvent` (`type: "step-complete"`, REQUIRED `stepId`) carries the per-step -> generation timing `ttftMs?` / `decodeMs?` / `genTotalMs?` (all optional — present only when the -> runtime had a clock; `ttftMs`/`decodeMs` additionally require a first content token). `TurnDoneEvent` -> gained an OPTIONAL `durationMs?` (total turn wall-clock) + OPTIONAL `usage?` (aggregate across -> steps). `TurnToolResultEvent` gained an OPTIONAL `durationMs?` (tool execution time). -> - **Durable, replayable metrics** (persisted, keyed per turn): NEW `StepMetrics` + `TurnMetrics` -> — the persisted counterparts of the live `usage` + `step-complete` + `done` packets. Served by -> `GET /conversations/:id/metrics` (see `transport-contract.reference.md`). Build the SAME -> `TurnMetrics` shape from the live events for the in-flight turn; the durable endpoint supplies it -> for sealed turns. TPS is derived (`usage.outputTokens / (genTotalMs / 1000)`), not on the wire. -> - **0.2.0 (still current — step grouping):** `ToolCallChunk`/`ToolResultChunk` carry an OPTIONAL -> `stepId?: StepId`; `TurnToolCallEvent`/`TurnToolResultEvent` carry a REQUIRED `stepId: StepId`. -> Group batched/parallel tool calls by `stepId` equality. Live: read `event.stepId`. Replay: read -> `storedChunk.chunk.stepId` (NOT the envelope; tolerate absence). `StoredChunk` envelope is -> UNCHANGED (`{ seq, role, chunk }` — carries NO `turnId`). +> **2026-06-23 delta (workspaces handoff — package bumped `0.11.0` → `0.12.0`, ADDITIVE):** adds +> `Workspace` + `WorkspaceEntry` (a list entry with a conversation count) and a required +> `workspaceId: string` on `ConversationMeta` (`"default"` for legacy/unspecified conversations). A +> workspace is a URL-driven grouping of conversations that owns a default cwd; conversations that +> haven't set their own cwd inherit `workspace.defaultCwd`. See `backend-handoff-workspaces-reply.md`. ```ts /** @@ -299,8 +214,8 @@ export interface StepMetrics { * Durable per-turn metrics for a completed (sealed) turn — the persisted, * replayable counterpart of the live `done` event's aggregate `usage` + * `durationMs`, plus the per-step breakdown. `usage` is the aggregate across all - * steps; `steps` carries each step's `StepMetrics` in step order. Stored by - * `conversation-store` keyed by `turnId` and served by + * steps; `steps` carries each step's `StepMetrics` in step order. Persisted per + * turn by `conversation-store` (returned in turn-append order) and served by * `GET /conversations/:id/metrics`. (`turnId` is the plain wire string carried * on every `AgentEvent`, the join key to the live stream.) */ @@ -373,6 +288,7 @@ export type AgentEvent = | TurnUsageEvent | TurnStepCompleteEvent | TurnErrorEvent + | TurnProviderRetryEvent | TurnDoneEvent | TurnSealedEvent | TurnSteeringEvent; @@ -393,13 +309,15 @@ export interface TurnStartEvent { /** * The user prompt that opened this turn, surfaced INTO the turn's outward event - * stream so a WATCHER (subscribed but not the sender) can render the prompt - * mid-turn — the user message is otherwise persisted only at seal. Emitted ONCE - * as the FIRST event of the turn (before `turn-start`); buffered + replayed to - * every subscriber (live + late-join). The sender echoes its own prompt - * optimistically, so DE-DUP against that (by text); a pure watcher renders it - * directly. Carries the raw `text` passed to the provider. (Turn-scoped: it - * carries `turnId`, so a multi-turn transcript attributes each prompt to its turn.) + * stream. The user message is persisted only when the turn seals (atomically with + * the assistant reply), so without this event a client that is merely WATCHING a + * conversation (subscribed but not the sender) has no source for the prompt text + * mid-turn — it would see the streaming reply with no preceding user bubble until + * seal. Emitted once, as the FIRST event of the turn (before `turn-start`), so it + * is buffered and replayed to every subscriber — live and late-join — exactly like + * the rest of the turn. The sender already echoes its own prompt optimistically, so + * a consumer should de-dup against that (e.g. by text); a pure watcher renders it + * directly. Carries the raw prompt `text` (the same text passed to the provider). */ export interface TurnInputEvent { readonly type: "user-message"; @@ -527,6 +445,31 @@ export interface TurnErrorEvent { readonly code?: string; } +/** + * A retryable provider error is being retried with backoff. Emitted once per + * scheduled retry, BEFORE the sleep, so the UI can show "⚠ Server overloaded — + * retrying in 5s…" immediately. TRANSIENT: emitted to the frontend but NOT + * persisted into the model's message history (it never pollutes the prompt). + * + * When the retry budget is exhausted, the existing `error` event is emitted and + * the turn seals — so the final failure is still a persisted error. `attempt` is + * 0-based (the Nth retry about to happen); `delayMs` is the scheduled sleep + * before that retry fires. + */ +export interface TurnProviderRetryEvent { + readonly type: "provider-retry"; + readonly conversationId: string; + readonly turnId: string; + /** 0-based: this is the Nth retry about to happen. */ + readonly attempt: number; + /** ms the client should expect to wait before the retry fires. */ + readonly delayMs: number; + /** The endpoint's error verbatim (e.g. "HTTP 429: {…overloaded_error…}"). */ + readonly message: string; + /** The HTTP code when known (e.g. "429"). */ + readonly code?: string; +} + /** The turn has completed (model finished generating). */ export interface TurnDoneEvent { readonly type: "done"; @@ -545,11 +488,11 @@ export interface TurnDoneEvent { */ readonly usage?: Usage; /** - * **Context size** — tokens the conversation occupies right now: the turn's - * FINAL step `inputTokens + outputTokens` (the prompt sent into the last LLM - * round-trip plus that round-trip's output). This is the "tokens in context" - * figure a client renders as the chat's current context usage, and a client - * treats the LATEST turn's value as the live total. + * **Context size** — the number of tokens the conversation now occupies: this + * (the most recent) turn's FINAL step `inputTokens + outputTokens` (the full + * prompt sent into the last LLM round-trip plus that round-trip's output). This + * is the "tokens in context" figure a client renders as the chat's current + * context usage, and a client treats the LATEST turn's value as the live total. * * Deliberately NOT the aggregate `usage` above: `usage` SUMS each step's * `inputTokens`, which overcounts a multi-step / tool-calling turn because every @@ -598,10 +541,11 @@ export interface TurnSteeringEvent { // ─── Conversation metadata ─────────────────────────────────────────────────── /** - * The per-conversation lifecycle status. `active` = a turn is generating; - * `idle` = exists, not generating; `closed` = dismissed (hidden from the tab - * bar, not deleted). Transitions are backend-owned and pushed via the - * `conversation.statusChanged` WS message (see `transport-contract`). + * The lifecycle status of a conversation, used for tab persistence across + * devices. `active` = an agent is currently generating; `idle` = exists but not + * generating; `closed` = user dismissed the tab (hidden from the tab bar, not + * deleted). New conversations start as `idle`; transitions to `active` on + * turn-start, back to `idle` on turn done/error, and to `closed` on user close. */ export type ConversationStatus = "active" | "idle" | "closed"; @@ -609,7 +553,8 @@ export type ConversationStatus = "active" | "idle" | "closed"; * Metadata for a conversation, returned by `GET /conversations` (the list * endpoint). The title defaults to the first user message (truncated) and can * be set via `PUT /conversations/:id/title`. `createdAt` is set on first write; - * `lastActivityAt` is updated on every append. + * `lastActivityAt` is updated on every append. `status` tracks the tab lifecycle + * for cross-device persistence. */ export interface ConversationMeta { readonly id: string; @@ -617,7 +562,17 @@ export interface ConversationMeta { readonly lastActivityAt: number; readonly title: string; readonly status: ConversationStatus; - /** Points to the archive conversation with full pre-compaction history. */ + /** + * The workspace this conversation belongs to. Always present; reads as + * `"default"` for legacy conversations that were never explicitly assigned. + * Conversations created with no `workspaceId` default to `"default"`. + */ + readonly workspaceId: string; + /** + * Set on a compacted conversation: points to the archive conversation ID + * that holds the full pre-compaction history. Absent on conversations + * that have never been compacted. + */ readonly compactedFrom?: string; } @@ -627,6 +582,8 @@ export interface ConversationMeta { * Result of a compaction operation. `summary` is the text the model produced; * `messagesKept` is how many recent messages were retained after the summary; * `messagesSummarized` is how many old messages were replaced by the summary. + * `newConversationId` is the ID of the new conversation that holds the full + * pre-compaction history (non-destructive — the original history is preserved). */ export interface CompactionResult { readonly summary: string; @@ -634,4 +591,38 @@ export interface CompactionResult { readonly messagesSummarized: number; readonly messagesKept: number; } + +// ─── Workspaces ────────────────────────────────────────────────────────────── + +/** + * A named, URL-driven grouping of conversations that owns a default cwd. + * Every conversation belongs to exactly one workspace; conversations that + * haven't set their own per-conversation cwd inherit `defaultCwd`. + * + * Workspaces are backend-owned (so cross-device just works): the workspace + * entity and each conversation's `workspaceId` live server-side. The + * `"default"` workspace is always present and non-deletable; conversations + * created with no `workspaceId` are assigned to `"default"`. + */ +export interface Workspace { + /** The URL slug (immutable). Lowercase `[a-z0-9-]`, 1–40 chars. */ + readonly id: string; + /** Display title (editable). Defaults to `id` on creation. */ + readonly title: string; + /** The workspace's default cwd, or `null` (fall through to server default). */ + readonly defaultCwd: string | null; + /** Epoch-ms when the workspace was first created. */ + readonly createdAt: number; + /** Epoch-ms of the most recent conversation activity in this workspace. */ + readonly lastActivityAt: number; +} + +/** + * A workspace entry in the list response (`GET /workspaces`) — a `Workspace` + * plus a conversation count. + */ +export interface WorkspaceEntry extends Workspace { + /** Number of conversations assigned to this workspace. */ + readonly conversationCount: number; +} ``` |
