zeitlich 0.2.29 → 0.2.30

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 (98) hide show
  1. package/dist/{activities-1xrWRrGJ.d.cts → activities-BeveyY9b.d.cts} +2 -2
  2. package/dist/{activities-DOViDCTE.d.ts → activities-NT3rcw66.d.ts} +2 -2
  3. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/bedrock/index.d.cts +3 -3
  5. package/dist/adapters/sandbox/bedrock/index.d.ts +3 -3
  6. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  7. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  8. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  9. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  10. package/dist/adapters/sandbox/daytona/index.d.cts +1 -1
  11. package/dist/adapters/sandbox/daytona/index.d.ts +1 -1
  12. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  13. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  14. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  15. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
  17. package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
  18. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  19. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  20. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  21. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  22. package/dist/adapters/sandbox/inmemory/index.d.cts +1 -1
  23. package/dist/adapters/sandbox/inmemory/index.d.ts +1 -1
  24. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  25. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  26. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  27. package/dist/adapters/thread/anthropic/index.cjs +0 -1
  28. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  29. package/dist/adapters/thread/anthropic/index.d.cts +5 -5
  30. package/dist/adapters/thread/anthropic/index.d.ts +5 -5
  31. package/dist/adapters/thread/anthropic/index.js +0 -1
  32. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  33. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  34. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  35. package/dist/adapters/thread/google-genai/index.cjs +0 -1
  36. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  37. package/dist/adapters/thread/google-genai/index.d.cts +5 -5
  38. package/dist/adapters/thread/google-genai/index.d.ts +5 -5
  39. package/dist/adapters/thread/google-genai/index.js +0 -1
  40. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  41. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  42. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  43. package/dist/adapters/thread/langchain/index.cjs +0 -1
  44. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  45. package/dist/adapters/thread/langchain/index.d.cts +5 -5
  46. package/dist/adapters/thread/langchain/index.d.ts +5 -5
  47. package/dist/adapters/thread/langchain/index.js +0 -1
  48. package/dist/adapters/thread/langchain/index.js.map +1 -1
  49. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  50. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  51. package/dist/index.cjs +69 -52
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +78 -15
  54. package/dist/index.d.ts +78 -15
  55. package/dist/index.js +69 -52
  56. package/dist/index.js.map +1 -1
  57. package/dist/{proxy-78nc985d.d.ts → proxy-BgswT47M.d.ts} +1 -1
  58. package/dist/{proxy-Bm2UTiO_.d.cts → proxy-OJihshQF.d.cts} +1 -1
  59. package/dist/{thread-manager-07BaYu_z.d.ts → thread-manager-BS477gj8.d.ts} +1 -1
  60. package/dist/{thread-manager-BRE5KkHB.d.cts → thread-manager-DH0zv05W.d.cts} +1 -1
  61. package/dist/{thread-manager-CxbWo7q_.d.ts → thread-manager-iUplxEZt.d.ts} +1 -1
  62. package/dist/{thread-manager-CatBkarc.d.cts → thread-manager-lfN0V-gH.d.cts} +1 -1
  63. package/dist/{types-ChAMwU3q.d.ts → types-AujBIMMn.d.cts} +5 -8
  64. package/dist/{types-ChAMwU3q.d.cts → types-AujBIMMn.d.ts} +5 -8
  65. package/dist/{types-DAv_SLN8.d.ts → types-CCIc7Eam.d.ts} +1 -1
  66. package/dist/{types-BkVoEyiH.d.ts → types-D90Q5aOh.d.ts} +140 -139
  67. package/dist/{types-BdCdR41N.d.ts → types-DBk-C8zM.d.ts} +1 -1
  68. package/dist/{types-ZHs2v9Ap.d.cts → types-DUvEZSDe.d.cts} +1 -1
  69. package/dist/{types-seDYom4M.d.cts → types-DVdT5ybA.d.cts} +140 -139
  70. package/dist/{types-Dpz2gXLk.d.cts → types-DgIVPOa1.d.cts} +1 -1
  71. package/dist/{workflow-B4T3la0p.d.cts → workflow-Cj4DxGdM.d.cts} +2 -2
  72. package/dist/{workflow-DCmaXLZ_.d.ts → workflow-CzrBdCcJ.d.ts} +2 -2
  73. package/dist/workflow.cjs +31 -43
  74. package/dist/workflow.cjs.map +1 -1
  75. package/dist/workflow.d.cts +3 -3
  76. package/dist/workflow.d.ts +3 -3
  77. package/dist/workflow.js +31 -43
  78. package/dist/workflow.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/adapters/thread/anthropic/thread-manager.ts +6 -6
  81. package/src/adapters/thread/google-genai/thread-manager.ts +6 -6
  82. package/src/adapters/thread/langchain/thread-manager.ts +6 -6
  83. package/src/index.ts +1 -0
  84. package/src/lib/lifecycle.ts +8 -3
  85. package/src/lib/sandbox/index.ts +2 -4
  86. package/src/lib/sandbox/manager.ts +128 -13
  87. package/src/lib/sandbox/sandbox.test.ts +136 -16
  88. package/src/lib/sandbox/types.ts +6 -5
  89. package/src/lib/session/session.integration.test.ts +7 -40
  90. package/src/lib/session/session.ts +63 -49
  91. package/src/lib/session/types.ts +22 -13
  92. package/src/lib/state/types.ts +9 -6
  93. package/src/lib/subagent/handler.ts +18 -12
  94. package/src/lib/subagent/register.ts +11 -12
  95. package/src/lib/types.ts +2 -0
  96. package/src/lib/virtual-fs/types.ts +8 -14
  97. package/src/lib/virtual-fs/with-virtual-fs.ts +4 -4
  98. package/src/tools/bash/bash.test.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,12 +1,12 @@
