@librechat/agents 3.1.57 → 3.1.61
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/cjs/agents/AgentContext.cjs +326 -62
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +13 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +7 -27
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +303 -222
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +60 -0
- package/dist/cjs/llm/init.cjs.map +1 -0
- package/dist/cjs/llm/invoke.cjs +90 -0
- package/dist/cjs/llm/invoke.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +2 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/request.cjs +41 -0
- package/dist/cjs/llm/request.cjs.map +1 -0
- package/dist/cjs/main.cjs +40 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +76 -89
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +156 -0
- package/dist/cjs/messages/contextPruning.cjs.map +1 -0
- package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
- package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
- package/dist/cjs/messages/core.cjs +23 -37
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +156 -11
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +1161 -49
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/reducer.cjs +87 -0
- package/dist/cjs/messages/reducer.cjs.map +1 -0
- package/dist/cjs/run.cjs +81 -42
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +54 -7
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/index.cjs +75 -0
- package/dist/cjs/summarization/index.cjs.map +1 -0
- package/dist/cjs/summarization/node.cjs +663 -0
- package/dist/cjs/summarization/node.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +16 -8
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +2 -0
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +115 -0
- package/dist/cjs/utils/errors.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +17 -0
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +16 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -1
- package/dist/cjs/utils/llm.cjs +10 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +247 -14
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/truncation.cjs +107 -0
- package/dist/cjs/utils/truncation.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +325 -61
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +13 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +8 -28
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +307 -226
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +58 -0
- package/dist/esm/llm/init.mjs.map +1 -0
- package/dist/esm/llm/invoke.mjs +87 -0
- package/dist/esm/llm/invoke.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +2 -0
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/request.mjs +38 -0
- package/dist/esm/llm/request.mjs.map +1 -0
- package/dist/esm/main.mjs +13 -3
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +76 -89
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +154 -0
- package/dist/esm/messages/contextPruning.mjs.map +1 -0
- package/dist/esm/messages/contextPruningSettings.mjs +50 -0
- package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
- package/dist/esm/messages/core.mjs +23 -37
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +156 -11
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +1158 -52
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/reducer.mjs +83 -0
- package/dist/esm/messages/reducer.mjs.map +1 -0
- package/dist/esm/run.mjs +82 -43
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +54 -7
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/index.mjs +73 -0
- package/dist/esm/summarization/index.mjs.map +1 -0
- package/dist/esm/summarization/node.mjs +659 -0
- package/dist/esm/summarization/node.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +16 -8
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +2 -0
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +111 -0
- package/dist/esm/utils/errors.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +17 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +16 -0
- package/dist/esm/utils/handlers.mjs.map +1 -1
- package/dist/esm/utils/llm.mjs +10 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +245 -15
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/truncation.mjs +102 -0
- package/dist/esm/utils/truncation.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +124 -6
- package/dist/types/common/enum.d.ts +14 -1
- package/dist/types/graphs/Graph.d.ts +22 -27
- package/dist/types/index.d.ts +5 -0
- package/dist/types/llm/init.d.ts +18 -0
- package/dist/types/llm/invoke.d.ts +48 -0
- package/dist/types/llm/request.d.ts +14 -0
- package/dist/types/messages/contextPruning.d.ts +42 -0
- package/dist/types/messages/contextPruningSettings.d.ts +44 -0
- package/dist/types/messages/core.d.ts +1 -1
- package/dist/types/messages/format.d.ts +17 -1
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/prune.d.ts +162 -1
- package/dist/types/messages/reducer.d.ts +18 -0
- package/dist/types/run.d.ts +12 -1
- package/dist/types/summarization/index.d.ts +20 -0
- package/dist/types/summarization/node.d.ts +29 -0
- package/dist/types/tools/ToolNode.d.ts +3 -1
- package/dist/types/types/graph.d.ts +44 -6
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/run.d.ts +30 -0
- package/dist/types/types/stream.d.ts +31 -4
- package/dist/types/types/summarize.d.ts +47 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/dist/types/utils/errors.d.ts +28 -0
- package/dist/types/utils/events.d.ts +13 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/llm.d.ts +4 -0
- package/dist/types/utils/tokens.d.ts +14 -1
- package/dist/types/utils/truncation.d.ts +49 -0
- package/package.json +3 -3
- package/src/agents/AgentContext.ts +388 -58
- package/src/agents/__tests__/AgentContext.test.ts +265 -5
- package/src/common/enum.ts +13 -0
- package/src/events.ts +9 -39
- package/src/graphs/Graph.ts +468 -331
- package/src/index.ts +7 -0
- package/src/llm/anthropic/llm.spec.ts +3 -3
- package/src/llm/anthropic/utils/message_inputs.ts +6 -4
- package/src/llm/bedrock/llm.spec.ts +1 -1
- package/src/llm/bedrock/utils/message_inputs.ts +6 -2
- package/src/llm/init.ts +63 -0
- package/src/llm/invoke.ts +144 -0
- package/src/llm/request.ts +55 -0
- package/src/messages/__tests__/observationMasking.test.ts +221 -0
- package/src/messages/cache.ts +77 -102
- package/src/messages/contextPruning.ts +191 -0
- package/src/messages/contextPruningSettings.ts +90 -0
- package/src/messages/core.ts +32 -53
- package/src/messages/ensureThinkingBlock.test.ts +39 -39
- package/src/messages/format.ts +227 -15
- package/src/messages/formatAgentMessages.test.ts +511 -1
- package/src/messages/index.ts +3 -0
- package/src/messages/prune.ts +1548 -62
- package/src/messages/reducer.ts +22 -0
- package/src/run.ts +104 -51
- package/src/scripts/bedrock-merge-test.ts +1 -1
- package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-thinking-handoff.ts +1 -1
- package/src/scripts/thinking-bedrock.ts +1 -1
- package/src/scripts/thinking.ts +1 -1
- package/src/specs/anthropic.simple.test.ts +1 -1
- package/src/specs/multi-agent-summarization.test.ts +396 -0
- package/src/specs/prune.test.ts +1196 -23
- package/src/specs/summarization-unit.test.ts +868 -0
- package/src/specs/summarization.test.ts +3827 -0
- package/src/specs/summarize-prune.test.ts +376 -0
- package/src/specs/thinking-handoff.test.ts +10 -10
- package/src/specs/thinking-prune.test.ts +7 -4
- package/src/specs/token-accounting-e2e.test.ts +1034 -0
- package/src/specs/token-accounting-pipeline.test.ts +882 -0
- package/src/specs/token-distribution-edge-case.test.ts +25 -26
- package/src/splitStream.test.ts +42 -33
- package/src/stream.ts +64 -11
- package/src/summarization/__tests__/aggregator.test.ts +153 -0
- package/src/summarization/__tests__/node.test.ts +708 -0
- package/src/summarization/__tests__/trigger.test.ts +50 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/node.ts +982 -0
- package/src/tools/ToolNode.ts +25 -3
- package/src/types/graph.ts +62 -7
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +32 -0
- package/src/types/stream.ts +45 -5
- package/src/types/summarize.ts +58 -0
- package/src/types/tools.ts +7 -0
- package/src/utils/errors.ts +117 -0
- package/src/utils/events.ts +31 -0
- package/src/utils/handlers.ts +18 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/llm.ts +12 -0
- package/src/utils/tokens.ts +336 -18
- package/src/utils/truncation.ts +124 -0
- package/src/scripts/image.ts +0 -180
package/src/messages/cache.ts
CHANGED
|
@@ -101,19 +101,6 @@ function cloneMessage<T extends MessageWithContent>(
|
|
|
101
101
|
return cloned;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
* Checks if a message's content needs cache control stripping.
|
|
106
|
-
* Returns true if content has cachePoint blocks or cache_control fields.
|
|
107
|
-
*/
|
|
108
|
-
function needsCacheStripping(content: MessageContentComplex[]): boolean {
|
|
109
|
-
for (let i = 0; i < content.length; i++) {
|
|
110
|
-
const block = content[i];
|
|
111
|
-
if (isCachePoint(block)) return true;
|
|
112
|
-
if ('cache_control' in block) return true;
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
104
|
/**
|
|
118
105
|
* Anthropic API: Adds cache control to the appropriate user messages in the payload.
|
|
119
106
|
* Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
|
|
@@ -139,58 +126,63 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
|
|
|
139
126
|
const isUserMessage =
|
|
140
127
|
('getType' in originalMessage && originalMessage.getType() === 'human') ||
|
|
141
128
|
('role' in originalMessage && originalMessage.role === 'user');
|
|
142
|
-
|
|
143
129
|
const hasArrayContent = Array.isArray(content);
|
|
144
|
-
const needsStripping =
|
|
145
|
-
hasArrayContent &&
|
|
146
|
-
needsCacheStripping(content as MessageContentComplex[]);
|
|
147
130
|
const needsCacheAdd =
|
|
148
131
|
userMessagesModified < 2 &&
|
|
149
132
|
isUserMessage &&
|
|
150
133
|
(typeof content === 'string' || hasArrayContent);
|
|
151
134
|
|
|
152
|
-
|
|
135
|
+
// Skip messages that don't need any work
|
|
136
|
+
if (!needsCacheAdd && !hasArrayContent) {
|
|
153
137
|
continue;
|
|
154
138
|
}
|
|
155
139
|
|
|
156
140
|
let workingContent: MessageContentComplex[];
|
|
141
|
+
let modified = false;
|
|
157
142
|
|
|
158
143
|
if (hasArrayContent) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
144
|
+
// Single pass: clone blocks, strip cache markers and cache points,
|
|
145
|
+
// find last text block index for cache insertion — all at once.
|
|
146
|
+
const src = content as MessageContentComplex[];
|
|
147
|
+
workingContent = [];
|
|
148
|
+
let lastTextIndex = -1;
|
|
149
|
+
for (let j = 0; j < src.length; j++) {
|
|
150
|
+
const block = src[j];
|
|
151
|
+
if (isCachePoint(block)) {
|
|
152
|
+
modified = true;
|
|
153
|
+
continue; // skip cache point blocks
|
|
167
154
|
}
|
|
155
|
+
const cloned = { ...block };
|
|
156
|
+
if ('cache_control' in cloned) {
|
|
157
|
+
delete (cloned as Record<string, unknown>).cache_control;
|
|
158
|
+
modified = true;
|
|
159
|
+
}
|
|
160
|
+
if ('type' in cloned && cloned.type === 'text') {
|
|
161
|
+
lastTextIndex = workingContent.length;
|
|
162
|
+
}
|
|
163
|
+
workingContent.push(cloned as MessageContentComplex);
|
|
168
164
|
}
|
|
169
|
-
} else if (typeof content === 'string') {
|
|
170
|
-
workingContent = [
|
|
171
|
-
{ type: 'text', text: content },
|
|
172
|
-
] as MessageContentComplex[];
|
|
173
|
-
} else {
|
|
174
|
-
workingContent = [];
|
|
175
|
-
}
|
|
176
165
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
workingContent
|
|
181
|
-
) as T;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
166
|
+
if (!modified && !needsCacheAdd) {
|
|
167
|
+
continue; // nothing to strip and no cache to add
|
|
168
|
+
}
|
|
184
169
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
170
|
+
// Add cache control to the last text block for user messages
|
|
171
|
+
if (needsCacheAdd && lastTextIndex >= 0) {
|
|
172
|
+
(
|
|
173
|
+
workingContent[lastTextIndex] as Anthropic.TextBlockParam
|
|
174
|
+
).cache_control = {
|
|
189
175
|
type: 'ephemeral',
|
|
190
176
|
};
|
|
191
177
|
userMessagesModified++;
|
|
192
|
-
break;
|
|
193
178
|
}
|
|
179
|
+
} else if (typeof content === 'string' && needsCacheAdd) {
|
|
180
|
+
workingContent = [
|
|
181
|
+
{ type: 'text', text: content, cache_control: { type: 'ephemeral' } },
|
|
182
|
+
] as unknown as MessageContentComplex[];
|
|
183
|
+
userMessagesModified++;
|
|
184
|
+
} else {
|
|
185
|
+
continue;
|
|
194
186
|
}
|
|
195
187
|
|
|
196
188
|
updatedMessages[i] = cloneMessage(
|
|
@@ -325,9 +317,6 @@ export function addBedrockCacheControl<
|
|
|
325
317
|
|
|
326
318
|
const content = originalMessage.content;
|
|
327
319
|
const hasArrayContent = Array.isArray(content);
|
|
328
|
-
const needsStripping =
|
|
329
|
-
hasArrayContent &&
|
|
330
|
-
needsCacheStripping(content as MessageContentComplex[]);
|
|
331
320
|
const isEmptyString = typeof content === 'string' && content === '';
|
|
332
321
|
const needsCacheAdd =
|
|
333
322
|
messagesModified < 2 &&
|
|
@@ -335,77 +324,63 @@ export function addBedrockCacheControl<
|
|
|
335
324
|
!isEmptyString &&
|
|
336
325
|
(typeof content === 'string' || hasArrayContent);
|
|
337
326
|
|
|
338
|
-
if (!
|
|
327
|
+
if (!needsCacheAdd && !hasArrayContent) {
|
|
339
328
|
continue;
|
|
340
329
|
}
|
|
341
330
|
|
|
342
331
|
let workingContent: MessageContentComplex[];
|
|
332
|
+
let modified = false;
|
|
343
333
|
|
|
344
334
|
if (hasArrayContent) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
for (let j = 0; j < workingContent.length; j++) {
|
|
350
|
-
const block = workingContent[j] as Record<string, unknown>;
|
|
351
|
-
if ('cache_control' in block) {
|
|
352
|
-
delete block.cache_control;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
} else if (typeof content === 'string') {
|
|
356
|
-
workingContent = [{ type: ContentTypes.TEXT, text: content }];
|
|
357
|
-
} else {
|
|
335
|
+
// Single pass: clone blocks, strip cache markers, find last
|
|
336
|
+
// non-empty text block for cache point insertion — all at once.
|
|
337
|
+
const src = content as MessageContentComplex[];
|
|
358
338
|
workingContent = [];
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
let hasCacheableContent = false;
|
|
371
|
-
for (const block of workingContent) {
|
|
372
|
-
if (block.type === ContentTypes.TEXT) {
|
|
373
|
-
if (typeof block.text === 'string' && block.text.trim() !== '') {
|
|
374
|
-
hasCacheableContent = true;
|
|
375
|
-
break;
|
|
339
|
+
let lastNonEmptyTextIndex = -1;
|
|
340
|
+
for (let j = 0; j < src.length; j++) {
|
|
341
|
+
const block = src[j];
|
|
342
|
+
if (isCachePoint(block)) {
|
|
343
|
+
modified = true;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const cloned = { ...block };
|
|
347
|
+
if ('cache_control' in cloned) {
|
|
348
|
+
delete (cloned as Record<string, unknown>).cache_control;
|
|
349
|
+
modified = true;
|
|
376
350
|
}
|
|
351
|
+
const type = (cloned as { type?: string }).type;
|
|
352
|
+
if (type === ContentTypes.TEXT || type === 'text') {
|
|
353
|
+
const text = (cloned as { text?: string }).text;
|
|
354
|
+
if (text != null && text.trim() !== '') {
|
|
355
|
+
lastNonEmptyTextIndex = workingContent.length;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
workingContent.push(cloned as MessageContentComplex);
|
|
377
359
|
}
|
|
378
|
-
}
|
|
379
360
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
361
|
+
if (!modified && !needsCacheAdd) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
384
364
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (type === ContentTypes.TEXT || type === 'text') {
|
|
390
|
-
const text = (block as { text?: string }).text;
|
|
391
|
-
if (text === '' || text === undefined || text.trim() === '') {
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
workingContent.splice(j + 1, 0, {
|
|
365
|
+
// Insert cache point after the last non-empty text block.
|
|
366
|
+
// Skip if no cacheable text content exists (whitespace-only messages).
|
|
367
|
+
if (needsCacheAdd && lastNonEmptyTextIndex >= 0) {
|
|
368
|
+
workingContent.splice(lastNonEmptyTextIndex + 1, 0, {
|
|
395
369
|
cachePoint: { type: 'default' },
|
|
396
370
|
} as MessageContentComplex);
|
|
397
|
-
|
|
398
|
-
break;
|
|
371
|
+
messagesModified++;
|
|
399
372
|
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
cachePoint: { type: 'default' },
|
|
404
|
-
|
|
373
|
+
} else if (typeof content === 'string' && needsCacheAdd) {
|
|
374
|
+
workingContent = [
|
|
375
|
+
{ type: ContentTypes.TEXT, text: content },
|
|
376
|
+
{ cachePoint: { type: 'default' } } as MessageContentComplex,
|
|
377
|
+
];
|
|
378
|
+
messagesModified++;
|
|
379
|
+
} else {
|
|
380
|
+
continue;
|
|
405
381
|
}
|
|
406
382
|
|
|
407
383
|
updatedMessages[i] = cloneMessage(originalMessage, workingContent);
|
|
408
|
-
messagesModified++;
|
|
409
384
|
}
|
|
410
385
|
|
|
411
386
|
return updatedMessages;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position-based context pruning for tool results.
|
|
3
|
+
*
|
|
4
|
+
* Uses position-based age: the distance of a message
|
|
5
|
+
* from the conversation end as a fraction of total messages.
|
|
6
|
+
*
|
|
7
|
+
* Two degradation levels:
|
|
8
|
+
* - Soft-trim: Keep head + tail of tool result content, drop middle.
|
|
9
|
+
* - Hard-clear: Replace entire content with a placeholder.
|
|
10
|
+
*
|
|
11
|
+
* Messages in the "protected zone" (recent assistant turns, system/pre-first-human
|
|
12
|
+
* messages, and messages with image content) are never pruned.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
|
|
16
|
+
import type { ContextPruningConfig } from '@/types/graph';
|
|
17
|
+
import type { TokenCounter } from '@/types/run';
|
|
18
|
+
import type { ContextPruningSettings } from './contextPruningSettings';
|
|
19
|
+
import { resolveContextPruningSettings } from './contextPruningSettings';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a message contains image content blocks.
|
|
23
|
+
* Messages with images are skipped by position-based content degradation
|
|
24
|
+
* because images cannot be meaningfully soft-trimmed or replaced with placeholders.
|
|
25
|
+
*/
|
|
26
|
+
function hasImageContent(message: BaseMessage): boolean {
|
|
27
|
+
if (!Array.isArray(message.content)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return message.content.some(
|
|
31
|
+
(block) =>
|
|
32
|
+
typeof block === 'object' &&
|
|
33
|
+
'type' in block &&
|
|
34
|
+
(block.type === 'image_url' || block.type === 'image')
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Applies head+tail soft-trim to tool result content.
|
|
40
|
+
*/
|
|
41
|
+
function softTrimContent(
|
|
42
|
+
content: string,
|
|
43
|
+
settings: ContextPruningSettings['softTrim']
|
|
44
|
+
): string {
|
|
45
|
+
const { headChars, tailChars } = settings;
|
|
46
|
+
const indicator = `\n\n… [soft-trimmed: ${content.length} chars → ${headChars + tailChars} chars, middle removed] …\n\n`;
|
|
47
|
+
return content.slice(0, headChars) + indicator + content.slice(-tailChars);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ContextPruningResult {
|
|
51
|
+
/** Number of messages that were soft-trimmed. */
|
|
52
|
+
softTrimmed: number;
|
|
53
|
+
/** Number of messages that were hard-cleared. */
|
|
54
|
+
hardCleared: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Applies position-based context pruning to tool result messages.
|
|
59
|
+
*
|
|
60
|
+
* Modifies messages in-place and updates indexTokenCountMap with recounted
|
|
61
|
+
* token values for modified messages.
|
|
62
|
+
*
|
|
63
|
+
* @param params.messages - The full message array (modified in-place).
|
|
64
|
+
* @param params.indexTokenCountMap - Token count map (updated in-place).
|
|
65
|
+
* @param params.tokenCounter - Function to recount tokens after modification.
|
|
66
|
+
* @param params.config - Partial context pruning config (merged with defaults).
|
|
67
|
+
* @returns Counts of soft-trimmed and hard-cleared messages.
|
|
68
|
+
*/
|
|
69
|
+
export function applyContextPruning(params: {
|
|
70
|
+
messages: BaseMessage[];
|
|
71
|
+
indexTokenCountMap: Record<string, number | undefined>;
|
|
72
|
+
tokenCounter: TokenCounter;
|
|
73
|
+
config?: ContextPruningConfig;
|
|
74
|
+
resolvedSettings?: ContextPruningSettings;
|
|
75
|
+
}): ContextPruningResult {
|
|
76
|
+
const {
|
|
77
|
+
messages,
|
|
78
|
+
indexTokenCountMap,
|
|
79
|
+
tokenCounter,
|
|
80
|
+
config,
|
|
81
|
+
resolvedSettings,
|
|
82
|
+
} = params;
|
|
83
|
+
const settings = resolvedSettings ?? resolveContextPruningSettings(config);
|
|
84
|
+
|
|
85
|
+
if (!settings.enabled || messages.length === 0) {
|
|
86
|
+
return { softTrimmed: 0, hardCleared: 0 };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const totalMessages = messages.length;
|
|
90
|
+
let softTrimmed = 0;
|
|
91
|
+
let hardCleared = 0;
|
|
92
|
+
|
|
93
|
+
// Find the protected zone: last N assistant turns from the end.
|
|
94
|
+
// An "assistant turn" is a contiguous sequence of AI + Tool messages.
|
|
95
|
+
const protectedIndices = new Set<number>();
|
|
96
|
+
|
|
97
|
+
// Always protect the system message (index 0 if present)
|
|
98
|
+
if (messages[0]?.getType() === 'system') {
|
|
99
|
+
protectedIndices.add(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Protect messages before the first human message
|
|
103
|
+
for (let i = 0; i < totalMessages; i++) {
|
|
104
|
+
if (messages[i].getType() === 'human') {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
protectedIndices.add(i);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Protect the last N assistant turns (walking backwards)
|
|
111
|
+
let assistantTurnsFound = 0;
|
|
112
|
+
let inAssistantSequence = false;
|
|
113
|
+
for (let i = totalMessages - 1; i >= 0; i--) {
|
|
114
|
+
const type = messages[i].getType();
|
|
115
|
+
if (type === 'ai' || type === 'tool') {
|
|
116
|
+
protectedIndices.add(i);
|
|
117
|
+
if (!inAssistantSequence) {
|
|
118
|
+
inAssistantSequence = true;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (inAssistantSequence) {
|
|
122
|
+
assistantTurnsFound++;
|
|
123
|
+
inAssistantSequence = false;
|
|
124
|
+
if (assistantTurnsFound >= settings.keepLastAssistants) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Protect the human message between assistant turns in the protected zone
|
|
129
|
+
if (assistantTurnsFound < settings.keepLastAssistants) {
|
|
130
|
+
protectedIndices.add(i);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Process each tool message outside the protected zone
|
|
136
|
+
for (let i = 0; i < totalMessages; i++) {
|
|
137
|
+
const message = messages[i];
|
|
138
|
+
if (message.getType() !== 'tool') {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (protectedIndices.has(i)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (hasImageContent(message)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const content = message.content;
|
|
149
|
+
if (typeof content !== 'string') {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (content.length < settings.minPrunableToolChars) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Compute age ratio: how far back from the end (0 = latest, 1 = oldest)
|
|
157
|
+
const ageRatio = (totalMessages - i) / totalMessages;
|
|
158
|
+
|
|
159
|
+
if (ageRatio >= settings.hardClearRatio && settings.hardClear.enabled) {
|
|
160
|
+
// Hard-clear: replace with placeholder
|
|
161
|
+
const cloned = new ToolMessage({
|
|
162
|
+
content: settings.hardClear.placeholder,
|
|
163
|
+
tool_call_id: (message as ToolMessage).tool_call_id,
|
|
164
|
+
name: message.name,
|
|
165
|
+
id: message.id,
|
|
166
|
+
additional_kwargs: message.additional_kwargs,
|
|
167
|
+
response_metadata: message.response_metadata,
|
|
168
|
+
});
|
|
169
|
+
messages[i] = cloned;
|
|
170
|
+
indexTokenCountMap[i] = tokenCounter(cloned);
|
|
171
|
+
hardCleared++;
|
|
172
|
+
} else if (ageRatio >= settings.softTrimRatio) {
|
|
173
|
+
// Soft-trim: keep head + tail
|
|
174
|
+
if (content.length > settings.softTrim.maxChars) {
|
|
175
|
+
const cloned = new ToolMessage({
|
|
176
|
+
content: softTrimContent(content, settings.softTrim),
|
|
177
|
+
tool_call_id: (message as ToolMessage).tool_call_id,
|
|
178
|
+
name: message.name,
|
|
179
|
+
id: message.id,
|
|
180
|
+
additional_kwargs: message.additional_kwargs,
|
|
181
|
+
response_metadata: message.response_metadata,
|
|
182
|
+
});
|
|
183
|
+
messages[i] = cloned;
|
|
184
|
+
indexTokenCountMap[i] = tokenCounter(cloned);
|
|
185
|
+
softTrimmed++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { softTrimmed, hardCleared };
|
|
191
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default settings for position-based context pruning.
|
|
3
|
+
*
|
|
4
|
+
* These are merged with user-provided overrides so any subset can be customized.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ContextPruningSettings {
|
|
8
|
+
/** Whether position-based pruning is enabled. Default: false (opt-in). */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** Number of recent assistant turns to protect from pruning. Default: 3 */
|
|
11
|
+
keepLastAssistants: number;
|
|
12
|
+
/** Age ratio (0-1) at which soft-trim fires. Default: 0.3 */
|
|
13
|
+
softTrimRatio: number;
|
|
14
|
+
/** Age ratio (0-1) at which hard-clear fires. Default: 0.5 */
|
|
15
|
+
hardClearRatio: number;
|
|
16
|
+
/** Minimum tool result size (chars) before pruning applies. Default: 50000 */
|
|
17
|
+
minPrunableToolChars: number;
|
|
18
|
+
softTrim: {
|
|
19
|
+
/** Maximum total chars after soft-trim. Default: 4000 */
|
|
20
|
+
maxChars: number;
|
|
21
|
+
/** Head portion to keep. Default: 1500 */
|
|
22
|
+
headChars: number;
|
|
23
|
+
/** Tail portion to keep. Default: 1500 */
|
|
24
|
+
tailChars: number;
|
|
25
|
+
};
|
|
26
|
+
hardClear: {
|
|
27
|
+
/** Whether hard-clear is enabled. Default: true */
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** Placeholder text for hard-cleared content. */
|
|
30
|
+
placeholder: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_CONTEXT_PRUNING_SETTINGS: ContextPruningSettings = {
|
|
35
|
+
enabled: false,
|
|
36
|
+
keepLastAssistants: 3,
|
|
37
|
+
softTrimRatio: 0.3,
|
|
38
|
+
hardClearRatio: 0.5,
|
|
39
|
+
minPrunableToolChars: 50_000,
|
|
40
|
+
softTrim: {
|
|
41
|
+
maxChars: 4_000,
|
|
42
|
+
headChars: 1_500,
|
|
43
|
+
tailChars: 1_500,
|
|
44
|
+
},
|
|
45
|
+
hardClear: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
placeholder: '[Old tool result content cleared]',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Merges user-provided partial overrides with the defaults.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveContextPruningSettings(
|
|
55
|
+
overrides?: Partial<{
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
keepLastAssistants?: number;
|
|
58
|
+
softTrimRatio?: number;
|
|
59
|
+
hardClearRatio?: number;
|
|
60
|
+
minPrunableToolChars?: number;
|
|
61
|
+
softTrim?: Partial<ContextPruningSettings['softTrim']>;
|
|
62
|
+
hardClear?: Partial<ContextPruningSettings['hardClear']>;
|
|
63
|
+
}>
|
|
64
|
+
): ContextPruningSettings {
|
|
65
|
+
if (!overrides) {
|
|
66
|
+
return { ...DEFAULT_CONTEXT_PRUNING_SETTINGS };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
enabled: overrides.enabled ?? DEFAULT_CONTEXT_PRUNING_SETTINGS.enabled,
|
|
70
|
+
keepLastAssistants:
|
|
71
|
+
overrides.keepLastAssistants ??
|
|
72
|
+
DEFAULT_CONTEXT_PRUNING_SETTINGS.keepLastAssistants,
|
|
73
|
+
softTrimRatio:
|
|
74
|
+
overrides.softTrimRatio ?? DEFAULT_CONTEXT_PRUNING_SETTINGS.softTrimRatio,
|
|
75
|
+
hardClearRatio:
|
|
76
|
+
overrides.hardClearRatio ??
|
|
77
|
+
DEFAULT_CONTEXT_PRUNING_SETTINGS.hardClearRatio,
|
|
78
|
+
minPrunableToolChars:
|
|
79
|
+
overrides.minPrunableToolChars ??
|
|
80
|
+
DEFAULT_CONTEXT_PRUNING_SETTINGS.minPrunableToolChars,
|
|
81
|
+
softTrim: {
|
|
82
|
+
...DEFAULT_CONTEXT_PRUNING_SETTINGS.softTrim,
|
|
83
|
+
...overrides.softTrim,
|
|
84
|
+
},
|
|
85
|
+
hardClear: {
|
|
86
|
+
...DEFAULT_CONTEXT_PRUNING_SETTINGS.hardClear,
|
|
87
|
+
...overrides.hardClear,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/messages/core.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/messages.ts
|
|
2
2
|
import {
|
|
3
|
-
AIMessageChunk,
|
|
4
|
-
HumanMessage,
|
|
5
|
-
ToolMessage,
|
|
6
3
|
AIMessage,
|
|
7
4
|
BaseMessage,
|
|
5
|
+
ToolMessage,
|
|
6
|
+
HumanMessage,
|
|
7
|
+
AIMessageChunk,
|
|
8
8
|
} from '@langchain/core/messages';
|
|
9
9
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
10
10
|
import type * as t from '@/types';
|
|
@@ -54,10 +54,10 @@ const modifyContent = ({
|
|
|
54
54
|
provider: Providers;
|
|
55
55
|
messageType: string;
|
|
56
56
|
content: t.ExtendedMessageContent[];
|
|
57
|
-
}): t.ExtendedMessageContent[] => {
|
|
57
|
+
}): (t.ExtendedMessageContent | null)[] => {
|
|
58
58
|
const allowedTypes =
|
|
59
59
|
allowedTypesByProvider[provider] ?? allowedTypesByProvider.default;
|
|
60
|
-
return content.map((item) => {
|
|
60
|
+
return content.map((item: t.ExtendedMessageContent | null) => {
|
|
61
61
|
if (
|
|
62
62
|
item &&
|
|
63
63
|
typeof item === 'object' &&
|
|
@@ -153,7 +153,7 @@ export function modifyDeltaProperties(
|
|
|
153
153
|
provider,
|
|
154
154
|
messageType,
|
|
155
155
|
content: obj.content,
|
|
156
|
-
});
|
|
156
|
+
}) as t.MessageContentComplex[];
|
|
157
157
|
}
|
|
158
158
|
if (
|
|
159
159
|
(obj as Partial<AIMessageChunk>).lc_kwargs &&
|
|
@@ -365,31 +365,29 @@ export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
|
|
|
365
365
|
|
|
366
366
|
if (latestAIParentIndex === -1) return;
|
|
367
367
|
|
|
368
|
-
//
|
|
369
|
-
const hasArtifactContent = messages.some(
|
|
370
|
-
(msg, i) =>
|
|
371
|
-
i > latestAIParentIndex &&
|
|
372
|
-
msg instanceof ToolMessage &&
|
|
373
|
-
msg.artifact != null &&
|
|
374
|
-
msg.artifact?.content != null &&
|
|
375
|
-
Array.isArray(msg.artifact.content)
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
if (!hasArtifactContent) return;
|
|
379
|
-
|
|
368
|
+
// Build tool call ID set and merge artifact content in a single forward pass.
|
|
380
369
|
const message = messages[latestAIParentIndex] as AIMessageChunk;
|
|
381
|
-
const
|
|
370
|
+
const toolCallIdSet = new Set<string>();
|
|
371
|
+
if (message.tool_calls) {
|
|
372
|
+
for (const tc of message.tool_calls) {
|
|
373
|
+
if (tc.id != null) {
|
|
374
|
+
toolCallIdSet.add(tc.id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
382
378
|
|
|
383
379
|
for (let j = latestAIParentIndex + 1; j < messages.length; j++) {
|
|
384
380
|
const msg = messages[j];
|
|
385
381
|
if (
|
|
386
382
|
msg instanceof ToolMessage &&
|
|
387
|
-
|
|
383
|
+
toolCallIdSet.has(msg.tool_call_id) &&
|
|
388
384
|
msg.artifact != null &&
|
|
389
|
-
Array.isArray(msg.artifact?.content)
|
|
390
|
-
Array.isArray(msg.content)
|
|
385
|
+
Array.isArray(msg.artifact?.content)
|
|
391
386
|
) {
|
|
392
|
-
|
|
387
|
+
const base = Array.isArray(msg.content)
|
|
388
|
+
? msg.content
|
|
389
|
+
: [{ type: 'text' as const, text: String(msg.content ?? '') }];
|
|
390
|
+
msg.content = base.concat(msg.artifact.content);
|
|
393
391
|
}
|
|
394
392
|
}
|
|
395
393
|
}
|
|
@@ -410,46 +408,27 @@ export function formatArtifactPayload(messages: BaseMessage[]): void {
|
|
|
410
408
|
|
|
411
409
|
if (latestAIParentIndex === -1) return;
|
|
412
410
|
|
|
413
|
-
//
|
|
414
|
-
const hasArtifactContent = messages.some(
|
|
415
|
-
(msg, i) =>
|
|
416
|
-
i > latestAIParentIndex &&
|
|
417
|
-
msg instanceof ToolMessage &&
|
|
418
|
-
msg.artifact != null &&
|
|
419
|
-
msg.artifact?.content != null &&
|
|
420
|
-
Array.isArray(msg.artifact.content)
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
if (!hasArtifactContent) return;
|
|
424
|
-
|
|
425
|
-
// Collect all relevant tool messages and their artifacts
|
|
426
|
-
const relevantMessages = messages
|
|
427
|
-
.slice(latestAIParentIndex + 1)
|
|
428
|
-
.filter((msg) => msg instanceof ToolMessage) as ToolMessage[];
|
|
429
|
-
|
|
430
|
-
// Aggregate all content and artifacts
|
|
411
|
+
// Single pass: collect relevant tool messages with artifacts and aggregate
|
|
431
412
|
const aggregatedContent: t.MessageContentComplex[] = [];
|
|
432
413
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
414
|
+
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
415
|
+
const msg = messages[i];
|
|
416
|
+
if (
|
|
417
|
+
!(msg instanceof ToolMessage) ||
|
|
418
|
+
!Array.isArray(msg.artifact?.content)
|
|
419
|
+
) {
|
|
420
|
+
continue;
|
|
436
421
|
}
|
|
437
422
|
let currentContent = msg.content;
|
|
438
423
|
if (!Array.isArray(currentContent)) {
|
|
439
|
-
currentContent = [
|
|
440
|
-
{
|
|
441
|
-
type: 'text',
|
|
442
|
-
text: msg.content,
|
|
443
|
-
},
|
|
444
|
-
];
|
|
424
|
+
currentContent = [{ type: 'text', text: msg.content }];
|
|
445
425
|
}
|
|
446
|
-
aggregatedContent.push(...currentContent);
|
|
426
|
+
aggregatedContent.push(...(currentContent as t.MessageContentComplex[]));
|
|
447
427
|
msg.content =
|
|
448
428
|
'Tool response is included in the next message as a Human message';
|
|
449
429
|
aggregatedContent.push(...msg.artifact.content);
|
|
450
|
-
}
|
|
430
|
+
}
|
|
451
431
|
|
|
452
|
-
// Add single HumanMessage with all aggregated content
|
|
453
432
|
if (aggregatedContent.length > 0) {
|
|
454
433
|
messages.push(new HumanMessage({ content: aggregatedContent }));
|
|
455
434
|
}
|