zeitlich 0.2.25 → 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 (117) hide show
  1. package/dist/activities-DE3_q9yq.d.ts +140 -0
  2. package/dist/activities-p8PDlRIK.d.cts +140 -0
  3. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/virtual/index.d.cts +8 -7
  5. package/dist/adapters/sandbox/virtual/index.d.ts +8 -7
  6. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  7. package/dist/adapters/sandbox/virtual/workflow.d.cts +3 -2
  8. package/dist/adapters/sandbox/virtual/workflow.d.ts +3 -2
  9. package/dist/adapters/thread/anthropic/index.cjs +363 -0
  10. package/dist/adapters/thread/anthropic/index.cjs.map +1 -0
  11. package/dist/adapters/thread/anthropic/index.d.cts +151 -0
  12. package/dist/adapters/thread/anthropic/index.d.ts +151 -0
  13. package/dist/adapters/thread/anthropic/index.js +358 -0
  14. package/dist/adapters/thread/anthropic/index.js.map +1 -0
  15. package/dist/adapters/thread/anthropic/workflow.cjs +38 -0
  16. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -0
  17. package/dist/adapters/thread/anthropic/workflow.d.cts +37 -0
  18. package/dist/adapters/thread/anthropic/workflow.d.ts +37 -0
  19. package/dist/adapters/thread/anthropic/workflow.js +36 -0
  20. package/dist/adapters/thread/anthropic/workflow.js.map +1 -0
  21. package/dist/adapters/thread/google-genai/index.cjs +102 -99
  22. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  23. package/dist/adapters/thread/google-genai/index.d.cts +14 -113
  24. package/dist/adapters/thread/google-genai/index.d.ts +14 -113
  25. package/dist/adapters/thread/google-genai/index.js +103 -99
  26. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  27. package/dist/adapters/thread/google-genai/workflow.cjs +9 -4
  28. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  29. package/dist/adapters/thread/google-genai/workflow.d.cts +10 -5
  30. package/dist/adapters/thread/google-genai/workflow.d.ts +10 -5
  31. package/dist/adapters/thread/google-genai/workflow.js +9 -4
  32. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  33. package/dist/adapters/thread/langchain/index.cjs +73 -63
  34. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  35. package/dist/adapters/thread/langchain/index.d.cts +39 -40
  36. package/dist/adapters/thread/langchain/index.d.ts +39 -40
  37. package/dist/adapters/thread/langchain/index.js +73 -64
  38. package/dist/adapters/thread/langchain/index.js.map +1 -1
  39. package/dist/adapters/thread/langchain/workflow.cjs +9 -4
  40. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  41. package/dist/adapters/thread/langchain/workflow.d.cts +10 -5
  42. package/dist/adapters/thread/langchain/workflow.d.ts +10 -5
  43. package/dist/adapters/thread/langchain/workflow.js +9 -4
  44. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  45. package/dist/index.cjs +27 -10
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +13 -12
  48. package/dist/index.d.ts +13 -12
  49. package/dist/index.js +28 -11
  50. package/dist/index.js.map +1 -1
  51. package/dist/proxy-BK1ydQt0.d.ts +24 -0
  52. package/dist/proxy-BMAsMHdp.d.cts +24 -0
  53. package/dist/{queries-DwBe2CAA.d.ts → queries-BCgJ9Sr5.d.ts} +1 -1
  54. package/dist/{queries-BYGBImeC.d.cts → queries-DwnE2bu3.d.cts} +1 -1
  55. package/dist/thread-manager-Bh9x847n.d.ts +31 -0
  56. package/dist/thread-manager-BlHua5_v.d.cts +39 -0
  57. package/dist/thread-manager-Bz8txKKj.d.cts +31 -0
  58. package/dist/thread-manager-dzaJHQEA.d.ts +39 -0
  59. package/dist/types-BfIQABzu.d.cts +73 -0
  60. package/dist/types-CIkYBoF8.d.ts +73 -0
  61. package/dist/{types-hmferhc2.d.ts → types-CvJyXDYt.d.ts} +44 -123
  62. package/dist/{types-LVKmCNds.d.ts → types-DFUNSYbj.d.ts} +1 -1
  63. package/dist/{types-Bf8KV0Ci.d.cts → types-DRnz-OZp.d.cts} +1 -1
  64. package/dist/{types-7PeMi1bD.d.cts → types-DSOefLpY.d.cts} +44 -123
  65. package/dist/{types-D_igp10o.d.cts → types-mCVxKIZb.d.cts} +233 -137
  66. package/dist/{types-D_igp10o.d.ts → types-mCVxKIZb.d.ts} +233 -137
  67. package/dist/workflow.cjs +25 -9
  68. package/dist/workflow.cjs.map +1 -1
  69. package/dist/workflow.d.cts +11 -11
  70. package/dist/workflow.d.ts +11 -11
  71. package/dist/workflow.js +26 -10
  72. package/dist/workflow.js.map +1 -1
  73. package/package.json +26 -1
  74. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +8 -3
  75. package/src/adapters/thread/anthropic/activities.ts +226 -0
  76. package/src/adapters/thread/anthropic/index.ts +44 -0
  77. package/src/adapters/thread/anthropic/model-invoker.ts +129 -0
  78. package/src/adapters/thread/anthropic/proxy.ts +33 -0
  79. package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
  80. package/src/adapters/thread/anthropic/thread-manager.ts +202 -0
  81. package/src/adapters/thread/google-genai/activities.ts +110 -33
  82. package/src/adapters/thread/google-genai/index.ts +3 -1
  83. package/src/adapters/thread/google-genai/model-invoker.ts +13 -42
  84. package/src/adapters/thread/google-genai/proxy.ts +6 -34
  85. package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
  86. package/src/adapters/thread/google-genai/thread-manager.ts +96 -105
  87. package/src/adapters/thread/langchain/activities.ts +56 -21
  88. package/src/adapters/thread/langchain/hooks.ts +37 -0
  89. package/src/adapters/thread/langchain/index.ts +6 -1
  90. package/src/adapters/thread/langchain/model-invoker.ts +13 -12
  91. package/src/adapters/thread/langchain/proxy.ts +6 -34
  92. package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
  93. package/src/adapters/thread/langchain/thread-manager.ts +55 -98
  94. package/src/index.ts +5 -1
  95. package/src/lib/activity.ts +4 -3
  96. package/src/lib/hooks/types.ts +12 -12
  97. package/src/lib/model/types.ts +2 -0
  98. package/src/lib/session/session-edge-cases.integration.test.ts +24 -6
  99. package/src/lib/session/session.ts +18 -14
  100. package/src/lib/session/types.ts +31 -14
  101. package/src/lib/subagent/handler.ts +15 -8
  102. package/src/lib/subagent/types.ts +3 -2
  103. package/src/lib/thread/index.ts +3 -0
  104. package/src/lib/thread/manager.ts +4 -7
  105. package/src/lib/thread/proxy.ts +57 -0
  106. package/src/lib/thread/types.ts +44 -0
  107. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +54 -0
  108. package/src/lib/tool-router/auto-append.ts +5 -2
  109. package/src/lib/tool-router/router-edge-cases.integration.test.ts +9 -5
  110. package/src/lib/tool-router/router.ts +13 -7
  111. package/src/lib/tool-router/types.ts +20 -13
  112. package/src/lib/tool-router/with-sandbox.ts +4 -3
  113. package/src/lib/types.ts +7 -14
  114. package/src/workflow.ts +0 -4
  115. package/tsup.config.ts +5 -0
  116. package/dist/types-35POpVfa.d.cts +0 -40
  117. package/dist/types-35POpVfa.d.ts +0 -40
