@librechat/agents 3.3.3 → 3.3.5

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 (274) 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/common/constants.cjs +21 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +10 -1
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +793 -111
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/hooks/HookRegistry.cjs +25 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/index.cjs +12 -0
  14. package/dist/cjs/hooks/index.cjs.map +1 -1
  15. package/dist/cjs/hooks/types.cjs +1 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -1
  17. package/dist/cjs/langfuse.cjs +8 -0
  18. package/dist/cjs/langfuse.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  20. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  21. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
  22. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  24. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  25. package/dist/cjs/llm/invoke.cjs +331 -5
  26. package/dist/cjs/llm/invoke.cjs.map +1 -1
  27. package/dist/cjs/llm/openai/index.cjs +41 -5
  28. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  29. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  30. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  31. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  32. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  33. package/dist/cjs/llm/preempt.cjs +132 -0
  34. package/dist/cjs/llm/preempt.cjs.map +1 -0
  35. package/dist/cjs/main.cjs +36 -5
  36. package/dist/cjs/messages/alternation.cjs +92 -0
  37. package/dist/cjs/messages/alternation.cjs.map +1 -0
  38. package/dist/cjs/messages/cache.cjs +54 -33
  39. package/dist/cjs/messages/cache.cjs.map +1 -1
  40. package/dist/cjs/messages/contextPruning.cjs +17 -43
  41. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  42. package/dist/cjs/messages/core.cjs +315 -23
  43. package/dist/cjs/messages/core.cjs.map +1 -1
  44. package/dist/cjs/messages/format.cjs +290 -60
  45. package/dist/cjs/messages/format.cjs.map +1 -1
  46. package/dist/cjs/messages/handoffCue.cjs +63 -0
  47. package/dist/cjs/messages/handoffCue.cjs.map +1 -0
  48. package/dist/cjs/messages/index.cjs +4 -1
  49. package/dist/cjs/messages/injected.cjs +60 -0
  50. package/dist/cjs/messages/injected.cjs.map +1 -0
  51. package/dist/cjs/messages/prune.cjs +503 -127
  52. package/dist/cjs/messages/prune.cjs.map +1 -1
  53. package/dist/cjs/run.cjs +80 -7
  54. package/dist/cjs/run.cjs.map +1 -1
  55. package/dist/cjs/session/handlers.cjs +18 -9
  56. package/dist/cjs/session/handlers.cjs.map +1 -1
  57. package/dist/cjs/stream.cjs +35 -10
  58. package/dist/cjs/stream.cjs.map +1 -1
  59. package/dist/cjs/summarization/node.cjs +28 -14
  60. package/dist/cjs/summarization/node.cjs.map +1 -1
  61. package/dist/cjs/tools/ToolNode.cjs +56 -63
  62. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  63. package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
  64. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
  65. package/dist/cjs/tools/search/tool.cjs +9 -1
  66. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  67. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  68. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  69. package/dist/cjs/utils/index.cjs +1 -1
  70. package/dist/cjs/utils/llm.cjs +1 -1
  71. package/dist/cjs/utils/llm.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +307 -20
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/cjs/utils/toolContent.cjs +1514 -0
  75. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  76. package/dist/esm/agents/AgentContext.mjs +2 -1
  77. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  78. package/dist/esm/common/constants.mjs +19 -1
  79. package/dist/esm/common/constants.mjs.map +1 -1
  80. package/dist/esm/events.mjs +10 -1
  81. package/dist/esm/events.mjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +798 -116
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  85. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  86. package/dist/esm/hooks/HookRegistry.mjs +25 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/index.mjs +12 -1
  89. package/dist/esm/hooks/index.mjs.map +1 -1
  90. package/dist/esm/hooks/types.mjs +1 -0
  91. package/dist/esm/hooks/types.mjs.map +1 -1
  92. package/dist/esm/langfuse.mjs +9 -1
  93. package/dist/esm/langfuse.mjs.map +1 -1
  94. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  95. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  96. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
  97. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  98. package/dist/esm/llm/google/utils/common.mjs +19 -7
  99. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  100. package/dist/esm/llm/invoke.mjs +332 -8
  101. package/dist/esm/llm/invoke.mjs.map +1 -1
  102. package/dist/esm/llm/openai/index.mjs +41 -5
  103. package/dist/esm/llm/openai/index.mjs.map +1 -1
  104. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  105. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  106. package/dist/esm/llm/openrouter/index.mjs +4 -1
  107. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  108. package/dist/esm/llm/preempt.mjs +131 -0
  109. package/dist/esm/llm/preempt.mjs.map +1 -0
  110. package/dist/esm/main.mjs +14 -11
  111. package/dist/esm/messages/alternation.mjs +91 -0
  112. package/dist/esm/messages/alternation.mjs.map +1 -0
  113. package/dist/esm/messages/cache.mjs +55 -34
  114. package/dist/esm/messages/cache.mjs.map +1 -1
  115. package/dist/esm/messages/contextPruning.mjs +17 -43
  116. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  117. package/dist/esm/messages/core.mjs +305 -24
  118. package/dist/esm/messages/core.mjs.map +1 -1
  119. package/dist/esm/messages/format.mjs +290 -61
  120. package/dist/esm/messages/format.mjs.map +1 -1
  121. package/dist/esm/messages/handoffCue.mjs +61 -0
  122. package/dist/esm/messages/handoffCue.mjs.map +1 -0
  123. package/dist/esm/messages/index.mjs +4 -1
  124. package/dist/esm/messages/injected.mjs +60 -0
  125. package/dist/esm/messages/injected.mjs.map +1 -0
  126. package/dist/esm/messages/prune.mjs +500 -128
  127. package/dist/esm/messages/prune.mjs.map +1 -1
  128. package/dist/esm/run.mjs +80 -7
  129. package/dist/esm/run.mjs.map +1 -1
  130. package/dist/esm/session/handlers.mjs +19 -10
  131. package/dist/esm/session/handlers.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +34 -11
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +28 -14
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/ToolNode.mjs +56 -63
  137. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  138. package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
  139. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
  140. package/dist/esm/tools/search/tool.mjs +9 -1
  141. package/dist/esm/tools/search/tool.mjs.map +1 -1
  142. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  143. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  144. package/dist/esm/utils/index.mjs +1 -1
  145. package/dist/esm/utils/llm.mjs +1 -1
  146. package/dist/esm/utils/llm.mjs.map +1 -1
  147. package/dist/esm/utils/tokens.mjs +307 -21
  148. package/dist/esm/utils/tokens.mjs.map +1 -1
  149. package/dist/esm/utils/toolContent.mjs +1503 -0
  150. package/dist/esm/utils/toolContent.mjs.map +1 -0
  151. package/dist/types/common/constants.d.ts +18 -0
  152. package/dist/types/graphs/Graph.d.ts +139 -2
  153. package/dist/types/hooks/HookRegistry.d.ts +15 -0
  154. package/dist/types/hooks/index.d.ts +12 -1
  155. package/dist/types/hooks/types.d.ts +45 -6
  156. package/dist/types/index.d.ts +1 -1
  157. package/dist/types/llm/invoke.d.ts +34 -1
  158. package/dist/types/llm/openai/index.d.ts +4 -0
  159. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  160. package/dist/types/llm/openrouter/index.d.ts +5 -0
  161. package/dist/types/llm/preempt.d.ts +40 -0
  162. package/dist/types/messages/alternation.d.ts +23 -0
  163. package/dist/types/messages/contextPruning.d.ts +2 -1
  164. package/dist/types/messages/core.d.ts +36 -0
  165. package/dist/types/messages/format.d.ts +6 -0
  166. package/dist/types/messages/handoffCue.d.ts +40 -0
  167. package/dist/types/messages/index.d.ts +3 -0
  168. package/dist/types/messages/injected.d.ts +3 -0
  169. package/dist/types/messages/prune.d.ts +16 -12
  170. package/dist/types/run.d.ts +7 -0
  171. package/dist/types/stream.d.ts +14 -0
  172. package/dist/types/tools/ToolNode.d.ts +0 -7
  173. package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
  174. package/dist/types/tools/search/types.d.ts +31 -2
  175. package/dist/types/types/graph.d.ts +8 -1
  176. package/dist/types/types/run.d.ts +65 -0
  177. package/dist/types/types/stream.d.ts +1 -26
  178. package/dist/types/utils/tokens.d.ts +7 -0
  179. package/dist/types/utils/toolContent.d.ts +107 -0
  180. package/package.json +1 -1
  181. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  182. package/src/agents/AgentContext.ts +1 -0
  183. package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
  184. package/src/common/constants.ts +21 -0
  185. package/src/events.ts +15 -1
  186. package/src/graphs/Graph.ts +1261 -198
  187. package/src/graphs/MultiAgentGraph.ts +6 -1
  188. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
  189. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
  190. package/src/hooks/HookRegistry.ts +40 -0
  191. package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
  192. package/src/hooks/index.ts +16 -2
  193. package/src/hooks/types.ts +47 -3
  194. package/src/index.ts +1 -1
  195. package/src/langfuse.ts +26 -1
  196. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  197. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  198. package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
  199. package/src/llm/bedrock/utils/message_inputs.ts +100 -24
  200. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
  201. package/src/llm/google/utils/common.test.ts +75 -9
  202. package/src/llm/google/utils/common.ts +34 -3
  203. package/src/llm/invoke.alternation.test.ts +87 -0
  204. package/src/llm/invoke.handoffCue.test.ts +112 -0
  205. package/src/llm/invoke.test.ts +488 -0
  206. package/src/llm/invoke.ts +611 -8
  207. package/src/llm/openai/index.ts +106 -6
  208. package/src/llm/openai/utils/index.ts +109 -53
  209. package/src/llm/openai/utils/messages.test.ts +330 -1
  210. package/src/llm/openrouter/index.ts +12 -2
  211. package/src/llm/openrouter/reasoning.test.ts +314 -0
  212. package/src/llm/preempt.test.ts +323 -0
  213. package/src/llm/preempt.ts +178 -0
  214. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  215. package/src/messages/alternation.test.ts +212 -0
  216. package/src/messages/alternation.ts +112 -0
  217. package/src/messages/cache.tail.test.ts +193 -0
  218. package/src/messages/cache.test.ts +113 -0
  219. package/src/messages/cache.ts +92 -60
  220. package/src/messages/contextPruning.test.ts +184 -0
  221. package/src/messages/contextPruning.ts +49 -42
  222. package/src/messages/core.ts +653 -40
  223. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  224. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  225. package/src/messages/format.ts +454 -91
  226. package/src/messages/formatAgentMessages.steer.test.ts +267 -0
  227. package/src/messages/formatAgentMessages.test.ts +604 -14
  228. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  229. package/src/messages/handoffCue.test.ts +96 -0
  230. package/src/messages/handoffCue.ts +78 -0
  231. package/src/messages/index.ts +3 -0
  232. package/src/messages/injected.test.ts +90 -0
  233. package/src/messages/injected.ts +74 -0
  234. package/src/messages/prune.ts +996 -183
  235. package/src/run.ts +91 -6
  236. package/src/scripts/preempt-probe.ts +330 -0
  237. package/src/scripts/preempt-scenarios.ts +388 -0
  238. package/src/session/handlers.ts +32 -12
  239. package/src/specs/handoffCue.test.ts +165 -0
  240. package/src/specs/langfuse-callbacks.test.ts +352 -2
  241. package/src/specs/preemptSeal.test.ts +309 -0
  242. package/src/specs/prune.test.ts +1083 -6
  243. package/src/specs/summarization-unit.test.ts +105 -0
  244. package/src/specs/tokens.test.ts +609 -32
  245. package/src/stream.dispatch.test.ts +63 -0
  246. package/src/stream.ts +59 -23
  247. package/src/summarization/__tests__/node.test.ts +77 -0
  248. package/src/summarization/node.ts +46 -18
  249. package/src/tools/ToolNode.ts +150 -99
  250. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  251. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  252. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  253. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  254. package/src/tools/search/keenable-scraper.test.ts +153 -0
  255. package/src/tools/search/keenable-scraper.ts +137 -0
  256. package/src/tools/search/tool.ts +13 -2
  257. package/src/tools/search/types.ts +50 -3
  258. package/src/tools/toolOutputReferences.ts +6 -0
  259. package/src/types/graph.ts +12 -1
  260. package/src/types/run.ts +67 -0
  261. package/src/types/stream.ts +1 -42
  262. package/src/utils/llm.test.ts +18 -0
  263. package/src/utils/llm.ts +4 -1
  264. package/src/utils/tokens.ts +586 -31
  265. package/src/utils/toolContent.test.ts +1432 -0
  266. package/src/utils/toolContent.ts +2707 -0
  267. package/dist/cjs/splitStream.cjs +0 -151
  268. package/dist/cjs/splitStream.cjs.map +0 -1
  269. package/dist/esm/splitStream.mjs +0 -150
  270. package/dist/esm/splitStream.mjs.map +0 -1
  271. package/dist/types/mockStream.d.ts +0 -32
  272. package/dist/types/splitStream.d.ts +0 -37
  273. package/src/mockStream.ts +0 -99
  274. package/src/splitStream.ts +0 -234
