zeitlich 0.2.36 → 0.2.38

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 (204) hide show
  1. package/README.md +146 -92
  2. package/dist/{activities-BVI2lTwr.d.ts → activities-BKhMtKDd.d.ts} +4 -2
  3. package/dist/{activities-hd4aNnZE.d.cts → activities-CDcwkRZs.d.cts} +4 -2
  4. package/dist/adapters/sandbox/bedrock/index.cjs +17 -14
  5. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/bedrock/index.d.cts +7 -6
  7. package/dist/adapters/sandbox/bedrock/index.d.ts +7 -6
  8. package/dist/adapters/sandbox/bedrock/index.js +17 -14
  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 +11 -3
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +5 -4
  19. package/dist/adapters/sandbox/daytona/index.d.ts +5 -4
  20. package/dist/adapters/sandbox/daytona/index.js +11 -3
  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 +73 -12
  29. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  30. package/dist/adapters/sandbox/e2b/index.d.cts +26 -4
  31. package/dist/adapters/sandbox/e2b/index.d.ts +26 -4
  32. package/dist/adapters/sandbox/e2b/index.js +73 -12
  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 +8 -3
  41. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  42. package/dist/adapters/sandbox/inmemory/index.d.cts +5 -4
  43. package/dist/adapters/sandbox/inmemory/index.d.ts +5 -4
  44. package/dist/adapters/sandbox/inmemory/index.js +8 -3
  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 +94 -39
  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 +94 -39
  57. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  58. package/dist/adapters/thread/anthropic/workflow.cjs +7 -2
  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 +7 -2
  63. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  64. package/dist/adapters/thread/google-genai/index.cjs +77 -28
  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 +77 -28
  69. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  70. package/dist/adapters/thread/google-genai/workflow.cjs +7 -2
  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 +7 -2
  75. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  76. package/dist/adapters/thread/langchain/index.cjs +57 -10
  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 +57 -10
  81. package/dist/adapters/thread/langchain/index.js.map +1 -1
  82. package/dist/adapters/thread/langchain/workflow.cjs +7 -2
  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 +7 -2
  87. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  88. package/dist/index.cjs +322 -146
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +20 -14
  91. package/dist/index.d.ts +20 -14
  92. package/dist/index.js +323 -147
  93. package/dist/index.js.map +1 -1
  94. package/dist/{proxy-BjdFGPTm.d.ts → proxy-CUlKSvZS.d.ts} +1 -1
  95. package/dist/{proxy-7RnVaPdJ.d.cts → proxy-D_3x7RN4.d.cts} +1 -1
  96. package/dist/{thread-manager-CbpiGq1L.d.ts → thread-manager-CVu7o2cs.d.ts} +4 -2
  97. package/dist/{thread-manager-DzXm9eeI.d.cts → thread-manager-HSwyh28L.d.cts} +4 -2
  98. package/dist/{thread-manager-BBzNgQWH.d.cts → thread-manager-c1gPopAG.d.ts} +4 -2
  99. package/dist/{thread-manager-DjN5JYul.d.ts → thread-manager-wGi-LqIP.d.cts} +4 -2
  100. package/dist/{types-Mc_4BCfT.d.cts → types-BH_IRryz.d.ts} +10 -1
  101. package/dist/{types-yiXmqedU.d.ts → types-BaOw4hKI.d.cts} +10 -1
  102. package/dist/{types-DQ1l_gXL.d.cts → types-C06FwR96.d.cts} +121 -17
  103. package/dist/{types-wiGLvxWf.d.ts → types-DAsQ21Rt.d.ts} +1 -1
  104. package/dist/{types-CADc5V_P.d.ts → types-DNr31FzL.d.ts} +121 -17
  105. package/dist/{types-CBH54cwr.d.cts → types-lm8tMNJQ.d.cts} +1 -1
  106. package/dist/{types-DxCpFNv_.d.cts → types-yx0LzPGn.d.cts} +44 -5
  107. package/dist/{types-DxCpFNv_.d.ts → types-yx0LzPGn.d.ts} +44 -5
  108. package/dist/{workflow-DhtWRovz.d.cts → workflow-CSCkpwAL.d.ts} +2 -2
  109. package/dist/{workflow-P2pTSfKu.d.ts → workflow-DuvMZ8Vm.d.cts} +2 -2
  110. package/dist/workflow.cjs +274 -130
  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 +275 -131
  115. package/dist/workflow.js.map +1 -1
  116. package/package.json +2 -2
  117. package/src/adapters/sandbox/bedrock/filesystem.ts +6 -12
  118. package/src/adapters/sandbox/bedrock/index.ts +22 -11
  119. package/src/adapters/sandbox/bedrock/proxy.ts +2 -0
  120. package/src/adapters/sandbox/daytona/index.ts +18 -3
  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 +87 -14
  124. package/src/adapters/sandbox/e2b/proxy.ts +2 -0
  125. package/src/adapters/sandbox/e2b/types.ts +16 -0
  126. package/src/adapters/sandbox/inmemory/index.ts +17 -3
  127. package/src/adapters/sandbox/inmemory/proxy.ts +2 -0
  128. package/src/adapters/thread/anthropic/activities.ts +58 -26
  129. package/src/adapters/thread/anthropic/model-invoker.ts +18 -7
  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 +63 -46
  133. package/src/adapters/thread/google-genai/activities.ts +20 -2
  134. package/src/adapters/thread/google-genai/model-invoker.ts +27 -7
  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 +57 -33
  138. package/src/adapters/thread/langchain/activities.ts +55 -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 +5 -4
  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 +23 -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 +18 -3
  149. package/src/lib/model/proxy.ts +2 -2
  150. package/src/lib/model/types.ts +10 -0
  151. package/src/lib/observability/hooks.ts +4 -5
  152. package/src/lib/observability/index.ts +1 -4
  153. package/src/lib/sandbox/manager.ts +45 -20
  154. package/src/lib/sandbox/node-fs.ts +3 -6
  155. package/src/lib/sandbox/sandbox.test.ts +36 -3
  156. package/src/lib/sandbox/tree.integration.test.ts +10 -3
  157. package/src/lib/sandbox/types.ts +60 -6
  158. package/src/lib/session/session-edge-cases.integration.test.ts +316 -14
  159. package/src/lib/session/session.integration.test.ts +161 -1
  160. package/src/lib/session/session.ts +106 -21
  161. package/src/lib/session/types.ts +25 -5
  162. package/src/lib/skills/fs-provider.ts +12 -8
  163. package/src/lib/skills/handler.ts +1 -1
  164. package/src/lib/skills/parse.ts +3 -1
  165. package/src/lib/skills/register.ts +1 -3
  166. package/src/lib/skills/skills.integration.test.ts +25 -15
  167. package/src/lib/state/manager.integration.test.ts +12 -2
  168. package/src/lib/subagent/define.ts +1 -1
  169. package/src/lib/subagent/handler.ts +186 -71
  170. package/src/lib/subagent/index.ts +1 -5
  171. package/src/lib/subagent/register.ts +3 -2
  172. package/src/lib/subagent/signals.ts +1 -10
  173. package/src/lib/subagent/subagent.integration.test.ts +526 -248
  174. package/src/lib/subagent/tool.ts +4 -3
  175. package/src/lib/subagent/types.ts +50 -20
  176. package/src/lib/subagent/workflow.ts +9 -49
  177. package/src/lib/thread/id.test.ts +1 -1
  178. package/src/lib/thread/id.ts +1 -2
  179. package/src/lib/thread/manager.ts +18 -0
  180. package/src/lib/thread/proxy.ts +4 -4
  181. package/src/lib/thread/types.ts +20 -3
  182. package/src/lib/tool-router/index.ts +3 -5
  183. package/src/lib/tool-router/router-edge-cases.integration.test.ts +93 -1
  184. package/src/lib/tool-router/router.integration.test.ts +12 -0
  185. package/src/lib/tool-router/router.ts +90 -16
  186. package/src/lib/tool-router/types.ts +45 -4
  187. package/src/lib/tool-router/with-sandbox.ts +19 -5
  188. package/src/lib/virtual-fs/filesystem.ts +1 -1
  189. package/src/lib/virtual-fs/index.ts +5 -1
  190. package/src/lib/virtual-fs/mutations.ts +2 -4
  191. package/src/lib/virtual-fs/queries.ts +9 -5
  192. package/src/lib/virtual-fs/types.ts +4 -1
  193. package/src/lib/virtual-fs/virtual-fs.test.ts +9 -11
  194. package/src/lib/workflow.test.ts +7 -4
  195. package/src/lib/workflow.ts +1 -5
  196. package/src/tools/ask-user-question/tool.ts +1 -3
  197. package/src/tools/glob/handler.ts +1 -4
  198. package/src/tools/task-get/handler.ts +4 -5
  199. package/src/tools/task-list/handler.ts +1 -4
  200. package/src/tools/task-update/handler.ts +4 -5
  201. package/src/workflow.ts +22 -7
  202. package/tsup.config.ts +9 -6
  203. package/src/lib/.env +0 -1
  204. package/src/tools/bash/.env +0 -1
