@yeaft/webchat-agent 1.0.476 → 1.0.478
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/local-runtime/server/context.js +25 -0
- package/local-runtime/server/handlers/agent-output.js +29 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +90 -145
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/debug-trace.js +39 -362
- package/yeaft/engine.js +16 -9
- package/yeaft/sessions/session-crud.js +27 -27
- package/yeaft/web-bridge.js +43 -6
|
Binary file
|
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -14,43 +14,22 @@ import { createReadStream, promises as fsp } from 'fs';
|
|
|
14
14
|
import { basename, dirname, extname, join } from 'path';
|
|
15
15
|
import { createHash, randomUUID } from 'crypto';
|
|
16
16
|
import { createInterface } from 'readline';
|
|
17
|
-
import { truncateUtf8Text } from './perf-trace.js';
|
|
18
17
|
|
|
19
18
|
const TRACE_VERSION = 3;
|
|
20
19
|
const REQUEST_RETENTION = 10;
|
|
21
20
|
const MAX_HISTORY_LIMIT = 5;
|
|
22
21
|
const MAX_DREAM_EVENTS = 100;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const MAX_RAW_REQUEST_BYTES = 2 * 1024 * 1024;
|
|
27
|
-
// Raw provider responses are diagnostic duplicates of the normalized response,
|
|
28
|
-
// tool calls, usage and request delta already stored on each loop. Keeping up
|
|
29
|
-
// to 1 MiB per loop made long tool turns produce 100+ MiB trace files. Every
|
|
30
|
-
// buffered flush then JSON.stringify()'d and rewrote that entire file on the
|
|
31
|
-
// agent's single event loop, so several active Sessions appeared deadlocked.
|
|
32
|
-
// A 64 KiB prefix is enough to inspect provider envelopes without letting
|
|
33
|
-
// always-on diagnostics dominate runtime latency.
|
|
34
|
-
const MAX_RAW_RESPONSE_BYTES = 64 * 1024;
|
|
22
|
+
// Debug records are diagnostic source data. They deliberately do not inherit
|
|
23
|
+
// model-context or ordinary history budgets, and explicit detail transport is
|
|
24
|
+
// chunked instead of truncating persisted values.
|
|
35
25
|
const TRACE_APPEND_BATCH_MS = 100;
|
|
36
26
|
const EVENT_FLUSH_INTERVAL_MS = 30_000;
|
|
37
27
|
const MAX_SEARCH_PATTERN_CHARS = 300;
|
|
38
|
-
const DEFAULT_TRACE_TEXT_MAX_BYTES = 256 * 1024;
|
|
39
|
-
// A turn detail is returned as one WebSocket message. Keep its UI projection
|
|
40
|
-
// comfortably below the Agent connection's 8 MiB outbound queue ceiling while
|
|
41
|
-
// preserving the canonical file-backed trace without any extra truncation.
|
|
42
|
-
const DEBUG_DETAIL_WIRE_MAX_BYTES = 6 * 1024 * 1024;
|
|
43
28
|
|
|
44
29
|
function isPlainObject(value) {
|
|
45
30
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
46
31
|
}
|
|
47
32
|
|
|
48
|
-
function normalizeTextMaxBytes(value, fallback = DEFAULT_TRACE_TEXT_MAX_BYTES) {
|
|
49
|
-
const parsed = Number(value);
|
|
50
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
51
|
-
return Math.min(4 * 1024 * 1024, Math.max(0, Math.floor(parsed)));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
33
|
function safeDirComponent(value, fallback = 'unknown') {
|
|
55
34
|
const raw = String(value || '').trim();
|
|
56
35
|
if (!raw) return fallback;
|
|
@@ -169,53 +148,12 @@ function traceMatchesRegex(trace, regex) {
|
|
|
169
148
|
}
|
|
170
149
|
}
|
|
171
150
|
|
|
172
|
-
function truncateText(value, maxBytes = MAX_TEXT_BYTES) {
|
|
173
|
-
if (value == null) return value ?? null;
|
|
174
|
-
const str = String(value);
|
|
175
|
-
const budget = Math.max(0, Number(maxBytes) || 0);
|
|
176
|
-
if (Buffer.byteLength(str, 'utf8') <= budget) return str;
|
|
177
|
-
if (budget <= 0) return '';
|
|
178
|
-
const marker = `\n... [truncated to ${budget} bytes]`;
|
|
179
|
-
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
180
|
-
if (markerBytes >= budget) return truncateUtf8Text(str, budget).value;
|
|
181
|
-
return `${truncateUtf8Text(str, budget - markerBytes).value}${marker}`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
151
|
function cloneJsonValue(value) {
|
|
185
152
|
if (value == null) return value;
|
|
186
153
|
try { return JSON.parse(JSON.stringify(value)); }
|
|
187
154
|
catch { return null; }
|
|
188
155
|
}
|
|
189
156
|
|
|
190
|
-
function safeJsonValue(value, maxBytes = MAX_INLINE_VALUE_BYTES) {
|
|
191
|
-
if (value == null) return value;
|
|
192
|
-
try {
|
|
193
|
-
const json = JSON.stringify(value);
|
|
194
|
-
const originalBytes = Buffer.byteLength(json, 'utf8');
|
|
195
|
-
if (originalBytes <= maxBytes) return JSON.parse(json);
|
|
196
|
-
// A bounded raw-exchange sentinel already carries the useful preview.
|
|
197
|
-
// Preserve a re-bounded preview rather than reducing persisted history to
|
|
198
|
-
// metadata after an outer trace record crosses its storage budget.
|
|
199
|
-
if (value && typeof value === 'object' && value.__truncated === true
|
|
200
|
-
&& typeof value.preview === 'string') {
|
|
201
|
-
const previewBudget = Math.max(0, maxBytes - 512);
|
|
202
|
-
const preview = truncateText(value.preview, previewBudget);
|
|
203
|
-
return {
|
|
204
|
-
__truncated: true,
|
|
205
|
-
...(value.reason ? { reason: value.reason } : {}),
|
|
206
|
-
originalBytes: Number.isFinite(Number(value.originalBytes))
|
|
207
|
-
? Number(value.originalBytes)
|
|
208
|
-
: originalBytes,
|
|
209
|
-
maxBytes,
|
|
210
|
-
...(preview ? { preview } : {}),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
return { __truncated: true, originalBytes, maxBytes };
|
|
214
|
-
} catch {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
157
|
function normalizeUsage(usage = {}, fallback = {}) {
|
|
220
158
|
const inputTokens = Number.isFinite(Number(usage?.inputTokens)) ? Number(usage.inputTokens) : Number(fallback.inputTokens || 0);
|
|
221
159
|
const outputTokens = Number.isFinite(Number(usage?.outputTokens)) ? Number(usage.outputTokens) : Number(fallback.outputTokens || 0);
|
|
@@ -241,34 +179,9 @@ function stableEqual(a, b) {
|
|
|
241
179
|
catch { return false; }
|
|
242
180
|
}
|
|
243
181
|
|
|
244
|
-
function
|
|
245
|
-
try { return Buffer.byteLength(JSON.stringify(value), 'utf8'); }
|
|
246
|
-
catch { return Infinity; }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function rawRequestSentinel(reason, value = null, maxBytes = MAX_RAW_REQUEST_BYTES) {
|
|
250
|
-
const previewSource = typeof value === 'string'
|
|
251
|
-
? value
|
|
252
|
-
: (() => { try { return JSON.stringify(value); } catch { return null; } })();
|
|
253
|
-
const preview = typeof previewSource === 'string'
|
|
254
|
-
? truncateText(previewSource, Math.min(64 * 1024, maxBytes))
|
|
255
|
-
: null;
|
|
256
|
-
const originalBytes = value == null ? null : jsonByteLength(value);
|
|
257
|
-
return {
|
|
258
|
-
__truncated: true,
|
|
259
|
-
reason,
|
|
260
|
-
...(originalBytes != null ? { originalBytes } : {}),
|
|
261
|
-
maxBytes,
|
|
262
|
-
...(preview ? { preview } : {}),
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function boundRawValue(value, reason = 'raw_request_budget') {
|
|
182
|
+
function cloneRawValue(value) {
|
|
267
183
|
if (value == null) return value;
|
|
268
|
-
|
|
269
|
-
if (cloned == null) return null;
|
|
270
|
-
if (jsonByteLength(cloned) <= MAX_RAW_REQUEST_BYTES) return cloned;
|
|
271
|
-
return rawRequestSentinel(reason, value);
|
|
184
|
+
return typeof value === 'string' ? value : cloneJsonValue(value);
|
|
272
185
|
}
|
|
273
186
|
|
|
274
187
|
function buildRawMessagesDelta(previousMessages, nextMessages) {
|
|
@@ -276,9 +189,9 @@ function buildRawMessagesDelta(previousMessages, nextMessages) {
|
|
|
276
189
|
const priorMessages = Array.isArray(previousMessages) ? previousMessages : [];
|
|
277
190
|
const prefix = messagesPrefixLength(priorMessages, nextMessages);
|
|
278
191
|
if (prefix === priorMessages.length && prefix <= nextMessages.length) {
|
|
279
|
-
return { messagesFrom: prefix, messagesAppend:
|
|
192
|
+
return { messagesFrom: prefix, messagesAppend: cloneRawValue(nextMessages.slice(prefix)) };
|
|
280
193
|
}
|
|
281
|
-
return { messagesFrom: 0, messagesAppend:
|
|
194
|
+
return { messagesFrom: 0, messagesAppend: cloneRawValue(nextMessages) };
|
|
282
195
|
}
|
|
283
196
|
|
|
284
197
|
function rawRequestMessageKey(body) {
|
|
@@ -290,18 +203,18 @@ function rawRequestMessageKey(body) {
|
|
|
290
203
|
|
|
291
204
|
function buildInitialRawRequestDelta(value) {
|
|
292
205
|
if (value == null) return null;
|
|
293
|
-
if (typeof value === 'string') return { replacement:
|
|
294
|
-
if (!isPlainObject(value)) return { replacement:
|
|
206
|
+
if (typeof value === 'string') return { replacement: value };
|
|
207
|
+
if (!isPlainObject(value)) return { replacement: cloneRawValue(value) };
|
|
295
208
|
const delta = { set: {}, body: {} };
|
|
296
209
|
for (const [key, item] of Object.entries(value)) {
|
|
297
210
|
if (key === 'body') continue;
|
|
298
|
-
delta.set[key] =
|
|
211
|
+
delta.set[key] = cloneRawValue(item);
|
|
299
212
|
}
|
|
300
213
|
const body = isPlainObject(value.body) ? value.body : null;
|
|
301
214
|
if (body) {
|
|
302
215
|
for (const [key, bodyValue] of Object.entries(body)) {
|
|
303
216
|
if (key === 'messages' || key === 'input') continue;
|
|
304
|
-
delta.body[key] =
|
|
217
|
+
delta.body[key] = cloneRawValue(bodyValue);
|
|
305
218
|
}
|
|
306
219
|
const messageKey = rawRequestMessageKey(body);
|
|
307
220
|
if (messageKey) {
|
|
@@ -309,12 +222,11 @@ function buildInitialRawRequestDelta(value) {
|
|
|
309
222
|
Object.assign(delta.body, buildRawMessagesDelta([], body[messageKey]) || {});
|
|
310
223
|
}
|
|
311
224
|
} else if (Object.prototype.hasOwnProperty.call(value, 'body')) {
|
|
312
|
-
delta.set.body =
|
|
225
|
+
delta.set.body = cloneRawValue(value.body);
|
|
313
226
|
}
|
|
314
227
|
if (Object.keys(delta.set).length === 0) delete delta.set;
|
|
315
228
|
if (Object.keys(delta.body).length === 0) delete delta.body;
|
|
316
229
|
if (!delta.set && !delta.body) return null;
|
|
317
|
-
if (jsonByteLength(delta) > MAX_RAW_REQUEST_BYTES) return { replacement: rawRequestSentinel('raw_request_delta_budget') };
|
|
318
230
|
return delta;
|
|
319
231
|
}
|
|
320
232
|
|
|
@@ -332,16 +244,16 @@ function buildRawRequestDelta(previous, next) {
|
|
|
332
244
|
const comparablePrevious = rawComparableRequest(previous);
|
|
333
245
|
const comparableNext = rawComparableRequest(next);
|
|
334
246
|
if (typeof comparablePrevious === 'string' || typeof comparableNext === 'string') {
|
|
335
|
-
return comparablePrevious === comparableNext ? null : { replacement:
|
|
247
|
+
return comparablePrevious === comparableNext ? null : { replacement: cloneRawValue(comparableNext) };
|
|
336
248
|
}
|
|
337
249
|
if (!isPlainObject(comparablePrevious) || !isPlainObject(comparableNext)) {
|
|
338
|
-
return stableEqual(comparablePrevious, comparableNext) ? null : { replacement:
|
|
250
|
+
return stableEqual(comparablePrevious, comparableNext) ? null : { replacement: cloneRawValue(comparableNext) };
|
|
339
251
|
}
|
|
340
252
|
|
|
341
253
|
const delta = { set: {}, body: {} };
|
|
342
254
|
for (const key of Object.keys(comparableNext)) {
|
|
343
255
|
if (key === 'body') continue;
|
|
344
|
-
if (!stableEqual(comparablePrevious[key], comparableNext[key])) delta.set[key] =
|
|
256
|
+
if (!stableEqual(comparablePrevious[key], comparableNext[key])) delta.set[key] = cloneRawValue(comparableNext[key]);
|
|
345
257
|
}
|
|
346
258
|
for (const key of Object.keys(comparablePrevious)) {
|
|
347
259
|
if (key !== 'body' && !Object.prototype.hasOwnProperty.call(comparableNext, key)) delta.set[key] = null;
|
|
@@ -352,7 +264,7 @@ function buildRawRequestDelta(previous, next) {
|
|
|
352
264
|
if (prevBody && nextBody) {
|
|
353
265
|
for (const key of Object.keys(nextBody)) {
|
|
354
266
|
if (key === 'messages' || key === 'input') continue;
|
|
355
|
-
if (!stableEqual(prevBody[key], nextBody[key])) delta.body[key] =
|
|
267
|
+
if (!stableEqual(prevBody[key], nextBody[key])) delta.body[key] = cloneRawValue(nextBody[key]);
|
|
356
268
|
}
|
|
357
269
|
for (const key of Object.keys(prevBody)) {
|
|
358
270
|
if (key !== 'messages' && key !== 'input' && !Object.prototype.hasOwnProperty.call(nextBody, key)) delta.body[key] = null;
|
|
@@ -362,15 +274,12 @@ function buildRawRequestDelta(previous, next) {
|
|
|
362
274
|
const msgDelta = buildRawMessagesDelta(prevMessages, nextMessages);
|
|
363
275
|
if (msgDelta) Object.assign(delta.body, msgDelta);
|
|
364
276
|
} else if (!stableEqual(previous.body, next.body)) {
|
|
365
|
-
delta.set.body =
|
|
277
|
+
delta.set.body = cloneRawValue(next.body);
|
|
366
278
|
}
|
|
367
279
|
|
|
368
280
|
if (Object.keys(delta.set).length === 0) delete delta.set;
|
|
369
281
|
if (Object.keys(delta.body).length === 0) delete delta.body;
|
|
370
282
|
if (!delta.set && !delta.body) return null;
|
|
371
|
-
if (jsonByteLength(delta) > MAX_RAW_REQUEST_BYTES) {
|
|
372
|
-
return { replacement: rawRequestSentinel('raw_request_delta_budget') };
|
|
373
|
-
}
|
|
374
283
|
return delta;
|
|
375
284
|
}
|
|
376
285
|
|
|
@@ -398,9 +307,7 @@ export function applyRawRequestDelta(previous, delta) {
|
|
|
398
307
|
const from = Number.isFinite(Number(delta.body.messagesFrom)) ? Number(delta.body.messagesFrom) : existing.length;
|
|
399
308
|
body[messageKey] = existing.slice(0, from).concat(cloneJsonValue(delta.body.messagesAppend) || []);
|
|
400
309
|
} else if (Object.prototype.hasOwnProperty.call(delta.body, 'messagesAppend')) {
|
|
401
|
-
//
|
|
402
|
-
// an array. Preserve that sentinel rather than silently dropping the
|
|
403
|
-
// entire request body during hydration.
|
|
310
|
+
// Preserve legacy non-array values written by older bounded traces.
|
|
404
311
|
body[messageKey] = cloneJsonValue(delta.body.messagesAppend) ?? delta.body.messagesAppend;
|
|
405
312
|
}
|
|
406
313
|
next.body = body;
|
|
@@ -418,82 +325,10 @@ function messagesPrefixLength(prevMessages, nextMessages) {
|
|
|
418
325
|
return i;
|
|
419
326
|
}
|
|
420
327
|
|
|
421
|
-
function
|
|
422
|
-
return { __truncated: true, path, maxBytes, originalBytes };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function boundedSnapshotString(value, maxBytes, path) {
|
|
426
|
-
const originalBytes = Buffer.byteLength(value, 'utf8');
|
|
427
|
-
if (originalBytes <= maxBytes) return value;
|
|
428
|
-
const marker = snapshotTruncation(path, maxBytes, originalBytes);
|
|
429
|
-
if (jsonByteLength(marker) > maxBytes) return null;
|
|
430
|
-
const markerText = `\n... [truncated to ${maxBytes} bytes]`;
|
|
431
|
-
const markerBytes = Buffer.byteLength(markerText, 'utf8');
|
|
432
|
-
if (markerBytes >= maxBytes) return marker;
|
|
433
|
-
return truncateUtf8Text(value, maxBytes - markerBytes).value + markerText;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function boundSnapshotValue(value, maxBytes, path = 'messages') {
|
|
437
|
-
if (maxBytes <= 0) return null;
|
|
438
|
-
const sourceBytes = jsonByteLength(value);
|
|
439
|
-
if (sourceBytes <= maxBytes) return cloneJsonValue(value);
|
|
440
|
-
const marker = snapshotTruncation(path, maxBytes, sourceBytes);
|
|
441
|
-
if (jsonByteLength(marker) > maxBytes) return null;
|
|
442
|
-
if (value == null || typeof value === 'number' || typeof value === 'boolean') return marker;
|
|
443
|
-
if (typeof value === 'string') return boundedSnapshotString(value, maxBytes, path);
|
|
444
|
-
|
|
445
|
-
// Account JSON framing incrementally. The old clone-and-stringify-prefix
|
|
446
|
-
// loop reserialized the full growing array/object per member and blocked
|
|
447
|
-
// DebugTrace.endTurn for tens of seconds on a large provider history.
|
|
448
|
-
if (Array.isArray(value)) {
|
|
449
|
-
const out = [];
|
|
450
|
-
let usedBytes = 2; // []
|
|
451
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
452
|
-
const separatorBytes = out.length > 0 ? 1 : 0;
|
|
453
|
-
const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes);
|
|
454
|
-
const candidate = boundSnapshotValue(value[index], remaining, `${path}[${index}]`);
|
|
455
|
-
if (candidate == null) break;
|
|
456
|
-
let encoded;
|
|
457
|
-
try { encoded = JSON.stringify(candidate); } catch { break; }
|
|
458
|
-
const candidateBytes = Buffer.byteLength(encoded, 'utf8');
|
|
459
|
-
if (usedBytes + separatorBytes + candidateBytes > maxBytes) break;
|
|
460
|
-
out.push(candidate);
|
|
461
|
-
usedBytes += separatorBytes + candidateBytes;
|
|
462
|
-
}
|
|
463
|
-
return out.length > 0 ? out : marker;
|
|
464
|
-
}
|
|
465
|
-
if (typeof value === 'object') {
|
|
466
|
-
const out = {};
|
|
467
|
-
let usedBytes = 2; // {}
|
|
468
|
-
let count = 0;
|
|
469
|
-
for (const [key, item] of Object.entries(value)) {
|
|
470
|
-
let keyJson;
|
|
471
|
-
try { keyJson = JSON.stringify(key); } catch { break; }
|
|
472
|
-
const keyBytes = Buffer.byteLength(keyJson, 'utf8');
|
|
473
|
-
const separatorBytes = count > 0 ? 1 : 0;
|
|
474
|
-
const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes - keyBytes - 1);
|
|
475
|
-
const candidate = boundSnapshotValue(item, remaining, `${path}.${key}`);
|
|
476
|
-
if (candidate == null) break;
|
|
477
|
-
let encoded;
|
|
478
|
-
try { encoded = JSON.stringify(candidate); } catch { break; }
|
|
479
|
-
const candidateBytes = Buffer.byteLength(encoded, 'utf8');
|
|
480
|
-
if (usedBytes + separatorBytes + keyBytes + 1 + candidateBytes > maxBytes) break;
|
|
481
|
-
out[key] = candidate;
|
|
482
|
-
usedBytes += separatorBytes + keyBytes + 1 + candidateBytes;
|
|
483
|
-
count += 1;
|
|
484
|
-
}
|
|
485
|
-
return count > 0 ? out : marker;
|
|
486
|
-
}
|
|
487
|
-
return marker;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function buildRequestSnapshot(info = {}, textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES) {
|
|
491
|
-
const messageBudget = normalizeTextMaxBytes(textMaxBytes);
|
|
328
|
+
function buildRequestSnapshot(info = {}) {
|
|
492
329
|
return {
|
|
493
|
-
systemPrompt:
|
|
494
|
-
messages: Array.isArray(info.messages)
|
|
495
|
-
? boundSnapshotValue(info.messages, messageBudget, 'messages')
|
|
496
|
-
: [],
|
|
330
|
+
systemPrompt: String(info.systemPrompt || ''),
|
|
331
|
+
messages: Array.isArray(info.messages) ? cloneJsonValue(info.messages) : [],
|
|
497
332
|
rawRequest: info.rawRequest ?? null,
|
|
498
333
|
};
|
|
499
334
|
}
|
|
@@ -812,167 +647,12 @@ function expandTrace(trace) {
|
|
|
812
647
|
return { loops, turns: Array.from(turnsById.values()) };
|
|
813
648
|
}
|
|
814
649
|
|
|
815
|
-
function
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const marker = `\n... [wire truncated ${path}; original ${originalBytes} bytes]`;
|
|
820
|
-
const rawBytes = Math.max(1, Buffer.byteLength(text, 'utf8'));
|
|
821
|
-
const expansion = Math.max(1, originalBytes / rawBytes);
|
|
822
|
-
const markerBytes = jsonByteLength(marker) - 2;
|
|
823
|
-
let previewBytes = Math.max(0, Math.floor((budget - markerBytes - 2) / expansion));
|
|
824
|
-
let projected = `${truncateUtf8Text(text, previewBytes).value}${marker}`;
|
|
825
|
-
let projectedBytes = jsonByteLength(projected);
|
|
826
|
-
if (projectedBytes > budget && previewBytes > 0) {
|
|
827
|
-
previewBytes = Math.max(0, Math.floor(previewBytes * (budget / projectedBytes)) - 8);
|
|
828
|
-
projected = `${truncateUtf8Text(text, previewBytes).value}${marker}`;
|
|
829
|
-
projectedBytes = jsonByteLength(projected);
|
|
830
|
-
}
|
|
831
|
-
if (projectedBytes <= budget) return projected;
|
|
832
|
-
if (jsonByteLength(marker) <= budget) return marker;
|
|
833
|
-
return '';
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function debugDetailJsonSentinel(value, maxBytes, path, originalBytes = jsonByteLength(value)) {
|
|
837
|
-
if (value == null) return value;
|
|
838
|
-
const budget = Math.max(0, Math.floor(Number(maxBytes) || 0));
|
|
839
|
-
if (originalBytes <= budget) return value;
|
|
840
|
-
const base = {
|
|
841
|
-
__truncated: true,
|
|
842
|
-
reason: 'debug_detail_wire_budget',
|
|
843
|
-
path,
|
|
844
|
-
originalBytes,
|
|
845
|
-
maxBytes: budget,
|
|
846
|
-
};
|
|
847
|
-
const baseBytes = jsonByteLength(base);
|
|
848
|
-
if (baseBytes > budget) return budget >= 4 ? null : '';
|
|
849
|
-
let preview = '';
|
|
850
|
-
try {
|
|
851
|
-
const encoded = JSON.stringify(value);
|
|
852
|
-
preview = debugDetailTextSentinel(
|
|
853
|
-
encoded,
|
|
854
|
-
Math.max(2, budget - baseBytes - 16),
|
|
855
|
-
path,
|
|
856
|
-
jsonByteLength(encoded),
|
|
857
|
-
);
|
|
858
|
-
} catch { /* metadata-only sentinel below */ }
|
|
859
|
-
const projected = preview ? { ...base, preview } : base;
|
|
860
|
-
return jsonByteLength(projected) <= budget ? projected : base;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function allocateDebugCandidateBudgets(candidates, totalBytes) {
|
|
864
|
-
const sorted = [...candidates].sort((a, b) => a.originalBytes - b.originalBytes || a.path.localeCompare(b.path));
|
|
865
|
-
let remainingBytes = Math.max(0, Math.floor(totalBytes));
|
|
866
|
-
let remainingCount = sorted.length;
|
|
867
|
-
for (let index = 0; index < sorted.length; index += 1) {
|
|
868
|
-
const candidate = sorted[index];
|
|
869
|
-
const fairShare = remainingCount > 0 ? Math.floor(remainingBytes / remainingCount) : 0;
|
|
870
|
-
if (candidate.originalBytes <= fairShare) {
|
|
871
|
-
candidate.budget = candidate.originalBytes;
|
|
872
|
-
remainingBytes -= candidate.budget;
|
|
873
|
-
remainingCount -= 1;
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
for (let tail = index; tail < sorted.length; tail += 1) {
|
|
877
|
-
const count = sorted.length - tail;
|
|
878
|
-
const budget = count > 0 ? Math.floor(remainingBytes / count) : 0;
|
|
879
|
-
sorted[tail].budget = budget;
|
|
880
|
-
remainingBytes -= budget;
|
|
881
|
-
}
|
|
882
|
-
break;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
export function projectDebugDetailForWire(detail, maxBytes = DEBUG_DETAIL_WIRE_MAX_BYTES) {
|
|
887
|
-
// `fetchTurnDebug()` hands us a freshly expanded response object; mutate that
|
|
888
|
-
// disposable projection rather than cloning tens or hundreds of cumulative
|
|
889
|
-
// request snapshots. Canonical file records and the request cache are separate.
|
|
890
|
-
const wire = detail && typeof detail === 'object'
|
|
650
|
+
export function projectDebugDetailForWire(detail) {
|
|
651
|
+
// Backward-compatible export: debug detail is diagnostic source data and must
|
|
652
|
+
// remain lossless. The transport splits large JSON into bounded wire frames.
|
|
653
|
+
return detail && typeof detail === 'object'
|
|
891
654
|
? detail
|
|
892
655
|
: { loops: [], turns: [], dreamEvents: [] };
|
|
893
|
-
const payloadBudget = Math.max(0, maxBytes - 2048);
|
|
894
|
-
const candidates = [];
|
|
895
|
-
const addCandidate = (container, field, path) => {
|
|
896
|
-
if (!container || container[field] == null) return;
|
|
897
|
-
const value = container[field];
|
|
898
|
-
candidates.push({
|
|
899
|
-
container,
|
|
900
|
-
field,
|
|
901
|
-
path,
|
|
902
|
-
value,
|
|
903
|
-
originalBytes: jsonByteLength(value),
|
|
904
|
-
budget: 0,
|
|
905
|
-
});
|
|
906
|
-
// Measure the non-candidate envelope exactly once without serializing every
|
|
907
|
-
// cumulative request again after each replacement.
|
|
908
|
-
container[field] = null;
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
const loopFields = ['rawRequest', 'rawResponse', 'messages', 'requestBase', 'requestDelta', 'toolCalls', 'response', 'systemPrompt'];
|
|
912
|
-
for (const [loopIndex, loop] of (Array.isArray(wire.loops) ? wire.loops : []).entries()) {
|
|
913
|
-
if (!loop || typeof loop !== 'object') continue;
|
|
914
|
-
for (const field of loopFields) addCandidate(loop, field, `loops[${loopIndex}].${field}`);
|
|
915
|
-
}
|
|
916
|
-
for (const [turnIndex, turn] of (Array.isArray(wire.turns) ? wire.turns : []).entries()) {
|
|
917
|
-
if (!turn || typeof turn !== 'object') continue;
|
|
918
|
-
for (const field of ['userPrompt', 'memoryLoaded', 'memoryLoadedMeta', 'memoryAdjust']) {
|
|
919
|
-
addCandidate(turn, field, `turns[${turnIndex}].${field}`);
|
|
920
|
-
}
|
|
921
|
-
for (const [toolIndex, tool] of (Array.isArray(turn.tools) ? turn.tools : []).entries()) {
|
|
922
|
-
if (!tool || typeof tool !== 'object') continue;
|
|
923
|
-
addCandidate(tool, 'toolInput', `turns[${turnIndex}].tools[${toolIndex}].toolInput`);
|
|
924
|
-
addCandidate(tool, 'toolOutput', `turns[${turnIndex}].tools[${toolIndex}].toolOutput`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
for (const [eventIndex] of (Array.isArray(wire.dreamEvents) ? wire.dreamEvents : []).entries()) {
|
|
928
|
-
addCandidate(wire.dreamEvents, eventIndex, `dreamEvents[${eventIndex}]`);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const skeletonBytes = jsonByteLength(wire);
|
|
932
|
-
const candidateBytes = candidates.reduce((sum, candidate) => sum + candidate.originalBytes, 0);
|
|
933
|
-
const originalBytes = skeletonBytes - (4 * candidates.length) + candidateBytes;
|
|
934
|
-
if (originalBytes <= payloadBudget) {
|
|
935
|
-
for (const candidate of candidates) candidate.container[candidate.field] = candidate.value;
|
|
936
|
-
return wire;
|
|
937
|
-
}
|
|
938
|
-
if (candidates.length === 0 || skeletonBytes > payloadBudget) {
|
|
939
|
-
throw new Error(`Debug detail metadata exceeds the ${maxBytes}-byte wire budget`);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Each candidate currently occupies JSON `null` (4 bytes) in the skeleton.
|
|
943
|
-
// Water-fill the exact value budget: small fields stay complete, while large
|
|
944
|
-
// cumulative requests and tool outputs receive fair, independently bounded
|
|
945
|
-
// previews. This is O(total input bytes + candidates log candidates), with
|
|
946
|
-
// one final whole-envelope serialization rather than one per candidate.
|
|
947
|
-
const valueBudget = payloadBudget - skeletonBytes + (4 * candidates.length);
|
|
948
|
-
allocateDebugCandidateBudgets(candidates, valueBudget);
|
|
949
|
-
let truncatedFields = 0;
|
|
950
|
-
for (const candidate of candidates) {
|
|
951
|
-
if (candidate.originalBytes <= candidate.budget) {
|
|
952
|
-
candidate.container[candidate.field] = candidate.value;
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
const fieldBudget = candidate.budget;
|
|
956
|
-
candidate.container[candidate.field] = typeof candidate.value === 'string'
|
|
957
|
-
? debugDetailTextSentinel(candidate.value, fieldBudget, candidate.path, candidate.originalBytes)
|
|
958
|
-
: debugDetailJsonSentinel(candidate.value, fieldBudget, candidate.path, candidate.originalBytes);
|
|
959
|
-
truncatedFields += 1;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
wire.projection = {
|
|
963
|
-
truncated: true,
|
|
964
|
-
reason: 'debug_detail_wire_budget',
|
|
965
|
-
maxBytes,
|
|
966
|
-
truncatedFields,
|
|
967
|
-
};
|
|
968
|
-
const projectedBytesBase = jsonByteLength(wire);
|
|
969
|
-
let projectedBytes = projectedBytesBase + Buffer.byteLength(`,\"projectedBytes\":${projectedBytesBase}`, 'utf8');
|
|
970
|
-
projectedBytes = projectedBytesBase + Buffer.byteLength(`,\"projectedBytes\":${projectedBytes}`, 'utf8');
|
|
971
|
-
wire.projection.projectedBytes = projectedBytes;
|
|
972
|
-
if (projectedBytes > maxBytes) {
|
|
973
|
-
throw new Error(`Debug detail projection still exceeds the ${maxBytes}-byte wire budget`);
|
|
974
|
-
}
|
|
975
|
-
return wire;
|
|
976
656
|
}
|
|
977
657
|
|
|
978
658
|
function traceToLegacyRows(trace) {
|
|
@@ -1192,8 +872,6 @@ export class DebugTrace {
|
|
|
1192
872
|
* @type {Promise<void>}
|
|
1193
873
|
*/
|
|
1194
874
|
#flushChain = Promise.resolve();
|
|
1195
|
-
/** @type {number} */
|
|
1196
|
-
#textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES;
|
|
1197
875
|
/** @type {boolean} */
|
|
1198
876
|
#acceptingWrites = true;
|
|
1199
877
|
|
|
@@ -1205,17 +883,16 @@ export class DebugTrace {
|
|
|
1205
883
|
const rootDir = fileTraceRoot(tracePath);
|
|
1206
884
|
if (!rootDir) throw new Error('DebugTrace requires a storage path');
|
|
1207
885
|
this.#rootDir = rootDir;
|
|
1208
|
-
|
|
886
|
+
// `textMaxBytes` remains an accepted constructor option for compatibility,
|
|
887
|
+
// but debug source data is intentionally lossless.
|
|
888
|
+
void options?.textMaxBytes;
|
|
1209
889
|
// Best-effort, fire-and-forget: atomicWriteText re-ensures the request
|
|
1210
890
|
// subdir before every write, so a missed mkdir here is harmless.
|
|
1211
891
|
ensureDir(rootDir).catch(() => {});
|
|
1212
892
|
}
|
|
1213
893
|
|
|
1214
|
-
refreshConfig(
|
|
1215
|
-
|
|
1216
|
-
? config.traceTextMaxBytes
|
|
1217
|
-
: config;
|
|
1218
|
-
this.#textMaxBytes = normalizeTextMaxBytes(nextValue, this.#textMaxBytes);
|
|
894
|
+
refreshConfig(_config = {}) {
|
|
895
|
+
// Compatibility no-op: debug trace content is no longer byte-truncated.
|
|
1219
896
|
}
|
|
1220
897
|
|
|
1221
898
|
startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null, memoryLoaded = null, memoryLoadedMeta = null } = {}) {
|
|
@@ -1259,7 +936,7 @@ export class DebugTrace {
|
|
|
1259
936
|
rawRequest: Object.prototype.hasOwnProperty.call(info, 'rawRequest')
|
|
1260
937
|
? info.rawRequest
|
|
1261
938
|
: (trace.baseRequest?.rawRequest ?? null),
|
|
1262
|
-
}
|
|
939
|
+
});
|
|
1263
940
|
const previousSnapshot = trace._lastSnapshot || this.#reconstructLastSnapshot(trace);
|
|
1264
941
|
if (!trace.baseRequest) {
|
|
1265
942
|
trace.baseRequest = {
|
|
@@ -1275,7 +952,7 @@ export class DebugTrace {
|
|
|
1275
952
|
loopNumber,
|
|
1276
953
|
startedAt: trace.openedAt || Date.now(),
|
|
1277
954
|
model: info.model || null,
|
|
1278
|
-
response:
|
|
955
|
+
response: String(info.responseText || ''),
|
|
1279
956
|
toolCalls: cloneJsonValue(Array.isArray(info.toolCalls) ? info.toolCalls : []),
|
|
1280
957
|
usage: normalizeUsage(info.usage || {}, {
|
|
1281
958
|
inputTokens: info.inputTokens || 0,
|
|
@@ -1288,8 +965,8 @@ export class DebugTrace {
|
|
|
1288
965
|
stopReason: info.stopReason || null,
|
|
1289
966
|
at: Date.now(),
|
|
1290
967
|
rawResponse: typeof info.rawResponse === 'string'
|
|
1291
|
-
?
|
|
1292
|
-
:
|
|
968
|
+
? info.rawResponse
|
|
969
|
+
: cloneJsonValue(info.rawResponse),
|
|
1293
970
|
// Raw request is canonical base-plus-delta data. Persisting the full
|
|
1294
971
|
// snapshot on every loop made a 512 KiB request consume N × 512 KiB for
|
|
1295
972
|
// an N-loop trace and kept the same duplication in the hydrated cache.
|
|
@@ -1327,8 +1004,8 @@ export class DebugTrace {
|
|
|
1327
1004
|
loopNumber: ctx.loopNumber || 0,
|
|
1328
1005
|
toolName: toolName || '?',
|
|
1329
1006
|
toolCallId,
|
|
1330
|
-
toolInput:
|
|
1331
|
-
toolOutput:
|
|
1007
|
+
toolInput: toolInput == null ? null : String(toolInput),
|
|
1008
|
+
toolOutput: toolOutput == null ? null : String(toolOutput),
|
|
1332
1009
|
durationMs: Number(durationMs || 0),
|
|
1333
1010
|
isError: !!isError,
|
|
1334
1011
|
suppressed: !!suppressed,
|
|
@@ -1350,7 +1027,7 @@ export class DebugTrace {
|
|
|
1350
1027
|
id,
|
|
1351
1028
|
traceId: traceId || String(eventType || 'event'),
|
|
1352
1029
|
eventType: eventType || 'event',
|
|
1353
|
-
eventData:
|
|
1030
|
+
eventData: cloneJsonValue(eventData),
|
|
1354
1031
|
createdAt: Date.now(),
|
|
1355
1032
|
});
|
|
1356
1033
|
if (this.#events.length > MAX_DREAM_EVENTS) {
|
|
@@ -1433,7 +1110,7 @@ export class DebugTrace {
|
|
|
1433
1110
|
const dreamEvents = this.#readDreamEvents({ sessionId: requestedSessionId, dreamLimit });
|
|
1434
1111
|
if (!trace) return { loops: [], turns: [], dreamEvents, detailTurnId: requestedTurnId };
|
|
1435
1112
|
const expanded = expandTrace(trace);
|
|
1436
|
-
return
|
|
1113
|
+
return { ...expanded, dreamEvents, detailTurnId: requestedTurnId };
|
|
1437
1114
|
}
|
|
1438
1115
|
|
|
1439
1116
|
async fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null, search = '' } = {}) {
|
|
@@ -1643,7 +1320,7 @@ export class DebugTrace {
|
|
|
1643
1320
|
sessionId: normalizedSessionId,
|
|
1644
1321
|
vpId: vpId || null,
|
|
1645
1322
|
threadId: threadId || null,
|
|
1646
|
-
userPrompt:
|
|
1323
|
+
userPrompt: String(userPrompt || ''),
|
|
1647
1324
|
memoryLoaded: Array.isArray(memoryLoaded) ? cloneJsonValue(memoryLoaded) : null,
|
|
1648
1325
|
memoryLoadedMeta: memoryLoadedMeta && typeof memoryLoadedMeta === 'object'
|
|
1649
1326
|
? cloneJsonValue(memoryLoadedMeta)
|
package/yeaft/engine.js
CHANGED
|
@@ -38,7 +38,7 @@ import { isVpForeign, readContent as readScopeContent } from './memory/store.js'
|
|
|
38
38
|
import { ActiveMemorySet } from './memory/ams.js';
|
|
39
39
|
import { cleanMemoryPromptText } from './memory/prompt-cleanup.js';
|
|
40
40
|
import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
|
|
41
|
-
import {
|
|
41
|
+
import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
42
42
|
// Default thread marker for legacy / non-group flows. Group VP runtime may
|
|
43
43
|
// pass a real threadId per (sessionId, vpId, threadId) engine instance.
|
|
44
44
|
const MAIN_THREAD_ID = 'main';
|
|
@@ -2622,17 +2622,24 @@ export class Engine {
|
|
|
2622
2622
|
const thinkingBlocks = []; // task-327d: collected from adapter for round-trip
|
|
2623
2623
|
let stopReason = 'end_turn';
|
|
2624
2624
|
const totalUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 };
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2627
|
-
//
|
|
2625
|
+
// Raw provider exchange is diagnostic source data, not model context or
|
|
2626
|
+
// ordinary history. The legacy telemetry byte setting must not destroy it;
|
|
2627
|
+
// explicit debug detail transport is chunked instead.
|
|
2628
2628
|
let rawRequest = null;
|
|
2629
2629
|
let rawResponse = null;
|
|
2630
|
-
const rawExchangeMaxBytes = Number.
|
|
2631
|
-
|
|
2632
|
-
|
|
2630
|
+
const rawExchangeMaxBytes = Number.MAX_SAFE_INTEGER;
|
|
2631
|
+
const cloneRawExchange = (value) => {
|
|
2632
|
+
if (value == null || typeof value === 'string') return value;
|
|
2633
|
+
try { return JSON.parse(JSON.stringify(value)); }
|
|
2634
|
+
catch { return null; }
|
|
2635
|
+
};
|
|
2633
2636
|
const captureRawExchange = (exchange) => {
|
|
2634
|
-
if (exchange
|
|
2635
|
-
|
|
2637
|
+
if (exchange && Object.prototype.hasOwnProperty.call(exchange, 'rawRequest')) {
|
|
2638
|
+
rawRequest = cloneRawExchange(exchange.rawRequest);
|
|
2639
|
+
}
|
|
2640
|
+
if (exchange && Object.prototype.hasOwnProperty.call(exchange, 'rawResponse')) {
|
|
2641
|
+
rawResponse = cloneRawExchange(exchange.rawResponse);
|
|
2642
|
+
}
|
|
2636
2643
|
};
|
|
2637
2644
|
|
|
2638
2645
|
// task-704b: resolve the live model's context window for this turn.
|