@tanstack/ai-sandbox 0.2.4 → 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
@@ -0,0 +1,83 @@
1
+ import { InternalLogger } from '@tanstack/ai/adapter-internals';
2
+ import { LockStore } from '@tanstack/ai/locks';
3
+ import { RunDriverOptions, RunStore, StreamChunk, StreamDurability } from '@tanstack/ai';
4
+ export interface SandboxRunDriverOptions<TOffset extends string = string> {
5
+ /** The attach request; core reads its run id with `resolveResumeRunId`. */
6
+ request: Request;
7
+ runs: RunStore;
8
+ locks: LockStore;
9
+ /**
10
+ * Per-run event log factory, the same shape `RunDeps.durability` takes — a
11
+ * `StreamDurability` is bound to one run, so the log is resolved FROM the
12
+ * `runId` rather than handed in pre-bound.
13
+ *
14
+ * Generic in the offset type, defaulted to `string` so an existing call site
15
+ * needs no change. Hardcoding the default made a branded-cursor backend
16
+ * unusable here: `durableStream` returns
17
+ * `StreamDurability<DurableStreamOffset>`, which is not assignable to
18
+ * `StreamDurability<string>` because `read` is contravariant in its offset.
19
+ */
20
+ durability: (runId: string) => StreamDurability<TOffset>;
21
+ /** Produce the run's remaining events. Called only once the claim is held. */
22
+ drive: (input: {
23
+ runId: string;
24
+ threadId: string;
25
+ signal: AbortSignal;
26
+ }) => AsyncIterable<StreamChunk>;
27
+ /** Quiescence window; defaults to {@link DEFAULT_FENCE_QUIET_MS}. */
28
+ fenceQuietMs?: number;
29
+ /** Platform keep-alive (e.g. `ctx.waitUntil`) for the background drive. */
30
+ waitUntil?: (promise: Promise<unknown>) => void;
31
+ logger?: InternalLogger;
32
+ }
33
+ /**
34
+ * `pipe` ran without a held claim. Not a recoverable condition: it means the
35
+ * returned options object was taken apart and `pipe` called outside `claim`, so
36
+ * there is no epoch to fence with and no lease guaranteeing exclusivity. Any
37
+ * append made in that state is exactly the duplicate-write bug the claim exists
38
+ * to prevent, so this fails loudly rather than appending unfenced.
39
+ */
40
+ export declare class RunDriverPipeOutsideClaimError extends Error {
41
+ readonly runId: string;
42
+ constructor(runId: string);
43
+ }
44
+ /**
45
+ * Fill in a core `driver` block with this package's claim and run log.
46
+ *
47
+ * `drive` receives an `AbortSignal` — the driver owns the abort, so it hands
48
+ * out a signal rather than a controller — but `chat()` takes an
49
+ * `AbortController`. Mirror one onto the other, exactly as
50
+ * {@link https://tanstack.com/ai/latest/docs/sandbox/takeover | Takeover & Detached Runs}'s
51
+ * `controllerFor` does, so a lost claim actually stops the drive.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * function controllerFor(signal: AbortSignal): AbortController {
56
+ * const controller = new AbortController()
57
+ * const abort = (): void => controller.abort(signal.reason)
58
+ * if (signal.aborted) abort()
59
+ * else signal.addEventListener('abort', abort, { once: true })
60
+ * return controller
61
+ * }
62
+ *
63
+ * export async function GET(request: Request) {
64
+ * return resumeServerSentEventsResponse({
65
+ * adapter: memoryStream(request),
66
+ * driver: sandboxRunDriver({
67
+ * request,
68
+ * runs,
69
+ * locks,
70
+ * durability: (runId) => logFor(runId),
71
+ * drive: ({ runId, threadId, signal }) =>
72
+ * chat({
73
+ * ...config,
74
+ * runId,
75
+ * threadId,
76
+ * abortController: controllerFor(signal),
77
+ * }),
78
+ * }),
79
+ * })
80
+ * }
81
+ * ```
82
+ */
83
+ export declare function sandboxRunDriver<TOffset extends string = string>(input: SandboxRunDriverOptions<TOffset>): RunDriverOptions;
@@ -0,0 +1,138 @@
1
+ import { awaitLogQuiescence, fenceDurability, fenceRunStore, withRunClaim } from "./claim.js";
2
+ import { pipeToRunLog } from "./run.js";
3
+ //#region src/driver.ts
4
+ /**
5
+ * The convenience that turns core's *injected* takeover seams into this
6
+ * package's real ones.
7
+ *
8
+ * `@tanstack/ai`'s `RunDriverOptions` deliberately takes `claim` and `pipe` as
9
+ * functions instead of importing them: {@link withRunClaim} and
10
+ * {@link pipeToRunLog} live here, and core must not depend on this package to
11
+ * serve a plain chat run. {@link sandboxRunDriver} fills both in so an
12
+ * application writes four fields instead of six, and — more importantly — so
13
+ * the *fencing* is wired correctly by construction rather than by every caller
14
+ * remembering to.
15
+ *
16
+ * WHAT IS EASY TO GET WRONG HERE, and therefore what this module exists to
17
+ * make impossible:
18
+ *
19
+ * 1. **Carrying the real epoch into `pipe`.** Core's `pipe` receives only
20
+ * `{ runId, threadId, signal }` — no epoch — because core has no concept of
21
+ * one. But {@link fenceDurability} needs the epoch this driver actually
22
+ * acquired: a hardcoded epoch (say `0`) is not a weaker fence, it is a
23
+ * permanently *tripped* one, since `withRunClaim` bumps `driverEpoch` to at
24
+ * least `1` before `fn` ever runs, so `observed > claim.epoch` holds on the
25
+ * very first append and EVERY takeover fails. The claim is therefore
26
+ * captured in a closure by the `claim` wrapper and read back by `pipe`.
27
+ * 2. **Fencing `close()`.** {@link fenceDurability} wraps only `append` for the
28
+ * reason spelled out in `claim.ts`: `close()` runs on every teardown path,
29
+ * including the teardown caused by losing the claim, and a fenced `close`
30
+ * would wedge the record at `'running'` with every live tailer parked
31
+ * forever. This module must not add a second fence around it.
32
+ * 2b. **Fencing only ONE of the two authoritative seams.** A run's facts live in
33
+ * its log *and* in its record, and `pipeToRunLog` reacts to a refused append
34
+ * by writing a terminal record — so wrapping the log alone just moves the harm
35
+ * from "a dead host poisons the successor's stream" to "a dead host marks the
36
+ * successor's live run failed". {@link fenceRunStore} must be wired here too,
37
+ * over the SAME claim, which is what makes the two fences share one latch.
38
+ * 3. **Skipping quiescence.** The successor's first append must come after the
39
+ * stored log has stopped growing, so a predecessor still writing is observed
40
+ * rather than raced. The gate belongs inside `pipe`, before `pipeToRunLog`
41
+ * takes its first `snapshot`.
42
+ */
43
+ /**
44
+ * `pipe` ran without a held claim. Not a recoverable condition: it means the
45
+ * returned options object was taken apart and `pipe` called outside `claim`, so
46
+ * there is no epoch to fence with and no lease guaranteeing exclusivity. Any
47
+ * append made in that state is exactly the duplicate-write bug the claim exists
48
+ * to prevent, so this fails loudly rather than appending unfenced.
49
+ */
50
+ var RunDriverPipeOutsideClaimError = class extends Error {
51
+ runId;
52
+ constructor(runId) {
53
+ super(`run ${runId}: sandboxRunDriver.pipe was called outside its claim, so the driver epoch is unknown; call it from within the claim callback`);
54
+ this.runId = runId;
55
+ this.name = "RunDriverPipeOutsideClaimError";
56
+ }
57
+ };
58
+ /**
59
+ * Fill in a core `driver` block with this package's claim and run log.
60
+ *
61
+ * `drive` receives an `AbortSignal` — the driver owns the abort, so it hands
62
+ * out a signal rather than a controller — but `chat()` takes an
63
+ * `AbortController`. Mirror one onto the other, exactly as
64
+ * {@link https://tanstack.com/ai/latest/docs/sandbox/takeover | Takeover & Detached Runs}'s
65
+ * `controllerFor` does, so a lost claim actually stops the drive.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * function controllerFor(signal: AbortSignal): AbortController {
70
+ * const controller = new AbortController()
71
+ * const abort = (): void => controller.abort(signal.reason)
72
+ * if (signal.aborted) abort()
73
+ * else signal.addEventListener('abort', abort, { once: true })
74
+ * return controller
75
+ * }
76
+ *
77
+ * export async function GET(request: Request) {
78
+ * return resumeServerSentEventsResponse({
79
+ * adapter: memoryStream(request),
80
+ * driver: sandboxRunDriver({
81
+ * request,
82
+ * runs,
83
+ * locks,
84
+ * durability: (runId) => logFor(runId),
85
+ * drive: ({ runId, threadId, signal }) =>
86
+ * chat({
87
+ * ...config,
88
+ * runId,
89
+ * threadId,
90
+ * abortController: controllerFor(signal),
91
+ * }),
92
+ * }),
93
+ * })
94
+ * }
95
+ * ```
96
+ */
97
+ function sandboxRunDriver(input) {
98
+ const fenceQuietMs = input.fenceQuietMs ?? 5e3;
99
+ let current;
100
+ return {
101
+ request: input.request,
102
+ runs: input.runs,
103
+ locks: input.locks,
104
+ drive: input.drive,
105
+ claim: (claimInput, fn) => withRunClaim({
106
+ ...claimInput,
107
+ fenceQuietMs,
108
+ ...input.logger === void 0 ? {} : { logger: input.logger }
109
+ }, async (claim) => {
110
+ const previous = current;
111
+ current = claim;
112
+ try {
113
+ return await fn(claim);
114
+ } finally {
115
+ current = previous;
116
+ }
117
+ }),
118
+ pipe: async (stream, i) => {
119
+ const claim = current;
120
+ if (claim === void 0) throw new RunDriverPipeOutsideClaimError(i.runId);
121
+ await awaitLogQuiescence(input.durability(i.runId), fenceQuietMs);
122
+ return pipeToRunLog(stream, {
123
+ runs: fenceRunStore(input.runs, claim, { ...input.logger === void 0 ? {} : { logger: input.logger } }),
124
+ durability: (runId) => fenceDurability(input.durability(runId), claim, { runs: input.runs }),
125
+ runId: i.runId,
126
+ threadId: i.threadId,
127
+ signal: i.signal,
128
+ ...input.logger === void 0 ? {} : { logger: input.logger }
129
+ });
130
+ },
131
+ ...input.waitUntil === void 0 ? {} : { waitUntil: input.waitUntil },
132
+ ...input.logger === void 0 ? {} : { logger: input.logger }
133
+ };
134
+ }
135
+ //#endregion
136
+ export { RunDriverPipeOutsideClaimError, sandboxRunDriver };
137
+
138
+ //# sourceMappingURL=driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver.js","names":[],"sources":["../../src/driver.ts"],"sourcesContent":["/**\n * The convenience that turns core's *injected* takeover seams into this\n * package's real ones.\n *\n * `@tanstack/ai`'s `RunDriverOptions` deliberately takes `claim` and `pipe` as\n * functions instead of importing them: {@link withRunClaim} and\n * {@link pipeToRunLog} live here, and core must not depend on this package to\n * serve a plain chat run. {@link sandboxRunDriver} fills both in so an\n * application writes four fields instead of six, and — more importantly — so\n * the *fencing* is wired correctly by construction rather than by every caller\n * remembering to.\n *\n * WHAT IS EASY TO GET WRONG HERE, and therefore what this module exists to\n * make impossible:\n *\n * 1. **Carrying the real epoch into `pipe`.** Core's `pipe` receives only\n * `{ runId, threadId, signal }` — no epoch — because core has no concept of\n * one. But {@link fenceDurability} needs the epoch this driver actually\n * acquired: a hardcoded epoch (say `0`) is not a weaker fence, it is a\n * permanently *tripped* one, since `withRunClaim` bumps `driverEpoch` to at\n * least `1` before `fn` ever runs, so `observed > claim.epoch` holds on the\n * very first append and EVERY takeover fails. The claim is therefore\n * captured in a closure by the `claim` wrapper and read back by `pipe`.\n * 2. **Fencing `close()`.** {@link fenceDurability} wraps only `append` for the\n * reason spelled out in `claim.ts`: `close()` runs on every teardown path,\n * including the teardown caused by losing the claim, and a fenced `close`\n * would wedge the record at `'running'` with every live tailer parked\n * forever. This module must not add a second fence around it.\n * 2b. **Fencing only ONE of the two authoritative seams.** A run's facts live in\n * its log *and* in its record, and `pipeToRunLog` reacts to a refused append\n * by writing a terminal record — so wrapping the log alone just moves the harm\n * from \"a dead host poisons the successor's stream\" to \"a dead host marks the\n * successor's live run failed\". {@link fenceRunStore} must be wired here too,\n * over the SAME claim, which is what makes the two fences share one latch.\n * 3. **Skipping quiescence.** The successor's first append must come after the\n * stored log has stopped growing, so a predecessor still writing is observed\n * rather than raced. The gate belongs inside `pipe`, before `pipeToRunLog`\n * takes its first `snapshot`.\n */\nimport { pipeToRunLog } from './run'\nimport {\n DEFAULT_FENCE_QUIET_MS,\n awaitLogQuiescence,\n fenceDurability,\n fenceRunStore,\n withRunClaim,\n} from './claim'\nimport type { RunClaim } from './claim'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type {\n RunDriverOptions,\n RunStore,\n StreamChunk,\n StreamDurability,\n} from '@tanstack/ai'\n\nexport interface SandboxRunDriverOptions<TOffset extends string = string> {\n /** The attach request; core reads its run id with `resolveResumeRunId`. */\n request: Request\n runs: RunStore\n locks: LockStore\n /**\n * Per-run event log factory, the same shape `RunDeps.durability` takes — a\n * `StreamDurability` is bound to one run, so the log is resolved FROM the\n * `runId` rather than handed in pre-bound.\n *\n * Generic in the offset type, defaulted to `string` so an existing call site\n * needs no change. Hardcoding the default made a branded-cursor backend\n * unusable here: `durableStream` returns\n * `StreamDurability<DurableStreamOffset>`, which is not assignable to\n * `StreamDurability<string>` because `read` is contravariant in its offset.\n */\n durability: (runId: string) => StreamDurability<TOffset>\n /** Produce the run's remaining events. Called only once the claim is held. */\n drive: (input: {\n runId: string\n threadId: string\n signal: AbortSignal\n }) => AsyncIterable<StreamChunk>\n /** Quiescence window; defaults to {@link DEFAULT_FENCE_QUIET_MS}. */\n fenceQuietMs?: number\n /** Platform keep-alive (e.g. `ctx.waitUntil`) for the background drive. */\n waitUntil?: (promise: Promise<unknown>) => void\n logger?: InternalLogger\n}\n\n/**\n * `pipe` ran without a held claim. Not a recoverable condition: it means the\n * returned options object was taken apart and `pipe` called outside `claim`, so\n * there is no epoch to fence with and no lease guaranteeing exclusivity. Any\n * append made in that state is exactly the duplicate-write bug the claim exists\n * to prevent, so this fails loudly rather than appending unfenced.\n */\nexport class RunDriverPipeOutsideClaimError extends Error {\n constructor(readonly runId: string) {\n super(\n `run ${runId}: sandboxRunDriver.pipe was called outside its claim, so the driver epoch is unknown; call it from within the claim callback`,\n )\n this.name = 'RunDriverPipeOutsideClaimError'\n }\n}\n\n/**\n * Fill in a core `driver` block with this package's claim and run log.\n *\n * `drive` receives an `AbortSignal` — the driver owns the abort, so it hands\n * out a signal rather than a controller — but `chat()` takes an\n * `AbortController`. Mirror one onto the other, exactly as\n * {@link https://tanstack.com/ai/latest/docs/sandbox/takeover | Takeover & Detached Runs}'s\n * `controllerFor` does, so a lost claim actually stops the drive.\n *\n * @example\n * ```typescript\n * function controllerFor(signal: AbortSignal): AbortController {\n * const controller = new AbortController()\n * const abort = (): void => controller.abort(signal.reason)\n * if (signal.aborted) abort()\n * else signal.addEventListener('abort', abort, { once: true })\n * return controller\n * }\n *\n * export async function GET(request: Request) {\n * return resumeServerSentEventsResponse({\n * adapter: memoryStream(request),\n * driver: sandboxRunDriver({\n * request,\n * runs,\n * locks,\n * durability: (runId) => logFor(runId),\n * drive: ({ runId, threadId, signal }) =>\n * chat({\n * ...config,\n * runId,\n * threadId,\n * abortController: controllerFor(signal),\n * }),\n * }),\n * })\n * }\n * ```\n */\nexport function sandboxRunDriver<TOffset extends string = string>(\n input: SandboxRunDriverOptions<TOffset>,\n): RunDriverOptions {\n const fenceQuietMs = input.fenceQuietMs ?? DEFAULT_FENCE_QUIET_MS\n // The seam between core's `claim` and core's `pipe`. One options object serves\n // one attach request and therefore one run, so a single slot is enough; it is\n // cleared on the way out so a `pipe` after the claim released cannot reuse a\n // stale epoch.\n let current: RunClaim | undefined\n\n return {\n request: input.request,\n runs: input.runs,\n locks: input.locks,\n drive: input.drive,\n claim: (claimInput, fn) =>\n withRunClaim(\n {\n ...claimInput,\n fenceQuietMs,\n ...(input.logger === undefined ? {} : { logger: input.logger }),\n },\n async (claim) => {\n const previous = current\n current = claim\n try {\n return await fn(claim)\n } finally {\n current = previous\n }\n },\n ),\n pipe: async (stream, i) => {\n const claim = current\n if (claim === undefined) {\n throw new RunDriverPipeOutsideClaimError(i.runId)\n }\n // Before the first append, never after: `pipeToRunLog` snapshots to align\n // and a predecessor still writing must be observed, not raced.\n await awaitLogQuiescence(input.durability(i.runId), fenceQuietMs)\n return pipeToRunLog(stream, {\n // BOTH authoritative seams are fenced at the epoch this driver actually\n // acquired, and they must be: `pipeToRunLog` answers a refused append by\n // recording a terminal record, so fencing only the log leaves a\n // superseded host marking a live run `'failed'` (see `fenceRunStore`).\n // Neither fence covers `close()` — that stays unfenced on purpose.\n runs: fenceRunStore(input.runs, claim, {\n ...(input.logger === undefined ? {} : { logger: input.logger }),\n }),\n durability: (runId) =>\n fenceDurability(input.durability(runId), claim, {\n runs: input.runs,\n }),\n runId: i.runId,\n threadId: i.threadId,\n signal: i.signal,\n ...(input.logger === undefined ? {} : { logger: input.logger }),\n })\n },\n ...(input.waitUntil === undefined ? {} : { waitUntil: input.waitUntil }),\n ...(input.logger === undefined ? {} : { logger: input.logger }),\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FA,IAAa,iCAAb,cAAoD,MAAM;CACnC;CAArB,YAAY,OAAwB;EAClC,MACE,OAAO,MAAM,6HACf;EAHmB,KAAA,QAAA;EAInB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,iBACd,OACkB;CAClB,MAAM,eAAe,MAAM,gBAAA;CAK3B,IAAI;CAEJ,OAAO;EACL,SAAS,MAAM;EACf,MAAM,MAAM;EACZ,OAAO,MAAM;EACb,OAAO,MAAM;EACb,QAAQ,YAAY,OAClB,aACE;GACE,GAAG;GACH;GACA,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;EAC/D,GACA,OAAO,UAAU;GACf,MAAM,WAAW;GACjB,UAAU;GACV,IAAI;IACF,OAAO,MAAM,GAAG,KAAK;GACvB,UAAU;IACR,UAAU;GACZ;EACF,CACF;EACF,MAAM,OAAO,QAAQ,MAAM;GACzB,MAAM,QAAQ;GACd,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,+BAA+B,EAAE,KAAK;GAIlD,MAAM,mBAAmB,MAAM,WAAW,EAAE,KAAK,GAAG,YAAY;GAChE,OAAO,aAAa,QAAQ;IAM1B,MAAM,cAAc,MAAM,MAAM,OAAO,EACrC,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO,EAC/D,CAAC;IACD,aAAa,UACX,gBAAgB,MAAM,WAAW,KAAK,GAAG,OAAO,EAC9C,MAAM,MAAM,KACd,CAAC;IACH,OAAO,EAAE;IACT,UAAU,EAAE;IACZ,QAAQ,EAAE;IACV,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;GAC/D,CAAC;EACH;EACA,GAAI,MAAM,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,MAAM,UAAU;EACtE,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;CAC/D;AACF"}
@@ -0,0 +1,263 @@
1
+ import { JournalOptions } from './runner.js';
2
+ import { InternalLogger } from '@tanstack/ai/adapter-internals';
3
+ import { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai';
4
+ /** `withSandbox(sandbox, { durability })`. */
5
+ export interface SandboxDurabilityOptions<TOffset extends string = string> {
6
+ /**
7
+ * Delivery-durable event log for the run. Same key and shape as the
8
+ * transport's `durability.adapter`, so one adapter instance can be handed to
9
+ * both `withSandbox` and `toServerSentEventsResponse`.
10
+ *
11
+ * Generic in the offset type, defaulted to `string`, for the same reason
12
+ * {@link SandboxRunDriverOptions} and {@link ReapOptions} are:
13
+ * `StreamDurability` is INVARIANT in `TOffset` (`read` takes an offset in),
14
+ * so a backend that brands its cursors — `@tanstack/ai-durable-stream`'s
15
+ * `durableStream`, the multi-host production backend the sandbox docs point
16
+ * at — is not assignable to `StreamDurability<string>`. Without the parameter
17
+ * the resume route could be wired with it and the route that STARTS the run
18
+ * could not.
19
+ */
20
+ adapter: StreamDurability<TOffset>;
21
+ /** Journal directory inside the sandbox. Defaults to `/tmp/tanstack-runs`. */
22
+ journal?: string;
23
+ /**
24
+ * Whether a client disconnect DETACHES (leave the agent running) instead of
25
+ * destroying the sandbox. Defaults to `true` whenever durability is wired,
26
+ * because that is the whole point of wiring it.
27
+ *
28
+ * Set `false` to keep today's destroy-on-disconnect cost profile while still
29
+ * getting resumable DELIVERY (a reload replays the log). An explicit cancel
30
+ * destroys either way.
31
+ */
32
+ detachOnDisconnect?: boolean;
33
+ /**
34
+ * Read an EXISTING run's journal instead of starting a new agent. Set by the
35
+ * attach route's `drive()` callback, never by an application's POST handler.
36
+ *
37
+ * This is where `attach` lives, and deliberately NOT on `chat()`: `chat()` is
38
+ * core and must not gain sandbox vocabulary, and the provider options are
39
+ * per-model type state, not per-request lifecycle.
40
+ */
41
+ attach?: boolean;
42
+ /** Journal poll interval for providers that cannot follow. */
43
+ pollIntervalMs?: number;
44
+ /**
45
+ * How long an ATTACH waits for a live run's journal to appear before failing
46
+ * with a `JournalAttachUnavailableError`. Defaults to
47
+ * `DEFAULT_ATTACH_JOURNAL_WAIT_MS` (10s). Only the wait is configurable: an
48
+ * unknown or terminal runId fails immediately regardless, since no amount of
49
+ * waiting changes either verdict.
50
+ */
51
+ attachWaitMs?: number;
52
+ }
53
+ /**
54
+ * The view of a caller's event log that the capability bus carries.
55
+ *
56
+ * Deliberately NOT the whole `StreamDurability`. `read` is the only member that
57
+ * takes an offset *in*, which is what makes `StreamDurability` invariant in
58
+ * `TOffset` and a branded-cursor backend unassignable to
59
+ * `StreamDurability<string>`. Every other member mentions the offset only in a
60
+ * return position, so this type is a genuine SUPERTYPE of
61
+ * `StreamDurability<TOffset>` for every `TOffset extends string` — which is the
62
+ * one property that lets a single concrete capability instantiation accept a
63
+ * branded backend. `createCapability<T>()` forces exactly one instantiation
64
+ * (the value type is a plain type argument, and TypeScript has no higher-kinded
65
+ * types), so the payload cannot be parameterized the way the *option* above is.
66
+ *
67
+ * Dropping `read` costs nothing, and that is a property of the seam rather than
68
+ * luck: the bus is the JOURNAL/ALIGNMENT seam, and alignment reads the stored
69
+ * prefix through `snapshot()` — never `read()`, which tails an open log forever
70
+ * (see `alignToStoredLog`). Replay *by offset* belongs to the delivery seam,
71
+ * and that seam (`toServerSentEventsResponse`, `sandboxRunDriver`) receives the
72
+ * application's own adapter directly, with its brand intact.
73
+ */
74
+ export type SandboxDurabilityLog = Omit<StreamDurability, 'read'>;
75
+ /**
76
+ * Resolved durability, published on the capability bus by `withSandbox`.
77
+ *
78
+ * Deliberately carries NO detached-run TTL. The only actor that enforces one is
79
+ * `reapDetachedRuns`, which runs from a cron with no chat in flight — so it has
80
+ * no `CapabilityContext` and cannot read this bus at all. A TTL published here
81
+ * could therefore only ever be read by nobody, while the sweep took its own
82
+ * `ReapOptions.detachedRunTtlMs`; the two would silently disagree. The reaper's
83
+ * required option is the single source of truth.
84
+ */
85
+ export interface SandboxRunDurability {
86
+ runs: RunStore;
87
+ adapter: SandboxDurabilityLog;
88
+ journalDir: string;
89
+ attach: boolean;
90
+ detachOnDisconnect: boolean;
91
+ pollIntervalMs?: number;
92
+ attachWaitMs?: number;
93
+ }
94
+ /**
95
+ * Provided by `withSandbox` only when a run is genuinely durable (both stores
96
+ * wired). Harness adapters read it with `getOptional` and treat its absence as
97
+ * "no journaling contract to honour", which is exactly today's behavior.
98
+ */
99
+ export declare const SandboxDurabilityCapability: import('@tanstack/ai').Capability<SandboxRunDurability, "sandbox-durability">;
100
+ /** Destructured accessors, matching `./capabilities`. */
101
+ export declare const getSandboxDurability: import('@tanstack/ai').CapabilityGetter<SandboxRunDurability>, provideSandboxDurability: import('@tanstack/ai').CapabilityProvider<SandboxRunDurability>;
102
+ /**
103
+ * A durable run was started without a caller-supplied `runId`.
104
+ *
105
+ * Thrown rather than defaulted because the failure is otherwise INVISIBLE: an
106
+ * adapter-generated id (`${name}-${Date.now()}-${Math.random()...}`) produces a
107
+ * journal path at `/tmp/tanstack-runs/<id>.ndjson` that no successor host can
108
+ * recompute, so the run streams normally, records normally, and is silently
109
+ * unrecoverable. A loud failure at the start of `chatStream` is strictly better
110
+ * than a run that only reveals itself as non-durable during an incident.
111
+ */
112
+ export declare class DurableRunIdRequiredError extends Error {
113
+ readonly adapter: string;
114
+ constructor(adapter: string);
115
+ }
116
+ /**
117
+ * Resolve the `runId` a harness adapter will journal under.
118
+ *
119
+ * Replaces the bare `options.runId ?? this.generateId()` in every harness
120
+ * adapter. The fallback is preserved for non-durable runs — several `chat()`
121
+ * paths pass `runId` as a conditional spread, so `undefined` is reachable and
122
+ * removing the fallback would break them for no benefit.
123
+ *
124
+ * The `durable` check runs BEFORE `fallback()`, and that ordering is load
125
+ * bearing: a generated id must never be minted for a durable run, not even one
126
+ * that is discarded, because the whole point is that no such id can exist.
127
+ */
128
+ export declare function resolveDurableRunId(runId: string | undefined, options: {
129
+ durable: boolean;
130
+ adapter: string;
131
+ fallback: () => string;
132
+ }): string;
133
+ /**
134
+ * An ATTACHING durable run was driven without the run record's `threadId`.
135
+ *
136
+ * The sibling of {@link DurableRunIdRequiredError}, for the other id an attach
137
+ * cannot mint for itself. `threadId` lands in EVERY chunk a harness adapter
138
+ * emits (see each package's `stream/translate.ts`), so a replay that generates a
139
+ * fresh one produces a stream that differs from the stored log in its very first
140
+ * chunk. `alignToStoredLog` then fails at index 0 with a
141
+ * `JournalReplayThreadIdMismatchError` — mid-stream, after the takeover has
142
+ * already claimed the run. Refusing up front is strictly better, and mirrors
143
+ * what `resolveDurableRunId` does for an id whose absence is equally fatal.
144
+ *
145
+ * Core already does its part: `startRunDriver` reads the record and hands
146
+ * `active.threadId` to `drive({ runId, threadId, signal })`. This error exists
147
+ * for the one gap it cannot close — application `drive` code that forgets to
148
+ * forward it into `chat()`.
149
+ */
150
+ export declare class DurableThreadIdRequiredError extends Error {
151
+ readonly adapter: string;
152
+ constructor(adapter: string);
153
+ }
154
+ /**
155
+ * Resolve the `threadId` a harness adapter will stamp on every chunk.
156
+ *
157
+ * Replaces the bare `options.threadId ?? this.generateId()` in the journaling
158
+ * harness adapters. Only the durable-AND-attaching quadrant throws; the other
159
+ * three keep the generated fallback and are byte-identical to before:
160
+ *
161
+ * | durable | attaching | behavior |
162
+ * | ------- | --------- | --------------------------------------------------- |
163
+ * | no | no | fallback — a plain non-durable run |
164
+ * | no | yes | fallback — not reachable today, and harmless anyway |
165
+ * | yes | no | fallback — the FRESH run that ESTABLISHES the id |
166
+ * | yes | yes | throw {@link DurableThreadIdRequiredError} |
167
+ *
168
+ * The durable-fresh row is the load-bearing one. A fresh durable run legitimately
169
+ * mints its `threadId` (there is no record to reuse one from), so throwing on
170
+ * `durable` alone — the obvious over-simplification — would break every durable
171
+ * run that has ever worked. Only re-entering an existing run has an id it MUST
172
+ * reuse, which is exactly the condition `attach` already expresses.
173
+ *
174
+ * As in `resolveDurableRunId`, the guard runs BEFORE `fallback()`: a generated id
175
+ * must never be minted on this path, not even one that is then discarded.
176
+ */
177
+ export declare function resolveDurableThreadId(threadId: string | undefined, options: {
178
+ durable: boolean;
179
+ attaching: boolean;
180
+ adapter: string;
181
+ fallback: () => string;
182
+ }): string;
183
+ /**
184
+ * An ATTACH was driven into a code path that can never replay a run.
185
+ *
186
+ * The third sibling of {@link DurableRunIdRequiredError} and
187
+ * {@link DurableThreadIdRequiredError}, and the one that is not about a missing
188
+ * id: here every id is present and the path itself is the problem.
189
+ *
190
+ * `sandboxRunDriver`'s `drive()` re-invokes `chat()` with `attach: true`. On a
191
+ * JOURNALING path that is genuinely a replay — `spawnNdjson` tails the journal
192
+ * the previous host wrote, `awaitAttachableJournal` refuses a hopeless attach up
193
+ * front, and `alignedIfAttaching` suppresses the prefix already delivered. A
194
+ * protocol path with none of those three has no journal to tail and nothing to
195
+ * align against, so `attach: true` does not resume anything: it starts the agent
196
+ * over from scratch against the workspace the first attempt already mutated, and
197
+ * appends its entire output to a log that still holds the first attempt's.
198
+ *
199
+ * Deliberately NOT a `JournalAttachUnavailableError`. That error means "a
200
+ * journal that should exist has not appeared yet" — retryable, scoped to a wait
201
+ * (`attachWaitMs`). This condition is categorically different: the path cannot
202
+ * attach AT ALL, so telling a caller to wait would point it at something that is
203
+ * never coming. A 5xx/501-shaped refusal, not a 504.
204
+ *
205
+ * `reason` names the missing capability in the adapter's own vocabulary (which
206
+ * protocol, which spawn path), because the fix is always to change how the run
207
+ * is spawned or routed, never to retry.
208
+ */
209
+ export declare class DurableAttachNotSupportedError extends Error {
210
+ readonly adapter: string;
211
+ readonly reason: string;
212
+ constructor(adapter: string, reason: string);
213
+ }
214
+ /**
215
+ * Resolve `withSandbox`'s two durability options into the capability payload, or
216
+ * `undefined` when the app has not opted in.
217
+ *
218
+ * BOTH `runs` and `durability` are required. A half-configured app gets
219
+ * `undefined` **silently** rather than a warning: it has not asked for
220
+ * durability, so there is nothing to warn about, and the resulting behavior
221
+ * (destroy on disconnect, no journal) is exactly today's.
222
+ */
223
+ export declare function resolveSandboxDurability<TOffset extends string = string>(options: {
224
+ runs?: RunStore;
225
+ durability?: SandboxDurabilityOptions<TOffset>;
226
+ } | undefined): SandboxRunDurability | undefined;
227
+ /**
228
+ * Build the `spawnNdjson` journal option for a run, or `undefined` when the run
229
+ * is not durable — in which case `spawnNdjson` takes its original, unjournaled
230
+ * path (`isJournaled` tests `options.journal !== undefined`, `runner.ts:70-72`)
231
+ * and behavior is byte-identical to a pre-durability run.
232
+ *
233
+ * `JournalOptions.dir` is optional, but this always supplies it: the resolved
234
+ * durability has already defaulted `journalDir`, and a successor host must
235
+ * recompute the same path rather than re-derive the default independently.
236
+ *
237
+ * `runs` and `attachWaitMs` are carried ONLY when attaching, and that is not a
238
+ * micro-optimization: they exist for `awaitAttachableJournal`, which the reader
239
+ * runs on the attach path alone. A fresh run has no journal yet BY DESIGN (its own
240
+ * `journaledCommand` spawn creates it moments later), so handing it a run store
241
+ * would only invite a future change to gate a path where absence proves nothing.
242
+ */
243
+ export declare function journalOptionsFor(durability: SandboxRunDurability | undefined, runId: string): JournalOptions | undefined;
244
+ /**
245
+ * Align a harness stream against the run's stored log — but ONLY on an attach.
246
+ *
247
+ * The `attach` guard is not an optimization, it is a CORRECTNESS requirement.
248
+ * `alignToStoredLog` snapshots the log before the first chunk is pulled and
249
+ * treats everything in that snapshot as "already delivered". On a FRESH run that
250
+ * premise is false: if such a run were aligned against a log that already holds
251
+ * entries — a `runId` collision, a retried request — its own chunks would be
252
+ * matched against those entries and silently SUPPRESSED instead of delivered,
253
+ * which is silent data loss rather than a slow path. Aligning only when
254
+ * re-entering an existing run keeps the transform's premise ("this stream is a
255
+ * replay of what is already stored") actually true.
256
+ *
257
+ * `isBridgeCustomChunk` is passed because the stored log holds the previous
258
+ * host's MERGED output, including live bridged-tool CUSTOM events that a replay
259
+ * cannot reproduce; without it a bridged-tool run could not be taken over at
260
+ * all. Wrap the merge RESULT, never the pre-merge translator, or the comparison
261
+ * is against a stream the log never contained.
262
+ */
263
+ export declare function alignedIfAttaching(chunks: AsyncIterable<StreamChunk>, durability: SandboxRunDurability | undefined, logger?: InternalLogger): AsyncIterable<StreamChunk>;