zeitlich 0.2.15 → 0.2.17
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 +125 -64
- package/dist/adapters/sandbox/daytona/index.cjs +52 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +10 -2
- package/dist/adapters/sandbox/daytona/index.d.ts +10 -2
- package/dist/adapters/sandbox/daytona/index.js +52 -23
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +21 -16
- 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 +21 -16
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/virtual/index.cjs +83 -38
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +6 -6
- package/dist/adapters/sandbox/virtual/index.d.ts +6 -6
- package/dist/adapters/sandbox/virtual/index.js +80 -38
- package/dist/adapters/sandbox/virtual/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +2 -2
- package/dist/adapters/thread/google-genai/index.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.cjs +2 -2
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +2 -2
- package/dist/adapters/thread/langchain/index.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.js +2 -2
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/index.cjs +102 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +98 -11
- package/dist/index.js.map +1 -1
- package/dist/{types-CwwgQ_9H.d.ts → queries-BlC1I3DK.d.ts} +48 -3
- package/dist/{types-BVP87m_W.d.cts → queries-DlJ3jE48.d.cts} +48 -3
- package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
- package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
- package/dist/{types-Dje1TdH6.d.cts → types-Bh-BbfCp.d.cts} +31 -12
- package/dist/{types-BWvIYK28.d.ts → types-NkiAxU4t.d.ts} +31 -12
- package/dist/workflow.cjs +102 -10
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +114 -40
- package/dist/workflow.d.ts +114 -40
- package/dist/workflow.js +98 -11
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
- package/src/adapters/sandbox/daytona/index.ts +16 -3
- package/src/adapters/sandbox/daytona/types.ts +4 -0
- package/src/adapters/sandbox/inmemory/index.ts +22 -16
- package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
- package/src/adapters/sandbox/virtual/index.ts +7 -3
- package/src/adapters/sandbox/virtual/provider.ts +5 -2
- package/src/adapters/sandbox/virtual/queries.ts +97 -0
- package/src/adapters/sandbox/virtual/types.ts +3 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
- package/src/adapters/thread/langchain/activities.ts +7 -5
- package/src/lib/sandbox/tree.integration.test.ts +153 -0
- package/src/lib/sandbox/types.ts +2 -0
- package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
- package/src/lib/session/session.integration.test.ts +853 -0
- package/src/lib/session/session.ts +5 -4
- package/src/lib/skills/skills.integration.test.ts +308 -0
- package/src/lib/state/manager.integration.test.ts +342 -0
- package/src/lib/subagent/define.ts +34 -47
- package/src/lib/subagent/handler.ts +9 -6
- package/src/lib/subagent/index.ts +4 -1
- package/src/lib/subagent/subagent.integration.test.ts +573 -0
- package/src/lib/subagent/types.ts +40 -10
- package/src/lib/subagent/workflow.ts +114 -0
- package/src/lib/thread/id.test.ts +50 -0
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
- package/src/lib/tool-router/router.integration.test.ts +699 -0
- package/src/lib/types.test.ts +29 -0
- package/src/lib/workflow.test.ts +131 -0
- package/src/lib/workflow.ts +45 -0
- package/src/workflow.ts +12 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
SubagentDefinition,
|
|
4
|
+
SubagentHandlerResponse,
|
|
5
|
+
SubagentWorkflowInput,
|
|
6
|
+
SubagentSessionInput,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Defines a subagent workflow with embedded metadata (name, description, resultSchema).
|
|
11
|
+
* The returned value can be passed directly to `defineSubagent` — no need to repeat
|
|
12
|
+
* the name, description, or resultSchema in the parent workflow.
|
|
13
|
+
*
|
|
14
|
+
* Internally maps `SubagentWorkflowInput` fields to session-compatible `SubagentSessionInput`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import {
|
|
19
|
+
* defineSubagentWorkflow,
|
|
20
|
+
* defineSubagent,
|
|
21
|
+
* createSession,
|
|
22
|
+
* createAgentStateManager,
|
|
23
|
+
* } from 'zeitlich/workflow';
|
|
24
|
+
*
|
|
25
|
+
* // Define once — carries name, description, resultSchema
|
|
26
|
+
* export const researcherWorkflow = defineSubagentWorkflow(
|
|
27
|
+
* {
|
|
28
|
+
* name: "researcher",
|
|
29
|
+
* description: "Researches topics on the web",
|
|
30
|
+
* resultSchema: z.object({ findings: z.string() }),
|
|
31
|
+
* },
|
|
32
|
+
* async (prompt, sessionInput) => {
|
|
33
|
+
* const stateManager = createAgentStateManager({
|
|
34
|
+
* initialState: { systemPrompt: "You are a researcher." },
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* const session = await createSession({
|
|
38
|
+
* ...sessionInput,
|
|
39
|
+
* agentName: "researcher",
|
|
40
|
+
* runAgent: runAgentActivity,
|
|
41
|
+
* buildContextMessage: () => [{ type: "text", text: prompt }],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* const { finalMessage, threadId } = await session.runSession({ stateManager });
|
|
45
|
+
* return { toolResponse: finalMessage ?? "No response", data: null, threadId };
|
|
46
|
+
* },
|
|
47
|
+
* );
|
|
48
|
+
*
|
|
49
|
+
* // Use in parent — only configure what's parent-specific
|
|
50
|
+
* export const researcher = defineSubagent(researcherWorkflow, {
|
|
51
|
+
* hooks: { onPostExecution: ({ result }) => console.log(result) },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
// Without resultSchema — data is null
|
|
56
|
+
export function defineSubagentWorkflow<
|
|
57
|
+
TContext extends Record<string, unknown> = Record<string, unknown>,
|
|
58
|
+
>(
|
|
59
|
+
config: { name: string; description: string },
|
|
60
|
+
fn: (
|
|
61
|
+
prompt: string,
|
|
62
|
+
sessionInput: SubagentSessionInput,
|
|
63
|
+
context: TContext
|
|
64
|
+
) => Promise<SubagentHandlerResponse<null>>
|
|
65
|
+
): SubagentDefinition<z.ZodNull, TContext>;
|
|
66
|
+
// With resultSchema — data is inferred from the schema
|
|
67
|
+
export function defineSubagentWorkflow<
|
|
68
|
+
TResult extends z.ZodType,
|
|
69
|
+
TContext extends Record<string, unknown> = Record<string, unknown>,
|
|
70
|
+
>(
|
|
71
|
+
config: { name: string; description: string; resultSchema: TResult },
|
|
72
|
+
fn: (
|
|
73
|
+
prompt: string,
|
|
74
|
+
sessionInput: SubagentSessionInput,
|
|
75
|
+
context: TContext
|
|
76
|
+
) => Promise<SubagentHandlerResponse<z.infer<TResult> | null>>
|
|
77
|
+
): SubagentDefinition<TResult, TContext>;
|
|
78
|
+
export function defineSubagentWorkflow(
|
|
79
|
+
config: { name: string; description: string; resultSchema?: z.ZodType },
|
|
80
|
+
fn: (
|
|
81
|
+
prompt: string,
|
|
82
|
+
sessionInput: SubagentSessionInput,
|
|
83
|
+
context: Record<string, unknown>
|
|
84
|
+
) => Promise<SubagentHandlerResponse<unknown>>
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
): SubagentDefinition<any, any> {
|
|
87
|
+
const workflow = async (
|
|
88
|
+
prompt: string,
|
|
89
|
+
workflowInput: SubagentWorkflowInput,
|
|
90
|
+
context?: Record<string, unknown>
|
|
91
|
+
): Promise<SubagentHandlerResponse<unknown>> => {
|
|
92
|
+
const sessionInput: SubagentSessionInput = {
|
|
93
|
+
agentName: config.name,
|
|
94
|
+
...(workflowInput.previousThreadId && {
|
|
95
|
+
threadId: workflowInput.previousThreadId,
|
|
96
|
+
continueThread: true,
|
|
97
|
+
}),
|
|
98
|
+
...(workflowInput.sandboxId && { sandboxId: workflowInput.sandboxId }),
|
|
99
|
+
};
|
|
100
|
+
return fn(prompt, sessionInput, context ?? {});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// for temporal workflow name
|
|
104
|
+
Object.defineProperty(workflow, "name", { value: config.name });
|
|
105
|
+
|
|
106
|
+
return Object.assign(workflow, {
|
|
107
|
+
agentName: config.name,
|
|
108
|
+
description: config.description,
|
|
109
|
+
...(config.resultSchema !== undefined && {
|
|
110
|
+
resultSchema: config.resultSchema,
|
|
111
|
+
}),
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
}) as SubagentDefinition<any, any>;
|
|
114
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
let uuidCounter = 0;
|
|
4
|
+
|
|
5
|
+
vi.mock("@temporalio/workflow", () => ({
|
|
6
|
+
uuid4: () => {
|
|
7
|
+
uuidCounter++;
|
|
8
|
+
const bytes = Array.from({ length: 16 }, (_, i) =>
|
|
9
|
+
((uuidCounter * 31 + i * 7 + uuidCounter * i) & 0xff)
|
|
10
|
+
.toString(16)
|
|
11
|
+
.padStart(2, "0"),
|
|
12
|
+
).join("");
|
|
13
|
+
return `${bytes.slice(0, 8)}-${bytes.slice(8, 12)}-${bytes.slice(12, 16)}-${bytes.slice(16, 20)}-${bytes.slice(20, 32)}`;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { getShortId } from "./id";
|
|
18
|
+
|
|
19
|
+
describe("getShortId", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
uuidCounter = 0;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns a string of default length 12", () => {
|
|
25
|
+
const id = getShortId();
|
|
26
|
+
expect(id).toHaveLength(12);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns a string of custom length", () => {
|
|
30
|
+
const id = getShortId(6);
|
|
31
|
+
expect(id).toHaveLength(6);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("contains only base-62 characters", () => {
|
|
35
|
+
const base62Regex = /^[A-Za-z0-9]+$/;
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
expect(getShortId()).toMatch(base62Regex);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("generates unique IDs on successive calls", () => {
|
|
42
|
+
const ids = new Set(Array.from({ length: 20 }, () => getShortId()));
|
|
43
|
+
expect(ids.size).toBe(20);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns empty string for length 0", () => {
|
|
47
|
+
const id = getShortId(0);
|
|
48
|
+
expect(id).toBe("");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { withAutoAppend } from "./auto-append";
|
|
3
|
+
import { withSandbox } from "./with-sandbox";
|
|
4
|
+
import type { RouterContext, ToolHandlerResponse } from "./types";
|
|
5
|
+
import type { ToolResultConfig } from "../types";
|
|
6
|
+
import type { Sandbox } from "../sandbox/types";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// withAutoAppend
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
describe("withAutoAppend", () => {
|
|
13
|
+
it("appends tool result via threadHandler and sets resultAppended", async () => {
|
|
14
|
+
const appended: ToolResultConfig[] = [];
|
|
15
|
+
const threadHandler = async (config: ToolResultConfig) => {
|
|
16
|
+
appended.push(config);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const innerHandler = async (
|
|
20
|
+
args: { text: string },
|
|
21
|
+
_ctx: RouterContext,
|
|
22
|
+
): Promise<ToolHandlerResponse<{ echoed: string }>> => ({
|
|
23
|
+
toolResponse: `Echo: ${args.text}`,
|
|
24
|
+
data: { echoed: args.text },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const wrapped = withAutoAppend(threadHandler, innerHandler);
|
|
28
|
+
|
|
29
|
+
const result = await wrapped(
|
|
30
|
+
{ text: "hello" },
|
|
31
|
+
{
|
|
32
|
+
threadId: "thread-1",
|
|
33
|
+
toolCallId: "tc-1",
|
|
34
|
+
toolName: "Echo",
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(result.resultAppended).toBe(true);
|
|
39
|
+
expect(result.toolResponse).toBe("Response appended via withAutoAppend");
|
|
40
|
+
expect(result.data).toEqual({ echoed: "hello" });
|
|
41
|
+
|
|
42
|
+
expect(appended).toHaveLength(1);
|
|
43
|
+
const firstAppended = appended.at(0);
|
|
44
|
+
if (!firstAppended) throw new Error("expected appended item");
|
|
45
|
+
expect(firstAppended.threadId).toBe("thread-1");
|
|
46
|
+
expect(firstAppended.toolCallId).toBe("tc-1");
|
|
47
|
+
expect(firstAppended.toolName).toBe("Echo");
|
|
48
|
+
expect(firstAppended.content).toBe("Echo: hello");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("preserves original data but replaces toolResponse", async () => {
|
|
52
|
+
const threadHandler = async () => {};
|
|
53
|
+
|
|
54
|
+
const innerHandler = async (): Promise<
|
|
55
|
+
ToolHandlerResponse<{ large: string }>
|
|
56
|
+
> => ({
|
|
57
|
+
toolResponse: "A".repeat(10000),
|
|
58
|
+
data: { large: "summary" },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const wrapped = withAutoAppend(threadHandler, innerHandler);
|
|
62
|
+
|
|
63
|
+
const result = await wrapped(
|
|
64
|
+
{},
|
|
65
|
+
{ threadId: "t", toolCallId: "tc", toolName: "BigTool" },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result.toolResponse).toBe("Response appended via withAutoAppend");
|
|
69
|
+
expect(result.data).toEqual({ large: "summary" });
|
|
70
|
+
expect(result.resultAppended).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("propagates handler errors without appending", async () => {
|
|
74
|
+
const appendSpy = vi.fn();
|
|
75
|
+
const threadHandler = appendSpy;
|
|
76
|
+
|
|
77
|
+
const innerHandler = async (): Promise<ToolHandlerResponse<null>> => {
|
|
78
|
+
throw new Error("handler failed");
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const wrapped = withAutoAppend(threadHandler, innerHandler);
|
|
82
|
+
|
|
83
|
+
await expect(
|
|
84
|
+
wrapped(
|
|
85
|
+
{},
|
|
86
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Fail" },
|
|
87
|
+
),
|
|
88
|
+
).rejects.toThrow("handler failed");
|
|
89
|
+
|
|
90
|
+
expect(appendSpy).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("uses correct context fields for thread handler config", async () => {
|
|
94
|
+
let capturedConfig: ToolResultConfig | null = null;
|
|
95
|
+
const threadHandler = async (config: ToolResultConfig) => {
|
|
96
|
+
capturedConfig = config;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const innerHandler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
100
|
+
toolResponse: "result content",
|
|
101
|
+
data: null,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const wrapped = withAutoAppend(threadHandler, innerHandler);
|
|
105
|
+
|
|
106
|
+
await wrapped(
|
|
107
|
+
{},
|
|
108
|
+
{
|
|
109
|
+
threadId: "my-thread",
|
|
110
|
+
toolCallId: "my-tc",
|
|
111
|
+
toolName: "MyTool",
|
|
112
|
+
sandboxId: "sb-1",
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(capturedConfig).toEqual({
|
|
117
|
+
threadId: "my-thread",
|
|
118
|
+
toolCallId: "my-tc",
|
|
119
|
+
toolName: "MyTool",
|
|
120
|
+
content: "result content",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// withSandbox
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
describe("withSandbox", () => {
|
|
130
|
+
function createMockSandbox(): Sandbox {
|
|
131
|
+
return {
|
|
132
|
+
id: "mock-sandbox",
|
|
133
|
+
capabilities: {
|
|
134
|
+
filesystem: true,
|
|
135
|
+
execution: true,
|
|
136
|
+
persistence: false,
|
|
137
|
+
},
|
|
138
|
+
fs: {
|
|
139
|
+
workspaceBase: "/",
|
|
140
|
+
exists: async () => false,
|
|
141
|
+
stat: async () => ({
|
|
142
|
+
isFile: false,
|
|
143
|
+
isDirectory: false,
|
|
144
|
+
isSymbolicLink: false,
|
|
145
|
+
size: 0,
|
|
146
|
+
mtime: new Date(),
|
|
147
|
+
}),
|
|
148
|
+
readdir: async () => [],
|
|
149
|
+
readdirWithFileTypes: async () => [],
|
|
150
|
+
readFile: async () => "",
|
|
151
|
+
readFileBuffer: async () => new Uint8Array(),
|
|
152
|
+
writeFile: async () => {},
|
|
153
|
+
appendFile: async () => {},
|
|
154
|
+
mkdir: async () => {},
|
|
155
|
+
rm: async () => {},
|
|
156
|
+
cp: async () => {},
|
|
157
|
+
mv: async () => {},
|
|
158
|
+
readlink: async () => "",
|
|
159
|
+
resolvePath: (base: string, path: string) => base + "/" + path,
|
|
160
|
+
},
|
|
161
|
+
exec: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
|
|
162
|
+
destroy: async () => {},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
it("resolves sandbox and passes it to handler", async () => {
|
|
167
|
+
const mockSandbox = createMockSandbox();
|
|
168
|
+
const manager = {
|
|
169
|
+
getSandbox: async (id: string) => {
|
|
170
|
+
expect(id).toBe("sb-42");
|
|
171
|
+
return mockSandbox;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let capturedSandbox: Sandbox | null = null;
|
|
176
|
+
let capturedSandboxId: string | null = null;
|
|
177
|
+
|
|
178
|
+
const handler = async (
|
|
179
|
+
_args: { text: string },
|
|
180
|
+
ctx: RouterContext & { sandbox: Sandbox; sandboxId: string },
|
|
181
|
+
): Promise<ToolHandlerResponse<null>> => {
|
|
182
|
+
capturedSandbox = ctx.sandbox;
|
|
183
|
+
capturedSandboxId = ctx.sandboxId;
|
|
184
|
+
return { toolResponse: "ok", data: null };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const wrapped = withSandbox(manager, handler);
|
|
188
|
+
|
|
189
|
+
const result = await wrapped(
|
|
190
|
+
{ text: "hello" },
|
|
191
|
+
{
|
|
192
|
+
threadId: "thread-1",
|
|
193
|
+
toolCallId: "tc-1",
|
|
194
|
+
toolName: "Test",
|
|
195
|
+
sandboxId: "sb-42",
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(result.toolResponse).toBe("ok");
|
|
200
|
+
expect(capturedSandbox).toBe(mockSandbox);
|
|
201
|
+
expect(capturedSandboxId).toBe("sb-42");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns error when no sandboxId is present", async () => {
|
|
205
|
+
const manager = {
|
|
206
|
+
getSandbox: vi.fn(),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handler = async (): Promise<ToolHandlerResponse<string>> => ({
|
|
210
|
+
toolResponse: "should not run",
|
|
211
|
+
data: "nope",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const wrapped = withSandbox(manager, handler);
|
|
215
|
+
|
|
216
|
+
const result = await wrapped(
|
|
217
|
+
{},
|
|
218
|
+
{
|
|
219
|
+
threadId: "thread-1",
|
|
220
|
+
toolCallId: "tc-1",
|
|
221
|
+
toolName: "Bash",
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(result.toolResponse).toContain("No sandbox configured");
|
|
226
|
+
expect(result.toolResponse).toContain("Bash");
|
|
227
|
+
expect(result.data).toBeNull();
|
|
228
|
+
expect(manager.getSandbox).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("returns error when sandboxId is undefined explicitly", async () => {
|
|
232
|
+
const manager = {
|
|
233
|
+
getSandbox: vi.fn(),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
237
|
+
toolResponse: "nope",
|
|
238
|
+
data: null,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const wrapped = withSandbox(manager, handler);
|
|
242
|
+
|
|
243
|
+
const result = await wrapped(
|
|
244
|
+
{},
|
|
245
|
+
{
|
|
246
|
+
threadId: "t",
|
|
247
|
+
toolCallId: "tc",
|
|
248
|
+
toolName: "Grep",
|
|
249
|
+
sandboxId: undefined,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(result.toolResponse).toContain("No sandbox configured");
|
|
254
|
+
expect(result.data).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("propagates getSandbox errors", async () => {
|
|
258
|
+
const manager = {
|
|
259
|
+
getSandbox: async () => {
|
|
260
|
+
throw new Error("sandbox not found");
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
265
|
+
toolResponse: "ok",
|
|
266
|
+
data: null,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const wrapped = withSandbox(manager, handler);
|
|
270
|
+
|
|
271
|
+
await expect(
|
|
272
|
+
wrapped(
|
|
273
|
+
{},
|
|
274
|
+
{
|
|
275
|
+
threadId: "t",
|
|
276
|
+
toolCallId: "tc",
|
|
277
|
+
toolName: "Test",
|
|
278
|
+
sandboxId: "sb-missing",
|
|
279
|
+
},
|
|
280
|
+
),
|
|
281
|
+
).rejects.toThrow("sandbox not found");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("passes all RouterContext fields through to inner handler", async () => {
|
|
285
|
+
const mockSandbox = createMockSandbox();
|
|
286
|
+
const manager = { getSandbox: async () => mockSandbox };
|
|
287
|
+
|
|
288
|
+
let capturedCtx: (RouterContext & { sandbox: Sandbox; sandboxId: string }) | null = null;
|
|
289
|
+
|
|
290
|
+
const handler = async (
|
|
291
|
+
_args: unknown,
|
|
292
|
+
ctx: RouterContext & { sandbox: Sandbox; sandboxId: string },
|
|
293
|
+
): Promise<ToolHandlerResponse<null>> => {
|
|
294
|
+
capturedCtx = ctx;
|
|
295
|
+
return { toolResponse: "ok", data: null };
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const wrapped = withSandbox(manager, handler);
|
|
299
|
+
|
|
300
|
+
await wrapped(
|
|
301
|
+
{},
|
|
302
|
+
{
|
|
303
|
+
threadId: "my-thread",
|
|
304
|
+
toolCallId: "my-tc",
|
|
305
|
+
toolName: "MyTool",
|
|
306
|
+
sandboxId: "my-sandbox",
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(capturedCtx).toEqual(
|
|
311
|
+
expect.objectContaining({
|
|
312
|
+
threadId: "my-thread",
|
|
313
|
+
toolCallId: "my-tc",
|
|
314
|
+
toolName: "MyTool",
|
|
315
|
+
sandboxId: "my-sandbox",
|
|
316
|
+
sandbox: mockSandbox,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("handles sandboxId with empty string as falsy", async () => {
|
|
322
|
+
const manager = { getSandbox: vi.fn() };
|
|
323
|
+
|
|
324
|
+
const handler = async (): Promise<ToolHandlerResponse<null>> => ({
|
|
325
|
+
toolResponse: "nope",
|
|
326
|
+
data: null,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const wrapped = withSandbox(manager, handler);
|
|
330
|
+
|
|
331
|
+
const result = await wrapped(
|
|
332
|
+
{},
|
|
333
|
+
{
|
|
334
|
+
threadId: "t",
|
|
335
|
+
toolCallId: "tc",
|
|
336
|
+
toolName: "Test",
|
|
337
|
+
sandboxId: "",
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(result.toolResponse).toContain("No sandbox configured");
|
|
342
|
+
expect(manager.getSandbox).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
});
|