zeitlich 0.2.22 → 0.2.23

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 (101) hide show
  1. package/README.md +242 -59
  2. package/dist/adapters/sandbox/daytona/index.cjs +4 -1
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +2 -1
  5. package/dist/adapters/sandbox/daytona/index.d.ts +2 -1
  6. package/dist/adapters/sandbox/daytona/index.js +4 -1
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.cjs +1 -0
  9. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  10. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  11. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  12. package/dist/adapters/sandbox/daytona/workflow.js +1 -0
  13. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  14. package/dist/adapters/sandbox/inmemory/index.cjs +16 -2
  15. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/inmemory/index.d.cts +3 -2
  17. package/dist/adapters/sandbox/inmemory/index.d.ts +3 -2
  18. package/dist/adapters/sandbox/inmemory/index.js +16 -2
  19. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  20. package/dist/adapters/sandbox/inmemory/workflow.cjs +1 -0
  21. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  22. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  23. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  24. package/dist/adapters/sandbox/inmemory/workflow.js +1 -0
  25. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  26. package/dist/adapters/sandbox/virtual/index.cjs +33 -9
  27. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/virtual/index.d.cts +6 -5
  29. package/dist/adapters/sandbox/virtual/index.d.ts +6 -5
  30. package/dist/adapters/sandbox/virtual/index.js +33 -9
  31. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  32. package/dist/adapters/sandbox/virtual/workflow.cjs +1 -0
  33. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -1
  34. package/dist/adapters/sandbox/virtual/workflow.d.cts +3 -3
  35. package/dist/adapters/sandbox/virtual/workflow.d.ts +3 -3
  36. package/dist/adapters/sandbox/virtual/workflow.js +1 -0
  37. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -1
  38. package/dist/adapters/thread/google-genai/index.d.cts +3 -3
  39. package/dist/adapters/thread/google-genai/index.d.ts +3 -3
  40. package/dist/adapters/thread/google-genai/workflow.d.cts +3 -3
  41. package/dist/adapters/thread/google-genai/workflow.d.ts +3 -3
  42. package/dist/adapters/thread/langchain/index.d.cts +3 -3
  43. package/dist/adapters/thread/langchain/index.d.ts +3 -3
  44. package/dist/adapters/thread/langchain/workflow.d.cts +3 -3
  45. package/dist/adapters/thread/langchain/workflow.d.ts +3 -3
  46. package/dist/index.cjs +247 -57
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +9 -8
  49. package/dist/index.d.ts +9 -8
  50. package/dist/index.js +245 -55
  51. package/dist/index.js.map +1 -1
  52. package/dist/{queries-Bw6WEPMw.d.cts → queries-DModcWRy.d.cts} +1 -1
  53. package/dist/{queries-C27raDaB.d.ts → queries-byD0jr1Y.d.ts} +1 -1
  54. package/dist/{types-ClsHhtwL.d.cts → types-B50pBPEV.d.ts} +159 -35
  55. package/dist/{types-YbL7JpEA.d.cts → types-Bll19FZJ.d.cts} +7 -0
  56. package/dist/{types-YbL7JpEA.d.ts → types-Bll19FZJ.d.ts} +7 -0
  57. package/dist/{types-BJ8itUAl.d.cts → types-BuXdFhaZ.d.cts} +6 -6
  58. package/dist/{types-HBosetv3.d.cts → types-ChAMwU3q.d.cts} +2 -0
  59. package/dist/{types-HBosetv3.d.ts → types-ChAMwU3q.d.ts} +2 -0
  60. package/dist/{types-C5bkx6kQ.d.ts → types-DQW8l7pY.d.cts} +159 -35
  61. package/dist/{types-ENYCKFBk.d.ts → types-GZ76HZSj.d.ts} +6 -6
  62. package/dist/workflow.cjs +241 -57
  63. package/dist/workflow.cjs.map +1 -1
  64. package/dist/workflow.d.cts +49 -32
  65. package/dist/workflow.d.ts +49 -32
  66. package/dist/workflow.js +239 -55
  67. package/dist/workflow.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  70. package/src/adapters/sandbox/daytona/index.ts +4 -0
  71. package/src/adapters/sandbox/daytona/proxy.ts +4 -3
  72. package/src/adapters/sandbox/e2b/index.ts +5 -0
  73. package/src/adapters/sandbox/inmemory/index.ts +24 -4
  74. package/src/adapters/sandbox/inmemory/proxy.ts +2 -2
  75. package/src/adapters/sandbox/virtual/filesystem.ts +41 -17
  76. package/src/adapters/sandbox/virtual/provider.ts +4 -0
  77. package/src/adapters/sandbox/virtual/proxy.ts +1 -0
  78. package/src/adapters/sandbox/virtual/types.ts +9 -4
  79. package/src/lib/lifecycle.ts +57 -0
  80. package/src/lib/sandbox/manager.ts +13 -1
  81. package/src/lib/sandbox/types.ts +13 -4
  82. package/src/lib/session/index.ts +1 -0
  83. package/src/lib/session/session-edge-cases.integration.test.ts +447 -33
  84. package/src/lib/session/session.integration.test.ts +52 -32
  85. package/src/lib/session/session.ts +107 -33
  86. package/src/lib/session/types.ts +55 -16
  87. package/src/lib/subagent/define.ts +5 -4
  88. package/src/lib/subagent/handler.ts +139 -14
  89. package/src/lib/subagent/index.ts +3 -0
  90. package/src/lib/subagent/register.ts +10 -3
  91. package/src/lib/subagent/signals.ts +8 -0
  92. package/src/lib/subagent/subagent.integration.test.ts +853 -150
  93. package/src/lib/subagent/tool.ts +2 -2
  94. package/src/lib/subagent/types.ts +77 -19
  95. package/src/lib/subagent/workflow.ts +83 -12
  96. package/src/lib/tool-router/router.integration.test.ts +137 -4
  97. package/src/lib/tool-router/router.ts +13 -3
  98. package/src/lib/tool-router/types.ts +7 -0
  99. package/src/lib/workflow.test.ts +89 -21
  100. package/src/lib/workflow.ts +33 -18
  101. package/src/workflow.ts +6 -1
@@ -1,21 +1,79 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { describe, expect, it, vi, afterEach } from "vitest";
2
2
  import { z } from "zod";
3
3
 
