zeitlich 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{activities-LVQdLF6I.d.ts → activities-DE3_q9yq.d.ts} +5 -2
  2. package/dist/{activities-BEJRyDVU.d.cts → activities-p8PDlRIK.d.cts} +5 -2
  3. package/dist/adapters/thread/anthropic/index.cjs +13 -6
  4. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  5. package/dist/adapters/thread/anthropic/index.d.cts +8 -5
  6. package/dist/adapters/thread/anthropic/index.d.ts +8 -5
  7. package/dist/adapters/thread/anthropic/index.js +13 -6
  8. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  9. package/dist/adapters/thread/anthropic/workflow.d.cts +2 -2
  10. package/dist/adapters/thread/anthropic/workflow.d.ts +2 -2
  11. package/dist/adapters/thread/google-genai/index.cjs +11 -6
  12. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  13. package/dist/adapters/thread/google-genai/index.d.cts +7 -4
  14. package/dist/adapters/thread/google-genai/index.d.ts +7 -4
  15. package/dist/adapters/thread/google-genai/index.js +11 -6
  16. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  17. package/dist/adapters/thread/google-genai/workflow.d.cts +2 -2
  18. package/dist/adapters/thread/google-genai/workflow.d.ts +2 -2
  19. package/dist/adapters/thread/langchain/index.cjs +32 -5
  20. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  21. package/dist/adapters/thread/langchain/index.d.cts +20 -7
  22. package/dist/adapters/thread/langchain/index.d.ts +20 -7
  23. package/dist/adapters/thread/langchain/index.js +32 -6
  24. package/dist/adapters/thread/langchain/index.js.map +1 -1
  25. package/dist/adapters/thread/langchain/workflow.d.cts +2 -2
  26. package/dist/adapters/thread/langchain/workflow.d.ts +2 -2
  27. package/dist/index.d.cts +2 -2
  28. package/dist/index.d.ts +2 -2
  29. package/dist/{thread-manager-DOnQzImf.d.cts → thread-manager-Bh9x847n.d.ts} +5 -3
  30. package/dist/{thread-manager-CH9krS3h.d.ts → thread-manager-BlHua5_v.d.cts} +4 -2
  31. package/dist/{thread-manager-Czhpxbt6.d.ts → thread-manager-Bz8txKKj.d.cts} +5 -3
  32. package/dist/{thread-manager-b4DML-qu.d.cts → thread-manager-dzaJHQEA.d.ts} +4 -2
  33. package/dist/{types-BDRDbm3h.d.cts → types-BfIQABzu.d.cts} +13 -1
  34. package/dist/{types-WNSeZbWa.d.ts → types-CIkYBoF8.d.ts} +13 -1
  35. package/package.json +1 -1
  36. package/src/adapters/thread/anthropic/activities.ts +3 -0
  37. package/src/adapters/thread/anthropic/model-invoker.ts +7 -2
  38. package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
  39. package/src/adapters/thread/anthropic/thread-manager.ts +13 -2
  40. package/src/adapters/thread/google-genai/activities.ts +3 -1
  41. package/src/adapters/thread/google-genai/model-invoker.ts +7 -3
  42. package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
  43. package/src/adapters/thread/google-genai/thread-manager.ts +13 -2
  44. package/src/adapters/thread/langchain/activities.ts +3 -1
  45. package/src/adapters/thread/langchain/hooks.ts +37 -0
  46. package/src/adapters/thread/langchain/index.ts +3 -0
  47. package/src/adapters/thread/langchain/model-invoker.ts +7 -4
  48. package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
  49. package/src/adapters/thread/langchain/thread-manager.ts +12 -1
  50. package/src/lib/thread/index.ts +1 -0
  51. package/src/lib/thread/types.ts +13 -0
@@ -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 stored) {
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: mergeConsecutiveMessages(conversationMessages),
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 stored) {
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: mergeConsecutiveContents(conversationContents),
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)
@@ -0,0 +1,37 @@
1
+ import type { BaseMessage, MessageContent } from "@langchain/core/messages";
2
+
3
+ type ContentBlock = MessageContent extends (infer U)[] | string ? U : never;
4
+
5
+ /**
6
+ * Creates an `onPreparedMessage` hook that appends a cache-point content
7
+ * block to the last message in the thread.
8
+ *
9
+ * Skips appending if the last message already contains a block with the
10
+ * same `type`.
11
+ */
12
+ export function appendCachePoint(
13
+ block: ContentBlock,
14
+ ): (message: BaseMessage, index: number, messages: readonly BaseMessage[]) => BaseMessage {
15
+ return (message, index, messages) => {
16
+ if (index !== messages.length - 1) {
17
+ return message;
18
+ }
19
+
20
+ const { content } = message;
21
+
22
+ if (Array.isArray(content)) {
23
+ if (content.some((b) => b.type === block.type)) {
24
+ return message;
25
+ }
26
+ message.content = [...content, block];
27
+ return message;
28
+ }
29
+
30
+ if (typeof content === "string") {
31
+ message.content = [{ type: "text", text: content }, block] satisfies MessageContent;
32
+ return message;
33
+ }
34
+
35
+ return message;
36
+ };
37
+ }
@@ -39,3 +39,6 @@ export {
39
39
  invokeLangChainModel,
40
40
  type LangChainModelInvokerConfig,
41
41
  } from "./model-invoker";
42
+
43
+ // Hooks / utilities
44
+ export { appendCachePoint } from "./hooks";
@@ -3,12 +3,13 @@ import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
3
3
  import type { StoredMessage } from "@langchain/core/messages";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
6
- import { createLangChainThreadManager } from "./thread-manager";
6
+ import { createLangChainThreadManager, type LangChainThreadManagerHooks } from "./thread-manager";
7
7
 
8
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
9
  export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> = BaseChatModel<any>> {
10
10
  redis: Redis;
11
11
  model: TModel;
12
+ hooks?: LangChainThreadManagerHooks;
12
13
  }
13
14
 
14
15
  /**
@@ -32,14 +33,14 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
32
33
  */
33
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
35
  export function createLangChainModelInvoker<TModel extends BaseChatModel<any> = BaseChatModel<any>>(
35
- { redis, model }: LangChainModelInvokerConfig<TModel>,
36
+ { redis, model, hooks }: LangChainModelInvokerConfig<TModel>,
36
37
  ) {
37
38
  return async function invokeLangChainModel(
38
39
  config: ModelInvokerConfig,
39
40
  ): Promise<AgentResponse<StoredMessage>> {
40
41
  const { threadId, threadKey, agentName, state, metadata } = config;
41
42
 
42
- const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
43
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey, hooks });
43
44
  const runId = uuidv4();
44
45
 
45
46
  const { messages } = await thread.prepareForInvocation();
@@ -86,12 +87,14 @@ export function createLangChainModelInvoker<TModel extends BaseChatModel<any> =
86
87
  export async function invokeLangChainModel<TModel extends BaseChatModel<any> = BaseChatModel<any>>({
87
88
  redis,
88
89
  model,
90
+ hooks,
89
91
  config,
90
92
  }: {
91
93
  redis: Redis;
92
94
  config: ModelInvokerConfig;
93
95
  model: TModel;
96
+ hooks?: LangChainThreadManagerHooks;
94
97
  }): Promise<AgentResponse<StoredMessage>> {
95
- const invoker = createLangChainModelInvoker({ redis, model });
98
+ const invoker = createLangChainModelInvoker({ redis, model, hooks });
96
99
  return invoker(config);
97
100
  }