@@ -33,6 +33,16 @@ vi.mock("@temporalio/workflow", () => {
33
33
  }
34
34
  }
35
35
 
36
+ class MockCancellationScope {
37
+ cancellable: boolean;
38
+ constructor(opts?: { cancellable?: boolean }) {
39
+ this.cancellable = opts?.cancellable ?? true;
40
+ }
41
+ async run<T>(fn: () => Promise<T>): Promise<T> {
42
+ return fn();
43
+ }
44
+ cancel(): void {}
45
+ }
36
46
  return {
37
47
  proxyActivities: <T>() => ({}) as T,
38
48
  condition: async (fn: () => boolean) => fn(),
@@ -46,6 +56,8 @@ vi.mock("@temporalio/workflow", () => {
46
56
  uuid4: () =>
47
57
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
48
58
  ApplicationFailure: MockApplicationFailure,
59
+ CancellationScope: MockCancellationScope,
60
+ isCancellation: (_err: unknown) => false,
49
61
  log: {
50
62
  trace: () => {},
51
63
  debug: () => {},
@@ -105,6 +117,9 @@ function createMockThreadOps() {
105
117
  forkThread: async (source, target) => {
106
118
  log.push({ op: "forkThread", args: [source, target] });
107
119
  },
120
+ truncateThread: async (threadId, length) => {
121
+ log.push({ op: "truncateThread", args: [threadId, length] });
122
+ },
108
123
  });
109
124
 
110
125
  return { ops, log };
@@ -123,12 +138,18 @@ function createScriptedRunAgent(
123
138
  return async () => {
124
139
  const turn = turns[call++];
125
140
  if (!turn) {
126
- return { message: "done", rawToolCalls: [], usage: undefined };
141
+ return {
142
+ message: "done",
143
+ rawToolCalls: [],
144
+ usage: undefined,
145
+ threadLengthAtCall: 0,
146
+ };
127
147
  }
128
148
  return {
129
149
  message: turn.message,
130
150
  rawToolCalls: turn.toolCalls,
131
151
  usage: turn.usage,
152
+ threadLengthAtCall: 0,
132
153
  };
133
154
  };
134
155
  }
@@ -517,6 +538,8 @@ describe("createSession integration", () => {
517
538
  createdAt: new Date().toISOString(),
518
539
  }),
519
540
  forkSandbox: async () => "forked-sandbox-id",
541
+ restoreSandbox: async () => "restored-sandbox-id",
542
+ deleteSandboxSnapshot: async () => {},
520
543
  pauseSandbox: async () => {},
521
544
  resumeSandbox: async () => {},
522
545
  };
@@ -559,6 +582,8 @@ describe("createSession integration", () => {
559
582
  createdAt: new Date().toISOString(),
560
583
  }),
561
584
  forkSandbox: async () => "forked-sandbox-id",
585
+ restoreSandbox: async () => "restored-sandbox-id",
586
+ deleteSandboxSnapshot: async () => {},
562
587
  pauseSandbox: async () => {},
563
588
  resumeSandbox: async () => {},
564
589
  };
@@ -610,6 +635,8 @@ describe("createSession integration", () => {
610
635
  createdAt: new Date().toISOString(),
611
636
  }),
612
637
  forkSandbox: async () => "forked-sb",
638
+ restoreSandbox: async () => "restored-sb",
639
+ deleteSandboxSnapshot: async () => {},
613
640
  };
