zeitlich 0.2.26 → 0.2.28
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/{activities-BEJRyDVU.d.cts → activities-3xj_fEJK.d.ts} +7 -4
- package/dist/{activities-LVQdLF6I.d.ts → activities-BzYq6jf7.d.cts} +7 -4
- package/dist/adapters/sandbox/e2b/index.cjs +230 -0
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -0
- package/dist/adapters/sandbox/e2b/index.d.cts +77 -0
- package/dist/adapters/sandbox/e2b/index.d.ts +77 -0
- package/dist/adapters/sandbox/e2b/index.js +227 -0
- package/dist/adapters/sandbox/e2b/index.js.map +1 -0
- package/dist/adapters/sandbox/e2b/workflow.cjs +33 -0
- package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -0
- package/dist/adapters/sandbox/e2b/workflow.d.cts +27 -0
- package/dist/adapters/sandbox/e2b/workflow.d.ts +27 -0
- package/dist/adapters/sandbox/e2b/workflow.js +31 -0
- package/dist/adapters/sandbox/e2b/workflow.js.map +1 -0
- package/dist/adapters/sandbox/virtual/index.d.cts +4 -4
- package/dist/adapters/sandbox/virtual/index.d.ts +4 -4
- package/dist/adapters/sandbox/virtual/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/virtual/workflow.d.ts +2 -2
- package/dist/adapters/thread/anthropic/index.cjs +13 -6
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +10 -7
- package/dist/adapters/thread/anthropic/index.d.ts +10 -7
- package/dist/adapters/thread/anthropic/index.js +13 -6
- 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 +11 -6
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +9 -6
- package/dist/adapters/thread/google-genai/index.d.ts +9 -6
- package/dist/adapters/thread/google-genai/index.js +11 -6
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
- package/dist/adapters/thread/langchain/index.cjs +44 -5
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +25 -9
- package/dist/adapters/thread/langchain/index.d.ts +25 -9
- package/dist/adapters/thread/langchain/index.js +44 -6
- 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/index.cjs +27 -16
- 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 +27 -16
- package/dist/index.js.map +1 -1
- package/dist/{proxy-BK1ydQt0.d.ts → proxy-7e7v8ccg.d.ts} +1 -1
- package/dist/{proxy-BMAsMHdp.d.cts → proxy-CsB8r0RR.d.cts} +1 -1
- package/dist/{queries-DwnE2bu3.d.cts → queries-DVnukByF.d.cts} +1 -1
- package/dist/{queries-BCgJ9Sr5.d.ts → queries-kjlvsUfz.d.ts} +1 -1
- package/dist/{thread-manager-DOnQzImf.d.cts → thread-manager-B5qA4v7V.d.ts} +5 -3
- package/dist/{thread-manager-b4DML-qu.d.cts → thread-manager-D8C5QvLi.d.ts} +4 -2
- package/dist/{thread-manager-CH9krS3h.d.ts → thread-manager-DFJ3sKKU.d.cts} +4 -2
- package/dist/{thread-manager-Czhpxbt6.d.ts → thread-manager-DdVFl1IY.d.cts} +5 -3
- package/dist/{types-BDRDbm3h.d.cts → types-BZ75HpYd.d.ts} +14 -2
- package/dist/{types-mCVxKIZb.d.cts → types-BclYm5Ic.d.cts} +0 -4
- package/dist/{types-mCVxKIZb.d.ts → types-BclYm5Ic.d.ts} +0 -4
- package/dist/{types-DRnz-OZp.d.cts → types-BgsAwN3L.d.cts} +1 -1
- package/dist/{types-CvJyXDYt.d.ts → types-BtqbM1bO.d.ts} +1 -1
- package/dist/{types-DSOefLpY.d.cts → types-BuCEZ4dF.d.cts} +1 -1
- package/dist/{types-WNSeZbWa.d.ts → types-HbjqzyJH.d.cts} +14 -2
- package/dist/{types-DFUNSYbj.d.ts → types-yU5AINiP.d.ts} +1 -1
- package/dist/workflow.cjs +27 -16
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +7 -7
- package/dist/workflow.d.ts +7 -7
- package/dist/workflow.js +27 -16
- package/dist/workflow.js.map +1 -1
- package/package.json +21 -1
- package/src/adapters/sandbox/e2b/proxy.ts +56 -0
- package/src/adapters/thread/anthropic/activities.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +7 -2
- package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
- package/src/adapters/thread/anthropic/thread-manager.ts +13 -2
- package/src/adapters/thread/google-genai/activities.ts +3 -1
- package/src/adapters/thread/google-genai/model-invoker.ts +7 -3
- package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +13 -2
- package/src/adapters/thread/langchain/activities.ts +3 -1
- package/src/adapters/thread/langchain/hooks.test.ts +195 -0
- package/src/adapters/thread/langchain/hooks.ts +54 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +7 -4
- package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
- package/src/adapters/thread/langchain/thread-manager.ts +12 -1
- package/src/lib/.env +1 -0
- package/src/lib/session/session.ts +16 -5
- package/src/lib/subagent/handler.ts +5 -2
- package/src/lib/subagent/register.ts +7 -2
- package/src/lib/thread/index.ts +1 -0
- package/src/lib/thread/types.ts +13 -0
- package/src/lib/tool-router/router.ts +2 -5
- package/src/lib/tool-router/types.ts +0 -4
- package/src/tools/bash/.env +1 -0
- package/tsup.config.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeitlich",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -147,6 +147,26 @@
|
|
|
147
147
|
"default": "./dist/adapters/sandbox/daytona/workflow.js"
|
|
148
148
|
}
|
|
149
149
|
},
|
|
150
|
+
"./adapters/sandbox/e2b": {
|
|
151
|
+
"import": {
|
|
152
|
+
"types": "./dist/adapters/sandbox/e2b/index.d.ts",
|
|
153
|
+
"default": "./dist/adapters/sandbox/e2b/index.js"
|
|
154
|
+
},
|
|
155
|
+
"require": {
|
|
156
|
+
"types": "./dist/adapters/sandbox/e2b/index.d.ts",
|
|
157
|
+
"default": "./dist/adapters/sandbox/e2b/index.js"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"./adapters/sandbox/e2b/workflow": {
|
|
161
|
+
"import": {
|
|
162
|
+
"types": "./dist/adapters/sandbox/e2b/workflow.d.ts",
|
|
163
|
+
"default": "./dist/adapters/sandbox/e2b/workflow.js"
|
|
164
|
+
},
|
|
165
|
+
"require": {
|
|
166
|
+
"types": "./dist/adapters/sandbox/e2b/workflow.d.ts",
|
|
167
|
+
"default": "./dist/adapters/sandbox/e2b/workflow.js"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
150
170
|
"./adapters/sandbox/bedrock": {
|
|
151
171
|
"import": {
|
|
152
172
|
"types": "./dist/adapters/sandbox/bedrock/index.d.ts",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-safe proxy for E2B sandbox operations.
|
|
3
|
+
*
|
|
4
|
+
* Uses longer timeouts than in-memory providers since E2B
|
|
5
|
+
* sandboxes are remote and creation involves provisioning.
|
|
6
|
+
*
|
|
7
|
+
* Import this from `zeitlich/adapters/sandbox/e2b/workflow`
|
|
8
|
+
* in your Temporal workflow files.
|
|
9
|
+
*
|
|
10
|
+
* By default the scope is derived from `workflowInfo().workflowType`,
|
|
11
|
+
* so activities are automatically namespaced per workflow.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { proxyE2bSandboxOps } from 'zeitlich/adapters/sandbox/e2b/workflow';
|
|
16
|
+
*
|
|
17
|
+
* const sandbox = proxyE2bSandboxOps();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { proxyActivities, workflowInfo } from "@temporalio/workflow";
|
|
21
|
+
import type { SandboxOps } from "../../../lib/sandbox/types";
|
|
22
|
+
import type { E2bSandboxCreateOptions } from "./types";
|
|
23
|
+
|
|
24
|
+
const ADAPTER_PREFIX = "e2b";
|
|
25
|
+
|
|
26
|
+
export function proxyE2bSandboxOps(
|
|
27
|
+
scope?: string,
|
|
28
|
+
options?: Parameters<typeof proxyActivities>[0]
|
|
29
|
+
): SandboxOps {
|
|
30
|
+
const resolvedScope = scope ?? workflowInfo().workflowType;
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
|
|
34
|
+
options ?? {
|
|
35
|
+
startToCloseTimeout: "120s",
|
|
36
|
+
retry: {
|
|
37
|
+
maximumAttempts: 3,
|
|
38
|
+
initialInterval: "5s",
|
|
39
|
+
maximumInterval: "60s",
|
|
40
|
+
backoffCoefficient: 3,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const prefix = `${ADAPTER_PREFIX}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
|
|
46
|
+
const p = (key: string): string =>
|
|
47
|
+
`${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
createSandbox: acts[p("createSandbox")],
|
|
51
|
+
destroySandbox: acts[p("destroySandbox")],
|
|
52
|
+
pauseSandbox: acts[p("pauseSandbox")],
|
|
53
|
+
snapshotSandbox: acts[p("snapshotSandbox")],
|
|
54
|
+
forkSandbox: acts[p("forkSandbox")],
|
|
55
|
+
} as SandboxOps<E2bSandboxCreateOptions>;
|
|
56
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { ModelInvoker } from "../../../lib/model";
|
|
|
15
15
|
import {
|
|
16
16
|
createAnthropicThreadManager,
|
|
17
17
|
type AnthropicContent,
|
|
18
|
+
type AnthropicThreadManagerHooks,
|
|
18
19
|
} from "./thread-manager";
|
|
19
20
|
import {
|
|
20
21
|
createAnthropicModelInvoker,
|
|
@@ -33,6 +34,7 @@ export interface AnthropicAdapterConfig {
|
|
|
33
34
|
model?: string;
|
|
34
35
|
/** Maximum tokens to generate. Defaults to 16384. */
|
|
35
36
|
maxTokens?: number;
|
|
37
|
+
hooks?: AnthropicThreadManagerHooks;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -201,6 +203,7 @@ export function createAnthropicAdapter(
|
|
|
201
203
|
...(config.maxTokens !== undefined && maxTokens === undefined
|
|
202
204
|
? { maxTokens: config.maxTokens }
|
|
203
205
|
: {}),
|
|
206
|
+
hooks: config.hooks,
|
|
204
207
|
};
|
|
205
208
|
return createAnthropicModelInvoker(invokerConfig);
|
|
206
209
|
};
|
|
@@ -2,7 +2,7 @@ import type Redis from "ioredis";
|
|
|
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
|
-
import { createAnthropicThreadManager } from "./thread-manager";
|
|
5
|
+
import { createAnthropicThreadManager, type AnthropicThreadManagerHooks } from "./thread-manager";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
|
|
8
8
|
export interface AnthropicModelInvokerConfig {
|
|
@@ -11,6 +11,7 @@ export interface AnthropicModelInvokerConfig {
|
|
|
11
11
|
model: string;
|
|
12
12
|
/** Maximum tokens to generate. Defaults to 16384. */
|
|
13
13
|
maxTokens?: number;
|
|
14
|
+
hooks?: AnthropicThreadManagerHooks;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function toAnthropicTools(
|
|
@@ -52,13 +53,14 @@ export function createAnthropicModelInvoker({
|
|
|
52
53
|
client,
|
|
53
54
|
model,
|
|
54
55
|
maxTokens = 16384,
|
|
56
|
+
hooks,
|
|
55
57
|
}: AnthropicModelInvokerConfig) {
|
|
56
58
|
return async function invokeAnthropicModel(
|
|
57
59
|
config: ModelInvokerConfig,
|
|
58
60
|
): Promise<AgentResponse<Anthropic.Messages.Message>> {
|
|
59
61
|
const { threadId, threadKey, state } = config;
|
|
60
62
|
|
|
61
|
-
const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
|
|
63
|
+
const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey, hooks });
|
|
62
64
|
const { messages, system } = await thread.prepareForInvocation();
|
|
63
65
|
|
|
64
66
|
const anthropicTools = toAnthropicTools(state.tools);
|
|
@@ -106,12 +108,14 @@ export async function invokeAnthropicModel({
|
|
|
106
108
|
client,
|
|
107
109
|
model,
|
|
108
110
|
maxTokens,
|
|
111
|
+
hooks,
|
|
109
112
|
config,
|
|
110
113
|
}: {
|
|
111
114
|
redis: Redis;
|
|
112
115
|
client: Anthropic;
|
|
113
116
|
model: string;
|
|
114
117
|
maxTokens?: number;
|
|
118
|
+
hooks?: AnthropicThreadManagerHooks;
|
|
115
119
|
config: ModelInvokerConfig;
|
|
116
120
|
}): Promise<AgentResponse<Anthropic.Messages.Message>> {
|
|
117
121
|
const invoker = createAnthropicModelInvoker({
|
|
@@ -119,6 +123,7 @@ export async function invokeAnthropicModel({
|
|
|
119
123
|
client,
|
|
120
124
|
model,
|
|
121
125
|
maxTokens,
|
|
126
|
+
hooks,
|
|
122
127
|
});
|
|
123
128
|
return invoker(config);
|
|
124
129
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { StoredMessage } from "./thread-manager";
|
|
3
|
+
import { createAnthropicThreadManager } from "./thread-manager";
|
|
4
|
+
|
|
5
|
+
function createMockRedis(stored: StoredMessage[]) {
|
|
6
|
+
return {
|
|
7
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
8
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
9
|
+
del: vi.fn().mockResolvedValue(1),
|
|
10
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
11
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
12
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
13
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const systemMsg: StoredMessage = {
|
|
18
|
+
id: "sys-1",
|
|
19
|
+
message: { role: "user", content: "You are helpful." },
|
|
20
|
+
isSystem: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const userMsg: StoredMessage = {
|
|
24
|
+
id: "msg-1",
|
|
25
|
+
message: { role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const assistantMsg: StoredMessage = {
|
|
29
|
+
id: "msg-2",
|
|
30
|
+
message: { role: "assistant", content: [{ type: "text", text: "Hi there!" }] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("Anthropic thread manager hooks", () => {
|
|
34
|
+
describe("onPrepareMessage", () => {
|
|
35
|
+
it("transforms stored messages before system extraction and merge", async () => {
|
|
36
|
+
const hook = vi.fn((msg: StoredMessage) => {
|
|
37
|
+
if (msg.isSystem) return msg;
|
|
38
|
+
const firstBlock = (msg.message.content as Array<{ text: string }>)[0];
|
|
39
|
+
return {
|
|
40
|
+
...msg,
|
|
41
|
+
message: {
|
|
42
|
+
...msg.message,
|
|
43
|
+
content: [{ type: "text" as const, text: `[modified] ${firstBlock?.text}` }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const redis = createMockRedis([systemMsg, userMsg, assistantMsg]);
|
|
49
|
+
const tm = createAnthropicThreadManager({
|
|
50
|
+
redis: redis as never,
|
|
51
|
+
threadId: "t1",
|
|
52
|
+
hooks: { onPrepareMessage: hook },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { messages, system } = await tm.prepareForInvocation();
|
|
56
|
+
|
|
57
|
+
expect(hook).toHaveBeenCalledTimes(3);
|
|
58
|
+
expect(hook).toHaveBeenCalledWith(systemMsg, 0, [systemMsg, userMsg, assistantMsg]);
|
|
59
|
+
expect(system).toBe("You are helpful.");
|
|
60
|
+
expect(messages[0]?.content).toEqual([{ type: "text", text: "[modified] Hello" }]);
|
|
61
|
+
expect(messages[1]?.content).toEqual([{ type: "text", text: "[modified] Hi there!" }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is not called when not configured", async () => {
|
|
65
|
+
const redis = createMockRedis([userMsg]);
|
|
66
|
+
const tm = createAnthropicThreadManager({
|
|
67
|
+
redis: redis as never,
|
|
68
|
+
threadId: "t1",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { messages } = await tm.prepareForInvocation();
|
|
72
|
+
expect(messages).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("onPreparedMessage", () => {
|
|
77
|
+
it("transforms SDK-native messages after merge", async () => {
|
|
78
|
+
const hook = vi.fn((msg) => ({
|
|
79
|
+
...msg,
|
|
80
|
+
content: [{ type: "text" as const, text: "[post] done" }],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const redis = createMockRedis([userMsg, assistantMsg]);
|
|
84
|
+
const tm = createAnthropicThreadManager({
|
|
85
|
+
redis: redis as never,
|
|
86
|
+
threadId: "t1",
|
|
87
|
+
hooks: { onPreparedMessage: hook },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { messages } = await tm.prepareForInvocation();
|
|
91
|
+
|
|
92
|
+
expect(hook).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(messages[0]?.content).toEqual([{ type: "text", text: "[post] done" }]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("receives the full prepared messages array", async () => {
|
|
97
|
+
const hook = vi.fn((msg) => msg);
|
|
98
|
+
|
|
99
|
+
const redis = createMockRedis([userMsg, assistantMsg]);
|
|
100
|
+
const tm = createAnthropicThreadManager({
|
|
101
|
+
redis: redis as never,
|
|
102
|
+
threadId: "t1",
|
|
103
|
+
hooks: { onPreparedMessage: hook },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await tm.prepareForInvocation();
|
|
107
|
+
|
|
108
|
+
const args = hook.mock.calls[0] as unknown as [unknown, number, unknown[]];
|
|
109
|
+
expect(args[2]).toHaveLength(2);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("both hooks combined", () => {
|
|
114
|
+
it("runs onPrepareMessage before onPreparedMessage", async () => {
|
|
115
|
+
const order: string[] = [];
|
|
116
|
+
|
|
117
|
+
const redis = createMockRedis([userMsg]);
|
|
118
|
+
const tm = createAnthropicThreadManager({
|
|
119
|
+
redis: redis as never,
|
|
120
|
+
threadId: "t1",
|
|
121
|
+
hooks: {
|
|
122
|
+
onPrepareMessage: (msg) => {
|
|
123
|
+
order.push("pre");
|
|
124
|
+
return msg;
|
|
125
|
+
},
|
|
126
|
+
onPreparedMessage: (msg) => {
|
|
127
|
+
order.push("post");
|
|
128
|
+
return msg;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await tm.prepareForInvocation();
|
|
134
|
+
expect(order).toEqual(["pre", "post"]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createThreadManager,
|
|
6
6
|
type ProviderThreadManager,
|
|
7
7
|
type ThreadManagerConfig,
|
|
8
|
+
type ThreadManagerHooks,
|
|
8
9
|
} from "../../../lib/thread";
|
|
9
10
|
|
|
10
11
|
/** SDK-native content type for Anthropic human messages */
|
|
@@ -20,11 +21,14 @@ export interface StoredMessage {
|
|
|
20
21
|
isSystem?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export type AnthropicThreadManagerHooks = ThreadManagerHooks<StoredMessage, Anthropic.Messages.MessageParam>;
|
|
25
|
+
|
|
23
26
|
export interface AnthropicThreadManagerConfig {
|
|
24
27
|
redis: Redis;
|
|
25
28
|
threadId: string;
|
|
26
29
|
/** Thread key, defaults to 'messages' */
|
|
27
30
|
key?: string;
|
|
31
|
+
hooks?: AnthropicThreadManagerHooks;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** Prepared payload ready to send to the Anthropic API */
|
|
@@ -165,11 +169,15 @@ export function createAnthropicThreadManager(
|
|
|
165
169
|
|
|
166
170
|
async prepareForInvocation(): Promise<AnthropicInvocationPayload> {
|
|
167
171
|
const stored = await base.load();
|
|
172
|
+
const { onPrepareMessage, onPreparedMessage } = config.hooks ?? {};
|
|
173
|
+
const mapped = onPrepareMessage
|
|
174
|
+
? stored.map((msg, i) => onPrepareMessage(msg, i, stored))
|
|
175
|
+
: stored;
|
|
168
176
|
|
|
169
177
|
let system: string | undefined;
|
|
170
178
|
const conversationMessages: Anthropic.Messages.MessageParam[] = [];
|
|
171
179
|
|
|
172
|
-
for (const item of
|
|
180
|
+
for (const item of mapped) {
|
|
173
181
|
if (item.isSystem) {
|
|
174
182
|
system =
|
|
175
183
|
typeof item.message.content === "string"
|
|
@@ -180,8 +188,11 @@ export function createAnthropicThreadManager(
|
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
const messages = mergeConsecutiveMessages(conversationMessages);
|
|
183
192
|
return {
|
|
184
|
-
messages:
|
|
193
|
+
messages: onPreparedMessage
|
|
194
|
+
? messages.map((msg, i) => onPreparedMessage(msg, i, messages))
|
|
195
|
+
: messages,
|
|
185
196
|
...(system ? { system } : {}),
|
|
186
197
|
};
|
|
187
198
|
},
|
|
@@ -15,6 +15,7 @@ import type { ModelInvoker } from "../../../lib/model";
|
|
|
15
15
|
import {
|
|
16
16
|
createGoogleGenAIThreadManager,
|
|
17
17
|
type GoogleGenAIContent,
|
|
18
|
+
type GoogleGenAIThreadManagerHooks,
|
|
18
19
|
} from "./thread-manager";
|
|
19
20
|
import { createGoogleGenAIModelInvoker } from "./model-invoker";
|
|
20
21
|
|
|
@@ -31,6 +32,7 @@ export interface GoogleGenAIAdapterConfig {
|
|
|
31
32
|
client?: GoogleGenAI;
|
|
32
33
|
/** Default model name (e.g. 'gemini-2.5-flash'). If omitted, use `createModelInvoker()` */
|
|
33
34
|
model?: string;
|
|
35
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -222,7 +224,7 @@ export function createGoogleGenAIAdapter(
|
|
|
222
224
|
model: string,
|
|
223
225
|
client: GoogleGenAI
|
|
224
226
|
): ModelInvoker<Content> =>
|
|
225
|
-
createGoogleGenAIModelInvoker({ redis, client, model });
|
|
227
|
+
createGoogleGenAIModelInvoker({ redis, client, model, hooks: config.hooks });
|
|
226
228
|
|
|
227
229
|
const invoker: ModelInvoker<Content> =
|
|
228
230
|
config.model && config.client
|
|
@@ -2,13 +2,14 @@ import type Redis from "ioredis";
|
|
|
2
2
|
import type { GoogleGenAI, Content, FunctionDeclaration } from "@google/genai";
|
|
3
3
|
import type { SerializableToolDefinition } from "../../../lib/types";
|
|
4
4
|
import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
|
|
5
|
-
import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
5
|
+
import { createGoogleGenAIThreadManager, type GoogleGenAIThreadManagerHooks } from "./thread-manager";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
|
|
8
8
|
export interface GoogleGenAIModelInvokerConfig {
|
|
9
9
|
redis: Redis;
|
|
10
10
|
client: GoogleGenAI;
|
|
11
11
|
model: string;
|
|
12
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function toFunctionDeclarations(
|
|
@@ -49,13 +50,14 @@ export function createGoogleGenAIModelInvoker({
|
|
|
49
50
|
redis,
|
|
50
51
|
client,
|
|
51
52
|
model,
|
|
53
|
+
hooks,
|
|
52
54
|
}: GoogleGenAIModelInvokerConfig) {
|
|
53
55
|
return async function invokeGoogleGenAIModel(
|
|
54
56
|
config: ModelInvokerConfig,
|
|
55
57
|
): Promise<AgentResponse<Content>> {
|
|
56
58
|
const { threadId, threadKey, state } = config;
|
|
57
59
|
|
|
58
|
-
const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey });
|
|
60
|
+
const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey, hooks });
|
|
59
61
|
const { contents, systemInstruction } =
|
|
60
62
|
await thread.prepareForInvocation();
|
|
61
63
|
|
|
@@ -104,13 +106,15 @@ export async function invokeGoogleGenAIModel({
|
|
|
104
106
|
redis,
|
|
105
107
|
client,
|
|
106
108
|
model,
|
|
109
|
+
hooks,
|
|
107
110
|
config,
|
|
108
111
|
}: {
|
|
109
112
|
redis: Redis;
|
|
110
113
|
client: GoogleGenAI;
|
|
111
114
|
model: string;
|
|
115
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
112
116
|
config: ModelInvokerConfig;
|
|
113
117
|
}): Promise<AgentResponse<Content>> {
|
|
114
|
-
const invoker = createGoogleGenAIModelInvoker({ redis, client, model });
|
|
118
|
+
const invoker = createGoogleGenAIModelInvoker({ redis, client, model, hooks });
|
|
115
119
|
return invoker(config);
|
|
116
120
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { Content } from "@google/genai";
|
|
3
|
+
import type { StoredContent } from "./thread-manager";
|
|
4
|
+
import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
5
|
+
|
|
6
|
+
function createMockRedis(stored: StoredContent[]) {
|
|
7
|
+
return {
|
|
8
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
del: vi.fn().mockResolvedValue(1),
|
|
11
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
12
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
13
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
14
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const systemContent: StoredContent = {
|
|
19
|
+
id: "sys-1",
|
|
20
|
+
content: { role: "system", parts: [{ text: "You are helpful." }] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const userContent: StoredContent = {
|
|
24
|
+
id: "msg-1",
|
|
25
|
+
content: { role: "user", parts: [{ text: "Hello" }] },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const modelContent: StoredContent = {
|
|
29
|
+
id: "msg-2",
|
|
30
|
+
content: { role: "model", parts: [{ text: "Hi there!" }] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("Google GenAI thread manager hooks", () => {
|
|
34
|
+
describe("onPrepareMessage", () => {
|
|
35
|
+
it("transforms stored messages before system extraction and merge", async () => {
|
|
36
|
+
const hook = vi.fn((msg: StoredContent) => {
|
|
37
|
+
if (msg.content.role === "system") return msg;
|
|
38
|
+
return {
|
|
39
|
+
...msg,
|
|
40
|
+
content: {
|
|
41
|
+
...msg.content,
|
|
42
|
+
parts: [{ text: `[modified] ${msg.content.parts?.[0]?.text ?? ""}` }],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const redis = createMockRedis([systemContent, userContent, modelContent]);
|
|
48
|
+
const tm = createGoogleGenAIThreadManager({
|
|
49
|
+
redis: redis as never,
|
|
50
|
+
threadId: "t1",
|
|
51
|
+
hooks: { onPrepareMessage: hook },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { contents, systemInstruction } = await tm.prepareForInvocation();
|
|
55
|
+
|
|
56
|
+
expect(hook).toHaveBeenCalledTimes(3);
|
|
57
|
+
expect(hook).toHaveBeenCalledWith(systemContent, 0, [systemContent, userContent, modelContent]);
|
|
58
|
+
expect(systemInstruction).toBe("You are helpful.");
|
|
59
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("[modified] Hello");
|
|
60
|
+
expect(contents[1]?.parts?.[0]?.text).toBe("[modified] Hi there!");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("is not called when not configured", async () => {
|
|
64
|
+
const redis = createMockRedis([userContent]);
|
|
65
|
+
const tm = createGoogleGenAIThreadManager({
|
|
66
|
+
redis: redis as never,
|
|
67
|
+
threadId: "t1",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { contents } = await tm.prepareForInvocation();
|
|
71
|
+
expect(contents).toHaveLength(1);
|
|
72
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("Hello");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("onPreparedMessage", () => {
|
|
77
|
+
it("transforms SDK-native Content after merge", async () => {
|
|
78
|
+
const hook = vi.fn((msg: Content) => ({
|
|
79
|
+
...msg,
|
|
80
|
+
parts: [{ text: `[post] ${msg.parts?.[0]?.text ?? ""}` }],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const redis = createMockRedis([userContent, modelContent]);
|
|
84
|
+
const tm = createGoogleGenAIThreadManager({
|
|
85
|
+
redis: redis as never,
|
|
86
|
+
threadId: "t1",
|
|
87
|
+
hooks: { onPreparedMessage: hook },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { contents } = await tm.prepareForInvocation();
|
|
91
|
+
|
|
92
|
+
expect(hook).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("[post] Hello");
|
|
94
|
+
expect(contents[1]?.parts?.[0]?.text).toBe("[post] Hi there!");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("receives the full prepared contents array", async () => {
|
|
98
|
+
const hook = vi.fn((msg: Content) => msg);
|
|
99
|
+
|
|
100
|
+
const redis = createMockRedis([userContent, modelContent]);
|
|
101
|
+
const tm = createGoogleGenAIThreadManager({
|
|
102
|
+
redis: redis as never,
|
|
103
|
+
threadId: "t1",
|
|
104
|
+
hooks: { onPreparedMessage: hook },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await tm.prepareForInvocation();
|
|
108
|
+
|
|
109
|
+
const args = hook.mock.calls[0] as unknown as [Content, number, Content[]];
|
|
110
|
+
expect(args[2]).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("both hooks combined", () => {
|
|
115
|
+
it("runs onPrepareMessage before onPreparedMessage", async () => {
|
|
116
|
+
const order: string[] = [];
|
|
117
|
+
|
|
118
|
+
const redis = createMockRedis([userContent]);
|
|
119
|
+
const tm = createGoogleGenAIThreadManager({
|
|
120
|
+
redis: redis as never,
|
|
121
|
+
threadId: "t1",
|
|
122
|
+
hooks: {
|
|
123
|
+
onPrepareMessage: (msg) => {
|
|
124
|
+
order.push("pre");
|
|
125
|
+
return msg;
|
|
126
|
+
},
|
|
127
|
+
onPreparedMessage: (msg) => {
|
|
128
|
+
order.push("post");
|
|
129
|
+
return msg;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await tm.prepareForInvocation();
|
|
135
|
+
expect(order).toEqual(["pre", "post"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("onPreparedMessage sees results of onPrepareMessage", async () => {
|
|
139
|
+
const redis = createMockRedis([userContent]);
|
|
140
|
+
const tm = createGoogleGenAIThreadManager({
|
|
141
|
+
redis: redis as never,
|
|
142
|
+
threadId: "t1",
|
|
143
|
+
hooks: {
|
|
144
|
+
onPrepareMessage: (msg) => ({
|
|
145
|
+
...msg,
|
|
146
|
+
content: { ...msg.content, parts: [{ text: "replaced" }] },
|
|
147
|
+
}),
|
|
148
|
+
onPreparedMessage: (msg) => {
|
|
149
|
+
expect(msg.parts?.[0]?.text).toBe("replaced");
|
|
150
|
+
return msg;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { contents } = await tm.prepareForInvocation();
|
|
156
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("replaced");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createThreadManager,
|
|
5
5
|
type ProviderThreadManager,
|
|
6
6
|
type ThreadManagerConfig,
|
|
7
|
+
type ThreadManagerHooks,
|
|
7
8
|
} from "../../../lib/thread";
|
|
8
9
|
import type { GoogleGenAIToolResponse } from "./activities";
|
|
9
10
|
|
|
@@ -16,11 +17,14 @@ export interface StoredContent {
|
|
|
16
17
|
content: Content;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export type GoogleGenAIThreadManagerHooks = ThreadManagerHooks<StoredContent, Content>;
|
|
21
|
+
|
|
19
22
|
export interface GoogleGenAIThreadManagerConfig {
|
|
20
23
|
redis: Redis;
|
|
21
24
|
threadId: string;
|
|
22
25
|
/** Thread key, defaults to 'messages' */
|
|
23
26
|
key?: string;
|
|
27
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** Prepared payload ready to send to the Google GenAI API */
|
|
@@ -141,11 +145,15 @@ export function createGoogleGenAIThreadManager(
|
|
|
141
145
|
|
|
142
146
|
async prepareForInvocation(): Promise<GoogleGenAIInvocationPayload> {
|
|
143
147
|
const stored = await base.load();
|
|
148
|
+
const { onPrepareMessage, onPreparedMessage } = config.hooks ?? {};
|
|
149
|
+
const mapped = onPrepareMessage
|
|
150
|
+
? stored.map((msg, i) => onPrepareMessage(msg, i, stored))
|
|
151
|
+
: stored;
|
|
144
152
|
|
|
145
153
|
let systemInstruction: string | undefined;
|
|
146
154
|
const conversationContents: Content[] = [];
|
|
147
155
|
|
|
148
|
-
for (const item of
|
|
156
|
+
for (const item of mapped) {
|
|
149
157
|
if (item.content.role === "system") {
|
|
150
158
|
systemInstruction = item.content.parts?.[0]?.text;
|
|
151
159
|
} else {
|
|
@@ -153,8 +161,11 @@ export function createGoogleGenAIThreadManager(
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
const contents = mergeConsecutiveContents(conversationContents);
|
|
156
165
|
return {
|
|
157
|
-
contents:
|
|
166
|
+
contents: onPreparedMessage
|
|
167
|
+
? contents.map((msg, i) => onPreparedMessage(msg, i, contents))
|
|
168
|
+
: contents,
|
|
158
169
|
...(systemInstruction ? { systemInstruction } : {}),
|
|
159
170
|
};
|
|
160
171
|
},
|
|
@@ -17,6 +17,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
|
17
17
|
import {
|
|
18
18
|
createLangChainThreadManager,
|
|
19
19
|
type LangChainContent,
|
|
20
|
+
type LangChainThreadManagerHooks,
|
|
20
21
|
} from "./thread-manager";
|
|
21
22
|
import { createLangChainModelInvoker } from "./model-invoker";
|
|
22
23
|
|
|
@@ -30,6 +31,7 @@ export interface LangChainAdapterConfig {
|
|
|
30
31
|
/** Optional default model — if omitted, use `createModelInvoker()` to create invokers later */
|
|
31
32
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
33
|
model?: BaseChatModel<any>;
|
|
34
|
+
hooks?: LangChainThreadManagerHooks;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -177,7 +179,7 @@ export function createLangChainAdapter(
|
|
|
177
179
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
180
|
model: BaseChatModel<any>,
|
|
179
181
|
): ModelInvoker<StoredMessage> =>
|
|
180
|
-
createLangChainModelInvoker({ redis, model });
|
|
182
|
+
createLangChainModelInvoker({ redis, model, hooks: config.hooks });
|
|
181
183
|
|
|
182
184
|
const invoker: ModelInvoker<StoredMessage> = config.model
|
|
183
185
|
? makeInvoker(config.model)
|