@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
@@ -38,6 +38,15 @@ import type {
38
38
  PostToolBatchEntry,
39
39
  } from '@/hooks';
40
40
  import type * as t from '@/types';
41
+ import {
42
+ cloneToolMessageWithContent,
43
+ compactToolContent,
44
+ hasComputerCallOutputMarker,
45
+ isComputerCallOutputContent,
46
+ isComputerCallOutputMessage,
47
+ serializeStructuredValueBounded,
48
+ serializeToolContentBounded,
49
+ } from '@/utils/toolContent';
41
50
  import {
42
51
  buildToolExecutionRequestPlan,
43
52
  resolveRuntimeSessionHint,
@@ -61,7 +70,7 @@ import {
61
70
  } from '@/tools/local';
62
71
  import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
63
72
  import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
64
- import { toLangChainContent } from '@/messages/langchain';
73
+ import { convertInjectedMessages } from '@/messages/injected';
65
74
  import { safeDispatchCustomEvent } from '@/utils/events';
66
75
  import { RunnableCallable } from '@/utils';
67
76
  import { executeHooks } from '@/hooks';
@@ -119,6 +128,40 @@ type RunToolBatchContext<T = unknown> = {
119
128
  runInput?: T;
120
129
  };
121
130
 
131
+ type BoundedToolOutput = {
132
+ content: string;
133
+ registryContent: string;
134
+ };
135
+
136
+ /**
137
+ * Produces the provider preview and, when requested, the exact registry prefix
138
+ * in one bounded traversal. String outputs already exist in memory when a tool
139
+ * returns them; structured outputs must never be fully JSON-materialized before
140
+ * either limit applies.
141
+ */
142
+ function serializeToolOutputWithinLimits(
143
+ output: unknown,
144
+ maxToolResultChars: number,
145
+ registryPrefixChars = 0
146
+ ): BoundedToolOutput {
147
+ if (typeof output === 'string') {
148
+ return {
149
+ content: truncateToolResultContent(output, maxToolResultChars),
150
+ registryContent: registryPrefixChars > 0 ? output : '',
151
+ };
152
+ }
153
+
154
+ const serialized = serializeStructuredValueBounded(
155
+ output,
156
+ maxToolResultChars,
157
+ registryPrefixChars
158
+ );
159
+ return {
160
+ content: serialized.content,
161
+ registryContent: serialized.prefix,
162
+ };
163
+ }
164
+
122
165
  const TOOL_NODE_RUN_NAME = 'tool_batch';
123
166
  const NANOID_URL_ALPHABET =
124
167
  '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -1107,6 +1150,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1107
1150
  }