@@ -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,34 @@ 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
+
478
+ /**
479
+ * Content for the synthetic assistant turn that separates a trailing steer
480
+ * from the next user turn. Non-empty by necessity — see the push site.
481
+ */
482
+ const STEER_ANCHOR_PLACEHOLDER = '_';
483
+
484
+ /**
485
+ * True when an assistant message replayed as a steer and nothing followed it,
486
+ * so the emitted run ends on the steer's `HumanMessage`.
487
+ */
488
+ function endsWithSteerMessage(
489
+ formatted: Array<RoleBearingMessage<BaseMessage>>
490
+ ): boolean {
491
+ if (formatted.length === 0) {
492
+ return false;
493
+ }
494
+ return formatted[formatted.length - 1].additional_kwargs.source === 'steer';
495
+ }
496
+
462
497
  /**
463
498
  * Helper function to format an assistant message
464
499
  * @param message The message to format
@@ -718,7 +753,7 @@ function formatAssistantMessage(
718
753
  new ToolMessage({
719
754
  tool_call_id: tool_call.id ?? '',
720
755
  name: tool_call.name,
721
- content: output != null ? output : '',
756
+ content: formatToolCallOutput(output),
722
757
  }),
723
758
  'tool'
724
759
  )
@@ -1250,7 +1285,11 @@ function contentPartCharLength(part: MessageContentComplex): number {
1250
1285
  if (typeof input === 'string') {
1251
1286
  len += input.length;
1252
1287
  } else if (input != null && typeof input === 'object') {
1253
- len += JSON.stringify(input).length;
1288
+ const measured = serializeStructuredValueBounded(input, 0).originalChars;
1289
+ len +=
1290
+ measured === Number.MAX_SAFE_INTEGER
1291
+ ? HARD_MAX_TOOL_RESULT_CHARS
1292
+ : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
1254
1293
  }
1255
1294
  return len;
1256
1295
  }
@@ -1316,6 +1355,36 @@ export const formatAgentMessages = (
1316
1355
  | RoleBearingMessage<SystemMessage>
1317
1356
  | RoleBearingMessage<ToolMessage>
1318
1357
  > = [];
1358
+ /**
1359
+ * A steer ended the previous payload entry, so the next message emitted —
1360
+ * whichever entry finally produces one — must be separated from it by an
1361
+ * assistant turn. Held rather than emitted so an entry that produces
1362
+ * nothing cannot leave the anchor stranded as the final turn.
1363
+ */
1364
+ let pendingSteerAnchor = false;
1365
+ /**
1366
+ * Emits the deferred anchor ahead of `next` — the message about to be
1367
+ * pushed. When that message is itself an assistant turn, it already IS the
1368
+ * separation the anchor exists to synthesize, so the intent is simply
1369
+ * discharged: emitting the placeholder anyway would put two assistant turns
1370
+ * back to back, which strict-alternation providers can reject and nothing downstream
1371
+ * repairs (`coalesceAdjacentUserTurns` merges user turns only).
1372
+ */
1373
+ const flushSteerAnchor = (next: { role?: LangChainMessageRole }): void => {
1374
+ if (!pendingSteerAnchor) {
1375
+ return;
1376
+ }
1377
+ pendingSteerAnchor = false;
1378
+ if (next.role === 'assistant') {
1379
+ return;
1380
+ }
1381
+ messages.push(
1382
+ withMessageRole(
1383
+ new AIMessage({ content: STEER_ANCHOR_PLACEHOLDER }),
1384
+ 'assistant'
1385
+ )
1386
+ );
1387
+ };
1319
1388
  // If indexTokenCountMap is provided, create a new map to track the updated indices
