zeitlich 0.2.45 → 0.2.47

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 (109) hide show
  1. package/README.md +137 -11
  2. package/dist/{activities-Coafq5zr.d.cts → activities-CPwKoUlD.d.cts} +22 -2
  3. package/dist/{activities-CrN-ghLo.d.ts → activities-DlaBxNID.d.ts} +22 -2
  4. package/dist/adapters/thread/anthropic/index.cjs +276 -71
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +62 -8
  7. package/dist/adapters/thread/anthropic/index.d.ts +62 -8
  8. package/dist/adapters/thread/anthropic/index.js +275 -72
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +38 -20
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +38 -20
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.cjs +171 -69
  17. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  18. package/dist/adapters/thread/google-genai/index.d.cts +6 -4
  19. package/dist/adapters/thread/google-genai/index.d.ts +6 -4
  20. package/dist/adapters/thread/google-genai/index.js +171 -69
  21. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/workflow.cjs +38 -20
  23. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/workflow.d.cts +7 -4
  25. package/dist/adapters/thread/google-genai/workflow.d.ts +7 -4
  26. package/dist/adapters/thread/google-genai/workflow.js +38 -20
  27. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  28. package/dist/adapters/thread/langchain/index.cjs +170 -66
  29. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  30. package/dist/adapters/thread/langchain/index.d.cts +19 -4
  31. package/dist/adapters/thread/langchain/index.d.ts +19 -4
  32. package/dist/adapters/thread/langchain/index.js +170 -66
  33. package/dist/adapters/thread/langchain/index.js.map +1 -1
  34. package/dist/adapters/thread/langchain/workflow.cjs +38 -20
  35. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  36. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  37. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  38. package/dist/adapters/thread/langchain/workflow.js +38 -20
  39. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  40. package/dist/cold-store-BDgJpwLI.d.ts +114 -0
  41. package/dist/cold-store-Z2wvK2cV.d.cts +114 -0
  42. package/dist/index.cjs +440 -67
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +150 -8
  45. package/dist/index.d.ts +150 -8
  46. package/dist/index.js +432 -68
  47. package/dist/index.js.map +1 -1
  48. package/dist/proxy-CDh3Rsa7.d.cts +40 -0
  49. package/dist/proxy-Du8ggERu.d.ts +40 -0
  50. package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-BjoYYXgd.d.cts} +8 -2
  51. package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-D8zKNFZ9.d.cts} +8 -2
  52. package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-DtHYws2F.d.ts} +8 -2
  53. package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-Dw96FKH1.d.ts} +8 -2
  54. package/dist/{types-C66-BVBr.d.cts → types-BMJrsHo0.d.cts} +17 -1
  55. package/dist/{types-BkX4HLzi.d.ts → types-CtdOquo3.d.ts} +17 -1
  56. package/dist/{types-CdALEF3z.d.cts → types-DNEl5uxQ.d.cts} +38 -0
  57. package/dist/{types-ChAy_jSP.d.ts → types-qQVZfhoT.d.ts} +38 -0
  58. package/dist/{workflow-DMmiaw6w.d.cts → workflow-BH9ImDGq.d.cts} +48 -2
  59. package/dist/{workflow-BwT5EybR.d.ts → workflow-Cdw3-RNB.d.ts} +48 -2
  60. package/dist/workflow.cjs +47 -4
  61. package/dist/workflow.cjs.map +1 -1
  62. package/dist/workflow.d.cts +2 -2
  63. package/dist/workflow.d.ts +2 -2
  64. package/dist/workflow.js +47 -5
  65. package/dist/workflow.js.map +1 -1
  66. package/package.json +14 -3
  67. package/src/adapters/thread/anthropic/activities.ts +82 -39
  68. package/src/adapters/thread/anthropic/index.ts +8 -0
  69. package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
  70. package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
  71. package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
  72. package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
  73. package/src/adapters/thread/anthropic/proxy.ts +1 -0
  74. package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
  75. package/src/adapters/thread/google-genai/activities.ts +64 -40
  76. package/src/adapters/thread/google-genai/proxy.ts +1 -0
  77. package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
  78. package/src/adapters/thread/langchain/activities.ts +63 -36
  79. package/src/adapters/thread/langchain/proxy.ts +1 -0
  80. package/src/adapters/thread/langchain/thread-manager.ts +9 -1
  81. package/src/index.ts +21 -2
  82. package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
  83. package/src/lib/session/session.integration.test.ts +138 -0
  84. package/src/lib/session/session.ts +29 -0
  85. package/src/lib/session/types.ts +22 -0
  86. package/src/lib/subagent/define.ts +1 -0
  87. package/src/lib/subagent/handler.ts +11 -2
  88. package/src/lib/subagent/subagent.integration.test.ts +139 -0
  89. package/src/lib/subagent/types.ts +16 -0
  90. package/src/lib/thread/cold-store.test.ts +221 -0
  91. package/src/lib/thread/cold-store.ts +269 -0
  92. package/src/lib/thread/index.ts +32 -0
  93. package/src/lib/thread/keys.ts +20 -0
  94. package/src/lib/thread/manager.ts +16 -27
  95. package/src/lib/thread/proxy.ts +79 -27
  96. package/src/lib/thread/snapshot.test.ts +443 -0
  97. package/src/lib/thread/snapshot.ts +163 -0
  98. package/src/lib/thread/test-utils.ts +228 -0
  99. package/src/lib/thread/tiered.test.ts +281 -0
  100. package/src/lib/thread/tiered.ts +135 -0
  101. package/src/lib/thread/types.ts +16 -0
  102. package/src/tools/edit/handler.test.ts +177 -0
  103. package/src/tools/edit/handler.ts +249 -47
  104. package/src/tools/edit/tool.ts +40 -0
  105. package/src/tools/task-create/handler.ts +1 -1
  106. package/src/tools/task-update/handler.ts +1 -1
  107. package/src/workflow.ts +2 -2
  108. package/dist/proxy-Bf7uI-Hw.d.cts +0 -24
  109. package/dist/proxy-COqA95FW.d.ts +0 -24
