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
@@ -38,7 +38,11 @@ vi.mock("@temporalio/workflow", () => {
38
38
  condition: async (fn: () => boolean) => fn(),
39
39
  defineUpdate: (name: string) => ({ __type: "update", name }),
40
40
  defineQuery: (name: string) => ({ __type: "query", name }),
41
+ defineSignal: (name: string) => ({ __type: "signal", name }),
41
42
  setHandler: (_def: unknown, _handler: unknown) => {},
43
+ startChild: async () => ({ result: () => Promise.resolve(null) }),
44
+ workflowInfo: () => ({ taskQueue: "default-queue" }),
45
+ getExternalWorkflowHandle: () => ({ signal: async () => {} }),
42
46
  uuid4: () =>
43
47
  `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
44
48
  ApplicationFailure: MockApplicationFailure,
@@ -150,7 +154,7 @@ describe("createSession integration", () => {
150
154
 
151
155
  const session = await createSession({
152
156
  agentName: "TestAgent",
153
- threadId: "thread-1",
157
+ thread: { mode: "new", threadId: "thread-1" },
154
158
  runAgent: createScriptedRunAgent([{ message: "Hello!", toolCalls: [] }]),
155
159
  threadOps: ops,
156
160
  buildContextMessage: () => "What is 2+2?",
@@ -182,7 +186,7 @@ describe("createSession integration", () => {
182
186
 
183
187
  const session = await createSession({
184
188
  agentName: "TestAgent",
185
- threadId: "thread-1",
189
+ thread: { mode: "new", threadId: "thread-1" },
186
190
  runAgent: createScriptedRunAgent([
187
191
  {
188
192
  message: "Let me echo that.",
@@ -221,7 +225,7 @@ describe("createSession integration", () => {
221
225
 
222
226
  const session = await createSession({
223
227
  agentName: "TestAgent",
224
- threadId: "thread-1",
228
+ thread: { mode: "new", threadId: "thread-1" },
225
229
  runAgent: createScriptedRunAgent([
226
230
  {
227
231
  message: "turn 1",
@@ -270,7 +274,7 @@ describe("createSession integration", () => {
270
274
 
271
275
  const session = await createSession({
272
276
  agentName: "TestAgent",
273
- threadId: "thread-1",
277
+ thread: { mode: "new", threadId: "thread-1" },
274
278
  maxTurns: 3,
275
279
  runAgent: infiniteAgent,
276
280
  threadOps: ops,
@@ -297,7 +301,7 @@ describe("createSession integration", () => {
297
301
 
298
302
  const session = await createSession({
299
303
  agentName: "TestAgent",
300
- threadId: "thread-1",
304
+ thread: { mode: "new", threadId: "thread-1" },
301
305
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
302
306
  threadOps: ops,
303
307
  buildContextMessage: () => "hi",
@@ -330,7 +334,7 @@ describe("createSession integration", () => {
330
334
 
331
335
  const session = await createSession({
332
336
  agentName: "TestAgent",
333
- threadId: "thread-1",
337
+ thread: { mode: "new", threadId: "thread-1" },
334
338
  runAgent: createScriptedRunAgent([]),
335
339
  threadOps: ops,
336
340
  buildContextMessage: () => "hi",
@@ -350,7 +354,7 @@ describe("createSession integration", () => {
350
354
 
351
355
  const session = await createSession({
352
356
  agentName: "TestAgent",
353
- threadId: "thread-1",
357
+ thread: { mode: "new", threadId: "thread-1" },
354
358
  appendSystemPrompt: false,
355
359
  runAgent: createScriptedRunAgent([{ message: "ok", toolCalls: [] }]),
356
360
  threadOps: ops,
@@ -377,7 +381,7 @@ describe("createSession integration", () => {
377
381
 
378
382
  const session = await createSession({
379
383
  agentName: "TestAgent",
380
- threadId: "thread-1",
384
+ thread: { mode: "new", threadId: "thread-1" },
381
385
  runAgent: createScriptedRunAgent([
382
386
  {
383
387
  message: "turn 1",
@@ -413,7 +417,7 @@ describe("createSession integration", () => {
413
417
 
414
418
  const session = await createSession({
415
419
  agentName: "TestAgent",
416
- threadId: "thread-1",
420
+ thread: { mode: "new", threadId: "thread-1" },
417
421
  runAgent: createScriptedRunAgent([
418
422
  {
419
423
  message: "bad call",
@@ -451,15 +455,14 @@ describe("createSession integration", () => {
451
455
  expect(errorConfig?.content).toContain("Invalid tool call");
452
456
  });
453
457
 
454
- // --- continueThread ---
458
+ // --- Thread fork mode ---
455
459
 
456
- it("forks thread when continueThread is set", async () => {
460
+ it("forks thread when thread mode is fork", async () => {
457
461
  const { ops, log } = createMockThreadOps();
458
462
 
459
463
  const session = await createSession({
460
464
  agentName: "TestAgent",
461
- threadId: "source-thread",
462
- continueThread: true,
465
+ thread: { mode: "fork", threadId: "source-thread" },
463
466
  runAgent: createScriptedRunAgent([
464
467
  { message: "continued", toolCalls: [] },
465
468
  ]),
@@ -490,9 +493,9 @@ describe("createSession integration", () => {
490
493
  const sandboxLog: string[] = [];
491
494
 
492
495
  const sandboxOps: SandboxOps = {
493
- createSandbox: async (options) => {
494
- sandboxLog.push(`create:${options?.id ?? "unknown"}`);
495
- return { sandboxId: `sb-${options?.id ?? "unknown"}` };
496
+ createSandbox: async () => {
497
+ sandboxLog.push("create");
498
+ return { sandboxId: "sb-1" };
496
499
  },
497
500
  destroySandbox: async (sandboxId: string) => {
498
501
  sandboxLog.push(`destroy:${sandboxId}`);
@@ -504,15 +507,16 @@ describe("createSession integration", () => {
504
507
  createdAt: new Date().toISOString(),
505
508
  }),
506
509
  forkSandbox: async () => "forked-sandbox-id",
510
+ pauseSandbox: async () => {},
507
511
  };
508
512
 
509
513
  const session = await createSession({
510
514
  agentName: "TestAgent",
511
- threadId: "thread-1",
515
+ thread: { mode: "new", threadId: "thread-1" },
512
516
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
513
517
  threadOps: ops,
514
518
  buildContextMessage: () => "go",
515
- sandbox: sandboxOps,
519
+ sandboxOps,
516
520
  });
517
521
 
518
522
  const stateManager = createAgentStateManager({
@@ -521,8 +525,8 @@ describe("createSession integration", () => {
521
525
 
522
526
  await session.runSession({ stateManager });
523
527
 
524
- expect(sandboxLog).toContain("create:thread-1");
525
- expect(sandboxLog).toContain("destroy:sb-thread-1");
528
+ expect(sandboxLog).toContain("create");
529
+ expect(sandboxLog).toContain("destroy:sb-1");
526
530
  });
527
531
 
528
532
  it("does not create or destroy sandbox when sandboxId is inherited", async () => {
@@ -544,16 +548,17 @@ describe("createSession integration", () => {
544
548
  createdAt: new Date().toISOString(),
545
549
  }),
546
550
  forkSandbox: async () => "forked-sandbox-id",
551
+ pauseSandbox: async () => {},
547
552
  };
548
553
 
549
554
  const session = await createSession({
550
555
  agentName: "TestAgent",
551
- threadId: "thread-1",
556
+ thread: { mode: "new", threadId: "thread-1" },
552
557
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
553
558
  threadOps: ops,
554
559
  buildContextMessage: () => "go",
555
- sandbox: sandboxOps,
556
- sandboxId: "inherited-sb",
560
+ sandboxOps,
561
+ sandbox: { mode: "inherit", sandboxId: "inherited-sb" },
557
562
  });
558
563
 
559
564
  const stateManager = createAgentStateManager({
@@ -581,9 +586,22 @@ describe("createSession integration", () => {
581
586
  },
582
587
  });
583
588
 
589
+ const sandboxOps: SandboxOps = {
590
+ createSandbox: async () => ({ sandboxId: "sb" }),
591
+ destroySandbox: async () => {},
592
+ pauseSandbox: async () => {},
593
+ snapshotSandbox: async () => ({
594
+ sandboxId: "sb",
595
+ providerId: "test",
596
+ data: null,
597
+ createdAt: new Date().toISOString(),
598
+ }),
599
+ forkSandbox: async () => "forked-sb",
600
+ };
601
+
584
602
  const session = await createSession({
585
603
  agentName: "TestAgent",
586
- threadId: "thread-1",
604
+ thread: { mode: "new", threadId: "thread-1" },
587
605
  runAgent: createScriptedRunAgent([
588
606
  {
589
607
  message: "spy",
@@ -594,7 +612,8 @@ describe("createSession integration", () => {
594
612
  threadOps: ops,
595
613
  tools: { Spy: spyTool },
596
614
  buildContextMessage: () => "go",
597
- sandboxId: "my-sandbox",
615
+ sandbox: { mode: "inherit", sandboxId: "my-sandbox" },
616
+ sandboxOps,
598
617
  });
599
618
 
600
619
  const stateManager = createAgentStateManager({
@@ -614,7 +633,7 @@ describe("createSession integration", () => {
614
633
 
615
634
  const session = await createSession({
616
635
  agentName: "TestAgent",
617
- threadId: "thread-1",
636
+ thread: { mode: "new", threadId: "thread-1" },
618
637
  runAgent: async () => {
619
638
  throw new Error("LLM went down");
620
639
  },
@@ -646,7 +665,7 @@ describe("createSession integration", () => {
646
665
 
647
666
  const session = await createSession({
648
667
  agentName: "TestAgent",
649
- threadId: "thread-1",
668
+ thread: { mode: "new", threadId: "thread-1" },
650
669
  runAgent: createScriptedRunAgent([
651
670
  {
652
671
  message: "call echo",
@@ -716,7 +735,7 @@ describe("createSession integration", () => {
716
735
 
717
736
  const session = await createSession({
718
737
  agentName: "TestAgent",
719
- threadId: "thread-1",
738
+ thread: { mode: "new", threadId: "thread-1" },
720
739
  runAgent: createScriptedRunAgent([
721
740
  {
722
741
  message: "computing",
@@ -752,7 +771,7 @@ describe("createSession integration", () => {
752
771
 
753
772
  const session = await createSession({
754
773
  agentName: "TestAgent",
755
- threadId: "thread-1",
774
+ thread: { mode: "new", threadId: "thread-1" },
756
775
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
757
776
  threadOps: ops,
758
777
  buildContextMessage: async () => {
@@ -789,15 +808,16 @@ describe("createSession integration", () => {
789
808
  createdAt: new Date().toISOString(),
790
809
  }),
791
810
  forkSandbox: async () => "forked-sandbox-id",
811
+ pauseSandbox: async () => {},
792
812
  };
793
813
 
794
814
  const session = await createSession({
795
815
  agentName: "TestAgent",
796
- threadId: "thread-1",
816
+ thread: { mode: "new", threadId: "thread-1" },
797
817
  runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
798
818
  threadOps: ops,
799
819
  buildContextMessage: () => "go",
800
- sandbox: sandboxOps,
820
+ sandboxOps,
801
821
  });
802
822
 
803
823
  const stateManager = createAgentStateManager<{ customField: string }>({
@@ -827,7 +847,7 @@ describe("createSession integration", () => {
827
847
 
828
848
  const session = await createSession({
829
849
  agentName: "TestAgent",
830
- threadId: "thread-1",
850
+ thread: { mode: "new", threadId: "thread-1" },
831
851
  runAgent: createScriptedRunAgent([
832
852
  {
833
853
  message: "t1",
@@ -6,6 +6,7 @@ import {
6
6
  } from "@temporalio/workflow";
7
7
  import type { SessionExitReason, MessageContent } from "../types";
8
8
  import type { SessionConfig, ZeitlichSession } from "./types";
9
+ import type { SandboxOps } from "../sandbox/types";
9
10
  import { type AgentStateManager, type JsonSerializable } from "../state/types";
10
11
  import { createToolRouter } from "../tool-router/router";
11
12
  import type { ParsedToolCallUnion, ToolMap } from "../tool-router/types";
@@ -18,6 +19,9 @@ import { uuid4 } from "@temporalio/workflow";
18
19
  * Creates an agent session that manages the agent loop: LLM invocation,
19
20
  * tool routing, subagent coordination, and lifecycle hooks.
20
21
  *
22
+ * When `sandboxOps` is provided the returned session result is guaranteed to
23
+ * include `sandboxId: string`. Without it, `sandboxId` is `undefined`.
24
+ *
21
25
  * @param config - Session and agent configuration (merged `SessionConfig` and `AgentConfig`)
22
26
  * @returns A session object with `runSession()` to start the agent loop
23
27
  *
@@ -29,8 +33,8 @@ import { uuid4 } from "@temporalio/workflow";
29
33
  * const session = await createSession({
30
34
  * agentName: "my-agent",
31
35
  * maxTurns: 20,
32
- * threadId: runId,
33
- * threadOps: proxyGoogleGenAIThreadOps(), // auto-scoped to current workflow
36
+ * thread: { mode: "new" },
37
+ * threadOps: proxyGoogleGenAIThreadOps(),
34
38
  * runAgent: runAgentActivity,
35
39
  * buildContextMessage: () => [{ type: "text", text: prompt }],
36
40
  * subagents: [researcherSubagent],
@@ -42,8 +46,13 @@ import { uuid4 } from "@temporalio/workflow";
42
46
  * const { finalMessage, exitReason } = await session.runSession({ stateManager });
43
47
  * ```
44
48
  */
45
- export const createSession = async <T extends ToolMap, M = unknown>({
46
- threadId: providedThreadId,
49
+ export async function createSession<T extends ToolMap, M = unknown>(
50
+ config: SessionConfig<T, M> & { sandboxOps: SandboxOps }
51
+ ): Promise<ZeitlichSession<M, true>>;
52
+ export async function createSession<T extends ToolMap, M = unknown>(
53
+ config: SessionConfig<T, M>
54
+ ): Promise<ZeitlichSession<M, false>>;
55
+ export async function createSession<T extends ToolMap, M = unknown>({
47
56
  agentName,
48
57
  maxTurns = 50,
49
58
  metadata = {},
@@ -56,16 +65,33 @@ export const createSession = async <T extends ToolMap, M = unknown>({
56
65
  processToolsInParallel = true,
57
66
  hooks = {},
58
67
  appendSystemPrompt = true,
59
- continueThread = false,
60
68
  waitForInputTimeout = "48h",
61
- sandbox: sandboxOps,
62
- sandboxId: inheritedSandboxId,
63
- }: SessionConfig<T, M>): Promise<ZeitlichSession<M>> => {
64
- const sourceThreadId = continueThread ? providedThreadId : undefined;
65
- const threadId =
66
- continueThread && providedThreadId
67
- ? getShortId()
68
- : (providedThreadId ?? getShortId());
69
+ sandboxOps,
70
+ thread: threadInit,
71
+ sandbox: sandboxInit,
72
+ sandboxShutdown = "destroy",
73
+ }: SessionConfig<T, M>): Promise<ZeitlichSession<M, boolean>> {
74
+ // ---------------------------------------------------------------------------
75
+ // Thread resolution
76
+ // ---------------------------------------------------------------------------
77
+ const threadMode = threadInit?.mode ?? "new";
78
+ let threadId: string;
79
+ let sourceThreadId: string | undefined;
80
+
81
+ switch (threadMode) {
82
+ case "new":
83
+ threadId = threadInit?.mode === "new" && threadInit.threadId
84
+ ? threadInit.threadId
85
+ : getShortId();
86
+ break;
87
+ case "continue":
88
+ threadId = (threadInit as { mode: "continue"; threadId: string }).threadId;
89
+ break;
90
+ case "fork":
91
+ sourceThreadId = (threadInit as { mode: "fork"; threadId: string }).threadId;
92
+ threadId = getShortId();
93
+ break;
94
+ }
69
95
 
70
96
  const {
71
97
  appendToolResult,
@@ -76,9 +102,13 @@ export const createSession = async <T extends ToolMap, M = unknown>({
76
102
  } = threadOps;
77
103
 
78
104
  const plugins: ToolMap[string][] = [];
105
+ let destroySubagentSandboxes: (() => Promise<void>) | undefined;
79
106
  if (subagents) {
80
- const reg = buildSubagentRegistration(subagents);
81
- if (reg) plugins.push(reg);
107
+ const result = buildSubagentRegistration(subagents);
108
+ if (result) {
109
+ plugins.push(result.registration);
110
+ destroySubagentSandboxes = result.destroySubagentSandboxes;
111
+ }
82
112
  }
83
113
  if (skills) {
84
114
  const reg = buildSkillRegistration(skills);
@@ -114,12 +144,7 @@ export const createSession = async <T extends ToolMap, M = unknown>({
114
144
  stateManager,
115
145
  }: {
116
146
  stateManager: AgentStateManager<TState>;
117
- }): Promise<{
118
- threadId: string;
119
- finalMessage: M | null;
120
- exitReason: SessionExitReason;
121
- usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
122
- }> => {
147
+ }) => {
123
148
  setHandler(
124
149
  defineUpdate<unknown, [MessageContent]>(`add${agentName}Message`),
125
150
  async (message: MessageContent) => {
@@ -140,12 +165,43 @@ export const createSession = async <T extends ToolMap, M = unknown>({
140
165
  }
141
166
  );
142
167
 
143
- // --- Sandbox lifecycle: create or inherit ---
144
- let sandboxId: string | undefined = inheritedSandboxId;
145
- const ownsSandbox = !sandboxId && !!sandboxOps;
146
- if (ownsSandbox) {
147
- const result = await sandboxOps.createSandbox({ id: threadId });
168
+ // --- Sandbox lifecycle: create, continue, fork, or inherit ----------
169
+ const sandboxMode = sandboxInit?.mode;
170
+ let sandboxId: string | undefined;
171
+ let sandboxOwned = false;
172
+
173
+ if (sandboxMode === "inherit") {
174
+ sandboxId = (sandboxInit as { mode: "inherit"; sandboxId: string }).sandboxId;
175
+ if (!sandboxOps) {
176
+ throw ApplicationFailure.create({
177
+ message: "sandboxId provided but no sandboxOps — cannot manage sandbox lifecycle",
178
+ nonRetryable: true,
179
+ });
180
+ }
181
+ } else if (sandboxMode === "continue") {
182
+ if (!sandboxOps) {
183
+ throw ApplicationFailure.create({
184
+ message: "No sandboxOps provided — cannot continue sandbox",
185
+ nonRetryable: true,
186
+ });
187
+ }
188
+ sandboxId = (sandboxInit as { mode: "continue"; sandboxId: string }).sandboxId;
189
+ sandboxOwned = true;
190
+ } else if (sandboxMode === "fork") {
191
+ if (!sandboxOps) {
192
+ throw ApplicationFailure.create({
193
+ message: "No sandboxOps provided — cannot fork sandbox",
194
+ nonRetryable: true,
195
+ });
196
+ }
197
+ sandboxId = await sandboxOps.forkSandbox(
198
+ (sandboxInit as { mode: "fork"; sandboxId: string }).sandboxId
199
+ );
200
+ sandboxOwned = true;
201
+ } else if (sandboxOps) {
202
+ const result = await sandboxOps.createSandbox();
148
203
  sandboxId = result.sandboxId;
204
+ sandboxOwned = true;
149
205
  if (result.stateUpdate) {
150
206
  stateManager.mergeUpdate(result.stateUpdate as Partial<TState>);
151
207
  }
@@ -161,8 +217,11 @@ export const createSession = async <T extends ToolMap, M = unknown>({
161
217
 
162
218
  const systemPrompt = stateManager.getSystemPrompt();
163
219
 
164
- if (continueThread && sourceThreadId) {
220
+ // --- Thread lifecycle: new, continue, or fork ----------------------
221
+ if (threadMode === "fork" && sourceThreadId) {
165
222
  await forkThread(sourceThreadId, threadId);
223
+ } else if (threadMode === "continue") {
224
+ // "continue" — thread already exists, just append the new message
166
225
  } else {
167
226
  if (appendSystemPrompt) {
168
227
  if (!systemPrompt || systemPrompt.trim() === "") {
@@ -209,7 +268,8 @@ export const createSession = async <T extends ToolMap, M = unknown>({
209
268
  finalMessage: message,
210
269
  exitReason,
211
270
  usage: stateManager.getTotalUsage(),
212
- };
271
+ sandboxId,
272
+ } as Awaited<ReturnType<ZeitlichSession<M, boolean>["runSession"]>>;
213
273
  }
214
274
 
215
275
  const parsedToolCalls: ParsedToolCallUnion<T>[] = [];
@@ -265,8 +325,22 @@ export const createSession = async <T extends ToolMap, M = unknown>({
265
325
  } finally {
266
326
  await callSessionEnd(exitReason, stateManager.getTurns());
267
327
 
268
- if (ownsSandbox && sandboxId && sandboxOps) {
269
- await sandboxOps.destroySandbox(sandboxId);
328
+ if (sandboxOwned && sandboxId && sandboxOps) {
329
+ switch (sandboxShutdown) {
330
+ case "destroy":
331
+ await sandboxOps.destroySandbox(sandboxId);
332
+ break;
333
+ case "pause":
334
+ case "pause-until-parent-close":
335
+ await sandboxOps.pauseSandbox(sandboxId);
336
+ break;
337
+ case "keep":
338
+ break;
339
+ }
340
+ }
341
+
342
+ if (destroySubagentSandboxes) {
343
+ await destroySubagentSandboxes();
270
344
  }
271
345
  }
272
346
 
@@ -275,8 +349,8 @@ export const createSession = async <T extends ToolMap, M = unknown>({
275
349
  finalMessage: null,
276
350
  exitReason,
277
351
  usage: stateManager.getTotalUsage(),
278
- };
352
+ sandboxId,
353
+ } as Awaited<ReturnType<ZeitlichSession<M, boolean>["runSession"]>>;
279
354
  },
280
355
  };
281
- };
282
-
356
+ }
@@ -16,6 +16,7 @@ import type { SandboxOps } from "../sandbox/types";
16
16
  import type { RunAgentActivity } from "../model/types";
17
17
  import type { AgentStateManager, JsonSerializable } from "../state/types";
18
18
  import type { ActivityInterfaceFor } from "@temporalio/workflow";
19
+ import type { ThreadInit, SandboxInit, SubagentSandboxShutdown } from "../lifecycle";
19
20
 
20
21
  /**
21
22
  * Thread operations required by a session.
@@ -79,8 +80,6 @@ export type PrefixedThreadOps<TPrefix extends string> = {
79
80
  export interface SessionConfig<T extends ToolMap, M = unknown> {
80
81
  /** The name of the agent, should be unique within the workflows */
81
82
  agentName: string;
82
- /** The thread ID to use for the session (defaults to a short generated ID) */
83
- threadId?: string;
84
83
  /** Metadata for the session */
85
84
  metadata?: Record<string, unknown>;
86
85
  /** Whether to append the system prompt as message to the thread */
@@ -106,27 +105,67 @@ export interface SessionConfig<T extends ToolMap, M = unknown> {
106
105
  * Returns MessageContent array for the initial HumanMessage.
107
106
  */
108
107
  buildContextMessage: () => MessageContent | Promise<MessageContent>;
109
- /** When true, skip thread initialization and system prompt — append only the new human message to the existing thread. */
110
- continueThread?: boolean;
111
108
  /** How long to wait for input before cancelling the workflow */
112
109
  waitForInputTimeout?: Duration;
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Thread lifecycle
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Thread initialization strategy (default: `{ mode: "new" }`).
117
+ *
118
+ * - `{ mode: "new" }` — start a fresh thread.
119
+ * - `{ mode: "new", threadId: "..." }` — start a fresh thread with a specific ID.
120
+ * - `{ mode: "continue", threadId: "..." }` — append to an existing thread in-place.
121
+ * - `{ mode: "fork", threadId: "..." }` — fork an existing thread and continue in the copy.
122
+ */
123
+ thread?: ThreadInit;
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Sandbox lifecycle
127
+ // ---------------------------------------------------------------------------
128
+
113
129
  /** Sandbox lifecycle operations (optional — omit for agents that don't need a sandbox) */
114
- sandbox?: SandboxOps;
130
+ sandboxOps?: SandboxOps;
131
+ /**
132
+ * Sandbox initialization strategy.
133
+ *
134
+ * - `{ mode: "new" }` — create a fresh sandbox.
135
+ * - `{ mode: "continue", sandboxId: "..." }` — resume a paused sandbox (session owns it).
136
+ * - `{ mode: "fork", sandboxId: "..." }` — fork from an existing sandbox.
137
+ * - `{ mode: "inherit", sandboxId: "..." }` — use a parent's sandbox without ownership.
138
+ *
139
+ * When omitted and `sandboxOps` is provided, defaults to `{ mode: "new" }`.
140
+ */
141
+ sandbox?: SandboxInit;
115
142
  /**
116
- * Pre-existing sandbox ID to reuse (e.g. inherited from a parent agent).
117
- * When set, the session skips `createSandbox` and will not destroy the
118
- * sandbox on exit (the owner is responsible for cleanup).
143
+ * What to do with the sandbox when this session exits.
144
+ *
145
+ * Defaults to `"destroy"` when omitted.
146
+ * Has no effect when the sandbox is inherited (`sandbox.mode === "inherit"`).
119
147
  */
120
- sandboxId?: string;
148
+ sandboxShutdown?: SubagentSandboxShutdown;
121
149
  }
122
150
 
123
- export interface ZeitlichSession<M = unknown> {
151
+ export type SessionResult<
152
+ M,
153
+ TState extends JsonSerializable<TState>,
154
+ HasSandbox extends boolean = boolean,
155
+ > = {
156
+ threadId: string;
157
+ finalMessage: M | null;
158
+ exitReason: SessionExitReason;
159
+ usage: ReturnType<AgentStateManager<TState>["getTotalUsage"]>;
160
+ } & (HasSandbox extends true
161
+ ? { sandboxId: string }
162
+ : { sandboxId?: undefined });
163
+
164
+ export interface ZeitlichSession<
165
+ M = unknown,
166
+ HasSandbox extends boolean = boolean,
167
+ > {
124
168
  runSession<T extends JsonSerializable<T>>(args: {
125
169
  stateManager: AgentStateManager<T>;
126
- }): Promise<{
127
- threadId: string;
128
- finalMessage: M | null;
129
- exitReason: SessionExitReason;
130
- usage: ReturnType<AgentStateManager<T>["getTotalUsage"]>;
131
- }>;
170
+ }): Promise<SessionResult<M, T, HasSandbox>>;
132
171
  }
@@ -3,6 +3,7 @@ import type {
3
3
  SubagentConfig,
4
4
  SubagentDefinition,
5
5
  SubagentHooks,
6
+ SubagentSandboxConfig,
6
7
  SubagentWorkflow,
7
8
  } from "./types";
8
9
  import type { SubagentArgs } from "./tool";
@@ -19,8 +20,8 @@ import type { SubagentArgs } from "./tool";
19
20
  *
20
21
  * // With parent-specific overrides
21
22
  * export const researcher = defineSubagent(researcherWorkflow, {
22
- * allowThreadContinuation: true,
23
- * sandbox: "own",
23
+ * thread: "fork",
24
+ * sandbox: { source: "own", shutdown: "pause" },
24
25
  * hooks: {
25
26
  * onPostExecution: ({ result }) => console.log(result),
26
27
  * },
@@ -42,8 +43,8 @@ export function defineSubagent<
42
43
  hooks?: SubagentHooks<SubagentArgs, z.infer<TResult>>;
43
44
  enabled?: boolean | (() => boolean);
44
45
  taskQueue?: string;
45
- allowThreadContinuation?: boolean;
46
- sandbox?: "inherit" | "own";
46
+ thread?: "new" | "fork" | "continue";
47
+ sandbox?: SubagentSandboxConfig;
47
48
  },
48
49
  ): SubagentConfig<TResult> {
49
50
  return {