1320
1389
  const updatedIndexTokenCountMap: Record<number, number> = {};
1321
1390
  let boundaryTokenAdjustment:
@@ -1377,6 +1446,7 @@ export const formatAgentMessages = (
1377
1446
  if (sourceMessageId != null && sourceMessageId !== '') {
1378
1447
  formattedMessage.id = sourceMessageId;
1379
1448
  }
1449
+ flushSteerAnchor(formattedMessage);
1380
1450
  messages.push(formattedMessage);
1381
1451
 
1382
1452
  // Update the index mapping for this message
@@ -1560,7 +1630,48 @@ export const formatAgentMessages = (
1560
1630
  formattedMessage.id = sourceMessageId;
1561
1631
  }
1562
1632
  }
1633
+ /**
1634
+ * A steer that ends an assistant message leaves the replay on a
1635
+ * `HumanMessage`. The next payload message is itself a user turn, so the
1636
+ * sequence would reach the provider as two adjacent user turns — rejected
1637
+ * by strict-alternation providers. Anchor it with a placeholder assistant
1638
+ * turn.
1639
+ *
1640
+ * The placeholder must be NON-EMPTY. A string-content assistant message
1641
+ * with no tool calls passes through `_convertMessagesToAnthropicPayload`
1642
+ * verbatim — the empty-text repair there only covers array content and
1643
+ * tool-call turns — so an empty anchor would reach Anthropic as
1644
+ * `{role: 'assistant', content: ''}` and trade one invalid sequence for
1645
+ * another. Same single-underscore convention the Anthropic converter
1646
+ * already uses when it has to synthesize a non-empty block.
1647
+ *
1648
+ * Deferred rather than decided by lookahead. `i < payload.length - 1` only
1649
+ * proves a later ENTRY exists, not that it EMITS: entries with empty
1650
+ * content, and entries dropped by `applySummaryBoundary`, are skipped
1651
+ * silently. A trailing steer followed only by those would get the anchor
1652
+ * as the FINAL turn — an assistant prefill with no request after it, which
1653
+ * the model may simply never answer. So the intent is recorded and flushed
1654
+ * only when a message actually follows.
1655
+ *
1656
+ * Pushed AFTER the id stamping above, deliberately. `messagesStateReducer`
1657
+ * treats a repeated id as replace-in-place, so an anchor carrying the
1658
+ * shared `sourceMessageId` would overwrite the steer it exists to protect.
1659
+ * Left unstamped, it reaches the reducer with a null id and is assigned a
1660
+ * fresh one. `endsWithSteerMessage` reads only `additional_kwargs.source`,
1661
+ * so the deferral cannot change which messages get anchored.
1662
+ */
1663
+ /**
1664
+ * Guarded on emission: an assistant entry whose blocks all filtered away
1665
+ * emits nothing, and flushing for it would strand the anchor as the final
1666
+ * turn — the pending flag stays set for whichever entry emits next.
1667
+ */
1668
+ if (formattedMessages.length > 0) {
1669
+ flushSteerAnchor(formattedMessages[0]);
1670
+ }
1563
1671
  messages.push(...formattedMessages);
