zeitlich 0.2.40 → 0.2.42

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 (134) hide show
  1. package/README.md +12 -1
  2. package/dist/{activities-CvUrG3YG.d.cts → activities-Coafq5zr.d.cts} +2 -2
  3. package/dist/{activities-CULxRzJ1.d.ts → activities-CrN-ghLo.d.ts} +2 -2
  4. package/dist/adapters/sandbox/daytona/index.cjs +4 -23
  5. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/daytona/index.d.cts +18 -86
  7. package/dist/adapters/sandbox/daytona/index.d.ts +18 -86
  8. package/dist/adapters/sandbox/daytona/index.js +4 -23
  9. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  10. package/dist/adapters/sandbox/daytona/workflow.cjs +1 -7
  11. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  12. package/dist/adapters/sandbox/daytona/workflow.d.cts +9 -2
  13. package/dist/adapters/sandbox/daytona/workflow.d.ts +9 -2
  14. package/dist/adapters/sandbox/daytona/workflow.js +1 -7
  15. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  16. package/dist/adapters/sandbox/e2b/index.cjs +21 -3
  17. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/e2b/index.d.cts +48 -7
  19. package/dist/adapters/sandbox/e2b/index.d.ts +48 -7
  20. package/dist/adapters/sandbox/e2b/index.js +22 -5
  21. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  22. package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -1
  23. package/dist/adapters/sandbox/e2b/workflow.d.cts +4 -2
  24. package/dist/adapters/sandbox/e2b/workflow.d.ts +4 -2
  25. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
  26. package/dist/adapters/sandbox/inmemory/index.cjs +11 -0
  27. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/inmemory/index.d.cts +11 -3
  29. package/dist/adapters/sandbox/inmemory/index.d.ts +11 -3
  30. package/dist/adapters/sandbox/inmemory/index.js +11 -1
  31. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  32. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  33. package/dist/adapters/sandbox/inmemory/workflow.d.cts +4 -2
  34. package/dist/adapters/sandbox/inmemory/workflow.d.ts +4 -2
  35. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  36. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  37. package/dist/adapters/thread/anthropic/index.d.cts +6 -6
  38. package/dist/adapters/thread/anthropic/index.d.ts +6 -6
  39. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  40. package/dist/adapters/thread/anthropic/workflow.d.cts +6 -6
  41. package/dist/adapters/thread/anthropic/workflow.d.ts +6 -6
  42. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  43. package/dist/adapters/thread/google-genai/index.d.cts +6 -6
  44. package/dist/adapters/thread/google-genai/index.d.ts +6 -6
  45. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  46. package/dist/adapters/thread/google-genai/workflow.d.cts +6 -6
  47. package/dist/adapters/thread/google-genai/workflow.d.ts +6 -6
  48. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  49. package/dist/adapters/thread/langchain/index.d.cts +6 -6
  50. package/dist/adapters/thread/langchain/index.d.ts +6 -6
  51. package/dist/adapters/thread/langchain/index.js.map +1 -1
  52. package/dist/adapters/thread/langchain/workflow.d.cts +6 -6
  53. package/dist/adapters/thread/langchain/workflow.d.ts +6 -6
  54. package/dist/index.cjs +316 -119
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +93 -17
  57. package/dist/index.d.ts +93 -17
  58. package/dist/index.js +317 -120
  59. package/dist/index.js.map +1 -1
  60. package/dist/{proxy-5EbwzaY4.d.cts → proxy-Bf7uI-Hw.d.cts} +1 -1
  61. package/dist/{proxy-wZufFfBh.d.ts → proxy-COqA95FW.d.ts} +1 -1
  62. package/dist/{thread-manager-BqBAIsED.d.ts → thread-manager-BhkOyQ1I.d.ts} +2 -2
  63. package/dist/{thread-manager-BNiIt5r8.d.ts → thread-manager-Bi1XlbpJ.d.ts} +2 -2
  64. package/dist/{thread-manager-DF8WuCRs.d.cts → thread-manager-BsLO3Fgc.d.cts} +2 -2
  65. package/dist/{thread-manager-BoN5DOvG.d.cts → thread-manager-wRVVBFgj.d.cts} +2 -2
  66. package/dist/{types-C7OoY7h8.d.ts → types-BkX4HLzi.d.ts} +1 -1
  67. package/dist/{types-CuISs0Ub.d.cts → types-C66-BVBr.d.cts} +1 -1
  68. package/dist/types-CJ7tCdl6.d.cts +266 -0
  69. package/dist/types-CJ7tCdl6.d.ts +266 -0
  70. package/dist/{types-DeQH84C_.d.ts → types-CdALEF3z.d.cts} +342 -23
  71. package/dist/{types-Cn2r3ol3.d.cts → types-ChAy_jSP.d.ts} +342 -23
  72. package/dist/types-CjY93AWZ.d.cts +84 -0
  73. package/dist/types-gVa5XCWD.d.ts +84 -0
  74. package/dist/{workflow-DhplIN65.d.cts → workflow-BwT5EybR.d.ts} +7 -6
  75. package/dist/{workflow-C2MZZj5K.d.ts → workflow-DMmiaw6w.d.cts} +7 -6
  76. package/dist/workflow.cjs +138 -77
  77. package/dist/workflow.cjs.map +1 -1
  78. package/dist/workflow.d.cts +4 -4
  79. package/dist/workflow.d.ts +4 -4
  80. package/dist/workflow.js +139 -78
  81. package/dist/workflow.js.map +1 -1
  82. package/package.json +17 -33
  83. package/src/adapters/sandbox/daytona/index.ts +25 -48
  84. package/src/adapters/sandbox/daytona/proxy.ts +7 -8
  85. package/src/adapters/sandbox/e2b/README.md +81 -0
  86. package/src/adapters/sandbox/e2b/index.ts +53 -11
  87. package/src/adapters/sandbox/e2b/keep-alive.test.ts +115 -0
  88. package/src/adapters/sandbox/e2b/proxy.ts +3 -2
  89. package/src/adapters/sandbox/e2b/types.ts +34 -2
  90. package/src/adapters/sandbox/inmemory/index.ts +21 -1
  91. package/src/adapters/sandbox/inmemory/proxy.ts +7 -3
  92. package/src/index.ts +1 -1
  93. package/src/lib/activity.ts +5 -0
  94. package/src/lib/sandbox/capability-types.test.ts +859 -0
  95. package/src/lib/sandbox/index.ts +1 -0
  96. package/src/lib/sandbox/manager.ts +187 -31
  97. package/src/lib/sandbox/types.ts +189 -46
  98. package/src/lib/session/index.ts +1 -0
  99. package/src/lib/session/session.integration.test.ts +58 -0
  100. package/src/lib/session/session.ts +109 -50
  101. package/src/lib/session/types.ts +189 -8
  102. package/src/lib/subagent/handler.ts +66 -43
  103. package/src/lib/subagent/subagent.integration.test.ts +2 -0
  104. package/src/lib/subagent/types.ts +492 -16
  105. package/src/lib/subagent/workflow.ts +11 -1
  106. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +158 -0
  107. package/src/lib/tool-router/index.ts +1 -1
  108. package/src/lib/tool-router/with-sandbox.ts +45 -2
  109. package/src/lib/virtual-fs/filesystem.ts +41 -16
  110. package/src/lib/virtual-fs/types.ts +19 -0
  111. package/src/lib/virtual-fs/virtual-fs.test.ts +204 -1
  112. package/src/tools/read-file/handler.test.ts +83 -0
  113. package/src/workflow.ts +3 -0
  114. package/tsup.config.ts +0 -4
  115. package/dist/adapters/sandbox/bedrock/index.cjs +0 -457
  116. package/dist/adapters/sandbox/bedrock/index.cjs.map +0 -1
  117. package/dist/adapters/sandbox/bedrock/index.d.cts +0 -25
  118. package/dist/adapters/sandbox/bedrock/index.d.ts +0 -25
  119. package/dist/adapters/sandbox/bedrock/index.js +0 -454
  120. package/dist/adapters/sandbox/bedrock/index.js.map +0 -1
  121. package/dist/adapters/sandbox/bedrock/workflow.cjs +0 -36
  122. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +0 -1
  123. package/dist/adapters/sandbox/bedrock/workflow.d.cts +0 -29
  124. package/dist/adapters/sandbox/bedrock/workflow.d.ts +0 -29
  125. package/dist/adapters/sandbox/bedrock/workflow.js +0 -34
  126. package/dist/adapters/sandbox/bedrock/workflow.js.map +0 -1
  127. package/dist/types-DAsQ21Rt.d.ts +0 -74
  128. package/dist/types-lm8tMNJQ.d.cts +0 -74
  129. package/dist/types-yx0LzPGn.d.cts +0 -173
  130. package/dist/types-yx0LzPGn.d.ts +0 -173
  131. package/src/adapters/sandbox/bedrock/filesystem.ts +0 -340
  132. package/src/adapters/sandbox/bedrock/index.ts +0 -274
  133. package/src/adapters/sandbox/bedrock/proxy.ts +0 -59
  134. package/src/adapters/sandbox/bedrock/types.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -156,26 +156,6 @@
