zeitlich 0.2.49 → 0.2.51

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 (127) hide show
  1. package/README.md +26 -23
  2. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  3. package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
  4. package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
  5. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  6. package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
  7. package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
  8. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  9. package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
  10. package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
  11. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  12. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  13. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  14. package/dist/adapters/thread/anthropic/index.cjs +60 -55
  15. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  16. package/dist/adapters/thread/anthropic/index.d.cts +20 -15
  17. package/dist/adapters/thread/anthropic/index.d.ts +20 -15
  18. package/dist/adapters/thread/anthropic/index.js +60 -55
  19. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  20. package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
  21. package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
  22. package/dist/adapters/thread/google-genai/index.cjs +135 -66
  23. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/index.d.cts +200 -26
  25. package/dist/adapters/thread/google-genai/index.d.ts +200 -26
  26. package/dist/adapters/thread/google-genai/index.js +135 -66
  27. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  28. package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
  29. package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
  30. package/dist/adapters/thread/langchain/index.cjs +67 -55
  31. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  32. package/dist/adapters/thread/langchain/index.d.cts +20 -15
  33. package/dist/adapters/thread/langchain/index.d.ts +20 -15
  34. package/dist/adapters/thread/langchain/index.js +67 -55
  35. package/dist/adapters/thread/langchain/index.js.map +1 -1
  36. package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
  37. package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
  38. package/dist/{cold-store-DKMAO1Dd.d.ts → cold-store-DyHodfAB.d.ts} +1 -1
  39. package/dist/{cold-store-CkWoNtMh.d.cts → cold-store-YOx9nmgR.d.cts} +1 -1
  40. package/dist/index.cjs +15050 -420
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +79 -83
  43. package/dist/index.d.ts +79 -83
  44. package/dist/index.js +15051 -417
  45. package/dist/index.js.map +1 -1
  46. package/dist/{proxy-B7CWEV-T.d.cts → proxy-2htgGQrc.d.cts} +1 -1
  47. package/dist/{proxy-ByFHMVRX.d.ts → proxy-CmiTP4pp.d.ts} +1 -1
  48. package/dist/{thread-manager-nK-WcFzM.d.ts → thread-manager-BJ5pz5Cx.d.cts} +6 -7
  49. package/dist/{thread-manager-7AW4rhfu.d.ts → thread-manager-BQAbrYXH.d.cts} +6 -7
  50. package/dist/{thread-manager-Cibe0X5m.d.cts → thread-manager-CcvltOuq.d.ts} +6 -7
  51. package/dist/{thread-manager-B9rtMEVn.d.cts → thread-manager-DHAbncHX.d.ts} +6 -7
  52. package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
  53. package/dist/{types-XUUFvrJ9.d.cts → types-BjdqxKYp.d.cts} +709 -709
  54. package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.cts} +3 -3
  55. package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.ts} +3 -3
  56. package/dist/{types-DO4Tkwxo.d.ts → types-DEbkLA06.d.ts} +3 -3
  57. package/dist/{types-DeVNWqlb.d.ts → types-DiI7mZhI.d.ts} +709 -709
  58. package/dist/{types-BR-k7h0e.d.cts → types-N_LTWe4b.d.cts} +3 -3
  59. package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
  60. package/dist/{workflow-uhOIj9D-.d.ts → workflow-CcgD6EUB.d.cts} +34 -3
  61. package/dist/{workflow-KbGsxpfh.d.cts → workflow-DBjPOKBr.d.ts} +34 -3
  62. package/dist/workflow.cjs +15008 -377
  63. package/dist/workflow.cjs.map +1 -1
  64. package/dist/workflow.d.cts +3 -3
  65. package/dist/workflow.d.ts +3 -3
  66. package/dist/workflow.js +15009 -374
  67. package/dist/workflow.js.map +1 -1
  68. package/package.json +10 -37
  69. package/src/adapters/thread/anthropic/activities.test.ts +115 -0
  70. package/src/adapters/thread/anthropic/activities.ts +11 -19
  71. package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
  72. package/src/adapters/thread/anthropic/model-invoker.test.ts +54 -3
  73. package/src/adapters/thread/anthropic/model-invoker.ts +11 -1
  74. package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
  75. package/src/adapters/thread/anthropic/thread-manager.ts +3 -4
  76. package/src/adapters/thread/google-genai/activities.test.ts +162 -0
  77. package/src/adapters/thread/google-genai/activities.ts +38 -15
  78. package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
  79. package/src/adapters/thread/google-genai/model-invoker.test.ts +386 -0
  80. package/src/adapters/thread/google-genai/model-invoker.ts +118 -23
  81. package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
  82. package/src/adapters/thread/google-genai/thread-manager.ts +3 -4
  83. package/src/adapters/thread/langchain/activities.test.ts +88 -0
  84. package/src/adapters/thread/langchain/activities.ts +15 -12
  85. package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
  86. package/src/adapters/thread/langchain/model-invoker.test.ts +74 -0
  87. package/src/adapters/thread/langchain/model-invoker.ts +16 -3
  88. package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
  89. package/src/adapters/thread/langchain/thread-manager.ts +3 -4
  90. package/src/index.ts +2 -2
  91. package/src/lib/sandbox/capability-types.test.ts +2 -2
  92. package/src/lib/sandbox/manager.ts +2 -6
  93. package/src/lib/sandbox/sandbox.test.ts +1 -1
  94. package/src/lib/sandbox/types.ts +2 -2
  95. package/src/lib/session/session.integration.test.ts +92 -0
  96. package/src/lib/session/session.ts +23 -11
  97. package/src/lib/thread/keys.test.ts +9 -9
  98. package/src/lib/thread/keys.ts +1 -1
  99. package/src/lib/thread/manager.test.ts +24 -14
  100. package/src/lib/thread/manager.ts +19 -23
  101. package/src/lib/thread/snapshot.test.ts +51 -43
  102. package/src/lib/thread/snapshot.ts +54 -32
  103. package/src/lib/thread/test-utils.ts +106 -59
  104. package/src/lib/thread/tiered.test.ts +1 -1
  105. package/src/lib/thread/types.ts +2 -2
  106. package/src/lib/tool-router/router.integration.test.ts +44 -0
  107. package/src/lib/tool-router/router.ts +140 -32
  108. package/src/lib/workflow.ts +49 -0
  109. package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
  110. package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
  111. package/src/tools/bash/bash.test.ts +1 -1
  112. package/src/tools/edit/handler.test.ts +1 -1
  113. package/tsup.config.ts +2 -4
  114. package/dist/activities-7OcT_vdR.d.cts +0 -162
  115. package/dist/activities-zG_FBoY2.d.ts +0 -162
  116. package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
  117. package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
  118. package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
  119. package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
  120. package/dist/adapters/sandbox/inmemory/index.js +0 -211
  121. package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
  122. package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
  123. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
  124. package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
  125. package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
  126. package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
  127. package/dist/adapters/sandbox/inmemory/workflow.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -97,26 +97,6 @@