1672
+ if (endsWithSteerMessage(formattedMessages)) {
1673
+ pendingSteerAnchor = true;
1674
+ }
1564
1675
 
1565
1676
  // Capture index range BEFORE skill body injection so injected
1566
1677
  // HumanMessages are excluded from the assistant's token distribution.
@@ -1689,7 +1800,14 @@ export const formatAgentMessages = (
1689
1800
  if (typeof args === 'string') {
1690
1801
  len += args.length;
1691
1802
  } else if (args != null) {
1692
- len += JSON.stringify(args).length;
1803
+ const measured = serializeStructuredValueBounded(
1804
+ args,
1805
+ 0
1806
+ ).originalChars;
1807
+ len +=
1808
+ measured === Number.MAX_SAFE_INTEGER
1809
+ ? HARD_MAX_TOOL_RESULT_CHARS
1810
+ : Math.min(measured, HARD_MAX_TOOL_RESULT_CHARS);
1693
1811
  }
1694
1812
  }
1695
1813
  }
@@ -1754,13 +1872,156 @@ export function shiftIndexTokenCountMap(
1754
1872
  return shiftedMap;
1755
1873
  }
1756
1874
 
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
1875
  /** Checks whether a BaseMessage is a tool-role message. */
1761
1876
  const isToolMessage = (m: BaseMessage): boolean =>
1762
1877
  m instanceof ToolMessage || ('role' in m && (m as any).role === 'tool');
1763
1878
 
