zeitlich 0.2.39 → 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.
Files changed (64) hide show
  1. package/README.md +12 -1
  2. package/dist/{activities-Bmu7XnaG.d.ts → activities-D_g13S3y.d.ts} +2 -2
  3. package/dist/{activities-ByBFLvm2.d.cts → activities-qUflxmfS.d.cts} +2 -2
  4. package/dist/adapters/sandbox/e2b/index.cjs +12 -3
  5. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/e2b/index.d.cts +35 -2
  7. package/dist/adapters/sandbox/e2b/index.d.ts +35 -2
  8. package/dist/adapters/sandbox/e2b/index.js +13 -4
  9. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/index.d.cts +5 -5
  11. package/dist/adapters/thread/anthropic/index.d.ts +5 -5
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  14. package/dist/adapters/thread/google-genai/index.d.cts +5 -5
  15. package/dist/adapters/thread/google-genai/index.d.ts +5 -5
  16. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  17. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  18. package/dist/adapters/thread/langchain/index.d.cts +5 -5
  19. package/dist/adapters/thread/langchain/index.d.ts +5 -5
  20. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  21. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  22. package/dist/index.cjs +93 -49
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +35 -9
  25. package/dist/index.d.ts +35 -9
  26. package/dist/index.js +94 -50
  27. package/dist/index.js.map +1 -1
  28. package/dist/{proxy-DO_MXbY4.d.ts → proxy-BbcgoXg1.d.ts} +1 -1
  29. package/dist/{proxy-BAKzNGRq.d.cts → proxy-D7mvDEO6.d.cts} +1 -1
  30. package/dist/{thread-manager-D-7lp1JK.d.ts → thread-manager-CTXPCu9W.d.ts} +2 -2
  31. package/dist/{thread-manager-CcRXasqs.d.ts → thread-manager-Dqstsw4i.d.ts} +2 -2
  32. package/dist/{thread-manager-Y8Ucf0Tf.d.cts → thread-manager-DrWfVjlj.d.cts} +2 -2
  33. package/dist/{thread-manager-ClwSaUnj.d.cts → thread-manager-cLhDhRRc.d.cts} +2 -2
  34. package/dist/{types-Dt8-HBBT.d.ts → types-BqTmyH31.d.ts} +43 -3
  35. package/dist/{types-hFFi-Zd9.d.cts → types-CdvcmXb6.d.cts} +1 -1
  36. package/dist/{types-DpHTX-iO.d.ts → types-CjF1_Idx.d.ts} +1 -1
  37. package/dist/{types-Bcbiq8iv.d.cts → types-DjaQKUJx.d.cts} +43 -3
  38. package/dist/{workflow-Bx9utBwb.d.cts → workflow-CuqxgS6X.d.cts} +1 -1
  39. package/dist/{workflow-Bmf9EtDW.d.ts → workflow-N1MNDoul.d.ts} +1 -1
  40. package/dist/workflow.cjs +40 -31
  41. package/dist/workflow.cjs.map +1 -1
  42. package/dist/workflow.d.cts +3 -3
  43. package/dist/workflow.d.ts +3 -3
  44. package/dist/workflow.js +41 -32
  45. package/dist/workflow.js.map +1 -1
  46. package/package.json +15 -6
  47. package/src/adapters/sandbox/e2b/README.md +81 -0
  48. package/src/adapters/sandbox/e2b/index.ts +32 -5
  49. package/src/adapters/sandbox/e2b/keep-alive.test.ts +115 -0
  50. package/src/adapters/sandbox/e2b/types.ts +34 -2
  51. package/src/index.ts +1 -1
  52. package/src/lib/session/session.integration.test.ts +58 -0
  53. package/src/lib/session/session.ts +13 -15
  54. package/src/lib/session/types.ts +9 -3
  55. package/src/lib/subagent/subagent.integration.test.ts +2 -0
  56. package/src/lib/subagent/types.ts +8 -0
  57. package/src/lib/subagent/workflow.ts +11 -1
  58. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +158 -0
  59. package/src/lib/tool-router/index.ts +1 -1
  60. package/src/lib/tool-router/with-sandbox.ts +45 -2
  61. package/src/lib/virtual-fs/filesystem.ts +41 -16
  62. package/src/lib/virtual-fs/types.ts +19 -0
  63. package/src/lib/virtual-fs/virtual-fs.test.ts +204 -1
  64. 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.39",
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/envconfig": "^1.15.0",
231
- "@temporalio/worker": "^1.15.0",
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 { 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,
@@ -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 = await E2bSdkSandbox.connect(sandboxId);
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
- throw new SandboxNotFoundError(sandboxId);
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
- /** 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
  }
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()) {
@@ -617,6 +613,8 @@ export async function createSession<
617
613
  onSessionExit({
618
614
  ...(sandboxId && { sandboxId }),
619
615
  ...(exitSnapshot && { snapshot: exitSnapshot }),
616
+ threadId,
617
+ usage: stateManager.getTotalUsage(),
620
618
  });
621
619
  }
622
620
 
@@ -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
@@ -245,8 +242,17 @@ export interface SessionConfig<
245
242
  * code to manually thread them through.
246
243
  */
247
244
  onSessionExit?: (result: {
245
+ threadId: string;
248
246
  sandboxId?: string;
249
247
  snapshot?: SandboxSnapshot;
248
+ usage: {
249
+ totalInputTokens: number;
250
+ totalOutputTokens: number;
251
+ totalCachedWriteTokens: number;
252
+ totalCachedReadTokens: number;
253
+ totalReasonTokens: number;
254
+ turns: number;
255
+ };
250
256
  }) => void;
251
257
 
252
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