zeitlich 0.2.36 → 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 (200) 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 +8 -0
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +2 -1
  19. package/dist/adapters/sandbox/daytona/index.d.ts +2 -1
  20. package/dist/adapters/sandbox/daytona/index.js +8 -0
  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/index.ts +6 -0
  121. package/src/adapters/sandbox/daytona/proxy.ts +2 -0
  122. package/src/adapters/sandbox/e2b/filesystem.ts +5 -4
  123. package/src/adapters/sandbox/e2b/index.ts +63 -12
  124. package/src/adapters/sandbox/e2b/proxy.ts +2 -0
  125. package/src/adapters/sandbox/inmemory/index.ts +5 -0
  126. package/src/adapters/sandbox/inmemory/proxy.ts +2 -0
  127. package/src/adapters/thread/anthropic/activities.ts +49 -26
  128. package/src/adapters/thread/anthropic/model-invoker.ts +15 -6
  129. package/src/adapters/thread/anthropic/proxy.ts +6 -2
  130. package/src/adapters/thread/anthropic/thread-manager.test.ts +26 -7
  131. package/src/adapters/thread/anthropic/thread-manager.ts +60 -46
  132. package/src/adapters/thread/google-genai/activities.ts +7 -2
  133. package/src/adapters/thread/google-genai/model-invoker.ts +26 -8
  134. package/src/adapters/thread/google-genai/proxy.ts +6 -2
  135. package/src/adapters/thread/google-genai/thread-manager.test.ts +13 -3
  136. package/src/adapters/thread/google-genai/thread-manager.ts +54 -33
  137. package/src/adapters/thread/langchain/activities.ts +46 -24
  138. package/src/adapters/thread/langchain/hooks.test.ts +36 -49
  139. package/src/adapters/thread/langchain/hooks.ts +18 -5
  140. package/src/adapters/thread/langchain/model-invoker.ts +3 -3
  141. package/src/adapters/thread/langchain/proxy.ts +6 -2
  142. package/src/adapters/thread/langchain/thread-manager.test.ts +5 -1
  143. package/src/adapters/thread/langchain/thread-manager.ts +20 -9
  144. package/src/index.ts +4 -1
  145. package/src/lib/activity.ts +16 -6
  146. package/src/lib/hooks/types.ts +6 -6
  147. package/src/lib/lifecycle.ts +9 -1
  148. package/src/lib/model/proxy.ts +2 -2
  149. package/src/lib/observability/hooks.ts +4 -5
  150. package/src/lib/observability/index.ts +1 -4
  151. package/src/lib/sandbox/manager.ts +21 -4
  152. package/src/lib/sandbox/node-fs.ts +3 -6
  153. package/src/lib/sandbox/sandbox.test.ts +36 -3
  154. package/src/lib/sandbox/tree.integration.test.ts +10 -3
  155. package/src/lib/sandbox/types.ts +35 -1
  156. package/src/lib/session/session-edge-cases.integration.test.ts +51 -13
  157. package/src/lib/session/session.integration.test.ts +139 -0
  158. package/src/lib/session/session.ts +50 -19
  159. package/src/lib/session/types.ts +13 -5
  160. package/src/lib/skills/fs-provider.ts +12 -8
  161. package/src/lib/skills/handler.ts +1 -1
  162. package/src/lib/skills/parse.ts +3 -1
  163. package/src/lib/skills/register.ts +1 -3
  164. package/src/lib/skills/skills.integration.test.ts +25 -15
  165. package/src/lib/state/manager.integration.test.ts +12 -2
  166. package/src/lib/subagent/define.ts +1 -1
  167. package/src/lib/subagent/handler.ts +186 -71
  168. package/src/lib/subagent/index.ts +1 -5
  169. package/src/lib/subagent/register.ts +3 -2
  170. package/src/lib/subagent/signals.ts +1 -10
  171. package/src/lib/subagent/subagent.integration.test.ts +438 -156
  172. package/src/lib/subagent/tool.ts +4 -3
  173. package/src/lib/subagent/types.ts +50 -20
  174. package/src/lib/subagent/workflow.ts +9 -49
  175. package/src/lib/thread/id.test.ts +1 -1
  176. package/src/lib/thread/id.ts +1 -2
  177. package/src/lib/thread/proxy.ts +3 -4
  178. package/src/lib/thread/types.ts +11 -3
  179. package/src/lib/tool-router/index.ts +1 -5
  180. package/src/lib/tool-router/router-edge-cases.integration.test.ts +1 -1
  181. package/src/lib/tool-router/router.ts +3 -2
  182. package/src/lib/tool-router/types.ts +11 -3
  183. package/src/lib/tool-router/with-sandbox.ts +19 -5
  184. package/src/lib/virtual-fs/filesystem.ts +1 -1
  185. package/src/lib/virtual-fs/index.ts +5 -1
  186. package/src/lib/virtual-fs/mutations.ts +2 -4
  187. package/src/lib/virtual-fs/queries.ts +9 -5
  188. package/src/lib/virtual-fs/types.ts +4 -1
  189. package/src/lib/virtual-fs/virtual-fs.test.ts +9 -11
  190. package/src/lib/workflow.test.ts +7 -4
  191. package/src/lib/workflow.ts +1 -5
  192. package/src/tools/ask-user-question/tool.ts +1 -3
  193. package/src/tools/glob/handler.ts +1 -4
  194. package/src/tools/task-get/handler.ts +4 -5
  195. package/src/tools/task-list/handler.ts +1 -4
  196. package/src/tools/task-update/handler.ts +4 -5
  197. package/src/workflow.ts +20 -7
  198. package/tsup.config.ts +9 -6
  199. package/src/lib/.env +0 -1
  200. package/src/tools/bash/.env +0 -1
@@ -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
  });
@@ -7,7 +7,7 @@ import {
7
7
  } from "@temporalio/workflow";
8
8
  import type { SessionExitReason } from "../types";
9
9
  import type { SessionConfig, ZeitlichSession } from "./types";
10
- import type { SandboxOps } from "../sandbox/types";
10
+ import type { SandboxOps, SandboxSnapshot } from "../sandbox/types";
11
11
  import type {
12
12
  AgentState,
13
13
  AgentStateManager,
@@ -146,12 +146,14 @@ export async function createSession<
146
146
 
147
147
  const plugins: ToolMap[string][] = [];
148
148
  let destroySubagentSandboxes: (() => Promise<void>) | undefined;
149
+ let cleanupSubagentSnapshots: (() => Promise<void>) | undefined;
149
150
 
150
151
  if (subagents) {
151
152
  const result = buildSubagentRegistration(subagents);
152
153
  if (result) {
153
154
  plugins.push(result.registration);
154
155
  destroySubagentSandboxes = result.destroySubagentSandboxes;
156
+ cleanupSubagentSnapshots = result.cleanupSubagentSnapshots;
155
157
  }
156
158
  }
157
159
  if (skills) {
@@ -210,10 +212,13 @@ export async function createSession<
210
212
  }
211
213
  );
212
214
 
213
- // --- Sandbox lifecycle: create, continue, fork, or inherit ----------
215
+ // --- Sandbox lifecycle: create, continue, fork, from-snapshot, or inherit ---
214
216
  const sandboxMode = sandboxInit?.mode;
215
217
  let sandboxId: string | undefined;
216
218
  let sandboxOwned = false;
219
+ let baseSnapshot: SandboxSnapshot | undefined;
220
+ let exitSnapshot: SandboxSnapshot | undefined;
221
+ let freshlyCreated = false;
217
222
 
218
223
  if (sandboxMode === "inherit") {
219
224
  const inheritInit = sandboxInit as {
@@ -252,6 +257,18 @@ export async function createSession<
252
257
  (sandboxInit as { mode: "fork"; sandboxId: string }).sandboxId
253
258
  );
254
259
  sandboxOwned = true;
260
+ } else if (sandboxMode === "from-snapshot") {
261
+ if (!sandboxOps) {
262
+ throw ApplicationFailure.create({
263
+ message: "No sandboxOps provided — cannot restore sandbox",
264
+ nonRetryable: true,
265
+ });
266
+ }
267
+ const snap = (
268
+ sandboxInit as { mode: "from-snapshot"; snapshot: SandboxSnapshot }
269
+ ).snapshot;
270
+ sandboxId = await sandboxOps.restoreSandbox(snap);
271
+ sandboxOwned = true;
255
272
  } else if (sandboxOps) {
256
273
  const skillFiles = skills ? collectSkillFiles(skills) : undefined;
257
274
  const ctx = (sandboxInit as { mode: "new"; ctx?: unknown } | undefined)
@@ -263,10 +280,24 @@ export async function createSession<
263
280
  if (result) {
264
281
  sandboxId = result.sandboxId;
265
282
  sandboxOwned = true;
283
+ freshlyCreated = true;
266
284
  }
267
285
  }
268
286
 
269
- if (sandboxId && onSandboxReady) {
287
+ // Capture a base snapshot immediately after seeding so it can be reused
288
+ // as a template for future runs that want to skip the (potentially
289
+ // expensive) seed step.
290
+ if (
291
+ sandboxId &&
292
+ sandboxOwned &&
293
+ freshlyCreated &&
294
+ sandboxShutdown === "snapshot" &&
295
+ sandboxOps
296
+ ) {
297
+ baseSnapshot = await sandboxOps.snapshotSandbox(sandboxId);
298
+ }
299
+
300
+ if (sandboxId && sandboxOwned && onSandboxReady) {
270
301
  onSandboxReady(sandboxId);
271
302
  }
272
303
 
@@ -347,6 +378,7 @@ export async function createSession<
347
378
  );