1108
1151
  if (isBaseMessage(output) && output._getType() === 'tool') {
1109
1152
  const toolMsg = output as ToolMessage;
1153
+ if (isComputerCallOutputMessage(toolMsg)) {
1154
+ return toolMsg;
1155
+ }
1156
+ const originalContent = toolMsg.content;
1157
+ const compacted = compactToolContent(
1158
+ originalContent,
1159
+ this.maxToolResultChars
1160
+ );
1161
+ if (compacted.changed) {
1162
+ toolMsg.content = compacted.content;
1163
+ }
1110
1164
  const isError = toolMsg.status === 'error';
1111
1165
  if (isError) {
1112
1166
  /**
@@ -1125,14 +1179,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1125
1179
  return toolMsg;
1126
1180
  }
1127
1181
  if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) {
1128
- if (typeof toolMsg.content === 'string') {
1129
- const rawContent = toolMsg.content;
1182
+ if (typeof originalContent === 'string') {
1183
+ const rawContent = originalContent;
1130
1184
  const registryContent = stripCodeSessionFileSummary(rawContent);
1131
- const llmContent = truncateToolResultContent(
1132
- rawContent,
1133
- this.maxToolResultChars
1134
- );
1135
- toolMsg.content = llmContent;
1136
1185
  const refMeta = this.recordOutputReference(
1137
1186
  runId,
1138
1187
  registryContent,
@@ -1148,7 +1197,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1148
1197
  } else {
1149
1198
  /**
1150
1199
  * Non-string content (multi-part content blocks — text +
1151
- * image). Known limitation: we cannot register under a
1200
+ * image). It is now bounded for the LLM, but cannot register under a
1152
1201
  * reference key because there's no canonical serialized
1153
1202
  * form. Warn once per tool per run when the caller
1154
1203
  * intended to register. The unresolved-refs hint is still
@@ -1176,22 +1225,23 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1176
1225
  }
1177
1226
  return toolMsg;
1178
1227
  }
1179
- const rawContent =
1180
- typeof output === 'string' ? output : JSON.stringify(output);
1181
- const truncated = truncateToolResultContent(
1182
- rawContent,
1183
- this.maxToolResultChars
1228
+ const serialized = serializeToolOutputWithinLimits(
1229
+ output,
1230
+ this.maxToolResultChars,
1231
+ this.toolOutputRegistry != null && refKey != null
1232
+ ? this.toolOutputRegistry.perOutputLimit
1233
+ : 0
1184
1234
  );
1185
1235
  const refMeta = this.recordOutputReference(
1186
1236
  runId,
1187
- stripCodeSessionFileSummary(rawContent),
1237
+ stripCodeSessionFileSummary(serialized.registryContent),
1188
1238
  refKey,
1189
1239
  unresolvedRefs
1190
1240
  );
1191
1241
  return new ToolMessage({
1192
1242
  status: 'success',
1193
1243
  name: tool.name,
1194
- content: truncated,
1244
+ content: serialized.content,
1195
1245
  tool_call_id: call.id!,
1196
1246
  ...(refMeta != null && {
1197
1247
  additional_kwargs: refMeta as Record<string, unknown>,
@@ -1258,7 +1308,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1258
1308
  });
1259
1309
  }
1260
1310
  }
1261
- const errorContent = `Error: ${e.message}\n Please fix your mistakes.`;
1311
+ const errorContent = truncateToolResultContent(
1312
+ `Error: ${e.message}\n Please fix your mistakes.`,
1313
+ this.maxToolResultChars
1314
+ );
1262
1315
  const refMeta =
1263
1316
  unresolvedRefs.length > 0
1264
1317
  ? this.recordOutputReference(
@@ -1630,7 +1683,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1630
1683
  error:
1631
1684
  typeof output.content === 'string'
1632
1685
  ? output.content
1633
- : JSON.stringify(output.content),
1686
+ : serializeStructuredValueBounded(
1687
+ output.content,
1688
+ this.maxToolResultChars
1689
+ ).content,
1634
1690
  stepId,
1635
1691
  turn,
1636
1692
  },
@@ -1682,10 +1738,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1682
1738
  }
1683
1739
 
1684
1740
  if (postResult?.updatedOutput != null) {
1685
- const replaced =
1686
- typeof postResult.updatedOutput === 'string'
1687
- ? postResult.updatedOutput
1688
- : JSON.stringify(postResult.updatedOutput);
1741
+ if (hasComputerCallOutputMarker(output)) {
1742
+ if (!isComputerCallOutputContent(postResult.updatedOutput)) {
1743
+ throw new Error(
1744
+ 'PostToolUse updatedOutput for a computer call must be a valid screenshot URL or screenshot content block.'
1745
+ );
1746
+ }
1747
+ return cloneToolMessageWithContent(output, postResult.updatedOutput);
1748
+ }
1689
1749
  // Keep the tool-output registry in sync with what the model
1690
1750
  // actually sees. Without this, `runTool` already registered
1691
1751
  // the PRE-hook content under `_refKey`, and a later
@@ -1700,17 +1760,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1700
1760
  | undefined;
1701
1761
  const refKey = refMeta?._refKey;
1702
1762
  const refScope = refMeta?._refScope;
1763
+ const replaced = serializeToolOutputWithinLimits(
1764
+ postResult.updatedOutput,
1765
+ this.maxToolResultChars,
1766
+ this.toolOutputRegistry != null && refKey != null
1767
+ ? this.toolOutputRegistry.perOutputLimit
1768
+ : 0
1769
+ );
1703
1770
  if (this.toolOutputRegistry != null && refKey != null) {
1704
- this.toolOutputRegistry.set(refScope, refKey, replaced);
1771
+ this.toolOutputRegistry.set(
1772
+ refScope,
1773
+ refKey,
1774
+ replaced.registryContent
1775
+ );
1705
1776
  }
1706
- return new ToolMessage({
1707
- status: output.status,
1708
- name: output.name,
1709
- content: replaced,
1710
- artifact: output.artifact,
1711
- tool_call_id: output.tool_call_id,
1712
- additional_kwargs: output.additional_kwargs,
1713
- });
1777
+ return cloneToolMessageWithContent(output, replaced.content);
1714
1778
  }
1715
1779
  }
1716
1780
 
@@ -1993,10 +2057,20 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1993
2057
  continue;
1994
2058
  }
1995
2059
 
1996
- const contentString =
1997
- typeof toolMessage.content === 'string'
1998
- ? toolMessage.content
1999
- : JSON.stringify(toolMessage.content);
2060
+ let contentString: string;
2061
+ if (isComputerCallOutputMessage(toolMessage)) {
2062
+ contentString = truncateToolResultContent(
2063
+ '[Computer screenshot omitted from completion event]',
2064
+ this.maxToolResultChars
2065
+ );
2066
+ } else if (typeof toolMessage.content === 'string') {
2067
+ contentString = toolMessage.content;
2068
+ } else {
2069
+ contentString = serializeStructuredValueBounded(
2070
+ toolMessage.content,
2071
+ this.maxToolResultChars
2072
+ ).content;
2073
+ }
2000
2074
 
2001
2075
  /**
2002
2076
  * Prefer the post-substitution args when a `{{…}}` placeholder
@@ -2006,10 +2080,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2006
2080
  */
2007
2081
  const effectiveArgs = resolvedArgsByCallId?.get(toolCallId) ?? call.args;
2008
2082
  const tool_call: t.ProcessedToolCall = {
2009
- args:
2010
- typeof effectiveArgs === 'string'
2011
- ? (effectiveArgs as string)
2012
- : JSON.stringify((effectiveArgs as unknown) ?? {}),
2083
+ args: serializeToolContentBounded(
2084
+ (effectiveArgs as unknown) ?? {},
2085
+ this.maxToolResultChars
2086
+ ),
2013
2087
  name: call.name,
2014
2088
  id: toolCallId,
2015
2089
  output: contentString,
@@ -2860,7 +2934,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2860
2934
  if (result.injectedMessages && result.injectedMessages.length > 0) {
2861
2935
  try {
2862
2936
  injected.push(
2863
- ...this.convertInjectedMessages(result.injectedMessages)
2937
+ ...convertInjectedMessages(result.injectedMessages)
2864
2938
  );
2865
2939
  } catch (e) {
2866
2940
  // eslint-disable-next-line no-console
@@ -2886,7 +2960,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2886
2960
  let finalToolOutput: unknown = result.content;
2887
2961
 
2888
2962
  if (result.status === 'error') {
2889
- contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
2963
+ contentString = truncateToolResultContent(
2964
+ `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`,
2965
+ this.maxToolResultChars
2966
+ );
2890
2967
  /**
2891
2968
  * Error results bypass registration but stamp the
2892
2969
  * unresolved-refs hint into `additional_kwargs` so the lazy
@@ -2949,14 +3026,22 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2949
3026
  }
2950
3027
  }
2951
3028
  } else {
2952
- let registryRaw =
2953
- typeof result.content === 'string'
2954
- ? result.content
2955
- : JSON.stringify(result.content);
2956
- contentString = truncateToolResultContent(
2957
- registryRaw,
2958
- this.maxToolResultChars
3029
+ const batchIndex = batchIndexByCallId.get(result.toolCallId);
3030
+ const refKey =
3031
+ this.toolOutputRegistry != null &&
3032
+ batchIndex != null &&
3033
+ turn != null
3034
+ ? buildReferenceKey(batchIndex, turn)
3035
+ : undefined;
3036
+ let serialized = serializeToolOutputWithinLimits(
3037
+ result.content,
3038
+ this.maxToolResultChars,
3039
+ this.toolOutputRegistry != null && refKey != null
3040
+ ? this.toolOutputRegistry.perOutputLimit
3041
+ : 0
2959
3042
  );
3043
+ let registryRaw = serialized.registryContent;
3044
+ contentString = serialized.content;
2960
3045
 
2961
3046
  if (hasPostHook) {
2962
3047
  const hookResult = await executeHooks({
@@ -2983,15 +3068,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2983
3068
  }
2984
3069
  }
2985
3070
  if (hookResult?.updatedOutput != null) {
2986
- const replaced =
2987
- typeof hookResult.updatedOutput === 'string'
2988
- ? hookResult.updatedOutput
2989
- : JSON.stringify(hookResult.updatedOutput);
2990
- registryRaw = replaced;
2991
- contentString = truncateToolResultContent(
2992
- replaced,
2993
- this.maxToolResultChars
3071
+ serialized = serializeToolOutputWithinLimits(
3072
+ hookResult.updatedOutput,
3073
+ this.maxToolResultChars,
3074
+ this.toolOutputRegistry != null && refKey != null
3075
+ ? this.toolOutputRegistry.perOutputLimit
3076
+ : 0
2994
3077
  );
3078
+ registryRaw = serialized.registryContent;
3079
+ contentString = serialized.content;
2995
3080
  finalToolOutput = hookResult.updatedOutput;
2996
3081
  /**
2997
3082
  * The hook ACTUALLY rewrote this output: any completion the
@@ -3005,14 +3090,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3005
3090
  }
3006
3091
  }
3007
3092
 
3008
- const batchIndex = batchIndexByCallId.get(result.toolCallId);
3009
3093
  const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
3010
- const refKey =
3011
- this.toolOutputRegistry != null &&
3012
- batchIndex != null &&
3013
- turn != null
3014
- ? buildReferenceKey(batchIndex, turn)
3015
- : undefined;
3016
3094
  const successRefMeta = this.recordOutputReference(
3017
3095
  registryRunId,
3018
3096
  stripCodeSessionFileSummary(registryRaw),
@@ -3266,7 +3344,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3266
3344
  */
3267
3345
  if (hookInjectedMessages.length > 0) {
3268
3346
  try {
3269
- injected.push(...this.convertInjectedMessages(hookInjectedMessages));
3347
+ injected.push(...convertInjectedMessages(hookInjectedMessages));
3270
3348
  } catch (e) {
3271
3349
  // eslint-disable-next-line no-console
3272
3350
  console.warn(
@@ -3303,7 +3381,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3303
3381
  index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
3304
3382
  type: 'tool_call' as const,
3305
3383
  tool_call: {
3306
- args: JSON.stringify(args),
3384
+ args: serializeToolContentBounded(args, this.maxToolResultChars),
3307
3385
  name: toolName,
3308
3386
  id: toolCallId,
3309
3387
  output,
@@ -3330,13 +3408,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3330
3408
  ): Promise<boolean> {
3331
3409
  const output =
3332
3410
  result.status === 'error'
3333
- ? `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`
3334
- : truncateToolResultContent(
3335
- typeof result.content === 'string'
3336
- ? result.content
3337
- : JSON.stringify(result.content),
3411
+ ? truncateToolResultContent(
3412
+ `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`,
3338
3413
  this.maxToolResultChars
3339
- );
3414
+ )
3415
+ : serializeToolOutputWithinLimits(
3416
+ result.content,
3417
+ this.maxToolResultChars
3418
+ ).content;
3340
3419
  return this.dispatchStepCompleted(
3341
3420
  result.toolCallId,
3342
3421
  request.name,
@@ -3347,34 +3426,6 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3347
3426
  );
3348
3427
  }
3349
3428
 
3350
- /**
3351
- * Converts InjectedMessage instances to LangChain HumanMessage objects.
3352
- * Both 'user' and 'system' roles become HumanMessage to avoid provider
3353
- * rejections (Anthropic/Google reject non-leading SystemMessages).
3354
- * The original role is preserved in additional_kwargs for downstream consumers.
3355
- */
3356
- private convertInjectedMessages(
3357
- messages: t.InjectedMessage[]
3358
- ): BaseMessage[] {
3359
- const converted: BaseMessage[] = [];
3360
- for (const msg of messages) {
3361
- const additional_kwargs: Record<string, unknown> = {
3362
- role: msg.role,
3363
- };
3364
- if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;
3365
- if (msg.source != null) additional_kwargs.source = msg.source;
3366
- if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
3367
-
3368
- converted.push(
3369
- new HumanMessage({
3370
- content: toLangChainContent(msg.content),
3371
- additional_kwargs,
3372
- })
3373
- );
3374
- }
3375
- return converted;
3376
- }
3377
-
3378
3429
  /**
3379
3430
  * Execute a group of direct (in-process) tool calls with interrupt-safe
3380
3431
  * ordering, returning outputs aligned 1:1 with `directCalls`.
@@ -29,7 +29,7 @@ function createAIMessageWithToolCalls(
29
29
  type CompletionEvent = {
30
30
  result: {
31
31
  id: string;
32
- tool_call: { id: string; output: string };
32
+ tool_call: { id: string; output: string; args?: string };
33
33
  };
34
34
  };
35
35
 
@@ -124,6 +124,153 @@ describe('ToolNode per-call onResult completion emission', () => {
124
124
  expect(result.messages.map((m) => m.content)).toEqual(['sunny', '42']);
125
125
  });
126
126
 
127
+ it('serializes bigint output before early and batch completion paths', async () => {
128
+ const completions: CompletionEvent[] = [];
129
+ const structuredOutput = [{ rowsRead: BigInt(42), status: 'complete' }];
130
+
131
+ jest
132
+ .spyOn(events, 'safeDispatchCustomEvent')
133
+ .mockImplementation(async (event, data): Promise<void> => {
134
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
135
+ completions.push(data as CompletionEvent);
136
+ return;
137
+ }
138
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
139
+ return;
140
+ }
141
+ const batch = data as t.ToolExecuteBatchRequest;
142
+ batch.onResult?.({
143
+ toolCallId: 'call_query',
144
+ status: 'success',
145
+ content: structuredOutput,
146
+ });
147
+ await flushAsync();
148
+ batch.resolve([
149
+ {
150
+ toolCallId: 'call_query',
151
+ status: 'success',
152
+ content: structuredOutput,
153
+ },
154
+ ]);
155
+ });
156
+
157
+ const toolNode = new ToolNode({
158
+ tools: [createDummyTool('query')],
159
+ eventDrivenMode: true,
160
+ toolCallStepIds: new Map([['call_query', 'step_query']]),
161
+ });
162
+ const result = (await toolNode.invoke({
163
+ messages: [
164
+ createAIMessageWithToolCalls([
165
+ { id: 'call_query', name: 'query', args: {} },
166
+ ]),
167
+ ],
168
+ })) as { messages: ToolMessage[] };
169
+
170
+ const serialized = '[{"rowsRead":"42","status":"complete"}]';
171
+ expect(completions).toHaveLength(1);
172
+ expect(completions[0].result.tool_call.output).toBe(serialized);
173
+ expect(result.messages[0].content).toBe(serialized);
174
+ });
175
+
176
+ it('omits native computer screenshots from completion events', async () => {
177
+ const completions: CompletionEvent[] = [];
178
+ const screenshot = `data:image/png;base64,${'A'.repeat(2_000)}`;
179
+ const computerOutput = new ToolMessage({
180
+ content: screenshot,
181
+ tool_call_id: 'call_computer',
182
+ additional_kwargs: { type: 'computer_call_output' },
183
+ });
184
+ const computer = createDummyTool('computer_use');
185
+ (
186
+ computer as unknown as {
187
+ invoke: () => Promise<ToolMessage>;
188
+ }
189
+ ).invoke = async () => computerOutput;
190
+ jest
191
+ .spyOn(events, 'safeDispatchCustomEvent')
192
+ .mockImplementation(async (event, data): Promise<void> => {
193
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
194
+ completions.push(data as CompletionEvent);
195
+ }
196
+ });
197
+ const toolNode = new ToolNode({
198
+ tools: [computer],
199
+ eventDrivenMode: true,
200
+ directToolNames: new Set(['computer_use']),
201
+ toolCallStepIds: new Map([['call_computer', 'step_computer']]),
202
+ maxToolResultChars: 80,
203
+ });
204
+
205
+ const result = (await toolNode.invoke({
206
+ messages: [
207
+ createAIMessageWithToolCalls([
208
+ { id: 'call_computer', name: 'computer_use', args: {} },
209
+ ]),
210
+ ],
211
+ })) as { messages: ToolMessage[] };
212
+
213
+ expect(result.messages[0]).toBe(computerOutput);
214
+ expect(result.messages[0].content).toBe(screenshot);
215
+ expect(completions).toHaveLength(1);
216
+ expect(completions[0].result.tool_call.output).toContain(
217
+ 'Computer screenshot omitted'
218
+ );
219
+ expect(completions[0].result.tool_call.output.length).toBeLessThanOrEqual(
220
+ 80
221
+ );
222
+ });
223
+
224
+ it('bounds cyclic tool args without losing the completion event', async () => {
225
+ const completions: CompletionEvent[] = [];
226
+ const cyclicArgs: Record<string, unknown> = { city: 'NYC' };
227
+ cyclicArgs.self = cyclicArgs;
228
+
229
+ jest
230
+ .spyOn(events, 'safeDispatchCustomEvent')
231
+ .mockImplementation(async (event, data): Promise<void> => {
232
+ if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
233
+ completions.push(data as CompletionEvent);
234
+ return;
235
+ }
236
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
237
+ return;
238
+ }
239
+ const batch = data as t.ToolExecuteBatchRequest;
240
+ batch.onResult?.({
241
+ toolCallId: 'call_weather',
242
+ status: 'success',
243
+ content: 'sunny',
244
+ });
245
+ await flushAsync();
246
+ batch.resolve([
247
+ {
248
+ toolCallId: 'call_weather',
249
+ status: 'success',
250
+ content: 'sunny',
251
+ },
252
+ ]);
253
+ });
254
+
255
+ const toolNode = new ToolNode({
256
+ tools: [createDummyTool('weather')],
257
+ eventDrivenMode: true,
258
+ toolCallStepIds: new Map([['call_weather', 'step_weather']]),
259
+ });
260
+
261
+ await toolNode.invoke({
262
+ messages: [
263
+ createAIMessageWithToolCalls([
264
+ { id: 'call_weather', name: 'weather', args: cyclicArgs },
265
+ ]),
266
+ ],
267
+ });
268
+
269
+ expect(completions).toHaveLength(1);
270
+ expect(completions[0].result.tool_call.args).toContain('[Circular]');
271
+ expect(completions[0].result.tool_call.output).toBe('sunny');
272
+ });
273
+
127
274
  it('ignores duplicate and unknown onResult reports', async () => {
128
275
  const completions: CompletionEvent[] = [];
129
276