/** * Four-quadrant RPC message model. Channels and messages are * decoupled: HTTP is the client→server physical channel, SSE the server→client one; logical * messages are channel-independent, and the wire full form is a four-member discriminated union. * api/ contract layer: zero Node dependencies, importable from the browser. */ import type { z as zCore } from 'zod' type ZodIssue = zCore.core.$ZodIssue import type { Branded } from '@deepseek-ai/dsh-brand' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { InboxItemId } from '@deepseek-ai/dsh-agent/brand' /** * Message correlation id: the initiator mints it on a request; a response * echoes the matching request's rpcId and never mints a new one. */ export type RpcId = Branded<'rpc-id'> /** * Brands a string as RpcId (same precedent as core `SessionId()`). Minted by the initiator: * client-request → client mints; server-request → host mints (answerable frames get a stable * logical id, pure pushes mint a fresh one each time). * @param id - Raw id string (implementations mint UUIDs; tests may pass fixtures). * @returns The same string, branded (compile-time cast, zero runtime cost). */ export function RpcId(id: string): RpcId { return id as RpcId } /** Error code → details type map (a second table isomorphic to RpcMethodMap). New code = one row here + one branch in the error schema. */ export interface RpcErrorDetailsMap { 'bad-request': { issues: ZodIssue[] } 'cancelled': {} 'session-not-found': { sessionId: SessionId } 'model-unavailable': { provider: string; model: string } 'session-conflict': { sessionId: SessionId; requestedCwd: string; existingCwd?: string } 'workspace-attach-failed': { sessionId: SessionId; workspaceId: string } 'workspace-not-found': { workspaceId: string } 'workspace-invalid-path': { path: string } 'workspace-name-conflict': { name: string } 'workspace-move-invalid': { workspaceId: string; sessionId: SessionId; beforeSessionId?: SessionId } 'directory-unreadable': { path: string } 'directory-exists': { path: string } 'directory-create-failed': { path: string } 'directory-picker-unavailable': { capability: string } 'agent-busy': { reason: string } 'queue-item-not-found': { itemId: InboxItemId } /** A known slash command reported a usage/state error; the message is the command's own text. */ 'command-error': {} /** A leading-/ prompt named no registered command; the message names the token. */ 'unknown-command': {} 'title-invalid': { sessionId: SessionId } 'internal': {} } /** Closed error-code union (the keys of RpcErrorDetailsMap). */ export type RpcErrorCode = keyof RpcErrorDetailsMap /** * Distributive union expanded from the map: code is the discriminant, so * `switch (error.code)` narrows details. details is required (internal uses an explicit {}). */ export type RpcError = { [C in RpcErrorCode]: { code: C; message: string; details: RpcErrorDetailsMap[C] } }[RpcErrorCode] /** Business success/failure result: the result slot of a unary response; methods never throw business errors. */ export type RpcResult = { ok: true; value: T } | { ok: false; error: RpcError } /** * Fold a transport exception into the RpcResult error branch (unified error * surface; 'internal' as the catch-all code). Lives with RpcResult so every * carrier consumer folds the same way. * @param error - the thrown value from the carrier. * @returns the error branch of an RpcResult. */ export function transportError(error: unknown): RpcResult { return { ok: false, error: { code: 'internal', message: error instanceof Error ? error.message : String(error), details: {} }, } } /** * Signature-layer narrow form, request side (domain-interface view, shared by * both directions): rpcId is explicit in the signature, never mixed into the * business payload; the type tag and method are filled in by the carrier layer. */ export interface RpcRequest

{ rpcId: RpcId payload: P } /** Signature-layer narrow form, response side: rpcId always echoes the matching request. */ export interface RpcResponse { rpcId: RpcId result: RpcResult } // ---- Wire full forms: four named members of a discriminated union (discriminant = the four `type` literals) ---- /** Call initiated by the client (wire carrier: POST /api/ body). */ export interface ClientRequest { type: 'client-request' rpcId: RpcId method: string payload: unknown } /** Response to a ClientRequest (wire carrier: the HTTP response body of that POST); rpcId echoed. */ export interface ServerResponse { type: 'server-response' rpcId: RpcId result: RpcResult } /** * Message initiated by the server (wire carrier: SSE frame). Answerable interactions * (approval/question requested — stable rpcId, reused on replay) and pure pushes * (session/event etc. — rpcId identifies that one push) share this shape; whether a * response is expected is determined statically by method (a strict dichotomy, no third kind). */ export interface ServerRequest { type: 'server-request' rpcId: RpcId method: string payload: unknown } /** Response to a ServerRequest (wire carrier: POST /api/respond body); rpcId echoed, never minted anew. */ export interface ClientResponse { type: 'client-response' rpcId: RpcId result: RpcResult } /** Authoritative wire full-form union; narrow via `switch (message.type)`. */ export type RpcMessage = ClientRequest | ServerResponse | ServerRequest | ClientResponse /** * Carrier receipt (not an RpcMessage — it belongs to the carrier layer, same * discipline as "HTTP status describes only the carrier"): the HTTP response * body of the POST carrying a client-response. Late/duplicate responses yield not-pending. */ export type RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }