@tanstack/ai-sandbox 0.3.4 → 0.5.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 (64) hide show
  1. package/README.md +26 -0
  2. package/dist/esm/approvals.js +5 -4
  3. package/dist/esm/approvals.js.map +1 -1
  4. package/dist/esm/bridge-events.js +7 -6
  5. package/dist/esm/bridge-events.js.map +1 -1
  6. package/dist/esm/checkpoint-store.d.ts +147 -0
  7. package/dist/esm/checkpoint-store.js +267 -0
  8. package/dist/esm/checkpoint-store.js.map +1 -0
  9. package/dist/esm/chunk-identity.d.ts +0 -17
  10. package/dist/esm/chunk-identity.js +28 -5
  11. package/dist/esm/chunk-identity.js.map +1 -1
  12. package/dist/esm/contracts.d.ts +19 -0
  13. package/dist/esm/index.d.ts +11 -1
  14. package/dist/esm/index.js +12 -7
  15. package/dist/esm/memory-snapshot-types.d.ts +129 -0
  16. package/dist/esm/memory-snapshots.d.ts +6 -0
  17. package/dist/esm/memory-snapshots.js +490 -0
  18. package/dist/esm/memory-snapshots.js.map +1 -0
  19. package/dist/esm/middleware.d.ts +33 -1
  20. package/dist/esm/middleware.js +339 -94
  21. package/dist/esm/middleware.js.map +1 -1
  22. package/dist/esm/ngrok.d.ts +1 -1
  23. package/dist/esm/sandbox.d.ts +16 -0
  24. package/dist/esm/sandbox.js +62 -9
  25. package/dist/esm/sandbox.js.map +1 -1
  26. package/dist/esm/snapshot-operations.d.ts +65 -0
  27. package/dist/esm/snapshot-operations.js +317 -0
  28. package/dist/esm/snapshot-operations.js.map +1 -0
  29. package/dist/esm/snapshot-tools.d.ts +185 -0
  30. package/dist/esm/snapshot-tools.js +160 -0
  31. package/dist/esm/snapshot-tools.js.map +1 -0
  32. package/dist/esm/snapshots.d.ts +51 -0
  33. package/dist/esm/snapshots.js +350 -0
  34. package/dist/esm/snapshots.js.map +1 -0
  35. package/dist/esm/testkit/checkpoint-conformance.d.ts +2 -0
  36. package/dist/esm/testkit/checkpoint-conformance.js +453 -0
  37. package/dist/esm/testkit/checkpoint-conformance.js.map +1 -0
  38. package/dist/esm/testkit/checkpoint-fork-conformance.d.ts +18 -0
  39. package/dist/esm/testkit/checkpoint-fork-conformance.js +191 -0
  40. package/dist/esm/testkit/checkpoint-fork-conformance.js.map +1 -0
  41. package/dist/esm/testkit/conformance.d.ts +4 -0
  42. package/dist/esm/testkit/conformance.js +3 -1
  43. package/dist/esm/testkit/conformance.js.map +1 -1
  44. package/dist/esm/tool-history.js +4 -5
  45. package/dist/esm/tool-history.js.map +1 -1
  46. package/package.json +8 -3
  47. package/skills/ai-sandbox/SKILL.md +96 -8
  48. package/src/approvals.ts +13 -12
  49. package/src/bridge-events.ts +16 -10
  50. package/src/checkpoint-store.ts +652 -0
  51. package/src/chunk-identity.ts +36 -5
  52. package/src/contracts.ts +12 -0
  53. package/src/index.ts +56 -0
  54. package/src/memory-snapshot-types.ts +167 -0
  55. package/src/memory-snapshots.ts +936 -0
  56. package/src/middleware.ts +610 -160
  57. package/src/sandbox.ts +107 -6
  58. package/src/snapshot-operations.ts +540 -0
  59. package/src/snapshot-tools.ts +208 -0
  60. package/src/snapshots.ts +711 -0
  61. package/src/testkit/checkpoint-conformance.ts +472 -0
  62. package/src/testkit/checkpoint-fork-conformance.ts +299 -0
  63. package/src/testkit/conformance.ts +7 -0
  64. package/src/tool-history.ts +9 -13
