illuma-agents 1.0.16 → 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 +63 -25
- 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 +65 -27
- 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 +84 -33
- 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({
|
|
@@ -733,16 +746,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
733
746
|
|
|
734
747
|
let messagesToUse = messages;
|
|
735
748
|
|
|
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
|
-
|
|
746
749
|
if (
|
|
747
750
|
!agentContext.pruneMessages &&
|
|
748
751
|
agentContext.tokenCounter &&
|
|
@@ -771,6 +774,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
771
774
|
indexTokenCountMap: agentContext.indexTokenCountMap,
|
|
772
775
|
});
|
|
773
776
|
}
|
|
777
|
+
|
|
774
778
|
if (agentContext.pruneMessages) {
|
|
775
779
|
const { context, indexTokenCountMap } = agentContext.pruneMessages({
|
|
776
780
|
messages,
|
|
@@ -798,13 +802,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
798
802
|
if (
|
|
799
803
|
agentContext.provider === Providers.BEDROCK &&
|
|
800
804
|
lastMessageX instanceof AIMessageChunk &&
|
|
801
|
-
lastMessageY
|
|
805
|
+
lastMessageY?.getType() === MessageTypes.TOOL &&
|
|
802
806
|
typeof lastMessageX.content === 'string'
|
|
803
807
|
) {
|
|
804
808
|
finalMessages[finalMessages.length - 2].content = '';
|
|
805
809
|
}
|
|
806
810
|
|
|
807
|
-
|
|
811
|
+
// Use getType() instead of instanceof to avoid module mismatch issues
|
|
812
|
+
const isLatestToolMessage = lastMessageY?.getType() === MessageTypes.TOOL;
|
|
808
813
|
|
|
809
814
|
if (
|
|
810
815
|
isLatestToolMessage &&
|
|
@@ -820,6 +825,33 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
820
825
|
formatArtifactPayload(finalMessages);
|
|
821
826
|
}
|
|
822
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
|
|
823
855
|
if (agentContext.provider === Providers.ANTHROPIC) {
|
|
824
856
|
const anthropicOptions = agentContext.clientOptions as
|
|
825
857
|
| t.AnthropicClientOptions
|
|
@@ -847,26 +879,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
847
879
|
}
|
|
848
880
|
}
|
|
849
881
|
|
|
850
|
-
/**
|
|
851
|
-
* Handle edge case: when switching from a non-thinking agent to a thinking-enabled agent,
|
|
852
|
-
* convert AI messages with tool calls to HumanMessages to avoid thinking block requirements.
|
|
853
|
-
* This is required by Anthropic/Bedrock when thinking is enabled.
|
|
854
|
-
*/
|
|
855
|
-
const isAnthropicWithThinking =
|
|
856
|
-
(agentContext.provider === Providers.ANTHROPIC &&
|
|
857
|
-
(agentContext.clientOptions as t.AnthropicClientOptions).thinking !=
|
|
858
|
-
null) ||
|
|
859
|
-
(agentContext.provider === Providers.BEDROCK &&
|
|
860
|
-
(agentContext.clientOptions as t.BedrockAnthropicInput)
|
|
861
|
-
.additionalModelRequestFields?.['thinking'] != null);
|
|
862
|
-
|
|
863
|
-
if (isAnthropicWithThinking) {
|
|
864
|
-
finalMessages = ensureThinkingBlockInMessages(
|
|
865
|
-
finalMessages,
|
|
866
|
-
agentContext.provider
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
882
|
if (
|
|
871
883
|
agentContext.lastStreamCall != null &&
|
|
872
884
|
agentContext.streamBuffer != null
|
|
@@ -896,6 +908,35 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
896
908
|
);
|
|
897
909
|
}
|
|
898
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
|
+
|
|
899
940
|
try {
|
|
900
941
|
result = await this.attemptInvoke(
|
|
901
942
|
{
|
|
@@ -1002,7 +1043,17 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
1002
1043
|
? formatContentStrings(messagesWithNotice)
|
|
1003
1044
|
: messagesWithNotice;
|
|
1004
1045
|
|
|
1005
|
-
// 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)
|
|
1006
1057
|
if (agentContext.provider === Providers.BEDROCK) {
|
|
1007
1058
|
const bedrockOptions = agentContext.clientOptions as
|
|
1008
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
|
+
}
|