zeitlich 0.2.35 → 0.2.37

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 (199) hide show
  1. package/README.md +146 -92
  2. package/dist/{activities-BVI2lTwr.d.ts → activities-Bb-nAjwQ.d.ts} +2 -2
  3. package/dist/{activities-hd4aNnZE.d.cts → activities-vkI4_3CC.d.cts} +2 -2
  4. package/dist/adapters/sandbox/bedrock/index.cjs +14 -11
  5. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/bedrock/index.d.cts +4 -3
  7. package/dist/adapters/sandbox/bedrock/index.d.ts +4 -3
  8. package/dist/adapters/sandbox/bedrock/index.js +14 -11
  9. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  10. package/dist/adapters/sandbox/bedrock/workflow.cjs +2 -0
  11. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -1
  12. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  13. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  14. package/dist/adapters/sandbox/bedrock/workflow.js +2 -0
  15. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -1
  16. package/dist/adapters/sandbox/daytona/index.cjs +35 -6
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  19. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  20. package/dist/adapters/sandbox/daytona/index.js +35 -6
  21. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  22. package/dist/adapters/sandbox/daytona/workflow.cjs +2 -0
  23. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  24. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  25. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  26. package/dist/adapters/sandbox/daytona/workflow.js +2 -0
  27. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.cjs +59 -10
  29. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  30. package/dist/adapters/sandbox/e2b/index.d.cts +5 -3
  31. package/dist/adapters/sandbox/e2b/index.d.ts +5 -3
  32. package/dist/adapters/sandbox/e2b/index.js +59 -10
  33. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  34. package/dist/adapters/sandbox/e2b/workflow.cjs +2 -0
  35. package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -1
  36. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  37. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  38. package/dist/adapters/sandbox/e2b/workflow.js +2 -0
  39. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/index.cjs +5 -0
  41. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  42. package/dist/adapters/sandbox/inmemory/index.d.cts +2 -1
  43. package/dist/adapters/sandbox/inmemory/index.d.ts +2 -1
  44. package/dist/adapters/sandbox/inmemory/index.js +5 -0
  45. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  46. package/dist/adapters/sandbox/inmemory/workflow.cjs +2 -0
  47. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  48. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  49. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  50. package/dist/adapters/sandbox/inmemory/workflow.js +2 -0
  51. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  52. package/dist/adapters/thread/anthropic/index.cjs +71 -36
  53. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  54. package/dist/adapters/thread/anthropic/index.d.cts +5 -5
  55. package/dist/adapters/thread/anthropic/index.d.ts +5 -5
  56. package/dist/adapters/thread/anthropic/index.js +71 -36
  57. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  58. package/dist/adapters/thread/anthropic/workflow.cjs +5 -1
  59. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  60. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  61. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  62. package/dist/adapters/thread/anthropic/workflow.js +5 -1
  63. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  64. package/dist/adapters/thread/google-genai/index.cjs +50 -25
  65. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  66. package/dist/adapters/thread/google-genai/index.d.cts +5 -5
  67. package/dist/adapters/thread/google-genai/index.d.ts +5 -5
  68. package/dist/adapters/thread/google-genai/index.js +50 -25
  69. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  70. package/dist/adapters/thread/google-genai/workflow.cjs +5 -1
  71. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  72. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  73. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  74. package/dist/adapters/thread/google-genai/workflow.js +5 -1
  75. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  76. package/dist/adapters/thread/langchain/index.cjs +34 -7
  77. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  78. package/dist/adapters/thread/langchain/index.d.cts +5 -5
  79. package/dist/adapters/thread/langchain/index.d.ts +5 -5
  80. package/dist/adapters/thread/langchain/index.js +34 -7
  81. package/dist/adapters/thread/langchain/index.js.map +1 -1
  82. package/dist/adapters/thread/langchain/workflow.cjs +5 -1
  83. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  84. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  85. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  86. package/dist/adapters/thread/langchain/workflow.js +5 -1
  87. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  88. package/dist/index.cjs +206 -120
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +17 -11
  91. package/dist/index.d.ts +17 -11
  92. package/dist/index.js +207 -121
  93. package/dist/index.js.map +1 -1
  94. package/dist/{proxy-BjdFGPTm.d.ts → proxy-0smGKvx8.d.ts} +1 -1
  95. package/dist/{proxy-7RnVaPdJ.d.cts → proxy-DEtowJyd.d.cts} +1 -1
  96. package/dist/{thread-manager-DjN5JYul.d.ts → thread-manager-3fszQih4.d.ts} +2 -2
  97. package/dist/{thread-manager-CbpiGq1L.d.ts → thread-manager-C-C4pI2z.d.ts} +2 -2
  98. package/dist/{thread-manager-BBzNgQWH.d.cts → thread-manager-CzYln2OC.d.cts} +2 -2
  99. package/dist/{thread-manager-DzXm9eeI.d.cts → thread-manager-D4vgzYrh.d.cts} +2 -2
  100. package/dist/{types-yiXmqedU.d.ts → types-B37hKoWA.d.ts} +1 -1
  101. package/dist/{types-DQ1l_gXL.d.cts → types-BO7Yju20.d.cts} +63 -14
  102. package/dist/{types-wiGLvxWf.d.ts → types-CNuWnvy9.d.ts} +1 -1
  103. package/dist/{types-CADc5V_P.d.ts → types-CPKDl-y_.d.ts} +63 -14
  104. package/dist/{types-Mc_4BCfT.d.cts → types-D08CXPh8.d.cts} +1 -1
  105. package/dist/{types-CBH54cwr.d.cts → types-DWEUmYAJ.d.cts} +1 -1
  106. package/dist/{types-DxCpFNv_.d.cts → types-tQL9njTu.d.cts} +25 -0
  107. package/dist/{types-DxCpFNv_.d.ts → types-tQL9njTu.d.ts} +25 -0
  108. package/dist/{workflow-P2pTSfKu.d.ts → workflow-CjXHbZZc.d.ts} +2 -2
  109. package/dist/{workflow-DhtWRovz.d.cts → workflow-Do_lzJpT.d.cts} +2 -2
  110. package/dist/workflow.cjs +182 -114
  111. package/dist/workflow.cjs.map +1 -1
  112. package/dist/workflow.d.cts +3 -3
  113. package/dist/workflow.d.ts +3 -3
  114. package/dist/workflow.js +183 -115
  115. package/dist/workflow.js.map +1 -1
  116. package/package.json +1 -1
  117. package/src/adapters/sandbox/bedrock/filesystem.ts +6 -12
  118. package/src/adapters/sandbox/bedrock/index.ts +10 -8
  119. package/src/adapters/sandbox/bedrock/proxy.ts +2 -0
  120. package/src/adapters/sandbox/daytona/filesystem.ts +29 -6
  121. package/src/adapters/sandbox/daytona/index.ts +6 -0
  122. package/src/adapters/sandbox/daytona/proxy.ts +2 -0
  123. package/src/adapters/sandbox/e2b/filesystem.ts +5 -4
  124. package/src/adapters/sandbox/e2b/index.ts +63 -12
  125. package/src/adapters/sandbox/e2b/proxy.ts +2 -0
  126. package/src/adapters/sandbox/inmemory/index.ts +5 -0
  127. package/src/adapters/sandbox/inmemory/proxy.ts +2 -0
  128. package/src/adapters/thread/anthropic/activities.ts +49 -26
  129. package/src/adapters/thread/anthropic/model-invoker.ts +15 -6
  130. package/src/adapters/thread/anthropic/proxy.ts +6 -2
  131. package/src/adapters/thread/anthropic/thread-manager.test.ts +26 -7
  132. package/src/adapters/thread/anthropic/thread-manager.ts +60 -46
  133. package/src/adapters/thread/google-genai/activities.ts +7 -2
  134. package/src/adapters/thread/google-genai/model-invoker.ts +26 -8
  135. package/src/adapters/thread/google-genai/proxy.ts +6 -2
  136. package/src/adapters/thread/google-genai/thread-manager.test.ts +13 -3
  137. package/src/adapters/thread/google-genai/thread-manager.ts +54 -33
  138. package/src/adapters/thread/langchain/activities.ts +46 -24
  139. package/src/adapters/thread/langchain/hooks.test.ts +36 -49
  140. package/src/adapters/thread/langchain/hooks.ts +18 -5
  141. package/src/adapters/thread/langchain/model-invoker.ts +3 -3
  142. package/src/adapters/thread/langchain/proxy.ts +6 -2
  143. package/src/adapters/thread/langchain/thread-manager.test.ts +5 -1
  144. package/src/adapters/thread/langchain/thread-manager.ts +20 -9
  145. package/src/index.ts +4 -1
  146. package/src/lib/activity.ts +16 -6
  147. package/src/lib/hooks/types.ts +6 -6
  148. package/src/lib/lifecycle.ts +9 -1
  149. package/src/lib/model/proxy.ts +2 -2
  150. package/src/lib/observability/hooks.ts +4 -5
  151. package/src/lib/observability/index.ts +1 -4
  152. package/src/lib/sandbox/manager.ts +21 -4
  153. package/src/lib/sandbox/node-fs.ts +3 -6
  154. package/src/lib/sandbox/sandbox.test.ts +36 -3
  155. package/src/lib/sandbox/tree.integration.test.ts +10 -3
  156. package/src/lib/sandbox/types.ts +35 -1
  157. package/src/lib/session/session-edge-cases.integration.test.ts +51 -13
  158. package/src/lib/session/session.integration.test.ts +139 -0
  159. package/src/lib/session/session.ts +50 -19
  160. package/src/lib/session/types.ts +13 -5
  161. package/src/lib/skills/fs-provider.ts +12 -8
  162. package/src/lib/skills/handler.ts +1 -1
  163. package/src/lib/skills/parse.ts +3 -1
  164. package/src/lib/skills/register.ts +1 -3
  165. package/src/lib/skills/skills.integration.test.ts +25 -15
  166. package/src/lib/state/manager.integration.test.ts +12 -2
  167. package/src/lib/subagent/define.ts +1 -1
  168. package/src/lib/subagent/handler.ts +186 -71
  169. package/src/lib/subagent/index.ts +1 -5
  170. package/src/lib/subagent/register.ts +3 -2
  171. package/src/lib/subagent/signals.ts +1 -10
  172. package/src/lib/subagent/subagent.integration.test.ts +438 -156
  173. package/src/lib/subagent/tool.ts +4 -3
  174. package/src/lib/subagent/types.ts +50 -20
  175. package/src/lib/subagent/workflow.ts +9 -49
  176. package/src/lib/thread/id.test.ts +1 -1
  177. package/src/lib/thread/id.ts +1 -2
  178. package/src/lib/thread/proxy.ts +3 -4
  179. package/src/lib/thread/types.ts +11 -3
  180. package/src/lib/tool-router/index.ts +1 -5
  181. package/src/lib/tool-router/router-edge-cases.integration.test.ts +1 -1
  182. package/src/lib/tool-router/router.ts +3 -2
  183. package/src/lib/tool-router/types.ts +11 -3
  184. package/src/lib/tool-router/with-sandbox.ts +19 -5
  185. package/src/lib/virtual-fs/filesystem.ts +1 -1
  186. package/src/lib/virtual-fs/index.ts +5 -1
  187. package/src/lib/virtual-fs/mutations.ts +2 -4
  188. package/src/lib/virtual-fs/queries.ts +9 -5
  189. package/src/lib/virtual-fs/types.ts +4 -1
  190. package/src/lib/virtual-fs/virtual-fs.test.ts +9 -11
  191. package/src/lib/workflow.test.ts +7 -4
  192. package/src/lib/workflow.ts +1 -5
  193. package/src/tools/ask-user-question/tool.ts +1 -3
  194. package/src/tools/glob/handler.ts +1 -4
  195. package/src/tools/task-get/handler.ts +4 -5
  196. package/src/tools/task-list/handler.ts +1 -4
  197. package/src/tools/task-update/handler.ts +4 -5
  198. package/src/workflow.ts +20 -7
  199. package/tsup.config.ts +9 -6