@@ -0,0 +1,163 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+
3
+ export interface AnthropicPromptCacheOptions {
4
+ /** TTL for the cache checkpoint. Defaults to 5m. */
5
+ ttl?: Anthropic.Messages.CacheControlEphemeral["ttl"];
6
+ /** Claude models support at most 4 cache breakpoints per request. */
7
+ maxBreakpoints?: number;
8
+ }
9
+
10
+ export type AnthropicPromptCacheConfig = boolean | AnthropicPromptCacheOptions;
11
+
12
+ interface PromptCachePayload {
13
+ messages: Anthropic.Messages.MessageParam[];
14
+ system?: string | Anthropic.Messages.TextBlockParam[];
15
+ tools?: Anthropic.Messages.Tool[];
16
+ }
17
+
18
+ type CacheControl = Anthropic.Messages.CacheControlEphemeral;
19
+ type CacheableRecord = Record<string, unknown> & {
20
+ cache_control?: CacheControl | null;
21
+ };
22
+
23
+ const DEFAULT_MAX_CACHE_BREAKPOINTS = 4;
24
+ const UNCACHEABLE_BLOCK_TYPES = new Set(["thinking", "redacted_thinking"]);
25
+
26
+ /**
27
+ * Resolve model-invoker prompt-cache config. Undefined means the default:
28
+ * enabled with an explicit 5 minute TTL.
29
+ */
30
+ export function resolvePromptCacheOptions(
31
+ promptCache?: AnthropicPromptCacheConfig
32
+ ): AnthropicPromptCacheOptions | undefined {
33
+ if (promptCache === false) return undefined;
34
+ if (promptCache === true || promptCache === undefined) return {};
35
+ return promptCache;
36
+ }
37
+
38
+ /**
39
+ * Add an explicit `cache_control` marker to the final cacheable message block.
40
+ *
41
+ * This intentionally uses block-level cache control rather than Anthropic's
42
+ * top-level automatic `cache_control` field because Amazon Bedrock does not
43
+ * support the top-level form. The block-level shape is accepted by both the
44
+ * Anthropic Messages API and Bedrock InvokeModel for Anthropic Claude models.
45
+ */
46
+ export function addPromptCacheControl<TPayload extends PromptCachePayload>(
47
+ payload: TPayload,
48
+ options: AnthropicPromptCacheOptions = {}
49
+ ): TPayload {
50
+ const maxBreakpoints =
51
+ options.maxBreakpoints ?? DEFAULT_MAX_CACHE_BREAKPOINTS;
52
+ if (maxBreakpoints <= 0) return payload;
53
+
54
+ if (countCacheControls(payload) >= maxBreakpoints) return payload;
55
+
56
+ const cacheControl: CacheControl = {
57
+ type: "ephemeral",
58
+ ttl: options.ttl ?? "5m",
59
+ };
60
+ const messages = addCacheControlToLastMessageBlock(
61
+ payload.messages,
62
+ cacheControl
63
+ );
64
+
65
+ if (messages === payload.messages) return payload;
66
+ return { ...payload, messages };
67
+ }
68
+
69
+ function addCacheControlToLastMessageBlock(
70
+ messages: Anthropic.Messages.MessageParam[],
71
+ cacheControl: CacheControl
72
+ ): Anthropic.Messages.MessageParam[] {
73
+ for (
74
+ let messageIndex = messages.length - 1;
75
+ messageIndex >= 0;
76
+ messageIndex--
77
+ ) {
78
+ const message = messages[messageIndex];
79
+ if (!message) continue;
80
+
81
+ if (typeof message.content === "string") {
82
+ if (message.content.length === 0) continue;
83
+ return replaceMessage(messages, messageIndex, {
84
+ ...message,
85
+ content: [
86
+ { type: "text", text: message.content, cache_control: cacheControl },
87
+ ],
88
+ });
89
+ }
90
+
91
+ if (!Array.isArray(message.content)) continue;
92
+
93
+ for (
94
+ let blockIndex = message.content.length - 1;
95
+ blockIndex >= 0;
96
+ blockIndex--
97
+ ) {
98
+ const block = message.content[blockIndex];
99
+ if (!isCacheableContentBlock(block)) continue;
100
+ if (hasCacheControl(block)) return messages;
101
+
102
+ const content = [...message.content];
103
+ content[blockIndex] = {
104
+ ...(block as Record<string, unknown>),
105
+ cache_control: cacheControl,
106
+ } as Anthropic.Messages.ContentBlockParam;
107
+ return replaceMessage(messages, messageIndex, { ...message, content });
108
+ }
109
+ }
110
+
111
+ return messages;
112
+ }
113
+
114
+ function replaceMessage(
115
+ messages: Anthropic.Messages.MessageParam[],
116
+ index: number,
117
+ message: Anthropic.Messages.MessageParam
118
+ ): Anthropic.Messages.MessageParam[] {
119
+ const next = [...messages];
120
+ next[index] = message;
121
+ return next;
122
+ }
123
+
124
+ function isCacheableContentBlock(
125
+ block: Anthropic.Messages.ContentBlockParam | undefined
126
+ ): block is Anthropic.Messages.ContentBlockParam & CacheableRecord {
127
+ if (!isRecord(block)) return false;
128
+ const type = typeof block.type === "string" ? block.type : undefined;
129
+ if (type && UNCACHEABLE_BLOCK_TYPES.has(type)) return false;
130
+ if (type === "text" && block.text === "") return false;
131
+ return true;
132
+ }
133
+
134
+ function countCacheControls(payload: PromptCachePayload): number {
135
+ let count = 0;
136
+
137
+ for (const tool of payload.tools ?? []) {
138
+ if (hasCacheControl(tool)) count++;
139
+ }
140
+
141
+ if (Array.isArray(payload.system)) {
142
+ for (const block of payload.system) {
143
+ if (hasCacheControl(block)) count++;
144
+ }
145
+ }
146
+
147
+ for (const message of payload.messages) {
148
+ if (!Array.isArray(message.content)) continue;
149
+ for (const block of message.content) {
150
+ if (hasCacheControl(block)) count++;
151
+ }
152
+ }
153
+
154
+ return count;
155
+ }
156
+
157
+ function hasCacheControl(value: unknown): value is CacheableRecord {
158
+ return isRecord(value) && value.cache_control != null;
159
+ }
160
+
161
+ function isRecord(value: unknown): value is Record<string, unknown> {
162
+ return typeof value === "object" && value !== null && !Array.isArray(value);
163
+ }
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
25
  import { ADAPTER_ID } from "./adapter-id";