614
641
 
615
642
  const session = await createSession({
@@ -822,6 +849,8 @@ describe("createSession integration", () => {
822
849
  createdAt: new Date().toISOString(),
823
850
  }),
824
851
  forkSandbox: async () => "forked-sandbox-id",
852
+ restoreSandbox: async () => "restored-sandbox-id",
853
+ deleteSandboxSnapshot: async () => {},
825
854
  pauseSandbox: async () => {},
826
855
  resumeSandbox: async () => {},
827
856
  };
@@ -873,6 +902,8 @@ describe("createSession integration", () => {
873
902
  createdAt: new Date().toISOString(),
874
903
  }),
875
904
  forkSandbox: async () => "forked-sandbox-id",
905
+ restoreSandbox: async () => "restored-sandbox-id",
906
+ deleteSandboxSnapshot: async () => {},
876
907
  pauseSandbox: async () => {},
877
908
  resumeSandbox: async () => {},
878
909
  };
@@ -949,4 +980,133 @@ describe("createSession integration", () => {
949
980
  expect(result.usage.totalInputTokens).toBe(180);
950
981
  expect(result.usage.totalOutputTokens).toBe(90);
951
982
  });
983
+
984
+ // --- Snapshot-driven shutdown ---
985
+
986
+ it("captures base + exit snapshot and destroys sandbox on sandboxShutdown=snapshot", async () => {
987
+ const { ops } = createMockThreadOps();
988
+ const sandboxLog: string[] = [];
989
+ let snapCounter = 0;
990
+
991
+ const sandboxOps: SandboxOps = {
992
+ createSandbox: async () => {
993
+ sandboxLog.push("create");
994
+ return { sandboxId: "sb-snap" };
995
+ },
996
+ destroySandbox: async (id: string) => {
997
+ sandboxLog.push(`destroy:${id}`);
998
+ },
999
+ pauseSandbox: async () => {
1000
+ sandboxLog.push("pause");
1001
+ },
1002
+ resumeSandbox: async () => {},
1003
+ snapshotSandbox: async (id: string) => {
1004
+ snapCounter += 1;
1005
+ sandboxLog.push(`snapshot:${id}`);
1006
+ return {
1007
+ sandboxId: id,
1008
+ providerId: "test",
1009
+ data: { tag: `snap-${snapCounter}` },
1010
+ createdAt: new Date().toISOString(),
1011
+ };
1012
+ },
1013
+ restoreSandbox: async () => "restored-sb",
1014
+ deleteSandboxSnapshot: async () => {},
1015
+ forkSandbox: async () => "forked-sb",
1016
+ };
1017
+
1018
+ const session = await createSession({
1019
+ agentName: "TestAgent",
1020
+ thread: { mode: "new", threadId: "thread-snap" },
1021
+ runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
1022
+ threadOps: ops,
1023
+ buildContextMessage: () => "go",
1024
+ sandboxOps,
1025
+ sandboxShutdown: "snapshot",
1026
+ });
1027
+
1028
+ const stateManager = createAgentStateManager({
1029
+ initialState: { systemPrompt: "test" },
1030
+ });
1031
+
1032
+ const result = await session.runSession({ stateManager });
1033
+
1034
+ expect(result.exitReason).toBe("completed");
1035
+ expect(result.sandboxId).toBe("sb-snap");
1036
+ expect(result.baseSnapshot?.data).toEqual({ tag: "snap-1" });
1037
+ expect(result.snapshot?.data).toEqual({ tag: "snap-2" });
1038
+ expect(sandboxLog).toEqual([
1039
+ "create",
1040
+ "snapshot:sb-snap",
1041
+ "snapshot:sb-snap",
1042
+ "destroy:sb-snap",
1043
+ ]);
1044
+ expect(sandboxLog).not.toContain("pause");
1045
+ });
1046
+
1047
+ it("restores a sandbox when sandbox.mode=from-snapshot and skips base snapshot", async () => {
1048
+ const { ops } = createMockThreadOps();
1049
+ const sandboxLog: string[] = [];
1050
+ const priorSnapshot = {
1051
+ sandboxId: "sb-prior",
1052
+ providerId: "test",
1053
+ data: { tag: "prior" },
1054
+ createdAt: new Date().toISOString(),
1055
+ };
1056
+
1057
+ const sandboxOps: SandboxOps = {
1058
+ createSandbox: async () => {
1059
+ sandboxLog.push("create");
1060
+ return { sandboxId: "sb-should-not-be-created" };
1061
+ },
1062
+ destroySandbox: async (id: string) => {
1063
+ sandboxLog.push(`destroy:${id}`);
1064
+ },
1065
+ pauseSandbox: async () => {},
1066
+ resumeSandbox: async () => {},
1067
+ snapshotSandbox: async (id: string) => {
1068
+ sandboxLog.push(`snapshot:${id}`);
1069
+ return {
1070
+ sandboxId: id,
1071
+ providerId: "test",
1072
+ data: { tag: "exit" },
1073
+ createdAt: new Date().toISOString(),
1074
+ };
1075
+ },
1076
+ restoreSandbox: async (snap) => {
1077
+ sandboxLog.push(`restore:${(snap.data as { tag: string }).tag}`);
1078
+ return "sb-restored";
1079
+ },
1080
+ deleteSandboxSnapshot: async () => {},
1081
+ forkSandbox: async () => "forked-sb",
1082
+ };
1083
+
1084
+ const session = await createSession({
1085
+ agentName: "TestAgent",
1086
+ thread: { mode: "new", threadId: "thread-restore" },
1087
+ runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
1088
+ threadOps: ops,
1089
+ buildContextMessage: () => "go",
1090
+ sandboxOps,
1091
+ sandbox: { mode: "from-snapshot", snapshot: priorSnapshot },
1092
+ sandboxShutdown: "snapshot",
1093
+ });
1094
+
1095
+ const stateManager = createAgentStateManager({
1096
+ initialState: { systemPrompt: "test" },
1097
+ });
1098
+
1099
+ const result = await session.runSession({ stateManager });
1100
+
1101
+ expect(result.sandboxId).toBe("sb-restored");
1102
+ // No base snapshot because the sandbox was restored, not freshly created.
1103
+ expect(result.baseSnapshot).toBeUndefined();
1104
+ expect(result.snapshot?.data).toEqual({ tag: "exit" });
1105
+ expect(sandboxLog).toEqual([
1106
+ "restore:prior",
1107
+ "snapshot:sb-restored",
1108
+ "destroy:sb-restored",
1109
+ ]);
1110
+ expect(sandboxLog).not.toContain("create");
1111
+ });
952
1112
  });