@@ -50,8 +50,12 @@ export interface SandboxManagerHooks<
50
50
 
51
51
  /**
52
52
  * Called after a sandbox has been successfully created.
53
+ *
54
+ * Receives the live {@link Sandbox} instance so the hook can run setup
55
+ * commands, seed files, or capture identifiers without an extra
56
+ * `provider.get()` round-trip.
53
57
  */
54
- onPostCreate?: (sandboxId: string) => Promise<void>;
58
+ onPostCreate?: (sandbox: Sandbox, ctx: TCtx) => Promise<void>;
55
59
  }
56
60
 
57
61
  /**
@@ -87,8 +91,9 @@ export interface SandboxManagerHooks<
87
91
  * for (const p of filePaths) files[p] = await db.readFile(projectId, p);
88
92
  * return { modifiedOptions: { initialFiles: files } };
89
93
  * },
90
- * onPostCreate: async (sandboxId) => {
91
- * console.log("Sandbox created:", sandboxId);
94
+ * onPostCreate: async (sandbox) => {
95
+ * console.log("Sandbox created:", sandbox.id);
96
+ * await sandbox.exec("git init");
92
97
  * },
93
98
  * },
94
99
  * },
@@ -148,7 +153,7 @@ export class SandboxManager<
148
153
  const { sandbox } = await this.provider.create(providerOptions);
149
154
 
150
155
  if (this.hooks.onPostCreate) {
151
- await this.hooks.onPostCreate(sandbox.id);
156
+ await this.hooks.onPostCreate(sandbox, ctx ?? ({} as TCtx));
152
157
  }
153
158
 
154
159
  return { sandboxId: sandbox.id };
@@ -179,6 +184,10 @@ export class SandboxManager<
179
184
  return sandbox.id;
180
185
  }
181
186
 
187
+ async deleteSnapshot(snapshot: SandboxSnapshot): Promise<void> {
188
+ await this.provider.deleteSnapshot(snapshot);
189
+ }
190
+
182
191
  async fork(sandboxId: string): Promise<string> {
183
192
  const sandbox = await this.provider.fork(sandboxId);
184
193
  return sandbox.id;
@@ -232,6 +241,14 @@ export class SandboxManager<
232
241
  snapshotSandbox: async (sandboxId: string): Promise<SandboxSnapshot> => {
233
242
  return this.snapshot(sandboxId);
234
243
  },
244
+ restoreSandbox: async (snapshot: SandboxSnapshot): Promise<string> => {
245
+ return this.restore(snapshot);
246
+ },
247
+ deleteSandboxSnapshot: async (
248
+ snapshot: SandboxSnapshot
249
+ ): Promise<void> => {
250
+ await this.deleteSnapshot(snapshot);
251
+ },
235
252
  forkSandbox: async (sandboxId: string): Promise<string> => {
236
253
  return this.fork(sandboxId);
237
254
  },
@@ -65,10 +65,7 @@ export class NodeFsSandboxFileSystem implements SandboxFileSystem {
65
65
  };
66
66
  }
67
67
 
68
- async mkdir(
69
- path: string,
70
- options?: { recursive?: boolean },
71
- ): Promise<void> {
68
+ async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
72
69
  await fsp.mkdir(this.abs(path), options);
73
70
  }
74
71
 
@@ -88,7 +85,7 @@ export class NodeFsSandboxFileSystem implements SandboxFileSystem {
88
85
 
89
86
  async rm(
90
87
  path: string,
91
- options?: { recursive?: boolean; force?: boolean },
88
+ options?: { recursive?: boolean; force?: boolean }
92
89
  ): Promise<void> {
93
90
  await fsp.rm(this.abs(path), options);
94
91
  }
@@ -96,7 +93,7 @@ export class NodeFsSandboxFileSystem implements SandboxFileSystem {
96
93
  async cp(
97
94
  src: string,
98
95
  dest: string,
99
- options?: { recursive?: boolean },
96
+ options?: { recursive?: boolean }
100
97
  ): Promise<void> {
101
98
  await fsp.cp(this.abs(src), this.abs(dest), options);
102
99
  }
@@ -157,18 +157,21 @@ describe("SandboxManager", () => {
157
157
  expect(await sandbox.fs.readFile("/file.txt")).toBe("explicit");
158
158
  });
159
159
 
160
- it("onPostCreate hook receives sandboxId", async () => {
160
+ it("onPostCreate hook receives the live sandbox", async () => {
161
161
  let capturedId: string | undefined;
162
162
  const mgr = new SandboxManager(new InMemorySandboxProvider(), {
163
163
  hooks: {
164
- onPostCreate: async (sandboxId) => {
165
- capturedId = sandboxId;
164
+ onPostCreate: async (sandbox) => {
165
+ capturedId = sandbox.id;
166
+ await sandbox.fs.writeFile("/hook.txt", "written-by-hook");
166
167
  },
167
168
  },
168
169
  });
169
170
 
170
171
  const { sandboxId } = await mustCreate(mgr);
171
172
  expect(capturedId).toBe(sandboxId);
173
+ const sandbox = await mgr.getSandbox(sandboxId);
174
+ expect(await sandbox.fs.readFile("/hook.txt")).toBe("written-by-hook");
172
175
  });
173
176
 
174
177
  it("onPostCreate hook does not run when creation is skipped", async () => {
@@ -192,6 +195,8 @@ describe("SandboxManager", () => {
192
195
  expect(activities.inMemoryTestCreateSandbox).toBeTypeOf("function");
193
196
  expect(activities.inMemoryTestDestroySandbox).toBeTypeOf("function");
194
197
  expect(activities.inMemoryTestSnapshotSandbox).toBeTypeOf("function");
198
+ expect(activities.inMemoryTestRestoreSandbox).toBeTypeOf("function");
199
+ expect(activities.inMemoryTestDeleteSandboxSnapshot).toBeTypeOf("function");
195
200
 
196
201
  const result = await activities.inMemoryTestCreateSandbox();
197
202
  expect(result).not.toBeNull();
@@ -203,6 +208,34 @@ describe("SandboxManager", () => {
203
208
  SandboxNotFoundError
204
209
  );
205
210
  });
211
+
212
+ it("restoreSandbox activity creates a new sandbox from a snapshot", async () => {
213
+ const activities = manager.createActivities("Test");
214
+
215
+ const created = await activities.inMemoryTestCreateSandbox({
216
+ initialFiles: { "/greeting.txt": "hello" },
217
+ });
218
+ const { sandboxId } = created as { sandboxId: string };
219
+
220
+ const snapshot = await activities.inMemoryTestSnapshotSandbox(sandboxId);
221
+ await activities.inMemoryTestDestroySandbox(sandboxId);
222
+
223
+ const restoredId = await activities.inMemoryTestRestoreSandbox(snapshot);
224
+ const restored = await manager.getSandbox(restoredId);
225
+ expect(await restored.fs.readFile("/greeting.txt")).toBe("hello");
226
+ });
227
+
228
+ it("deleteSandboxSnapshot activity is a no-op for in-memory snapshots", async () => {
229
+ const activities = manager.createActivities("Test");
230
+
231
+ const created = await activities.inMemoryTestCreateSandbox();
232
+ const { sandboxId } = created as { sandboxId: string };
233
+
234
+ const snapshot = await activities.inMemoryTestSnapshotSandbox(sandboxId);
235
+ await expect(
236
+ activities.inMemoryTestDeleteSandboxSnapshot(snapshot)
237
+ ).resolves.toBeUndefined();
238
+ });
206
239
  });
207
240
 
208
241
  describe("InMemorySandboxProvider", () => {
@@ -3,7 +3,10 @@ import { toTree } from "./tree";
3
3
  import type { SandboxFileSystem, DirentEntry } from "./types";
4
4
 
5
5
  function createMockFs(
6
- structure: Record<string, { isDir: boolean; isLink?: boolean; linkTarget?: string }>,
6
+ structure: Record<
7
+ string,
8
+ { isDir: boolean; isLink?: boolean; linkTarget?: string }
9
+ >
7
10
  ): SandboxFileSystem {
8
11
  return {
9
12
  workspaceBase: "/",
@@ -21,13 +24,17 @@ function createMockFs(
21
24
  readdir: async (dir: string) => {
22
25
  const prefix = dir.endsWith("/") ? dir : dir + "/";
23
26
  return Object.keys(structure)
24
- .filter((p) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/"))
27
+ .filter(
28
+ (p) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/")
29
+ )
25
30
  .map((p) => p.slice(prefix.length));
26
31
  },
27
32
  readdirWithFileTypes: async (dir: string): Promise<DirentEntry[]> => {
28
33
  const prefix = dir.endsWith("/") ? dir : dir + "/";
29
34
  return Object.entries(structure)
30
- .filter(([p]) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/"))
35
+ .filter(
36
+ ([p]) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/")
37
+ )
31
38
  .map(([p, meta]) => ({
32
39
  name: p.slice(prefix.length),
33
40
  isFile: !meta.isDir && !meta.isLink,
@@ -17,6 +17,21 @@ export interface FileStat {
17
17
  mtime: Date;
18
18
  }
19
19
 
20
+ // ============================================================================
21
+ // Network & lifecycle
22
+ // ============================================================================
23
+
24
+ export interface SandboxNetworkConfig {
25
+ allowOut?: string[];
26
+ denyOut?: string[];
27
+ allowPublicTraffic?: boolean;
28
+ }
29
+
30
+ export interface SandboxLifecycleConfig {
31
+ onTimeout: "kill" | "pause";
32
+ autoResume?: boolean;
33
+ }
34
+
20
35
  /**
21
36
  * Provider-agnostic filesystem interface.
22
37
  *
@@ -114,6 +129,16 @@ export interface SandboxCreateOptions {
114
129
  initialFiles?: Record<string, string | Uint8Array>;
115
130
  /** Environment variables available inside the sandbox */