26
26
 
27
27
  export { ADAPTER_ID, type AdapterId } from "./adapter-id";
28
+ export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
28
29
 
29
30
  export function proxyAnthropicThreadOps(
30
31
  scope?: string,
@@ -35,6 +35,12 @@ export interface AnthropicThreadManagerConfig {
35
35
  /** Thread key, defaults to 'messages' */
36
36
  key?: string;
37
37
  hooks?: AnthropicThreadManagerHooks;
38
+ /**
39
+ * Override the default thread TTL (90 days). When pairing the
40
+ * adapter with a durable cold tier, a shorter TTL (hours) is
41
+ * typically more appropriate.
42
+ */
43
+ ttlSeconds?: number;
38
44
  }
39
45
 
40
46
  /** Prepared payload ready to send to the Anthropic API */
@@ -57,7 +63,8 @@ export interface AnthropicThreadManager extends ProviderThreadManager<
57
63
  prepareForInvocation(): Promise<AnthropicInvocationPayload>;
58
64
  }
59
65
 
60
- function storedMessageId(msg: StoredMessage): string {
66
+ /** Extract the unique id from a {@link StoredMessage}. */
67
+ export function storedMessageId(msg: StoredMessage): string {
61
68
  return msg.id;
62
69
  }
63
70
 
@@ -113,6 +120,7 @@ export function createAnthropicThreadManager(
113
120
  threadId: config.threadId,
114
121
  key: config.key,
115
122
  idOf: storedMessageId,
123
+ ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
116
124
  };
117
125
 
118
126
  const base = createThreadManager(baseConfig);
@@ -13,11 +13,15 @@ import type {
13
13
  ScopedPrefix,
14
14
  } from "../../../lib/session/types";
15
15
  import type { ModelInvoker } from "../../../lib/model";
16
+ import { createTieredThreadManager } from "../../../lib/thread/tiered";
17
+ import type { ColdThreadStore } from "../../../lib/thread/cold-store";
16
18
  import {
17
19
  createGoogleGenAIThreadManager,
20
+ storedContentId,
18
21
  type GoogleGenAIContent,
19
22
  type GoogleGenAISystemContent,
20
23
  type GoogleGenAIThreadManagerHooks,
24
+ type StoredContent,
21
25
  } from "./thread-manager";
22
26
  import { createGoogleGenAIModelInvoker } from "./model-invoker";
23
27
  import { ADAPTER_ID } from "./adapter-id";
@@ -34,6 +38,19 @@ export interface GoogleGenAIAdapterConfig {
34
38
  /** Default model name (e.g. 'gemini-2.5-flash'). If omitted, use `createModelInvoker()` */
35
39
  model?: string;
36
40
  hooks?: GoogleGenAIThreadManagerHooks;
41
+ /**
42
+ * Optional durable cold tier (e.g. S3, R2, GCS). When provided,
43
+ * the session hydrates the thread on entry (`continue`/`fork`) and
44
+ * flushes it on every exit path. When omitted, the adapter is
45
+ * Redis-only and `hydrateThread`/`flushThread` activities are no-ops.
46
+ */
47
+ coldStore?: ColdThreadStore;
48
+ /**
49
+ * Override the default Redis TTL (90 days). When pairing the
50
+ * adapter with a `coldStore`, a shorter TTL (hours) is typically
51
+ * more appropriate.
52
+ */
53
+ ttlSeconds?: number;
37
54
  }
38
55
 
39
56
  /**
@@ -140,16 +157,34 @@ export function createGoogleGenAIAdapter(
140
157
  ): GoogleGenAIAdapter {
141
158
  const { redis } = config;
142
159
 
160
+ const baseExtras = {
161
+ ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
162
+ };
163
+
164
+ const makeProviderThread = (threadId: string, threadKey?: string) =>
165
+ createGoogleGenAIThreadManager({
166
+ redis,
167
+ threadId,
168
+ key: threadKey,
169
+ ...baseExtras,
170
+ });
171
+
172
+ const makeTieredBase = (threadId: string, threadKey?: string) =>
173
+ createTieredThreadManager<StoredContent>({
174
+ redis,
175
+ threadId,
176
+ key: threadKey,
177
+ idOf: storedContentId,
178
+ ...baseExtras,
179
+ ...(config.coldStore && { coldStore: config.coldStore }),
180
+ });
181
+
143
182
  const threadOps: ThreadOps<GoogleGenAIContent> = {
144
183
  async initializeThread(
145
184
  threadId: string,
146
185
  threadKey?: string
147
186
  ): Promise<void> {
148
- const thread = createGoogleGenAIThreadManager({
149
- redis,
150
- threadId,
151
- key: threadKey,
152
- });
187
+ const thread = makeProviderThread(threadId, threadKey);
153
188
  await thread.initialize();
154
189
  },
155
190
 
@@ -159,11 +194,7 @@ export function createGoogleGenAIAdapter(
159
194
  content: GoogleGenAIContent,
160
195
  threadKey?: string
161
196
  ): Promise<void> {
162
- const thread = createGoogleGenAIThreadManager({
163
- redis,
164
- threadId,
165
- key: threadKey,
166
- });
197
+ const thread = makeProviderThread(threadId, threadKey);
167
198
  await thread.appendUserMessage(id, content);
168
199
  },
169
200
 
@@ -173,21 +204,13 @@ export function createGoogleGenAIAdapter(
173
204
  content: GoogleGenAISystemContent,
174
205
  threadKey?: string
175
206
  ): Promise<void> {
176
- const thread = createGoogleGenAIThreadManager({
177
- redis,
178
- threadId,
179
- key: threadKey,
180
- });
207
+ const thread = makeProviderThread(threadId, threadKey);
181
208
  await thread.appendSystemMessage(id, content);
182
209
  },
183
210
 
184
211
  async appendToolResult(id: string, cfg: ToolResultConfig): Promise<void> {
185
212
  const { threadId, threadKey, toolCallId, toolName, content } = cfg;
186
- const thread = createGoogleGenAIThreadManager({
187
- redis,
188
- threadId,
189
- key: threadKey,
190
- });
213
+ const thread = makeProviderThread(threadId, threadKey);
191
214
  await thread.appendToolResult(
192
215
  id,
193
216
  toolCallId,
@@ -202,11 +225,7 @@ export function createGoogleGenAIAdapter(
202
225
  message: Content,
203
226
  threadKey?: string
204
227
  ): Promise<void> {
205
- const thread = createGoogleGenAIThreadManager({
206
- redis,
207
- threadId,
208
- key: threadKey,
209
- });
228
+ const thread = makeProviderThread(threadId, threadKey);
210
229
  await thread.appendModelContent(id, message.parts ?? []);
211
230
  },
212
231
 
@@ -220,6 +239,7 @@ export function createGoogleGenAIAdapter(
220
239
  threadId: sourceThreadId,
221
240
  key: threadKey,
222
241
  hooks: config.hooks,
242
+ ...baseExtras,
223
243
  });
224
244
  await thread.fork(targetThreadId);
225
245
  },
@@ -229,11 +249,7 @@ export function createGoogleGenAIAdapter(
229
249
  messageId: string,
230
250
  threadKey?: string,
231
251
  ): Promise<void> {
232
- const thread = createGoogleGenAIThreadManager({
233
- redis,
234
- threadId,
235
- key: threadKey,
236
- });
252
+ const thread = makeProviderThread(threadId, threadKey);
237
253
  await thread.truncateFromId(messageId);
238
254
  },
239
255
 
@@ -241,11 +257,7 @@ export function createGoogleGenAIAdapter(
241
257
  threadId: string,
242
258
  threadKey?: string
243
259
  ): Promise<PersistedThreadState | null> {
244
- const thread = createGoogleGenAIThreadManager({
245
- redis,
246
- threadId,
247
- key: threadKey,
248
- });
260
+ const thread = makeProviderThread(threadId, threadKey);
249
261
  return thread.loadState();
250
262
  },
251
263
 
@@ -254,13 +266,25 @@ export function createGoogleGenAIAdapter(
254
266
  state: PersistedThreadState,
255
267
  threadKey?: string
256
268
  ): Promise<void> {
257
- const thread = createGoogleGenAIThreadManager({
258
- redis,
259
- threadId,
260
- key: threadKey,
261
- });
269
+ const thread = makeProviderThread(threadId, threadKey);
262
270
  await thread.saveState(state);
263
271
  },
272
+
273
+ async hydrateThread(
274
+ threadId: string,
275
+ threadKey?: string
276
+ ): Promise<void> {
277
+ if (!config.coldStore) return;
278
+ await makeTieredBase(threadId, threadKey).hydrate();
279
+ },
280
+
281
+ async flushThread(
282
+ threadId: string,
283
+ threadKey?: string
284
+ ): Promise<void> {
285
+ if (!config.coldStore) return;
286
+ await makeTieredBase(threadId, threadKey).flush();
287
+ },
264
288
  };
265
289
 
266
290
  function createActivities<S extends string = "">(
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
25
  import { ADAPTER_ID } from "./adapter-id";
26
26
 
27
27
  export { ADAPTER_ID, type AdapterId } from "./adapter-id";
28
+ export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
28
29
 
29
30
  export function proxyGoogleGenAIThreadOps(
30
31
  scope?: string,
@@ -31,6 +31,12 @@ export interface GoogleGenAIThreadManagerConfig {
31
31
  /** Thread key, defaults to 'messages' */
32
32
  key?: string;
33
33
  hooks?: GoogleGenAIThreadManagerHooks;
34
+ /**
35
+ * Override the default thread TTL (90 days). When pairing the
36
+ * adapter with a durable cold tier, a shorter TTL (hours) is
37
+ * typically more appropriate.
38
+ */
39
+ ttlSeconds?: number;
34
40
  }
