@tanstack/ai-sandbox 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/esm/checkpoint-store.d.ts +147 -0
- package/dist/esm/checkpoint-store.js +267 -0
- package/dist/esm/checkpoint-store.js.map +1 -0
- package/dist/esm/contracts.d.ts +19 -0
- package/dist/esm/index.d.ts +11 -1
- package/dist/esm/index.js +12 -7
- package/dist/esm/memory-snapshot-types.d.ts +129 -0
- package/dist/esm/memory-snapshots.d.ts +6 -0
- package/dist/esm/memory-snapshots.js +490 -0
- package/dist/esm/memory-snapshots.js.map +1 -0
- package/dist/esm/middleware.d.ts +33 -1
- package/dist/esm/middleware.js +339 -94
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.d.ts +1 -1
- package/dist/esm/sandbox.d.ts +16 -0
- package/dist/esm/sandbox.js +62 -9
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/snapshot-operations.d.ts +65 -0
- package/dist/esm/snapshot-operations.js +317 -0
- package/dist/esm/snapshot-operations.js.map +1 -0
- package/dist/esm/snapshot-tools.d.ts +185 -0
- package/dist/esm/snapshot-tools.js +160 -0
- package/dist/esm/snapshot-tools.js.map +1 -0
- package/dist/esm/snapshots.d.ts +51 -0
- package/dist/esm/snapshots.js +350 -0
- package/dist/esm/snapshots.js.map +1 -0
- package/dist/esm/testkit/checkpoint-conformance.d.ts +2 -0
- package/dist/esm/testkit/checkpoint-conformance.js +453 -0
- package/dist/esm/testkit/checkpoint-conformance.js.map +1 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.d.ts +18 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js +191 -0
- package/dist/esm/testkit/checkpoint-fork-conformance.js.map +1 -0
- package/dist/esm/testkit/conformance.d.ts +4 -0
- package/dist/esm/testkit/conformance.js +3 -1
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/package.json +8 -3
- package/skills/ai-sandbox/SKILL.md +96 -8
- package/src/checkpoint-store.ts +652 -0
- package/src/contracts.ts +12 -0
- package/src/index.ts +56 -0
- package/src/memory-snapshot-types.ts +167 -0
- package/src/memory-snapshots.ts +936 -0
- package/src/middleware.ts +610 -160
- package/src/sandbox.ts +107 -6
- package/src/snapshot-operations.ts +540 -0
- package/src/snapshot-tools.ts +208 -0
- package/src/snapshots.ts +711 -0
- package/src/testkit/checkpoint-conformance.ts +472 -0
- package/src/testkit/checkpoint-fork-conformance.ts +299 -0
- package/src/testkit/conformance.ts +7 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ModelMessage } from '@tanstack/ai'
|
|
3
|
+
import type {
|
|
4
|
+
ForkCapableSandboxCheckpointStore,
|
|
5
|
+
SandboxCheckpoint,
|
|
6
|
+
SandboxCheckpointForkInput,
|
|
7
|
+
SandboxCheckpointWriter,
|
|
8
|
+
SandboxCheckpointWriterLease,
|
|
9
|
+
} from '../checkpoint-store'
|
|
10
|
+
import { InMemorySandboxCheckpointStore } from '../checkpoint-store'
|
|
11
|
+
|
|
12
|
+
/** Combined stores required to exercise an atomic checkpoint fork. */
|
|
13
|
+
export interface SandboxCheckpointForkConformanceInput {
|
|
14
|
+
persistence: {
|
|
15
|
+
stores: {
|
|
16
|
+
messages: {
|
|
17
|
+
loadThread: (threadId: string) => Promise<Array<ModelMessage>>
|
|
18
|
+
saveThread: (
|
|
19
|
+
threadId: string,
|
|
20
|
+
messages: Array<ModelMessage>,
|
|
21
|
+
) => Promise<void>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
checkpoints: ForkCapableSandboxCheckpointStore
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SandboxCheckpointForkConformanceFactory {
|
|
29
|
+
():
|
|
30
|
+
| SandboxCheckpointForkConformanceInput
|
|
31
|
+
| Promise<SandboxCheckpointForkConformanceInput>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ForkInputWithoutWriter = Omit<SandboxCheckpointForkInput, 'writer'>
|
|
35
|
+
|
|
36
|
+
function sourceCheckpoint(): SandboxCheckpoint {
|
|
37
|
+
return {
|
|
38
|
+
id: 'source-root',
|
|
39
|
+
threadId: 'source',
|
|
40
|
+
parentCheckpointId: null,
|
|
41
|
+
createdAt: 10,
|
|
42
|
+
reason: 'named',
|
|
43
|
+
label: 'source label',
|
|
44
|
+
sourceRunId: 'run-1',
|
|
45
|
+
files: [
|
|
46
|
+
{
|
|
47
|
+
path: 'a.txt',
|
|
48
|
+
kind: 'file',
|
|
49
|
+
blobKey: `sandbox-files/sha256/${'a'.repeat(64)}`,
|
|
50
|
+
size: 1,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
conversation: [{ role: 'user', content: 'checkpoint conversation' }],
|
|
54
|
+
artifacts: [],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function appendSource(
|
|
59
|
+
checkpoints: ForkCapableSandboxCheckpointStore,
|
|
60
|
+
): Promise<{
|
|
61
|
+
source: SandboxCheckpoint
|
|
62
|
+
sourceWriter: SandboxCheckpointWriterLease
|
|
63
|
+
}> {
|
|
64
|
+
const sourceWriter = await checkpoints.acquireWriter('source')
|
|
65
|
+
const source = sourceCheckpoint()
|
|
66
|
+
await checkpoints.append({
|
|
67
|
+
checkpoint: source,
|
|
68
|
+
expectedHeadId: null,
|
|
69
|
+
writer: sourceWriter,
|
|
70
|
+
})
|
|
71
|
+
return { source, sourceWriter }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function forkInput(
|
|
75
|
+
writer: SandboxCheckpointWriter,
|
|
76
|
+
overrides: Partial<ForkInputWithoutWriter> = {},
|
|
77
|
+
): SandboxCheckpointForkInput {
|
|
78
|
+
return {
|
|
79
|
+
sourceThreadId: 'source',
|
|
80
|
+
sourceCheckpointId: 'source-root',
|
|
81
|
+
destinationThreadId: 'destination',
|
|
82
|
+
destinationCheckpointId: 'fork-root',
|
|
83
|
+
createdAt: 20,
|
|
84
|
+
writer,
|
|
85
|
+
...overrides,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function destinationState(
|
|
90
|
+
persistence: SandboxCheckpointForkConformanceInput['persistence'],
|
|
91
|
+
checkpoints: ForkCapableSandboxCheckpointStore,
|
|
92
|
+
) {
|
|
93
|
+
return {
|
|
94
|
+
transcript: await persistence.stores.messages.loadThread('destination'),
|
|
95
|
+
head: await checkpoints.getHead('destination'),
|
|
96
|
+
checkpoints: await checkpoints.list('destination'),
|
|
97
|
+
references: await checkpoints.listBlobReferences(),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function expectRejectedWithoutDestinationChanges(
|
|
102
|
+
persistence: SandboxCheckpointForkConformanceInput['persistence'],
|
|
103
|
+
checkpoints: ForkCapableSandboxCheckpointStore,
|
|
104
|
+
operation: Promise<unknown>,
|
|
105
|
+
code: string,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const before = await destinationState(persistence, checkpoints)
|
|
108
|
+
await expect(operation).rejects.toMatchObject({ code })
|
|
109
|
+
expect(await destinationState(persistence, checkpoints)).toEqual(before)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function runSandboxCheckpointForkConformance(
|
|
113
|
+
name: string,
|
|
114
|
+
makeSnapshots: SandboxCheckpointForkConformanceFactory,
|
|
115
|
+
): void {
|
|
116
|
+
describe(`Sandbox checkpoint fork conformance: ${name}`, () => {
|
|
117
|
+
it('copies a selected historical checkpoint and creates an exact fork root', async () => {
|
|
118
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
119
|
+
const { source, sourceWriter } = await appendSource(checkpoints)
|
|
120
|
+
await checkpoints.append({
|
|
121
|
+
checkpoint: {
|
|
122
|
+
id: 'source-head',
|
|
123
|
+
threadId: 'source',
|
|
124
|
+
parentCheckpointId: source.id,
|
|
125
|
+
createdAt: 11,
|
|
126
|
+
reason: 'automatic',
|
|
127
|
+
files: [],
|
|
128
|
+
conversation: [{ role: 'user', content: 'newer conversation' }],
|
|
129
|
+
artifacts: [],
|
|
130
|
+
},
|
|
131
|
+
expectedHeadId: source.id,
|
|
132
|
+
writer: sourceWriter,
|
|
133
|
+
})
|
|
134
|
+
await persistence.stores.messages.saveThread('source', [
|
|
135
|
+
{ role: 'user', content: 'current source message' },
|
|
136
|
+
])
|
|
137
|
+
const destinationWriter = await checkpoints.acquireWriter('destination')
|
|
138
|
+
const result = await checkpoints.forkFromCheckpoint(
|
|
139
|
+
forkInput(destinationWriter),
|
|
140
|
+
)
|
|
141
|
+
expect(result.checkpoint).toEqual({
|
|
142
|
+
id: 'fork-root',
|
|
143
|
+
threadId: 'destination',
|
|
144
|
+
parentCheckpointId: null,
|
|
145
|
+
createdAt: 20,
|
|
146
|
+
reason: 'fork-root',
|
|
147
|
+
files: source.files,
|
|
148
|
+
conversation: source.conversation,
|
|
149
|
+
artifacts: [],
|
|
150
|
+
})
|
|
151
|
+
expect(
|
|
152
|
+
await persistence.stores.messages.loadThread('destination'),
|
|
153
|
+
).toEqual(source.conversation)
|
|
154
|
+
expect(await checkpoints.getHead('source')).toBe('source-head')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('rejects a plain checkpoint store because it has no fork capability', () => {
|
|
158
|
+
const store = new InMemorySandboxCheckpointStore()
|
|
159
|
+
expect('forkFromCheckpoint' in store).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('rejects a missing source without changing the destination', async () => {
|
|
163
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
164
|
+
const writer = await checkpoints.acquireWriter('destination')
|
|
165
|
+
await expectRejectedWithoutDestinationChanges(
|
|
166
|
+
persistence,
|
|
167
|
+
checkpoints,
|
|
168
|
+
checkpoints.forkFromCheckpoint(
|
|
169
|
+
forkInput(writer, { sourceCheckpointId: 'missing' }),
|
|
170
|
+
),
|
|
171
|
+
'SANDBOX_SNAPSHOT_FORK_SOURCE_NOT_FOUND',
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('rejects a source thread mismatch without changing the destination', async () => {
|
|
176
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
177
|
+
await appendSource(checkpoints)
|
|
178
|
+
const writer = await checkpoints.acquireWriter('destination')
|
|
179
|
+
await expectRejectedWithoutDestinationChanges(
|
|
180
|
+
persistence,
|
|
181
|
+
checkpoints,
|
|
182
|
+
checkpoints.forkFromCheckpoint(
|
|
183
|
+
forkInput(writer, { sourceThreadId: 'another-source' }),
|
|
184
|
+
),
|
|
185
|
+
'SANDBOX_SNAPSHOT_FORK_SOURCE_THREAD_MISMATCH',
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('rejects equal source and destination threads without changing the source', async () => {
|
|
190
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
191
|
+
const { source, sourceWriter } = await appendSource(checkpoints)
|
|
192
|
+
const before = {
|
|
193
|
+
transcript: await persistence.stores.messages.loadThread('source'),
|
|
194
|
+
head: await checkpoints.getHead('source'),
|
|
195
|
+
checkpoints: await checkpoints.list('source'),
|
|
196
|
+
references: await checkpoints.listBlobReferences(),
|
|
197
|
+
}
|
|
198
|
+
await expect(
|
|
199
|
+
checkpoints.forkFromCheckpoint(
|
|
200
|
+
forkInput(sourceWriter, {
|
|
201
|
+
destinationThreadId: source.threadId,
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
).rejects.toMatchObject({
|
|
205
|
+
code: 'SANDBOX_SNAPSHOT_FORK_SOURCE_THREAD_MISMATCH',
|
|
206
|
+
})
|
|
207
|
+
expect({
|
|
208
|
+
transcript: await persistence.stores.messages.loadThread('source'),
|
|
209
|
+
head: await checkpoints.getHead('source'),
|
|
210
|
+
checkpoints: await checkpoints.list('source'),
|
|
211
|
+
references: await checkpoints.listBlobReferences(),
|
|
212
|
+
}).toEqual(before)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('rejects a missing destination writer without changing the destination', async () => {
|
|
216
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
217
|
+
await appendSource(checkpoints)
|
|
218
|
+
const missingWriter: SandboxCheckpointWriter = {
|
|
219
|
+
threadId: 'destination',
|
|
220
|
+
ownerToken: 'missing-owner',
|
|
221
|
+
fence: 1,
|
|
222
|
+
}
|
|
223
|
+
await expectRejectedWithoutDestinationChanges(
|
|
224
|
+
persistence,
|
|
225
|
+
checkpoints,
|
|
226
|
+
checkpoints.forkFromCheckpoint(forkInput(missingWriter)),
|
|
227
|
+
'SANDBOX_SNAPSHOT_WRITER_LOST',
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('rejects a wrong destination writer without changing the destination', async () => {
|
|
232
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
233
|
+
await appendSource(checkpoints)
|
|
234
|
+
const wrongWriter = await checkpoints.acquireWriter('another-thread')
|
|
235
|
+
await expectRejectedWithoutDestinationChanges(
|
|
236
|
+
persistence,
|
|
237
|
+
checkpoints,
|
|
238
|
+
checkpoints.forkFromCheckpoint(forkInput(wrongWriter)),
|
|
239
|
+
'SANDBOX_SNAPSHOT_WRITER_LOST',
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('rejects a stale destination writer without changing the destination', async () => {
|
|
244
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
245
|
+
await appendSource(checkpoints)
|
|
246
|
+
const staleWriter = await checkpoints.acquireWriter('destination')
|
|
247
|
+
await staleWriter.release()
|
|
248
|
+
await expectRejectedWithoutDestinationChanges(
|
|
249
|
+
persistence,
|
|
250
|
+
checkpoints,
|
|
251
|
+
checkpoints.forkFromCheckpoint(forkInput(staleWriter)),
|
|
252
|
+
'SANDBOX_SNAPSHOT_WRITER_LOST',
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('rejects a destination transcript without changing its checkpoint state', async () => {
|
|
257
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
258
|
+
await appendSource(checkpoints)
|
|
259
|
+
await persistence.stores.messages.saveThread('destination', [
|
|
260
|
+
{ role: 'user', content: 'already here' },
|
|
261
|
+
])
|
|
262
|
+
const writer = await checkpoints.acquireWriter('destination')
|
|
263
|
+
await expectRejectedWithoutDestinationChanges(
|
|
264
|
+
persistence,
|
|
265
|
+
checkpoints,
|
|
266
|
+
checkpoints.forkFromCheckpoint(forkInput(writer)),
|
|
267
|
+
'SANDBOX_SNAPSHOT_FORK_DESTINATION_NOT_EMPTY',
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('rejects an orphaned checkpoint id without changing the destination', async () => {
|
|
272
|
+
const { persistence, checkpoints } = await makeSnapshots()
|
|
273
|
+
const { source } = await appendSource(checkpoints)
|
|
274
|
+
const orphanWriter = await checkpoints.acquireWriter('orphan-thread')
|
|
275
|
+
await checkpoints.append({
|
|
276
|
+
checkpoint: {
|
|
277
|
+
...source,
|
|
278
|
+
id: 'orphaned-id',
|
|
279
|
+
threadId: 'orphan-thread',
|
|
280
|
+
parentCheckpointId: null,
|
|
281
|
+
createdAt: 11,
|
|
282
|
+
},
|
|
283
|
+
expectedHeadId: null,
|
|
284
|
+
writer: orphanWriter,
|
|
285
|
+
})
|
|
286
|
+
const destinationWriter = await checkpoints.acquireWriter('destination')
|
|
287
|
+
await expectRejectedWithoutDestinationChanges(
|
|
288
|
+
persistence,
|
|
289
|
+
checkpoints,
|
|
290
|
+
checkpoints.forkFromCheckpoint(
|
|
291
|
+
forkInput(destinationWriter, {
|
|
292
|
+
destinationCheckpointId: 'orphaned-id',
|
|
293
|
+
}),
|
|
294
|
+
),
|
|
295
|
+
'SANDBOX_SNAPSHOT_FORK_DESTINATION_NOT_EMPTY',
|
|
296
|
+
)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
}
|
|
@@ -29,6 +29,13 @@ export type { ReaperConformanceConfig } from './reaper-conformance'
|
|
|
29
29
|
export { runDurableRunFieldsConformance } from './durable-run-fields-conformance'
|
|
30
30
|
export type { MakeRunStore } from './durable-run-fields-conformance'
|
|
31
31
|
export { makeFakeShellSpawn } from './shell-spawn'
|
|
32
|
+
export { runSandboxCheckpointStoreConformance } from './checkpoint-conformance'
|
|
33
|
+
export type { SandboxCheckpointStoreOptions } from '../checkpoint-store'
|
|
34
|
+
export { runSandboxCheckpointForkConformance } from './checkpoint-fork-conformance'
|
|
35
|
+
export type {
|
|
36
|
+
SandboxCheckpointForkConformanceInput,
|
|
37
|
+
SandboxCheckpointForkConformanceFactory,
|
|
38
|
+
} from './checkpoint-fork-conformance'
|
|
32
39
|
|
|
33
40
|
function makeRecord(
|
|
34
41
|
overrides?: Partial<SandboxInstanceRecord>,
|