@tanstack/ai-sandbox 0.2.3 → 0.3.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.
Files changed (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
package/dist/esm/run.d.ts CHANGED
@@ -1,27 +1,74 @@
1
- import { StreamChunk } from '@tanstack/ai';
2
- import { RunEvent, RunEventLog, RunRecord } from './run-log.js';
3
- export interface PipeToRunLogOptions {
4
- log: RunEventLog;
1
+ import { InternalLogger } from '@tanstack/ai/adapter-internals';
2
+ import { RunRecord, RunStore, StreamChunk, StreamDurability } from '@tanstack/ai';
3
+ /**
4
+ * The two durable seams a run driver needs: lifecycle record + event log.
5
+ *
6
+ * Generic in the log's offset type, and DEFAULTED to `string` so every existing
7
+ * call site keeps compiling unchanged. The parameter is not decoration: a
8
+ * backend that brands its cursors — `@tanstack/ai-durable-stream`'s
9
+ * `durableStream` returns `StreamDurability<DurableStreamOffset>` — is NOT
10
+ * assignable to `StreamDurability<string>`, because `read` takes an offset and
11
+ * is therefore contravariant in it. Hardcoding the default here made the
12
+ * production multi-host backend unusable without a cast (see
13
+ * `tests/offset-generics.test-d.ts`).
14
+ */
15
+ export interface RunDeps<TOffset extends string = string> {
16
+ /** Run lifecycle record (status, thread, timings). */
17
+ runs: RunStore;
18
+ /**
19
+ * Per-run delivery-durable event log the run's chunks are appended to.
20
+ *
21
+ * A FACTORY, not an instance, and that is load-bearing rather than stylistic.
22
+ * A `StreamDurability` is bound to one run — `memoryStream(request)` resolves
23
+ * its `runId` from the request, and a backend adapter's offsets embed a cursor
24
+ * into one log. Holding a single instance made two failures reachable:
25
+ *
26
+ * - **Silent mis-binding at concurrency 1.** `start({ runId })` accepted an
27
+ * arbitrary id while the instance was bound to another, writing the record
28
+ * under one id and the events under another with no error raised. Resolving
29
+ * the log FROM the `runId` makes that unrepresentable.
30
+ * - **Cross-talk at concurrency > 1.** Parallel runs interleaved their chunks
31
+ * into one log, and whichever finished first called `close()` and
32
+ * terminalized every other run's stream.
33
+ *
34
+ * Called exactly once per run, at the start of {@link pipeToRunLog}. An
35
+ * implementation MUST return the same instance for the same `runId` within a
36
+ * process if it wants `snapshot()` to see its own appends.
37
+ */
38
+ durability: (runId: string) => StreamDurability<TOffset>;
39
+ /**
40
+ * Optional sink for failures this driver absorbs rather than rejecting with.
41
+ * A detached run has no caller to receive an error, so without a logger a
42
+ * failing store or event log is invisible to an operator. Same
43
+ * `logger?.errors(...)` contract core uses in `stream-to-response.ts`.
44
+ */
45
+ logger?: InternalLogger;
46
+ }
47
+ export interface PipeToRunLogOptions<TOffset extends string = string> extends RunDeps<TOffset> {
5
48
  runId: string;
6
- threadId?: string;
49
+ threadId: string;
7
50
  /** Abort consumption mid-stream; the run finishes as `aborted`. */
8
51
  signal?: AbortSignal;
9
52
  }
10
53
  /**
11
54
  * Open the run, append every chunk from `stream`, and finish with the right
12
55
  * terminal status. Resolves with the final {@link RunRecord} and never rejects:
13
- * a thrown stream error is surfaced as a `RUN_ERROR` event + the record's
14
- * `error`, which is what tailing clients see.
56
+ * a thrown stream error is surfaced as a `RUN_ERROR` event plus the record's
57
+ * `error`, which is what tailing clients see. A store or event-log failure
58
+ * along the way is logged through {@link RunDeps.logger} and still terminalizes
59
+ * the run rather than escaping to a caller that does not exist.
15
60
  *
16
- * - normal completion → `finish('done')`
17
- * - a `RUN_ERROR` chunk → append it, then `finish('error', { message, code })`
18
- * - the stream throws → append a synthesized `RUN_ERROR`, then `finish('error')`
19
- * - `signal` aborts mid-stream → stop consuming, `finish('aborted')`
61
+ * - normal completion → `completed`
62
+ * - a `RUN_ERROR` chunk → append it, then `failed`
63
+ * - the stream throws → append a synthesized `RUN_ERROR`, then `failed`
64
+ * - `signal` aborts at ANY point before the stream ends → `aborted`, whether the
65
+ * producer keeps yielding, ends its stream, or is never asked for another
66
+ * chunk. An abort outranks a clean exit: the run did not complete.
20
67
  */
21
- export declare function pipeToRunLog(stream: AsyncIterable<StreamChunk>, opts: PipeToRunLogOptions): Promise<RunRecord>;
68
+ export declare function pipeToRunLog<TOffset extends string = string>(stream: AsyncIterable<StreamChunk>, opts: PipeToRunLogOptions<TOffset>): Promise<RunRecord>;
22
69
  export interface RunControllerStartInput {
23
70
  runId: string;
24
- threadId?: string;
71
+ threadId: string;
25
72
  stream: AsyncIterable<StreamChunk>;
26
73
  /** Abort consumption mid-stream; the run finishes as `aborted`. */
27
74
  signal?: AbortSignal;
@@ -32,27 +79,48 @@ export interface RunHandle {
32
79
  done: Promise<RunRecord>;
33
80
  }
34
81
  /**
35
- * Thin orchestration helper over a {@link RunEventLog}: fire-and-track a run via
36
- * {@link pipeToRunLog}, tail it from a cursor, and `drain()` all in-flight runs
82
+ * Thin orchestration helper over {@link RunDeps}: fire-and-track a run via
83
+ * {@link pipeToRunLog}, tail one run by id, and `drain()` all in-flight runs
37
84
  * (e.g. inside a `ctx.waitUntil`). Holds no run state of its own beyond the set
38
85
  * of currently in-flight `done` promises.
86
+ *
87
+ * Safe for concurrent runs. {@link RunDeps.durability} is a per-run factory, so
88
+ * each run appends to its own log and no run's `close()` terminalizes another's.
89
+ * The identity trap this class used to document — `start({ runId })` writing the
90
+ * lifecycle record under one id and the events under another, silently and at
91
+ * concurrency 1 — is unrepresentable now that the log is resolved FROM the
92
+ * `runId`. Every method is keyed by run accordingly: `attach(runId, …)` and
93
+ * `status(runId)` no longer disagree about whether the surface is per-run.
39
94
  */
40
- export declare class RunController {
41
- private readonly log;
95
+ export declare class RunController<TOffset extends string = string> {
96
+ private readonly deps;
42
97
  private readonly inFlight;
43
- constructor(log: RunEventLog);
98
+ constructor(deps: RunDeps<TOffset>);
44
99
  /**
45
100
  * Kick off `pipeToRunLog` without awaiting it and return the `runId`
46
101
  * immediately plus a `done` promise the orchestrator may await or detach.
47
102
  */
48
103
  start(input: RunControllerStartInput): RunHandle;
49
- /** Resumable client tail — replay from `fromSeq`, then live-tail to terminal. */
50
- attach(runId: string, opts?: {
51
- fromSeq?: number;
52
- signal?: AbortSignal;
53
- }): AsyncIterable<RunEvent>;
54
- /** Current run record, or null if the run is unknown. */
104
+ /**
105
+ * Resumable client tail for ONE run — replay from `fromOffset`, then
106
+ * live-tail. Takes `runId` because the log it reads is per-run; the old
107
+ * `attach(fromOffset)` signature advertised a multi-run surface the type could
108
+ * not deliver.
109
+ */
110
+ attach(runId: string, fromOffset: TOffset, signal?: AbortSignal): AsyncIterable<{
111
+ offset: TOffset;
112
+ chunk: StreamChunk;
113
+ }>;
114
+ /** Current run record, or null when the run is unknown. */
55
115
  status(runId: string): Promise<RunRecord | null>;
56
- /** Await every currently in-flight run's `done` promise. */
116
+ /**
117
+ * Await every currently in-flight run's `done` promise.
118
+ *
119
+ * Uses `allSettled` rather than `all` because this is typically awaited
120
+ * inside a `ctx.waitUntil`: `all` would reject on the first failure, abandon
121
+ * the wait on every other run, and surface that rejection to the platform.
122
+ * Draining is about keeping the isolate alive until the runs settle; each
123
+ * run's own outcome is already recorded in its record and log.
124
+ */
57
125
  drain(): Promise<void>;
58
126
  }
package/dist/esm/run.js CHANGED
@@ -1,89 +1,284 @@
1
1
  import { EventType } from "@tanstack/ai";
2
+ import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
3
+ //#region src/run.ts
4
+ /**
5
+ * The "run driver" for the inverted/serverless sandbox model: pump a `chat()`
6
+ * stream into core's two durable seams — a {@link RunStore} for the run's
7
+ * lifecycle record and a {@link StreamDurability} for its event log — so a
8
+ * trigger can return immediately while a durable orchestrator drives the run
9
+ * and clients tail from an opaque offset.
10
+ *
11
+ * The key inversion vs. a classic request/response handler: there is no caller
12
+ * holding the stream open, so nothing to throw an error *back to*. The event log
13
+ * is the only channel — every chunk (including a terminal
14
+ * {@link EventType.RUN_ERROR}) is appended and assigned a resumable offset, and
15
+ * a thrown stream error is recorded as a synthesized `RUN_ERROR` event plus the
16
+ * record's `error` field. Tailing clients therefore always observe failures;
17
+ * {@link pipeToRunLog} never rejects.
18
+ *
19
+ * "Never rejects" is load-bearing rather than aspirational: {@link RunController}
20
+ * consumes the returned promise fire-and-forget, so a rejection would be an
21
+ * unhandled rejection (process-fatal on modern Node, instance-fatal inside a
22
+ * Durable Object) with nobody to report it to. Every store/log call is therefore
23
+ * individually guarded, and because absorbing a failure silently in the one
24
+ * module whose premise is that nobody is listening would make the failure
25
+ * invisible, each guard reports through the optional {@link RunDeps.logger}.
26
+ */
27
+ /** Whether a chunk is the terminal error event the chat engine emits. */
2
28
  function isRunErrorChunk(chunk) {
3
- return chunk.type === EventType.RUN_ERROR;
29
+ return chunk.type === EventType.RUN_ERROR;
4
30
  }
5
- function runErrorFromChunk(chunk) {
6
- return chunk.code !== void 0 ? { message: chunk.message, code: chunk.code } : { message: chunk.message };
31
+ /**
32
+ * Narrow a thrown value (or a `RUN_ERROR` chunk's payload) to the record's
33
+ * structured error, keeping the provider's `code` when it supplies one: a bare
34
+ * message is prose that changes between model versions, while `code` is what a
35
+ * consumer branches on to retry, escalate, or show specific UI.
36
+ */
37
+ function toRunError(error) {
38
+ const payload = toRunErrorPayload(error);
39
+ return {
40
+ message: payload.message,
41
+ ...payload.code === void 0 ? {} : { code: payload.code }
42
+ };
7
43
  }
8
- function messageOf(error) {
9
- return error instanceof Error ? error.message : String(error);
44
+ /**
45
+ * Fold a secondary failure into the primary error, mirroring `combineFailures`
46
+ * in `packages/ai/src/stream-to-response.ts`: the primary cause stays first and
47
+ * keeps its `code`, and the phase that produced the secondary failure is named.
48
+ * The secondary must never *replace* the primary: the provider's error is what
49
+ * an operator needs, and a failure while recording it is the lesser fact.
50
+ */
51
+ function withSecondaryFailure(primary, secondary, phase) {
52
+ return {
53
+ ...primary,
54
+ message: `${primary.message}; ${phase}: ${toRunError(secondary).message}`
55
+ };
10
56
  }
11
- function syntheticRunError(message) {
12
- const chunk = {
13
- type: EventType.RUN_ERROR,
14
- message
15
- };
16
- return chunk;
57
+ /** Build the synthetic RUN_ERROR chunk appended when the stream throws. */
58
+ function syntheticRunError(error) {
59
+ return {
60
+ type: EventType.RUN_ERROR,
61
+ message: error.message,
62
+ ...error.code === void 0 ? {} : { code: error.code }
63
+ };
17
64
  }
18
- async function pipeToRunLog(stream, opts) {
19
- const { log, runId, threadId, signal } = opts;
20
- await log.open(threadId !== void 0 ? { runId, threadId } : { runId });
21
- if (signal?.aborted) {
22
- await log.finish(runId, "aborted");
23
- return reread(log, runId);
24
- }
25
- try {
26
- for await (const chunk of stream) {
27
- if (signal?.aborted) {
28
- await log.finish(runId, "aborted");
29
- return reread(log, runId);
30
- }
31
- await log.append(runId, chunk);
32
- if (isRunErrorChunk(chunk)) {
33
- await log.finish(runId, "error", runErrorFromChunk(chunk));
34
- return reread(log, runId);
35
- }
36
- }
37
- } catch (error) {
38
- const message = messageOf(error);
39
- await log.append(runId, syntheticRunError(message));
40
- await log.finish(runId, "error", { message });
41
- return reread(log, runId);
42
- }
43
- await log.finish(runId, "done");
44
- return reread(log, runId);
65
+ /**
66
+ * Report through a consumer-supplied logger without letting it break the
67
+ * caller. Every logger call in this module sits inside a `catch` body, so an
68
+ * throwing sink would escape that body and defeat the totality the guards
69
+ * exist to provide. Swallowing here is deliberate: there is no second channel
70
+ * left to report a reporting failure on.
71
+ */
72
+ function safeLog(logger, message, context) {
73
+ try {
74
+ logger?.errors(message, context);
75
+ } catch {}
45
76
  }
46
- async function reread(log, runId) {
47
- const latest = await log.get(runId);
48
- if (!latest) throw new Error(`run: record for "${runId}" vanished mid-run`);
49
- return latest;
77
+ /**
78
+ * Record the terminal status, terminalize the event log, and answer with the
79
+ * run's final record.
80
+ *
81
+ * TOTAL BY CONSTRUCTION: every step is individually guarded, so this never
82
+ * throws and never rejects. Two consequences the guards buy:
83
+ *
84
+ * - `durability.close()` runs on EVERY exit path, including a failed `update`.
85
+ * Skipping it would wedge the record at `running` *and* park every live
86
+ * tailer forever, because a durability `read` only ends once the log closes.
87
+ * - The re-read of the record is best effort. An eventually-consistent or
88
+ * read-replica store may answer `null` for a run that was just driven, which
89
+ * must not turn a successful run into a rejection; the locally rebuilt record
90
+ * is returned instead. It is also preferred outright when `update` failed,
91
+ * since the store then still holds the stale `running` row.
92
+ *
93
+ * THE TERMINAL WRITE IS NOT GUARANTEED TO LAND, and this function deliberately
94
+ * does not check whether it did. Under `sandboxRunDriver` the `runs` handed in is
95
+ * `fenceRunStore`d (`src/claim.ts`), which SUPPRESSES a terminal write — resolving
96
+ * without writing — when the driver has lost its claim, because a host that no
97
+ * longer owns the run must not declare it over while the successor is streaming
98
+ * it. From here that is indistinguishable from a successful write, on purpose:
99
+ * the epoch belongs to the claim module, not to this generic driver, and the
100
+ * re-read below then answers with the successor's live record, which is the
101
+ * truthful thing to resolve with. A driver wired without a claim (a plain
102
+ * `pipeToRunLog` call) is unfenced and always writes.
103
+ */
104
+ async function finish(ctx, status, error) {
105
+ const { runs, durability, runId, logger } = ctx;
106
+ const patch = {
107
+ status,
108
+ finishedAt: Date.now(),
109
+ ...error === void 0 ? {} : { error }
110
+ };
111
+ const local = {
112
+ runId,
113
+ threadId: ctx.threadId,
114
+ startedAt: ctx.startedAt,
115
+ ...patch
116
+ };
117
+ let recorded = true;
118
+ try {
119
+ await runs.update(runId, patch);
120
+ } catch (updateError) {
121
+ recorded = false;
122
+ safeLog(logger, "run: recording the terminal run record failed", {
123
+ runId,
124
+ status,
125
+ error: updateError
126
+ });
127
+ }
128
+ try {
129
+ await durability.close();
130
+ } catch (closeError) {
131
+ safeLog(logger, "run: closing the run event log failed", {
132
+ runId,
133
+ status,
134
+ error: closeError
135
+ });
136
+ }
137
+ if (!recorded) return local;
138
+ try {
139
+ const latest = await runs.get(runId);
140
+ if (latest !== null) return latest;
141
+ safeLog(logger, "run: record vanished before the terminal re-read", {
142
+ runId,
143
+ status
144
+ });
145
+ } catch (getError) {
146
+ safeLog(logger, "run: re-reading the terminal run record failed", {
147
+ runId,
148
+ status,
149
+ error: getError
150
+ });
151
+ }
152
+ return local;
50
153
  }
51
- class RunController {
52
- constructor(log) {
53
- this.log = log;
54
- }
55
- log;
56
- inFlight = /* @__PURE__ */ new Set();
57
- /**
58
- * Kick off `pipeToRunLog` without awaiting it and return the `runId`
59
- * immediately plus a `done` promise the orchestrator may await or detach.
60
- */
61
- start(input) {
62
- const done = pipeToRunLog(input.stream, {
63
- log: this.log,
64
- runId: input.runId,
65
- ...input.threadId !== void 0 ? { threadId: input.threadId } : {},
66
- ...input.signal !== void 0 ? { signal: input.signal } : {}
67
- });
68
- this.inFlight.add(done);
69
- void done.finally(() => this.inFlight.delete(done));
70
- return { runId: input.runId, done };
71
- }
72
- /** Resumable client tail — replay from `fromSeq`, then live-tail to terminal. */
73
- attach(runId, opts) {
74
- return this.log.read(runId, opts);
75
- }
76
- /** Current run record, or null if the run is unknown. */
77
- status(runId) {
78
- return this.log.get(runId);
79
- }
80
- /** Await every currently in-flight run's `done` promise. */
81
- async drain() {
82
- await Promise.all([...this.inFlight]);
83
- }
154
+ /**
155
+ * Open the run, append every chunk from `stream`, and finish with the right
156
+ * terminal status. Resolves with the final {@link RunRecord} and never rejects:
157
+ * a thrown stream error is surfaced as a `RUN_ERROR` event plus the record's
158
+ * `error`, which is what tailing clients see. A store or event-log failure
159
+ * along the way is logged through {@link RunDeps.logger} and still terminalizes
160
+ * the run rather than escaping to a caller that does not exist.
161
+ *
162
+ * - normal completion → `completed`
163
+ * - a `RUN_ERROR` chunk → append it, then `failed`
164
+ * - the stream throws → append a synthesized `RUN_ERROR`, then `failed`
165
+ * - `signal` aborts at ANY point before the stream ends → `aborted`, whether the
166
+ * producer keeps yielding, ends its stream, or is never asked for another
167
+ * chunk. An abort outranks a clean exit: the run did not complete.
168
+ */
169
+ async function pipeToRunLog(stream, opts) {
170
+ const { runs, runId, threadId, signal, logger } = opts;
171
+ const durability = opts.durability(runId);
172
+ const ctx = {
173
+ runs,
174
+ durability,
175
+ runId,
176
+ threadId,
177
+ startedAt: Date.now(),
178
+ ...logger === void 0 ? {} : { logger }
179
+ };
180
+ try {
181
+ await runs.createOrResume({
182
+ runId,
183
+ threadId,
184
+ startedAt: ctx.startedAt
185
+ });
186
+ if (signal?.aborted) return finish(ctx, "aborted");
187
+ for await (const chunk of stream) {
188
+ if (signal?.aborted) return finish(ctx, "aborted");
189
+ await durability.append([chunk]);
190
+ if (isRunErrorChunk(chunk)) return finish(ctx, "failed", toRunError({
191
+ message: chunk.message,
192
+ code: chunk.code
193
+ }));
194
+ }
195
+ } catch (streamError) {
196
+ let recorded = toRunError(streamError);
197
+ safeLog(logger, "run: the run failed before completing", {
198
+ runId,
199
+ error: streamError
200
+ });
201
+ try {
202
+ await durability.append([syntheticRunError(recorded)]);
203
+ } catch (appendError) {
204
+ const phase = "appending the synthesized RUN_ERROR failed";
205
+ safeLog(logger, `run: ${phase}`, {
206
+ runId,
207
+ error: appendError
208
+ });
209
+ recorded = withSecondaryFailure(recorded, appendError, phase);
210
+ }
211
+ return finish(ctx, "failed", recorded);
212
+ }
213
+ if (signal?.aborted) return finish(ctx, "aborted");
214
+ return finish(ctx, "completed");
84
215
  }
85
- export {
86
- RunController,
87
- pipeToRunLog
216
+ /**
217
+ * Thin orchestration helper over {@link RunDeps}: fire-and-track a run via
218
+ * {@link pipeToRunLog}, tail one run by id, and `drain()` all in-flight runs
219
+ * (e.g. inside a `ctx.waitUntil`). Holds no run state of its own beyond the set
220
+ * of currently in-flight `done` promises.
221
+ *
222
+ * Safe for concurrent runs. {@link RunDeps.durability} is a per-run factory, so
223
+ * each run appends to its own log and no run's `close()` terminalizes another's.
224
+ * The identity trap this class used to document — `start({ runId })` writing the
225
+ * lifecycle record under one id and the events under another, silently and at
226
+ * concurrency 1 — is unrepresentable now that the log is resolved FROM the
227
+ * `runId`. Every method is keyed by run accordingly: `attach(runId, …)` and
228
+ * `status(runId)` no longer disagree about whether the surface is per-run.
229
+ */
230
+ var RunController = class {
231
+ deps;
232
+ inFlight = /* @__PURE__ */ new Set();
233
+ constructor(deps) {
234
+ this.deps = deps;
235
+ }
236
+ /**
237
+ * Kick off `pipeToRunLog` without awaiting it and return the `runId`
238
+ * immediately plus a `done` promise the orchestrator may await or detach.
239
+ */
240
+ start(input) {
241
+ const done = pipeToRunLog(input.stream, {
242
+ ...this.deps,
243
+ runId: input.runId,
244
+ threadId: input.threadId,
245
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
246
+ });
247
+ this.inFlight.add(done);
248
+ const forget = () => void this.inFlight.delete(done);
249
+ done.then(forget, forget);
250
+ return {
251
+ runId: input.runId,
252
+ done
253
+ };
254
+ }
255
+ /**
256
+ * Resumable client tail for ONE run — replay from `fromOffset`, then
257
+ * live-tail. Takes `runId` because the log it reads is per-run; the old
258
+ * `attach(fromOffset)` signature advertised a multi-run surface the type could
259
+ * not deliver.
260
+ */
261
+ attach(runId, fromOffset, signal) {
262
+ return this.deps.durability(runId).read(fromOffset, signal);
263
+ }
264
+ /** Current run record, or null when the run is unknown. */
265
+ status(runId) {
266
+ return this.deps.runs.get(runId);
267
+ }
268
+ /**
269
+ * Await every currently in-flight run's `done` promise.
270
+ *
271
+ * Uses `allSettled` rather than `all` because this is typically awaited
272
+ * inside a `ctx.waitUntil`: `all` would reject on the first failure, abandon
273
+ * the wait on every other run, and surface that rejection to the platform.
274
+ * Draining is about keeping the isolate alive until the runs settle; each
275
+ * run's own outcome is already recorded in its record and log.
276
+ */
277
+ async drain() {
278
+ await Promise.allSettled([...this.inFlight]);
279
+ }
88
280
  };
89
- //# sourceMappingURL=run.js.map
281
+ //#endregion
282
+ export { RunController, pipeToRunLog };
283
+
284
+ //# sourceMappingURL=run.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sources":["../../src/run.ts"],"sourcesContent":["/**\n * The \"run driver\" for the inverted/serverless sandbox model: pump a `chat()`\n * stream into a {@link RunEventLog} so a trigger can return immediately while a\n * durable orchestrator drives the run and clients tail from a cursor.\n *\n * The key inversion vs. a classic request/response handler: there is no caller\n * holding the stream open, so nothing to throw an error *back to*. The log is\n * the only channel — every chunk (including a terminal {@link EventType.RUN_ERROR})\n * is persisted under a `seq`, and a thrown stream error is recorded as a\n * synthesized `RUN_ERROR` event plus the record's `error` field. Tailing clients\n * therefore always observe failures; {@link pipeToRunLog} never rejects.\n */\nimport { EventType } from '@tanstack/ai'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type { RunError, RunEvent, RunEventLog, RunRecord } from './run-log'\n\n/** Whether a chunk is the terminal error event the chat engine emits. */\nfunction isRunErrorChunk(\n chunk: StreamChunk,\n): chunk is StreamChunk & { message: string; code?: string } {\n return chunk.type === EventType.RUN_ERROR\n}\n\n/** Pull `{ message, code }` off a RUN_ERROR chunk for the run record. */\nfunction runErrorFromChunk(\n chunk: StreamChunk & { message: string; code?: string },\n): RunError {\n return chunk.code !== undefined\n ? { message: chunk.message, code: chunk.code }\n : { message: chunk.message }\n}\n\n/** Render an unknown thrown value as a stable error message. */\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/** Build the synthetic RUN_ERROR chunk appended when the stream throws. */\nfunction syntheticRunError(message: string): StreamChunk {\n const chunk: { type: EventType.RUN_ERROR; message: string } = {\n type: EventType.RUN_ERROR,\n message,\n }\n return chunk\n}\n\nexport interface PipeToRunLogOptions {\n log: RunEventLog\n runId: string\n threadId?: string\n /** Abort consumption mid-stream; the run finishes as `aborted`. */\n signal?: AbortSignal\n}\n\n/**\n * Open the run, append every chunk from `stream`, and finish with the right\n * terminal status. Resolves with the final {@link RunRecord} and never rejects:\n * a thrown stream error is surfaced as a `RUN_ERROR` event + the record's\n * `error`, which is what tailing clients see.\n *\n * - normal completion → `finish('done')`\n * - a `RUN_ERROR` chunk → append it, then `finish('error', { message, code })`\n * - the stream throws → append a synthesized `RUN_ERROR`, then `finish('error')`\n * - `signal` aborts mid-stream → stop consuming, `finish('aborted')`\n */\nexport async function pipeToRunLog(\n stream: AsyncIterable<StreamChunk>,\n opts: PipeToRunLogOptions,\n): Promise<RunRecord> {\n const { log, runId, threadId, signal } = opts\n await log.open(threadId !== undefined ? { runId, threadId } : { runId })\n if (signal?.aborted) {\n await log.finish(runId, 'aborted')\n return reread(log, runId)\n }\n\n try {\n for await (const chunk of stream) {\n if (signal?.aborted) {\n await log.finish(runId, 'aborted')\n return reread(log, runId)\n }\n await log.append(runId, chunk)\n if (isRunErrorChunk(chunk)) {\n await log.finish(runId, 'error', runErrorFromChunk(chunk))\n return reread(log, runId)\n }\n }\n } catch (error) {\n // Detached run: no caller to throw to. Record the failure in the log so\n // tailing clients observe it, then return — do NOT rethrow.\n const message = messageOf(error)\n await log.append(runId, syntheticRunError(message))\n await log.finish(runId, 'error', { message })\n return reread(log, runId)\n }\n\n await log.finish(runId, 'done')\n return reread(log, runId)\n}\n\n/** Re-read the now-terminal record; the run was just driven, so it must exist. */\nasync function reread(log: RunEventLog, runId: string): Promise<RunRecord> {\n const latest = await log.get(runId)\n if (!latest) throw new Error(`run: record for \"${runId}\" vanished mid-run`)\n return latest\n}\n\nexport interface RunControllerStartInput {\n runId: string\n threadId?: string\n stream: AsyncIterable<StreamChunk>\n /** Abort consumption mid-stream; the run finishes as `aborted`. */\n signal?: AbortSignal\n}\n\nexport interface RunHandle {\n runId: string\n /** Resolves with the final record once the run reaches a terminal status. */\n done: Promise<RunRecord>\n}\n\n/**\n * Thin orchestration helper over a {@link RunEventLog}: fire-and-track a run via\n * {@link pipeToRunLog}, tail it from a cursor, and `drain()` all in-flight runs\n * (e.g. inside a `ctx.waitUntil`). Holds no run state of its own beyond the set\n * of currently in-flight `done` promises.\n */\nexport class RunController {\n private readonly inFlight = new Set<Promise<RunRecord>>()\n\n constructor(private readonly log: RunEventLog) {}\n\n /**\n * Kick off `pipeToRunLog` without awaiting it and return the `runId`\n * immediately plus a `done` promise the orchestrator may await or detach.\n */\n start(input: RunControllerStartInput): RunHandle {\n const done = pipeToRunLog(input.stream, {\n log: this.log,\n runId: input.runId,\n ...(input.threadId !== undefined ? { threadId: input.threadId } : {}),\n ...(input.signal !== undefined ? { signal: input.signal } : {}),\n })\n this.inFlight.add(done)\n void done.finally(() => this.inFlight.delete(done))\n return { runId: input.runId, done }\n }\n\n /** Resumable client tail — replay from `fromSeq`, then live-tail to terminal. */\n attach(\n runId: string,\n opts?: { fromSeq?: number; signal?: AbortSignal },\n ): AsyncIterable<RunEvent> {\n return this.log.read(runId, opts)\n }\n\n /** Current run record, or null if the run is unknown. */\n status(runId: string): Promise<RunRecord | null> {\n return this.log.get(runId)\n }\n\n /** Await every currently in-flight run's `done` promise. */\n async drain(): Promise<void> {\n await Promise.all([...this.inFlight])\n }\n}\n"],"names":[],"mappings":";AAiBA,SAAS,gBACP,OAC2D;AAC3D,SAAO,MAAM,SAAS,UAAU;AAClC;AAGA,SAAS,kBACP,OACU;AACV,SAAO,MAAM,SAAS,SAClB,EAAE,SAAS,MAAM,SAAS,MAAM,MAAM,KAAA,IACtC,EAAE,SAAS,MAAM,QAAA;AACvB;AAGA,SAAS,UAAU,OAAwB;AACzC,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAGA,SAAS,kBAAkB,SAA8B;AACvD,QAAM,QAAwD;AAAA,IAC5D,MAAM,UAAU;AAAA,IAChB;AAAA,EAAA;AAEF,SAAO;AACT;AAqBA,eAAsB,aACpB,QACA,MACoB;AACpB,QAAM,EAAE,KAAK,OAAO,UAAU,WAAW;AACzC,QAAM,IAAI,KAAK,aAAa,SAAY,EAAE,OAAO,SAAA,IAAa,EAAE,OAAO;AACvE,MAAI,QAAQ,SAAS;AACnB,UAAM,IAAI,OAAO,OAAO,SAAS;AACjC,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AAEA,MAAI;AACF,qBAAiB,SAAS,QAAQ;AAChC,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,OAAO,OAAO,SAAS;AACjC,eAAO,OAAO,KAAK,KAAK;AAAA,MAC1B;AACA,YAAM,IAAI,OAAO,OAAO,KAAK;AAC7B,UAAI,gBAAgB,KAAK,GAAG;AAC1B,cAAM,IAAI,OAAO,OAAO,SAAS,kBAAkB,KAAK,CAAC;AACzD,eAAO,OAAO,KAAK,KAAK;AAAA,MAC1B;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AAGd,UAAM,UAAU,UAAU,KAAK;AAC/B,UAAM,IAAI,OAAO,OAAO,kBAAkB,OAAO,CAAC;AAClD,UAAM,IAAI,OAAO,OAAO,SAAS,EAAE,SAAS;AAC5C,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AAEA,QAAM,IAAI,OAAO,OAAO,MAAM;AAC9B,SAAO,OAAO,KAAK,KAAK;AAC1B;AAGA,eAAe,OAAO,KAAkB,OAAmC;AACzE,QAAM,SAAS,MAAM,IAAI,IAAI,KAAK;AAClC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oBAAoB,KAAK,oBAAoB;AAC1E,SAAO;AACT;AAsBO,MAAM,cAAc;AAAA,EAGzB,YAA6B,KAAkB;AAAlB,SAAA,MAAA;AAAA,EAAmB;AAAA,EAAnB;AAAA,EAFZ,+BAAe,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhC,MAAM,OAA2C;AAC/C,UAAM,OAAO,aAAa,MAAM,QAAQ;AAAA,MACtC,KAAK,KAAK;AAAA,MACV,OAAO,MAAM;AAAA,MACb,GAAI,MAAM,aAAa,SAAY,EAAE,UAAU,MAAM,SAAA,IAAa,CAAA;AAAA,MAClE,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,WAAW,CAAA;AAAA,IAAC,CAC9D;AACD,SAAK,SAAS,IAAI,IAAI;AACtB,SAAK,KAAK,QAAQ,MAAM,KAAK,SAAS,OAAO,IAAI,CAAC;AAClD,WAAO,EAAE,OAAO,MAAM,OAAO,KAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,OACE,OACA,MACyB;AACzB,WAAO,KAAK,IAAI,KAAK,OAAO,IAAI;AAAA,EAClC;AAAA;AAAA,EAGA,OAAO,OAA0C;AAC/C,WAAO,KAAK,IAAI,IAAI,KAAK;AAAA,EAC3B;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,UAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC;AAAA,EACtC;AACF;"}
1
+ {"version":3,"file":"run.js","names":[],"sources":["../../src/run.ts"],"sourcesContent":["/**\n * The \"run driver\" for the inverted/serverless sandbox model: pump a `chat()`\n * stream into core's two durable seams — a {@link RunStore} for the run's\n * lifecycle record and a {@link StreamDurability} for its event log — so a\n * trigger can return immediately while a durable orchestrator drives the run\n * and clients tail from an opaque offset.\n *\n * The key inversion vs. a classic request/response handler: there is no caller\n * holding the stream open, so nothing to throw an error *back to*. The event log\n * is the only channel — every chunk (including a terminal\n * {@link EventType.RUN_ERROR}) is appended and assigned a resumable offset, and\n * a thrown stream error is recorded as a synthesized `RUN_ERROR` event plus the\n * record's `error` field. Tailing clients therefore always observe failures;\n * {@link pipeToRunLog} never rejects.\n *\n * \"Never rejects\" is load-bearing rather than aspirational: {@link RunController}\n * consumes the returned promise fire-and-forget, so a rejection would be an\n * unhandled rejection (process-fatal on modern Node, instance-fatal inside a\n * Durable Object) with nobody to report it to. Every store/log call is therefore\n * individually guarded, and because absorbing a failure silently in the one\n * module whose premise is that nobody is listening would make the failure\n * invisible, each guard reports through the optional {@link RunDeps.logger}.\n */\nimport { EventType } from '@tanstack/ai'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n RunError,\n RunRecord,\n RunStore,\n StreamChunk,\n StreamDurability,\n TerminalRunStatus,\n} from '@tanstack/ai'\n\n/** Whether a chunk is the terminal error event the chat engine emits. */\nfunction isRunErrorChunk(\n chunk: StreamChunk,\n): chunk is StreamChunk & { message: string; code?: string } {\n return chunk.type === EventType.RUN_ERROR\n}\n\n/**\n * Narrow a thrown value (or a `RUN_ERROR` chunk's payload) to the record's\n * structured error, keeping the provider's `code` when it supplies one: a bare\n * message is prose that changes between model versions, while `code` is what a\n * consumer branches on to retry, escalate, or show specific UI.\n */\nfunction toRunError(error: unknown): RunError {\n const payload = toRunErrorPayload(error)\n return {\n message: payload.message,\n ...(payload.code === undefined ? {} : { code: payload.code }),\n }\n}\n\n/**\n * Fold a secondary failure into the primary error, mirroring `combineFailures`\n * in `packages/ai/src/stream-to-response.ts`: the primary cause stays first and\n * keeps its `code`, and the phase that produced the secondary failure is named.\n * The secondary must never *replace* the primary: the provider's error is what\n * an operator needs, and a failure while recording it is the lesser fact.\n */\nfunction withSecondaryFailure(\n primary: RunError,\n secondary: unknown,\n phase: string,\n): RunError {\n return {\n ...primary,\n message: `${primary.message}; ${phase}: ${toRunError(secondary).message}`,\n }\n}\n\n/** Build the synthetic RUN_ERROR chunk appended when the stream throws. */\nfunction syntheticRunError(error: RunError): StreamChunk {\n const chunk: { type: EventType.RUN_ERROR; message: string; code?: string } = {\n type: EventType.RUN_ERROR,\n message: error.message,\n ...(error.code === undefined ? {} : { code: error.code }),\n }\n return chunk\n}\n\n/**\n * The two durable seams a run driver needs: lifecycle record + event log.\n *\n * Generic in the log's offset type, and DEFAULTED to `string` so every existing\n * call site keeps compiling unchanged. The parameter is not decoration: a\n * backend that brands its cursors — `@tanstack/ai-durable-stream`'s\n * `durableStream` returns `StreamDurability<DurableStreamOffset>` — is NOT\n * assignable to `StreamDurability<string>`, because `read` takes an offset and\n * is therefore contravariant in it. Hardcoding the default here made the\n * production multi-host backend unusable without a cast (see\n * `tests/offset-generics.test-d.ts`).\n */\nexport interface RunDeps<TOffset extends string = string> {\n /** Run lifecycle record (status, thread, timings). */\n runs: RunStore\n /**\n * Per-run delivery-durable event log the run's chunks are appended to.\n *\n * A FACTORY, not an instance, and that is load-bearing rather than stylistic.\n * A `StreamDurability` is bound to one run — `memoryStream(request)` resolves\n * its `runId` from the request, and a backend adapter's offsets embed a cursor\n * into one log. Holding a single instance made two failures reachable:\n *\n * - **Silent mis-binding at concurrency 1.** `start({ runId })` accepted an\n * arbitrary id while the instance was bound to another, writing the record\n * under one id and the events under another with no error raised. Resolving\n * the log FROM the `runId` makes that unrepresentable.\n * - **Cross-talk at concurrency > 1.** Parallel runs interleaved their chunks\n * into one log, and whichever finished first called `close()` and\n * terminalized every other run's stream.\n *\n * Called exactly once per run, at the start of {@link pipeToRunLog}. An\n * implementation MUST return the same instance for the same `runId` within a\n * process if it wants `snapshot()` to see its own appends.\n */\n durability: (runId: string) => StreamDurability<TOffset>\n /**\n * Optional sink for failures this driver absorbs rather than rejecting with.\n * A detached run has no caller to receive an error, so without a logger a\n * failing store or event log is invisible to an operator. Same\n * `logger?.errors(...)` contract core uses in `stream-to-response.ts`.\n */\n logger?: InternalLogger\n}\n\nexport interface PipeToRunLogOptions<\n TOffset extends string = string,\n> extends RunDeps<TOffset> {\n runId: string\n threadId: string\n /** Abort consumption mid-stream; the run finishes as `aborted`. */\n signal?: AbortSignal\n}\n\n/** Everything {@link finish} needs, including the fields it rebuilds a record from. */\ninterface FinishContext {\n runs: RunStore\n /**\n * `close()` only — see {@link finish}. Narrowed to that one member rather than\n * threading `TOffset` through here, because `close` is the sole method this\n * context touches and it is offset-free, so a `Pick` accepts a log at ANY\n * offset instantiation without making `finish` generic for nothing.\n */\n durability: Pick<StreamDurability, 'close'>\n runId: string\n threadId: string\n startedAt: number\n logger?: InternalLogger\n}\n\n/**\n * Report through a consumer-supplied logger without letting it break the\n * caller. Every logger call in this module sits inside a `catch` body, so an\n * throwing sink would escape that body and defeat the totality the guards\n * exist to provide. Swallowing here is deliberate: there is no second channel\n * left to report a reporting failure on.\n */\nfunction safeLog(\n logger: InternalLogger | undefined,\n message: string,\n context: Record<string, unknown>,\n): void {\n try {\n logger?.errors(message, context)\n } catch {\n // Intentionally empty: see above.\n }\n}\n\n/**\n * Record the terminal status, terminalize the event log, and answer with the\n * run's final record.\n *\n * TOTAL BY CONSTRUCTION: every step is individually guarded, so this never\n * throws and never rejects. Two consequences the guards buy:\n *\n * - `durability.close()` runs on EVERY exit path, including a failed `update`.\n * Skipping it would wedge the record at `running` *and* park every live\n * tailer forever, because a durability `read` only ends once the log closes.\n * - The re-read of the record is best effort. An eventually-consistent or\n * read-replica store may answer `null` for a run that was just driven, which\n * must not turn a successful run into a rejection; the locally rebuilt record\n * is returned instead. It is also preferred outright when `update` failed,\n * since the store then still holds the stale `running` row.\n *\n * THE TERMINAL WRITE IS NOT GUARANTEED TO LAND, and this function deliberately\n * does not check whether it did. Under `sandboxRunDriver` the `runs` handed in is\n * `fenceRunStore`d (`src/claim.ts`), which SUPPRESSES a terminal write — resolving\n * without writing — when the driver has lost its claim, because a host that no\n * longer owns the run must not declare it over while the successor is streaming\n * it. From here that is indistinguishable from a successful write, on purpose:\n * the epoch belongs to the claim module, not to this generic driver, and the\n * re-read below then answers with the successor's live record, which is the\n * truthful thing to resolve with. A driver wired without a claim (a plain\n * `pipeToRunLog` call) is unfenced and always writes.\n */\nasync function finish(\n ctx: FinishContext,\n status: TerminalRunStatus,\n error?: RunError,\n): Promise<RunRecord> {\n // NOTE: every logger call below goes through `safeLog`. The logger is\n // consumer-supplied and is handed arbitrary thrown values, so a sink that\n // cannot serialize one (a circular payload, say) would otherwise throw from\n // inside a `catch` body and escape, skipping `durability.close()` and leaving\n // the run wedged at `'running'` with live tailers parked. The reporting\n // channel must never be able to break the guarantee it exists to report on.\n const { runs, durability, runId, logger } = ctx\n const finishedAt = Date.now()\n const patch = {\n status,\n finishedAt,\n ...(error === undefined ? {} : { error }),\n }\n const local: RunRecord = {\n runId,\n threadId: ctx.threadId,\n startedAt: ctx.startedAt,\n ...patch,\n }\n\n let recorded = true\n try {\n await runs.update(runId, patch)\n } catch (updateError) {\n recorded = false\n safeLog(logger, 'run: recording the terminal run record failed', {\n runId,\n status,\n error: updateError,\n })\n }\n\n try {\n await durability.close()\n } catch (closeError) {\n safeLog(logger, 'run: closing the run event log failed', {\n runId,\n status,\n error: closeError,\n })\n }\n\n if (!recorded) return local\n\n try {\n const latest = await runs.get(runId)\n if (latest !== null) return latest\n safeLog(logger, 'run: record vanished before the terminal re-read', {\n runId,\n status,\n })\n } catch (getError) {\n safeLog(logger, 'run: re-reading the terminal run record failed', {\n runId,\n status,\n error: getError,\n })\n }\n return local\n}\n\n/**\n * Open the run, append every chunk from `stream`, and finish with the right\n * terminal status. Resolves with the final {@link RunRecord} and never rejects:\n * a thrown stream error is surfaced as a `RUN_ERROR` event plus the record's\n * `error`, which is what tailing clients see. A store or event-log failure\n * along the way is logged through {@link RunDeps.logger} and still terminalizes\n * the run rather than escaping to a caller that does not exist.\n *\n * - normal completion → `completed`\n * - a `RUN_ERROR` chunk → append it, then `failed`\n * - the stream throws → append a synthesized `RUN_ERROR`, then `failed`\n * - `signal` aborts at ANY point before the stream ends → `aborted`, whether the\n * producer keeps yielding, ends its stream, or is never asked for another\n * chunk. An abort outranks a clean exit: the run did not complete.\n */\nexport async function pipeToRunLog<TOffset extends string = string>(\n stream: AsyncIterable<StreamChunk>,\n opts: PipeToRunLogOptions<TOffset>,\n): Promise<RunRecord> {\n const { runs, runId, threadId, signal, logger } = opts\n // Resolved ONCE, from the runId being driven. Everything below — including\n // `finish`'s `close()` — uses this one instance, so a factory that mints a\n // fresh log per call cannot split one run across two logs, and the log can\n // never belong to a run other than the one whose record is being written.\n const durability = opts.durability(runId)\n const ctx: FinishContext = {\n runs,\n durability,\n runId,\n threadId,\n startedAt: Date.now(),\n ...(logger === undefined ? {} : { logger }),\n }\n\n try {\n // Inside the `try` so a store failure at creation is handled like any\n // other: recorded as a failed run with a terminalized log, not rejected.\n await runs.createOrResume({ runId, threadId, startedAt: ctx.startedAt })\n if (signal?.aborted) return finish(ctx, 'aborted')\n\n for await (const chunk of stream) {\n if (signal?.aborted) return finish(ctx, 'aborted')\n await durability.append([chunk])\n if (isRunErrorChunk(chunk)) {\n return finish(\n ctx,\n 'failed',\n toRunError({ message: chunk.message, code: chunk.code }),\n )\n }\n }\n } catch (streamError) {\n // Detached run: no caller to throw to. Record the failure in the log so\n // tailing clients observe it, then return — do NOT rethrow.\n let recorded = toRunError(streamError)\n // Deliberately not \"the stream failed\": `runs.createOrResume` above is\n // inside this `try`, so an operator reading a wedged run must not be told\n // the provider stream broke when the store never let the run start.\n safeLog(logger, 'run: the run failed before completing', {\n runId,\n error: streamError,\n })\n try {\n await durability.append([syntheticRunError(recorded)])\n } catch (appendError) {\n // The recovery append is itself a failure path. It must not destroy the\n // cause it was recording, so the provider's error stays primary and this\n // secondary failure is merged in and logged separately. When the cause IS\n // a lost claim, this append is refused too and the `finish` below writes\n // nothing either — a fenced store suppresses the terminal record for the\n // same reason the fenced log refused the chunk (see `finish`).\n const phase = 'appending the synthesized RUN_ERROR failed'\n safeLog(logger, `run: ${phase}`, { runId, error: appendError })\n recorded = withSecondaryFailure(recorded, appendError, phase)\n }\n return finish(ctx, 'failed', recorded)\n }\n\n // RE-CHECKED after the loop, and this is the common shape rather than the\n // exotic one. The in-loop check only fires if the producer yields at least\n // once MORE after the abort; two ways past it are routine:\n //\n // - The producer is signal-aware and reacts by ENDING its stream. `chat()`\n // does exactly this, so the loop exits NORMALLY.\n // - The abort lands BETWEEN two chunks, while the loop is suspended on a\n // producer that then finishes on its own.\n //\n // Falling through to `'completed'` in either case is a false transcript, not\n // a cosmetic mislabel: it was measured on the reaper's TTL-expiry path, where\n // a run the reaper had force-expired — and whose sandbox it had already\n // destroyed — was recorded as having completed successfully. Any caller whose\n // producer ends its stream on abort reaches the same gap, a takeover that\n // loses its claim mid-drive included, which is why the check belongs here and\n // not in one caller.\n //\n // A producer that THREW on the abort is deliberately untouched: it returned\n // from inside the `catch` above as `'failed'`, because a thrown value is a\n // reported failure a tailing client must be shown, and this driver's log is\n // that client's only channel.\n if (signal?.aborted) return finish(ctx, 'aborted')\n return finish(ctx, 'completed')\n}\n\nexport interface RunControllerStartInput {\n runId: string\n threadId: string\n stream: AsyncIterable<StreamChunk>\n /** Abort consumption mid-stream; the run finishes as `aborted`. */\n signal?: AbortSignal\n}\n\nexport interface RunHandle {\n runId: string\n /** Resolves with the final record once the run reaches a terminal status. */\n done: Promise<RunRecord>\n}\n\n/**\n * Thin orchestration helper over {@link RunDeps}: fire-and-track a run via\n * {@link pipeToRunLog}, tail one run by id, and `drain()` all in-flight runs\n * (e.g. inside a `ctx.waitUntil`). Holds no run state of its own beyond the set\n * of currently in-flight `done` promises.\n *\n * Safe for concurrent runs. {@link RunDeps.durability} is a per-run factory, so\n * each run appends to its own log and no run's `close()` terminalizes another's.\n * The identity trap this class used to document — `start({ runId })` writing the\n * lifecycle record under one id and the events under another, silently and at\n * concurrency 1 — is unrepresentable now that the log is resolved FROM the\n * `runId`. Every method is keyed by run accordingly: `attach(runId, …)` and\n * `status(runId)` no longer disagree about whether the surface is per-run.\n */\nexport class RunController<TOffset extends string = string> {\n private readonly inFlight = new Set<Promise<RunRecord>>()\n\n constructor(private readonly deps: RunDeps<TOffset>) {}\n\n /**\n * Kick off `pipeToRunLog` without awaiting it and return the `runId`\n * immediately plus a `done` promise the orchestrator may await or detach.\n */\n start(input: RunControllerStartInput): RunHandle {\n const done = pipeToRunLog(input.stream, {\n ...this.deps,\n runId: input.runId,\n threadId: input.threadId,\n ...(input.signal !== undefined ? { signal: input.signal } : {}),\n })\n this.inFlight.add(done)\n // Two-argument `then`, deliberately NOT `.finally`: `.finally` returns a new\n // promise that adopts any rejection, and discarding that promise would make\n // the rejection unhandled (fatal on modern Node defaults, and it kills the\n // instance inside a Durable Object). Handling both outcomes here means the\n // derived promise always settles fulfilled, so nothing is left unhandled\n // even if `pipeToRunLog`'s \"never rejects\" contract is ever broken.\n const forget = (): void => void this.inFlight.delete(done)\n void done.then(forget, forget)\n return { runId: input.runId, done }\n }\n\n /**\n * Resumable client tail for ONE run — replay from `fromOffset`, then\n * live-tail. Takes `runId` because the log it reads is per-run; the old\n * `attach(fromOffset)` signature advertised a multi-run surface the type could\n * not deliver.\n */\n attach(\n runId: string,\n fromOffset: TOffset,\n signal?: AbortSignal,\n ): AsyncIterable<{ offset: TOffset; chunk: StreamChunk }> {\n return this.deps.durability(runId).read(fromOffset, signal)\n }\n\n /** Current run record, or null when the run is unknown. */\n status(runId: string): Promise<RunRecord | null> {\n return this.deps.runs.get(runId)\n }\n\n /**\n * Await every currently in-flight run's `done` promise.\n *\n * Uses `allSettled` rather than `all` because this is typically awaited\n * inside a `ctx.waitUntil`: `all` would reject on the first failure, abandon\n * the wait on every other run, and surface that rejection to the platform.\n * Draining is about keeping the isolate alive until the runs settle; each\n * run's own outcome is already recorded in its record and log.\n */\n async drain(): Promise<void> {\n await Promise.allSettled([...this.inFlight])\n }\n}\n"],"mappings":"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"}