@@ -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
+ });
@@ -2,10 +2,14 @@ import type Redis from "ioredis";
2
2
  import type { Content, Part } from "@google/genai";
3
3
  import {
4
4
  createThreadManager,
5
- type BaseThreadManager,
5
+ type ProviderThreadManager,
6
6
  type ThreadManagerConfig,
7
+ type ThreadManagerHooks,
7
8
  } from "../../../lib/thread";
8
- import type { MessageContent, ToolMessageContent } from "../../../lib/types";
9
+ import type { GoogleGenAIToolResponse } from "./activities";
10
+
11
+ /** SDK-native content type for Google GenAI human messages */
12
+ export type GoogleGenAIContent = string | Part[];
9
13
 
10
14
  /** A Content with a unique ID for idempotent Redis storage */
11
15
  export interface StoredContent {
@@ -13,78 +17,65 @@ export interface StoredContent {
13
17
  content: Content;
14
18
  }
15
19
 
20
+ export type GoogleGenAIThreadManagerHooks = ThreadManagerHooks<StoredContent, Content>;
21
+
16
22
  export interface GoogleGenAIThreadManagerConfig {
17
23
  redis: Redis;
18
24
  threadId: string;
19
25
  /** Thread key, defaults to 'messages' */
20
26
  key?: string;
27
+ hooks?: GoogleGenAIThreadManagerHooks;
28
+ }
29
+
30
+ /** Prepared payload ready to send to the Google GenAI API */
31
+ export interface GoogleGenAIInvocationPayload {
32
+ contents: Content[];
33
+ systemInstruction?: string;
21
34
  }
22
35
 
23
36
  /** Thread manager with Google GenAI Content convenience helpers */
24
- export interface GoogleGenAIThreadManager extends BaseThreadManager<StoredContent> {
25
- createUserContent(
26
- id: string,
27
- content: string | MessageContent
28
- ): StoredContent;
29
- createSystemContent(id: string, content: string): StoredContent;
30
- createModelContent(id: string, parts: Part[]): StoredContent;
31
- createToolResponseContent(
32
- id: string,
33
- toolCallId: string,
34
- toolName: string,
35
- content: ToolMessageContent
36
- ): StoredContent;
37
- appendUserMessage(
38
- id: string,
39
- content: string | MessageContent
40
- ): Promise<void>;
41
- appendSystemMessage(id: string, content: string): Promise<void>;
37
+ export interface GoogleGenAIThreadManager
38
+ extends ProviderThreadManager<StoredContent, GoogleGenAIContent, GoogleGenAIToolResponse> {
42
39
  appendModelContent(id: string, parts: Part[]): Promise<void>;
43
- appendToolResult(
44
- id: string,
45
- toolCallId: string,
46
- toolName: string,
47
- content: ToolMessageContent
48
- ): Promise<void>;
40
+ prepareForInvocation(): Promise<GoogleGenAIInvocationPayload>;
49
41
  }
50
42
 
51
43
  function storedContentId(msg: StoredContent): string {
52
44
  return msg.id;
53
45
  }
54
46
 
55
- /** Convert zeitlich MessageContent to Google GenAI Part[] */
56
- export function messageContentToParts(
57
- content: string | MessageContent
58
- ): Part[] {
47
+ /** Normalise content into Part[] */
48
+ function toParts(content: GoogleGenAIContent): Part[] {
59
49
  if (typeof content === "string") {
60
50
  return [{ text: content }];
61
51
  }
62
- if (Array.isArray(content)) {
63
- return content.map((part) => {
64
- if (part.type === "text") {
65
- return { text: part.text as string };
66
- }
67
- return part as unknown as Part;
68
- });
52
+ return content;
53
+ }
54
+
55
+ /** Convert a string or object into a Record suitable for functionResponse.response */
56
+ function toFunctionResponse(content: string | Record<string, unknown>): Record<string, unknown> {
57
+ if (typeof content === "object") {
58
+ return content;
69
59
  }
70
- return [{ text: String(content) }];
60
+ return { result: content };
71
61
  }
72
62
 
73
- /** Parse ToolMessageContent into a Record suitable for functionResponse */
74
- function parseToolResponse(
75
- content: ToolMessageContent
76
- ): Record<string, unknown> {
77
- if (typeof content === "string") {
78
- try {
79
- const parsed: unknown = JSON.parse(content);
80
- return typeof parsed === "object" && parsed !== null
81
- ? (parsed as Record<string, unknown>)
82
- : { result: content };
83
- } catch {
84
- return { result: content };
63
+ /**
64
+ * Merge consecutive Content objects sharing the same role.
65
+ * The Gemini API requires alternating user/model turns; without
66
+ * merging, multiple sequential tool-result messages would violate this.
67
+ */
68
+ function mergeConsecutiveContents(contents: Content[]): Content[] {
69
+ const merged: Content[] = [];
70
+ for (const content of contents) {
71
+ const last = merged[merged.length - 1];
72
+ if (last && last.role === content.role) {
73
+ last.parts = [...(last.parts ?? []), ...(content.parts ?? [])];
74
+ } else {
75
+ merged.push({ ...content, parts: [...(content.parts ?? [])] });
85
76
  }
86
77
  }
87
- return { result: content };
78
+ return merged;
88
79
  }
89
80
 
90
81
  /**
@@ -93,7 +84,7 @@ function parseToolResponse(
93
84
  * appending typed Content messages.
94
85
  */
95
86
  export function createGoogleGenAIThreadManager(
96
- config: GoogleGenAIThreadManagerConfig
87
+ config: GoogleGenAIThreadManagerConfig,
97
88
  ): GoogleGenAIThreadManager {
98
89
  const baseConfig: ThreadManagerConfig<StoredContent> = {
99
90
  redis: config.redis,
@@ -104,79 +95,79 @@ export function createGoogleGenAIThreadManager(
104
95
 
105
96
  const base = createThreadManager(baseConfig);
106
97
 
107
- const helpers = {
108
- createUserContent(
98
+ const helpers: Omit<GoogleGenAIThreadManager, keyof typeof base> = {
99
+ async appendUserMessage(
109
100
  id: string,
110
- content: string | MessageContent
111
- ): StoredContent {
112
- return {
101
+ content: GoogleGenAIContent,
102
+ ): Promise<void> {
103
+ await base.append([{
113
104
  id,
114
- content: { role: "user", parts: messageContentToParts(content) },
115
- };
105
+ content: { role: "user", parts: toParts(content) },
106
+ }]);
116
107
  },
117
108
 
118
- createSystemContent(id: string, content: string): StoredContent {
119
- return {
109
+ async appendSystemMessage(id: string, content: string): Promise<void> {
110
+ await base.initialize();
111
+ await base.append([{
120
112
  id,
121
113
  content: { role: "system", parts: [{ text: content }] },
122
- };
114
+ }]);
123
115
  },
124
116
 
125
- createModelContent(id: string, parts: Part[]): StoredContent {
126
- return {
117
+ async appendModelContent(id: string, parts: Part[]): Promise<void> {
118
+ await base.append([{
127
119
  id,
128
120
  content: { role: "model", parts },
129
- };
121
+ }]);
130
122
  },
131
123
 
132
- createToolResponseContent(
124
+ async appendToolResult(
133
125
  id: string,
134
126
  toolCallId: string,
135
127
  toolName: string,
136
- content: ToolMessageContent
137
- ): StoredContent {
138
- return {
139
- id,
140
- content: {
141
- role: "user",
142
- parts: [
143
- {
144
- functionResponse: {
145
- id: toolCallId,
146
- name: toolName,
147
- response: parseToolResponse(content),
148
- },
149
- },
150
- ],
151
- },
152
- };
153
- },
154
-
155
- async appendUserMessage(
156
- id: string,
157
- content: string | MessageContent
128
+ content: GoogleGenAIToolResponse,
158
129
  ): Promise<void> {
159
- await base.append([helpers.createUserContent(id, content)]);
160
- },
130
+ const parts: Part[] = Array.isArray(content)
131
+ ? content as Part[]
132
+ : [{
133
+ functionResponse: {
134
+ id: toolCallId,
135
+ name: toolName,
136
+ response: toFunctionResponse(content),
137
+ },
138
+ }];
161
139
 
162
- async appendSystemMessage(id: string, content: string): Promise<void> {
163
- await base.initialize();
164
- await base.append([helpers.createSystemContent(id, content)]);
140
+ await base.append([{
141
+ id,
142
+ content: { role: "user", parts },
143
+ }]);
165
144
  },
166
145
 
167
- async appendModelContent(id: string, parts: Part[]): Promise<void> {
168
- await base.append([helpers.createModelContent(id, parts)]);
169
- },
146
+ async prepareForInvocation(): Promise<GoogleGenAIInvocationPayload> {
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;
152
+
153
+ let systemInstruction: string | undefined;
154
+ const conversationContents: Content[] = [];
155
+
156
+ for (const item of mapped) {
157
+ if (item.content.role === "system") {
158
+ systemInstruction = item.content.parts?.[0]?.text;
159
+ } else {
160
+ conversationContents.push(item.content);
161
+ }
162
+ }
170
163
 
171
- async appendToolResult(
172
- id: string,
173
- toolCallId: string,
174
- toolName: string,
175
- content: ToolMessageContent
176
- ): Promise<void> {
177
- await base.append([
178
- helpers.createToolResponseContent(id, toolCallId, toolName, content),
179
- ]);
164
+ const contents = mergeConsecutiveContents(conversationContents);
165
+ return {
166
+ contents: onPreparedMessage
167
+ ? contents.map((msg, i) => onPreparedMessage(msg, i, contents))
168
+ : contents,
169
+ ...(systemInstruction ? { systemInstruction } : {}),
170
+ };
180
171
  },
181
172
  };
182
173
 
@@ -1,6 +1,11 @@
1
1
  import type Redis from "ioredis";
2
2
  import type { ToolResultConfig } from "../../../lib/types";
3
3
  import type { MessageContent } from "@langchain/core/messages";
4
+ import type {
5
+ ActivityToolHandler,
6
+ RouterContext,
7
+ ToolHandlerResponse,
8
+ } from "../../../lib/tool-router/types";
4
9
  import type {
5
10
  ThreadOps,
6
11
  PrefixedThreadOps,
@@ -9,21 +14,35 @@ import type {
9
14
  import type { ModelInvoker } from "../../../lib/model";
10
15
  import type { StoredMessage } from "@langchain/core/messages";
11
16
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
12
- import { createLangChainThreadManager } from "./thread-manager";
17
+ import {
18
+ createLangChainThreadManager,
19
+ type LangChainContent,
20
+ type LangChainThreadManagerHooks,
21
+ } from "./thread-manager";
13
22
  import { createLangChainModelInvoker } from "./model-invoker";
14
23
 
15
24
  const ADAPTER_PREFIX = "langChain" as const;
16
25
 
17
26
  export type LangChainThreadOps<TScope extends string = ""> =
18
- PrefixedThreadOps<ScopedPrefix<TScope, typeof ADAPTER_PREFIX>>;
27
+ PrefixedThreadOps<ScopedPrefix<TScope, typeof ADAPTER_PREFIX>, LangChainContent>;
19
28
 
20
29
  export interface LangChainAdapterConfig {
21
30
  redis: Redis;
22
31
  /** Optional default model — if omitted, use `createModelInvoker()` to create invokers later */
23
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
33
  model?: BaseChatModel<any>;
34
+ hooks?: LangChainThreadManagerHooks;
25
35
  }
26
36
 
37
+ /**
38
+ * Tool response type accepted by the LangChain adapter.
39
+ *
40
+ * Content is passed directly to `ToolMessage` as `MessageContent`.
41
+ * Handlers can return a string or an array of content blocks
42
+ * (e.g. `{ type: "text", text: "..." }`, `{ type: "image_url", image_url: { ... } }`).
43
+ */
44
+ export type LangChainToolResponse = MessageContent;
45
+
27
46
  export interface LangChainAdapter {
28
47
  /** Model invoker using the default model (only available when `model` was provided) */
29
48
  invoker: ModelInvoker<StoredMessage>;
@@ -42,8 +61,19 @@ export interface LangChainAdapter {
42
61
  * ```
43
62
  */
44
63
  createActivities<S extends string = "">(
45
- scope?: S
64
+ scope?: S,
46
65
  ): LangChainThreadOps<S>;
66
+
67
+ /**
68
+ * Identity wrapper that types a tool handler for this adapter.
69
+ * Constrains `toolResponse` to {@link LangChainToolResponse}.
70
+ */
71
+ wrapHandler<TArgs, TResult, TContext extends RouterContext = RouterContext>(
72
+ handler: (
73
+ args: TArgs,
74
+ context: TContext,
75
+ ) => Promise<ToolHandlerResponse<TResult, LangChainToolResponse>>,
76
+ ): ActivityToolHandler<TArgs, TResult, TContext, LangChainToolResponse>;
47
77
  }
48
78
 
49
79
  /**
@@ -82,54 +112,58 @@ export interface LangChainAdapter {
82
112
  * ```
83
113
  */
84
114
  export function createLangChainAdapter(
85
- config: LangChainAdapterConfig
115
+ config: LangChainAdapterConfig,
86
116
  ): LangChainAdapter {
87
117
  const { redis } = config;
88
118
 
89
- const threadOps: ThreadOps = {
90
- async initializeThread(threadId: string): Promise<void> {
91
- const thread = createLangChainThreadManager({ redis, threadId });
119
+ const threadOps: ThreadOps<LangChainContent> = {
120
+ async initializeThread(threadId: string, threadKey?: string): Promise<void> {
121
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
92
122
  await thread.initialize();
93
123
  },
94
124
 
95
125
  async appendHumanMessage(
96
126
  threadId: string,
97
127
  id: string,
98
- content: string | MessageContent
128
+ content: LangChainContent,
129
+ threadKey?: string,
99
130
  ): Promise<void> {
100
- const thread = createLangChainThreadManager({ redis, threadId });
101
- await thread.appendHumanMessage(id, content);
131
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
132
+ await thread.appendUserMessage(id, content);
102
133
  },
103
134
 
104
135
  async appendSystemMessage(
105
136
  threadId: string,
106
137
  id: string,
107
- content: string
138
+ content: string,
139
+ threadKey?: string,
108
140
  ): Promise<void> {
109
- const thread = createLangChainThreadManager({ redis, threadId });
141
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
110
142
  await thread.appendSystemMessage(id, content);
111
143
  },
112
144
 
113
145
  async appendToolResult(id: string, cfg: ToolResultConfig): Promise<void> {
114
- const { threadId, toolCallId, content } = cfg;
115
- const thread = createLangChainThreadManager({ redis, threadId });
116
- await thread.appendToolMessage(id, content, toolCallId);
146
+ const { threadId, threadKey, toolCallId, content } = cfg;
147
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
148
+ await thread.appendToolResult(id, toolCallId, "", content);
117
149
  },
118
150
 
119
151
  async forkThread(
120
152
  sourceThreadId: string,
121
- targetThreadId: string
153
+ targetThreadId: string,
154
+ threadKey?: string,
122
155
  ): Promise<void> {
123
156
  const thread = createLangChainThreadManager({
124
157
  redis,
125
158
  threadId: sourceThreadId,
159
+ key: threadKey,
126
160
  });
127
161
  await thread.fork(targetThreadId);
128
162
  },
129
163
  };
130
164
 
131
165
  function createActivities<S extends string = "">(
132
- scope?: S
166
+ scope?: S,
133
167
  ): LangChainThreadOps<S> {
134
168
  const prefix = scope
135
169
  ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
@@ -137,22 +171,22 @@ export function createLangChainAdapter(
137
171
  const cap = (s: string): string =>
138
172
  s.charAt(0).toUpperCase() + s.slice(1);
139
173
  return Object.fromEntries(
140
- Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
174
+ Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v]),
141
175
  ) as LangChainThreadOps<S>;
142
176
  }
143
177
 
144
178
  const makeInvoker = (
145
179
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
- model: BaseChatModel<any>
180
+ model: BaseChatModel<any>,
147
181
  ): ModelInvoker<StoredMessage> =>
148
- createLangChainModelInvoker({ redis, model });
182
+ createLangChainModelInvoker({ redis, model, hooks: config.hooks });
149
183
 
150
184
  const invoker: ModelInvoker<StoredMessage> = config.model
151
185
  ? makeInvoker(config.model)
152
186
  : () => {
153
187
  throw new Error(
154
188
  "No default model provided to createLangChainAdapter. " +
155
- "Either pass `model` in the config or use `createModelInvoker(model)` instead."
189
+ "Either pass `model` in the config or use `createModelInvoker(model)` instead.",
156
190
  );
157
191
  };
158
192
 
@@ -160,5 +194,6 @@ export function createLangChainAdapter(
160
194
  createActivities,
161
195
  invoker,
162
196
  createModelInvoker: makeInvoker,
197
+ wrapHandler: (handler) => handler,
163
198
  };
164
199
  }
@@ -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
+ }
@@ -21,6 +21,7 @@ export {
21
21
  type LangChainAdapter,
22
22
  type LangChainAdapterConfig,
23
23
  type LangChainThreadOps,
24
+ type LangChainToolResponse,
24
25
  } from "./activities";
25
26
 
26
27
  // Thread manager
@@ -28,7 +29,8 @@ export {
28
29
  createLangChainThreadManager,
29
30
  type LangChainThreadManager,
30
31
  type LangChainThreadManagerConfig,
31
- type LangChainToolMessageContent,
32
+ type LangChainContent,
33
+ type LangChainInvocationPayload,
32
34
  } from "./thread-manager";
33
35
 
34
36
  // Model invoker (for advanced use — prefer adapter.createModelInvoker)
@@ -37,3 +39,6 @@ export {
37
39
  invokeLangChainModel,
38
40
  type LangChainModelInvokerConfig,
39
41
  } from "./model-invoker";
42
+
43
+ // Hooks / utilities
44
+ export { appendCachePoint } from "./hooks";