@@ -29,8 +29,16 @@
29
29
  * exactly that field — nothing downstream keys on a chunk's timestamp, so
30
30
  * leaving it wall-clock is safe, but every other field must participate in
31
31
  * the comparison or a real divergence would go undetected.
32
+ * 3. Adapter yields still carry leftover TanStack extras (`content`, `args`,
33
+ * `finishReason`). The durability log stores spec chunks. Fingerprints keep
34
+ * only AG-UI spec keys and drop `metadata.tanstack`, so a live adapter yield
35
+ * matches the stored spec chunk.
32
36
  */
33
37
  import type { StreamChunk } from '@tanstack/ai'
38
+ import {
39
+ isSpecTopLevelKey,
40
+ tanstackMetadata,
41
+ } from '@tanstack/ai/adapter-internals'
34
42
 
35
43
  /**
36
44
  * A deterministic id generator scoped to one run.
@@ -112,16 +120,37 @@ function stableStringify(
112
120
  * cannot spuriously diverge.
113
121
  * - **Recurses into nested arrays and objects**: tool-call arguments are
114
122
  * nested, and a shallow fingerprint would miss a changed argument.
115
- * - **Excludes exactly `VOLATILE_FIELDS`** (`timestamp`) — everything else
116
- * participates, including fields whose value is `undefined`.
123
+ * - **Excludes `timestamp` and leftover adapter extras.** Spec keys
124
+ * participate, including `undefined` values. `metadata.tanstack` is dropped
125
+ * so stored spec chunks match live adapter yields.
117
126
  * - **Distinguishes present-but-`undefined` from absent**: `undefined` is
118
127
  * encoded as the sentinel string `"__undefined__"` rather than dropped, so
119
128
  * `{a: undefined}` and `{}` do not collide. A translator emitting an
120
129
  * explicit `undefined` is a different chunk shape and must fingerprint
121
130
  * differently.
122
131
  */
132
+ function fingerprintableChunk(chunk: StreamChunk): Record<string, unknown> {
133
+ const out: Record<string, unknown> = {}
134
+ for (const [key, value] of Object.entries(chunk)) {
135
+ if (key === 'timestamp') continue
136
+ if (!isSpecTopLevelKey(chunk.type, key)) continue
137
+ if (key === 'metadata' && value != null && typeof value === 'object') {
138
+ const rest: Record<string, unknown> = {}
139
+ for (const [metaKey, metaValue] of Object.entries(value)) {
140
+ if (metaKey === 'tanstack') continue
141
+ rest[metaKey] = metaValue
142
+ }
143
+ if (Object.keys(rest).length === 0) continue
144
+ out.metadata = rest
145
+ continue
146
+ }
147
+ out[key] = value
148
+ }
149
+ return out
150
+ }
151
+
123
152
  export function chunkFingerprint(chunk: StreamChunk): string {
124
- return stableStringify(chunk, VOLATILE_FIELDS)
153
+ return stableStringify(fingerprintableChunk(chunk), VOLATILE_FIELDS)
125
154
  }
126
155
 
127
156
  /**
@@ -136,7 +165,7 @@ export function chunkFingerprint(chunk: StreamChunk): string {
136
165
  * `JournalReplayThreadIdMismatchError` in `align.ts`).
137
166
  */
138
167
  export function chunkFingerprintIgnoringThreadId(chunk: StreamChunk): string {
139
- return stableStringify(chunk, VOLATILE_AND_THREAD_ID)
168
+ return stableStringify(fingerprintableChunk(chunk), VOLATILE_AND_THREAD_ID)
140
169
  }