1879
+ const PORTABLE_FOLDED_MEDIA_TYPES = new Set(['image', 'image_url']);
1880
+ const MAX_FOLDED_BLOCK_CHARS = 8_000;
1881
+ const MAX_FOLDED_CONTEXT_CHARS = HARD_MAX_TOOL_RESULT_CHARS;
1882
+ const MAX_FOLDED_CONTEXT_WORK = 100_000;
1883
+ const FOLDED_CONTEXT_TRUNCATION_NOTICE =
1884
+ '… [additional folded context omitted]';
1885
+ const syntheticProviderContextMessages = new WeakSet<BaseMessage>();
1886
+
1887
+ type FoldContextBudget = {
1888
+ remainingChars: number;
1889
+ remainingWork: number;
1890
+ truncated: boolean;
1891
+ };
1892
+
1893
+ function createFoldContextBudget(): FoldContextBudget {
1894
+ return {
1895
+ remainingChars: MAX_FOLDED_CONTEXT_CHARS,
1896
+ remainingWork: MAX_FOLDED_CONTEXT_WORK,
1897
+ truncated: false,
1898
+ };
1899
+ }
1900
+
1901
+ function readFoldedDataProperty(value: unknown, key: string): unknown {
1902
+ if (value == null || typeof value !== 'object') {
1903
+ return undefined;
1904
+ }
1905
+ try {
1906
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1907
+ return descriptor != null && 'value' in descriptor
1908
+ ? descriptor.value
1909
+ : undefined;
1910
+ } catch {
1911
+ return undefined;
1912
+ }
1913
+ }
1914
+
1915
+ /**
1916
+ * Adds one logical line without first concatenating caller-controlled strings.
1917
+ * The shared budget covers every synthetic message emitted by one transform,
1918
+ * so many individually bounded blocks cannot build an unbounded aggregate.
1919
+ */
1920
+ function appendFoldedLine(
1921
+ textChunks: string[],
1922
+ budget: FoldContextBudget,
1923
+ pieces: readonly string[]
1924
+ ): boolean {
1925
+ if (budget.remainingChars <= 0) {
1926
+ budget.truncated = true;
1927
+ return false;
1928
+ }
1929
+
1930
+ const separatorChars = textChunks.length > 0 ? 1 : 0;
1931
+ const available = budget.remainingChars - separatorChars;
1932
+ if (available <= 0) {
1933
+ budget.remainingChars = 0;
1934
+ budget.truncated = true;
1935
+ return false;
1936
+ }
1937
+
1938
+ let totalChars = 0;
1939
+ for (const piece of pieces) {
1940
+ totalChars = Math.min(Number.MAX_SAFE_INTEGER, totalChars + piece.length);
1941
+ }
1942
+ if (totalChars <= available) {
1943
+ const line = pieces.join('');
1944
+ textChunks.push(line);
1945
+ budget.remainingChars -= separatorChars + line.length;
1946
+ return true;
1947
+ }
1948
+
1949
+ const notice = FOLDED_CONTEXT_TRUNCATION_NOTICE.slice(0, available);
1950
+ let remainingHeadChars = Math.max(0, available - notice.length);
1951
+ const boundedPieces: string[] = [];
1952
+ for (const piece of pieces) {
1953
+ if (remainingHeadChars <= 0) {
1954
+ break;
1955
+ }
1956
+ const retained = piece.slice(0, remainingHeadChars);
1957
+ boundedPieces.push(retained);
1958
+ remainingHeadChars -= retained.length;
1959
+ }
1960
+ boundedPieces.push(notice);
1961
+ textChunks.push(boundedPieces.join(''));
1962
+ budget.remainingChars = 0;
1963
+ budget.truncated = true;
1964
+ return false;
1965
+ }
1966
+
1967
+ function consumeFoldedWork(
1968
+ textChunks: string[],
1969
+ budget: FoldContextBudget
1970
+ ): boolean {
1971
+ if (budget.remainingChars <= 0) {
1972
+ return false;
1973
+ }
1974
+ if (budget.remainingWork-- > 0) {
1975
+ return true;
1976
+ }
1977
+ appendFoldedLine(textChunks, budget, [FOLDED_CONTEXT_TRUNCATION_NOTICE]);
1978
+ budget.remainingChars = 0;
1979
+ budget.truncated = true;
1980
+ return false;
1981
+ }
1982
+
1983
+ function serializeFoldedValue(value: unknown): string {
1984
+ const compacted = compactToolContent(value, MAX_FOLDED_BLOCK_CHARS).content;
1985
+ return typeof compacted === 'string'
1986
+ ? compacted
1987
+ : serializeStructuredValueBounded(compacted, MAX_FOLDED_BLOCK_CHARS)
1988
+ .content;
1989
+ }
1990
+
1991
+ function markSyntheticProviderContext<T extends BaseMessage>(message: T): T {
1992
+ syntheticProviderContextMessages.add(message);
1993
+ return message;
1994
+ }
1995
+
1996
+ function appendSyntheticProviderContextMessage(
1997
+ result: BaseMessage[],
1998
+ parts: MessageContentComplex[]
1999
+ ): boolean {
2000
+ if (parts.length === 0) {
2001
+ return false;
2002
+ }
2003
+ result.push(
2004
+ markSyntheticProviderContext(
2005
+ withMessageRole(
2006
+ new HumanMessage({ content: toLangChainContent(parts) }),
2007
+ 'user'
2008
+ )
2009
+ )
2010
+ );
2011
+ return true;
2012
+ }
2013
+
2014
+ /**
2015
+ * Identifies provider-context placeholders created by this module without
2016
+ * trusting user-controlled content prefixes or leaking marker metadata onto
2017
+ * the provider wire.
2018
+ */
2019
+ export function isSyntheticProviderContextMessage(
2020
+ message: BaseMessage
2021
+ ): boolean {
2022
+ return syntheticProviderContextMessages.has(message);
2023
+ }
2024
+
1764
2025
  /** Flushes accumulated text chunks into `parts` as a single text block. */
