zeitlich 0.2.48 → 0.2.50
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/README.md +26 -23
- package/dist/{activities-DCaIPQBT.d.ts → activities-IuOIvPHO.d.ts} +6 -6
- package/dist/{activities-BlQR5gX4.d.cts → activities-cIlq1y1y.d.cts} +6 -6
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
- package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +45 -42
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +10 -10
- package/dist/adapters/thread/anthropic/index.d.ts +10 -10
- package/dist/adapters/thread/anthropic/index.js +45 -42
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
- package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
- package/dist/adapters/thread/google-genai/index.cjs +117 -54
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +27 -23
- package/dist/adapters/thread/google-genai/index.d.ts +27 -23
- package/dist/adapters/thread/google-genai/index.js +117 -54
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
- package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
- package/dist/adapters/thread/langchain/index.cjs +45 -42
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +10 -10
- package/dist/adapters/thread/langchain/index.d.ts +10 -10
- package/dist/adapters/thread/langchain/index.js +45 -42
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
- package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
- package/dist/{cold-store-UL13Sstw.d.cts → cold-store-C0uvYTSi.d.cts} +1 -1
- package/dist/{cold-store-aD4TSKlU.d.ts → cold-store-CCnZYWjx.d.ts} +1 -1
- package/dist/index.cjs +15063 -405
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -83
- package/dist/index.d.ts +79 -83
- package/dist/index.js +15064 -402
- package/dist/index.js.map +1 -1
- package/dist/{proxy-BAty3CWM.d.cts → proxy-BVznA2_p.d.cts} +1 -1
- package/dist/{proxy-mbnwBhHw.d.ts → proxy-C4J1pNUk.d.ts} +1 -1
- package/dist/{thread-manager-CICj68PI.d.ts → thread-manager-BqjzWsP7.d.ts} +4 -4
- package/dist/{thread-manager-R6c3lnJy.d.cts → thread-manager-CzIs47uG.d.cts} +4 -4
- package/dist/{thread-manager-DsXvJ5cJ.d.cts → thread-manager-Dzl1fHhV.d.cts} +4 -4
- package/dist/{thread-manager-DtEtbUkp.d.ts → thread-manager-SkSWRPRc.d.ts} +4 -4
- package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
- package/dist/{types-DF4wzWQG.d.ts → types-CbPnU4RM.d.ts} +3 -3
- package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.cts} +3 -3
- package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.ts} +3 -3
- package/dist/{types-DwBYd0ij.d.ts → types-DZnUqCAP.d.cts} +709 -686
- package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
- package/dist/{types-DWeyCTYK.d.cts → types-YNesmGKV.d.ts} +709 -686
- package/dist/{types-DDLPnxBh.d.cts → types-d2RvEP6v.d.cts} +3 -3
- package/dist/{workflow-DdaU7_j4.d.ts → workflow-B3oTe2_D.d.cts} +34 -3
- package/dist/{workflow-DVNPR7eX.d.cts → workflow-Bkzg0cjB.d.ts} +34 -3
- package/dist/workflow.cjs +15021 -362
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +15022 -359
- package/dist/workflow.js.map +1 -1
- package/package.json +10 -37
- package/src/adapters/thread/anthropic/activities.ts +1 -1
- package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
- package/src/adapters/thread/anthropic/model-invoker.test.ts +4 -3
- package/src/adapters/thread/anthropic/model-invoker.ts +1 -1
- package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +1 -1
- package/src/adapters/thread/google-genai/activities.ts +1 -1
- package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
- package/src/adapters/thread/google-genai/model-invoker.test.ts +337 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +107 -23
- package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +1 -1
- package/src/adapters/thread/langchain/activities.ts +1 -1
- package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
- package/src/adapters/thread/langchain/model-invoker.ts +1 -1
- package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
- package/src/adapters/thread/langchain/thread-manager.ts +1 -1
- package/src/index.ts +2 -2
- package/src/lib/sandbox/capability-types.test.ts +2 -2
- package/src/lib/sandbox/manager.ts +2 -6
- package/src/lib/sandbox/sandbox.test.ts +1 -1
- package/src/lib/sandbox/types.ts +2 -2
- package/src/lib/session/session.integration.test.ts +92 -0
- package/src/lib/session/session.ts +23 -0
- package/src/lib/subagent/handler.ts +23 -0
- package/src/lib/subagent/subagent.integration.test.ts +198 -0
- package/src/lib/thread/keys.test.ts +9 -9
- package/src/lib/thread/keys.ts +1 -1
- package/src/lib/thread/manager.test.ts +24 -14
- package/src/lib/thread/manager.ts +19 -23
- package/src/lib/thread/snapshot.test.ts +51 -43
- package/src/lib/thread/snapshot.ts +54 -32
- package/src/lib/thread/test-utils.ts +106 -59
- package/src/lib/thread/tiered.test.ts +1 -1
- package/src/lib/thread/types.ts +2 -2
- package/src/lib/tool-router/router.integration.test.ts +44 -0
- package/src/lib/tool-router/router.ts +149 -33
- package/src/lib/tool-router/types.ts +23 -0
- package/src/lib/workflow.ts +49 -0
- package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
- package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
- package/src/tools/bash/bash.test.ts +1 -1
- package/src/tools/edit/handler.test.ts +1 -1
- package/tsup.config.ts +2 -4
- package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
- package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
- package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
- package/dist/adapters/sandbox/inmemory/index.js +0 -211
- package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
- package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
- 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.
|
|
3
|
+
"version": "0.2.50",
|
|
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.
|
|
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.
|
|
187
|
+
"@daytonaio/sdk": "^0.184.0",
|
|
208
188
|
"@e2b/code-interpreter": "^2.3.3",
|
|
209
189
|
"@eslint/js": "^10.0.1",
|
|
210
|
-
"@google/genai": "^
|
|
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.
|
|
197
|
+
"eslint": "^10.4.1",
|
|
218
198
|
"husky": "^9.1.7",
|
|
219
|
-
"just-bash": "^
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Redis from "
|
|
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";
|
|
@@ -12,32 +12,32 @@ function createStatefulRedis() {
|
|
|
12
12
|
const strings = new Map<string, string>();
|
|
13
13
|
|
|
14
14
|
return {
|
|
15
|
-
exists: vi.fn(async (
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
rPush: vi.fn(async (key: string, element: string | string[]) => {
|
|
27
27
|
const list = lists.get(key) ?? [];
|
|
28
|
-
list.push(...
|
|
28
|
+
list.push(...(Array.isArray(element) ? element : [element]));
|
|
29
29
|
lists.set(key, list);
|
|
30
30
|
return list.length;
|
|
31
31
|
}),
|
|
32
|
-
|
|
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 (
|
|
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
|
-
|
|
52
|
+
lLen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
|
|
53
53
|
eval: vi.fn(
|
|
54
|
-
async (
|
|
55
|
-
|
|
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) ?? [];
|
|
@@ -6,11 +6,11 @@ import type { StoredMessage } from "./thread-manager";
|
|
|
6
6
|
function createMockRedis(stored: StoredMessage[]) {
|
|
7
7
|
return {
|
|
8
8
|
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
lTrim: vi.fn().mockResolvedValue("OK"),
|
|
11
11
|
del: vi.fn().mockResolvedValue(1),
|
|
12
12
|
set: vi.fn().mockResolvedValue("OK"),
|
|
13
|
-
|
|
13
|
+
rPush: vi.fn().mockResolvedValue(1),
|
|
14
14
|
expire: vi.fn().mockResolvedValue(1),
|
|
15
15
|
eval: vi.fn().mockResolvedValue(1),
|
|
16
16
|
};
|
|
@@ -36,6 +36,7 @@ function createMockClient() {
|
|
|
36
36
|
output_tokens: 1,
|
|
37
37
|
server_tool_use: null,
|
|
38
38
|
service_tier: null,
|
|
39
|
+
output_tokens_details: null,
|
|
39
40
|
},
|
|
40
41
|
};
|
|
41
42
|
const stream = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Redis from "
|
|
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";
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 "
|
|
1
|
+
import type { RedisClientType as Redis } from "redis";
|
|
2
2
|
import type { GoogleGenAI, Content, Part } from "@google/genai";
|
|
3
3
|
import type { ToolResultConfig } from "../../../lib/types";
|
|
4
4
|
import type { PersistedThreadState } from "../../../lib/state/types";
|
|
@@ -7,27 +7,27 @@ function createStatefulRedis() {
|
|
|
7
7
|
const strings = new Map<string, string>();
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
|
-
exists: vi.fn(async (
|
|
11
|
-
keys.reduce(
|
|
10
|
+
exists: vi.fn(async (keys: string | string[]) =>
|
|
11
|
+
(Array.isArray(keys) ? keys : [keys]).reduce(
|
|
12
12
|
(acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
|
|
13
13
|
0
|
|
14
14
|
)
|
|
15
15
|
),
|
|
16
|
-
|
|
16
|
+
lRange: vi.fn(async (key: string, start: number, stop: number) => {
|
|
17
17
|
const list = lists.get(key) ?? [];
|
|
18
18
|
const end = stop === -1 ? list.length : stop + 1;
|
|
19
19
|
return list.slice(start, end);
|
|
20
20
|
}),
|
|
21
|
-
|
|
21
|
+
rPush: vi.fn(async (key: string, element: string | string[]) => {
|
|
22
22
|
const list = lists.get(key) ?? [];
|
|
23
|
-
list.push(...
|
|
23
|
+
list.push(...(Array.isArray(element) ? element : [element]));
|
|
24
24
|
lists.set(key, list);
|
|
25
25
|
return list.length;
|
|
26
26
|
}),
|
|
27
|
-
|
|
28
|
-
del: vi.fn(async (
|
|
27
|
+
lTrim: vi.fn(async () => "OK"),
|
|
28
|
+
del: vi.fn(async (keys: string | string[]) => {
|
|
29
29
|
let removed = 0;
|
|
30
|
-
for (const k of keys) {
|
|
30
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) {
|
|
31
31
|
if (lists.delete(k)) removed++;
|
|
32
32
|
if (strings.delete(k)) removed++;
|
|
33
33
|
}
|
|
@@ -39,10 +39,16 @@ function createStatefulRedis() {
|
|
|
39
39
|
}),
|
|
40
40
|
get: vi.fn(async (key: string) => strings.get(key) ?? null),
|
|
41
41
|
expire: vi.fn(async () => 1),
|
|
42
|
-
|
|
42
|
+
lLen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
|
|
43
43
|
eval: vi.fn(
|
|
44
|
-
async (
|
|
45
|
-
|
|
44
|
+
async (
|
|
45
|
+
_script: string,
|
|
46
|
+
options: { keys?: string[]; arguments?: string[] }
|
|
47
|
+
) => {
|
|
48
|
+
const keys = options.keys ?? [];
|
|
49
|
+
const argv = options.arguments ?? [];
|
|
50
|
+
const [dedupKey, listKey] = keys;
|
|
51
|
+
const serialised = argv.slice(1);
|
|
46
52
|
if (!dedupKey || !listKey) return 0;
|
|
47
53
|
if (strings.has(dedupKey)) return 0;
|
|
48
54
|
const list = lists.get(listKey) ?? [];
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
FunctionCallingConfigMode,
|
|
4
|
+
type Content,
|
|
5
|
+
type GenerateContentResponse,
|
|
6
|
+
type Part,
|
|
7
|
+
} from "@google/genai";
|
|
8
|
+
import { createGoogleGenAIModelInvoker } from "./model-invoker";
|
|
9
|
+
import type { StoredContent } from "./thread-manager";
|
|
10
|
+
import type { AgentResponse } from "../../../lib/model";
|
|
11
|
+
|
|
12
|
+
const textReply: Part[] = [{ text: "ok" }];
|
|
13
|
+
|
|
14
|
+
function createMockRedis(
|
|
15
|
+
stored: StoredContent[],
|
|
16
|
+
extra?: Record<string, string>
|
|
17
|
+
) {
|
|
18
|
+
return {
|
|
19
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
20
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
21
|
+
get: vi
|
|
22
|
+
.fn()
|
|
23
|
+
.mockImplementation((key: string) =>
|
|
24
|
+
Promise.resolve(extra?.[key] ?? null)
|
|
25
|
+
),
|
|
26
|
+
del: vi.fn().mockResolvedValue(1),
|
|
27
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
28
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
29
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
30
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createMockClient(parts: Part[] = textReply) {
|
|
35
|
+
const chunk: Partial<GenerateContentResponse> = {
|
|
36
|
+
candidates: [{ content: { role: "model", parts } }],
|
|
37
|
+
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 },
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
models: {
|
|
41
|
+
generateContentStream: vi.fn().mockResolvedValue({
|
|
42
|
+
async *[Symbol.asyncIterator]() {
|
|
43
|
+
yield chunk;
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
caches: {
|
|
48
|
+
create: vi.fn().mockResolvedValue({ name: "cached-content-ref" }),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultStored: StoredContent[] = [
|
|
54
|
+
{
|
|
55
|
+
id: "msg-1",
|
|
56
|
+
content: { role: "user", parts: [{ text: "classify these files" }] },
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const invokerConfig = {
|
|
61
|
+
threadId: "thread-1",
|
|
62
|
+
assistantMessageId: "assistant-1",
|
|
63
|
+
state: { tools: [] } as never,
|
|
64
|
+
agentName: "TestAgent",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function invoke(parts: Part[]): Promise<AgentResponse<Content>> {
|
|
68
|
+
const redis = createMockRedis(defaultStored);
|
|
69
|
+
const client = createMockClient(parts);
|
|
70
|
+
|
|
71
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
72
|
+
redis: redis as never,
|
|
73
|
+
client: client as never,
|
|
74
|
+
model: "gemini-2.5-flash",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return invoker(invokerConfig);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("Google GenAI model invoker — function call IDs", () => {
|
|
81
|
+
it("assigns synthetic IDs when Gemini omits them", async () => {
|
|
82
|
+
const result = await invoke([
|
|
83
|
+
{ functionCall: { name: "classifyFile", args: { index: 0 } } },
|
|
84
|
+
{ functionCall: { name: "classifyFile", args: { index: 1 } } },
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
expect(result.rawToolCalls).toHaveLength(2);
|
|
88
|
+
for (const tc of result.rawToolCalls) {
|
|
89
|
+
expect(tc.id).toBeDefined();
|
|
90
|
+
expect(tc.id).not.toBe("");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("preserves existing IDs from Gemini when present", async () => {
|
|
95
|
+
const result = await invoke([
|
|
96
|
+
{
|
|
97
|
+
functionCall: {
|
|
98
|
+
id: "gemini-abc123",
|
|
99
|
+
name: "lookupFile",
|
|
100
|
+
args: { path: "/a" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
expect(result.rawToolCalls[0]?.id).toBe("gemini-abc123");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("generates unique IDs across multiple function calls", async () => {
|
|
109
|
+
const parts: Part[] = Array.from({ length: 5 }, (_, i) => ({
|
|
110
|
+
functionCall: { name: "inspect", args: { index: i } },
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
const result = await invoke(parts);
|
|
114
|
+
|
|
115
|
+
const ids = result.rawToolCalls.map((tc) => tc.id);
|
|
116
|
+
expect(new Set(ids).size).toBe(5);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("matches IDs between message parts and rawToolCalls", async () => {
|
|
120
|
+
const result = await invoke([
|
|
121
|
+
{ functionCall: { name: "toolA", args: {} } },
|
|
122
|
+
{ functionCall: { name: "toolB", args: {} } },
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const partIds = result.message.parts
|
|
126
|
+
?.filter((p) => p.functionCall)
|
|
127
|
+
.map((p) => p.functionCall?.id);
|
|
128
|
+
const rawIds = result.rawToolCalls.map((tc) => tc.id);
|
|
129
|
+
|
|
130
|
+
expect(partIds).toEqual(rawIds);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("handles a mix of parts with and without existing IDs", async () => {
|
|
134
|
+
const result = await invoke([
|
|
135
|
+
{ functionCall: { id: "existing-id", name: "toolA", args: {} } },
|
|
136
|
+
{ functionCall: { name: "toolB", args: {} } },
|
|
137
|
+
{ text: "some reasoning text" },
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
expect(result.rawToolCalls).toHaveLength(2);
|
|
141
|
+
expect(result.rawToolCalls[0]?.id).toBe("existing-id");
|
|
142
|
+
expect(result.rawToolCalls[1]?.id).toBeDefined();
|
|
143
|
+
expect(result.rawToolCalls[1]?.id).not.toBe("");
|
|
144
|
+
expect(result.rawToolCalls[1]?.id).not.toBe("existing-id");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("Google GenAI model invoker — context caching", () => {
|
|
149
|
+
const multiMessageThread: StoredContent[] = [
|
|
150
|
+
{
|
|
151
|
+
id: "msg-1",
|
|
152
|
+
content: {
|
|
153
|
+
role: "user",
|
|
154
|
+
parts: [{ inlineData: { data: "base64img", mimeType: "image/png" } }],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "msg-2",
|
|
159
|
+
content: { role: "model", parts: [{ text: "I see the image" }] },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "msg-3",
|
|
163
|
+
content: { role: "user", parts: [{ text: "classify it" }] },
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
it("creates a cache and sends only live contents when contents exceed splitIndex", async () => {
|
|
168
|
+
const redis = createMockRedis(multiMessageThread);
|
|
169
|
+
const client = createMockClient();
|
|
170
|
+
|
|
171
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
172
|
+
redis: redis as never,
|
|
173
|
+
client: client as never,
|
|
174
|
+
model: "gemini-2.5-flash",
|
|
175
|
+
cache: { splitIndex: 1 },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await invoker(invokerConfig);
|
|
179
|
+
|
|
180
|
+
expect(client.caches.create).toHaveBeenCalledOnce();
|
|
181
|
+
const cacheCall = client.caches.create.mock.calls[0]?.[0];
|
|
182
|
+
expect(cacheCall.model).toBe("gemini-2.5-flash");
|
|
183
|
+
expect(cacheCall.config.contents).toHaveLength(1);
|
|
184
|
+
expect(cacheCall.config.ttl).toBe("300s");
|
|
185
|
+
|
|
186
|
+
const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
|
|
187
|
+
expect(streamCall.contents).toHaveLength(2);
|
|
188
|
+
expect(streamCall.config.cachedContent).toBe("cached-content-ref");
|
|
189
|
+
expect(streamCall.config.systemInstruction).toBeUndefined();
|
|
190
|
+
expect(streamCall.config.tools).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("skips caching when contents.length <= splitIndex", async () => {
|
|
194
|
+
const redis = createMockRedis(defaultStored);
|
|
195
|
+
const client = createMockClient();
|
|
196
|
+
|
|
197
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
198
|
+
redis: redis as never,
|
|
199
|
+
client: client as never,
|
|
200
|
+
model: "gemini-2.5-flash",
|
|
201
|
+
cache: { splitIndex: 1 },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await invoker(invokerConfig);
|
|
205
|
+
|
|
206
|
+
expect(client.caches.create).not.toHaveBeenCalled();
|
|
207
|
+
const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
|
|
208
|
+
expect(streamCall.contents).toHaveLength(1);
|
|
209
|
+
expect(streamCall.config.cachedContent).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("uses custom TTL", async () => {
|
|
213
|
+
const redis = createMockRedis(multiMessageThread);
|
|
214
|
+
const client = createMockClient();
|
|
215
|
+
|
|
216
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
217
|
+
redis: redis as never,
|
|
218
|
+
client: client as never,
|
|
219
|
+
model: "gemini-2.5-flash",
|
|
220
|
+
cache: { splitIndex: 1, ttlSeconds: 600 },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await invoker(invokerConfig);
|
|
224
|
+
|
|
225
|
+
const cacheCall = client.caches.create.mock.calls[0]?.[0];
|
|
226
|
+
expect(cacheCall.config.ttl).toBe("600s");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("moves toolConfig into cache and clears it from live request", async () => {
|
|
230
|
+
const redis = createMockRedis(multiMessageThread);
|
|
231
|
+
const client = createMockClient();
|
|
232
|
+
|
|
233
|
+
const toolConfig = {
|
|
234
|
+
functionCallingConfig: { mode: FunctionCallingConfigMode.ANY },
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
238
|
+
redis: redis as never,
|
|
239
|
+
client: client as never,
|
|
240
|
+
model: "gemini-2.5-flash",
|
|
241
|
+
cache: { splitIndex: 1 },
|
|
242
|
+
config: { toolConfig },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await invoker(invokerConfig);
|
|
246
|
+
|
|
247
|
+
const cacheCall = client.caches.create.mock.calls[0]?.[0];
|
|
248
|
+
expect(cacheCall.config.toolConfig).toEqual(toolConfig);
|
|
249
|
+
|
|
250
|
+
const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
|
|
251
|
+
expect(streamCall.config.toolConfig).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("skips caching when splitIndex is 0", async () => {
|
|
255
|
+
const redis = createMockRedis(multiMessageThread);
|
|
256
|
+
const client = createMockClient();
|
|
257
|
+
|
|
258
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
259
|
+
redis: redis as never,
|
|
260
|
+
client: client as never,
|
|
261
|
+
model: "gemini-2.5-flash",
|
|
262
|
+
cache: { splitIndex: 0 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await invoker(invokerConfig);
|
|
266
|
+
|
|
267
|
+
expect(client.caches.create).not.toHaveBeenCalled();
|
|
268
|
+
const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
|
|
269
|
+
expect(streamCall.config.cachedContent).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("reuses cached content name from Redis instead of creating a new cache", async () => {
|
|
273
|
+
const redis = createMockRedis(multiMessageThread, {
|
|
274
|
+
"messages:gemini-cache:gemini-2.5-flash:1:thread:thread-1":
|
|
275
|
+
"cachedContents/existing",
|
|
276
|
+
});
|
|
277
|
+
const client = createMockClient();
|
|
278
|
+
|
|
279
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
280
|
+
redis: redis as never,
|
|
281
|
+
client: client as never,
|
|
282
|
+
model: "gemini-2.5-flash",
|
|
283
|
+
cache: { splitIndex: 1 },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await invoker(invokerConfig);
|
|
287
|
+
|
|
288
|
+
expect(client.caches.create).not.toHaveBeenCalled();
|
|
289
|
+
const streamCall = client.models.generateContentStream.mock.calls[0]?.[0];
|
|
290
|
+
expect(streamCall.config.cachedContent).toBe("cachedContents/existing");
|
|
291
|
+
expect(streamCall.contents).toHaveLength(2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("stores cache name in Redis after creation", async () => {
|
|
295
|
+
const redis = createMockRedis(multiMessageThread);
|
|
296
|
+
const client = createMockClient();
|
|
297
|
+
|
|
298
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
299
|
+
redis: redis as never,
|
|
300
|
+
client: client as never,
|
|
301
|
+
model: "gemini-2.5-flash",
|
|
302
|
+
cache: { splitIndex: 1, ttlSeconds: 600 },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await invoker(invokerConfig);
|
|
306
|
+
|
|
307
|
+
expect(client.caches.create).toHaveBeenCalledOnce();
|
|
308
|
+
const setCall = redis.set.mock.calls.find(
|
|
309
|
+
(c: string[]) =>
|
|
310
|
+
c[0] === "messages:gemini-cache:gemini-2.5-flash:1:thread:thread-1"
|
|
311
|
+
);
|
|
312
|
+
expect(setCall).toBeDefined();
|
|
313
|
+
expect(setCall?.[1]).toBe("cached-content-ref");
|
|
314
|
+
expect(setCall?.[2]).toBe("EX");
|
|
315
|
+
expect(setCall?.[3]).toBe(595);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("reports cachedWriteTokens from cache creation", async () => {
|
|
319
|
+
const redis = createMockRedis(multiMessageThread);
|
|
320
|
+
const client = createMockClient();
|
|
321
|
+
client.caches.create.mockResolvedValue({
|
|
322
|
+
name: "cached-content-ref",
|
|
323
|
+
usageMetadata: { totalTokenCount: 4200 },
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const invoker = createGoogleGenAIModelInvoker({
|
|
327
|
+
redis: redis as never,
|
|
328
|
+
client: client as never,
|
|
329
|
+
model: "gemini-2.5-flash",
|
|
330
|
+
cache: { splitIndex: 1 },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = await invoker(invokerConfig);
|
|
334
|
+
|
|
335
|
+
expect(result.usage?.cachedWriteTokens).toBe(4200);
|
|
336
|
+
});
|
|
337
|
+
});
|