156
156
  "types": "./dist/adapters/sandbox/e2b/workflow.d.ts",
157
157
  "default": "./dist/adapters/sandbox/e2b/workflow.js"
158
158
  }
159
- },
160
- "./adapters/sandbox/bedrock": {
161
- "import": {
162
- "types": "./dist/adapters/sandbox/bedrock/index.d.ts",
163
- "default": "./dist/adapters/sandbox/bedrock/index.js"
164
- },
165
- "require": {
166
- "types": "./dist/adapters/sandbox/bedrock/index.d.ts",
167
- "default": "./dist/adapters/sandbox/bedrock/index.js"
168
- }
169
- },
170
- "./adapters/sandbox/bedrock/workflow": {
171
- "import": {
172
- "types": "./dist/adapters/sandbox/bedrock/workflow.d.ts",
173
- "default": "./dist/adapters/sandbox/bedrock/workflow.js"
174
- },
175
- "require": {
176
- "types": "./dist/adapters/sandbox/bedrock/workflow.d.ts",
177
- "default": "./dist/adapters/sandbox/bedrock/workflow.js"
178
- }
179
159
  }
180
160
  },
181
161
  "files": [
@@ -220,15 +200,16 @@
220
200
  "node": ">=18"
221
201
  },
222
202
  "devDependencies": {
223
- "@anthropic-ai/sdk": "^0.81.0",
224
- "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
225
- "@daytonaio/sdk": "^0.158.1",
203
+ "@anthropic-ai/sdk": "^0.93.0",
204
+ "@daytonaio/sdk": "^0.171.0",
226
205
  "@e2b/code-interpreter": "^2.3.3",
227
206
  "@eslint/js": "^10.0.1",
228
207
  "@google/genai": "^1.44.0",
229
208
  "@langchain/core": "^1.1.30",
230
- "@temporalio/envconfig": "^1.15.0",
231
- "@temporalio/worker": "^1.15.0",
209
+ "@temporalio/common": "^1.17.0",
210
+ "@temporalio/envconfig": "^1.17.0",
211
+ "@temporalio/worker": "^1.17.0",
212
+ "@temporalio/workflow": "^1.17.0",
232
213
  "@types/node": "^25.3.3",
233
214
  "eslint": "^10.0.2",
234
215
  "husky": "^9.1.7",
@@ -242,18 +223,18 @@
242
223
  },