1
1
  import type Redis from "ioredis";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { JsonValue } from "../../../lib/state/types";
4
- import {
5
- createThreadManager,
6
- type ProviderThreadManager,
7
- type ThreadManagerConfig,
8
- type ThreadManagerHooks,
9
- } from "../../../lib/thread";
4
+ import { createThreadManager } from "../../../lib/thread/manager";
5
+ import type {
6
+ ProviderThreadManager,
7
+ ThreadManagerConfig,
8
+ ThreadManagerHooks,
9
+ } from "../../../lib/thread/types";
10
10
 
11
11
  /** SDK-native content type for Anthropic human messages */
12
12
  export type AnthropicContent =
@@ -1,11 +1,11 @@
1
1
  import type Redis from "ioredis";
2
2
  import type { Content, Part } from "@google/genai";
3
- import {
4
- createThreadManager,
5
- type ProviderThreadManager,
6
- type ThreadManagerConfig,
7
- type ThreadManagerHooks,
8
- } from "../../../lib/thread";
3
+ import { createThreadManager } from "../../../lib/thread/manager";
4
+ import type {
5
+ ProviderThreadManager,
6
+ ThreadManagerConfig,
7
+ ThreadManagerHooks,
8
+ } from "../../../lib/thread/types";
9
9
  import type { GoogleGenAIToolResponse } from "./activities";
10
10
 
11
11
  /** SDK-native content type for Google GenAI human messages */
@@ -10,12 +10,12 @@ import {
10
10
  ToolMessage,
11
11
  mapStoredMessagesToChatMessages,
12
12
  } from "@langchain/core/messages";
13
- import {
14
- createThreadManager,
15
- type ProviderThreadManager,
16
- type ThreadManagerConfig,
17
- type ThreadManagerHooks,
18
- } from "../../../lib/thread";
13
+ import { createThreadManager } from "../../../lib/thread/manager";
14
+ import type {
15
+ ProviderThreadManager,
16
+ ThreadManagerConfig,
17
+ ThreadManagerHooks,
18
+ } from "../../../lib/thread/types";
19
19
 
20
20
  /** SDK-native content type for LangChain human messages */
21
21
  export type LangChainContent = string | MessageContent;
package/src/index.ts CHANGED
@@ -57,6 +57,7 @@ export type { AgentStateContext } from "./lib/activity";
57
57
 
58
58
  // Sandbox (activity-side: manager + Node.js filesystem adapter)
59
59
  export { SandboxManager } from "./lib/sandbox/manager";
60
+ export type { SandboxManagerHooks, PreCreateHookResult } from "./lib/sandbox/manager";
60
61
  export { NodeFsSandboxFileSystem } from "./lib/sandbox/node-fs";
61
62
 
62
63
  // Virtual filesystem (activity-side)
