zeitlich 0.2.37 → 0.2.39

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 (172) hide show
  1. package/README.md +18 -0
  2. package/dist/{activities-Bb-nAjwQ.d.ts → activities-Bmu7XnaG.d.ts} +4 -4
  3. package/dist/{activities-vkI4_3CC.d.cts → activities-ByBFLvm2.d.cts} +4 -4
  4. package/dist/adapter-id-BB-mmrts.d.cts +17 -0
  5. package/dist/adapter-id-BB-mmrts.d.ts +17 -0
  6. package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
  7. package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
  8. package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
  9. package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
  10. package/dist/adapters/sandbox/bedrock/index.cjs +3 -3
  11. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  12. package/dist/adapters/sandbox/bedrock/index.d.cts +6 -6
  13. package/dist/adapters/sandbox/bedrock/index.d.ts +6 -6
  14. package/dist/adapters/sandbox/bedrock/index.js +3 -3
  15. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  16. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  17. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  18. package/dist/adapters/sandbox/daytona/index.cjs +3 -3
  19. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  20. package/dist/adapters/sandbox/daytona/index.d.cts +4 -4
  21. package/dist/adapters/sandbox/daytona/index.d.ts +4 -4
  22. package/dist/adapters/sandbox/daytona/index.js +3 -3
  23. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  24. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  25. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  26. package/dist/adapters/sandbox/e2b/index.cjs +26 -14
  27. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.d.cts +24 -4
  29. package/dist/adapters/sandbox/e2b/index.d.ts +24 -4
  30. package/dist/adapters/sandbox/e2b/index.js +26 -14
  31. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  32. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  33. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  34. package/dist/adapters/sandbox/inmemory/index.cjs +3 -3
  35. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  36. package/dist/adapters/sandbox/inmemory/index.d.cts +4 -4
  37. package/dist/adapters/sandbox/inmemory/index.d.ts +4 -4
  38. package/dist/adapters/sandbox/inmemory/index.js +3 -3
  39. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  41. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  42. package/dist/adapters/thread/anthropic/index.cjs +150 -13
  43. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  44. package/dist/adapters/thread/anthropic/index.d.cts +9 -8
  45. package/dist/adapters/thread/anthropic/index.d.ts +9 -8
  46. package/dist/adapters/thread/anthropic/index.js +150 -14
  47. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  48. package/dist/adapters/thread/anthropic/workflow.cjs +9 -3
  49. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  50. package/dist/adapters/thread/anthropic/workflow.d.cts +6 -5
  51. package/dist/adapters/thread/anthropic/workflow.d.ts +6 -5
  52. package/dist/adapters/thread/anthropic/workflow.js +9 -4
  53. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  54. package/dist/adapters/thread/google-genai/index.cjs +154 -13
  55. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  56. package/dist/adapters/thread/google-genai/index.d.cts +6 -5
  57. package/dist/adapters/thread/google-genai/index.d.ts +6 -5
  58. package/dist/adapters/thread/google-genai/index.js +154 -14
  59. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  60. package/dist/adapters/thread/google-genai/workflow.cjs +9 -3
  61. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  62. package/dist/adapters/thread/google-genai/workflow.d.cts +6 -5
  63. package/dist/adapters/thread/google-genai/workflow.d.ts +6 -5
  64. package/dist/adapters/thread/google-genai/workflow.js +9 -4
  65. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  66. package/dist/adapters/thread/index.cjs +16 -0
  67. package/dist/adapters/thread/index.cjs.map +1 -0
  68. package/dist/adapters/thread/index.d.cts +34 -0
  69. package/dist/adapters/thread/index.d.ts +34 -0
  70. package/dist/adapters/thread/index.js +12 -0
  71. package/dist/adapters/thread/index.js.map +1 -0
  72. package/dist/adapters/thread/langchain/index.cjs +149 -14
  73. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  74. package/dist/adapters/thread/langchain/index.d.cts +9 -8
  75. package/dist/adapters/thread/langchain/index.d.ts +9 -8
  76. package/dist/adapters/thread/langchain/index.js +149 -15
  77. package/dist/adapters/thread/langchain/index.js.map +1 -1
  78. package/dist/adapters/thread/langchain/workflow.cjs +9 -3
  79. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  80. package/dist/adapters/thread/langchain/workflow.d.cts +6 -5
  81. package/dist/adapters/thread/langchain/workflow.d.ts +6 -5
  82. package/dist/adapters/thread/langchain/workflow.js +9 -4
  83. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  84. package/dist/index.cjs +367 -59
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.d.cts +11 -11
  87. package/dist/index.d.ts +11 -11
  88. package/dist/index.js +365 -61
  89. package/dist/index.js.map +1 -1
  90. package/dist/{proxy-DEtowJyd.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
  91. package/dist/{proxy-0smGKvx8.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
  92. package/dist/{thread-manager-C-C4pI2z.d.ts → thread-manager-CcRXasqs.d.ts} +2 -2
  93. package/dist/{thread-manager-D4vgzYrh.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -2
  94. package/dist/{thread-manager-3fszQih4.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -2
  95. package/dist/{thread-manager-CzYln2OC.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -2
  96. package/dist/{types-CPKDl-y_.d.ts → types-Bcbiq8iv.d.cts} +195 -22
  97. package/dist/{types-CNuWnvy9.d.ts → types-DAsQ21Rt.d.ts} +1 -1
  98. package/dist/{types-B37hKoWA.d.ts → types-DpHTX-iO.d.ts} +58 -1
  99. package/dist/{types-BO7Yju20.d.cts → types-Dt8-HBBT.d.ts} +195 -22
  100. package/dist/{types-D08CXPh8.d.cts → types-hFFi-Zd9.d.cts} +58 -1
  101. package/dist/{types-DWEUmYAJ.d.cts → types-lm8tMNJQ.d.cts} +1 -1
  102. package/dist/{types-tQL9njTu.d.cts → types-yx0LzPGn.d.cts} +21 -7
  103. package/dist/{types-tQL9njTu.d.ts → types-yx0LzPGn.d.ts} +21 -7
  104. package/dist/{workflow-CjXHbZZc.d.ts → workflow-Bmf9EtDW.d.ts} +83 -3
  105. package/dist/{workflow-Do_lzJpT.d.cts → workflow-Bx9utBwb.d.cts} +83 -3
  106. package/dist/workflow.cjs +266 -39
  107. package/dist/workflow.cjs.map +1 -1
  108. package/dist/workflow.d.cts +3 -3
  109. package/dist/workflow.d.ts +3 -3
  110. package/dist/workflow.js +264 -41
  111. package/dist/workflow.js.map +1 -1
  112. package/package.json +12 -2
  113. package/src/adapters/sandbox/bedrock/index.ts +12 -3
  114. package/src/adapters/sandbox/daytona/index.ts +12 -3
  115. package/src/adapters/sandbox/e2b/index.ts +36 -14
  116. package/src/adapters/sandbox/e2b/types.ts +16 -0
  117. package/src/adapters/sandbox/inmemory/index.ts +12 -3
  118. package/src/adapters/thread/adapter-id.test.ts +42 -0
  119. package/src/adapters/thread/anthropic/activities.ts +40 -5
  120. package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
  121. package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
  122. package/src/adapters/thread/anthropic/index.ts +3 -0
  123. package/src/adapters/thread/anthropic/model-invoker.ts +7 -1
  124. package/src/adapters/thread/anthropic/proxy.ts +3 -2
  125. package/src/adapters/thread/anthropic/thread-manager.ts +27 -1
  126. package/src/adapters/thread/google-genai/activities.ts +44 -5
  127. package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
  128. package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
  129. package/src/adapters/thread/google-genai/index.ts +3 -0
  130. package/src/adapters/thread/google-genai/model-invoker.ts +8 -2
  131. package/src/adapters/thread/google-genai/proxy.ts +3 -2
  132. package/src/adapters/thread/google-genai/thread-manager.ts +27 -1
  133. package/src/adapters/thread/index.ts +39 -0
  134. package/src/adapters/thread/langchain/activities.ts +40 -5
  135. package/src/adapters/thread/langchain/adapter-id.ts +16 -0
  136. package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
  137. package/src/adapters/thread/langchain/index.ts +3 -0
  138. package/src/adapters/thread/langchain/model-invoker.ts +7 -1
  139. package/src/adapters/thread/langchain/proxy.ts +3 -2
  140. package/src/adapters/thread/langchain/thread-manager.ts +27 -1
  141. package/src/lib/lifecycle.ts +14 -5
  142. package/src/lib/model/types.ts +7 -0
  143. package/src/lib/sandbox/manager.ts +26 -18
  144. package/src/lib/sandbox/types.ts +27 -7
  145. package/src/lib/session/session-edge-cases.integration.test.ts +336 -4
  146. package/src/lib/session/session.integration.test.ts +192 -2
  147. package/src/lib/session/session.ts +102 -8
  148. package/src/lib/session/types.ts +66 -3
  149. package/src/lib/state/index.ts +1 -0
  150. package/src/lib/state/manager.integration.test.ts +109 -0
  151. package/src/lib/state/manager.ts +38 -8
  152. package/src/lib/state/types.ts +25 -0
  153. package/src/lib/subagent/handler.ts +124 -11
  154. package/src/lib/subagent/index.ts +5 -1
  155. package/src/lib/subagent/subagent.integration.test.ts +628 -104
  156. package/src/lib/subagent/types.ts +63 -14
  157. package/src/lib/subagent/workflow.ts +29 -2
  158. package/src/lib/thread/index.ts +5 -0
  159. package/src/lib/thread/keys.test.ts +101 -0
  160. package/src/lib/thread/keys.ts +94 -0
  161. package/src/lib/thread/manager.test.ts +139 -0
  162. package/src/lib/thread/manager.ts +105 -9
  163. package/src/lib/thread/proxy.ts +3 -0
  164. package/src/lib/thread/types.ts +64 -1
  165. package/src/lib/tool-router/index.ts +2 -0
  166. package/src/lib/tool-router/router-edge-cases.integration.test.ts +92 -0
  167. package/src/lib/tool-router/router.integration.test.ts +12 -0
  168. package/src/lib/tool-router/router.ts +89 -16
  169. package/src/lib/tool-router/types.ts +42 -1
  170. package/src/lib/types.ts +12 -0
  171. package/src/workflow.ts +14 -1
  172. package/tsup.config.ts +1 -0
@@ -1,5 +1,5 @@
1
1
  import type Redis from "ioredis";
2
- import type { JsonValue } from "../state/types";
2
+ import type { JsonValue, PersistedThreadState } from "../state/types";
3
3
  export interface ThreadManagerConfig<T> {
4
4
  redis: Redis;
5
5
  threadId: string;
@@ -34,8 +34,50 @@ export interface BaseThreadManager<T> {
34
34
  * forks — each call creates an independent copy.
35
35
  */
36
36
  fork(newThreadId: string): Promise<BaseThreadManager<T>>;
37
+ /**
38
+ * Atomically replace the entire contents of the thread with `messages`.
39
+ * The existing list is cleared, the new messages are appended in order,
40
+ * and dedup markers from prior appends are cleared so future idempotent
41
+ * appends with ids that were removed aren't silently skipped.
42
+ *
43
+ * Requires the thread manager to be configured with `idOf`.
44
+ */
45
+ replaceAll(messages: T[]): Promise<void>;
37
46
  /** Delete the thread */
38
47
  delete(): Promise<void>;
48
+ /** Get the number of stored messages currently in the thread */
49
+ length(): Promise<number>;
50
+ /**
51
+ * Truncate the thread starting at the message with id `messageId`.
52
+ * That message and every message after it are removed. If `messageId`
53
+ * is not present in the thread this is a no-op — useful as the
54
+ * "truncate on entry" step of the `runAgent` activity, which becomes a
55
+ * no-op on the first attempt and a cleanup on Temporal workflow reset
56
+ * or in-workflow rewind retries.
57
+ *
58
+ * Dedup markers for removed single-message appends are also cleared so
59
+ * that appending the same id again (e.g. the same assistant message id
60
+ * on a rewind retry) is not silently skipped.
61
+ *
62
+ * Requires the thread manager to be configured with `idOf`.
63
+ */
64
+ truncateFromId(messageId: string): Promise<void>;
65
+ /**
66
+ * Load the persisted state slice associated with this thread, or
67
+ * `null` if none has been saved yet. Safe to call on any thread —
68
+ * treats a missing slice as a non-error.
69
+ */
70
+ loadState(): Promise<PersistedThreadState | null>;
71
+ /**
72
+ * Overwrite the persisted state slice for this thread. The thread
73
+ * itself must already exist (same TTL as the message list).
74
+ *
75
+ * Note: {@link BaseThreadManager.fork} already copies the slice to
76
+ * the new thread, so there's no separate `forkState` method.
77
+ */
78
+ saveState(state: PersistedThreadState): Promise<void>;
79
+ /** Delete just the persisted state slice, leaving messages intact. */
80
+ deleteState(): Promise<void>;
39
81
  }
40
82
 
41
83
  /**
@@ -72,6 +114,27 @@ export interface ThreadManagerHooks<TStored, TPrepared = TStored> {
72
114
  index: number,
73
115
  messages: readonly TPrepared[]
74
116
  ) => TPrepared;
117
+ /**
118
+ * One-shot list-level pre-pass applied once when a thread is forked with
119
+ * `transform: true`. Runs before {@link onForkTransform}. May filter,
120
+ * compact, prepend, or otherwise rewrite the whole forked thread — so the
121
+ * returned length need not match the input length. Async, so implementations
122
+ * may call an LLM or other I/O.
123
+ */
124
+ onForkPrepareThread?: (
125
+ messages: readonly TStored[]
126
+ ) => TStored[] | Promise<TStored[]>;
127
+ /**
128
+ * Per-message final pass applied once when a thread is forked with
129
+ * `transform: true`. Runs after {@link onForkPrepareThread}. Pure 1:1 map —
130
+ * must return a value for every input message; length cannot change. Same
131
+ * shape as {@link onPreparedMessage}.
132
+ */
133
+ onForkTransform?: (
134
+ message: TStored,
135
+ index: number,
136
+ messages: readonly TStored[]
137
+ ) => TStored;
75
138
  }
76
139
 
77
140
  export interface ProviderThreadManager<
@@ -22,6 +22,8 @@ export type {
22
22
  InferToolResults,
23
23
  ToolCallResultUnion,
24
24
  ProcessToolCallsContext,
25
+ ProcessToolCallsResult,
26
+ RewindSignal,
25
27
  PreToolUseHookResult,
26
28
  PostToolUseFailureHookResult,
27
29
  ToolHooks,
@@ -23,8 +23,20 @@ vi.mock("@temporalio/workflow", () => {
23
23
  }
24
24
  }
25
25
  const noop = () => {};
26
+ class MockCancellationScope {
27
+ cancellable: boolean;
28
+ constructor(opts?: { cancellable?: boolean }) {
29
+ this.cancellable = opts?.cancellable ?? true;
30
+ }
31
+ async run<T>(fn: () => Promise<T>): Promise<T> {
32
+ return fn();
33
+ }
34
+ cancel(): void {}
35
+ }
26
36
  return {
27
37
  ApplicationFailure: MockApplicationFailure,
38
+ CancellationScope: MockCancellationScope,
39
+ isCancellation: (_err: unknown) => false,
28
40
  uuid4: () => "00000000-0000-0000-0000-000000000000",
29
41
  log: { trace: noop, debug: noop, info: noop, warn: noop, error: noop },
30
42
  };
@@ -648,6 +660,86 @@ describe("createToolRouter edge cases", () => {
648
660
  recovered: true,
649
661
  });
650
662
  });
663
+
664
+ // --- Rewind signal -------------------------------------------------------
665
+
666
+ it("attaches a rewind signal and skips result append when handler returns rewind:true", async () => {
667
+ const rewindTool = defineTool({
668
+ name: "Rewind" as const,
669
+ description: "rewinds",
670
+ schema: z.object({}),
671
+ handler: async () => ({
672
+ toolResponse: "ignored",
673
+ data: null,
674
+ rewind: true,
675
+ }),
676
+ });
677
+
678
+ const router = createToolRouter({
679
+ tools: { Rewind: rewindTool } as const,
680
+ threadId: "t-1",
681
+ appendToolResult: appendSpy.fn,
682
+ });
683
+
684
+ const parsed = router.parseToolCall({
685
+ id: "tc-1",
686
+ name: "Rewind",
687
+ args: {},
688
+ });
689
+
690
+ const results = await router.processToolCalls([parsed]);
691
+
692
+ expect(results).toHaveLength(0);
693
+ expect(results.rewind).toEqual({
694
+ toolCallId: "tc-1",
695
+ toolName: "Rewind",
696
+ });
697
+ expect(appendSpy.calls).toHaveLength(0);
698
+ });
699
+
700
+ it("short-circuits further sequential tool calls when one requests rewind", async () => {
701
+ let laterCalled = false;
702
+ const laterTool = defineTool({
703
+ name: "Later" as const,
704
+ description: "runs after rewind",
705
+ schema: z.object({}),
706
+ handler: async () => {
707
+ laterCalled = true;
708
+ return { toolResponse: "ok", data: null };
709
+ },
710
+ });
711
+ const rewindTool = defineTool({
712
+ name: "Rewind" as const,
713
+ description: "rewinds",
714
+ schema: z.object({}),
715
+ handler: async () => ({
716
+ toolResponse: "ignored",
717
+ data: null,
718
+ rewind: true,
719
+ }),
720
+ });
721
+
722
+ const router = createToolRouter({
723
+ tools: { Rewind: rewindTool, Later: laterTool } as const,
724
+ threadId: "t-1",
725
+ appendToolResult: appendSpy.fn,
726
+ parallel: false,
727
+ });
728
+
729
+ const calls = [
730
+ router.parseToolCall({ id: "tc-1", name: "Rewind", args: {} }),
731
+ router.parseToolCall({ id: "tc-2", name: "Later", args: {} }),
732
+ ];
733
+
734
+ const results = await router.processToolCalls(calls);
735
+
736
+ expect(results).toHaveLength(0);
737
+ expect(results.rewind).toEqual({
738
+ toolCallId: "tc-1",
739
+ toolName: "Rewind",
740
+ });
741
+ expect(laterCalled).toBe(false);
742
+ });
651
743
  });
652
744
 
653
745
  describe("hasNoOtherToolCalls", () => {
@@ -23,8 +23,20 @@ vi.mock("@temporalio/workflow", () => {
23
23
  }
24
24
  }
25
25
  const noop = () => {};
26
+ class MockCancellationScope {
27
+ cancellable: boolean;
28
+ constructor(opts?: { cancellable?: boolean }) {
29
+ this.cancellable = opts?.cancellable ?? true;
30
+ }
31
+ async run<T>(fn: () => Promise<T>): Promise<T> {
32
+ return fn();
33
+ }
34
+ cancel(): void {}
35
+ }
26
36
  return {
27
37
  ApplicationFailure: MockApplicationFailure,
38
+ CancellationScope: MockCancellationScope,
39
+ isCancellation: (_err: unknown) => false,
28
40
  uuid4: () => "00000000-0000-0000-0000-000000000000",
29
41
  log: { trace: noop, debug: noop, info: noop, warn: noop, error: noop },
30
42
  };
@@ -15,12 +15,19 @@ import type {
15
15
  ToolArgs,
16
16
  ToolResult,
17
17
  ProcessToolCallsContext,
18
+ ProcessToolCallsResult,
19
+ RewindSignal,
18
20
  ToolWithHandler,
19
21
  } from "./types";
20
22
 
21
23
  import type { JsonValue } from "../state/types";
22
24
  import type { z } from "zod";
23
- import { uuid4, log } from "@temporalio/workflow";
25
+ import {
26
+ uuid4,
27
+ log,
28
+ CancellationScope,
29
+ isCancellation,
30
+ } from "@temporalio/workflow";
24
31
 
25
32
  /**
26
33
  * Creates a tool router for declarative tool call processing.
@@ -199,11 +206,22 @@ export function createToolRouter<T extends ToolMap>(
199
206
  }
200
207
  }
201
208
 
209
+ /**
210
+ * Internal per-tool-call outcome. `rewind` signals the caller that the
211
+ * handler requested a session-level rewind; when present, the result is
212
+ * not appended to the thread and siblings should be cancelled.
213
+ */
214
+ type ProcessedToolCall =
215
+ | { kind: "result"; value: ToolCallResultUnion<TResults> }
216
+ | { kind: "rewind"; signal: RewindSignal }
217
+ | { kind: "skipped" };
218
+
202
219
  async function processToolCall(
203
220
  toolCall: ParsedToolCallUnion<T>,
204
221
  turn: number,
205
- sandboxId?: string
206
- ): Promise<ToolCallResultUnion<TResults> | null> {
222
+ sandboxId?: string,
223
+ onRewindRequested?: (signal: RewindSignal) => void
224
+ ): Promise<ProcessedToolCall> {
207
225
  const startTime = Date.now();
208
226
  const tool = toolMap.get(toolCall.name);
209
227
 
@@ -220,7 +238,7 @@ export function createToolRouter<T extends ToolMap>(
220
238
  reason: "Skipped by PreToolUse hook",
221
239
  }),
222
240
  });
223
- return null;
241
+ return { kind: "skipped" };
224
242
  }
225
243
  const effectiveArgs = preResult.args;
226
244
 
@@ -235,6 +253,7 @@ export function createToolRouter<T extends ToolMap>(
235
253
  let content!: JsonValue;
236
254
  let resultAppended = false;
237
255
  let metadata: Record<string, unknown> | undefined;
256
+ let rewindRequested = false;
238
257
 
239
258
  try {
240
259
  if (tool) {
@@ -253,11 +272,15 @@ export function createToolRouter<T extends ToolMap>(
253
272
  content = response.toolResponse as JsonValue;
254
273
  resultAppended = response.resultAppended === true;
255
274
  metadata = response.metadata;
275
+ rewindRequested = response.rewind === true;
256
276
  } else {
257
277
  result = { error: `Unknown tool: ${toolCall.name}` };
258
278
  content = JSON.stringify(result, null, 2);
259
279
  }
260
280
  } catch (error) {
281
+ if (isCancellation(error)) {
282
+ throw error;
283
+ }
261
284
  log.warn("tool call failed", {
262
285
  toolName: toolCall.name,
263
286
  toolCallId: toolCall.id,
@@ -276,6 +299,16 @@ export function createToolRouter<T extends ToolMap>(
276
299
  content = recovery.content;
277
300
  }
278
301
 
302
+ if (rewindRequested) {
303
+ const signal: RewindSignal = {
304
+ toolCallId: toolCall.id,
305
+ toolName: toolCall.name,
306
+ };
307
+ log.info("tool requested rewind", { ...signal });
308
+ onRewindRequested?.(signal);
309
+ return { kind: "rewind", signal };
310
+ }
311
+
279
312
  // --- Append result to thread (unless handler already did) ---
280
313
  if (!resultAppended) {
281
314
  const config = {
@@ -319,7 +352,7 @@ export function createToolRouter<T extends ToolMap>(
319
352
  durationMs
320
353
  );
321
354
 
322
- return toolResult;
355
+ return { kind: "result", value: toolResult };
323
356
  }
324
357
 
325
358
  return {
@@ -369,31 +402,71 @@ export function createToolRouter<T extends ToolMap>(
369
402
  async processToolCalls(
370
403
  toolCalls: ParsedToolCallUnion<T>[],
371
404
  context?: ProcessToolCallsContext
372
- ): Promise<ToolCallResultUnion<TResults>[]> {
405
+ ): Promise<ProcessToolCallsResult<TResults>> {
406
+ const attachRewind = (
407
+ arr: ToolCallResultUnion<TResults>[],
408
+ rewind: RewindSignal | undefined,
409
+ ): ProcessToolCallsResult<TResults> => {
410
+ if (rewind) {
411
+ (arr as ProcessToolCallsResult<TResults>).rewind = rewind;
412
+ }
413
+ return arr as ProcessToolCallsResult<TResults>;
414
+ };
415
+
373
416
  if (toolCalls.length === 0) {
374
- return [];
417
+ return attachRewind([], undefined);
375
418
  }
376
419
 
377
420
  const turn = context?.turn ?? 0;
378
421
  const sandboxId = context?.sandboxId;
379
422
 
423
+ let rewindSignal: RewindSignal | undefined;
424
+
380
425
  if (options.parallel) {
381
- const results = await Promise.all(
382
- toolCalls.map((tc) => processToolCall(tc, turn, sandboxId))
426
+ const scope = new CancellationScope({ cancellable: true });
427
+ const onRewindRequested = (signal: RewindSignal): void => {
428
+ if (!rewindSignal) {
429
+ rewindSignal = signal;
430
+ // Cancel all other in-flight tool calls in this batch.
431
+ scope.cancel();
432
+ }
433
+ };
434
+
435
+ const outcomes = await scope.run(async () =>
436
+ Promise.allSettled(
437
+ toolCalls.map((tc) =>
438
+ processToolCall(tc, turn, sandboxId, onRewindRequested)
439
+ )
440
+ )
383
441
  );
384
- return results.filter(
385
- (r): r is NonNullable<typeof r> => r !== null
386
- ) as ToolCallResultUnion<TResults>[];
442
+
443
+ const results: ToolCallResultUnion<TResults>[] = [];
444
+ for (const outcome of outcomes) {
445
+ if (outcome.status === "rejected") {
446
+ if (isCancellation(outcome.reason)) {
447
+ continue;
448
+ }
449
+ throw outcome.reason;
450
+ }
451
+ if (outcome.value.kind === "result") {
452
+ results.push(outcome.value.value);
453
+ }
454
+ }
455
+ return attachRewind(results, rewindSignal);
387
456
  }
388
457
 
389
458
  const results: ToolCallResultUnion<TResults>[] = [];
390
459
  for (const toolCall of toolCalls) {
391
- const result = await processToolCall(toolCall, turn, sandboxId);
392
- if (result !== null) {
393
- results.push(result);
460
+ const outcome = await processToolCall(toolCall, turn, sandboxId);
461
+ if (outcome.kind === "rewind") {
462
+ rewindSignal = outcome.signal;
463
+ break;
464
+ }
465
+ if (outcome.kind === "result") {
466
+ results.push(outcome.value);
394
467
  }
395
468
  }
396
- return results;
469
+ return attachRewind(results, rewindSignal);
397
470
  },
398
471
 
399
472
  async processToolCallsByName<TName extends ToolNames<T>, TResult>(
@@ -2,6 +2,7 @@ import type { TokenUsage, ToolResultConfig } from "../types";
2
2
  import type { JsonValue } from "../state/types";
3
3
  import type { z } from "zod";
4
4
  import type { ActivityFunctionWithOptions } from "@temporalio/workflow";
5
+ import type { SandboxSnapshot } from "../sandbox/types";
5
6
 
6
7
  // ============================================================================
7
8
  // Tool Definition Types
@@ -139,12 +140,29 @@ export interface ToolHandlerResponse<
139
140
  * payloads through Temporal's activity payload limit.
140
141
  */
141
142
  resultAppended?: boolean;
143
+ /**
144
+ * When true, the session will rewind: any in-flight parallel tool
145
+ * calls are cancelled and the LLM call is retried. The session reuses
146
+ * the same `assistantMessageId` for the retry; the next `runAgent`
147
+ * activity truncates the thread from that id on entry, wiping the
148
+ * triggering assistant message and any tool results already appended
149
+ * before re-invoking the LLM.
150
+ *
151
+ * The `toolResponse` for a rewinding tool call is ignored (never
152
+ * appended) since the thread is rewound back to the pre-assistant
153
+ * state on the next invocation.
154
+ */
155
+ rewind?: boolean;
142
156
  /** Token usage from the tool execution (e.g. child agent invocations) */
143
157
  usage?: TokenUsage;
144
158
  /** Thread ID used by the handler (surfaced to the LLM for subagent thread continuation) */
145
159
  threadId?: string;
146
160
  /** Sandbox ID created or used by the handler (e.g. child agent sandbox) */
147
161
  sandboxId?: string;
162
+ /** Snapshot captured on exit when `sandboxShutdown === "snapshot"`. */
163
+ snapshot?: SandboxSnapshot;
164
+ /** Snapshot captured immediately after sandbox seeding (before the agent loop starts) when `sandbox.mode === "new"` and `sandboxShutdown === "snapshot"`. Intended as a reusable "base" for new threads that want to skip re-seeding. */
165
+ baseSnapshot?: SandboxSnapshot;
148
166
  /** Unvalidated metadata passthrough from handler to hooks (e.g. infrastructure state) */
149
167
  metadata?: Record<string, unknown>;
150
168
  }
@@ -278,6 +296,29 @@ export interface ProcessToolCallsContext {
278
296
  sandboxId?: string;
279
297
  }
280
298
 
299
+ /**
300
+ * Signal that a tool handler requested a rewind. Attached to the
301
+ * {@link ProcessToolCallsResult} so the session can reuse the same
302
+ * `assistantMessageId` for the retry; the next `runAgent` activity
303
+ * then truncates the thread from that id on entry.
304
+ */
305
+ export interface RewindSignal {
306
+ toolCallId: string;
307
+ toolName: string;
308
+ }
309
+
310
+ /**
311
+ * Result returned by {@link ToolRouter.processToolCalls}.
312
+ *
313
+ * The object is a standard array of tool call results for successful
314
+ * tool calls (cancelled or rewinding siblings are omitted), extended
315
+ * with a `rewind` property when any tool in the batch requested a
316
+ * rewind. Using an array-with-property lets existing code that treats
317
+ * the return value as `ToolCallResultUnion[]` continue to work.
318
+ */
319
+ export type ProcessToolCallsResult<TResults extends Record<string, unknown>> =
320
+ ToolCallResultUnion<TResults>[] & { rewind?: RewindSignal };
321
+
281
322
  // ============================================================================
282
323
  // Hook Types
283
324
  // ============================================================================
@@ -469,7 +510,7 @@ export interface ToolRouter<T extends ToolMap> {
469
510
  processToolCalls(
470
511
  toolCalls: ParsedToolCallUnion<T>[],
471
512
  context?: ProcessToolCallsContext
472
- ): Promise<ToolCallResultUnion<InferToolResults<T>>[]>;
513
+ ): Promise<ProcessToolCallsResult<InferToolResults<T>>>;
473
514
 
474
515
  /**
475
516
  * Process tool calls matching a specific name with a custom handler.
package/src/lib/types.ts CHANGED
@@ -92,6 +92,18 @@ export interface RunAgentConfig extends AgentConfig {
92
92
  threadKey?: string;
93
93
  /** Metadata for the session */
94
94
  metadata?: Record<string, unknown>;
95
+ /**
96
+ * The id under which the assistant message produced by this call will
97
+ * be appended. The activity truncates the thread from this id on
98
+ * entry (no-op on the first attempt) so that:
99
+ *
100
+ * - Rewind retries can reuse the same id and the previous (bad)
101
+ * assistant + its tool results are wiped before the retry LLM call.
102
+ * - Resetting the Temporal workflow to this activity restores the
103
+ * pre-call thread state: replay re-truncates, re-invokes, and
104
+ * appends under the same id.
105
+ */
106
+ assistantMessageId: string;
95
107
  }
96
108
 
97
109
  /**
package/src/workflow.ts CHANGED
@@ -40,6 +40,11 @@ export type {
40
40
 
41
41
  // Thread utilities
42
42
  export { getShortId } from "./lib/thread/id";
43
+ export {
44
+ THREAD_TTL_SECONDS,
45
+ getThreadListKey,
46
+ getThreadMetaKey,
47
+ } from "./lib/thread/keys";
43
48
 
44
49
  // State management
45
50
  export { createAgentStateManager } from "./lib/state";
@@ -49,6 +54,7 @@ export type {
49
54
  JsonSerializable,
50
55
  JsonValue,
51
56
  JsonPrimitive,
57
+ PersistedThreadState,
52
58
  } from "./lib/state";
53
59
 
54
60
  // Tool router (includes registry functionality)
@@ -57,7 +63,11 @@ export {
57
63
  hasNoOtherToolCalls,
58
64
  defineTool,
59
65
  } from "./lib/tool-router";
60
- export { defineSubagent, defineSubagentWorkflow } from "./lib/subagent";
66
+ export {
67
+ defineSubagent,
68
+ defineSubagentWorkflow,
69
+ DEFAULT_SUBAGENT_WORKFLOW_RUN_TIMEOUT,
70
+ } from "./lib/subagent";
61
71
  export type {
62
72
  // Tool definition types
63
73
  ToolDefinition,
@@ -94,6 +104,8 @@ export type {
94
104
  // Other
95
105
  AppendToolResultFn,
96
106
  ProcessToolCallsContext,
107
+ ProcessToolCallsResult,
108
+ RewindSignal,
97
109
  } from "./lib/tool-router";
98
110
 
99
111
  // Session & message lifecycle hooks
@@ -151,6 +163,7 @@ export { proxyRunAgent } from "./lib/model/proxy";
151
163
  // Subagent types
152
164
  export type {
153
165
  SubagentConfig,
166
+ SubagentChildWorkflowOptions,
154
167
  SubagentDefinition,
155
168
  SubagentFnResult,
156
169
  SubagentHooks,
package/tsup.config.ts CHANGED
@@ -4,6 +4,7 @@ export default defineConfig({
4
4
  entry: {
5
5
  index: "src/index.ts",
6
6
  workflow: "src/workflow.ts",
7
+ "adapters/thread/index": "src/adapters/thread/index.ts",
7
8
  "adapters/thread/langchain/index": "src/adapters/thread/langchain/index.ts",
8
9
  "adapters/thread/langchain/workflow":
9
10
  "src/adapters/thread/langchain/proxy.ts",