illuma-agents 1.0.17 → 1.0.19

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