243
224
  "peerDependencies": {
244
225
  "@anthropic-ai/sdk": ">=0.50.0",
245
- "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
246
226
  "@daytonaio/sdk": ">=0.153.0",
247
227
  "@e2b/code-interpreter": "^2.3.3",
248
228
  "@google/genai": "^1.43.0",
249
229
  "@langchain/core": ">=1.0.0",
230
+ "@temporalio/common": ">=1.16.0 <2.0.0",
231
+ "@temporalio/envconfig": ">=1.16.0 <2.0.0",
232
+ "@temporalio/worker": ">=1.16.0 <2.0.0",
233
+ "@temporalio/workflow": ">=1.16.0 <2.0.0",
250
234
  "ioredis": ">=5.0.0",
251
235
  "just-bash": ">=2.0.0"
252
236
  },
253
237
  "peerDependenciesMeta": {
254
- "@aws-sdk/client-bedrock-agentcore": {
255
- "optional": true
256
- },
257
238
  "@daytonaio/sdk": {
258
239
  "optional": true
259
240
  },
@@ -266,6 +247,12 @@
266
247
  "@langchain/core": {
267
248
  "optional": true
268
249
  },
250
+ "@temporalio/envconfig": {
251
+ "optional": true
252
+ },
253
+ "@temporalio/worker": {
254
+ "optional": true
255
+ },
269
256
  "just-bash": {
270
257
  "optional": true
271
258
  }
@@ -276,9 +263,6 @@
276
263
  },
277
264
  "homepage": "https://github.com/bead-ai/zeitlich#readme",
278
265
  "dependencies": {
279
- "@temporalio/common": "^1.15.0",
280
- "@temporalio/plugin": "^1.15.0",
281
- "@temporalio/workflow": "^1.15.0",
282
266
  "zod": "^4.3.6"
283
267
  }
284
268
  }
@@ -2,16 +2,13 @@ import { Daytona, type Sandbox as DaytonaSdkSandbox } from "@daytonaio/sdk";
2
2
  import type {
3
3
  Sandbox,
4
4
  SandboxCapabilities,
5
+ SandboxCapability,
5
6
  SandboxCreateResult,
6
7
  SandboxProvider,
7
- SandboxSnapshot,
8
8
  ExecOptions,
9
9
  ExecResult,
10
10
  } from "../../../lib/sandbox/types";
11
- import {
12
- SandboxNotFoundError,
13
- SandboxNotSupportedError,
14
- } from "../../../lib/sandbox/types";
11
+ import { SandboxNotFoundError } from "../../../lib/sandbox/types";
15
12
  import { DaytonaSandboxFileSystem } from "./filesystem";
