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.
- package/dist/{activities-1xrWRrGJ.d.cts → activities-BeveyY9b.d.cts} +2 -2
- package/dist/{activities-DOViDCTE.d.ts → activities-NT3rcw66.d.ts} +2 -2
- package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
- package/dist/adapters/sandbox/bedrock/index.d.cts +3 -3
- package/dist/adapters/sandbox/bedrock/index.d.ts +3 -3
- package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +1 -1
- package/dist/adapters/sandbox/daytona/index.d.ts +1 -1
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +0 -1
- package/dist/adapters/thread/anthropic/index.cjs.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/index.js +0 -1
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- 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.cjs +0 -1
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- 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/index.js +0 -1
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- 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.cjs +0 -1
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- 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/index.js +0 -1
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
- package/dist/index.cjs +69 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -15
- package/dist/index.d.ts +78 -15
- package/dist/index.js +69 -52
- package/dist/index.js.map +1 -1
- package/dist/{proxy-78nc985d.d.ts → proxy-BgswT47M.d.ts} +1 -1
- package/dist/{proxy-Bm2UTiO_.d.cts → proxy-OJihshQF.d.cts} +1 -1
- package/dist/{thread-manager-07BaYu_z.d.ts → thread-manager-BS477gj8.d.ts} +1 -1
- package/dist/{thread-manager-BRE5KkHB.d.cts → thread-manager-DH0zv05W.d.cts} +1 -1
- package/dist/{thread-manager-CxbWo7q_.d.ts → thread-manager-iUplxEZt.d.ts} +1 -1
- package/dist/{thread-manager-CatBkarc.d.cts → thread-manager-lfN0V-gH.d.cts} +1 -1
- package/dist/{types-ChAMwU3q.d.ts → types-AujBIMMn.d.cts} +5 -8
- package/dist/{types-ChAMwU3q.d.cts → types-AujBIMMn.d.ts} +5 -8
- package/dist/{types-DAv_SLN8.d.ts → types-CCIc7Eam.d.ts} +1 -1
- package/dist/{types-BkVoEyiH.d.ts → types-D90Q5aOh.d.ts} +140 -139
- package/dist/{types-BdCdR41N.d.ts → types-DBk-C8zM.d.ts} +1 -1
- package/dist/{types-ZHs2v9Ap.d.cts → types-DUvEZSDe.d.cts} +1 -1
- package/dist/{types-seDYom4M.d.cts → types-DVdT5ybA.d.cts} +140 -139
- package/dist/{types-Dpz2gXLk.d.cts → types-DgIVPOa1.d.cts} +1 -1
- package/dist/{workflow-B4T3la0p.d.cts → workflow-Cj4DxGdM.d.cts} +2 -2
- package/dist/{workflow-DCmaXLZ_.d.ts → workflow-CzrBdCcJ.d.ts} +2 -2
- package/dist/workflow.cjs +31 -43
- 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 +31 -43
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/thread/anthropic/thread-manager.ts +6 -6
- package/src/adapters/thread/google-genai/thread-manager.ts +6 -6
- package/src/adapters/thread/langchain/thread-manager.ts +6 -6
- package/src/index.ts +1 -0
- package/src/lib/lifecycle.ts +8 -3
- package/src/lib/sandbox/index.ts +2 -4
- package/src/lib/sandbox/manager.ts +128 -13
- package/src/lib/sandbox/sandbox.test.ts +136 -16
- package/src/lib/sandbox/types.ts +6 -5
- package/src/lib/session/session.integration.test.ts +7 -40
- package/src/lib/session/session.ts +63 -49
- package/src/lib/session/types.ts +22 -13
- package/src/lib/state/types.ts +9 -6
- package/src/lib/subagent/handler.ts +18 -12
- package/src/lib/subagent/register.ts +11 -12
- package/src/lib/types.ts +2 -0
- package/src/lib/virtual-fs/types.ts +8 -14
- package/src/lib/virtual-fs/with-virtual-fs.ts +4 -4
- package/src/tools/bash/bash.test.ts +2 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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)
|
package/src/lib/lifecycle.ts
CHANGED
|
@@ -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
|
-
| {
|
|
39
|
+
| {
|
|
40
|
+
mode: "inherit";
|
|
41
|
+
sandboxId: string;
|
|
42
|
+
};
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
45
|
* What to do with the sandbox when the session exits.
|
package/src/lib/sandbox/index.ts
CHANGED
|
@@ -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> & {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
268
|
+
const { sandboxId } = await mustCreate(manager, {
|
|
149
269
|
initialFiles: {
|
|
150
270
|
"/dir/a.txt": "a",
|
|
151
271
|
"/dir/b.txt": "b",
|
package/src/lib/sandbox/types.ts
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
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 () => {
|