@@ -7,7 +7,11 @@ 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 {
11
+ SandboxCreateOptions,
12
+ SandboxOps,
13
+ SandboxSnapshot,
14
+ } from "../sandbox/types";
11
15
  import type {
12
16
  AgentState,
13
17
  AgentStateManager,
@@ -142,16 +146,19 @@ export async function createSession<
142
146
  appendSystemMessage,
143
147
  appendAgentMessage,
144
148
  forkThread,
149
+ truncateThread,
145
150
  } = threadOps;
146
151
 
147
152
  const plugins: ToolMap[string][] = [];
148
153
  let destroySubagentSandboxes: (() => Promise<void>) | undefined;
154
+ let cleanupSubagentSnapshots: (() => Promise<void>) | undefined;
149
155
 
150
156
  if (subagents) {
151
157
  const result = buildSubagentRegistration(subagents);
152
158
  if (result) {
153
159
  plugins.push(result.registration);
154
160
  destroySubagentSandboxes = result.destroySubagentSandboxes;
161
+ cleanupSubagentSnapshots = result.cleanupSubagentSnapshots;
155
162
  }
156
163
  }
157
164
  if (skills) {
@@ -210,10 +217,13 @@ export async function createSession<
210
217
  }
211
218
  );
212
219
 
213
- // --- Sandbox lifecycle: create, continue, fork, or inherit ----------
220
+ // --- Sandbox lifecycle: create, continue, fork, from-snapshot, or inherit ---
214
221
  const sandboxMode = sandboxInit?.mode;
