@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
@@ -0,0 +1,2707 @@
1
+ import { isProxy } from 'node:util/types';
2
+ import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
3
+ import {
4
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
5
+ truncateToolResultContent,
6
+ } from './truncation';
7
+
8
+ type ToolContent = BaseMessage['content'];
9
+ type ToolContentBlock = Exclude<ToolContent, string>[number];
10
+ type TextToolContentBlock = ToolContentBlock & { type: 'text'; text: string };
11
+
12
+ const ATOMIC_CONTENT_TYPES = new Set([
13
+ 'audio',
14
+ 'computer_screenshot',
15
+ 'document',
16
+ 'file',
17
+ 'image',
18
+ 'image_file',
19
+ 'image_url',
20
+ 'input_audio',
21
+ 'input_image',
22
+ 'media',
23
+ 'resource',
24
+ 'resource_link',
25
+ 'video',
26
+ 'video_url',
27
+ ]);
28
+ const MAX_TOOL_CONTENT_TYPE_CHARS = 256;
29
+ const MAX_PROVIDER_IMAGE_URL_CHARS = 16_384;
30
+ const MAX_PROVIDER_FILE_ID_CHARS = 4_096;
31
+
32
+ const PROVIDER_NATIVE_TOOL_RESULT_TYPES = new Set([
33
+ 'search_result',
34
+ 'server_tool_call_result',
35
+ 'tool_result',
36
+ 'toolResponse',
37
+ 'web_search_result',
38
+ 'web_search_tool_result',
39
+ ]);
40
+
41
+ /**
42
+ * Serializes values that providers render as JSON without throwing on values
43
+ * such as bigint or circular diagnostic metadata.
44
+ */
45
+ export function serializeStructuredValue(value: unknown): string {
46
+ return serializeStructuredValueBounded(value, Number.MAX_SAFE_INTEGER)
47
+ .content;
48
+ }
49
+
50
+ const SERIALIZATION_CHUNK_SIZE = 4_096;
51
+ const MAX_STRUCTURED_SERIALIZATION_DEPTH = 200;
52
+ const MAX_STRUCTURED_SERIALIZATION_WORK = 1_000_000;
53
+ const MAX_STRUCTURED_CHARACTER_WORK = 1_000_000;
54
+ const MAX_STRUCTURED_PROPERTY_CACHE_ENTRIES = 10_000;
55
+ const PROXY_VALUE_PLACEHOLDER = '[Proxy value omitted]';
56
+ const CHARACTER_WORK_PLACEHOLDER =
57
+ '[truncated: character traversal limit exceeded]';
58
+
59
+ type StructuredWorkState = {
60
+ remaining: number;
61
+ characterRemaining: number;
62
+ exceeded: boolean;
63
+ failurePlaceholder?: string;
64
+ propertyCache: WeakMap<object, Map<string, unknown>>;
65
+ propertyCacheEntries: number;
66
+ };
67
+
68
+ function createStructuredWorkState(): StructuredWorkState {
69
+ return {
70
+ remaining: MAX_STRUCTURED_SERIALIZATION_WORK,
71
+ characterRemaining: MAX_STRUCTURED_CHARACTER_WORK,
72
+ exceeded: false,
73
+ propertyCache: new WeakMap<object, Map<string, unknown>>(),
74
+ propertyCacheEntries: 0,
75
+ };
76
+ }
77
+
78
+ function consumeStructuredWork(state: StructuredWorkState): boolean {
79
+ if (state.remaining <= 0) {
80
+ state.exceeded = true;
81
+ state.failurePlaceholder ??= '[Traversal limit exceeded]';
82
+ return false;
83
+ }
84
+ state.remaining--;
85
+ return true;
86
+ }
87
+
88
+ function consumeStructuredCharacterWork(state: StructuredWorkState): boolean {
89
+ if (state.characterRemaining <= 0) {
90
+ state.exceeded = true;
91
+ state.failurePlaceholder ??= CHARACTER_WORK_PLACEHOLDER;
92
+ return false;
93
+ }
94
+ state.characterRemaining--;
95
+ return true;
96
+ }
97
+
98
+ function hasExceededStructuredWork(state: StructuredWorkState): boolean {
99
+ return state.exceeded;
100
+ }
101
+
102
+ function conservativeStructuredLength(limit: number): number {
103
+ return limit >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : limit + 1;
104
+ }
105
+
106
+ function jsonStringLength(
107
+ value: string,
108
+ limit: number,
109
+ work: StructuredWorkState
110
+ ): number {
111
+ let length = 2;
112
+ for (let i = 0; i < value.length; i++) {
113
+ if (!consumeStructuredCharacterWork(work)) {
114
+ return conservativeStructuredLength(limit);
115
+ }
116
+ const code = value.charCodeAt(i);
117
+ if (
118
+ code === 0x08 ||
119
+ code === 0x09 ||
120
+ code === 0x0a ||
121
+ code === 0x0c ||
122
+ code === 0x0d ||
123
+ code === 0x22 ||
124
+ code === 0x5c
125
+ ) {
126
+ length += 2;
127
+ } else if (code < 0x20) {
128
+ length += 6;
129
+ } else if (code >= 0xd800 && code <= 0xdbff) {
130
+ if (!consumeStructuredCharacterWork(work)) {
131
+ return conservativeStructuredLength(limit);
132
+ }
133
+ const next = value.charCodeAt(i + 1);
134
+ if (next >= 0xdc00 && next <= 0xdfff) {
135
+ length += 2;
136
+ i++;
137
+ } else {
138
+ length += 6;
139
+ }
140
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
141
+ length += 6;
142
+ } else {
143
+ length++;
144
+ }
145
+ if (length > limit) {
146
+ return conservativeStructuredLength(limit);
147
+ }
148
+ }
149
+ return length;
150
+ }
151
+
152
+ type NativeDateRead = { matched: true; time: number } | { matched: false };
153
+
154
+ function readNativeArrayBufferByteLength(value: unknown): number | undefined {
155
+ if (
156
+ value == null ||
157
+ typeof value !== 'object' ||
158
+ NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER == null
159
+ ) {
160
+ return undefined;
161
+ }
162
+ try {
163
+ return NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value) as number;
164
+ } catch {
165
+ return undefined;
166
+ }
167
+ }
168
+
169
+ function readNativeDate(value: unknown): NativeDateRead {
170
+ if (value == null || typeof value !== 'object') {
171
+ return { matched: false };
172
+ }
173
+ try {
174
+ return { matched: true, time: NATIVE_DATE_GET_TIME.call(value) };
175
+ } catch {
176
+ return { matched: false };
177
+ }
178
+ }
179
+
180
+ function readNativeBoxedString(value: object): string | undefined {
181
+ try {
182
+ return String.prototype.valueOf.call(value);
183
+ } catch {
184
+ return undefined;
185
+ }
186
+ }
187
+
188
+ function readNativeBoxedNumber(value: object): number | undefined {
189
+ try {
190
+ return Number.prototype.valueOf.call(value);
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ }
195
+
196
+ function readNativeBoxedBoolean(value: object): boolean | undefined {
197
+ try {
198
+ return Boolean.prototype.valueOf.call(value);
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ }
203
+
204
+ function estimateStructuredChars(
205
+ value: unknown,
206
+ limit = Number.MAX_SAFE_INTEGER,
207
+ ancestors: object[] = [],
208
+ work = createStructuredWorkState(),
209
+ depth = 0
210
+ ): number {
211
+ if (hasExceededStructuredWork(work)) {
212
+ return conservativeStructuredLength(limit);
213
+ }
214
+ if (typeof value === 'string') {
215
+ return jsonStringLength(value, limit, work);
216
+ }
217
+ if (typeof value === 'bigint') {
218
+ return jsonStringLength(value.toString(), limit, work);
219
+ }
220
+ if (typeof value === 'number') {
221
+ return Number.isFinite(value) ? String(value === 0 ? 0 : value).length : 4;
222
+ }
223
+ if (typeof value === 'boolean' || value == null) {
224
+ return String(value).length;
225
+ }
226
+ if (isProxy(value)) {
227
+ work.exceeded = true;
228
+ work.failurePlaceholder = PROXY_VALUE_PLACEHOLDER;
229
+ return conservativeStructuredLength(limit);
230
+ }
231
+ if (typeof value === 'undefined' || typeof value === 'function') {
232
+ return 4;
233
+ }
234
+ if (typeof value !== 'object') {
235
+ return jsonStringLength(String(value), limit, work);
236
+ }
237
+
238
+ const valueIsArray = isArray(value);
239
+ if (!valueIsArray) {
240
+ const arrayBufferByteLength = readNativeArrayBufferByteLength(value);
241
+ if (arrayBufferByteLength != null) {
242
+ if (!isSafeNativeArrayBuffer(value as ArrayBuffer)) {
243
+ work.exceeded = true;
244
+ return conservativeStructuredLength(limit);
245
+ }
246
+ return Math.min(
247
+ limit + 1,
248
+ Math.ceil((arrayBufferByteLength * 4) / 3) + 32
249
+ );
250
+ }
251
+ if (ArrayBuffer.isView(value)) {
252
+ const view = readSafeNativeArrayBufferView(
253
+ value,
254
+ undefined,
255
+ undefined,
256
+ false
257
+ );
258
+ if (view == null) {
259
+ work.exceeded = true;
260
+ return conservativeStructuredLength(limit);
261
+ }
262
+ return Math.min(limit + 1, Math.ceil((view.byteLength * 4) / 3) + 32);
263
+ }
264
+ const date = readNativeDate(value);
265
+ if (date.matched) {
266
+ if (!isSafeNativeDate(value as Date)) {
267
+ work.exceeded = true;
268
+ return conservativeStructuredLength(limit);
269
+ }
270
+ return Number.isFinite(date.time)
271
+ ? jsonStringLength(Date.prototype.toISOString.call(value), limit, work)
272
+ : 4;
273
+ }
274
+ const boxedString = readNativeBoxedString(value);
275
+ if (boxedString != null) {
276
+ return jsonStringLength(boxedString, limit, work);
277
+ }
278
+ const boxedNumber = readNativeBoxedNumber(value);
279
+ if (boxedNumber != null) {
280
+ return Number.isFinite(boxedNumber)
281
+ ? String(boxedNumber === 0 ? 0 : boxedNumber).length
282
+ : 4;
283
+ }
284
+ const boxedBoolean = readNativeBoxedBoolean(value);
285
+ if (boxedBoolean != null) {
286
+ return String(boxedBoolean).length;
287
+ }
288
+ let prototype: object | null;
289
+ try {
290
+ prototype = Object.getPrototypeOf(value) as object | null;
291
+ } catch {
292
+ work.exceeded = true;
293
+ return conservativeStructuredLength(limit);
294
+ }
295
+ if (
296
+ isProxy(prototype) ||
297
+ (prototype !== Object.prototype && prototype !== null)
298
+ ) {
299
+ work.exceeded = true;
300
+ return conservativeStructuredLength(limit);
301
+ }
302
+ } else {
303
+ let prototype: object | null;
304
+ try {
305
+ prototype = Object.getPrototypeOf(value) as object | null;
306
+ } catch {
307
+ work.exceeded = true;
308
+ return conservativeStructuredLength(limit);
309
+ }
310
+ if (
311
+ isProxy(prototype) ||
312
+ (prototype !== Array.prototype && prototype !== null)
313
+ ) {
314
+ work.exceeded = true;
315
+ return conservativeStructuredLength(limit);
316
+ }
317
+ }
318
+ if (depth >= MAX_STRUCTURED_SERIALIZATION_DEPTH) {
319
+ return jsonStringLength('[Max serialization depth]', limit, work);
320
+ }
321
+ if (ancestors.includes(value)) {
322
+ return 12;
323
+ }
324
+
325
+ ancestors.push(value);
326
+ let length = 2;
327
+ try {
328
+ if (valueIsArray) {
329
+ const arrayLength = readStructuredArrayLength(value, work);
330
+ if (arrayLength == null) {
331
+ return conservativeStructuredLength(limit);
332
+ }
333
+ for (let i = 0; i < arrayLength && length <= limit; i++) {
334
+ if (!consumeStructuredWork(work)) {
335
+ return conservativeStructuredLength(limit);
336
+ }
337
+ if (i > 0) {
338
+ length++;
339
+ }
340
+ length += estimateStructuredChars(
341
+ readStructuredProperty(value, i, work),
342
+ Math.max(0, limit - length),
343
+ ancestors,
344
+ work,
345
+ depth + 1
346
+ );
347
+ }
348
+ } else {
349
+ let emitted = 0;
350
+ for (const key in value) {
351
+ if (!consumeStructuredWork(work)) {
352
+ return conservativeStructuredLength(limit);
353
+ }
354
+ if (!Object.prototype.propertyIsEnumerable.call(value, key)) {
355
+ continue;
356
+ }
357
+ if (length > limit) {
358
+ break;
359
+ }
360
+ const nested = readStructuredProperty(
361
+ value as Record<string, unknown>,
362
+ key,
363
+ work
364
+ );
365
+ if (
366
+ typeof nested === 'undefined' ||
367
+ typeof nested === 'function' ||
368
+ typeof nested === 'symbol'
369
+ ) {
370
+ continue;
371
+ }
372
+ if (emitted > 0) {
373
+ length++;
374
+ }
375
+ length += jsonStringLength(key, Math.max(0, limit - length), work) + 1;
376
+ length += estimateStructuredChars(
377
+ nested,
378
+ Math.max(0, limit - length),
379
+ ancestors,
380
+ work,
381
+ depth + 1
382
+ );
383
+ emitted++;
384
+ }
385
+ }
386
+ } catch {
387
+ work.exceeded = true;
388
+ return conservativeStructuredLength(limit);
389
+ } finally {
390
+ ancestors.pop();
391
+ }
392
+ if (hasExceededStructuredWork(work)) {
393
+ return conservativeStructuredLength(limit);
394
+ }
395
+ return Math.min(limit + 1, length);
396
+ }
397
+
398
+ type StructuredSerializationCollector = {
399
+ totalChars: number;
400
+ prefix: SegmentedStringBuffer;
401
+ suffix: SegmentedStringBuffer;
402
+ prefixLimit: number;
403
+ suffixLimit: number;
404
+ work: StructuredWorkState;
405
+ };
406
+
407
+ type SegmentedStringBuffer = {
408
+ segments: string[];
409
+ head: number;
410
+ pending: string[];
411
+ pendingChars: number;
412
+ length: number;
413
+ };
414
+
415
+ export type BoundedStructuredSerialization = {
416
+ /** Provider-facing head/tail preview, bounded by `maxChars`. */
417
+ content: string;
418
+ /** Exact serialized prefix up to the defensive traversal-work ceiling. */
419
+ prefix: string;
420
+ /** Exact length, or `Number.MAX_SAFE_INTEGER` when traversal was capped. */
421
+ originalChars: number;
422
+ truncated: boolean;
423
+ };
424
+
425
+ function normalizeCharLimit(limit: number): number {
426
+ return Number.isFinite(limit)
427
+ ? Math.max(0, Math.floor(limit))
428
+ : Number.MAX_SAFE_INTEGER;
429
+ }
430
+
431
+ function createSegmentedStringBuffer(): SegmentedStringBuffer {
432
+ return {
433
+ segments: [],
434
+ head: 0,
435
+ pending: [],
436
+ pendingChars: 0,
437
+ length: 0,
438
+ };
439
+ }
440
+
441
+ function flushSegmentedStringBuffer(buffer: SegmentedStringBuffer): void {
442
+ if (buffer.pendingChars === 0) {
443
+ return;
444
+ }
445
+ buffer.segments.push(buffer.pending.join(''));
446
+ buffer.pending = [];
447
+ buffer.pendingChars = 0;
448
+ }
449
+
450
+ function appendToSegmentedStringBuffer(
451
+ buffer: SegmentedStringBuffer,
452
+ chunk: string
453
+ ): void {
454
+ if (chunk.length === 0) {
455
+ return;
456
+ }
457
+ buffer.pending.push(chunk);
458
+ buffer.pendingChars += chunk.length;
459
+ buffer.length += chunk.length;
460
+ if (buffer.pendingChars >= SERIALIZATION_CHUNK_SIZE) {
461
+ flushSegmentedStringBuffer(buffer);
462
+ }
463
+ }
464
+
465
+ function trimSegmentedStringBufferStart(
466
+ buffer: SegmentedStringBuffer,
467
+ chars: number
468
+ ): void {
469
+ let remaining = Math.min(chars, buffer.length);
470
+ if (remaining <= 0) {
471
+ return;
472
+ }
473
+ flushSegmentedStringBuffer(buffer);
474
+ while (remaining > 0 && buffer.head < buffer.segments.length) {
475
+ const segment = buffer.segments[buffer.head];
476
+ if (segment.length <= remaining) {
477
+ remaining -= segment.length;
478
+ buffer.length -= segment.length;
479
+ buffer.head++;
480
+ } else {
481
+ buffer.segments[buffer.head] = segment.slice(remaining);
482
+ buffer.length -= remaining;
483
+ remaining = 0;
484
+ }
485
+ }
486
+ if (buffer.head >= 1_024 && buffer.head * 2 >= buffer.segments.length) {
487
+ buffer.segments = buffer.segments.slice(buffer.head);
488
+ buffer.head = 0;
489
+ }
490
+ }
491
+
492
+ function materializeSegmentedStringBuffer(
493
+ buffer: SegmentedStringBuffer
494
+ ): string {
495
+ const segments = buffer.segments.slice(buffer.head);
496
+ if (buffer.pendingChars > 0) {
497
+ segments.push(buffer.pending.join(''));
498
+ }
499
+ return segments.join('');
500
+ }
501
+
502
+ function appendSerializedChunk(
503
+ collector: StructuredSerializationCollector,
504
+ chunk: string
505
+ ): void {
506
+ if (chunk.length === 0) {
507
+ return;
508
+ }
509
+
510
+ collector.totalChars += chunk.length;
511
+
512
+ const prefixRemaining = collector.prefixLimit - collector.prefix.length;
513
+ if (prefixRemaining > 0) {
514
+ appendToSegmentedStringBuffer(
515
+ collector.prefix,
516
+ chunk.slice(0, prefixRemaining)
517
+ );
518
+ }
519
+
520
+ if (collector.suffixLimit <= 0) {
521
+ return;
522
+ }
523
+ appendToSegmentedStringBuffer(collector.suffix, chunk);
524
+ if (
525
+ collector.suffix.length >=
526
+ collector.suffixLimit + SERIALIZATION_CHUNK_SIZE
527
+ ) {
528
+ trimSegmentedStringBufferStart(
529
+ collector.suffix,
530
+ collector.suffix.length - collector.suffixLimit
531
+ );
532
+ }
533
+ }
534
+
535
+ function appendJsonSafeRange(
536
+ collector: StructuredSerializationCollector,
537
+ value: string,
538
+ start: number,
539
+ end: number
540
+ ): void {
541
+ let offset = start;
542
+ while (offset < end) {
543
+ let chunkEnd = Math.min(end, offset + SERIALIZATION_CHUNK_SIZE);
544
+ if (
545
+ chunkEnd < end &&
546
+ chunkEnd > offset &&
547
+ value.charCodeAt(chunkEnd - 1) >= 0xd800 &&
548
+ value.charCodeAt(chunkEnd - 1) <= 0xdbff &&
549
+ value.charCodeAt(chunkEnd) >= 0xdc00 &&
550
+ value.charCodeAt(chunkEnd) <= 0xdfff
551
+ ) {
552
+ chunkEnd--;
553
+ }
554
+ appendSerializedChunk(collector, value.slice(offset, chunkEnd));
555
+ offset = chunkEnd;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Emits one JSON string incrementally. Calling JSON.stringify on the complete
561
+ * string would allocate an escaped copy before the output cap could apply.
562
+ */
563
+ function appendJsonString(
564
+ collector: StructuredSerializationCollector,
565
+ value: string
566
+ ): void {
567
+ if (collector.work.exceeded) {
568
+ appendSerializedChunk(
569
+ collector,
570
+ `"${collector.work.failurePlaceholder ?? CHARACTER_WORK_PLACEHOLDER}"`
571
+ );
572
+ return;
573
+ }
574
+ appendSerializedChunk(collector, '"');
575
+ let safeStart = 0;
576
+
577
+ for (let i = 0; i < value.length; i++) {
578
+ if (!consumeStructuredCharacterWork(collector.work)) {
579
+ appendJsonSafeRange(collector, value, safeStart, i);
580
+ appendSerializedChunk(collector, `${CHARACTER_WORK_PLACEHOLDER}"`);
581
+ return;
582
+ }
583
+ const code = value.charCodeAt(i);
584
+ let escaped: string | undefined;
585
+ switch (code) {
586
+ case 0x08:
587
+ escaped = '\\b';
588
+ break;
589
+ case 0x09:
590
+ escaped = '\\t';
591
+ break;
592
+ case 0x0a:
593
+ escaped = '\\n';
594
+ break;
595
+ case 0x0c:
596
+ escaped = '\\f';
597
+ break;
598
+ case 0x0d:
599
+ escaped = '\\r';
600
+ break;
601
+ case 0x22:
602
+ escaped = '\\"';
603
+ break;
604
+ case 0x5c:
605
+ escaped = '\\\\';
606
+ break;
607
+ default:
608
+ if (code < 0x20) {
609
+ escaped = `\\u${code.toString(16).padStart(4, '0')}`;
610
+ } else if (code >= 0xd800 && code <= 0xdbff) {
611
+ if (!consumeStructuredCharacterWork(collector.work)) {
612
+ appendJsonSafeRange(collector, value, safeStart, i);
613
+ appendSerializedChunk(collector, `${CHARACTER_WORK_PLACEHOLDER}"`);
614
+ return;
615
+ }
616
+ const next = value.charCodeAt(i + 1);
617
+ if (next >= 0xdc00 && next <= 0xdfff) {
618
+ i++;
619
+ } else {
620
+ escaped = `\\u${code.toString(16).padStart(4, '0')}`;
621
+ }
622
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
623
+ escaped = `\\u${code.toString(16).padStart(4, '0')}`;
624
+ }
625
+ }
626
+
627
+ if (escaped != null) {
628
+ appendJsonSafeRange(collector, value, safeStart, i);
629
+ appendSerializedChunk(collector, escaped);
630
+ safeStart = i + 1;
631
+ }
632
+ }
633
+ appendJsonSafeRange(collector, value, safeStart, value.length);
634
+ appendSerializedChunk(collector, '"');
635
+ }
636
+
637
+ function readStructuredProperty(
638
+ value: Record<string, unknown> | unknown[],
639
+ key: string | number,
640
+ work: StructuredWorkState
641
+ ): unknown {
642
+ const normalizedKey = String(key);
643
+ // Callers still consume traversal work for every occurrence. This bounded
644
+ // cache only avoids re-reading the same descriptor through shared references.
645
+ const cachedProperties = work.propertyCache.get(value);
646
+ if (cachedProperties != null && cachedProperties.has(normalizedKey)) {
647
+ return cachedProperties.get(normalizedKey);
648
+ }
649
+
650
+ try {
651
+ const descriptor = Object.getOwnPropertyDescriptor(value, normalizedKey);
652
+ let propertyValue: unknown;
653
+ if (descriptor == null) {
654
+ propertyValue = undefined;
655
+ } else if ('value' in descriptor) {
656
+ propertyValue = descriptor.value;
657
+ } else {
658
+ propertyValue = '[Property accessor omitted]';
659
+ }
660
+ if (work.propertyCacheEntries < MAX_STRUCTURED_PROPERTY_CACHE_ENTRIES) {
661
+ const properties = cachedProperties ?? new Map<string, unknown>();
662
+ if (cachedProperties == null) {
663
+ work.propertyCache.set(value, properties);
664
+ }
665
+ properties.set(normalizedKey, propertyValue);
666
+ work.propertyCacheEntries++;
667
+ }
668
+ return propertyValue;
669
+ } catch {
670
+ work.exceeded = true;
671
+ work.remaining = 0;
672
+ return '[Unreadable property omitted]';
673
+ }
674
+ }
675
+
676
+ function readStructuredArrayLength(
677
+ value: unknown[],
678
+ work: StructuredWorkState
679
+ ): number | undefined {
680
+ const length = readStructuredProperty(value, 'length', work);
681
+ if (
682
+ typeof length === 'number' &&
683
+ Number.isSafeInteger(length) &&
684
+ length >= 0
685
+ ) {
686
+ return length;
687
+ }
688
+ work.exceeded = true;
689
+ work.remaining = 0;
690
+ return undefined;
691
+ }
692
+
693
+ /**
694
+ * Provider-neutral JSON writer which deliberately does not invoke `toJSON` or
695
+ * property accessors. Native `JSON.stringify` can invoke both before a replacer
696
+ * can bound their output.
697
+ */
698
+ function appendStructuredValue(
699
+ collector: StructuredSerializationCollector,
700
+ value: unknown,
701
+ ancestors: Set<object>,
702
+ depth: number,
703
+ position: 'top' | 'array' | 'object'
704
+ ): boolean {
705
+ if (collector.work.exceeded) {
706
+ appendSerializedChunk(
707
+ collector,
708
+ `"${collector.work.failurePlaceholder ?? CHARACTER_WORK_PLACEHOLDER}"`
709
+ );
710
+ return true;
711
+ }
712
+ if (typeof value === 'string') {
713
+ appendJsonString(collector, value);
714
+ return true;
715
+ }
716
+ if (typeof value === 'bigint') {
717
+ appendJsonString(collector, value.toString());
718
+ return true;
719
+ }
720
+ if (typeof value === 'number') {
721
+ appendSerializedChunk(
722
+ collector,
723
+ Number.isFinite(value) ? String(value === 0 ? 0 : value) : 'null'
724
+ );
725
+ return true;
726
+ }
727
+ if (typeof value === 'boolean' || value === null) {
728
+ appendSerializedChunk(collector, String(value));
729
+ return true;
730
+ }
731
+ if (isProxy(value)) {
732
+ appendSerializedChunk(collector, `"${PROXY_VALUE_PLACEHOLDER}"`);
733
+ collector.work.exceeded = true;
734
+ collector.work.failurePlaceholder = PROXY_VALUE_PLACEHOLDER;
735
+ return true;
736
+ }
737
+ if (
738
+ typeof value === 'undefined' ||
739
+ typeof value === 'function' ||
740
+ typeof value === 'symbol'
741
+ ) {
742
+ if (position === 'object') {
743
+ return false;
744
+ }
745
+ if (position === 'array') {
746
+ appendSerializedChunk(collector, 'null');
747
+ } else if (typeof value === 'undefined') {
748
+ appendSerializedChunk(collector, 'undefined');
749
+ } else {
750
+ appendJsonString(
751
+ collector,
752
+ typeof value === 'function' ? '[Function]' : String(value)
753
+ );
754
+ }
755
+ return true;
756
+ }
757
+ const valueIsArray = isArray(value);
758
+ if (!valueIsArray) {
759
+ const arrayBufferByteLength = readNativeArrayBufferByteLength(value);
760
+ if (arrayBufferByteLength != null) {
761
+ if (!isSafeNativeArrayBuffer(value as ArrayBuffer)) {
762
+ appendSerializedChunk(collector, '"[Unsafe ArrayBuffer omitted]"');
763
+ collector.work.exceeded = true;
764
+ return true;
765
+ }
766
+ appendJsonString(
767
+ collector,
768
+ `[ArrayBuffer: ${arrayBufferByteLength} bytes]`
769
+ );
770
+ return true;
771
+ }
772
+ if (ArrayBuffer.isView(value)) {
773
+ const view = readSafeNativeArrayBufferView(
774
+ value,
775
+ undefined,
776
+ undefined,
777
+ false
778
+ );
779
+ if (view == null) {
780
+ appendSerializedChunk(collector, '"[Unsafe ArrayBufferView omitted]"');
781
+ collector.work.exceeded = true;
782
+ return true;
783
+ }
784
+ appendJsonString(collector, `[${view.name}: ${view.byteLength} bytes]`);
785
+ return true;
786
+ }
787
+ const date = readNativeDate(value);
788
+ if (date.matched) {
789
+ if (!isSafeNativeDate(value as Date)) {
790
+ appendSerializedChunk(collector, '"[Unsafe Date omitted]"');
791
+ collector.work.exceeded = true;
792
+ } else if (!Number.isFinite(date.time)) {
793
+ appendSerializedChunk(collector, 'null');
794
+ } else {
795
+ appendJsonString(collector, Date.prototype.toISOString.call(value));
796
+ }
797
+ return true;
798
+ }
799
+ const boxedString = readNativeBoxedString(value);
800
+ if (boxedString != null) {
801
+ appendJsonString(collector, boxedString);
802
+ return true;
803
+ }
804
+ const boxedNumber = readNativeBoxedNumber(value);
805
+ if (boxedNumber != null) {
806
+ appendSerializedChunk(
807
+ collector,
808
+ Number.isFinite(boxedNumber)
809
+ ? String(boxedNumber === 0 ? 0 : boxedNumber)
810
+ : 'null'
811
+ );
812
+ return true;
813
+ }
814
+ const boxedBoolean = readNativeBoxedBoolean(value);
815
+ if (boxedBoolean != null) {
816
+ appendSerializedChunk(collector, String(boxedBoolean));
817
+ return true;
818
+ }
819
+ let prototype: object | null;
820
+ try {
821
+ prototype = Object.getPrototypeOf(value) as object | null;
822
+ } catch {
823
+ appendSerializedChunk(collector, '"[Unreadable object omitted]"');
824
+ collector.work.exceeded = true;
825
+ return true;
826
+ }
827
+ if (
828
+ isProxy(prototype) ||
829
+ (prototype !== Object.prototype && prototype !== null)
830
+ ) {
831
+ appendSerializedChunk(collector, '"[Unsupported object omitted]"');
832
+ collector.work.exceeded = true;
833
+ return true;
834
+ }
835
+ } else {
836
+ let prototype: object | null;
837
+ try {
838
+ prototype = Object.getPrototypeOf(value) as object | null;
839
+ } catch {
840
+ appendSerializedChunk(collector, '"[Unreadable array omitted]"');
841
+ collector.work.exceeded = true;
842
+ return true;
843
+ }
844
+ if (
845
+ isProxy(prototype) ||
846
+ (prototype !== Array.prototype && prototype !== null)
847
+ ) {
848
+ appendSerializedChunk(collector, '"[Unsafe array omitted]"');
849
+ collector.work.exceeded = true;
850
+ return true;
851
+ }
852
+ }
853
+ if (depth >= MAX_STRUCTURED_SERIALIZATION_DEPTH) {
854
+ appendJsonString(collector, '[Max serialization depth]');
855
+ return true;
856
+ }
857
+ if (ancestors.has(value)) {
858
+ appendJsonString(collector, '[Circular]');
859
+ return true;
860
+ }
861
+
862
+ ancestors.add(value);
863
+ try {
864
+ if (valueIsArray) {
865
+ const arrayLength = readStructuredArrayLength(value, collector.work);
866
+ if (arrayLength == null) {
867
+ appendJsonString(collector, '[Unreadable array omitted]');
868
+ return true;
869
+ }
870
+ appendSerializedChunk(collector, '[');
871
+ for (let i = 0; i < arrayLength; i++) {
872
+ if (!consumeStructuredWork(collector.work)) {
873
+ if (i > 0) {
874
+ appendSerializedChunk(collector, ',');
875
+ }
876
+ appendJsonString(
877
+ collector,
878
+ `[Traversal limit exceeded: ${arrayLength - i} array entries omitted]`
879
+ );
880
+ break;
881
+ }
882
+ if (i > 0) {
883
+ appendSerializedChunk(collector, ',');
884
+ }
885
+ appendStructuredValue(
886
+ collector,
887
+ readStructuredProperty(value, i, collector.work),
888
+ ancestors,
889
+ depth + 1,
890
+ 'array'
891
+ );
892
+ }
893
+ appendSerializedChunk(collector, ']');
894
+ return true;
895
+ }
896
+
897
+ appendSerializedChunk(collector, '{');
898
+ let emitted = 0;
899
+ // `Object.keys()` allocates an array proportional to the complete object
900
+ // before either output limit applies. Filter a streaming `for…in` walk to
901
+ // retain JSON's own-enumerable-key semantics without that extra array.
902
+ for (const key in value) {
903
+ if (!consumeStructuredWork(collector.work)) {
904
+ if (emitted > 0) {
905
+ appendSerializedChunk(collector, ',');
906
+ }
907
+ appendSerializedChunk(
908
+ collector,
909
+ '"_truncated":"[Traversal limit exceeded]"'
910
+ );
911
+ break;
912
+ }
913
+ if (!Object.prototype.propertyIsEnumerable.call(value, key)) {
914
+ continue;
915
+ }
916
+ const nested = readStructuredProperty(
917
+ value as Record<string, unknown>,
918
+ key,
919
+ collector.work
920
+ );
921
+ if (
922
+ typeof nested === 'undefined' ||
923
+ typeof nested === 'function' ||
924
+ typeof nested === 'symbol'
925
+ ) {
926
+ continue;
927
+ }
928
+ if (emitted > 0) {
929
+ appendSerializedChunk(collector, ',');
930
+ }
931
+ appendJsonString(collector, key);
932
+ appendSerializedChunk(collector, ':');
933
+ appendStructuredValue(collector, nested, ancestors, depth + 1, 'object');
934
+ emitted++;
935
+ }
936
+ appendSerializedChunk(collector, '}');
937
+ return true;
938
+ } finally {
939
+ ancestors.delete(value);
940
+ }
941
+ }
942
+
943
+ function formatBoundedStructuredContent(
944
+ collector: StructuredSerializationCollector,
945
+ maxChars: number,
946
+ prefix: string,
947
+ suffix: string
948
+ ): string {
949
+ if (collector.totalChars <= maxChars) {
950
+ return prefix.slice(0, collector.totalChars);
951
+ }
952
+
953
+ const indicator = collector.work.exceeded
954
+ ? '\n\n… [truncated: safe traversal limit exceeded] …\n\n'
955
+ : `\n\n… [truncated: ${collector.totalChars} chars exceeded ` +
956
+ `${maxChars} limit] …\n\n`;
957
+ const available = maxChars - indicator.length;
958
+ if (available <= 0) {
959
+ return prefix.slice(0, maxChars);
960
+ }
961
+ if (available < 200) {
962
+ return prefix.slice(0, available) + indicator.trimEnd();
963
+ }
964
+
965
+ const headSize = Math.ceil(available * 0.7);
966
+ const tailSize = available - headSize;
967
+ let headEnd = headSize;
968
+ const headNewline = prefix.lastIndexOf('\n', headSize);
969
+ if (headNewline > headSize - 200 && headNewline > 0) {
970
+ headEnd = headNewline;
971
+ }
972
+
973
+ let tailStart = Math.max(0, suffix.length - tailSize);
974
+ const tailNewline = suffix.indexOf('\n', tailStart);
975
+ if (tailNewline > 0 && tailNewline < tailStart + 200) {
976
+ tailStart = tailNewline + 1;
977
+ }
978
+
979
+ return prefix.slice(0, headEnd) + indicator + suffix.slice(tailStart);
980
+ }
981
+
982
+ /**
983
+ * Serializes structured output while retaining at most the requested provider
984
+ * preview and registry prefix. The traversal never calls `toJSON` and never
985
+ * materializes the complete serialized string when it exceeds those limits.
986
+ *
987
+ * `prefixChars` is useful for the tool-output registry: it receives the exact
988
+ * serialized prefix up to `registry.perOutputLimit`, while `content` retains
989
+ * the provider-facing head/tail truncation behavior at `maxChars`. Property
990
+ * accessors are represented by a fixed placeholder instead of being invoked.
991
+ */
992
+ export function serializeStructuredValueBounded(
993
+ value: unknown,
994
+ maxChars: number,
995
+ prefixChars = 0,
996
+ work = createStructuredWorkState()
997
+ ): BoundedStructuredSerialization {
998
+ const normalizedMaxChars = normalizeCharLimit(maxChars);
999
+ const normalizedPrefixChars = normalizeCharLimit(prefixChars);
1000
+ const prefixLimit = Math.max(normalizedMaxChars, normalizedPrefixChars);
1001
+ const collector: StructuredSerializationCollector = {
1002
+ totalChars: 0,
1003
+ prefix: createSegmentedStringBuffer(),
1004
+ suffix: createSegmentedStringBuffer(),
1005
+ prefixLimit,
1006
+ suffixLimit:
1007
+ normalizedMaxChars === Number.MAX_SAFE_INTEGER ? 0 : normalizedMaxChars,
1008
+ work,
1009
+ };
1010
+
1011
+ try {
1012
+ appendStructuredValue(collector, value, new Set<object>(), 0, 'top');
1013
+ } catch {
1014
+ collector.totalChars = 0;
1015
+ collector.prefix = createSegmentedStringBuffer();
1016
+ collector.suffix = createSegmentedStringBuffer();
1017
+ collector.work.exceeded = true;
1018
+ appendSerializedChunk(collector, '"[Unserializable structured value]"');
1019
+ }
1020
+
1021
+ const prefix = materializeSegmentedStringBuffer(collector.prefix);
1022
+ const suffix = materializeSegmentedStringBuffer(collector.suffix);
1023
+ const originalChars = collector.work.exceeded
1024
+ ? Number.MAX_SAFE_INTEGER
1025
+ : collector.totalChars;
1026
+ return {
1027
+ content: formatBoundedStructuredContent(
1028
+ collector,
1029
+ normalizedMaxChars,
1030
+ prefix,
1031
+ suffix
1032
+ ),
1033
+ prefix: prefix.slice(0, normalizedPrefixChars),
1034
+ originalChars,
1035
+ truncated:
1036
+ collector.work.exceeded || collector.totalChars > normalizedMaxChars,
1037
+ };
1038
+ }
1039
+
1040
+ function serializeStructuredValueWithinLimit(
1041
+ value: unknown,
1042
+ maxChars: number,
1043
+ work = createStructuredWorkState()
1044
+ ): string {
1045
+ return serializeStructuredValueBounded(value, maxChars, 0, work).content;
1046
+ }
1047
+
1048
+ function isTextBlockUnchecked(value: unknown): value is TextToolContentBlock {
1049
+ if (!isRecord(value) || isProxy(value)) {
1050
+ return false;
1051
+ }
1052
+ try {
1053
+ const prototype = Object.getPrototypeOf(value);
1054
+ if (prototype !== Object.prototype && prototype !== null) {
1055
+ return false;
1056
+ }
1057
+ const keys = Object.keys(value);
1058
+ if (keys.length !== 2 || !keys.includes('type') || !keys.includes('text')) {
1059
+ return false;
1060
+ }
1061
+ const type = readOwnEnumerableDataProperty(value, 'type');
1062
+ const text = readOwnEnumerableDataProperty(value, 'text');
1063
+ return (
1064
+ type.found &&
1065
+ type.value === 'text' &&
1066
+ text.found &&
1067
+ typeof text.value === 'string'
1068
+ );
1069
+ } catch {
1070
+ return false;
1071
+ }
1072
+ }
1073
+
1074
+ function isArray(value: unknown): value is unknown[] {
1075
+ try {
1076
+ return Array.isArray(value);
1077
+ } catch {
1078
+ return false;
1079
+ }
1080
+ }
1081
+
1082
+ function getDenseTextBlockArrayLength(content: unknown[]): number | undefined {
1083
+ if (isProxy(content)) {
1084
+ return undefined;
1085
+ }
1086
+ const validation = createToolBlockClassificationContext();
1087
+ const containerWork = { value: MAX_ATOMIC_VALIDATION_WORK };
1088
+ let contentLength: number;
1089
+ try {
1090
+ const prototype = Object.getPrototypeOf(content);
1091
+ if (
1092
+ (prototype !== Array.prototype && prototype !== null) ||
1093
+ hasPotentiallyCallableToJSON(
1094
+ content,
1095
+ containerWork,
1096
+ validation.totalRemaining
1097
+ )
1098
+ ) {
1099
+ return undefined;
1100
+ }
1101
+ contentLength = content.length;
1102
+ } catch {
1103
+ return undefined;
1104
+ }
1105
+ if (
1106
+ contentLength === 0 ||
1107
+ contentLength > MAX_STRUCTURED_SERIALIZATION_WORK
1108
+ ) {
1109
+ return undefined;
1110
+ }
1111
+
1112
+ let length = contentLength - 1;
1113
+ try {
1114
+ for (let i = 0; i < contentLength; i++) {
1115
+ const descriptor = Object.getOwnPropertyDescriptor(content, String(i));
1116
+ if (descriptor == null || !('value' in descriptor)) {
1117
+ return undefined;
1118
+ }
1119
+ const block = descriptor.value;
1120
+ if (
1121
+ classifyToolContentBlock(block, validation) !== TOOL_BLOCK_TEXT ||
1122
+ validation.totalRemaining.value <= 0
1123
+ ) {
1124
+ return undefined;
1125
+ }
1126
+ const text = readOwnEnumerableDataProperty(block, 'text');
1127
+ length += typeof text.value === 'string' ? text.value.length : 0;
1128
+ if (length >= Number.MAX_SAFE_INTEGER) {
1129
+ return Number.MAX_SAFE_INTEGER;
1130
+ }
1131
+ }
1132
+ } catch {
1133
+ return undefined;
1134
+ }
1135
+ return length;
1136
+ }
1137
+
1138
+ function isRecord(value: unknown): value is Record<string, unknown> {
1139
+ return typeof value === 'object' && value != null;
1140
+ }
1141
+
1142
+ type OwnDataProperty =
1143
+ | { found: true; value: unknown }
1144
+ | { found: false; value?: never };
1145
+
1146
+ function readOwnEnumerableDataProperty(
1147
+ record: Record<string, unknown>,
1148
+ key: string
1149
+ ): OwnDataProperty {
1150
+ try {
1151
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
1152
+ if (
1153
+ descriptor == null ||
1154
+ descriptor.enumerable !== true ||
1155
+ !('value' in descriptor)
1156
+ ) {
1157
+ return { found: false };
1158
+ }
1159
+ return { found: true, value: descriptor.value };
1160
+ } catch {
1161
+ return { found: false };
1162
+ }
1163
+ }
1164
+
1165
+ function hasString(record: Record<string, unknown>, key: string): boolean {
1166
+ const property = readOwnEnumerableDataProperty(record, key);
1167
+ return (
1168
+ property.found &&
1169
+ typeof property.value === 'string' &&
1170
+ property.value !== ''
1171
+ );
1172
+ }
1173
+
1174
+ function hasValue(record: Record<string, unknown>, key: string): boolean {
1175
+ const property = readOwnEnumerableDataProperty(record, key);
1176
+ return property.found && property.value !== undefined;
1177
+ }
1178
+
1179
+ function isStringPayload(value: unknown): boolean {
1180
+ return typeof value === 'string' && value.length > 0;
1181
+ }
1182
+
1183
+ function isBinaryPayload(value: unknown): boolean {
1184
+ if (typeof value === 'string') {
1185
+ return value.length > 0;
1186
+ }
1187
+ if (isArray(value)) {
1188
+ return false;
1189
+ }
1190
+ const arrayBufferByteLength = readNativeArrayBufferByteLength(value);
1191
+ if (arrayBufferByteLength != null) {
1192
+ return (
1193
+ isSafeNativeArrayBuffer(value as ArrayBuffer) && arrayBufferByteLength > 0
1194
+ );
1195
+ }
1196
+ if (ArrayBuffer.isView(value)) {
1197
+ const view = readSafeNativeArrayBufferView(value);
1198
+ return view != null && view.byteLength > 0;
1199
+ }
1200
+ return false;
1201
+ }
1202
+
1203
+ function isContentPayload(value: unknown): boolean {
1204
+ return isArray(value) && value.length > 0;
1205
+ }
1206
+
1207
+ function hasPayloadPath(
1208
+ record: Record<string, unknown>,
1209
+ path: string[],
1210
+ validator: (value: unknown) => boolean = isBinaryPayload
1211
+ ): boolean {
1212
+ let value: unknown = record;
1213
+ for (const key of path) {
1214
+ if (!isRecord(value)) {
1215
+ return false;
1216
+ }
1217
+ const property = readOwnEnumerableDataProperty(value, key);
1218
+ if (!property.found) {
1219
+ return false;
1220
+ }
1221
+ value = property.value;
1222
+ }
1223
+ return validator(value);
1224
+ }
1225
+
1226
+ function hasAnyPayloadPath(
1227
+ record: Record<string, unknown>,
1228
+ paths: string[][],
1229
+ validator: (value: unknown) => boolean = isBinaryPayload
1230
+ ): boolean {
1231
+ return paths.some((path) => hasPayloadPath(record, path, validator));
1232
+ }
1233
+
1234
+ function isProviderNativeToolResultBlock(
1235
+ record: Record<string, unknown>,
1236
+ type: string
1237
+ ): boolean {
1238
+ if (type === 'search_result') {
1239
+ return (
1240
+ hasString(record, 'title') &&
1241
+ hasString(record, 'source') &&
1242
+ hasPayloadPath(record, ['content'], isArray)
1243
+ );
1244
+ }
1245
+ if (type === 'web_search_result') {
1246
+ return hasString(record, 'url');
1247
+ }
1248
+ if (type === 'web_search_tool_result') {
1249
+ const content = readOwnEnumerableDataProperty(record, 'content');
1250
+ return (
1251
+ hasString(record, 'tool_use_id') &&
1252
+ content.found &&
1253
+ (isArray(content.value) || isRecord(content.value))
1254
+ );
1255
+ }
1256
+ if (type === 'tool_result') {
1257
+ return hasString(record, 'tool_use_id');
1258
+ }
1259
+ if (type === 'server_tool_call_result') {
1260
+ const status = readOwnEnumerableDataProperty(record, 'status');
1261
+ return (
1262
+ hasString(record, 'toolCallId') &&
1263
+ status.found &&
1264
+ (status.value === 'success' || status.value === 'error') &&
1265
+ hasValue(record, 'output')
1266
+ );
1267
+ }
1268
+ if (type === 'toolResponse') {
1269
+ const response = readOwnEnumerableDataProperty(record, 'toolResponse');
1270
+ return response.found && isRecord(response.value);
1271
+ }
1272
+ return false;
1273
+ }
1274
+
1275
+ const MAX_ATOMIC_VALIDATION_WORK = 10_000;
1276
+ const MAX_ATOMIC_VALIDATION_DEPTH = 50;
1277
+ type AtomicValidationWork = { value: number };
1278
+ const NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
1279
+ ArrayBuffer.prototype,
1280
+ 'byteLength'
1281
+ )?.get;
1282
+ const NATIVE_TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(
1283
+ Uint8Array.prototype
1284
+ ) as object;
1285
+ const NATIVE_TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
1286
+ NATIVE_TYPED_ARRAY_PROTOTYPE,
1287
+ 'byteLength'
1288
+ )?.get;
1289
+ const NATIVE_DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
1290
+ DataView.prototype,
1291
+ 'byteLength'
1292
+ )?.get;
1293
+ const NATIVE_ARRAY_BUFFER_VIEW_NAMES = new Map<object, string>([
1294
+ [Buffer.prototype, 'Buffer'],
1295
+ [DataView.prototype, 'DataView'],
1296
+ [Int8Array.prototype, 'Int8Array'],
1297
+ [Uint8Array.prototype, 'Uint8Array'],
1298
+ [Uint8ClampedArray.prototype, 'Uint8ClampedArray'],
1299
+ [Int16Array.prototype, 'Int16Array'],
1300
+ [Uint16Array.prototype, 'Uint16Array'],
1301
+ [Int32Array.prototype, 'Int32Array'],
1302
+ [Uint32Array.prototype, 'Uint32Array'],
1303
+ [Float32Array.prototype, 'Float32Array'],
1304
+ [Float64Array.prototype, 'Float64Array'],
1305
+ [BigInt64Array.prototype, 'BigInt64Array'],
1306
+ [BigUint64Array.prototype, 'BigUint64Array'],
1307
+ ]);
1308
+ const NATIVE_DATE_GET_TIME = Date.prototype.getTime;
1309
+ const NATIVE_DATE_TO_JSON = Date.prototype.toJSON;
1310
+ const NATIVE_DATE_TO_ISO_STRING = Date.prototype.toISOString;
1311
+ const NATIVE_DATE_VALUE_OF = Date.prototype.valueOf;
1312
+ const NATIVE_DATE_TO_STRING = Date.prototype.toString;
1313
+ const NATIVE_DATE_TO_PRIMITIVE = Date.prototype[Symbol.toPrimitive];
1314
+ const NATIVE_DATE_PROTOTYPE_METHODS: ReadonlyArray<
1315
+ readonly [PropertyKey, unknown]
1316
+ > = [
1317
+ ['getTime', NATIVE_DATE_GET_TIME],
1318
+ ['toJSON', NATIVE_DATE_TO_JSON],
1319
+ ['toISOString', NATIVE_DATE_TO_ISO_STRING],
1320
+ ['valueOf', NATIVE_DATE_VALUE_OF],
1321
+ ['toString', NATIVE_DATE_TO_STRING],
1322
+ [Symbol.toPrimitive, NATIVE_DATE_TO_PRIMITIVE],
1323
+ ];
1324
+
1325
+ function consumeAtomicValidationWork(
1326
+ remaining: AtomicValidationWork,
1327
+ totalRemaining: AtomicValidationWork | undefined,
1328
+ amount = 1
1329
+ ): boolean {
1330
+ if (
1331
+ amount > remaining.value ||
1332
+ (totalRemaining != null && amount > totalRemaining.value)
1333
+ ) {
1334
+ remaining.value = 0;
1335
+ if (totalRemaining != null) {
1336
+ totalRemaining.value = 0;
1337
+ }
1338
+ return false;
1339
+ }
1340
+ remaining.value -= amount;
1341
+ if (totalRemaining != null) {
1342
+ totalRemaining.value -= amount;
1343
+ }
1344
+ return true;
1345
+ }
1346
+
1347
+ function hasPotentiallyCallableToJSON(
1348
+ value: object,
1349
+ remaining?: AtomicValidationWork,
1350
+ totalRemaining?: AtomicValidationWork
1351
+ ): boolean {
1352
+ let current: object | null = value;
1353
+ const seen = new Set<object>();
1354
+ for (let depth = 0; current != null && depth < 100; depth++) {
1355
+ if (isProxy(current)) {
1356
+ return true;
1357
+ }
1358
+ if (
1359
+ remaining != null &&
1360
+ !consumeAtomicValidationWork(remaining, totalRemaining)
1361
+ ) {
1362
+ return true;
1363
+ }
1364
+ if (seen.has(current)) {
1365
+ return true;
1366
+ }
1367
+ seen.add(current);
1368
+ const descriptor = Object.getOwnPropertyDescriptor(current, 'toJSON');
1369
+ if (descriptor != null) {
1370
+ return (
1371
+ typeof descriptor.value === 'function' ||
1372
+ typeof descriptor.get === 'function'
1373
+ );
1374
+ }
1375
+ current = Object.getPrototypeOf(current) as object | null;
1376
+ }
1377
+ return current != null;
1378
+ }
1379
+
1380
+ type NativeArrayBufferViewRead = {
1381
+ byteLength: number;
1382
+ name: string;
1383
+ };
1384
+
1385
+ function readSafeNativeArrayBufferView(
1386
+ value: ArrayBufferView,
1387
+ remaining?: AtomicValidationWork,
1388
+ totalRemaining?: AtomicValidationWork,
1389
+ rejectCallableToJSON = true
1390
+ ): NativeArrayBufferViewRead | undefined {
1391
+ try {
1392
+ const prototype = Object.getPrototypeOf(value) as object | null;
1393
+ if (prototype == null || isProxy(prototype)) {
1394
+ return undefined;
1395
+ }
1396
+ const name = NATIVE_ARRAY_BUFFER_VIEW_NAMES.get(prototype);
1397
+ if (
1398
+ name == null ||
1399
+ Object.getOwnPropertyDescriptor(value, 'byteLength') != null ||
1400
+ Object.getOwnPropertyDescriptor(value, 'constructor') != null ||
1401
+ Object.getOwnPropertyDescriptor(value, 'toJSON') != null ||
1402
+ (rejectCallableToJSON &&
1403
+ hasPotentiallyCallableToJSON(value, remaining, totalRemaining))
1404
+ ) {
1405
+ return undefined;
1406
+ }
1407
+ const getter =
1408
+ prototype === DataView.prototype
1409
+ ? NATIVE_DATA_VIEW_BYTE_LENGTH_GETTER
1410
+ : NATIVE_TYPED_ARRAY_BYTE_LENGTH_GETTER;
1411
+ if (getter == null) {
1412
+ return undefined;
1413
+ }
1414
+ const byteLength = getter.call(value) as number;
1415
+ return Number.isSafeInteger(byteLength) && byteLength >= 0
1416
+ ? { byteLength, name }
1417
+ : undefined;
1418
+ } catch {
1419
+ return undefined;
1420
+ }
1421
+ }
1422
+
1423
+ function isSafeNativeArrayBuffer(
1424
+ value: ArrayBuffer,
1425
+ remaining?: AtomicValidationWork,
1426
+ totalRemaining?: AtomicValidationWork
1427
+ ): boolean {
1428
+ try {
1429
+ if (
1430
+ Object.getPrototypeOf(value) !== ArrayBuffer.prototype ||
1431
+ NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER == null ||
1432
+ Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength')
1433
+ ?.get !== NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER ||
1434
+ hasPotentiallyCallableToJSON(value, remaining, totalRemaining)
1435
+ ) {
1436
+ return false;
1437
+ }
1438
+ NATIVE_ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value);
1439
+ for (const key in value) {
1440
+ if (
1441
+ remaining != null &&
1442
+ !consumeAtomicValidationWork(remaining, totalRemaining)
1443
+ ) {
1444
+ return false;
1445
+ }
1446
+ if (Object.prototype.propertyIsEnumerable.call(value, key)) {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ return Object.getOwnPropertyDescriptor(value, 'byteLength') == null;
1451
+ } catch {
1452
+ return false;
1453
+ }
1454
+ }
1455
+
1456
+ function isSafeNativeDate(value: Date): boolean {
1457
+ try {
1458
+ if (Object.getPrototypeOf(value) !== Date.prototype) {
1459
+ return false;
1460
+ }
1461
+ NATIVE_DATE_GET_TIME.call(value);
1462
+ for (const [key, nativeMethod] of NATIVE_DATE_PROTOTYPE_METHODS) {
1463
+ if (
1464
+ Object.getOwnPropertyDescriptor(value, key) != null ||
1465
+ Object.getOwnPropertyDescriptor(Date.prototype, key)?.value !==
1466
+ nativeMethod
1467
+ ) {
1468
+ return false;
1469
+ }
1470
+ }
1471
+ return true;
1472
+ } catch {
1473
+ return false;
1474
+ }
1475
+ }
1476
+
1477
+ function hasUnsafeCallableToJSON(
1478
+ value: unknown,
1479
+ seen = new Set<object>(),
1480
+ remaining = { value: MAX_ATOMIC_VALIDATION_WORK },
1481
+ depth = 0,
1482
+ totalRemaining?: AtomicValidationWork
1483
+ ): boolean {
1484
+ if (isProxy(value)) {
1485
+ return true;
1486
+ }
1487
+ if (!isRecord(value)) {
1488
+ return false;
1489
+ }
1490
+ if (
1491
+ seen.has(value) ||
1492
+ depth >= MAX_ATOMIC_VALIDATION_DEPTH ||
1493
+ !consumeAtomicValidationWork(remaining, totalRemaining)
1494
+ ) {
1495
+ return true;
1496
+ }
1497
+ const valueIsArray = isArray(value);
1498
+ if (!valueIsArray) {
1499
+ const arrayBufferByteLength = readNativeArrayBufferByteLength(value);
1500
+ if (arrayBufferByteLength != null) {
1501
+ return !isSafeNativeArrayBuffer(
1502
+ value as unknown as ArrayBuffer,
1503
+ remaining,
1504
+ totalRemaining
1505
+ );
1506
+ }
1507
+ const date = readNativeDate(value);
1508
+ if (date.matched) {
1509
+ return !isSafeNativeDate(value as unknown as Date);
1510
+ }
1511
+ if (ArrayBuffer.isView(value)) {
1512
+ return (
1513
+ readSafeNativeArrayBufferView(value, remaining, totalRemaining) == null
1514
+ );
1515
+ }
1516
+ }
1517
+
1518
+ seen.add(value);
1519
+ try {
1520
+ if (hasPotentiallyCallableToJSON(value, remaining, totalRemaining)) {
1521
+ return true;
1522
+ }
1523
+ if (valueIsArray) {
1524
+ const prototype = Object.getPrototypeOf(value);
1525
+ if (prototype !== Array.prototype && prototype !== null) {
1526
+ return true;
1527
+ }
1528
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
1529
+ const length = lengthDescriptor?.value;
1530
+ if (
1531
+ typeof length !== 'number' ||
1532
+ !Number.isSafeInteger(length) ||
1533
+ length < 0 ||
1534
+ !consumeAtomicValidationWork(remaining, totalRemaining, length)
1535
+ ) {
1536
+ return true;
1537
+ }
1538
+ for (let i = 0; i < length; i++) {
1539
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
1540
+ if (
1541
+ descriptor == null ||
1542
+ typeof descriptor.get === 'function' ||
1543
+ typeof descriptor.set === 'function'
1544
+ ) {
1545
+ return true;
1546
+ }
1547
+ if (
1548
+ hasUnsafeCallableToJSON(
1549
+ descriptor.value,
1550
+ seen,
1551
+ remaining,
1552
+ depth + 1,
1553
+ totalRemaining
1554
+ )
1555
+ ) {
1556
+ return true;
1557
+ }
1558
+ }
1559
+ return false;
1560
+ }
1561
+
1562
+ const prototype = Object.getPrototypeOf(value);
1563
+ if (prototype !== Object.prototype && prototype !== null) {
1564
+ return true;
1565
+ }
1566
+ for (const key in value) {
1567
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1568
+ if (descriptor == null) {
1569
+ return true;
1570
+ }
1571
+ if (descriptor.enumerable !== true) {
1572
+ continue;
1573
+ }
1574
+ if (
1575
+ typeof descriptor.get === 'function' ||
1576
+ typeof descriptor.set === 'function' ||
1577
+ !consumeAtomicValidationWork(remaining, totalRemaining)
1578
+ ) {
1579
+ return true;
1580
+ }
1581
+ if (
1582
+ hasUnsafeCallableToJSON(
1583
+ descriptor.value,
1584
+ seen,
1585
+ remaining,
1586
+ depth + 1,
1587
+ totalRemaining
1588
+ )
1589
+ ) {
1590
+ return true;
1591
+ }
1592
+ }
1593
+ return false;
1594
+ } catch {
1595
+ return true;
1596
+ } finally {
1597
+ seen.delete(value);
1598
+ }
1599
+ }
1600
+
1601
+ export function isAtomicToolContentBlock(value: unknown): boolean {
1602
+ try {
1603
+ return (
1604
+ !hasUnsafeCallableToJSON(value) &&
1605
+ isAtomicToolContentBlockUnchecked(value)
1606
+ );
1607
+ } catch {
1608
+ return false;
1609
+ }
1610
+ }
1611
+
1612
+ function isAtomicToolContentBlockUnchecked(value: unknown): boolean {
1613
+ if (!isRecord(value)) {
1614
+ return false;
1615
+ }
1616
+ const record = value;
1617
+ const typeProperty = readOwnEnumerableDataProperty(record, 'type');
1618
+ if (!typeProperty.found) {
1619
+ const cachePointProperty = readOwnEnumerableDataProperty(
1620
+ record,
1621
+ 'cachePoint'
1622
+ );
1623
+ if (!cachePointProperty.found || !isRecord(cachePointProperty.value)) {
1624
+ return false;
1625
+ }
1626
+ const cachePointType = readOwnEnumerableDataProperty(
1627
+ cachePointProperty.value,
1628
+ 'type'
1629
+ );
1630
+ return cachePointType.found && cachePointType.value === 'default';
1631
+ }
1632
+ const type = typeof typeProperty.value === 'string' ? typeProperty.value : '';
1633
+ if (type.length > MAX_TOOL_CONTENT_TYPE_CHARS) {
1634
+ return false;
1635
+ }
1636
+ const slashIndex = type.indexOf('/');
1637
+ const isMimeType =
1638
+ slashIndex > 0 &&
1639
+ slashIndex < type.length - 1 &&
1640
+ slashIndex === type.lastIndexOf('/');
1641
+ if (
1642
+ !ATOMIC_CONTENT_TYPES.has(type) &&
1643
+ !PROVIDER_NATIVE_TOOL_RESULT_TYPES.has(type) &&
1644
+ !isMimeType
1645
+ ) {
1646
+ return false;
1647
+ }
1648
+
1649
+ if (PROVIDER_NATIVE_TOOL_RESULT_TYPES.has(type)) {
1650
+ return isProviderNativeToolResultBlock(record, type);
1651
+ }
1652
+
1653
+ if (type === 'computer_screenshot') {
1654
+ return hasAnyPayloadPath(
1655
+ record,
1656
+ [['image_url'], ['file_id']],
1657
+ isStringPayload
1658
+ );
1659
+ }
1660
+ if (type === 'input_image') {
1661
+ return hasAnyPayloadPath(
1662
+ record,
1663
+ [['image_url'], ['file_id']],
1664
+ isStringPayload
1665
+ );
1666
+ }
1667
+ if (type === 'image_url') {
1668
+ return hasAnyPayloadPath(
1669
+ record,
1670
+ [['image_url'], ['image_url', 'url']],
1671
+ isStringPayload
1672
+ );
1673
+ }
1674
+ if (type === 'image_file') {
1675
+ return hasAnyPayloadPath(
1676
+ record,
1677
+ [
1678
+ ['image_file', 'file_id'],
1679
+ ['image_file', 'fileId'],
1680
+ ],
1681
+ isStringPayload
1682
+ );
1683
+ }
1684
+ if (type === 'input_audio') {
1685
+ return hasAnyPayloadPath(record, [
1686
+ ['input_audio', 'data'],
1687
+ ['input_audio', 'bytes'],
1688
+ ]);
1689
+ }
1690
+ if (type === 'resource_link') {
1691
+ return hasString(record, 'uri');
1692
+ }
1693
+ if (type === 'video_url') {
1694
+ return hasAnyPayloadPath(
1695
+ record,
1696
+ [['video_url'], ['video_url', 'url']],
1697
+ isStringPayload
1698
+ );
1699
+ }
1700
+ if (type === 'resource') {
1701
+ return (
1702
+ hasAnyPayloadPath(record, [
1703
+ ['resource', 'blob'],
1704
+ ['resource', 'bytes'],
1705
+ ['resource', 'data'],
1706
+ ['resource', 'text'],
1707
+ ]) || hasPayloadPath(record, ['resource', 'content'], isContentPayload)
1708
+ );
1709
+ }
1710
+
1711
+ if (isMimeType) {
1712
+ return hasPayloadPath(record, ['data'], isStringPayload);
1713
+ }
1714
+
1715
+ if (type === 'image' || type === 'audio' || type === 'video') {
1716
+ return hasAnyPayloadPath(record, [
1717
+ ['data'],
1718
+ ['bytes'],
1719
+ ['url'],
1720
+ ['source', 'data'],
1721
+ ['source', 'bytes'],
1722
+ ['source', 'url'],
1723
+ [type, 'data'],
1724
+ [type, 'bytes'],
1725
+ [type, 'url'],
1726
+ [type, 'source', 'data'],
1727
+ [type, 'source', 'bytes'],
1728
+ [type, 'source', 'url'],
1729
+ ]);
1730
+ }
1731
+
1732
+ if (type === 'media') {
1733
+ const hasMimeType =
1734
+ hasString(record, 'mimeType') || hasString(record, 'mime_type');
1735
+ return (
1736
+ hasMimeType &&
1737
+ hasAnyPayloadPath(record, [
1738
+ ['data'],
1739
+ ['bytes'],
1740
+ ['fileUri'],
1741
+ ['media', 'data'],
1742
+ ['media', 'bytes'],
1743
+ ['media', 'fileUri'],
1744
+ ['media', 'source', 'bytes'],
1745
+ ])
1746
+ );
1747
+ }
1748
+
1749
+ if (type === 'document' || type === 'file') {
1750
+ const nestedType = type;
1751
+ return (
1752
+ hasAnyPayloadPath(record, [
1753
+ ['data'],
1754
+ ['bytes'],
1755
+ ['url'],
1756
+ ['file_data'],
1757
+ ['file_id'],
1758
+ ['fileId'],
1759
+ ['fileUri'],
1760
+ ['source', 'data'],
1761
+ ['source', 'bytes'],
1762
+ ['source', 'url'],
1763
+ ['source', 'file_data'],
1764
+ ['source', 'file_id'],
1765
+ ['source', 'fileId'],
1766
+ ['source', 'fileUri'],
1767
+ [nestedType, 'data'],
1768
+ [nestedType, 'bytes'],
1769
+ [nestedType, 'url'],
1770
+ [nestedType, 'file_data'],
1771
+ [nestedType, 'file_id'],
1772
+ [nestedType, 'fileId'],
1773
+ [nestedType, 'fileUri'],
1774
+ [nestedType, 'source', 'data'],
1775
+ [nestedType, 'source', 'bytes'],
1776
+ [nestedType, 'source', 'url'],
1777
+ ]) ||
1778
+ hasPayloadPath(record, ['source', 'content'], isContentPayload) ||
1779
+ hasPayloadPath(
1780
+ record,
1781
+ [nestedType, 'source', 'content'],
1782
+ isContentPayload
1783
+ ) ||
1784
+ (readOwnEnumerableDataProperty(record, 'source_type').value === 'text' &&
1785
+ hasString(record, 'text')) ||
1786
+ (readOwnEnumerableDataProperty(record, 'source_type').value === 'id' &&
1787
+ hasString(record, 'id'))
1788
+ );
1789
+ }
1790
+
1791
+ return false;
1792
+ }
1793
+
1794
+ const TOOL_BLOCK_OPAQUE = 0;
1795
+ const TOOL_BLOCK_TEXT = 1;
1796
+ const TOOL_BLOCK_ATOMIC = 2;
1797
+
1798
+ type ToolBlockClassificationContext = {
1799
+ totalRemaining: AtomicValidationWork;
1800
+ cache: WeakMap<object, number>;
1801
+ };
1802
+
1803
+ function createToolBlockClassificationContext(): ToolBlockClassificationContext {
1804
+ return {
1805
+ totalRemaining: { value: MAX_STRUCTURED_SERIALIZATION_WORK },
1806
+ cache: new WeakMap<object, number>(),
1807
+ };
1808
+ }
1809
+
1810
+ function classifyToolContentBlock(
1811
+ value: unknown,
1812
+ context: ToolBlockClassificationContext
1813
+ ): number {
1814
+ if (isRecord(value)) {
1815
+ const cached = context.cache.get(value);
1816
+ if (cached != null) {
1817
+ return cached;
1818
+ }
1819
+ }
1820
+ const remaining = { value: MAX_ATOMIC_VALIDATION_WORK };
1821
+ let kind = TOOL_BLOCK_OPAQUE;
1822
+ try {
1823
+ if (
1824
+ hasUnsafeCallableToJSON(
1825
+ value,
1826
+ new Set<object>(),
1827
+ remaining,
1828
+ 0,
1829
+ context.totalRemaining
1830
+ )
1831
+ ) {
1832
+ kind = TOOL_BLOCK_OPAQUE;
1833
+ } else if (isAtomicToolContentBlockUnchecked(value)) {
1834
+ kind = TOOL_BLOCK_ATOMIC;
1835
+ } else {
1836
+ kind = isTextBlockUnchecked(value) ? TOOL_BLOCK_TEXT : TOOL_BLOCK_OPAQUE;
1837
+ }
1838
+ } catch {
1839
+ kind = TOOL_BLOCK_OPAQUE;
1840
+ }
1841
+ if (isRecord(value) && context.totalRemaining.value > 0) {
1842
+ context.cache.set(value, kind);
1843
+ }
1844
+ return kind;
1845
+ }
1846
+
1847
+ /**
1848
+ * Produces the text representation providers use for structured tool results.
1849
+ * Text-only block arrays stay readable; opaque blocks use canonical JSON.
1850
+ */
1851
+ export function serializeToolContent(content: unknown): string {
1852
+ if (typeof content === 'string') {
1853
+ return content;
1854
+ }
1855
+ if (!isArray(content)) {
1856
+ return serializeStructuredValue(content);
1857
+ }
1858
+ if (getDenseTextBlockArrayLength(content) != null) {
1859
+ return (content as TextToolContentBlock[])
1860
+ .map((block) => block.text)
1861
+ .join('\n');
1862
+ }
1863
+ return serializeStructuredValue(content);
1864
+ }
1865
+
1866
+ export function getToolContentCharLength(content: unknown): number {
1867
+ if (typeof content === 'string') {
1868
+ return content.length;
1869
+ }
1870
+ if (isArray(content)) {
1871
+ const denseTextLength = getDenseTextBlockArrayLength(content);
1872
+ if (denseTextLength != null) {
1873
+ return denseTextLength;
1874
+ }
1875
+ }
1876
+ try {
1877
+ return estimateStructuredChars(content);
1878
+ } catch {
1879
+ return Number.MAX_SAFE_INTEGER;
1880
+ }
1881
+ }
1882
+
1883
+ /**
1884
+ * Produces provider-facing tool-result text without ever exceeding `maxChars`.
1885
+ */
1886
+ export function serializeToolContentBounded(
1887
+ content: unknown,
1888
+ maxChars: number
1889
+ ): string {
1890
+ const normalizedMaxChars = normalizeCharLimit(maxChars);
1891
+ if (typeof content === 'string') {
1892
+ return truncateToolResultContent(content, normalizedMaxChars);
1893
+ }
1894
+ if (!isArray(content)) {
1895
+ return serializeStructuredValueWithinLimit(content, normalizedMaxChars);
1896
+ }
1897
+
1898
+ const originalChars = getDenseTextBlockArrayLength(content);
1899
+ if (originalChars == null) {
1900
+ return serializeStructuredValueWithinLimit(content, normalizedMaxChars);
1901
+ }
1902
+ return serializeDenseTextBlocksWithinLimit(
1903
+ content as TextToolContentBlock[],
1904
+ originalChars,
1905
+ normalizedMaxChars
1906
+ );
1907
+ }
1908
+
1909
+ function serializeDenseTextBlocksWithinLimit(
1910
+ textBlocks: readonly TextToolContentBlock[],
1911
+ originalChars: number,
1912
+ normalizedMaxChars: number
1913
+ ): string {
1914
+ if (originalChars <= normalizedMaxChars) {
1915
+ return textBlocks.map((block) => block.text).join('\n');
1916
+ }
1917
+ const indicator =
1918
+ `\n\n… [truncated: ${originalChars} chars exceeded ` +
1919
+ `${normalizedMaxChars} limit] …`;
1920
+ const available = Math.max(0, normalizedMaxChars - indicator.length);
1921
+ let preview = '';
1922
+ for (let i = 0; i < textBlocks.length && preview.length < available; i++) {
1923
+ if (i > 0) {
1924
+ preview += '\n';
1925
+ }
1926
+ preview += textBlocks[i].text.slice(0, available - preview.length);
1927
+ }
1928
+ return (preview + indicator).slice(0, normalizedMaxChars);
1929
+ }
1930
+
1931
+ export type CompactToolContentResult = {
1932
+ content: ToolContent;
1933
+ changed: boolean;
1934
+ originalChars: number;
1935
+ };
1936
+
1937
+ function isProviderImageUrl(value: unknown): value is string {
1938
+ if (typeof value !== 'string' || value === '') {
1939
+ return false;
1940
+ }
1941
+ const isDataImage = value.startsWith('data:image/');
1942
+ if (
1943
+ value.length >
1944
+ (isDataImage
1945
+ ? HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
1946
+ : MAX_PROVIDER_IMAGE_URL_CHARS)
1947
+ ) {
1948
+ return false;
1949
+ }
1950
+ for (let i = 0; i < value.length; i++) {
1951
+ const code = value.charCodeAt(i);
1952
+ if (code <= 0x20 || code === 0x7f) {
1953
+ return false;
1954
+ }
1955
+ }
1956
+ if (isDataImage) {
1957
+ const separator = value.indexOf(',');
1958
+ if (
1959
+ separator < 0 ||
1960
+ !value.slice(0, separator).toLowerCase().endsWith(';base64')
1961
+ ) {
1962
+ return false;
1963
+ }
1964
+ const payload = value.slice(separator + 1);
1965
+ return (
1966
+ payload.length > 0 &&
1967
+ payload.length % 4 === 0 &&
1968
+ /^[a-z0-9+/]+={0,2}$/i.test(payload)
1969
+ );
1970
+ }
1971
+ try {
1972
+ const url = new URL(value);
1973
+ return url.protocol === 'http:' || url.protocol === 'https:';
1974
+ } catch {
1975
+ return false;
1976
+ }
1977
+ }
1978
+
1979
+ function isProviderFileId(value: unknown): value is string {
1980
+ return (
1981
+ typeof value === 'string' &&
1982
+ value.length <= MAX_PROVIDER_FILE_ID_CHARS &&
1983
+ /^file[-_][a-z0-9_-]+$/i.test(value)
1984
+ );
1985
+ }
1986
+
1987
+ function hasOnlyEnumerableKeys(
1988
+ record: Record<string, unknown>,
1989
+ allowed: ReadonlySet<string>
1990
+ ): boolean {
1991
+ try {
1992
+ const keys = Object.keys(record);
1993
+ return keys.every((key) => allowed.has(key));
1994
+ } catch {
1995
+ return false;
1996
+ }
1997
+ }
1998
+
1999
+ export type CacheControlledTextToolContent = [
2000
+ {
2001
+ type: 'text';
2002
+ text: string;
2003
+ cache_control: { type: 'ephemeral'; ttl?: '1h' };
2004
+ },
2005
+ ];
2006
+
2007
+ const SINGLE_TEXT_TOOL_CONTENT_KEYS = new Set(['type', 'text']);
2008
+ const CACHE_CONTROLLED_TEXT_TOOL_CONTENT_KEYS = new Set([
2009
+ 'type',
2010
+ 'text',
2011
+ 'cache_control',
2012
+ ]);
2013
+ const CACHE_CONTROL_KEYS = new Set(['type', 'ttl']);
2014
+
2015
+ function hasSafePlainPrototype(value: object): boolean {
2016
+ try {
2017
+ const prototype = Object.getPrototypeOf(value) as object | null;
2018
+ return (
2019
+ (prototype === Object.prototype || prototype === null) &&
2020
+ !hasPotentiallyCallableToJSON(value)
2021
+ );
2022
+ } catch {
2023
+ return false;
2024
+ }
2025
+ }
2026
+
2027
+ function readSafeArrayItem(
2028
+ content: unknown,
2029
+ expectedLength: number,
2030
+ index: number
2031
+ ): OwnDataProperty {
2032
+ if (!isArray(content) || isProxy(content)) {
2033
+ return { found: false };
2034
+ }
2035
+ try {
2036
+ const length = Object.getOwnPropertyDescriptor(content, 'length');
2037
+ if (length?.value !== expectedLength) {
2038
+ return { found: false };
2039
+ }
2040
+ const prototype = Object.getPrototypeOf(content) as object | null;
2041
+ if (
2042
+ (prototype !== Array.prototype && prototype !== null) ||
2043
+ hasPotentiallyCallableToJSON(content)
2044
+ ) {
2045
+ return { found: false };
2046
+ }
2047
+ const item = Object.getOwnPropertyDescriptor(content, String(index));
2048
+ if (item?.enumerable !== true || !('value' in item)) {
2049
+ return { found: false };
2050
+ }
2051
+ return { found: true, value: item.value };
2052
+ } catch {
2053
+ return { found: false };
2054
+ }
2055
+ }
2056
+
2057
+ /** Reads one safe text block and returns its bounded text payload. */
2058
+ export function getBoundedSingleTextToolContent(
2059
+ content: unknown,
2060
+ maxChars: number
2061
+ ): string | undefined {
2062
+ const block = readSafeArrayItem(content, 1, 0);
2063
+ if (
2064
+ !block.found ||
2065
+ !isRecord(block.value) ||
2066
+ isProxy(block.value) ||
2067
+ !hasSafePlainPrototype(block.value) ||
2068
+ !hasOnlyEnumerableKeys(block.value, SINGLE_TEXT_TOOL_CONTENT_KEYS)
2069
+ ) {
2070
+ return undefined;
2071
+ }
2072
+ const type = readOwnEnumerableDataProperty(block.value, 'type');
2073
+ const text = readOwnEnumerableDataProperty(block.value, 'text');
2074
+ if (
2075
+ !type.found ||
2076
+ type.value !== 'text' ||
2077
+ !text.found ||
2078
+ typeof text.value !== 'string'
2079
+ ) {
2080
+ return undefined;
2081
+ }
2082
+ const normalizedMaxChars = Number.isFinite(maxChars)
2083
+ ? Math.max(0, Math.floor(maxChars))
2084
+ : Number.MAX_SAFE_INTEGER;
2085
+ return truncateToolResultContent(text.value, normalizedMaxChars);
2086
+ }
2087
+
2088
+ /**
2089
+ * Canonicalizes the single cache-decorated text block produced by tail prompt
2090
+ * caching without invoking accessors or custom serialization hooks.
2091
+ */
2092
+ export function getBoundedCacheControlledTextToolContent(
2093
+ content: unknown,
2094
+ maxChars: number
2095
+ ): CacheControlledTextToolContent | undefined {
2096
+ const block = readSafeArrayItem(content, 1, 0);
2097
+ if (
2098
+ !block.found ||
2099
+ !isRecord(block.value) ||
2100
+ isProxy(block.value) ||
2101
+ !hasSafePlainPrototype(block.value) ||
2102
+ !hasOnlyEnumerableKeys(block.value, CACHE_CONTROLLED_TEXT_TOOL_CONTENT_KEYS)
2103
+ ) {
2104
+ return undefined;
2105
+ }
2106
+ const type = readOwnEnumerableDataProperty(block.value, 'type');
2107
+ const text = readOwnEnumerableDataProperty(block.value, 'text');
2108
+ const cacheControl = readOwnEnumerableDataProperty(
2109
+ block.value,
2110
+ 'cache_control'
2111
+ );
2112
+ if (
2113
+ !type.found ||
2114
+ type.value !== 'text' ||
2115
+ !text.found ||
2116
+ typeof text.value !== 'string' ||
2117
+ !cacheControl.found ||
2118
+ !isRecord(cacheControl.value) ||
2119
+ isProxy(cacheControl.value) ||
2120
+ !hasSafePlainPrototype(cacheControl.value) ||
2121
+ !hasOnlyEnumerableKeys(cacheControl.value, CACHE_CONTROL_KEYS)
2122
+ ) {
2123
+ return undefined;
2124
+ }
2125
+
2126
+ const cacheType = readOwnEnumerableDataProperty(cacheControl.value, 'type');
2127
+ const ttl = readOwnEnumerableDataProperty(cacheControl.value, 'ttl');
2128
+ let hasOwnTtl: boolean;
2129
+ try {
2130
+ hasOwnTtl =
2131
+ Object.getOwnPropertyDescriptor(cacheControl.value, 'ttl') != null;
2132
+ } catch {
2133
+ return undefined;
2134
+ }
2135
+ if (
2136
+ !cacheType.found ||
2137
+ cacheType.value !== 'ephemeral' ||
2138
+ (hasOwnTtl && (!ttl.found || ttl.value !== '1h'))
2139
+ ) {
2140
+ return undefined;
2141
+ }
2142
+
2143
+ const normalizedMaxChars = Number.isFinite(maxChars)
2144
+ ? Math.max(0, Math.floor(maxChars))
2145
+ : Number.MAX_SAFE_INTEGER;
2146
+ return [
2147
+ {
2148
+ type: 'text',
2149
+ text: truncateToolResultContent(text.value, normalizedMaxChars),
2150
+ cache_control: {
2151
+ type: 'ephemeral',
2152
+ ...(ttl.found ? { ttl: '1h' as const } : {}),
2153
+ },
2154
+ },
2155
+ ];
2156
+ }
2157
+
2158
+ export type ComputerCallOutputScreenshot =
2159
+ | { type: 'input_image'; image_url: string; detail?: InputImageDetail }
2160
+ | { type: 'input_image'; file_id: string; detail?: InputImageDetail }
2161
+ | { type: 'computer_screenshot'; image_url: string }
2162
+ | { type: 'computer_screenshot'; file_id: string };
2163
+
2164
+ type InputImageDetail = 'low' | 'high' | 'auto' | 'original';
2165
+
2166
+ const INPUT_IMAGE_KEYS = new Set(['type', 'image_url', 'file_id', 'detail']);
2167
+ const COMPUTER_SCREENSHOT_KEYS = new Set(['type', 'image_url', 'file_id']);
2168
+ const IMAGE_URL_KEYS = new Set(['type', 'image_url', 'detail']);
2169
+ const NESTED_IMAGE_URL_KEYS = new Set(['url', 'detail']);
2170
+ const INPUT_IMAGE_DETAILS = new Set(['low', 'high', 'auto', 'original']);
2171
+
2172
+ /**
2173
+ * Selects the exact screenshot shape accepted by LangChain's Responses
2174
+ * converter without invoking accessors or user-defined serialization hooks.
2175
+ */
2176
+ export function getComputerCallOutputScreenshot(
2177
+ content: unknown
2178
+ ): ComputerCallOutputScreenshot | undefined {
2179
+ if (isProviderImageUrl(content)) {
2180
+ return { type: 'input_image', image_url: content };
2181
+ }
2182
+ if (
2183
+ !isArray(content) ||
2184
+ hasUnsafeCallableToJSON(content) ||
2185
+ content.length !== 1
2186
+ ) {
2187
+ return undefined;
2188
+ }
2189
+
2190
+ const descriptor = Object.getOwnPropertyDescriptor(content, '0');
2191
+ if (descriptor == null || !('value' in descriptor)) {
2192
+ return undefined;
2193
+ }
2194
+ const block = descriptor.value;
2195
+ if (!isRecord(block)) {
2196
+ return undefined;
2197
+ }
2198
+ const type = readOwnEnumerableDataProperty(block, 'type');
2199
+ if (!type.found) {
2200
+ return undefined;
2201
+ }
2202
+ const imageUrl = readOwnEnumerableDataProperty(block, 'image_url');
2203
+ const fileId = readOwnEnumerableDataProperty(block, 'file_id');
2204
+ const validImageUrl =
2205
+ imageUrl.found && isProviderImageUrl(imageUrl.value)
2206
+ ? imageUrl.value
2207
+ : undefined;
2208
+ const validFileId =
2209
+ fileId.found && isProviderFileId(fileId.value) ? fileId.value : undefined;
2210
+ const hasImageUrl = validImageUrl != null;
2211
+ const hasFileId = validFileId != null;
2212
+
2213
+ if (
2214
+ type.value === 'input_image' &&
2215
+ hasOnlyEnumerableKeys(block, INPUT_IMAGE_KEYS)
2216
+ ) {
2217
+ if (
2218
+ (imageUrl.found && !hasImageUrl) ||
2219
+ (fileId.found && !hasFileId) ||
2220
+ hasImageUrl === hasFileId
2221
+ ) {
2222
+ return undefined;
2223
+ }
2224
+ const detail = readOwnEnumerableDataProperty(block, 'detail');
2225
+ if (
2226
+ detail.found &&
2227
+ (typeof detail.value !== 'string' ||
2228
+ !INPUT_IMAGE_DETAILS.has(detail.value))
2229
+ ) {
2230
+ return undefined;
2231
+ }
2232
+ return hasImageUrl
2233
+ ? {
2234
+ type: 'input_image',
2235
+ image_url: validImageUrl,
2236
+ ...(detail.found ? { detail: detail.value as InputImageDetail } : {}),
2237
+ }
2238
+ : {
2239
+ type: 'input_image',
2240
+ file_id: validFileId as string,
2241
+ ...(detail.found ? { detail: detail.value as InputImageDetail } : {}),
2242
+ };
2243
+ }
2244
+ if (
2245
+ type.value === 'computer_screenshot' &&
2246
+ hasOnlyEnumerableKeys(block, COMPUTER_SCREENSHOT_KEYS)
2247
+ ) {
2248
+ if (
2249
+ (imageUrl.found && !hasImageUrl) ||
2250
+ (fileId.found && !hasFileId) ||
2251
+ hasImageUrl === hasFileId
2252
+ ) {
2253
+ return undefined;
2254
+ }
2255
+ return hasImageUrl
2256
+ ? { type: 'computer_screenshot', image_url: validImageUrl }
2257
+ : {
2258
+ type: 'computer_screenshot',
2259
+ file_id: validFileId as string,
2260
+ };
2261
+ }
2262
+ if (
2263
+ type.value === 'image_url' &&
2264
+ !fileId.found &&
2265
+ hasOnlyEnumerableKeys(block, IMAGE_URL_KEYS)
2266
+ ) {
2267
+ if (hasImageUrl) {
2268
+ return { type: 'input_image', image_url: validImageUrl };
2269
+ }
2270
+ if (
2271
+ imageUrl.found &&
2272
+ isRecord(imageUrl.value) &&
2273
+ hasOnlyEnumerableKeys(imageUrl.value, NESTED_IMAGE_URL_KEYS)
2274
+ ) {
2275
+ const url = readOwnEnumerableDataProperty(imageUrl.value, 'url');
2276
+ if (url.found && isProviderImageUrl(url.value)) {
2277
+ return { type: 'input_image', image_url: url.value };
2278
+ }
2279
+ }
2280
+ }
2281
+ return undefined;
2282
+ }
2283
+
2284
+ /** Returns the provider image URL when a screenshot is URL-backed. */
2285
+ export function getComputerCallOutputImageUrl(
2286
+ content: unknown
2287
+ ): string | undefined {
2288
+ const screenshot = getComputerCallOutputScreenshot(content);
2289
+ return screenshot != null && 'image_url' in screenshot
2290
+ ? screenshot.image_url
2291
+ : undefined;
2292
+ }
2293
+
2294
+ /** Returns whether content can be sent as a native computer screenshot. */
2295
+ export function isComputerCallOutputContent(
2296
+ content: unknown
2297
+ ): content is ToolContent {
2298
+ return getComputerCallOutputScreenshot(content) != null;
2299
+ }
2300
+
2301
+ /** Returns whether a ToolMessage carries the native computer-output marker. */
2302
+ export function hasComputerCallOutputMarker(message: BaseMessage): boolean {
2303
+ try {
2304
+ if (message.getType() !== 'tool') {
2305
+ return false;
2306
+ }
2307
+ const metadata = message.additional_kwargs as unknown;
2308
+ if (!isRecord(metadata)) {
2309
+ return false;
2310
+ }
2311
+ const type = readOwnEnumerableDataProperty(metadata, 'type');
2312
+ return type.found && type.value === 'computer_call_output';
2313
+ } catch {
2314
+ return false;
2315
+ }
2316
+ }
2317
+
2318
+ /** Native Responses computer screenshots are indivisible provider payloads. */
2319
+ export function isComputerCallOutputMessage(message: BaseMessage): boolean {
2320
+ return (
2321
+ hasComputerCallOutputMarker(message) &&
2322
+ isComputerCallOutputContent(message.content)
2323
+ );
2324
+ }
2325
+
2326
+ /**
2327
+ * Bounds structured tool output without slicing binary/media payloads.
2328
+ * Purely textual/structured arrays become a universally valid string tool
2329
+ * result. For mixed-media arrays, compactable blocks become one text preview
2330
+ * while atomic blocks remain intact.
2331
+ */
2332
+ export function compactToolContent(
2333
+ content: unknown,
2334
+ maxChars: number
2335
+ ): CompactToolContentResult {
2336
+ const normalizedMaxChars = Number.isFinite(maxChars)
2337
+ ? Math.max(0, Math.floor(maxChars))
2338
+ : Number.MAX_SAFE_INTEGER;
2339
+ if (typeof content === 'string') {
2340
+ return {
2341
+ content: truncateToolResultContent(content, normalizedMaxChars),
2342
+ changed: content.length > normalizedMaxChars,
2343
+ originalChars: content.length,
2344
+ };
2345
+ }
2346
+ if (!isArray(content)) {
2347
+ const serialized = serializeStructuredValueBounded(
2348
+ content,
2349
+ normalizedMaxChars
2350
+ );
2351
+ return {
2352
+ content: serialized.content,
2353
+ changed: true,
2354
+ originalChars: serialized.originalChars,
2355
+ };
2356
+ }
2357
+ if (isProxy(content)) {
2358
+ const serialized = serializeStructuredValueBounded(
2359
+ content,
2360
+ normalizedMaxChars
2361
+ );
2362
+ return {
2363
+ content: serialized.content,
2364
+ changed: true,
2365
+ originalChars: serialized.originalChars,
2366
+ };
2367
+ }
2368
+ const contentBlocks = content as ToolContentBlock[];
2369
+
2370
+ let contentLength: number;
2371
+ try {
2372
+ contentLength = contentBlocks.length;
2373
+ } catch {
2374
+ const serialized = serializeStructuredValueBounded(
2375
+ contentBlocks,
2376
+ normalizedMaxChars
2377
+ );
2378
+ return {
2379
+ content: serialized.content,
2380
+ changed: true,
2381
+ originalChars: Number.MAX_SAFE_INTEGER,
2382
+ };
2383
+ }
2384
+ if (contentLength > MAX_STRUCTURED_SERIALIZATION_WORK) {
2385
+ const serialized = serializeStructuredValueBounded(
2386
+ contentBlocks,
2387
+ normalizedMaxChars
2388
+ );
2389
+ return {
2390
+ content: serialized.content,
2391
+ changed: true,
2392
+ originalChars: serialized.originalChars,
2393
+ };
2394
+ }
2395
+
2396
+ try {
2397
+ const blockKinds = new Uint8Array(contentLength);
2398
+ const validation = createToolBlockClassificationContext();
2399
+ const structuredWork = createStructuredWorkState();
2400
+ const containerValidationWork = {
2401
+ value: MAX_ATOMIC_VALIDATION_WORK,
2402
+ };
2403
+ const containerPrototype = Object.getPrototypeOf(contentBlocks);
2404
+ if (
2405
+ (containerPrototype !== Array.prototype && containerPrototype !== null) ||
2406
+ hasPotentiallyCallableToJSON(
2407
+ contentBlocks,
2408
+ containerValidationWork,
2409
+ validation.totalRemaining
2410
+ )
2411
+ ) {
2412
+ const serialized = serializeStructuredValueBounded(
2413
+ contentBlocks,
2414
+ normalizedMaxChars
2415
+ );
2416
+ return {
2417
+ content: serialized.content,
2418
+ changed: true,
2419
+ originalChars: serialized.originalChars,
2420
+ };
2421
+ }
2422
+ let atomicBlockCount = 0;
2423
+ let denseTextLength = contentLength > 0 ? contentLength - 1 : undefined;
2424
+ for (let i = 0; i < contentLength; i++) {
2425
+ const descriptor = Object.getOwnPropertyDescriptor(
2426
+ contentBlocks,
2427
+ String(i)
2428
+ );
2429
+ if (
2430
+ descriptor == null ||
2431
+ typeof descriptor.get === 'function' ||
2432
+ typeof descriptor.set === 'function'
2433
+ ) {
2434
+ const serialized = serializeStructuredValueBounded(
2435
+ contentBlocks,
2436
+ normalizedMaxChars
2437
+ );
2438
+ return {
2439
+ content: serialized.content,
2440
+ changed: true,
2441
+ originalChars: serialized.originalChars,
2442
+ };
2443
+ }
2444
+ const block = descriptor.value as ToolContentBlock;
2445
+ const kind = classifyToolContentBlock(block, validation);
2446
+ if (validation.totalRemaining.value <= 0) {
2447
+ const serialized = serializeStructuredValueBounded(
2448
+ contentBlocks,
2449
+ normalizedMaxChars
2450
+ );
2451
+ return {
2452
+ content: serialized.content,
2453
+ changed: true,
2454
+ originalChars: serialized.originalChars,
2455
+ };
2456
+ }
2457
+ blockKinds[i] = kind;
2458
+ if (kind === TOOL_BLOCK_ATOMIC) {
2459
+ atomicBlockCount++;
2460
+ }
2461
+ if (denseTextLength == null) {
2462
+ continue;
2463
+ }
2464
+ if (kind !== TOOL_BLOCK_TEXT) {
2465
+ denseTextLength = undefined;
2466
+ continue;
2467
+ }
2468
+ const text = readOwnEnumerableDataProperty(block, 'text').value;
2469
+ denseTextLength += typeof text === 'string' ? text.length : 0;
2470
+ if (denseTextLength >= Number.MAX_SAFE_INTEGER) {
2471
+ denseTextLength = Number.MAX_SAFE_INTEGER;
2472
+ }
2473
+ }
2474
+ const originalChars =
2475
+ denseTextLength ??
2476
+ estimateStructuredChars(
2477
+ contentBlocks,
2478
+ Number.MAX_SAFE_INTEGER,
2479
+ [],
2480
+ structuredWork
2481
+ );
2482
+ if (atomicBlockCount === 0) {
2483
+ if (denseTextLength != null && originalChars <= normalizedMaxChars) {
2484
+ return {
2485
+ content: contentBlocks,
2486
+ changed: false,
2487
+ originalChars,
2488
+ };
2489
+ }
2490
+ const serialized =
2491
+ denseTextLength != null
2492
+ ? serializeDenseTextBlocksWithinLimit(
2493
+ contentBlocks as TextToolContentBlock[],
2494
+ denseTextLength,
2495
+ normalizedMaxChars
2496
+ )
2497
+ : serializeStructuredValueWithinLimit(
2498
+ contentBlocks,
2499
+ normalizedMaxChars,
2500
+ structuredWork
2501
+ );
2502
+ return {
2503
+ content: truncateToolResultContent(serialized, normalizedMaxChars),
2504
+ // Opaque arrays are normalized even when they are small so every
2505
+ // provider receives a universally valid tool-result string.
2506
+ changed: true,
2507
+ originalChars,
2508
+ };
2509
+ }
2510
+
2511
+ let hasOpaqueBlocks = false;
2512
+ const normalizedBlocks: ToolContentBlock[] = [];
2513
+ const normalizedKinds: number[] = [];
2514
+ let opaqueRun: unknown[] = [];
2515
+ const flushOpaqueRun = (): void => {
2516
+ if (opaqueRun.length === 0) {
2517
+ return;
2518
+ }
2519
+ normalizedBlocks.push({
2520
+ type: 'text',
2521
+ text: serializeStructuredValueWithinLimit(
2522
+ opaqueRun,
2523
+ normalizedMaxChars,
2524
+ structuredWork
2525
+ ),
2526
+ });
2527
+ normalizedKinds.push(TOOL_BLOCK_TEXT);
2528
+ opaqueRun = [];
2529
+ };
2530
+ for (let i = 0; i < contentLength; i++) {
2531
+ const block = contentBlocks[i];
2532
+ const kind = blockKinds[i];
2533
+ if (kind !== TOOL_BLOCK_OPAQUE) {
2534
+ flushOpaqueRun();
2535
+ normalizedBlocks.push(block);
2536
+ normalizedKinds.push(kind);
2537
+ } else {
2538
+ hasOpaqueBlocks = true;
2539
+ opaqueRun.push(block);
2540
+ }
2541
+ }
2542
+ flushOpaqueRun();
2543
+
2544
+ const normalizedLength = estimateStructuredChars(
2545
+ normalizedBlocks,
2546
+ Number.MAX_SAFE_INTEGER,
2547
+ [],
2548
+ structuredWork
2549
+ );
2550
+ if (normalizedLength <= normalizedMaxChars) {
2551
+ return {
2552
+ content: hasOpaqueBlocks ? normalizedBlocks : contentBlocks,
2553
+ changed: hasOpaqueBlocks,
2554
+ originalChars,
2555
+ };
2556
+ }
2557
+
2558
+ // Keep small media/resource blocks intact, but never let a single inline
2559
+ // base64/blob block defeat the cap. Half of the budget is reserved for
2560
+ // compactable text so the model still receives an explanation.
2561
+ const atomicBudget = Math.floor(normalizedMaxChars / 2);
2562
+ let atomicChars = 0;
2563
+ let preservedAtomicCount = 0;
2564
+ const preservedAtomicFlags = new Uint8Array(normalizedBlocks.length);
2565
+ let omittedAtomicCount = 0;
2566
+ const omittedAtomicTypes: string[] = [];
2567
+ const compactableBlocks: ToolContentBlock[] = [];
2568
+ let compactableChars = 0;
2569
+ for (let i = 0; i < normalizedBlocks.length; i++) {
2570
+ const block = normalizedBlocks[i];
2571
+ if (normalizedKinds[i] !== TOOL_BLOCK_ATOMIC) {
2572
+ if (compactableBlocks.length > 0) {
2573
+ compactableChars++;
2574
+ }
2575
+ compactableBlocks.push(block);
2576
+ compactableChars += (block as TextToolContentBlock).text.length;
2577
+ continue;
2578
+ }
2579
+ const blockChars = estimateStructuredChars(
2580
+ block,
2581
+ atomicBudget - atomicChars,
2582
+ [],
2583
+ structuredWork
2584
+ );
2585
+ if (atomicChars + blockChars <= atomicBudget) {
2586
+ preservedAtomicFlags[i] = 1;
2587
+ preservedAtomicCount++;
2588
+ atomicChars += blockChars;
2589
+ } else {
2590
+ omittedAtomicCount++;
2591
+ const typeProperty = isRecord(block)
2592
+ ? readOwnEnumerableDataProperty(block, 'type')
2593
+ : { found: false as const };
2594
+ if (omittedAtomicTypes.length < 8) {
2595
+ const rawType =
2596
+ typeProperty.found && typeof typeProperty.value === 'string'
2597
+ ? typeProperty.value
2598
+ : 'media';
2599
+ omittedAtomicTypes.push(truncateToolResultContent(rawType, 80));
2600
+ }
2601
+ }
2602
+ }
2603
+
2604
+ let previewSource = serializeDenseTextBlocksWithinLimit(
2605
+ compactableBlocks as TextToolContentBlock[],
2606
+ compactableChars,
2607
+ normalizedMaxChars
2608
+ );
2609
+ if (omittedAtomicCount > 0) {
2610
+ const undisplayedCount = omittedAtomicCount - omittedAtomicTypes.length;
2611
+ const omittedNotice =
2612
+ `[omitted ${omittedAtomicCount} oversized atomic tool-content ` +
2613
+ `block${omittedAtomicCount === 1 ? '' : 's'}: ` +
2614
+ `${omittedAtomicTypes.join(', ')}` +
2615
+ `${undisplayedCount > 0 ? `, +${undisplayedCount} more` : ''}]`;
2616
+ previewSource =
2617
+ previewSource.length > 0
2618
+ ? `${previewSource}\n${omittedNotice}`
2619
+ : omittedNotice;
2620
+ }
2621
+
2622
+ const structuralAllowance =
2623
+ preservedAtomicCount * 4 + (previewSource.length > 0 ? 32 : 2);
2624
+ const previewBudget = Math.max(
2625
+ 0,
2626
+ normalizedMaxChars - atomicChars - structuralAllowance
2627
+ );
2628
+ const preview = truncateToolResultContent(previewSource, previewBudget);
2629
+ const compacted: ToolContentBlock[] = [];
2630
+ let previewInserted = false;
2631
+ for (let i = 0; i < normalizedBlocks.length; i++) {
2632
+ const block = normalizedBlocks[i];
2633
+ if (normalizedKinds[i] === TOOL_BLOCK_ATOMIC) {
2634
+ if (preservedAtomicFlags[i] === 1) {
2635
+ compacted.push(block);
2636
+ }
2637
+ } else if (!previewInserted) {
2638
+ if (preview.length > 0) {
2639
+ compacted.push({ type: 'text', text: preview });
2640
+ }
2641
+ previewInserted = true;
2642
+ }
2643
+ }
2644
+ if (!previewInserted && preview.length > 0) {
2645
+ compacted.push({ type: 'text', text: preview });
2646
+ }
2647
+
2648
+ // JSON framing can make the block array a few bytes larger than the
2649
+ // character budget. Fall back to bounded provider-neutral text rather than
2650
+ // leaking an oversized media payload.
2651
+ const compactedChars =
2652
+ preservedAtomicCount > 0 || compacted.length === 0
2653
+ ? estimateStructuredChars(
2654
+ compacted,
2655
+ Number.MAX_SAFE_INTEGER,
2656
+ [],
2657
+ structuredWork
2658
+ )
2659
+ : preview.length;
2660
+ if (compactedChars > normalizedMaxChars) {
2661
+ return {
2662
+ content: serializeStructuredValueWithinLimit(
2663
+ normalizedBlocks,
2664
+ normalizedMaxChars,
2665
+ structuredWork
2666
+ ),
2667
+ changed: true,
2668
+ originalChars,
2669
+ };
2670
+ }
2671
+
2672
+ return {
2673
+ content: compacted,
2674
+ changed: true,
2675
+ originalChars,
2676
+ };
2677
+ } catch {
2678
+ const serialized = serializeStructuredValueBounded(
2679
+ contentBlocks,
2680
+ normalizedMaxChars
2681
+ );
2682
+ return {
2683
+ content: serialized.content,
2684
+ changed: true,
2685
+ originalChars: serialized.originalChars,
2686
+ };
2687
+ }
2688
+ }
2689
+
2690
+ /** Clones a ToolMessage while retaining fields omitted by ad-hoc clones. */
2691
+ export function cloneToolMessageWithContent(
2692
+ message: ToolMessage,
2693
+ content: ToolContent,
2694
+ artifact: unknown = message.artifact
2695
+ ): ToolMessage {
2696
+ return new ToolMessage({
2697
+ content,
2698
+ tool_call_id: message.tool_call_id,
2699
+ name: message.name,
2700
+ id: message.id,
2701
+ status: message.status,
2702
+ artifact,
2703
+ metadata: message.metadata,
2704
+ additional_kwargs: message.additional_kwargs,
2705
+ response_metadata: message.response_metadata,
2706
+ });
2707
+ }