@librechat/agents 3.3.3 → 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 (156) 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/llm/anthropic/utils/message_inputs.cjs +79 -6
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  10. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +64 -3
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +41 -5
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  18. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  20. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  21. package/dist/cjs/main.cjs +19 -2
  22. package/dist/cjs/messages/cache.cjs +54 -33
  23. package/dist/cjs/messages/cache.cjs.map +1 -1
  24. package/dist/cjs/messages/contextPruning.cjs +17 -43
  25. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  26. package/dist/cjs/messages/core.cjs +315 -23
  27. package/dist/cjs/messages/core.cjs.map +1 -1
  28. package/dist/cjs/messages/format.cjs +218 -60
  29. package/dist/cjs/messages/format.cjs.map +1 -1
  30. package/dist/cjs/messages/index.cjs +1 -1
  31. package/dist/cjs/messages/prune.cjs +503 -127
  32. package/dist/cjs/messages/prune.cjs.map +1 -1
  33. package/dist/cjs/stream.cjs +7 -3
  34. package/dist/cjs/stream.cjs.map +1 -1
  35. package/dist/cjs/summarization/node.cjs +28 -14
  36. package/dist/cjs/summarization/node.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +54 -41
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  40. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  41. package/dist/cjs/utils/index.cjs +1 -1
  42. package/dist/cjs/utils/llm.cjs +1 -1
  43. package/dist/cjs/utils/llm.cjs.map +1 -1
  44. package/dist/cjs/utils/tokens.cjs +307 -20
  45. package/dist/cjs/utils/tokens.cjs.map +1 -1
  46. package/dist/cjs/utils/toolContent.cjs +1514 -0
  47. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  48. package/dist/esm/agents/AgentContext.mjs +2 -1
  49. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  50. package/dist/esm/graphs/Graph.mjs +349 -116
  51. package/dist/esm/graphs/Graph.mjs.map +1 -1
  52. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  54. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  55. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  57. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  58. package/dist/esm/llm/google/utils/common.mjs +19 -7
  59. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  60. package/dist/esm/llm/invoke.mjs +65 -5
  61. package/dist/esm/llm/invoke.mjs.map +1 -1
  62. package/dist/esm/llm/openai/index.mjs +41 -5
  63. package/dist/esm/llm/openai/index.mjs.map +1 -1
  64. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  65. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  66. package/dist/esm/llm/openrouter/index.mjs +4 -1
  67. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  68. package/dist/esm/main.mjs +7 -7
  69. package/dist/esm/messages/cache.mjs +55 -34
  70. package/dist/esm/messages/cache.mjs.map +1 -1
  71. package/dist/esm/messages/contextPruning.mjs +17 -43
  72. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  73. package/dist/esm/messages/core.mjs +305 -24
  74. package/dist/esm/messages/core.mjs.map +1 -1
  75. package/dist/esm/messages/format.mjs +218 -61
  76. package/dist/esm/messages/format.mjs.map +1 -1
  77. package/dist/esm/messages/index.mjs +1 -1
  78. package/dist/esm/messages/prune.mjs +500 -128
  79. package/dist/esm/messages/prune.mjs.map +1 -1
  80. package/dist/esm/stream.mjs +7 -3
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/summarization/node.mjs +28 -14
  83. package/dist/esm/summarization/node.mjs.map +1 -1
  84. package/dist/esm/tools/ToolNode.mjs +54 -41
  85. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  86. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  87. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  88. package/dist/esm/utils/index.mjs +1 -1
  89. package/dist/esm/utils/llm.mjs +1 -1
  90. package/dist/esm/utils/llm.mjs.map +1 -1
  91. package/dist/esm/utils/tokens.mjs +307 -21
  92. package/dist/esm/utils/tokens.mjs.map +1 -1
  93. package/dist/esm/utils/toolContent.mjs +1503 -0
  94. package/dist/esm/utils/toolContent.mjs.map +1 -0
  95. package/dist/types/graphs/Graph.d.ts +1 -1
  96. package/dist/types/llm/invoke.d.ts +27 -1
  97. package/dist/types/llm/openai/index.d.ts +4 -0
  98. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  99. package/dist/types/llm/openrouter/index.d.ts +5 -0
  100. package/dist/types/messages/contextPruning.d.ts +2 -1
  101. package/dist/types/messages/core.d.ts +36 -0
  102. package/dist/types/messages/format.d.ts +6 -0
  103. package/dist/types/messages/prune.d.ts +16 -12
  104. package/dist/types/types/stream.d.ts +1 -1
  105. package/dist/types/utils/tokens.d.ts +7 -0
  106. package/dist/types/utils/toolContent.d.ts +107 -0
  107. package/package.json +1 -1
  108. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  109. package/src/agents/AgentContext.ts +1 -0
  110. package/src/graphs/Graph.ts +698 -200
  111. package/src/graphs/MultiAgentGraph.ts +6 -1
  112. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  113. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  114. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  115. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  116. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  117. package/src/llm/google/utils/common.test.ts +75 -9
  118. package/src/llm/google/utils/common.ts +34 -3
  119. package/src/llm/invoke.test.ts +488 -0
  120. package/src/llm/invoke.ts +196 -2
  121. package/src/llm/openai/index.ts +106 -6
  122. package/src/llm/openai/utils/index.ts +109 -53
  123. package/src/llm/openai/utils/messages.test.ts +330 -1
  124. package/src/llm/openrouter/index.ts +12 -2
  125. package/src/llm/openrouter/reasoning.test.ts +314 -0
  126. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  127. package/src/messages/cache.tail.test.ts +193 -0
  128. package/src/messages/cache.test.ts +113 -0
  129. package/src/messages/cache.ts +92 -60
  130. package/src/messages/contextPruning.test.ts +184 -0
  131. package/src/messages/contextPruning.ts +49 -42
  132. package/src/messages/core.ts +653 -40
  133. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  134. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  135. package/src/messages/format.ts +363 -91
  136. package/src/messages/formatAgentMessages.test.ts +604 -14
  137. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  138. package/src/messages/prune.ts +996 -183
  139. package/src/specs/prune.test.ts +1083 -6
  140. package/src/specs/summarization-unit.test.ts +105 -0
  141. package/src/specs/tokens.test.ts +609 -32
  142. package/src/stream.ts +23 -15
  143. package/src/summarization/__tests__/node.test.ts +77 -0
  144. package/src/summarization/node.ts +46 -18
  145. package/src/tools/ToolNode.ts +147 -68
  146. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  147. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  148. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  149. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  150. package/src/tools/toolOutputReferences.ts +6 -0
  151. package/src/types/stream.ts +1 -1
  152. package/src/utils/llm.test.ts +18 -0
  153. package/src/utils/llm.ts +4 -1
  154. package/src/utils/tokens.ts +586 -31
  155. package/src/utils/toolContent.test.ts +1432 -0
  156. package/src/utils/toolContent.ts +2707 -0
@@ -1,9 +1,11 @@
1
+ import { isProxy } from 'node:util/types';
1
2
  import {
2
3
  AIMessage,
3
4
  BaseMessage,
4
5
  ToolMessage,
5
6
  UsageMetadata,
6
7
  } from '@langchain/core/messages';