4
+ let capturedSignalHandler:
5
+ | ((payload: { childWorkflowId: string; result: unknown }) => void)
6
+ | null = null;
7
+
8
+ let nextStartChildResult: ((prompt: string) => unknown) | null = null;
9
+
4
10
  vi.mock("@temporalio/workflow", () => {
5
11
  let counter = 0;
12
+
13
+ class MockApplicationFailure extends Error {
14
+ nonRetryable?: boolean;
15
+ static create({
16
+ message,
17
+ nonRetryable,
18
+ }: {
19
+ message: string;
20
+ nonRetryable?: boolean;
21
+ }) {
22
+ const err = new MockApplicationFailure(message);
23
+ err.nonRetryable = nonRetryable;
24
+ return err;
25
+ }
26
+ static fromError(error: unknown) {
27
+ const src = error instanceof Error ? error : new Error(String(error));
28
+ return new MockApplicationFailure(src.message);
29
+ }
30
+ }
31
+
6
32
  return {
7
- workflowInfo: () => ({ taskQueue: "default-queue" }),
8
- executeChild: vi.fn(
9
- async (_workflow: unknown, opts: { args: unknown[] }) => {
33
+ workflowInfo: () => ({
34
+ taskQueue: "default-queue",
35
+ workflowId: "child-wf-1",
36
+ parent: { workflowId: "parent-wf-1" },
37
+ }),
38
+ defineSignal: vi.fn((_name: string) => ({ __signal: true })),
39
+ setHandler: vi.fn(
40
+ (_signal: unknown, handler: (...a: unknown[]) => void) => {
41
+ capturedSignalHandler = handler as typeof capturedSignalHandler;
42
+ }
43
+ ),
44
+ condition: vi.fn(async (fn: () => boolean) => {
45
+ if (!fn()) throw new Error("condition predicate was not satisfied");
46
+ }),
47
+ startChild: vi.fn(
48
+ async (
49
+ _workflow: unknown,
50
+ opts: { workflowId: string; args: unknown[] }
51
+ ) => {
10
52
  const prompt = (opts.args as [string])[0];
53
+ const result = nextStartChildResult
54
+ ? nextStartChildResult(prompt)
55
+ : {
56
+ toolResponse: `Response to: ${prompt}`,
57
+ data: { result: "child-data" },
58
+ threadId: "child-thread-1",
59
+ usage: { inputTokens: 100, outputTokens: 50 },
60
+ };
61
+
62
+ if (capturedSignalHandler) {
63
+ capturedSignalHandler({ childWorkflowId: opts.workflowId, result });
64
+ }
65
+
11
66
  return {
12
- toolResponse: `Response to: ${prompt}`,
13
- data: { result: "child-data" },
14
- threadId: "child-thread-1",
15
- usage: { inputTokens: 100, outputTokens: 50 },
67
+ signal: vi.fn(),
68
+ result: () => Promise.resolve(result),
69
+ workflowId: opts.workflowId,
16
70
  };
17
71
  }
18
72
  ),
73
+ getExternalWorkflowHandle: vi.fn((_id: string) => ({
74
+ signal: vi.fn(),
75
+ })),
76
+ ApplicationFailure: MockApplicationFailure,
19
77
  uuid4: () => {
20
78
  counter++;
21
79
  const bytes = Array.from({ length: 16 }, (_, i) =>
@@ -34,8 +92,23 @@ import { defineSubagent } from "./define";
34
92
  import type {
35
93
  SubagentConfig,
36
94
  SubagentSessionInput,
95
+ SubagentWorkflow,
37
96
  SubagentWorkflowInput,
38
97
  } from "./types";
98
+ afterEach(() => {
99
+ nextStartChildResult = null;
100
+ capturedSignalHandler = null;
101
+ });
102
+
103
+ function mockWorkflow(name?: string): SubagentWorkflow {
104
+ const fn = async () => ({
105
+ toolResponse: "ok",
106
+ data: null,
107
+ threadId: "t-1",
108
+ });
109
+ if (name) Object.defineProperty(fn, "name", { value: name });
110
+ return fn as SubagentWorkflow;
111
+ }
39
112
 
40
113
  // ---------------------------------------------------------------------------
41
114
  // createSubagentTool
@@ -47,7 +120,7 @@ describe("createSubagentTool", () => {
47
120
  {
48
121
  agentName: "researcher",
49
122
  description: "Researches topics",
50
- workflow: "researcherWorkflow",
123
+ workflow: mockWorkflow(),
51
124
  },
52
125
  ]);
53
126
 
@@ -68,77 +141,80 @@ describe("createSubagentTool", () => {
68
141
  {
69
142
  agentName: "researcher",
70
143
  description: "Researches",
71
- workflow: "researcherWorkflow",
144
+ workflow: mockWorkflow(),
72
145
  },
73
146
  {
74
147
  agentName: "writer",
75
148
  description: "Writes",
76
- workflow: "writerWorkflow",
149
+ workflow: mockWorkflow(),
77
150
  },
78
151
  ]);
79
152
 
80
- const validResearcher = tool.schema.safeParse({
81
- subagent: "researcher",
82
- description: "desc",
83
- prompt: "prompt",
84
- });
85
- expect(validResearcher.success).toBe(true);
86
-
87
- const validWriter = tool.schema.safeParse({
88
- subagent: "writer",
89
- description: "desc",
90
- prompt: "prompt",
91
- });
92
- expect(validWriter.success).toBe(true);
93
-
94
- const invalidAgent = tool.schema.safeParse({
95
- subagent: "nonexistent",
96
- description: "desc",
97
- prompt: "prompt",
98
- });
99
- expect(invalidAgent.success).toBe(false);
153
+ expect(
154
+ tool.schema.safeParse({
155
+ subagent: "researcher",
156
+ description: "d",
157
+ prompt: "p",
158
+ }).success
159
+ ).toBe(true);
160
+ expect(
161
+ tool.schema.safeParse({
162
+ subagent: "writer",
163
+ description: "d",
164
+ prompt: "p",
165
+ }).success
166
+ ).toBe(true);
167
+ expect(
168
+ tool.schema.safeParse({
169
+ subagent: "nonexistent",
170
+ description: "d",
171
+ prompt: "p",
172
+ }).success
173
+ ).toBe(false);
100
174
  });
101
175
 
102
- it("adds threadId field when allowThreadContinuation is set", () => {
176
+ it("adds threadId field when thread mode allows continuation", () => {
103
177
  const tool = createSubagentTool([
104
178
  {
105
179
  agentName: "agent",
106
180
  description: "supports continuation",
107
- workflow: "workflow",
108
- allowThreadContinuation: true,
181
+ workflow: mockWorkflow(),
182
+ thread: "fork",
109
183
  },
110
184
  ]);
111
185
 
112
- const withThread = tool.schema.safeParse({
113
- subagent: "agent",
114
- description: "desc",
115
- prompt: "prompt",
116
- threadId: "some-thread",
117
- });
118
- expect(withThread.success).toBe(true);
119
-
120
- const withNull = tool.schema.safeParse({
121
- subagent: "agent",
122
- description: "desc",
123
- prompt: "prompt",
124
- threadId: null,
125
- });
126
- expect(withNull.success).toBe(true);
186
+ expect(
187
+ tool.schema.safeParse({
188
+ subagent: "agent",
189
+ description: "d",
190
+ prompt: "p",
191
+ threadId: "some-thread",
192
+ }).success
193
+ ).toBe(true);
194
+
195
+ expect(
196
+ tool.schema.safeParse({
197
+ subagent: "agent",
198
+ description: "d",
199
+ prompt: "p",
200
+ threadId: null,
201
+ }).success
202
+ ).toBe(true);
127
203
  });
128
204
 
129
- it("does not include threadId field when no subagent has allowThreadContinuation", () => {
205
+ it("does not include threadId field when thread mode is new", () => {
130
206
  const tool = createSubagentTool([
131
207
  {
132
208
  agentName: "basic",
133
209
  description: "basic agent",
134
- workflow: "workflow",
210
+ workflow: mockWorkflow(),
135
211
  },
136
212
  ]);
137
213
 
138
214
  const result = tool.schema.safeParse({
139
215
  subagent: "basic",
140
- description: "desc",
141
- prompt: "prompt",
216
+ description: "d",
217
+ prompt: "p",
142
218
  threadId: "should-strip",
143
219
  });
144
220
  expect(result.success).toBe(true);
@@ -158,8 +234,8 @@ describe("createSubagentTool", () => {
158
234
  {
159
235
  agentName: "cont-agent",
160
236
  description: "Supports continuation",
161
- workflow: "workflow",
162
- allowThreadContinuation: true,
237
+ workflow: mockWorkflow(),
238
+ thread: "fork",
163
239
  },
164
240
  ]);
165
241
 
@@ -175,11 +251,11 @@ describe("createSubagentHandler", () => {
175
251
  const basicSubagent: SubagentConfig = {
176
252
  agentName: "researcher",
177
253
  description: "Researches topics",
178
- workflow: "researcherWorkflow",
254
+ workflow: mockWorkflow("researcherWorkflow"),
179
255
  };
180
256
 
181
257
  it("executes child workflow and returns response", async () => {
182
- const handler = createSubagentHandler([basicSubagent]);
258
+ const { handler } = createSubagentHandler([basicSubagent]);
183
259
 
184
260
  const result = await handler(
185
261
  { subagent: "researcher", description: "test", prompt: "Find info" },
@@ -191,7 +267,7 @@ describe("createSubagentHandler", () => {
191
267
  });
192
268
 
193
269
  it("throws for unknown subagent name", async () => {
194
- const handler = createSubagentHandler([basicSubagent]);
270
+ const { handler } = createSubagentHandler([basicSubagent]);
195
271
 
196
272
  await expect(
197
273
  handler(
@@ -202,12 +278,12 @@ describe("createSubagentHandler", () => {
202
278
  });
203
279
 
204
280
  it("includes available subagent names in error message", async () => {
205
- const handler = createSubagentHandler([
281
+ const { handler } = createSubagentHandler([
206
282
  basicSubagent,
207
283
  {
208
284
  agentName: "writer",
209
285
  description: "Writes",
210
- workflow: "writerWorkflow",
286
+ workflow: mockWorkflow("writerWorkflow"),
211
287
  },
212
288
  ]);
213
289
 
@@ -220,8 +296,7 @@ describe("createSubagentHandler", () => {
220
296
  });
221
297
 
222
298
  it("validates result against resultSchema", async () => {
223
- const { executeChild } = await import("@temporalio/workflow");
224
- (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
299
+ nextStartChildResult = () => ({
225
300
  toolResponse: "result",
226
301
  data: { invalid: "data" },
227
302
  threadId: "child-t",
@@ -230,11 +305,11 @@ describe("createSubagentHandler", () => {
230
305
  const validatedSubagent: SubagentConfig = {
231
306
  agentName: "validated",
232
307
  description: "Has validation",
233
- workflow: "workflow",
308
+ workflow: mockWorkflow(),
234
309
  resultSchema: z.object({ expected: z.string() }),
235
310
  };
236
311
 
237
- const handler = createSubagentHandler([validatedSubagent]);
312
+ const { handler } = createSubagentHandler([validatedSubagent]);
238
313
 
239
314
  const result = await handler(
240
315
  { subagent: "validated", description: "test", prompt: "test" },
@@ -245,9 +320,8 @@ describe("createSubagentHandler", () => {
245
320
  expect(result.data).toBeNull();
246
321
  });
247
322
 
248
- it("appends thread ID when allowThreadContinuation is set", async () => {
249
- const { executeChild } = await import("@temporalio/workflow");
250
- (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
323
+ it("appends thread ID when thread is fork", async () => {
324
+ nextStartChildResult = () => ({
251
325
  toolResponse: "Some response",
252
326
  data: null,
253
327
  threadId: "child-thread-99",
@@ -256,11 +330,11 @@ describe("createSubagentHandler", () => {
256
330
  const contSubagent: SubagentConfig = {
257
331
  agentName: "cont",
258
332
  description: "Continues threads",
259
- workflow: "workflow",
260
- allowThreadContinuation: true,
333
+ workflow: mockWorkflow(),
334
+ thread: "fork",
261
335
  };
262
336
 
263
- const handler = createSubagentHandler([contSubagent]);
337
+ const { handler } = createSubagentHandler([contSubagent]);
264
338
 
265
339
  const result = await handler(
266
340
  { subagent: "cont", description: "test", prompt: "test" },
@@ -271,14 +345,13 @@ describe("createSubagentHandler", () => {
271
345
  });
272
346
 
273
347
  it("returns fallback when child workflow returns no toolResponse", async () => {
274
- const { executeChild } = await import("@temporalio/workflow");
275
- (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
348
+ nextStartChildResult = () => ({
276
349
  toolResponse: null,
277
350
  data: null,
278
351
  threadId: "child-t",
279
352
  });
280
353
 
281
- const handler = createSubagentHandler([basicSubagent]);
354
+ const { handler } = createSubagentHandler([basicSubagent]);
282
355
 
283
356
  const result = await handler(
284
357
  { subagent: "researcher", description: "test", prompt: "test" },
@@ -289,23 +362,18 @@ describe("createSubagentHandler", () => {
289
362
  expect(result.data).toBeNull();
290
363
  });
291
364
 
292
- it("passes sandboxId to child when sandbox is inherit", async () => {
293
- const { executeChild } = await import("@temporalio/workflow");
294
- const execMock = executeChild as ReturnType<typeof vi.fn>;
295
- execMock.mockResolvedValueOnce({
296
- toolResponse: "ok",
297
- data: null,
298
- threadId: "child-t",
299
- });
365
+ it("passes sandbox inherit to child when sandbox is inherit", async () => {
366
+ const { startChild } = await import("@temporalio/workflow");
367
+ const startMock = startChild as ReturnType<typeof vi.fn>;
300
368
 
301
369
  const inheritSubagent: SubagentConfig = {
302
370
  agentName: "inherit-agent",
303
371
  description: "Inherits sandbox",
304
- workflow: "workflow",
372
+ workflow: mockWorkflow(),
305
373
  sandbox: "inherit",
306
374
  };
307
375
 
308
- const handler = createSubagentHandler([inheritSubagent]);
376
+ const { handler } = createSubagentHandler([inheritSubagent]);
309
377
 
310
378
  await handler(
311
379
  { subagent: "inherit-agent", description: "test", prompt: "test" },
@@ -317,94 +385,505 @@ describe("createSubagentHandler", () => {
317
385
  }
318
386
  );
319
387
 
320
- const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
321
- if (!lastCall) throw new Error("expected exec call");
388
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
389
+ if (!lastCall) throw new Error("expected startChild call");
322
390
  const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
323
- expect(workflowInput.sandboxId).toBe("parent-sb");
391
+ expect(workflowInput.sandbox).toEqual({
392
+ mode: "inherit",
393
+ sandboxId: "parent-sb",
394
+ });
395
+ });
396
+
397
+ it("throws when sandbox is inherit but parent has no sandbox", async () => {
398
+ const inheritSubagent: SubagentConfig = {
399
+ agentName: "inherit-agent",
400
+ description: "Inherits sandbox",
401
+ workflow: mockWorkflow(),
402
+ sandbox: "inherit",
403
+ };
404
+
405
+ const { handler } = createSubagentHandler([inheritSubagent]);
406
+
407
+ await expect(
408
+ handler(
409
+ { subagent: "inherit-agent", description: "test", prompt: "test" },
410
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
411
+ )
412
+ ).rejects.toThrow(
413
+ 'sandbox: "inherit" but the parent has no sandbox'
414
+ );
415
+ });
416
+
417
+ it("does not pass sandboxId to child when sandbox is own", async () => {
418
+ const { startChild } = await import("@temporalio/workflow");
419
+ const startMock = startChild as ReturnType<typeof vi.fn>;
420
+
421
+ const ownSubagent: SubagentConfig = {
422
+ agentName: "own-agent",
423
+ description: "Own sandbox",
424
+ workflow: mockWorkflow(),
425
+ sandbox: "own",
426
+ };
427
+
428
+ const { handler } = createSubagentHandler([ownSubagent]);
429
+
430
+ await handler(
431
+ { subagent: "own-agent", description: "test", prompt: "test" },
432
+ {
433
+ threadId: "t",
434
+ toolCallId: "tc",
435
+ toolName: "Subagent",
436
+ sandboxId: "parent-sb",
437
+ }
438
+ );
439
+
440
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
441
+ if (!lastCall) throw new Error("expected startChild call");
442
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
443
+ expect(workflowInput.sandbox).toBeUndefined();
324
444
  });
325
445
 
326
446
  it("resolves context function at invocation time", async () => {
327
- const { executeChild } = await import("@temporalio/workflow");
328
- const execMock = executeChild as ReturnType<typeof vi.fn>;
329
- execMock.mockResolvedValueOnce({
330
- toolResponse: "ok",
331
- data: null,
332
- threadId: "child-t",
333
- });
447
+ const { startChild } = await import("@temporalio/workflow");
448
+ const startMock = startChild as ReturnType<typeof vi.fn>;
334
449
 
335
450
  let counter = 0;
336
451
  const dynamicSubagent: SubagentConfig = {
337
452
  agentName: "dynamic-ctx",
338
453
  description: "Dynamic context",
339
- workflow: "workflow",
454
+ workflow: mockWorkflow(),
340
455
  context: () => {
341
456
  counter++;
342
457
  return { invocation: counter };
343
458
  },
344
459
  };
345
460
 
346
- const handler = createSubagentHandler([dynamicSubagent]);
461
+ const { handler } = createSubagentHandler([dynamicSubagent]);
347
462
 
348
463
  await handler(
349
464
  { subagent: "dynamic-ctx", description: "test", prompt: "test" },
350
465
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
351
466
  );
352
467
 
353
- const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
354
- if (!lastCall) throw new Error("expected exec call");
468
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
469
+ if (!lastCall) throw new Error("expected startChild call");
355
470
  const context = lastCall[1].args[2] as Record<string, unknown>;
356
471
  expect(context).toEqual({ invocation: 1 });
357
472
  });
358
473
 
359
474
  it("passes static context unchanged", async () => {
360
- const { executeChild } = await import("@temporalio/workflow");
361
- const execMock = executeChild as ReturnType<typeof vi.fn>;
362
- execMock.mockResolvedValueOnce({
363
- toolResponse: "ok",
364
- data: null,
365
- threadId: "child-t",
366
- });
475
+ const { startChild } = await import("@temporalio/workflow");
476
+ const startMock = startChild as ReturnType<typeof vi.fn>;
367
477
 
368
478
  const staticSubagent: SubagentConfig = {
369
479
  agentName: "static-ctx",
370
480
  description: "Static context",
371
- workflow: "workflow",
481
+ workflow: mockWorkflow(),
372
482
  context: { key: "value" },
373
483
  };
374
484
 
375
- const handler = createSubagentHandler([staticSubagent]);
485
+ const { handler } = createSubagentHandler([staticSubagent]);
376
486
 
377
487
  await handler(
378
488
  { subagent: "static-ctx", description: "test", prompt: "test" },
379
489
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
380
490
  );
381
491
 
382
- const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
383
- if (!lastCall) throw new Error("expected exec call");
492
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
493
+ if (!lastCall) throw new Error("expected startChild call");
384
494
  const context = lastCall[1].args[2] as Record<string, unknown>;
385
495
  expect(context).toEqual({ key: "value" });
386
496
  });
387
497
 
388
498
  it("does not pass sandboxId when sandbox is own", async () => {
389
- const { executeChild } = await import("@temporalio/workflow");
390
- const execMock = executeChild as ReturnType<typeof vi.fn>;
391
- execMock.mockResolvedValueOnce({
392
- toolResponse: "ok",
499
+ const { startChild } = await import("@temporalio/workflow");
500
+ const startMock = startChild as ReturnType<typeof vi.fn>;
501
+
502
+ const ownSubagent: SubagentConfig = {
503
+ agentName: "own-agent",
504
+ description: "Own sandbox",
505
+ workflow: mockWorkflow(),
506
+ sandbox: "own",
507
+ };
508
+
509
+ const { handler } = createSubagentHandler([ownSubagent]);
510
+
511
+ await handler(
512
+ { subagent: "own-agent", description: "test", prompt: "test" },
513
+ {
514
+ threadId: "t",
515
+ toolCallId: "tc",
516
+ toolName: "Subagent",
517
+ sandboxId: "parent-sb",
518
+ }
519
+ );
520
+
521
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
522
+ if (!lastCall) throw new Error("expected startChild call");
523
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
524
+ expect(workflowInput.sandbox).toBeUndefined();
525
+ });
526
+
527
+ // --- Thread mode ---
528
+
529
+ it("passes thread fork when thread is fork and threadId provided", async () => {
530
+ const { startChild } = await import("@temporalio/workflow");
531
+ const startMock = startChild as ReturnType<typeof vi.fn>;
532
+
533
+ const contSubagent: SubagentConfig = {
534
+ agentName: "cont",
535
+ description: "Continues",
536
+ workflow: mockWorkflow(),
537
+ thread: "fork",
538
+ };
539
+
540
+ const { handler } = createSubagentHandler([contSubagent]);
541
+
542
+ await handler(
543
+ {
544
+ subagent: "cont",
545
+ description: "test",
546
+ prompt: "test",
547
+ threadId: "prev-thread-42",
548
+ },
549
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
550
+ );
551
+
552
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
553
+ if (!lastCall) throw new Error("expected startChild call");
554
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
555
+ expect(workflowInput.thread).toEqual({
556
+ mode: "fork",
557
+ threadId: "prev-thread-42",
558
+ });
559
+ });
560
+
561
+ it("passes thread continue when thread is continue", async () => {
562
+ const { startChild } = await import("@temporalio/workflow");
563
+ const startMock = startChild as ReturnType<typeof vi.fn>;
564
+
565
+ const contSubagent: SubagentConfig = {
566
+ agentName: "cont-mode",
567
+ description: "Continue mode",
568
+ workflow: mockWorkflow(),
569
+ thread: "continue",
570
+ };
571
+
572
+ const { handler } = createSubagentHandler([contSubagent]);
573
+
574
+ await handler(
575
+ {
576
+ subagent: "cont-mode",
577
+ description: "test",
578
+ prompt: "test",
579
+ threadId: "prev-thread-99",
580
+ },
581
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
582
+ );
583
+
584
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
585
+ if (!lastCall) throw new Error("expected startChild call");
586
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
587
+ expect(workflowInput.thread).toEqual({
588
+ mode: "continue",
589
+ threadId: "prev-thread-99",
590
+ });
591
+ });
592
+
593
+ it("does not pass thread when thread is new", async () => {
594
+ const { startChild } = await import("@temporalio/workflow");
595
+ const startMock = startChild as ReturnType<typeof vi.fn>;
596
+
597
+ const noContSubagent: SubagentConfig = {
598
+ agentName: "no-cont",
599
+ description: "No continuation",
600
+ workflow: mockWorkflow(),
601
+ };
602
+
603
+ const { handler } = createSubagentHandler([noContSubagent]);
604
+
605
+ await handler(
606
+ {
607
+ subagent: "no-cont",
608
+ description: "test",
609
+ prompt: "test",
610
+ threadId: "prev-thread",
611
+ },
612
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
613
+ );
614
+
615
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
616
+ if (!lastCall) throw new Error("expected startChild call");
617
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
618
+ expect(workflowInput.thread).toBeUndefined();
619
+ });
620
+
621
+ // --- Sandbox continuation ---
622
+
623
+ it("does not pass sandbox when thread is fork (own sandbox)", async () => {
624
+ const { startChild } = await import("@temporalio/workflow");
625
+ const startMock = startChild as ReturnType<typeof vi.fn>;
626
+
627
+ const contSandboxSubagent: SubagentConfig = {
628
+ agentName: "sb-cont",
629
+ description: "Sandbox continuation",
630
+ workflow: mockWorkflow(),
631
+ thread: "fork",
632
+ };
633
+
634
+ const { handler } = createSubagentHandler([contSandboxSubagent]);
635
+
636
+ await handler(
637
+ { subagent: "sb-cont", description: "test", prompt: "first run" },
638
+ {
639
+ threadId: "t",
640
+ toolCallId: "tc",
641
+ toolName: "Subagent",
642
+ sandboxId: "parent-sb",
643
+ }
644
+ );
645
+
646
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
647
+ if (!lastCall) throw new Error("expected startChild call");
648
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
649
+ expect(workflowInput.sandbox).toBeUndefined();
650
+ });
651
+
652
+ it("tracks sandbox ID and passes sandbox fork on continuation", async () => {
653
+ const { startChild } = await import("@temporalio/workflow");
654
+ const startMock = startChild as ReturnType<typeof vi.fn>;
655
+
656
+ nextStartChildResult = () => ({
657
+ toolResponse: "first run done",
393
658
  data: null,
394
- threadId: "child-t",
659
+ threadId: "child-thread-A",
660
+ sandboxId: "child-sb-1",
661
+ });
662
+
663
+ const contSandboxSubagent: SubagentConfig = {
664
+ agentName: "sb-cont",
665
+ description: "Sandbox continuation",
666
+ workflow: mockWorkflow(),
667
+ thread: "fork",
668
+ sandbox: "own",
669
+ };
670
+
671
+ const { handler } = createSubagentHandler([contSandboxSubagent]);
672
+
673
+ await handler(
674
+ { subagent: "sb-cont", description: "test", prompt: "first" },
675
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
676
+ );
677
+
678
+ nextStartChildResult = () => ({
679
+ toolResponse: "second run done",
680
+ data: null,
681
+ threadId: "child-thread-B",
682
+ sandboxId: "child-sb-2",
683
+ });
684
+
685
+ await handler(
686
+ {
687
+ subagent: "sb-cont",
688
+ description: "test",
689
+ prompt: "second",
690
+ threadId: "child-thread-A",
691
+ },
692
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
693
+ );
694
+
695
+ const secondCall = startMock.mock.calls[startMock.mock.calls.length - 1];
696
+ if (!secondCall) throw new Error("expected second startChild call");
697
+ const workflowInput = secondCall[1].args[1] as SubagentWorkflowInput;
698
+ expect(workflowInput.thread).toEqual({
699
+ mode: "fork",
700
+ threadId: "child-thread-A",
701
+ });
702
+ expect(workflowInput.sandbox).toEqual({
703
+ mode: "fork",
704
+ sandboxId: "child-sb-1",
705
+ });
706
+ });
707
+
708
+ it("does not pass sandbox fork without thread continuation", async () => {
709
+ const { startChild } = await import("@temporalio/workflow");
710
+ const startMock = startChild as ReturnType<typeof vi.fn>;
711
+
712
+ nextStartChildResult = () => ({
713
+ toolResponse: "done",
714
+ data: null,
715
+ threadId: "child-thread-A",
716
+ sandboxId: "child-sb-1",
395
717
  });
396
718
 
719
+ const contSandboxSubagent: SubagentConfig = {
720
+ agentName: "sb-cont",
721
+ description: "Sandbox continuation",
722
+ workflow: mockWorkflow(),
723
+ thread: "fork",
724
+ sandbox: "own",
725
+ };
726
+
727
+ const { handler } = createSubagentHandler([contSandboxSubagent]);
728
+
729
+ await handler(
730
+ { subagent: "sb-cont", description: "test", prompt: "first" },
731
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
732
+ );
733
+
734
+ nextStartChildResult = () => ({
735
+ toolResponse: "new run",
736
+ data: null,
737
+ threadId: "child-thread-B",
738
+ sandboxId: "child-sb-2",
739
+ });
740
+
741
+ await handler(
742
+ { subagent: "sb-cont", description: "test", prompt: "no continuation" },
743
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
744
+ );
745
+
746
+ const secondCall = startMock.mock.calls[startMock.mock.calls.length - 1];
747
+ if (!secondCall) throw new Error("expected startChild call");
748
+ const workflowInput = secondCall[1].args[1] as SubagentWorkflowInput;
749
+ expect(workflowInput.sandbox).toBeUndefined();
750
+ expect(workflowInput.thread).toBeUndefined();
751
+ });
752
+
753
+ it("adds fork-mode subagent to pendingDestroys", async () => {
754
+ const { startChild } = await import("@temporalio/workflow");
755
+ const startMock = startChild as ReturnType<typeof vi.fn>;
756
+
757
+ const contSandboxSubagent: SubagentConfig = {
758
+ agentName: "sb-cont",
759
+ description: "Sandbox continuation",
760
+ workflow: mockWorkflow(),
761
+ thread: "fork",
762
+ };
763
+
764
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
765
+ contSandboxSubagent,
766
+ ]);
767
+
768
+ await handler(
769
+ { subagent: "sb-cont", description: "test", prompt: "run" },
770
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
771
+ );
772
+
773
+ await destroySubagentSandboxes();
774
+
775
+ const lastResult = startMock.mock.results.at(-1);
776
+ if (!lastResult) throw new Error("expected startChild call");
777
+ const childHandle = await lastResult.value;
778
+ expect(childHandle.signal).toHaveBeenCalled();
779
+ });
780
+
781
+ it("signals destroy and awaits result for sandbox=own subagents at cleanup", async () => {
782
+ const { startChild } = await import("@temporalio/workflow");
783
+ const startMock = startChild as ReturnType<typeof vi.fn>;
784
+
397
785
  const ownSubagent: SubagentConfig = {
398
786
  agentName: "own-agent",
399
787
  description: "Own sandbox",
400
- workflow: "workflow",
788
+ workflow: mockWorkflow(),
401
789
  sandbox: "own",
402
790
  };
403
791
 
404
- const handler = createSubagentHandler([ownSubagent]);
792
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
793
+ ownSubagent,
794
+ ]);
405
795
 
406
796
  await handler(
407
- { subagent: "own-agent", description: "test", prompt: "test" },
797
+ { subagent: "own-agent", description: "test", prompt: "run" },
798
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
799
+ );
800
+
801
+ await destroySubagentSandboxes();
802
+
803
+ const lastResult = startMock.mock.results.at(-1);
804
+ if (!lastResult) throw new Error("expected startChild call");
805
+ const childHandle = await lastResult.value;
806
+ expect(childHandle.signal).toHaveBeenCalled();
807
+ });
808
+
809
+ it("does not signal destroy for inherit subagents", async () => {
810
+ const { startChild } = await import("@temporalio/workflow");
811
+ const startMock = startChild as ReturnType<typeof vi.fn>;
812
+
813
+ const inheritSubagent: SubagentConfig = {
814
+ agentName: "inherit-agent",
815
+ description: "Inherits sandbox",
816
+ workflow: mockWorkflow(),
817
+ sandbox: "inherit",
818
+ };
819
+
820
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
821
+ inheritSubagent,
822
+ ]);
823
+
824
+ await handler(
825
+ { subagent: "inherit-agent", description: "test", prompt: "run" },
826
+ {
827
+ threadId: "t",
828
+ toolCallId: "tc",
829
+ toolName: "Subagent",
830
+ sandboxId: "parent-sb",
831
+ }
832
+ );
833
+
834
+ const lastResult = startMock.mock.results.at(-1);
835
+ if (!lastResult) throw new Error("expected startChild call");
836
+ const childHandle = await lastResult.value;
837
+ childHandle.signal.mockClear();
838
+
839
+ await destroySubagentSandboxes();
840
+
841
+ expect(childHandle.signal).not.toHaveBeenCalled();
842
+ });
843
+
844
+ it("does not pass sandboxId when sandbox is none (default)", async () => {
845
+ const { startChild } = await import("@temporalio/workflow");
846
+ const startMock = startChild as ReturnType<typeof vi.fn>;
847
+
848
+ const noneSubagent: SubagentConfig = {
849
+ agentName: "none-agent",
850
+ description: "No sandbox",
851
+ workflow: mockWorkflow(),
852
+ };
853
+
854
+ const { handler } = createSubagentHandler([noneSubagent]);
855
+
856
+ await handler(
857
+ { subagent: "none-agent", description: "test", prompt: "test" },
858
+ {
859
+ threadId: "t",
860
+ toolCallId: "tc",
861
+ toolName: "Subagent",
862
+ sandboxId: "parent-sb",
863
+ }
864
+ );
865
+
866
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
867
+ if (!lastCall) throw new Error("expected startChild call");
868
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
869
+ expect(workflowInput.sandbox).toBeUndefined();
870
+ });
871
+
872
+ it("does not pass sandboxId when sandbox is explicitly none", async () => {
873
+ const { startChild } = await import("@temporalio/workflow");
874
+ const startMock = startChild as ReturnType<typeof vi.fn>;
875
+
876
+ const noneSubagent: SubagentConfig = {
877
+ agentName: "none-agent",
878
+ description: "No sandbox",
879
+ workflow: mockWorkflow(),
880
+ sandbox: "none",
881
+ };
882
+
883
+ const { handler } = createSubagentHandler([noneSubagent]);
884
+
885
+ await handler(
886
+ { subagent: "none-agent", description: "test", prompt: "test" },
408
887
  {
409
888
  threadId: "t",
410
889
  toolCallId: "tc",
@@ -413,10 +892,163 @@ describe("createSubagentHandler", () => {
413
892
  }
414
893
  );
415
894
 
416
- const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
417
- if (!lastCall) throw new Error("expected exec call");
895
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
896
+ if (!lastCall) throw new Error("expected startChild call");
418
897
  const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
419
- expect(workflowInput.sandboxId).toBeUndefined();
898
+ expect(workflowInput.sandbox).toBeUndefined();
899
+ });
900
+
901
+ it("does not signal destroy for none subagents", async () => {
902
+ const { startChild } = await import("@temporalio/workflow");
903
+ const startMock = startChild as ReturnType<typeof vi.fn>;
904
+
905
+ const noneSubagent: SubagentConfig = {
906
+ agentName: "none-agent",
907
+ description: "No sandbox",
908
+ workflow: mockWorkflow(),
909
+ sandbox: "none",
910
+ };
911
+
912
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
913
+ noneSubagent,
914
+ ]);
915
+
916
+ await handler(
917
+ { subagent: "none-agent", description: "test", prompt: "run" },
918
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
919
+ );
920
+
921
+ const lastResult = startMock.mock.results.at(-1);
922
+ if (!lastResult) throw new Error("expected startChild call");
923
+ const childHandle = await lastResult.value;
924
+ childHandle.signal.mockClear();
925
+
926
+ await destroySubagentSandboxes();
927
+
928
+ expect(childHandle.signal).not.toHaveBeenCalled();
929
+ });
930
+
931
+ it("returns sandboxId in response when child creates a sandbox", async () => {
932
+ nextStartChildResult = () => ({
933
+ toolResponse: "done",
934
+ data: null,
935
+ threadId: "child-t",
936
+ sandboxId: "child-sb-42",
937
+ });
938
+
939
+ const ownSubagent: SubagentConfig = {
940
+ agentName: "own-agent",
941
+ description: "Own sandbox",
942
+ workflow: mockWorkflow(),
943
+ sandbox: "own",
944
+ };
945
+
946
+ const { handler } = createSubagentHandler([ownSubagent]);
947
+
948
+ const result = await handler(
949
+ { subagent: "own-agent", description: "test", prompt: "test" },
950
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
951
+ );
952
+
953
+ expect(result.sandboxId).toBe("child-sb-42");
954
+ });
955
+
956
+ it("does not include sandboxId in response when child has none", async () => {
957
+ nextStartChildResult = () => ({
958
+ toolResponse: "done",
959
+ data: null,
960
+ threadId: "child-t",
961
+ });
962
+
963
+ const { handler } = createSubagentHandler([basicSubagent]);
964
+
965
+ const result = await handler(
966
+ { subagent: "researcher", description: "test", prompt: "test" },
967
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
968
+ );
969
+
970
+ expect(result.sandboxId).toBeUndefined();
971
+ });
972
+
973
+ it("passes metadata through on success", async () => {
974
+ nextStartChildResult = () => ({
975
+ toolResponse: "result",
976
+ data: { result: "ok" },
977
+ threadId: "child-t",
978
+ metadata: { jobId: "j-123", env: "staging" },
979
+ });
980
+
981
+ const { handler } = createSubagentHandler([basicSubagent]);
982
+
983
+ const result = await handler(
984
+ { subagent: "researcher", description: "test", prompt: "test" },
985
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
986
+ );
987
+
988
+ expect(result.metadata).toEqual({ jobId: "j-123", env: "staging" });
989
+ });
990
+
991
+ it("passes metadata through when toolResponse is null", async () => {
992
+ nextStartChildResult = () => ({
993
+ toolResponse: null,
994
+ data: null,
995
+ threadId: "child-t",
996
+ metadata: { state: "pending" },
997
+ });
998
+
999
+ const { handler } = createSubagentHandler([basicSubagent]);
1000
+
1001
+ const result = await handler(
1002
+ { subagent: "researcher", description: "test", prompt: "test" },
1003
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1004
+ );
1005
+
1006
+ expect(result.toolResponse).toContain("no response");
1007
+ expect(result.metadata).toEqual({ state: "pending" });
1008
+ });
1009
+
1010
+ it("passes metadata through when validation fails", async () => {
1011
+ nextStartChildResult = () => ({
1012
+ toolResponse: "result",
1013
+ data: { wrong: "shape" },
1014
+ threadId: "child-t",
1015
+ metadata: { deployId: "d-456" },
1016
+ });
1017
+
1018
+ const validatedSubagent: SubagentConfig = {
1019
+ agentName: "validated",
1020
+ description: "Has validation",
1021
+ workflow: mockWorkflow(),
1022
+ resultSchema: z.object({ expected: z.string() }),
1023
+ };
1024
+
1025
+ const { handler } = createSubagentHandler([validatedSubagent]);
1026
+
1027
+ const result = await handler(
1028
+ { subagent: "validated", description: "test", prompt: "test" },
1029
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1030
+ );
1031
+
1032
+ expect(result.toolResponse).toContain("invalid data");
1033
+ expect(result.data).toBeNull();
1034
+ expect(result.metadata).toEqual({ deployId: "d-456" });
1035
+ });
1036
+
1037
+ it("omits metadata when child does not return it", async () => {
1038
+ nextStartChildResult = () => ({
1039
+ toolResponse: "result",
1040
+ data: null,
1041
+ threadId: "child-t",
1042
+ });
1043
+
1044
+ const { handler } = createSubagentHandler([basicSubagent]);
1045
+
1046
+ const result = await handler(
1047
+ { subagent: "researcher", description: "test", prompt: "test" },
1048
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1049
+ );
1050
+
1051
+ expect(result.metadata).toBeUndefined();
420
1052
  });
421
1053
  });
422
1054
 
@@ -434,15 +1066,14 @@ describe("buildSubagentRegistration", () => {
434
1066
  {
435
1067
  agentName: "agent",
436
1068
  description: "An agent",
437
- workflow: "workflow",
1069
+ workflow: mockWorkflow(),
438
1070
  },
439
1071
  ]);
440
1072
 
441
1073
  expect(reg).not.toBeNull();
442
- expect(reg).toBeDefined();
443
1074
  if (reg) {
444
- expect(reg.name).toBe(SUBAGENT_TOOL_NAME);
445
- expect(typeof reg.handler).toBe("function");
1075
+ expect(reg.registration.name).toBe(SUBAGENT_TOOL_NAME);
1076
+ expect(typeof reg.registration.handler).toBe("function");
446
1077
  }
447
1078
  });
448
1079
 
@@ -452,17 +1083,17 @@ describe("buildSubagentRegistration", () => {
452
1083
  {
453
1084
  agentName: "toggle",
454
1085
  description: "Toggleable",
455
- workflow: "workflow",
1086
+ workflow: mockWorkflow(),
456
1087
  enabled: () => flag,
457
1088
  },
458
1089
  ]);
459
1090
 
460
1091
  expect(reg).toBeDefined();
461
1092
  if (!reg) return;
462
- expect((reg.enabled as () => boolean)()).toBe(true);
1093
+ expect((reg.registration.enabled as () => boolean)()).toBe(true);
463
1094
 
464
1095
  flag = false;
465
- expect((reg.enabled as () => boolean)()).toBe(false);
1096
+ expect((reg.registration.enabled as () => boolean)()).toBe(false);
466
1097
  });
467
1098
 
468
1099
  it("disabled when all subagents are disabled", () => {
@@ -470,14 +1101,14 @@ describe("buildSubagentRegistration", () => {
470
1101
  {
471
1102
  agentName: "off",
472
1103
  description: "Disabled",
473
- workflow: "workflow",
1104
+ workflow: mockWorkflow(),
474
1105
  enabled: false,
475
1106
  },
476
1107
  ]);
477
1108
 
478
1109
  expect(reg).toBeDefined();
479
1110
  if (reg) {
480
- expect((reg.enabled as () => boolean)()).toBe(false);
1111
+ expect((reg.registration.enabled as () => boolean)()).toBe(false);
481
1112
  }
482
1113
  });
483
1114
 
@@ -488,7 +1119,7 @@ describe("buildSubagentRegistration", () => {
488
1119
  {
489
1120
  agentName: "hooked",
490
1121
  description: "Has hooks",
491
- workflow: "workflow",
1122
+ workflow: mockWorkflow(),
492
1123
  hooks: {
493
1124
  onPreExecution: hookSpy,
494
1125
  },
@@ -497,9 +1128,9 @@ describe("buildSubagentRegistration", () => {
497
1128
 
498
1129
  expect(reg).toBeDefined();
499
1130
  if (reg) {
500
- expect(reg.hooks).toBeDefined();
501
- if (reg.hooks) {
502
- expect(reg.hooks.onPreToolUse).toBeDefined();
1131
+ expect(reg.registration.hooks).toBeDefined();
1132
+ if (reg.registration.hooks) {
1133
+ expect(reg.registration.hooks.onPreToolUse).toBeDefined();
503
1134
  }
504
1135
  }
505
1136
  });
@@ -509,13 +1140,13 @@ describe("buildSubagentRegistration", () => {
509
1140
  {
510
1141
  agentName: "plain",
511
1142
  description: "No hooks",
512
- workflow: "workflow",
1143
+ workflow: mockWorkflow(),
513
1144
  },
514
1145
  ]);
515
1146
 
516
1147
  expect(reg).toBeDefined();
517
1148
  if (reg) {
518
- expect(reg.hooks).toBeUndefined();
1149
+ expect(reg.registration.hooks).toBeUndefined();
519
1150
  }
520
1151
  });
521
1152
 
@@ -525,20 +1156,20 @@ describe("buildSubagentRegistration", () => {
525
1156
  {
526
1157
  agentName: "a",
527
1158
  description: "Agent A",
528
- workflow: "workflow",
1159
+ workflow: mockWorkflow(),
529
1160
  enabled: true,
530
1161
  },
531
1162
  {
532
1163
  agentName: "b",
533
1164
  description: "Agent B",
534
- workflow: "workflow",
1165
+ workflow: mockWorkflow(),
535
1166
  enabled: () => bEnabled,
536
1167
  },
537
1168
  ]);
538
1169
 
539
1170
  expect(reg).toBeDefined();
540
1171
  if (reg) {
541
- const desc = reg.description as () => string;
1172
+ const desc = reg.registration.description as () => string;
542
1173
  expect(desc()).toContain("Agent A");
543
1174
  expect(desc()).toContain("Agent B");
544
1175
 
@@ -582,10 +1213,24 @@ describe("defineSubagent", () => {
582
1213
  const reg = buildSubagentRegistration([config]);
583
1214
  expect(reg).toBeDefined();
584
1215
  if (!reg) return;
585
- expect((reg.enabled as () => boolean)()).toBe(true);
1216
+ expect((reg.registration.enabled as () => boolean)()).toBe(true);
586
1217
 
587
1218
  flag = false;
588
- expect((reg.enabled as () => boolean)()).toBe(false);
1219
+ expect((reg.registration.enabled as () => boolean)()).toBe(false);
1220
+ });
1221
+
1222
+ it("passes sandbox none through to config", () => {
1223
+ const config = defineSubagent(makeDef("no-sb"), {
1224
+ sandbox: "none",
1225
+ });
1226
+
1227
+ expect(config.sandbox).toBe("none");
1228
+ });
1229
+
1230
+ it("defaults sandbox to undefined (none behavior)", () => {
1231
+ const config = defineSubagent(makeDef("default-sb"));
1232
+
1233
+ expect(config.sandbox).toBeUndefined();
589
1234
  });
590
1235
  });
591
1236
 
@@ -594,30 +1239,45 @@ describe("defineSubagent", () => {
594
1239
  // ---------------------------------------------------------------------------
595
1240
 
596
1241
  describe("defineSubagentWorkflow", () => {
597
- it("maps previousThreadId to threadId + continueThread", async () => {
598
- let capturedPrompt: string | undefined;
1242
+ it("maps thread fork into sessionInput", async () => {
599
1243
  let capturedSession: SubagentSessionInput | undefined;
600
1244
 
601
1245
  const workflow = defineSubagentWorkflow(
602
1246
  { name: "test", description: "test agent" },
603
- async (prompt, sessionInput) => {
604
- capturedPrompt = prompt;
1247
+ async (_prompt, sessionInput) => {
605
1248
  capturedSession = sessionInput;
606
1249
  return { toolResponse: "ok", data: null, threadId: "t" };
607
1250
  }
608
1251
  );
609
1252
 
610
- await workflow("go", { previousThreadId: "prev-42" });
1253
+ await workflow("go", { thread: { mode: "fork", threadId: "prev-42" } });
1254
+
1255
+ expect(capturedSession).toEqual({
1256
+ agentName: "test",
1257
+ sandboxShutdown: "destroy",
1258
+ thread: { mode: "fork", threadId: "prev-42" },
1259
+ });
1260
+ });
1261
+
1262
+ it("maps sandbox inherit", async () => {
1263
+ let capturedSession: SubagentSessionInput | undefined;
1264
+ const workflow = defineSubagentWorkflow(
1265
+ { name: "test", description: "test agent" },
1266
+ async (_prompt, sessionInput) => {
1267
+ capturedSession = sessionInput;
1268
+ return { toolResponse: "ok", data: null, threadId: "t" };
1269
+ }
1270
+ );
611
1271
 
612
- expect(capturedPrompt).toBe("go");
1272
+ await workflow("go", { sandbox: { mode: "inherit", sandboxId: "sb-123" } });
613
1273
  expect(capturedSession).toEqual({
614
1274
  agentName: "test",
615
- threadId: "prev-42",
616
- continueThread: true,
1275
+ sandboxShutdown: "destroy",
1276
+ sandbox: { mode: "inherit", sandboxId: "sb-123" },
617
1277
  });
618
1278
  });
619
1279
 
620
- it("maps sandboxId", async () => {
1280
+ it("maps sandbox fork", async () => {
621
1281
  let capturedSession: SubagentSessionInput | undefined;
622
1282
  const workflow = defineSubagentWorkflow(
623
1283
  { name: "test", description: "test agent" },
@@ -627,8 +1287,34 @@ describe("defineSubagentWorkflow", () => {
627
1287
  }
628
1288
  );
629
1289
 
630
- await workflow("go", { sandboxId: "sb-123" });
631
- expect(capturedSession).toEqual({ agentName: "test", sandboxId: "sb-123" });
1290
+ await workflow("go", { sandbox: { mode: "fork", sandboxId: "prev-sb-1" } });
1291
+ expect(capturedSession).toEqual({
1292
+ agentName: "test",
1293
+ sandboxShutdown: "destroy",
1294
+ sandbox: { mode: "fork", sandboxId: "prev-sb-1" },
1295
+ });
1296
+ });
1297
+
1298
+ it("maps thread fork and sandbox fork together", async () => {
1299
+ let capturedSession: SubagentSessionInput | undefined;
1300
+ const workflow = defineSubagentWorkflow(
1301
+ { name: "test", description: "test agent" },
1302
+ async (_prompt, sessionInput) => {
1303
+ capturedSession = sessionInput;
1304
+ return { toolResponse: "ok", data: null, threadId: "t" };
1305
+ }
1306
+ );
1307
+
1308
+ await workflow("go", {
1309
+ thread: { mode: "fork", threadId: "prev-t" },
1310
+ sandbox: { mode: "fork", sandboxId: "prev-sb" },
1311
+ });
1312
+ expect(capturedSession).toEqual({
1313
+ agentName: "test",
1314
+ sandboxShutdown: "destroy",
1315
+ thread: { mode: "fork", threadId: "prev-t" },
1316
+ sandbox: { mode: "fork", sandboxId: "prev-sb" },
1317
+ });
632
1318
  });
633
1319
 
634
1320
  it("passes context as optional third argument", async () => {
@@ -682,4 +1368,21 @@ describe("defineSubagentWorkflow", () => {
682
1368
  expect(workflow.description).toBe("Researches topics");
683
1369
  expect(workflow.resultSchema).toBe(schema);
684
1370
  });
1371
+
1372
+ it("passes empty workflowInput fields as empty sessionInput", async () => {
1373
+ let capturedSession: SubagentSessionInput | undefined;
1374
+ const workflow = defineSubagentWorkflow(
1375
+ { name: "test", description: "test agent" },
1376
+ async (_prompt, sessionInput) => {
1377
+ capturedSession = sessionInput;
1378
+ return { toolResponse: "ok", data: null, threadId: "t" };
1379
+ }
1380
+ );
1381
+
1382
+ await workflow("go", {});
1383
+ expect(capturedSession).toEqual({
1384
+ agentName: "test",
1385
+ sandboxShutdown: "destroy",
1386
+ });
1387
+ });
685
1388
  });