141
170
 
142
171
  /**
@@ -150,5 +179,7 @@ export function chunkFingerprintIgnoringThreadId(chunk: StreamChunk): string {
150
179
  export function chunkThreadId(chunk: StreamChunk): string | undefined {
151
180
  const record: Record<string, unknown> = chunk as Record<string, unknown>
152
181
  const value = record[THREAD_ID_FIELD]
153
- return typeof value === 'string' ? value : undefined
182
+ if (typeof value === 'string') return value
183
+ const nested = tanstackMetadata(chunk)?.threadId
184
+ return typeof nested === 'string' ? nested : undefined
154
185
  }
package/src/contracts.ts CHANGED
@@ -117,6 +117,11 @@ export interface SandboxFs {
117
117
  remove: (path: string) => Promise<void>
118
118
  rename: (from: string, to: string) => Promise<void>
119
119
  exists: (path: string) => Promise<boolean>
120
+ /**
121
+ * Optional metadata lookup. Implementations must not follow symlinks.
122
+ * Returns undefined only for a confirmed missing path. All other errors reject.
123
+ */
124
+ lstat?: (path: string) => Promise<SandboxFsStat | undefined>
120
125
  /** Optional — present only when `capabilities.fs` providers advertise watch. */
121
126
  watch?: (
122
127
  path: string,
@@ -124,6 +129,13 @@ export interface SandboxFs {
124
129
  ) => Promise<{ stop: () => Promise<void> }>
125
130
  }
126
131
 
