@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
@@ -0,0 +1,63 @@
1
+ import type * as t from '@/types';
2
+ import {
3
+ ChatModelStreamHandler,
4
+ dispatchesChatModelStream,
5
+ SDK_STREAM_DISPATCH,
6
+ } from '@/stream';
7
+ import { composeEventHandlers } from '@/events';
8
+ import { GraphEvents } from '@/common';
9
+
10
+ const inert: t.EventHandler = {
11
+ handle: (): void => {
12
+ /* renders nothing */
13
+ },
14
+ };
15
+
16
+ describe('dispatchesChatModelStream', () => {
17
+ it('recognizes the dispatcher itself', () => {
18
+ expect(dispatchesChatModelStream(new ChatModelStreamHandler())).toBe(true);
19
+ });
20
+
21
+ it('is false for a handler that owns no content-part dispatch', () => {
22
+ expect(dispatchesChatModelStream(inert)).toBe(false);
23
+ });
24
+
25
+ it('is false for nothing registered', () => {
26
+ expect(dispatchesChatModelStream(undefined)).toBe(false);
27
+ });
28
+
29
+ /**
30
+ * The reason identity is not a usable test: hosts wrap handlers, and every
31
+ * wrapper fails `instanceof` while still driving the same dispatch. A run
32
+ * that seals on that basis would be sealing against its own contract.
33
+ */
34
+ it('sees through a composed wrapper in either position', () => {
35
+ const first = composeEventHandlers(
36
+ { [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler() },
37
+ { [GraphEvents.CHAT_MODEL_STREAM]: inert }
38
+ )[GraphEvents.CHAT_MODEL_STREAM];
39
+ const second = composeEventHandlers(
40
+ { [GraphEvents.CHAT_MODEL_STREAM]: inert },
41
+ { [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler() }
42
+ )[GraphEvents.CHAT_MODEL_STREAM];
43
+
44
+ expect(first).not.toBeInstanceOf(ChatModelStreamHandler);
45
+ expect(second).not.toBeInstanceOf(ChatModelStreamHandler);
46
+ expect(dispatchesChatModelStream(first)).toBe(true);
47
+ expect(dispatchesChatModelStream(second)).toBe(true);
48
+ });
49
+
50
+ it('does not brand a composition of inert handlers', () => {
51
+ const composed = composeEventHandlers(
52
+ { [GraphEvents.CHAT_MODEL_STREAM]: inert },
53
+ { [GraphEvents.CHAT_MODEL_STREAM]: inert }
54
+ )[GraphEvents.CHAT_MODEL_STREAM];
55
+ expect(dispatchesChatModelStream(composed)).toBe(false);
56
+ });
57
+
58
+ it('exposes the brand as a cross-realm-safe registered symbol', () => {
59
+ expect(SDK_STREAM_DISPATCH).toBe(
60
+ Symbol.for('@librechat/agents:chatModelStreamDispatch')
61
+ );
62
+ });
63
+ });
package/src/stream.ts CHANGED
@@ -26,6 +26,10 @@ import {
26
26
  coerceRecordArgs,
27
27
  normalizeError,
28
28
  } from '@/tools/eagerEventExecution';
29
+ import {
30
+ serializeStructuredValueBounded,
31
+ serializeToolContentBounded,
32
+ } from '@/utils/toolContent';
29
33
  import {
30
34
  handleServerToolResult,
31
35
  handleToolCallChunks,
@@ -841,15 +845,20 @@ async function dispatchEagerToolCompletions(args: {
841
845
  if (stepId === '') {
842
846
  continue;
843
847
  }
844
- const output =
845
- result.status === 'error'
846
- ? `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`
847
- : truncateToolResultContent(
848
- typeof result.content === 'string'
849
- ? result.content
850
- : JSON.stringify(result.content),
851
- maxToolResultChars
852
- );
848
+ let output: string;
849
+ if (result.status === 'error') {
850
+ output = truncateToolResultContent(
851
+ `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`,
852
+ maxToolResultChars
853
+ );
854
+ } else if (typeof result.content === 'string') {
855
+ output = truncateToolResultContent(result.content, maxToolResultChars);
856
+ } else {
857
+ output = serializeStructuredValueBounded(
858
+ result.content,
859
+ maxToolResultChars
860
+ ).content;
861
+ }
853
862
 
854
863
  try {
855
864
  const dispatched = await safeDispatchCustomEvent(
@@ -861,7 +870,10 @@ async function dispatchEagerToolCompletions(args: {
861
870
  type: 'tool_call' as const,
862
871
  eager: true,
863
872
  tool_call: {
864
- args: JSON.stringify(record.request.args),
873
+ args: serializeToolContentBounded(
874
+ record.request.args,
875
+ maxToolResultChars
876
+ ),
865
877
  name: record.toolName,
866
878
  id: result.toolCallId,
867
879
  output,
@@ -1388,7 +1400,34 @@ function shouldSkipLateOpenRouterReasoningChunk({
1388
1400
  );
1389
1401
  }
1390
1402
 
1403
+ /**
1404
+ * Brands a handler as one that dispatches content parts for the SDK — either
1405
+ * `ChatModelStreamHandler` itself or a wrapper forwarding to one.
1406
+ *
1407
+ * Identity alone is not a usable contract here. Hosts compose and wrap
1408
+ * handlers (`composeEventHandlers`, `createRunHandlers`), and every wrapper
1409
+ * fails `instanceof` while still driving the same dispatch. A brand survives
1410
+ * wrapping, so "does this handler own content-part dispatch" can be answered
1411
+ * about a value the SDK did not construct.
1412
+ */
1413
+ export const SDK_STREAM_DISPATCH = Symbol.for(
1414
+ '@librechat/agents:chatModelStreamDispatch'
1415
+ );
1416
+
1417
+ /** True when `handler` is, or forwards to, the SDK's stream dispatcher. */
1418
+ export function dispatchesChatModelStream(handler?: t.EventHandler): boolean {
1419
+ if (handler == null) {
1420
+ return false;
1421
+ }
1422
+ if (handler instanceof ChatModelStreamHandler) {
1423
+ return true;
1424
+ }
1425
+ return Reflect.get(handler, SDK_STREAM_DISPATCH) === true;
1426
+ }
1427
+
1391
1428
  export class ChatModelStreamHandler implements t.EventHandler {
1429
+ readonly [SDK_STREAM_DISPATCH] = true;
1430
+
1392
1431
  async handle(
1393
1432
  event: string,
1394
1433
  data: t.StreamEventData,
@@ -1834,13 +1873,16 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1834
1873
  number,
1835
1874
  { agentId?: string; groupId?: number }
1836
1875
  >();
1837
- const getFirstContentPart = (
1876
+ /** A delta's content may carry several parts (e.g. Google server-side tool
1877
+ * chunks emit multiple reasoning entries at once); every entry must reach
1878
+ * the step's slot, in order, or streamed text is silently lost. */
1879
+ const getDeltaContentParts = (
1838
1880
  content?: t.MessageDelta['content'] | t.MessageContentComplex
1839
- ): t.MessageContentComplex | undefined => {
1881
+ ): t.MessageContentComplex[] => {
1840
1882
  if (content == null) {
1841
- return undefined;
1883
+ return [];
1842
1884
  }
1843
- return Array.isArray(content) ? content[0] : content;
1885
+ return Array.isArray(content) ? content : [content];
1844
1886
  };
1845
1887
  const indexContentPart = (
1846
1888
  index: number,
@@ -2248,11 +2290,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2248
2290
  const contentIndex =
2249
2291
  toolCallIndices[toolCallIndex] ?? runStep.index;
2250
2292
  const toolCallId = toolCall.id ?? '';
2251
- registerToolContentIndex(
2252
- toolStepContent,
2253
- contentIndex,
2254
- toolCallId
2255
- );
2293
+ registerToolContentIndex(toolStepContent, contentIndex, toolCallId);
2256
2294
  const contentPart: t.MessageContentComplex = {
2257
2295
  type: ContentTypes.TOOL_CALL,
2258
2296
  tool_call: {
@@ -2276,8 +2314,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2276
2314
  return;
2277
2315
  }
2278
2316
 
2279
- const contentPart = getFirstContentPart(messageDelta.delta.content);
2280
- if (contentPart != null) {
2317
+ for (const contentPart of getDeltaContentParts(messageDelta.delta.content)) {
2281
2318
  updateContent(runStep.index, contentPart);
2282
2319
  }
2283
2320
  } else if (
@@ -2306,8 +2343,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2306
2343
  return;
2307
2344
  }
2308
2345
 
2309
- const contentPart = getFirstContentPart(reasoningDelta.delta.content);
2310
- if (contentPart != null) {
2346
+ for (const contentPart of getDeltaContentParts(reasoningDelta.delta.content)) {
2311
2347
  updateContent(runStep.index, contentPart);
2312
2348
  }
2313
2349
  } else if (event === GraphEvents.ON_RUN_STEP_DELTA) {
@@ -1045,6 +1045,83 @@ describe('recency window — first-turn protection', () => {
1045
1045
  expect(tailToolMsg!.content).not.toContain('FULL_ORIGINAL_OUTPUT');
1046
1046
  });
1047
1047
 
1048
+ it('bounds restored tool originals and preserves ToolMessage metadata', async () => {
1049
+ captureEvents();
1050
+
1051
+ let restoredToolMessage: ToolMessage | undefined;
1052
+ const invokeMock = jest.fn().mockImplementation((messages: unknown) => {
1053
+ restoredToolMessage = (messages as Array<unknown>).find(
1054
+ (message) => message instanceof ToolMessage
1055
+ ) as ToolMessage | undefined;
1056
+ return Promise.resolve({ content: 'summary' });
1057
+ });
1058
+ jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
1059
+ class {
1060
+ constructor() {
1061
+ return { invoke: invokeMock };
1062
+ }
1063
+ } as never
1064
+ );
1065
+
1066
+ const artifact = { source: 'clickhouse', rows: 1_000 };
1067
+ const originalToolMessage = new ToolMessage({
1068
+ content: 'masked-stub',
1069
+ tool_call_id: 'bounded',
1070
+ name: 'run_select_query',
1071
+ status: 'success',
1072
+ artifact,
1073
+ metadata: { traceId: 'trace-1' },
1074
+ additional_kwargs: { retained: true },
1075
+ response_metadata: { requestId: 'request-1' },
1076
+ });
1077
+ const agentContext = createAgentContext({
1078
+ maxContextTokens: 2_000,
1079
+ summarizationConfig: { retainRecent: { turns: 0 } },
1080
+ setSummary: jest.fn(),
1081
+ });
1082
+ agentContext.pendingOriginalToolContent = new Map([
1083
+ [2, `[{"rows":"${'x'.repeat(20_000)}"}]`],
1084
+ ]);
1085
+
1086
+ const summarizeNode = createSummarizeNode({
1087
+ agentContext,
1088
+ graph: mockGraph() as never,
1089
+ generateStepId,
1090
+ });
1091
+ await summarizeNode(
1092
+ {
1093
+ messages: [
1094
+ new HumanMessage('query the table'),
1095
+ new AIMessage({
1096
+ content: '',
1097
+ tool_calls: [{ id: 'bounded', name: 'run_select_query', args: {} }],
1098
+ }),
1099
+ originalToolMessage,
1100
+ ],
1101
+ summarizationRequest: {
1102
+ remainingContextTokens: 0,
1103
+ agentId: 'agent_0',
1104
+ },
1105
+ },
1106
+ {} as RunnableConfig
1107
+ );
1108
+
1109
+ expect(restoredToolMessage).toBeDefined();
1110
+ expect(typeof restoredToolMessage!.content).toBe('string');
1111
+ expect(String(restoredToolMessage!.content).length).toBeLessThanOrEqual(
1112
+ 2_400
1113
+ );
1114
+ expect(restoredToolMessage!.content).toContain('[truncated:');
1115
+ expect(restoredToolMessage!.status).toBe('success');
1116
+ expect(restoredToolMessage!.artifact).toBe(artifact);
1117
+ expect(restoredToolMessage!.metadata).toEqual({ traceId: 'trace-1' });
1118
+ expect(restoredToolMessage!.additional_kwargs).toEqual({ retained: true });
1119
+ expect(restoredToolMessage!.response_metadata).toEqual({
1120
+ requestId: 'request-1',
1121
+ });
1122
+ expect(originalToolMessage.content).toBe('masked-stub');
1123
+ });
1124
+
1048
1125
  it('preserves tail-relevant pendingOriginalToolContent entries (reindexed) for future summaries', async () => {
1049
1126
  captureEvents();
1050
1127
 
@@ -10,6 +10,12 @@ import type { AgentContext } from '@/agents/AgentContext';
10
10
  import type { HookRegistry } from '@/hooks';
11
11
  import type { OnChunk } from '@/llm/invoke';
12
12
  import type * as t from '@/types';
13
+ import {
14
+ cloneToolMessageWithContent,
15
+ compactToolContent,
16
+ isComputerCallOutputMessage,
17
+ serializeToolContentBounded,
18
+ } from '@/utils/toolContent';
13
19
  import {
14
20
  addTailCacheControl,
15
21
  resolvePromptCacheTtl,
@@ -28,6 +34,7 @@ import {
28
34
  } from '@/common';
29
35
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
30
36
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
37
+ import { calculateMaxToolResultChars } from '@/utils/truncation';
31
38
  import { createRemoveAllMessage } from '@/messages/reducer';
32
39
  import { getMaxOutputTokensKey } from '@/llm/request';
33
40
  import { initializeModel } from '@/llm/init';
@@ -206,10 +213,10 @@ function extractToolFailuresSection(messages: BaseMessage[]): string {
206
213
  }
207
214
 
208
215
  const toolName = toolMsg.name ?? 'tool';
209
- const content =
210
- typeof toolMsg.content === 'string'
211
- ? toolMsg.content
212
- : JSON.stringify(toolMsg.content);
216
+ const content = serializeToolContentBounded(
217
+ toolMsg.content,
218
+ MAX_TOOL_FAILURE_CHARS * 4
219
+ );
213
220
  const normalized = content.replace(/\s+/g, ' ').trim();
214
221
  const summary =
215
222
  normalized.length > MAX_TOOL_FAILURE_CHARS
@@ -250,25 +257,45 @@ function enrichSummary(summaryText: string, messages: BaseMessage[]): string {
250
257
  */
251
258
  function restoreOriginalToolContent(
252
259
  messages: BaseMessage[],
253
- originalToolContent: Map<number, string> | undefined
260
+ originalToolContent: Map<number, string> | undefined,
261
+ maxContextTokens?: number
254
262
  ): BaseMessage[] {
255
263
  if (originalToolContent == null || originalToolContent.size === 0) {
256
264
  return messages;
257
265
  }
258
- const restored = [...messages];
259
- for (const [idx, content] of originalToolContent) {
260
- const msg = restored[idx];
261
- if (msg instanceof ToolMessage) {
262
- restored[idx] = new ToolMessage({
263
- content,
264
- tool_call_id: msg.tool_call_id,
265
- name: msg.name,
266
- id: msg.id,
267
- additional_kwargs: msg.additional_kwargs,
268
- response_metadata: msg.response_metadata,
269
- });
266
+
267
+ const restorable: Array<{
268
+ index: number;
269
+ message: ToolMessage;
270
+ content: string;
271
+ }> = [];
272
+ for (const [index, content] of originalToolContent) {
273
+ const message = messages[index];
274
+ if (
275
+ message instanceof ToolMessage &&
276
+ !isComputerCallOutputMessage(message)
277
+ ) {
278
+ restorable.push({ index, message, content });
270
279
  }
271
280
  }
281
+ if (restorable.length === 0) {
282
+ return messages;
283
+ }
284
+
285
+ /**
286
+ * Restored originals improve checkpoint quality, but they still feed a
287
+ * provider call. Share one tool-result budget across every restoration so
288
+ * several previously masked results cannot overflow the summarizer.
289
+ */
290
+ let remainingChars = calculateMaxToolResultChars(maxContextTokens);
291
+ const restored = [...messages];
292
+ for (let i = 0; i < restorable.length; i++) {
293
+ const { index, message, content } = restorable[i];
294
+ const maxChars = Math.floor(remainingChars / (restorable.length - i));
295
+ const compacted = compactToolContent(content, maxChars).content;
296
+ restored[index] = cloneToolMessageWithContent(message, compacted);
297
+ remainingChars -= serializeToolContentBounded(compacted, maxChars).length;
298
+ }
272
299
  return restored;
273
300
  }
274
301
 
@@ -845,7 +872,8 @@ export function createSummarizeNode({
845
872
 
846
873
  const restoredMessages = restoreOriginalToolContent(
847
874
  state.messages,
848
- originalPending
875
+ originalPending,
876
+ agentContext.maxContextTokens
849
877
  );
850
878
 
851
879
  const runnableConfig = config ?? graph.config;