215
222
  let sandboxId: string | undefined;
216
223
  let sandboxOwned = false;
224
+ let baseSnapshot: SandboxSnapshot | undefined;
225
+ let exitSnapshot: SandboxSnapshot | undefined;
226
+ let freshlyCreated = false;
217
227
 
218
228
  if (sandboxMode === "inherit") {
219
229
  const inheritInit = sandboxInit as {
@@ -248,8 +258,31 @@ export async function createSession<
248
258
  nonRetryable: true,
249
259
  });
250
260
  }
261
+ const forkInit = sandboxInit as {
262
+ mode: "fork";
263
+ sandboxId: string;
264
+ options?: SandboxCreateOptions;
265
+ };
251
266
  sandboxId = await sandboxOps.forkSandbox(
252
- (sandboxInit as { mode: "fork"; sandboxId: string }).sandboxId
267
+ forkInit.sandboxId,
268
+ forkInit.options
269
+ );
270
+ sandboxOwned = true;
271
+ } else if (sandboxMode === "from-snapshot") {
272
+ if (!sandboxOps) {
273
+ throw ApplicationFailure.create({
274
+ message: "No sandboxOps provided — cannot restore sandbox",
275
+ nonRetryable: true,
276
+ });
277
+ }
278
+ const restoreInit = sandboxInit as {
279
+ mode: "from-snapshot";
280
+ snapshot: SandboxSnapshot;
281
+ options?: SandboxCreateOptions;
282
+ };
283
+ sandboxId = await sandboxOps.restoreSandbox(
284
+ restoreInit.snapshot,
285
+ restoreInit.options
253
286
  );
254
287
  sandboxOwned = true;
255
288
  } else if (sandboxOps) {
@@ -263,10 +296,24 @@ export async function createSession<
263
296
  if (result) {
264
297
  sandboxId = result.sandboxId;
265
298
  sandboxOwned = true;
299
+ freshlyCreated = true;
266
300
  }
267
301
  }
