zeitlich 0.2.50 → 0.2.53

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 (74) hide show
  1. package/dist/adapters/thread/anthropic/index.cjs +15 -13
  2. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  3. package/dist/adapters/thread/anthropic/index.d.cts +15 -10
  4. package/dist/adapters/thread/anthropic/index.d.ts +15 -10
  5. package/dist/adapters/thread/anthropic/index.js +15 -13
  6. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  7. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  8. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  9. package/dist/adapters/thread/google-genai/index.cjs +18 -12
  10. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  11. package/dist/adapters/thread/google-genai/index.d.cts +181 -11
  12. package/dist/adapters/thread/google-genai/index.d.ts +181 -11
  13. package/dist/adapters/thread/google-genai/index.js +18 -12
  14. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  15. package/dist/adapters/thread/google-genai/workflow.d.cts +6 -6
  16. package/dist/adapters/thread/google-genai/workflow.d.ts +6 -6
  17. package/dist/adapters/thread/langchain/index.cjs +22 -13
  18. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  19. package/dist/adapters/thread/langchain/index.d.cts +15 -10
  20. package/dist/adapters/thread/langchain/index.d.ts +15 -10
  21. package/dist/adapters/thread/langchain/index.js +22 -13
  22. package/dist/adapters/thread/langchain/index.js.map +1 -1
  23. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  24. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  25. package/dist/{cold-store-CCnZYWjx.d.ts → cold-store-BbvJLhXJ.d.ts} +1 -1
  26. package/dist/{cold-store-C0uvYTSi.d.cts → cold-store-Ki_U0jyd.d.cts} +1 -1
  27. package/dist/index.cjs +38 -3
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +8 -8
  30. package/dist/index.d.ts +8 -8
  31. package/dist/index.js +38 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/{proxy-C4J1pNUk.d.ts → proxy-CwniAm8W.d.ts} +1 -1
  34. package/dist/{proxy-BVznA2_p.d.cts → proxy-wsNrEh2u.d.cts} +1 -1
  35. package/dist/{thread-manager-BqjzWsP7.d.ts → thread-manager-D1zfZnxi.d.ts} +4 -5
  36. package/dist/{thread-manager-SkSWRPRc.d.ts → thread-manager-DCXkMqHH.d.ts} +4 -5
  37. package/dist/{thread-manager-CzIs47uG.d.cts → thread-manager-DW7FqMdN.d.cts} +4 -5
  38. package/dist/{thread-manager-Dzl1fHhV.d.cts → thread-manager-DhvA5oDL.d.cts} +4 -5
  39. package/dist/{types-YNesmGKV.d.ts → types-DQQKF5FQ.d.ts} +24 -2
  40. package/dist/{types-DZnUqCAP.d.cts → types-DpHBKA8c.d.cts} +24 -2
  41. package/dist/{types-d2RvEP6v.d.cts → types-tJ9Or7u_.d.cts} +1 -1
  42. package/dist/{types-CbPnU4RM.d.ts → types-ziu6HZPh.d.ts} +1 -1
  43. package/dist/{workflow-Bkzg0cjB.d.ts → workflow-BeMiPEq4.d.ts} +2 -1
  44. package/dist/{workflow-B3oTe2_D.d.cts → workflow-CNTNwEnj.d.cts} +2 -1
  45. package/dist/workflow.cjs +38 -3
  46. package/dist/workflow.cjs.map +1 -1
  47. package/dist/workflow.d.cts +2 -2
  48. package/dist/workflow.d.ts +2 -2
  49. package/dist/workflow.js +38 -3
  50. package/dist/workflow.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/adapters/thread/anthropic/activities.test.ts +115 -0
  53. package/src/adapters/thread/anthropic/activities.ts +10 -18
  54. package/src/adapters/thread/anthropic/model-invoker.test.ts +50 -0
  55. package/src/adapters/thread/anthropic/model-invoker.ts +10 -0
  56. package/src/adapters/thread/anthropic/thread-manager.ts +2 -3
  57. package/src/adapters/thread/google-genai/activities.test.ts +162 -0
  58. package/src/adapters/thread/google-genai/activities.ts +37 -14
  59. package/src/adapters/thread/google-genai/model-invoker.test.ts +53 -4
  60. package/src/adapters/thread/google-genai/model-invoker.ts +11 -0
  61. package/src/adapters/thread/google-genai/thread-manager.ts +2 -3
  62. package/src/adapters/thread/langchain/activities.test.ts +88 -0
  63. package/src/adapters/thread/langchain/activities.ts +14 -11
  64. package/src/adapters/thread/langchain/model-invoker.test.ts +74 -0
  65. package/src/adapters/thread/langchain/model-invoker.ts +15 -2
  66. package/src/adapters/thread/langchain/thread-manager.ts +2 -3
  67. package/src/lib/hooks/index.ts +2 -0
  68. package/src/lib/hooks/types.ts +26 -1
  69. package/src/lib/observability/hooks.ts +17 -2
  70. package/src/lib/session/session.ts +31 -3
  71. package/src/lib/state/types.ts +9 -11
  72. package/src/workflow.ts +2 -0
  73. package/dist/activities-IuOIvPHO.d.ts +0 -162
  74. package/dist/activities-cIlq1y1y.d.cts +0 -162
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.50",
3
+ "version": "0.2.53",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -181,7 +181,7 @@
181
181
  "node": ">=18"
