zeitlich 0.2.21 → 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 (129) hide show
  1. package/README.md +303 -105
  2. package/dist/adapters/sandbox/daytona/index.cjs +7 -1
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  5. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  6. package/dist/adapters/sandbox/daytona/index.js +7 -1
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/daytona/workflow.cjs +33 -0
  9. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -0
  10. package/dist/adapters/sandbox/daytona/workflow.d.cts +27 -0
  11. package/dist/adapters/sandbox/daytona/workflow.d.ts +27 -0
  12. package/dist/adapters/sandbox/daytona/workflow.js +31 -0
  13. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -0
  14. package/dist/adapters/sandbox/inmemory/index.cjs +18 -1
  15. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -2
  17. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -2
  18. package/dist/adapters/sandbox/inmemory/index.js +18 -1
  19. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  20. package/dist/adapters/sandbox/inmemory/workflow.cjs +33 -0
  21. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -0
  22. package/dist/adapters/sandbox/inmemory/workflow.d.cts +25 -0
  23. package/dist/adapters/sandbox/inmemory/workflow.d.ts +25 -0
  24. package/dist/adapters/sandbox/inmemory/workflow.js +31 -0
  25. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -0
  26. package/dist/adapters/sandbox/virtual/index.cjs +36 -9
  27. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/virtual/index.d.cts +8 -5
  29. package/dist/adapters/sandbox/virtual/index.d.ts +8 -5
  30. package/dist/adapters/sandbox/virtual/index.js +36 -9
  31. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  32. package/dist/adapters/sandbox/virtual/workflow.cjs +33 -0
  33. package/dist/adapters/sandbox/virtual/workflow.cjs.map +1 -0
  34. package/dist/adapters/sandbox/virtual/workflow.d.cts +27 -0
  35. package/dist/adapters/sandbox/virtual/workflow.d.ts +27 -0
  36. package/dist/adapters/sandbox/virtual/workflow.js +31 -0
  37. package/dist/adapters/sandbox/virtual/workflow.js.map +1 -0
  38. package/dist/adapters/thread/google-genai/index.cjs +9 -1
  39. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  40. package/dist/adapters/thread/google-genai/index.d.cts +31 -19
  41. package/dist/adapters/thread/google-genai/index.d.ts +31 -19
  42. package/dist/adapters/thread/google-genai/index.js +9 -1
  43. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  44. package/dist/adapters/thread/google-genai/workflow.cjs +33 -0
  45. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -0
  46. package/dist/adapters/thread/google-genai/workflow.d.cts +32 -0
  47. package/dist/adapters/thread/google-genai/workflow.d.ts +32 -0
  48. package/dist/adapters/thread/google-genai/workflow.js +31 -0
  49. package/dist/adapters/thread/google-genai/workflow.js.map +1 -0
  50. package/dist/adapters/thread/langchain/index.cjs +9 -1
  51. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  52. package/dist/adapters/thread/langchain/index.d.cts +27 -16
  53. package/dist/adapters/thread/langchain/index.d.ts +27 -16
  54. package/dist/adapters/thread/langchain/index.js +9 -1
  55. package/dist/adapters/thread/langchain/index.js.map +1 -1
  56. package/dist/adapters/thread/langchain/workflow.cjs +33 -0
  57. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -0
  58. package/dist/adapters/thread/langchain/workflow.d.cts +32 -0
  59. package/dist/adapters/thread/langchain/workflow.d.ts +32 -0
  60. package/dist/adapters/thread/langchain/workflow.js +31 -0
  61. package/dist/adapters/thread/langchain/workflow.js.map +1 -0
  62. package/dist/index.cjs +282 -90
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +38 -16
  65. package/dist/index.d.ts +38 -16
  66. package/dist/index.js +281 -87
  67. package/dist/index.js.map +1 -1
  68. package/dist/queries-DModcWRy.d.cts +44 -0
  69. package/dist/queries-byD0jr1Y.d.ts +44 -0
  70. package/dist/{types-BkAYmc96.d.ts → types-B50pBPEV.d.ts} +190 -38
  71. package/dist/{types-YbL7JpEA.d.cts → types-Bll19FZJ.d.cts} +7 -0
  72. package/dist/{types-YbL7JpEA.d.ts → types-Bll19FZJ.d.ts} +7 -0
  73. package/dist/{queries-6Avfh74U.d.ts → types-BuXdFhaZ.d.cts} +7 -48
  74. package/dist/{types-BMRzfELQ.d.cts → types-ChAMwU3q.d.cts} +17 -1
  75. package/dist/{types-BMRzfELQ.d.ts → types-ChAMwU3q.d.ts} +17 -1
  76. package/dist/{types-CES_30qx.d.cts → types-DQW8l7pY.d.cts} +190 -38
  77. package/dist/{queries-CHa2iv_I.d.cts → types-GZ76HZSj.d.ts} +7 -48
  78. package/dist/workflow.cjs +244 -86
  79. package/dist/workflow.cjs.map +1 -1
  80. package/dist/workflow.d.cts +54 -65
  81. package/dist/workflow.d.ts +54 -65
  82. package/dist/workflow.js +243 -83
  83. package/dist/workflow.js.map +1 -1
  84. package/package.json +54 -2
  85. package/src/adapters/sandbox/daytona/filesystem.ts +1 -1
  86. package/src/adapters/sandbox/daytona/index.ts +8 -0
  87. package/src/adapters/sandbox/daytona/proxy.ts +56 -0
  88. package/src/adapters/sandbox/e2b/filesystem.ts +147 -0
  89. package/src/adapters/sandbox/e2b/index.ts +164 -0
  90. package/src/adapters/sandbox/e2b/types.ts +23 -0
  91. package/src/adapters/sandbox/inmemory/index.ts +27 -3
  92. package/src/adapters/sandbox/inmemory/proxy.ts +53 -0
  93. package/src/adapters/sandbox/virtual/filesystem.ts +41 -17
  94. package/src/adapters/sandbox/virtual/provider.ts +9 -1
  95. package/src/adapters/sandbox/virtual/proxy.ts +53 -0
  96. package/src/adapters/sandbox/virtual/types.ts +9 -4
  97. package/src/adapters/thread/google-genai/activities.ts +51 -17
  98. package/src/adapters/thread/google-genai/index.ts +1 -0
  99. package/src/adapters/thread/google-genai/proxy.ts +61 -0
  100. package/src/adapters/thread/langchain/activities.ts +47 -14
  101. package/src/adapters/thread/langchain/index.ts +1 -0
  102. package/src/adapters/thread/langchain/proxy.ts +61 -0
  103. package/src/lib/lifecycle.ts +57 -0
  104. package/src/lib/sandbox/manager.ts +52 -6
  105. package/src/lib/sandbox/sandbox.test.ts +12 -11
  106. package/src/lib/sandbox/types.ts +31 -4
  107. package/src/lib/session/index.ts +4 -5
  108. package/src/lib/session/session-edge-cases.integration.test.ts +491 -66
  109. package/src/lib/session/session.integration.test.ts +92 -80
  110. package/src/lib/session/session.ts +108 -96
  111. package/src/lib/session/types.ts +87 -17
  112. package/src/lib/subagent/define.ts +6 -5
  113. package/src/lib/subagent/handler.ts +148 -16
  114. package/src/lib/subagent/index.ts +4 -0
  115. package/src/lib/subagent/register.ts +10 -3
  116. package/src/lib/subagent/signals.ts +8 -0
  117. package/src/lib/subagent/subagent.integration.test.ts +893 -128
  118. package/src/lib/subagent/tool.ts +2 -2
  119. package/src/lib/subagent/types.ts +84 -21
  120. package/src/lib/subagent/workflow.ts +83 -12
  121. package/src/lib/tool-router/router-edge-cases.integration.test.ts +4 -1
  122. package/src/lib/tool-router/router.integration.test.ts +141 -5
  123. package/src/lib/tool-router/router.ts +13 -3
  124. package/src/lib/tool-router/types.ts +7 -0
  125. package/src/lib/workflow.test.ts +104 -27
  126. package/src/lib/workflow.ts +37 -19
  127. package/src/tools/bash/bash.test.ts +16 -7
  128. package/src/workflow.ts +11 -14
  129. package/tsup.config.ts +6 -0