@@ -22,7 +22,9 @@ export type ThreadInit =
22
22
  /**
23
23
  * Sandbox initialization strategy.
24
24
  *
25
- * - `"new"` — create a fresh sandbox.
25
+ * - `"new"` — create a fresh sandbox. Optionally pass `ctx` to
26
+ * have the {@link SandboxManager}'s resolver produce creation options
27
+ * (e.g. initial files) from workflow arguments.
26
28
  * - `"continue"` — resume a previously-paused sandbox (this session takes
27
29
  * ownership and the shutdown policy applies on exit).
28
30
  * - `"fork"` — fork from an existing (or paused) sandbox; a new sandbox is
@@ -31,10 +33,13 @@ export type ThreadInit =
31
33
  * The session will **not** manage its lifecycle on exit.
32
34
  */
33
35
  export type SandboxInit =
34
- | { mode: "new" }
36
+ | { mode: "new"; ctx?: unknown }
35
37
  | { mode: "continue"; sandboxId: string }
36
38
  | { mode: "fork"; sandboxId: string }
37
- | { mode: "inherit"; sandboxId: string; stateUpdate?: Record<string, unknown> };
39
+ | {
40
+ mode: "inherit";
41
+ sandboxId: string;
42
+ };
38
43
 
39
44
  /**
40
45
  * What to do with the sandbox when the session exits.
@@ -1,4 +1,5 @@
1
1
  export { SandboxManager } from "./manager";
2
+ export type { SandboxManagerHooks, PreCreateHookResult } from "./manager";
2
3
  export { toTree } from "./tree";
3
4
  export type {
4
5
  Sandbox,
@@ -13,7 +14,4 @@ export type {
13
14
  DirentEntry,
14
15
  FileStat,
15
16
  } from "./types";
16
- export {
17
- SandboxNotFoundError,
18
- SandboxNotSupportedError,
19
- } from "./types";
17
+ export { SandboxNotFoundError, SandboxNotSupportedError } from "./types";
@@ -7,12 +7,64 @@ import type {
7
7
  SandboxSnapshot,
8
8
  } from "./types";
9
9
 
10
+ /**
11
+ * Result returned by {@link SandboxManagerHooks.onPreCreate}.
12
+ *
13
+ * - Set `skip: true` to prevent sandbox creation entirely.
14
+ * - Set `modifiedOptions` to override/extend the creation options that will
15
+ * be forwarded to the provider. Fields in `modifiedOptions` are merged on
16
+ * top of the original options (`initialFiles` and `env` are shallow-merged;
17
+ * everything else is overwritten).
18
+ */
19
+ export interface PreCreateHookResult<
20
+ TOptions extends SandboxCreateOptions = SandboxCreateOptions,
21
+ > {
22
+ skip?: boolean;
23
+ modifiedOptions?: Partial<TOptions>;
24
+ }
25
+
26
+ /**
27
+ * Lifecycle hooks for {@link SandboxManager}.
28
+ *
29
+ * Hooks run inside the existing `createSandbox` activity — no additional
30
+ * activity registration required.
31
+ */
32
+ export interface SandboxManagerHooks<
33
+ TOptions extends SandboxCreateOptions = SandboxCreateOptions,
34
+ TCtx = unknown,
35
+ > {
36
+ /**
37
+ * Called before sandbox creation.
38
+ *
39
+ * Receives the provider options and an opaque `ctx` value set from the
40
+ * workflow's {@link SandboxInit}. Use `ctx` to derive additional creation
41
+ * options (e.g. initial files from workflow arguments).
42
+ *
43
+ * Return `{ skip: true }` to prevent creation, or `{ modifiedOptions }`
44
+ * to alter the options before they reach the provider.
45
+ */
46
+ onPreCreate?: (
47
+ options: TOptions,
48
+ ctx: TCtx
49
+ ) => Promise<PreCreateHookResult<TOptions> | undefined>;
50
+
51
+ /**
52
+ * Called after a sandbox has been successfully created.
53
+ */
54
+ onPostCreate?: (sandboxId: string) => Promise<void>;
55
+ }
56
+
10
57
  /**
11
58
  * Stateless facade over a {@link SandboxProvider}.
12
59
  *
13
60
  * Delegates all lifecycle operations to the provider, which is responsible
14
61
  * for its own instance management strategy (e.g. in-memory map, remote API).
15
62
  *
63
+ * Optional {@link SandboxManagerHooks} can be passed at construction time.
64
+ * The `onPreCreate` hook runs inside the `createSandbox` activity, receiving
65
+ * the provider options and an opaque `ctx` value from the workflow's
66
+ * {@link SandboxInit}. It can modify options or skip creation entirely.
67
+ *
16
68
  * @example
17
69
  * ```typescript
18
70
  * const manager = new SandboxManager(new InMemorySandboxProvider());
@@ -22,21 +74,84 @@ import type {
22
74
  * };
23
75
  * // registers: inMemoryCodingAgentCreateSandbox, …
24
76
  * ```
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const manager = new SandboxManager(
81
+ * new DaytonaSandboxProvider(config),
82
+ * {
83
+ * hooks: {
84
+ * onPreCreate: async (options, ctx) => {
85
+ * const { projectId, filePaths } = ctx as { projectId: string; filePaths: string[] };
86
+ * const files: Record<string, string> = {};
87
+ * for (const p of filePaths) files[p] = await db.readFile(projectId, p);
88
+ * return { modifiedOptions: { initialFiles: files } };
89
+ * },
90
+ * onPostCreate: async (sandboxId) => {
91
+ * console.log("Sandbox created:", sandboxId);
92
+ * },
93
+ * },
94
+ * },
95
+ * );
96
+ * ```
25
97
  */
