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.
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 +63 -25
  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 +65 -27
  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 +84 -33
  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({
@@ -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 instanceof ToolMessage &&
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
- const isLatestToolMessage = lastMessageY instanceof ToolMessage;
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 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)
1006
1057
  if (agentContext.provider === Providers.BEDROCK) {
1007
1058
  const bedrockOptions = agentContext.clientOptions as
1008
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
+ }
@@ -4,3 +4,5 @@ export * from './misc';
4
4
  export * from './handlers';
5
5
  export * from './run';
6
6
  export * from './tokens';
7
+ export * from './toonFormat';
8
+ export * from './contextAnalytics';