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.
- package/dist/adapters/thread/anthropic/index.cjs +15 -13
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +15 -10
- package/dist/adapters/thread/anthropic/index.d.ts +15 -10
- package/dist/adapters/thread/anthropic/index.js +15 -13
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
- package/dist/adapters/thread/google-genai/index.cjs +18 -12
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +181 -11
- package/dist/adapters/thread/google-genai/index.d.ts +181 -11
- package/dist/adapters/thread/google-genai/index.js +18 -12
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +6 -6
- package/dist/adapters/thread/google-genai/workflow.d.ts +6 -6
- package/dist/adapters/thread/langchain/index.cjs +22 -13
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +15 -10
- package/dist/adapters/thread/langchain/index.d.ts +15 -10
- package/dist/adapters/thread/langchain/index.js +22 -13
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
- package/dist/{cold-store-CCnZYWjx.d.ts → cold-store-BbvJLhXJ.d.ts} +1 -1
- package/dist/{cold-store-C0uvYTSi.d.cts → cold-store-Ki_U0jyd.d.cts} +1 -1
- package/dist/index.cjs +38 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +38 -3
- package/dist/index.js.map +1 -1
- package/dist/{proxy-C4J1pNUk.d.ts → proxy-CwniAm8W.d.ts} +1 -1
- package/dist/{proxy-BVznA2_p.d.cts → proxy-wsNrEh2u.d.cts} +1 -1
- package/dist/{thread-manager-BqjzWsP7.d.ts → thread-manager-D1zfZnxi.d.ts} +4 -5
- package/dist/{thread-manager-SkSWRPRc.d.ts → thread-manager-DCXkMqHH.d.ts} +4 -5
- package/dist/{thread-manager-CzIs47uG.d.cts → thread-manager-DW7FqMdN.d.cts} +4 -5
- package/dist/{thread-manager-Dzl1fHhV.d.cts → thread-manager-DhvA5oDL.d.cts} +4 -5
- package/dist/{types-YNesmGKV.d.ts → types-DQQKF5FQ.d.ts} +24 -2
- package/dist/{types-DZnUqCAP.d.cts → types-DpHBKA8c.d.cts} +24 -2
- package/dist/{types-d2RvEP6v.d.cts → types-tJ9Or7u_.d.cts} +1 -1
- package/dist/{types-CbPnU4RM.d.ts → types-ziu6HZPh.d.ts} +1 -1
- package/dist/{workflow-Bkzg0cjB.d.ts → workflow-BeMiPEq4.d.ts} +2 -1
- package/dist/{workflow-B3oTe2_D.d.cts → workflow-CNTNwEnj.d.cts} +2 -1
- package/dist/workflow.cjs +38 -3
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +2 -2
- package/dist/workflow.d.ts +2 -2
- package/dist/workflow.js +38 -3
- package/dist/workflow.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/thread/anthropic/activities.test.ts +115 -0
- package/src/adapters/thread/anthropic/activities.ts +10 -18
- package/src/adapters/thread/anthropic/model-invoker.test.ts +50 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +10 -0
- package/src/adapters/thread/anthropic/thread-manager.ts +2 -3
- package/src/adapters/thread/google-genai/activities.test.ts +162 -0
- package/src/adapters/thread/google-genai/activities.ts +37 -14
- package/src/adapters/thread/google-genai/model-invoker.test.ts +53 -4
- package/src/adapters/thread/google-genai/model-invoker.ts +11 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +2 -3
- package/src/adapters/thread/langchain/activities.test.ts +88 -0
- package/src/adapters/thread/langchain/activities.ts +14 -11
- package/src/adapters/thread/langchain/model-invoker.test.ts +74 -0
- package/src/adapters/thread/langchain/model-invoker.ts +15 -2
- package/src/adapters/thread/langchain/thread-manager.ts +2 -3
- package/src/lib/hooks/index.ts +2 -0
- package/src/lib/hooks/types.ts +26 -1
- package/src/lib/observability/hooks.ts +17 -2
- package/src/lib/session/session.ts +31 -3
- package/src/lib/state/types.ts +9 -11
- package/src/workflow.ts +2 -0
- package/dist/activities-IuOIvPHO.d.ts +0 -162
- 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.
|
|
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.
|
|
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
|
-
*
|
|
61
|
-
*
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> =
|