116
131
  env?: Record<string, string>;
132
+ /** Key-value metadata surfaced via provider list/query APIs */
133
+ metadata?: Record<string, string>;
134
+ /** Sandbox idle timeout in milliseconds */
135
+ timeoutMs?: number;
136
+ /** Enable or disable outbound internet access */
137
+ allowInternetAccess?: boolean;
138
+ /** Outbound network allow/deny rules */
139
+ network?: SandboxNetworkConfig;
140
+ /** Sandbox timeout behaviour */
141
+ lifecycle?: SandboxLifecycleConfig;
117
142
  }
118
143
 
119
144
  export interface SandboxCreateResult {
@@ -135,6 +160,8 @@ export interface SandboxProvider<
135
160
  resume(sandboxId: string): Promise<void>;
136
161
  snapshot(sandboxId: string): Promise<SandboxSnapshot>;
137
162
  restore(snapshot: SandboxSnapshot): Promise<Sandbox>;
163
+ /** Delete a previously captured snapshot. No-op if already deleted. */
164
+ deleteSnapshot(snapshot: SandboxSnapshot): Promise<void>;
138
165
  fork(sandboxId: string): Promise<Sandbox>;
139
166
  }
140
167
 
@@ -155,6 +182,10 @@ export interface SandboxOps<
155
182
  /** Resume a paused sandbox. No-op if already running. */
