@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
@@ -5,13 +5,30 @@ import {
5
5
  SystemMessage,
6
6
  ToolMessage,
7
7
  } from '@langchain/core/messages';
8
+ import {
9
+ convertMessagesToCompletionsMessageParams,
10
+ convertResponsesDeltaToChatGenerationChunk,
11
+ convertMessagesToResponsesInput,
12
+ convertResponsesMessageToAIMessage,
13
+ } from '@langchain/openai';
8
14
  import type { MessageContentComplex, TPayload } from '@/types';
9
- import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
10
15
  import {
11
16
  convertMessagesToContent,
12
17
  formatAnthropicArtifactContent,
18
+ formatArtifactPayload,
19
+ projectAnthropicArtifactContent,
20
+ projectArtifactPayload,
21
+ projectComputerCallOutputsToText,
22
+ projectOpenAIToolMessageContent,
23
+ projectOpenAIResponsesToolMessageContent,
13
24
  } from './core';
25
+ import {
26
+ _convertMessagesToOpenAIParams,
27
+ _convertMessagesToOpenAIResponsesParams,
28
+ } from '@/llm/openai/utils';
29
+ import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
14
30
  import { Constants, ContentTypes, Providers } from '@/common';
31
+ import { serializeToolContent } from '@/utils/toolContent';
15
32
  import { formatAgentMessages } from './format';
16
33
 
