@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.
- package/dist/cjs/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +342 -109
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +64 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +19 -2
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +54 -41
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +349 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +65 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -7
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +54 -41
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- 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
|
+
}
|