@tanstack/ai-sandbox 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,13 @@ Pick a **provider** package for where the sandbox runs:
74
74
  | `@tanstack/ai-sandbox-daytona` | Daytona cloud sandboxes, snapshots |
75
75
  | `@tanstack/ai-sandbox-upstash-box` | Upstash Box cloud sandboxes, snapshots |
76
76
  | `@tanstack/ai-sandbox-sprites` | Sprites stateful sandboxes |
77
+ | `@tanstack/ai-sandbox-blaxel` | Blaxel cloud sandboxes and previews |
78
+
79
+ Install the provider you select separately. For Blaxel:
80
+
81
+ ```bash
82
+ npm install @tanstack/ai-sandbox-blaxel
83
+ ```
77
84
 
78
85
  **Harness adapters** are separate packages. The default path is **Grok Build** (`@tanstack/ai-grok-build`); others include `@tanstack/ai-claude-code`, `@tanstack/ai-codex`, and `@tanstack/ai-opencode`. All require `withSandbox(...)` middleware — `chat()` fails fast without it.
79
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-sandbox",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -56,13 +56,13 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
- "@tanstack/ai-skills": "^0.1.2"
59
+ "@tanstack/ai-skills": "^0.1.3"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@ngrok/ngrok": "^1.0.0",
63
63
  "vitest": "^4.1.10",
