@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
@@ -27,8 +27,15 @@ import type {
27
27
  TPayload,
28
28
  TMessage,
29
29
  } from '@/types';
30
+ import {
31
+ compactToolContent,
32
+ getToolContentCharLength,
33
+ isAtomicToolContentBlock,
34
+ serializeStructuredValueBounded,
35
+ } from '@/utils/toolContent';
30
36
  import { normalizeAnthropicToolCallId } from '@/llm/anthropic/utils/message_inputs';
31
37
  import { toLangChainContent, toLangChainMessageFields } from './langchain';
38
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
32
39
  import { Providers, ContentTypes, Constants } from '@/common';
33
40
  import { emitAgentLog } from '@/utils/events';
34
41
 
@@ -459,6 +466,15 @@ function hasToolCallOutput(part: MessageContentComplex): boolean {
459
466
  return output != null && output !== '';
460
467
  }
461
468
 
469
+ function formatToolCallOutput(
470
+ output: ToolCallPart['output'] | undefined
471
+ ): MessageContent {
472
+ if (output == null) {
473
+ return '';
474
+ }
475
+ return compactToolContent(output, HARD_MAX_TOOL_RESULT_CHARS).content;
476
+ }
477
+
462
478
  /**
463
479
  * Helper function to format an assistant message
464
480
  * @param message The message to format
@@ -718,7 +734,7 @@ function formatAssistantMessage(
718
734
  new ToolMessage({
719
735
  tool_call_id: tool_call.id ?? '',
720
736
  name: tool_call.name,
721
- content: output != null ? output : '',
737
+ content: formatToolCallOutput(output),
722
738
  }),
723
739
  'tool'
724
740
  )
@@ -1250,7 +1266,11 @@ function contentPartCharLength(part: MessageContentComplex): number {
1250
1266
  if (typeof input === 'string') {
1251
1267
  len += input.length;
1252
1268
  } else if (input != null && typeof input === 'object') {
1253
- len += JSON.stringify(input).length;
1269
+ const measured = serializeStructuredValueBounded(input, 0).originalChars;
1270
+ len +=
1271
+ measured === Number.MAX_SAFE_INTEGER
1272
+ ? HARD_MAX_TOOL_RESULT_CHARS
1273
+ : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
1254
1274
  }
1255
1275
  return len;
1256
1276
  }
@@ -1689,7 +1709,14 @@ export const formatAgentMessages = (
1689
1709
  if (typeof args === 'string') {
1690
1710
  len += args.length;
1691
1711
  } else if (args != null) {
1692
- len += JSON.stringify(args).length;
1712
+ const measured = serializeStructuredValueBounded(
1713
+ args,
1714
+ 0
1715
+ ).originalChars;
1716
+ len +=
1717
+ measured === Number.MAX_SAFE_INTEGER
1718
+ ? HARD_MAX_TOOL_RESULT_CHARS
1719
+ : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
1693
1720
  }
1694
1721
  }
1695
1722
  }
@@ -1754,13 +1781,156 @@ export function shiftIndexTokenCountMap(
1754
1781
  return shiftedMap;
1755
1782
  }
1756
1783
 
1757
- /** Block types that contain binary image data and must be preserved structurally. */
1758
- const IMAGE_BLOCK_TYPES = new Set(['image_url', 'image']);
1759
-
1760
1784
  /** Checks whether a BaseMessage is a tool-role message. */
1761
1785
  const isToolMessage = (m: BaseMessage): boolean =>
1762
1786
  m instanceof ToolMessage || ('role' in m && (m as any).role === 'tool');
1763
1787
 