@@ -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,32 +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");
390
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
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");
322
442
  const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
323
- expect(workflowInput.sandboxId).toBe("parent-sb");
443
+ expect(workflowInput.sandbox).toBeUndefined();
444
+ });
445
+
446
+ it("resolves context function at invocation time", async () => {
447
+ const { startChild } = await import("@temporalio/workflow");
448
+ const startMock = startChild as ReturnType<typeof vi.fn>;
449
+
450
+ let counter = 0;
451
+ const dynamicSubagent: SubagentConfig = {
452
+ agentName: "dynamic-ctx",
453
+ description: "Dynamic context",
454
+ workflow: mockWorkflow(),
455
+ context: () => {
456
+ counter++;
457
+ return { invocation: counter };
458
+ },
459
+ };
460
+
461
+ const { handler } = createSubagentHandler([dynamicSubagent]);
462
+
463
+ await handler(
464
+ { subagent: "dynamic-ctx", description: "test", prompt: "test" },
465
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
466
+ );
467
+
468
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
469
+ if (!lastCall) throw new Error("expected startChild call");
470
+ const context = lastCall[1].args[2] as Record<string, unknown>;
471
+ expect(context).toEqual({ invocation: 1 });
472
+ });
473
+
474
+ it("passes static context unchanged", async () => {
475
+ const { startChild } = await import("@temporalio/workflow");
476
+ const startMock = startChild as ReturnType<typeof vi.fn>;
477
+
478
+ const staticSubagent: SubagentConfig = {
479
+ agentName: "static-ctx",
480
+ description: "Static context",
481
+ workflow: mockWorkflow(),
482
+ context: { key: "value" },
483
+ };
484
+
485
+ const { handler } = createSubagentHandler([staticSubagent]);
486
+
487
+ await handler(
488
+ { subagent: "static-ctx", description: "test", prompt: "test" },
489
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
490
+ );
491
+
492
+ const lastCall = startMock.mock.calls[startMock.mock.calls.length - 1];
493
+ if (!lastCall) throw new Error("expected startChild call");
494
+ const context = lastCall[1].args[2] as Record<string, unknown>;
495
+ expect(context).toEqual({ key: "value" });
324
496
  });