1765
2026
  function flushTextChunks(
1766
2027
  textChunks: string[],
@@ -1778,10 +2039,10 @@ function flushTextChunks(
1778
2039
 
1779
2040
  /**
1780
2041
  * 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.
2042
+ * accumulators. Portable image blocks are shallow-copied into `parts` so
2043
+ * binary data never becomes text tokens. Provider-specific media/resource
2044
+ * blocks are retained as bounded text so a folded cross-provider history
2045
+ * cannot contain an unsupported native block or JSON-expand without limit.
1785
2046
  *
1786
2047
  * When `content` is an array containing tool_use blocks, `tool_calls` is NOT
1787
2048
  * additionally serialized (avoiding double output). `tool_calls` is used as
@@ -1791,37 +2052,69 @@ function appendMessageContent(
1791
2052
  msg: BaseMessage,
1792
2053
  role: string,
1793
2054
  textChunks: string[],
1794
- parts: MessageContentComplex[]
2055
+ parts: MessageContentComplex[],
2056
+ budget: FoldContextBudget
1795
2057
  ): void {
1796
2058
  const { content } = msg;
1797
2059
 
1798
2060
  if (typeof content === 'string') {
1799
- if (content) {
1800
- textChunks.push(`${role}: ${content}`);
2061
+ if (content && consumeFoldedWork(textChunks, budget)) {
2062
+ appendFoldedLine(textChunks, budget, [`${role}: `, content]);
1801
2063
  }
1802
- appendToolCalls(msg, role, textChunks);
2064
+ appendToolCalls(msg, role, textChunks, budget);
1803
2065
  return;
1804
2066
  }
1805
2067
 
1806
2068
  if (!Array.isArray(content)) {
1807
- appendToolCalls(msg, role, textChunks);
2069
+ appendToolCalls(msg, role, textChunks, budget);
1808
2070
  return;
1809
2071
  }
1810
2072
 
1811
2073
  let hasToolUseBlock = false;
1812
2074
 
1813
2075
  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);
2076
+ if (!consumeFoldedWork(textChunks, budget)) {
2077
+ hasToolUseBlock = true;
2078
+ break;
2079
+ }
2080
+ const blockTypeValue = readFoldedDataProperty(block, 'type');
2081
+ const blockType =
2082
+ typeof blockTypeValue === 'string' ? blockTypeValue : undefined;
2083
+
2084
+ if (
2085
+ blockType !== 'tool_use' &&
2086
+ blockType !== 'tool_call' &&
2087
+ blockType !== 'tool_result' &&
2088
+ isAtomicToolContentBlock(block)
2089
+ ) {
2090
+ if (PORTABLE_FOLDED_MEDIA_TYPES.has(blockType ?? '')) {
2091
+ const blockChars = getToolContentCharLength([block]);
2092
+ if (blockChars > budget.remainingChars) {
2093
+ appendFoldedLine(textChunks, budget, [
2094
+ `${role}: [${blockType ?? 'media'} omitted: folded context limit]`,
2095
+ ]);
2096
+ continue;
2097
+ }
2098
+ flushTextChunks(textChunks, parts);
2099
+ parts.push({ ...block } as MessageContentComplex);
2100
+ budget.remainingChars -= blockChars;
2101
+ } else {
2102
+ appendFoldedLine(textChunks, budget, [
2103
+ `${role}: [${blockType ?? 'media'}] `,
2104
+ serializeFoldedValue(block),
2105
+ ]);
2106
+ }
1817
2107
  continue;
1818
2108
  }
1819
2109
 
1820
- if (block.type === 'tool_use') {
2110
+ if (blockType === 'tool_use') {
1821
2111
  hasToolUseBlock = true;
1822
- textChunks.push(
1823
- `${role}: [tool_use] ${String(block.name ?? '')} ${JSON.stringify(block.input ?? {})}`
1824
- );
2112
+ const name = readFoldedDataProperty(block, 'name');
2113
+ const input = readFoldedDataProperty(block, 'input');
2114
+ appendFoldedLine(textChunks, budget, [
2115
+ `${role}: [tool_use] ${typeof name === 'string' ? name : ''} `,
2116
+ serializeFoldedValue(input ?? {}),
2117
+ ]);
1825
2118
  continue;
1826
2119
  }
1827
2120
 
@@ -1830,17 +2123,32 @@ function appendMessageContent(
1830
2123
  // toolUse) or this repo's `ToolCallContent` (`{ tool_call: { name, args,
1831
2124
  // output } }`, from `convertMessagesToContent` / persisted history). Handle
1832
2125
  // both, and emit any embedded output, so the name/args/result survive.
1833
- if (block.type === 'tool_call') {
2126
+ if (blockType === 'tool_call') {
1834
2127
  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 ?? {};
2128
+ const nested = readFoldedDataProperty(block, 'tool_call');
2129
+ const nestedName = readFoldedDataProperty(nested, 'name');
2130
+ const topLevelName = readFoldedDataProperty(block, 'name');
2131
+ let name = '';
2132
+ if (typeof nestedName === 'string') {
2133
+ name = nestedName;
2134
+ } else if (typeof topLevelName === 'string') {
2135
+ name = topLevelName;
2136
+ }
2137
+ const nestedArgs = readFoldedDataProperty(nested, 'args');
2138
+ const topLevelArgs = readFoldedDataProperty(block, 'args');
2139
+ const rawArgs = nestedArgs ?? topLevelArgs ?? {};
1838
2140
  const argsText =
1839
- typeof rawArgs === 'string' ? rawArgs : JSON.stringify(rawArgs);
1840
- textChunks.push(`${role}: [tool_use] ${name} ${argsText}`.trimEnd());
1841
- const output = nested?.output;
2141
+ typeof rawArgs === 'string' ? rawArgs : serializeFoldedValue(rawArgs);
2142
+ appendFoldedLine(textChunks, budget, [
2143
+ `${role}: [tool_use] ${name}${argsText ? ' ' : ''}`,
2144
+ argsText,
2145
+ ]);
2146
+ const output = readFoldedDataProperty(nested, 'output');
1842
2147
  if (output != null && output !== '') {
1843
- textChunks.push(`Tool: ${String(output)}`);
2148
+ appendFoldedLine(textChunks, budget, [
2149
+ 'Tool: ',
2150
+ typeof output === 'string' ? output : serializeFoldedValue(output),
2151
+ ]);
1844
2152
  }
1845
2153
  continue;
1846
2154
  }
@@ -1848,65 +2156,101 @@ function appendMessageContent(
1848
2156
  // A `tool_result` content block (e.g. an AIMessage(tool_call) followed by a
1849
2157
  // user message carrying the result). Preserve nested image blocks as-is
1850
2158
  // instead of JSON-stringifying them through the generic fallback.
1851
- if (block.type === 'tool_result') {
2159
+ if (blockType === 'tool_result') {
1852
2160
  hasToolUseBlock = true;
1853
- const inner = (block as { content?: ToolResultContent['content'] })
1854
- .content;
2161
+ const inner = readFoldedDataProperty(block, 'content') as
2162
+ | ToolResultContent['content']
2163
+ | undefined;
1855
2164
  if (typeof inner === 'string') {
1856
2165
  if (inner) {
1857
- textChunks.push(`${role}: [tool_result] ${inner}`);
2166
+ appendFoldedLine(textChunks, budget, [
2167
+ `${role}: [tool_result] `,
2168
+ inner,
2169
+ ]);
1858
2170
  }
1859
2171
  } else if (Array.isArray(inner)) {
1860
2172
  for (const innerBlock of inner as Array<
1861
2173
  string | ExtendedMessageContent
1862
2174
  >) {
2175
+ if (!consumeFoldedWork(textChunks, budget)) {
2176
+ break;
2177
+ }
1863
2178
  if (typeof innerBlock === 'string') {
1864
2179
  if (innerBlock) {
1865
- textChunks.push(`${role}: [tool_result] ${innerBlock}`);
2180
+ appendFoldedLine(textChunks, budget, [
2181
+ `${role}: [tool_result] `,
2182
+ innerBlock,
2183
+ ]);
1866
2184
  }
1867
- } else if (IMAGE_BLOCK_TYPES.has(innerBlock.type ?? '')) {
1868
- flushTextChunks(textChunks, parts);
1869
- parts.push({ ...innerBlock } as MessageContentComplex);
1870
2185
  } else {
1871
- const innerText = innerBlock.text ?? innerBlock.input;
1872
- textChunks.push(
1873
- `${role}: [tool_result] ${
2186
+ const innerTypeValue = readFoldedDataProperty(innerBlock, 'type');
2187
+ const innerType =
2188
+ typeof innerTypeValue === 'string' ? innerTypeValue : undefined;
2189
+ if (
2190
+ isAtomicToolContentBlock(innerBlock) &&
2191
+ PORTABLE_FOLDED_MEDIA_TYPES.has(innerType ?? '')
2192
+ ) {
2193
+ const blockChars = getToolContentCharLength([innerBlock]);
2194
+ if (blockChars <= budget.remainingChars) {
2195
+ flushTextChunks(textChunks, parts);
2196
+ parts.push({ ...innerBlock } as MessageContentComplex);
2197
+ budget.remainingChars -= blockChars;
2198
+ } else {
2199
+ appendFoldedLine(textChunks, budget, [
2200
+ `${role}: [${innerType ?? 'media'} omitted: folded context limit]`,
2201
+ ]);
2202
+ }
2203
+ } else {
2204
+ const textValue = readFoldedDataProperty(innerBlock, 'text');
2205
+ const inputValue = readFoldedDataProperty(innerBlock, 'input');
2206
+ const innerText = textValue ?? inputValue;
2207
+ appendFoldedLine(textChunks, budget, [
2208
+ `${role}: [tool_result] `,
1874
2209
  typeof innerText === 'string' && innerText
1875
2210
  ? innerText
1876
- : JSON.stringify(innerBlock)
1877
- }`
1878
- );
2211
+ : serializeFoldedValue(innerBlock),
2212
+ ]);
2213
+ }
1879
2214
  }
1880
2215
  }
1881
2216
  } else if (inner != null) {
1882
- textChunks.push(`${role}: [tool_result] ${JSON.stringify(inner)}`);
2217
+ appendFoldedLine(textChunks, budget, [
2218
+ `${role}: [tool_result] `,
2219
+ serializeFoldedValue(inner),
2220
+ ]);
1883
2221
  }
1884
2222
  continue;
1885
2223
  }
1886
2224
 
1887
- const text = block.text ?? block.input;
2225
+ const text =
2226
+ readFoldedDataProperty(block, 'text') ??
2227
+ readFoldedDataProperty(block, 'input');
1888
2228
  if (typeof text === 'string' && text) {
1889
- textChunks.push(`${role}: ${text}`);
2229
+ appendFoldedLine(textChunks, budget, [`${role}: `, text]);
1890
2230
  continue;
1891
2231
  }
1892
2232
 
1893
2233
  // Fallback: serialize unrecognized block types to preserve context
1894
- if (block.type != null && block.type !== '') {
1895
- textChunks.push(`${role}: [${block.type}] ${JSON.stringify(block)}`);
2234
+ if (blockType != null && blockType !== '') {
2235
+ appendFoldedLine(textChunks, budget, [
2236
+ `${role}: [${blockType}] `,
2237
+ serializeFoldedValue(block),
2238
+ ]);
1896
2239
  }
1897
2240
  }
1898
2241
 
1899
2242
  // If content array had no tool_use blocks, fall back to tool_calls metadata
1900
2243
  // (handles edge case: empty content array with tool_calls populated)
1901
2244
  if (!hasToolUseBlock) {
1902
- appendToolCalls(msg, role, textChunks);
2245
+ appendToolCalls(msg, role, textChunks, budget);
1903
2246
  }
1904
2247
  }
1905
2248
 
1906
2249
  function appendToolCalls(
1907
2250
  msg: BaseMessage,
1908
2251
  role: string,
1909
- textChunks: string[]
2252
+ textChunks: string[],
2253
+ budget: FoldContextBudget
1910
2254
  ): void {
1911
2255
  if (role !== 'AI') {
1912
2256
  return;
@@ -1914,7 +2258,16 @@ function appendToolCalls(
1914
2258
  const aiMsg = msg as AIMessage;
1915
2259
  if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
1916
2260
  for (const tc of aiMsg.tool_calls) {
1917
- textChunks.push(`AI: [tool_call] ${tc.name}(${JSON.stringify(tc.args)})`);
2261
+ if (!consumeFoldedWork(textChunks, budget)) {
2262
+ break;
2263
+ }
2264
+ const name = readFoldedDataProperty(tc, 'name');
2265
+ const args = readFoldedDataProperty(tc, 'args');
2266
+ appendFoldedLine(textChunks, budget, [
2267
+ `AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
2268
+ serializeFoldedValue(args),
2269
+ ')',
2270
+ ]);
1918
2271
  }
1919
2272
  return;
1920
2273
  }
@@ -1924,14 +2277,20 @@ function appendToolCalls(
1924
2277
  return;
1925
2278
  }
1926
2279
  for (const tc of rawToolCalls) {
1927
- const fn = (tc as { function?: { name?: string; arguments?: string } })
1928
- .function;
2280
+ if (!consumeFoldedWork(textChunks, budget)) {
2281
+ break;
2282
+ }
2283
+ const fn = readFoldedDataProperty(tc, 'function');
1929
2284
  if (fn == null) {
1930
2285
  continue;
1931
2286
  }
1932
- textChunks.push(
1933
- `AI: [tool_call] ${String(fn.name ?? '')}(${String(fn.arguments ?? '')})`
1934
- );
2287
+ const name = readFoldedDataProperty(fn, 'name');
2288
+ const args = readFoldedDataProperty(fn, 'arguments');
2289
+ appendFoldedLine(textChunks, budget, [
2290
+ `AI: [tool_call] ${typeof name === 'string' ? name : ''}(`,
2291
+ typeof args === 'string' ? args : '',
2292
+ ')',
2293
+ ]);
1935
2294
  }
1936
2295
  }
1937
2296
 
@@ -1991,6 +2350,7 @@ export function ensureThinkingBlockInMessages(
1991
2350
 
1992
2351
  const result: BaseMessage[] =
1993
2352
  lastHumanIndex >= 0 ? messages.slice(0, lastHumanIndex + 1) : [];
2353
+ const foldBudget = createFoldContextBudget();
1994
2354
  let i = lastHumanIndex + 1;
1995
2355
 
1996
2356
  while (i < messages.length) {
@@ -2021,13 +2381,14 @@ export function ensureThinkingBlockInMessages(
2021
2381
  if (typeof c !== 'object') {
2022
2382
  continue;
2023
2383
  }
2024
- if (c.type === 'tool_use') {
2384
+ const type = readFoldedDataProperty(c, 'type');
2385
+ if (type === 'tool_use') {
2025
2386
  hasToolUse = true;
2026
2387
  } else if (
2027
- c.type === ContentTypes.THINKING ||
2028
- c.type === ContentTypes.REASONING_CONTENT ||
2029
- c.type === ContentTypes.REASONING ||
2030
- c.type === 'redacted_thinking'
2388
+ type === ContentTypes.THINKING ||
2389
+ type === ContentTypes.REASONING_CONTENT ||
2390
+ type === ContentTypes.REASONING ||
2391
+ type === 'redacted_thinking'
2031
2392
  ) {
2032
2393
  hasThinkingBlock = true;
2033
2394
  }
@@ -2082,30 +2443,33 @@ export function ensureThinkingBlockInMessages(
2082
2443
  // ToolMessages — preserves image blocks as-is to avoid serializing
2083
2444
  // binary data as text (which caused 174× token amplification).
2084
2445
  const parts: MessageContentComplex[] = [];
2085
- const textChunks: string[] = ['[Previous agent context]'];
2446
+ const textChunks: string[] = [];
2447
+ appendFoldedLine(textChunks, foldBudget, ['[Previous agent context]']);
2086
2448
 
2087
- appendMessageContent(msg, 'AI', textChunks, parts);
2449
+ appendMessageContent(msg, 'AI', textChunks, parts, foldBudget);
2088
2450
 
2089
2451
  let j = i + 1;
2090
2452
  while (j < messages.length && isToolMessage(messages[j])) {
2091
- appendMessageContent(messages[j], 'Tool', textChunks, parts);
2453
+ appendMessageContent(
2454
+ messages[j],
2455
+ 'Tool',
2456
+ textChunks,
2457
+ parts,
2458
+ foldBudget
2459
+ );
2092
2460
  j++;
2093
2461
  }
2094
2462
 
2095
2463
  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
- );
2464
+ if (appendSyntheticProviderContextMessage(result, parts)) {
2465
+ emitAgentLog(
2466
+ config,
2467
+ 'warn',
2468
+ 'format',
2469
+ 'ensureThinkingBlockInMessages: injecting [Previous agent context] HumanMessage' +
2470
+ ` (${parts.length} msgs at index ${i}, no thinking block in chain)`
2471
+ );
2472
+ }
2109
2473
  i = j;
2110
2474
  } else {
2111
2475
  // Keep the message as is
@@ -2140,11 +2504,10 @@ function messageHasToolContent(msg: BaseMessage): boolean {
2140
2504
  }
2141
2505
  if (Array.isArray(msg.content)) {
2142
2506
  for (const block of msg.content as ExtendedMessageContent[]) {
2507
+ const type = readFoldedDataProperty(block, 'type');
2143
2508
  if (
2144
2509
  typeof block === 'object' &&
2145
- (block.type === 'tool_use' ||
2146
- block.type === 'tool_call' ||
2147
- block.type === 'tool_result')
2510
+ (type === 'tool_use' || type === 'tool_call' || type === 'tool_result')
2148
2511
  ) {
2149
2512
  return true;
2150
2513
  }
@@ -2164,7 +2527,9 @@ function isToolResultMessage(msg: BaseMessage): boolean {
2164
2527
  }
2165
2528
  if (Array.isArray(msg.content)) {
2166
2529
  return (msg.content as ExtendedMessageContent[]).some(
2167
- (block) => typeof block === 'object' && block.type === 'tool_result'
2530
+ (block) =>
2531
+ typeof block === 'object' &&
2532
+ readFoldedDataProperty(block, 'type') === 'tool_result'
2168
2533
  );
2169
2534
  }
2170
2535
  return false;
@@ -2197,6 +2562,7 @@ export function foldToolBlocksForToollessAgent(
2197
2562
  ): BaseMessage[] {
2198
2563
  let result: BaseMessage[] | null = null;
2199
2564
  let foldedCount = 0;
2565
+ const foldBudget = createFoldContextBudget();
2200
2566
  let i = 0;
2201
2567
  while (i < messages.length) {
2202
2568
  const msg = messages[i];
@@ -2212,29 +2578,26 @@ export function foldToolBlocksForToollessAgent(
2212
2578
  }
2213
2579
 
2214
2580
  const parts: MessageContentComplex[] = [];
2215
- const textChunks: string[] = ['[Previous tool interaction]'];
2581
+ const textChunks: string[] = [];
2582
+ appendFoldedLine(textChunks, foldBudget, ['[Previous tool interaction]']);
2216
2583
  appendMessageContent(
2217
2584
  msg,
2218
2585
  isToolResultMessage(msg) ? 'Tool' : 'AI',
2219
2586
  textChunks,
2220
- parts
2587
+ parts,
2588
+ foldBudget
2221
2589
  );
2222
2590
  foldedCount++;
2223
2591
 
2224
2592
  let j = i + 1;
2225
2593
  while (j < messages.length && isToolResultMessage(messages[j])) {
2226
- appendMessageContent(messages[j], 'Tool', textChunks, parts);
2594
+ appendMessageContent(messages[j], 'Tool', textChunks, parts, foldBudget);
2227
2595
  foldedCount++;
2228
2596
  j++;
2229
2597
  }
2230
2598
 
2231
2599
  flushTextChunks(textChunks, parts);
2232
- result.push(
2233
- withMessageRole(
2234
- new HumanMessage({ content: toLangChainContent(parts) }),
2235
- 'user'
2236
- )
2237
- );
2600
+ appendSyntheticProviderContextMessage(result, parts);
2238
2601
  i = j;
2239
2602
  }
2240
2603