97
97
  "default": "./dist/adapters/thread/anthropic/workflow.js"
98
98
  }
99
99
  },
100
- "./adapters/sandbox/inmemory": {
101
- "import": {
102
- "types": "./dist/adapters/sandbox/inmemory/index.d.ts",
103
- "default": "./dist/adapters/sandbox/inmemory/index.js"
104
- },
105
- "require": {
106
- "types": "./dist/adapters/sandbox/inmemory/index.d.ts",
107
- "default": "./dist/adapters/sandbox/inmemory/index.js"
108
- }
109
- },
110
- "./adapters/sandbox/inmemory/workflow": {
111
- "import": {
112
- "types": "./dist/adapters/sandbox/inmemory/workflow.d.ts",
113
- "default": "./dist/adapters/sandbox/inmemory/workflow.js"
114
- },
115
- "require": {
116
- "types": "./dist/adapters/sandbox/inmemory/workflow.d.ts",
117
- "default": "./dist/adapters/sandbox/inmemory/workflow.js"
118
- }
119
- },
120
100
  "./adapters/sandbox/daytona": {
121
101
  "import": {
122
102
  "types": "./dist/adapters/sandbox/daytona/index.d.ts",
@@ -201,22 +181,22 @@
201
181
  "node": ">=18"
202
182
  },
203
183
  "devDependencies": {
204
- "@anthropic-ai/sdk": "^0.98.0",
184
+ "@anthropic-ai/sdk": "^0.100.1",
205
185
  "@aws-sdk/client-s3": "^3.1000.0",
206
186
  "@aws-sdk/lib-storage": "^3.1000.0",
207
- "@daytonaio/sdk": "^0.171.0",
187
+ "@daytonaio/sdk": "^0.184.0",
208
188
  "@e2b/code-interpreter": "^2.3.3",
209
189
  "@eslint/js": "^10.0.1",
210
- "@google/genai": "^1.44.0",
190
+ "@google/genai": "^2.7.0",
211
191
  "@langchain/core": "^1.1.48",
212
192
  "@temporalio/common": "^1.17.2",
213
193
  "@temporalio/envconfig": "^1.17.2",
214
194
  "@temporalio/worker": "^1.17.2",
215
195
  "@temporalio/workflow": "^1.17.2",
216
196
  "@types/node": "^25.3.3",
217
- "eslint": "^10.0.2",
197
+ "eslint": "^10.4.1",
218
198
  "husky": "^9.1.7",
219
- "just-bash": "^2.12.8",
199
+ "just-bash": "^3.0.1",
220
200
  "prettier": "^3.8.1",
221
201
  "release-please": "^17.3.0",
222
202
  "tsup": "^8.5.1",
@@ -225,19 +205,18 @@
225
205
  "vitest": "^4.0.18"
226
206
  },
227
207
  "peerDependencies": {
228
- "@anthropic-ai/sdk": ">=0.50.0",
208
+ "@anthropic-ai/sdk": ">=0.100.0",
229
209
  "@aws-sdk/client-s3": ">=3.700.0",
230
210
  "@aws-sdk/lib-storage": ">=3.700.0",
231
211
  "@daytonaio/sdk": ">=0.153.0",
232
212
  "@e2b/code-interpreter": "^2.3.3",
233
- "@google/genai": "^1.43.0",
213
+ "@google/genai": ">=2.5.0",
234
214
  "@langchain/core": ">=1.0.0",
235
215
  "@temporalio/common": ">=1.16.0 <2.0.0",
236
216
  "@temporalio/envconfig": ">=1.16.0 <2.0.0",
237
217
  "@temporalio/worker": ">=1.16.0 <2.0.0",
238
218
  "@temporalio/workflow": ">=1.16.0 <2.0.0",
239
- "ioredis": ">=5.0.0",
240
- "just-bash": ">=2.0.0"
219
+ "redis": ">=4.6.0"
241
220
  },
242
221
  "peerDependenciesMeta": {
243
222
  "@daytonaio/sdk": {
@@ -263,17 +242,11 @@
263
242
  },
264
243
  "@temporalio/worker": {
265
244
  "optional": true
266
- },
267
- "just-bash": {
268
- "optional": true
269
245
  }
270
246
  },
271
247
  "type": "module",
272
248
  "bugs": {
273
249
  "url": "https://github.com/bead-ai/zeitlich/issues"
274
250
  },
275
- "homepage": "https://github.com/bead-ai/zeitlich#readme",
276
- "dependencies": {
277
- "zod": "^4.3.6"
278
- }
251
+ "homepage": "https://github.com/bead-ai/zeitlich#readme"
279
252
  }
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type Anthropic from "@anthropic-ai/sdk";
3
+ import { createAnthropicAdapter } from "./activities";
4
+ import type { StoredMessage } from "./thread-manager";
5
+ import { THREAD_TTL_SECONDS } from "../../../lib/thread/keys";
6
+
7
+ function createMockRedis(stored: StoredMessage[]) {
8
+ return {
9
+ exists: vi.fn().mockResolvedValue(1),
10
+ lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
11
+ lTrim: vi.fn().mockResolvedValue("OK"),
12
+ del: vi.fn().mockResolvedValue(1),
13
+ set: vi.fn().mockResolvedValue("OK"),
14
+ rPush: vi.fn().mockResolvedValue(1),
15
+ expire: vi.fn().mockResolvedValue(1),
16
+ eval: vi.fn().mockResolvedValue(1),
17
+ };
18
+ }
19
+
20
+ function createMockClient() {
21
+ const finalMessage: Anthropic.Messages.Message = {
22
+ id: "msg-response",
23
+ type: "message",
24
+ role: "assistant",
25
+ container: null,
26
+ model: "claude-test",
27
+ content: [{ type: "text", text: "ok", citations: null }],
28
+ stop_details: null,
29
+ stop_reason: "end_turn",
30
+ stop_sequence: null,
31
+ usage: {
32
+ cache_creation: null,
33
+ cache_creation_input_tokens: null,
34
+ cache_read_input_tokens: null,
35
+ inference_geo: null,
36
+ input_tokens: 1,
37
+ output_tokens: 1,
38
+ server_tool_use: null,
39
+ service_tier: null,
40
+ output_tokens_details: null,
41
+ },
42
+ };
43
+ const stream = {
44
+ async *[Symbol.asyncIterator]() {},
45
+ finalMessage: vi.fn().mockResolvedValue(finalMessage),
46
+ };
47
+ return { messages: { stream: vi.fn().mockReturnValue(stream) } };
48
+ }
49
+
50
+ // Tail stored under the `assistantMessageId`, so the invoker's
51
+ // `truncateFromId` trims it and re-stamps the surviving list key's TTL.
52
+ const retriedThread: StoredMessage[] = [
53
+ { id: "msg-1", message: { role: "user", content: "hi" } },
54
+ { id: "assistant-1", message: { role: "assistant", content: "prior" } },
55
+ ];
56
+ const listKey = "messages:thread:thread-1";
57
+ const metaKey = "messages:meta:thread:thread-1";
58
+ const invokerCall = {
59
+ threadId: "thread-1",
60
+ assistantMessageId: "assistant-1",
61
+ state: { tools: [] } as never,
62
+ agentName: "TestAgent",
63
+ };
64
+
65
+ describe("createAnthropicAdapter — TTL propagation", () => {
66
+ it("forwards adapter ttlSeconds to a created invoker's writes", async () => {
67
+ const redis = createMockRedis(retriedThread);
68
+ const client = createMockClient();
69
+ const adapter = createAnthropicAdapter({
70
+ redis: redis as never,
71
+ client: client as never,
72
+ ttlSeconds: 3600,
73
+ });
74
+
75
+ await adapter.createModelInvoker("claude-test")(invokerCall);
76
+
77
+ expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
78
+ expect(redis.expire).not.toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
79
+ });
80
+
81
+ it("forwards adapter ttlSeconds to thread-op writes", async () => {
82
+ const redis = createMockRedis([]);
83
+ const client = createMockClient();
84
+ const adapter = createAnthropicAdapter({
85
+ redis: redis as never,
86
+ client: client as never,
87
+ ttlSeconds: 3600,
88
+ });
89
+ const acts = adapter.createActivities() as unknown as Record<
90
+ string,
91
+ (threadId: string, threadKey?: string) => Promise<void>
92
+ >;
93
+ const initialize = Object.entries(acts).find(([k]) =>
94
+ k.endsWith("InitializeThread")
95
+ )?.[1];
96
+ if (!initialize) throw new Error("initializeThread activity not found");
97
+
98
+ await initialize("thread-1");
99
+
100
+ expect(redis.set).toHaveBeenCalledWith(metaKey, "1", { EX: 3600 });
101
+ });
102
+
103
+ it("defaults to THREAD_TTL_SECONDS when adapter ttlSeconds is omitted", async () => {
104
+ const redis = createMockRedis(retriedThread);
105
+ const client = createMockClient();
106
+ const adapter = createAnthropicAdapter({
107
+ redis: redis as never,
108
+ client: client as never,
109
+ });
110
+
111
+ await adapter.createModelInvoker("claude-test")(invokerCall);
112
+
113
+ expect(redis.expire).toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
114
+ });
115
+ });
@@ -1,4 +1,4 @@
1
- import type Redis from "ioredis";
1
+ import type { RedisClientType as Redis } from "redis";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { ToolResultConfig } from "../../../lib/types";
4
4
  import type { PersistedThreadState } from "../../../lib/state/types";