132
+ export type SandboxFsStat =
133
+ // `mode` is the complete POSIX mode value, including the file-type bits.
134
+ | { type: 'file'; mode: number; size: number }
135
+ | { type: 'dir'; mode: number }
136
+ | { type: 'symlink'; mode: number }
137
+ | { type: 'other'; mode: number }
138
+
127
139
  /**
128
140
  * Uniform git surface. Implementations either delegate to the provider's
129
141
  * native git (when advertised) or desugar to `process.exec("git …")`, so the
package/src/index.ts CHANGED
@@ -27,6 +27,61 @@ export type {
27
27
  SandboxInstanceRecord,
28
28
  } from './instance-store'
29
29
 
30
+ // Portable immutable sandbox checkpoint metadata.
31
+ export {
32
+ SandboxCheckpointError,
33
+ SandboxCheckpointConflictError,
34
+ SandboxCheckpointDuplicateIdError,
35
+ SandboxCheckpointInvalidIdError,
36
+ SandboxCheckpointInvalidEntryError,
37
+ SandboxCheckpointParentMismatchError,
38
+ SandboxCheckpointNotHeadError,
39
+ SandboxCheckpointWriterConflictError,
40
+ SandboxCheckpointWriterLostError,
41
+ isForkCapableSandboxCheckpointStore,
42
+ InMemorySandboxCheckpointStore,
43
+ defineSandboxCheckpointStore,
44
+ } from './checkpoint-store'
45
+
46
+ export type {
47
+ SandboxCheckpoint,
48
+ SandboxCheckpointStore,
49
+ SandboxSnapshotEntry,
50
+ SandboxSnapshotFileEntry,
51
+ SandboxSnapshotDirectoryEntry,
52
+ SandboxSnapshotArtifact,
53
+ SandboxCheckpointErrorCode,
54
+ SandboxCheckpointWriter,
55
+ SandboxCheckpointWriterLease,
56
+ SandboxCheckpointStoreOptions,
57
+ SandboxCheckpointForkInput,
58
+ SandboxCheckpointForkCapability,
59
+ ForkCapableSandboxCheckpointStore,
60
+ } from './checkpoint-store'
61
+
62
+ // File snapshot policy used by provider snapshot create/restore inputs.
63
+ export { SandboxSnapshotError, defaultSandboxSnapshotPolicy } from './snapshots'
64
+ export type {
65
+ SandboxSnapshotErrorCode,
66
+ SandboxSnapshotPolicy,
67
+ } from './snapshots'
68
+ export { memorySandboxSnapshots } from './memory-snapshots'
69
+ export type {
70
+ MemorySandboxSnapshots,
71
+ MemorySandboxSnapshotsOptions,
72
+ } from './memory-snapshots'
73
+ export { createSandboxSnapshots } from './snapshot-operations'
74
+ export type {
75
+ CreateSandboxSnapshotsInput,
76
+ ForkSandboxSnapshotInput,
77
+ ReadSandboxSnapshotArtifactInput,
78
+ SandboxSnapshots,
79
+ SaveSandboxSnapshotInput,
80
+ SnapshotPersistence,
81
+ } from './snapshot-operations'
82
+ export { createSnapshotTools } from './snapshot-tools'
83
+ export type { CreateSnapshotToolsOptions } from './snapshot-tools'
84
+
30
85
  // Workspace projection capability (provided by withSandbox, consumed by harness adapters)
31
86
  export {
32
87
  ProjectionCapability,
@@ -102,6 +157,7 @@ export type {
102
157
  SandboxHandle,
103
158
  SandboxCapabilities,
104
159
  SandboxFs,
160
+ SandboxFsStat,
105
161
  SandboxGit,
106
162
  SandboxProcess,
107
163
  SandboxPorts,
@@ -0,0 +1,167 @@
1
+ import type {
2
+ ModelMessage,
3
+ RunRecord,
4
+ RunStatus,
5
+ RunStore,
6
+ PersistedArtifactRef,
7
+ TokenUsage,
8
+ } from '@tanstack/ai'
9
+
10
+ export interface MemoryMessageStore {
11
+ loadThread: (threadId: string) => Promise<Array<ModelMessage>>
12
+ saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>
13
+ }
14
+
15
+ export type MemoryRunRecord = RunRecord
16
+
17
+ export type MemoryRunStore = RunStore
18
+
19
+ export interface MemoryGenerationRunRecord {
20
+ runId: string
21
+ threadId: string
22
+ activity: string
23
+ provider: string
24
+ model: string
25
+ status: RunStatus
26
+ startedAt: number
27
+ finishedAt?: number
28
+ error?: { message: string; code?: string }
29
+ result?: unknown
30
+ artifacts?: Array<PersistedArtifactRef>
31
+ usage?: TokenUsage
32
+ }
33
+
34
+ export interface MemoryGenerationRunStore {
35
+ createOrResume: (
36
+ input: Pick<
37
+ MemoryGenerationRunRecord,
38
+ 'runId' | 'threadId' | 'activity' | 'provider' | 'model' | 'startedAt'
39
+ > & { status?: RunStatus },
40
+ ) => Promise<MemoryGenerationRunRecord>
41
+ update: (
42
+ runId: string,
43
+ patch: Partial<
44
+ Pick<
45
+ MemoryGenerationRunRecord,
46
+ 'status' | 'finishedAt' | 'error' | 'result' | 'artifacts' | 'usage'
47
+ >
48
+ >,
49
+ ) => Promise<void>
50
+ get: (runId: string) => Promise<MemoryGenerationRunRecord | null>
51
+ findLatestForThread: (
52
+ threadId: string,
53
+ ) => Promise<MemoryGenerationRunRecord | null>
54
+ }
55
+
56
+ export interface MemoryInterruptRecord {
57
+ interruptId: string
58
+ runId: string
59
+ threadId: string
60
+ status: 'pending' | 'resolved' | 'cancelled'
61
+ requestedAt: number
62
+ resolvedAt?: number
63
+ payload: Record<string, unknown>
64
+ response?: unknown
65
+ }
66
+
67
+ export interface MemoryInterruptStore {
68
+ create: (
69
+ record: Omit<MemoryInterruptRecord, 'status' | 'resolvedAt'>,
70
+ ) => Promise<void>
71
+ resolve: (interruptId: string, response?: unknown) => Promise<void>
72
+ cancel: (interruptId: string) => Promise<void>
73
+ get: (interruptId: string) => Promise<MemoryInterruptRecord | null>
74
+ list: (threadId: string) => Promise<Array<MemoryInterruptRecord>>
75
+ listPending: (threadId: string) => Promise<Array<MemoryInterruptRecord>>
76
+ listByRun: (runId: string) => Promise<Array<MemoryInterruptRecord>>
77
+ listPendingByRun: (runId: string) => Promise<Array<MemoryInterruptRecord>>
78
+ }
79
+
80
+ export interface MemoryMetadataStore {
81
+ get: (namespace: string, key: string) => Promise<unknown | null>
82
+ set: (namespace: string, key: string, value: unknown) => Promise<void>
83
+ delete: (namespace: string, key: string) => Promise<void>
84
+ }
85
+
86
+ export interface MemoryArtifactRecord {
87
+ artifactId: string
88
+ runId: string
89
+ threadId: string
90
+ blobKey?: string
91
+ name: string
92
+ mimeType: string
93
+ size: number
94
+ sourceUrl?: string
95
+ createdAt: number
96
+ }
97
+
98
+ export interface MemoryArtifactStore {
99
+ save: (record: MemoryArtifactRecord) => Promise<void>
100
+ get: (artifactId: string) => Promise<MemoryArtifactRecord | null>
101
+ list: (runId: string) => Promise<Array<MemoryArtifactRecord>>
102
+ listForThread: (threadId: string) => Promise<Array<MemoryArtifactRecord>>
103
+ delete: (artifactId: string) => Promise<void>
104
+ deleteForRun: (runId: string) => Promise<void>
105
+ }
106
+
107
+ export type MemoryBlobBody =
108
+ | ReadableStream<Uint8Array>
109
+ | ArrayBuffer
110
+ | ArrayBufferView
111
+ | string
112
+ | Blob
113
+ export interface MemoryBlobRecord {
114
+ key: string
115
+ size?: number
116
+ etag?: string
117
+ contentType?: string
118
+ customMetadata?: Record<string, string>
119
+ createdAt?: number
120
+ updatedAt?: number
121
+ }
122
+ export interface MemoryBlobStore {
123
+ put: (
124
+ key: string,
125
+ body: MemoryBlobBody,
126
+ options?: {
127
+ contentType?: string
128
+ customMetadata?: Record<string, string>
129
+ expectedLength?: number
130
+ },
131
+ ) => Promise<MemoryBlobRecord>
132
+ get: (
133
+ key: string,
134
+ options?: { range?: { offset: number; length?: number } },
135
+ ) => Promise<
136
+ | (MemoryBlobRecord & {
137
+ arrayBuffer: () => Promise<ArrayBuffer>
138
+ text: () => Promise<string>
139
+ body?: ReadableStream<Uint8Array>
140
+ range?: { offset: number; length: number }
141
+ })
142
+ | null
143
+ >
144
+ head: (key: string) => Promise<MemoryBlobRecord | null>
145
+ delete: (key: string) => Promise<void>
146
+ list: (options?: {
147
+ prefix?: string
148
+ cursor?: string
149
+ limit?: number
150
+ }) => Promise<{
151
+ objects: Array<MemoryBlobRecord>
152
+ cursor?: string
153
+ truncated?: boolean
154
+ }>
155
+ }
156
+
157
+ export interface MemorySnapshotPersistence {
158
+ stores: {
159
+ messages: MemoryMessageStore
160
+ runs: MemoryRunStore
161
+ generationRuns: MemoryGenerationRunStore
162
+ interrupts: MemoryInterruptStore
163
+ metadata: MemoryMetadataStore
164
+ artifacts: MemoryArtifactStore
165
+ blobs: MemoryBlobStore
166
+ }
167
+ }