35
41
 
36
42
  /** Prepared payload ready to send to the Google GenAI API */
@@ -50,7 +56,8 @@ export interface GoogleGenAIThreadManager extends ProviderThreadManager<
50
56
  prepareForInvocation(): Promise<GoogleGenAIInvocationPayload>;
51
57
  }
52
58
 
53
- function storedContentId(msg: StoredContent): string {
59
+ /** Extract the unique id from a {@link StoredContent}. */
60
+ export function storedContentId(msg: StoredContent): string {
54
61
  return msg.id;
55
62
  }
56
63
 
@@ -103,6 +110,7 @@ export function createGoogleGenAIThreadManager(
103
110
  threadId: config.threadId,
104
111
  key: config.key,
105
112
  idOf: storedContentId,
113
+ ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
106
114
  };
107
115
 
108
116
  const base = createThreadManager(baseConfig);
@@ -13,10 +13,13 @@ import type {
13
13
  ScopedPrefix,
14
14
  } from "../../../lib/session/types";
15
15
  import type { ModelInvoker } from "../../../lib/model";
16
+ import { createTieredThreadManager } from "../../../lib/thread/tiered";
17
+ import type { ColdThreadStore } from "../../../lib/thread/cold-store";
16
18
  import type { StoredMessage } from "@langchain/core/messages";
17
19
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
18
20
  import {
19
21
  createLangChainThreadManager,
22
+ storedMessageId,
20
23
  type LangChainContent,
21
24
  type LangChainSystemContent,
22
25
  type LangChainThreadManagerHooks,
@@ -35,6 +38,19 @@ export interface LangChainAdapterConfig {
35
38
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
39
  model?: BaseChatModel<any>;
37
40
  hooks?: LangChainThreadManagerHooks;
41
+ /**
42
+ * Optional durable cold tier (e.g. S3, R2, GCS). When provided,
43
+ * the session hydrates the thread on entry (`continue`/`fork`) and
44
+ * flushes it on every exit path. When omitted, the adapter is
45
+ * Redis-only and `hydrateThread`/`flushThread` activities are no-ops.
46
+ */
47
+ coldStore?: ColdThreadStore;
48
+ /**
49
+ * Override the default Redis TTL (90 days). When pairing the
50
+ * adapter with a `coldStore`, a shorter TTL (hours) is typically
51
+ * more appropriate.
52
+ */
53
+ ttlSeconds?: number;
38
54
  }
39
55
 
40
56
  /**
@@ -117,16 +133,34 @@ export function createLangChainAdapter(
117
133
  ): LangChainAdapter {
118
134
  const { redis } = config;
119
135
 
136
+ const baseExtras = {
137
+ ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
138
+ };
139
+
140
+ const makeProviderThread = (threadId: string, threadKey?: string) =>
141
+ createLangChainThreadManager({
142
+ redis,
143
+ threadId,
144
+ key: threadKey,
145
+ ...baseExtras,
146
+ });
147
+
148
+ const makeTieredBase = (threadId: string, threadKey?: string) =>
149
+ createTieredThreadManager<StoredMessage>({
150
+ redis,
151
+ threadId,
152
+ key: threadKey,
153
+ idOf: storedMessageId,
154
+ ...baseExtras,
155
+ ...(config.coldStore && { coldStore: config.coldStore }),
156
+ });
157
+
120
158
  const threadOps: ThreadOps<LangChainContent> = {
121
159
  async initializeThread(
122
160
  threadId: string,
123
161
  threadKey?: string
124
162
  ): Promise<void> {
125
- const thread = createLangChainThreadManager({
126
- redis,
127
- threadId,
128
- key: threadKey,
129
- });
163
+ const thread = makeProviderThread(threadId, threadKey);
130
164
  await thread.initialize();
131
165
  },
132
166
 
@@ -136,11 +170,7 @@ export function createLangChainAdapter(
136
170
  content: LangChainContent,
137
171
  threadKey?: string
138
172
  ): Promise<void> {
139
- const thread = createLangChainThreadManager({
140
- redis,
141
- threadId,
142
- key: threadKey,
143
- });
173
+ const thread = makeProviderThread(threadId, threadKey);
144
174
  await thread.appendUserMessage(id, content);
145
175
  },
146
176
 
@@ -150,21 +180,13 @@ export function createLangChainAdapter(
150
180
  content: LangChainSystemContent,
151
181
  threadKey?: string
152
182
  ): Promise<void> {
153
- const thread = createLangChainThreadManager({
154
- redis,
155
- threadId,
156
- key: threadKey,
157
- });
183
+ const thread = makeProviderThread(threadId, threadKey);
158
184
  await thread.appendSystemMessage(id, content);
159
185
  },
160
186
 
161
187
  async appendToolResult(id: string, cfg: ToolResultConfig): Promise<void> {
162
188
  const { threadId, threadKey, toolCallId, content } = cfg;
163
- const thread = createLangChainThreadManager({
164
- redis,
165
- threadId,
166
- key: threadKey,
167
- });
189
+ const thread = makeProviderThread(threadId, threadKey);
168
190
  await thread.appendToolResult(id, toolCallId, "", content);
169
191
  },
170
192
 
@@ -174,11 +196,7 @@ export function createLangChainAdapter(
174
196
  message: StoredMessage,
175
197
  threadKey?: string
176
198
  ): Promise<void> {
177
- const thread = createLangChainThreadManager({
178
- redis,
179
- threadId,
180
- key: threadKey,
181
- });
199
+ const thread = makeProviderThread(threadId, threadKey);
182
200
  const patched = { ...message, data: { ...message.data, id } };
183
201
  await thread.append([patched]);
184
202
  },
@@ -193,6 +211,7 @@ export function createLangChainAdapter(
193
211
  threadId: sourceThreadId,
194
212
  key: threadKey,
195
213
  hooks: config.hooks,
214
+ ...baseExtras,
196
215
  });
197
216
  await thread.fork(targetThreadId);
198
217
  },
@@ -202,7 +221,7 @@ export function createLangChainAdapter(
202
221
  messageId: string,
203
222
  threadKey?: string,
204
223
  ): Promise<void> {
205
- const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
224
+ const thread = makeProviderThread(threadId, threadKey);
206
225
  await thread.truncateFromId(messageId);
207
226
  },
208
227
 
@@ -210,11 +229,7 @@ export function createLangChainAdapter(
210
229
  threadId: string,
211
230
  threadKey?: string
212
231
  ): Promise<PersistedThreadState | null> {
213
- const thread = createLangChainThreadManager({
214
- redis,
215
- threadId,
216
- key: threadKey,
217
- });
232
+ const thread = makeProviderThread(threadId, threadKey);
218
233
  return thread.loadState();
219
234
  },
220
235
 
@@ -223,13 +238,25 @@ export function createLangChainAdapter(
223
238
  state: PersistedThreadState,
224
239
  threadKey?: string
225
240
  ): Promise<void> {
226
- const thread = createLangChainThreadManager({
227
- redis,
228
- threadId,
229
- key: threadKey,
230
- });
241
+ const thread = makeProviderThread(threadId, threadKey);
231
242
  await thread.saveState(state);
232
243
  },
244
+
245
+ async hydrateThread(
246
+ threadId: string,
247
+ threadKey?: string
248
+ ): Promise<void> {
249
+ if (!config.coldStore) return;
250
+ await makeTieredBase(threadId, threadKey).hydrate();
251
+ },
252
+
253
+ async flushThread(
254
+ threadId: string,
255
+ threadKey?: string
256
+ ): Promise<void> {
257
+ if (!config.coldStore) return;
258
+ await makeTieredBase(threadId, threadKey).flush();
259
+ },
233
260
  };
234
261
 
235
262
  function createActivities<S extends string = "">(
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
25
  import { ADAPTER_ID } from "./adapter-id";
26
26
 
27
27
  export { ADAPTER_ID, type AdapterId } from "./adapter-id";
28
+ export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
28
29
 
29
30
  export function proxyLangChainThreadOps(
30
31
  scope?: string,
@@ -34,6 +34,12 @@ export interface LangChainThreadManagerConfig {
34
34
  /** Thread key, defaults to 'messages' */
35
35
  key?: string;
36
36
  hooks?: LangChainThreadManagerHooks;
37
+ /**
38
+ * Override the default thread TTL (90 days). When pairing the
39
+ * adapter with a durable cold tier, a shorter TTL (hours) is
40
+ * typically more appropriate.
41
+ */
42
+ ttlSeconds?: number;
37
43
  }
38
44
 
39
45
  /** Prepared payload ready to send to a LangChain chat model */
@@ -52,7 +58,8 @@ export interface LangChainThreadManager extends ProviderThreadManager<
52
58
  prepareForInvocation(): Promise<LangChainInvocationPayload>;
53
59
  }
