@workerdeck/protocol 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Strebitzer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @workerdeck/protocol
2
+
3
+ The WorkerDeck wire protocol: typed session events, commands, and REST shapes shared by the
4
+ server and every client. Dependency-free, browser-safe. This protocol is the product boundary —
5
+ versioned from day one.
6
+
7
+ Part of [WorkerDeck](https://github.com/workerdeck/workerdeck), the web-controlled
8
+ Agent SDK session runner. Everything else in the stack depends on this package; it depends on
9
+ nothing. [`@workerdeck/core`](https://www.npmjs.com/package/@workerdeck/core) produces these
10
+ events, [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) puts them
11
+ on the wire, and [`@workerdeck/client`](https://www.npmjs.com/package/@workerdeck/client)
12
+ consumes them. Anthropic API message content is modeled structurally (`ApiMessage`,
13
+ `ContentBlock`) so browsers can render transcripts without the Agent SDK.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @workerdeck/protocol
19
+ ```
20
+
21
+ Type-only for most consumers; the runtime exports are `PROTOCOL_VERSION` and
22
+ `supportsPermissionMode()`.
23
+
24
+ ## Usage
25
+
26
+ One session = one ordered stream of `SessionEvent`s, each stamped with a monotonically increasing
27
+ `seq`, plus a small `SessionCommand` set. Clients attach over WebSocket, optionally replaying from
28
+ a known `seq`, and drive the session with commands:
29
+
30
+ ```ts
31
+ import {
32
+ PROTOCOL_VERSION,
33
+ type ServerFrame,
34
+ type SessionCommand,
35
+ } from '@workerdeck/protocol'
36
+
37
+ ws.onmessage = ({ data }) => {
38
+ const frame = JSON.parse(data) as ServerFrame
39
+ if (frame.type === 'attached' && frame.protocolVersion !== PROTOCOL_VERSION) {
40
+ throw new Error('protocol mismatch')
41
+ }
42
+ if (frame.type === 'event' && frame.event.type === 'assistant_message') {
43
+ render(frame.event.message) // ApiMessage — plain Anthropic content blocks
44
+ }
45
+ }
46
+
47
+ const approve: SessionCommand = { type: 'permission_decision', requestId, behavior: 'allow' }
48
+ ws.send(JSON.stringify(approve))
49
+ ```
50
+
51
+ `PROTOCOL_VERSION` is bumped on any breaking change to events, commands, or REST shapes; the
52
+ server reports it in the `attached` (and `queue_attached`) frame so clients can detect skew.
53
+
54
+ ## At a glance
55
+
56
+ **Events (server → client)** — `system_init`, `status_changed`, `capabilities`, `model_changed`,
57
+ `permission_mode_changed`, `context_usage`, `rate_limit`, `assistant_message`, `user_message`,
58
+ `stream_delta`, `turn_result`, `permission_requested`, `permission_resolved`,
59
+ `execution_dispatched` / `execution_result` / `execution_failed` (tool-execution lifecycle,
60
+ correlated by `executionId`), `file_delivered`, `sdk_event` (forward-compatible passthrough for
61
+ unmodeled SDK messages), `session_error`, `session_closed`.
62
+
63
+ **Commands (client → server)** — `user_message`, `permission_decision`, `interrupt`,
64
+ `set_permission_mode`, `set_model`, `tool_call_result` (answering a bridged execution), `close`.
65
+
66
+ **Other server frames** — `attached`, `event`, `tool_call_request` / `tool_call_canceled` (the
67
+ browser tool bridge: the server asks an attached client to run a *sandboxed* tool call), and
68
+ `protocol_error`.
69
+
70
+ **REST shapes** — `CreateSessionRequest` / `SessionInfo` and their response wrappers,
71
+ `ResolvePermissionRequest` (the REST counterpart of `permission_decision`),
72
+ `SdkSessionSummary` for listing the Agent SDK's on-disk sessions to offer resume,
73
+ `ProfileInfo` for what a session may run as, `ListSessionFilesResponse` for a session's
74
+ deliverables, and `SubmitExecutionResultRequest` for delivering a deferred execution's result.
75
+
76
+ **Job queue** — `CreateJobRequest` / `JobInfo` / `JobEvent` (including `job_parked` /
77
+ `job_resumed`) / `QueueStats` and the `QueueServerFrame` union for the one-way queue WebSocket,
78
+ used when the server mounts the
79
+ [`@workerdeck/queue`](https://www.npmjs.com/package/@workerdeck/queue) routes.
80
+
81
+ Two engines ride this one protocol: `SessionInfo.engine` says which (`claude` or `provider`), and
82
+ `supportsPermissionMode(engine, mode)` — a real runtime export, the single source of truth for the
83
+ restriction — is what create forms filter with and the gateway rejects with.
84
+
85
+ Forward compatibility is deliberate: unknown content blocks fall back to `UnknownBlock`, unions
86
+ the SDK may grow (`apiKeySource`, rate-limit fields) stay `string`, and unmodeled SDK messages
87
+ ride through as `sdk_event` rather than breaking older clients.
88
+
89
+ ## License
90
+
91
+ MIT © Tobias Strebitzer —
92
+ [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE)
@@ -0,0 +1,899 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * @workerdeck/protocol — the wire protocol between a workerdeck server and its clients.
4
+ *
5
+ * One session = one ordered stream of {@link SessionEvent}s (each stamped with a monotonically
6
+ * increasing `seq`) plus a small command set ({@link SessionCommand}). Clients attach over
7
+ * WebSocket, optionally replaying from a known `seq`, and drive the session with commands.
8
+ *
9
+ * This package is dependency-free and browser-safe. Anthropic API message content is modeled
10
+ * structurally (see {@link ApiMessage}) so clients don't need the Agent SDK to render transcripts.
11
+ */
12
+ /** Bumped on any breaking change to events, commands, or REST shapes. */
13
+ declare const PROTOCOL_VERSION = 4;
14
+ /**
15
+ * - `starting` — runner spawned, waiting for the SDK init handshake
16
+ * - `running` — a turn is in progress
17
+ * - `awaiting_approval` — blocked on at least one pending permission request
18
+ * - `idle` — between turns; accepting user messages
19
+ * - `parked` — waiting on a deferred tool execution. The live runner has been torn
20
+ * down and the session's state persisted; delivering the execution's result
21
+ * (`POST {basePath}/executions/:executionId/result`) rehydrates it under the same
22
+ * id and the run continues. Not terminal.
23
+ * - `failed` — the underlying query errored; terminal
24
+ * - `closed` — closed by a client or the host; terminal
25
+ */
26
+ type SessionStatus = 'starting' | 'running' | 'awaiting_approval' | 'idle' | 'parked' | 'failed' | 'closed';
27
+ type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';
28
+ type TextBlock = {
29
+ type: 'text';
30
+ text: string;
31
+ };
32
+ type ThinkingBlock = {
33
+ type: 'thinking';
34
+ thinking: string;
35
+ };
36
+ type ToolUseBlock = {
37
+ type: 'tool_use';
38
+ id: string;
39
+ name: string;
40
+ input: unknown;
41
+ };
42
+ type ToolResultBlock = {
43
+ type: 'tool_result';
44
+ tool_use_id: string;
45
+ content?: string | Array<{
46
+ type: string;
47
+ text?: string;
48
+ [key: string]: unknown;
49
+ }>;
50
+ is_error?: boolean;
51
+ };
52
+ /** Forward-compatible fallback for block types this protocol version doesn't model. */
53
+ type UnknownBlock = {
54
+ type: string;
55
+ [key: string]: unknown;
56
+ };
57
+ type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock;
58
+ type ApiMessage = {
59
+ role: 'user' | 'assistant';
60
+ content: string | ContentBlock[];
61
+ model?: string;
62
+ stop_reason?: string | null;
63
+ /** Per-API-call token usage when the message carries it (assistant messages do).
64
+ * Enables mid-run token accounting; result-message usage stays authoritative. */
65
+ usage?: {
66
+ input_tokens?: number;
67
+ output_tokens?: number;
68
+ cache_creation_input_tokens?: number;
69
+ cache_read_input_tokens?: number;
70
+ };
71
+ };
72
+ /** A tool call promoted into a pending approval by the runner's canUseTool hook. */
73
+ type PermissionRequest = {
74
+ /** Server-assigned id; used by the `permission_decision` command. */id: string;
75
+ toolName: string;
76
+ input: Record<string, unknown>;
77
+ toolUseId: string; /** Full prompt sentence from the SDK, e.g. "Claude wants to read foo.txt". */
78
+ title?: string; /** Short noun phrase for the tool action, e.g. "Read file". */
79
+ displayName?: string; /** Human-readable subtitle, e.g. "Claude will have read access to ~/x". */
80
+ description?: string; /** Why this permission request was triggered. */
81
+ decisionReason?: string; /** If raised from within a subagent, that subagent's id. */
82
+ agentId?: string; /** Epoch ms after which the server resolves it via its timeout policy. */
83
+ expiresAt?: number;
84
+ };
85
+ type PermissionDecisionSource = 'client' | 'timeout' | 'policy';
86
+ /** One choice of an AskUserQuestion question (SDK tool-input mirror). */
87
+ type UserQuestionOption = {
88
+ label: string;
89
+ description?: string;
90
+ /** Optional preview content (markdown unless the session configures html)
91
+ * rendered when the option is focused. */
92
+ preview?: string;
93
+ };
94
+ /** One question from the AskUserQuestion tool's input. By the tool's convention the
95
+ * first option is the model's recommended choice. */
96
+ type UserQuestion = {
97
+ question: string; /** Short chip/tag label (max ~12 chars), e.g. "Auth method". */
98
+ header: string;
99
+ options: UserQuestionOption[];
100
+ multiSelect?: boolean;
101
+ };
102
+ /** How a session treats the AskUserQuestion tool:
103
+ * - 'ask' (default) — a pending permission like any other: interactive UIs render the
104
+ * question form; job webhooks carry the full request so a remote controller can
105
+ * answer over REST (POST /sessions/:id/permissions/:requestId).
106
+ * - 'auto' — resolved immediately with each question's first (recommended) option.
107
+ * - 'deny' — the tool is refused with guidance to decide autonomously (unattended runs).
108
+ * Answers ride a permission allow as `updatedInput.answers`: question text → chosen
109
+ * option label(s), multi-select labels comma-joined — the shape the CLI's own UI uses. */
110
+ type QuestionBehavior = 'ask' | 'auto' | 'deny';
111
+ /** A model the session can switch to (SDK ModelInfo mirror; fields it may grow stay unknown). */
112
+ type ModelOption = {
113
+ /** Model id for createSession.model / set_model. */value: string;
114
+ displayName: string;
115
+ description?: string;
116
+ };
117
+ /** A slash command the CLI accepts as user-message text (SDK SlashCommand mirror). */
118
+ type SlashCommandInfo = {
119
+ /** Command name without the leading slash. */name: string;
120
+ description?: string; /** Hint for arguments, e.g. "<file>". */
121
+ argumentHint?: string; /** Alternate names resolving to this command. */
122
+ aliases?: string[];
123
+ };
124
+ /** One category row from the CLI's context-usage breakdown (system prompt, tools, ...). */
125
+ type ContextUsageCategory = {
126
+ name: string;
127
+ tokens: number;
128
+ /** Color the CLI assigns the category. Often a CLI theme token name ('inactive',
129
+ * 'promptBorder', ...), not a CSS color — validate before styling with it. */
130
+ color: string;
131
+ };
132
+ /** Context-window usage snapshot (SDK getContextUsage mirror), polled after each turn. */
133
+ type ContextUsage = {
134
+ categories: ContextUsageCategory[];
135
+ totalTokens: number;
136
+ maxTokens: number; /** Used share of the window, 0–100. */
137
+ percentage: number; /** Model the window sizing applies to. */
138
+ model?: string;
139
+ };
140
+ /**
141
+ * One rate-limit window snapshot (SDK SDKRateLimitInfo mirror). Emitted only for
142
+ * claude.ai subscription sessions — API-key sessions may never produce one, so
143
+ * clients must render nothing (not 0%) until data arrives.
144
+ */
145
+ type RateLimitInfo = {
146
+ /** 'allowed' | 'allowed_warning' | 'rejected' — kept as string, the SDK union may grow. */status: string;
147
+ /** Which window: 'five_hour' (session), 'seven_day' (weekly), 'seven_day_opus',
148
+ * 'seven_day_sonnet', 'overage', ... — kept as string, the SDK union may grow. */
149
+ rateLimitType?: string;
150
+ /** Used share of the window, 0–100. The CLI omits it on some updates — treat
151
+ * absent as unknown, not 0. */
152
+ utilization?: number; /** Epoch **seconds** when the window resets (render countdowns client-side). */
153
+ resetsAt?: number;
154
+ isUsingOverage?: boolean;
155
+ };
156
+ /**
157
+ * Lifecycle of one tool execution, correlated by `executionId` end to end.
158
+ *
159
+ * - `pending` — dispatched, result not in yet (bridged to a client, or queued).
160
+ * - `deferred` — parked beyond this turn/process; may outlive the session's
161
+ * liveness and be applied on rehydration.
162
+ * - `settled` / `failed` — terminal. Results are applied idempotently by id, so
163
+ * a duplicate delivery is a no-op rather than a second application.
164
+ */
165
+ type ToolExecutionStatus = 'pending' | 'deferred' | 'settled' | 'failed';
166
+ /** Where a tool execution ran (or is running). Advisory: for display and routing. */
167
+ type ToolExecutionBackend = 'server' | 'browser' | 'managed' | 'remote';
168
+ /** Result payload of a tool execution, by value — never a live host reference. */
169
+ type ToolExecutionOutput = {
170
+ type: 'text';
171
+ value: string;
172
+ } | {
173
+ type: 'json';
174
+ value: unknown;
175
+ };
176
+ type SessionEventBody = /** SDK init handshake: what this session actually is. */{
177
+ type: 'system_init';
178
+ sdkSessionId: string;
179
+ model: string;
180
+ cwd: string;
181
+ /** Where the session's Anthropic auth came from: 'oauth' means a claude.ai
182
+ * subscription login; other values ('user' | 'project' | 'org' | 'temporary')
183
+ * are API-key provenance. Kept as string — the SDK union may grow. */
184
+ apiKeySource: string;
185
+ tools: string[];
186
+ skills: string[];
187
+ slashCommands: string[];
188
+ permissionMode: PermissionMode;
189
+ claudeCodeVersion: string;
190
+ mcpServers: Array<{
191
+ name: string;
192
+ status: string;
193
+ }>;
194
+ } | {
195
+ type: 'status_changed';
196
+ status: SessionStatus;
197
+ detail?: string;
198
+ }
199
+ /** Models and slash commands available to this session; fetched from the CLI after
200
+ * init. Late attachers get it via replay like any other event. */
201
+ | {
202
+ type: 'capabilities';
203
+ models: ModelOption[];
204
+ commands: SlashCommandInfo[];
205
+ } /** The session's model changed via `set_model`. `model` undefined = back to default. */ | {
206
+ type: 'model_changed';
207
+ model?: string;
208
+ } /** The session's permission mode changed via `set_permission_mode`. */ | {
209
+ type: 'permission_mode_changed';
210
+ mode: PermissionMode;
211
+ } /** Context-window usage snapshot; the runner polls it after each turn. */ | {
212
+ type: 'context_usage';
213
+ usage: ContextUsage;
214
+ } /** Subscription rate-limit update for one window (see {@link RateLimitInfo}). */ | {
215
+ type: 'rate_limit';
216
+ info: RateLimitInfo;
217
+ } | {
218
+ type: 'assistant_message';
219
+ message: ApiMessage; /** Set when the message was produced inside a subagent (Task tool). */
220
+ parentToolUseId: string | null; /** True when backfilled from a resumed session's history. */
221
+ replay?: boolean;
222
+ uuid: string;
223
+ } | {
224
+ type: 'user_message';
225
+ message: ApiMessage;
226
+ parentToolUseId: string | null; /** True when replayed from a resumed session's history. */
227
+ replay?: boolean; /** True for tool results and other synthetic user-role messages. */
228
+ synthetic?: boolean;
229
+ uuid?: string;
230
+ }
231
+ /** Raw Anthropic streaming event (message_start/content_block_delta/...); emitted only
232
+ * when the session was created with `includePartialMessages`. */
233
+ | {
234
+ type: 'stream_delta';
235
+ event: {
236
+ type: string;
237
+ [key: string]: unknown;
238
+ };
239
+ parentToolUseId: string | null;
240
+ uuid: string;
241
+ } | {
242
+ type: 'turn_result';
243
+ subtype: 'success' | 'error_during_execution' | 'error_max_turns' | 'error_max_budget_usd' | 'error_max_structured_output_retries';
244
+ isError: boolean;
245
+ durationMs: number;
246
+ numTurns: number;
247
+ totalCostUsd: number; /** Final text of the turn (success only). */
248
+ result?: string;
249
+ errors?: string[];
250
+ usage?: unknown;
251
+ } | {
252
+ type: 'permission_requested';
253
+ request: PermissionRequest;
254
+ } | {
255
+ type: 'permission_resolved';
256
+ requestId: string;
257
+ behavior: 'allow' | 'deny';
258
+ resolvedBy: PermissionDecisionSource; /** Denial message, when denied. */
259
+ message?: string;
260
+ }
261
+ /** A tool execution was dispatched to a backend. For bridged executions this
262
+ * precedes the `tool_call_request` frame; for deferred ones it is the record
263
+ * that survives a teardown. */
264
+ | {
265
+ type: 'execution_dispatched';
266
+ executionId: string;
267
+ toolName: string;
268
+ backend: ToolExecutionBackend; /** True when the execution may outlive this turn or process. */
269
+ deferred?: boolean; /** Epoch ms after which the server applies its timeout policy. */
270
+ expiresAt?: number;
271
+ } /** A dispatched execution produced a result. Applied idempotently by `executionId`. */ | {
272
+ type: 'execution_result';
273
+ executionId: string;
274
+ output: ToolExecutionOutput; /** Guest/agent-visible logs, if the backend captured any. */
275
+ logs?: string[];
276
+ durationMs?: number;
277
+ }
278
+ /** A dispatched execution failed, timed out, or was orphaned. The failure is fed
279
+ * back into the loop as tool output so the agent can adapt — it is not a session error. */
280
+ | {
281
+ type: 'execution_failed';
282
+ executionId: string; /** Machine-readable cause: 'timeout' | 'oom' | 'exception' | 'orphaned' | backend-specific. */
283
+ reason: string;
284
+ error: string;
285
+ logs?: string[];
286
+ durationMs?: number;
287
+ }
288
+ /** The agent handed over a file from its session scratch filesystem (the
289
+ * `deliver_file` tool). Download it via `GET {basePath}/sessions/:id/files/<path>`
290
+ * for as long as the session lives (the VFS is in-memory). */
291
+ | {
292
+ type: 'file_delivered';
293
+ path: string;
294
+ bytes: number;
295
+ description?: string;
296
+ }
297
+ /** Any SDKMessage this protocol version doesn't model first-class (task progress,
298
+ * compaction boundaries, auth status, ...). Payload is the raw SDK message. */
299
+ | {
300
+ type: 'sdk_event';
301
+ payload: {
302
+ type: string;
303
+ [key: string]: unknown;
304
+ };
305
+ } | {
306
+ type: 'session_error';
307
+ message: string;
308
+ } | {
309
+ type: 'session_closed';
310
+ reason: 'client' | 'server' | 'error';
311
+ };
312
+ type SessionEvent = SessionEventBody & {
313
+ /** Monotonic per-session sequence number, starting at 1. */seq: number; /** Epoch ms when the server emitted the event. */
314
+ ts: number;
315
+ };
316
+ type SessionCommand = {
317
+ type: 'user_message';
318
+ text: string;
319
+ } | {
320
+ type: 'permission_decision';
321
+ requestId: string;
322
+ behavior: 'allow' | 'deny'; /** allow only: modified tool input to run instead of the original. */
323
+ updatedInput?: Record<string, unknown>; /** deny only: reason surfaced to the model. */
324
+ message?: string; /** deny only: also interrupt the running turn. */
325
+ interrupt?: boolean;
326
+ } | {
327
+ type: 'interrupt';
328
+ } | {
329
+ type: 'set_permission_mode';
330
+ mode: PermissionMode;
331
+ } /** Switch the model for subsequent responses; omit `model` for the default. */ | {
332
+ type: 'set_model';
333
+ model?: string;
334
+ }
335
+ /**
336
+ * Result of a tool execution the server bridged to this client (see
337
+ * {@link ToolCallRequestFrame}). Unknown or already-settled `executionId`s are
338
+ * ignored — delivery is idempotent, and a late result after a timeout must not
339
+ * re-open a settled call.
340
+ *
341
+ * Browser-returned results are UNTRUSTED input: acceptable for the user's own
342
+ * data, never a source for server-authoritative state.
343
+ */
344
+ | {
345
+ type: 'tool_call_result';
346
+ executionId: string;
347
+ output: ToolExecutionOutput;
348
+ logs?: string[];
349
+ }
350
+ /** The client could not execute a bridged call (unsupported tool, guest error,
351
+ * tab closing). Fed back to the agent as tool output. */
352
+ | {
353
+ type: 'tool_call_error';
354
+ executionId: string;
355
+ reason: string;
356
+ error: string;
357
+ logs?: string[];
358
+ } | {
359
+ type: 'close';
360
+ };
361
+ /** First frame the server sends after a successful attach. */
362
+ type AttachedFrame = {
363
+ type: 'attached';
364
+ protocolVersion: number;
365
+ session: SessionInfo; /** Events with seq > the client's `afterSeq` follow as `event` frames. */
366
+ replayingFrom: number;
367
+ };
368
+ /**
369
+ * Ask the attached client to execute a tool call in its own sandbox (browser
370
+ * bridge). The client answers with `tool_call_result` or `tool_call_error`
371
+ * carrying the same `executionId`.
372
+ *
373
+ * Only sandbox-benefiting tools are ever bridged. Authenticated/authoritative
374
+ * tools (MCP, secret-bearing APIs) execute server-side and never appear here.
375
+ */
376
+ type ToolCallRequestFrame = {
377
+ type: 'tool_call_request';
378
+ executionId: string;
379
+ toolName: string;
380
+ input: unknown; /** Files to seed the client's scratch VFS with, path → contents. */
381
+ vfsSeed?: Record<string, string>;
382
+ limits?: {
383
+ timeoutMs?: number;
384
+ memoryLimitBytes?: number;
385
+ }; /** Epoch ms after which the server gives up and fails the execution. */
386
+ expiresAt?: number;
387
+ };
388
+ type ServerFrame = AttachedFrame | {
389
+ type: 'event';
390
+ event: SessionEvent;
391
+ } | ToolCallRequestFrame
392
+ /** A bridged execution no longer needs an answer (turn interrupted, timed out,
393
+ * or the session closed) — the client should abandon it. */
394
+ | {
395
+ type: 'tool_call_canceled';
396
+ executionId: string;
397
+ reason: string;
398
+ } | {
399
+ type: 'protocol_error';
400
+ message: string;
401
+ };
402
+ type ClientFrame = SessionCommand;
403
+ /** Per-profile fallbacks filled into session/job requests that leave the field
404
+ * unset. Defaults, not enforced caps — an explicit request value always wins. */
405
+ type ProfileDefaults = {
406
+ model?: string;
407
+ permissionMode?: PermissionMode;
408
+ };
409
+ /**
410
+ * A named Claude Code config directory sessions can run under: the session's CLI
411
+ * process gets it as CLAUDE_CONFIG_DIR, so the profile carries that directory's
412
+ * settings, memory, skills, and whatever credentials the SDK/CLI resolves from it.
413
+ * Profiles are declared in server options at startup (or a 'default' one is
414
+ * auto-created from the operator's own config dir) — the API only reads them.
415
+ */
416
+ /**
417
+ * Which engine a profile runs on.
418
+ * - `claude` (default) — Claude Code via the Agent SDK, configured by a config dir.
419
+ * - `provider` — a model-agnostic provider (OpenAI-compatible, Anthropic, Moonshot),
420
+ * configured by provider id and credentials from the operator's environment.
421
+ */
422
+ type ProfileEngine = 'claude' | 'provider';
423
+ /**
424
+ * Permission modes the model-agnostic provider engine understands. The rest of
425
+ * {@link PermissionMode}'s vocabulary is Claude Code's: `acceptEdits`, `plan` and
426
+ * `auto` name CLI-side behaviours a provider session has no equivalent of.
427
+ */
428
+ declare const PROVIDER_PERMISSION_MODES: readonly PermissionMode[];
429
+ /**
430
+ * Whether a profile's engine can run a permission mode. The single source of
431
+ * truth for the restriction: create forms filter what they offer with it, the
432
+ * gateway rejects with it. An absent `engine` means 'claude' (every mode).
433
+ */
434
+ declare function supportsPermissionMode(engine: ProfileEngine | undefined, mode: PermissionMode): boolean;
435
+ /**
436
+ * A model provider a `provider` profile can run on. Credentials are ALWAYS
437
+ * resolved from the operator's environment — never carried on the wire, never
438
+ * stored here. `apiKeyEnv` names the variable to read, it does not hold a key.
439
+ */
440
+ type ProviderConfig = {
441
+ /** Provider adapter to use, e.g. 'anthropic' | 'openai' | 'moonshotai' |
442
+ * 'openai-compatible'. Kept as a string: the set is host-extensible. */
443
+ id: string; /** Default model id, e.g. 'kimi-k3'. Overridable per session. */
444
+ model?: string;
445
+ /** Model ids this profile offers, for the dashboard's picker. Operator-declared
446
+ * rather than discovered: provider engines have no equivalent of the CLI's
447
+ * `supportedModels()`, and only the operator knows which ids their endpoint and
448
+ * key actually serve. Unset → the picker offers {@link ProviderConfig.model} alone. */
449
+ models?: string[]; /** Base URL for OpenAI-compatible providers. */
450
+ baseUrl?: string; /** Environment variable the operator put the key in. Never the key itself. */
451
+ apiKeyEnv?: string;
452
+ };
453
+ /**
454
+ * A grantable capability of the model-agnostic engine, named after the tool it
455
+ * yields. The always-present tools (`fs_*`, `eval_script`) are not listed: they
456
+ * are the engine's scratch filesystem and sandbox, not a grant.
457
+ */
458
+ type SessionCapability = 'web_search' | 'download' | 'web_fetch' | 'deliver_file';
459
+ /**
460
+ * What sessions under a `provider` profile get, declared by the operator. Meaning-
461
+ * less for `claude` profiles, whose equivalents live in the config directory.
462
+ *
463
+ * MCP servers are named, never configured, here: a server's transport config can
464
+ * carry credentials in its headers, and this type is served by `GET /profiles`.
465
+ * The names refer to servers the host connected in `createEngineRunner`, which is
466
+ * where the configs (and the credentials) stay.
467
+ */
468
+ type ProfileSessionDefaults = {
469
+ /** Capabilities granted to sessions under this profile. Absent = no
470
+ * declaration, so a session gets whatever backends the host wired. A session
471
+ * request may narrow this set, never widen it. */
472
+ capabilities?: SessionCapability[];
473
+ /** MCP servers, by name, whose tools sessions under this profile may use.
474
+ * Absent = no declaration (every server the host connected). */
475
+ mcpServers?: string[]; /** Prepended to the session's system prompt. */
476
+ instructions?: string;
477
+ };
478
+ type ProfileInfo = {
479
+ /** Unique name, used as {@link CreateSessionRequest.profile}. */name: string;
480
+ /** Engine this profile runs on. Defaults to 'claude' when absent, so profiles
481
+ * written before provider support keep working unchanged. */
482
+ engine?: ProfileEngine;
483
+ /** Absolute path set as CLAUDE_CONFIG_DIR for the session's CLI process.
484
+ * Required for 'claude' profiles; meaningless for 'provider' ones. */
485
+ configDir?: string; /** Provider wiring for 'provider' profiles. */
486
+ provider?: ProviderConfig;
487
+ description?: string;
488
+ defaults?: ProfileDefaults; /** Provider-engine session grants (capabilities, MCP servers, instructions). */
489
+ session?: ProfileSessionDefaults;
490
+ /** Response-only, computed by the server: this profile came from the profile
491
+ * store and can be edited or deleted through the API. Profiles declared in
492
+ * server options are absent/false — they are code. Ignored on the way in. */
493
+ managed?: boolean;
494
+ };
495
+ /**
496
+ * Curated, read-only snapshot of what a profile's config directory contains —
497
+ * the parts relevant to running worker sessions. Values that could carry secrets
498
+ * (env var values) never leave the server; only names are listed.
499
+ */
500
+ type ProfileConfigSnapshot = {
501
+ /** From the config dir's settings.json; absent when missing or unparseable. */settings?: {
502
+ /** Configured default model. */model?: string; /** permissions.defaultMode — the CLI's default permission mode. */
503
+ defaultPermissionMode?: string; /** Rule counts from permissions.allow / ask / deny. */
504
+ permissionRules?: {
505
+ allow: number;
506
+ ask: number;
507
+ deny: number;
508
+ }; /** Env var NAMES declared in settings.json env (values never included). */
509
+ envKeys?: string[]; /** Hook event names with at least one hook configured. */
510
+ hooks?: string[];
511
+ }; /** CLAUDE.md (user memory) present in the config dir. */
512
+ hasUserMemory: boolean; /** Skill names (skills/<name>/). */
513
+ skills: string[]; /** Agent names (agents/<name>.md). */
514
+ agents: string[]; /** Custom slash-command names (commands/<name>.md). */
515
+ commands: string[];
516
+ };
517
+ type McpServerConfigWire = {
518
+ type?: 'stdio';
519
+ command: string;
520
+ args?: string[];
521
+ env?: Record<string, string>;
522
+ } | {
523
+ type: 'http';
524
+ url: string;
525
+ headers?: Record<string, string>;
526
+ } | {
527
+ type: 'sse';
528
+ url: string;
529
+ headers?: Record<string, string>;
530
+ };
531
+ type CreateSessionRequest = {
532
+ /** Directory the session is rooted at. Required: `cwd` is per-query in the SDK
533
+ * and the server re-pins it on every call. */
534
+ cwd: string;
535
+ /** Profile (named Claude Code config dir) to run under. Required when the server
536
+ * declares more than one profile; implicit when exactly one exists. */
537
+ profile?: string; /** Optional initial prompt (may be a skill invocation like "/verify-content 123"). */
538
+ prompt?: string;
539
+ permissionMode?: PermissionMode;
540
+ /** Pre-authorize 'bypassPermissions' (the CLI's --dangerously-skip-permissions
541
+ * capability) so the mode can be switched on mid-session. Without it the CLI
542
+ * rejects `set_permission_mode: 'bypassPermissions'` on a running session.
543
+ * Implied when `permissionMode` is already 'bypassPermissions'. */
544
+ allowDangerouslySkipPermissions?: boolean;
545
+ allowedTools?: string[];
546
+ disallowedTools?: string[];
547
+ mcpServers?: Record<string, McpServerConfigWire>;
548
+ /** Which filesystem settings the session loads. Include 'project' to pick up the
549
+ * target repo's skills and CLAUDE.md ("close-to-real" fidelity). */
550
+ settingSources?: Array<'user' | 'project' | 'local'>;
551
+ model?: string;
552
+ maxTurns?: number;
553
+ maxBudgetUsd?: number; /** Resume an existing SDK session by id. */
554
+ resume?: string; /** With `resume`: fork to a new session id instead of continuing. */
555
+ forkSession?: boolean; /** Emit `stream_delta` events for token-by-token rendering. Default true. */
556
+ includePartialMessages?: boolean; /** Per-session override of the server's permission-request timeout (ms). */
557
+ approvalTimeoutMs?: number; /** AskUserQuestion handling (see {@link QuestionBehavior}). Default 'ask'. */
558
+ questionBehavior?: QuestionBehavior;
559
+ /** Provider engine only: run with fewer capabilities than the profile grants
560
+ * (see {@link ProfileSessionDefaults.capabilities}). Narrowing only — naming a
561
+ * capability the profile does not grant is a 400, not a silent upgrade. */
562
+ capabilities?: SessionCapability[]; /** Free-form metadata echoed back on SessionInfo (host app bookkeeping). */
563
+ meta?: Record<string, unknown>;
564
+ };
565
+ type SessionInfo = {
566
+ /** Server-assigned id (stable across SDK session forks/resumes). */id: string; /** Underlying Agent SDK session id, once known; use for `resume`. */
567
+ sdkSessionId?: string;
568
+ status: SessionStatus;
569
+ cwd: string; /** Profile the session runs under (resolved name, present even when implicit). */
570
+ profile?: string;
571
+ /** Engine actually running this session, reported by the runner itself. Lets a
572
+ * session surface gate CLI-only affordances (permission modes, context usage,
573
+ * rate limits) without looking the profile back up. Absent = 'claude'. */
574
+ engine?: ProfileEngine;
575
+ model?: string;
576
+ permissionMode?: PermissionMode; /** See the `system_init` event; 'oauth' = claude.ai subscription credentials. */
577
+ apiKeySource?: string;
578
+ createdAt: number; /** Highest event seq emitted so far; attach with `afterSeq` to catch up. */
579
+ lastSeq: number;
580
+ pendingPermissionCount: number;
581
+ meta?: Record<string, unknown>; /** Display title: `meta.title` if the host set one, else derived (e.g. first prompt). */
582
+ title?: string; /** Cumulative cost across all turns so far (sum of turn_result totals). */
583
+ totalCostUsd?: number; /** Cumulative turn count across the session. */
584
+ numTurns?: number; /** Epoch ms of the most recent emitted event. */
585
+ lastActivityAt?: number;
586
+ };
587
+ /**
588
+ * A session in the Agent SDK's on-disk store (independent of this server's registry).
589
+ * Listed so hosts can offer "resume" across server restarts: feed `sessionId` to
590
+ * CreateSessionRequest.resume. Mirrors the SDK's SDKSessionInfo, kept browser-safe.
591
+ */
592
+ type SdkSessionSummary = {
593
+ sessionId: string; /** Custom title, auto summary, or first prompt — whichever the SDK has. */
594
+ summary: string; /** Epoch ms of last modification. */
595
+ lastModified: number;
596
+ createdAt?: number;
597
+ customTitle?: string;
598
+ firstPrompt?: string;
599
+ gitBranch?: string;
600
+ cwd?: string;
601
+ };
602
+ /** One deliverable in the session's scratch filesystem (see the `file_delivered` event). */
603
+ type SessionFileInfo = {
604
+ path: string;
605
+ bytes: number;
606
+ };
607
+ /** `GET {basePath}/sessions/:id/files` — every file currently in the session's VFS.
608
+ * `GET {basePath}/sessions/:id/files/<path>` downloads one (attachment disposition).
609
+ * 404 when the session's engine exposes no VFS (Claude-engine sessions). */
610
+ type ListSessionFilesResponse = {
611
+ files: SessionFileInfo[];
612
+ };
613
+ type ListSessionsResponse = {
614
+ sessions: SessionInfo[];
615
+ };
616
+ type CreateSessionResponse = {
617
+ session: SessionInfo;
618
+ };
619
+ type GetSessionResponse = {
620
+ session: SessionInfo;
621
+ };
622
+ /** Body of `POST {basePath}/sessions/:id/permissions/:requestId` — the REST counterpart
623
+ * of the WS `permission_decision` command, for remote controllers without a socket
624
+ * (e.g. answering a job's AskUserQuestion from a webhook consumer). 404 = the request
625
+ * is unknown, already resolved, or expired. */
626
+ type ResolvePermissionRequest = {
627
+ behavior: 'allow';
628
+ updatedInput?: Record<string, unknown>;
629
+ } | {
630
+ behavior: 'deny';
631
+ message?: string;
632
+ interrupt?: boolean;
633
+ };
634
+ type ResolvePermissionResponse = {
635
+ resolved: true;
636
+ };
637
+ /**
638
+ * Body of `POST {basePath}/executions/:executionId/result` — the way a deferred
639
+ * executor (a remote worker, a batch job, a human) delivers the outcome of an
640
+ * execution the session parked on. The session is rehydrated if its runner was
641
+ * torn down, and the result is folded back into the agent loop; a `failed` result
642
+ * is ordinary tool output the agent adapts to, not a session error.
643
+ *
644
+ * Applied **idempotently by `executionId`**: a duplicate or late delivery (one
645
+ * racing the execution watchdog) answers 200 with `applied: false` rather than
646
+ * erroring or applying twice. 404 means no session is parked on that id.
647
+ */
648
+ type SubmitExecutionResultRequest = {
649
+ status: 'ok';
650
+ output: ToolExecutionOutput;
651
+ logs?: string[];
652
+ } | {
653
+ status: 'failed';
654
+ reason: string;
655
+ error: string;
656
+ logs?: string[];
657
+ };
658
+ type SubmitExecutionResultResponse = {
659
+ /** False when the id was already settled — the delivery was a no-op. */applied: boolean; /** Session the execution belonged to. */
660
+ sessionId: string;
661
+ };
662
+ type ListSdkSessionsResponse = {
663
+ sdkSessions: SdkSessionSummary[];
664
+ };
665
+ /** `GET {basePath}/profiles` — filtered to the profiles the caller may use. */
666
+ type ListProfilesResponse = {
667
+ profiles: ProfileInfo[];
668
+ /** Whether this caller may create profiles here — true only when the server has
669
+ * a profile store AND the principal carries `canManageProfiles`. Lets a UI hide
670
+ * controls that would always be refused. */
671
+ canManage?: boolean;
672
+ };
673
+ /**
674
+ * `POST {basePath}/profiles` — create a managed profile. Available only when the
675
+ * server was given a profile store, and only to a principal with
676
+ * `canManageProfiles`. Profiles declared in server options are code, not data:
677
+ * they cannot be created, edited, or deleted through these routes.
678
+ */
679
+ type CreateProfileRequest = ProfileInfo;
680
+ /** `PATCH {basePath}/profiles/:name` — merge into a managed profile. The name is
681
+ * the route, not the body; pass `null` to clear an optional field. */
682
+ type UpdateProfileRequest = Omit<Partial<ProfileInfo>, 'name'>;
683
+ type SaveProfileResponse = {
684
+ profile: ProfileInfo;
685
+ };
686
+ /** `GET {basePath}/profiles/:name` — the profile plus a fresh config snapshot. */
687
+ type GetProfileResponse = {
688
+ profile: ProfileInfo;
689
+ config: ProfileConfigSnapshot;
690
+ };
691
+ type ErrorResponse = {
692
+ error: string;
693
+ };
694
+ /**
695
+ * The moments in an *interactive* session a person needs to hear about when they
696
+ * are not watching it — the whole point being that a phone cannot hold a
697
+ * WebSocket open in the background, so the server has to reach out.
698
+ *
699
+ * Deliberately four: this is a human-attention channel, not an event mirror. The
700
+ * event log stays on the session WS (attach with `afterSeq` to catch up); if you
701
+ * want every assistant message, subscribe there instead.
702
+ */
703
+ type SessionNotificationType = /** The agent is blocked on an approval — the one that matters most. */'permission_requested' /** A turn finished; the session is idle and waiting for the human. */ | 'turn_completed' /** The session failed (`session_error`). */ | 'session_error' /** The session ended (`session_closed`), whoever ended it. */ | 'session_closed';
704
+ /** One delivery on the session-notification channel (JSON body of a webhook POST). */
705
+ type SessionNotification = {
706
+ type: SessionNotificationType;
707
+ sessionId: string; /** Snapshot at notification time — status, title, cwd, cost, `lastSeq`. */
708
+ session: SessionInfo;
709
+ /** Seq of the event behind this notification; attach with `afterSeq: seq - 1` to
710
+ * land on it. */
711
+ seq: number;
712
+ ts: number;
713
+ /** One line fit for a notification body: the permission title, the turn's final
714
+ * text, the error message. */
715
+ preview?: string;
716
+ /** `permission_requested` only: the full request, so a consumer can answer it via
717
+ * `POST {basePath}/sessions/:id/permissions/:requestId` — which is what makes an
718
+ * Approve/Deny action on a lock-screen notification possible. */
719
+ request?: PermissionRequest; /** `turn_completed` only. */
720
+ result?: {
721
+ isError: boolean;
722
+ durationMs: number;
723
+ numTurns: number;
724
+ totalCostUsd: number;
725
+ }; /** `session_closed` only. */
726
+ reason?: 'client' | 'server' | 'error';
727
+ };
728
+ /** Where session notifications are POSTed (JSON body = {@link SessionNotification}).
729
+ * Server-wide, not per session: the point is to hear about sessions you did not
730
+ * create yourself and are not attached to. */
731
+ type SessionWebhookConfig = {
732
+ url: string; /** Extra headers sent with every delivery (auth tokens etc.). */
733
+ headers?: Record<string, string>; /** Types to deliver. Default: all of them. */
734
+ events?: SessionNotificationType[];
735
+ };
736
+ /**
737
+ * - `queued` — accepted, waiting for a concurrency slot (or the daily token budget)
738
+ * - `running` — a session is executing the prompt
739
+ * - `parked` — waiting on an external event (a deferred tool execution). Not
740
+ * terminal and not consuming a concurrency slot; resumes to `running` when the
741
+ * result arrives, or fails via the execution watchdog if it never does.
742
+ * - `succeeded` / `failed` — terminal; `result` (and `error` on failure) are set
743
+ * - `canceled` — terminal; canceled by a client before or during the run
744
+ */
745
+ type JobStatus = 'queued' | 'running' | 'parked' | 'succeeded' | 'failed' | 'canceled';
746
+ /** Where job progress/completion deliveries are POSTed (JSON body = {@link JobEvent}). */
747
+ type WebhookConfig = {
748
+ url: string; /** Extra headers sent with every delivery (auth tokens etc.). */
749
+ headers?: Record<string, string>;
750
+ /** Delivery granularity: 'messages' also POSTs job_progress per assistant message /
751
+ * permission request; 'completion' only job_started + job_completed. Default 'messages'. */
752
+ progress?: 'messages' | 'completion';
753
+ };
754
+ /**
755
+ * Schedule a one-shot run: the session executes `prompt` unattended and the job
756
+ * completes with that run's result. `session.prompt` is the task and is required;
757
+ * `resume`/`forkSession` are not supported for queued jobs.
758
+ */
759
+ type CreateJobRequest = {
760
+ session: CreateSessionRequest & {
761
+ prompt: string;
762
+ };
763
+ webhook?: WebhookConfig; /** Per-job token cap; the effective cap is min(this, the server's sessionTokenLimit). */
764
+ maxTokens?: number; /** Per-job wall-clock cap; the effective cap is min(this, the server's maxJobDurationMs). */
765
+ maxDurationMs?: number;
766
+ /** Total run attempts: failed (not canceled) runs re-queue until this many attempts
767
+ * have been made. Default 1 (no retries). */
768
+ attempts?: number; /** Delay before the first retry, doubled for each subsequent one. Default 5000. */
769
+ retryDelayMs?: number; /** Host bookkeeping echoed back on JobInfo. */
770
+ meta?: Record<string, unknown>;
771
+ };
772
+ /** Cumulative resource usage of a job's run. `tokens` counts input + output +
773
+ * cache-creation + cache-read tokens across all turns. */
774
+ type JobUsage = {
775
+ tokens: number;
776
+ totalCostUsd: number;
777
+ numTurns: number;
778
+ };
779
+ /** Terminal outcome of the job's run (mirrors the final turn_result). */
780
+ type JobResult = {
781
+ subtype: string;
782
+ isError: boolean; /** Final text of the run (success only). */
783
+ result?: string;
784
+ errors?: string[];
785
+ durationMs: number;
786
+ };
787
+ type JobInfo = {
788
+ id: string;
789
+ status: JobStatus;
790
+ cwd: string; /** Profile the run executes under (resolved name, present even when implicit). */
791
+ profile?: string;
792
+ prompt: string; /** Server session id once started — attach via the sessions WS to watch the run live. */
793
+ sessionId?: string;
794
+ sdkSessionId?: string;
795
+ createdAt: number;
796
+ startedAt?: number;
797
+ finishedAt?: number; /** 1-based run attempt this info reflects. */
798
+ attempt?: number; /** Total attempts configured on the request (see CreateJobRequest.attempts). */
799
+ maxAttempts?: number; /** For a job re-queued by retry backoff: earliest time the next attempt may start. */
800
+ nextRunAt?: number;
801
+ /** Set while `status` is 'parked': when the run parked, and the execution it is
802
+ * waiting on — the id to POST a result to. Cleared when it resumes. */
803
+ parkedAt?: number;
804
+ parkedExecutionId?: string; /** Cumulative across attempts. */
805
+ usage: JobUsage;
806
+ result?: JobResult; /** Failure or cancellation reason (for a queued retry: the previous attempt's error). */
807
+ error?: string;
808
+ meta?: Record<string, unknown>;
809
+ };
810
+ /** Latest mid-run activity, carried on job_progress deliveries. */
811
+ type JobProgress = {
812
+ kind: 'assistant_text' | 'tool_use' | 'permission_requested' | 'permission_resolved'; /** Short human-readable preview (message excerpt, tool name, permission title). */
813
+ preview?: string;
814
+ /** 'permission_requested' only: the full request (including AskUserQuestion input) so
815
+ * webhook consumers can answer via POST /sessions/:sessionId/permissions/:requestId. */
816
+ request?: PermissionRequest;
817
+ };
818
+ /** Webhook delivery payload (also the queue's local event shape). `job_submitted` goes
819
+ * to local observers and the queue WS only — the submitter already has the POST
820
+ * response, so webhooks start at `job_started`. `job_retrying` marks a failed run that
821
+ * was re-queued (`job.nextRunAt` says when); `job_completed` is always terminal. */
822
+ type JobEvent = {
823
+ type: 'job_submitted';
824
+ job: JobInfo;
825
+ ts: number;
826
+ } | {
827
+ type: 'job_started';
828
+ job: JobInfo;
829
+ ts: number;
830
+ } | {
831
+ type: 'job_progress';
832
+ job: JobInfo;
833
+ progress: JobProgress;
834
+ ts: number;
835
+ }
836
+ /** The run parked on a deferred execution; `executionId` says what it waits on —
837
+ * the id to POST a result to. The *work itself* (tool name, input, VFS seed) went
838
+ * to the executor's own dispatch hook, not over this channel: a webhook consumer
839
+ * learns that a run is waiting, the worker learns what to do. */
840
+ | {
841
+ type: 'job_parked';
842
+ job: JobInfo;
843
+ executionId: string;
844
+ ts: number;
845
+ } /** A parked run resumed because its execution result arrived. */ | {
846
+ type: 'job_resumed';
847
+ job: JobInfo;
848
+ executionId: string;
849
+ ts: number;
850
+ } | {
851
+ type: 'job_retrying';
852
+ job: JobInfo;
853
+ ts: number;
854
+ } | {
855
+ type: 'job_completed';
856
+ job: JobInfo;
857
+ ts: number;
858
+ };
859
+ type QueueStats = {
860
+ maxConcurrency: number;
861
+ running: number;
862
+ queued: number;
863
+ /** Jobs waiting on a deferred execution. They hold no concurrency slot and
864
+ * their wall-clock budget is not ticking. */
865
+ parked: number;
866
+ sessionTokenLimit?: number;
867
+ dailyTokenLimit?: number; /** Tokens consumed by queue jobs in the current UTC day. */
868
+ dailyTokensUsed: number; /** True when the daily budget is exhausted and queued jobs are being held. */
869
+ paused: boolean;
870
+ };
871
+ /** Frames sent on the queue WS (`{basePath}/queue/ws`). The stream is one-way
872
+ * (server→client): every job's lifecycle as it happens, plus refreshed stats after
873
+ * lifecycle changes. Clients send nothing; job mutations stay on REST. */
874
+ type QueueServerFrame = {
875
+ type: 'queue_attached';
876
+ protocolVersion: number;
877
+ stats: QueueStats;
878
+ } | {
879
+ type: 'job_event';
880
+ event: JobEvent;
881
+ } | {
882
+ type: 'queue_stats';
883
+ stats: QueueStats;
884
+ };
885
+ type CreateJobResponse = {
886
+ job: JobInfo;
887
+ };
888
+ type GetJobResponse = {
889
+ job: JobInfo;
890
+ };
891
+ type ListJobsResponse = {
892
+ jobs: JobInfo[];
893
+ };
894
+ type QueueStatsResponse = {
895
+ stats: QueueStats;
896
+ };
897
+ //#endregion
898
+ export { ApiMessage, AttachedFrame, ClientFrame, ContentBlock, ContextUsage, ContextUsageCategory, CreateJobRequest, CreateJobResponse, CreateProfileRequest, CreateSessionRequest, CreateSessionResponse, ErrorResponse, GetJobResponse, GetProfileResponse, GetSessionResponse, JobEvent, JobInfo, JobProgress, JobResult, JobStatus, JobUsage, ListJobsResponse, ListProfilesResponse, ListSdkSessionsResponse, ListSessionFilesResponse, ListSessionsResponse, McpServerConfigWire, ModelOption, PROTOCOL_VERSION, PROVIDER_PERMISSION_MODES, PermissionDecisionSource, PermissionMode, PermissionRequest, ProfileConfigSnapshot, ProfileDefaults, ProfileEngine, ProfileInfo, ProfileSessionDefaults, ProviderConfig, QuestionBehavior, QueueServerFrame, QueueStats, QueueStatsResponse, RateLimitInfo, ResolvePermissionRequest, ResolvePermissionResponse, SaveProfileResponse, SdkSessionSummary, ServerFrame, SessionCapability, SessionCommand, SessionEvent, SessionEventBody, SessionFileInfo, SessionInfo, SessionNotification, SessionNotificationType, SessionStatus, SessionWebhookConfig, SlashCommandInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, TextBlock, ThinkingBlock, ToolCallRequestFrame, ToolExecutionBackend, ToolExecutionOutput, ToolExecutionStatus, ToolResultBlock, ToolUseBlock, UnknownBlock, UpdateProfileRequest, UserQuestion, UserQuestionOption, WebhookConfig, supportsPermissionMode };
899
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,35 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * @workerdeck/protocol — the wire protocol between a workerdeck server and its clients.
4
+ *
5
+ * One session = one ordered stream of {@link SessionEvent}s (each stamped with a monotonically
6
+ * increasing `seq`) plus a small command set ({@link SessionCommand}). Clients attach over
7
+ * WebSocket, optionally replaying from a known `seq`, and drive the session with commands.
8
+ *
9
+ * This package is dependency-free and browser-safe. Anthropic API message content is modeled
10
+ * structurally (see {@link ApiMessage}) so clients don't need the Agent SDK to render transcripts.
11
+ */
12
+ /** Bumped on any breaking change to events, commands, or REST shapes. */
13
+ const PROTOCOL_VERSION = 4;
14
+ /**
15
+ * Permission modes the model-agnostic provider engine understands. The rest of
16
+ * {@link PermissionMode}'s vocabulary is Claude Code's: `acceptEdits`, `plan` and
17
+ * `auto` name CLI-side behaviours a provider session has no equivalent of.
18
+ */
19
+ const PROVIDER_PERMISSION_MODES = [
20
+ "default",
21
+ "bypassPermissions",
22
+ "dontAsk"
23
+ ];
24
+ /**
25
+ * Whether a profile's engine can run a permission mode. The single source of
26
+ * truth for the restriction: create forms filter what they offer with it, the
27
+ * gateway rejects with it. An absent `engine` means 'claude' (every mode).
28
+ */
29
+ function supportsPermissionMode(engine, mode) {
30
+ return engine === "provider" ? PROVIDER_PERMISSION_MODES.includes(mode) : true;
31
+ }
32
+ //#endregion
33
+ export { PROTOCOL_VERSION, PROVIDER_PERMISSION_MODES, supportsPermissionMode };
34
+
35
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @workerdeck/protocol — the wire protocol between a workerdeck server and its clients.\n *\n * One session = one ordered stream of {@link SessionEvent}s (each stamped with a monotonically\n * increasing `seq`) plus a small command set ({@link SessionCommand}). Clients attach over\n * WebSocket, optionally replaying from a known `seq`, and drive the session with commands.\n *\n * This package is dependency-free and browser-safe. Anthropic API message content is modeled\n * structurally (see {@link ApiMessage}) so clients don't need the Agent SDK to render transcripts.\n */\n\n/** Bumped on any breaking change to events, commands, or REST shapes. */\nexport const PROTOCOL_VERSION = 4\n\n// ---------------------------------------------------------------------------\n// Session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * - `starting` — runner spawned, waiting for the SDK init handshake\n * - `running` — a turn is in progress\n * - `awaiting_approval` — blocked on at least one pending permission request\n * - `idle` — between turns; accepting user messages\n * - `parked` — waiting on a deferred tool execution. The live runner has been torn\n * down and the session's state persisted; delivering the execution's result\n * (`POST {basePath}/executions/:executionId/result`) rehydrates it under the same\n * id and the run continues. Not terminal.\n * - `failed` — the underlying query errored; terminal\n * - `closed` — closed by a client or the host; terminal\n */\nexport type SessionStatus =\n | 'starting'\n | 'running'\n | 'awaiting_approval'\n | 'idle'\n | 'parked'\n | 'failed'\n | 'closed'\n\nexport type PermissionMode =\n | 'default'\n | 'acceptEdits'\n | 'bypassPermissions'\n | 'plan'\n | 'dontAsk'\n | 'auto'\n\n// ---------------------------------------------------------------------------\n// API message content (structural mirror of Anthropic message shapes)\n// ---------------------------------------------------------------------------\n\nexport type TextBlock = { type: 'text'; text: string }\nexport type ThinkingBlock = { type: 'thinking'; thinking: string }\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: unknown }\nexport type ToolResultBlock = {\n type: 'tool_result'\n tool_use_id: string\n content?: string | Array<{ type: string; text?: string; [key: string]: unknown }>\n is_error?: boolean\n}\n/** Forward-compatible fallback for block types this protocol version doesn't model. */\nexport type UnknownBlock = { type: string; [key: string]: unknown }\n\nexport type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock\n\nexport type ApiMessage = {\n role: 'user' | 'assistant'\n content: string | ContentBlock[]\n model?: string\n stop_reason?: string | null\n /** Per-API-call token usage when the message carries it (assistant messages do).\n * Enables mid-run token accounting; result-message usage stays authoritative. */\n usage?: {\n input_tokens?: number\n output_tokens?: number\n cache_creation_input_tokens?: number\n cache_read_input_tokens?: number\n }\n}\n\n// ---------------------------------------------------------------------------\n// Permission requests\n// ---------------------------------------------------------------------------\n\n/** A tool call promoted into a pending approval by the runner's canUseTool hook. */\nexport type PermissionRequest = {\n /** Server-assigned id; used by the `permission_decision` command. */\n id: string\n toolName: string\n input: Record<string, unknown>\n toolUseId: string\n /** Full prompt sentence from the SDK, e.g. \"Claude wants to read foo.txt\". */\n title?: string\n /** Short noun phrase for the tool action, e.g. \"Read file\". */\n displayName?: string\n /** Human-readable subtitle, e.g. \"Claude will have read access to ~/x\". */\n description?: string\n /** Why this permission request was triggered. */\n decisionReason?: string\n /** If raised from within a subagent, that subagent's id. */\n agentId?: string\n /** Epoch ms after which the server resolves it via its timeout policy. */\n expiresAt?: number\n}\n\nexport type PermissionDecisionSource = 'client' | 'timeout' | 'policy'\n\n// ---------------------------------------------------------------------------\n// User questions (the AskUserQuestion tool)\n// ---------------------------------------------------------------------------\n\n/** One choice of an AskUserQuestion question (SDK tool-input mirror). */\nexport type UserQuestionOption = {\n label: string\n description?: string\n /** Optional preview content (markdown unless the session configures html)\n * rendered when the option is focused. */\n preview?: string\n}\n\n/** One question from the AskUserQuestion tool's input. By the tool's convention the\n * first option is the model's recommended choice. */\nexport type UserQuestion = {\n question: string\n /** Short chip/tag label (max ~12 chars), e.g. \"Auth method\". */\n header: string\n options: UserQuestionOption[]\n multiSelect?: boolean\n}\n\n/** How a session treats the AskUserQuestion tool:\n * - 'ask' (default) — a pending permission like any other: interactive UIs render the\n * question form; job webhooks carry the full request so a remote controller can\n * answer over REST (POST /sessions/:id/permissions/:requestId).\n * - 'auto' — resolved immediately with each question's first (recommended) option.\n * - 'deny' — the tool is refused with guidance to decide autonomously (unattended runs).\n * Answers ride a permission allow as `updatedInput.answers`: question text → chosen\n * option label(s), multi-select labels comma-joined — the shape the CLI's own UI uses. */\nexport type QuestionBehavior = 'ask' | 'auto' | 'deny'\n\n// ---------------------------------------------------------------------------\n// Session capabilities (models / slash commands the CLI reports)\n// ---------------------------------------------------------------------------\n\n/** A model the session can switch to (SDK ModelInfo mirror; fields it may grow stay unknown). */\nexport type ModelOption = {\n /** Model id for createSession.model / set_model. */\n value: string\n displayName: string\n description?: string\n}\n\n/** A slash command the CLI accepts as user-message text (SDK SlashCommand mirror). */\nexport type SlashCommandInfo = {\n /** Command name without the leading slash. */\n name: string\n description?: string\n /** Hint for arguments, e.g. \"<file>\". */\n argumentHint?: string\n /** Alternate names resolving to this command. */\n aliases?: string[]\n}\n\n// ---------------------------------------------------------------------------\n// Usage telemetry (context window + subscription rate limits)\n// ---------------------------------------------------------------------------\n\n/** One category row from the CLI's context-usage breakdown (system prompt, tools, ...). */\nexport type ContextUsageCategory = {\n name: string\n tokens: number\n /** Color the CLI assigns the category. Often a CLI theme token name ('inactive',\n * 'promptBorder', ...), not a CSS color — validate before styling with it. */\n color: string\n}\n\n/** Context-window usage snapshot (SDK getContextUsage mirror), polled after each turn. */\nexport type ContextUsage = {\n categories: ContextUsageCategory[]\n totalTokens: number\n maxTokens: number\n /** Used share of the window, 0–100. */\n percentage: number\n /** Model the window sizing applies to. */\n model?: string\n}\n\n/**\n * One rate-limit window snapshot (SDK SDKRateLimitInfo mirror). Emitted only for\n * claude.ai subscription sessions — API-key sessions may never produce one, so\n * clients must render nothing (not 0%) until data arrives.\n */\nexport type RateLimitInfo = {\n /** 'allowed' | 'allowed_warning' | 'rejected' — kept as string, the SDK union may grow. */\n status: string\n /** Which window: 'five_hour' (session), 'seven_day' (weekly), 'seven_day_opus',\n * 'seven_day_sonnet', 'overage', ... — kept as string, the SDK union may grow. */\n rateLimitType?: string\n /** Used share of the window, 0–100. The CLI omits it on some updates — treat\n * absent as unknown, not 0. */\n utilization?: number\n /** Epoch **seconds** when the window resets (render countdowns client-side). */\n resetsAt?: number\n isUsingOverage?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Tool execution (bridged, deferred, and remote)\n// ---------------------------------------------------------------------------\n\n/**\n * Lifecycle of one tool execution, correlated by `executionId` end to end.\n *\n * - `pending` — dispatched, result not in yet (bridged to a client, or queued).\n * - `deferred` — parked beyond this turn/process; may outlive the session's\n * liveness and be applied on rehydration.\n * - `settled` / `failed` — terminal. Results are applied idempotently by id, so\n * a duplicate delivery is a no-op rather than a second application.\n */\nexport type ToolExecutionStatus = 'pending' | 'deferred' | 'settled' | 'failed'\n\n/** Where a tool execution ran (or is running). Advisory: for display and routing. */\nexport type ToolExecutionBackend = 'server' | 'browser' | 'managed' | 'remote'\n\n/** Result payload of a tool execution, by value — never a live host reference. */\nexport type ToolExecutionOutput =\n | { type: 'text'; value: string }\n | { type: 'json'; value: unknown }\n\n// ---------------------------------------------------------------------------\n// Session events (server -> client)\n// ---------------------------------------------------------------------------\n\nexport type SessionEventBody =\n /** SDK init handshake: what this session actually is. */\n | {\n type: 'system_init'\n sdkSessionId: string\n model: string\n cwd: string\n /** Where the session's Anthropic auth came from: 'oauth' means a claude.ai\n * subscription login; other values ('user' | 'project' | 'org' | 'temporary')\n * are API-key provenance. Kept as string — the SDK union may grow. */\n apiKeySource: string\n tools: string[]\n skills: string[]\n slashCommands: string[]\n permissionMode: PermissionMode\n claudeCodeVersion: string\n mcpServers: Array<{ name: string; status: string }>\n }\n | { type: 'status_changed'; status: SessionStatus; detail?: string }\n /** Models and slash commands available to this session; fetched from the CLI after\n * init. Late attachers get it via replay like any other event. */\n | { type: 'capabilities'; models: ModelOption[]; commands: SlashCommandInfo[] }\n /** The session's model changed via `set_model`. `model` undefined = back to default. */\n | { type: 'model_changed'; model?: string }\n /** The session's permission mode changed via `set_permission_mode`. */\n | { type: 'permission_mode_changed'; mode: PermissionMode }\n /** Context-window usage snapshot; the runner polls it after each turn. */\n | { type: 'context_usage'; usage: ContextUsage }\n /** Subscription rate-limit update for one window (see {@link RateLimitInfo}). */\n | { type: 'rate_limit'; info: RateLimitInfo }\n | {\n type: 'assistant_message'\n message: ApiMessage\n /** Set when the message was produced inside a subagent (Task tool). */\n parentToolUseId: string | null\n /** True when backfilled from a resumed session's history. */\n replay?: boolean\n uuid: string\n }\n | {\n type: 'user_message'\n message: ApiMessage\n parentToolUseId: string | null\n /** True when replayed from a resumed session's history. */\n replay?: boolean\n /** True for tool results and other synthetic user-role messages. */\n synthetic?: boolean\n uuid?: string\n }\n /** Raw Anthropic streaming event (message_start/content_block_delta/...); emitted only\n * when the session was created with `includePartialMessages`. */\n | {\n type: 'stream_delta'\n event: { type: string; [key: string]: unknown }\n parentToolUseId: string | null\n uuid: string\n }\n | {\n type: 'turn_result'\n subtype:\n | 'success'\n | 'error_during_execution'\n | 'error_max_turns'\n | 'error_max_budget_usd'\n | 'error_max_structured_output_retries'\n isError: boolean\n durationMs: number\n numTurns: number\n totalCostUsd: number\n /** Final text of the turn (success only). */\n result?: string\n errors?: string[]\n usage?: unknown\n }\n | { type: 'permission_requested'; request: PermissionRequest }\n | {\n type: 'permission_resolved'\n requestId: string\n behavior: 'allow' | 'deny'\n resolvedBy: PermissionDecisionSource\n /** Denial message, when denied. */\n message?: string\n }\n /** A tool execution was dispatched to a backend. For bridged executions this\n * precedes the `tool_call_request` frame; for deferred ones it is the record\n * that survives a teardown. */\n | {\n type: 'execution_dispatched'\n executionId: string\n toolName: string\n backend: ToolExecutionBackend\n /** True when the execution may outlive this turn or process. */\n deferred?: boolean\n /** Epoch ms after which the server applies its timeout policy. */\n expiresAt?: number\n }\n /** A dispatched execution produced a result. Applied idempotently by `executionId`. */\n | {\n type: 'execution_result'\n executionId: string\n output: ToolExecutionOutput\n /** Guest/agent-visible logs, if the backend captured any. */\n logs?: string[]\n durationMs?: number\n }\n /** A dispatched execution failed, timed out, or was orphaned. The failure is fed\n * back into the loop as tool output so the agent can adapt — it is not a session error. */\n | {\n type: 'execution_failed'\n executionId: string\n /** Machine-readable cause: 'timeout' | 'oom' | 'exception' | 'orphaned' | backend-specific. */\n reason: string\n error: string\n logs?: string[]\n durationMs?: number\n }\n /** The agent handed over a file from its session scratch filesystem (the\n * `deliver_file` tool). Download it via `GET {basePath}/sessions/:id/files/<path>`\n * for as long as the session lives (the VFS is in-memory). */\n | { type: 'file_delivered'; path: string; bytes: number; description?: string }\n /** Any SDKMessage this protocol version doesn't model first-class (task progress,\n * compaction boundaries, auth status, ...). Payload is the raw SDK message. */\n | { type: 'sdk_event'; payload: { type: string; [key: string]: unknown } }\n | { type: 'session_error'; message: string }\n | { type: 'session_closed'; reason: 'client' | 'server' | 'error' }\n\nexport type SessionEvent = SessionEventBody & {\n /** Monotonic per-session sequence number, starting at 1. */\n seq: number\n /** Epoch ms when the server emitted the event. */\n ts: number\n}\n\n// ---------------------------------------------------------------------------\n// Commands (client -> server)\n// ---------------------------------------------------------------------------\n\nexport type SessionCommand =\n | { type: 'user_message'; text: string }\n | {\n type: 'permission_decision'\n requestId: string\n behavior: 'allow' | 'deny'\n /** allow only: modified tool input to run instead of the original. */\n updatedInput?: Record<string, unknown>\n /** deny only: reason surfaced to the model. */\n message?: string\n /** deny only: also interrupt the running turn. */\n interrupt?: boolean\n }\n | { type: 'interrupt' }\n | { type: 'set_permission_mode'; mode: PermissionMode }\n /** Switch the model for subsequent responses; omit `model` for the default. */\n | { type: 'set_model'; model?: string }\n /**\n * Result of a tool execution the server bridged to this client (see\n * {@link ToolCallRequestFrame}). Unknown or already-settled `executionId`s are\n * ignored — delivery is idempotent, and a late result after a timeout must not\n * re-open a settled call.\n *\n * Browser-returned results are UNTRUSTED input: acceptable for the user's own\n * data, never a source for server-authoritative state.\n */\n | {\n type: 'tool_call_result'\n executionId: string\n output: ToolExecutionOutput\n logs?: string[]\n }\n /** The client could not execute a bridged call (unsupported tool, guest error,\n * tab closing). Fed back to the agent as tool output. */\n | {\n type: 'tool_call_error'\n executionId: string\n reason: string\n error: string\n logs?: string[]\n }\n | { type: 'close' }\n\n// ---------------------------------------------------------------------------\n// WebSocket frames\n// ---------------------------------------------------------------------------\n\n/** First frame the server sends after a successful attach. */\nexport type AttachedFrame = {\n type: 'attached'\n protocolVersion: number\n session: SessionInfo\n /** Events with seq > the client's `afterSeq` follow as `event` frames. */\n replayingFrom: number\n}\n\n/**\n * Ask the attached client to execute a tool call in its own sandbox (browser\n * bridge). The client answers with `tool_call_result` or `tool_call_error`\n * carrying the same `executionId`.\n *\n * Only sandbox-benefiting tools are ever bridged. Authenticated/authoritative\n * tools (MCP, secret-bearing APIs) execute server-side and never appear here.\n */\nexport type ToolCallRequestFrame = {\n type: 'tool_call_request'\n executionId: string\n toolName: string\n input: unknown\n /** Files to seed the client's scratch VFS with, path → contents. */\n vfsSeed?: Record<string, string>\n limits?: { timeoutMs?: number; memoryLimitBytes?: number }\n /** Epoch ms after which the server gives up and fails the execution. */\n expiresAt?: number\n}\n\nexport type ServerFrame =\n | AttachedFrame\n | { type: 'event'; event: SessionEvent }\n | ToolCallRequestFrame\n /** A bridged execution no longer needs an answer (turn interrupted, timed out,\n * or the session closed) — the client should abandon it. */\n | { type: 'tool_call_canceled'; executionId: string; reason: string }\n | { type: 'protocol_error'; message: string }\n\nexport type ClientFrame = SessionCommand\n\n// ---------------------------------------------------------------------------\n// Profiles (named Claude Code config directories)\n// ---------------------------------------------------------------------------\n\n/** Per-profile fallbacks filled into session/job requests that leave the field\n * unset. Defaults, not enforced caps — an explicit request value always wins. */\nexport type ProfileDefaults = {\n model?: string\n permissionMode?: PermissionMode\n}\n\n/**\n * A named Claude Code config directory sessions can run under: the session's CLI\n * process gets it as CLAUDE_CONFIG_DIR, so the profile carries that directory's\n * settings, memory, skills, and whatever credentials the SDK/CLI resolves from it.\n * Profiles are declared in server options at startup (or a 'default' one is\n * auto-created from the operator's own config dir) — the API only reads them.\n */\n/**\n * Which engine a profile runs on.\n * - `claude` (default) — Claude Code via the Agent SDK, configured by a config dir.\n * - `provider` — a model-agnostic provider (OpenAI-compatible, Anthropic, Moonshot),\n * configured by provider id and credentials from the operator's environment.\n */\nexport type ProfileEngine = 'claude' | 'provider'\n\n/**\n * Permission modes the model-agnostic provider engine understands. The rest of\n * {@link PermissionMode}'s vocabulary is Claude Code's: `acceptEdits`, `plan` and\n * `auto` name CLI-side behaviours a provider session has no equivalent of.\n */\nexport const PROVIDER_PERMISSION_MODES: readonly PermissionMode[] = [\n 'default',\n 'bypassPermissions',\n 'dontAsk',\n]\n\n/**\n * Whether a profile's engine can run a permission mode. The single source of\n * truth for the restriction: create forms filter what they offer with it, the\n * gateway rejects with it. An absent `engine` means 'claude' (every mode).\n */\nexport function supportsPermissionMode(\n engine: ProfileEngine | undefined,\n mode: PermissionMode,\n): boolean {\n return engine === 'provider' ? PROVIDER_PERMISSION_MODES.includes(mode) : true\n}\n\n/**\n * A model provider a `provider` profile can run on. Credentials are ALWAYS\n * resolved from the operator's environment — never carried on the wire, never\n * stored here. `apiKeyEnv` names the variable to read, it does not hold a key.\n */\nexport type ProviderConfig = {\n /** Provider adapter to use, e.g. 'anthropic' | 'openai' | 'moonshotai' |\n * 'openai-compatible'. Kept as a string: the set is host-extensible. */\n id: string\n /** Default model id, e.g. 'kimi-k3'. Overridable per session. */\n model?: string\n /** Model ids this profile offers, for the dashboard's picker. Operator-declared\n * rather than discovered: provider engines have no equivalent of the CLI's\n * `supportedModels()`, and only the operator knows which ids their endpoint and\n * key actually serve. Unset → the picker offers {@link ProviderConfig.model} alone. */\n models?: string[]\n /** Base URL for OpenAI-compatible providers. */\n baseUrl?: string\n /** Environment variable the operator put the key in. Never the key itself. */\n apiKeyEnv?: string\n}\n\n/**\n * A grantable capability of the model-agnostic engine, named after the tool it\n * yields. The always-present tools (`fs_*`, `eval_script`) are not listed: they\n * are the engine's scratch filesystem and sandbox, not a grant.\n */\nexport type SessionCapability = 'web_search' | 'download' | 'web_fetch' | 'deliver_file'\n\n/**\n * What sessions under a `provider` profile get, declared by the operator. Meaning-\n * less for `claude` profiles, whose equivalents live in the config directory.\n *\n * MCP servers are named, never configured, here: a server's transport config can\n * carry credentials in its headers, and this type is served by `GET /profiles`.\n * The names refer to servers the host connected in `createEngineRunner`, which is\n * where the configs (and the credentials) stay.\n */\nexport type ProfileSessionDefaults = {\n /** Capabilities granted to sessions under this profile. Absent = no\n * declaration, so a session gets whatever backends the host wired. A session\n * request may narrow this set, never widen it. */\n capabilities?: SessionCapability[]\n /** MCP servers, by name, whose tools sessions under this profile may use.\n * Absent = no declaration (every server the host connected). */\n mcpServers?: string[]\n /** Prepended to the session's system prompt. */\n instructions?: string\n}\n\nexport type ProfileInfo = {\n /** Unique name, used as {@link CreateSessionRequest.profile}. */\n name: string\n /** Engine this profile runs on. Defaults to 'claude' when absent, so profiles\n * written before provider support keep working unchanged. */\n engine?: ProfileEngine\n /** Absolute path set as CLAUDE_CONFIG_DIR for the session's CLI process.\n * Required for 'claude' profiles; meaningless for 'provider' ones. */\n configDir?: string\n /** Provider wiring for 'provider' profiles. */\n provider?: ProviderConfig\n description?: string\n defaults?: ProfileDefaults\n /** Provider-engine session grants (capabilities, MCP servers, instructions). */\n session?: ProfileSessionDefaults\n /** Response-only, computed by the server: this profile came from the profile\n * store and can be edited or deleted through the API. Profiles declared in\n * server options are absent/false — they are code. Ignored on the way in. */\n managed?: boolean\n}\n\n/**\n * Curated, read-only snapshot of what a profile's config directory contains —\n * the parts relevant to running worker sessions. Values that could carry secrets\n * (env var values) never leave the server; only names are listed.\n */\nexport type ProfileConfigSnapshot = {\n /** From the config dir's settings.json; absent when missing or unparseable. */\n settings?: {\n /** Configured default model. */\n model?: string\n /** permissions.defaultMode — the CLI's default permission mode. */\n defaultPermissionMode?: string\n /** Rule counts from permissions.allow / ask / deny. */\n permissionRules?: { allow: number; ask: number; deny: number }\n /** Env var NAMES declared in settings.json env (values never included). */\n envKeys?: string[]\n /** Hook event names with at least one hook configured. */\n hooks?: string[]\n }\n /** CLAUDE.md (user memory) present in the config dir. */\n hasUserMemory: boolean\n /** Skill names (skills/<name>/). */\n skills: string[]\n /** Agent names (agents/<name>.md). */\n agents: string[]\n /** Custom slash-command names (commands/<name>.md). */\n commands: string[]\n}\n\n// ---------------------------------------------------------------------------\n// REST shapes\n// ---------------------------------------------------------------------------\n\nexport type McpServerConfigWire =\n | { type?: 'stdio'; command: string; args?: string[]; env?: Record<string, string> }\n | { type: 'http'; url: string; headers?: Record<string, string> }\n | { type: 'sse'; url: string; headers?: Record<string, string> }\n\nexport type CreateSessionRequest = {\n /** Directory the session is rooted at. Required: `cwd` is per-query in the SDK\n * and the server re-pins it on every call. */\n cwd: string\n /** Profile (named Claude Code config dir) to run under. Required when the server\n * declares more than one profile; implicit when exactly one exists. */\n profile?: string\n /** Optional initial prompt (may be a skill invocation like \"/verify-content 123\"). */\n prompt?: string\n permissionMode?: PermissionMode\n /** Pre-authorize 'bypassPermissions' (the CLI's --dangerously-skip-permissions\n * capability) so the mode can be switched on mid-session. Without it the CLI\n * rejects `set_permission_mode: 'bypassPermissions'` on a running session.\n * Implied when `permissionMode` is already 'bypassPermissions'. */\n allowDangerouslySkipPermissions?: boolean\n allowedTools?: string[]\n disallowedTools?: string[]\n mcpServers?: Record<string, McpServerConfigWire>\n /** Which filesystem settings the session loads. Include 'project' to pick up the\n * target repo's skills and CLAUDE.md (\"close-to-real\" fidelity). */\n settingSources?: Array<'user' | 'project' | 'local'>\n model?: string\n maxTurns?: number\n maxBudgetUsd?: number\n /** Resume an existing SDK session by id. */\n resume?: string\n /** With `resume`: fork to a new session id instead of continuing. */\n forkSession?: boolean\n /** Emit `stream_delta` events for token-by-token rendering. Default true. */\n includePartialMessages?: boolean\n /** Per-session override of the server's permission-request timeout (ms). */\n approvalTimeoutMs?: number\n /** AskUserQuestion handling (see {@link QuestionBehavior}). Default 'ask'. */\n questionBehavior?: QuestionBehavior\n /** Provider engine only: run with fewer capabilities than the profile grants\n * (see {@link ProfileSessionDefaults.capabilities}). Narrowing only — naming a\n * capability the profile does not grant is a 400, not a silent upgrade. */\n capabilities?: SessionCapability[]\n /** Free-form metadata echoed back on SessionInfo (host app bookkeeping). */\n meta?: Record<string, unknown>\n}\n\nexport type SessionInfo = {\n /** Server-assigned id (stable across SDK session forks/resumes). */\n id: string\n /** Underlying Agent SDK session id, once known; use for `resume`. */\n sdkSessionId?: string\n status: SessionStatus\n cwd: string\n /** Profile the session runs under (resolved name, present even when implicit). */\n profile?: string\n /** Engine actually running this session, reported by the runner itself. Lets a\n * session surface gate CLI-only affordances (permission modes, context usage,\n * rate limits) without looking the profile back up. Absent = 'claude'. */\n engine?: ProfileEngine\n model?: string\n permissionMode?: PermissionMode\n /** See the `system_init` event; 'oauth' = claude.ai subscription credentials. */\n apiKeySource?: string\n createdAt: number\n /** Highest event seq emitted so far; attach with `afterSeq` to catch up. */\n lastSeq: number\n pendingPermissionCount: number\n meta?: Record<string, unknown>\n /** Display title: `meta.title` if the host set one, else derived (e.g. first prompt). */\n title?: string\n /** Cumulative cost across all turns so far (sum of turn_result totals). */\n totalCostUsd?: number\n /** Cumulative turn count across the session. */\n numTurns?: number\n /** Epoch ms of the most recent emitted event. */\n lastActivityAt?: number\n}\n\n/**\n * A session in the Agent SDK's on-disk store (independent of this server's registry).\n * Listed so hosts can offer \"resume\" across server restarts: feed `sessionId` to\n * CreateSessionRequest.resume. Mirrors the SDK's SDKSessionInfo, kept browser-safe.\n */\nexport type SdkSessionSummary = {\n sessionId: string\n /** Custom title, auto summary, or first prompt — whichever the SDK has. */\n summary: string\n /** Epoch ms of last modification. */\n lastModified: number\n createdAt?: number\n customTitle?: string\n firstPrompt?: string\n gitBranch?: string\n cwd?: string\n}\n\n/** One deliverable in the session's scratch filesystem (see the `file_delivered` event). */\nexport type SessionFileInfo = { path: string; bytes: number }\n/** `GET {basePath}/sessions/:id/files` — every file currently in the session's VFS.\n * `GET {basePath}/sessions/:id/files/<path>` downloads one (attachment disposition).\n * 404 when the session's engine exposes no VFS (Claude-engine sessions). */\nexport type ListSessionFilesResponse = { files: SessionFileInfo[] }\nexport type ListSessionsResponse = { sessions: SessionInfo[] }\nexport type CreateSessionResponse = { session: SessionInfo }\nexport type GetSessionResponse = { session: SessionInfo }\n\n/** Body of `POST {basePath}/sessions/:id/permissions/:requestId` — the REST counterpart\n * of the WS `permission_decision` command, for remote controllers without a socket\n * (e.g. answering a job's AskUserQuestion from a webhook consumer). 404 = the request\n * is unknown, already resolved, or expired. */\nexport type ResolvePermissionRequest =\n | { behavior: 'allow'; updatedInput?: Record<string, unknown> }\n | { behavior: 'deny'; message?: string; interrupt?: boolean }\nexport type ResolvePermissionResponse = { resolved: true }\n\n/**\n * Body of `POST {basePath}/executions/:executionId/result` — the way a deferred\n * executor (a remote worker, a batch job, a human) delivers the outcome of an\n * execution the session parked on. The session is rehydrated if its runner was\n * torn down, and the result is folded back into the agent loop; a `failed` result\n * is ordinary tool output the agent adapts to, not a session error.\n *\n * Applied **idempotently by `executionId`**: a duplicate or late delivery (one\n * racing the execution watchdog) answers 200 with `applied: false` rather than\n * erroring or applying twice. 404 means no session is parked on that id.\n */\nexport type SubmitExecutionResultRequest =\n | { status: 'ok'; output: ToolExecutionOutput; logs?: string[] }\n | { status: 'failed'; reason: string; error: string; logs?: string[] }\nexport type SubmitExecutionResultResponse = {\n /** False when the id was already settled — the delivery was a no-op. */\n applied: boolean\n /** Session the execution belonged to. */\n sessionId: string\n}\n\nexport type ListSdkSessionsResponse = { sdkSessions: SdkSessionSummary[] }\n/** `GET {basePath}/profiles` — filtered to the profiles the caller may use. */\nexport type ListProfilesResponse = {\n profiles: ProfileInfo[]\n /** Whether this caller may create profiles here — true only when the server has\n * a profile store AND the principal carries `canManageProfiles`. Lets a UI hide\n * controls that would always be refused. */\n canManage?: boolean\n}\n\n/**\n * `POST {basePath}/profiles` — create a managed profile. Available only when the\n * server was given a profile store, and only to a principal with\n * `canManageProfiles`. Profiles declared in server options are code, not data:\n * they cannot be created, edited, or deleted through these routes.\n */\nexport type CreateProfileRequest = ProfileInfo\n\n/** `PATCH {basePath}/profiles/:name` — merge into a managed profile. The name is\n * the route, not the body; pass `null` to clear an optional field. */\nexport type UpdateProfileRequest = Omit<Partial<ProfileInfo>, 'name'>\n\nexport type SaveProfileResponse = { profile: ProfileInfo }\n/** `GET {basePath}/profiles/:name` — the profile plus a fresh config snapshot. */\nexport type GetProfileResponse = { profile: ProfileInfo; config: ProfileConfigSnapshot }\nexport type ErrorResponse = { error: string }\n\n// ---------------------------------------------------------------------------\n// Session notifications (the out-of-band \"something wants you\" channel)\n// ---------------------------------------------------------------------------\n\n/**\n * The moments in an *interactive* session a person needs to hear about when they\n * are not watching it — the whole point being that a phone cannot hold a\n * WebSocket open in the background, so the server has to reach out.\n *\n * Deliberately four: this is a human-attention channel, not an event mirror. The\n * event log stays on the session WS (attach with `afterSeq` to catch up); if you\n * want every assistant message, subscribe there instead.\n */\nexport type SessionNotificationType =\n /** The agent is blocked on an approval — the one that matters most. */\n | 'permission_requested'\n /** A turn finished; the session is idle and waiting for the human. */\n | 'turn_completed'\n /** The session failed (`session_error`). */\n | 'session_error'\n /** The session ended (`session_closed`), whoever ended it. */\n | 'session_closed'\n\n/** One delivery on the session-notification channel (JSON body of a webhook POST). */\nexport type SessionNotification = {\n type: SessionNotificationType\n sessionId: string\n /** Snapshot at notification time — status, title, cwd, cost, `lastSeq`. */\n session: SessionInfo\n /** Seq of the event behind this notification; attach with `afterSeq: seq - 1` to\n * land on it. */\n seq: number\n ts: number\n /** One line fit for a notification body: the permission title, the turn's final\n * text, the error message. */\n preview?: string\n /** `permission_requested` only: the full request, so a consumer can answer it via\n * `POST {basePath}/sessions/:id/permissions/:requestId` — which is what makes an\n * Approve/Deny action on a lock-screen notification possible. */\n request?: PermissionRequest\n /** `turn_completed` only. */\n result?: { isError: boolean; durationMs: number; numTurns: number; totalCostUsd: number }\n /** `session_closed` only. */\n reason?: 'client' | 'server' | 'error'\n}\n\n/** Where session notifications are POSTed (JSON body = {@link SessionNotification}).\n * Server-wide, not per session: the point is to hear about sessions you did not\n * create yourself and are not attached to. */\nexport type SessionWebhookConfig = {\n url: string\n /** Extra headers sent with every delivery (auth tokens etc.). */\n headers?: Record<string, string>\n /** Types to deliver. Default: all of them. */\n events?: SessionNotificationType[]\n}\n\n// ---------------------------------------------------------------------------\n// Job queue (one-shot scheduled runs over the session runner)\n// ---------------------------------------------------------------------------\n\n/**\n * - `queued` — accepted, waiting for a concurrency slot (or the daily token budget)\n * - `running` — a session is executing the prompt\n * - `parked` — waiting on an external event (a deferred tool execution). Not\n * terminal and not consuming a concurrency slot; resumes to `running` when the\n * result arrives, or fails via the execution watchdog if it never does.\n * - `succeeded` / `failed` — terminal; `result` (and `error` on failure) are set\n * - `canceled` — terminal; canceled by a client before or during the run\n */\nexport type JobStatus = 'queued' | 'running' | 'parked' | 'succeeded' | 'failed' | 'canceled'\n\n/** Where job progress/completion deliveries are POSTed (JSON body = {@link JobEvent}). */\nexport type WebhookConfig = {\n url: string\n /** Extra headers sent with every delivery (auth tokens etc.). */\n headers?: Record<string, string>\n /** Delivery granularity: 'messages' also POSTs job_progress per assistant message /\n * permission request; 'completion' only job_started + job_completed. Default 'messages'. */\n progress?: 'messages' | 'completion'\n}\n\n/**\n * Schedule a one-shot run: the session executes `prompt` unattended and the job\n * completes with that run's result. `session.prompt` is the task and is required;\n * `resume`/`forkSession` are not supported for queued jobs.\n */\nexport type CreateJobRequest = {\n session: CreateSessionRequest & { prompt: string }\n webhook?: WebhookConfig\n /** Per-job token cap; the effective cap is min(this, the server's sessionTokenLimit). */\n maxTokens?: number\n /** Per-job wall-clock cap; the effective cap is min(this, the server's maxJobDurationMs). */\n maxDurationMs?: number\n /** Total run attempts: failed (not canceled) runs re-queue until this many attempts\n * have been made. Default 1 (no retries). */\n attempts?: number\n /** Delay before the first retry, doubled for each subsequent one. Default 5000. */\n retryDelayMs?: number\n /** Host bookkeeping echoed back on JobInfo. */\n meta?: Record<string, unknown>\n}\n\n/** Cumulative resource usage of a job's run. `tokens` counts input + output +\n * cache-creation + cache-read tokens across all turns. */\nexport type JobUsage = {\n tokens: number\n totalCostUsd: number\n numTurns: number\n}\n\n/** Terminal outcome of the job's run (mirrors the final turn_result). */\nexport type JobResult = {\n subtype: string\n isError: boolean\n /** Final text of the run (success only). */\n result?: string\n errors?: string[]\n durationMs: number\n}\n\nexport type JobInfo = {\n id: string\n status: JobStatus\n cwd: string\n /** Profile the run executes under (resolved name, present even when implicit). */\n profile?: string\n prompt: string\n /** Server session id once started — attach via the sessions WS to watch the run live. */\n sessionId?: string\n sdkSessionId?: string\n createdAt: number\n startedAt?: number\n finishedAt?: number\n /** 1-based run attempt this info reflects. */\n attempt?: number\n /** Total attempts configured on the request (see CreateJobRequest.attempts). */\n maxAttempts?: number\n /** For a job re-queued by retry backoff: earliest time the next attempt may start. */\n nextRunAt?: number\n /** Set while `status` is 'parked': when the run parked, and the execution it is\n * waiting on — the id to POST a result to. Cleared when it resumes. */\n parkedAt?: number\n parkedExecutionId?: string\n /** Cumulative across attempts. */\n usage: JobUsage\n result?: JobResult\n /** Failure or cancellation reason (for a queued retry: the previous attempt's error). */\n error?: string\n meta?: Record<string, unknown>\n}\n\n/** Latest mid-run activity, carried on job_progress deliveries. */\nexport type JobProgress = {\n kind: 'assistant_text' | 'tool_use' | 'permission_requested' | 'permission_resolved'\n /** Short human-readable preview (message excerpt, tool name, permission title). */\n preview?: string\n /** 'permission_requested' only: the full request (including AskUserQuestion input) so\n * webhook consumers can answer via POST /sessions/:sessionId/permissions/:requestId. */\n request?: PermissionRequest\n}\n\n/** Webhook delivery payload (also the queue's local event shape). `job_submitted` goes\n * to local observers and the queue WS only — the submitter already has the POST\n * response, so webhooks start at `job_started`. `job_retrying` marks a failed run that\n * was re-queued (`job.nextRunAt` says when); `job_completed` is always terminal. */\nexport type JobEvent =\n | { type: 'job_submitted'; job: JobInfo; ts: number }\n | { type: 'job_started'; job: JobInfo; ts: number }\n | { type: 'job_progress'; job: JobInfo; progress: JobProgress; ts: number }\n /** The run parked on a deferred execution; `executionId` says what it waits on —\n * the id to POST a result to. The *work itself* (tool name, input, VFS seed) went\n * to the executor's own dispatch hook, not over this channel: a webhook consumer\n * learns that a run is waiting, the worker learns what to do. */\n | { type: 'job_parked'; job: JobInfo; executionId: string; ts: number }\n /** A parked run resumed because its execution result arrived. */\n | { type: 'job_resumed'; job: JobInfo; executionId: string; ts: number }\n | { type: 'job_retrying'; job: JobInfo; ts: number }\n | { type: 'job_completed'; job: JobInfo; ts: number }\n\nexport type QueueStats = {\n maxConcurrency: number\n running: number\n queued: number\n /** Jobs waiting on a deferred execution. They hold no concurrency slot and\n * their wall-clock budget is not ticking. */\n parked: number\n sessionTokenLimit?: number\n dailyTokenLimit?: number\n /** Tokens consumed by queue jobs in the current UTC day. */\n dailyTokensUsed: number\n /** True when the daily budget is exhausted and queued jobs are being held. */\n paused: boolean\n}\n\n/** Frames sent on the queue WS (`{basePath}/queue/ws`). The stream is one-way\n * (server→client): every job's lifecycle as it happens, plus refreshed stats after\n * lifecycle changes. Clients send nothing; job mutations stay on REST. */\nexport type QueueServerFrame =\n | { type: 'queue_attached'; protocolVersion: number; stats: QueueStats }\n | { type: 'job_event'; event: JobEvent }\n | { type: 'queue_stats'; stats: QueueStats }\n\nexport type CreateJobResponse = { job: JobInfo }\nexport type GetJobResponse = { job: JobInfo }\nexport type ListJobsResponse = { jobs: JobInfo[] }\nexport type QueueStatsResponse = { stats: QueueStats }\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,mBAAmB;;;;;;AA4dhC,MAAa,4BAAuD;CAClE;CACA;CACA;CACD;;;;;;AAOD,SAAgB,uBACd,QACA,MACS;AACT,QAAO,WAAW,aAAa,0BAA0B,SAAS,KAAK,GAAG"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@workerdeck/protocol",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "description": "The WorkerDeck wire protocol: typed session events, commands, and REST shapes shared by server and clients. Dependency-free, browser-safe. This protocol is the product boundary — versioned from day one.",
6
+ "license": "MIT",
7
+ "main": "./build/index.mjs",
8
+ "types": "./build/index.d.mts",
9
+ "files": [
10
+ "build"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "@workerdeck/source": "./src/index.ts",
15
+ "types": "./build/index.d.mts",
16
+ "default": "./build/index.mjs"
17
+ }
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.10.0",
21
+ "rimraf": "^6.1.3",
22
+ "tsdown": "^0.21.10"
23
+ },
24
+ "author": "Tobias Strebitzer",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/workerdeck/workerdeck.git",
28
+ "directory": "packages/protocol"
29
+ },
30
+ "homepage": "https://workerdeck.github.io/workerdeck/",
31
+ "bugs": "https://github.com/workerdeck/workerdeck/issues",
32
+ "keywords": [
33
+ "claude",
34
+ "claude-code",
35
+ "anthropic",
36
+ "agent",
37
+ "protocol",
38
+ "websocket",
39
+ "types"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "clean": "rimraf build",
46
+ "build": "tsdown",
47
+ "typecheck": "tsgo -p tsconfig.json"
48
+ }
49
+ }