@@ -57,9 +57,8 @@ export interface AnthropicAdapterConfig {
57
57
  */
58
58
  coldStore?: ColdThreadStore;
59
59
  /**
60
- * Override the default Redis TTL (90 days) for thread keys. When
61
- * pairing the adapter with a `coldStore`, a shorter TTL (hours)
62
- * is typically more appropriate.
60
+ * Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
61
+ * value (hours) with a cold tier.
63
62
  */
64
63
  ttlSeconds?: number;
65
64
  }
@@ -160,32 +159,26 @@ export function createAnthropicAdapter(
160
159
  ): AnthropicAdapter {
161
160
  const { redis, client } = config;
162
161
 
163
- /**
164
- * Common per-call config plumbed into both the provider thread
165
- * manager (for message I/O) and the tiered base manager (for
166
- * hot↔cold lifecycle ops). Keeping them in lockstep means a single
167
- * `coldStore` / `ttlSeconds` configuration controls every Redis
168
- * write the adapter does.
169
- */
170
- const baseExtras = {
162
+ // Single source for the adapter's `redis` handle and configured TTL, spread
163
+ // into every internal thread manager so all of them share one configuration.
164
+ const base = {
165
+ redis,
171
166
  ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
172
167
  };
173
168
 
174
169
  const makeProviderThread = (threadId: string, threadKey?: string) =>
175
170
  createAnthropicThreadManager({
176
- redis,
171
+ ...base,
177
172
  threadId,
178
173
  key: threadKey,
179
- ...baseExtras,
180
174
  });
181
175
 
182
176
  const makeTieredBase = (threadId: string, threadKey?: string) =>
183
177
  createTieredThreadManager<StoredMessage>({
184
- redis,
178
+ ...base,
185
179
  threadId,
186
180
  key: threadKey,
187
181
  idOf: storedMessageId,
188
- ...baseExtras,
189
182
  ...(config.coldStore && { coldStore: config.coldStore }),
190
183
  });
191
184
 
@@ -240,11 +233,10 @@ export function createAnthropicAdapter(
240
233
  threadKey?: string
241
234
  ): Promise<void> {
242
235
  const thread = createAnthropicThreadManager({
243
- redis,
236
+ ...base,
244
237
  threadId: sourceThreadId,
245
238
  key: threadKey,
246
239
  hooks: config.hooks,
247
- ...baseExtras,
248
240
  });
249
241
  await thread.fork(targetThreadId);
250
242
  },
@@ -304,7 +296,7 @@ export function createAnthropicAdapter(
304
296
  promptCache?: AnthropicPromptCacheConfig
305
297
  ): ModelInvoker<Anthropic.Messages.Message> => {
306
298
  const invokerConfig: AnthropicModelInvokerConfig = {
307
- redis,
299
+ ...base,
308
300
  client,
309
301
  model,
310
302
  ...(maxTokens !== undefined ? { maxTokens } : {}),
@@ -12,32 +12,32 @@ function createStatefulRedis() {
12
12
  const strings = new Map<string, string>();
13
13
 
14
14
  return {
15
- exists: vi.fn(async (...keys: string[]) =>
16
- keys.reduce(
15
+ exists: vi.fn(async (keys: string | string[]) =>
16
+ (Array.isArray(keys) ? keys : [keys]).reduce(
17
17
  (acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
18
18
  0
19
19
  )
20
20
  ),
21
- lrange: vi.fn(async (key: string, start: number, stop: number) => {
21
+ lRange: vi.fn(async (key: string, start: number, stop: number) => {
22
22
  const list = lists.get(key) ?? [];
23
23
  const end = stop === -1 ? list.length : stop + 1;
24
24
  return list.slice(start, end);
25
25
  }),
26
- rpush: vi.fn(async (key: string, ...values: string[]) => {
26
+ rPush: vi.fn(async (key: string, element: string | string[]) => {
27
27
  const list = lists.get(key) ?? [];
28
- list.push(...values);
28
+ list.push(...(Array.isArray(element) ? element : [element]));
29
29
  lists.set(key, list);
30
30
  return list.length;
31
31
  }),
32
- ltrim: vi.fn(async (key: string, start: number, stop: number) => {
32
+ lTrim: vi.fn(async (key: string, start: number, stop: number) => {
33
33
  const list = lists.get(key) ?? [];
34
34
  const end = stop === -1 ? list.length : stop + 1;
35
35
  lists.set(key, list.slice(start, end));
36
36
  return "OK";
37
37
  }),
38
- del: vi.fn(async (...keys: string[]) => {
38
+ del: vi.fn(async (keys: string | string[]) => {
39
39
  let removed = 0;
40
- for (const k of keys) {
40
+ for (const k of Array.isArray(keys) ? keys : [keys]) {
41
41
  if (lists.delete(k)) removed++;
42
42
  if (strings.delete(k)) removed++;
43
43
  }
@@ -49,10 +49,16 @@ function createStatefulRedis() {
49
49
  }),
50
50
  get: vi.fn(async (key: string) => strings.get(key) ?? null),
51
51
  expire: vi.fn(async (_key: string, _ttl: number) => 1),
52
- llen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
52
+ lLen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
53
53
  eval: vi.fn(
54
- async (_script: string, _numKeys: number, ...args: string[]) => {
55
- const [dedupKey, listKey, , ...serialised] = args;
54
+ async (
55
+ _script: string,
56
+ options: { keys?: string[]; arguments?: string[] }
57
+ ) => {
58
+ const keys = options.keys ?? [];
59
+ const argv = options.arguments ?? [];
60
+ const [dedupKey, listKey] = keys;
61
+ const serialised = argv.slice(1);
56
62
  if (!dedupKey || !listKey) return 0;
57
63
  if (strings.has(dedupKey)) return 0;
58
64
  const list = lists.get(listKey) ?? [];
@@ -2,15 +2,16 @@ import { describe, expect, it, vi } from "vitest";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import { createAnthropicModelInvoker } from "./model-invoker";
4
4
  import type { StoredMessage } from "./thread-manager";
5
+ import { THREAD_TTL_SECONDS } from "../../../lib/thread/keys";
5
6
 
6
7
  function createMockRedis(stored: StoredMessage[]) {
7
8
  return {
8
9
  exists: vi.fn().mockResolvedValue(1),
9
- lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
10
- ltrim: vi.fn().mockResolvedValue("OK"),
10
+ lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
11
+ lTrim: vi.fn().mockResolvedValue("OK"),
11
12
  del: vi.fn().mockResolvedValue(1),
12
13
  set: vi.fn().mockResolvedValue("OK"),
13
- rpush: vi.fn().mockResolvedValue(1),
14
+ rPush: vi.fn().mockResolvedValue(1),
14
15
  expire: vi.fn().mockResolvedValue(1),
15
16
  eval: vi.fn().mockResolvedValue(1),
16
17
  };
@@ -36,6 +37,7 @@ function createMockClient() {
36
37
  output_tokens: 1,
37
38
  server_tool_use: null,
38
39
  service_tier: null,
40
+ output_tokens_details: null,
39
41
  },
40
42
  };
41
43
  const stream = {
@@ -108,3 +110,52 @@ describe("createAnthropicModelInvoker prompt caching", () => {
108
110
  expect(params?.messages[0]?.content).toBe("hello");
109
111
  });
110
112
  });
113
+
114
+ describe("createAnthropicModelInvoker thread TTL", () => {
115
+ // The tail message is stored under `assistant-1`, so the invoker's
116
+ // `truncateFromId(assistant-1)` trims it and re-stamps the surviving
117
+ // list key's TTL.
118
+ const retriedThread: StoredMessage[] = [
119
+ { id: "msg-1", message: { role: "user", content: "hi" } },
120
+ { id: "assistant-1", message: { role: "assistant", content: "prior" } },
121
+ ];
122
+ const listKey = "messages:thread:thread-1";
123
+ const invokerConfig = {
124
+ threadId: "thread-1",
125
+ assistantMessageId: "assistant-1",
126
+ state: { tools: [] } as never,
127
+ agentName: "Agent",
128
+ };
129
+
130
+ it("re-stamps trimmed hot keys at the configured ttlSeconds", async () => {
131
+ const redis = createMockRedis(retriedThread);
132
+ const { client } = createMockClient();
133
+ const invoker = createAnthropicModelInvoker({
134
+ redis: redis as never,
135
+ client: client as never,
136
+ model: "claude-test",
137
+ ttlSeconds: 3600,
138
+ });
139
+
140
+ await invoker(invokerConfig);
141
+
142
+ expect(redis.lTrim).toHaveBeenCalledWith(listKey, 0, 0);
143
+ expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
144
+ expect(redis.expire).not.toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
145
+ });
146
+
147
+ it("defaults to THREAD_TTL_SECONDS when ttlSeconds is omitted", async () => {
148
+ const redis = createMockRedis(retriedThread);
149
+ const { client } = createMockClient();
150
+ const invoker = createAnthropicModelInvoker({
151
+ redis: redis as never,
152
+ client: client as never,
153
+ model: "claude-test",
154
+ });
155
+
156
+ await invoker(invokerConfig);
157
+
158
+ expect(redis.lTrim).toHaveBeenCalledWith(listKey, 0, 0);
159
+ expect(redis.expire).toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
160
+ });
161
+ });
@@ -1,4 +1,4 @@
1
- import type Redis from "ioredis";
1
+ import type { RedisClientType as Redis } from "redis";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { SerializableToolDefinition } from "../../../lib/types";
4
4
  import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
@@ -25,6 +25,11 @@ export interface AnthropicModelInvokerConfig {
25
25
  */
26
26
  promptCache?: AnthropicPromptCacheConfig;
27
27
  hooks?: AnthropicThreadManagerHooks;
28
+ /**
29
+ * Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
30
+ * value (hours) with a cold tier.
31
+ */
32
+ ttlSeconds?: number;
28
33
  }
29
34
 
30
35
  function toAnthropicTools(
@@ -68,6 +73,7 @@ export function createAnthropicModelInvoker({
68
73
  maxTokens = 16384,
69
74
  promptCache,
70
75
  hooks,
76
+ ttlSeconds,
71
77
  }: AnthropicModelInvokerConfig) {
72
78
  return async function invokeAnthropicModel(
73
79
  config: ModelInvokerConfig
@@ -80,6 +86,7 @@ export function createAnthropicModelInvoker({
80
86
  threadId,
81
87
  key: threadKey,
82
88
  hooks,
89
+ ...(ttlSeconds !== undefined && { ttlSeconds }),
83
90
  });
84
91
  // Truncate the thread starting at the id the assistant message
85
92
  // will be stored under. On the happy path this is a no-op; on a
@@ -150,6 +157,7 @@ export async function invokeAnthropicModel({
150
157
  maxTokens,
151
158
  promptCache,
152
159
  hooks,
160
+ ttlSeconds,
153
161
  config,
154
162
  }: {
155
163
  redis: Redis;
@@ -158,6 +166,7 @@ export async function invokeAnthropicModel({
158
166
  maxTokens?: number;
159
167
  promptCache?: AnthropicPromptCacheConfig;
160
168
  hooks?: AnthropicThreadManagerHooks;
169
+ ttlSeconds?: number;
161
170
  config: ModelInvokerConfig;
162
171
  }): Promise<AgentResponse<Anthropic.Messages.Message>> {
163
172
  const invoker = createAnthropicModelInvoker({
@@ -167,6 +176,7 @@ export async function invokeAnthropicModel({
167
176
  maxTokens,
168
177
  promptCache,
169
178
  hooks,
179
+ ...(ttlSeconds !== undefined && { ttlSeconds }),
170
180
  });
171
181
  return invoker(config);
172
182
  }
@@ -5,10 +5,10 @@ import { createAnthropicThreadManager } from "./thread-manager";
5
5
  function createMockRedis(stored: StoredMessage[]) {
6
6
  return {
7
7
  exists: vi.fn().mockResolvedValue(1),
8
- lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
8
+ lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
9
9
  del: vi.fn().mockResolvedValue(1),
10
10
  set: vi.fn().mockResolvedValue("OK"),
11
- rpush: vi.fn().mockResolvedValue(1),
11
+ rPush: vi.fn().mockResolvedValue(1),
12
12
  expire: vi.fn().mockResolvedValue(1),
13
13
  eval: vi.fn().mockResolvedValue(1),
14
14
  };
@@ -1,4 +1,4 @@
1
- import type Redis from "ioredis";
1
+ import type { RedisClientType as Redis } from "redis";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { JsonValue } from "../../../lib/state/types";
4
4
  import { createThreadManager } from "../../../lib/thread/manager";
@@ -36,9 +36,8 @@ export interface AnthropicThreadManagerConfig {
36
36
  key?: string;
37
37
  hooks?: AnthropicThreadManagerHooks;
38
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.
39
+ * Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
40
+ * value (hours) with a cold tier.
42
41
  */
43
42
  ttlSeconds?: number;
44
43
  }