54
60
 
55
- function storedMessageId(msg: StoredMessage): string {
61
+ /** Extract the unique id from a LangChain {@link StoredMessage}. */
62
+ export function storedMessageId(msg: StoredMessage): string {
56
63
  if (msg.type === "tool" && msg.data.tool_call_id) {
57
64
  return msg.data.tool_call_id;
58
65
  }
@@ -77,6 +84,7 @@ export function createLangChainThreadManager(
77
84
  threadId: config.threadId,
78
85
  key: config.key,
79
86
  idOf: storedMessageId,
87
+ ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
80
88
  };
81
89
 
82
90
  const base = createThreadManager(baseConfig);
package/src/index.ts CHANGED
@@ -33,11 +33,30 @@ export * from "./workflow";
33
33
  export { FileSystemSkillProvider } from "./lib/skills/fs-provider";
34
34
 
35
35
  // Thread manager (generic, framework-agnostic)
36
- export { createThreadManager } from "./lib/thread";
36
+ export {
37
+ createThreadManager,
38
+ createTieredThreadManager,
39
+ createS3ColdStore,
40
+ encodeSnapshot,
41
+ applySnapshot,
42
+ clearHotTier,
43
+ getThreadStateKey,
44
+ getThreadDedupKey,
45
+ } from "./lib/thread";
37
46
  export type {
38
47
  BaseThreadManager,
39
48
  ProviderThreadManager,
40
49
  ThreadManagerConfig,
50
+ TieredThreadManager,
51
+ TieredThreadManagerConfig,
52
+ FlushOptions,
53
+ ColdThreadStore,
54
+ ThreadSnapshot,
55
+ S3LikeClient,
56
+ S3ColdStoreConfig,
57
+ EncodeSnapshotConfig,
58
+ ApplySnapshotConfig,
59
+ ClearHotTierConfig,
41
60
  } from "./lib/thread";
42
61
 
43
62
  // Model invoker contract (framework-agnostic)
@@ -74,7 +93,7 @@ export type { VirtualFsContext } from "./lib/virtual-fs/types";
74
93
  // Tool handlers (activity implementations)
75
94
  // Wrap sandbox handlers with withSandbox(manager, handler) at registration time
76
95
  export { bashHandler } from "./tools/bash/handler";
77
- export { editHandler } from "./tools/edit/handler";
96
+ export { editHandler, multiEditHandler } from "./tools/edit/handler";
78
97
  export { globHandler } from "./tools/glob/handler";
79
98
  export { readFileHandler } from "./tools/read-file/handler";
80
99
  export { writeFileHandler } from "./tools/write-file/handler";