1788
+ const PORTABLE_FOLDED_MEDIA_TYPES = new Set(['image', 'image_url']);
1789
+ const MAX_FOLDED_BLOCK_CHARS = 8_000;
1790
+ const MAX_FOLDED_CONTEXT_CHARS = HARD_MAX_TOOL_RESULT_CHARS;
1791
+ const MAX_FOLDED_CONTEXT_WORK = 100_000;
1792
+ const FOLDED_CONTEXT_TRUNCATION_NOTICE =
1793
+ '… [additional folded context omitted]';
1794
+ const syntheticProviderContextMessages = new WeakSet<BaseMessage>();
1795
+
1796
+ type FoldContextBudget = {
1797
+ remainingChars: number;
1798
+ remainingWork: number;
1799
+ truncated: boolean;
1800
+ };
1801
+
1802
+ function createFoldContextBudget(): FoldContextBudget {
1803
+ return {
1804
+ remainingChars: MAX_FOLDED_CONTEXT_CHARS,
1805
+ remainingWork: MAX_FOLDED_CONTEXT_WORK,
1806
+ truncated: false,
1807
+ };
1808
+ }
1809
+
1810
+ function readFoldedDataProperty(value: unknown, key: string): unknown {
1811
+ if (value == null || typeof value !== 'object') {
1812
+ return undefined;
1813
+ }
1814
+ try {
1815
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1816
+ return descriptor != null && 'value' in descriptor
1817
+ ? descriptor.value
1818
+ : undefined;
1819
+ } catch {
1820
+ return undefined;
1821
+ }
1822
+ }
1823
+
1824
+ /**
1825
+ * Adds one logical line without first concatenating caller-controlled strings.
1826
+ * The shared budget covers every synthetic message emitted by one transform,
1827
+ * so many individually bounded blocks cannot build an unbounded aggregate.
1828
+ */
1829
+ function appendFoldedLine(
1830
+ textChunks: string[],
1831
+ budget: FoldContextBudget,
1832
+ pieces: readonly string[]
1833
+ ): boolean {
1834
+ if (budget.remainingChars <= 0) {
1835
+ budget.truncated = true;
1836
+ return false;
1837
+ }
1838
+
1839
+ const separatorChars = textChunks.length > 0 ? 1 : 0;
1840
+ const available = budget.remainingChars - separatorChars;
1841
+ if (available <= 0) {
1842
+ budget.remainingChars = 0;
1843
+ budget.truncated = true;
1844
+ return false;
1845
+ }
1846
+
1847
+ let totalChars = 0;
1848
+ for (const piece of pieces) {
1849
+ totalChars = Math.min(Number.MAX_SAFE_INTEGER, totalChars + piece.length);
1850
+ }
1851
+ if (totalChars <= available) {
1852
+ const line = pieces.join('');
1853
+ textChunks.push(line);
1854
+ budget.remainingChars -= separatorChars + line.length;
1855
+ return true;
1856
+ }
1857
+
1858
+ const notice = FOLDED_CONTEXT_TRUNCATION_NOTICE.slice(0, available);
1859
+ let remainingHeadChars = Math.max(0, available - notice.length);
1860
+ const boundedPieces: string[] = [];
1861
+ for (const piece of pieces) {
1862
+ if (remainingHeadChars <= 0) {
1863
+ break;
1864
+ }
1865
+ const retained = piece.slice(0, remainingHeadChars);
1866
+ boundedPieces.push(retained);
1867
+ remainingHeadChars -= retained.length;
1868
+ }
1869
+ boundedPieces.push(notice);
1870
+ textChunks.push(boundedPieces.join(''));
1871
+ budget.remainingChars = 0;
1872
+ budget.truncated = true;
1873
+ return false;
1874
+ }
1875
+
1876
+ function consumeFoldedWork(
1877
+ textChunks: string[],
1878
+ budget: FoldContextBudget
1879
+ ): boolean {
1880
+ if (budget.remainingChars <= 0) {
1881
+ return false;
1882
+ }
1883
+ if (budget.remainingWork-- > 0) {
1884
+ return true;
1885
+ }
1886
+ appendFoldedLine(textChunks, budget, [FOLDED_CONTEXT_TRUNCATION_NOTICE]);
1887
+ budget.remainingChars = 0;
1888
+ budget.truncated = true;
1889
+ return false;
1890
+ }
1891
+
1892
+ function serializeFoldedValue(value: unknown): string {
1893
+ const compacted = compactToolContent(value, MAX_FOLDED_BLOCK_CHARS).content;
1894
+ return typeof compacted === 'string'
1895
+ ? compacted
1896
+ : serializeStructuredValueBounded(compacted, MAX_FOLDED_BLOCK_CHARS)
1897
+ .content;
1898
+ }
1899
+
1900
+ function markSyntheticProviderContext<T extends BaseMessage>(message: T): T {
1901
+ syntheticProviderContextMessages.add(message);
1902
+ return message;
1903
+ }
1904
+
1905
+ function appendSyntheticProviderContextMessage(
1906
+ result: BaseMessage[],
1907
+ parts: MessageContentComplex[]
1908
+ ): boolean {
1909
+ if (parts.length === 0) {
1910
+ return false;
1911
+ }
1912
+ result.push(
1913
+ markSyntheticProviderContext(
1914
+ withMessageRole(
1915
+ new HumanMessage({ content: toLangChainContent(parts) }),
1916
+ 'user'
1917
+ )
1918
+ )
1919
+ );
1920
+ return true;
1921
+ }
1922
+
1923
+ /**
1924
+ * Identifies provider-context placeholders created by this module without
1925
+ * trusting user-controlled content prefixes or leaking marker metadata onto
1926
+ * the provider wire.
1927
+ */
1928
+ export function isSyntheticProviderContextMessage(
1929
+ message: BaseMessage
1930
+ ): boolean {
1931
+ return syntheticProviderContextMessages.has(message);
1932
+ }
1933
+
1764
1934
  /** Flushes accumulated text chunks into `parts` as a single text block. */
