@yeaft/webchat-agent 1.0.172 → 1.0.174

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.
@@ -0,0 +1,541 @@
1
+ const MAX_DEBUG_STRING_BYTES = 64 * 1024;
2
+ export const MAX_ACTION_REQUEST_DETAIL_BYTES = 256 * 1024;
3
+ const MAX_ACTION_REQUEST_INPUT_BYTES = MAX_ACTION_REQUEST_DETAIL_BYTES;
4
+ const MAX_ACTION_REQUEST_INPUT_LOOPS = 16;
5
+ const MAX_ACTION_REQUEST_INPUT_TOOLS = MAX_ACTION_REQUEST_INPUT_LOOPS * 128;
6
+ const MAX_DEBUG_ARRAY_ITEMS = 128;
7
+ const SENSITIVE_NAMES = new Set([
8
+ 'apikey', 'xapikey', 'token', 'accesstoken', 'refreshtoken', 'idtoken', 'clientsecret',
9
+ 'secret', 'signature', 'sig', 'credential', 'password', 'passwd',
10
+ 'authorization', 'proxyauthorization', 'auth', 'code', 'cookie', 'setcookie',
11
+ ]);
12
+ const BINARY_DATA_TYPES = new Set(['base64', 'redactedthinking', 'redacted_thinking']);
13
+ const MAX_SENSITIVE_NAME_LENGTH = Math.max(...[...SENSITIVE_NAMES].map(name => name.length));
14
+ const MAX_SENSITIVE_NAME_SOURCE_CHARS = 256;
15
+ const MAX_QUOTED_SECRET_VALUE_CHARS = 64 * 1024;
16
+ const MAX_URL_NAME_DECODE_STEPS = 3;
17
+
18
+ function byteLength(value) {
19
+ return Buffer.byteLength(String(value || ''), 'utf8');
20
+ }
21
+
22
+ function jsonByteLength(value) {
23
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
24
+ }
25
+
26
+ function truncateUtf8(value, maxBytes = MAX_DEBUG_STRING_BYTES) {
27
+ const bytes = Buffer.from(String(value || ''), 'utf8');
28
+ if (bytes.length <= maxBytes) return bytes.toString('utf8');
29
+ const marker = '\n[truncated to browser debug budget]';
30
+ const contentBytes = Math.max(0, maxBytes - byteLength(marker));
31
+ let end = Math.min(contentBytes, bytes.length);
32
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
33
+ return `${bytes.subarray(0, end).toString('utf8')}${marker}`;
34
+ }
35
+
36
+ function normalizeSensitiveName(name) {
37
+ return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
38
+ }
39
+
40
+ function isSensitiveName(name) {
41
+ return SENSITIVE_NAMES.has(normalizeSensitiveName(name));
42
+ }
43
+
44
+ function decodeQuotedName(value, quote) {
45
+ if (quote === "'") return value;
46
+ try {
47
+ return JSON.parse(`"${value}"`);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function quotedNameBeforeOperator(text, operatorIndex) {
54
+ let end = operatorIndex;
55
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
56
+ const quote = text[end - 1];
57
+ if (quote !== '"' && quote !== "'") return undefined;
58
+ const earliest = Math.max(0, end - MAX_SENSITIVE_NAME_SOURCE_CHARS);
59
+ for (let start = end - 2; start >= earliest; start -= 1) {
60
+ if (text[start] !== quote) continue;
61
+ let slashCount = 0;
62
+ for (let cursor = start - 1; cursor >= earliest && text[cursor] === '\\'; cursor -= 1) {
63
+ slashCount += 1;
64
+ }
65
+ if (slashCount % 2 === 0) {
66
+ return decodeQuotedName(text.slice(start + 1, end - 1), quote);
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function candidateStart(text, end, colonOperator) {
73
+ let start = end;
74
+ const boundary = colonOperator
75
+ ? /[\r\n,:=;{}\[\]()"'?&]/
76
+ : /[\r\n,=;{}\[\]()"'?&]/;
77
+ while (start > 0 && !boundary.test(text[start - 1])) start -= 1;
78
+ return start;
79
+ }
80
+
81
+ function hasSensitiveSuffix(text, start, end) {
82
+ let reversed = '';
83
+ const earliest = Math.max(start, end - MAX_SENSITIVE_NAME_SOURCE_CHARS);
84
+ for (let index = end - 1; index >= earliest; index -= 1) {
85
+ const character = text[index].toLowerCase();
86
+ if (/[a-z0-9]/.test(character)) {
87
+ reversed += character;
88
+ if (reversed.length > MAX_SENSITIVE_NAME_LENGTH) return false;
89
+ continue;
90
+ }
91
+ if (!reversed) continue;
92
+ const normalized = [...reversed].reverse().join('');
93
+ if (/[\s:&?;,={}\[\]()"']/.test(character)) {
94
+ if (SENSITIVE_NAMES.has(normalized)) return true;
95
+ if (!/[\s:]/.test(character)) return false;
96
+ }
97
+ }
98
+ return SENSITIVE_NAMES.has([...reversed].reverse().join(''));
99
+ }
100
+
101
+ function assignmentHasSensitiveName(text, operatorIndex, operator) {
102
+ let end = operatorIndex;
103
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
104
+ if (end === 0) return false;
105
+ const quotedName = quotedNameBeforeOperator(text, operatorIndex);
106
+ if (quotedName !== undefined) return quotedName == null || isSensitiveName(quotedName);
107
+ if (operator === '=') return hasSensitiveSuffix(text, 0, end);
108
+ const start = candidateStart(text, end, true);
109
+ const candidate = text.slice(start, end).trim();
110
+ if (/(?:^|\s)status\s+code$/i.test(candidate)) return false;
111
+ return isSensitiveName(candidate) || hasSensitiveSuffix(text, start, end);
112
+ }
113
+
114
+ function sensitiveAssignmentOperators(value, allowColon = true) {
115
+ const text = String(value || '');
116
+ const operators = [];
117
+ for (let index = 0; index < text.length; index += 1) {
118
+ const operator = text[index];
119
+ if (operator !== '=' && operator !== ':') continue;
120
+ if (operator === ':') {
121
+ if (!allowColon) continue;
122
+ let valueStart = index + 1;
123
+ const valueLimit = Math.min(text.length, index + MAX_SENSITIVE_NAME_SOURCE_CHARS);
124
+ while (valueStart < valueLimit && /[\t ]/.test(text[valueStart])) valueStart += 1;
125
+ const quotedName = quotedNameBeforeOperator(text, index);
126
+ const hasStructuredValue = valueStart > index + 1 || /["']/.test(text[valueStart] || '');
127
+ if (quotedName === undefined && !hasStructuredValue) {
128
+ let end = index;
129
+ const earliest = Math.max(0, index - MAX_SENSITIVE_NAME_SOURCE_CHARS);
130
+ while (end > earliest && /\s/.test(text[end - 1])) end -= 1;
131
+ const start = candidateStart(text, end, true);
132
+ if (start > 0 && text[start - 1] === ':') continue;
133
+ }
134
+ }
135
+ if (assignmentHasSensitiveName(text, index, operator)) operators.push(index);
136
+ }
137
+ return operators;
138
+ }
139
+
140
+ function containsSensitiveFieldSyntax(value) {
141
+ return sensitiveAssignmentOperators(value).length > 0;
142
+ }
143
+
144
+ function assignmentUsesWholeLogicalValue(text, operatorIndex) {
145
+ let end = operatorIndex;
146
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
147
+ const start = candidateStart(text, end, text[operatorIndex] === ':');
148
+ const normalized = normalizeSensitiveName(text.slice(start, end));
149
+ return ['authorization', 'proxyauthorization', 'cookie', 'setcookie']
150
+ .some(name => normalized.endsWith(name));
151
+ }
152
+
153
+ function assignmentValueReplacement(text, operatorIndex) {
154
+ let start = operatorIndex + 1;
155
+ const valueLimit = Math.min(text.length, operatorIndex + MAX_SENSITIVE_NAME_SOURCE_CHARS);
156
+ while (start < valueLimit && /[\t ]/.test(text[start])) start += 1;
157
+ if (start === valueLimit && start < text.length) {
158
+ return { start: operatorIndex + 1, end: text.length, value: '***' };
159
+ }
160
+ if (start >= text.length || /[\r\n]/.test(text[start])) return null;
161
+ if (assignmentUsesWholeLogicalValue(text, operatorIndex)) {
162
+ let end = start;
163
+ while (end < text.length && !/[;\r\n]/.test(text[end])) end += 1;
164
+ return { start, end, value: '***' };
165
+ }
166
+ const quote = text[start];
167
+ if (quote === '"' || quote === "'") {
168
+ let end = start + 1;
169
+ const quoteLimit = Math.min(text.length, start + MAX_QUOTED_SECRET_VALUE_CHARS);
170
+ while (end < quoteLimit && !/[\r\n]/.test(text[end])) {
171
+ if (text[end] === quote && text[end - 1] !== '\\') {
172
+ return { start, end: end + 1, value: `${quote}***${quote}` };
173
+ }
174
+ end += 1;
175
+ }
176
+ return { start, end, value: `${quote}***${quote}` };
177
+ }
178
+ let end = start;
179
+ while (end < text.length && !/[\s,;=&}\]]/.test(text[end])) end += 1;
180
+ return end > start ? { start, end, value: '***' } : null;
181
+ }
182
+
183
+ function redactSensitiveAssignments(value, allowColon = true) {
184
+ const text = String(value || '');
185
+ const replacements = sensitiveAssignmentOperators(text, allowColon)
186
+ .map(index => assignmentValueReplacement(text, index))
187
+ .filter(Boolean);
188
+ const merged = [];
189
+ for (const replacement of replacements) {
190
+ const previous = merged.at(-1);
191
+ if (!previous || replacement.start >= previous.end) {
192
+ merged.push({ ...replacement });
193
+ continue;
194
+ }
195
+ if (replacement.end <= previous.end) continue;
196
+ previous.end = replacement.end;
197
+ previous.value = '***';
198
+ }
199
+ if (merged.length === 0) return text;
200
+ const parts = [];
201
+ let cursor = 0;
202
+ for (const replacement of merged) {
203
+ parts.push(text.slice(cursor, replacement.start), replacement.value);
204
+ cursor = replacement.end;
205
+ }
206
+ parts.push(text.slice(cursor));
207
+ return parts.join('');
208
+ }
209
+
210
+ function isSensitiveUrlName(name) {
211
+ let decoded = String(name || '');
212
+ for (let step = 0; step < MAX_URL_NAME_DECODE_STEPS; step += 1) {
213
+ if (isSensitiveName(decoded)) return true;
214
+ let next;
215
+ try {
216
+ next = decodeURIComponent(decoded.replace(/\+/g, ' '));
217
+ } catch {
218
+ return true;
219
+ }
220
+ if (next === decoded) return false;
221
+ decoded = next;
222
+ }
223
+ return isSensitiveName(decoded) || decoded.includes('%');
224
+ }
225
+
226
+ function boundedDebugInputBytes(value, maxBytes) {
227
+ let bytes = 0;
228
+ const seen = new Set();
229
+ const stack = [value];
230
+ while (stack.length > 0 && bytes <= maxBytes) {
231
+ const item = stack.pop();
232
+ if (item == null) {
233
+ bytes += 4;
234
+ continue;
235
+ }
236
+ if (typeof item === 'string') {
237
+ if (item.length > maxBytes - bytes) return maxBytes + 1;
238
+ bytes += byteLength(item);
239
+ continue;
240
+ }
241
+ if (typeof item !== 'object') {
242
+ bytes += byteLength(item);
243
+ continue;
244
+ }
245
+ if (seen.has(item)) continue;
246
+ seen.add(item);
247
+ if (Array.isArray(item)) {
248
+ const length = Math.min(item.length, MAX_DEBUG_ARRAY_ITEMS);
249
+ if (item.length > length) bytes += 32;
250
+ for (let index = length - 1; index >= 0; index -= 1) stack.push(item[index]);
251
+ continue;
252
+ }
253
+ for (const key in item) {
254
+ if (!Object.hasOwn(item, key)) continue;
255
+ bytes += byteLength(key);
256
+ if (bytes > maxBytes) break;
257
+ stack.push(item[key]);
258
+ }
259
+ }
260
+ return bytes > maxBytes ? maxBytes + 1 : bytes;
261
+ }
262
+
263
+ export function limitActionRequestDebugInput(loopValues, toolValues) {
264
+ const sourceLoops = Array.isArray(loopValues) ? loopValues : [];
265
+ const sourceTools = Array.isArray(toolValues) ? toolValues : [];
266
+ const candidates = sourceLoops.slice(-MAX_ACTION_REQUEST_INPUT_LOOPS);
267
+ const candidateNumbers = new Set(candidates.map(loop => Number(loop?.loopNumber) || 0));
268
+ const toolsByLoop = new Map();
269
+ for (const tool of sourceTools.slice(-MAX_ACTION_REQUEST_INPUT_TOOLS)) {
270
+ const loopNumber = Number(tool?.loopNumber) || 0;
271
+ if (!candidateNumbers.has(loopNumber)) continue;
272
+ const tools = toolsByLoop.get(loopNumber) || [];
273
+ tools.push(tool);
274
+ toolsByLoop.set(loopNumber, tools);
275
+ }
276
+
277
+ let remainingBytes = MAX_ACTION_REQUEST_INPUT_BYTES;
278
+ const loops = [];
279
+ const tools = [];
280
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
281
+ const loop = candidates[index];
282
+ const loopTools = toolsByLoop.get(Number(loop?.loopNumber) || 0) || [];
283
+ const inputBytes = boundedDebugInputBytes({ loop, tools: loopTools }, remainingBytes);
284
+ if (inputBytes > remainingBytes) continue;
285
+ loops.unshift(loop);
286
+ tools.unshift(...loopTools);
287
+ remainingBytes -= inputBytes;
288
+ }
289
+ return {
290
+ loops,
291
+ tools,
292
+ omittedLoopCount: sourceLoops.length - loops.length,
293
+ };
294
+ }
295
+
296
+ function sanitizeUrlQueryNames(value) {
297
+ return String(value || '').replace(
298
+ /([?&])([^=&#]+)=([^&#]*)/g,
299
+ (match, prefix, name) => (isSensitiveUrlName(name) ? `${prefix}${name}=***` : match),
300
+ );
301
+ }
302
+
303
+ export function sanitizeDebugUrl(value) {
304
+ const text = sanitizeUrlQueryNames(truncateUtf8(value, MAX_DEBUG_STRING_BYTES));
305
+ try {
306
+ const url = new URL(text);
307
+ url.username = '';
308
+ url.password = '';
309
+ return truncateUtf8(url.toString());
310
+ } catch {
311
+ return truncateUtf8(text.replace(/\/\/[^/@\s]+@/g, '//'));
312
+ }
313
+ }
314
+
315
+ function redactSensitiveHeaderLines(value) {
316
+ return String(value || '').replace(
317
+ /(^|\r\n|\r|\n)([\t ]*)([^:\r\n]{1,256})([\t ]*:[\t ]*)([^\r\n]*)/g,
318
+ (match, boundary, whitespace, name, operator) => (isSensitiveName(name)
319
+ ? `${boundary}${whitespace}${name}${operator}***`
320
+ : match),
321
+ );
322
+ }
323
+
324
+ export function sanitizeDiagnosticText(value, maxBytes = 8 * 1024) {
325
+ let text = truncateUtf8(value, maxBytes);
326
+ text = text.replace(/https?:\/\/[^\s"'<>]+/gi, match => sanitizeDebugUrl(match));
327
+ text = redactSensitiveHeaderLines(text);
328
+ text = redactSensitiveAssignments(text);
329
+ text = text.replace(/\b(Bearer)\s+[^\s,;}\]]+/gi, '$1 ***');
330
+ return truncateUtf8(text, maxBytes);
331
+ }
332
+
333
+ function omittedBinary(value) {
334
+ return `[binary data omitted: ${byteLength(value)} bytes]`;
335
+ }
336
+
337
+ function sanitizeSseMetadataLine(line) {
338
+ const match = String(line || '').match(/^(\s*(?:event|id|retry)?\s*:)(.*)$/i);
339
+ if (!match) return sanitizeDiagnosticText(line, MAX_DEBUG_STRING_BYTES);
340
+ let value = truncateUtf8(match[2], MAX_DEBUG_STRING_BYTES);
341
+ value = value.replace(/https?:\/\/[^\s"'<>]+/gi, url => sanitizeDebugUrl(url));
342
+ value = redactSensitiveAssignments(value, false);
343
+ const colonValue = value.match(/^(\s*)([^:=\s]+)(\s+|\s*:\s+)(.+)$/);
344
+ if (colonValue && isSensitiveName(colonValue[2])) {
345
+ value = `${colonValue[1]}${colonValue[2]}${colonValue[3]}***`;
346
+ }
347
+ value = value.replace(/\b(Bearer)\s+[^\s,;}\]]+/gi, '$1 ***');
348
+ return truncateUtf8(`${match[1]}${value}`, MAX_DEBUG_STRING_BYTES);
349
+ }
350
+
351
+ function sanitizeSseEvent(event, seen) {
352
+ const lines = event.split(/\r?\n/);
353
+ const dataIndexes = [];
354
+ const dataParts = [];
355
+ for (const [index, line] of lines.entries()) {
356
+ const match = line.match(/^\s*data:\s?(.*)$/);
357
+ if (!match) continue;
358
+ dataIndexes.push(index);
359
+ dataParts.push(match[1]);
360
+ }
361
+ if (dataParts.length === 0) {
362
+ return lines.map(line => sanitizeSseMetadataLine(line)).join('\n');
363
+ }
364
+
365
+ const data = dataParts.join('\n');
366
+ let sanitized = data;
367
+ if (data !== '[DONE]') {
368
+ try {
369
+ sanitized = JSON.stringify(sanitizeDebugValue(JSON.parse(data), null, '', seen));
370
+ } catch {
371
+ sanitized = containsSensitiveFieldSyntax(data)
372
+ ? '[redacted SSE event: malformed sensitive data]'
373
+ : sanitizeDiagnosticText(data, MAX_DEBUG_STRING_BYTES);
374
+ }
375
+ }
376
+
377
+ const firstDataIndex = dataIndexes[0];
378
+ const dataIndexSet = new Set(dataIndexes);
379
+ const sanitizedDataLines = String(sanitized).split('\n').map(line => `data: ${line}`).join('\n');
380
+ const sanitizedLines = [];
381
+ for (const [index, line] of lines.entries()) {
382
+ if (dataIndexSet.has(index)) {
383
+ if (index === firstDataIndex) sanitizedLines.push(sanitizedDataLines);
384
+ continue;
385
+ }
386
+ sanitizedLines.push(sanitizeSseMetadataLine(line));
387
+ }
388
+ return sanitizedLines.join('\n');
389
+ }
390
+
391
+ function sanitizeSseBody(value, seen) {
392
+ const text = truncateUtf8(value, MAX_DEBUG_STRING_BYTES);
393
+ const trailingSeparator = /(?:\r?\n){2}$/.test(text) ? '\n\n' : '';
394
+ const events = text.split(/(?:\r?\n){2}/);
395
+ if (events.at(-1) === '') events.pop();
396
+ return truncateUtf8(`${events.map(event => sanitizeSseEvent(event, seen)).join('\n\n')}${trailingSeparator}`);
397
+ }
398
+
399
+ function sanitizeHeaderEntries(entries, seen) {
400
+ return entries.map(([name, headerValue]) => [
401
+ String(name || ''),
402
+ isSensitiveName(name) ? '***' : sanitizeDebugValue(headerValue, null, String(name || ''), seen),
403
+ ]);
404
+ }
405
+
406
+ function sanitizeHeaders(value, seen, raw = false) {
407
+ if (typeof value === 'string') return sanitizeDiagnosticText(value, MAX_DEBUG_STRING_BYTES);
408
+ if (!value || typeof value !== 'object') return sanitizeDebugValue(value, null, '', seen);
409
+ if (!Array.isArray(value)) {
410
+ return Object.fromEntries(sanitizeHeaderEntries(Object.entries(value), seen));
411
+ }
412
+ const items = value.slice(0, MAX_DEBUG_ARRAY_ITEMS);
413
+ if (raw || items.every(item => !Array.isArray(item))) {
414
+ const out = [];
415
+ for (let index = 0; index < items.length; index += 2) {
416
+ const name = items[index];
417
+ out.push(name);
418
+ if (index + 1 < items.length) {
419
+ out.push(isSensitiveName(name)
420
+ ? '***'
421
+ : sanitizeDebugValue(items[index + 1], null, String(name || ''), seen));
422
+ }
423
+ }
424
+ if (value.length > items.length) out.push(`[${value.length - items.length} additional items omitted]`);
425
+ return out;
426
+ }
427
+ const out = items.map(item => {
428
+ if (!Array.isArray(item) || item.length < 2) return sanitizeDebugValue(item, value, '', seen);
429
+ const [name, headerValue, ...rest] = item;
430
+ return [name, isSensitiveName(name) ? '***' : sanitizeDebugValue(headerValue, null, String(name || ''), seen),
431
+ ...rest.map(valueItem => sanitizeDebugValue(valueItem, item, '', seen))];
432
+ });
433
+ if (value.length > items.length) out.push(`[${value.length - items.length} additional items omitted]`);
434
+ return out;
435
+ }
436
+
437
+ export function sanitizeDebugValue(value, parent = null, key = '', seen = new WeakSet()) {
438
+ if (key && isSensitiveName(key)) return '***';
439
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
440
+ if (typeof value === 'string') {
441
+ const text = truncateUtf8(value, MAX_DEBUG_STRING_BYTES);
442
+ if (key === 'url') return sanitizeDebugUrl(text);
443
+ if (text.startsWith('data:') && text.includes(';base64,')) return omittedBinary(value);
444
+ const parentType = String(parent?.type || '').toLowerCase().replace(/[^a-z0-9_]/g, '');
445
+ if (key === 'data' && BINARY_DATA_TYPES.has(parentType)) return omittedBinary(value);
446
+ if (key === 'body' && /^[\s\r\n]*[\[{]/.test(text)) {
447
+ try {
448
+ return truncateUtf8(JSON.stringify(sanitizeDebugValue(JSON.parse(text), null, '', seen)));
449
+ } catch {
450
+ if (containsSensitiveFieldSyntax(text)) {
451
+ return '[redacted malformed JSON: sensitive data]';
452
+ }
453
+ }
454
+ }
455
+ return sanitizeDiagnosticText(text, MAX_DEBUG_STRING_BYTES);
456
+ }
457
+ if (typeof value !== 'object') return truncateUtf8(String(value));
458
+ if (seen.has(value)) return '[circular value omitted]';
459
+ seen.add(value);
460
+ try {
461
+ if (Array.isArray(value)) {
462
+ const items = value.slice(0, MAX_DEBUG_ARRAY_ITEMS)
463
+ .map(item => sanitizeDebugValue(item, value, '', seen));
464
+ if (value.length > items.length) {
465
+ items.push(`[${value.length - items.length} additional items omitted]`);
466
+ }
467
+ return items;
468
+ }
469
+ const out = {};
470
+ for (const [childKey, item] of Object.entries(value)) {
471
+ if (childKey === 'headers' || childKey === 'rawHeaders') {
472
+ out[childKey] = sanitizeHeaders(item, seen, childKey === 'rawHeaders');
473
+ } else if (childKey === 'body' && value.format === 'sse' && typeof item === 'string') {
474
+ out.body = sanitizeSseBody(item, seen);
475
+ } else {
476
+ out[childKey] = sanitizeDebugValue(item, value, childKey, seen);
477
+ }
478
+ }
479
+ return out;
480
+ } finally {
481
+ seen.delete(value);
482
+ }
483
+ }
484
+
485
+ export function enforceActionRequestDetailBudget(detail, omittedLoopCount = 0) {
486
+ if (omittedLoopCount > 0 && detail?.request) {
487
+ detail.request.truncated = true;
488
+ detail.request.omittedLoopCount = omittedLoopCount;
489
+ }
490
+ if (!detail?.request || jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
491
+ detail.request.truncated = true;
492
+ const loops = Array.isArray(detail.request.loops) ? detail.request.loops : [];
493
+ const stages = [
494
+ loop => { loop.rawResponse = null; },
495
+ loop => { loop.rawRequest = null; },
496
+ loop => {
497
+ for (const tool of Array.isArray(loop.tools) ? loop.tools : []) {
498
+ tool.output = null;
499
+ tool.input = null;
500
+ }
501
+ },
502
+ loop => { loop.messages = []; },
503
+ loop => { loop.systemPrompt = ''; },
504
+ loop => { loop.response = ''; },
505
+ ];
506
+ for (const stage of stages) {
507
+ for (const loop of loops) stage(loop);
508
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
509
+ }
510
+ while (loops.length > 1 && jsonByteLength(detail) > MAX_ACTION_REQUEST_DETAIL_BYTES) {
511
+ loops.shift();
512
+ detail.request.omittedLoopCount = (detail.request.omittedLoopCount || 0) + 1;
513
+ }
514
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
515
+ detail.request.omittedLoopCount = (detail.request.omittedLoopCount || 0) + loops.length;
516
+ detail.request.loops = [];
517
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
518
+ const request = detail.request;
519
+ const metadata = value => truncateUtf8(value, 4 * 1024);
520
+ return {
521
+ actionId: metadata(detail.actionId),
522
+ request: {
523
+ id: metadata(request.id),
524
+ runId: metadata(request.runId),
525
+ status: metadata(request.status),
526
+ model: metadata(request.model),
527
+ vp: request.vp ? {
528
+ id: metadata(request.vp.id),
529
+ name: metadata(request.vp.name),
530
+ } : null,
531
+ openedAt: request.openedAt,
532
+ closedAt: request.closedAt,
533
+ loopCount: request.loopCount,
534
+ totalMs: request.totalMs,
535
+ totalTokens: request.totalTokens,
536
+ loops: [],
537
+ truncated: true,
538
+ omittedLoopCount: detail.request.omittedLoopCount,
539
+ },
540
+ };
541
+ }