268
302
 
269
- if (sandboxId && onSandboxReady) {
303
+ // Capture a base snapshot immediately after seeding so it can be reused
304
+ // as a template for future runs that want to skip the (potentially
305
+ // expensive) seed step.
306
+ if (
307
+ sandboxId &&
308
+ sandboxOwned &&
309
+ freshlyCreated &&
310
+ sandboxShutdown === "snapshot" &&
311
+ sandboxOps
312
+ ) {
313
+ baseSnapshot = await sandboxOps.snapshotSandbox(sandboxId);
314
+ }
315
+
316
+ if (sandboxId && sandboxOwned && onSandboxReady) {
270
317
  onSandboxReady(sandboxId);
271
318
  }
272
319
 
@@ -347,6 +394,7 @@ export async function createSession<
347
394
  );
348
395
 
349
396
  let exitReason: SessionExitReason = "completed";
397
+ let finalMessage: M | null = null;
350
398
 
351
399
  try {
352
400
  while (
@@ -361,13 +409,24 @@ export async function createSession<
361
409
 
362
410
  stateManager.setTools(toolRouter.getToolDefinitions());
363
411
 
364
- const { message, rawToolCalls, usage } = await runAgent({
412
+ const {
413
+ message,
414
+ rawToolCalls,
415
+ usage,
416
+ threadLengthAtCall,
417
+ } = await runAgent({
365
418
  threadId,
366
419
  threadKey,
367
420
  agentName,
368
421
  metadata,
369
422
  });
370
423
 
424
+ // The invoker loaded the thread right before calling the LLM,
425
+ // so it already knows how many messages were stored at that
426
+ // point — we use that directly as the rewind snapshot instead
427
+ // of a separate activity round-trip.
428
+ const preAssistantLength = threadLengthAtCall;
429
+
371
430
  await appendAgentMessage(threadId, uuid4(), message, threadKey);
372
431
 
373
432
  if (usage) {
@@ -385,21 +444,8 @@ export async function createSession<
385
444
  if (!toolRouter.hasTools() || rawToolCalls.length === 0) {
386
445
  stateManager.complete();
387
446
  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"]>>;
447
+ finalMessage = message;
448
+ break;
403
449
  }
404
450
 
405
451
  const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
@@ -433,6 +479,33 @@ export async function createSession<
433
479
  }
434
480
  }
435
481
 
482
+ const rewind = toolCallResults.rewind;
483
+ if (rewind) {
484
+ log.info("rewinding turn", {
485
+ agentName,
486
+ threadId,
487
+ turn: currentTurn,
488
+ toolCallId: rewind.toolCallId,
489
+ toolName: rewind.toolName,
490
+ });
491
+ if (preAssistantLength === undefined) {
492
+ throw ApplicationFailure.create({
493
+ message:
494
+ "Rewind requested but runAgent did not report " +
495
+ "`threadLengthAtCall`; the adapter must populate it to " +
496
+ "support rewinds.",
497
+ nonRetryable: true,
498
+ });
499
+ }
500
+ // Drop the assistant message + any already-saved tool results
501
+ // so the LLM call can be retried from the pre-assistant state.
502
+ // The turn counter is intentionally NOT rolled back — each
503
+ // rewind still consumes one of the `maxTurns` budget so a
504
+ // misbehaving tool cannot spin the session forever.
505
+ await truncateThread(threadId, preAssistantLength, threadKey);
506
+ continue;
507
+ }
508
+
436
509
  if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
437
510
  const conditionMet = await condition(
438
511
  () => stateManager.getStatus() === "RUNNING",
@@ -480,12 +553,20 @@ export async function createSession<
480
553
  case "keep":
481
554
  case "keep-until-parent-close":
482
555
  break;
556
+ case "snapshot":
557
+ exitSnapshot = await sandboxOps.snapshotSandbox(sandboxId);
558
+ await sandboxOps.destroySandbox(sandboxId);
559
+ break;
483
560
  }
484
561
  }
485
562
 
486
563
  if (destroySubagentSandboxes) {
487
564
  await destroySubagentSandboxes();
488
565
  }
566
+
567
+ if (cleanupSubagentSnapshots) {
568
+ await cleanupSubagentSnapshots();
569
+ }
489
570
  }
490
571
 
491
572
  log.info("session ended", {
@@ -495,14 +576,18 @@ export async function createSession<
495
576
  turns: stateManager.getTurns(),
496
577
  durationMs: Date.now() - sessionStartMs,
497
578
  usage: stateManager.getTotalUsage(),
579
+ ...(baseSnapshot && { hasBaseSnapshot: true }),
580
+ ...(exitSnapshot && { hasExitSnapshot: true }),
498
581
  });
499
582
 
500
583
  return {
501
584
  threadId,
502
- finalMessage: null,
585
+ finalMessage,
503
586
  exitReason,
504
587
  usage: stateManager.getTotalUsage(),
505
588
  sandboxId,
589
+ ...(baseSnapshot && { baseSnapshot }),
590
+ ...(exitSnapshot && { snapshot: exitSnapshot }),
506
591
  } as Awaited<ReturnType<ZeitlichSession<M, boolean>["runSession"]>>;
507
592
  },