8
+ import type { AIMessageChunk } from '@langchain/core/messages';
7
9
  import type {
8
10
  ThinkingContentText,
9
11
  MessageContentComplex,
@@ -12,11 +14,20 @@ import type {
12
14
  import type { ContextPruningConfig } from '@/types/graph';
13
15
  import type { TokenCounter } from '@/types/run';
14
16
  import {
17
+ cloneToolMessageWithContent,
18
+ compactToolContent,
19
+ getToolContentCharLength,
20
+ isComputerCallOutputMessage,
21
+ serializeStructuredValueBounded,
22
+ serializeToolContentBounded,
23
+ } from '@/utils/toolContent';
24
+ import {
25
+ HARD_MAX_TOOL_RESULT_CHARS,
15
26
  calculateMaxToolResultChars,
16
27
  truncateToolResultContent,
17
- truncateToolInput,
18
28
  } from '@/utils/truncation';
19
29
  import { resolveContextPruningSettings } from './contextPruningSettings';
30
+ import { hasUnsafeStructuredSerialization } from '@/utils/tokens';
20
31
  import { ContentTypes, Providers, Constants } from '@/common';
21
32
  import { applyContextPruning } from './contextPruning';
22
33
  import { toLangChainContent } from './langchain';
@@ -35,6 +46,9 @@ function sumTokenCounts(
35
46
  /** Default fraction of the token budget reserved as headroom (5 %). */
36
47
  export const DEFAULT_RESERVE_RATIO = 0.05;
37
48
 
49
+ /** Provider framing reserved for the assistant reply label. */
50
+ export const REPLY_PRIMER_TOKENS = 3;
51
+
38
52
  /** Context pressure at which observation masking and context fading activate. */
39
53
  const PRESSURE_THRESHOLD_MASKING = 0.8;
40
54
 
@@ -96,6 +110,8 @@ export function clampCalibrationRatio(ratio: number): number {
96
110
  export type PruneMessagesFactoryParams = {
97
111
  provider?: Providers;
98
112
  maxTokens: number;
113
+ /** Per-tool-result character cap applied while reconciling cached counts. */
114
+ maxToolResultChars?: number;
99
115
  startIndex: number;
100
116
  tokenCounter: TokenCounter;
101
117
  indexTokenCountMap: Record<string, number | undefined>;
@@ -161,7 +177,8 @@ export type PruneMessagesParams = {
161
177
  };
162
178
 
163
179
  function getToolCallIds(message: BaseMessage): Set<string> {
164
- if (message.getType() !== 'ai') {
180
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
181
+ if (message.getType() !== 'ai' && messageRole !== 'assistant') {
165
182
  return new Set<string>();
166
183
  }
167
184
 
@@ -189,9 +206,71 @@ function getToolCallIds(message: BaseMessage): Set<string> {
189
206
  }
190
207
  }
191
208
 
209
+ const toolOutputSource = getResponsesToolOutputSource(message);
210
+ if (toolOutputSource != null) {
211
+ for (const item of toolOutputSource.items) {
212
+ if (item == null || typeof item !== 'object') {
213
+ continue;
214
+ }
215
+ const record = item as {
216
+ type?: unknown;
217
+ call_id?: unknown;
218
+ };
219
+ if (
220
+ (record.type === 'function_call' ||
221
+ record.type === 'custom_tool_call' ||
222
+ record.type === 'computer_call') &&
223
+ typeof record.call_id === 'string' &&
224
+ record.call_id !== ''
225
+ ) {
226
+ ids.add(record.call_id);
227
+ }
228
+ }
229
+ }
230
+
192
231
  return ids;
193
232
  }
194
233
 
234
+ type ResponsesToolOutputSource = {
235
+ source: 'response_metadata' | 'tool_outputs';
236
+ items: unknown[];
237
+ };
238
+
239
+ function getResponsesToolOutputSource(
240
+ message: BaseMessage
241
+ ): ResponsesToolOutputSource | undefined {
242
+ const responseOutput = readPropertyWithoutAccessors(
243
+ message.response_metadata,
244
+ 'output'
245
+ );
246
+ if (
247
+ !responseOutput.accessor &&
248
+ Array.isArray(responseOutput.value) &&
249
+ !isProxy(responseOutput.value) &&
250
+ responseOutput.value.length > 0
251
+ ) {
252
+ return {
253
+ source: 'response_metadata',
254
+ items: responseOutput.value,
255
+ };
256
+ }
257
+ const fallbackOutput = readPropertyWithoutAccessors(
258
+ message.additional_kwargs,
259
+ 'tool_outputs'
260
+ );
261
+ if (
262
+ !fallbackOutput.accessor &&
263
+ Array.isArray(fallbackOutput.value) &&
264
+ !isProxy(fallbackOutput.value)
265
+ ) {
266
+ return {
267
+ source: 'tool_outputs',
268
+ items: fallbackOutput.value,
269
+ };
270
+ }
271
+ return undefined;
272
+ }
273
+
195
274
  function getToolResultId(message: BaseMessage): string | null {
196
275
  if (message.getType() !== 'tool') {
197
276
  return null;
@@ -292,7 +371,8 @@ export function repairOrphanedToolMessages({
292
371
  continue;
293
372
  }
294
373
 
295
- if (message.getType() === 'ai' && message instanceof AIMessage) {
374
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
375
+ if (message.getType() === 'ai' || messageRole === 'assistant') {
296
376
  const toolCallIds = getToolCallIds(message);
297
377
  if (toolCallIds.size > 0) {
298
378
  let hasOrphanToolCalls = false;
@@ -344,16 +424,17 @@ export function repairOrphanedToolMessages({
344
424
  * Returns null if the message has no content left after stripping.
345
425
  */
346
426
  function stripOrphanToolUseBlocks(
347
- message: AIMessage,
427
+ message: BaseMessage,
348
428
  presentToolResultIds: Set<string>
349
- ): AIMessage | null {
350
- const keptToolCalls = (message.tool_calls ?? []).filter(
429
+ ): BaseMessage | null {
430
+ const aiMessage = message as AIMessage;
431
+ const keptToolCalls = (aiMessage.tool_calls ?? []).filter(
351
432
  (tc) => typeof tc.id === 'string' && presentToolResultIds.has(tc.id)
352
433
  );
353
434
 
354
435
  let keptContent: MessageContentComplex[] | string;
355
- if (Array.isArray(message.content)) {
356
- const filtered = (message.content as MessageContentComplex[]).filter(
436
+ if (Array.isArray(aiMessage.content)) {
437
+ const filtered = (aiMessage.content as MessageContentComplex[]).filter(
357
438
  (block) => {
358
439
  if (typeof block !== 'object') {
359
440
  return true;
@@ -369,18 +450,60 @@ function stripOrphanToolUseBlocks(
369
450
  }
370
451
  );
371
452
 
372
- if (filtered.length === 0) {
373
- return null;
374
- }
375
453
  keptContent = filtered;
376
454
  } else {
377
- keptContent = message.content;
455
+ keptContent = aiMessage.content;
378
456
  }
379
457
 
380
- return new AIMessage({
381
- ...message,
458
+ const toolOutputSource = getResponsesToolOutputSource(message);
459
+ const keptToolOutputs =
460
+ toolOutputSource?.items.filter((item) => {
461
+ if (item == null || typeof item !== 'object') {
462
+ return true;
463
+ }
464
+ const record = item as {
465
+ type?: unknown;
466
+ call_id?: unknown;
467
+ };
468
+ if (
469
+ record.type !== 'function_call' &&
470
+ record.type !== 'custom_tool_call' &&
471
+ record.type !== 'computer_call'
472
+ ) {
473
+ return true;
474
+ }
475
+ return (
476
+ typeof record.call_id === 'string' &&
477
+ presentToolResultIds.has(record.call_id)
478
+ );
479
+ }) ?? [];
480
+
481
+ if (
482
+ keptToolCalls.length === 0 &&
483
+ Array.isArray(keptContent) &&
484
+ keptContent.length === 0 &&
485
+ keptToolOutputs.length === 0
486
+ ) {
487
+ return null;
488
+ }
489
+
490
+ let responseMetadata = message.response_metadata;
491
+ let additionalKwargs = message.additional_kwargs;
492
+ if (toolOutputSource?.source === 'response_metadata') {
493
+ responseMetadata = cloneWithProjectedProperties(responseMetadata, {
494
+ output: keptToolOutputs,
495
+ });
496
+ } else if (toolOutputSource?.source === 'tool_outputs') {
497
+ additionalKwargs = cloneWithProjectedProperties(additionalKwargs, {
498
+ tool_outputs: keptToolOutputs,
499
+ });
500
+ }
501
+
502
+ return cloneWithProjectedProperties(message, {
382
503
  content: toLangChainContent(keptContent),
383
504
  tool_calls: keptToolCalls.length > 0 ? keptToolCalls : undefined,
505
+ response_metadata: responseMetadata,
506
+ additional_kwargs: additionalKwargs,
384
507
  });
385
508
  }
386
509
 
@@ -400,13 +523,19 @@ function stripOrphanToolUseBlocks(
400
523
  * vice-versa, the original array is returned immediately with zero allocation.
401
524
  */
402
525
  export function sanitizeOrphanToolBlocks(
403
- messages: BaseMessage[]
526
+ messages: BaseMessage[],
527
+ onMessageCloned?: (source: BaseMessage, clone: BaseMessage) => void
404
528
  ): BaseMessage[] {
405
529
  const allToolCallIds = new Set<string>();
406
530
  const allToolResultIds = new Set<string>();
407
531
 
408
532
  for (const msg of messages) {
409
533
  const msgAny = msg as unknown as Record<string, unknown>;
534
+ if (typeof (msg as { getType?: unknown }).getType === 'function') {
535
+ for (const id of getToolCallIds(msg)) {
536
+ allToolCallIds.add(id);
537
+ }
538
+ }
410
539
  const toolCalls = msgAny.tool_calls as Array<{ id?: string }> | undefined;
411
540
  if (Array.isArray(toolCalls)) {
412
541
  for (const tc of toolCalls) {
@@ -492,6 +621,7 @@ export function sanitizeOrphanToolBlocks(
492
621
  const stripped = stripOrphanToolUseBlocks(msg, allToolResultIds);
493
622
  if (stripped != null) {
494
623
  strippedAiIndices.add(result.length);
624
+ onMessageCloned?.(msg, stripped);
495
625
  result.push(stripped);
496
626
  }
497
627
  continue;
@@ -527,6 +657,7 @@ export function sanitizeOrphanToolBlocks(
527
657
  );
528
658
  patched.tool_calls = keptToolCalls.length > 0 ? keptToolCalls : [];
529
659
  patched.content = keptContent;
660
+ onMessageCloned?.(msg, patched as BaseMessage);
530
661
  result.push(patched as BaseMessage);
531
662
  continue;
532
663
  }
@@ -680,7 +811,7 @@ export function getMessagesWithinTokenLimit({
680
811
  }): PruningResult {
681
812
  // Every reply is primed with <|start|>assistant<|message|>, so we
682
813
  // start with 3 tokens for the label after all messages have been counted.
683
- let currentTokenCount = 3;
814
+ let currentTokenCount = REPLY_PRIMER_TOKENS;
684
815
  const instructions =
685
816
  _messages[0]?.getType() === 'system' ? _messages[0] : undefined;
686
817
  const instructionsTokenCount =
@@ -908,7 +1039,7 @@ export function getMessagesWithinTokenLimit({
908
1039
  const newThinkingMessageTokenCount =
909
1040
  (indexTokenCountMap[thinkingStartIndex] ?? 0) + thinkingTokenCount;
910
1041
  remainingContextTokens = initialContextTokens - newThinkingMessageTokenCount;
911
- currentTokenCount = 3;
1042
+ currentTokenCount = REPLY_PRIMER_TOKENS;
912
1043
  let newContext: BaseMessage[] = [];
913
1044
  const secondRoundMessages = [..._messages];
914
1045
  let currentIndex = secondRoundMessages.length;
@@ -1064,10 +1195,10 @@ export function maskConsumedToolResults(params: {
1064
1195
  for (let c = 0; c < count; c++) {
1065
1196
  const i = consumedIndices[c];
1066
1197
  const message = messages[i];
1067
- const content = message.content;
1068
- if (typeof content !== 'string') {
1198
+ if (isComputerCallOutputMessage(message)) {
1069
1199
  continue;
1070
1200
  }
1201
+ const content = message.content;
1071
1202
 
1072
1203
  let maxChars: number;
1073
1204
  if (totalBudgetChars > 0) {
@@ -1080,25 +1211,26 @@ export function maskConsumedToolResults(params: {
1080
1211
  maxChars = MASKED_RESULT_MAX_CHARS;
1081
1212
  }
1082
1213
 
1083
- if (content.length <= maxChars) {
1214
+ const compacted = compactToolContent(content, maxChars);
1215
+ if (!compacted.changed) {
1084
1216
  continue;
1085
1217
  }
1086
1218
 
1087
1219
  if (params.originalContentStore && !params.originalContentStore.has(i)) {
1088
- params.originalContentStore.set(i, content);
1220
+ const original = serializeToolContentBounded(
1221
+ content,
1222
+ ORIGINAL_CONTENT_MAX_CHARS
1223
+ );
1224
+ params.originalContentStore.set(i, original);
1089
1225
  if (params.onContentStored) {
1090
- params.onContentStored(i, content);
1226
+ params.onContentStored(i, original);
1091
1227
  }
1092
1228
  }
1093
1229
 
1094
- const cloned = new ToolMessage({
1095
- content: truncateToolResultContent(content, maxChars),
1096
- tool_call_id: (message as ToolMessage).tool_call_id,
1097
- name: message.name,
1098
- id: message.id,
1099
- additional_kwargs: message.additional_kwargs,
1100
- response_metadata: message.response_metadata,
1101
- });
1230
+ const cloned = cloneToolMessageWithContent(
1231
+ message as ToolMessage,
1232
+ compacted.content
1233
+ );
1102
1234
  messages[i] = cloned;
1103
1235
  indexTokenCountMap[i] = tokenCounter(cloned);
1104
1236
  maskedCount++;
@@ -1131,7 +1263,10 @@ export function preFlightTruncateToolResults(params: {
1131
1263
 
1132
1264
  const toolIndices: number[] = [];
1133
1265
  for (let i = 0; i < messages.length; i++) {
1134
- if (messages[i].getType() === 'tool') {
1266
+ if (
1267
+ messages[i].getType() === 'tool' &&
1268
+ !isComputerCallOutputMessage(messages[i])
1269
+ ) {
1135
1270
  toolIndices.push(i);
1136
1271
  }
1137
1272
  }
@@ -1140,27 +1275,19 @@ export function preFlightTruncateToolResults(params: {
1140
1275
  const i = toolIndices[t];
1141
1276
  const message = messages[i];
1142
1277
  const content = message.content;
1143
- if (typeof content !== 'string') {
1144
- continue;
1145
- }
1146
1278
 
1147
1279
  const position = toolIndices.length > 1 ? t / (toolIndices.length - 1) : 1;
1148
1280
  const recencyFactor = 0.2 + 0.8 * position;
1149
1281
  const maxChars = Math.max(200, Math.floor(baseMaxChars * recencyFactor));
1150
1282
 
1151
- if (content.length <= maxChars) {
1283
+ const compacted = compactToolContent(content, maxChars);
1284
+ if (!compacted.changed) {
1152
1285
  continue;
1153
1286
  }
1154
-
1155
- const truncated = truncateToolResultContent(content, maxChars);
1156
- const cloned = new ToolMessage({
1157
- content: truncated,
1158
- tool_call_id: (message as ToolMessage).tool_call_id,
1159
- name: message.name,
1160
- id: message.id,
1161
- additional_kwargs: message.additional_kwargs,
1162
- response_metadata: message.response_metadata,
1163
- });
1287
+ const cloned = cloneToolMessageWithContent(
1288
+ message as ToolMessage,
1289
+ compacted.content
1290
+ );
1164
1291
  messages[i] = cloned;
1165
1292
  indexTokenCountMap[i] = tokenCounter(cloned);
1166
1293
  truncatedCount++;
@@ -1182,86 +1309,721 @@ export function preFlightTruncateToolResults(params: {
1182
1309
  *
1183
1310
  * @returns The number of AI messages that had tool_use inputs truncated.
1184
1311
  */
1185
- export function preFlightTruncateToolCallInputs(params: {
1186
- messages: BaseMessage[];
1187
- maxContextTokens: number;
1188
- indexTokenCountMap: Record<string, number | undefined>;
1189
- tokenCounter: TokenCounter;
1190
- }): number {
1191
- const { messages, maxContextTokens, indexTokenCountMap, tokenCounter } =
1192
- params;
1193
- const maxInputChars = Math.min(
1194
- Math.floor(maxContextTokens * 0.15) * 4,
1195
- 200_000
1312
+ const HARD_MAX_TOOL_CALL_INPUT_CHARS = 200_000;
1313
+ const MIN_JSON_VALUE_CHARS = 4;
1314
+ const ACCESSOR_INPUT_PLACEHOLDER = '[Property accessor omitted]';
1315
+
1316
+ type ToolInputProjection = {
1317
+ value: unknown;
1318
+ changed: boolean;
1319
+ };
1320
+
1321
+ type PropertyRead = {
1322
+ found: boolean;
1323
+ own: boolean;
1324
+ accessor: boolean;
1325
+ value?: unknown;
1326
+ };
1327
+
1328
+ function normalizeToolInputLimit(maxChars: number): number {
1329
+ if (!Number.isFinite(maxChars)) {
1330
+ return HARD_MAX_TOOL_CALL_INPUT_CHARS;
1331
+ }
1332
+ return Math.max(MIN_JSON_VALUE_CHARS, Math.floor(maxChars));
1333
+ }
1334
+
1335
+ function readPropertyWithoutAccessors(
1336
+ value: object,
1337
+ key: PropertyKey
1338
+ ): PropertyRead {
1339
+ let current: object | null = value;
1340
+ for (let depth = 0; current != null && depth < 100; depth++) {
1341
+ if (isProxy(current)) {
1342
+ return { found: true, own: current === value, accessor: true };
1343
+ }
1344
+ let descriptor: PropertyDescriptor | undefined;
1345
+ try {
1346
+ descriptor = Object.getOwnPropertyDescriptor(current, key);
1347
+ } catch {
1348
+ return { found: true, own: current === value, accessor: true };
1349
+ }
1350
+ if (descriptor != null) {
1351
+ if (!('value' in descriptor)) {
1352
+ return { found: true, own: current === value, accessor: true };
1353
+ }
1354
+ return {
1355
+ found: true,
1356
+ own: current === value,
1357
+ accessor: false,
1358
+ value: descriptor.value,
1359
+ };
1360
+ }
1361
+ try {
1362
+ current = Object.getPrototypeOf(current) as object | null;
1363
+ } catch {
1364
+ return { found: true, own: false, accessor: true };
1365
+ }
1366
+ }
1367
+ return current == null
1368
+ ? { found: false, own: false, accessor: false }
1369
+ : { found: true, own: false, accessor: true };
1370
+ }
1371
+
1372
+ function cloneWithProjectedProperties<T extends object>(
1373
+ value: T,
1374
+ changes: Readonly<Record<string, unknown>>
1375
+ ): T {
1376
+ const descriptors = Object.getOwnPropertyDescriptors(value) as Record<
1377
+ string,
1378
+ PropertyDescriptor | undefined
1379
+ >;
1380
+ for (const [key, projectedValue] of Object.entries(changes)) {
1381
+ const descriptor = descriptors[key];
1382
+ descriptors[key] = {
1383
+ configurable: descriptor?.configurable ?? true,
1384
+ enumerable: descriptor?.enumerable ?? true,
1385
+ value: projectedValue,
1386
+ writable: descriptor?.writable ?? true,
1387
+ };
1388
+ }
1389
+ return Object.create(
1390
+ Object.getPrototypeOf(value),
1391
+ descriptors as PropertyDescriptorMap
1392
+ ) as T;
1393
+ }
1394
+
1395
+ function createBoundedTruncationValue(
1396
+ preview: string,
1397
+ originalChars: number,
1398
+ maxChars: number
1399
+ ): unknown {
1400
+ const normalizedMaxChars = normalizeToolInputLimit(maxChars);
1401
+ const emptyEnvelope = {
1402
+ _truncated: '',
1403
+ _originalChars: originalChars,
1404
+ };
1405
+ if (JSON.stringify(emptyEnvelope).length > normalizedMaxChars) {
1406
+ return null;
1407
+ }
1408
+
1409
+ let low = 0;
1410
+ let high = Math.min(preview.length, normalizedMaxChars);
1411
+ while (low < high) {
1412
+ const next = Math.ceil((low + high) / 2);
1413
+ const candidate = {
1414
+ _truncated: preview.slice(0, next),
1415
+ _originalChars: originalChars,
1416
+ };
1417
+ if (JSON.stringify(candidate).length <= normalizedMaxChars) {
1418
+ low = next;
1419
+ } else {
1420
+ high = next - 1;
1421
+ }
1422
+ }
1423
+ const truncationMarker = '… [truncated]';
1424
+ const boundedPreview =
1425
+ low < preview.length && low >= truncationMarker.length
1426
+ ? preview.slice(0, low - truncationMarker.length) + truncationMarker
1427
+ : preview.slice(0, low);
1428
+ return {
1429
+ _truncated: boundedPreview,
1430
+ _originalChars: originalChars,
1431
+ };
1432
+ }
1433
+
1434
+ function projectToolInputWithinLimit(
1435
+ input: unknown,
1436
+ maxChars: number
1437
+ ): ToolInputProjection {
1438
+ const normalizedMaxChars = normalizeToolInputLimit(maxChars);
1439
+ const serialized = serializeStructuredValueBounded(input, normalizedMaxChars);
1440
+ if (serialized.truncated) {
1441
+ return {
1442
+ value: createBoundedTruncationValue(
1443
+ serialized.content,
1444
+ serialized.originalChars,
1445
+ normalizedMaxChars
1446
+ ),
1447
+ changed: true,
1448
+ };
1449
+ }
1450
+ if (!hasUnsafeStructuredSerialization(input)) {
1451
+ return { value: input, changed: false };
1452
+ }
1453
+
1454
+ try {
1455
+ return { value: JSON.parse(serialized.content), changed: true };
1456
+ } catch {
1457
+ return {
1458
+ value: createBoundedTruncationValue(
1459
+ serialized.content,
1460
+ serialized.originalChars,
1461
+ normalizedMaxChars
1462
+ ),
1463
+ changed: true,
1464
+ };
1465
+ }
1466
+ }
1467
+
1468
+ /**
1469
+ * Serializes one structured tool-call input as valid, bounded JSON without
1470
+ * invoking user-defined accessors or `toJSON`.
1471
+ */
1472
+ export function serializeToolCallInput(
1473
+ input: unknown,
1474
+ maxChars = HARD_MAX_TOOL_CALL_INPUT_CHARS
1475
+ ): string {
1476
+ const normalizedMaxChars = normalizeToolInputLimit(maxChars);
1477
+ const projected = projectToolInputWithinLimit(input, normalizedMaxChars);
1478
+ const serialized = serializeStructuredValueBounded(
1479
+ projected.value,
1480
+ normalizedMaxChars
1196
1481
  );
1197
- let truncatedCount = 0;
1482
+ if (!serialized.truncated) {
1483
+ return serialized.content === 'undefined' ? 'null' : serialized.content;
1484
+ }
1485
+ const fallback = createBoundedTruncationValue(
1486
+ serialized.content,
1487
+ serialized.originalChars,
1488
+ normalizedMaxChars
1489
+ );
1490
+ return JSON.stringify(fallback);
1491
+ }
1492
+
1493
+ function projectKnownInputProperty(
1494
+ value: object,
1495
+ key: string,
1496
+ maxChars: number,
1497
+ changes: Record<string, unknown>
1498
+ ): void {
1499
+ const property = readPropertyWithoutAccessors(value, key);
1500
+ if (!property.found) {
1501
+ return;
1502
+ }
1503
+ const projected = property.accessor
1504
+ ? { value: ACCESSOR_INPUT_PLACEHOLDER, changed: true }
1505
+ : projectToolInputWithinLimit(property.value, maxChars);
1506
+ if (projected.changed || !property.own) {
1507
+ changes[key] = projected.value;
1508
+ }
1509
+ }
1510
+
1511
+ function getStringProperty(value: object, key: string): string | undefined {
1512
+ const property = readPropertyWithoutAccessors(value, key);
1513
+ return !property.accessor && typeof property.value === 'string'
1514
+ ? property.value
1515
+ : undefined;
1516
+ }
1517
+
1518
+ function projectInlineToolInput(
1519
+ block: MessageContentComplex,
1520
+ maxChars: number
1521
+ ): MessageContentComplex {
1522
+ const type = getStringProperty(block, 'type');
1523
+ if (type !== 'tool_use' && type !== 'tool_call') {
1524
+ return block;
1525
+ }
1526
+
1527
+ const changes: Record<string, unknown> = {};
1528
+ projectKnownInputProperty(block, 'input', maxChars, changes);
1529
+ projectKnownInputProperty(block, 'args', maxChars, changes);
1530
+
1531
+ const nestedProperty = readPropertyWithoutAccessors(block, 'tool_call');
1532
+ if (
1533
+ nestedProperty.found &&
1534
+ !nestedProperty.accessor &&
1535
+ nestedProperty.value != null &&
1536
+ typeof nestedProperty.value === 'object'
1537
+ ) {
1538
+ const nestedChanges: Record<string, unknown> = {};
1539
+ projectKnownInputProperty(
1540
+ nestedProperty.value,
1541
+ 'args',
1542
+ maxChars,
1543
+ nestedChanges
1544
+ );
1545
+ if (Object.keys(nestedChanges).length > 0) {
1546
+ changes.tool_call = cloneWithProjectedProperties(
1547
+ nestedProperty.value,
1548
+ nestedChanges
1549
+ );
1550
+ } else if (!nestedProperty.own) {
1551
+ changes.tool_call = nestedProperty.value;
1552
+ }
1553
+ } else if (nestedProperty.accessor) {
1554
+ changes.tool_call = { args: ACCESSOR_INPUT_PLACEHOLDER };
1555
+ }
1198
1556
 
1557
+ return Object.keys(changes).length > 0
1558
+ ? cloneWithProjectedProperties(block, changes)
1559
+ : block;
1560
+ }
1561
+
1562
+ function projectSerializedArguments(
1563
+ value: unknown,
1564
+ maxChars: number
1565
+ ): { value: string; changed: boolean } {
1566
+ const normalizedMaxChars = normalizeToolInputLimit(maxChars);
1567
+ if (typeof value === 'string' && value.length <= normalizedMaxChars) {
1568
+ return { value, changed: false };
1569
+ }
1570
+ return {
1571
+ value: serializeToolCallInput(value, normalizedMaxChars),
1572
+ changed: true,
1573
+ };
1574
+ }
1575
+
1576
+ function selectProjectedSerializedArguments(
1577
+ property: PropertyRead,
1578
+ canonical: string | undefined,
1579
+ maxChars: number
1580
+ ): { value: string; changed: boolean } {
1581
+ if (canonical != null) {
1582
+ return { value: canonical, changed: property.value !== canonical };
1583
+ }
1584
+ if (property.accessor) {
1585
+ return {
1586
+ value: serializeToolCallInput(ACCESSOR_INPUT_PLACEHOLDER, maxChars),
1587
+ changed: true,
1588
+ };
1589
+ }
1590
+ return projectSerializedArguments(property.value, maxChars);
1591
+ }
1592
+
1593
+ function projectRawOpenAIToolCalls(
1594
+ rawToolCalls: unknown,
1595
+ maxChars: number,
1596
+ canonicalArguments: ReadonlyMap<string, string>
1597
+ ): { value: unknown; changed: boolean } {
1598
+ if (!Array.isArray(rawToolCalls)) {
1599
+ return { value: rawToolCalls, changed: false };
1600
+ }
1601
+
1602
+ const state = { changed: false };
1603
+ const projected = rawToolCalls.map((rawToolCall) => {
1604
+ if (rawToolCall == null || typeof rawToolCall !== 'object') {
1605
+ return rawToolCall;
1606
+ }
1607
+ const functionProperty = readPropertyWithoutAccessors(
1608
+ rawToolCall,
1609
+ 'function'
1610
+ );
1611
+ if (
1612
+ !functionProperty.found ||
1613
+ functionProperty.accessor ||
1614
+ functionProperty.value == null ||
1615
+ typeof functionProperty.value !== 'object'
1616
+ ) {
1617
+ return rawToolCall;
1618
+ }
1619
+
1620
+ const argsProperty = readPropertyWithoutAccessors(
1621
+ functionProperty.value,
1622
+ 'arguments'
1623
+ );
1624
+ if (!argsProperty.found) {
1625
+ return rawToolCall;
1626
+ }
1627
+ const callId = getStringProperty(rawToolCall, 'id');
1628
+ const canonical =
1629
+ callId != null ? canonicalArguments.get(callId) : undefined;
1630
+ const projectedArgs = selectProjectedSerializedArguments(
1631
+ argsProperty,
1632
+ canonical,
1633
+ maxChars
1634
+ );
1635
+ if (!projectedArgs.changed && functionProperty.own && argsProperty.own) {
1636
+ return rawToolCall;
1637
+ }
1638
+
1639
+ state.changed = true;
1640
+ const projectedFunction = cloneWithProjectedProperties(
1641
+ functionProperty.value,
1642
+ { arguments: projectedArgs.value }
1643
+ );
1644
+ return cloneWithProjectedProperties(rawToolCall, {
1645
+ function: projectedFunction,
1646
+ });
1647
+ });
1648
+ return {
1649
+ value: state.changed ? projected : rawToolCalls,
1650
+ changed: state.changed,
1651
+ };
1652
+ }
1653
+
1654
+ function projectLegacyFunctionCall(
1655
+ property: PropertyRead,
1656
+ maxChars: number
1657
+ ): { value: unknown; changed: boolean } {
1658
+ if (!property.found) {
1659
+ return { value: property.value, changed: false };
1660
+ }
1661
+ if (property.own && !property.accessor && property.value === undefined) {
1662
+ return { value: undefined, changed: false };
1663
+ }
1664
+ if (
1665
+ property.accessor ||
1666
+ !property.own ||
1667
+ property.value == null ||
1668
+ typeof property.value !== 'object'
1669
+ ) {
1670
+ return { value: undefined, changed: true };
1671
+ }
1672
+
1673
+ const nameProperty = readPropertyWithoutAccessors(property.value, 'name');
1674
+ const argsProperty = readPropertyWithoutAccessors(
1675
+ property.value,
1676
+ 'arguments'
1677
+ );
1678
+ if (
1679
+ nameProperty.accessor ||
1680
+ !nameProperty.own ||
1681
+ typeof nameProperty.value !== 'string' ||
1682
+ nameProperty.value === '' ||
1683
+ nameProperty.value.length > normalizeToolInputLimit(maxChars) ||
1684
+ !argsProperty.found ||
1685
+ argsProperty.accessor ||
1686
+ !argsProperty.own
1687
+ ) {
1688
+ return { value: undefined, changed: true };
1689
+ }
1690
+ try {
1691
+ const prototype = Object.getPrototypeOf(property.value);
1692
+ const enumerableKeys = Object.keys(property.value);
1693
+ if (
1694
+ (prototype === Object.prototype || prototype === null) &&
1695
+ enumerableKeys.length === 2 &&
1696
+ enumerableKeys.includes('name') &&
1697
+ enumerableKeys.includes('arguments') &&
1698
+ typeof argsProperty.value === 'string' &&
1699
+ argsProperty.value.length <= normalizeToolInputLimit(maxChars) &&
1700
+ !hasUnsafeStructuredSerialization(property.value)
1701
+ ) {
1702
+ return { value: property.value, changed: false };
1703
+ }
1704
+ } catch {
1705
+ return { value: undefined, changed: true };
1706
+ }
1707
+ const projectedArgs = selectProjectedSerializedArguments(
1708
+ argsProperty,
1709
+ undefined,
1710
+ maxChars
1711
+ );
1712
+ return {
1713
+ value: {
1714
+ name: nameProperty.value,
1715
+ arguments: projectedArgs.value,
1716
+ },
1717
+ changed: true,
1718
+ };
1719
+ }
1720
+
1721
+ function projectResponsesOutput(
1722
+ output: unknown,
1723
+ maxChars: number,
1724
+ canonicalArguments: ReadonlyMap<string, string>,
1725
+ canonicalInputs: ReadonlyMap<string, unknown>
1726
+ ): { value: unknown; changed: boolean } {
1727
+ if (!Array.isArray(output)) {
1728
+ return { value: output, changed: false };
1729
+ }
1730
+
1731
+ const state = { changed: false };
1732
+ const projected = output.map((item) => {
1733
+ if (item == null || typeof item !== 'object') {
1734
+ return item;
1735
+ }
1736
+ const type = getStringProperty(item, 'type');
1737
+ if (
1738
+ type !== 'function_call' &&
1739
+ type !== 'custom_tool_call' &&
1740
+ type !== 'computer_call'
1741
+ ) {
1742
+ return item;
1743
+ }
1744
+ let inputKey = 'action';
1745
+ if (type === 'function_call') {
1746
+ inputKey = 'arguments';
1747
+ } else if (type === 'custom_tool_call') {
1748
+ inputKey = 'input';
1749
+ }
1750
+ const inputProperty = readPropertyWithoutAccessors(item, inputKey);
1751
+ if (!inputProperty.found) {
1752
+ return item;
1753
+ }
1754
+ const callId =
1755
+ getStringProperty(item, 'call_id') ?? getStringProperty(item, 'id');
1756
+ let projectedInput: ToolInputProjection;
1757
+ if (type === 'function_call') {
1758
+ const canonical =
1759
+ callId != null ? canonicalArguments.get(callId) : undefined;
1760
+ projectedInput = selectProjectedSerializedArguments(
1761
+ inputProperty,
1762
+ canonical,
1763
+ maxChars
1764
+ );
1765
+ } else {
1766
+ const canonicalArgs =
1767
+ callId != null ? canonicalInputs.get(callId) : undefined;
1768
+ const canonicalProperty =
1769
+ canonicalArgs != null && typeof canonicalArgs === 'object'
1770
+ ? readPropertyWithoutAccessors(canonicalArgs, inputKey)
1771
+ : undefined;
1772
+ let source = inputProperty.accessor
1773
+ ? ACCESSOR_INPUT_PLACEHOLDER
1774
+ : inputProperty.value;
1775
+ if (canonicalProperty != null && canonicalProperty.found) {
1776
+ source = canonicalProperty.accessor
1777
+ ? ACCESSOR_INPUT_PLACEHOLDER
1778
+ : canonicalProperty.value;
1779
+ }
1780
+ if (
1781
+ type === 'custom_tool_call' &&
1782
+ typeof source === 'string' &&
1783
+ source.length <= normalizeToolInputLimit(maxChars)
1784
+ ) {
1785
+ projectedInput = {
1786
+ value: source,
1787
+ changed: !inputProperty.own || inputProperty.value !== source,
1788
+ };
1789
+ } else if (type === 'custom_tool_call') {
1790
+ const value =
1791
+ typeof source === 'string'
1792
+ ? truncateToolResultContent(
1793
+ source,
1794
+ normalizeToolInputLimit(maxChars)
1795
+ )
1796
+ : serializeToolCallInput(source, maxChars);
1797
+ projectedInput = {
1798
+ value,
1799
+ changed: !inputProperty.own || inputProperty.value !== value,
1800
+ };
1801
+ } else {
1802
+ const projected = projectToolInputWithinLimit(source, maxChars);
1803
+ projectedInput = {
1804
+ value: projected.value,
1805
+ changed:
1806
+ projected.changed ||
1807
+ !inputProperty.own ||
1808
+ inputProperty.value !== projected.value,
1809
+ };
1810
+ }
1811
+ }
1812
+ if (!projectedInput.changed && inputProperty.own) {
1813
+ return item;
1814
+ }
1815
+ state.changed = true;
1816
+ return cloneWithProjectedProperties(item, {
1817
+ [inputKey]: projectedInput.value,
1818
+ });
1819
+ });
1820
+ return {
1821
+ value: state.changed ? projected : output,
1822
+ changed: state.changed,
1823
+ };
1824
+ }
1825
+
1826
+ /** Per-input cap: 15% of context at ~4 chars/token, never above 200K chars. */
1827
+ export function calculateMaxToolCallInputChars(
1828
+ maxContextTokens?: number
1829
+ ): number {
1830
+ if (maxContextTokens == null || maxContextTokens <= 0) {
1831
+ return HARD_MAX_TOOL_CALL_INPUT_CHARS;
1832
+ }
1833
+ return Math.max(
1834
+ MIN_JSON_VALUE_CHARS,
1835
+ Math.min(
1836
+ Math.floor(maxContextTokens * 0.15) * 4,
1837
+ HARD_MAX_TOOL_CALL_INPUT_CHARS
1838
+ )
1839
+ );
1840
+ }
1841
+
1842
+ /**
1843
+ * Projects historical tool-call inputs into a provider-safe bounded form.
1844
+ * Returns the original array when no message changes and otherwise clones only
1845
+ * the array and AI messages whose inline input or `tool_calls` args changed.
1846
+ */
1847
+ export function projectToolCallInputs(
1848
+ messages: BaseMessage[],
1849
+ maxInputChars: number
1850
+ ): BaseMessage[] {
1851
+ const normalizedMaxInputChars = normalizeToolInputLimit(maxInputChars);
1852
+ let projectedMessages: BaseMessage[] | undefined;
1199
1853
  for (let i = 0; i < messages.length; i++) {
1200
1854
  const message = messages[i];
1201
- if (message.getType() !== 'ai') {
1855
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
1856
+ if (message.getType() !== 'ai' && messageRole !== 'assistant') {
1202
1857
  continue;
1203
1858
  }
1204
- if (!Array.isArray(message.content)) {
1205
- continue;
1859
+
1860
+ const aiMessage = message as AIMessage | AIMessageChunk;
1861
+ let projectedContent = aiMessage.content;
1862
+ let contentChanged = false;
1863
+ if (Array.isArray(aiMessage.content)) {
1864
+ const originalContent = aiMessage.content as MessageContentComplex[];
1865
+ const mappedContent = originalContent.map((block) =>
1866
+ projectInlineToolInput(block, normalizedMaxInputChars)
1867
+ );
1868
+ contentChanged = mappedContent.some(
1869
+ (block, blockIndex) => block !== originalContent[blockIndex]
1870
+ );
1871
+ projectedContent = toLangChainContent(mappedContent);
1206
1872
  }
1207
1873
 
1208
- const originalContent = message.content as MessageContentComplex[];
1209
- const state = { changed: false };
1210
- const newContent = originalContent.map((block) => {
1211
- if (typeof block !== 'object') {
1212
- return block;
1213
- }
1214
- const record = block as Record<string, unknown>;
1215
- if (record.type !== 'tool_use' && record.type !== 'tool_call') {
1216
- return block;
1874
+ const originalToolCalls = aiMessage.tool_calls ?? [];
1875
+ const projectedToolCalls = originalToolCalls.map((toolCall) => {
1876
+ const argsProperty = readPropertyWithoutAccessors(toolCall, 'args');
1877
+ const projected = argsProperty.accessor
1878
+ ? { value: ACCESSOR_INPUT_PLACEHOLDER, changed: true }
1879
+ : projectToolInputWithinLimit(
1880
+ argsProperty.value,
1881
+ normalizedMaxInputChars
1882
+ );
1883
+ if (argsProperty.own && !projected.changed) {
1884
+ return toolCall;
1217
1885
  }
1886
+ return cloneWithProjectedProperties(toolCall, {
1887
+ args: projected.value as Record<string, unknown>,
1888
+ });
1889
+ });
1890
+ const toolCallsChanged = projectedToolCalls.some(
1891
+ (toolCall, toolCallIndex) => toolCall !== originalToolCalls[toolCallIndex]
1892
+ );
1218
1893
 
1219
- const input = record.input;
1220
- if (input == null) {
1221
- return block;
1222
- }
1223
- const serialized =
1224
- typeof input === 'string' ? input : JSON.stringify(input);
1225
- if (serialized.length <= maxInputChars) {
1226
- return block;
1894
+ const rawAdditionalToolCallsProperty = readPropertyWithoutAccessors(
1895
+ aiMessage.additional_kwargs,
1896
+ 'tool_calls'
1897
+ );
1898
+ const rawAdditionalToolCalls = rawAdditionalToolCallsProperty.accessor
1899
+ ? undefined
1900
+ : rawAdditionalToolCallsProperty.value;
1901
+ const rawResponsesOutputProperty = readPropertyWithoutAccessors(
1902
+ aiMessage.response_metadata,
1903
+ 'output'
1904
+ );
1905
+ const rawResponsesOutput = rawResponsesOutputProperty.accessor
1906
+ ? undefined
1907
+ : rawResponsesOutputProperty.value;
1908
+ const canonicalArguments = new Map<string, string>();
1909
+ const canonicalInputs = new Map<string, unknown>();
1910
+ if (
1911
+ Array.isArray(rawAdditionalToolCalls) ||
1912
+ Array.isArray(rawResponsesOutput)
1913
+ ) {
1914
+ for (const toolCall of projectedToolCalls) {
1915
+ const id = getStringProperty(toolCall, 'id');
1916
+ if (id == null) {
1917
+ continue;
1918
+ }
1919
+ const args = readPropertyWithoutAccessors(toolCall, 'args');
1920
+ const canonicalInput = args.accessor
1921
+ ? ACCESSOR_INPUT_PLACEHOLDER
1922
+ : args.value;
1923
+ canonicalInputs.set(id, canonicalInput);
1924
+ canonicalArguments.set(
1925
+ id,
1926
+ serializeToolCallInput(canonicalInput, normalizedMaxInputChars)
1927
+ );
1227
1928
  }
1929
+ }
1228
1930
 
1229
- state.changed = true;
1230
- // Replaces original input with { _truncated, _originalChars }
1231
- // safe because the tool call already executed in a prior turn.
1232
- return {
1233
- ...record,
1234
- input: truncateToolInput(serialized, maxInputChars),
1235
- };
1236
- });
1931
+ const additionalKwargsChanges: Record<string, unknown> =
1932
+ rawAdditionalToolCallsProperty.accessor ? { tool_calls: undefined } : {};
1933
+ const rawToolCalls = projectRawOpenAIToolCalls(
1934
+ rawAdditionalToolCalls,
1935
+ normalizedMaxInputChars,
1936
+ canonicalArguments
1937
+ );
1938
+ if (rawToolCalls.changed) {
1939
+ additionalKwargsChanges.tool_calls = rawToolCalls.value;
1940
+ }
1941
+ const legacyFunctionCall = projectLegacyFunctionCall(
1942
+ readPropertyWithoutAccessors(
1943
+ aiMessage.additional_kwargs,
1944
+ 'function_call'
1945
+ ),
1946
+ normalizedMaxInputChars
1947
+ );
1948
+ if (legacyFunctionCall.changed) {
1949
+ additionalKwargsChanges.function_call = legacyFunctionCall.value;
1950
+ }
1951
+ const additionalKwargsChanged =
1952
+ Object.keys(additionalKwargsChanges).length > 0;
1953
+ let projectedAdditionalKwargs = aiMessage.additional_kwargs;
1954
+ if (additionalKwargsChanged) {
1955
+ projectedAdditionalKwargs = isProxy(aiMessage.additional_kwargs)
1956
+ ? { ...additionalKwargsChanges }
1957
+ : cloneWithProjectedProperties(
1958
+ aiMessage.additional_kwargs,
1959
+ additionalKwargsChanges
1960
+ );
1961
+ }
1962
+
1963
+ let projectedResponseMetadata = aiMessage.response_metadata;
1964
+ let responseMetadataChanged = rawResponsesOutputProperty.accessor;
1965
+ const responsesOutput = projectResponsesOutput(
1966
+ rawResponsesOutput,
1967
+ normalizedMaxInputChars,
1968
+ canonicalArguments,
1969
+ canonicalInputs
1970
+ );
1971
+ if (responsesOutput.changed || rawResponsesOutputProperty.accessor) {
1972
+ const output = rawResponsesOutputProperty.accessor
1973
+ ? undefined
1974
+ : responsesOutput.value;
1975
+ projectedResponseMetadata = isProxy(aiMessage.response_metadata)
1976
+ ? { output }
1977
+ : cloneWithProjectedProperties(aiMessage.response_metadata, {
1978
+ output,
1979
+ });
1980
+ responseMetadataChanged = true;
1981
+ }
1237
1982
 
1238
- if (!state.changed) {
1983
+ if (
1984
+ !contentChanged &&
1985
+ !toolCallsChanged &&
1986
+ !additionalKwargsChanged &&
1987
+ !responseMetadataChanged
1988
+ ) {
1239
1989
  continue;
1240
1990
  }
1241
1991
 
1242
- const aiMsg = message as AIMessage;
1243
- const newToolCalls = (aiMsg.tool_calls ?? []).map((tc) => {
1244
- const serializedArgs = JSON.stringify(tc.args);
1245
- if (serializedArgs.length <= maxInputChars) {
1246
- return tc;
1247
- }
1248
- // Replaces original args with { _truncated, _originalChars } —
1249
- // safe because the tool call already executed in a prior turn.
1250
- return {
1251
- ...tc,
1252
- args: truncateToolInput(serializedArgs, maxInputChars),
1253
- };
1992
+ const projectedMessage = cloneWithProjectedProperties(aiMessage, {
1993
+ content: projectedContent,
1994
+ tool_calls: projectedToolCalls.length > 0 ? projectedToolCalls : [],
1995
+ additional_kwargs: projectedAdditionalKwargs,
1996
+ response_metadata: projectedResponseMetadata,
1254
1997
  });
1998
+ projectedMessages ??= [...messages];
1999
+ projectedMessages[i] = projectedMessage;
2000
+ }
2001
+ return projectedMessages ?? messages;
2002
+ }
1255
2003
 
1256
- messages[i] = new AIMessage({
1257
- ...aiMsg,
1258
- content: toLangChainContent(newContent),
1259
- tool_calls: newToolCalls.length > 0 ? newToolCalls : undefined,
1260
- });
1261
- indexTokenCountMap[i] = tokenCounter(messages[i]);
1262
- truncatedCount++;
2004
+ export function preFlightTruncateToolCallInputs(params: {
2005
+ messages: BaseMessage[];
2006
+ maxContextTokens: number;
2007
+ indexTokenCountMap: Record<string, number | undefined>;
2008
+ tokenCounter: TokenCounter;
2009
+ }): number {
2010
+ const { messages, maxContextTokens, indexTokenCountMap, tokenCounter } =
2011
+ params;
2012
+ const maxInputChars = calculateMaxToolCallInputChars(maxContextTokens);
2013
+ const projected = projectToolCallInputs(messages, maxInputChars);
2014
+ if (projected === messages) {
2015
+ return 0;
1263
2016
  }
1264
2017
 
2018
+ let truncatedCount = 0;
2019
+ for (let i = 0; i < messages.length; i++) {
2020
+ if (projected[i] === messages[i]) {
2021
+ continue;
2022
+ }
2023
+ messages[i] = projected[i];
2024
+ indexTokenCountMap[i] = tokenCounter(projected[i]);
2025
+ truncatedCount++;
2026
+ }
1265
2027
  return truncatedCount;
1266
2028
  }
1267
2029
 
@@ -1281,6 +2043,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1281
2043
  for (const key in indexTokenCountMap) {
1282
2044
  totalTokens += indexTokenCountMap[key] ?? 0;
1283
2045
  }
2046
+ const reconciledToolMessages = new WeakSet<BaseMessage>();
1284
2047
  let runThinkingStartIndex = -1;
1285
2048
  /** Cumulative raw tiktoken tokens we've sent to the provider (messages only,
1286
2049
  * excludes instruction overhead and new outputs not yet seen by provider). */
@@ -1296,6 +2059,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1296
2059
  /** Best observed instruction overhead from a near-zero variance turn.
1297
2060
  * Self-seeds from provider observations within the run. */
1298
2061
  let bestInstructionOverhead: number | undefined;
2062
+ const reconciledLegacyAiMessages = new WeakSet<BaseMessage>();
1299
2063
  let bestVarianceAbs = Infinity;
1300
2064
  /** Local estimate at the time bestInstructionOverhead was observed.
1301
2065
  * Used to invalidate the cached overhead when instructions change
@@ -1340,6 +2104,9 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1340
2104
  (emptyReserveRatio > 0 && emptyReserveRatio < 1
1341
2105
  ? Math.round(factoryParams.maxTokens * emptyReserveRatio)
1342
2106
  : 0);
2107
+ const emptyReplyPrimerTokens = Math.round(
2108
+ REPLY_PRIMER_TOKENS * calibrationRatio
2109
+ );
1343
2110
  return {
1344
2111
  context: [],
1345
2112
  indexTokenCountMap,
@@ -1347,7 +2114,7 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1347
2114
  prePruneContextTokens: 0,
1348
2115
  remainingContextTokens: Math.max(
1349
2116
  0,
1350
- emptyBudget - emptyInstructionTokens
2117
+ emptyBudget - emptyInstructionTokens - emptyReplyPrimerTokens
1351
2118
  ),
1352
2119
  calibrationRatio,
1353
2120
  resolvedInstructionOverhead: bestInstructionOverhead,
@@ -1416,6 +2183,90 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1416
2183
  }
1417
2184
 
1418
2185
  const newOutputs = new Set<number>();
2186
+
2187
+ // Host token maps predate LangChain formatting and may omit tool output or
2188
+ // legacy function-call arguments. Reconcile those provider-bound shapes
2189
+ // once before making any budget decision.
2190
+ const applyReconciledCount = (
2191
+ index: number,
2192
+ cachedCount: number | undefined,
2193
+ reconciledCount: number
2194
+ ): void => {
2195
+ if (cachedCount === undefined) {
2196
+ indexTokenCountMap[index] = reconciledCount;
2197
+ totalTokens += reconciledCount;
2198
+ if (index >= lastTurnStartIndex) {
2199
+ newOutputs.add(index);
2200
+ }
2201
+ return;
2202
+ }
2203
+ // Preserve a larger host estimate: reconciliation is a safety floor,
2204
+ // not permission to reduce an upstream count that may include provider
2205
+ // serialization overhead unavailable to the local counter.
2206
+ if (reconciledCount <= cachedCount) {
2207
+ return;
2208
+ }
2209
+ indexTokenCountMap[index] = reconciledCount;
2210
+ totalTokens += reconciledCount - cachedCount;
2211
+ if (index >= lastTurnStartIndex) {
2212
+ newOutputs.add(index);
2213
+ }
2214
+ };
2215
+ for (let i = 0; i < params.messages.length; i++) {
2216
+ let message = params.messages[i];
2217
+ const cachedCount = indexTokenCountMap[i];
2218
+ const messageType = message.getType();
2219
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
2220
+ const isAssistant = messageType === 'ai' || messageRole === 'assistant';
2221
+ const legacyFunctionCall = isAssistant
2222
+ ? readPropertyWithoutAccessors(
2223
+ message.additional_kwargs,
2224
+ 'function_call'
2225
+ )
2226
+ : undefined;
2227
+ if (
2228
+ legacyFunctionCall?.found === true &&
2229
+ !reconciledLegacyAiMessages.has(message)
2230
+ ) {
2231
+ const [projected] = projectToolCallInputs(
2232
+ [message],
2233
+ calculateMaxToolCallInputChars(factoryParams.maxTokens)
2234
+ );
2235
+ if (projected !== message) {
2236
+ message = projected;
2237
+ params.messages[i] = message;
2238
+ }
2239
+ reconciledLegacyAiMessages.add(message);
2240
+ if (cachedCount !== undefined || i < lastTurnStartIndex) {
2241
+ applyReconciledCount(
2242
+ i,
2243
+ cachedCount,
2244
+ factoryParams.tokenCounter(message)
2245
+ );
2246
+ }
2247
+ continue;
2248
+ }
2249
+ if (messageType !== 'tool' || reconciledToolMessages.has(message)) {
2250
+ continue;
2251
+ }
2252
+ if (!isComputerCallOutputMessage(message)) {
2253
+ const normalized = compactToolContent(
2254
+ message.content,
2255
+ factoryParams.maxToolResultChars ?? HARD_MAX_TOOL_RESULT_CHARS
2256
+ );
2257
+ if (normalized.changed) {
2258
+ message = cloneToolMessageWithContent(
2259
+ message as ToolMessage,
2260
+ normalized.content
2261
+ );
2262
+ params.messages[i] = message;
2263
+ }
2264
+ }
2265
+ const reconciledCount = factoryParams.tokenCounter(message);
2266
+ reconciledToolMessages.add(message);
2267
+ applyReconciledCount(i, cachedCount, reconciledCount);
2268
+ }
2269
+
1419
2270
  let outputTokensAssigned = false;
1420
2271
  for (let i = lastTurnStartIndex; i < params.messages.length; i++) {
1421
2272
  const message = params.messages[i];
@@ -1795,9 +2646,15 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1795
2646
  }
1796
2647
 
1797
2648
  lastTurnStartIndex = params.messages.length;
2649
+ const calibratedReplyPrimerTokens = Math.round(
2650
+ REPLY_PRIMER_TOKENS * calibrationRatio
2651
+ );
1798
2652
  if (
1799
2653
  lastCutOffIndex === 0 &&
1800
- calibratedTotalTokens + currentInstructionTokens <= pruningBudget
2654
+ calibratedTotalTokens +
2655
+ calibratedReplyPrimerTokens +
2656
+ currentInstructionTokens <=
2657
+ pruningBudget
1801
2658
  ) {
1802
2659
  return {
1803
2660
  context: params.messages,
@@ -1805,7 +2662,10 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
1805
2662
  messagesToRefine: [],
1806
2663
  prePruneContextTokens: calibratedTotalTokens,
1807
2664
  remainingContextTokens:
1808
- pruningBudget - calibratedTotalTokens - currentInstructionTokens,
2665
+ pruningBudget -
2666
+ calibratedTotalTokens -
2667
+ calibratedReplyPrimerTokens -
2668
+ currentInstructionTokens,
1809
2669
  contextPressure,
1810
2670
  originalToolContent:
1811
2671
  originalToolContent.size > 0 ? originalToolContent : undefined,
@@ -2022,88 +2882,41 @@ export function createPruneMessages(factoryParams: PruneMessagesFactoryParams) {
2022
2882
  let emergencyTruncatedCount = 0;
2023
2883
  for (let i = 0; i < emergencyMessages.length; i++) {
2024
2884
  const message = emergencyMessages[i];
2025
- if (message.getType() === 'tool') {
2885
+ if (
2886
+ message.getType() === 'tool' &&
2887
+ !isComputerCallOutputMessage(message)
2888
+ ) {
2026
2889
  const content = message.content;
2027
- if (
2028
- typeof content === 'string' &&
2029
- content.length > emergencyMaxChars
2030
- ) {
2031
- const cloned = new ToolMessage({
2032
- content: truncateToolResultContent(content, emergencyMaxChars),
2033
- tool_call_id: (message as ToolMessage).tool_call_id,
2034
- name: message.name,
2035
- id: message.id,
2036
- additional_kwargs: message.additional_kwargs,
2037
- response_metadata: message.response_metadata,
2038
- });
2890
+ if (getToolContentCharLength(content) > emergencyMaxChars) {
2891
+ const compacted = compactToolContent(content, emergencyMaxChars);
2892
+ if (!compacted.changed) {
2893
+ continue;
2894
+ }
2895
+ const cloned = cloneToolMessageWithContent(
2896
+ message as ToolMessage,
2897
+ compacted.content
2898
+ );
2039
2899
  emergencyMessages[i] = cloned;
2040
2900
  indexTokenCountMap[i] = factoryParams.tokenCounter(cloned);
2041
2901
  emergencyTruncatedCount++;
2042
2902
  }
2043
2903
  }
2044
- if (message.getType() === 'ai' && Array.isArray(message.content)) {
2045
- const aiMsg = message as AIMessage;
2046
- const contentBlocks = aiMsg.content as MessageContentComplex[];
2047
- const needsTruncation = contentBlocks.some((block) => {
2048
- if (typeof block !== 'object') return false;
2049
- const record = block as Record<string, unknown>;
2050
- if (
2051
- (record.type === 'tool_use' || record.type === 'tool_call') &&
2052
- record.input != null
2053
- ) {
2054
- const serialized =
2055
- typeof record.input === 'string'
2056
- ? record.input
2057
- : JSON.stringify(record.input);
2058
- return serialized.length > emergencyMaxChars;
2059
- }
2060
- return false;
2061
- });
2062
- if (needsTruncation) {
2063
- const newContent = contentBlocks.map((block) => {
2064
- if (typeof block !== 'object') return block;
2065
- const record = block as Record<string, unknown>;
2066
- if (
2067
- (record.type === 'tool_use' || record.type === 'tool_call') &&
2068
- record.input != null
2069
- ) {
2070
- const serialized =
2071
- typeof record.input === 'string'
2072
- ? record.input
2073
- : JSON.stringify(record.input);
2074
- if (serialized.length > emergencyMaxChars) {
2075
- // Replaces original input with { _truncated, _originalChars } —
2076
- // safe because the tool call already executed in a prior turn.
2077
- return {
2078
- ...record,
2079
- input: truncateToolInput(serialized, emergencyMaxChars),
2080
- };
2081
- }
2082
- }
2083
- return block;
2084
- });
2085
- const newToolCalls = (aiMsg.tool_calls ?? []).map((tc) => {
2086
- const serializedArgs = JSON.stringify(tc.args);
2087
- if (serializedArgs.length > emergencyMaxChars) {
2088
- // Replaces original args with { _truncated, _originalChars } —
2089
- // safe because the tool call already executed in a prior turn.
2090
- return {
2091
- ...tc,
2092
- args: truncateToolInput(serializedArgs, emergencyMaxChars),
2093
- };
2094
- }
2095
- return tc;
2096
- });
2097
- emergencyMessages[i] = new AIMessage({
2098
- ...aiMsg,
2099
- content: toLangChainContent(newContent),
2100
- tool_calls: newToolCalls.length > 0 ? newToolCalls : undefined,
2101
- });
2102
- indexTokenCountMap[i] = factoryParams.tokenCounter(
2103
- emergencyMessages[i]
2104
- );
2105
- emergencyTruncatedCount++;
2904
+ }
2905
+
2906
+ const projectedToolInputs = projectToolCallInputs(
2907
+ emergencyMessages,
2908
+ emergencyMaxChars
2909
+ );
2910
+ if (projectedToolInputs !== emergencyMessages) {
2911
+ for (let i = 0; i < emergencyMessages.length; i++) {
2912
+ if (projectedToolInputs[i] === emergencyMessages[i]) {
2913
+ continue;
2106
2914
  }
2915
+ emergencyMessages[i] = projectedToolInputs[i];
2916
+ indexTokenCountMap[i] = factoryParams.tokenCounter(
2917
+ projectedToolInputs[i]
2918
+ );
2919
+ emergencyTruncatedCount++;
2107
2920
  }
2108
2921
  }
2109
2922