17
34
  type AnthropicPayloadBlock = {
@@ -1551,7 +1568,7 @@ describe('formatAgentMessages', () => {
1551
1568
  configurable: true,
1552
1569
  });
1553
1570
 
1554
- formatAnthropicArtifactContent([
1571
+ const originalMessages = [
1555
1572
  new AIMessageChunk({
1556
1573
  content: '',
1557
1574
  tool_calls: [
@@ -1564,12 +1581,575 @@ describe('formatAgentMessages', () => {
1564
1581
  ],
1565
1582
  }),
1566
1583
  toolMessage,
1567
- ]);
1584
+ ];
1585
+ const formattedMessages = projectAnthropicArtifactContent(originalMessages);
1586
+ const formattedToolMessage = formattedMessages[1] as ToolMessage;
1568
1587
 
1569
- expect(toolMessage.content).toEqual([
1588
+ expect(formattedToolMessage.content).toEqual([
1570
1589
  { type: ContentTypes.TEXT, text: '' },
1571
1590
  { type: ContentTypes.TEXT, text: 'artifact text' },
1572
1591
  ]);
1592
+ expect(toolMessage.content).toBeUndefined();
1593
+ expect(formattedMessages).not.toBe(originalMessages);
1594
+ });
1595
+
1596
+ it('caps Anthropic artifact expansion after pruning', () => {
1597
+ const toolMessage = new ToolMessage({
1598
+ content: 'short result',
1599
+ tool_call_id: 'call_artifact_capped',
1600
+ artifact: {
1601
+ content: 'artifact'.repeat(1_000),
1602
+ },
1603
+ });
1604
+ const messages = [
1605
+ new AIMessageChunk({
1606
+ content: '',
1607
+ tool_calls: [
1608
+ {
1609
+ id: 'call_artifact_capped',
1610
+ name: 'render',
1611
+ args: {},
1612
+ type: 'tool_call' as const,
1613
+ },
1614
+ ],
1615
+ }),
1616
+ toolMessage,
1617
+ ];
1618
+
1619
+ const formatted = projectAnthropicArtifactContent(messages, 200);
1620
+ const formattedTool = formatted[1] as ToolMessage;
1621
+
1622
+ expect(
1623
+ serializeToolContent(formattedTool.content).length
1624
+ ).toBeLessThanOrEqual(200);
1625
+ expect(serializeToolContent(formattedTool.content)).toContain('truncated');
1626
+ expect(toolMessage.content).toBe('short result');
1627
+ expect(toolMessage.artifact.content).toContain('artifact');
1628
+ expect(projectAnthropicArtifactContent(formatted, 200)).toBe(formatted);
1629
+ });
1630
+
1631
+ it('caps the aggregate OpenAI/Google artifact payload', () => {
1632
+ const messages = [
1633
+ new AIMessageChunk({
1634
+ content: '',
1635
+ tool_calls: [
1636
+ {
1637
+ id: 'call_artifact_payload',
1638
+ name: 'render',
1639
+ args: {},
1640
+ type: 'tool_call' as const,
1641
+ },
1642
+ ],
1643
+ }),
1644
+ new ToolMessage({
1645
+ content: 'short result',
1646
+ tool_call_id: 'call_artifact_payload',
1647
+ artifact: {
1648
+ content: [
1649
+ { type: ContentTypes.TEXT, text: 'artifact'.repeat(1_000) },
1650
+ ],
1651
+ },
1652
+ }),
1653
+ ];
1654
+
1655
+ const formatted = projectArtifactPayload(messages, 200);
1656
+
1657
+ const payload = formatted[formatted.length - 1] as HumanMessage;
1658
+ expect(payload).toBeInstanceOf(HumanMessage);
1659
+ expect(serializeToolContent(payload.content).length).toBeLessThanOrEqual(
1660
+ 200
1661
+ );
1662
+ expect(messages).toHaveLength(2);
1663
+ expect((messages[1] as ToolMessage).content).toBe('short result');
1664
+ expect(projectArtifactPayload(formatted, 200)).toBe(formatted);
1665
+ });
1666
+
1667
+ it('bounds artifact block arrays without spreading them into call arguments', () => {
1668
+ const repeatedBlock = {
1669
+ type: ContentTypes.TEXT,
1670
+ text: 'x',
1671
+ } as MessageContentComplex;
1672
+ const artifactContent = new Array<MessageContentComplex>(150_000).fill(
1673
+ repeatedBlock
1674
+ );
1675
+ const toolMessage = new ToolMessage({
1676
+ content: 'short result',
1677
+ tool_call_id: 'call_artifact_many_blocks',
1678
+ artifact: { content: artifactContent },
1679
+ });
1680
+ const messages = [
1681
+ new AIMessageChunk({
1682
+ content: '',
1683
+ tool_calls: [
1684
+ {
1685
+ id: 'call_artifact_many_blocks',
1686
+ name: 'render',
1687
+ args: {},
1688
+ type: 'tool_call' as const,
1689
+ },
1690
+ ],
1691
+ }),
1692
+ toolMessage,
1693
+ ];
1694
+
1695
+ const formatted = projectArtifactPayload(messages, 200);
1696
+ const payload = formatted[formatted.length - 1] as HumanMessage;
1697
+
1698
+ expect(payload).toBeInstanceOf(HumanMessage);
1699
+ expect(serializeToolContent(payload.content).length).toBeLessThanOrEqual(
1700
+ 200
1701
+ );
1702
+ expect(toolMessage.content).toBe('short result');
1703
+ expect(toolMessage.artifact.content).toBe(artifactContent);
1704
+ });
1705
+
1706
+ it('projects structured tool content to the bounded string both OpenAI APIs send', () => {
1707
+ let toJSONCalls = 0;
1708
+ const toolMessage = new ToolMessage({
1709
+ id: 'tool-message-id',
1710
+ name: 'render',
1711
+ status: 'success',
1712
+ tool_call_id: 'call_openai_structured',
1713
+ content: [
1714
+ { type: ContentTypes.TEXT, text: 'rendered chart' },
1715
+ {
1716
+ type: 'image_url',
1717
+ image_url: {
1718
+ url: `data:image/png;base64,${'A'.repeat(2_000)}`,
1719
+ },
1720
+ toJSON() {
1721
+ toJSONCalls++;
1722
+ return { expanded: 'B'.repeat(20_000) };
1723
+ },
1724
+ },
1725
+ ],
1726
+ artifact: { retained: true },
1727
+ });
1728
+
1729
+ const original = [toolMessage];
1730
+ const projected = projectOpenAIToolMessageContent(original, 200);
1731
+ const projectedTool = projected[0] as ToolMessage;
1732
+ const chatPayload = _convertMessagesToOpenAIParams(projected);
1733
+ const responsesPayload = _convertMessagesToOpenAIResponsesParams(projected);
1734
+
1735
+ expect(projected).not.toBe(original);
1736
+ expect(typeof projectedTool.content).toBe('string');
1737
+ expect((projectedTool.content as string).length).toBeLessThanOrEqual(200);
1738
+ expect(projectedTool.tool_call_id).toBe(toolMessage.tool_call_id);
1739
+ expect(projectedTool.status).toBe(toolMessage.status);
1740
+ expect(projectedTool.artifact).toBe(toolMessage.artifact);
1741
+ expect(Array.isArray(toolMessage.content)).toBe(true);
1742
+ expect(toJSONCalls).toBe(0);
1743
+ expect(chatPayload[0]).toMatchObject({
1744
+ role: 'tool',
1745
+ content: projectedTool.content,
1746
+ tool_call_id: toolMessage.tool_call_id,
1747
+ });
1748
+ expect(responsesPayload[0]).toMatchObject({
1749
+ type: 'function_call_output',
1750
+ output: projectedTool.content,
1751
+ call_id: toolMessage.tool_call_id,
1752
+ });
1753
+ });
1754
+
1755
+ it('preserves Responses computer outputs handled as provider-native media', () => {
1756
+ const computerCall = new AIMessage({
1757
+ content: '',
1758
+ response_metadata: {
1759
+ output: [
1760
+ {
1761
+ type: 'computer_call',
1762
+ id: 'computer-item',
1763
+ call_id: 'call_computer',
1764
+ action: { type: 'screenshot' },
1765
+ },
1766
+ ],
1767
+ },
1768
+ });
1769
+ const computerOutput = new ToolMessage({
1770
+ content: [
1771
+ {
1772
+ type: 'computer_screenshot',
1773
+ image_url: 'data:image/png;base64,AA==',
1774
+ },
1775
+ ],
1776
+ tool_call_id: 'call_computer',
1777
+ additional_kwargs: { type: 'computer_call_output' },
1778
+ });
1779
+ const messages = [computerCall, computerOutput];
1780
+ const projected = projectOpenAIToolMessageContent(messages, 10);
1781
+ const responsesInput = convertMessagesToResponsesInput({
1782
+ messages: projected,
1783
+ zdrEnabled: false,
1784
+ model: 'computer-use-preview',
1785
+ });
1786
+
1787
+ expect(projected).not.toBe(messages);
1788
+ expect(
1789
+ responsesInput.find((item) => item.type === 'computer_call_output')
1790
+ ).toMatchObject({
1791
+ type: 'computer_call_output',
1792
+ call_id: 'call_computer',
1793
+ output: {
1794
+ type: 'computer_screenshot',
1795
+ image_url: 'data:image/png;base64,AA==',
1796
+ },
1797
+ });
1798
+ });
1799
+
1800
+ it('deduplicates parsed and raw Responses computer calls in ZDR mode', () => {
1801
+ const response = {
1802
+ id: 'resp_computer',
1803
+ object: 'response',
1804
+ created_at: 0,
1805
+ model: 'computer-use-preview',
1806
+ output: [
1807
+ {
1808
+ type: 'computer_call',
1809
+ id: 'computer-item',
1810
+ call_id: 'call_computer',
1811
+ action: { type: 'screenshot' },
1812
+ status: 'completed',
1813
+ },
1814
+ ],
1815
+ status: 'completed',
1816
+ usage: {
1817
+ input_tokens: 1,
1818
+ output_tokens: 1,
1819
+ total_tokens: 2,
1820
+ input_tokens_details: { cached_tokens: 0 },
1821
+ output_tokens_details: { reasoning_tokens: 0 },
1822
+ },
1823
+ error: null,
1824
+ incomplete_details: null,
1825
+ metadata: {},
1826
+ user: null,
1827
+ service_tier: 'default',
1828
+ } as unknown as Parameters<typeof convertResponsesMessageToAIMessage>[0];
1829
+ const computerCall = convertResponsesMessageToAIMessage(response);
1830
+ const computerOutput = new ToolMessage({
1831
+ content: 'data:image/png;base64,AA==',
1832
+ tool_call_id: 'call_computer',
1833
+ additional_kwargs: { type: 'computer_call_output' },
1834
+ });
1835
+
1836
+ const projected = projectOpenAIResponsesToolMessageContent([
1837
+ computerCall,
1838
+ computerOutput,
1839
+ ]);
1840
+ const zdrInput = convertMessagesToResponsesInput({
1841
+ messages: projected,
1842
+ zdrEnabled: true,
1843
+ model: 'computer-use-preview',
1844
+ });
1845
+ const retainedInput = convertMessagesToResponsesInput({
1846
+ messages: projected,
1847
+ zdrEnabled: false,
1848
+ model: 'computer-use-preview',
1849
+ });
1850
+
1851
+ expect(computerCall.tool_calls).toHaveLength(1);
1852
+ expect((projected[0] as AIMessage).tool_calls).toHaveLength(0);
1853
+ for (const input of [zdrInput, retainedInput]) {
1854
+ expect(
1855
+ input.filter(
1856
+ (item) =>
1857
+ item.type === 'computer_call' && item.call_id === 'call_computer'
1858
+ )
1859
+ ).toHaveLength(1);
1860
+ expect(
1861
+ input.filter(
1862
+ (item) =>
1863
+ item.type === 'computer_call_output' &&
1864
+ item.call_id === 'call_computer'
1865
+ )
1866
+ ).toHaveLength(1);
1867
+ }
1868
+ });
1869
+
1870
+ it('deduplicates streaming Responses computer calls before replay', () => {
1871
+ const event = {
1872
+ type: 'response.output_item.done',
1873
+ sequence_number: 1,
1874
+ output_index: 0,
1875
+ item: {
1876
+ type: 'computer_call',
1877
+ id: 'computer-stream-item',
1878
+ call_id: 'call_computer_stream',
1879
+ action: { type: 'screenshot' },
1880
+ status: 'completed',
1881
+ },
1882
+ } as unknown as Parameters<
1883
+ typeof convertResponsesDeltaToChatGenerationChunk
1884
+ >[0];
1885
+ const generation = convertResponsesDeltaToChatGenerationChunk(event);
1886
+ const computerCall = generation?.message;
1887
+ expect(computerCall).toBeDefined();
1888
+
1889
+ const computerOutput = new ToolMessage({
1890
+ content: 'data:image/png;base64,AA==',
1891
+ tool_call_id: 'call_computer_stream',
1892
+ additional_kwargs: { type: 'computer_call_output' },
1893
+ });
1894
+ const projected = projectOpenAIResponsesToolMessageContent([
1895
+ computerCall!,
1896
+ computerOutput,
1897
+ ]);
1898
+
1899
+ expect((computerCall as AIMessage).tool_calls).toHaveLength(1);
1900
+ expect((computerCall as AIMessageChunk).tool_call_chunks).toHaveLength(1);
1901
+ expect((projected[0] as AIMessage).tool_calls).toHaveLength(0);
1902
+ expect((projected[0] as AIMessageChunk).tool_call_chunks).toHaveLength(0);
1903
+ expect(Object.getPrototypeOf(projected[0])).toBe(
1904
+ Object.getPrototypeOf(computerCall)
1905
+ );
1906
+ expect(
1907
+ new AIMessageChunk(projected[0] as AIMessageChunk).tool_calls
1908
+ ).toHaveLength(0);
1909
+ for (const zdrEnabled of [true, false]) {
1910
+ const input = convertMessagesToResponsesInput({
1911
+ messages: projected,
1912
+ zdrEnabled,
1913
+ model: 'computer-use-preview',
1914
+ });
1915
+ expect(
1916
+ input.filter(
1917
+ (item) =>
1918
+ item.type === 'computer_call' &&
1919
+ item.call_id === 'call_computer_stream'
1920
+ )
1921
+ ).toHaveLength(1);
1922
+ expect(
1923
+ input.filter(
1924
+ (item) =>
1925
+ (item.type === 'function_call' || item.type === 'computer_call') &&
1926
+ item.call_id === 'call_computer_stream'
1927
+ )
1928
+ ).toHaveLength(1);
1929
+ }
1930
+
1931
+ const chatProjected = projectComputerCallOutputsToText(
1932
+ projectOpenAIToolMessageContent([computerCall!, computerOutput])
1933
+ );
1934
+ const chatInput = convertMessagesToCompletionsMessageParams({
1935
+ messages: chatProjected,
1936
+ model: 'gpt-4o',
1937
+ });
1938
+
1939
+ expect((chatProjected[0] as AIMessage).tool_calls).toHaveLength(1);
1940
+ expect(chatInput).toEqual([
1941
+ expect.objectContaining({
1942
+ role: 'assistant',
1943
+ tool_calls: [
1944
+ expect.objectContaining({
1945
+ id: 'call_computer_stream',
1946
+ type: 'function',
1947
+ }),
1948
+ ],
1949
+ }),
1950
+ expect.objectContaining({
1951
+ role: 'tool',
1952
+ tool_call_id: 'call_computer_stream',
1953
+ content: '[Computer screenshot omitted for this provider]',
1954
+ }),
1955
+ ]);
1956
+ });
1957
+
1958
+ it('canonicalizes real OpenAI file-backed computer screenshots on the production converter path', () => {
1959
+ const computerCall = new AIMessage({
1960
+ content: '',
1961
+ additional_kwargs: {
1962
+ tool_outputs: [
1963
+ {
1964
+ type: 'computer_call',
1965
+ call_id: 'call_computer_file',
1966
+ action: { type: 'screenshot' },
1967
+ },
1968
+ ],
1969
+ },
1970
+ });
1971
+ const computerOutput = new ToolMessage({
1972
+ content: [
1973
+ {
1974
+ type: 'input_image',
1975
+ file_id: 'file-abc123',
1976
+ detail: 'low',
1977
+ },
1978
+ ],
1979
+ tool_call_id: 'call_computer_file',
1980
+ additional_kwargs: { type: 'computer_call_output' },
1981
+ });
1982
+
1983
+ const projected = projectOpenAIToolMessageContent(
1984
+ [computerCall, computerOutput],
1985
+ 100
1986
+ );
1987
+ const responsesInput = convertMessagesToResponsesInput({
1988
+ messages: projected,
1989
+ zdrEnabled: false,
1990
+ model: 'computer-use-preview',
1991
+ });
1992
+
1993
+ expect(responsesInput).toContainEqual({
1994
+ type: 'computer_call_output',
1995
+ call_id: 'call_computer_file',
1996
+ output: {
1997
+ type: 'input_image',
1998
+ file_id: 'file-abc123',
1999
+ detail: 'low',
2000
+ },
2001
+ });
2002
+ });
2003
+
2004
+ it('rejects extra invalid screenshot fields instead of shipping the original block', () => {
2005
+ const computerCall = new AIMessage({
2006
+ content: '',
2007
+ response_metadata: {
2008
+ output: [
2009
+ {
2010
+ type: 'computer_call',
2011
+ call_id: 'call_computer_extra',
2012
+ action: { type: 'screenshot' },
2013
+ },
2014
+ ],
2015
+ },
2016
+ });
2017
+ const malformed = new ToolMessage({
2018
+ content: [
2019
+ {
2020
+ type: 'input_image',
2021
+ image_url: 'data:image/png;base64,AA==',
2022
+ file_id: 'not-a-file-id',
2023
+ },
2024
+ ],
2025
+ tool_call_id: 'call_computer_extra',
2026
+ additional_kwargs: { type: 'computer_call_output' },
2027
+ });
2028
+
2029
+ expect(() =>
2030
+ projectOpenAIToolMessageContent([computerCall, malformed], 100)
2031
+ ).toThrow('Invalid computer call output screenshot');
2032
+ });
2033
+
2034
+ it('rejects computer outputs that precede their call', () => {
2035
+ const computerCall = new AIMessage({
2036
+ content: '',
2037
+ response_metadata: {
2038
+ output: [
2039
+ {
2040
+ type: 'computer_call',
2041
+ call_id: 'call_computer_order',
2042
+ action: { type: 'screenshot' },
2043
+ },
2044
+ ],
2045
+ },
2046
+ });
2047
+ const computerOutput = new ToolMessage({
2048
+ content: 'data:image/png;base64,AA==',
2049
+ tool_call_id: 'call_computer_order',
2050
+ additional_kwargs: { type: 'computer_call_output' },
2051
+ });
2052
+
2053
+ expect(() =>
2054
+ projectOpenAIToolMessageContent([computerOutput, computerCall], 100)
2055
+ ).toThrow('Invalid computer call output pairing');
2056
+ });
2057
+
2058
+ it('rejects a malformed marked computer output before provider conversion', () => {
2059
+ const malformed = new ToolMessage({
2060
+ content: [{ type: 'text', text: 'A'.repeat(2_000) }],
2061
+ tool_call_id: 'call_computer_malformed',
2062
+ additional_kwargs: { type: 'computer_call_output' },
2063
+ });
2064
+
2065
+ expect(() => projectOpenAIToolMessageContent([malformed], 100)).toThrow(
2066
+ 'Invalid computer call output screenshot'
2067
+ );
2068
+ });
2069
+
2070
+ it('rejects a marked screenshot paired with a normal function call', () => {
2071
+ const functionCall = new AIMessage({
2072
+ content: '',
2073
+ tool_calls: [
2074
+ {
2075
+ id: 'call_function',
2076
+ name: 'render',
2077
+ args: {},
2078
+ type: 'tool_call',
2079
+ },
2080
+ ],
2081
+ });
2082
+ const malformedPair = new ToolMessage({
2083
+ content: 'data:image/png;base64,AA==',
2084
+ tool_call_id: 'call_function',
2085
+ additional_kwargs: { type: 'computer_call_output' },
2086
+ });
2087
+
2088
+ expect(() =>
2089
+ projectOpenAIToolMessageContent([functionCall, malformedPair], 100)
2090
+ ).toThrow('Invalid computer call output pairing');
2091
+ });
2092
+
2093
+ it('keeps the mutating artifact formatter API backward compatible', () => {
2094
+ const anthropicToolMessage = new ToolMessage({
2095
+ content: 'result',
2096
+ tool_call_id: 'anthropic_legacy',
2097
+ artifact: {
2098
+ content: [{ type: ContentTypes.TEXT, text: 'anthropic artifact' }],
2099
+ },
2100
+ });
2101
+ const anthropicMessages = [
2102
+ new AIMessageChunk({
2103
+ content: '',
2104
+ tool_calls: [
2105
+ {
2106
+ id: 'anthropic_legacy',
2107
+ name: 'render',
2108
+ args: {},
2109
+ type: 'tool_call' as const,
2110
+ },
2111
+ ],
2112
+ }),
2113
+ anthropicToolMessage,
2114
+ ];
2115
+
2116
+ expect(formatAnthropicArtifactContent(anthropicMessages)).toBeUndefined();
2117
+ expect(anthropicToolMessage.content).toEqual([
2118
+ { type: ContentTypes.TEXT, text: 'result' },
2119
+ { type: ContentTypes.TEXT, text: 'anthropic artifact' },
2120
+ ]);
2121
+ expect(anthropicToolMessage.artifact.content).toHaveLength(1);
2122
+
2123
+ const payloadToolMessage = new ToolMessage({
2124
+ content: 'result',
2125
+ tool_call_id: 'payload_legacy',
2126
+ artifact: {
2127
+ content: [{ type: ContentTypes.TEXT, text: 'payload artifact' }],
2128
+ },
2129
+ });
2130
+ const payloadMessages = [
2131
+ new AIMessageChunk({
2132
+ content: '',
2133
+ tool_calls: [
2134
+ {
2135
+ id: 'payload_legacy',
2136
+ name: 'render',
2137
+ args: {},
2138
+ type: 'tool_call' as const,
2139
+ },
2140
+ ],
2141
+ }),
2142
+ payloadToolMessage,
2143
+ ];
2144
+
2145
+ expect(formatArtifactPayload(payloadMessages)).toBeUndefined();
2146
+ expect(payloadToolMessage.content).toContain(
2147
+ 'Tool response is included in the next message'
2148
+ );
2149
+ expect(payloadMessages[payloadMessages.length - 1]).toBeInstanceOf(
2150
+ HumanMessage
2151
+ );
2152
+ expect(payloadToolMessage.artifact.content).toHaveLength(1);
1573
2153
  });
1574
2154
 
1575
2155
  it('should dynamically discover tools from tool_search output and keep their tool calls', () => {
@@ -2216,9 +2796,15 @@ describe('formatAgentMessages', () => {
2216
2796
  },
2217
2797
  ];
2218
2798
 
2219
- const result = formatAgentMessages(payload, undefined, undefined, undefined, {
2220
- preserveReasoningContent: true,
2221
- });
2799
+ const result = formatAgentMessages(
2800
+ payload,
2801
+ undefined,
2802
+ undefined,
2803
+ undefined,
2804
+ {
2805
+ preserveReasoningContent: true,
2806
+ }
2807
+ );
2222
2808
 
2223
2809
  const toolCallMessage = result.messages[0] as AIMessage;
2224
2810
  expect(toolCallMessage.tool_calls).toHaveLength(1);
@@ -2254,15 +2840,19 @@ describe('formatAgentMessages', () => {
2254
2840
  },
2255
2841
  ];
2256
2842
 
2257
- const result = formatAgentMessages(payload, undefined, undefined, undefined, {
2258
- provider: Providers.DEEPSEEK,
2259
- preserveReasoningContent: false,
2260
- });
2843
+ const result = formatAgentMessages(
2844
+ payload,
2845
+ undefined,
2846
+ undefined,
2847
+ undefined,
2848
+ {
2849
+ provider: Providers.DEEPSEEK,
2850
+ preserveReasoningContent: false,
2851
+ }
2852
+ );
2261
2853
 
2262
2854
  const toolCallMessage = result.messages[0] as AIMessage;
2263
- expect(
2264
- toolCallMessage.additional_kwargs.reasoning_content
2265
- ).toBeUndefined();
2855
+ expect(toolCallMessage.additional_kwargs.reasoning_content).toBeUndefined();
2266
2856
  });
2267
2857
 
2268
2858
  it('should preserve DeepSeek reasoning from supported hidden content blocks', () => {