26
98
  export class SandboxManager<
27
99
  TOptions extends SandboxCreateOptions = SandboxCreateOptions,
28
100
  TSandbox extends Sandbox = Sandbox,
29
101
  TId extends string = string,
102
+ TCtx = unknown,
30
103
  > {
104
+ private hooks: SandboxManagerHooks<TOptions, TCtx>;
105
+
31
106
  constructor(
32
- private provider: SandboxProvider<TOptions, TSandbox> & { readonly id: TId }
33
- ) {}
107
+ private provider: SandboxProvider<TOptions, TSandbox> & {
108
+ readonly id: TId;
109
+ },
110
+ options?: { hooks?: SandboxManagerHooks<TOptions, TCtx> }
111
+ ) {
112
+ this.hooks = options?.hooks ?? {};
113
+ }
34
114
 
35
115
  async create(
36
- options?: TOptions
37
- ): Promise<{ sandboxId: string; stateUpdate?: Record<string, unknown> }> {
38
- const { sandbox, stateUpdate } = await this.provider.create(options);
39
- return { sandboxId: sandbox.id, ...(stateUpdate && { stateUpdate }) };
116
+ options?: TOptions,
117
+ ctx?: TCtx
118
+ ): Promise<{
119
+ sandboxId: string;
120
+ } | null> {
121
+ let providerOptions = options;
122
+
123
+ if (this.hooks.onPreCreate) {
124
+ const hookResult = await this.hooks.onPreCreate(
125
+ options ?? ({} as TOptions),
126
+ ctx ?? ({} as TCtx)
127
+ );
128
+ if (hookResult?.skip) return null;
129
+
130
+ if (hookResult?.modifiedOptions) {
131
+ const orig = options ?? ({} as TOptions);
132
+ const mod = hookResult.modifiedOptions;
133
+ providerOptions = {
134
+ ...mod,
135
+ ...orig,
136
+ initialFiles: {
137
+ ...mod.initialFiles,
138
+ ...orig.initialFiles,
139
+ },
140
+ env: {
141
+ ...mod.env,
142
+ ...orig.env,
143
+ },
144
+ } as TOptions;
145
+ }
146
+ }
147
+
148
+ const { sandbox } = await this.provider.create(providerOptions);
149
+
150
+ if (this.hooks.onPostCreate) {
151
+ await this.hooks.onPostCreate(sandbox.id);
152
+ }
153
+
154
+ return { sandboxId: sandbox.id };
40
155
  }
41
156
 