182
182
  },
183
183
  "devDependencies": {
184
- "@anthropic-ai/sdk": "^0.100.1",
184
+ "@anthropic-ai/sdk": "^0.102.0",
185
185
  "@aws-sdk/client-s3": "^3.1000.0",
186
186
  "@aws-sdk/lib-storage": "^3.1000.0",
187
187
  "@daytonaio/sdk": "^0.184.0",
@@ -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
+ });
@@ -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 } : {}),
@@ -2,6 +2,7 @@ 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 {
@@ -109,3 +110,52 @@ describe("createAnthropicModelInvoker prompt caching", () => {
109
110
  expect(params?.messages[0]?.content).toBe("hello");
110
111
  });
111
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
+ });
@@ -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
  }
@@ -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
  }
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { GenerateContentResponse, Part } from "@google/genai";
3
+ import { createGoogleGenAIAdapter } from "./activities";
4
+ import type { StoredContent } from "./thread-manager";
5
+ import { THREAD_TTL_SECONDS } from "../../../lib/thread/keys";
6
+
7
+ function createMockRedis(stored: StoredContent[]) {
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
+ get: vi.fn().mockResolvedValue(null),
13
+ del: vi.fn().mockResolvedValue(1),
14
+ set: vi.fn().mockResolvedValue("OK"),
15
+ rPush: vi.fn().mockResolvedValue(1),
16
+ expire: vi.fn().mockResolvedValue(1),
17
+ eval: vi.fn().mockResolvedValue(1),
18
+ };
19
+ }
20
+
21
+ function createMockClient(parts: Part[] = [{ text: "ok" }]) {
22
+ const chunk: Partial<GenerateContentResponse> = {
23
+ candidates: [{ content: { role: "model", parts } }],
24
+ usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 },
25
+ };
26
+ return {
27
+ models: {
28
+ generateContentStream: vi.fn().mockResolvedValue({
29
+ async *[Symbol.asyncIterator]() {
30
+ yield chunk;
31
+ },
32
+ }),
33
+ },
34
+ caches: {
35
+ create: vi.fn().mockResolvedValue({ name: "cached-content-ref" }),
36
+ },
37
+ };
38
+ }
39
+
40
+ // Tail stored under the `assistantMessageId`, so the invoker's
41
+ // `truncateFromId` trims it and re-stamps the surviving list key's TTL.
42
+ const retriedThread: StoredContent[] = [
43
+ { id: "msg-1", content: { role: "user", parts: [{ text: "hi" }] } },
44
+ { id: "assistant-1", content: { role: "model", parts: [{ text: "prior" }] } },
45
+ ];
46
+ const listKey = "messages:thread:thread-1";
47
+ const metaKey = "messages:meta:thread:thread-1";
48
+ const invokerCall = {
49
+ threadId: "thread-1",
50
+ assistantMessageId: "assistant-1",
51
+ state: { tools: [] } as never,
52
+ agentName: "TestAgent",
53
+ };
54
+
55
+ describe("createGoogleGenAIAdapter — TTL propagation", () => {
56
+ it("forwards adapter ttlSeconds to a created invoker's writes", async () => {
57
+ const redis = createMockRedis(retriedThread);
58
+ const client = createMockClient();
59
+ const adapter = createGoogleGenAIAdapter({
60
+ redis: redis as never,
61
+ ttlSeconds: 3600,
62
+ });
63
+
64
+ await adapter.createModelInvoker(
65
+ "gemini-2.5-flash",
66
+ client as never
67
+ )(invokerCall);
68
+
69
+ expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
70
+ expect(redis.expire).not.toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
71
+ });
72
+
73
+ it("forwards adapter ttlSeconds to the default invoker", async () => {
74
+ const redis = createMockRedis(retriedThread);
75
+ const client = createMockClient();
76
+ const adapter = createGoogleGenAIAdapter({
77
+ redis: redis as never,
78
+ client: client as never,
79
+ model: "gemini-2.5-flash",
80
+ ttlSeconds: 3600,
81
+ });
82
+
83
+ await adapter.invoker(invokerCall);
84
+
85
+ expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
86
+ });
87
+
88
+ it("forwards adapter ttlSeconds to thread-op writes", async () => {
89
+ const redis = createMockRedis([]);
90
+ const adapter = createGoogleGenAIAdapter({
91
+ redis: redis as never,
92
+ ttlSeconds: 3600,
93
+ });
94
+ const acts = adapter.createActivities() as unknown as Record<
95
+ string,
96
+ (threadId: string, threadKey?: string) => Promise<void>
97
+ >;
98
+ const initialize = Object.entries(acts).find(([k]) =>
99
+ k.endsWith("InitializeThread")
100
+ )?.[1];
101
+ if (!initialize) throw new Error("initializeThread activity not found");
102
+
103
+ await initialize("thread-1");
104
+
105
+ expect(redis.set).toHaveBeenCalledWith(metaKey, "1", { EX: 3600 });
106
+ });
107
+
108
+ it("defaults to THREAD_TTL_SECONDS when adapter ttlSeconds is omitted", async () => {
109
+ const redis = createMockRedis(retriedThread);
110
+ const client = createMockClient();
111
+ const adapter = createGoogleGenAIAdapter({ redis: redis as never });
112
+
113
+ await adapter.createModelInvoker(
114
+ "gemini-2.5-flash",
115
+ client as never
116
+ )(invokerCall);
117
+
118
+ expect(redis.expire).toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
119
+ });
120
+ });
121
+
122
+ describe("createGoogleGenAIAdapter — cache/config forwarding", () => {
123
+ it("forwards adapter cache config to the invoker", async () => {
124
+ const multiThread: StoredContent[] = [
125
+ { id: "m1", content: { role: "user", parts: [{ text: "a" }] } },
126
+ { id: "m2", content: { role: "model", parts: [{ text: "b" }] } },
127
+ { id: "m3", content: { role: "user", parts: [{ text: "c" }] } },
128
+ ];
129
+ const redis = createMockRedis(multiThread);
130
+ const client = createMockClient();
131
+ const adapter = createGoogleGenAIAdapter({
132
+ redis: redis as never,
133
+ cache: { splitIndex: 1 },
134
+ });
135
+
136
+ await adapter.createModelInvoker(
137
+ "gemini-2.5-flash",
138
+ client as never
139
+ )(invokerCall);
140
+
141
+ expect(client.caches.create).toHaveBeenCalledOnce();
142
+ });
143
+
144
+ it("forwards adapter generationConfig to generateContentStream", async () => {
145
+ const redis = createMockRedis([
146
+ { id: "m1", content: { role: "user", parts: [{ text: "a" }] } },
147
+ ]);
148
+ const client = createMockClient();
149
+ const adapter = createGoogleGenAIAdapter({
150
+ redis: redis as never,
151
+ generationConfig: { temperature: 0.5 },
152
+ });
153
+
154
+ await adapter.createModelInvoker(
155
+ "gemini-2.5-flash",
156
+ client as never
157
+ )(invokerCall);
158
+
159
+ const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
160
+ expect(streamCall.config.temperature).toBe(0.5);
161
+ });
162
+ });
@@ -1,5 +1,10 @@
1
1
  import type { RedisClientType as Redis } from "redis";
