@librechat/agents 3.3.2 → 3.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +6 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/invoke.cjs +64 -3
  16. package/dist/cjs/llm/invoke.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +41 -5
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +19 -2
  24. package/dist/cjs/messages/cache.cjs +54 -33
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/contextPruning.cjs +17 -43
  27. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  28. package/dist/cjs/messages/core.cjs +315 -23
  29. package/dist/cjs/messages/core.cjs.map +1 -1
  30. package/dist/cjs/messages/format.cjs +218 -60
  31. package/dist/cjs/messages/format.cjs.map +1 -1
  32. package/dist/cjs/messages/index.cjs +1 -1
  33. package/dist/cjs/messages/prune.cjs +503 -127
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +7 -3
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/summarization/node.cjs +28 -14
  38. package/dist/cjs/summarization/node.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +54 -41
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  42. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  43. package/dist/cjs/utils/index.cjs +1 -1
  44. package/dist/cjs/utils/llm.cjs +1 -1
  45. package/dist/cjs/utils/llm.cjs.map +1 -1
  46. package/dist/cjs/utils/tokens.cjs +307 -20
  47. package/dist/cjs/utils/tokens.cjs.map +1 -1
  48. package/dist/cjs/utils/toolContent.cjs +1514 -0
  49. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  50. package/dist/esm/agents/AgentContext.mjs +2 -1
  51. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  52. package/dist/esm/graphs/Graph.mjs +349 -116
  53. package/dist/esm/graphs/Graph.mjs.map +1 -1
  54. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  56. package/dist/esm/langfuse.mjs +6 -0
  57. package/dist/esm/langfuse.mjs.map +1 -1
  58. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  59. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  60. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  61. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  62. package/dist/esm/llm/google/utils/common.mjs +19 -7
  63. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  64. package/dist/esm/llm/invoke.mjs +65 -5
  65. package/dist/esm/llm/invoke.mjs.map +1 -1
  66. package/dist/esm/llm/openai/index.mjs +41 -5
  67. package/dist/esm/llm/openai/index.mjs.map +1 -1
  68. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  69. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  70. package/dist/esm/llm/openrouter/index.mjs +4 -1
  71. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  72. package/dist/esm/main.mjs +7 -7
  73. package/dist/esm/messages/cache.mjs +55 -34
  74. package/dist/esm/messages/cache.mjs.map +1 -1
  75. package/dist/esm/messages/contextPruning.mjs +17 -43
  76. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  77. package/dist/esm/messages/core.mjs +305 -24
  78. package/dist/esm/messages/core.mjs.map +1 -1
  79. package/dist/esm/messages/format.mjs +218 -61
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/index.mjs +1 -1
  82. package/dist/esm/messages/prune.mjs +500 -128
  83. package/dist/esm/messages/prune.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +7 -3
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +28 -14
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +54 -41
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  91. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  92. package/dist/esm/utils/index.mjs +1 -1
  93. package/dist/esm/utils/llm.mjs +1 -1
  94. package/dist/esm/utils/llm.mjs.map +1 -1
  95. package/dist/esm/utils/tokens.mjs +307 -21
  96. package/dist/esm/utils/tokens.mjs.map +1 -1
  97. package/dist/esm/utils/toolContent.mjs +1503 -0
  98. package/dist/esm/utils/toolContent.mjs.map +1 -0
  99. package/dist/types/graphs/Graph.d.ts +1 -1
  100. package/dist/types/llm/invoke.d.ts +27 -1
  101. package/dist/types/llm/openai/index.d.ts +4 -0
  102. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  103. package/dist/types/llm/openrouter/index.d.ts +5 -0
  104. package/dist/types/messages/contextPruning.d.ts +2 -1
  105. package/dist/types/messages/core.d.ts +36 -0
  106. package/dist/types/messages/format.d.ts +6 -0
  107. package/dist/types/messages/prune.d.ts +16 -12
  108. package/dist/types/types/stream.d.ts +1 -1
  109. package/dist/types/utils/tokens.d.ts +7 -0
  110. package/dist/types/utils/toolContent.d.ts +107 -0
  111. package/package.json +13 -9
  112. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  113. package/src/agents/AgentContext.ts +1 -0
  114. package/src/graphs/Graph.ts +698 -200
  115. package/src/graphs/MultiAgentGraph.ts +6 -1
  116. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  117. package/src/langfuse.ts +15 -0
  118. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  119. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  120. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  121. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  122. package/src/llm/google/utils/common.test.ts +75 -9
  123. package/src/llm/google/utils/common.ts +34 -3
  124. package/src/llm/invoke.test.ts +488 -0
  125. package/src/llm/invoke.ts +196 -2
  126. package/src/llm/openai/index.ts +106 -6
  127. package/src/llm/openai/utils/index.ts +109 -53
  128. package/src/llm/openai/utils/messages.test.ts +330 -1
  129. package/src/llm/openrouter/index.ts +12 -2
  130. package/src/llm/openrouter/reasoning.test.ts +314 -0
  131. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  132. package/src/messages/cache.tail.test.ts +193 -0
  133. package/src/messages/cache.test.ts +113 -0
  134. package/src/messages/cache.ts +92 -60
  135. package/src/messages/contextPruning.test.ts +184 -0
  136. package/src/messages/contextPruning.ts +49 -42
  137. package/src/messages/core.ts +653 -40
  138. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  139. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  140. package/src/messages/format.ts +363 -91
  141. package/src/messages/formatAgentMessages.test.ts +604 -14
  142. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  143. package/src/messages/prune.ts +996 -183
  144. package/src/specs/langfuse-callbacks.test.ts +203 -1
  145. package/src/specs/prune.test.ts +1083 -6
  146. package/src/specs/summarization-unit.test.ts +105 -0
  147. package/src/specs/tokens.test.ts +609 -32
  148. package/src/stream.ts +23 -15
  149. package/src/summarization/__tests__/node.test.ts +77 -0
  150. package/src/summarization/node.ts +46 -18
  151. package/src/tools/ToolNode.ts +147 -68
  152. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  153. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  154. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  155. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  156. package/src/tools/toolOutputReferences.ts +6 -0
  157. package/src/types/stream.ts +1 -1
  158. package/src/utils/llm.test.ts +18 -0
  159. package/src/utils/llm.ts +4 -1
  160. package/src/utils/tokens.ts +586 -31
  161. package/src/utils/toolContent.test.ts +1432 -0
  162. package/src/utils/toolContent.ts +2707 -0
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,
@@ -2248,11 +2260,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2248
2260
  const contentIndex =
2249
2261
  toolCallIndices[toolCallIndex] ?? runStep.index;
2250
2262
  const toolCallId = toolCall.id ?? '';
2251
- registerToolContentIndex(
2252
- toolStepContent,
2253
- contentIndex,
2254
- toolCallId
2255
- );
2263
+ registerToolContentIndex(toolStepContent, contentIndex, toolCallId);
2256
2264
  const contentPart: t.MessageContentComplex = {
2257
2265
  type: ContentTypes.TOOL_CALL,
2258
2266
  tool_call: {
@@ -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;
@@ -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,
@@ -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,
@@ -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),
@@ -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,