@librechat/agents 3.3.2 → 3.3.4

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +6 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/invoke.cjs +64 -3
  16. package/dist/cjs/llm/invoke.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +41 -5
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +19 -2
  24. package/dist/cjs/messages/cache.cjs +54 -33
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/contextPruning.cjs +17 -43
  27. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  28. package/dist/cjs/messages/core.cjs +315 -23
  29. package/dist/cjs/messages/core.cjs.map +1 -1
  30. package/dist/cjs/messages/format.cjs +218 -60
  31. package/dist/cjs/messages/format.cjs.map +1 -1
  32. package/dist/cjs/messages/index.cjs +1 -1
  33. package/dist/cjs/messages/prune.cjs +503 -127
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +7 -3
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/summarization/node.cjs +28 -14
  38. package/dist/cjs/summarization/node.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +54 -41
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  42. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  43. package/dist/cjs/utils/index.cjs +1 -1
  44. package/dist/cjs/utils/llm.cjs +1 -1
  45. package/dist/cjs/utils/llm.cjs.map +1 -1
  46. package/dist/cjs/utils/tokens.cjs +307 -20
  47. package/dist/cjs/utils/tokens.cjs.map +1 -1
  48. package/dist/cjs/utils/toolContent.cjs +1514 -0
  49. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  50. package/dist/esm/agents/AgentContext.mjs +2 -1
  51. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  52. package/dist/esm/graphs/Graph.mjs +349 -116
  53. package/dist/esm/graphs/Graph.mjs.map +1 -1
  54. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  56. package/dist/esm/langfuse.mjs +6 -0
  57. package/dist/esm/langfuse.mjs.map +1 -1
  58. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  59. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  60. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  61. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  62. package/dist/esm/llm/google/utils/common.mjs +19 -7
  63. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  64. package/dist/esm/llm/invoke.mjs +65 -5
  65. package/dist/esm/llm/invoke.mjs.map +1 -1
  66. package/dist/esm/llm/openai/index.mjs +41 -5
  67. package/dist/esm/llm/openai/index.mjs.map +1 -1
  68. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  69. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  70. package/dist/esm/llm/openrouter/index.mjs +4 -1
  71. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  72. package/dist/esm/main.mjs +7 -7
  73. package/dist/esm/messages/cache.mjs +55 -34
  74. package/dist/esm/messages/cache.mjs.map +1 -1
  75. package/dist/esm/messages/contextPruning.mjs +17 -43
  76. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  77. package/dist/esm/messages/core.mjs +305 -24
  78. package/dist/esm/messages/core.mjs.map +1 -1
  79. package/dist/esm/messages/format.mjs +218 -61
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/index.mjs +1 -1
  82. package/dist/esm/messages/prune.mjs +500 -128
  83. package/dist/esm/messages/prune.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +7 -3
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +28 -14
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +54 -41
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  91. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  92. package/dist/esm/utils/index.mjs +1 -1
  93. package/dist/esm/utils/llm.mjs +1 -1
  94. package/dist/esm/utils/llm.mjs.map +1 -1
  95. package/dist/esm/utils/tokens.mjs +307 -21
  96. package/dist/esm/utils/tokens.mjs.map +1 -1
  97. package/dist/esm/utils/toolContent.mjs +1503 -0
  98. package/dist/esm/utils/toolContent.mjs.map +1 -0
  99. package/dist/types/graphs/Graph.d.ts +1 -1
  100. package/dist/types/llm/invoke.d.ts +27 -1
  101. package/dist/types/llm/openai/index.d.ts +4 -0
  102. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  103. package/dist/types/llm/openrouter/index.d.ts +5 -0
  104. package/dist/types/messages/contextPruning.d.ts +2 -1
  105. package/dist/types/messages/core.d.ts +36 -0
  106. package/dist/types/messages/format.d.ts +6 -0
  107. package/dist/types/messages/prune.d.ts +16 -12
  108. package/dist/types/types/stream.d.ts +1 -1
  109. package/dist/types/utils/tokens.d.ts +7 -0
  110. package/dist/types/utils/toolContent.d.ts +107 -0
  111. package/package.json +13 -9
  112. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  113. package/src/agents/AgentContext.ts +1 -0
  114. package/src/graphs/Graph.ts +698 -200
  115. package/src/graphs/MultiAgentGraph.ts +6 -1
  116. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  117. package/src/langfuse.ts +15 -0
  118. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  119. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  120. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  121. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  122. package/src/llm/google/utils/common.test.ts +75 -9
  123. package/src/llm/google/utils/common.ts +34 -3
  124. package/src/llm/invoke.test.ts +488 -0
  125. package/src/llm/invoke.ts +196 -2
  126. package/src/llm/openai/index.ts +106 -6
  127. package/src/llm/openai/utils/index.ts +109 -53
  128. package/src/llm/openai/utils/messages.test.ts +330 -1
  129. package/src/llm/openrouter/index.ts +12 -2
  130. package/src/llm/openrouter/reasoning.test.ts +314 -0
  131. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  132. package/src/messages/cache.tail.test.ts +193 -0
  133. package/src/messages/cache.test.ts +113 -0
  134. package/src/messages/cache.ts +92 -60
  135. package/src/messages/contextPruning.test.ts +184 -0
  136. package/src/messages/contextPruning.ts +49 -42
  137. package/src/messages/core.ts +653 -40
  138. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  139. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  140. package/src/messages/format.ts +363 -91
  141. package/src/messages/formatAgentMessages.test.ts +604 -14
  142. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  143. package/src/messages/prune.ts +996 -183
  144. package/src/specs/langfuse-callbacks.test.ts +203 -1
  145. package/src/specs/prune.test.ts +1083 -6
  146. package/src/specs/summarization-unit.test.ts +105 -0
  147. package/src/specs/tokens.test.ts +609 -32
  148. package/src/stream.ts +23 -15
  149. package/src/summarization/__tests__/node.test.ts +77 -0
  150. package/src/summarization/node.ts +46 -18
  151. package/src/tools/ToolNode.ts +147 -68
  152. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  153. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  154. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  155. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  156. package/src/tools/toolOutputReferences.ts +6 -0
  157. package/src/types/stream.ts +1 -1
  158. package/src/utils/llm.test.ts +18 -0
  159. package/src/utils/llm.ts +4 -1
  160. package/src/utils/tokens.ts +586 -31
  161. package/src/utils/toolContent.test.ts +1432 -0
  162. package/src/utils/toolContent.ts +2707 -0
