illuma-agents 1.0.17 → 1.0.18
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 +3 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +18 -9
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +5 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +10 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +7 -8
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +11 -6
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +2 -2
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +2 -1
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +2 -2
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/stream.cjs +29 -16
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +209 -47
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +3 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/contextAnalytics.cjs +7 -5
- package/dist/cjs/utils/contextAnalytics.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/cjs/utils/toonFormat.cjs +42 -12
- package/dist/cjs/utils/toonFormat.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +3 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +18 -9
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +5 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +10 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +7 -8
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/cache.mjs +11 -6
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +2 -2
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +2 -1
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +2 -2
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/stream.mjs +29 -16
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +208 -48
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +3 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/contextAnalytics.mjs +7 -5
- package/dist/esm/utils/contextAnalytics.mjs.map +1 -1
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/esm/utils/toonFormat.mjs +42 -12
- package/dist/esm/utils/toonFormat.mjs.map +1 -1
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +19 -0
- package/dist/types/types/tools.d.ts +3 -1
- package/package.json +2 -2
- package/src/agents/AgentContext.ts +28 -20
- package/src/graphs/Graph.ts +76 -37
- package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +495 -473
- package/src/llm/bedrock/index.ts +47 -35
- package/src/llm/openrouter/index.ts +11 -1
- package/src/llm/vertexai/index.ts +9 -10
- package/src/messages/cache.ts +104 -55
- package/src/messages/core.ts +5 -3
- package/src/messages/format.ts +6 -2
- package/src/messages/tools.ts +2 -2
- package/src/scripts/simple.ts +1 -1
- package/src/specs/emergency-prune.test.ts +407 -355
- package/src/stream.ts +28 -20
- package/src/tools/ProgrammaticToolCalling.ts +246 -52
- package/src/tools/ToolNode.ts +4 -4
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +155 -0
- package/src/tools/search/jina-reranker.test.ts +32 -28
- package/src/tools/search/search.ts +3 -1
- package/src/tools/search/tool.ts +16 -7
- package/src/types/tools.ts +3 -1
- package/src/utils/contextAnalytics.ts +103 -95
- package/src/utils/llmConfig.ts +8 -1
- package/src/utils/run.ts +5 -4
- package/src/utils/toonFormat.ts +475 -437
|
@@ -1,355 +1,407 @@
|
|
|
1
|
-
// src/specs/emergency-prune.test.ts
|
|
2
|
-
/**
|
|
3
|
-
* Tests for the emergency pruning feature that handles "input too long" errors
|
|
4
|
-
* by retrying with more aggressive message pruning and adding a context notice.
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
HumanMessage,
|
|
8
|
-
AIMessage,
|
|
9
|
-
SystemMessage,
|
|
10
|
-
BaseMessage,
|
|
11
|
-
} from '@langchain/core/messages';
|
|
12
|
-
import type * as t from '@/types';
|
|
13
|
-
import { createPruneMessages } from '@/messages/prune';
|
|
14
|
-
import { Providers } from '@/common';
|
|
15
|
-
|
|
16
|
-
// Simple token counter for testing (1 character = 1 token)
|
|
17
|
-
const createTestTokenCounter = (): t.TokenCounter => {
|
|
18
|
-
return (message: BaseMessage): number => {
|
|
19
|
-
const content = message.content as
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
return content.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
83
|
-
} else if (messageCount <=
|
|
84
|
-
return 'the
|
|
85
|
-
} else {
|
|
86
|
-
return '
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
startIndex: 0,
|
|
116
|
-
provider: Providers.BEDROCK,
|
|
117
|
-
tokenCounter,
|
|
118
|
-
maxTokens:
|
|
119
|
-
thinkingEnabled: false,
|
|
120
|
-
indexTokenCountMap,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const { context:
|
|
124
|
-
|
|
125
|
-
// Emergency
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
expect(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
expect(context
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1
|
+
// src/specs/emergency-prune.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the emergency pruning feature that handles "input too long" errors
|
|
4
|
+
* by retrying with more aggressive message pruning and adding a context notice.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
HumanMessage,
|
|
8
|
+
AIMessage,
|
|
9
|
+
SystemMessage,
|
|
10
|
+
BaseMessage,
|
|
11
|
+
} from '@langchain/core/messages';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
import { createPruneMessages } from '@/messages/prune';
|
|
14
|
+
import { Providers } from '@/common';
|
|
15
|
+
|
|
16
|
+
// Simple token counter for testing (1 character = 1 token)
|
|
17
|
+
const createTestTokenCounter = (): t.TokenCounter => {
|
|
18
|
+
return (message: BaseMessage): number => {
|
|
19
|
+
const content = message.content as
|
|
20
|
+
| string
|
|
21
|
+
| Array<t.MessageContentComplex | string>
|
|
22
|
+
| undefined;
|
|
23
|
+
if (typeof content === 'string') {
|
|
24
|
+
return content.length;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(content)) {
|
|
27
|
+
return content.reduce((total, item) => {
|
|
28
|
+
if (typeof item === 'string') return total + item.length;
|
|
29
|
+
if (
|
|
30
|
+
typeof item === 'object' &&
|
|
31
|
+
'text' in item &&
|
|
32
|
+
typeof item.text === 'string'
|
|
33
|
+
) {
|
|
34
|
+
return total + item.text.length;
|
|
35
|
+
}
|
|
36
|
+
return total;
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Helper to create test messages
|
|
44
|
+
const createTestMessages = (
|
|
45
|
+
count: number,
|
|
46
|
+
tokensPer: number
|
|
47
|
+
): BaseMessage[] => {
|
|
48
|
+
const messages: BaseMessage[] = [
|
|
49
|
+
new SystemMessage('You are a helpful assistant.'),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const content = 'x'.repeat(tokensPer);
|
|
54
|
+
if (i % 2 === 0) {
|
|
55
|
+
messages.push(new HumanMessage(content));
|
|
56
|
+
} else {
|
|
57
|
+
messages.push(new AIMessage(content));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return messages;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Helper to build indexTokenCountMap
|
|
65
|
+
const buildIndexTokenCountMap = (
|
|
66
|
+
messages: BaseMessage[],
|
|
67
|
+
tokenCounter: t.TokenCounter
|
|
68
|
+
): Record<string, number> => {
|
|
69
|
+
const map: Record<string, number> = {};
|
|
70
|
+
messages.forEach((msg, index) => {
|
|
71
|
+
map[index] = tokenCounter(msg);
|
|
72
|
+
});
|
|
73
|
+
return map;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Estimates a human-friendly description of the conversation timeframe based on message count.
|
|
78
|
+
* This mirrors the implementation in Graph.ts
|
|
79
|
+
*/
|
|
80
|
+
const getContextTimeframeDescription = (messageCount: number): string => {
|
|
81
|
+
if (messageCount <= 5) {
|
|
82
|
+
return 'just the last few exchanges';
|
|
83
|
+
} else if (messageCount <= 15) {
|
|
84
|
+
return 'the last several minutes';
|
|
85
|
+
} else if (messageCount <= 30) {
|
|
86
|
+
return 'roughly the past hour';
|
|
87
|
+
} else if (messageCount <= 60) {
|
|
88
|
+
return 'the past couple of hours';
|
|
89
|
+
} else if (messageCount <= 150) {
|
|
90
|
+
return 'the past few hours';
|
|
91
|
+
} else if (messageCount <= 300) {
|
|
92
|
+
return 'roughly a day\'s worth';
|
|
93
|
+
} else if (messageCount <= 700) {
|
|
94
|
+
return 'the past few days';
|
|
95
|
+
} else {
|
|
96
|
+
return 'about a week or more';
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
describe('Emergency Pruning Feature', () => {
|
|
101
|
+
const tokenCounter = createTestTokenCounter();
|
|
102
|
+
|
|
103
|
+
describe('Normal Pruning vs Emergency Pruning', () => {
|
|
104
|
+
it('should prune more aggressively with 50% reduced context', () => {
|
|
105
|
+
// Create 20 messages, each with 100 tokens = 2000 tokens total (excluding system)
|
|
106
|
+
const messages = createTestMessages(20, 100);
|
|
107
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
108
|
+
messages,
|
|
109
|
+
tokenCounter
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Normal prune with 1500 token limit
|
|
113
|
+
const normalMaxTokens = 1500;
|
|
114
|
+
const normalPrune = createPruneMessages({
|
|
115
|
+
startIndex: 0,
|
|
116
|
+
provider: Providers.BEDROCK,
|
|
117
|
+
tokenCounter,
|
|
118
|
+
maxTokens: normalMaxTokens,
|
|
119
|
+
thinkingEnabled: false,
|
|
120
|
+
indexTokenCountMap,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { context: normalContext } = normalPrune({ messages });
|
|
124
|
+
|
|
125
|
+
// Emergency prune with 50% (750 tokens)
|
|
126
|
+
const emergencyMaxTokens = Math.floor(normalMaxTokens * 0.5);
|
|
127
|
+
const emergencyPrune = createPruneMessages({
|
|
128
|
+
startIndex: 0,
|
|
129
|
+
provider: Providers.BEDROCK,
|
|
130
|
+
tokenCounter,
|
|
131
|
+
maxTokens: emergencyMaxTokens,
|
|
132
|
+
thinkingEnabled: false,
|
|
133
|
+
indexTokenCountMap,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const { context: emergencyContext } = emergencyPrune({ messages });
|
|
137
|
+
|
|
138
|
+
// Emergency should have fewer messages
|
|
139
|
+
expect(emergencyContext.length).toBeLessThan(normalContext.length);
|
|
140
|
+
console.log(
|
|
141
|
+
`Normal prune: ${normalContext.length} messages, Emergency prune: ${emergencyContext.length} messages`
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should preserve system message and latest user message after emergency prune', () => {
|
|
146
|
+
const messages = createTestMessages(10, 200);
|
|
147
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
148
|
+
messages,
|
|
149
|
+
tokenCounter
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Very aggressive prune - only 300 tokens
|
|
153
|
+
const emergencyPrune = createPruneMessages({
|
|
154
|
+
startIndex: 0,
|
|
155
|
+
provider: Providers.BEDROCK,
|
|
156
|
+
tokenCounter,
|
|
157
|
+
maxTokens: 300,
|
|
158
|
+
thinkingEnabled: false,
|
|
159
|
+
indexTokenCountMap,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { context } = emergencyPrune({ messages });
|
|
163
|
+
|
|
164
|
+
// Should still have system message if it fits
|
|
165
|
+
if (context.length > 0) {
|
|
166
|
+
// Check that we have at least the most recent messages
|
|
167
|
+
const lastMessage = context[context.length - 1];
|
|
168
|
+
expect(lastMessage).toBeDefined();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Pruning Notice Message Injection', () => {
|
|
174
|
+
it('should calculate correct number of pruned messages', () => {
|
|
175
|
+
const originalCount = 20;
|
|
176
|
+
const messages = createTestMessages(originalCount, 100);
|
|
177
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
178
|
+
messages,
|
|
179
|
+
tokenCounter
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const emergencyPrune = createPruneMessages({
|
|
183
|
+
startIndex: 0,
|
|
184
|
+
provider: Providers.BEDROCK,
|
|
185
|
+
tokenCounter,
|
|
186
|
+
maxTokens: 500, // Very small to force aggressive pruning
|
|
187
|
+
thinkingEnabled: false,
|
|
188
|
+
indexTokenCountMap,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const { context: reducedMessages } = emergencyPrune({ messages });
|
|
192
|
+
|
|
193
|
+
// Calculate how many were pruned (this is what we inject in the notice)
|
|
194
|
+
const prunedCount = messages.length - reducedMessages.length;
|
|
195
|
+
|
|
196
|
+
expect(prunedCount).toBeGreaterThan(0);
|
|
197
|
+
console.log(
|
|
198
|
+
`Original: ${messages.length}, After prune: ${reducedMessages.length}, Pruned: ${prunedCount}`
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should inject personalized notice message after system message', () => {
|
|
203
|
+
const messages = createTestMessages(10, 100);
|
|
204
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
205
|
+
messages,
|
|
206
|
+
tokenCounter
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const emergencyPrune = createPruneMessages({
|
|
210
|
+
startIndex: 0,
|
|
211
|
+
provider: Providers.BEDROCK,
|
|
212
|
+
tokenCounter,
|
|
213
|
+
maxTokens: 800,
|
|
214
|
+
thinkingEnabled: false,
|
|
215
|
+
indexTokenCountMap,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { context: reducedMessages } = emergencyPrune({ messages });
|
|
219
|
+
|
|
220
|
+
// Simulate the notice injection logic from Graph.ts
|
|
221
|
+
const prunedCount = messages.length - reducedMessages.length;
|
|
222
|
+
const remainingCount = reducedMessages.length;
|
|
223
|
+
const estimatedContextDescription =
|
|
224
|
+
getContextTimeframeDescription(remainingCount);
|
|
225
|
+
|
|
226
|
+
const pruneNoticeMessage = new HumanMessage({
|
|
227
|
+
content: `[CONTEXT NOTICE]
|
|
228
|
+
Our conversation has grown quite long, so I've focused on ${estimatedContextDescription} of our chat (${remainingCount} recent messages). ${prunedCount} earlier messages are no longer in my immediate memory.
|
|
229
|
+
|
|
230
|
+
If I seem to be missing something we discussed earlier, just give me a quick reminder and I'll pick right back up! I'm still fully engaged and ready to help with whatever you need.`,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Insert after system message
|
|
234
|
+
const hasSystemMessage = reducedMessages[0]?.getType() === 'system';
|
|
235
|
+
const insertIndex = hasSystemMessage ? 1 : 0;
|
|
236
|
+
|
|
237
|
+
const messagesWithNotice = [
|
|
238
|
+
...reducedMessages.slice(0, insertIndex),
|
|
239
|
+
pruneNoticeMessage,
|
|
240
|
+
...reducedMessages.slice(insertIndex),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
// Verify notice is in correct position
|
|
244
|
+
if (hasSystemMessage) {
|
|
245
|
+
expect(messagesWithNotice[0].getType()).toBe('system');
|
|
246
|
+
expect(messagesWithNotice[1].getType()).toBe('human');
|
|
247
|
+
expect(messagesWithNotice[1].content as string).toContain(
|
|
248
|
+
'[CONTEXT NOTICE]'
|
|
249
|
+
);
|
|
250
|
+
expect(messagesWithNotice[1].content as string).toContain(
|
|
251
|
+
'recent messages'
|
|
252
|
+
);
|
|
253
|
+
expect(messagesWithNotice[1].content as string).toContain(
|
|
254
|
+
'quick reminder'
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
expect(messagesWithNotice[0].getType()).toBe('human');
|
|
258
|
+
expect(messagesWithNotice[0].content as string).toContain(
|
|
259
|
+
'[CONTEXT NOTICE]'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Total messages should be reduced + 1 notice
|
|
264
|
+
expect(messagesWithNotice.length).toBe(reducedMessages.length + 1);
|
|
265
|
+
|
|
266
|
+
console.log(
|
|
267
|
+
`Notice preview:\n${(pruneNoticeMessage.content as string).substring(0, 200)}...`
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('Context Timeframe Description', () => {
|
|
273
|
+
it('should return appropriate descriptions for different message counts', () => {
|
|
274
|
+
expect(getContextTimeframeDescription(3)).toBe(
|
|
275
|
+
'just the last few exchanges'
|
|
276
|
+
);
|
|
277
|
+
expect(getContextTimeframeDescription(10)).toBe(
|
|
278
|
+
'the last several minutes'
|
|
279
|
+
);
|
|
280
|
+
expect(getContextTimeframeDescription(25)).toBe('roughly the past hour');
|
|
281
|
+
expect(getContextTimeframeDescription(45)).toBe(
|
|
282
|
+
'the past couple of hours'
|
|
283
|
+
);
|
|
284
|
+
expect(getContextTimeframeDescription(100)).toBe('the past few hours');
|
|
285
|
+
expect(getContextTimeframeDescription(200)).toBe('roughly a day\'s worth');
|
|
286
|
+
expect(getContextTimeframeDescription(500)).toBe('the past few days');
|
|
287
|
+
expect(getContextTimeframeDescription(1000)).toBe('about a week or more');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Error Detection Patterns', () => {
|
|
292
|
+
const errorPatterns = [
|
|
293
|
+
'Input is too long for the model',
|
|
294
|
+
'context length exceeded',
|
|
295
|
+
'maximum context length',
|
|
296
|
+
'ValidationException: Input is too long',
|
|
297
|
+
'prompt is too long for this model',
|
|
298
|
+
'The input is too long',
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
it('should detect various "input too long" error patterns', () => {
|
|
302
|
+
const isInputTooLongError = (errorMessage: string): boolean => {
|
|
303
|
+
const lowerMessage = errorMessage.toLowerCase();
|
|
304
|
+
return (
|
|
305
|
+
lowerMessage.includes('too long') ||
|
|
306
|
+
lowerMessage.includes('input is too long') ||
|
|
307
|
+
lowerMessage.includes('context length') ||
|
|
308
|
+
lowerMessage.includes('maximum context') ||
|
|
309
|
+
lowerMessage.includes('validationexception') ||
|
|
310
|
+
lowerMessage.includes('prompt is too long')
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
for (const pattern of errorPatterns) {
|
|
315
|
+
expect(isInputTooLongError(pattern)).toBe(true);
|
|
316
|
+
console.log(`✓ Detected: "${pattern}"`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Should not match unrelated errors
|
|
320
|
+
expect(isInputTooLongError('Network timeout')).toBe(false);
|
|
321
|
+
expect(isInputTooLongError('Invalid API key')).toBe(false);
|
|
322
|
+
expect(isInputTooLongError('Rate limit exceeded')).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Edge Cases', () => {
|
|
327
|
+
it('should handle empty messages after pruning', () => {
|
|
328
|
+
// Single very long message that exceeds the limit
|
|
329
|
+
const messages: BaseMessage[] = [
|
|
330
|
+
new SystemMessage('System prompt'),
|
|
331
|
+
new HumanMessage('x'.repeat(10000)), // Way too long
|
|
332
|
+
];
|
|
333
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
334
|
+
messages,
|
|
335
|
+
tokenCounter
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const emergencyPrune = createPruneMessages({
|
|
339
|
+
startIndex: 0,
|
|
340
|
+
provider: Providers.BEDROCK,
|
|
341
|
+
tokenCounter,
|
|
342
|
+
maxTokens: 100, // Very small limit
|
|
343
|
+
thinkingEnabled: false,
|
|
344
|
+
indexTokenCountMap,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const { context } = emergencyPrune({ messages });
|
|
348
|
+
|
|
349
|
+
// Should have at least tried to keep something or be empty
|
|
350
|
+
// The key is it shouldn't throw
|
|
351
|
+
expect(Array.isArray(context)).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should work with only system message and one user message', () => {
|
|
355
|
+
const messages: BaseMessage[] = [
|
|
356
|
+
new SystemMessage('You are helpful.'),
|
|
357
|
+
new HumanMessage('Hello'),
|
|
358
|
+
];
|
|
359
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
360
|
+
messages,
|
|
361
|
+
tokenCounter
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const emergencyPrune = createPruneMessages({
|
|
365
|
+
startIndex: 0,
|
|
366
|
+
provider: Providers.BEDROCK,
|
|
367
|
+
tokenCounter,
|
|
368
|
+
maxTokens: 500,
|
|
369
|
+
thinkingEnabled: false,
|
|
370
|
+
indexTokenCountMap,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const { context } = emergencyPrune({ messages });
|
|
374
|
+
|
|
375
|
+
expect(context.length).toBe(2);
|
|
376
|
+
expect(context[0].getType()).toBe('system');
|
|
377
|
+
expect(context[1].getType()).toBe('human');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle conversation without system message', () => {
|
|
381
|
+
const messages: BaseMessage[] = [
|
|
382
|
+
new HumanMessage('Hello'),
|
|
383
|
+
new AIMessage('Hi there!'),
|
|
384
|
+
new HumanMessage('How are you?'),
|
|
385
|
+
];
|
|
386
|
+
const indexTokenCountMap = buildIndexTokenCountMap(
|
|
387
|
+
messages,
|
|
388
|
+
tokenCounter
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const emergencyPrune = createPruneMessages({
|
|
392
|
+
startIndex: 0,
|
|
393
|
+
provider: Providers.BEDROCK,
|
|
394
|
+
tokenCounter,
|
|
395
|
+
maxTokens: 100,
|
|
396
|
+
thinkingEnabled: false,
|
|
397
|
+
indexTokenCountMap,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const { context } = emergencyPrune({ messages });
|
|
401
|
+
|
|
402
|
+
// Should keep the most recent messages that fit
|
|
403
|
+
expect(context.length).toBeGreaterThan(0);
|
|
404
|
+
expect(context[0].getType()).not.toBe('system');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|