1765
1935
  function flushTextChunks(
1766
1936
  textChunks: string[],
@@ -1778,10 +1948,10 @@ function flushTextChunks(
1778
1948
 
1779
1949
  /**
1780
1950
  * Appends a single message's content to the running `textChunks` / `parts`
1781
- * accumulators. Image blocks are shallow-copied into `parts` as-is so that
1782
- * binary data (base64 images) never becomes text tokens. All other block
1783
- * types are serialized to text unrecognized types are JSON-serialized
1784
- * rather than silently dropped.
1951
+ * accumulators. Portable image blocks are shallow-copied into `parts` so
1952
+ * binary data never becomes text tokens. Provider-specific media/resource
1953
+ * blocks are retained as bounded text so a folded cross-provider history
1954
+ * cannot contain an unsupported native block or JSON-expand without limit.
1785
1955
  *
1786
1956
  * When `content` is an array containing tool_use blocks, `tool_calls` is NOT
1787
1957
  * additionally serialized (avoiding double output). `tool_calls` is used as
@@ -1791,37 +1961,69 @@ function appendMessageContent(
1791
1961
  msg: BaseMessage,
1792
1962
  role: string,
1793
1963
  textChunks: string[],
1794
- parts: MessageContentComplex[]
1964
+ parts: MessageContentComplex[],
1965
+ budget: FoldContextBudget
1795
1966
  ): void {
1796
1967
  const { content } = msg;
1797
1968
 
1798
1969
  if (typeof content === 'string') {
1799
- if (content) {
1800
- textChunks.push(`${role}: ${content}`);
1970
+ if (content && consumeFoldedWork(textChunks, budget)) {
1971
+ appendFoldedLine(textChunks, budget, [`${role}: `, content]);
1801
1972
  }
1802
- appendToolCalls(msg, role, textChunks);
1973
+ appendToolCalls(msg, role, textChunks, budget);
1803
1974
  return;
1804
1975
  }
1805
1976
 
1806
1977
  if (!Array.isArray(content)) {
1807
- appendToolCalls(msg, role, textChunks);
1978
+ appendToolCalls(msg, role, textChunks, budget);
1808
1979
  return;
1809
1980
  }
1810
1981
 
1811
1982
  let hasToolUseBlock = false;
1812
1983
 
1813
1984
  for (const block of content as ExtendedMessageContent[]) {
1814
- if (IMAGE_BLOCK_TYPES.has(block.type ?? '')) {
1815
- flushTextChunks(textChunks, parts);
1816
- parts.push({ ...block } as MessageContentComplex);
1985
+ if (!consumeFoldedWork(textChunks, budget)) {
1986
+ hasToolUseBlock = true;
1987
+ break;
1988
+ }
1989
+ const blockTypeValue = readFoldedDataProperty(block, 'type');
1990
+ const blockType =
1991
+ typeof blockTypeValue === 'string' ? blockTypeValue : undefined;
1992
+
1993
+ if (
1994
+ blockType !== 'tool_use' &&
1995
+ blockType !== 'tool_call' &&
1996
+ blockType !== 'tool_result' &&
1997
+ isAtomicToolContentBlock(block)
1998
+ ) {
1999
+ if (PORTABLE_FOLDED_MEDIA_TYPES.has(blockType ?? '')) {
2000
+ const blockChars = getToolContentCharLength([block]);
2001
+ if (blockChars > budget.remainingChars) {
2002
+ appendFoldedLine(textChunks, budget, [
2003
+ `${role}: [${blockType ?? 'media'} omitted: folded context limit]`,
2004
+ ]);
2005
+ continue;
2006
+ }
2007
+ flushTextChunks(textChunks, parts);
2008
+ parts.push({ ...block } as MessageContentComplex);
2009
+ budget.remainingChars -= blockChars;
2010
+ } else {
2011
+ appendFoldedLine(textChunks, budget, [
2012
+ `${role}: [${blockType ?? 'media'}] `,
2013
+ serializeFoldedValue(block),
2014
+ ]);
2015
+ }
1817
2016
  continue;
1818
2017
  }
1819
2018
 
1820
- if (block.type === 'tool_use') {
2019
+ if (blockType === 'tool_use') {
1821
2020
  hasToolUseBlock = true;
1822
- textChunks.push(
1823
- `${role}: [tool_use] ${String(block.name ?? '')} ${JSON.stringify(block.input ?? {})}`
1824
- );
2021
+ const name = readFoldedDataProperty(block, 'name');
2022
+ const input = readFoldedDataProperty(block, 'input');
2023
+ appendFoldedLine(textChunks, budget, [
2024
+ `${role}: [tool_use] ${typeof name === 'string' ? name : ''} `,
2025
+ serializeFoldedValue(input ?? {}),
2026
+ ]);
1825
2027
  continue;
1826
2028
  }
1827
2029
 
@@ -1830,17 +2032,32 @@ function appendMessageContent(
1830
2032
  // toolUse) or this repo's `ToolCallContent` (`{ tool_call: { name, args,
1831
2033
  // output } }`, from `convertMessagesToContent` / persisted history). Handle
1832
2034
  // both, and emit any embedded output, so the name/args/result survive.
1833
- if (block.type === 'tool_call') {
2035
+ if (blockType === 'tool_call') {
1834
2036
  hasToolUseBlock = true;
1835
- const nested = (block as { tool_call?: ToolCallPart }).tool_call;
1836
- const name = String(nested?.name ?? block.name ?? '');
1837
- const rawArgs = nested?.args ?? block.args ?? {};
2037
+ const nested = readFoldedDataProperty(block, 'tool_call');
2038
+ const nestedName = readFoldedDataProperty(nested, 'name');
2039
+ const topLevelName = readFoldedDataProperty(block, 'name');
2040
+ let name = '';
2041
+ if (typeof nestedName === 'string') {
2042
+ name = nestedName;
2043
+ } else if (typeof topLevelName === 'string') {
2044
+ name = topLevelName;
2045
+ }
2046
+ const nestedArgs = readFoldedDataProperty(nested, 'args');
2047
+ const topLevelArgs = readFoldedDataProperty(block, 'args');
2048
+ const rawArgs = nestedArgs ?? topLevelArgs ?? {};
1838
2049
  const argsText =
1839
- typeof rawArgs === 'string' ? rawArgs : JSON.stringify(rawArgs);
1840
- textChunks.push(`${role}: [tool_use] ${name} ${argsText}`.trimEnd());
1841
- const output = nested?.output;
2050
+ typeof rawArgs === 'string' ? rawArgs : serializeFoldedValue(rawArgs);
2051
+ appendFoldedLine(textChunks, budget, [
2052
+ `${role}: [tool_use] ${name}${argsText ? ' ' : ''}`,
2053
+ argsText,
2054
+ ]);
2055
+ const output = readFoldedDataProperty(nested, 'output');
1842
2056
  if (output != null && output !== '') {
1843
- textChunks.push(`Tool: ${String(output)}`);
2057
+ appendFoldedLine(textChunks, budget, [
2058
+ 'Tool: ',
2059
+ typeof output === 'string' ? output : serializeFoldedValue(output),
2060
+ ]);
1844
2061
  }
1845
2062
  continue;
1846
2063
  }
@@ -1848,65 +2065,101 @@ function appendMessageContent(
1848
2065
  // A `tool_result` content block (e.g. an AIMessage(tool_call) followed by a
1849
2066
  // user message carrying the result). Preserve nested image blocks as-is
1850
2067
  // instead of JSON-stringifying them through the generic fallback.
1851
- if (block.type === 'tool_result') {
2068
+ if (blockType === 'tool_result') {
1852
2069
  hasToolUseBlock = true;
1853
- const inner = (block as { content?: ToolResultContent['content'] })
1854
- .content;
2070
+ const inner = readFoldedDataProperty(block, 'content') as
2071
+ | ToolResultContent['content']
2072
+ | undefined;
1855
2073
  if (typeof inner === 'string') {
1856
2074
  if (inner) {
1857
- textChunks.push(`${role}: [tool_result] ${inner}`);
2075
+ appendFoldedLine(textChunks, budget, [
2076
+ `${role}: [tool_result] `,
2077
+ inner,
2078
+ ]);
1858
2079
  }
1859
2080
  } else if (Array.isArray(inner)) {
1860
2081
  for (const innerBlock of inner as Array<
1861
2082
  string | ExtendedMessageContent
1862
2083
  >) {
2084
+ if (!consumeFoldedWork(textChunks, budget)) {
2085
+ break;
2086
+ }
1863
2087
  if (typeof innerBlock === 'string') {
1864
2088
  if (innerBlock) {
1865
- textChunks.push(`${role}: [tool_result] ${innerBlock}`);
2089
+ appendFoldedLine(textChunks, budget, [
2090
+ `${role}: [tool_result] `,
2091
+ innerBlock,
2092
+ ]);
1866
2093
  }
1867
- } else if (IMAGE_BLOCK_TYPES.has(innerBlock.type ?? '')) {
1868
- flushTextChunks(textChunks, parts);
1869
- parts.push({ ...innerBlock } as MessageContentComplex);
1870
2094
  } else {
1871
- const innerText = innerBlock.text ?? innerBlock.input;
1872
- textChunks.push(
1873
- `${role}: [tool_result] ${
2095
+ const innerTypeValue = readFoldedDataProperty(innerBlock, 'type');
2096
+ const innerType =
2097
+ typeof innerTypeValue === 'string' ? innerTypeValue : undefined;
2098
+ if (
2099
+ isAtomicToolContentBlock(innerBlock) &&
2100
+ PORTABLE_FOLDED_MEDIA_TYPES.has(innerType ?? '')
2101
+ ) {
2102
+ const blockChars = getToolContentCharLength([innerBlock]);
2103
+ if (blockChars <= budget.remainingChars) {
2104
+ flushTextChunks(textChunks, parts);
2105
+ parts.push({ ...innerBlock } as MessageContentComplex);
2106
+ budget.remainingChars -= blockChars;
2107
+ } else {
2108
+ appendFoldedLine(textChunks, budget, [
2109
+ `${role}: [${innerType ?? 'media'} omitted: folded context limit]`,
2110
+ ]);
2111
+ }
2112
+ } else {
2113
+ const textValue = readFoldedDataProperty(innerBlock, 'text');
2114
+ const inputValue = readFoldedDataProperty(innerBlock, 'input');
2115
+ const innerText = textValue ?? inputValue;
2116
+ appendFoldedLine(textChunks, budget, [
2117
+ `${role}: [tool_result] `,
1874
2118
  typeof innerText === 'string' && innerText
1875
2119
  ? innerText
1876
- : JSON.stringify(innerBlock)
1877
- }`
1878
- );
2120
+ : serializeFoldedValue(innerBlock),
2121
+ ]);
2122
+ }
1879
2123
  }
1880
2124
  }
1881
2125
  } else if (inner != null) {
1882
- textChunks.push(`${role}: [tool_result] ${JSON.stringify(inner)}`);
2126
+ appendFoldedLine(textChunks, budget, [
2127
+ `${role}: [tool_result] `,
2128
+ serializeFoldedValue(inner),
2129
+ ]);
1883
2130
  }
1884
2131
  continue;
1885
2132
  }
1886
2133
 
1887
- const text = block.text ?? block.input;
2134
+ const text =
2135
+ readFoldedDataProperty(block, 'text') ??
2136
+ readFoldedDataProperty(block, 'input');
1888
2137
  if (typeof text === 'string' && text) {
1889
- textChunks.push(`${role}: ${text}`);
2138
+ appendFoldedLine(textChunks, budget, [`${role}: `, text]);
1890
2139
  continue;
1891
2140
  }
1892
2141
 
1893
2142
  // Fallback: serialize unrecognized block types to preserve context
1894
- if (block.type != null && block.type !== '') {
1895
- textChunks.push(`${role}: [${block.type}] ${JSON.stringify(block)}`);
2143
+ if (blockType != null && blockType !== '') {
2144
+ appendFoldedLine(textChunks, budget, [
2145
+ `${role}: [${blockType}] `,
2146
+ serializeFoldedValue(block),
2147
+ ]);
1896
2148
  }
1897
2149
  }
1898
2150
 
1899
2151
  // If content array had no tool_use blocks, fall back to tool_calls metadata
1900
2152
  // (handles edge case: empty content array with tool_calls populated)
1901
2153
  if (!hasToolUseBlock) {
1902
- appendToolCalls(msg, role, textChunks);
2154
+ appendToolCalls(msg, role, textChunks, budget);
1903
2155
  }
1904
2156
  }
1905
2157
 
1906
2158
  function appendToolCalls(
1907
2159
  msg: BaseMessage,
1908
2160
  role: string,
1909
- textChunks: string[]
2161
+ textChunks: string[],
2162
+ budget: FoldContextBudget
1910
2163
  ): void {
1911
2164
  if (role !== 'AI') {
1912
2165
  return;
@@ -1914,7 +2167,16 @@ function appendToolCalls(
1914
2167
  const aiMsg = msg as AIMessage;
1915
2168
  if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
1916
2169
  for (const tc of aiMsg.tool_calls) {
1917
- textChunks.push(`AI: [tool_call] ${tc.name}(${JSON.stringify(tc.args)})`);
2170
+ if (!consumeFoldedWork(textChunks, budget)) {
2171
+ break;
2172
+ }
2173
+ const name = readFoldedDataProperty(tc, 'name');
2174
+ const args = readFoldedDataProperty(tc, 'args');
2175
+ appendFoldedLine(textChunks, budget, [
2176
+ `AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
2177
+ serializeFoldedValue(args),
2178
+ ')',
2179
+ ]);
1918
2180
  }
1919
2181
  return;
1920
2182
  }
@@ -1924,14 +2186,20 @@ function appendToolCalls(
1924
2186
  return;
1925
2187
  }
1926
2188
  for (const tc of rawToolCalls) {
1927
- const fn = (tc as { function?: { name?: string; arguments?: string } })
1928
- .function;
2189
+ if (!consumeFoldedWork(textChunks, budget)) {
2190
+ break;
2191
+ }
2192
+ const fn = readFoldedDataProperty(tc, 'function');
1929
2193
  if (fn == null) {
1930
2194
  continue;
1931
2195
  }
1932
- textChunks.push(
1933
- `AI: [tool_call] ${String(fn.name ?? '')}(${String(fn.arguments ?? '')})`
1934
- );
2196
+ const name = readFoldedDataProperty(fn, 'name');
2197
+ const args = readFoldedDataProperty(fn, 'arguments');
2198
+ appendFoldedLine(textChunks, budget, [
2199
+ `AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
2200
+ typeof args === 'string' ? args : '',
2201
+ ')',
2202
+ ]);
1935
2203
  }
1936
2204
  }
1937
2205
 
@@ -1991,6 +2259,7 @@ export function ensureThinkingBlockInMessages(
1991
2259
 
1992
2260
  const result: BaseMessage[] =
1993
2261
  lastHumanIndex >= 0 ? messages.slice(0, lastHumanIndex + 1) : [];
2262
+ const foldBudget = createFoldContextBudget();
1994
2263
  let i = lastHumanIndex + 1;
1995
2264
 
1996
2265
  while (i < messages.length) {
@@ -2021,13 +2290,14 @@ export function ensureThinkingBlockInMessages(
2021
2290
  if (typeof c !== 'object') {
2022
2291
  continue;
2023
2292
  }
2024
- if (c.type === 'tool_use') {
2293
+ const type = readFoldedDataProperty(c, 'type');
2294
+ if (type === 'tool_use') {
2025
2295
  hasToolUse = true;
2026
2296
  } else if (
2027
- c.type === ContentTypes.THINKING ||
2028
- c.type === ContentTypes.REASONING_CONTENT ||
2029
- c.type === ContentTypes.REASONING ||
2030
- c.type === 'redacted_thinking'
2297
+ type === ContentTypes.THINKING ||
2298
+ type === ContentTypes.REASONING_CONTENT ||
2299
+ type === ContentTypes.REASONING ||
2300
+ type === 'redacted_thinking'
2031
2301
  ) {
2032
2302
  hasThinkingBlock = true;
2033
2303
  }
@@ -2082,30 +2352,33 @@ export function ensureThinkingBlockInMessages(
2082
2352
  // ToolMessages — preserves image blocks as-is to avoid serializing
2083
2353
  // binary data as text (which caused 174× token amplification).
2084
2354
  const parts: MessageContentComplex[] = [];
2085
- const textChunks: string[] = ['[Previous agent context]'];
2355
+ const textChunks: string[] = [];
2356
+ appendFoldedLine(textChunks, foldBudget, ['[Previous agent context]']);
2086
2357
 
2087
- appendMessageContent(msg, 'AI', textChunks, parts);
2358
+ appendMessageContent(msg, 'AI', textChunks, parts, foldBudget);
2088
2359
 
2089
2360
  let j = i + 1;
2090
2361
  while (j < messages.length && isToolMessage(messages[j])) {
2091
- appendMessageContent(messages[j], 'Tool', textChunks, parts);
2362
+ appendMessageContent(
2363
+ messages[j],
2364
+ 'Tool',
2365
+ textChunks,
2366
+ parts,
2367
+ foldBudget
2368
+ );
2092
2369
  j++;
2093
2370
  }
2094
2371
 
2095
2372
  flushTextChunks(textChunks, parts);
2096
- emitAgentLog(
2097
- config,
2098
- 'warn',
2099
- 'format',
2100
- 'ensureThinkingBlockInMessages: injecting [Previous agent context] HumanMessage' +
2101
- ` (${parts.length} msgs at index ${i}, no thinking block in chain)`
2102
- );
2103
- result.push(
2104
- withMessageRole(
2105
- new HumanMessage({ content: toLangChainContent(parts) }),
2106
- 'user'
2107
- )
2108
- );
2373
+ if (appendSyntheticProviderContextMessage(result, parts)) {
2374
+ emitAgentLog(
2375
+ config,
2376
+ 'warn',
2377
+ 'format',
2378
+ 'ensureThinkingBlockInMessages: injecting [Previous agent context] HumanMessage' +
2379
+ ` (${parts.length} msgs at index ${i}, no thinking block in chain)`
2380
+ );
2381
+ }
2109
2382
  i = j;
2110
2383
  } else {
2111
2384
  // Keep the message as is
@@ -2140,11 +2413,10 @@ function messageHasToolContent(msg: BaseMessage): boolean {
2140
2413
  }
2141
2414
  if (Array.isArray(msg.content)) {
2142
2415
  for (const block of msg.content as ExtendedMessageContent[]) {
2416
+ const type = readFoldedDataProperty(block, 'type');
2143
2417
  if (
2144
2418
  typeof block === 'object' &&
2145
- (block.type === 'tool_use' ||
2146
- block.type === 'tool_call' ||
2147
- block.type === 'tool_result')
2419
+ (type === 'tool_use' || type === 'tool_call' || type === 'tool_result')
2148
2420
  ) {
2149
2421
  return true;
2150
2422
  }
@@ -2164,7 +2436,9 @@ function isToolResultMessage(msg: BaseMessage): boolean {
2164
2436
  }
2165
2437
  if (Array.isArray(msg.content)) {
2166
2438
  return (msg.content as ExtendedMessageContent[]).some(
2167
- (block) => typeof block === 'object' && block.type === 'tool_result'
2439
+ (block) =>
2440
+ typeof block === 'object' &&
2441
+ readFoldedDataProperty(block, 'type') === 'tool_result'
2168
2442
  );
2169
2443
  }
2170
2444
  return false;
@@ -2197,6 +2471,7 @@ export function foldToolBlocksForToollessAgent(
2197
2471
  ): BaseMessage[] {
2198
2472
  let result: BaseMessage[] | null = null;
2199
2473
  let foldedCount = 0;
2474
+ const foldBudget = createFoldContextBudget();
2200
2475
  let i = 0;
2201
2476
  while (i < messages.length) {
2202
2477
  const msg = messages[i];
@@ -2212,29 +2487,26 @@ export function foldToolBlocksForToollessAgent(
2212
2487
  }
2213
2488
 
2214
2489
  const parts: MessageContentComplex[] = [];
2215
- const textChunks: string[] = ['[Previous tool interaction]'];
2490
+ const textChunks: string[] = [];
2491
+ appendFoldedLine(textChunks, foldBudget, ['[Previous tool interaction]']);
2216
2492
  appendMessageContent(
2217
2493
  msg,
2218
2494
  isToolResultMessage(msg) ? 'Tool' : 'AI',
2219
2495
  textChunks,
2220
- parts
2496
+ parts,
2497
+ foldBudget
2221
2498
  );
2222
2499
  foldedCount++;
2223
2500
 
2224
2501
  let j = i + 1;
2225
2502
  while (j < messages.length && isToolResultMessage(messages[j])) {
2226
- appendMessageContent(messages[j], 'Tool', textChunks, parts);
2503
+ appendMessageContent(messages[j], 'Tool', textChunks, parts, foldBudget);
2227
2504
  foldedCount++;
2228
2505
  j++;
2229
2506
  }
2230
2507
 
2231
2508
  flushTextChunks(textChunks, parts);
2232
- result.push(
2233
- withMessageRole(
2234
- new HumanMessage({ content: toLangChainContent(parts) }),
2235
- 'user'
2236
- )
2237
- );
2509
+ appendSyntheticProviderContextMessage(result, parts);
2238
2510
  i = j;
2239
2511
  }
2240
2512