2
- import type { GoogleGenAI, Content, Part } from "@google/genai";
2
+ import type {
3
+ GoogleGenAI,
4
+ Content,
5
+ Part,
6
+ GenerateContentConfig,
7
+ } from "@google/genai";
3
8
  import type { ToolResultConfig } from "../../../lib/types";
4
9
  import type { PersistedThreadState } from "../../../lib/state/types";
5
10
  import type {
@@ -23,7 +28,10 @@ import {
23
28
  type GoogleGenAIThreadManagerHooks,
24
29
  type StoredContent,
25
30
  } from "./thread-manager";
26
- import { createGoogleGenAIModelInvoker } from "./model-invoker";
31
+ import {
32
+ createGoogleGenAIModelInvoker,
33
+ type GoogleGenAIModelInvokerConfig,
34
+ } from "./model-invoker";
27
35
  import { ADAPTER_ID } from "./adapter-id";
28
36
 
29
37
  export type GoogleGenAIThreadOps<TScope extends string = ""> =
@@ -46,11 +54,22 @@ export interface GoogleGenAIAdapterConfig {
46
54
  */
47
55
  coldStore?: ColdThreadStore;
48
56
  /**
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.
57
+ * Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
58
+ * value (hours) with a cold tier.
52
59
  */
53
60
  ttlSeconds?: number;
61
+ /**
62
+ * Default generation config forwarded to every invoker the adapter
63
+ * builds (`invoker` and `createModelInvoker`). `systemInstruction`,
64
+ * `tools`, and `abortSignal` are managed by the invoker and override
65
+ * any values set here.
66
+ */
67
+ generationConfig?: GenerateContentConfig;
68
+ /**
69
+ * Default server-side context caching config forwarded to every
70
+ * invoker the adapter builds. See {@link createGoogleGenAIModelInvoker}.
71
+ */
72
+ cache?: GoogleGenAIModelInvokerConfig["cache"];
54
73
  }
55
74
 
56
75
  /**
@@ -145,7 +164,7 @@ export interface GoogleGenAIAdapter {
145
164
  * ...createRunAgentActivity(temporalClient, adapter.invoker, "codingAgent"),
146
165
  * ...createRunAgentActivity(
147
166
  * temporalClient,
148
- * adapter.createModelInvoker('gemini-2.5-pro'),
167
+ * adapter.createModelInvoker('gemini-2.5-pro', client),
149
168
  * "researchAgent",
150
169
  * ),
151
170
  * };
@@ -157,25 +176,26 @@ export function createGoogleGenAIAdapter(
157
176
  ): GoogleGenAIAdapter {
158
177
  const { redis } = config;
159
178
 
160
- const baseExtras = {
179
+ // Single source for the adapter's `redis` handle and configured TTL, spread
180
+ // into every internal thread manager so all of them share one configuration.
181
+ const base = {
182
+ redis,
161
183
  ...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
162
184
  };
163
185
 
164
186
  const makeProviderThread = (threadId: string, threadKey?: string) =>
165
187
  createGoogleGenAIThreadManager({
166
- redis,
188
+ ...base,
167
189
  threadId,
168
190
  key: threadKey,
169
- ...baseExtras,
170
191
  });
171
192
 
172
193
  const makeTieredBase = (threadId: string, threadKey?: string) =>
173
194
  createTieredThreadManager<StoredContent>({
174
- redis,
195
+ ...base,
175
196
  threadId,
176
197
  key: threadKey,
177
198
  idOf: storedContentId,
178
- ...baseExtras,
179
199
  ...(config.coldStore && { coldStore: config.coldStore }),
180
200
  });
181
201
 
@@ -235,11 +255,10 @@ export function createGoogleGenAIAdapter(
235
255
  threadKey?: string
236
256
  ): Promise<void> {
237
257
  const thread = createGoogleGenAIThreadManager({
238
- redis,
258
+ ...base,
239
259
  threadId: sourceThreadId,
240
260
  key: threadKey,
241
261
  hooks: config.hooks,
242
- ...baseExtras,
243
262
  });
244
263
  await thread.fork(targetThreadId);
245
264
  },
@@ -304,10 +323,14 @@ export function createGoogleGenAIAdapter(
304
323
  client: GoogleGenAI
305
324
  ): ModelInvoker<Content> =>
306
325
  createGoogleGenAIModelInvoker({
307
- redis,
326
+ ...base,
308
327
  client,
309
328
  model,
310
329
  hooks: config.hooks,
330
+ ...(config.generationConfig !== undefined && {
331
+ config: config.generationConfig,
332
+ }),
333
+ ...(config.cache !== undefined && { cache: config.cache }),
311
334
  });
312
335
 
313
336
  const invoker: ModelInvoker<Content> =