zeitlich 0.2.40 → 0.2.41
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.
- package/README.md +12 -1
- package/dist/{activities-CULxRzJ1.d.ts → activities-D_g13S3y.d.ts} +2 -2
- package/dist/{activities-CvUrG3YG.d.cts → activities-qUflxmfS.d.cts} +2 -2
- package/dist/adapters/sandbox/e2b/index.cjs +12 -3
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +35 -2
- package/dist/adapters/sandbox/e2b/index.d.ts +35 -2
- package/dist/adapters/sandbox/e2b/index.js +13 -4
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +5 -5
- package/dist/adapters/thread/anthropic/index.d.ts +5 -5
- package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
- package/dist/adapters/thread/google-genai/index.d.cts +5 -5
- package/dist/adapters/thread/google-genai/index.d.ts +5 -5
- package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
- package/dist/adapters/thread/langchain/index.d.cts +5 -5
- package/dist/adapters/thread/langchain/index.d.ts +5 -5
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
- package/dist/index.cjs +92 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -9
- package/dist/index.d.ts +35 -9
- package/dist/index.js +93 -50
- package/dist/index.js.map +1 -1
- package/dist/{proxy-wZufFfBh.d.ts → proxy-BbcgoXg1.d.ts} +1 -1
- package/dist/{proxy-5EbwzaY4.d.cts → proxy-D7mvDEO6.d.cts} +1 -1
- package/dist/{thread-manager-BqBAIsED.d.ts → thread-manager-CTXPCu9W.d.ts} +2 -2
- package/dist/{thread-manager-BNiIt5r8.d.ts → thread-manager-Dqstsw4i.d.ts} +2 -2
- package/dist/{thread-manager-BoN5DOvG.d.cts → thread-manager-DrWfVjlj.d.cts} +2 -2
- package/dist/{thread-manager-DF8WuCRs.d.cts → thread-manager-cLhDhRRc.d.cts} +2 -2
- package/dist/{types-DeQH84C_.d.ts → types-BqTmyH31.d.ts} +42 -3
- package/dist/{types-CuISs0Ub.d.cts → types-CdvcmXb6.d.cts} +1 -1
- package/dist/{types-C7OoY7h8.d.ts → types-CjF1_Idx.d.ts} +1 -1
- package/dist/{types-Cn2r3ol3.d.cts → types-DjaQKUJx.d.cts} +42 -3
- package/dist/{workflow-DhplIN65.d.cts → workflow-CuqxgS6X.d.cts} +1 -1
- package/dist/{workflow-C2MZZj5K.d.ts → workflow-N1MNDoul.d.ts} +1 -1
- package/dist/workflow.cjs +39 -31
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +40 -32
- package/dist/workflow.js.map +1 -1
- package/package.json +15 -6
- package/src/adapters/sandbox/e2b/README.md +81 -0
- package/src/adapters/sandbox/e2b/index.ts +32 -5
- package/src/adapters/sandbox/e2b/keep-alive.test.ts +115 -0
- package/src/adapters/sandbox/e2b/types.ts +34 -2
- package/src/index.ts +1 -1
- package/src/lib/session/session.integration.test.ts +58 -0
- package/src/lib/session/session.ts +12 -15
- package/src/lib/session/types.ts +8 -3
- package/src/lib/subagent/subagent.integration.test.ts +2 -0
- package/src/lib/subagent/types.ts +8 -0
- package/src/lib/subagent/workflow.ts +11 -1
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +158 -0
- package/src/lib/tool-router/index.ts +1 -1
- package/src/lib/tool-router/with-sandbox.ts +45 -2
- package/src/lib/virtual-fs/filesystem.ts +41 -16
- package/src/lib/virtual-fs/types.ts +19 -0
- package/src/lib/virtual-fs/virtual-fs.test.ts +204 -1
- package/src/tools/read-file/handler.test.ts +83 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeitlich",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -227,8 +227,10 @@
|
|
|
227
227
|
"@eslint/js": "^10.0.1",
|
|
228
228
|
"@google/genai": "^1.44.0",
|
|
229
229
|
"@langchain/core": "^1.1.30",
|
|
230
|
-
"@temporalio/
|
|
231
|
-
"@temporalio/
|
|
230
|
+
"@temporalio/common": "1.16.0",
|
|
231
|
+
"@temporalio/envconfig": "1.16.0",
|
|
232
|
+
"@temporalio/worker": "1.16.0",
|
|
233
|
+
"@temporalio/workflow": "1.16.0",
|
|
232
234
|
"@types/node": "^25.3.3",
|
|
233
235
|
"eslint": "^10.0.2",
|
|
234
236
|
"husky": "^9.1.7",
|
|
@@ -247,6 +249,10 @@
|
|
|
247
249
|
"@e2b/code-interpreter": "^2.3.3",
|
|
248
250
|
"@google/genai": "^1.43.0",
|
|
249
251
|
"@langchain/core": ">=1.0.0",
|
|
252
|
+
"@temporalio/common": ">=1.16.0 <2.0.0",
|
|
253
|
+
"@temporalio/envconfig": ">=1.16.0 <2.0.0",
|
|
254
|
+
"@temporalio/worker": ">=1.16.0 <2.0.0",
|
|
255
|
+
"@temporalio/workflow": ">=1.16.0 <2.0.0",
|
|
250
256
|
"ioredis": ">=5.0.0",
|
|
251
257
|
"just-bash": ">=2.0.0"
|
|
252
258
|
},
|
|
@@ -266,6 +272,12 @@
|
|
|
266
272
|
"@langchain/core": {
|
|
267
273
|
"optional": true
|
|
268
274
|
},
|
|
275
|
+
"@temporalio/envconfig": {
|
|
276
|
+
"optional": true
|
|
277
|
+
},
|
|
278
|
+
"@temporalio/worker": {
|
|
279
|
+
"optional": true
|
|
280
|
+
},
|
|
269
281
|
"just-bash": {
|
|
270
282
|
"optional": true
|
|
271
283
|
}
|
|
@@ -276,9 +288,6 @@
|
|
|
276
288
|
},
|
|
277
289
|
"homepage": "https://github.com/bead-ai/zeitlich#readme",
|
|
278
290
|
"dependencies": {
|
|
279
|
-
"@temporalio/common": "^1.15.0",
|
|
280
|
-
"@temporalio/plugin": "^1.15.0",
|
|
281
|
-
"@temporalio/workflow": "^1.15.0",
|
|
282
291
|
"zod": "^4.3.6"
|
|
283
292
|
}
|
|
284
293
|
}
|
|
@@ -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,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
@@ -19,6 +23,20 @@ import type {
|
|
|
19
23
|
E2bSandboxCreateOptions,
|
|
20
24
|
} from "./types";
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* True iff `err` is the E2B SDK's "this sandbox doesn't exist (anymore)"
|
|
28
|
+
* signal. We narrow to `SandboxNotFoundError` (the canonical class) and to
|
|
29
|
+
* its deprecated parent `NotFoundError` as a defensive fallback — older
|
|
30
|
+
* SDK paths still throw the parent for sandbox-not-found cases. Any other
|
|
31
|
+
* error (auth failure, network blip, 5xx, validation) is propagated
|
|
32
|
+
* unchanged so callers can react to it specifically.
|
|
33
|
+
*/
|
|
34
|
+
function isE2bSandboxNotFound(err: unknown): boolean {
|
|
35
|
+
return (
|
|
36
|
+
err instanceof E2bSandboxNotFoundError || err instanceof E2bNotFoundError
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
// ============================================================================
|
|
23
41
|
// E2bSandbox
|
|
24
42
|
// ============================================================================
|
|
@@ -76,6 +94,7 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
76
94
|
private readonly defaultTemplate?: string;
|
|
77
95
|
private readonly defaultWorkspaceBase: string;
|
|
78
96
|
private readonly defaultTimeoutMs?: number;
|
|
97
|
+
private readonly defaultKeepAliveMs?: number;
|
|
79
98
|
private readonly defaultAllowInternetAccess?: boolean;
|
|
80
99
|
private readonly defaultNetwork?: E2bSandboxConfig["network"];
|
|
81
100
|
private readonly defaultMetadata?: E2bSandboxConfig["metadata"];
|
|
@@ -85,6 +104,7 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
85
104
|
this.defaultTemplate = config?.template;
|
|
86
105
|
this.defaultWorkspaceBase = config?.workspaceBase ?? "/home/user";
|
|
87
106
|
this.defaultTimeoutMs = config?.timeoutMs;
|
|
107
|
+
this.defaultKeepAliveMs = config?.keepAliveMs;
|
|
88
108
|
this.defaultAllowInternetAccess = config?.allowInternetAccess;
|
|
89
109
|
this.defaultNetwork = config?.network;
|
|
90
110
|
this.defaultMetadata = config?.metadata;
|
|
@@ -120,15 +140,22 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
async get(sandboxId: string): Promise<E2bSandbox> {
|
|
143
|
+
const keepAliveMs = this.defaultKeepAliveMs;
|
|
123
144
|
try {
|
|
124
|
-
const sdkSandbox =
|
|
145
|
+
const sdkSandbox =
|
|
146
|
+
keepAliveMs !== undefined
|
|
147
|
+
? await E2bSdkSandbox.connect(sandboxId, { timeoutMs: keepAliveMs })
|
|
148
|
+
: await E2bSdkSandbox.connect(sandboxId);
|
|
125
149
|
return new E2bSandboxImpl(
|
|
126
150
|
sandboxId,
|
|
127
151
|
sdkSandbox,
|
|
128
152
|
this.defaultWorkspaceBase
|
|
129
153
|
);
|
|
130
|
-
} catch {
|
|
131
|
-
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (isE2bSandboxNotFound(err)) {
|
|
156
|
+
throw new SandboxNotFoundError(sandboxId);
|
|
157
|
+
}
|
|
158
|
+
throw err;
|
|
132
159
|
}
|
|
133
160
|
}
|
|
134
161
|
|
|
@@ -137,7 +164,7 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
137
164
|
const sdkSandbox = await E2bSdkSandbox.connect(sandboxId);
|
|
138
165
|
await sdkSandbox.kill();
|
|
139
166
|
} catch {
|
|
140
|
-
// Already gone or not found
|
|
167
|
+
// Already gone or not found — destroy is idempotent.
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
170
|
|
|
@@ -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,8 +19,35 @@ export interface E2bSandboxConfig {
|
|
|
19
19
|
template?: string;
|
|
20
20
|
/** Default working directory inside the sandbox */
|
|
21
21
|
workspaceBase?: string;
|
|
22
|
-
/**
|
|
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
|
-
/**
|
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type { ModelInvoker, ModelInvokerConfig } from "./lib/model";
|
|
|
45
45
|
|
|
46
46
|
// Activity-side handler wrappers
|
|
47
47
|
export { withAutoAppend, withSandbox } from "./lib/tool-router";
|
|
48
|
-
export type { SandboxContext } from "./lib/tool-router";
|
|
48
|
+
export type { SandboxContext, WithSandboxOptions } from "./lib/tool-router";
|
|
49
49
|
|
|
50
50
|
// Activity-side wrappers (requires Temporal client)
|
|
51
51
|
export {
|
|
@@ -895,6 +895,64 @@ describe("createSession integration", () => {
|
|
|
895
895
|
});
|
|
896
896
|
});
|
|
897
897
|
|
|
898
|
+
it("embeds skill resourceContents on synthetic file tree entries via inlineContent", async () => {
|
|
899
|
+
const { ops } = createMockThreadOps();
|
|
900
|
+
|
|
901
|
+
const session = await createSession({
|
|
902
|
+
agentName: "TestAgent",
|
|
903
|
+
thread: { mode: "new", threadId: "thread-1" },
|
|
904
|
+
runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
|
|
905
|
+
threadOps: ops,
|
|
906
|
+
buildContextMessage: () => "go",
|
|
907
|
+
virtualFs: { ctx: { projectId: "p" } },
|
|
908
|
+
virtualFsOps: {
|
|
909
|
+
resolveFileTree: async () => ({ fileTree: [] }),
|
|
910
|
+
},
|
|
911
|
+
skills: [
|
|
912
|
+
{
|
|
913
|
+
name: "test-skill",
|
|
914
|
+
description: "Test",
|
|
915
|
+
instructions: "Do test",
|
|
916
|
+
location: "/skills/test-skill",
|
|
917
|
+
resourceContents: {
|
|
918
|
+
"references/alpha.md": "# Alpha doc",
|
|
919
|
+
"references/beta.md": "# Beta doc",
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
hooks: {
|
|
924
|
+
onSessionStart: async () => {},
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
const stateManager = createAgentStateManager({
|
|
929
|
+
initialState: { systemPrompt: "test" },
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
await session.runSession({ stateManager });
|
|
933
|
+
|
|
934
|
+
const capturedFileTree = stateManager.getCurrentState().fileTree;
|
|
935
|
+
expect(Array.isArray(capturedFileTree)).toBe(true);
|
|
936
|
+
const entries = capturedFileTree as Array<{
|
|
937
|
+
path: string;
|
|
938
|
+
inlineContent?: string;
|
|
939
|
+
}>;
|
|
940
|
+
|
|
941
|
+
const alpha = entries.find(
|
|
942
|
+
(e) => e.path === "/skills/test-skill/references/alpha.md"
|
|
943
|
+
);
|
|
944
|
+
const beta = entries.find(
|
|
945
|
+
(e) => e.path === "/skills/test-skill/references/beta.md"
|
|
946
|
+
);
|
|
947
|
+
expect(alpha?.inlineContent).toBe("# Alpha doc");
|
|
948
|
+
expect(beta?.inlineContent).toBe("# Beta doc");
|
|
949
|
+
|
|
950
|
+
expect(stateManager.getCurrentState().inlineFiles).toEqual({
|
|
951
|
+
"/skills/test-skill/references/alpha.md": "# Alpha doc",
|
|
952
|
+
"/skills/test-skill/references/beta.md": "# Beta doc",
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
898
956
|
it("does not pass initialFiles when skills have no resourceContents", async () => {
|
|
899
957
|
const { ops } = createMockThreadOps();
|
|
900
958
|
let capturedOptions: Record<string, unknown> | undefined;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
condition,
|
|
3
2
|
defineUpdate,
|
|
4
3
|
setHandler,
|
|
5
4
|
ApplicationFailure,
|
|
@@ -105,7 +104,6 @@ export async function createSession<
|
|
|
105
104
|
processToolsInParallel = true,
|
|
106
105
|
hooks = {},
|
|
107
106
|
appendSystemPrompt = true,
|
|
108
|
-
waitForInputTimeout = "48h",
|
|
109
107
|
threadKey,
|
|
110
108
|
sandboxOps,
|
|
111
109
|
thread: threadInit,
|
|
@@ -342,12 +340,23 @@ export async function createSession<
|
|
|
342
340
|
size: content.length,
|
|
343
341
|
mtime: new Date().toISOString(),
|
|
344
342
|
metadata: {},
|
|
343
|
+
// Carry the content directly on the entry so any handler that
|
|
344
|
+
// constructs a VirtualFileSystem from `fileTree` can read it
|
|
345
|
+
// without needing to also wire up `inlineFiles` from state.
|
|
346
|
+
inlineContent: content,
|
|
345
347
|
})),
|
|
346
348
|
]
|
|
347
349
|
: result.fileTree;
|
|
348
350
|
stateManager.mergeUpdate({
|
|
349
351
|
fileTree,
|
|
350
352
|
virtualFsCtx: virtualFsConfig.ctx,
|
|
353
|
+
// `inlineFiles` is still the source of truth at read time:
|
|
354
|
+
// VirtualFileSystem checks the inlineFiles map first and only
|
|
355
|
+
// falls through to entry.inlineContent. Embedding the content on
|
|
356
|
+
// the entry is the migration target so that handlers building a
|
|
357
|
+
// VirtualFileSystem from `fileTree` alone (without forwarding
|
|
358
|
+
// `inlineFiles` from state) can read skill resources. Until a
|
|
359
|
+
// follow-up drops `inlineFiles`, both fields are populated.
|
|
351
360
|
...(skillFiles && { inlineFiles: skillFiles }),
|
|
352
361
|
} as Partial<AgentState<TState>>);
|
|
353
362
|
}
|
|
@@ -516,19 +525,6 @@ export async function createSession<
|
|
|
516
525
|
|
|
517
526
|
// Turn committed: fresh id for the next turn.
|
|
518
527
|
assistantId = undefined;
|
|
519
|
-
|
|
520
|
-
if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
|
|
521
|
-
const conditionMet = await condition(
|
|
522
|
-
() => stateManager.getStatus() === "RUNNING",
|
|
523
|
-
waitForInputTimeout
|
|
524
|
-
);
|
|
525
|
-
if (!conditionMet) {
|
|
526
|
-
stateManager.cancel();
|
|
527
|
-
exitReason = "cancelled";
|
|
528
|
-
await condition(() => false, "2s");
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
528
|
}
|
|
533
529
|
|
|
534
530
|
if (stateManager.getTurns() >= maxTurns && stateManager.isRunning()) {
|
|
@@ -618,6 +614,7 @@ export async function createSession<
|
|
|
618
614
|
...(sandboxId && { sandboxId }),
|
|
619
615
|
...(exitSnapshot && { snapshot: exitSnapshot }),
|
|
620
616
|
threadId,
|
|
617
|
+
usage: stateManager.getTotalUsage(),
|
|
621
618
|
});
|
|
622
619
|
}
|
|
623
620
|
|
package/src/lib/session/types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Duration } from "@temporalio/common";
|
|
2
1
|
import type { SessionExitReason, ToolResultConfig } from "../types";
|
|
3
2
|
import type {
|
|
4
3
|
ToolMap,
|
|
@@ -177,8 +176,6 @@ export interface SessionConfig<
|
|
|
177
176
|
* Returns SDK-native content for the initial human message.
|
|
178
177
|
*/
|
|
179
178
|
buildContextMessage: () => TContent | Promise<TContent>;
|
|
180
|
-
/** How long to wait for input before cancelling the workflow */
|
|
181
|
-
waitForInputTimeout?: Duration;
|
|
182
179
|
|
|
183
180
|
// ---------------------------------------------------------------------------
|
|
184
181
|
// Thread lifecycle
|
|
@@ -248,6 +245,14 @@ export interface SessionConfig<
|
|
|
248
245
|
threadId: string;
|
|
249
246
|
sandboxId?: string;
|
|
250
247
|
snapshot?: SandboxSnapshot;
|
|
248
|
+
usage: {
|
|
249
|
+
totalInputTokens: number;
|
|
250
|
+
totalOutputTokens: number;
|
|
251
|
+
totalCachedWriteTokens: number;
|
|
252
|
+
totalCachedReadTokens: number;
|
|
253
|
+
totalReasonTokens: number;
|
|
254
|
+
turns: number;
|
|
255
|
+
};
|
|
251
256
|
}) => void;
|
|
252
257
|
|
|
253
258
|
// ---------------------------------------------------------------------------
|
|
@@ -2602,6 +2602,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2602
2602
|
sandboxId: "sb-1",
|
|
2603
2603
|
snapshot,
|
|
2604
2604
|
threadId: "t",
|
|
2605
|
+
usage: { totalInputTokens: 0, totalOutputTokens: 0, totalCachedWriteTokens: 0, totalCachedReadTokens: 0, totalReasonTokens: 0, turns: 0 },
|
|
2605
2606
|
});
|
|
2606
2607
|
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2607
2608
|
}
|
|
@@ -2625,6 +2626,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2625
2626
|
createdAt: new Date().toISOString(),
|
|
2626
2627
|
},
|
|
2627
2628
|
threadId: "t",
|
|
2629
|
+
usage: { totalInputTokens: 0, totalOutputTokens: 0, totalCachedWriteTokens: 0, totalCachedReadTokens: 0, totalReasonTokens: 0, turns: 0 },
|
|
2628
2630
|
});
|
|
2629
2631
|
return {
|
|
2630
2632
|
toolResponse: "ok",
|
|
@@ -278,5 +278,13 @@ export interface SubagentSessionInput {
|
|
|
278
278
|
sandboxId?: string;
|
|
279
279
|
snapshot?: SandboxSnapshot;
|
|
280
280
|
threadId: string;
|
|
281
|
+
usage: {
|
|
282
|
+
totalInputTokens: number;
|
|
283
|
+
totalOutputTokens: number;
|
|
284
|
+
totalCachedWriteTokens: number;
|
|
285
|
+
totalCachedReadTokens: number;
|
|
286
|
+
totalReasonTokens: number;
|
|
287
|
+
turns: number;
|
|
288
|
+
};
|
|
281
289
|
}) => void;
|
|
282
290
|
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from "./types";
|
|
14
14
|
import type { SubagentSandboxShutdown } from "../lifecycle";
|
|
15
15
|
import type { SandboxSnapshot } from "../sandbox/types";
|
|
16
|
+
import type { TokenUsage } from "../types";
|
|
16
17
|
import { childSandboxReadySignal } from "./signals";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -132,6 +133,7 @@ export function defineSubagentWorkflow(
|
|
|
132
133
|
let capturedSnapshot: SandboxSnapshot | undefined;
|
|
133
134
|
let capturedBaseSnapshot: SandboxSnapshot | undefined;
|
|
134
135
|
let capturedThreadId: string | undefined;
|
|
136
|
+
let capturedUsage: TokenUsage | undefined;
|
|
135
137
|
const sessionInput: SubagentSessionInput = {
|
|
136
138
|
agentName: config.name,
|
|
137
139
|
sandboxShutdown: effectiveShutdown,
|
|
@@ -148,10 +150,17 @@ export function defineSubagentWorkflow(
|
|
|
148
150
|
});
|
|
149
151
|
}
|
|
150
152
|
},
|
|
151
|
-
onSessionExit: ({ sandboxId, snapshot, threadId }) => {
|
|
153
|
+
onSessionExit: ({ sandboxId, snapshot, threadId, usage }) => {
|
|
152
154
|
capturedSandboxId = sandboxId;
|
|
153
155
|
capturedSnapshot = snapshot;
|
|
154
156
|
capturedThreadId = threadId;
|
|
157
|
+
capturedUsage = {
|
|
158
|
+
inputTokens: usage.totalInputTokens,
|
|
159
|
+
outputTokens: usage.totalOutputTokens,
|
|
160
|
+
cachedWriteTokens: usage.totalCachedWriteTokens,
|
|
161
|
+
cachedReadTokens: usage.totalCachedReadTokens,
|
|
162
|
+
reasonTokens: usage.totalReasonTokens,
|
|
163
|
+
};
|
|
155
164
|
},
|
|
156
165
|
};
|
|
157
166
|
|
|
@@ -168,6 +177,7 @@ export function defineSubagentWorkflow(
|
|
|
168
177
|
...(capturedBaseSnapshot !== undefined && {
|
|
169
178
|
baseSnapshot: capturedBaseSnapshot,
|
|
170
179
|
}),
|
|
180
|
+
...(capturedUsage !== undefined && { usage: capturedUsage }),
|
|
171
181
|
};
|
|
172
182
|
};
|
|
173
183
|
|