@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.
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,117 @@
1
+ /**
2
+ * Conformance suite for a {@link SandboxInstanceStore} implementation.
3
+ *
4
+ * Run this against a fresh BYO store (or the in-memory reference) to prove it
5
+ * satisfies the get / upsert / delete contract `@tanstack/ai-sandbox`'s ensure
6
+ * algorithm relies on — including insert-vs-overwrite and optional-field
7
+ * handling (full replace must clear omitted optionals).
8
+ *
9
+ * Vitest is an OPTIONAL peer dependency: this module is imported only from test
10
+ * files, which already run under Vitest.
11
+ */
12
+ import { describe, expect, it } from 'vitest'
13
+ import type {
14
+ SandboxInstanceRecord,
15
+ SandboxInstanceStore,
16
+ } from '../instance-store'
17
+
18
+ // The `./testkit` package.json subpath resolves to THIS module's build output
19
+ // only (`dist/esm/testkit/conformance.js`), not a barrel over `src/testkit/`.
20
+ // Re-export the journal, takeover, and reaper conformance suites here so they
21
+ // are reachable from `@tanstack/ai-sandbox/testkit` without changing that
22
+ // mapping.
23
+ export { runJournalConformance } from './journal-conformance'
24
+ export type { JournalConformanceConfig } from './journal-conformance'
25
+ export { runTakeoverConformance } from './takeover-conformance'
26
+ export type { TakeoverConformanceConfig } from './takeover-conformance'
27
+ export { runReaperConformance } from './reaper-conformance'
28
+ export type { ReaperConformanceConfig } from './reaper-conformance'
29
+ export { runDurableRunFieldsConformance } from './durable-run-fields-conformance'
30
+ export type { MakeRunStore } from './durable-run-fields-conformance'
31
+ export { makeFakeShellSpawn } from './shell-spawn'
32
+
33
+ function makeRecord(
34
+ overrides?: Partial<SandboxInstanceRecord>,
35
+ ): SandboxInstanceRecord {
36
+ return {
37
+ key: 'thread-1',
38
+ provider: 'fake',
39
+ providerSandboxId: 'sb-1',
40
+ threadId: 'thread-1',
41
+ updatedAt: 1,
42
+ ...overrides,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Assert `makeStore()` produces a spec-compliant {@link SandboxInstanceStore}. Each
48
+ * `it` gets a fresh store, so implementations may share process state across
49
+ * calls without cross-test bleed only if `makeStore` returns an isolated store.
50
+ */
51
+ export function runSandboxInstanceStoreConformance(
52
+ name: string,
53
+ makeStore: () => SandboxInstanceStore | Promise<SandboxInstanceStore>,
54
+ ): void {
55
+ describe(`SandboxInstanceStore conformance: ${name}`, () => {
56
+ it('returns null for a missing key', async () => {
57
+ const store = await makeStore()
58
+ expect(await store.get('absent')).toBeNull()
59
+ })
60
+
61
+ it('round-trips an upserted record with all fields', async () => {
62
+ const store = await makeStore()
63
+ const record = makeRecord({
64
+ latestSnapshotId: 'snap-1',
65
+ latestRunId: 'run-1',
66
+ })
67
+ await store.upsert(record)
68
+ expect(await store.get(record.key)).toEqual(record)
69
+ })
70
+
71
+ it('omits absent optional fields on read', async () => {
72
+ const store = await makeStore()
73
+ const record = makeRecord()
74
+ await store.upsert(record)
75
+ const loaded = await store.get(record.key)
76
+ expect(loaded).toEqual(record)
77
+ expect(loaded && 'latestSnapshotId' in loaded).toBe(false)
78
+ expect(loaded && 'latestRunId' in loaded).toBe(false)
79
+ })
80
+
81
+ it('overwrites an existing record on re-upsert', async () => {
82
+ const store = await makeStore()
83
+ await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' }))
84
+ await store.upsert(
85
+ makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }),
86
+ )
87
+ const loaded = await store.get('thread-1')
88
+ expect(loaded?.providerSandboxId).toBe('sb-2')
89
+ expect(loaded?.updatedAt).toBe(2)
90
+ // The overwrite dropped latestSnapshotId — a durable store must clear it,
91
+ // not retain the prior value.
92
+ expect(loaded && 'latestSnapshotId' in loaded).toBe(false)
93
+ })
94
+
95
+ it('isolates records by key', async () => {
96
+ const store = await makeStore()
97
+ await store.upsert(makeRecord({ key: 'a', threadId: 'a' }))
98
+ await store.upsert(
99
+ makeRecord({ key: 'b', threadId: 'b', providerSandboxId: 'sb-b' }),
100
+ )
101
+ expect((await store.get('a'))?.providerSandboxId).toBe('sb-1')
102
+ expect((await store.get('b'))?.providerSandboxId).toBe('sb-b')
103
+ })
104
+
105
+ it('deletes a record', async () => {
106
+ const store = await makeStore()
107
+ await store.upsert(makeRecord())
108
+ await store.delete('thread-1')
109
+ expect(await store.get('thread-1')).toBeNull()
110
+ })
111
+
112
+ it('delete of a missing key is a no-op', async () => {
113
+ const store = await makeStore()
114
+ await expect(store.delete('absent')).resolves.toBeUndefined()
115
+ })
116
+ })
117
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Conformance for the DURABLE-RUN fields on a `RunStore`.
3
+ *
4
+ * These four fields (`sandboxKey`, `detachedSince`, `cancelRequested`,
5
+ * `driverEpoch`) exist for durable sandboxed runs: detach on disconnect, takeover
6
+ * by a later host, and the reaper. A chat-only app never writes them, so proving
7
+ * them is NOT part of `runPersistenceConformance` in `@tanstack/ai-persistence`.
8
+ * They live here, next to the takeover and reaper suites that depend on them.
9
+ *
10
+ * Run this when your app wires `withSandbox(sandbox, { runs, durability })`. The
11
+ * fields round-trip through the REQUIRED `update`/`get` pair, so a backend can
12
+ * pass every persistence case while silently dropping one of them, and the
13
+ * failure then shows up as a run that looks permanently detached or a takeover
14
+ * that cannot fence a superseded host.
15
+ *
16
+ * ```ts
17
+ * import { runDurableRunFieldsConformance } from '@tanstack/ai-sandbox/testkit'
18
+ * import { myPersistence } from './persistence'
19
+ *
20
+ * runDurableRunFieldsConformance('my postgres runs', () => myPersistence().stores.runs)
21
+ * ```
22
+ */
23
+ import { describe, expect, it } from 'vitest'
24
+ import type { RunStore } from '@tanstack/ai'
25
+
26
+ /** Factory for the store under test. A fresh one per case keeps them isolated. */
27
+ export type MakeRunStore = () => RunStore | Promise<RunStore>
28
+
29
+ export function runDurableRunFieldsConformance(
30
+ name: string,
31
+ makeStore: MakeRunStore,
32
+ ): void {
33
+ describe(`durable run fields conformance: ${name}`, () => {
34
+ // One case, because the four fields share one failure mode: a backend that
35
+ // filters `undefined` out of its `SET` clause, or coerces an absent column to
36
+ // a falsy default, passes every other assertion while breaking detach and
37
+ // takeover. Splitting it per field would hide that they must all behave the
38
+ // same way through one `update`.
39
+ it('round-trips the durable run fields, overwrites driverEpoch, and clears every one of them on explicit undefined', async () => {
40
+ const store = await makeStore()
41
+
42
+ await store.createOrResume({
43
+ runId: 'fc-1',
44
+ threadId: 'fc-t',
45
+ startedAt: 1,
46
+ })
47
+
48
+ // 0. A fresh run that was never patched with these fields must read
49
+ // back as undefined -- not null, not false, not 0. A backend that
50
+ // coerces a NULL/absent column to a falsy default (e.g.
51
+ // `cancelRequested: false`) is claiming knowledge ("explicitly not
52
+ // cancelled") it does not have, and `toBeFalsy()` would not catch
53
+ // it since `false` is falsy too.
54
+ const fresh = await store.get('fc-1')
55
+ expect(fresh?.cancelRequested).toBeUndefined()
56
+ expect(fresh?.detachedSince).toBeUndefined()
57
+ expect(fresh?.sandboxKey).toBeUndefined()
58
+ expect(fresh?.driverEpoch).toBeUndefined()
59
+
60
+ // 1. All four fields round-trip through update -> get.
61
+ await store.update('fc-1', {
62
+ sandboxKey: 'sandbox-abc',
63
+ detachedSince: 500,
64
+ cancelRequested: true,
65
+ driverEpoch: 1,
66
+ })
67
+ const afterFirstUpdate = await store.get('fc-1')
68
+ expect(afterFirstUpdate?.sandboxKey).toBe('sandbox-abc')
69
+ expect(afterFirstUpdate?.detachedSince).toBe(500)
70
+ expect(afterFirstUpdate?.cancelRequested).toBe(true)
71
+ expect(afterFirstUpdate?.driverEpoch).toBe(1)
72
+
73
+ // 2. A monotonic driverEpoch bump overwrites, it is not ignored (a
74
+ // takeover host bumping the fencing token must actually stick).
75
+ await store.update('fc-1', { driverEpoch: 2 })
76
+ const afterEpochBump = await store.get('fc-1')
77
+ expect(afterEpochBump?.driverEpoch).toBe(2)
78
+ // Sibling fields untouched by an update that only names driverEpoch.
79
+ expect(afterEpochBump?.sandboxKey).toBe('sandbox-abc')
80
+ expect(afterEpochBump?.cancelRequested).toBe(true)
81
+
82
+ // 3. update({ detachedSince: undefined }) actually CLEARS the field.
83
+ // A backend whose SQL adapter filters `undefined` out of its `SET`
84
+ // clause leaves the old value, and every re-attached run then looks
85
+ // permanently detached to the reaper.
86
+ await store.update('fc-1', { detachedSince: undefined })
87
+ const afterClear = await store.get('fc-1')
88
+ expect(afterClear?.detachedSince).toBeUndefined()
89
+ // Clearing detachedSince must not clobber the other durable fields.
90
+ expect(afterClear?.sandboxKey).toBe('sandbox-abc')
91
+ expect(afterClear?.cancelRequested).toBe(true)
92
+ expect(afterClear?.driverEpoch).toBe(2)
93
+
94
+ // 4. cancelRequested: false written EXPLICITLY must round-trip as
95
+ // `false`, distinct from the fresh-run `undefined` checked in step 0.
96
+ // A backend storing this boolean in an integer/NULL-able column has
97
+ // to preserve the false/undefined distinction in both directions,
98
+ // not just collapse both to falsy.
99
+ await store.update('fc-1', { cancelRequested: false })
100
+ const afterExplicitFalse = await store.get('fc-1')
101
+ expect(afterExplicitFalse?.cancelRequested).toBe(false)
102
+ expect(afterExplicitFalse?.cancelRequested).not.toBeUndefined()
103
+
104
+ // 5. An explicit `undefined` clears EVERY durable field, not just
105
+ // `detachedSince`. Step 3 only exercised one of the four, so a backend
106
+ // half-converted to `'field' in patch` -- `in` for `detachedSince`,
107
+ // still `patch.field !== undefined` for the rest -- passed the whole
108
+ // suite while its clears silently no-opped. Step 4's explicit `false`
109
+ // also survives a `!== undefined` guard, so nothing else here bites
110
+ // either. Re-populate first, so each clear has a value to remove and
111
+ // an assertion that fails when the clear is dropped.
112
+ await store.update('fc-1', {
113
+ sandboxKey: 'sandbox-xyz',
114
+ detachedSince: 900,
115
+ cancelRequested: true,
116
+ driverEpoch: 3,
117
+ })
118
+ const beforeFullClear = await store.get('fc-1')
119
+ expect(beforeFullClear?.sandboxKey).toBe('sandbox-xyz')
120
+ expect(beforeFullClear?.detachedSince).toBe(900)
121
+ expect(beforeFullClear?.cancelRequested).toBe(true)
122
+ expect(beforeFullClear?.driverEpoch).toBe(3)
123
+
124
+ await store.update('fc-1', {
125
+ sandboxKey: undefined,
126
+ detachedSince: undefined,
127
+ cancelRequested: undefined,
128
+ driverEpoch: undefined,
129
+ })
130
+ const afterFullClear = await store.get('fc-1')
131
+ expect(afterFullClear?.sandboxKey).toBeUndefined()
132
+ expect(afterFullClear?.detachedSince).toBeUndefined()
133
+ expect(afterFullClear?.cancelRequested).toBeUndefined()
134
+ expect(afterFullClear?.driverEpoch).toBeUndefined()
135
+ // Clearing the durable fields is not a delete: the run row survives,
136
+ // and the fields the patch never named keep their values.
137
+ expect(afterFullClear?.status).toBe('running')
138
+ expect(afterFullClear?.startedAt).toBe(1)
139
+ })
140
+
141
+ // `findActiveRun` is REQUIRED on the RunStore contract — every backend that
142
+ // provides a `runs` store must satisfy these invariants (most-recent-running
143
+ // wins, thread-scoped, null when idle). Reconnect is built on it, and a
144
+ // backend that always answers `null` disables reconnect indistinguishably
145
+ // from one that is merely idle, so this must never degrade to a skip.
146
+ })
147
+ }