illuma-agents 1.0.15 → 1.0.17
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/common/enum.cjs +18 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +88 -20
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +1 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +13 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +16 -8
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +8 -2
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +17 -10
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/stream.cjs +1 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +73 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -0
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/utils/contextAnalytics.cjs +64 -0
- package/dist/cjs/utils/contextAnalytics.cjs.map +1 -0
- package/dist/cjs/utils/toonFormat.cjs +358 -0
- package/dist/cjs/utils/toonFormat.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +19 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +90 -22
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +1 -0
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +18 -10
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +9 -3
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +19 -12
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/stream.mjs +1 -0
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +73 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -0
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/utils/contextAnalytics.mjs +62 -0
- package/dist/esm/utils/contextAnalytics.mjs.map +1 -0
- package/dist/esm/utils/toonFormat.mjs +351 -0
- package/dist/esm/utils/toonFormat.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +17 -0
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/utils/contextAnalytics.d.ts +37 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/toonFormat.d.ts +111 -0
- package/package.json +2 -1
- package/src/common/enum.ts +18 -0
- package/src/graphs/Graph.ts +113 -27
- package/src/messages/core.ts +27 -19
- package/src/messages/format.ts +10 -3
- package/src/messages/tools.ts +20 -13
- package/src/tools/ToolNode.ts +78 -5
- package/src/utils/contextAnalytics.ts +95 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/toonFormat.ts +437 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
GraphEvents,
|
|
37
37
|
Providers,
|
|
38
38
|
StepTypes,
|
|
39
|
+
MessageTypes,
|
|
39
40
|
} from '@/common';
|
|
40
41
|
import {
|
|
41
42
|
formatAnthropicArtifactContent,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
joinKeys,
|
|
57
58
|
sleep,
|
|
58
59
|
} from '@/utils';
|
|
60
|
+
import { buildContextAnalytics, type ContextAnalytics } from '@/utils/contextAnalytics';
|
|
59
61
|
import { getChatModelClass, manualToolStreamProviders } from '@/llm/providers';
|
|
60
62
|
import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
61
63
|
import { ChatOpenAI, AzureChatOpenAI } from '@/llm/openai';
|
|
@@ -445,6 +447,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
445
447
|
return primaryContext.getContextBreakdown();
|
|
446
448
|
}
|
|
447
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Get the latest context analytics from the graph.
|
|
452
|
+
* Returns metrics like utilization %, TOON stats, message breakdown.
|
|
453
|
+
*/
|
|
454
|
+
getContextAnalytics(): ContextAnalytics | null {
|
|
455
|
+
return this.lastContextAnalytics ?? null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Store the latest context analytics for retrieval after run */
|
|
459
|
+
private lastContextAnalytics: ContextAnalytics | null = null;
|
|
460
|
+
|
|
448
461
|
/* Graph */
|
|
449
462
|
|
|
450
463
|
createSystemRunnable({
|
|
@@ -732,6 +745,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
732
745
|
this.config = config;
|
|
733
746
|
|
|
734
747
|
let messagesToUse = messages;
|
|
748
|
+
|
|
735
749
|
if (
|
|
736
750
|
!agentContext.pruneMessages &&
|
|
737
751
|
agentContext.tokenCounter &&
|
|
@@ -760,6 +774,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
760
774
|
indexTokenCountMap: agentContext.indexTokenCountMap,
|
|
761
775
|
});
|
|
762
776
|
}
|
|
777
|
+
|
|
763
778
|
if (agentContext.pruneMessages) {
|
|
764
779
|
const { context, indexTokenCountMap } = agentContext.pruneMessages({
|
|
765
780
|
messages,
|
|
@@ -787,13 +802,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
787
802
|
if (
|
|
788
803
|
agentContext.provider === Providers.BEDROCK &&
|
|
789
804
|
lastMessageX instanceof AIMessageChunk &&
|
|
790
|
-
lastMessageY
|
|
805
|
+
lastMessageY?.getType() === MessageTypes.TOOL &&
|
|
791
806
|
typeof lastMessageX.content === 'string'
|
|
792
807
|
) {
|
|
793
808
|
finalMessages[finalMessages.length - 2].content = '';
|
|
794
809
|
}
|
|
795
810
|
|
|
796
|
-
|
|
811
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
812
|
+
const isLatestToolMessage = lastMessageY?.getType() === MessageTypes.TOOL;
|
|
797
813
|
|
|
798
814
|
if (
|
|
799
815
|
isLatestToolMessage &&
|
|
@@ -809,6 +825,33 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
809
825
|
formatArtifactPayload(finalMessages);
|
|
810
826
|
}
|
|
811
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Handle edge case: when switching from a non-thinking agent to a thinking-enabled agent,
|
|
830
|
+
* convert AI messages with tool calls to HumanMessages to avoid thinking block requirements.
|
|
831
|
+
* This is required by Anthropic/Bedrock when thinking is enabled.
|
|
832
|
+
*
|
|
833
|
+
* IMPORTANT: This MUST happen BEFORE cache control is applied.
|
|
834
|
+
* If we add cachePoint to an AI message first, then convert that AI message to a HumanMessage,
|
|
835
|
+
* the cachePoint is lost. By converting first, we ensure cache control is applied to the
|
|
836
|
+
* final message structure that will be sent to the API.
|
|
837
|
+
*/
|
|
838
|
+
const isAnthropicWithThinking =
|
|
839
|
+
(agentContext.provider === Providers.ANTHROPIC &&
|
|
840
|
+
(agentContext.clientOptions as t.AnthropicClientOptions).thinking !=
|
|
841
|
+
null) ||
|
|
842
|
+
(agentContext.provider === Providers.BEDROCK &&
|
|
843
|
+
(agentContext.clientOptions as t.BedrockAnthropicInput)
|
|
844
|
+
.additionalModelRequestFields?.['thinking'] != null);
|
|
845
|
+
|
|
846
|
+
if (isAnthropicWithThinking) {
|
|
847
|
+
finalMessages = ensureThinkingBlockInMessages(
|
|
848
|
+
finalMessages,
|
|
849
|
+
agentContext.provider
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Apply cache control AFTER thinking block handling to ensure cachePoints aren't lost
|
|
854
|
+
// when AI messages are converted to HumanMessages
|
|
812
855
|
if (agentContext.provider === Providers.ANTHROPIC) {
|
|
813
856
|
const anthropicOptions = agentContext.clientOptions as
|
|
814
857
|
| t.AnthropicClientOptions
|
|
@@ -836,26 +879,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
836
879
|
}
|
|
837
880
|
}
|
|
838
881
|
|
|
839
|
-
/**
|
|
840
|
-
* Handle edge case: when switching from a non-thinking agent to a thinking-enabled agent,
|
|
841
|
-
* convert AI messages with tool calls to HumanMessages to avoid thinking block requirements.
|
|
842
|
-
* This is required by Anthropic/Bedrock when thinking is enabled.
|
|
843
|
-
*/
|
|
844
|
-
const isAnthropicWithThinking =
|
|
845
|
-
(agentContext.provider === Providers.ANTHROPIC &&
|
|
846
|
-
(agentContext.clientOptions as t.AnthropicClientOptions).thinking !=
|
|
847
|
-
null) ||
|
|
848
|
-
(agentContext.provider === Providers.BEDROCK &&
|
|
849
|
-
(agentContext.clientOptions as t.BedrockAnthropicInput)
|
|
850
|
-
.additionalModelRequestFields?.['thinking'] != null);
|
|
851
|
-
|
|
852
|
-
if (isAnthropicWithThinking) {
|
|
853
|
-
finalMessages = ensureThinkingBlockInMessages(
|
|
854
|
-
finalMessages,
|
|
855
|
-
agentContext.provider
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
882
|
if (
|
|
860
883
|
agentContext.lastStreamCall != null &&
|
|
861
884
|
agentContext.streamBuffer != null
|
|
@@ -885,6 +908,35 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
885
908
|
);
|
|
886
909
|
}
|
|
887
910
|
|
|
911
|
+
// Get model info for analytics
|
|
912
|
+
const bedrockOpts = agentContext.clientOptions as t.BedrockAnthropicClientOptions | undefined;
|
|
913
|
+
const modelId = bedrockOpts?.model || (agentContext.clientOptions as t.AnthropicClientOptions | undefined)?.modelName;
|
|
914
|
+
const thinkingConfig = bedrockOpts?.additionalModelRequestFields?.['thinking'] ||
|
|
915
|
+
(agentContext.clientOptions as t.AnthropicClientOptions | undefined)?.thinking;
|
|
916
|
+
|
|
917
|
+
// Build and emit context analytics for traces
|
|
918
|
+
const contextAnalytics = buildContextAnalytics(finalMessages, {
|
|
919
|
+
tokenCounter: agentContext.tokenCounter,
|
|
920
|
+
maxContextTokens: agentContext.maxContextTokens,
|
|
921
|
+
instructionTokens: agentContext.instructionTokens,
|
|
922
|
+
indexTokenCountMap: agentContext.indexTokenCountMap,
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Store for retrieval via getContextAnalytics() after run completes
|
|
926
|
+
this.lastContextAnalytics = contextAnalytics;
|
|
927
|
+
|
|
928
|
+
await safeDispatchCustomEvent(
|
|
929
|
+
GraphEvents.ON_CONTEXT_ANALYTICS,
|
|
930
|
+
{
|
|
931
|
+
provider: agentContext.provider,
|
|
932
|
+
model: modelId,
|
|
933
|
+
thinkingEnabled: thinkingConfig != null,
|
|
934
|
+
cacheEnabled: bedrockOpts?.promptCache === true,
|
|
935
|
+
analytics: contextAnalytics,
|
|
936
|
+
},
|
|
937
|
+
config
|
|
938
|
+
);
|
|
939
|
+
|
|
888
940
|
try {
|
|
889
941
|
result = await this.attemptInvoke(
|
|
890
942
|
{
|
|
@@ -906,26 +958,50 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
906
958
|
errorMessage.includes('validationexception') ||
|
|
907
959
|
errorMessage.includes('prompt is too long');
|
|
908
960
|
|
|
909
|
-
//
|
|
910
|
-
if (isInputTooLongError
|
|
961
|
+
// Log when we detect the error
|
|
962
|
+
if (isInputTooLongError) {
|
|
963
|
+
console.warn('[Graph] Detected input too long error:', errorMessage.substring(0, 200));
|
|
964
|
+
console.warn('[Graph] Checking emergency pruning conditions:', {
|
|
965
|
+
hasPruneMessages: !!agentContext.pruneMessages,
|
|
966
|
+
hasTokenCounter: !!agentContext.tokenCounter,
|
|
967
|
+
maxContextTokens: agentContext.maxContextTokens,
|
|
968
|
+
indexTokenMapKeys: Object.keys(agentContext.indexTokenCountMap).length
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// If input too long and we have pruning capability OR tokenCounter, retry with progressively more aggressive pruning
|
|
973
|
+
// Note: We can create emergency pruneMessages dynamically if we have tokenCounter and maxContextTokens
|
|
974
|
+
const canPrune = agentContext.tokenCounter && agentContext.maxContextTokens;
|
|
975
|
+
if (isInputTooLongError && canPrune) {
|
|
911
976
|
// Progressive reduction: 50% -> 25% -> 10% of original context
|
|
912
977
|
const reductionLevels = [0.5, 0.25, 0.1];
|
|
913
978
|
|
|
914
979
|
for (const reductionFactor of reductionLevels) {
|
|
915
980
|
if (result) break; // Exit if we got a result
|
|
916
981
|
|
|
917
|
-
const reducedMaxTokens = Math.floor(agentContext.maxContextTokens * reductionFactor);
|
|
982
|
+
const reducedMaxTokens = Math.floor(agentContext.maxContextTokens! * reductionFactor);
|
|
918
983
|
console.warn(
|
|
919
984
|
`[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
|
|
920
985
|
);
|
|
921
986
|
|
|
987
|
+
// Build fresh indexTokenCountMap if missing/incomplete
|
|
988
|
+
// This is needed when messages were dynamically added without updating the token map
|
|
989
|
+
let tokenMapForPruning = agentContext.indexTokenCountMap;
|
|
990
|
+
if (Object.keys(tokenMapForPruning).length < messages.length) {
|
|
991
|
+
console.warn('[Graph] Building fresh token count map for emergency pruning...');
|
|
992
|
+
tokenMapForPruning = {};
|
|
993
|
+
for (let i = 0; i < messages.length; i++) {
|
|
994
|
+
tokenMapForPruning[i] = agentContext.tokenCounter!(messages[i]);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
922
998
|
const emergencyPrune = createPruneMessages({
|
|
923
999
|
startIndex: this.startIndex,
|
|
924
1000
|
provider: agentContext.provider,
|
|
925
1001
|
tokenCounter: agentContext.tokenCounter!,
|
|
926
1002
|
maxTokens: reducedMaxTokens,
|
|
927
1003
|
thinkingEnabled: false, // Disable thinking for emergency prune
|
|
928
|
-
indexTokenCountMap:
|
|
1004
|
+
indexTokenCountMap: tokenMapForPruning,
|
|
929
1005
|
});
|
|
930
1006
|
|
|
931
1007
|
const { context: reducedMessages } = emergencyPrune({
|
|
@@ -967,7 +1043,17 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
967
1043
|
? formatContentStrings(messagesWithNotice)
|
|
968
1044
|
: messagesWithNotice;
|
|
969
1045
|
|
|
970
|
-
// Apply
|
|
1046
|
+
// Apply thinking block handling first (before cache control)
|
|
1047
|
+
// This ensures AI+Tool sequences are converted to HumanMessages
|
|
1048
|
+
// before we add cache points that could be lost in the conversion
|
|
1049
|
+
if (isAnthropicWithThinking) {
|
|
1050
|
+
retryMessages = ensureThinkingBlockInMessages(
|
|
1051
|
+
retryMessages,
|
|
1052
|
+
agentContext.provider
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Apply Bedrock cache control if needed (after thinking block handling)
|
|
971
1057
|
if (agentContext.provider === Providers.BEDROCK) {
|
|
972
1058
|
const bedrockOptions = agentContext.clientOptions as
|
|
973
1059
|
| t.BedrockAnthropicClientOptions
|
package/src/messages/core.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from '@langchain/core/messages';
|
|
9
9
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
10
10
|
import type * as t from '@/types';
|
|
11
|
-
import { Providers } from '@/common';
|
|
11
|
+
import { Providers, MessageTypes } from '@/common';
|
|
12
12
|
|
|
13
13
|
export function getConverseOverrideMessage({
|
|
14
14
|
userMessage,
|
|
@@ -346,7 +346,9 @@ export function convertMessagesToContent(
|
|
|
346
346
|
|
|
347
347
|
export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
|
|
348
348
|
const lastMessage = messages[messages.length - 1];
|
|
349
|
-
|
|
349
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
350
|
+
if (lastMessage?.getType() !== 'tool') return;
|
|
351
|
+
const lastToolMessage = lastMessage as ToolMessage;
|
|
350
352
|
|
|
351
353
|
// Find the latest AIMessage with tool_calls that this tool message belongs to
|
|
352
354
|
const latestAIParentIndex = findLastIndex(
|
|
@@ -354,20 +356,21 @@ export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
|
|
|
354
356
|
(msg) =>
|
|
355
357
|
(msg instanceof AIMessageChunk &&
|
|
356
358
|
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
357
|
-
msg.tool_calls?.some((tc) => tc.id ===
|
|
359
|
+
msg.tool_calls?.some((tc) => tc.id === lastToolMessage.tool_call_id)) ??
|
|
358
360
|
false
|
|
359
361
|
);
|
|
360
362
|
|
|
361
363
|
if (latestAIParentIndex === -1) return;
|
|
362
364
|
|
|
363
365
|
// Check if any tool message after the AI message has array artifact content
|
|
366
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
364
367
|
const hasArtifactContent = messages.some(
|
|
365
368
|
(msg, i) =>
|
|
366
369
|
i > latestAIParentIndex &&
|
|
367
|
-
msg
|
|
368
|
-
msg.artifact != null &&
|
|
369
|
-
msg.artifact?.content != null &&
|
|
370
|
-
Array.isArray(msg.artifact.content)
|
|
370
|
+
msg.getType() === MessageTypes.TOOL &&
|
|
371
|
+
(msg as ToolMessage).artifact != null &&
|
|
372
|
+
(msg as ToolMessage).artifact?.content != null &&
|
|
373
|
+
Array.isArray((msg as ToolMessage).artifact.content)
|
|
371
374
|
);
|
|
372
375
|
|
|
373
376
|
if (!hasArtifactContent) return;
|
|
@@ -377,21 +380,24 @@ export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
|
|
|
377
380
|
|
|
378
381
|
for (let j = latestAIParentIndex + 1; j < messages.length; j++) {
|
|
379
382
|
const msg = messages[j];
|
|
383
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
380
384
|
if (
|
|
381
|
-
msg
|
|
382
|
-
toolCallIds.includes(msg.tool_call_id) &&
|
|
383
|
-
msg.artifact != null &&
|
|
384
|
-
Array.isArray(msg.artifact?.content) &&
|
|
385
|
+
msg.getType() === MessageTypes.TOOL &&
|
|
386
|
+
toolCallIds.includes((msg as ToolMessage).tool_call_id) &&
|
|
387
|
+
(msg as ToolMessage).artifact != null &&
|
|
388
|
+
Array.isArray((msg as ToolMessage).artifact?.content) &&
|
|
385
389
|
Array.isArray(msg.content)
|
|
386
390
|
) {
|
|
387
|
-
msg.content = msg.content.concat(msg.artifact.content);
|
|
391
|
+
msg.content = (msg.content as t.MessageContentComplex[]).concat((msg as ToolMessage).artifact.content);
|
|
388
392
|
}
|
|
389
393
|
}
|
|
390
394
|
}
|
|
391
395
|
|
|
392
396
|
export function formatArtifactPayload(messages: BaseMessage[]): void {
|
|
393
397
|
const lastMessageY = messages[messages.length - 1];
|
|
394
|
-
|
|
398
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
399
|
+
if (lastMessageY?.getType() !== 'tool') return;
|
|
400
|
+
const lastToolMessage = lastMessageY as ToolMessage;
|
|
395
401
|
|
|
396
402
|
// Find the latest AIMessage with tool_calls that this tool message belongs to
|
|
397
403
|
const latestAIParentIndex = findLastIndex(
|
|
@@ -399,28 +405,30 @@ export function formatArtifactPayload(messages: BaseMessage[]): void {
|
|
|
399
405
|
(msg) =>
|
|
400
406
|
(msg instanceof AIMessageChunk &&
|
|
401
407
|
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
402
|
-
msg.tool_calls?.some((tc) => tc.id ===
|
|
408
|
+
msg.tool_calls?.some((tc) => tc.id === lastToolMessage.tool_call_id)) ??
|
|
403
409
|
false
|
|
404
410
|
);
|
|
405
411
|
|
|
406
412
|
if (latestAIParentIndex === -1) return;
|
|
407
413
|
|
|
408
414
|
// Check if any tool message after the AI message has array artifact content
|
|
415
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
409
416
|
const hasArtifactContent = messages.some(
|
|
410
417
|
(msg, i) =>
|
|
411
418
|
i > latestAIParentIndex &&
|
|
412
|
-
msg
|
|
413
|
-
msg.artifact != null &&
|
|
414
|
-
msg.artifact?.content != null &&
|
|
415
|
-
Array.isArray(msg.artifact.content)
|
|
419
|
+
msg.getType() === MessageTypes.TOOL &&
|
|
420
|
+
(msg as ToolMessage).artifact != null &&
|
|
421
|
+
(msg as ToolMessage).artifact?.content != null &&
|
|
422
|
+
Array.isArray((msg as ToolMessage).artifact.content)
|
|
416
423
|
);
|
|
417
424
|
|
|
418
425
|
if (!hasArtifactContent) return;
|
|
419
426
|
|
|
420
427
|
// Collect all relevant tool messages and their artifacts
|
|
428
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
421
429
|
const relevantMessages = messages
|
|
422
430
|
.slice(latestAIParentIndex + 1)
|
|
423
|
-
.filter((msg) => msg
|
|
431
|
+
.filter((msg) => msg.getType() === MessageTypes.TOOL) as ToolMessage[];
|
|
424
432
|
|
|
425
433
|
// Aggregate all content and artifacts
|
|
426
434
|
const aggregatedContent: t.MessageContentComplex[] = [];
|
package/src/messages/format.ts
CHANGED
|
@@ -19,7 +19,8 @@ import type {
|
|
|
19
19
|
TPayload,
|
|
20
20
|
TMessage,
|
|
21
21
|
} from '@/types';
|
|
22
|
-
import { Providers, ContentTypes } from '@/common';
|
|
22
|
+
import { Providers, ContentTypes, MessageTypes } from '@/common';
|
|
23
|
+
import { processToolOutput } from '@/utils/toonFormat';
|
|
23
24
|
|
|
24
25
|
interface MediaMessageParams {
|
|
25
26
|
message: {
|
|
@@ -359,11 +360,15 @@ function formatAssistantMessage(
|
|
|
359
360
|
}
|
|
360
361
|
lastAIMessage.tool_calls.push(tool_call as ToolCall);
|
|
361
362
|
|
|
363
|
+
// Apply TOON compression to historical tool outputs for context efficiency
|
|
364
|
+
// processToolOutput handles: JSON→TOON conversion, already-TOON detection (skip), truncation
|
|
365
|
+
const processedOutput = output != null ? processToolOutput(output).content : '';
|
|
366
|
+
|
|
362
367
|
formattedMessages.push(
|
|
363
368
|
new ToolMessage({
|
|
364
369
|
tool_call_id: tool_call.id ?? '',
|
|
365
370
|
name: tool_call.name,
|
|
366
|
-
content:
|
|
371
|
+
content: processedOutput,
|
|
367
372
|
})
|
|
368
373
|
);
|
|
369
374
|
} else if (part.type === ContentTypes.THINK) {
|
|
@@ -898,7 +903,9 @@ export function ensureThinkingBlockInMessages(
|
|
|
898
903
|
let j = i + 1;
|
|
899
904
|
|
|
900
905
|
// Look ahead for tool messages that belong to this AI message
|
|
901
|
-
|
|
906
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
907
|
+
// where different copies of ToolMessage class might be loaded
|
|
908
|
+
while (j < messages.length && messages[j].getType() === MessageTypes.TOOL) {
|
|
902
909
|
toolSequence.push(messages[j]);
|
|
903
910
|
j++;
|
|
904
911
|
}
|
package/src/messages/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/messages/toolDiscovery.ts
|
|
2
2
|
import { AIMessageChunk, ToolMessage } from '@langchain/core/messages';
|
|
3
3
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
|
-
import { Constants } from '@/common';
|
|
4
|
+
import { Constants, MessageTypes } from '@/common';
|
|
5
5
|
import { findLastIndex } from './core';
|
|
6
6
|
|
|
7
7
|
type ToolSearchArtifact = {
|
|
@@ -20,7 +20,9 @@ type ToolSearchArtifact = {
|
|
|
20
20
|
*/
|
|
21
21
|
export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
22
22
|
const lastMessage = messages[messages.length - 1];
|
|
23
|
-
|
|
23
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
24
|
+
if (lastMessage?.getType() !== MessageTypes.TOOL) return [];
|
|
25
|
+
const lastToolMessage = lastMessage as ToolMessage;
|
|
24
26
|
|
|
25
27
|
// Find the latest AIMessage with tool_calls that this tool message belongs to
|
|
26
28
|
const latestAIParentIndex = findLastIndex(
|
|
@@ -28,7 +30,7 @@ export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
|
28
30
|
(msg) =>
|
|
29
31
|
(msg instanceof AIMessageChunk &&
|
|
30
32
|
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
31
|
-
msg.tool_calls?.some((tc) => tc.id ===
|
|
33
|
+
msg.tool_calls?.some((tc) => tc.id === lastToolMessage.tool_call_id)) ??
|
|
32
34
|
false
|
|
33
35
|
);
|
|
34
36
|
|
|
@@ -42,13 +44,15 @@ export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
|
42
44
|
const discoveredNames: string[] = [];
|
|
43
45
|
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
44
46
|
const msg = messages[i];
|
|
45
|
-
|
|
46
|
-
if (msg.
|
|
47
|
-
|
|
47
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
48
|
+
if (msg.getType() !== MessageTypes.TOOL) continue;
|
|
49
|
+
const toolMsg = msg as ToolMessage;
|
|
50
|
+
if (toolMsg.name !== Constants.TOOL_SEARCH_REGEX) continue;
|
|
51
|
+
if (!toolCallIds.has(toolMsg.tool_call_id)) continue;
|
|
48
52
|
|
|
49
53
|
// This is a tool search result from the current turn
|
|
50
|
-
if (typeof
|
|
51
|
-
const artifact =
|
|
54
|
+
if (typeof toolMsg.artifact === 'object' && toolMsg.artifact != null) {
|
|
55
|
+
const artifact = toolMsg.artifact as ToolSearchArtifact;
|
|
52
56
|
if (artifact.tool_references && artifact.tool_references.length > 0) {
|
|
53
57
|
for (const ref of artifact.tool_references) {
|
|
54
58
|
discoveredNames.push(ref.tool_name);
|
|
@@ -66,7 +70,9 @@ export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
|
66
70
|
*/
|
|
67
71
|
export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
|
|
68
72
|
const lastMessage = messages[messages.length - 1];
|
|
69
|
-
|
|
73
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
74
|
+
if (lastMessage?.getType() !== MessageTypes.TOOL) return false;
|
|
75
|
+
const lastToolMessage = lastMessage as ToolMessage;
|
|
70
76
|
|
|
71
77
|
// Find the latest AIMessage with tool_calls
|
|
72
78
|
const latestAIParentIndex = findLastIndex(
|
|
@@ -74,7 +80,7 @@ export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
|
|
|
74
80
|
(msg) =>
|
|
75
81
|
(msg instanceof AIMessageChunk &&
|
|
76
82
|
(msg.tool_calls?.length ?? 0) > 0 &&
|
|
77
|
-
msg.tool_calls?.some((tc) => tc.id ===
|
|
83
|
+
msg.tool_calls?.some((tc) => tc.id === lastToolMessage.tool_call_id)) ??
|
|
78
84
|
false
|
|
79
85
|
);
|
|
80
86
|
|
|
@@ -84,12 +90,13 @@ export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
|
|
|
84
90
|
const toolCallIds = new Set(aiMessage.tool_calls?.map((tc) => tc.id) ?? []);
|
|
85
91
|
|
|
86
92
|
// Check if any tool search results exist after the AI message
|
|
93
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
87
94
|
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
88
95
|
const msg = messages[i];
|
|
89
96
|
if (
|
|
90
|
-
msg
|
|
91
|
-
msg.name === Constants.TOOL_SEARCH_REGEX &&
|
|
92
|
-
toolCallIds.has(msg.tool_call_id)
|
|
97
|
+
msg.getType() === MessageTypes.TOOL &&
|
|
98
|
+
(msg as ToolMessage).name === Constants.TOOL_SEARCH_REGEX &&
|
|
99
|
+
toolCallIds.has((msg as ToolMessage).tool_call_id)
|
|
93
100
|
) {
|
|
94
101
|
return true;
|
|
95
102
|
}
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { BaseMessage, AIMessage } from '@langchain/core/messages';
|
|
|
20
20
|
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
21
21
|
import type * as t from '@/types';
|
|
22
22
|
import { RunnableCallable } from '@/utils';
|
|
23
|
+
import { processToolOutput } from '@/utils/toonFormat';
|
|
23
24
|
import { Constants } from '@/common';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -29,6 +30,43 @@ function isSend(value: unknown): value is Send {
|
|
|
29
30
|
return value instanceof Send;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Extract text content from a ToolMessage content field.
|
|
35
|
+
* Handles both string and MessageContentComplex[] formats.
|
|
36
|
+
* For array content (e.g., from content_and_artifact tools), extracts text from text blocks.
|
|
37
|
+
*/
|
|
38
|
+
function extractStringContent(content: unknown): string {
|
|
39
|
+
// Already a string - return as is
|
|
40
|
+
if (typeof content === 'string') {
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Array of content blocks - extract text from each
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
const textParts: string[] = [];
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
if (typeof block === 'string') {
|
|
49
|
+
textParts.push(block);
|
|
50
|
+
} else if (block && typeof block === 'object') {
|
|
51
|
+
// Handle {type: 'text', text: '...'} format
|
|
52
|
+
const obj = block as Record<string, unknown>;
|
|
53
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
54
|
+
textParts.push(obj.text);
|
|
55
|
+
} else if (typeof obj.text === 'string') {
|
|
56
|
+
// Just has 'text' property
|
|
57
|
+
textParts.push(obj.text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (textParts.length > 0) {
|
|
62
|
+
return textParts.join('\n');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fallback: stringify whatever it is
|
|
67
|
+
return JSON.stringify(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
71
|
export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
34
72
|
private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
|
|
@@ -140,16 +178,51 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
const output = await tool.invoke(invokeParams, config);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
) {
|
|
181
|
+
|
|
182
|
+
// Handle Command outputs directly
|
|
183
|
+
if (isCommand(output)) {
|
|
147
184
|
return output;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ========================================================================
|
|
188
|
+
// TOOL OUTPUT PROCESSING - Single point for all tools (MCP and non-MCP)
|
|
189
|
+
// 1. Extract string content from any output format
|
|
190
|
+
// 2. Apply TOON conversion if content contains JSON
|
|
191
|
+
// 3. Apply truncation if still too large
|
|
192
|
+
// 4. Return ToolMessage with processed string content
|
|
193
|
+
// ========================================================================
|
|
194
|
+
|
|
195
|
+
// Step 1: Extract string content from the output
|
|
196
|
+
let rawContent: string;
|
|
197
|
+
if (isBaseMessage(output) && output._getType() === 'tool') {
|
|
198
|
+
const toolMsg = output as ToolMessage;
|
|
199
|
+
rawContent = extractStringContent(toolMsg.content);
|
|
200
|
+
} else {
|
|
201
|
+
rawContent = extractStringContent(output);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Step 2 & 3: Apply TOON conversion and truncation
|
|
205
|
+
const processed = processToolOutput(rawContent, {
|
|
206
|
+
maxLength: 100000, // 100K char limit
|
|
207
|
+
enableToon: true,
|
|
208
|
+
minSizeForToon: 1000,
|
|
209
|
+
minReductionPercent: 10, // Only apply TOON when clearly beneficial
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Step 4: Return ToolMessage with processed string content
|
|
213
|
+
if (isBaseMessage(output) && output._getType() === 'tool') {
|
|
214
|
+
const toolMsg = output as ToolMessage;
|
|
215
|
+
return new ToolMessage({
|
|
216
|
+
status: toolMsg.status,
|
|
217
|
+
name: toolMsg.name,
|
|
218
|
+
content: processed.content,
|
|
219
|
+
tool_call_id: toolMsg.tool_call_id,
|
|
220
|
+
});
|
|
148
221
|
} else {
|
|
149
222
|
return new ToolMessage({
|
|
150
223
|
status: 'success',
|
|
151
224
|
name: tool.name,
|
|
152
|
-
content:
|
|
225
|
+
content: processed.content,
|
|
153
226
|
tool_call_id: call.id!,
|
|
154
227
|
});
|
|
155
228
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Analytics Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides context analytics data for observability/traces.
|
|
5
|
+
* No console logging - just data structures for event emission.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
9
|
+
import type { TokenCounter } from '@/types/run';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Context analytics data for traces
|
|
13
|
+
*/
|
|
14
|
+
export interface ContextAnalytics {
|
|
15
|
+
/** Total messages in context */
|
|
16
|
+
messageCount: number;
|
|
17
|
+
/** Total tokens in context */
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
/** Maximum allowed context tokens */
|
|
20
|
+
maxContextTokens?: number;
|
|
21
|
+
/** Instruction/system tokens */
|
|
22
|
+
instructionTokens?: number;
|
|
23
|
+
/** Context utilization percentage (0-100) */
|
|
24
|
+
utilizationPercent?: number;
|
|
25
|
+
/** Breakdown by message type */
|
|
26
|
+
breakdown?: Record<string, { tokens: number; percent: number }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build context analytics for traces (no logging)
|
|
31
|
+
*/
|
|
32
|
+
export function buildContextAnalytics(
|
|
33
|
+
messages: BaseMessage[],
|
|
34
|
+
options: {
|
|
35
|
+
tokenCounter?: TokenCounter;
|
|
36
|
+
maxContextTokens?: number;
|
|
37
|
+
instructionTokens?: number;
|
|
38
|
+
indexTokenCountMap?: Record<string, number | undefined>;
|
|
39
|
+
}
|
|
40
|
+
): ContextAnalytics {
|
|
41
|
+
const { tokenCounter, maxContextTokens, instructionTokens, indexTokenCountMap } = options;
|
|
42
|
+
|
|
43
|
+
// Calculate total tokens
|
|
44
|
+
let totalTokens = 0;
|
|
45
|
+
const breakdown: Record<string, { tokens: number; percent: number }> = {};
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < messages.length; i++) {
|
|
48
|
+
const msg = messages[i];
|
|
49
|
+
const type = msg.getType();
|
|
50
|
+
|
|
51
|
+
let tokens = 0;
|
|
52
|
+
if (indexTokenCountMap && indexTokenCountMap[i] != null) {
|
|
53
|
+
tokens = indexTokenCountMap[i]!;
|
|
54
|
+
} else if (tokenCounter) {
|
|
55
|
+
try {
|
|
56
|
+
tokens = tokenCounter(msg);
|
|
57
|
+
} catch {
|
|
58
|
+
// Estimate from content length
|
|
59
|
+
const content = typeof msg.content === 'string'
|
|
60
|
+
? msg.content
|
|
61
|
+
: JSON.stringify(msg.content);
|
|
62
|
+
tokens = Math.ceil(content.length / 4);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
totalTokens += tokens;
|
|
67
|
+
|
|
68
|
+
if (!breakdown[type]) {
|
|
69
|
+
breakdown[type] = { tokens: 0, percent: 0 };
|
|
70
|
+
}
|
|
71
|
+
breakdown[type].tokens += tokens;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Calculate percentages
|
|
75
|
+
for (const type of Object.keys(breakdown)) {
|
|
76
|
+
breakdown[type].percent = totalTokens > 0
|
|
77
|
+
? Math.round((breakdown[type].tokens / totalTokens) * 1000) / 10
|
|
78
|
+
: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Calculate utilization
|
|
82
|
+
let utilizationPercent: number | undefined;
|
|
83
|
+
if (maxContextTokens && maxContextTokens > 0) {
|
|
84
|
+
utilizationPercent = Math.round((totalTokens / maxContextTokens) * 1000) / 10;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
messageCount: messages.length,
|
|
89
|
+
totalTokens,
|
|
90
|
+
maxContextTokens,
|
|
91
|
+
instructionTokens,
|
|
92
|
+
utilizationPercent,
|
|
93
|
+
breakdown,
|
|
94
|
+
};
|
|
95
|
+
}
|