@workerdeck/core 0.16.0 → 0.17.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/README.md CHANGED
@@ -165,10 +165,32 @@ selectExecutor: () => new DeferredExecutor({
165
165
  for you — a `SessionStore` plus `POST /executions/:id/result` — but the mechanism is here, and works
166
166
  with no server at all.
167
167
 
168
+ `snapshot()` is the same value **without** the teardown: the runner stays live, attached and warm.
169
+ That separation is what makes a provider session survive a process restart, since it has no
170
+ engine-side store to resume from the way claude and codex do — the host writes the snapshot through
171
+ after each turn and rebuilds from the last one. The gate differs from `park()`'s in one direction
172
+ only: it refuses a turn in flight and pending *in-process* executions (whose results die with the
173
+ process), and allows the idle case `park()` exists to refuse.
174
+
168
175
  ## Rules you cannot infer from the types
169
176
 
170
177
  Things the compiler will not tell you, each of which has cost someone real time:
171
178
 
179
+ - **Truncation happens on the replay path, never at emit.** `subscribe(..., { truncateResults })`
180
+ hands out a copy; `#events` keeps the whole result, because the live path, the parking snapshot
181
+ and `Runner.eventAt` (which serves `GET /sessions/:id/events/:seq/result`) all read it. Refuse
182
+ the temptation to truncate into a snapshot: it would break the fetch for exactly the sessions
183
+ most likely to be read late.
184
+
185
+ - **Image refs happen there too — and on the live path as well.** `subscribe(..., { imageRefs })`
186
+ replaces a `tool_result`'s base64 `image` parts with `image_ref` addresses, and unlike truncation
187
+ it applies to live events as well as the replay, because a client's one render path is
188
+ ref-then-fetch. The same "never at emit" rule holds for the same reason: `#events` keeps every
189
+ byte, which is what the fetch route serves back. `SubscriberSet` (`src/lib/subscribers.ts`) is
190
+ where that per-subscriber decision lives — a subscriber is a listener *plus what it asked for*,
191
+ so the three runners no longer each own a copy of the answer. Consumers that subscribe with no
192
+ options — parking, notifications, the queue — see everything, as they do for every rule here.
193
+
172
194
  - **A declared MCP server that never connected is refused, not degraded.** If a profile's
173
195
  `session.mcpServers` names a server and it isn't there, `createEngineSession` throws. The old
174
196
  behaviour — start anyway, minus those tools — produced a session that reported perfectly healthy
package/build/index.d.mts CHANGED
@@ -184,10 +184,45 @@ interface Runner {
184
184
  * default must stay off: it is only sound for a consumer whose handling of
185
185
  * those events is last-write-wins, and `parking.ts` — which subscribes from
186
186
  * seq 0 — branches on `status_changed` instead. Live events are never
187
- * affected; this touches the buffered replay alone. */
187
+ * affected; this touches the buffered replay alone.
188
+ *
189
+ * `truncateResults` delivers an oversized `tool_result` block as its head plus
190
+ * the markers that say so (protocol's {@link TOOL_RESULT_HEAD_CHARS}), leaving
191
+ * the whole thing one fetch away. Measured, that is 68% of a long session's
192
+ * attach in three frames. Opt-in for the same reason and with one extra
193
+ * condition: **the opt-in must be issued by the unit that renders**, because
194
+ * `client` and `react` are separate packages an embedder can skew, and a
195
+ * client that asked for heads without knowing how to fetch the rest would show
196
+ * one as though it were the whole result. Live events are untouched — a result
197
+ * arriving while you watch is already on screen — and so is the stored log,
198
+ * which parking snapshots and the fetch route both read.
199
+ *
200
+ * `imageRefs` replaces a `tool_result`'s base64 image parts with `image_ref`
201
+ * addresses (protocol's {@link ImageRefPart}), their bytes one REST fetch
202
+ * away. Opt-in under the same rule — issued by the unit that renders — but
203
+ * unlike the other two it applies to **live events as well as the replay**,
204
+ * because the client's one render path is ref-then-fetch and bytes on a live
205
+ * event would only be discarded or pinned. Measured, this is 91% of all
206
+ * tool-result payload and 0% of what any client draws. The stored log keeps
207
+ * every byte, which is what the fetch route serves back. */
188
208
  subscribe(listener: SessionEventListener, afterSeq?: number, options?: {
189
209
  coalesceReplay?: boolean;
210
+ truncateResults?: boolean;
211
+ imageRefs?: boolean;
190
212
  }): () => void;
213
+ /** One buffered event by seq, or undefined — the read side of the log the
214
+ * replay already walks.
215
+ *
216
+ * Optional, like every member added after `Runner` became public API: an
217
+ * out-of-tree runner that declines it declines only the on-demand tool result
218
+ * with it (the route 404s), which is exactly the degradation a runner with no
219
+ * `truncateResults` support wants anyway.
220
+ *
221
+ * Deliberately **not** a "give me the whole log" accessor. The one caller
222
+ * needs a single event by a seq a client is holding, and a method that handed
223
+ * out the array would invite a second copy of the bytes this feature exists
224
+ * to stop shipping. */
225
+ eventAt?(seq: number): SessionEvent | undefined;
191
226
  /** Queue a user message for the session (starts the next turn when idle).
192
227
  * `attachments` carry their bytes to the engine and their reference to the
193
228
  * event log (see {@link AttachmentInput}). */
@@ -226,12 +261,39 @@ interface Runner {
226
261
  * doesn't: the CLI owns its own process state).
227
262
  */
228
263
  park?(): RunnerSnapshot | undefined;
264
+ /**
265
+ * The same snapshot, taken **without ending anything** — the runner stays live,
266
+ * attached and warm.
267
+ *
268
+ * Park and snapshot are two operations that happen to produce the same value,
269
+ * and separating them is what makes restart-survival possible for an engine
270
+ * that has no on-disk session of its own. A park is for a session with nothing
271
+ * to do for possibly days; this is for one whose user is mid-conversation and
272
+ * whose process might be redeployed out from under it. The host writes it
273
+ * through after each turn — never on a shutdown hook, because a `kill -9` runs
274
+ * no hook and that is precisely the case worth surviving — and rebuilds from
275
+ * the last write through the ordinary `restore` path.
276
+ *
277
+ * Returns undefined when a snapshot would capture a half-happened turn: one in
278
+ * flight, or pending in-process executions whose results die with the process.
279
+ * Optional for the same reason `park()` is — claude and codex run behind a
280
+ * binary that owns its process state, and have engine-side resume instead.
281
+ */
282
+ snapshot?(): RunnerSnapshot | undefined;
229
283
  /** Emit a session_error and terminate. For host-enforced policy (e.g. requireApiKey). */
230
284
  fail(message: string): void;
231
285
  /** Terminate the session and any underlying engine process. */
232
286
  close(reason?: 'client' | 'server' | 'error'): void;
233
287
  }
234
288
  //#endregion
289
+ //#region src/lib/subscribers.d.ts
290
+ /** What a subscriber asked for. Absent fields mean the untransformed stream. */
291
+ type SubscribeOptions = {
292
+ coalesceReplay?: boolean;
293
+ truncateResults?: boolean;
294
+ imageRefs?: boolean;
295
+ };
296
+ //#endregion
235
297
  //#region src/engines/claude/runner.d.ts
236
298
  type QueryFn = (params: {
237
299
  prompt: AsyncIterable<SDKUserMessage>;
@@ -302,6 +364,10 @@ declare class SessionRunner implements Runner {
302
364
  fail(message: string): void;
303
365
  /** Terminate the session and the underlying CLI subprocess. */
304
366
  close(reason?: 'client' | 'server' | 'error'): void;
367
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
368
+ * "show everything" on one row, so a per-runner seq index would be a map
369
+ * maintained on every emit to save a walk nobody makes twice a minute. */
370
+ eventAt(seq: number): SessionEvent | undefined;
305
371
  /**
306
372
  * Replay buffered events with seq > afterSeq, then deliver live events.
307
373
  * Returns an unsubscribe function.
@@ -315,11 +381,64 @@ declare class SessionRunner implements Runner {
315
381
  * reconnecting client still holding pre-reset rows; superseded resets are
316
382
  * content below the newer one and are skipped with what they cleared.
317
383
  */
318
- subscribe(listener: SessionEventListener, afterSeq?: number, options?: {
319
- coalesceReplay?: boolean;
320
- }): () => void;
384
+ subscribe(listener: SessionEventListener, afterSeq?: number, options?: SubscribeOptions): () => void;
321
385
  }
322
386
  //#endregion
387
+ //#region src/lib/replay.d.ts
388
+ /**
389
+ * The one replay body, and what a socket receives from it.
390
+ *
391
+ * Every runner had a byte-identical copy of this loop — three spellings of four
392
+ * rules, one of which ("never drop the highest-seq event, whatever the rule
393
+ * says") is load-bearing and was three copies of a comment. Not a base class:
394
+ * the runners share nothing else, and a base class would have to own `#emit`,
395
+ * the most engine-specific method each of them has.
396
+ *
397
+ * The rules, in the order they are applied:
398
+ *
399
+ * 1. `afterSeq` — the caller already holds everything at or below it.
400
+ * 2. `resetSeq` — transcript *content* strictly below the latest
401
+ * `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
402
+ * conversation while state events still replay. Claude's alone; the other
403
+ * engines pass 0.
404
+ * 3. `coalesceReplay` — last-write-wins state readings superseded later in the
405
+ * same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
406
+ * reducer reads and discards. Opt-in, and only sound for a consumer whose
407
+ * handling of those events is last-write-wins.
408
+ * 4. `imageRefs` — a base64 image part is delivered as an `image_ref` address
409
+ * (protocol's {@link imagePartRef}), its bytes one REST fetch away. Applied
410
+ * **before** rule 5, because it stamps indices from the stored part array
411
+ * which rule 5 then reshapes. Unlike rule 5 this also applies to the live
412
+ * path (see `SubscriberSet`), which is the one place these two rules differ.
413
+ * 5. `truncateResults` — a huge `tool_result` block is delivered as its head
414
+ * plus the markers that say so. **Never mutates the stored event**: the live
415
+ * path, the parking snapshot and the fetch route all need the whole thing,
416
+ * so this builds a copy and the log stays the log.
417
+ *
418
+ * The highest-seq event is delivered whatever rules 2 and 3 say — a client's
419
+ * replay hold waits for `state.lastSeq` to reach the attach's and would
420
+ * otherwise hang forever — but it is still *truncated* when rule 4 applies. A
421
+ * session that ends on a `find /` puts its 641 KB frame exactly there.
422
+ */
423
+ declare function replaySlice(events: readonly SessionEvent[], options: {
424
+ afterSeq: number;
425
+ resetSeq?: number;
426
+ coalesceReplay?: boolean;
427
+ truncateResults?: boolean;
428
+ imageRefs?: boolean;
429
+ }): SessionEvent[];
430
+ /**
431
+ * A copy of `event` whose oversized `tool_result` blocks carry their head and
432
+ * say so — or `event` itself, unchanged and un-copied, when nothing is over the
433
+ * budget. That identity matters: an attach is mostly small events, and a fresh
434
+ * object for every one of them would cost more than the feature saves.
435
+ *
436
+ * Blocks are measured and cut **individually**. A message answering three calls
437
+ * where one is a `find /` keeps the two small results whole, which is what makes
438
+ * the per-block marker (rather than a per-event one) honest.
439
+ */
440
+ declare function truncateResultBlocks(event: SessionEvent): SessionEvent;
441
+ //#endregion
323
442
  //#region src/engines/provider/runner.d.ts
324
443
  /** `cwd` is optional for this engine: the loop has no host-filesystem coupling
325
444
  * (tools get scoped VFS handles instead). Defaults to process.cwd() for display. */
@@ -457,6 +576,34 @@ declare class AiSdkRunner implements Runner {
457
576
  * or an already-closed/parked runner.
458
577
  */
459
578
  park(): RunnerSnapshot | undefined;
579
+ /**
580
+ * The same snapshot, taken without ending anything.
581
+ *
582
+ * `park()` and this are two operations that happen to produce the same value,
583
+ * and the difference is the whole point: `park()` *ends* the live runner
584
+ * (inert, listeners dropped, `onClose` called), which is right for deferred
585
+ * execution — the session has nothing to do for possibly days — and wrong for
586
+ * restart-survival, where the session is active and someone is mid-
587
+ * conversation. This one changes nothing at all: no status emit, no listener
588
+ * clear, no disposer. The host writes the value through to durable storage
589
+ * after each turn and keeps the runner live and warm, so a restart rebuilds
590
+ * from the last write through the existing `restore` path and the next message
591
+ * costs no wake.
592
+ *
593
+ * The gate is `park()`'s minus the requirement that there be something parked:
594
+ *
595
+ * - `#abort` set is refused for the reason it always was — a `generate()` in
596
+ * flight has produced messages that are not in the history yet, so the
597
+ * snapshot would be of a turn that half-happened.
598
+ * - Pending calls that are **not** all deferred are refused, which is
599
+ * `park()`'s rule wearing a different hat. An in-process execution's result
600
+ * is coming back to *this* runner and dies with the process; a restore would
601
+ * wait on it forever, and `state.dispatched` is what would stop the rebuilt
602
+ * runner from simply calling it again.
603
+ * - Idle with nothing pending — the case `park()` exists to refuse — is
604
+ * exactly the case this exists to allow.
605
+ */
606
+ snapshot(): RunnerSnapshot | undefined;
460
607
  sendMessage(text: string, attachments?: readonly AttachmentInput[]): void;
461
608
  /**
462
609
  * Deliver the result of an external (execute-less) tool call. Appends the
@@ -485,9 +632,11 @@ declare class AiSdkRunner implements Runner {
485
632
  setModel(model?: string): Promise<void>;
486
633
  fail(message: string): void;
487
634
  close(reason?: 'client' | 'server' | 'error'): void;
488
- subscribe(listener: SessionEventListener, afterSeq?: number, options?: {
489
- coalesceReplay?: boolean;
490
- }): () => void;
635
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
636
+ * "show everything" on one row, so a per-runner seq index would be a map
637
+ * maintained on every emit to save a walk nobody makes twice a minute. */
638
+ eventAt(seq: number): SessionEvent | undefined;
639
+ subscribe(listener: SessionEventListener, afterSeq?: number, options?: SubscribeOptions): () => void;
491
640
  /**
492
641
  * Deliver the result of an execution this runner dispatched. Used by the host
493
642
  * when a backend settled out-of-band (a browser bridge answering later, a
@@ -1528,9 +1677,11 @@ declare class CodexRunner implements Runner {
1528
1677
  setModel(model?: string): Promise<void>;
1529
1678
  fail(message: string): void;
1530
1679
  close(reason?: 'client' | 'server' | 'error'): void;
1531
- subscribe(listener: SessionEventListener, afterSeq?: number, options?: {
1532
- coalesceReplay?: boolean;
1533
- }): () => void;
1680
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
1681
+ * "show everything" on one row, so a per-runner seq index would be a map
1682
+ * maintained on every emit to save a walk nobody makes twice a minute. */
1683
+ eventAt(seq: number): SessionEvent | undefined;
1684
+ subscribe(listener: SessionEventListener, afterSeq?: number, options?: SubscribeOptions): () => void;
1534
1685
  /**
1535
1686
  * The session's MCP servers, live from the binary.
1536
1687
  *
@@ -1625,5 +1776,5 @@ declare class JsonRpcStdioConnection {
1625
1776
  */
1626
1777
  declare const providerAdapter: EngineAdapter;
1627
1778
  //#endregion
1628
- export { AiSdkRunner, type AiSdkRunnerConfig, type AiSdkSessionState, type AppServerConnectFn, type AppServerConnectOptions, type AppServerConnection, type AppServerHistoryTurn, type AppServerItem, type AppServerThreadListResponse, type AppServerThreadSummary, type AppServerTokenUsage, type AppServerTurn, type AppServerUserInput, type AttachmentInput, type AttachmentKind, type BridgeAnswer, BrowserBridgeExecutor, type BrowserBridgeExecutorOptions, CLAUDE_CATALOG, CODEX_CATALOG, type ClaudeAuthProbe, type ClaudeAuthStatus, CodexRunner, type CodexRunnerConfig, type DeferredDispatch, DeferredExecutor, type DeferredExecutorOptions, type EngineAdapter, type EngineAvailability, type EngineRunnerRequest, type EngineSessionOptions, type HistoryFn, type HostFetch, type HostToolDefinition, InputQueue, JsonRpcError, JsonRpcStdioConnection, type LanguageModel, type McpConnection, type ModelCatalog, type ParkedExecution, type PendingEntry, type PendingKind, type PendingOutcome, PendingRequestRegistry, type PendingToolCall, type PermissionDecision, type QueryFn, QuickJsExecutor, type QuickJsExecutorOptions, type RegisterOptions, type Runner, type RunnerSnapshot, SUPPORTED_ATTACHMENT_TYPES, type SessionEventListener, SessionRunner, type SessionRunnerConfig, type SettledBy, type Tool, type ToolCallOutput, type ToolContext, type ToolContextOptions, type ToolDefinition, type ToolExecutionCall, type ToolExecutionDispatch, type ToolExecutionResult, type ToolExecutor, type ToolSet, type ToolTrust, type WebFetchDigest, type WebFetchFn, type WebFetchOptions, type WebFetchResult, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
1779
+ export { AiSdkRunner, type AiSdkRunnerConfig, type AiSdkSessionState, type AppServerConnectFn, type AppServerConnectOptions, type AppServerConnection, type AppServerHistoryTurn, type AppServerItem, type AppServerThreadListResponse, type AppServerThreadSummary, type AppServerTokenUsage, type AppServerTurn, type AppServerUserInput, type AttachmentInput, type AttachmentKind, type BridgeAnswer, BrowserBridgeExecutor, type BrowserBridgeExecutorOptions, CLAUDE_CATALOG, CODEX_CATALOG, type ClaudeAuthProbe, type ClaudeAuthStatus, CodexRunner, type CodexRunnerConfig, type DeferredDispatch, DeferredExecutor, type DeferredExecutorOptions, type EngineAdapter, type EngineAvailability, type EngineRunnerRequest, type EngineSessionOptions, type HistoryFn, type HostFetch, type HostToolDefinition, InputQueue, JsonRpcError, JsonRpcStdioConnection, type LanguageModel, type McpConnection, type ModelCatalog, type ParkedExecution, type PendingEntry, type PendingKind, type PendingOutcome, PendingRequestRegistry, type PendingToolCall, type PermissionDecision, type QueryFn, QuickJsExecutor, type QuickJsExecutorOptions, type RegisterOptions, type Runner, type RunnerSnapshot, SUPPORTED_ATTACHMENT_TYPES, type SessionEventListener, SessionRunner, type SessionRunnerConfig, type SettledBy, type Tool, type ToolCallOutput, type ToolContext, type ToolContextOptions, type ToolDefinition, type ToolExecutionCall, type ToolExecutionDispatch, type ToolExecutionResult, type ToolExecutor, type ToolSet, type ToolTrust, type WebFetchDigest, type WebFetchFn, type WebFetchOptions, type WebFetchResult, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, truncateResultBlocks, withHostTools, withMcpTools };
1629
1780
  //# sourceMappingURL=index.d.mts.map