348
379
 
349
380
  let exitReason: SessionExitReason = "completed";
381
+ let finalMessage: M | null = null;
350
382
 
351
383
  try {
352
384
  while (
@@ -385,21 +417,8 @@ export async function createSession<
385
417
  if (!toolRouter.hasTools() || rawToolCalls.length === 0) {
386
418
  stateManager.complete();
387
419
  exitReason = "completed";
388
- log.info("session ended", {
389
- agentName,
390
- threadId,
391
- exitReason,
392
- turns: currentTurn,
393
- durationMs: Date.now() - sessionStartMs,
394
- usage: stateManager.getTotalUsage(),
395
- });
396
- return {
397
- threadId,
398
- finalMessage: message,
399
- exitReason,
400
- usage: stateManager.getTotalUsage(),
401
- sandboxId,
402
- } as Awaited<ReturnType<ZeitlichSession<M, boolean>["runSession"]>>;
420
+ finalMessage = message;
421
+ break;
403
422
  }
404
423
 
405
424
  const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
@@ -480,12 +499,20 @@ export async function createSession<
480
499
  case "keep":
481
500
  case "keep-until-parent-close":
482
501
  break;
502
+ case "snapshot":
503
+ exitSnapshot = await sandboxOps.snapshotSandbox(sandboxId);
504
+ await sandboxOps.destroySandbox(sandboxId);
505
+ break;
483
506
  }
484
507
  }
485
508
 
486
509
  if (destroySubagentSandboxes) {
487
510
  await destroySubagentSandboxes();
488
511
  }
512
+
513
+ if (cleanupSubagentSnapshots) {
514
+ await cleanupSubagentSnapshots();
515
+ }
489
516
  }
490
517
 
491
518
  log.info("session ended", {
@@ -495,14 +522,18 @@ export async function createSession<
495
522
  turns: stateManager.getTurns(),
496
523
  durationMs: Date.now() - sessionStartMs,
497
524
  usage: stateManager.getTotalUsage(),
525
+ ...(baseSnapshot && { hasBaseSnapshot: true }),
526
+ ...(exitSnapshot && { hasExitSnapshot: true }),
498
527
  });
499
528
 
500
529
  return {
501
530
  threadId,
502
- finalMessage: null,
531
+ finalMessage,
503
532
  exitReason,
504
533
  usage: stateManager.getTotalUsage(),
505
534
  sandboxId,
535
+ ...(baseSnapshot && { baseSnapshot }),
536
+ ...(exitSnapshot && { snapshot: exitSnapshot }),
506
537
  } as Awaited<ReturnType<ZeitlichSession<M, boolean>["runSession"]>>;
507
538
  },
508
539
  };
@@ -1,8 +1,5 @@
1
1
  import type { Duration } from "@temporalio/common";
2
- import type {
3
- SessionExitReason,
4
- ToolResultConfig,
5
- } from "../types";
2
+ import type { SessionExitReason, ToolResultConfig } from "../types";
6
3
  import type {
7
4
  ToolMap,
8
5
  ToolCallResultUnion,
@@ -11,7 +8,7 @@ import type {
11
8
  import type { Hooks } from "../hooks/types";
12
9
  import type { SubagentConfig } from "../subagent/types";
13
10
  import type { Skill } from "../skills/types";
14
- import type { SandboxOps } from "../sandbox/types";
11
+ import type { SandboxOps, SandboxSnapshot } from "../sandbox/types";
15
12
  import type { VirtualFsOps } from "../virtual-fs/types";
16
13
  import type { RunAgentActivity } from "../model/types";
17
14
  import type { AgentStateManager, JsonSerializable } from "../state/types";
@@ -219,6 +216,17 @@ export type SessionResult<
219
216
  finalMessage: M | null;
220
217
  exitReason: SessionExitReason;
221
218
  usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
219
+ /**
220
+ * Snapshot captured on exit when `sandboxShutdown === "snapshot"`.
221
+ */
222
+ snapshot?: SandboxSnapshot;
223
+ /**
224
+ * Snapshot captured immediately after sandbox seeding (before the agent
225
+ * loop starts) when `sandbox.mode === "new"` and
226
+ * `sandboxShutdown === "snapshot"`. Intended as a reusable "base" for new
227
+ * threads that want to skip re-seeding.
228
+ */
229
+ baseSnapshot?: SandboxSnapshot;
222
230
  } & (HasSandbox extends true
223
231
  ? { sandboxId: string }
224
232
  : { sandboxId?: undefined });