zeitlich 0.2.37 → 0.2.39

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 (172) hide show
  1. package/README.md +18 -0
  2. package/dist/{activities-Bb-nAjwQ.d.ts → activities-Bmu7XnaG.d.ts} +4 -4
  3. package/dist/{activities-vkI4_3CC.d.cts → activities-ByBFLvm2.d.cts} +4 -4
  4. package/dist/adapter-id-BB-mmrts.d.cts +17 -0
  5. package/dist/adapter-id-BB-mmrts.d.ts +17 -0
  6. package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
  7. package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
  8. package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
  9. package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
  10. package/dist/adapters/sandbox/bedrock/index.cjs +3 -3
  11. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  12. package/dist/adapters/sandbox/bedrock/index.d.cts +6 -6
  13. package/dist/adapters/sandbox/bedrock/index.d.ts +6 -6
  14. package/dist/adapters/sandbox/bedrock/index.js +3 -3
  15. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  16. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  17. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  18. package/dist/adapters/sandbox/daytona/index.cjs +3 -3
  19. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  20. package/dist/adapters/sandbox/daytona/index.d.cts +4 -4
  21. package/dist/adapters/sandbox/daytona/index.d.ts +4 -4
  22. package/dist/adapters/sandbox/daytona/index.js +3 -3
  23. package/dist/adapters/sandbox/daytona/index.js.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/e2b/index.cjs +26 -14
  27. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.d.cts +24 -4
  29. package/dist/adapters/sandbox/e2b/index.d.ts +24 -4
  30. package/dist/adapters/sandbox/e2b/index.js +26 -14
  31. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  32. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  33. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  34. package/dist/adapters/sandbox/inmemory/index.cjs +3 -3
  35. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  36. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -4
  37. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -4
  38. package/dist/adapters/sandbox/inmemory/index.js +3 -3
  39. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  41. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  42. package/dist/adapters/thread/anthropic/index.cjs +150 -13
  43. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  44. package/dist/adapters/thread/anthropic/index.d.cts +9 -8
  45. package/dist/adapters/thread/anthropic/index.d.ts +9 -8
  46. package/dist/adapters/thread/anthropic/index.js +150 -14
  47. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  48. package/dist/adapters/thread/anthropic/workflow.cjs +9 -3
  49. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  50. package/dist/adapters/thread/anthropic/workflow.d.cts +6 -5
  51. package/dist/adapters/thread/anthropic/workflow.d.ts +6 -5
  52. package/dist/adapters/thread/anthropic/workflow.js +9 -4
  53. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  54. package/dist/adapters/thread/google-genai/index.cjs +154 -13
  55. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  56. package/dist/adapters/thread/google-genai/index.d.cts +6 -5
  57. package/dist/adapters/thread/google-genai/index.d.ts +6 -5
  58. package/dist/adapters/thread/google-genai/index.js +154 -14
  59. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  60. package/dist/adapters/thread/google-genai/workflow.cjs +9 -3
  61. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  62. package/dist/adapters/thread/google-genai/workflow.d.cts +6 -5
  63. package/dist/adapters/thread/google-genai/workflow.d.ts +6 -5
  64. package/dist/adapters/thread/google-genai/workflow.js +9 -4
  65. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  66. package/dist/adapters/thread/index.cjs +16 -0
  67. package/dist/adapters/thread/index.cjs.map +1 -0
  68. package/dist/adapters/thread/index.d.cts +34 -0
  69. package/dist/adapters/thread/index.d.ts +34 -0
  70. package/dist/adapters/thread/index.js +12 -0
  71. package/dist/adapters/thread/index.js.map +1 -0
  72. package/dist/adapters/thread/langchain/index.cjs +149 -14
  73. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  74. package/dist/adapters/thread/langchain/index.d.cts +9 -8
  75. package/dist/adapters/thread/langchain/index.d.ts +9 -8
  76. package/dist/adapters/thread/langchain/index.js +149 -15
  77. package/dist/adapters/thread/langchain/index.js.map +1 -1
  78. package/dist/adapters/thread/langchain/workflow.cjs +9 -3
  79. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  80. package/dist/adapters/thread/langchain/workflow.d.cts +6 -5
  81. package/dist/adapters/thread/langchain/workflow.d.ts +6 -5
  82. package/dist/adapters/thread/langchain/workflow.js +9 -4
  83. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  84. package/dist/index.cjs +367 -59
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.d.cts +11 -11
  87. package/dist/index.d.ts +11 -11
  88. package/dist/index.js +365 -61
  89. package/dist/index.js.map +1 -1
  90. package/dist/{proxy-DEtowJyd.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
  91. package/dist/{proxy-0smGKvx8.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
  92. package/dist/{thread-manager-C-C4pI2z.d.ts → thread-manager-CcRXasqs.d.ts} +2 -2
  93. package/dist/{thread-manager-D4vgzYrh.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -2
  94. package/dist/{thread-manager-3fszQih4.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -2
  95. package/dist/{thread-manager-CzYln2OC.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -2
  96. package/dist/{types-CPKDl-y_.d.ts → types-Bcbiq8iv.d.cts} +195 -22
  97. package/dist/{types-CNuWnvy9.d.ts → types-DAsQ21Rt.d.ts} +1 -1
  98. package/dist/{types-B37hKoWA.d.ts → types-DpHTX-iO.d.ts} +58 -1
  99. package/dist/{types-BO7Yju20.d.cts → types-Dt8-HBBT.d.ts} +195 -22
  100. package/dist/{types-D08CXPh8.d.cts → types-hFFi-Zd9.d.cts} +58 -1
  101. package/dist/{types-DWEUmYAJ.d.cts → types-lm8tMNJQ.d.cts} +1 -1
  102. package/dist/{types-tQL9njTu.d.cts → types-yx0LzPGn.d.cts} +21 -7
  103. package/dist/{types-tQL9njTu.d.ts → types-yx0LzPGn.d.ts} +21 -7
  104. package/dist/{workflow-CjXHbZZc.d.ts → workflow-Bmf9EtDW.d.ts} +83 -3
  105. package/dist/{workflow-Do_lzJpT.d.cts → workflow-Bx9utBwb.d.cts} +83 -3
  106. package/dist/workflow.cjs +266 -39
  107. package/dist/workflow.cjs.map +1 -1
  108. package/dist/workflow.d.cts +3 -3
  109. package/dist/workflow.d.ts +3 -3
  110. package/dist/workflow.js +264 -41
  111. package/dist/workflow.js.map +1 -1
  112. package/package.json +12 -2
  113. package/src/adapters/sandbox/bedrock/index.ts +12 -3
  114. package/src/adapters/sandbox/daytona/index.ts +12 -3
  115. package/src/adapters/sandbox/e2b/index.ts +36 -14
  116. package/src/adapters/sandbox/e2b/types.ts +16 -0
  117. package/src/adapters/sandbox/inmemory/index.ts +12 -3
  118. package/src/adapters/thread/adapter-id.test.ts +42 -0
  119. package/src/adapters/thread/anthropic/activities.ts +40 -5
  120. package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
  121. package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
  122. package/src/adapters/thread/anthropic/index.ts +3 -0
  123. package/src/adapters/thread/anthropic/model-invoker.ts +7 -1
  124. package/src/adapters/thread/anthropic/proxy.ts +3 -2
  125. package/src/adapters/thread/anthropic/thread-manager.ts +27 -1
  126. package/src/adapters/thread/google-genai/activities.ts +44 -5
  127. package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
  128. package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
  129. package/src/adapters/thread/google-genai/index.ts +3 -0
  130. package/src/adapters/thread/google-genai/model-invoker.ts +8 -2
  131. package/src/adapters/thread/google-genai/proxy.ts +3 -2
  132. package/src/adapters/thread/google-genai/thread-manager.ts +27 -1
  133. package/src/adapters/thread/index.ts +39 -0
  134. package/src/adapters/thread/langchain/activities.ts +40 -5
  135. package/src/adapters/thread/langchain/adapter-id.ts +16 -0
  136. package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
  137. package/src/adapters/thread/langchain/index.ts +3 -0
  138. package/src/adapters/thread/langchain/model-invoker.ts +7 -1
  139. package/src/adapters/thread/langchain/proxy.ts +3 -2
  140. package/src/adapters/thread/langchain/thread-manager.ts +27 -1
  141. package/src/lib/lifecycle.ts +14 -5
  142. package/src/lib/model/types.ts +7 -0
  143. package/src/lib/sandbox/manager.ts +26 -18
  144. package/src/lib/sandbox/types.ts +27 -7
  145. package/src/lib/session/session-edge-cases.integration.test.ts +336 -4
  146. package/src/lib/session/session.integration.test.ts +192 -2
  147. package/src/lib/session/session.ts +102 -8
  148. package/src/lib/session/types.ts +66 -3
  149. package/src/lib/state/index.ts +1 -0
  150. package/src/lib/state/manager.integration.test.ts +109 -0
  151. package/src/lib/state/manager.ts +38 -8
  152. package/src/lib/state/types.ts +25 -0
  153. package/src/lib/subagent/handler.ts +124 -11
  154. package/src/lib/subagent/index.ts +5 -1
  155. package/src/lib/subagent/subagent.integration.test.ts +628 -104
  156. package/src/lib/subagent/types.ts +63 -14
  157. package/src/lib/subagent/workflow.ts +29 -2
  158. package/src/lib/thread/index.ts +5 -0
  159. package/src/lib/thread/keys.test.ts +101 -0
  160. package/src/lib/thread/keys.ts +94 -0
  161. package/src/lib/thread/manager.test.ts +139 -0
  162. package/src/lib/thread/manager.ts +105 -9
  163. package/src/lib/thread/proxy.ts +3 -0
  164. package/src/lib/thread/types.ts +64 -1
  165. package/src/lib/tool-router/index.ts +2 -0
  166. package/src/lib/tool-router/router-edge-cases.integration.test.ts +92 -0
  167. package/src/lib/tool-router/router.integration.test.ts +12 -0
  168. package/src/lib/tool-router/router.ts +89 -16
  169. package/src/lib/tool-router/types.ts +42 -1
  170. package/src/lib/types.ts +12 -0
  171. package/src/workflow.ts +14 -1
  172. package/tsup.config.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.37",
3
+ "version": "0.2.39",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -27,6 +27,16 @@
27
27
  "default": "./dist/workflow.js"
28
28
  }
29
29
  },
30
+ "./adapters/thread": {
31
+ "import": {
32
+ "types": "./dist/adapters/thread/index.d.ts",
33
+ "default": "./dist/adapters/thread/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/adapters/thread/index.d.ts",
37
+ "default": "./dist/adapters/thread/index.js"
38
+ }
39
+ },
30
40
  "./adapters/thread/langchain": {
31
41
  "import": {
32
42
  "types": "./dist/adapters/thread/langchain/index.d.ts",
@@ -210,7 +220,7 @@
210
220
  "node": ">=18"
211
221
  },
212
222
  "devDependencies": {
213
- "@anthropic-ai/sdk": "^0.80.0",
223
+ "@anthropic-ai/sdk": "^0.81.0",
214
224
  "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
215
225
  "@daytonaio/sdk": "^0.158.1",
216
226
  "@e2b/code-interpreter": "^2.3.3",
@@ -239,15 +239,24 @@ export class BedrockSandboxProvider implements SandboxProvider<
239
239
  // Bedrock sandboxes don't support pause, so resume is a no-op
240
240
  }
241
241
 
242
- async snapshot(_sandboxId: string): Promise<SandboxSnapshot> {
242
+ async snapshot(
243
+ _sandboxId: string,
244
+ _options?: BedrockSandboxCreateOptions
245
+ ): Promise<SandboxSnapshot> {
243
246
  throw new SandboxNotSupportedError("snapshot");
244
247
  }
245
248
 
246
- async restore(_snapshot: SandboxSnapshot): Promise<never> {
249
+ async restore(
250
+ _snapshot: SandboxSnapshot,
251
+ _options?: BedrockSandboxCreateOptions
252
+ ): Promise<never> {
247
253
  throw new SandboxNotSupportedError("restore");
248
254
  }
249
255
 
250
- async fork(_sandboxId: string): Promise<Sandbox> {
256
+ async fork(
257
+ _sandboxId: string,
258
+ _options?: BedrockSandboxCreateOptions
259
+ ): Promise<Sandbox> {
251
260
  throw new SandboxNotSupportedError("fork");
252
261
  }
253
262
 
@@ -149,17 +149,26 @@ export class DaytonaSandboxProvider implements SandboxProvider<
149
149
  // Daytona sandboxes don't support pause, so resume is a no-op
150
150
  }
151
151
 
152
- async fork(_sandboxId: string): Promise<Sandbox> {
152
+ async fork(
153
+ _sandboxId: string,
154
+ _options?: DaytonaSandboxCreateOptions
155
+ ): Promise<Sandbox> {
153
156
  throw new Error("Not implemented");
154
157
  }
155
158
 
156
- async snapshot(_sandboxId: string): Promise<SandboxSnapshot> {
159
+ async snapshot(
160
+ _sandboxId: string,
161
+ _options?: DaytonaSandboxCreateOptions
162
+ ): Promise<SandboxSnapshot> {
157
163
  throw new SandboxNotSupportedError(
158
164
  "snapshot (use Daytona's native snapshot API directly)"
159
165
  );
160
166
  }
161
167
 
162
- async restore(_snapshot: SandboxSnapshot): Promise<never> {
168
+ async restore(
169
+ _snapshot: SandboxSnapshot,
170
+ _options?: DaytonaSandboxCreateOptions
171
+ ): Promise<never> {
163
172
  throw new SandboxNotSupportedError(
164
173
  "restore (use Daytona's native snapshot API directly)"
165
174
  );
@@ -76,11 +76,19 @@ export class E2bSandboxProvider implements SandboxProvider<
76
76
  private readonly defaultTemplate?: string;
77
77
  private readonly defaultWorkspaceBase: string;
78
78
  private readonly defaultTimeoutMs?: number;
79
+ private readonly defaultAllowInternetAccess?: boolean;
80
+ private readonly defaultNetwork?: E2bSandboxConfig["network"];
81
+ private readonly defaultMetadata?: E2bSandboxConfig["metadata"];
82
+ private readonly defaultLifecycle?: E2bSandboxConfig["lifecycle"];
79
83
 
80
84
  constructor(config?: E2bSandboxConfig) {
81
85
  this.defaultTemplate = config?.template;
82
86
  this.defaultWorkspaceBase = config?.workspaceBase ?? "/home/user";
83
87
  this.defaultTimeoutMs = config?.timeoutMs;
88
+ this.defaultAllowInternetAccess = config?.allowInternetAccess;
89
+ this.defaultNetwork = config?.network;
90
+ this.defaultMetadata = config?.metadata;
91
+ this.defaultLifecycle = config?.lifecycle;
84
92
  }
85
93
 
86
94
  async create(
@@ -142,7 +150,10 @@ export class E2bSandboxProvider implements SandboxProvider<
142
150
  await E2bSdkSandbox.connect(sandboxId);
143
151
  }
144
152
 
145
- async snapshot(sandboxId: string): Promise<SandboxSnapshot> {
153
+ async snapshot(
154
+ sandboxId: string,
155
+ _options?: E2bSandboxCreateOptions
156
+ ): Promise<SandboxSnapshot> {
146
157
  const { snapshotId } = await E2bSdkSandbox.createSnapshot(sandboxId);
147
158
  return {
148
159
  sandboxId,
@@ -152,14 +163,18 @@ export class E2bSandboxProvider implements SandboxProvider<
152
163
  };
153
164
  }
154
165
 
155
- async restore(snapshot: SandboxSnapshot): Promise<Sandbox> {
166
+ async restore(
167
+ snapshot: SandboxSnapshot,
168
+ options?: E2bSandboxCreateOptions
169
+ ): Promise<Sandbox> {
156
170
  const data = snapshot.data as { snapshotId?: string } | null;
157
171
  if (!data?.snapshotId) {
158
172
  throw new SandboxNotSupportedError(
159
173
  "restore: snapshot is missing snapshotId"
160
174
  );
161
175
  }
162
- const sdkSandbox = await E2bSdkSandbox.create(data.snapshotId);
176
+ const sdkOpts = this.buildSdkCreateOpts(options);
177
+ const sdkSandbox = await E2bSdkSandbox.create(data.snapshotId, sdkOpts);
163
178
  return new E2bSandboxImpl(
164
179
  sdkSandbox.sandboxId,
165
180
  sdkSandbox,
@@ -177,9 +192,13 @@ export class E2bSandboxProvider implements SandboxProvider<
177
192
  }
178
193
  }
179
194
 
180
- async fork(sandboxId: string): Promise<Sandbox> {
195
+ async fork(
196
+ sandboxId: string,
197
+ options?: E2bSandboxCreateOptions
198
+ ): Promise<Sandbox> {
181
199
  const { snapshotId } = await E2bSdkSandbox.createSnapshot(sandboxId);
182
- const sdkSandbox = await E2bSdkSandbox.create(snapshotId);
200
+ const sdkOpts = this.buildSdkCreateOpts(options);
201
+ const sdkSandbox = await E2bSdkSandbox.create(snapshotId, sdkOpts);
183
202
  return new E2bSandboxImpl(
184
203
  sdkSandbox.sandboxId,
185
204
  sdkSandbox,
@@ -188,22 +207,25 @@ export class E2bSandboxProvider implements SandboxProvider<
188
207
  }
189
208
 
190
209
  private buildSdkCreateOpts(options?: E2bSandboxCreateOptions) {
210
+ const network = options?.network ?? this.defaultNetwork;
211
+ const lifecycle = options?.lifecycle ?? this.defaultLifecycle;
191
212
  return {
192
213
  envs: options?.env,
193
214
  timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
194
- metadata: options?.metadata,
195
- allowInternetAccess: options?.allowInternetAccess,
196
- network: options?.network
215
+ metadata: options?.metadata ?? this.defaultMetadata,
216
+ allowInternetAccess:
217
+ options?.allowInternetAccess ?? this.defaultAllowInternetAccess,
218
+ network: network
197
219
  ? {
198
- allowOut: options.network.allowOut,
199
- denyOut: options.network.denyOut,
200
- allowPublicTraffic: options.network.allowPublicTraffic,
220
+ allowOut: network.allowOut,
221
+ denyOut: network.denyOut,
222
+ allowPublicTraffic: network.allowPublicTraffic,
201
223
  }
202
224
  : undefined,
203
- lifecycle: options?.lifecycle
225
+ lifecycle: lifecycle
204
226
  ? {
205
- onTimeout: options.lifecycle.onTimeout,
206
- autoResume: options.lifecycle.autoResume,
227
+ onTimeout: lifecycle.onTimeout,
228
+ autoResume: lifecycle.autoResume,
207
229
  }
208
230
  : undefined,
209
231
  };
@@ -6,6 +6,14 @@ import type { E2bSandboxFileSystem } from "./filesystem";
6
6
  */
7
7
  export type E2bSandbox = Sandbox & { fs: E2bSandboxFileSystem };
8
8
 
9
+ /**
10
+ * Provider-level defaults for E2B sandboxes. Every lifecycle op
11
+ * (`create` / `restore` / `fork`) merges per-call options on top of these —
12
+ * per-call options win per-field. This is how E2B preserves sandbox-level
13
+ * config (network policy, metadata, lifecycle, timeout) across
14
+ * snapshot/restore and fork, since the E2B API treats those as pure
15
+ * create-time inputs that aren't carried in the snapshot blob.
16
+ */
9
17
  export interface E2bSandboxConfig {
10
18
  /** Sandbox template name or ID */
11
19
  template?: string;
@@ -13,6 +21,14 @@ export interface E2bSandboxConfig {
13
21
  workspaceBase?: string;
14
22
  /** Sandbox idle timeout in milliseconds */
15
23
  timeoutMs?: number;
24
+ /** Default outbound internet access policy */
25
+ allowInternetAccess?: boolean;
26
+ /** Default outbound network allow/deny rules */
27
+ network?: SandboxCreateOptions["network"];
28
+ /** Default metadata surfaced via provider list/query APIs */
29
+ metadata?: SandboxCreateOptions["metadata"];
30
+ /** Default sandbox timeout behaviour */
31
+ lifecycle?: SandboxCreateOptions["lifecycle"];
16
32
  }
17
33
 
18
34
  export interface E2bSandboxCreateOptions extends SandboxCreateOptions {
@@ -183,7 +183,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
183
183
  return { sandbox };
184
184
  }
185
185
 
186
- async snapshot(sandboxId: string): Promise<SandboxSnapshot> {
186
+ async snapshot(
187
+ sandboxId: string,
188
+ _options?: SandboxCreateOptions
189
+ ): Promise<SandboxSnapshot> {
187
190
  const sandbox = this.sandboxes.get(sandboxId);
188
191
  if (!sandbox) throw new SandboxNotFoundError(sandboxId);
189
192
 
@@ -210,7 +213,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
210
213
  };
211
214
  }
212
215
 
213
- async fork(sandboxId: string): Promise<Sandbox> {
216
+ async fork(
217
+ sandboxId: string,
218
+ _options?: SandboxCreateOptions
219
+ ): Promise<Sandbox> {
214
220
  const sandbox = await this.get(sandboxId);
215
221
 
216
222
  const entries = await sandbox.fs.readdirWithFileTypes("/");
@@ -228,7 +234,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
228
234
  return newSandbox.sandbox;
229
235
  }
230
236
 
231
- async restore(snapshot: SandboxSnapshot): Promise<Sandbox> {
237
+ async restore(
238
+ snapshot: SandboxSnapshot,
239
+ _options?: SandboxCreateOptions
240
+ ): Promise<Sandbox> {
232
241
  const { files } = snapshot.data as { files: Record<string, string> };
233
242
  const initialFiles: InitialFiles = {};
234
243
  for (const [path, content] of Object.entries(files)) {
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, expectTypeOf } from "vitest";
2
+ import { ADAPTER_ID as LANGCHAIN } from "./langchain/adapter-id";
3
+ import { ADAPTER_ID as GOOGLE_GENAI } from "./google-genai/adapter-id";
4
+ import { ADAPTER_ID as ANTHROPIC } from "./anthropic/adapter-id";
5
+ import {
6
+ LANGCHAIN_ADAPTER_ID,
7
+ GOOGLE_GENAI_ADAPTER_ID,
8
+ ANTHROPIC_ADAPTER_ID,
9
+ type ThreadAdapterId,
10
+ } from "./index";
11
+
12
+ describe("thread adapter identity", () => {
13
+ it("langchain ADAPTER_ID is the wire-format string", () => {
14
+ expect(LANGCHAIN).toBe("langChain");
15
+ expect(LANGCHAIN_ADAPTER_ID).toBe("langChain");
16
+ });
17
+
18
+ it("google-genai ADAPTER_ID is the wire-format string", () => {
19
+ expect(GOOGLE_GENAI).toBe("googleGenAI");
20
+ expect(GOOGLE_GENAI_ADAPTER_ID).toBe("googleGenAI");
21
+ });
22
+
23
+ it("anthropic ADAPTER_ID is the wire-format string", () => {
24
+ expect(ANTHROPIC).toBe("anthropic");
25
+ expect(ANTHROPIC_ADAPTER_ID).toBe("anthropic");
26
+ });
27
+
28
+ it("ADAPTER_ID values narrow to string literals, not `string`", () => {
29
+ expectTypeOf(LANGCHAIN).toEqualTypeOf<"langChain">();
30
+ expectTypeOf(GOOGLE_GENAI).toEqualTypeOf<"googleGenAI">();
31
+ expectTypeOf(ANTHROPIC).toEqualTypeOf<"anthropic">();
32
+ });
33
+
34
+ it("ThreadAdapterId is the discriminated union of every built-in id", () => {
35
+ const allow = (_id: ThreadAdapterId): void => undefined;
36
+ allow(LANGCHAIN);
37
+ allow(GOOGLE_GENAI);
38
+ allow(ANTHROPIC);
39
+ // @ts-expect-error — arbitrary strings aren't members of the union
40
+ allow("someOtherAdapter");
41
+ });
42
+ });
@@ -1,6 +1,7 @@
1
1
  import type Redis from "ioredis";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { ToolResultConfig } from "../../../lib/types";
4
+ import type { PersistedThreadState } from "../../../lib/state/types";
4
5
  import type {
5
6
  ActivityToolHandler,
6
7
  RouterContext,
@@ -22,11 +23,10 @@ import {
22
23
  createAnthropicModelInvoker,
23
24
  type AnthropicModelInvokerConfig,
24
25
  } from "./model-invoker";
25
-
26
- const ADAPTER_PREFIX = "anthropic" as const;
26
+ import { ADAPTER_ID } from "./adapter-id";
27
27
 
28
28
  export type AnthropicThreadOps<TScope extends string = ""> = PrefixedThreadOps<
29
- ScopedPrefix<TScope, typeof ADAPTER_PREFIX>,
29
+ ScopedPrefix<TScope, typeof ADAPTER_ID>,
30
30
  AnthropicContent
31
31
  >;
32
32
 
@@ -209,17 +209,52 @@ export function createAnthropicAdapter(
209
209
  redis,
210
210
  threadId: sourceThreadId,
211
211
  key: threadKey,
212
+ hooks: config.hooks,
212
213
  });
213
214
  await thread.fork(targetThreadId);
214
215
  },
216
+
217
+ async truncateThread(
218
+ threadId: string,
219
+ messageId: string,
220
+ threadKey?: string,
221
+ ): Promise<void> {
222
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
223
+ await thread.truncateFromId(messageId);
224
+ },
225
+
226
+ async loadThreadState(
227
+ threadId: string,
228
+ threadKey?: string
229
+ ): Promise<PersistedThreadState | null> {
230
+ const thread = createAnthropicThreadManager({
231
+ redis,
232
+ threadId,
233
+ key: threadKey,
234
+ });
235
+ return thread.loadState();
236
+ },
237
+
238
+ async saveThreadState(
239
+ threadId: string,
240
+ state: PersistedThreadState,
241
+ threadKey?: string
242
+ ): Promise<void> {
243
+ const thread = createAnthropicThreadManager({
244
+ redis,
245
+ threadId,
246
+ key: threadKey,
247
+ });
248
+ await thread.saveState(state);
249
+ },
215
250
  };
216
251
 
217
252
  function createActivities<S extends string = "">(
218
253
  scope?: S
219
254
  ): AnthropicThreadOps<S> {
220
255
  const prefix = scope
221
- ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
222
- : ADAPTER_PREFIX;
256
+ ? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
257
+ : ADAPTER_ID;
223
258
  const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
224
259
  return Object.fromEntries(
225
260
  Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public adapter identity for the Anthropic thread adapter.
3
+ *
4
+ * This value is wire format — it appears as the prefix for Temporal
5
+ * activity names (e.g. `anthropicCodingAgentInitializeThread`) and must
6
+ * never change, since renaming it would orphan existing persisted
7
+ * threads and break in-flight workflows.
8
+ *
9
+ * Re-exported from `zeitlich/adapters/thread/anthropic` so downstream
10
+ * consumers can use the exact same literal the adapter uses internally,
11
+ * typed as the narrow string literal `"anthropic"`.
12
+ */
13
+ export const ADAPTER_ID = "anthropic" as const;
14
+
15
+ /** Narrow string-literal type for {@link ADAPTER_ID}. */
16
+ export type AdapterId = typeof ADAPTER_ID;
@@ -0,0 +1,291 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { StoredMessage } from "./thread-manager";
3
+ import { createAnthropicThreadManager } from "./thread-manager";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Stateful in-memory Redis mock sufficient for fork / replaceAll flows.
7
+ // Only the commands used by createThreadManager are implemented.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function createStatefulRedis() {
11
+ const lists = new Map<string, string[]>();
12
+ const strings = new Map<string, string>();
13
+
14
+ return {
15
+ exists: vi.fn(async (...keys: string[]) =>
16
+ keys.reduce(
17
+ (acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
18
+ 0
19
+ )
20
+ ),
21
+ lrange: vi.fn(async (key: string, start: number, stop: number) => {
22
+ const list = lists.get(key) ?? [];
23
+ const end = stop === -1 ? list.length : stop + 1;
24
+ return list.slice(start, end);
25
+ }),
26
+ rpush: vi.fn(async (key: string, ...values: string[]) => {
27
+ const list = lists.get(key) ?? [];
28
+ list.push(...values);
29
+ lists.set(key, list);
30
+ return list.length;
31
+ }),
32
+ ltrim: vi.fn(async (key: string, start: number, stop: number) => {
33
+ const list = lists.get(key) ?? [];
34
+ const end = stop === -1 ? list.length : stop + 1;
35
+ lists.set(key, list.slice(start, end));
36
+ return "OK";
37
+ }),
38
+ del: vi.fn(async (...keys: string[]) => {
39
+ let removed = 0;
40
+ for (const k of keys) {
41
+ if (lists.delete(k)) removed++;
42
+ if (strings.delete(k)) removed++;
43
+ }
44
+ return removed;
45
+ }),
46
+ set: vi.fn(async (key: string, value: string) => {
47
+ strings.set(key, value);
48
+ return "OK";
49
+ }),
50
+ get: vi.fn(async (key: string) => strings.get(key) ?? null),
51
+ expire: vi.fn(async (_key: string, _ttl: number) => 1),
52
+ llen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
53
+ eval: vi.fn(
54
+ async (_script: string, _numKeys: number, ...args: string[]) => {
55
+ const [dedupKey, listKey, , ...serialised] = args;
56
+ if (!dedupKey || !listKey) return 0;
57
+ if (strings.has(dedupKey)) return 0;
58
+ const list = lists.get(listKey) ?? [];
59
+ list.push(...serialised);
60
+ lists.set(listKey, list);
61
+ strings.set(dedupKey, "1");
62
+ return 1;
63
+ }
64
+ ),
65
+ __peek: {
66
+ list: (key: string): string[] => [...(lists.get(key) ?? [])],
67
+ strings,
68
+ },
69
+ };
70
+ }
71
+
72
+ const userMsg: StoredMessage = {
73
+ id: "msg-1",
74
+ message: { role: "user", content: [{ type: "text", text: "Hello" }] },
75
+ };
76
+
77
+ const assistantMsg: StoredMessage = {
78
+ id: "msg-2",
79
+ message: {
80
+ role: "assistant",
81
+ content: [{ type: "text", text: "Hi there!" }],
82
+ },
83
+ };
84
+
85
+ const userMsg2: StoredMessage = {
86
+ id: "msg-3",
87
+ message: { role: "user", content: [{ type: "text", text: "Again please" }] },
88
+ };
89
+
90
+ async function seedSource(
91
+ redis: ReturnType<typeof createStatefulRedis>,
92
+ threadId: string,
93
+ messages: StoredMessage[]
94
+ ): Promise<void> {
95
+ const tm = createAnthropicThreadManager({
96
+ redis: redis as never,
97
+ threadId,
98
+ });
99
+ await tm.initialize();
100
+ await tm.append(messages);
101
+ }
102
+
103
+ describe("Anthropic fork + transform hooks", () => {
104
+ it("behaves like fork when neither onFork hook is configured", async () => {
105
+ const redis = createStatefulRedis();
106
+ await seedSource(redis, "src", [userMsg, assistantMsg]);
107
+
108
+ const tm = createAnthropicThreadManager({
109
+ redis: redis as never,
110
+ threadId: "src",
111
+ });
112
+ const forked = await tm.fork("dst");
113
+ const loaded = await forked.load();
114
+
115
+ expect(loaded).toEqual([userMsg, assistantMsg]);
116
+
117
+ // Source is untouched
118
+ const srcLoaded = await tm.load();
119
+ expect(srcLoaded).toEqual([userMsg, assistantMsg]);
120
+ });
121
+
122
+ it("applies onForkTransform alone as a per-message map", async () => {
123
+ const redis = createStatefulRedis();
124
+ await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
125
+
126
+ const calls: Array<{
127
+ idx: number;
128
+ id: string;
129
+ total: number;
130
+ }> = [];
131
+ const onForkTransform = vi.fn(
132
+ (msg: StoredMessage, index: number, messages: readonly StoredMessage[]) => {
133
+ calls.push({ idx: index, id: msg.id, total: messages.length });
134
+ const firstBlock = (msg.message.content as Array<{ text?: string }>)[0];
135
+ return {
136
+ ...msg,
137
+ message: {
138
+ ...msg.message,
139
+ content: [
140
+ {
141
+ type: "text" as const,
142
+ text: `[T${index}] ${firstBlock?.text ?? ""}`,
143
+ },
144
+ ],
145
+ },
146
+ };
147
+ }
148
+ );
149
+
150
+ const tm = createAnthropicThreadManager({
151
+ redis: redis as never,
152
+ threadId: "src",
153
+ hooks: { onForkTransform },
154
+ });
155
+ const forked = await tm.fork("dst");
156
+ const loaded = await forked.load();
157
+
158
+ expect(onForkTransform).toHaveBeenCalledTimes(3);
159
+ expect(calls).toEqual([
160
+ { idx: 0, id: "msg-1", total: 3 },
161
+ { idx: 1, id: "msg-2", total: 3 },
162
+ { idx: 2, id: "msg-3", total: 3 },
163
+ ]);
164
+ expect(loaded).toHaveLength(3);
165
+ expect(loaded[0]?.message.content).toEqual([
166
+ { type: "text", text: "[T0] Hello" },
167
+ ]);
168
+ expect(loaded[1]?.message.content).toEqual([
169
+ { type: "text", text: "[T1] Hi there!" },
170
+ ]);
171
+ expect(loaded[2]?.message.content).toEqual([
172
+ { type: "text", text: "[T2] Again please" },
173
+ ]);
174
+
175
+ // Source is unchanged.
176
+ const srcLoaded = await tm.load();
177
+ expect(srcLoaded.map((m) => m.id)).toEqual(["msg-1", "msg-2", "msg-3"]);
178
+ });
179
+
180
+ it("applies onForkPrepareThread alone and may change list length", async () => {
181
+ const redis = createStatefulRedis();
182
+ await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
183
+
184
+ const onForkPrepareThread = vi.fn(
185
+ async (messages: readonly StoredMessage[]) =>
186
+ // Drop first message and prepend a summary.
187
+ [
188
+ {
189
+ id: "summary-1",
190
+ message: {
191
+ role: "user" as const,
192
+ content: [{ type: "text" as const, text: "[summary]" }],
193
+ },
194
+ },
195
+ ...messages.slice(1),
196
+ ]
197
+ );
198
+
199
+ const tm = createAnthropicThreadManager({
200
+ redis: redis as never,
201
+ threadId: "src",
202
+ hooks: { onForkPrepareThread },
203
+ });
204
+ const forked = await tm.fork("dst");
205
+ const loaded = await forked.load();
206
+
207
+ expect(onForkPrepareThread).toHaveBeenCalledTimes(1);
208
+ expect(loaded.map((m) => m.id)).toEqual(["summary-1", "msg-2", "msg-3"]);
209
+ });
210
+
211
+ it("runs onForkPrepareThread before onForkTransform and passes prepared list as messages", async () => {
212
+ const redis = createStatefulRedis();
213
+ await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
214
+
215
+ const order: string[] = [];
216
+ const indicesSeen: Array<{ idx: number; total: number; id: string }> = [];
217
+
218
+ const onForkPrepareThread = vi.fn(
219
+ async (messages: readonly StoredMessage[]) => {
220
+ order.push("prepare");
221
+ // Drop the last message (length changes).
222
+ return messages.slice(0, -1);
223
+ }
224
+ );
225
+
226
+ const onForkTransform = vi.fn(
227
+ (
228
+ msg: StoredMessage,
229
+ index: number,
230
+ messages: readonly StoredMessage[]
231
+ ) => {
232
+ order.push("transform");
233
+ indicesSeen.push({ idx: index, total: messages.length, id: msg.id });
234
+ return {
235
+ ...msg,
236
+ message: {
237
+ ...msg.message,
238
+ content: [{ type: "text" as const, text: `[x${index}]` }],
239
+ },
240
+ };
241
+ }
242
+ );
243
+
244
+ const tm = createAnthropicThreadManager({
245
+ redis: redis as never,
246
+ threadId: "src",
247
+ hooks: { onForkPrepareThread, onForkTransform },
248
+ });
249
+ const forked = await tm.fork("dst");
250
+ const loaded = await forked.load();
251
+
252
+ // prepare runs once, transform once per survivor.
253
+ expect(order).toEqual(["prepare", "transform", "transform"]);
254
+ expect(indicesSeen).toEqual([
255
+ { idx: 0, total: 2, id: "msg-1" },
256
+ { idx: 1, total: 2, id: "msg-2" },
257
+ ]);
258
+ expect(loaded).toHaveLength(2);
259
+ expect(loaded[0]?.message.content).toEqual([{ type: "text", text: "[x0]" }]);
260
+ expect(loaded[1]?.message.content).toEqual([{ type: "text", text: "[x1]" }]);
261
+ });
262
+
263
+ it("leaves dedup markers cleared so the transformed thread can accept replays", async () => {
264
+ const redis = createStatefulRedis();
265
+ await seedSource(redis, "src", [userMsg, assistantMsg]);
266
+
267
+ const onForkTransform = vi.fn(
268
+ (msg: StoredMessage) => ({
269
+ ...msg,
270
+ message: {
271
+ ...msg.message,
272
+ content: [{ type: "text" as const, text: "[replaced]" }],
273
+ },
274
+ })
275
+ );
276
+
277
+ const tm = createAnthropicThreadManager({
278
+ redis: redis as never,
279
+ threadId: "src",
280
+ hooks: { onForkTransform },
281
+ });
282
+ await tm.fork("dst");
283
+
284
+ // After replaceAll, dedup markers from the pre-replacement writes must be
285
+ // gone — otherwise an append with the same id would be silently skipped.
286
+ const lingering = Array.from(redis.__peek.strings.keys()).filter((k) =>
287
+ k.startsWith("messages:thread:dst:dedup:")
288
+ );
289
+ expect(lingering).toEqual([]);
290
+ });
291
+ });