42
157
  async getSandbox(id: string): Promise<TSandbox> {
@@ -87,16 +202,16 @@ export class SandboxManager<
87
202
  */
88
203
  createActivities<S extends string>(
89
204
  scope: S
90
- ): PrefixedSandboxOps<`${TId}${Capitalize<S>}`, TOptions> {
205
+ ): PrefixedSandboxOps<`${TId}${Capitalize<S>}`, TOptions, TCtx> {
91
206
  const prefix = `${this.provider.id}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`;
92
- const ops: SandboxOps<TOptions> = {
207
+ const ops: SandboxOps<TOptions, TCtx> = {
93
208
  createSandbox: async (
94
- options?: TOptions
209
+ options?: TOptions,
210
+ ctx?: TCtx
95
211
  ): Promise<{
96
212
  sandboxId: string;
97
- stateUpdate?: Record<string, unknown>;
98
- }> => {
99
- return this.create(options);
213
+ } | null> => {
214
+ return this.create(options, ctx);
100
215
  },
101
216
  destroySandbox: async (sandboxId: string): Promise<void> => {
102
217
  await this.destroy(sandboxId);
@@ -117,6 +232,6 @@ export class SandboxManager<
117
232
  const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
118
233
  return Object.fromEntries(
119
234
  Object.entries(ops).map(([k, v]) => [`${prefix}${cap(k)}`, v])
120
- ) as PrefixedSandboxOps<`${TId}${Capitalize<S>}`, TOptions>;
235
+ ) as PrefixedSandboxOps<`${TId}${Capitalize<S>}`, TOptions, TCtx>;
121
236
  }
122
237
  }
@@ -1,7 +1,20 @@
1
1
  import { describe, expect, it, beforeEach } from "vitest";
2
2
  import { SandboxManager } from "./manager";
3
3
  import { InMemorySandboxProvider } from "../../adapters/sandbox/inmemory/index";
4
- import { SandboxNotFoundError, type Sandbox, type SandboxCreateOptions } from "./types";
4
+ import {
5
+ SandboxNotFoundError,
6
+ type Sandbox,
7
+ type SandboxCreateOptions,
8
+ } from "./types";
9
+
10
+ async function mustCreate<T extends SandboxCreateOptions, TId extends string>(
11
+ mgr: SandboxManager<T, Sandbox, TId>,
12
+ options?: T
13
+ ): Promise<{ sandboxId: string }> {
14
+ const result = await mgr.create(options);
15
+ expect(result).not.toBeNull();
16
+ return result as NonNullable<typeof result>;
17
+ }
5
18
 
6
19
  describe("SandboxManager", () => {
7
20
  let manager: SandboxManager<SandboxCreateOptions, Sandbox, "inMemory">;
@@ -11,34 +24,36 @@ describe("SandboxManager", () => {
11
24
  });
12
25
 
13
26
  it("creates a sandbox and returns an id", async () => {
14
- const { sandboxId } = await manager.create();
27
+ const { sandboxId } = await mustCreate(manager);
15
28
  expect(sandboxId).toBeTruthy();
16
29
  const sandbox = await manager.getSandbox(sandboxId);
17
30
  expect(sandbox.id).toBe(sandboxId);
18
31
  });
19
32
 
20
33
  it("creates a sandbox with a custom id", async () => {
21
- const { sandboxId } = await manager.create({ id: "my-sandbox" });
34
+ const { sandboxId } = await mustCreate(manager, { id: "my-sandbox" });
22
35
  expect(sandboxId).toBe("my-sandbox");
23
36
  });
24
37
 
25
38
  it("gets an existing sandbox", async () => {
26
- const { sandboxId } = await manager.create();
39
+ const { sandboxId } = await mustCreate(manager);
27
40
  const sandbox = await manager.getSandbox(sandboxId);
28
41
  expect(sandbox.id).toBe(sandboxId);
29
42
  });
30
43
 
31
44
  it("throws SandboxNotFoundError for unknown id", async () => {
32
45
  await expect(manager.getSandbox("nonexistent")).rejects.toThrow(
33
- SandboxNotFoundError,
46
+ SandboxNotFoundError
34
47
  );
35
48
  });
36
49
 
37
50
  it("destroys a sandbox", async () => {
38
- const { sandboxId } = await manager.create();
51
+ const { sandboxId } = await mustCreate(manager);
39
52
  await manager.getSandbox(sandboxId);
40
53
  await manager.destroy(sandboxId);
41
- await expect(manager.getSandbox(sandboxId)).rejects.toThrow(SandboxNotFoundError);
54
+ await expect(manager.getSandbox(sandboxId)).rejects.toThrow(
55
+ SandboxNotFoundError
56
+ );
42
57
  });
43
58
 
44
59
  it("destroy is idempotent for unknown ids", async () => {
@@ -46,7 +61,7 @@ describe("SandboxManager", () => {
46
61
  });
47
62
 
48
63
  it("snapshots and restores a sandbox", async () => {
49
- const { sandboxId } = await manager.create({
64
+ const { sandboxId } = await mustCreate(manager, {
50
65
  initialFiles: { "/data.txt": "hello" },
51
66
  });
52
67
  const sandbox = await manager.getSandbox(sandboxId);
@@ -57,7 +72,9 @@ describe("SandboxManager", () => {
57
72
  expect(snapshot.providerId).toBe("inMemory");
58
73
 
59
74
  await manager.destroy(sandboxId);
60
- await expect(manager.getSandbox(sandboxId)).rejects.toThrow(SandboxNotFoundError);
75
+ await expect(manager.getSandbox(sandboxId)).rejects.toThrow(
76
+ SandboxNotFoundError
77
+ );
61
78
 
62
79
  const restoredId = await manager.restore(snapshot);
63
80
  expect(restoredId).toBe(sandboxId);
@@ -68,6 +85,107 @@ describe("SandboxManager", () => {
68
85
  expect(extra).toBe("world");
69
86
  });
70
87
 
88
+ it("onPreCreate hook merges modifiedOptions into create options", async () => {
89
+ const mgr = new SandboxManager(new InMemorySandboxProvider(), {
90
+ hooks: {
91
+ onPreCreate: async (_options, ctx) => {
92
+ const { paths } = ctx as { paths: string[] };
93
+ const files: Record<string, string> = {};
94
+ for (const p of paths) files[p] = `content of ${p}`;
95
+ return {
96
+ modifiedOptions: { initialFiles: files, env: { RESOLVED: "true" } },
97
+ };
98
+ },
99
+ },
100
+ });
101
+
102
+ const result = await mgr.create(
103
+ { initialFiles: { "/extra.txt": "extra" } },
104
+ { paths: ["/a.txt", "/b.txt"] }
105
+ );
106
+ expect(result).not.toBeNull();
107
+ const { sandboxId } = result as NonNullable<typeof result>;
108
+
109
+ const sandbox = await mgr.getSandbox(sandboxId);
110
+ expect(await sandbox.fs.readFile("/a.txt")).toBe("content of /a.txt");
111
+ expect(await sandbox.fs.readFile("/b.txt")).toBe("content of /b.txt");
112
+ expect(await sandbox.fs.readFile("/extra.txt")).toBe("extra");
113
+ });
114
+
115
+ it("ctx is not forwarded to provider when no hooks registered", async () => {
116
+ const result = await manager.create(
117
+ { initialFiles: { "/test.txt": "ok" } },
118
+ { foo: "bar" }
119
+ );
120
+ expect(result).not.toBeNull();
121
+ const { sandboxId } = result as NonNullable<typeof result>;
122
+ const sandbox = await manager.getSandbox(sandboxId);
123
+ expect(await sandbox.fs.readFile("/test.txt")).toBe("ok");
124
+ });
125
+
126
+ it("onPreCreate hook can skip sandbox creation", async () => {
127
+ const mgr = new SandboxManager(new InMemorySandboxProvider(), {
128
+ hooks: {
129
+ onPreCreate: async () => ({ skip: true }),
130
+ },
131
+ });
132
+
133
+ const result = await mgr.create(undefined, { skip: true });
134
+ expect(result).toBeNull();
135
+ });
136
+
137
+ it("original options take precedence over hook modifiedOptions", async () => {
138
+ const mgr = new SandboxManager(new InMemorySandboxProvider(), {
139
+ hooks: {
140
+ onPreCreate: async () => ({
141
+ modifiedOptions: {
142
+ initialFiles: { "/file.txt": "from-hook" },
143
+ env: { KEY: "hook" },
144
+ },
145
+ }),
146
+ },
147
+ });
148
+
149
+ const result = await mgr.create(
150
+ { initialFiles: { "/file.txt": "explicit" } },
151
+ {}
152
+ );
153
+ expect(result).not.toBeNull();
154
+ const { sandboxId } = result as NonNullable<typeof result>;
155
+
156
+ const sandbox = await mgr.getSandbox(sandboxId);
157
+ expect(await sandbox.fs.readFile("/file.txt")).toBe("explicit");
158
+ });
159
+
160
+ it("onPostCreate hook receives sandboxId", async () => {
161
+ let capturedId: string | undefined;
162
+ const mgr = new SandboxManager(new InMemorySandboxProvider(), {
163
+ hooks: {
164
+ onPostCreate: async (sandboxId) => {
165
+ capturedId = sandboxId;
166
+ },
167
+ },
168
+ });
169
+
170
+ const { sandboxId } = await mustCreate(mgr);
171
+ expect(capturedId).toBe(sandboxId);
172
+ });
173
+
174
+ it("onPostCreate hook does not run when creation is skipped", async () => {
175
+ let postCalled = false;
176
+ const mgr = new SandboxManager(new InMemorySandboxProvider(), {
177
+ hooks: {
178
+ onPreCreate: async () => ({ skip: true }),
179
+ onPostCreate: async () => {
180
+ postCalled = true;
181
+ },
182
+ },
183
+ });
184
+
185
+ await mgr.create();
186
+ expect(postCalled).toBe(false);
187
+ });
188
+
71
189
  it("createActivities returns prefixed SandboxOps-shaped object", async () => {
72
190
  // provider.id is "inMemory", scope is "Test" → prefix "inMemoryTest"
73
191
  const activities = manager.createActivities("Test");
@@ -75,12 +193,14 @@ describe("SandboxManager", () => {
75
193
  expect(activities.inMemoryTestDestroySandbox).toBeTypeOf("function");
76
194
  expect(activities.inMemoryTestSnapshotSandbox).toBeTypeOf("function");
77
195
 
78
- const { sandboxId } = await activities.inMemoryTestCreateSandbox();
196
+ const result = await activities.inMemoryTestCreateSandbox();
197
+ expect(result).not.toBeNull();
198
+ const { sandboxId } = result as NonNullable<typeof result>;
79
199
  await expect(manager.getSandbox(sandboxId)).resolves.toBeTruthy();
80
200
 
81
201
  await activities.inMemoryTestDestroySandbox(sandboxId);
82
202
  await expect(manager.getSandbox(sandboxId)).rejects.toThrow(
83
- SandboxNotFoundError,
203
+ SandboxNotFoundError
84
204
  );
85
205
  });
86
206
  });
@@ -93,7 +213,7 @@ describe("InMemorySandboxProvider", () => {
93
213
  });
94
214
 
95
215
  it("creates sandbox with initial files", async () => {
96
- const { sandboxId } = await manager.create({
216
+ const { sandboxId } = await mustCreate(manager, {
97
217
  initialFiles: {
98
218
  "/src/index.ts": 'console.log("hello");',
99
219
  "/README.md": "# Hello",
@@ -105,7 +225,7 @@ describe("InMemorySandboxProvider", () => {
105
225
  });
106
226
 
107
227
  it("supports filesystem operations", async () => {
108
- const { sandboxId } = await manager.create();
228
+ const { sandboxId } = await mustCreate(manager);
109
229
  const { fs } = await manager.getSandbox(sandboxId);
110
230
 
111
231
  await fs.writeFile("/test.txt", "hello");
@@ -124,7 +244,7 @@ describe("InMemorySandboxProvider", () => {
124
244
  });
125
245
 
126
246
  it("supports shell execution", async () => {
127
- const { sandboxId } = await manager.create({
247
+ const { sandboxId } = await mustCreate(manager, {
128
248
  initialFiles: { "/data.txt": "hello world" },
129
249
  });
130
250
  const sandbox = await manager.getSandbox(sandboxId);
@@ -135,7 +255,7 @@ describe("InMemorySandboxProvider", () => {
135
255
  });
136
256
 
137
257
  it("reports correct capabilities", async () => {
138
- const { sandboxId } = await manager.create();
258
+ const { sandboxId } = await mustCreate(manager);
139
259
  const sandbox = await manager.getSandbox(sandboxId);
140
260
  expect(sandbox.capabilities).toEqual({
141
261
  filesystem: true,
@@ -145,7 +265,7 @@ describe("InMemorySandboxProvider", () => {
145
265
  });
146
266
 
147
267
  it("readdirWithFileTypes works", async () => {
148
- const { sandboxId } = await manager.create({
268
+ const { sandboxId } = await mustCreate(manager, {
149
269
  initialFiles: {
150
270
  "/dir/a.txt": "a",
151
271
  "/dir/b.txt": "b",
@@ -118,8 +118,6 @@ export interface SandboxCreateOptions {
118
118
 
119
119
  export interface SandboxCreateResult {
120
120
  sandbox: Sandbox;
121
- /** Optional state to merge into the workflow's `AgentState` via the session. */
122
- stateUpdate?: Record<string, unknown>;
123
121
  }
124
122
 
125
123
  export interface SandboxProvider<
@@ -144,10 +142,12 @@ export interface SandboxProvider<
144
142
 
145
143
  export interface SandboxOps<
146
144
  TOptions extends SandboxCreateOptions = SandboxCreateOptions,
145
+ TCtx = unknown,
147
146
  > {
148
147
  createSandbox(
149
- options?: TOptions
150
- ): Promise<{ sandboxId: string; stateUpdate?: Record<string, unknown> }>;
148
+ options?: TOptions,
149
+ ctx?: TCtx
150
+ ): Promise<{ sandboxId: string } | null>;
151
151
  destroySandbox(sandboxId: string): Promise<void>;
152
152
  pauseSandbox(sandboxId: string): Promise<void>;
153
153
  snapshotSandbox(sandboxId: string): Promise<SandboxSnapshot>;
@@ -166,8 +166,9 @@ export interface SandboxOps<
166
166
  export type PrefixedSandboxOps<
167
167
  TPrefix extends string,
168
168
  TOptions extends SandboxCreateOptions = SandboxCreateOptions,
169
+ TCtx = unknown,
169
170
  > = {
170
- [K in keyof SandboxOps<TOptions> as `${TPrefix}${Capitalize<K & string>}`]: SandboxOps<TOptions>[K];
171
+ [K in keyof SandboxOps<TOptions, TCtx> as `${TPrefix}${Capitalize<K & string>}`]: SandboxOps<TOptions, TCtx>[K];
171
172
  };
172
173
 
173
174
  // ============================================================================
@@ -46,7 +46,13 @@ vi.mock("@temporalio/workflow", () => {
46
46
  uuid4: () =>
47
47
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
48
48
  ApplicationFailure: MockApplicationFailure,
49
- log: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
49
+ log: {
50
+ trace: () => {},
51
+ debug: () => {},
52
+ info: () => {},
53
+ warn: () => {},
54
+ error: () => {},
55
+ },
50
56
  };
51
57
  });
52
58
 
@@ -791,45 +797,6 @@ describe("createSession integration", () => {
791
797
  expect(at(humanOps, 0).args[2]).toBe("async context");
792
798
  });
793
799
 
794
- // --- Sandbox stateUpdate merge ---
795
-
796
- it("merges sandbox stateUpdate into state manager", async () => {
797
- const { ops } = createMockThreadOps();
798
-
799
- const sandboxOps: SandboxOps = {
800
- createSandbox: async () => ({
801
- sandboxId: "sb-1",
802
- stateUpdate: { customField: "from-sandbox" },
803
- }),
804
- destroySandbox: async () => {},
805
- snapshotSandbox: async () => ({
806
- sandboxId: "sb-1",
807
- providerId: "test",
808
- data: null,
809
- createdAt: new Date().toISOString(),
810
- }),
811
- forkSandbox: async () => "forked-sandbox-id",
812
- pauseSandbox: async () => {},
813
- };
814
-
815
- const session = await createSession({
816
- agentName: "TestAgent",
817
- thread: { mode: "new", threadId: "thread-1" },
818
- runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
819
- threadOps: ops,
820
- buildContextMessage: () => "go",
821
- sandboxOps,
822
- });
823
-
824
- const stateManager = createAgentStateManager<{ customField: string }>({
825
- initialState: { systemPrompt: "test", customField: "" },
826
- });
827
-
828
- await session.runSession({ stateManager });
829
-
830
- expect(stateManager.get("customField")).toBe("from-sandbox");
831
- });
832
-
833
800
  // --- Skill resourceContents seeded as initialFiles ---
834
801
 
835
802
  it("passes skill resourceContents as initialFiles to createSandbox", async () => {