325
497
 
326
498
  it("does not pass sandboxId when sandbox is own", async () => {
327
- const { executeChild } = await import("@temporalio/workflow");
328
- const execMock = executeChild as ReturnType<typeof vi.fn>;
329
- execMock.mockResolvedValueOnce({
330
- 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",
331
658
  data: null,
332
- threadId: "child-t",
659
+ threadId: "child-thread-A",
660
+ sandboxId: "child-sb-1",
333
661
  });
334
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",
717
+ });
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
+
335
785
  const ownSubagent: SubagentConfig = {
336
786
  agentName: "own-agent",
337
787
  description: "Own sandbox",
338
- workflow: "workflow",
788
+ workflow: mockWorkflow(),
339
789
  sandbox: "own",
340
790
  };
341
791
 
342
- const handler = createSubagentHandler([ownSubagent]);
792
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
793
+ ownSubagent,
794
+ ]);
343
795
 
344
796
  await handler(
345
- { 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" },
346
887
  {
347
888
  threadId: "t",
348
889
  toolCallId: "tc",
@@ -351,10 +892,163 @@ describe("createSubagentHandler", () => {
351
892
  }
352
893
  );
353
894
 
354
- const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
355
- 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");
356
897
  const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
357
- 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();
358
1052
  });
359
1053
  });
360
1054
 
@@ -372,15 +1066,14 @@ describe("buildSubagentRegistration", () => {
372
1066
  {
373
1067
  agentName: "agent",
374
1068
  description: "An agent",
375
- workflow: "workflow",
1069
+ workflow: mockWorkflow(),
376
1070
  },
377
1071
  ]);
378
1072
 
379
1073
  expect(reg).not.toBeNull();