156
183
  resumeSandbox(sandboxId: string): Promise<void>;
157
184
  snapshotSandbox(sandboxId: string): Promise<SandboxSnapshot>;
185
+ /** Create a fresh sandbox from a previously captured snapshot. */
186
+ restoreSandbox(snapshot: SandboxSnapshot): Promise<string>;
187
+ /** Delete a previously captured snapshot. No-op if already deleted. */
188
+ deleteSandboxSnapshot(snapshot: SandboxSnapshot): Promise<void>;
158
189
  forkSandbox(sandboxId: string): Promise<string>;
159
190
  }
160
191
 
@@ -172,7 +203,10 @@ export type PrefixedSandboxOps<
172
203
  TOptions extends SandboxCreateOptions = SandboxCreateOptions,
173
204
  TCtx = unknown,
174
205
  > = {
175
- [K in keyof SandboxOps<TOptions, TCtx> as `${TPrefix}${Capitalize<K & string>}`]: SandboxOps<TOptions, TCtx>[K];
206
+ [K in keyof SandboxOps<
207
+ TOptions,
208
+ TCtx
209
+ > as `${TPrefix}${Capitalize<K & string>}`]: SandboxOps<TOptions, TCtx>[K];
176
210
  };
177
211
 
178
212
  // ============================================================================
