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
@@ -17,6 +17,9 @@
17
17
  * ```
18
18
  */
19
19
 
20
+ // Adapter identity (wire format — matches Temporal activity-name prefix)
21
+ export { ADAPTER_ID, type AdapterId } from "./adapter-id";
22
+
20
23
  // Adapter (primary API)
21
24
  export {
22
25
  createAnthropicAdapter,
@@ -61,7 +61,7 @@ export function createAnthropicModelInvoker({
61
61
  return async function invokeAnthropicModel(
62
62
  config: ModelInvokerConfig
63
63
  ): Promise<AgentResponse<Anthropic.Messages.Message>> {
64
- const { threadId, threadKey, state } = config;
64
+ const { threadId, threadKey, state, assistantMessageId } = config;
65
65
  const { heartbeat, signal } = getActivityContext();
66
66
 
67
67
  const thread = createAnthropicThreadManager({
@@ -70,6 +70,12 @@ export function createAnthropicModelInvoker({
70
70
  key: threadKey,
71
71
  hooks,
72
72
  });
73
+ // Truncate the thread starting at the id the assistant message
74
+ // will be stored under. On the happy path this is a no-op; on a
75
+ // rewind retry or a Temporal workflow reset it wipes the prior
76
+ // attempt's assistant + tool results so the LLM sees the same
77
+ // pre-call state that it saw originally.
78
+ await thread.truncateFromId(assistantMessageId);
73
79
  const { messages, system } = await thread.prepareForInvocation();
74
80
 
75
81
  const anthropicTools = toAnthropicTools(state.tools);
@@ -22,15 +22,16 @@ import { type ActivityInterfaceFor } from "@temporalio/workflow";
22
22
  import type { ThreadOps } from "../../../lib/session/types";
23
23
  import type { AnthropicContent } from "./thread-manager";
24
24
  import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
+ import { ADAPTER_ID } from "./adapter-id";
25
26
 
26
- const ADAPTER_PREFIX = "anthropic";
27
+ export { ADAPTER_ID, type AdapterId } from "./adapter-id";
27
28
 
28
29
  export function proxyAnthropicThreadOps(
29
30
  scope?: string,
30
31
  options?: Parameters<typeof createThreadOpsProxy>[2]
31
32
  ): ActivityInterfaceFor<ThreadOps<AnthropicContent>> {
32
33
  return createThreadOpsProxy(
33
- ADAPTER_PREFIX,
34
+ ADAPTER_ID,
34
35
  scope,
35
36
  options
36
37
  ) as ActivityInterfaceFor<ThreadOps<AnthropicContent>>;
@@ -224,5 +224,31 @@ export function createAnthropicThreadManager(
224
224
  },
225
225
  };
226
226
 
227
- return Object.assign(base, helpers);
227
+ const manager = Object.assign(base, helpers);
228
+
229
+ const originalFork = manager.fork.bind(manager);
230
+ manager.fork = async (
231
+ newThreadId: string
232
+ ): Promise<AnthropicThreadManager> => {
233
+ await originalFork(newThreadId);
234
+ const forked = createAnthropicThreadManager({
235
+ ...config,
236
+ threadId: newThreadId,
237
+ });
238
+ const { onForkPrepareThread, onForkTransform } = config.hooks ?? {};
239
+ if (!onForkPrepareThread && !onForkTransform) {
240
+ return forked;
241
+ }
242
+ let next = await forked.load();
243
+ if (onForkPrepareThread) {
244
+ next = await onForkPrepareThread(next);
245
+ }
246
+ if (onForkTransform) {
247
+ next = next.map((msg, i) => onForkTransform(msg, i, next));
248
+ }
249
+ await forked.replaceAll(next);
250
+ return forked;
251
+ };
252
+
253
+ return manager;
228
254
  }
@@ -1,6 +1,7 @@
1
1
  import type Redis from "ioredis";
2
2
  import type { GoogleGenAI, Content, Part } from "@google/genai";
3
3
  import type { ToolResultConfig } from "../../../lib/types";
4
+ import type { PersistedThreadState } from "../../../lib/state/types";
4
5
  import type {
5
6
  ActivityToolHandler,
6
7
  RouterContext,
@@ -19,12 +20,11 @@ import {
19
20
  type GoogleGenAIThreadManagerHooks,
20
21
  } from "./thread-manager";
21
22
  import { createGoogleGenAIModelInvoker } from "./model-invoker";
22
-
23
- const ADAPTER_PREFIX = "googleGenAI" as const;
23
+ import { ADAPTER_ID } from "./adapter-id";
24
24
 
25
25
  export type GoogleGenAIThreadOps<TScope extends string = ""> =
26
26
  PrefixedThreadOps<
27
- ScopedPrefix<TScope, typeof ADAPTER_PREFIX>,
27
+ ScopedPrefix<TScope, typeof ADAPTER_ID>,
28
28
  GoogleGenAIContent
29
29
  >;
30
30
 
@@ -219,17 +219,56 @@ export function createGoogleGenAIAdapter(
219
219
  redis,
220
220
  threadId: sourceThreadId,
221
221
  key: threadKey,
222
+ hooks: config.hooks,
222
223
  });
223
224
  await thread.fork(targetThreadId);
224
225
  },
226
+
227
+ async truncateThread(
228
+ threadId: string,
229
+ messageId: string,
230
+ threadKey?: string,
231
+ ): Promise<void> {
232
+ const thread = createGoogleGenAIThreadManager({
233
+ redis,
234
+ threadId,
235
+ key: threadKey,
236
+ });
237
+ await thread.truncateFromId(messageId);
238
+ },
239
+
240
+ async loadThreadState(
241
+ threadId: string,
242
+ threadKey?: string
243
+ ): Promise<PersistedThreadState | null> {
244
+ const thread = createGoogleGenAIThreadManager({
245
+ redis,
246
+ threadId,
247
+ key: threadKey,
248
+ });
249
+ return thread.loadState();
250
+ },
251
+
252
+ async saveThreadState(
253
+ threadId: string,
254
+ state: PersistedThreadState,
255
+ threadKey?: string
256
+ ): Promise<void> {
257
+ const thread = createGoogleGenAIThreadManager({
258
+ redis,
259
+ threadId,
260
+ key: threadKey,
261
+ });
262
+ await thread.saveState(state);
263
+ },
225
264
  };
226
265
 
227
266
  function createActivities<S extends string = "">(
228
267
  scope?: S
229
268
  ): GoogleGenAIThreadOps<S> {
230
269
  const prefix = scope
231
- ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
232
- : ADAPTER_PREFIX;
270
+ ? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
271
+ : ADAPTER_ID;
233
272
  const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
234
273
  return Object.fromEntries(
235
274
  Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public adapter identity for the Google GenAI thread adapter.
3
+ *
4
+ * This value is wire format — it appears as the prefix for Temporal
5
+ * activity names (e.g. `googleGenAICodingAgentInitializeThread`) and
6
+ * must never change, since renaming it would orphan existing persisted
7
+ * threads and break in-flight workflows.
8
+ *
9
+ * Re-exported from `zeitlich/adapters/thread/google-genai` so downstream
10
+ * consumers can use the exact same literal the adapter uses internally,
11
+ * typed as the narrow string literal `"googleGenAI"`.
12
+ */
13
+ export const ADAPTER_ID = "googleGenAI" as const;
14
+
15
+ /** Narrow string-literal type for {@link ADAPTER_ID}. */
16
+ export type AdapterId = typeof ADAPTER_ID;
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { StoredContent } from "./thread-manager";
3
+ import { createGoogleGenAIThreadManager } from "./thread-manager";
4
+
5
+ function createStatefulRedis() {
6
+ const lists = new Map<string, string[]>();
7
+ const strings = new Map<string, string>();
8
+
9
+ return {
10
+ exists: vi.fn(async (...keys: string[]) =>
11
+ keys.reduce(
12
+ (acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
13
+ 0
14
+ )
15
+ ),
16
+ lrange: vi.fn(async (key: string, start: number, stop: number) => {
17
+ const list = lists.get(key) ?? [];
18
+ const end = stop === -1 ? list.length : stop + 1;
19
+ return list.slice(start, end);
20
+ }),
21
+ rpush: vi.fn(async (key: string, ...values: string[]) => {
22
+ const list = lists.get(key) ?? [];
23
+ list.push(...values);
24
+ lists.set(key, list);
25
+ return list.length;
26
+ }),
27
+ ltrim: vi.fn(async () => "OK"),
28
+ del: vi.fn(async (...keys: string[]) => {
29
+ let removed = 0;
30
+ for (const k of keys) {
31
+ if (lists.delete(k)) removed++;
32
+ if (strings.delete(k)) removed++;
33
+ }
34
+ return removed;
35
+ }),
36
+ set: vi.fn(async (key: string, value: string) => {
37
+ strings.set(key, value);
38
+ return "OK";
39
+ }),
40
+ get: vi.fn(async (key: string) => strings.get(key) ?? null),
41
+ expire: vi.fn(async () => 1),
42
+ llen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
43
+ eval: vi.fn(
44
+ async (_script: string, _numKeys: number, ...args: string[]) => {
45
+ const [dedupKey, listKey, , ...serialised] = args;
46
+ if (!dedupKey || !listKey) return 0;
47
+ if (strings.has(dedupKey)) return 0;
48
+ const list = lists.get(listKey) ?? [];
49
+ list.push(...serialised);
50
+ lists.set(listKey, list);
51
+ strings.set(dedupKey, "1");
52
+ return 1;
53
+ }
54
+ ),
55
+ };
56
+ }
57
+
58
+ const userContent: StoredContent = {
59
+ id: "msg-1",
60
+ content: { role: "user", parts: [{ text: "Hello" }] },
61
+ };
62
+
63
+ const modelContent: StoredContent = {
64
+ id: "msg-2",
65
+ content: { role: "model", parts: [{ text: "Hi there!" }] },
66
+ };
67
+
68
+ const userContent2: StoredContent = {
69
+ id: "msg-3",
70
+ content: { role: "user", parts: [{ text: "Again please" }] },
71
+ };
72
+
73
+ async function seed(
74
+ redis: ReturnType<typeof createStatefulRedis>,
75
+ threadId: string,
76
+ messages: StoredContent[]
77
+ ): Promise<void> {
78
+ const tm = createGoogleGenAIThreadManager({
79
+ redis: redis as never,
80
+ threadId,
81
+ });
82
+ await tm.initialize();
83
+ await tm.append(messages);
84
+ }
85
+
86
+ describe("Google GenAI fork + transform hooks", () => {
87
+ it("falls back to plain fork when no hooks are set", async () => {
88
+ const redis = createStatefulRedis();
89
+ await seed(redis, "src", [userContent, modelContent]);
90
+
91
+ const tm = createGoogleGenAIThreadManager({
92
+ redis: redis as never,
93
+ threadId: "src",
94
+ });
95
+ const forked = await tm.fork("dst");
96
+ expect(await forked.load()).toEqual([userContent, modelContent]);
97
+ });
98
+
99
+ it("applies onForkPrepareThread then onForkTransform in order", async () => {
100
+ const redis = createStatefulRedis();
101
+ await seed(redis, "src", [userContent, modelContent, userContent2]);
102
+
103
+ const order: string[] = [];
104
+ const tm = createGoogleGenAIThreadManager({
105
+ redis: redis as never,
106
+ threadId: "src",
107
+ hooks: {
108
+ onForkPrepareThread: async (messages) => {
109
+ order.push("prepare");
110
+ return messages.slice(0, -1);
111
+ },
112
+ onForkTransform: (msg, i) => {
113
+ order.push("transform");
114
+ return {
115
+ ...msg,
116
+ content: { ...msg.content, parts: [{ text: `[x${i}]` }] },
117
+ };
118
+ },
119
+ },
120
+ });
121
+
122
+ const forked = await tm.fork("dst");
123
+ const loaded = await forked.load();
124
+
125
+ expect(order).toEqual(["prepare", "transform", "transform"]);
126
+ expect(loaded).toHaveLength(2);
127
+ expect(loaded[0]?.content.parts).toEqual([{ text: "[x0]" }]);
128
+ expect(loaded[1]?.content.parts).toEqual([{ text: "[x1]" }]);
129
+ });
130
+
131
+ it("keeps the source thread untouched", async () => {
132
+ const redis = createStatefulRedis();
133
+ await seed(redis, "src", [userContent, modelContent]);
134
+
135
+ const tm = createGoogleGenAIThreadManager({
136
+ redis: redis as never,
137
+ threadId: "src",
138
+ hooks: {
139
+ onForkTransform: (msg) => ({
140
+ ...msg,
141
+ content: { ...msg.content, parts: [{ text: "mutated" }] },
142
+ }),
143
+ },
144
+ });
145
+
146
+ await tm.fork("dst");
147
+ expect(await tm.load()).toEqual([userContent, modelContent]);
148
+ });
149
+ });
@@ -17,6 +17,9 @@
17
17
  * ```