508
593
  };
@@ -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";
@@ -62,6 +59,18 @@ export interface ThreadOps<TContent = string> {
62
59
  targetThreadId: string,
63
60
  threadKey?: string
64
61
  ): Promise<void>;
62
+ /**
63
+ * Truncate the thread back to `length` messages. Used by the session's
64
+ * rewind flow to roll the thread back before retrying a turn. The
65
+ * session obtains `length` from `AgentResponse.threadLengthAtCall`,
66
+ * which the model invoker computes for free from the messages it
67
+ * loaded before invoking the LLM.
68
+ */
69
+ truncateThread(
70
+ threadId: string,
71
+ length: number,
72
+ threadKey?: string
73
+ ): Promise<void>;
65
74
  }
66
75
 
67
76
  /**
@@ -219,6 +228,17 @@ export type SessionResult<
219
228
  finalMessage: M | null;
220
229
  exitReason: SessionExitReason;
221
230
  usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
231
+ /**
232
+ * Snapshot captured on exit when `sandboxShutdown === "snapshot"`.
233
+ */
234
+ snapshot?: SandboxSnapshot;
235
+ /**
236
+ * Snapshot captured immediately after sandbox seeding (before the agent
237
+ * loop starts) when `sandbox.mode === "new"` and
238
+ * `sandboxShutdown === "snapshot"`. Intended as a reusable "base" for new
239
+ * threads that want to skip re-seeding.
240
+ */
241
+ baseSnapshot?: SandboxSnapshot;
222
242
  } & (HasSandbox extends true
223
243
  ? { sandboxId: string }
224
244
  : { sandboxId?: undefined });
