@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.
- package/dist/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
package/src/index.ts
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
|
-
// Capability tokens + accessors
|
|
1
|
+
// Capability tokens + accessors (sandbox-owned only).
|
|
2
|
+
// LockStore / withLocks / defineLock: import from @tanstack/ai/locks.
|
|
2
3
|
export {
|
|
3
4
|
SandboxCapability,
|
|
4
|
-
SandboxStoreCapability,
|
|
5
|
-
LocksCapability,
|
|
6
5
|
SandboxPolicyCapability,
|
|
7
6
|
ToolBridgeProvisionerCapability,
|
|
8
7
|
getSandbox,
|
|
9
8
|
provideSandbox,
|
|
10
|
-
getSandboxStore,
|
|
11
|
-
provideSandboxStore,
|
|
12
|
-
getLocks,
|
|
13
|
-
provideLocks,
|
|
14
9
|
getSandboxPolicy,
|
|
15
10
|
provideSandboxPolicy,
|
|
16
11
|
getToolBridgeProvisioner,
|
|
17
12
|
provideToolBridgeProvisioner,
|
|
18
13
|
} from './capabilities'
|
|
19
14
|
|
|
15
|
+
// Durable instance map (resume-or-create across processes).
|
|
16
|
+
// Pass a store to `withSandbox(sandbox, { instances })`; the capability is the
|
|
17
|
+
// ambient alternative for platform-level wiring.
|
|
18
|
+
export {
|
|
19
|
+
SandboxInstanceStoreCapability,
|
|
20
|
+
getSandboxInstanceStore,
|
|
21
|
+
provideSandboxInstanceStore,
|
|
22
|
+
InMemorySandboxInstanceStore,
|
|
23
|
+
defineSandboxInstanceStore,
|
|
24
|
+
} from './instance-store'
|
|
25
|
+
export type {
|
|
26
|
+
SandboxInstanceStore,
|
|
27
|
+
SandboxInstanceRecord,
|
|
28
|
+
} from './instance-store'
|
|
29
|
+
|
|
20
30
|
// Workspace projection capability (provided by withSandbox, consumed by harness adapters)
|
|
21
31
|
export {
|
|
22
32
|
ProjectionCapability,
|
|
@@ -27,6 +37,13 @@ export type { WorkspaceProjection } from './projection'
|
|
|
27
37
|
|
|
28
38
|
// Middleware
|
|
29
39
|
export { withSandbox } from './middleware'
|
|
40
|
+
export type { SandboxMiddlewareOptions } from './middleware'
|
|
41
|
+
|
|
42
|
+
// Harness tool history: `withSandbox` records the tool calls a harness ran INSIDE the
|
|
43
|
+
// sandbox into the transcript, so a finished run restores its tool cards. This is how
|
|
44
|
+
// an app recognises them — e.g. in its own `MessageStore.saveThread`, to cap or drop
|
|
45
|
+
// what it does not want to store.
|
|
46
|
+
export { isSandboxToolCall } from './tool-history'
|
|
30
47
|
|
|
31
48
|
// Sandbox definition + lifecycle
|
|
32
49
|
export { defineSandbox } from './sandbox'
|
|
@@ -100,10 +117,6 @@ export type {
|
|
|
100
117
|
SandboxDestroyInput,
|
|
101
118
|
} from './contracts'
|
|
102
119
|
|
|
103
|
-
// Stores (interfaces + in-memory defaults)
|
|
104
|
-
export { InMemorySandboxStore, InMemoryLockStore } from './store'
|
|
105
|
-
export type { SandboxStore, LockStore, SandboxRecord } from './store'
|
|
106
|
-
|
|
107
120
|
// Bootstrap engine (exported for provider/adapter authors + tests)
|
|
108
121
|
export {
|
|
109
122
|
bootstrapWorkspace,
|
|
@@ -125,8 +138,191 @@ export {
|
|
|
125
138
|
export { createExecBackedGit } from './git-exec'
|
|
126
139
|
|
|
127
140
|
// Harness runner: spawn an agent CLI in a sandbox + stream NDJSON stdout
|
|
128
|
-
export {
|
|
129
|
-
|
|
141
|
+
export {
|
|
142
|
+
spawnNdjson,
|
|
143
|
+
toLines,
|
|
144
|
+
startJournaledAgent,
|
|
145
|
+
readJournalNdjson,
|
|
146
|
+
} from './runner'
|
|
147
|
+
export type { SpawnNdjsonOptions, JournalOptions } from './runner'
|
|
148
|
+
|
|
149
|
+
// The agent output journal: the durability boundary for a sandboxed run.
|
|
150
|
+
//
|
|
151
|
+
// journalListCommand/journalMtimeListCommand/parseJournalMtimeListing back the
|
|
152
|
+
// journal-directory sweep (`journal-sweep.ts`); journalExitProbeCommand/
|
|
153
|
+
// parseJournalExit/parseExitSentinel back the reaper's out-of-band exit probe
|
|
154
|
+
// (`reap.ts`) and the streaming reader, which must agree on what "the run
|
|
155
|
+
// ended" means; EXIT_SENTINEL_NONCE_KEY/exitSentinelLine are what make that
|
|
156
|
+
// sentinel unforgeable by the agent's own stdout and are needed by anything
|
|
157
|
+
// that seeds a journal by hand (a fake host, a test); the
|
|
158
|
+
// mechanism that lets a sweep learn a detached run finished WITHOUT driving
|
|
159
|
+
// it; decodeJournalRunId recovers the runId a listed filename encodes, fail
|
|
160
|
+
// closed. `encodeRunId` is exported for adapters that derive their OWN
|
|
161
|
+
// in-sandbox paths from a caller-supplied `runId` (prompt files, MCP bridge
|
|
162
|
+
// configs) and must not hand-roll a second, divergent encoder.
|
|
163
|
+
// `normalizeJournalDir` is intentionally NOT exported: it is a path
|
|
164
|
+
// formatting detail of this module's own commands, not something a caller
|
|
165
|
+
// composes with.
|
|
166
|
+
export {
|
|
167
|
+
DEFAULT_JOURNAL_DIR,
|
|
168
|
+
encodeRunId,
|
|
169
|
+
EXIT_SENTINEL_KEY,
|
|
170
|
+
EXIT_SENTINEL_NONCE_KEY,
|
|
171
|
+
exitSentinelLine,
|
|
172
|
+
parseExitSentinel,
|
|
173
|
+
journalPaths,
|
|
174
|
+
journaledCommand,
|
|
175
|
+
journalFollowCommand,
|
|
176
|
+
journalReadCommand,
|
|
177
|
+
journalExistsCommand,
|
|
178
|
+
journalListCommand,
|
|
179
|
+
journalMtimeListCommand,
|
|
180
|
+
parseJournalMtimeListing,
|
|
181
|
+
journalExitProbeCommand,
|
|
182
|
+
parseJournalExit,
|
|
183
|
+
decodeJournalRunId,
|
|
184
|
+
} from './journal'
|
|
185
|
+
export type {
|
|
186
|
+
JournalPaths,
|
|
187
|
+
JournalMtimeListing,
|
|
188
|
+
JournalDirEntry,
|
|
189
|
+
DecodedJournalRunId,
|
|
190
|
+
} from './journal'
|
|
191
|
+
|
|
192
|
+
// Journal-directory sweep: bound the journals a detached run's sentinel never
|
|
193
|
+
// got OBSERVED for (see `journal-sweep.ts`'s module doc for why almost every
|
|
194
|
+
// branch keeps rather than deletes — deleting a live run's journal makes it
|
|
195
|
+
// unresumable, with no undo).
|
|
196
|
+
export {
|
|
197
|
+
pruneJournals,
|
|
198
|
+
DEFAULT_ORPHAN_TTL_MS,
|
|
199
|
+
DEFAULT_MAX_DELETES,
|
|
200
|
+
} from './journal-sweep'
|
|
201
|
+
export type {
|
|
202
|
+
PruneJournalsOptions,
|
|
203
|
+
PruneJournalsResult,
|
|
204
|
+
KeptJournal,
|
|
205
|
+
KeptJournalReason,
|
|
206
|
+
PruneJournalsFailure,
|
|
207
|
+
} from './journal-sweep'
|
|
208
|
+
|
|
209
|
+
// Detached-run reaper: sweep a `RunStore`'s reclaimable runs, driving a run to
|
|
210
|
+
// terminal ONLY once the out-of-band journal probe (`probeRunExit`) already
|
|
211
|
+
// knows the agent exited, or once its TTL has expired — never to find out
|
|
212
|
+
// whether it finished (see `reap.ts`'s module doc for why that design was
|
|
213
|
+
// rejected).
|
|
214
|
+
export {
|
|
215
|
+
reapDetachedRuns,
|
|
216
|
+
probeRunExit,
|
|
217
|
+
DEFAULT_RUN_BUDGET_MS,
|
|
218
|
+
DEFAULT_MAX_RUNS,
|
|
219
|
+
DEFAULT_EXIT_PROBE_BYTES,
|
|
220
|
+
} from './reap'
|
|
221
|
+
export type {
|
|
222
|
+
RunExitProbe,
|
|
223
|
+
ReapRunOutcome,
|
|
224
|
+
ReapRunEntry,
|
|
225
|
+
ReapResult,
|
|
226
|
+
ReapOptions,
|
|
227
|
+
} from './reap'
|
|
228
|
+
|
|
229
|
+
// Sandbox reclaim: tear down the sandbox behind a terminal run.
|
|
230
|
+
// `sandboxReclaimer` adapts `reclaimSandbox` to `ReapOptions.reclaim`.
|
|
231
|
+
export {
|
|
232
|
+
reclaimSandbox,
|
|
233
|
+
sandboxReclaimer,
|
|
234
|
+
SandboxReclaimFailedError,
|
|
235
|
+
} from './reclaim'
|
|
236
|
+
export type { ReclaimOutcome, ReclaimSandboxOptions } from './reclaim'
|
|
237
|
+
export {
|
|
238
|
+
DEFAULT_JOURNAL_POLL_MS,
|
|
239
|
+
journalReadStrategy,
|
|
240
|
+
readJournal,
|
|
241
|
+
} from './journal-reader'
|
|
242
|
+
export type { ReadJournalOptions } from './journal-reader'
|
|
243
|
+
export { decodeBase64Stream, toJournalLines } from './journal-bytes'
|
|
244
|
+
export type { JournalLine } from './journal-bytes'
|
|
245
|
+
export {
|
|
246
|
+
createRunScopedIdGen,
|
|
247
|
+
chunkFingerprint,
|
|
248
|
+
chunkFingerprintIgnoringThreadId,
|
|
249
|
+
chunkThreadId,
|
|
250
|
+
} from './chunk-identity'
|
|
251
|
+
export {
|
|
252
|
+
alignToStoredLog,
|
|
253
|
+
isBridgeCustomChunk,
|
|
254
|
+
JournalReplayDivergedError,
|
|
255
|
+
JournalReplayThreadIdMismatchError,
|
|
256
|
+
DEFAULT_MAX_OUT_OF_BAND_SKIP,
|
|
257
|
+
} from './align'
|
|
258
|
+
export type { AlignToStoredLogOptions } from './align'
|
|
259
|
+
|
|
260
|
+
// Attach preflight: the gate that makes a hopeless attach fail instead of
|
|
261
|
+
// tailing an empty journal forever. `JournalAttachUnavailableError` and its
|
|
262
|
+
// `reason` are the branchable surface (404 / 410 / 504 at an attach route), and
|
|
263
|
+
// the bounded-wait default is exported because it bounds an attach REQUEST.
|
|
264
|
+
export {
|
|
265
|
+
awaitAttachableJournal,
|
|
266
|
+
JournalAttachUnavailableError,
|
|
267
|
+
DEFAULT_ATTACH_JOURNAL_WAIT_MS,
|
|
268
|
+
DEFAULT_ATTACH_PROBE_INTERVAL_MS,
|
|
269
|
+
} from './attach-preflight'
|
|
270
|
+
export type {
|
|
271
|
+
AttachUnavailableReason,
|
|
272
|
+
AwaitAttachableJournalOptions,
|
|
273
|
+
} from './attach-preflight'
|
|
274
|
+
|
|
275
|
+
// Durability seam: the `withSandbox(sandbox, { runs, durability })` option
|
|
276
|
+
// shape, the capability harness adapters read back via `getSandboxDurability`,
|
|
277
|
+
// and the two helpers that turn a resolved durability into the pieces a
|
|
278
|
+
// harness adapter actually drives with — a `journalOptionsFor` journal option
|
|
279
|
+
// and an attach-only `alignedIfAttaching` alignment transform.
|
|
280
|
+
//
|
|
281
|
+
// `resolveSandboxDurability` is deliberately NOT exported: it is
|
|
282
|
+
// `withSandbox`'s own path from raw options to the capability payload (see
|
|
283
|
+
// `middleware.ts`), and a harness adapter only ever needs the ALREADY-RESOLVED
|
|
284
|
+
// value read back off the capability bus, never to re-run that resolution
|
|
285
|
+
// itself.
|
|
286
|
+
export {
|
|
287
|
+
SandboxDurabilityCapability,
|
|
288
|
+
getSandboxDurability,
|
|
289
|
+
provideSandboxDurability,
|
|
290
|
+
DurableAttachNotSupportedError,
|
|
291
|
+
DurableRunIdRequiredError,
|
|
292
|
+
DurableThreadIdRequiredError,
|
|
293
|
+
resolveDurableRunId,
|
|
294
|
+
resolveDurableThreadId,
|
|
295
|
+
journalOptionsFor,
|
|
296
|
+
alignedIfAttaching,
|
|
297
|
+
} from './durability'
|
|
298
|
+
export type {
|
|
299
|
+
SandboxDurabilityOptions,
|
|
300
|
+
SandboxDurabilityLog,
|
|
301
|
+
SandboxRunDurability,
|
|
302
|
+
} from './durability'
|
|
303
|
+
|
|
304
|
+
// Run driver: fills in core's injected takeover seams (`claim`/`pipe`) with
|
|
305
|
+
// this package's single-writer claim (`claim.ts`) and run log (`run.ts`), so
|
|
306
|
+
// an application wires `request`/`runs`/`locks`/`durability`/`drive` instead of
|
|
307
|
+
// hand-rolling the claim/fence dance itself.
|
|
308
|
+
//
|
|
309
|
+
// `claim.ts`'s own primitives — `withRunClaim`, `fenceDurability`,
|
|
310
|
+
// `awaitLogQuiescence`, `runDriverLockKey`, and their `RunClaim` /
|
|
311
|
+
// `WithRunClaimOptions` types — are deliberately NOT exported. They are
|
|
312
|
+
// exactly the "easy to get wrong" seam `sandboxRunDriver` exists to make
|
|
313
|
+
// impossible (see `driver.ts`'s module doc, points 1-3), and publishing them
|
|
314
|
+
// would invite the same hand-rolled fencing bugs as a supported path. The two
|
|
315
|
+
// error classes below ARE exported despite that: both can surface through
|
|
316
|
+
// `sandboxRunDriver` itself, so a caller needs `instanceof` to branch on them,
|
|
317
|
+
// and `DEFAULT_FENCE_QUIET_MS` is exported because it is the documented
|
|
318
|
+
// default for `sandboxRunDriver`'s own `fenceQuietMs` option.
|
|
319
|
+
export { sandboxRunDriver, RunDriverPipeOutsideClaimError } from './driver'
|
|
320
|
+
export type { SandboxRunDriverOptions } from './driver'
|
|
321
|
+
export {
|
|
322
|
+
RunClaimNotAcquiredError,
|
|
323
|
+
RunClaimLostError,
|
|
324
|
+
DEFAULT_FENCE_QUIET_MS,
|
|
325
|
+
} from './claim'
|
|
130
326
|
|
|
131
327
|
// MCP tool-proxy bridge (shared by harness adapters): transport-agnostic core
|
|
132
328
|
// + the node:http host transport + a fetch-friendly JSON-RPC dispatcher.
|
|
@@ -173,23 +369,12 @@ export type {
|
|
|
173
369
|
ToolExecRequest,
|
|
174
370
|
} from './remote-tools'
|
|
175
371
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
export { InMemoryRunEventLog, isTerminalRunStatus } from './run-log'
|
|
179
|
-
export type {
|
|
180
|
-
RunEventLog,
|
|
181
|
-
RunRecord,
|
|
182
|
-
RunEvent,
|
|
183
|
-
RunStatus,
|
|
184
|
-
TerminalRunStatus,
|
|
185
|
-
RunError,
|
|
186
|
-
RunEventLogReadOptions,
|
|
187
|
-
} from './run-log'
|
|
188
|
-
|
|
189
|
-
// Run driver — pump a chat() stream into the event-log so a trigger returns
|
|
372
|
+
// Run driver — pumps a chat() stream into core's `StreamDurability` and
|
|
373
|
+
// records run status/lifecycle in core's `RunStore`, so a trigger returns
|
|
190
374
|
// immediately while a durable orchestrator drives the run and clients tail it.
|
|
191
375
|
export { pipeToRunLog, RunController } from './run'
|
|
192
376
|
export type {
|
|
377
|
+
RunDeps,
|
|
193
378
|
PipeToRunLogOptions,
|
|
194
379
|
RunControllerStartInput,
|
|
195
380
|
RunHandle,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable sandbox **instance** map — which provider sandbox (and snapshot) to
|
|
3
|
+
* resume for a compound key. Owned by `@tanstack/ai-sandbox` (not chat
|
|
4
|
+
* persistence): domain is runtime placement for `ensure`, not conversation state.
|
|
5
|
+
*
|
|
6
|
+
* Pass to `withSandbox(sandbox, { instances })`, which uses it in `ensure`
|
|
7
|
+
* (in-memory fallback when absent). {@link SandboxInstanceStoreCapability} is
|
|
8
|
+
* the ambient alternative for platform-level wiring.
|
|
9
|
+
*/
|
|
10
|
+
import { createCapability } from '@tanstack/ai'
|
|
11
|
+
|
|
12
|
+
/** One persisted sandbox instance, keyed by the compound sandbox instance key. */
|
|
13
|
+
export interface SandboxInstanceRecord {
|
|
14
|
+
/** Compound key (see `computeSandboxKey`). */
|
|
15
|
+
key: string
|
|
16
|
+
/** Provider name that owns `providerSandboxId`. */
|
|
17
|
+
provider: string
|
|
18
|
+
/** Provider-assigned sandbox id used to resume. */
|
|
19
|
+
providerSandboxId: string
|
|
20
|
+
/** Most recent snapshot id, when the provider supports snapshots. */
|
|
21
|
+
latestSnapshotId?: string
|
|
22
|
+
threadId: string
|
|
23
|
+
latestRunId?: string
|
|
24
|
+
/**
|
|
25
|
+
* Epoch ms of last write (for keepAlive / GC by the host app).
|
|
26
|
+
*/
|
|
27
|
+
updatedAt: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maps a compound key to the provider sandbox that should be resumed.
|
|
32
|
+
*
|
|
33
|
+
* Implement against your own database (BYO). Prove the contract with
|
|
34
|
+
* `runSandboxInstanceStoreConformance` from `@tanstack/ai-sandbox/testkit`.
|
|
35
|
+
*/
|
|
36
|
+
export interface SandboxInstanceStore {
|
|
37
|
+
/**
|
|
38
|
+
* Return the record for `key`, or `null` if none exists.
|
|
39
|
+
*
|
|
40
|
+
* INVARIANT: missing keys return `null` (never throw).
|
|
41
|
+
*/
|
|
42
|
+
get: (key: string) => Promise<SandboxInstanceRecord | null>
|
|
43
|
+
/**
|
|
44
|
+
* Insert or fully replace the record for `record.key`.
|
|
45
|
+
*
|
|
46
|
+
* INVARIANT (full replace): omitted optional fields (`latestSnapshotId`,
|
|
47
|
+
* `latestRunId`) MUST clear any previously stored values. Do not merge with
|
|
48
|
+
* the prior row — a create-without-snapshot path must not leave a stale
|
|
49
|
+
* snapshot id.
|
|
50
|
+
*/
|
|
51
|
+
upsert: (record: SandboxInstanceRecord) => Promise<void>
|
|
52
|
+
/**
|
|
53
|
+
* Remove the record for `key`.
|
|
54
|
+
*
|
|
55
|
+
* INVARIANT: deleting a missing key is a **no-op** (must not throw).
|
|
56
|
+
*/
|
|
57
|
+
delete: (key: string) => Promise<void>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type a {@link SandboxInstanceStore} implementation inline: pass the object and
|
|
62
|
+
* get autocomplete + contract checking, with no separate
|
|
63
|
+
* `: SandboxInstanceStore` annotation. Hand the result to
|
|
64
|
+
* `withSandbox(sandbox, { instances })`. Matches `defineLock` /
|
|
65
|
+
* `defineMessageStore` style helpers elsewhere in the monorepo.
|
|
66
|
+
*/
|
|
67
|
+
export function defineSandboxInstanceStore(
|
|
68
|
+
store: SandboxInstanceStore,
|
|
69
|
+
): SandboxInstanceStore {
|
|
70
|
+
return store
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Capability for the instance map — the ambient alternative to
|
|
75
|
+
* `withSandbox(sandbox, { instances })`. Provide it from any middleware with
|
|
76
|
+
* {@link provideSandboxInstanceStore}; `withSandbox` reads it when no explicit
|
|
77
|
+
* option was passed.
|
|
78
|
+
*/
|
|
79
|
+
export const SandboxInstanceStoreCapability =
|
|
80
|
+
createCapability<SandboxInstanceStore>()('sandbox-instance-store')
|
|
81
|
+
|
|
82
|
+
/** Destructured accessors: `getSandboxInstanceStore` / `provideSandboxInstanceStore`. */
|
|
83
|
+
export const [getSandboxInstanceStore, provideSandboxInstanceStore] =
|
|
84
|
+
SandboxInstanceStoreCapability
|
|
85
|
+
|
|
86
|
+
/** In-memory {@link SandboxInstanceStore}. Resume works only within one process. */
|
|
87
|
+
export class InMemorySandboxInstanceStore implements SandboxInstanceStore {
|
|
88
|
+
private readonly map = new Map<string, SandboxInstanceRecord>()
|
|
89
|
+
|
|
90
|
+
get(key: string): Promise<SandboxInstanceRecord | null> {
|
|
91
|
+
return Promise.resolve(this.map.get(key) ?? null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
upsert(record: SandboxInstanceRecord): Promise<void> {
|
|
95
|
+
this.map.set(record.key, record)
|
|
96
|
+
return Promise.resolve()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
delete(key: string): Promise<void> {
|
|
100
|
+
this.map.delete(key)
|
|
101
|
+
return Promise.resolve()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Wiring note: hand the store straight to the consumer —
|
|
107
|
+
* `withSandbox(sandbox, { instances: store })`. That cannot be mis-ordered,
|
|
108
|
+
* unlike a separate provider middleware composed after `withSandbox` (which
|
|
109
|
+
* silently degrades to the in-memory fallback).
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* middleware: [
|
|
113
|
+
* withLocks(locks), // from @tanstack/ai/locks — multi-replica
|
|
114
|
+
* withSandbox(sandbox, { instances: instanceStore }),
|
|
115
|
+
* ]
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* For ambient/platform wiring (a hosting layer injecting infra without touching
|
|
119
|
+
* the call site), any middleware may still
|
|
120
|
+
* `provideSandboxInstanceStore(ctx, store)` on the capability bus; an explicit
|
|
121
|
+
* option takes precedence over it.
|
|
122
|
+
*/
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-exact framing for journal reads.
|
|
3
|
+
*
|
|
4
|
+
* Both read paths end in {@link toJournalLines}, which counts absolute file
|
|
5
|
+
* offsets over BYTES, because a position is only useful if `tail -c +N` can
|
|
6
|
+
* resume from it. The two paths differ only in how they get bytes:
|
|
7
|
+
*
|
|
8
|
+
* - The **bounded** read is base64-framed (see `journal.ts` rule 2) and arrives
|
|
9
|
+
* as one complete `ExecResult.stdout` string, so {@link decodeBase64Stream}
|
|
10
|
+
* recovers the file's exact bytes regardless of how the provider decoded its
|
|
11
|
+
* stdout.
|
|
12
|
+
* - The **follow** read cannot be base64-framed — the encoder's stdio buffer
|
|
13
|
+
* would swallow the stream — so it arrives as provider-decoded text chunks
|
|
14
|
+
* and {@link encodeUtf8Stream} turns them back into bytes.
|
|
15
|
+
*
|
|
16
|
+
* `atob`, not `Buffer`: this module runs on the host, and the host can itself be
|
|
17
|
+
* a Cloudflare Worker (`ai-sandbox-cloudflare` drives its sandbox over Workers
|
|
18
|
+
* RPC from Worker code), where `Buffer` is not a global unless the `nodejs_compat`
|
|
19
|
+
* flag is on. `atob` is a Web/DOM API available in every host runtime this
|
|
20
|
+
* package targets, so it is the portable choice here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NEWLINE = 0x0a
|
|
24
|
+
|
|
25
|
+
/** Decode one complete base64 quantum group to bytes. */
|
|
26
|
+
function decodeQuantumGroup(group: string): Uint8Array {
|
|
27
|
+
const binary = atob(group)
|
|
28
|
+
const out = new Uint8Array(binary.length)
|
|
29
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
30
|
+
out[index] = binary.charCodeAt(index)
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decode a streaming base64 frame into raw bytes.
|
|
37
|
+
*
|
|
38
|
+
* Whitespace is stripped because `base64(1)` wraps at 76 columns by default and
|
|
39
|
+
* busybox's build does not accept `-w 0`, so the wrapping cannot be turned off
|
|
40
|
+
* portably. Only complete 4-character quanta are decoded; a remainder is held
|
|
41
|
+
* for the next chunk. Padding (`=`) appears only in the final quantum, so an
|
|
42
|
+
* intermediate group always decodes to exactly 3 bytes.
|
|
43
|
+
*/
|
|
44
|
+
export async function* decodeBase64Stream(
|
|
45
|
+
chunks: AsyncIterable<string>,
|
|
46
|
+
): AsyncIterable<Uint8Array> {
|
|
47
|
+
let pending = ''
|
|
48
|
+
for await (const chunk of chunks) {
|
|
49
|
+
pending += chunk.replace(/\s+/g, '')
|
|
50
|
+
const usable = pending.length - (pending.length % 4)
|
|
51
|
+
if (usable === 0) continue
|
|
52
|
+
const group = pending.slice(0, usable)
|
|
53
|
+
pending = pending.slice(usable)
|
|
54
|
+
yield decodeQuantumGroup(group)
|
|
55
|
+
}
|
|
56
|
+
if (pending.length > 0) {
|
|
57
|
+
// Fail loud. A remainder means the frame was cut off mid-quantum, i.e. the
|
|
58
|
+
// reader died partway through. Rounding it away would silently drop journal
|
|
59
|
+
// bytes and desync every position derived from this stream.
|
|
60
|
+
throw new Error(
|
|
61
|
+
`journal: base64 frame ended mid-quantum with ${pending.length} character(s) pending`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Re-encode provider-decoded text chunks as UTF-8 bytes.
|
|
68
|
+
*
|
|
69
|
+
* This is the follow path's replacement for {@link decodeBase64Stream}: the
|
|
70
|
+
* follow command emits the journal's raw bytes, the provider hands them over as
|
|
71
|
+
* `AsyncIterable<string>`, and `TextEncoder` round-trips that text back to the
|
|
72
|
+
* bytes it was decoded from. Chunk boundaries do not need to fall on character
|
|
73
|
+
* boundaries here — {@link toJournalLines} buffers bytes until a newline, so a
|
|
74
|
+
* multi-byte character split across two chunks is reassembled there, exactly as
|
|
75
|
+
* it is on the base64 path.
|
|
76
|
+
*
|
|
77
|
+
* Empty chunks are dropped rather than forwarded: a zero-length `Uint8Array`
|
|
78
|
+
* carries no bytes and would only make the downstream loop spin.
|
|
79
|
+
*/
|
|
80
|
+
export async function* encodeUtf8Stream(
|
|
81
|
+
chunks: AsyncIterable<string>,
|
|
82
|
+
): AsyncIterable<Uint8Array> {
|
|
83
|
+
const encoder = new TextEncoder()
|
|
84
|
+
for await (const chunk of chunks) {
|
|
85
|
+
if (chunk.length === 0) continue
|
|
86
|
+
yield encoder.encode(chunk)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One complete journal line plus the absolute byte position just past its newline. */
|
|
91
|
+
export interface JournalLine {
|
|
92
|
+
/** The line's text, newline excluded. */
|
|
93
|
+
line: string
|
|
94
|
+
/**
|
|
95
|
+
* Absolute byte offset immediately AFTER this line's newline — i.e. the count
|
|
96
|
+
* of journal bytes fully consumed once this line has been handled, and
|
|
97
|
+
* therefore the exact value to resume a `tail -c +N` from.
|
|
98
|
+
*/
|
|
99
|
+
endPosition: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {
|
|
103
|
+
const out = new Uint8Array(left.length + right.length)
|
|
104
|
+
out.set(left, 0)
|
|
105
|
+
out.set(right, left.length)
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Split a byte stream into newline-terminated lines, tracking absolute
|
|
111
|
+
* positions from `startPosition`.
|
|
112
|
+
*
|
|
113
|
+
* Deliberately unlike `toLines` in `runner.ts`, which yields a trailing
|
|
114
|
+
* unterminated line: here a trailing partial line is a line the agent is still
|
|
115
|
+
* writing. Yielding it would hand a truncated JSON string downstream AND
|
|
116
|
+
* advance the position past bytes the next read must re-see.
|
|
117
|
+
*/
|
|
118
|
+
export async function* toJournalLines(
|
|
119
|
+
byteChunks: AsyncIterable<Uint8Array>,
|
|
120
|
+
startPosition: number,
|
|
121
|
+
): AsyncIterable<JournalLine> {
|
|
122
|
+
const decoder = new TextDecoder()
|
|
123
|
+
let buffer: Uint8Array = new Uint8Array(0)
|
|
124
|
+
let position = startPosition
|
|
125
|
+
for await (const bytes of byteChunks) {
|
|
126
|
+
buffer = concatBytes(buffer, bytes)
|
|
127
|
+
let newline = buffer.indexOf(NEWLINE)
|
|
128
|
+
while (newline !== -1) {
|
|
129
|
+
const lineBytes = buffer.subarray(0, newline)
|
|
130
|
+
position += newline + 1
|
|
131
|
+
yield { line: decoder.decode(lineBytes), endPosition: position }
|
|
132
|
+
buffer = buffer.slice(newline + 1)
|
|
133
|
+
newline = buffer.indexOf(NEWLINE)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|