/** * SSE wire-protocol frame shapes — the byte-level contract between the * hub's chat stream emitter (`multi-platform-hub/lib/utils/claude-util.ts`) * and the lib's SSE decoder (`./decode.ts`). * * Wire format (custom byte framing, NOT standard SSE `data:` lines): * * [JSON frame]\0 [JSON frame]\0 … \x1E \x1F [JSON usage trailer] * * 1. LEADING frames — each a JSON object terminated by `\0` * (FRAME_TERMINATOR). Status, metadata, routing, live thinking * deltas, usage:start, tool approval frames. * 2. ONE `\x1E` byte (END_OF_LEADING) — flips the stream into raw * UTF-8 answer-text mode. * 3. `\x1F` (TRAILER_SENTINEL) + one JSON usage frame running to * stream end (no terminator). * * Server-safe: no React, no browser APIs beyond TextEncoder/TextDecoder. */ /** Terminates every leading JSON frame. */ export declare const FRAME_TERMINATOR = "\0"; /** Record Separator — end of leading frames, start of raw answer text. */ export declare const END_OF_LEADING = "\u001E"; /** Unit Separator — start of the trailing usage JSON frame. */ export declare const TRAILER_SENTINEL = "\u001F"; /** Server-side routing complexity tiers. Mirror of the hub's * `lib/constants/ai-models.ts` union — kept as a lib-local literal type * so the wire SSOT has zero hub imports. The DECODER accepts any string * (older/newer servers may ship values outside this union). */ export type RouteComplexity = 'trivial' | 'default' | 'complex' | 'deep'; /** * Routing decision frame. Emitted once per turn as a LEADING frame * (between metadata and the `\x1E` end-of-leading-frames sentinel) when * the server's `decideRoute()` produces a `Route`. * * Field names use the `routed*` prefix on PURPOSE — the client's * catch-all leading-frame branch inspects `meta.model` / `meta.modelLabel`. * A bare `model` field on this frame would OVERWRITE the metadata frame's * modelLabel and break the badge. The `routed` prefix * sidesteps that collision. * * Older clients that don't recognize `kind: 'routing'` silently drop * the frame (the catch-all only matches frames carrying known fields * like `sources` / `modelLabel`). */ export interface RoutingFrame { kind: 'routing'; routedComplexity: RouteComplexity; routedModel: string; routedThinkingBudget: number; } /** * Optional field on the trailing usage frame's `breakdown`. Surfaces * per-route attribution to the admin test console so an operator can * see "this turn ran on Haiku/trivial/$0.0005" without re-parsing the * stream. Additive: clients that ignore unknown keys keep working. */ export interface RoutedAnswerBreakdown { model: string; complexity: RouteComplexity; thinkingBudget: number; } /** First frame on every stream — lets the shell render "Thinking…" * within ~200ms, before any upstream byte arrives. */ export interface StatusThinkingFrame { status: 'thinking'; } /** Per-turn metadata frame. The emitter sends `options.metadata` * verbatim. All fields optional — the decoder's catch-all treats ANY * frame that didn't match a `kind` discriminant as metadata-ish. */ export interface ChatMetadataFrame { /** Retrieval sources for the source-chip strip. Host-defined row * shape (`ChatSource` on the adapter side) — opaque at wire level. */ sources?: unknown[]; /** Raw model id (e.g. 'claude-sonnet-x'). Presence alone triggers the * client's meta merge; the value itself is not stored. */ model?: string; modelLabel?: string; provider?: string; contextWindowMaxTokens?: number; /** Per-message viewport-positioning hint ('top' | 'bottom'). */ scrollAnchor?: string; /** SERVER-minted conversation trace id (`chat_conversations.id`), * echoed on every turn's leading metadata frame. The client never * generates ids — the first turn of a session goes out id-less, the * server mints one and echoes it here, and the adapter persists the * echoed id as its ONLY local state (see * `chat-conversation-storage.ts` on the consumer side). */ conversationId?: string; } /** Live adaptive-thinking delta. WIRE IS ALREADY DELTA (Anthropic * thinking_delta slices) — decoders must NOT diff or re-accumulate. */ export interface ThinkingDeltaFrame { kind: 'thinking-delta'; text: string; } /** Usage frame emitted as a LEADING frame right after Anthropic's * message_start (stage='start'). */ export interface UsageStartFrame { kind: 'usage'; stage: 'start'; input_tokens: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; } /** Model preamble prose written BEFORE a tool_use block — emitted as a * standalone leading frame so it isn't stranded behind the approval * card (the `\x1E` sentinel is suppressed on tool turns). */ export interface TextLeadingFrame { kind: 'text-leading'; text: string; } /** Tool invocation failed server-side (proposal persist error, * validation, ownership denial …). */ export interface ToolErrorFrame { kind: 'tool_error'; toolName?: string; toolUseId?: string; message: string; } export interface ApprovalRequestField { label: string; value: string; } /** The model called a write tool; the server persisted a proposal and * ships a card-ready payload. Shape mirrors the hub's `onToolUse` * return in `lib/data/doc-chat-utils.ts`. */ export interface ApprovalRequestFrame { kind: 'approval_request'; proposalId: string; toolName: string; title?: string; fields?: ApprovalRequestField[]; preamble?: string | null; args?: Record; expiresAt?: string; ttlSeconds?: number; } /** A single write-tool turn produced MULTIPLE proposals (batch * operations — "delete both", "close all three"). The SERVER is the * authority on the grouping: it collects the turn's proposals and * ships ONE batch frame instead of N `approval_request` frames, so * clients render the bulk-approval component without any adjacency * heuristics. Single-proposal turns keep the classic * `approval_request` frame. */ export interface ApprovalBatchFrame { kind: 'approval_batch'; /** Stable batch anchor — client-side only (locates the batch segment * for status flips). MUST NOT equal any row's `proposalId`: the * click-time optimistic status flip keys on this id, and an anchor * that doubles as a row id would falsely tick that row's execution * as succeeded before its confirm ran (masking a failed write). The * server emits `batch:`. */ batchId: string; proposals: Array<{ proposalId: string; toolName: string; title?: string; fields?: ApprovalRequestField[]; expiresAt?: string; ttlSeconds?: number; }>; } /** Server-driven post-approve / post-reject frame (confirm-tool route). * Mirror of the hub's `DecisionResolvedFrame` in * `lib/data/chat-agent-utils.ts` (tool_name widened to string — the * known-write-tool whitelist is host-side knowledge). */ export interface DecisionResolvedFrame { kind: 'decision_resolved'; /** The proposal this decision resolved — locates the SOURCE approval * card so the shell can flip its status + append the receipt. */ proposalId: string; ok: boolean; action: 'approved' | 'rejected'; tool_name?: string; result?: { ticket_id?: string; status?: string | null; mirror_synced?: boolean; }; /** True when the server WILL pipe an auto-continuation turn after * this frame. */ willAutoContinue: boolean; /** Pre-rendered receipt copy from the source strategy's * `receiptRenderer` — client renders verbatim. */ receiptText?: string; } /** Every leading frame the emitter can produce. */ export type SseLeadingFrame = StatusThinkingFrame | ChatMetadataFrame | RoutingFrame | ThinkingDeltaFrame | UsageStartFrame | TextLeadingFrame | ToolErrorFrame | ApprovalRequestFrame | ApprovalBatchFrame | DecisionResolvedFrame; /** Card/chip emission counts for admin adoption tooling. */ export interface UsageTelemetry { cards: number; chips: number; sentences: number; answerLen: number; } /** Trailing usage frame — `\x1F` + JSON, runs to stream end (no * terminator). `stage: 'display'` is a legacy alias the decoder treats * identically to `'end'`. */ export interface SseTrailingUsageFrame { kind: 'usage'; stage: 'end' | 'display'; input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; hit_rate_pct?: number; telemetry?: UsageTelemetry; /** Cross-call usage breakdown (Haiku rewriter / classifier / * summarizer + routed-answer attribution). */ breakdown?: { haikuRewriter?: { input: number; output: number; }; haikuClassifier?: { input: number; output: number; }; haikuSummarizer?: { input: number; output: number; }; routedAnswer?: RoutedAnswerBreakdown; }; /** Gated deep-debug payload (only present when the request carried * the privileged debug secret). */ debug?: Record; } //# sourceMappingURL=frames.d.ts.map