380
- expect(reg).toBeDefined();
381
1074
  if (reg) {
382
- expect(reg.name).toBe(SUBAGENT_TOOL_NAME);
383
- expect(typeof reg.handler).toBe("function");
1075
+ expect(reg.registration.name).toBe(SUBAGENT_TOOL_NAME);
1076
+ expect(typeof reg.registration.handler).toBe("function");
384
1077
  }
385
1078
  });
386
1079
 
@@ -390,17 +1083,17 @@ describe("buildSubagentRegistration", () => {
390
1083
  {
391
1084
  agentName: "toggle",
392
1085
  description: "Toggleable",
393
- workflow: "workflow",
1086
+ workflow: mockWorkflow(),
394
1087
  enabled: () => flag,
395
1088
  },
396
1089
  ]);
397
1090
 
398
1091
  expect(reg).toBeDefined();
399
1092
  if (!reg) return;
400
- expect((reg.enabled as () => boolean)()).toBe(true);
1093
+ expect((reg.registration.enabled as () => boolean)()).toBe(true);
401
1094
 
402
1095
  flag = false;
403
- expect((reg.enabled as () => boolean)()).toBe(false);
1096
+ expect((reg.registration.enabled as () => boolean)()).toBe(false);
404
1097
  });
405
1098
 
406
1099
  it("disabled when all subagents are disabled", () => {
@@ -408,14 +1101,14 @@ describe("buildSubagentRegistration", () => {
408
1101
  {
409
1102
  agentName: "off",
410
1103
  description: "Disabled",
411
- workflow: "workflow",
1104
+ workflow: mockWorkflow(),
412
1105
  enabled: false,
413
1106
  },
414
1107
  ]);
415
1108
 
416
1109
  expect(reg).toBeDefined();
417
1110
  if (reg) {
418
- expect((reg.enabled as () => boolean)()).toBe(false);
1111
+ expect((reg.registration.enabled as () => boolean)()).toBe(false);
419
1112
  }
420
1113
  });
421
1114
 
