illuma-agents 1.0.14 → 1.0.16
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/graphs/Graph.cjs +186 -24
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +186 -24
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +212 -28
- package/src/specs/emergency-prune.test.ts +355 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -209,6 +209,39 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Estimates a human-friendly description of the conversation timeframe based on message count.
|
|
214
|
+
* Uses rough heuristics to provide context about how much history is available.
|
|
215
|
+
*
|
|
216
|
+
* @param messageCount - Number of messages in the remaining context
|
|
217
|
+
* @returns A friendly description like "the last few minutes", "the past hour", etc.
|
|
218
|
+
*/
|
|
219
|
+
getContextTimeframeDescription(messageCount: number): string {
|
|
220
|
+
// Rough heuristics based on typical conversation patterns:
|
|
221
|
+
// - Very active chat: ~20-30 messages per hour
|
|
222
|
+
// - Normal chat: ~10-15 messages per hour
|
|
223
|
+
// - Slow/thoughtful chat: ~5-8 messages per hour
|
|
224
|
+
// We use a middle estimate of ~12 messages per hour
|
|
225
|
+
|
|
226
|
+
if (messageCount <= 5) {
|
|
227
|
+
return 'just the last few exchanges';
|
|
228
|
+
} else if (messageCount <= 15) {
|
|
229
|
+
return 'the last several minutes';
|
|
230
|
+
} else if (messageCount <= 30) {
|
|
231
|
+
return 'roughly the past hour';
|
|
232
|
+
} else if (messageCount <= 60) {
|
|
233
|
+
return 'the past couple of hours';
|
|
234
|
+
} else if (messageCount <= 150) {
|
|
235
|
+
return 'the past few hours';
|
|
236
|
+
} else if (messageCount <= 300) {
|
|
237
|
+
return 'roughly a day\'s worth';
|
|
238
|
+
} else if (messageCount <= 700) {
|
|
239
|
+
return 'the past few days';
|
|
240
|
+
} else {
|
|
241
|
+
return 'about a week or more';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
212
245
|
/* Run Step Processing */
|
|
213
246
|
|
|
214
247
|
getRunStep(stepId: string): t.RunStep | undefined {
|
|
@@ -699,6 +732,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
699
732
|
this.config = config;
|
|
700
733
|
|
|
701
734
|
let messagesToUse = messages;
|
|
735
|
+
|
|
736
|
+
// Debug logging for pruneMessages creation conditions
|
|
737
|
+
const hasPruneMessages = !!agentContext.pruneMessages;
|
|
738
|
+
const hasTokenCounter = !!agentContext.tokenCounter;
|
|
739
|
+
const hasMaxContextTokens = agentContext.maxContextTokens != null;
|
|
740
|
+
const hasIndex0TokenCount = agentContext.indexTokenCountMap[0] != null;
|
|
741
|
+
|
|
742
|
+
if (!hasPruneMessages && hasTokenCounter && hasMaxContextTokens && !hasIndex0TokenCount) {
|
|
743
|
+
console.warn('[Graph] Cannot create pruneMessages - missing indexTokenCountMap[0]. Token map keys:', Object.keys(agentContext.indexTokenCountMap));
|
|
744
|
+
}
|
|
745
|
+
|
|
702
746
|
if (
|
|
703
747
|
!agentContext.pruneMessages &&
|
|
704
748
|
agentContext.tokenCounter &&
|
|
@@ -863,37 +907,177 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
863
907
|
config
|
|
864
908
|
);
|
|
865
909
|
} catch (primaryError) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
910
|
+
// Check if this is a "input too long" error from Bedrock/Anthropic
|
|
911
|
+
const errorMessage = (primaryError as Error)?.message?.toLowerCase() ?? '';
|
|
912
|
+
const isInputTooLongError =
|
|
913
|
+
errorMessage.includes('too long') ||
|
|
914
|
+
errorMessage.includes('input is too long') ||
|
|
915
|
+
errorMessage.includes('context length') ||
|
|
916
|
+
errorMessage.includes('maximum context') ||
|
|
917
|
+
errorMessage.includes('validationexception') ||
|
|
918
|
+
errorMessage.includes('prompt is too long');
|
|
919
|
+
|
|
920
|
+
// Log when we detect the error
|
|
921
|
+
if (isInputTooLongError) {
|
|
922
|
+
console.warn('[Graph] Detected input too long error:', errorMessage.substring(0, 200));
|
|
923
|
+
console.warn('[Graph] Checking emergency pruning conditions:', {
|
|
924
|
+
hasPruneMessages: !!agentContext.pruneMessages,
|
|
925
|
+
hasTokenCounter: !!agentContext.tokenCounter,
|
|
926
|
+
maxContextTokens: agentContext.maxContextTokens,
|
|
927
|
+
indexTokenMapKeys: Object.keys(agentContext.indexTokenCountMap).length
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// If input too long and we have pruning capability OR tokenCounter, retry with progressively more aggressive pruning
|
|
932
|
+
// Note: We can create emergency pruneMessages dynamically if we have tokenCounter and maxContextTokens
|
|
933
|
+
const canPrune = agentContext.tokenCounter && agentContext.maxContextTokens;
|
|
934
|
+
if (isInputTooLongError && canPrune) {
|
|
935
|
+
// Progressive reduction: 50% -> 25% -> 10% of original context
|
|
936
|
+
const reductionLevels = [0.5, 0.25, 0.1];
|
|
937
|
+
|
|
938
|
+
for (const reductionFactor of reductionLevels) {
|
|
939
|
+
if (result) break; // Exit if we got a result
|
|
940
|
+
|
|
941
|
+
const reducedMaxTokens = Math.floor(agentContext.maxContextTokens! * reductionFactor);
|
|
942
|
+
console.warn(
|
|
943
|
+
`[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
|
|
887
944
|
);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
945
|
+
|
|
946
|
+
// Build fresh indexTokenCountMap if missing/incomplete
|
|
947
|
+
// This is needed when messages were dynamically added without updating the token map
|
|
948
|
+
let tokenMapForPruning = agentContext.indexTokenCountMap;
|
|
949
|
+
if (Object.keys(tokenMapForPruning).length < messages.length) {
|
|
950
|
+
console.warn('[Graph] Building fresh token count map for emergency pruning...');
|
|
951
|
+
tokenMapForPruning = {};
|
|
952
|
+
for (let i = 0; i < messages.length; i++) {
|
|
953
|
+
tokenMapForPruning[i] = agentContext.tokenCounter!(messages[i]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const emergencyPrune = createPruneMessages({
|
|
958
|
+
startIndex: this.startIndex,
|
|
959
|
+
provider: agentContext.provider,
|
|
960
|
+
tokenCounter: agentContext.tokenCounter!,
|
|
961
|
+
maxTokens: reducedMaxTokens,
|
|
962
|
+
thinkingEnabled: false, // Disable thinking for emergency prune
|
|
963
|
+
indexTokenCountMap: tokenMapForPruning,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
const { context: reducedMessages } = emergencyPrune({
|
|
967
|
+
messages,
|
|
968
|
+
usageMetadata: agentContext.currentUsage,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Skip if we can't fit any messages
|
|
972
|
+
if (reducedMessages.length === 0) {
|
|
973
|
+
console.warn(`[Graph] Cannot fit any messages at ${reductionFactor * 100}% reduction, trying next level...`);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Calculate how many messages were pruned and estimate context timeframe
|
|
978
|
+
const prunedCount = finalMessages.length - reducedMessages.length;
|
|
979
|
+
const remainingCount = reducedMessages.length;
|
|
980
|
+
const estimatedContextDescription = this.getContextTimeframeDescription(remainingCount);
|
|
981
|
+
|
|
982
|
+
// Inject a personalized context message to inform the agent about pruning
|
|
983
|
+
const pruneNoticeMessage = new HumanMessage({
|
|
984
|
+
content: `[CONTEXT NOTICE]
|
|
985
|
+
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.
|
|
986
|
+
|
|
987
|
+
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.`,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// Insert the notice after the system message (if any) but before conversation
|
|
991
|
+
const hasSystemMessage = reducedMessages[0]?.getType() === 'system';
|
|
992
|
+
const insertIndex = hasSystemMessage ? 1 : 0;
|
|
993
|
+
|
|
994
|
+
// Create new array with the pruning notice
|
|
995
|
+
const messagesWithNotice = [
|
|
996
|
+
...reducedMessages.slice(0, insertIndex),
|
|
997
|
+
pruneNoticeMessage,
|
|
998
|
+
...reducedMessages.slice(insertIndex),
|
|
999
|
+
];
|
|
1000
|
+
|
|
1001
|
+
let retryMessages = agentContext.useLegacyContent
|
|
1002
|
+
? formatContentStrings(messagesWithNotice)
|
|
1003
|
+
: messagesWithNotice;
|
|
1004
|
+
|
|
1005
|
+
// Apply Bedrock cache control if needed
|
|
1006
|
+
if (agentContext.provider === Providers.BEDROCK) {
|
|
1007
|
+
const bedrockOptions = agentContext.clientOptions as
|
|
1008
|
+
| t.BedrockAnthropicClientOptions
|
|
1009
|
+
| undefined;
|
|
1010
|
+
const modelId = bedrockOptions?.model?.toLowerCase() ?? '';
|
|
1011
|
+
const supportsCaching = modelId.includes('claude') || modelId.includes('anthropic') || modelId.includes('nova');
|
|
1012
|
+
if (bedrockOptions?.promptCache === true && supportsCaching) {
|
|
1013
|
+
retryMessages = addBedrockCacheControl<BaseMessage>(retryMessages);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
result = await this.attemptInvoke(
|
|
1019
|
+
{
|
|
1020
|
+
currentModel: model,
|
|
1021
|
+
finalMessages: retryMessages,
|
|
1022
|
+
provider: agentContext.provider,
|
|
1023
|
+
tools: agentContext.tools,
|
|
1024
|
+
},
|
|
1025
|
+
config
|
|
1026
|
+
);
|
|
1027
|
+
// Success with reduced context
|
|
1028
|
+
console.info(`[Graph] ✅ Retry successful at ${reductionFactor * 100}% with ${reducedMessages.length} messages (reduced from ${finalMessages.length})`);
|
|
1029
|
+
} catch (retryError) {
|
|
1030
|
+
const retryErrorMsg = (retryError as Error)?.message?.toLowerCase() ?? '';
|
|
1031
|
+
const stillTooLong =
|
|
1032
|
+
retryErrorMsg.includes('too long') ||
|
|
1033
|
+
retryErrorMsg.includes('context length') ||
|
|
1034
|
+
retryErrorMsg.includes('validationexception');
|
|
1035
|
+
|
|
1036
|
+
if (stillTooLong && reductionFactor > 0.1) {
|
|
1037
|
+
console.warn(`[Graph] Still too long at ${reductionFactor * 100}%, trying more aggressive pruning...`);
|
|
1038
|
+
} else {
|
|
1039
|
+
console.error(`[Graph] Retry at ${reductionFactor * 100}% failed:`, (retryError as Error)?.message);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
893
1042
|
}
|
|
894
1043
|
}
|
|
895
|
-
|
|
896
|
-
|
|
1044
|
+
|
|
1045
|
+
// If we got a result from retry, skip fallbacks
|
|
1046
|
+
if (result) {
|
|
1047
|
+
// result already set from retry
|
|
1048
|
+
} else {
|
|
1049
|
+
let lastError: unknown = primaryError;
|
|
1050
|
+
for (const fb of fallbacks) {
|
|
1051
|
+
try {
|
|
1052
|
+
let model = this.getNewModel({
|
|
1053
|
+
provider: fb.provider,
|
|
1054
|
+
clientOptions: fb.clientOptions,
|
|
1055
|
+
});
|
|
1056
|
+
const bindableTools = agentContext.tools;
|
|
1057
|
+
model = (
|
|
1058
|
+
!bindableTools || bindableTools.length === 0
|
|
1059
|
+
? model
|
|
1060
|
+
: model.bindTools(bindableTools)
|
|
1061
|
+
) as t.ChatModelInstance;
|
|
1062
|
+
result = await this.attemptInvoke(
|
|
1063
|
+
{
|
|
1064
|
+
currentModel: model,
|
|
1065
|
+
finalMessages,
|
|
1066
|
+
provider: fb.provider,
|
|
1067
|
+
tools: agentContext.tools,
|
|
1068
|
+
},
|
|
1069
|
+
config
|
|
1070
|
+
);
|
|
1071
|
+
lastError = undefined;
|
|
1072
|
+
break;
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
lastError = e;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (lastError !== undefined) {
|
|
1079
|
+
throw lastError;
|
|
1080
|
+
}
|
|
897
1081
|
}
|
|
898
1082
|
}
|
|
899
1083
|
|
|
@@ -0,0 +1,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 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
|
+
});
|