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.
Files changed (63) hide show
  1. package/dist/cjs/common/enum.cjs +18 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +88 -20
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/openai/index.cjs +1 -0
  6. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +13 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/core.cjs +16 -8
  10. package/dist/cjs/messages/core.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +8 -2
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/messages/tools.cjs +17 -10
  14. package/dist/cjs/messages/tools.cjs.map +1 -1
  15. package/dist/cjs/stream.cjs +1 -0
  16. package/dist/cjs/stream.cjs.map +1 -1
  17. package/dist/cjs/tools/ToolNode.cjs +73 -3
  18. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  19. package/dist/cjs/tools/handlers.cjs +1 -0
  20. package/dist/cjs/tools/handlers.cjs.map +1 -1
  21. package/dist/cjs/utils/contextAnalytics.cjs +64 -0
  22. package/dist/cjs/utils/contextAnalytics.cjs.map +1 -0
  23. package/dist/cjs/utils/toonFormat.cjs +358 -0
  24. package/dist/cjs/utils/toonFormat.cjs.map +1 -0
  25. package/dist/esm/common/enum.mjs +19 -1
  26. package/dist/esm/common/enum.mjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +90 -22
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/llm/openai/index.mjs +1 -0
  30. package/dist/esm/llm/openai/index.mjs.map +1 -1
  31. package/dist/esm/main.mjs +3 -1
  32. package/dist/esm/main.mjs.map +1 -1
  33. package/dist/esm/messages/core.mjs +18 -10
  34. package/dist/esm/messages/core.mjs.map +1 -1
  35. package/dist/esm/messages/format.mjs +9 -3
  36. package/dist/esm/messages/format.mjs.map +1 -1
  37. package/dist/esm/messages/tools.mjs +19 -12
  38. package/dist/esm/messages/tools.mjs.map +1 -1
  39. package/dist/esm/stream.mjs +1 -0
  40. package/dist/esm/stream.mjs.map +1 -1
  41. package/dist/esm/tools/ToolNode.mjs +73 -3
  42. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  43. package/dist/esm/tools/handlers.mjs +1 -0
  44. package/dist/esm/tools/handlers.mjs.map +1 -1
  45. package/dist/esm/utils/contextAnalytics.mjs +62 -0
  46. package/dist/esm/utils/contextAnalytics.mjs.map +1 -0
  47. package/dist/esm/utils/toonFormat.mjs +351 -0
  48. package/dist/esm/utils/toonFormat.mjs.map +1 -0
  49. package/dist/types/common/enum.d.ts +17 -0
  50. package/dist/types/graphs/Graph.d.ts +8 -0
  51. package/dist/types/utils/contextAnalytics.d.ts +37 -0
  52. package/dist/types/utils/index.d.ts +2 -0
  53. package/dist/types/utils/toonFormat.d.ts +111 -0
  54. package/package.json +2 -1
  55. package/src/common/enum.ts +18 -0
  56. package/src/graphs/Graph.ts +113 -27
  57. package/src/messages/core.ts +27 -19
  58. package/src/messages/format.ts +10 -3
  59. package/src/messages/tools.ts +20 -13
  60. package/src/tools/ToolNode.ts +78 -5
  61. package/src/utils/contextAnalytics.ts +95 -0
  62. package/src/utils/index.ts +2 -0
  63. package/src/utils/toonFormat.ts +437 -0
@@ -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 instanceof ToolMessage &&
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
- const isLatestToolMessage = lastMessageY instanceof ToolMessage;
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
- // If input too long and we have pruning capability, retry with progressively more aggressive pruning
910
- if (isInputTooLongError && agentContext.pruneMessages && agentContext.maxContextTokens) {
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: agentContext.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 Bedrock cache control if needed
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
@@ -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
- if (!(lastMessage instanceof ToolMessage)) return;
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 === lastMessage.tool_call_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 instanceof ToolMessage &&
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 instanceof ToolMessage &&
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
- if (!(lastMessageY instanceof ToolMessage)) return;
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 === lastMessageY.tool_call_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 instanceof ToolMessage &&
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 instanceof ToolMessage) as ToolMessage[];
431
+ .filter((msg) => msg.getType() === MessageTypes.TOOL) as ToolMessage[];
424
432
 
425
433
  // Aggregate all content and artifacts
426
434
  const aggregatedContent: t.MessageContentComplex[] = [];
@@ -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: output != null ? output : '',
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
- while (j < messages.length && messages[j] instanceof ToolMessage) {
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
  }
@@ -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
- if (!(lastMessage instanceof ToolMessage)) return [];
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 === lastMessage.tool_call_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
- if (!(msg instanceof ToolMessage)) continue;
46
- if (msg.name !== Constants.TOOL_SEARCH_REGEX) continue;
47
- if (!toolCallIds.has(msg.tool_call_id)) continue;
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 msg.artifact === 'object' && msg.artifact != null) {
51
- const artifact = msg.artifact as ToolSearchArtifact;
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
- if (!(lastMessage instanceof ToolMessage)) return false;
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 === lastMessage.tool_call_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 instanceof ToolMessage &&
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
  }
@@ -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
- if (
144
- (isBaseMessage(output) && output._getType() === 'tool') ||
145
- isCommand(output)
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: typeof output === 'string' ? output : JSON.stringify(output),
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
+ }