64
- "@tanstack/ai": "^0.53.0",
65
- "@tanstack/ai-persistence": "^0.5.6"
64
+ "@tanstack/ai": "^0.54.0",
65
+ "@tanstack/ai-persistence": "^0.5.7"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "@ngrok/ngrok": {
@@ -79,8 +79,8 @@
79
79
  "@ngrok/ngrok": "^1.7.0",
80
80
  "@vitest/coverage-v8": "4.1.10",
81
81
  "vitest": "^4.1.10",
82
- "@tanstack/ai": "0.53.0",
83
- "@tanstack/ai-persistence": "0.5.6"
82
+ "@tanstack/ai": "0.54.0",
83
+ "@tanstack/ai-persistence": "0.5.7"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "vite build",
@@ -48,9 +48,10 @@ agent CLI **inside** the sandbox and streams its events back.
48
48
  ## Setup — Claude Code in a Docker sandbox
49
49
 
50
50
  ```typescript
51
- import { chat } from '@tanstack/ai'
51
+ import { chat, toServerSentEventsResponse } from '@tanstack/ai'
52
52
  import { claudeCodeText } from '@tanstack/ai-claude-code'
53
53
  import {
54
+ createSecrets,
54
55
  defineSandbox,
55
56
  defineWorkspace,
56
57
  withSandbox,
@@ -65,17 +66,25 @@ const sandbox = defineSandbox({
65
66
  packageManager: 'pnpm',
66
67
  setup: ['corepack enable', 'pnpm install'],
67
68
  scripts: { test: 'pnpm test' },
68
- secrets: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
69
+ secrets: createSecrets({
70
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
71
+ }),
69
72
  }),
70
73
  lifecycle: { reuse: 'thread', snapshot: 'after-setup', keepAlive: '30m' },
71
74
  })
72
75
 
73
- const stream = chat({
74
- threadId,
75
- adapter: claudeCodeText('sonnet'),
76
- messages,
77
- middleware: [withSandbox(sandbox)],
78
- })
76
+ export async function POST(request: Request) {
77
+ const { threadId, messages } = await request.json()
78
+
79
+ const stream = chat({
80
+ threadId,
81
+ adapter: claudeCodeText('sonnet'),
82
+ messages,
83
+ middleware: [withSandbox(sandbox)],
84
+ })
85
+
86
+ return toServerSentEventsResponse(stream)
87
+ }
79
88
  ```
80
89
 
81
90
  ## Type-safe secrets
@@ -165,6 +174,8 @@ serial and parallel groups over a **persistent shell** whose cwd/env carry over
165
174
  between serial steps:
166
175
 
167
176
  ```typescript
177
+ import { githubRepo, defineWorkspace } from '@tanstack/ai-sandbox'
178
+
168
179
  defineWorkspace({
169
180
  source: githubRepo({ repo: 'owner/app' }),
170
181
  setup: ({ serial, parallel }) => {
@@ -183,10 +194,17 @@ When the provider supports snapshots, bootstrap takes one automatically after
183
194
  Override or add a TTL:
184
195
 
185
196
  ```typescript
186
- lifecycle: {
187
- snapshot: 'after-setup', // default when provider.capabilities().snapshots
188
- snapshotMaxAge: '24h', // re-create when the snapshot is older than this
189
- }
197
+ import { defineSandbox } from '@tanstack/ai-sandbox'
198
+ import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
199
+
200
+ const sandbox = defineSandbox({
201
+ id: 'repo-agent',
202
+ provider: dockerSandbox({ image: 'node:22' }),
203
+ lifecycle: {
204
+ snapshot: 'after-setup', // default when provider.capabilities().snapshots
205
+ snapshotMaxAge: '24h', // re-create when the snapshot is older than this
206
+ },
207
+ })
190
208
  ```
191
209
 
192
210
  Providers without snapshot support skip the step silently.
@@ -199,8 +217,15 @@ middleware in this order, with the same persistence value in both places:
199
217
 
200
218
  ```typescript
201
219
  import { withPersistence } from '@tanstack/ai-persistence'
202
- import { memorySandboxSnapshots, withSandbox } from '@tanstack/ai-sandbox'
220
+ import {
221
+ InMemorySandboxInstanceStore,
222
+ memorySandboxSnapshots,
223
+ withSandbox,
224
+ } from '@tanstack/ai-sandbox'
225
+ // Your `defineSandbox(...)` result.
226
+ import { sandbox } from './sandbox'
203
227
 
228
+ const instances = new InMemorySandboxInstanceStore()
204
229
  const snapshots = await memorySandboxSnapshots({ sandbox, instances })
205
230
 
206
231
  const middleware = [
@@ -321,20 +346,30 @@ distributed lock: either `withLocks` from `@tanstack/ai/locks` (ordered
321
346
  **before** `withSandbox`) or the `locks` option.
322
347
 
323
348
  ```typescript
324
- import { chat } from '@tanstack/ai'
349
+ import { chat, toServerSentEventsResponse } from '@tanstack/ai'
325
350
  import { InMemoryLockStore, withLocks } from '@tanstack/ai/locks'
351
+ import { claudeCodeText } from '@tanstack/ai-claude-code'
326
352
  import { withSandbox } from '@tanstack/ai-sandbox'
353
+ // Your `defineSandbox(...)` result.
354
+ import { sandbox } from './sandbox'
327
355
  // Production: your BYO store — docs/sandbox/durability.md
328
356
  import { instanceStore } from './sandbox-instance-store'
329
357
 
330
- chat({
331
- adapter,
332
- messages,
333
- middleware: [
334
- withLocks(new InMemoryLockStore()), // multi-replica: distributed lock
335
- withSandbox(sandbox, { instances: instanceStore }),
336
- ],
337
- })
358
+ export async function POST(request: Request) {
359
+ const { threadId, messages } = await request.json()
360
+
361
+ const stream = chat({
362
+ threadId,
363
+ adapter: claudeCodeText('sonnet'),
364
+ messages,
365
+ middleware: [
366
+ withLocks(new InMemoryLockStore()), // multi-replica: distributed lock
367
+ withSandbox(sandbox, { instances: instanceStore }),
368
+ ],
369
+ })
370
+
371
+ return toServerSentEventsResponse(stream)
372
+ }
338
373
  ```
339
374
 
340
375
  The store option takes precedence over an ambient `SandboxInstanceStoreCapability`
@@ -360,8 +395,11 @@ middleware via the `sandbox` group (run-scoped):
360
395
  import { defineSandbox, withSandbox } from '@tanstack/ai-sandbox'
361
396
  // `defineChatMiddleware` is core's, not this package's — `@tanstack/ai-sandbox`
362
397
  // consumes it too (see its own `src/middleware.ts`).
363
- import { defineChatMiddleware } from '@tanstack/ai'
398
+ import { chat, defineChatMiddleware } from '@tanstack/ai'
399
+ import { claudeCodeText } from '@tanstack/ai-claude-code'
364
400
  import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
401
+ import { db } from './db'
402
+ import { metrics } from './metrics'
365
403
 
366
404
  // Sandbox-scoped hooks (all optional):
367
405
  const sandbox = defineSandbox({
@@ -392,6 +430,13 @@ const auditMiddleware = defineChatMiddleware({
392
430
 
393
431
  // No extra middleware needed — sandbox.file CUSTOM events are emitted
394
432
  // automatically. Read them from the stream:
433
+ const stream = chat({
434
+ threadId: 'thread-1',
435
+ adapter: claudeCodeText('sonnet'),
436
+ messages: [{ role: 'user', content: 'Add a README.' }],
437
+ middleware: [auditMiddleware, withSandbox(sandbox)],
438
+ })
439
+
395
440
  for await (const chunk of stream) {
396
441
  if (chunk.type === 'CUSTOM' && chunk.name === 'sandbox.file') {
397
442
  const value = chunk.value
@@ -412,7 +457,10 @@ outside a `chat()` run:
412
457
 
413
458
  ```typescript
414
459
  import { watchWorkspace } from '@tanstack/ai-sandbox'
460
+ // Your `defineSandbox(...)` result.
461
+ import { sandbox } from './sandbox'
415
462
 
463
+ const handle = await sandbox.ensure({ threadId: 'thread-1', runId: 'run-1' })
416
464
  const watcher = await watchWorkspace(handle, {
417
465
  onEvent: (e) => console.log(e.type, e.path),
418
466
  ignore: ['.git', 'node_modules'], // default
@@ -424,8 +472,24 @@ Enable the `sandbox` debug category to log watcher start/stop, event dispatch,
424
472
  and lifecycle transitions:
425
473
 
426
474
  ```typescript
427
- chat({ threadId, adapter, messages, debug: { sandbox: true } })
428
- // or debug: true to enable all categories
475
+ import { chat, toServerSentEventsResponse } from '@tanstack/ai'
476
+ import { claudeCodeText } from '@tanstack/ai-claude-code'
477
+ import { withSandbox } from '@tanstack/ai-sandbox'
478
+ import { sandbox } from './sandbox'
479
+
480
+ export async function POST(request: Request) {
481
+ const { threadId, messages } = await request.json()
482
+
483
+ const stream = chat({
484
+ threadId,
485
+ adapter: claudeCodeText('sonnet'),
486
+ messages,
487
+ middleware: [withSandbox(sandbox)],
488
+ debug: { sandbox: true }, // or debug: true to enable all categories
489
+ })
490
+
491
+ return toServerSentEventsResponse(stream)
492
+ }
429
493
  ```
430
494
 
431
495
  ## Edge / serverless execution
@@ -551,12 +615,17 @@ file from byte 0 at any point, including after the original host has died.
551
615
 
552
616
  ```typescript
553
617
  import { spawnNdjson } from '@tanstack/ai-sandbox'
554
-
555
- for await (const event of spawnNdjson(sandbox, agentCommand, {
556
- cwd,
557
- journal: { runId }, // durability is opt-in: pass `journal` to route through it
558
- })) {
559
- // parsed NDJSON objects, translated by the harness adapter as usual
618
+ import type { SandboxHandle } from '@tanstack/ai-sandbox'
619
+
620
+ export async function runAgent(handle: SandboxHandle, runId: string) {
621
+ const agentCommand = 'claude -p --output-format stream-json'
622
+ for await (const event of spawnNdjson(handle, agentCommand, {
623
+ cwd: '/workspace',
624
+ journal: { runId }, // durability is opt-in: pass `journal` to route through it
625
+ })) {
626
+ // parsed NDJSON objects, translated by the harness adapter as usual
627
+ console.log(event)
628
+ }
560
629
  }
561
630
  ```
562
631
 
@@ -1006,6 +1075,17 @@ import {
1006
1075
  } from '@tanstack/ai-sandbox'
1007
1076
  import type { RunRecord } from '@tanstack/ai'
1008
1077
  import type { ReapResult, RunExitProbe } from '@tanstack/ai-sandbox'
1078
+ // Your distributed LockStore, the same one `withSandbox` gets.
1079
+ import { locks } from './locks'
1080
+ // Your persistence — the SAME RunStore the chat routes use.
1081
+ import { runs } from './persistence'
1082
+ // Your `defineSandbox(...)` result and the `SandboxInstanceStore` you passed to
1083
+ // `withSandbox(sandbox, { instances })`.
1084
+ import { instances, sandbox } from './sandbox'
1085
+ // The per-run log factory, resolving the SAME log the producing route wrote.
1086
+ import { durabilityFor } from './durability'
1087
+ // The same `drive` the attach route passes to `sandboxRunDriver`.
1088
+ import { driveRun } from './drive-run'
1009
1089
 
1010
1090
  async function hasFinished(record: RunRecord): Promise<RunExitProbe> {
1011
1091
  if (record.sandboxKey === undefined) return { state: 'unknown' }