16
13
  import type {
17
14
  DaytonaSandbox,
@@ -64,16 +61,35 @@ class DaytonaSandboxImpl implements Sandbox {
64
61
  // DaytonaSandboxProvider
65
62
  // ============================================================================
66
63
 
67
- export class DaytonaSandboxProvider implements SandboxProvider<
68
- DaytonaSandboxCreateOptions,
69
- DaytonaSandbox
70
- > {
64
+ /**
65
+ * Single source of truth for the Daytona adapter's capability set. Daytona
66
+ * implements only base lifecycle (`create` / `get` / `destroy`); both the
67
+ * type-level `TCaps` (`never`) and the runtime `supportedCapabilities`
68
+ * set fall out of this empty array, so the two surfaces cannot drift.
69
+ */
70
+ const DAYTONA_CAPS = [] as const satisfies readonly SandboxCapability[];
71
+ type DaytonaCaps = (typeof DAYTONA_CAPS)[number]; // → never
72
+
73
+ /**
74
+ * Daytona implements only base sandbox lifecycle (`create` / `get` /
75
+ * `destroy`). Snapshot, restore, fork, pause, and resume are not supported
76
+ * — the type-level capability set is `never`, so calling any of those
77
+ * methods on a Daytona provider, manager, or `SandboxOps` proxy is a
78
+ * compile-time TypeScript error.
79
+ */
80
+ export class DaytonaSandboxProvider
81
+ implements
82
+ SandboxProvider<DaytonaSandboxCreateOptions, DaytonaSandbox, DaytonaCaps>
83
+ {
71
84
  readonly id = "daytona";
72
85
  readonly capabilities: SandboxCapabilities = {
73
86
  filesystem: true,
74
87
  execution: true,
75
88
  persistence: false,
76
89
  };
90
+ readonly supportedCapabilities: ReadonlySet<DaytonaCaps> = new Set(
91
+ DAYTONA_CAPS
92
+ );
77
93
 
78
94
  private client: Daytona;
79
95
  private readonly defaultWorkspaceBase: string;
@@ -140,45 +156,6 @@ export class DaytonaSandboxProvider implements SandboxProvider<
140
156
  // Already gone
141
157
  }
142
158
  }
143
-
144
- async pause(_sandboxId: string, _ttlSeconds?: number): Promise<void> {
145
- throw new SandboxNotSupportedError("pause");
146
- }
147
-
148
- async resume(_sandboxId: string): Promise<void> {
149
- // Daytona sandboxes don't support pause, so resume is a no-op
150
- }
151
-
152
- async fork(
153
- _sandboxId: string,
154
- _options?: DaytonaSandboxCreateOptions
155
- ): Promise<Sandbox> {
156
- throw new Error("Not implemented");
157
- }
158
-
159
- async snapshot(
160
- _sandboxId: string,
161
- _options?: DaytonaSandboxCreateOptions
162
- ): Promise<SandboxSnapshot> {
163
- throw new SandboxNotSupportedError(
164
- "snapshot (use Daytona's native snapshot API directly)"
165
- );
166
- }
167
-
168
- async restore(
169
- _snapshot: SandboxSnapshot,
170
- _options?: DaytonaSandboxCreateOptions
171
- ): Promise<never> {
172
- throw new SandboxNotSupportedError(
173
- "restore (use Daytona's native snapshot API directly)"
174
- );
175
- }
176
-
177
- async deleteSnapshot(_snapshot: SandboxSnapshot): Promise<void> {
178
- throw new SandboxNotSupportedError(
179
- "deleteSnapshot (use Daytona's native snapshot API directly)"
180
- );
181
- }
182
159
  }
183
160
 
184
161
  // Re-exports
@@ -10,6 +10,11 @@
10
10
  * By default the scope is derived from `workflowInfo().workflowType`,
11
11
  * so activities are automatically namespaced per workflow.
12
12
  *
13
+ * Daytona only exposes base sandbox lifecycle (`create`/`destroy`) — the
14
+ * returned proxy is typed with `TCaps = never`, so calling
15
+ * `pauseSandbox` / `snapshotSandbox` / `forkSandbox` / etc. on it is a
16
+ * TypeScript error rather than a runtime throw.
17
+ *
13
18
  * @example
14
19
  * ```typescript
15
20
  * import { proxyDaytonaSandboxOps } from 'zeitlich/adapters/sandbox/daytona/workflow';
@@ -26,7 +31,7 @@ const ADAPTER_PREFIX = "daytona";
26
31
  export function proxyDaytonaSandboxOps(
27
32
  scope?: string,
28
33
  options?: Parameters<typeof proxyActivities>[0]
29
- ): SandboxOps {
34
+ ): SandboxOps<DaytonaSandboxCreateOptions, unknown, never> {
30
35
  const resolvedScope = scope ?? workflowInfo().workflowType;
31
36
 
32
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -49,11 +54,5 @@ export function proxyDaytonaSandboxOps(
49
54
  return {
50
55
  createSandbox: acts[p("createSandbox")],
51
56
  destroySandbox: acts[p("destroySandbox")],
52
- pauseSandbox: acts[p("pauseSandbox")],
53
- resumeSandbox: acts[p("resumeSandbox")],
54
- snapshotSandbox: acts[p("snapshotSandbox")],
55
- restoreSandbox: acts[p("restoreSandbox")],
56
- deleteSandboxSnapshot: acts[p("deleteSandboxSnapshot")],
57
- forkSandbox: acts[p("forkSandbox")],
58
- } as SandboxOps<DaytonaSandboxCreateOptions>;
57
+ } as SandboxOps<DaytonaSandboxCreateOptions, unknown, never>;
59
58
  }
@@ -0,0 +1,81 @@
1
+ # E2B Sandbox Adapter
2
+
3
+ Adapter that exposes [E2B](https://e2b.dev/) cloud sandboxes through the
4
+ standard `SandboxProvider` interface used by the rest of Zeitlich.
5
+
6
+ ## Configuration
7
+
8
+ `E2bSandboxProvider` accepts an `E2bSandboxConfig` (provider-level defaults).
9
+ Per-create overrides may be passed to `create()` via `E2bSandboxCreateOptions`.
10
+
11
+ ```ts
12
+ import { E2bSandboxProvider } from "zeitlich/adapters/sandbox/e2b";
13
+
14
+ const provider = new E2bSandboxProvider({
15
+ template: "my-template",
16
+ timeoutMs: 15 * 60 * 1000, // kill-on-abandon safety net
17
+ keepAliveMs: 15 * 60 * 1000, // refreshed on every tool call
18
+ });
19
+ ```
20
+
21
+ ## Keep-alive pattern
22
+
23
+ E2B's `timeoutMs` on `Sandbox.create` is a **sandbox lifetime**, not an idle
24
+ timeout: when it elapses, E2B kills the sandbox regardless of activity. Long
25
+ agent loops (LLM thinking + many tool calls) can outlive that window, and the
26
+ next tool call hits `Sandbox.connect(sandboxId)` and surfaces a
27
+ `SandboxNotFoundError` mid-run.
28
+
29
+ `keepAliveMs` solves this without giving up the kill-on-abandon safety net.
30
+ When set, every call to `provider.get(sandboxId)` passes
31
+ `{ timeoutMs: keepAliveMs }` to `Sandbox.connect()`. Per the E2B SDK's
32
+ `SandboxConnectOpts.timeoutMs` JSDoc:
33
+
34
+ > For running sandboxes, the timeout will update only if the new timeout is
35
+ > longer than the existing one.
36
+
37
+ So `connect()` with a `timeoutMs` is **monotonic**: it never shrinks the
38
+ lifetime of a running sandbox. Pick `keepAliveMs` as the **full per-call
39
+ refresh window** you want — passing a value smaller than the time remaining
40
+ is a no-op rather than a shrink, but you should still pick the value with
41
+ "every tool call should give me at least this much headroom" in mind, not
42
+ "floor to add".
43
+
44
+ `provider.get()` is invoked exactly once per tool call by `withSandbox`, so:
45
+
46
+ - An active session's tool calls each refresh the lifetime to at least
47
+ `keepAliveMs`. The sandbox cannot be killed mid-run as long as tools are
48
+ still firing — conceptually this is the sandbox equivalent of a Temporal
49
+ activity heartbeat.
50
+ - An abandoned sandbox still dies `keepAliveMs` after the last tool call. The
51
+ existing kill-on-timeout safety net is preserved.
52
+ - Consumers can drop `timeoutMs` back down to short, safe values (e.g.
53
+ 15 minutes) without tuning against worst-case run length.
54
+
55
+ ### Recommended usage
56
+
57
+ - Set `timeoutMs` to a value that bounds how long a sandbox can sit unused
58
+ after the consumer abnormally terminates (worker crash, workflow terminated,
59
+ `workflowRunTimeout`). This is your **abandon safety net**.
60
+ - Set `keepAliveMs` to your **per-call refresh window** — typically the same
61
+ value as `timeoutMs`, or shorter if you want sandboxes to be reaped sooner
62
+ after the last tool call.
63
+
64
+ ### Provider-level only
65
+
66
+ `keepAliveMs` is a provider-construction-time config. There is intentionally
67
+ no per-create override: every sandbox managed by the provider refreshes by
68
+ the same amount on each `get()`. If a real use case for per-sandbox refresh
69
+ windows ever shows up we can add it without breaking changes.
70
+
71
+ ### When connect-with-options is not enough
72
+
73
+ If you ever need to **shrink** a sandbox's remaining lifetime (e.g. force an
74
+ early reap), `connect()` won't do it because of the monotonic-extend rule
75
+ above. Use `Sandbox.setTimeout(timeoutMs)` or the static
76
+ `SandboxApi.setTimeout(sandboxId, timeoutMs)` instead — those can extend or
77
+ reduce.
78
+
79
+ If E2B ever changes the semantics of `Sandbox.connect(sandboxId, { timeoutMs })`
80
+ so it stops extending a running sandbox's lifetime at all, `setTimeout` is
81
+ also a drop-in replacement for the call site in `provider.get()`.
@@ -1,7 +1,12 @@
1
- import { Sandbox as E2bSdkSandbox } from "@e2b/code-interpreter";
1
+ import {
2
+ NotFoundError as E2bNotFoundError,
3
+ Sandbox as E2bSdkSandbox,
4
+ SandboxNotFoundError as E2bSandboxNotFoundError,
5
+ } from "@e2b/code-interpreter";
2
6
  import type {
3
7
  Sandbox,
4
8
  SandboxCapabilities,
9
+ SandboxCapability,
5
10
  SandboxCreateResult,
6
11
  SandboxProvider,
7
12
  SandboxSnapshot,
@@ -19,6 +24,20 @@ import type {
19
24
  E2bSandboxCreateOptions,
20
25
  } from "./types";
21
26
 
27
+ /**
28
+ * True iff `err` is the E2B SDK's "this sandbox doesn't exist (anymore)"
29
+ * signal. We narrow to `SandboxNotFoundError` (the canonical class) and to
30
+ * its deprecated parent `NotFoundError` as a defensive fallback — older
31
+ * SDK paths still throw the parent for sandbox-not-found cases. Any other
32
+ * error (auth failure, network blip, 5xx, validation) is propagated
33
+ * unchanged so callers can react to it specifically.
34
+ */
35
+ function isE2bSandboxNotFound(err: unknown): boolean {
36
+ return (
37
+ err instanceof E2bSandboxNotFoundError || err instanceof E2bNotFoundError
38
+ );
39
+ }
40
+
22
41
  // ============================================================================
23
42
  // E2bSandbox
24
43
  // ============================================================================
@@ -62,20 +81,35 @@ class E2bSandboxImpl implements Sandbox {
62
81
  // E2bSandboxProvider
63
82
  // ============================================================================
64
83
 
65
- export class E2bSandboxProvider implements SandboxProvider<
66
- E2bSandboxCreateOptions,
67
- E2bSandbox
68
- > {
84
+ /**
85
+ * Single source of truth for the E2B adapter's capability set. Both the
86
+ * runtime `supportedCapabilities` set and the type-level `TCaps` flow
87
+ * out of this array, so the two surfaces cannot drift.
88
+ */
89
+ export const E2B_CAPS = [
90
+ "pause",
91
+ "resume",
92
+ "snapshot",
93
+ "restore",
94
+ "fork",
95
+ ] as const satisfies readonly SandboxCapability[];
96
+ export type E2bCaps = (typeof E2B_CAPS)[number];
97
+
98
+ export class E2bSandboxProvider
99
+ implements SandboxProvider<E2bSandboxCreateOptions, E2bSandbox, E2bCaps>
100
+ {
69
101
  readonly id = "e2b";
70
102
  readonly capabilities: SandboxCapabilities = {
71
103
  filesystem: true,
72
104
  execution: true,
73
105
  persistence: true,
74
106
  };
107
+ readonly supportedCapabilities: ReadonlySet<E2bCaps> = new Set(E2B_CAPS);
75
108
 
76
109
  private readonly defaultTemplate?: string;
77
110
  private readonly defaultWorkspaceBase: string;
78
111
  private readonly defaultTimeoutMs?: number;
112
+ private readonly defaultKeepAliveMs?: number;
79
113
  private readonly defaultAllowInternetAccess?: boolean;
80
114
  private readonly defaultNetwork?: E2bSandboxConfig["network"];
81
115
  private readonly defaultMetadata?: E2bSandboxConfig["metadata"];
@@ -85,6 +119,7 @@ export class E2bSandboxProvider implements SandboxProvider<
85
119
  this.defaultTemplate = config?.template;
86
120
  this.defaultWorkspaceBase = config?.workspaceBase ?? "/home/user";
87
121
  this.defaultTimeoutMs = config?.timeoutMs;
122
+ this.defaultKeepAliveMs = config?.keepAliveMs;
88
123
  this.defaultAllowInternetAccess = config?.allowInternetAccess;
89
124
  this.defaultNetwork = config?.network;
90
125
  this.defaultMetadata = config?.metadata;
@@ -120,15 +155,22 @@ export class E2bSandboxProvider implements SandboxProvider<
120
155
  }
121
156
 
122
157
  async get(sandboxId: string): Promise<E2bSandbox> {
158
+ const keepAliveMs = this.defaultKeepAliveMs;
123
159
  try {
124
- const sdkSandbox = await E2bSdkSandbox.connect(sandboxId);
160
+ const sdkSandbox =
161
+ keepAliveMs !== undefined
162
+ ? await E2bSdkSandbox.connect(sandboxId, { timeoutMs: keepAliveMs })
163
+ : await E2bSdkSandbox.connect(sandboxId);
125
164
  return new E2bSandboxImpl(
126
165
  sandboxId,
127
166
  sdkSandbox,
128
167
  this.defaultWorkspaceBase
129
168
  );
130
- } catch {
131
- throw new SandboxNotFoundError(sandboxId);
169
+ } catch (err) {
170
+ if (isE2bSandboxNotFound(err)) {
171
+ throw new SandboxNotFoundError(sandboxId);
172
+ }
173
+ throw err;
132
174
  }
133
175
  }
134
176
 
@@ -137,7 +179,7 @@ export class E2bSandboxProvider implements SandboxProvider<
137
179
  const sdkSandbox = await E2bSdkSandbox.connect(sandboxId);
138
180
  await sdkSandbox.kill();
139
181
  } catch {
140
- // Already gone or not found
182
+ // Already gone or not found — destroy is idempotent.
141
183
  }
142
184
  }
143
185
 
@@ -166,7 +208,7 @@ export class E2bSandboxProvider implements SandboxProvider<
166
208
  async restore(
167
209
  snapshot: SandboxSnapshot,
168
210
  options?: E2bSandboxCreateOptions
169
- ): Promise<Sandbox> {
211
+ ): Promise<E2bSandbox> {
170
212
  const data = snapshot.data as { snapshotId?: string } | null;
171
213
  if (!data?.snapshotId) {
172
214
  throw new SandboxNotSupportedError(
@@ -195,7 +237,7 @@ export class E2bSandboxProvider implements SandboxProvider<
195
237
  async fork(
196
238
  sandboxId: string,
197
239
  options?: E2bSandboxCreateOptions
198
- ): Promise<Sandbox> {
240
+ ): Promise<E2bSandbox> {
199
241
  const { snapshotId } = await E2bSdkSandbox.createSnapshot(sandboxId);
200
242
  const sdkOpts = this.buildSdkCreateOpts(options);
201
243
  const sdkSandbox = await E2bSdkSandbox.create(snapshotId, sdkOpts);
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import {
3
+ Sandbox as E2bSdkSandbox,
4
+ SandboxNotFoundError as E2bSandboxNotFoundError,
5
+ } from "@e2b/code-interpreter";
6
+ import { E2bSandboxProvider } from "./index";
7
+ import { SandboxNotFoundError } from "../../../lib/sandbox/types";
8
+
9
+ vi.mock("@e2b/code-interpreter", () => {
10
+ class FakeSdkSandbox {
11
+ static create = vi.fn();
12
+ static connect = vi.fn();
13
+ static createSnapshot = vi.fn();
14
+ static deleteSnapshot = vi.fn();
15
+ sandboxId: string;
16
+ constructor(sandboxId: string) {
17
+ this.sandboxId = sandboxId;
18
+ }
19
+ commands = { run: vi.fn() };
20
+ files = {};
21
+ async kill() {}
22
+ async pause() {}
23
+ }
24
+ // Mirror the real SDK error class hierarchy: SandboxNotFoundError extends
25
+ // (deprecated) NotFoundError extends SandboxError extends Error.
26
+ class FakeSandboxError extends Error {}
27
+ class FakeNotFoundError extends FakeSandboxError {}
28
+ class FakeSandboxNotFoundError extends FakeNotFoundError {}
29
+ return {
30
+ Sandbox: FakeSdkSandbox,
31
+ SandboxError: FakeSandboxError,
32
+ NotFoundError: FakeNotFoundError,
33
+ SandboxNotFoundError: FakeSandboxNotFoundError,
34
+ };
35
+ });
36
+
37
+ const sdk = E2bSdkSandbox as unknown as {
38
+ create: ReturnType<typeof vi.fn>;
39
+ connect: ReturnType<typeof vi.fn>;
40
+ createSnapshot: ReturnType<typeof vi.fn>;
41
+ };
42
+
43
+ function makeFakeSdkSandbox(id = "sbx-1") {
44
+ return {
45
+ sandboxId: id,
46
+ commands: { run: vi.fn() },
47
+ files: {},
48
+ kill: vi.fn(),
49
+ pause: vi.fn(),
50
+ };
51
+ }
52
+
53
+ describe("E2bSandboxProvider keep-alive", () => {
54
+ beforeEach(() => {
55
+ sdk.create.mockReset();
56
+ sdk.connect.mockReset();
57
+ sdk.createSnapshot.mockReset();
58
+ });
59
+
60
+ it("forwards timeoutMs to connect() when keepAliveMs is configured at the provider level", async () => {
61
+ const fake = makeFakeSdkSandbox();
62
+ sdk.connect.mockResolvedValue(fake);
63
+
64
+ const provider = new E2bSandboxProvider({ keepAliveMs: 15 * 60 * 1000 });
65
+ const sandbox = await provider.get("sbx-1");
66
+
67
+ expect(sandbox.id).toBe("sbx-1");
68
+ expect(sdk.connect).toHaveBeenCalledTimes(1);
69
+ expect(sdk.connect).toHaveBeenCalledWith("sbx-1", {
70
+ timeoutMs: 15 * 60 * 1000,
71
+ });
72
+ });
73
+
74
+ it("omits timeoutMs from connect() when keepAliveMs is not configured", async () => {
75
+ const fake = makeFakeSdkSandbox();
76
+ sdk.connect.mockResolvedValue(fake);
77
+
78
+ const provider = new E2bSandboxProvider();
79
+ await provider.get("sbx-1");
80
+
81
+ expect(sdk.connect).toHaveBeenCalledTimes(1);
82
+ expect(sdk.connect).toHaveBeenCalledWith("sbx-1");
83
+ });
84
+
85
+ it("uses provider-level keepAliveMs for every sandbox managed by the provider", async () => {
86
+ const fake = makeFakeSdkSandbox("sbx-default");
87
+ sdk.connect.mockResolvedValue(fake);
88
+
89
+ const provider = new E2bSandboxProvider({ keepAliveMs: 60_000 });
90
+ await provider.get("sbx-default");
91
+
92
+ expect(sdk.connect).toHaveBeenCalledWith("sbx-default", {
93
+ timeoutMs: 60_000,
94
+ });
95
+ });
96
+
97
+ it("translates the SDK's SandboxNotFoundError into our SandboxNotFoundError", async () => {
98
+ sdk.connect.mockRejectedValue(
99
+ new E2bSandboxNotFoundError("sandbox missing-sbx not found")
100
+ );
101
+
102
+ const provider = new E2bSandboxProvider({ keepAliveMs: 60_000 });
103
+ await expect(provider.get("missing-sbx")).rejects.toBeInstanceOf(
104
+ SandboxNotFoundError
105
+ );
106
+ });
107
+
108
+ it("propagates non-not-found connect() errors unchanged (auth, network, 5xx)", async () => {
109
+ const transient = new Error("ECONNRESET: socket hang up");
110
+ sdk.connect.mockRejectedValue(transient);
111
+
112
+ const provider = new E2bSandboxProvider({ keepAliveMs: 60_000 });
113
+ await expect(provider.get("sbx-1")).rejects.toBe(transient);
114
+ });
115
+ });
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { proxyActivities, workflowInfo } from "@temporalio/workflow";
21
21
  import type { SandboxOps } from "../../../lib/sandbox/types";
22
+ import type { E2bCaps } from "./index";
22
23
  import type { E2bSandboxCreateOptions } from "./types";
23
24
 
24
25
  const ADAPTER_PREFIX = "e2b";
@@ -26,7 +27,7 @@ const ADAPTER_PREFIX = "e2b";
26
27
  export function proxyE2bSandboxOps(
27
28
  scope?: string,
28
29
  options?: Parameters<typeof proxyActivities>[0]
29
- ): SandboxOps {
30
+ ): SandboxOps<E2bSandboxCreateOptions, unknown, E2bCaps> {
30
31
  const resolvedScope = scope ?? workflowInfo().workflowType;
31
32
 
32
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -55,5 +56,5 @@ export function proxyE2bSandboxOps(
55
56
  restoreSandbox: acts[p("restoreSandbox")],
56
57
  deleteSandboxSnapshot: acts[p("deleteSandboxSnapshot")],
57
58
  forkSandbox: acts[p("forkSandbox")],
58
- } as SandboxOps<E2bSandboxCreateOptions>;
59
+ } as SandboxOps<E2bSandboxCreateOptions, unknown, E2bCaps>;
59
60
  }
@@ -19,8 +19,35 @@ export interface E2bSandboxConfig {
19
19
  template?: string;
20
20
  /** Default working directory inside the sandbox */
21
21
  workspaceBase?: string;
22
- /** Sandbox idle timeout in milliseconds */
22
+ /**
23
+ * Sandbox lifetime in milliseconds. Despite the name, this is **not** an
24
+ * idle timeout: E2B kills the sandbox once this many milliseconds elapse
25
+ * from creation regardless of activity. Pair with {@link keepAliveMs} to
26
+ * refresh the lifetime on every `provider.get()` call so that this value
27
+ * acts as a kill-on-abandon safety net rather than a hard cap on run
28
+ * length.
29
+ */
23
30
  timeoutMs?: number;
31
+ /**
32
+ * If set, every call to `provider.get(sandboxId)` passes
33
+ * `{ timeoutMs: keepAliveMs }` to `Sandbox.connect()`, refreshing the
34
+ * sandbox lifetime on each tool invocation. The provider-level
35
+ * `timeoutMs` then acts as a kill-on-abandon safety net rather than a
36
+ * hard cap on run length.
37
+ *
38
+ * E2B's `Sandbox.connect()` is monotonic for running sandboxes: per the
39
+ * SDK's `SandboxConnectOpts.timeoutMs` doc, "the timeout will update
40
+ * only if the new timeout is longer than the existing one". Pick
41
+ * `keepAliveMs` as the full per-call refresh window you want; passing a
42
+ * value smaller than the time remaining is a no-op rather than a
43
+ * shrink. (If you ever need to shrink, use `Sandbox.setTimeout` /
44
+ * `SandboxApi.setTimeout`, which can extend or reduce.)
45
+ *
46
+ * Per-sandbox overrides are intentionally not exposed — this is a
47
+ * provider-level config only. Every sandbox managed by the provider
48
+ * refreshes by the same amount on each `get()`.
49
+ */
50
+ keepAliveMs?: number;
24
51
  /** Default outbound internet access policy */
25
52
  allowInternetAccess?: boolean;
26
53
  /** Default outbound network allow/deny rules */
@@ -34,6 +61,11 @@ export interface E2bSandboxConfig {
34
61
  export interface E2bSandboxCreateOptions extends SandboxCreateOptions {
35
62
  /** Sandbox template name or ID — overrides the provider default */
36
63
  template?: string;
37
- /** Sandbox idle timeout in milliseconds — overrides the provider default */
64
+ /**
65
+ * Sandbox lifetime in milliseconds — overrides the provider default. See
66
+ * {@link E2bSandboxConfig.timeoutMs} for the full semantics; pair with
67
+ * the provider-level `keepAliveMs` to refresh on every `provider.get()`
68
+ * call.
69
+ */
38
70
  timeoutMs?: number;
39
71
  }
@@ -8,6 +8,7 @@ import {
8
8
  import type {
9
9
  Sandbox,
10
10
  SandboxCapabilities,
11
+ SandboxCapability,
11
12
  SandboxCreateOptions,
12
13
  SandboxCreateResult,
13
14
  SandboxFileSystem,
@@ -133,13 +134,32 @@ class InMemorySandboxImpl implements Sandbox {
133
134
  // InMemorySandboxProvider
134
135
  // ============================================================================
135
136
 
136
- export class InMemorySandboxProvider implements SandboxProvider {
137
+ /**
138
+ * Single source of truth for the in-memory adapter's capability set. The
139
+ * runtime `supportedCapabilities` set and the type-level `TCaps` are both
140
+ * derived from this array, so the two surfaces cannot drift.
141
+ */
142
+ export const IN_MEMORY_CAPS = [
143
+ "pause",
144
+ "resume",
145
+ "snapshot",
146
+ "restore",
147
+ "fork",
148
+ ] as const satisfies readonly SandboxCapability[];
149
+ export type InMemoryCaps = (typeof IN_MEMORY_CAPS)[number];
150
+
151
+ export class InMemorySandboxProvider
152
+ implements SandboxProvider<SandboxCreateOptions, Sandbox, InMemoryCaps>
153
+ {
137
154
  readonly id = "inMemory";
138
155
  readonly capabilities: SandboxCapabilities = {
139
156
  filesystem: true,
140
157
  execution: true,
141
158
  persistence: true,
142
159
  };
160
+ readonly supportedCapabilities: ReadonlySet<InMemoryCaps> = new Set(
161
+ IN_MEMORY_CAPS
162
+ );
143
163
 
144
164
  private sandboxes = new Map<string, InMemorySandboxImpl>();
145
165