@@ -426,7 +1119,7 @@ describe("buildSubagentRegistration", () => {
426
1119
  {
427
1120
  agentName: "hooked",
428
1121
  description: "Has hooks",
429
- workflow: "workflow",
1122
+ workflow: mockWorkflow(),
430
1123
  hooks: {
431
1124
  onPreExecution: hookSpy,
432
1125
  },
@@ -435,9 +1128,9 @@ describe("buildSubagentRegistration", () => {
435
1128
 
436
1129
  expect(reg).toBeDefined();
437
1130
  if (reg) {
438
- expect(reg.hooks).toBeDefined();
439
- if (reg.hooks) {
440
- expect(reg.hooks.onPreToolUse).toBeDefined();
1131
+ expect(reg.registration.hooks).toBeDefined();
1132
+ if (reg.registration.hooks) {
1133
+ expect(reg.registration.hooks.onPreToolUse).toBeDefined();
441
1134
  }
442
1135
  }
443
1136
  });
@@ -447,13 +1140,13 @@ describe("buildSubagentRegistration", () => {
447
1140
  {
448
1141
  agentName: "plain",
449
1142
  description: "No hooks",
450
- workflow: "workflow",
1143
+ workflow: mockWorkflow(),
451
1144
  },
452
1145
  ]);
453
1146
 
454
1147
  expect(reg).toBeDefined();
455
1148
  if (reg) {
456
- expect(reg.hooks).toBeUndefined();
1149
+ expect(reg.registration.hooks).toBeUndefined();
457
1150
  }
458
1151
  });
459
1152
 
@@ -463,20 +1156,20 @@ describe("buildSubagentRegistration", () => {
463
1156
  {
464
1157
  agentName: "a",
465
1158
  description: "Agent A",
466
- workflow: "workflow",
1159
+ workflow: mockWorkflow(),
467
1160
  enabled: true,
468
1161
  },
469
1162
  {
470
1163
  agentName: "b",
471
1164
  description: "Agent B",
472
- workflow: "workflow",
1165
+ workflow: mockWorkflow(),
473
1166
  enabled: () => bEnabled,
474
1167
  },
475
1168
  ]);
476
1169
 
477
1170
  expect(reg).toBeDefined();
478
1171
  if (reg) {
479
- const desc = reg.description as () => string;
1172
+ const desc = reg.registration.description as () => string;
480
1173
  expect(desc()).toContain("Agent A");
481
1174
  expect(desc()).toContain("Agent B");
482
1175
 
@@ -520,10 +1213,24 @@ describe("defineSubagent", () => {
520
1213
  const reg = buildSubagentRegistration([config]);
521
1214
  expect(reg).toBeDefined();
522
1215
  if (!reg) return;
523
- expect((reg.enabled as () => boolean)()).toBe(true);
1216
+ expect((reg.registration.enabled as () => boolean)()).toBe(true);
524
1217
 
525
1218
  flag = false;
526
- 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();
527
1234
  });
528
1235
  });
529
1236
 
@@ -532,30 +1239,27 @@ describe("defineSubagent", () => {
532
1239
  // ---------------------------------------------------------------------------
533
1240
 
534
1241
  describe("defineSubagentWorkflow", () => {
535
- it("maps previousThreadId to threadId + continueThread", async () => {
536
- let capturedPrompt: string | undefined;
1242
+ it("maps thread fork into sessionInput", async () => {
537
1243
  let capturedSession: SubagentSessionInput | undefined;
538
1244
 
539
1245
  const workflow = defineSubagentWorkflow(
540
1246
  { name: "test", description: "test agent" },
541
- async (prompt, sessionInput) => {
542
- capturedPrompt = prompt;
1247
+ async (_prompt, sessionInput) => {
543
1248
  capturedSession = sessionInput;
544
1249
  return { toolResponse: "ok", data: null, threadId: "t" };
545
1250
  }
546
1251
  );
547
1252
 
548
- await workflow("go", { previousThreadId: "prev-42" });
1253
+ await workflow("go", { thread: { mode: "fork", threadId: "prev-42" } });
549
1254
 
550
- expect(capturedPrompt).toBe("go");
551
1255
  expect(capturedSession).toEqual({
552
1256
  agentName: "test",
553
- threadId: "prev-42",
554
- continueThread: true,
1257
+ sandboxShutdown: "destroy",
1258
+ thread: { mode: "fork", threadId: "prev-42" },
555
1259
  });
556
1260
  });
557
1261
 
558
- it("maps sandboxId", async () => {
1262
+ it("maps sandbox inherit", async () => {
559
1263
  let capturedSession: SubagentSessionInput | undefined;
560
1264
  const workflow = defineSubagentWorkflow(
561
1265
  { name: "test", description: "test agent" },
@@ -565,8 +1269,52 @@ describe("defineSubagentWorkflow", () => {
565
1269
  }
566
1270
  );
567
1271
 
568
- await workflow("go", { sandboxId: "sb-123" });
569
- expect(capturedSession).toEqual({ agentName: "test", sandboxId: "sb-123" });
1272
+ await workflow("go", { sandbox: { mode: "inherit", sandboxId: "sb-123" } });
1273
+ expect(capturedSession).toEqual({
1274
+ agentName: "test",
1275
+ sandboxShutdown: "destroy",
1276
+ sandbox: { mode: "inherit", sandboxId: "sb-123" },
1277
+ });
1278
+ });
1279
+
1280
+ it("maps sandbox fork", async () => {
1281
+ let capturedSession: SubagentSessionInput | undefined;
1282
+ const workflow = defineSubagentWorkflow(
1283
+ { name: "test", description: "test agent" },
1284
+ async (_prompt, sessionInput) => {
1285
+ capturedSession = sessionInput;
1286
+ return { toolResponse: "ok", data: null, threadId: "t" };
1287
+ }
1288
+ );
1289
+
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
+ });
570
1318
  });
571
1319
 
572
1320
  it("passes context as optional third argument", async () => {
@@ -620,4 +1368,21 @@ describe("defineSubagentWorkflow", () => {
620
1368
  expect(workflow.description).toBe("Researches topics");
621
1369
  expect(workflow.resultSchema).toBe(schema);
622
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
+ });
623
1388
  });