@@ -1,4 +1,5 @@
1
1
  // src/messages.ts
2
+ import { isProxy } from 'node:util/types';
2
3
  import {
3
4
  AIMessage,
4
5
  BaseMessage,
@@ -6,8 +7,20 @@ import {
6
7
  HumanMessage,
7
8
  AIMessageChunk,
8
9
  } from '@langchain/core/messages';
9
- import type { ToolCall } from '@langchain/core/messages/tool';
10
+ import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
10
11
  import type * as t from '@/types';
12
+ import {
13
+ cloneToolMessageWithContent,
14
+ compactToolContent,
15
+ getBoundedCacheControlledTextToolContent,
16
+ getBoundedSingleTextToolContent,
17
+ getComputerCallOutputScreenshot,
18
+ hasComputerCallOutputMarker,
19
+ isComputerCallOutputMessage,
20
+ serializeToolContentBounded,
21
+ } from '@/utils/toolContent';
22
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
23
+ import { stripAnthropicCacheControl } from './cache';
11
24
  import { ContentTypes, Providers } from '@/common';
12
25
  import { toLangChainContent } from './langchain';
13
26
 
@@ -181,15 +194,14 @@ function getReasoningDetailsText(
181
194
  function getAdditionalReasoningContent(
182
195
  message: BaseMessage
183
196
  ): string | undefined {
184
- const additionalKwargs =
185
- message.additional_kwargs as ReasoningAdditionalKwargs | undefined;
197
+ const additionalKwargs = message.additional_kwargs as
198
+ | ReasoningAdditionalKwargs
199
+ | undefined;
186
200
  if (additionalKwargs == null) {
187
201
  return undefined;
188
202
  }
189
203
 
190
- const reasoningContent = getReasoningText(
191
- additionalKwargs.reasoning_content
192
- );
204
+ const reasoningContent = getReasoningText(additionalKwargs.reasoning_content);
193
205
  if (reasoningContent != null) {
194
206
  return reasoningContent;
195
207
  }
@@ -432,8 +444,7 @@ export function convertMessagesToContent(
432
444
  toolCallMap.set(tool_call.id, tool_call);
433
445
  }
434
446
 
435
- currentAIMessageIndex =
436
- addContentPart(message) ?? addToolCallBoundary();
447
+ currentAIMessageIndex = addContentPart(message) ?? addToolCallBoundary();
437
448
  continue;
438
449
  } else if (
439
450
  messageType === 'tool' &&
@@ -471,24 +482,545 @@ function stringifyToolMessageContent(
471
482
  return content == null ? '' : String(content);
472
483
  }
473
484
 
474
- export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
485
+ function appendContentBlocks(
486
+ target: t.MessageContentComplex[],
487
+ content: BaseMessage['content']
488
+ ): void {
489
+ if (typeof content === 'string') {
490
+ target.push({ type: ContentTypes.TEXT, text: content });
491
+ return;
492
+ }
493
+ for (const block of content) {
494
+ target.push(block as t.MessageContentComplex);
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Appends one artifact/tool-content segment without retaining an unbounded
500
+ * intermediate block array. Both operands are compacted before they are
501
+ * combined, and the combined result is compacted again under the aggregate
502
+ * cap.
503
+ */
504
+ function appendBoundedContent(
505
+ current: BaseMessage['content'] | undefined,
506
+ next: unknown,
507
+ maxChars: number
508
+ ): BaseMessage['content'] {
509
+ const boundedNext = compactToolContent(next, maxChars).content;
510
+ if (current == null) {
511
+ return boundedNext;
512
+ }
513
+
514
+ const combined: t.MessageContentComplex[] = [];
515
+ appendContentBlocks(combined, current);
516
+ appendContentBlocks(combined, boundedNext);
517
+ return compactToolContent(toLangChainContent(combined), maxChars).content;
518
+ }
519
+
520
+ function cloneAIMessageWithToolCalls(
521
+ message: AIMessage,
522
+ toolCalls: ToolCall[],
523
+ removedCallIds: ReadonlySet<string>
524
+ ): AIMessage {
525
+ const descriptors = Object.getOwnPropertyDescriptors(message) as Record<
526
+ string,
527
+ PropertyDescriptor | undefined
528
+ >;
529
+ let descriptor = descriptors.tool_calls;
530
+ descriptors.tool_calls = {
531
+ configurable: descriptor?.configurable ?? true,
532
+ enumerable: descriptor?.enumerable ?? true,
533
+ value: toolCalls,
534
+ writable: descriptor?.writable ?? true,
535
+ };
536
+ const toolCallChunks = descriptors.tool_call_chunks?.value as
537
+ | ToolCallChunk[]
538
+ | undefined;
539
+ if (Array.isArray(toolCallChunks)) {
540
+ descriptor = descriptors.tool_call_chunks;
541
+ descriptors.tool_call_chunks = {
542
+ configurable: descriptor?.configurable ?? true,
543
+ enumerable: descriptor?.enumerable ?? true,
544
+ value: toolCallChunks.filter(
545
+ (chunk) => typeof chunk.id !== 'string' || !removedCallIds.has(chunk.id)
546
+ ),
547
+ writable: descriptor?.writable ?? true,
548
+ };
549
+ }
550
+ return Object.create(
551
+ Object.getPrototypeOf(message),
552
+ descriptors as PropertyDescriptorMap
553
+ ) as AIMessage;
554
+ }
555
+
556
+ function cloneAIMessageWithContent(
557
+ message: AIMessage,
558
+ content: AIMessage['content']
559
+ ): AIMessage {
560
+ const descriptors = Object.getOwnPropertyDescriptors(message) as Record<
561
+ string,
562
+ PropertyDescriptor | undefined
563
+ >;
564
+ const descriptor = descriptors.content;
565
+ descriptors.content = {
566
+ configurable: descriptor?.configurable ?? true,
567
+ enumerable: descriptor?.enumerable ?? true,
568
+ value: content,
569
+ writable: descriptor?.writable ?? true,
570
+ };
571
+ const lcKwargs = descriptors.lc_kwargs;
572
+ if (
573
+ lcKwargs != null &&
574
+ 'value' in lcKwargs &&
575
+ typeof lcKwargs.value === 'object' &&
576
+ lcKwargs.value != null
577
+ ) {
578
+ descriptors.lc_kwargs = {
579
+ ...lcKwargs,
580
+ value: {
581
+ ...(lcKwargs.value as Record<string, unknown>),
582
+ content,
583
+ },
584
+ };
585
+ }
586
+ return Object.create(
587
+ Object.getPrototypeOf(message),
588
+ descriptors as PropertyDescriptorMap
589
+ ) as AIMessage;
590
+ }
591
+
592
+ /**
593
+ * Drops incomplete streamed text-input fragments that some providers retain
594
+ * beside the assembled parsed tool call. They are neither user-visible text
595
+ * nor valid content blocks for a subsequent provider.
596
+ */
597
+ export function projectToolStreamContentForProvider(
598
+ messages: BaseMessage[]
599
+ ): BaseMessage[] {
600
+ let projected: BaseMessage[] | undefined;
601
+ for (let i = 0; i < messages.length; i++) {
602
+ const message = messages[i];
603
+ if (message.getType() !== 'ai' || !Array.isArray(message.content)) {
604
+ continue;
605
+ }
606
+ const content = message.content.filter((block) => {
607
+ if (block == null || typeof block !== 'object') {
608
+ return true;
609
+ }
610
+ try {
611
+ if (isProxy(block)) {
612
+ return false;
613
+ }
614
+ const type = Object.getOwnPropertyDescriptor(block, 'type');
615
+ if (type == null) {
616
+ return true;
617
+ }
618
+ if (type.enumerable !== true || !('value' in type)) {
619
+ return false;
620
+ }
621
+ if (type.value !== 'text') {
622
+ return true;
623
+ }
624
+ const text = Object.getOwnPropertyDescriptor(block, 'text');
625
+ return (
626
+ text?.enumerable === true &&
627
+ 'value' in text &&
628
+ typeof text.value === 'string' &&
629
+ text.value !== ''
630
+ );
631
+ } catch {
632
+ return false;
633
+ }
634
+ });
635
+ if (content.length === message.content.length) {
636
+ continue;
637
+ }
638
+ projected ??= [...messages];
639
+ projected[i] = cloneAIMessageWithContent(
640
+ message as AIMessage,
641
+ toLangChainContent(content)
642
+ );
643
+ }
644
+ return projected ?? messages;
645
+ }
646
+
647
+ type CacheControlledTextProjection = 'serialize' | 'preserve' | 'text';
648
+
649
+ function projectStructuredOpenAIToolContent(
650
+ content: ToolMessage['content'],
651
+ maxChars: number,
652
+ cacheControlledTextProjection: CacheControlledTextProjection
653
+ ): ToolMessage['content'] {
654
+ if (cacheControlledTextProjection !== 'serialize') {
655
+ const cacheControlledContent = getBoundedCacheControlledTextToolContent(
656
+ content,
657
+ maxChars
658
+ );
659
+ if (cacheControlledContent != null) {
660
+ return cacheControlledTextProjection === 'preserve'
661
+ ? cacheControlledContent
662
+ : cacheControlledContent[0].text;
663
+ }
664
+ const singleTextContent = getBoundedSingleTextToolContent(
665
+ content,
666
+ maxChars
667
+ );
668
+ if (singleTextContent != null) {
669
+ return singleTextContent;
670
+ }
671
+ }
672
+ const serializableContent =
673
+ cacheControlledTextProjection === 'preserve'
674
+ ? stripAnthropicCacheControl([{ content }])[0].content
675
+ : content;
676
+ return serializeToolContentBounded(serializableContent, maxChars);
677
+ }
678
+
679
+ /**
680
+ * OpenAI Chat tool messages only accept strings or text-only parts, while the
681
+ * Responses API serializes any structured ToolMessage after graph accounting.
682
+ * Project every non-string tool result to one bounded string before the final
683
+ * provider payload is measured so both APIs receive the exact representation
684
+ * the budget guard counted. Native Responses computer screenshots stay
685
+ * structured because their dedicated converter sends the media block directly.
686
+ */
687
+ function projectOpenAIToolMessageContentInternal(
688
+ messages: BaseMessage[],
689
+ maxChars: number,
690
+ deduplicateResponsesComputerCalls: boolean,
691
+ cacheControlledTextProjection: CacheControlledTextProjection
692
+ ): BaseMessage[] {
693
+ const pendingComputerCallIds: string[] = [];
694
+ const seenComputerCallIds = new Set<string>();
695
+ let projected: BaseMessage[] | undefined;
696
+ for (let i = 0; i < messages.length; i++) {
697
+ const message = messages[i];
698
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
699
+ const isAssistant =
700
+ message.getType() === 'ai' || messageRole === 'assistant';
701
+ if (isAssistant) {
702
+ const parsedComputerCallIds = new Set<string>();
703
+ const toolCalls = (message as AIMessage).tool_calls;
704
+ if (Array.isArray(toolCalls)) {
705
+ for (const toolCall of toolCalls) {
706
+ const record = toolCall as ToolCall & {
707
+ isComputerTool?: unknown;
708
+ };
709
+ if (
710
+ record.type !== 'tool_call' ||
711
+ record.isComputerTool !== true ||
712
+ typeof record.id !== 'string' ||
713
+ record.id === ''
714
+ ) {
715
+ continue;
716
+ }
717
+ if (parsedComputerCallIds.has(record.id)) {
718
+ throw new Error(`Duplicate computer call id "${record.id}"`);
719
+ }
720
+ parsedComputerCallIds.add(record.id);
721
+ if (seenComputerCallIds.has(record.id)) {
722
+ throw new Error(`Duplicate computer call id "${record.id}"`);
723
+ }
724
+ seenComputerCallIds.add(record.id);
725
+ pendingComputerCallIds.push(record.id);
726
+ }
727
+ }
728
+
729
+ const rawOutput = (
730
+ message.response_metadata as {
731
+ output?: unknown;
732
+ }
733
+ ).output;
734
+ const fallbackOutput = (
735
+ message.additional_kwargs as {
736
+ tool_outputs?: unknown;
737
+ }
738
+ ).tool_outputs;
739
+ let actualToolOutputs: unknown[] = [];
740
+ if (Array.isArray(rawOutput) && rawOutput.length > 0) {
741
+ actualToolOutputs = rawOutput;
742
+ } else if (Array.isArray(fallbackOutput)) {
743
+ actualToolOutputs = fallbackOutput;
744
+ }
745
+ const rawComputerCallIds = new Set<string>();
746
+ for (const item of actualToolOutputs) {
747
+ if (item == null || typeof item !== 'object') {
748
+ continue;
749
+ }
750
+ const record = item as {
751
+ type?: unknown;
752
+ call_id?: unknown;
753
+ };
754
+ if (
755
+ record.type !== 'computer_call' ||
756
+ typeof record.call_id !== 'string' ||
757
+ record.call_id === ''
758
+ ) {
759
+ continue;
760
+ }
761
+ if (rawComputerCallIds.has(record.call_id)) {
762
+ throw new Error(`Duplicate computer call id "${record.call_id}"`);
763
+ }
764
+ rawComputerCallIds.add(record.call_id);
765
+ // LangChain can retain the same call in parsed and raw forms. It sends
766
+ // one logical call, so collapse that representation duplicate.
767
+ if (parsedComputerCallIds.has(record.call_id)) {
768
+ continue;
769
+ }
770
+ if (seenComputerCallIds.has(record.call_id)) {
771
+ throw new Error(`Duplicate computer call id "${record.call_id}"`);
772
+ }
773
+ seenComputerCallIds.add(record.call_id);
774
+ pendingComputerCallIds.push(record.call_id);
775
+ }
776
+
777
+ if (
778
+ deduplicateResponsesComputerCalls &&
779
+ Array.isArray(toolCalls) &&
780
+ rawComputerCallIds.size > 0
781
+ ) {
782
+ /**
783
+ * The non-streaming Responses converter marks parsed computer calls,
784
+ * but the streaming converter currently emits the same call as an
785
+ * ordinary parsed `computer_use` tool call. In both cases the raw
786
+ * `computer_call` item is authoritative and is replayed by LangChain,
787
+ * so remove every parsed representation with the same call id.
788
+ */
789
+ const projectedToolCalls = toolCalls.filter(
790
+ (toolCall) =>
791
+ typeof toolCall.id !== 'string' ||
792
+ !rawComputerCallIds.has(toolCall.id)
793
+ );
794
+ if (projectedToolCalls.length !== toolCalls.length) {
795
+ projected ??= [...messages];
796
+ projected[i] = cloneAIMessageWithToolCalls(
797
+ message as AIMessage,
798
+ projectedToolCalls,
799
+ rawComputerCallIds
800
+ );
801
+ }
802
+ }
803
+ }
804
+
805
+ if (
806
+ message instanceof ToolMessage &&
807
+ hasComputerCallOutputMarker(message)
808
+ ) {
809
+ const screenshot = getComputerCallOutputScreenshot(message.content);
810
+ if (screenshot == null) {
811
+ throw new Error('Invalid computer call output screenshot');
812
+ }
813
+ if (pendingComputerCallIds[0] !== message.tool_call_id) {
814
+ throw new Error(
815
+ `Invalid computer call output pairing for "${message.tool_call_id}"`
816
+ );
817
+ }
818
+ pendingComputerCallIds.shift();
819
+ projected ??= [...messages];
820
+ projected[i] = cloneToolMessageWithContent(message, [screenshot]);
821
+ continue;
822
+ }
823
+ if (
824
+ !(message instanceof ToolMessage) ||
825
+ typeof message.content === 'string'
826
+ ) {
827
+ continue;
828
+ }
829
+ projected ??= [...messages];
830
+ projected[i] = cloneToolMessageWithContent(
831
+ message,
832
+ projectStructuredOpenAIToolContent(
833
+ message.content,
834
+ maxChars,
835
+ cacheControlledTextProjection
836
+ )
837
+ );
838
+ }
839
+ if (pendingComputerCallIds.length > 0) {
840
+ throw new Error(
841
+ `Missing computer call output for "${pendingComputerCallIds[0]}"`
842
+ );
843
+ }
844
+ return projected ?? messages;
845
+ }
846
+
847
+ /** Projects OpenAI-compatible tool content without changing parsed call parents. */
848
+ export function projectOpenAIToolMessageContent(
849
+ messages: BaseMessage[],
850
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
851
+ ): BaseMessage[] {
852
+ return projectOpenAIToolMessageContentInternal(
853
+ messages,
854
+ maxChars,
855
+ false,
856
+ 'serialize'
857
+ );
858
+ }
859
+
860
+ /** Projects an actual OpenAI-compatible Chat attempt and removes cache metadata. */
861
+ export function projectOpenAIChatToolMessageContent(
862
+ messages: BaseMessage[],
863
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
864
+ ): BaseMessage[] {
865
+ return projectOpenAIToolMessageContentInternal(
866
+ messages,
867
+ maxChars,
868
+ false,
869
+ 'text'
870
+ );
871
+ }
872
+
873
+ /** Preserves OpenRouter's cache-decorated text blocks for a Chat attempt. */
874
+ export function projectOpenRouterToolMessageContent(
875
+ messages: BaseMessage[],
876
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
877
+ ): BaseMessage[] {
878
+ return projectOpenAIToolMessageContentInternal(
879
+ messages,
880
+ maxChars,
881
+ false,
882
+ 'preserve'
883
+ );
884
+ }
885
+
886
+ /** Projects Responses tool content and collapses parsed/raw computer-call mirrors. */
887
+ export function projectOpenAIResponsesToolMessageContent(
888
+ messages: BaseMessage[],
889
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
890
+ ): BaseMessage[] {
891
+ return projectOpenAIToolMessageContentInternal(
892
+ messages,
893
+ maxChars,
894
+ true,
895
+ 'text'
896
+ );
897
+ }
898
+
899
+ /** Removes Anthropic/OpenRouter cache metadata before unsupported providers run. */
900
+ export function projectCacheControlledToolOutputsToText(
901
+ messages: BaseMessage[],
902
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
903
+ ): BaseMessage[] {
904
+ let projected: BaseMessage[] | undefined;
905
+ for (let i = 0; i < messages.length; i++) {
906
+ const message = messages[i];
907
+ if (
908
+ !(message instanceof ToolMessage) ||
909
+ typeof message.content === 'string'
910
+ ) {
911
+ continue;
912
+ }
913
+ const cacheControlledContent = getBoundedCacheControlledTextToolContent(
914
+ message.content,
915
+ maxChars
916
+ );
917
+ if (cacheControlledContent == null) {
918
+ continue;
919
+ }
920
+ projected ??= [...messages];
921
+ projected[i] = cloneToolMessageWithContent(
922
+ message,
923
+ cacheControlledContent[0].text
924
+ );
925
+ }
926
+ return projected ?? messages;
927
+ }
928
+
929
+ /** Unwraps a canonical single text block after provider cache markers are removed. */
930
+ export function projectSingleTextToolOutputsToText(
931
+ messages: BaseMessage[],
932
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
933
+ ): BaseMessage[] {
934
+ let projected: BaseMessage[] | undefined;
935
+ for (let i = 0; i < messages.length; i++) {
936
+ const message = messages[i];
937
+ if (
938
+ !(message instanceof ToolMessage) ||
939
+ typeof message.content === 'string'
940
+ ) {
941
+ continue;
942
+ }
943
+ const text = getBoundedSingleTextToolContent(message.content, maxChars);
944
+ if (text == null) {
945
+ continue;
946
+ }
947
+ projected ??= [...messages];
948
+ projected[i] = cloneToolMessageWithContent(message, text);
949
+ }
950
+ return projected ?? messages;
951
+ }
952
+
953
+ /** Serializes provider-neutral structured tool outputs without media pairing. */
954
+ export function projectStructuredToolOutputsToText(
955
+ messages: BaseMessage[],
956
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
957
+ ): BaseMessage[] {
958
+ let projected: BaseMessage[] | undefined;
959
+ for (let i = 0; i < messages.length; i++) {
960
+ const message = messages[i];
961
+ if (
962
+ !(message instanceof ToolMessage) ||
963
+ typeof message.content === 'string' ||
964
+ hasComputerCallOutputMarker(message)
965
+ ) {
966
+ continue;
967
+ }
968
+ projected ??= [...messages];
969
+ projected[i] = cloneToolMessageWithContent(
970
+ message,
971
+ serializeToolContentBounded(message.content, maxChars)
972
+ );
973
+ }
974
+ return projected ?? messages;
975
+ }
976
+
977
+ /**
978
+ * Non-Responses providers cannot consume native computer screenshots. Keep
979
+ * the tool-call structure intact, but replace screenshot bytes with a bounded
980
+ * text marker at the actual invocation boundary.
981
+ */
982
+ export function projectComputerCallOutputsToText(
983
+ messages: BaseMessage[]
984
+ ): BaseMessage[] {
985
+ let projected: BaseMessage[] | undefined;
986
+ for (let i = 0; i < messages.length; i++) {
987
+ const message = messages[i];
988
+ if (
989
+ !(message instanceof ToolMessage) ||
990
+ !hasComputerCallOutputMarker(message)
991
+ ) {
992
+ continue;
993
+ }
994
+ projected ??= [...messages];
995
+ projected[i] = cloneToolMessageWithContent(
996
+ message,
997
+ '[Computer screenshot omitted for this provider]'
998
+ );
999
+ }
1000
+ return projected ?? messages;
1001
+ }
1002
+
1003
+ export function projectAnthropicArtifactContent(
1004
+ messages: BaseMessage[],
1005
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
1006
+ ): BaseMessage[] {
475
1007
  const lastMessage = messages[messages.length - 1];
476
- if (!(lastMessage instanceof ToolMessage)) return;
1008
+ if (!(lastMessage instanceof ToolMessage)) return messages;
477
1009
 
478
1010
  // Find the latest AIMessage with tool_calls that this tool message belongs to
479
1011
  const latestAIParentIndex = findLastIndex(
480
1012
  messages,
481
1013
  (msg) =>
482
- (msg instanceof AIMessageChunk &&
1014
+ ((msg instanceof AIMessage || msg instanceof AIMessageChunk) &&
483
1015
  (msg.tool_calls?.length ?? 0) > 0 &&
484
1016
  msg.tool_calls?.some((tc) => tc.id === lastMessage.tool_call_id)) ??
485
1017
  false
486
1018
  );
487
1019
 
488
- if (latestAIParentIndex === -1) return;
1020
+ if (latestAIParentIndex === -1) return messages;
489
1021
 
490
1022
  // Build tool call ID set and merge artifact content in a single forward pass.
491
- const message = messages[latestAIParentIndex] as AIMessageChunk;
1023
+ const message = messages[latestAIParentIndex] as AIMessage | AIMessageChunk;
492
1024
  const toolCallIdSet = new Set<string>();
493
1025
  if (message.tool_calls) {
494
1026
  for (const tc of message.tool_calls) {
@@ -498,69 +1030,150 @@ export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
498
1030
  }
499
1031
  }
500
1032
 
1033
+ let formattedMessages: BaseMessage[] | undefined;
501
1034
  for (let j = latestAIParentIndex + 1; j < messages.length; j++) {
502
1035
  const msg = messages[j];
503
1036
  if (
504
1037
  msg instanceof ToolMessage &&
1038
+ !isComputerCallOutputMessage(msg) &&
505
1039
  toolCallIdSet.has(msg.tool_call_id) &&
506
1040
  msg.artifact != null &&
507
- Array.isArray(msg.artifact?.content)
1041
+ ((typeof msg.artifact?.content === 'string' &&
1042
+ msg.artifact.content.length > 0) ||
1043
+ (Array.isArray(msg.artifact?.content) &&
1044
+ msg.artifact.content.length > 0))
508
1045
  ) {
509
- const base = Array.isArray(msg.content)
1046
+ const artifactContent =
1047
+ typeof msg.artifact.content === 'string'
1048
+ ? [
1049
+ {
1050
+ type: ContentTypes.TEXT,
1051
+ text: msg.artifact.content,
1052
+ },
1053
+ ]
1054
+ : msg.artifact.content;
1055
+ const baseContent = Array.isArray(msg.content)
510
1056
  ? msg.content
511
- : [
512
- {
513
- type: ContentTypes.TEXT,
514
- text: stringifyToolMessageContent(msg.content),
515
- },
516
- ];
517
- msg.content = base.concat(msg.artifact.content);
1057
+ : stringifyToolMessageContent(msg.content);
1058
+ const content = appendBoundedContent(
1059
+ compactToolContent(baseContent, maxChars).content,
1060
+ artifactContent,
1061
+ maxChars
1062
+ );
1063
+ formattedMessages ??= [...messages];
1064
+ formattedMessages[j] = cloneToolMessageWithContent(msg, content, {
1065
+ ...msg.artifact,
1066
+ content: [],
1067
+ });
518
1068
  }
519
1069
  }
1070
+ return formattedMessages ?? messages;
520
1071
  }
521
1072
 
522
- export function formatArtifactPayload(messages: BaseMessage[]): void {
1073
+ /**
1074
+ * Mutating compatibility wrapper retained for existing package consumers.
1075
+ * New provider-call paths should use `projectAnthropicArtifactContent`.
1076
+ */
1077
+ export function formatAnthropicArtifactContent(messages: BaseMessage[]): void {
1078
+ const projected = projectAnthropicArtifactContent(messages);
1079
+ if (projected === messages) {
1080
+ return;
1081
+ }
1082
+ for (let i = 0; i < messages.length; i++) {
1083
+ if (
1084
+ messages[i] instanceof ToolMessage &&
1085
+ projected[i] instanceof ToolMessage &&
1086
+ projected[i] !== messages[i]
1087
+ ) {
1088
+ messages[i].content = projected[i].content;
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ export function projectArtifactPayload(
1094
+ messages: BaseMessage[],
1095
+ maxChars = HARD_MAX_TOOL_RESULT_CHARS
1096
+ ): BaseMessage[] {
523
1097
  const lastMessageY = messages[messages.length - 1];
524
- if (!(lastMessageY instanceof ToolMessage)) return;
1098
+ if (!(lastMessageY instanceof ToolMessage)) return messages;
525
1099
 
526
1100
  // Find the latest AIMessage with tool_calls that this tool message belongs to
527
1101
  const latestAIParentIndex = findLastIndex(
528
1102
  messages,
529
1103
  (msg) =>
530
- (msg instanceof AIMessageChunk &&
1104
+ ((msg instanceof AIMessage || msg instanceof AIMessageChunk) &&
531
1105
  (msg.tool_calls?.length ?? 0) > 0 &&
532
1106
  msg.tool_calls?.some((tc) => tc.id === lastMessageY.tool_call_id)) ??
533
1107
  false
534
1108
  );
535
1109
 
536
- if (latestAIParentIndex === -1) return;
1110
+ if (latestAIParentIndex === -1) return messages;
537
1111
 
538
1112
  // Single pass: collect relevant tool messages with artifacts and aggregate
539
- const aggregatedContent: t.MessageContentComplex[] = [];
1113
+ let aggregatedContent: BaseMessage['content'] | undefined;
1114
+ let formattedMessages: BaseMessage[] | undefined;
540
1115
 
541
1116
  for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
542
1117
  const msg = messages[i];
543
1118
  if (
544
1119
  !(msg instanceof ToolMessage) ||
545
- !Array.isArray(msg.artifact?.content)
1120
+ isComputerCallOutputMessage(msg) ||
1121
+ !(
1122
+ (typeof msg.artifact?.content === 'string' &&
1123
+ msg.artifact.content.length > 0) ||
1124
+ (Array.isArray(msg.artifact?.content) &&
1125
+ msg.artifact.content.length > 0)
1126
+ )
546
1127
  ) {
547
1128
  continue;
548
1129
  }
549
- let currentContent = msg.content;
550
- if (!Array.isArray(currentContent)) {
551
- currentContent = [{ type: 'text', text: msg.content }];
552
- }
553
- aggregatedContent.push(...(currentContent as t.MessageContentComplex[]));
554
- msg.content =
555
- 'Tool response is included in the next message as a Human message';
556
- aggregatedContent.push(...msg.artifact.content);
1130
+ aggregatedContent = appendBoundedContent(
1131
+ aggregatedContent,
1132
+ msg.content,
1133
+ maxChars
1134
+ );
1135
+ formattedMessages ??= [...messages];
1136
+ formattedMessages[i] = cloneToolMessageWithContent(
1137
+ msg,
1138
+ 'Tool response is included in the next message as a Human message',
1139
+ {
1140
+ ...msg.artifact,
1141
+ content: [],
1142
+ }
1143
+ );
1144
+ aggregatedContent = appendBoundedContent(
1145
+ aggregatedContent,
1146
+ msg.artifact.content,
1147
+ maxChars
1148
+ );
557
1149
  }
558
1150
 
559
- if (aggregatedContent.length > 0) {
560
- messages.push(
561
- new HumanMessage({ content: toLangChainContent(aggregatedContent) })
562
- );
1151
+ if (aggregatedContent != null) {
1152
+ formattedMessages?.push(new HumanMessage({ content: aggregatedContent }));
1153
+ }
1154
+ return formattedMessages ?? messages;
1155
+ }
1156
+
1157
+ /**
1158
+ * Mutating compatibility wrapper retained for existing package consumers.
1159
+ * New provider-call paths should use `projectArtifactPayload`.
1160
+ */
1161
+ export function formatArtifactPayload(messages: BaseMessage[]): void {
1162
+ const originalLength = messages.length;
1163
+ const projected = projectArtifactPayload(messages);
1164
+ if (projected === messages) {
1165
+ return;
1166
+ }
1167
+ for (let i = 0; i < originalLength; i++) {
1168
+ if (
1169
+ messages[i] instanceof ToolMessage &&
1170
+ projected[i] instanceof ToolMessage &&
1171
+ projected[i] !== messages[i]
1172
+ ) {
1173
+ messages[i].content = projected[i].content;
1174
+ }
563
1175
  }
1176
+ messages.push(...projected.slice(originalLength));
564
1177
  }
565
1178
 
566
1179
  export function findLastIndex<T>(