@@ -42,7 +42,13 @@ vi.mock("@temporalio/workflow", () => {
42
42
  uuid4: () =>
43
43
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
44
44
  ApplicationFailure: MockApplicationFailure,
45
- log: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
45
+ log: {
46
+ trace: () => {},
47
+ debug: () => {},
48
+ info: () => {},
49
+ warn: () => {},
50
+ error: () => {},
51
+ },
46
52
  };
47
53
  });
48
54
 
@@ -61,7 +67,9 @@ type TurnScript = {
61
67
  * Wraps every method on a ThreadOps object so it also has `.executeWithOptions()`,
62
68
  * matching Temporal's `ActivityInterfaceFor<ThreadOps>` shape.
63
69
  */
64
- function toActivityInterface<TContent = string>(raw: ThreadOps<TContent>): ActivityInterfaceFor<ThreadOps<TContent>> {
70
+ function toActivityInterface<TContent = string>(
71
+ raw: ThreadOps<TContent>
72
+ ): ActivityInterfaceFor<ThreadOps<TContent>> {
65
73
  const result = {} as Record<string, unknown>;
66
74
  for (const [key, fn] of Object.entries(raw)) {
67
75
  const wrapped = (...args: unknown[]) =>
@@ -433,6 +441,8 @@ describe("createSession edge cases", () => {
433
441
  createdAt: new Date().toISOString(),
434
442
  }),
435
443
  forkSandbox: async () => "forked-sandbox-id",
444
+ restoreSandbox: async () => "restored-sandbox-id",
445
+ deleteSandboxSnapshot: async () => {},
436
446
  pauseSandbox: async () => {},
437
447
  resumeSandbox: async () => {},
438
448
  };
@@ -476,6 +486,8 @@ describe("createSession edge cases", () => {
476
486
  createdAt: new Date().toISOString(),
477
487
  }),
478
488
  forkSandbox: async () => "forked-sandbox-id",
489
+ restoreSandbox: async () => "restored-sandbox-id",
490
+ deleteSandboxSnapshot: async () => {},
479
491
  pauseSandbox: async () => {},
480
492
  resumeSandbox: async () => {},
481
493
  };
@@ -765,7 +777,11 @@ describe("createSession edge cases", () => {
765
777
  },
766
778
  });
767
779
 
768
- const session = await createSession<Record<string, never>, unknown, TestContent>({
780
+ const session = await createSession<
781
+ Record<string, never>,
782
+ unknown,
783
+ TestContent
784
+ >({
769
785
  agentName: "TestAgent",
770
786
  thread: { mode: "new", threadId: "thread-1" },
771
787
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
@@ -940,6 +956,8 @@ describe("createSession edge cases", () => {
940
956
  destroySandbox: async () => {},
941
957
  snapshotSandbox: snapshotSpy,
942
958
  forkSandbox: async () => "forked-sandbox-id",
959
+ restoreSandbox: async () => "restored-sandbox-id",
960
+ deleteSandboxSnapshot: async () => {},
943
961
  pauseSandbox: async () => {},
944
962
  resumeSandbox: async () => {},
945
963
  };
@@ -1040,6 +1058,8 @@ describe("createSession edge cases", () => {
1040
1058
  createdAt: new Date().toISOString(),
1041
1059
  }),
1042
1060
  forkSandbox: async () => "forked-sb",
1061
+ restoreSandbox: async () => "restored-sb",
1062
+ deleteSandboxSnapshot: async () => {},
1043
1063
  };
1044
1064
 
1045
1065
  const session = await createSession({
@@ -1074,6 +1094,8 @@ describe("createSession edge cases", () => {
1074
1094
  createdAt: new Date().toISOString(),
1075
1095
  }),
1076
1096
  forkSandbox: async () => "forked-sb",
1097
+ restoreSandbox: async () => "restored-sb",
1098
+ deleteSandboxSnapshot: async () => {},
1077
1099
  };
1078
1100
 
1079
1101
  const session = await createSession({
@@ -1119,6 +1141,8 @@ describe("createSession edge cases", () => {
1119
1141
  createdAt: new Date().toISOString(),
1120
1142
  }),
1121
1143
  forkSandbox: async () => "forked-sb",
1144
+ restoreSandbox: async () => "restored-sb",
1145
+ deleteSandboxSnapshot: async () => {},
1122
1146
  };
1123
1147
 
1124
1148
  const session = await createSession({
@@ -1166,6 +1190,8 @@ describe("createSession edge cases", () => {
1166
1190
  sandboxLog.push(`fork:${id}`);
1167
1191
  return `forked-from-${id}`;
1168
1192
  },
1193
+ restoreSandbox: async () => "restored-sb",
1194
+ deleteSandboxSnapshot: async () => {},
1169
1195
  };
1170
1196
 
1171
1197
  const session = await createSession({
@@ -1186,7 +1212,9 @@ describe("createSession edge cases", () => {
1186
1212
 
1187
1213
  expect(sandboxLog).toContain("fork:paused-sb-1");
1188
1214
  expect(sandboxLog).not.toContain("create");
1189
- expect((result as { sandboxId?: string }).sandboxId).toBe("forked-from-paused-sb-1");
1215
+ expect((result as { sandboxId?: string }).sandboxId).toBe(
1216
+ "forked-from-paused-sb-1"
1217
+ );
1190
1218
  expect(sandboxLog).toContain("destroy:forked-from-paused-sb-1");
1191
1219
  });
1192
1220
 
@@ -1210,6 +1238,8 @@ describe("createSession edge cases", () => {
1210
1238
  createdAt: new Date().toISOString(),
1211
1239
  }),
1212
1240
  forkSandbox: async () => "forked-sb",
1241
+ restoreSandbox: async () => "restored-sb",
1242
+ deleteSandboxSnapshot: async () => {},
1213
1243
  };
1214
1244
 
1215
1245
  const session = await createSession({
@@ -1253,6 +1283,8 @@ describe("createSession edge cases", () => {
1253
1283
  createdAt: new Date().toISOString(),
1254
1284
  }),
1255
1285
  forkSandbox: async () => "forked-sb",
1286
+ restoreSandbox: async () => "restored-sb",
1287
+ deleteSandboxSnapshot: async () => {},
1256
1288
  };
1257
1289
 
1258
1290
  const session = await createSession({
@@ -1297,6 +1329,8 @@ describe("createSession edge cases", () => {
1297
1329
  createdAt: new Date().toISOString(),
1298
1330
  }),
1299
1331
  forkSandbox: async () => "forked-sb",
1332
+ restoreSandbox: async () => "restored-sb",
1333
+ deleteSandboxSnapshot: async () => {},
1300
1334
  };
1301
1335
 
1302
1336
  const session = await createSession({
@@ -1370,9 +1404,7 @@ describe("createSession edge cases", () => {
1370
1404
 
1371
1405
  const session = await createSession({
1372
1406
  agentName: "TestAgent",
1373
- runAgent: createScriptedRunAgent([
1374
- { message: "done", toolCalls: [] },
1375
- ]),
1407
+ runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
1376
1408
  threadOps: ops,
1377
1409
  buildContextMessage: () => "go",
1378
1410
  });
@@ -1413,6 +1445,8 @@ describe("createSession edge cases", () => {
1413
1445
  createdAt: new Date().toISOString(),
1414
1446
  }),
1415
1447
  forkSandbox: async () => "forked-sb",
1448
+ restoreSandbox: async () => "restored-sb",
1449
+ deleteSandboxSnapshot: async () => {},
1416
1450
  };
1417
1451
 
1418
1452
  const session = await createSession({
@@ -1431,9 +1465,7 @@ describe("createSession edge cases", () => {
1431
1465
  initialState: { systemPrompt: "test" },
1432
1466
  });
1433
1467
 
1434
- await expect(session.runSession({ stateManager })).rejects.toThrow(
1435
- "crash"
1436
- );
1468
+ await expect(session.runSession({ stateManager })).rejects.toThrow("crash");
1437
1469
 
1438
1470
  expect(sandboxLog).toContain("pause:sb-err");
1439
1471
  expect(sandboxLog).not.toContain("destroy:sb-err");
@@ -1461,6 +1493,8 @@ describe("createSession edge cases", () => {
1461
1493
  createdAt: new Date().toISOString(),
1462
1494
  }),
1463
1495
  forkSandbox: async () => "forked-sb",
1496
+ restoreSandbox: async () => "restored-sb",
1497
+ deleteSandboxSnapshot: async () => {},
1464
1498
  };
1465
1499
 
1466
1500
  const session = await createSession({
@@ -1503,6 +1537,8 @@ describe("createSession edge cases", () => {
1503
1537
  createdAt: new Date().toISOString(),
1504
1538
  }),
1505
1539
  forkSandbox: async () => "forked-sb",
1540
+ restoreSandbox: async () => "restored-sb",
1541
+ deleteSandboxSnapshot: async () => {},
1506
1542
  };
1507
1543
 
1508
1544
  const session = await createSession({
@@ -1521,9 +1557,7 @@ describe("createSession edge cases", () => {
1521
1557
  initialState: { systemPrompt: "test" },
1522
1558
  });
1523
1559
 
1524
- await expect(session.runSession({ stateManager })).rejects.toThrow(
1525
- "crash"
1526
- );
1560
+ await expect(session.runSession({ stateManager })).rejects.toThrow("crash");
1527
1561
 
1528
1562
  expect(sandboxLog).not.toContain("pause:sb-keep-err");
1529
1563
  expect(sandboxLog).not.toContain("destroy:sb-keep-err");
@@ -1553,6 +1587,8 @@ describe("createSession edge cases", () => {
1553
1587
  createdAt: new Date().toISOString(),
1554
1588
  }),
1555
1589
  forkSandbox: async () => "forked-sb",
1590
+ restoreSandbox: async () => "restored-sb",
1591
+ deleteSandboxSnapshot: async () => {},
1556
1592
  };
1557
1593
 
1558
1594
  const session = await createSession({
@@ -1598,6 +1634,8 @@ describe("createSession edge cases", () => {
1598
1634
  createdAt: new Date().toISOString(),
1599
1635
  }),
1600
1636
  forkSandbox: async () => "forked-sb",
1637
+ restoreSandbox: async () => "restored-sb",
1638
+ deleteSandboxSnapshot: async () => {},
1601
1639
  };
1602
1640
 
1603
1641
  const session = await createSession({
@@ -517,6 +517,8 @@ describe("createSession integration", () => {
517
517
  createdAt: new Date().toISOString(),
518
518
  }),
519
519
  forkSandbox: async () => "forked-sandbox-id",
520
+ restoreSandbox: async () => "restored-sandbox-id",
521
+ deleteSandboxSnapshot: async () => {},
520
522
  pauseSandbox: async () => {},
521
523
  resumeSandbox: async () => {},
522
524
  };
@@ -559,6 +561,8 @@ describe("createSession integration", () => {
559
561
  createdAt: new Date().toISOString(),
560
562
  }),
561
563
  forkSandbox: async () => "forked-sandbox-id",
564
+ restoreSandbox: async () => "restored-sandbox-id",
565
+ deleteSandboxSnapshot: async () => {},
562
566
  pauseSandbox: async () => {},
563
567
  resumeSandbox: async () => {},
564
568
  };
@@ -610,6 +614,8 @@ describe("createSession integration", () => {
610
614
  createdAt: new Date().toISOString(),
611
615
  }),
612
616
  forkSandbox: async () => "forked-sb",
617
+ restoreSandbox: async () => "restored-sb",
618
+ deleteSandboxSnapshot: async () => {},
613
619
  };
614
620
 
615
621
  const session = await createSession({
@@ -822,6 +828,8 @@ describe("createSession integration", () => {
822
828
  createdAt: new Date().toISOString(),
823
829
  }),
824
830
  forkSandbox: async () => "forked-sandbox-id",
831
+ restoreSandbox: async () => "restored-sandbox-id",
832
+ deleteSandboxSnapshot: async () => {},
825
833
  pauseSandbox: async () => {},
826
834
  resumeSandbox: async () => {},
827
835
  };
@@ -873,6 +881,8 @@ describe("createSession integration", () => {
873
881
  createdAt: new Date().toISOString(),
874
882
  }),
875
883
  forkSandbox: async () => "forked-sandbox-id",
884
+ restoreSandbox: async () => "restored-sandbox-id",
885
+ deleteSandboxSnapshot: async () => {},
876
886
  pauseSandbox: async () => {},
877
887
  resumeSandbox: async () => {},
878
888
  };
@@ -949,4 +959,133 @@ describe("createSession integration", () => {
949
959
  expect(result.usage.totalInputTokens).toBe(180);
950
960
  expect(result.usage.totalOutputTokens).toBe(90);
951
961
  });
962
+
963
+ // --- Snapshot-driven shutdown ---
964
+
965
+ it("captures base + exit snapshot and destroys sandbox on sandboxShutdown=snapshot", async () => {
966
+ const { ops } = createMockThreadOps();
967
+ const sandboxLog: string[] = [];
968
+ let snapCounter = 0;
969
+
970
+ const sandboxOps: SandboxOps = {
971
+ createSandbox: async () => {
972
+ sandboxLog.push("create");
973
+ return { sandboxId: "sb-snap" };
974
+ },
975
+ destroySandbox: async (id: string) => {
976
+ sandboxLog.push(`destroy:${id}`);
977
+ },
978
+ pauseSandbox: async () => {
979
+ sandboxLog.push("pause");
980
+ },
981
+ resumeSandbox: async () => {},
982
+ snapshotSandbox: async (id: string) => {
983
+ snapCounter += 1;
984
+ sandboxLog.push(`snapshot:${id}`);
985
+ return {
986
+ sandboxId: id,
987
+ providerId: "test",
988
+ data: { tag: `snap-${snapCounter}` },
989
+ createdAt: new Date().toISOString(),
990
+ };
991
+ },
992
+ restoreSandbox: async () => "restored-sb",
993
+ deleteSandboxSnapshot: async () => {},
994
+ forkSandbox: async () => "forked-sb",
995
+ };
996
+
997
+ const session = await createSession({
998
+ agentName: "TestAgent",
999
+ thread: { mode: "new", threadId: "thread-snap" },
1000
+ runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
1001
+ threadOps: ops,
1002
+ buildContextMessage: () => "go",
1003
+ sandboxOps,
1004
+ sandboxShutdown: "snapshot",
1005
+ });
1006
+
1007
+ const stateManager = createAgentStateManager({
1008
+ initialState: { systemPrompt: "test" },
1009
+ });
1010
+
1011
+ const result = await session.runSession({ stateManager });
1012
+
1013
+ expect(result.exitReason).toBe("completed");
1014
+ expect(result.sandboxId).toBe("sb-snap");
1015
+ expect(result.baseSnapshot?.data).toEqual({ tag: "snap-1" });
1016
+ expect(result.snapshot?.data).toEqual({ tag: "snap-2" });
1017
+ expect(sandboxLog).toEqual([
1018
+ "create",
1019
+ "snapshot:sb-snap",
1020
+ "snapshot:sb-snap",
1021
+ "destroy:sb-snap",
1022
+ ]);
1023
+ expect(sandboxLog).not.toContain("pause");
1024
+ });
1025
+
1026
+ it("restores a sandbox when sandbox.mode=from-snapshot and skips base snapshot", async () => {
1027
+ const { ops } = createMockThreadOps();
1028
+ const sandboxLog: string[] = [];
1029
+ const priorSnapshot = {
1030
+ sandboxId: "sb-prior",
1031
+ providerId: "test",
1032
+ data: { tag: "prior" },
1033
+ createdAt: new Date().toISOString(),
1034
+ };
1035
+
1036
+ const sandboxOps: SandboxOps = {
1037
+ createSandbox: async () => {
1038
+ sandboxLog.push("create");
1039
+ return { sandboxId: "sb-should-not-be-created" };
1040
+ },
1041
+ destroySandbox: async (id: string) => {
1042
+ sandboxLog.push(`destroy:${id}`);
1043
+ },
1044
+ pauseSandbox: async () => {},
1045
+ resumeSandbox: async () => {},
1046
+ snapshotSandbox: async (id: string) => {
1047
+ sandboxLog.push(`snapshot:${id}`);
1048
+ return {
1049
+ sandboxId: id,
1050
+ providerId: "test",
1051
+ data: { tag: "exit" },
1052
+ createdAt: new Date().toISOString(),
1053
+ };
1054
+ },
1055
+ restoreSandbox: async (snap) => {
1056
+ sandboxLog.push(`restore:${(snap.data as { tag: string }).tag}`);
1057
+ return "sb-restored";
1058
+ },
1059
+ deleteSandboxSnapshot: async () => {},
1060
+ forkSandbox: async () => "forked-sb",
1061
+ };
1062
+
1063
+ const session = await createSession({
1064
+ agentName: "TestAgent",
1065
+ thread: { mode: "new", threadId: "thread-restore" },
1066
+ runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
1067
+ threadOps: ops,
1068
+ buildContextMessage: () => "go",
1069
+ sandboxOps,
1070
+ sandbox: { mode: "from-snapshot", snapshot: priorSnapshot },
1071
+ sandboxShutdown: "snapshot",
1072
+ });
1073
+
1074
+ const stateManager = createAgentStateManager({
1075
+ initialState: { systemPrompt: "test" },
1076
+ });
1077
+
1078
+ const result = await session.runSession({ stateManager });
1079
+
1080
+ expect(result.sandboxId).toBe("sb-restored");
1081
+ // No base snapshot because the sandbox was restored, not freshly created.
1082
+ expect(result.baseSnapshot).toBeUndefined();
1083
+ expect(result.snapshot?.data).toEqual({ tag: "exit" });
1084
+ expect(sandboxLog).toEqual([
1085
+ "restore:prior",
1086
+ "snapshot:sb-restored",
1087
+ "destroy:sb-restored",
1088
+ ]);
1089
+ expect(sandboxLog).not.toContain("create");
1090
+ });
952
1091
  });