18
18
  */
19
19
 
20
+ // Adapter identity (wire format — matches Temporal activity-name prefix)
21
+ export { ADAPTER_ID, type AdapterId } from "./adapter-id";
22
+
20
23
  // Adapter (primary API)
21
24
  export {
22
25
  createGoogleGenAIAdapter,
@@ -64,7 +64,7 @@ export function createGoogleGenAIModelInvoker({
64
64
  return async function invokeGoogleGenAIModel(
65
65
  config: ModelInvokerConfig
66
66
  ): Promise<AgentResponse<Content>> {
67
- const { threadId, threadKey, state } = config;
67
+ const { threadId, threadKey, state, assistantMessageId } = config;
68
68
  const { heartbeat, signal } = getActivityContext();
69
69
 
70
70
  const thread = createGoogleGenAIThreadManager({
@@ -73,7 +73,13 @@ export function createGoogleGenAIModelInvoker({
73
73
  key: threadKey,
74
74
  hooks,
75
75
  });
76
- const { contents, systemInstruction } = await thread.prepareForInvocation();
76
+ // Truncate the thread starting at the id the assistant message
77
+ // will be stored under. No-op on the first attempt; on rewind
78
+ // retry / Temporal reset it wipes the prior attempt's assistant
79
+ // + tool results so the LLM sees the original pre-call state.
80
+ await thread.truncateFromId(assistantMessageId);
81
+ const { contents, systemInstruction } =
82
+ await thread.prepareForInvocation();
77
83
 
78
84
  const functionDeclarations = toFunctionDeclarations(state.tools);
79
85
  const tools =
@@ -22,15 +22,16 @@ import { type ActivityInterfaceFor } from "@temporalio/workflow";
22
22
  import type { ThreadOps } from "../../../lib/session/types";
23
23
  import type { GoogleGenAIContent } from "./thread-manager";
24
24
  import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
+ import { ADAPTER_ID } from "./adapter-id";
25
26
 
26
- const ADAPTER_PREFIX = "googleGenAI";
27
+ export { ADAPTER_ID, type AdapterId } from "./adapter-id";
27
28
 
28
29
  export function proxyGoogleGenAIThreadOps(
29
30
  scope?: string,
30
31
  options?: Parameters<typeof createThreadOpsProxy>[2]
31
32
  ): ActivityInterfaceFor<ThreadOps<GoogleGenAIContent>> {
32
33
  return createThreadOpsProxy(
33
- ADAPTER_PREFIX,
34
+ ADAPTER_ID,
34
35
  scope,
35
36
  options
36
37
  ) as ActivityInterfaceFor<ThreadOps<GoogleGenAIContent>>;
@@ -200,5 +200,31 @@ export function createGoogleGenAIThreadManager(
200
200
  },
201
201
  };
202
202
 
203
- return Object.assign(base, helpers);
203
+ const manager = Object.assign(base, helpers);
204
+
205
+ const originalFork = manager.fork.bind(manager);
206
+ manager.fork = async (
207
+ newThreadId: string
208
+ ): Promise<GoogleGenAIThreadManager> => {
209
+ await originalFork(newThreadId);
210
+ const forked = createGoogleGenAIThreadManager({
211
+ ...config,
212
+ threadId: newThreadId,
213
+ });
214
+ const { onForkPrepareThread, onForkTransform } = config.hooks ?? {};
215
+ if (!onForkPrepareThread && !onForkTransform) {
216
+ return forked;
217
+ }
218
+ let next = await forked.load();
219
+ if (onForkPrepareThread) {
220
+ next = await onForkPrepareThread(next);
221
+ }
222
+ if (onForkTransform) {
223
+ next = next.map((msg, i) => onForkTransform(msg, i, next));
224
+ }
225
+ await forked.replaceAll(next);
226
+ return forked;
227
+ };
228
+
229
+ return manager;
204
230
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Barrel re-exports for every built-in thread adapter's public identity.
3
+ *
4
+ * Downstream consumers reading persisted threads can import a narrow
5
+ * discriminated union of adapter identifiers without pulling the full
6
+ * adapter implementation (Redis, provider SDKs, etc.) as a dependency —
7
+ * each individual re-export resolves to an `adapter-id.ts` module with
8
+ * no runtime dependencies.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import {
13
+ * LANGCHAIN_ADAPTER_ID,
14
+ * GOOGLE_GENAI_ADAPTER_ID,
15
+ * ANTHROPIC_ADAPTER_ID,
16
+ * type ThreadAdapterId,
17
+ * } from 'zeitlich/adapters/thread';
18
+ *
19
+ * interface ThreadIdentity {
20
+ * adapter: ThreadAdapterId;
21
+ * threadKey: string;
22
+ * threadId: string;
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ export { ADAPTER_ID as LANGCHAIN_ADAPTER_ID } from "./langchain/adapter-id";
28
+ export { ADAPTER_ID as GOOGLE_GENAI_ADAPTER_ID } from "./google-genai/adapter-id";
29
+ export { ADAPTER_ID as ANTHROPIC_ADAPTER_ID } from "./anthropic/adapter-id";
30
+
31
+ import type { ADAPTER_ID as LANGCHAIN } from "./langchain/adapter-id";
32
+ import type { ADAPTER_ID as GOOGLE_GENAI } from "./google-genai/adapter-id";
33
+ import type { ADAPTER_ID as ANTHROPIC } from "./anthropic/adapter-id";
34
+
35
+ /** Narrow discriminated union of every built-in thread adapter id. */
36
+ export type ThreadAdapterId =
37
+ | typeof LANGCHAIN
38
+ | typeof GOOGLE_GENAI
39
+ | typeof ANTHROPIC;
@@ -1,5 +1,6 @@
1
1
  import type Redis from "ioredis";
2
2
  import type { ToolResultConfig } from "../../../lib/types";
3
+ import type { PersistedThreadState } from "../../../lib/state/types";
3
4
  import type { MessageContent } from "@langchain/core/messages";
4
5
  import type {
5
6
  ActivityToolHandler,
@@ -21,11 +22,10 @@ import {
21
22
  type LangChainThreadManagerHooks,
22
23
  } from "./thread-manager";
23
24
  import { createLangChainModelInvoker } from "./model-invoker";
24
-
25
- const ADAPTER_PREFIX = "langChain" as const;
25
+ import { ADAPTER_ID } from "./adapter-id";
26
26
 
27
27
  export type LangChainThreadOps<TScope extends string = ""> = PrefixedThreadOps<
28
- ScopedPrefix<TScope, typeof ADAPTER_PREFIX>,
28
+ ScopedPrefix<TScope, typeof ADAPTER_ID>,
29
29
  LangChainContent
30
30
  >;
31
31
 
@@ -192,17 +192,52 @@ export function createLangChainAdapter(
192
192
  redis,
193
193
  threadId: sourceThreadId,
194
194
  key: threadKey,
195
+ hooks: config.hooks,
195
196
  });
196
197
  await thread.fork(targetThreadId);
197
198
  },
199
+
200
+ async truncateThread(
201
+ threadId: string,
202
+ messageId: string,
203
+ threadKey?: string,
204
+ ): Promise<void> {
205
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
206
+ await thread.truncateFromId(messageId);
207
+ },
208
+
209
+ async loadThreadState(
210
+ threadId: string,
211
+ threadKey?: string
212
+ ): Promise<PersistedThreadState | null> {
213
+ const thread = createLangChainThreadManager({
214
+ redis,
215
+ threadId,
216
+ key: threadKey,
217
+ });
218
+ return thread.loadState();
219
+ },
220
+
221
+ async saveThreadState(
222
+ threadId: string,
223
+ state: PersistedThreadState,
224
+ threadKey?: string
225
+ ): Promise<void> {
226
+ const thread = createLangChainThreadManager({
227
+ redis,
228
+ threadId,
229
+ key: threadKey,
230
+ });
231
+ await thread.saveState(state);
232
+ },
198
233
  };
199
234
 
200
235
  function createActivities<S extends string = "">(
201
236
  scope?: S
202
237
  ): LangChainThreadOps<S> {
203
238
  const prefix = scope
204
- ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
205
- : ADAPTER_PREFIX;
239
+ ? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
240
+ : ADAPTER_ID;
206
241
  const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
207
242
  return Object.fromEntries(
208
243
  Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public adapter identity for the LangChain thread adapter.
3
+ *
4
+ * This value is wire format — it appears as the prefix for Temporal
5
+ * activity names (e.g. `langChainCodingAgentInitializeThread`) and must
6
+ * never change, since renaming it would orphan existing persisted
7
+ * threads and break in-flight workflows.
8
+ *
9
+ * Re-exported from `zeitlich/adapters/thread/langchain` so downstream
10
+ * consumers can use the exact same literal the adapter uses internally,
11
+ * typed as the narrow string literal `"langChain"`.
12
+ */
13
+ export const ADAPTER_ID = "langChain" as const;
14
+
15
+ /** Narrow string-literal type for {@link ADAPTER_ID}. */
16
+ export type AdapterId = typeof ADAPTER_ID;