@@ -24,7 +24,7 @@ import { parseSkillFile } from "./parse";
24
24
  export class FileSystemSkillProvider implements SkillProvider {
25
25
  constructor(
26
26
  private readonly fs: SandboxFileSystem,
27
- private readonly baseDir: string,
27
+ private readonly baseDir: string
28
28
  ) {}
29
29
 
30
30
  async listSkills(): Promise<SkillMetadata[]> {
@@ -45,20 +45,21 @@ export class FileSystemSkillProvider implements SkillProvider {
45
45
  }
46
46
 
47
47
  async getSkill(name: string): Promise<Skill> {
48
- const raw = await this.fs.readFile(
49
- join(this.baseDir, name, "SKILL.md"),
50
- );
48
+ const raw = await this.fs.readFile(join(this.baseDir, name, "SKILL.md"));
51
49
  const { frontmatter, body } = parseSkillFile(raw);
52
50
 
53
51
  if (frontmatter.name !== name) {
54
52
  throw new Error(
55
- `Skill directory "${name}" contains SKILL.md with mismatched name "${frontmatter.name}"`,
53
+ `Skill directory "${name}" contains SKILL.md with mismatched name "${frontmatter.name}"`
56
54
  );
57
55
  }
58
56
 
59
57
  const location = join(this.baseDir, name);
60
58
  const resourcePaths = await this.discoverResources(name);
61
- const resourceContents = await this.readResourceContents(location, resourcePaths);
59
+ const resourceContents = await this.readResourceContents(
60
+ location,
61
+ resourcePaths
62
+ );
62
63
  return {
63
64
  ...frontmatter,
64
65
  instructions: body,
@@ -80,7 +81,10 @@ export class FileSystemSkillProvider implements SkillProvider {
80
81
  const { frontmatter, body } = parseSkillFile(raw);
81
82
  const location = join(this.baseDir, dir);
82
83
  const resourcePaths = await this.discoverResources(dir);
83
- const resourceContents = await this.readResourceContents(location, resourcePaths);
84
+ const resourceContents = await this.readResourceContents(
85
+ location,
86
+ resourcePaths
87
+ );
84
88
  skills.push({
85
89
  ...frontmatter,
86
90
  instructions: body,
@@ -119,7 +123,7 @@ export class FileSystemSkillProvider implements SkillProvider {
119
123
 
120
124
  private async readResourceContents(
121
125
  location: string,
122
- resources: string[],
126
+ resources: string[]
123
127
  ): Promise<Record<string, string> | undefined> {
124
128
  if (resources.length === 0) return undefined;
125
129
  const contents: Record<string, string> = {};
@@ -18,7 +18,7 @@ function formatSkillResponse(skill: Skill): string {
18
18
  if (skill.location) {
19
19
  parts.push(`\nSkill directory: ${skill.location}`);
20
20
  parts.push(
21
- "Relative paths in this skill resolve against the skill directory above.",
21
+ "Relative paths in this skill resolve against the skill directory above."
22
22
  );
23
23
  }
24
24
 
@@ -12,7 +12,9 @@ export function parseSkillFile(raw: string): {
12
12
  body: string;
13
13
  } {
14
14
  const trimmed = raw.replace(/^\uFEFF/, ""); // strip BOM
15
- const match = trimmed.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/);
15
+ const match = trimmed.match(
16
+ /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/
17
+ );
16
18
 
17
19
  if (!match) {
18
20
  throw new Error(
@@ -11,9 +11,7 @@ function validateSkillNames(skills: SkillMetadata[]): void {
11
11
  const names = skills.map((s) => s.name);
12
12
  const dupes = names.filter((n, i) => names.indexOf(n) !== i);
13
13
  if (dupes.length > 0) {
14
- throw new Error(
15
- `Duplicate skill names: ${[...new Set(dupes)].join(", ")}`
16
- );
14
+ throw new Error(`Duplicate skill names: ${[...new Set(dupes)].join(", ")}`);
17
15
  }
18
16
  }
19
17