@yeaft/webchat-agent 1.0.549 → 1.0.550
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/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/debug-trace.js +126 -20
- package/yeaft/engine.js +8 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.550"}
|
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -353,7 +353,9 @@ function buildRequestSnapshot(info = {}) {
|
|
|
353
353
|
systemPrompt: String(info.systemPrompt || ''),
|
|
354
354
|
messages: Array.isArray(info.messages) ? cloneJsonValue(info.messages) : [],
|
|
355
355
|
requestInputBreakdown: normalizeRequestInputBreakdown(info.requestInputBreakdown),
|
|
356
|
-
|
|
356
|
+
// Own the latest snapshot: adapters/callers may extend their message array
|
|
357
|
+
// after endTurn. Keeping that live reference would silently skip raw deltas.
|
|
358
|
+
rawRequest: cloneRawValue(info.rawRequest) ?? null,
|
|
357
359
|
};
|
|
358
360
|
}
|
|
359
361
|
|
|
@@ -501,6 +503,8 @@ function serializableTraceMeta(trace) {
|
|
|
501
503
|
delete meta._lastSnapshot;
|
|
502
504
|
delete meta._persistedFormat;
|
|
503
505
|
delete meta._persistedRequestDir;
|
|
506
|
+
delete meta._payloadReleased;
|
|
507
|
+
delete meta._failedAppend;
|
|
504
508
|
delete meta.baseRequest;
|
|
505
509
|
delete meta.loops;
|
|
506
510
|
delete meta.tools;
|
|
@@ -553,6 +557,8 @@ async function countRequestEvents(requestDir) {
|
|
|
553
557
|
}
|
|
554
558
|
let loopCount = 0;
|
|
555
559
|
let toolCount = 0;
|
|
560
|
+
const loopIds = new Set();
|
|
561
|
+
const toolIds = new Set();
|
|
556
562
|
const stream = createReadStream(requestEventsPath(requestDir), { encoding: 'utf8' });
|
|
557
563
|
stream.on('error', () => {});
|
|
558
564
|
try {
|
|
@@ -561,8 +567,13 @@ async function countRequestEvents(requestDir) {
|
|
|
561
567
|
if (!line.trim()) continue;
|
|
562
568
|
try {
|
|
563
569
|
const event = JSON.parse(line);
|
|
564
|
-
if (event?.type === 'loop' && event.record)
|
|
565
|
-
|
|
570
|
+
if (event?.type === 'loop' && event.record) {
|
|
571
|
+
const id = event.record.turnRowId || event.record.loopInstanceId || `${event.record.loopNumber || 0}`;
|
|
572
|
+
if (!loopIds.has(id)) { loopIds.add(id); loopCount += 1; }
|
|
573
|
+
} else if (event?.type === 'tool' && event.record) {
|
|
574
|
+
const id = event.record.id || `${event.record.turnRowId || ''}:${event.record.toolCallId || ''}`;
|
|
575
|
+
if (!toolIds.has(id)) { toolIds.add(id); toolCount += 1; }
|
|
576
|
+
}
|
|
566
577
|
} catch { /* Ignore one torn final append. */ }
|
|
567
578
|
}
|
|
568
579
|
} catch { /* Missing/unreadable event file counts as empty. */ }
|
|
@@ -853,6 +864,28 @@ async function readHeaderDetail(rootDir, header) {
|
|
|
853
864
|
return sameTraceIdentity(trace, header) ? trace : null;
|
|
854
865
|
}
|
|
855
866
|
|
|
867
|
+
/** Detail reads combine durable payload with records whose append failed. The
|
|
868
|
+
* compact live rows must never overwrite full disk rows or hide unwritten data. */
|
|
869
|
+
async function readAvailableTrace(rootDir, trace) {
|
|
870
|
+
if (!trace?._payloadReleased) return trace;
|
|
871
|
+
const stored = await readHeaderDetail(rootDir, trace);
|
|
872
|
+
const loops = new Map((stored?.loops || []).map(loop => [loop.turnRowId || loop.loopInstanceId, loop]));
|
|
873
|
+
for (const loop of trace.loops || []) {
|
|
874
|
+
if (Object.hasOwn(loop, 'requestDelta')) loops.set(loop.turnRowId || loop.loopInstanceId, loop);
|
|
875
|
+
}
|
|
876
|
+
const tools = new Map((stored?.tools || []).map(tool => [tool.id, tool]));
|
|
877
|
+
for (const tool of trace.tools || []) {
|
|
878
|
+
if (Object.hasOwn(tool, 'toolOutput')) tools.set(tool.id, tool);
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
...trace,
|
|
882
|
+
baseRequest: stored?.baseRequest || null,
|
|
883
|
+
loops: [...loops.values()].sort((a, b) => (a.loopNumber || 0) - (b.loopNumber || 0)
|
|
884
|
+
|| String(a.turnRowId || '').localeCompare(String(b.turnRowId || ''))),
|
|
885
|
+
tools: [...tools.values()],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
856
889
|
async function countDirFiles(rootDir) {
|
|
857
890
|
let files = 0;
|
|
858
891
|
let bytes = 0;
|
|
@@ -986,10 +1019,10 @@ export class DebugTrace {
|
|
|
986
1019
|
// it to a transport failure or another uncaptured attempt.
|
|
987
1020
|
rawRequest: Object.prototype.hasOwnProperty.call(info, 'rawRequest')
|
|
988
1021
|
? info.rawRequest
|
|
989
|
-
: (trace.baseRequest?.rawRequest ?? null),
|
|
1022
|
+
: (trace._lastSnapshot?.rawRequest ?? trace.baseRequest?.rawRequest ?? null),
|
|
990
1023
|
});
|
|
991
1024
|
const previousSnapshot = trace._lastSnapshot || this.#reconstructLastSnapshot(trace);
|
|
992
|
-
if (!
|
|
1025
|
+
if (!previousSnapshot) {
|
|
993
1026
|
trace.baseRequest = {
|
|
994
1027
|
systemPrompt: snapshot.systemPrompt,
|
|
995
1028
|
messages: Array.isArray(snapshot.messages) ? cloneJsonValue(snapshot.messages) : [],
|
|
@@ -1065,7 +1098,7 @@ export class DebugTrace {
|
|
|
1065
1098
|
};
|
|
1066
1099
|
trace.tools.push(tool);
|
|
1067
1100
|
trace.updatedAt = tool.createdAt;
|
|
1068
|
-
this.#appendTraceRecord(trace, 'tool', tool, { writeMeta: false });
|
|
1101
|
+
this.#appendTraceRecord(trace, 'tool', tool, { writeMeta: trace.active === false });
|
|
1069
1102
|
return id;
|
|
1070
1103
|
}
|
|
1071
1104
|
|
|
@@ -1142,6 +1175,7 @@ export class DebugTrace {
|
|
|
1142
1175
|
item?.sessionId === requestedSessionId
|
|
1143
1176
|
&& (item.requestId === requestedTurnId || item.traceId === requestedTurnId)
|
|
1144
1177
|
)) || null;
|
|
1178
|
+
if (trace) trace = await readAvailableTrace(this.#rootDir, trace);
|
|
1145
1179
|
if (!trace) {
|
|
1146
1180
|
const locator = await readJson(turnLocatorPath(this.#rootDir, requestedSessionId, requestedTurnId));
|
|
1147
1181
|
if (locator?.requestKey && locator.sessionId === requestedSessionId && locator.requestId === requestedTurnId) {
|
|
@@ -1218,8 +1252,8 @@ export class DebugTrace {
|
|
|
1218
1252
|
await this.#drainWrites();
|
|
1219
1253
|
const tools = [];
|
|
1220
1254
|
for (const { trace: header } of this.#traceSummaries().slice().reverse()) {
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1255
|
+
const live = this.#requestCache.get(header.requestKey);
|
|
1256
|
+
const trace = live ? await readAvailableTrace(this.#rootDir, live) : await readHeaderDetail(this.#rootDir, header);
|
|
1223
1257
|
if (!trace) continue;
|
|
1224
1258
|
for (const tool of Array.isArray(trace.tools) ? trace.tools : []) {
|
|
1225
1259
|
const row = traceToolToLegacy(trace, tool);
|
|
@@ -1240,8 +1274,8 @@ export class DebugTrace {
|
|
|
1240
1274
|
if (!needle) return [];
|
|
1241
1275
|
const results = [];
|
|
1242
1276
|
for (const { trace: header } of this.#traceSummaries().slice().reverse()) {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1277
|
+
const live = this.#requestCache.get(header.requestKey);
|
|
1278
|
+
const trace = live ? await readAvailableTrace(this.#rootDir, live) : await readHeaderDetail(this.#rootDir, header);
|
|
1245
1279
|
if (!trace || !JSON.stringify(trace).toLowerCase().includes(needle)) continue;
|
|
1246
1280
|
results.push(...traceToLegacyRows(trace));
|
|
1247
1281
|
if (results.length >= 50) break;
|
|
@@ -1319,6 +1353,23 @@ export class DebugTrace {
|
|
|
1319
1353
|
async close() {
|
|
1320
1354
|
this.#acceptingWrites = false;
|
|
1321
1355
|
await this.#drainWrites();
|
|
1356
|
+
// One final bounded retry, also for a failure with no later loop/finalize.
|
|
1357
|
+
// Persistent disk errors retain diagnostics rather than silently evicting.
|
|
1358
|
+
for (const trace of this.#requestCache.values()) {
|
|
1359
|
+
const failed = trace._failedAppend;
|
|
1360
|
+
if (!failed) continue;
|
|
1361
|
+
delete trace._failedAppend;
|
|
1362
|
+
for (const entry of failed.records) {
|
|
1363
|
+
this.#pendingWrites.push({ trace, ...entry, initialize: true,
|
|
1364
|
+
writeMeta: failed.writeMeta, evictAfterWrite: failed.evictAfterWrite });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
await this.#drainWrites();
|
|
1368
|
+
if ([...this.#requestCache.values()].some(trace => trace._failedAppend)) return;
|
|
1369
|
+
this.#requestCache.clear();
|
|
1370
|
+
this.#turnIndex.clear();
|
|
1371
|
+
this.#retentionIndex.clear();
|
|
1372
|
+
this.#reconciledRetentionSessions.clear();
|
|
1322
1373
|
}
|
|
1323
1374
|
|
|
1324
1375
|
/**
|
|
@@ -1410,6 +1461,12 @@ export class DebugTrace {
|
|
|
1410
1461
|
.find(item => traceMatchesIdentity(item, sessionId, turnId)) || null;
|
|
1411
1462
|
}
|
|
1412
1463
|
if (!trace) return false;
|
|
1464
|
+
// Legacy traces must keep their source payload until format migration has
|
|
1465
|
+
// durably copied it. Event traces already own their history on disk.
|
|
1466
|
+
if (trace._persistedFormat === 'events') {
|
|
1467
|
+
trace._lastSnapshot = this.#reconstructLastSnapshot(trace);
|
|
1468
|
+
this.#releasePersistedPayload(trace, [...trace.loops, ...trace.tools]);
|
|
1469
|
+
}
|
|
1413
1470
|
this.#requestCache.set(trace.requestKey, trace);
|
|
1414
1471
|
return true;
|
|
1415
1472
|
}
|
|
@@ -1477,6 +1534,25 @@ export class DebugTrace {
|
|
|
1477
1534
|
return tracePathFor(this.#rootDir, trace.sessionId || null, trace.requestKey);
|
|
1478
1535
|
}
|
|
1479
1536
|
|
|
1537
|
+
/** Release only exact records known to be durable; newer replacements and
|
|
1538
|
+
* in-flight records keep their payload. Lightweight rows preserve identity,
|
|
1539
|
+
* counts and usage without keeping tool output/response/request bodies. */
|
|
1540
|
+
#releasePersistedPayload(trace, records) {
|
|
1541
|
+
const persisted = new Set(records);
|
|
1542
|
+
trace.loops = (trace.loops || []).map(loop => {
|
|
1543
|
+
if (!persisted.has(loop)) return loop;
|
|
1544
|
+
const { response, toolCalls, rawRequest, rawResponse, requestDelta, ...meta } = loop;
|
|
1545
|
+
return meta;
|
|
1546
|
+
});
|
|
1547
|
+
trace.tools = (trace.tools || []).map(tool => {
|
|
1548
|
+
if (!persisted.has(tool)) return tool;
|
|
1549
|
+
const { toolInput, toolOutput, ...meta } = tool;
|
|
1550
|
+
return meta;
|
|
1551
|
+
});
|
|
1552
|
+
trace.baseRequest = null;
|
|
1553
|
+
trace._payloadReleased = true;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1480
1556
|
#appendTraceRecord(trace, type, record, { writeMeta = false, evictAfterWrite = false } = {}) {
|
|
1481
1557
|
if (!this.#acceptingWrites || !trace?.requestKey || !record) return;
|
|
1482
1558
|
this.#requestCache.set(trace.requestKey, trace);
|
|
@@ -1485,7 +1561,9 @@ export class DebugTrace {
|
|
|
1485
1561
|
this.#pendingWrites.push({
|
|
1486
1562
|
trace,
|
|
1487
1563
|
type,
|
|
1488
|
-
|
|
1564
|
+
// Records are private immutable snapshots, already detached from caller
|
|
1565
|
+
// input by endTurn/logTool. Do not serialize+parse a second full copy.
|
|
1566
|
+
record,
|
|
1489
1567
|
initialize,
|
|
1490
1568
|
writeMeta: !!writeMeta,
|
|
1491
1569
|
evictAfterWrite: !!evictAfterWrite,
|
|
@@ -1528,17 +1606,29 @@ export class DebugTrace {
|
|
|
1528
1606
|
for (const entry of entries) {
|
|
1529
1607
|
if (!this.#requestCache.has(entry.trace.requestKey)) continue;
|
|
1530
1608
|
const requestDir = requestDirFor(this.#rootDir, entry.trace.sessionId || null, entry.trace.requestKey);
|
|
1531
|
-
const batch = batches.get(requestDir) || { trace: entry.trace, initialize: false, writeMeta: false, evictAfterWrite: false,
|
|
1609
|
+
const batch = batches.get(requestDir) || { trace: entry.trace, initialize: false, writeMeta: false, evictAfterWrite: false, records: [] };
|
|
1532
1610
|
batch.trace = entry.trace;
|
|
1533
1611
|
batch.initialize ||= entry.initialize;
|
|
1534
1612
|
batch.writeMeta ||= entry.writeMeta;
|
|
1535
1613
|
batch.evictAfterWrite ||= entry.evictAfterWrite;
|
|
1536
|
-
batch.
|
|
1614
|
+
batch.records.push({ type: entry.type, record: entry.record });
|
|
1537
1615
|
batches.set(requestDir, batch);
|
|
1538
1616
|
}
|
|
1539
1617
|
for (const [requestDir, batch] of batches) {
|
|
1540
|
-
const { trace
|
|
1541
|
-
|
|
1618
|
+
const { trace } = batch;
|
|
1619
|
+
// A failed append may contain the base/delta for every later loop.
|
|
1620
|
+
// Retry it before new records (including finalize); do not run a timer
|
|
1621
|
+
// retry loop on a full disk. Record identity makes partial replay safe.
|
|
1622
|
+
if (trace._failedAppend) {
|
|
1623
|
+
const failed = trace._failedAppend;
|
|
1624
|
+
batch.records.unshift(...failed.records);
|
|
1625
|
+
batch.initialize = true; // recheck metadata and repair any partial tail
|
|
1626
|
+
batch.writeMeta ||= failed.writeMeta;
|
|
1627
|
+
batch.evictAfterWrite ||= failed.evictAfterWrite;
|
|
1628
|
+
}
|
|
1629
|
+
const { initialize, writeMeta, evictAfterWrite } = batch;
|
|
1630
|
+
const records = [...batch.records];
|
|
1631
|
+
const durableRecords = batch.records.map(entry => entry.record);
|
|
1542
1632
|
const legacyRequestDir = trace._persistedFormat === 'legacy'
|
|
1543
1633
|
? trace._persistedRequestDir || null
|
|
1544
1634
|
: null;
|
|
@@ -1548,14 +1638,16 @@ export class DebugTrace {
|
|
|
1548
1638
|
const meta = serializableTraceMeta(trace);
|
|
1549
1639
|
if (legacyRequestDir) {
|
|
1550
1640
|
meta.legacyBaseRequest = cloneJsonValue(trace.baseRequest);
|
|
1551
|
-
const
|
|
1641
|
+
const legacyRecords = [];
|
|
1552
1642
|
for (const loop of Array.isArray(trace.loops) ? trace.loops : []) {
|
|
1553
|
-
|
|
1643
|
+
legacyRecords.push({ type: 'loop', record: loop });
|
|
1644
|
+
durableRecords.push(loop);
|
|
1554
1645
|
}
|
|
1555
1646
|
for (const tool of Array.isArray(trace.tools) ? trace.tools : []) {
|
|
1556
|
-
|
|
1647
|
+
legacyRecords.push({ type: 'tool', record: tool });
|
|
1648
|
+
durableRecords.push(tool);
|
|
1557
1649
|
}
|
|
1558
|
-
|
|
1650
|
+
records.unshift(...legacyRecords);
|
|
1559
1651
|
}
|
|
1560
1652
|
await atomicWriteText(metaPath, JSON.stringify(meta));
|
|
1561
1653
|
await atomicWriteText(
|
|
@@ -1566,18 +1658,29 @@ export class DebugTrace {
|
|
|
1566
1658
|
await ensureDir(requestDir);
|
|
1567
1659
|
const eventPath = requestEventsPath(requestDir);
|
|
1568
1660
|
if (initialize) await prepareJsonlAppend(eventPath);
|
|
1569
|
-
|
|
1661
|
+
// Serialize/write one record at a time, not all batch strings plus a
|
|
1662
|
+
// second joined string. Raw diagnostics remain lossless on disk.
|
|
1663
|
+
const handle = await fsp.open(eventPath, 'a');
|
|
1664
|
+
try {
|
|
1665
|
+
for (const record of records) await handle.writeFile(`${JSON.stringify(record)}\n`, 'utf8');
|
|
1666
|
+
} finally { await handle.close(); }
|
|
1570
1667
|
if (writeMeta) {
|
|
1571
1668
|
const meta = serializableTraceMeta(trace);
|
|
1572
1669
|
await atomicWriteText(metaPath, JSON.stringify(meta));
|
|
1573
1670
|
this.#diskHeaders.set(trace.requestKey, meta);
|
|
1574
1671
|
}
|
|
1672
|
+
delete trace._failedAppend;
|
|
1673
|
+
this.#releasePersistedPayload(trace, durableRecords);
|
|
1575
1674
|
trace._persistedFormat = 'events';
|
|
1576
1675
|
trace._persistedRequestDir = requestDir;
|
|
1577
1676
|
if (legacyRequestDir && legacyRequestDir !== requestDir) {
|
|
1578
1677
|
await removeRequestDirIfIdentityMatches(legacyRequestDir, trace);
|
|
1579
1678
|
}
|
|
1580
1679
|
if (evictAfterWrite) {
|
|
1680
|
+
// Retention must not keep a second strong reference to finalized
|
|
1681
|
+
// _lastSnapshot after the primary cache has evicted the request.
|
|
1682
|
+
const retained = this.#retentionIndex.get(trace.sessionId || '')?.get(trace.requestKey);
|
|
1683
|
+
if (retained) retained.trace = serializableTraceMeta(trace);
|
|
1581
1684
|
this.#requestCache.delete(trace.requestKey);
|
|
1582
1685
|
this.#initializedRequestKeys.delete(trace.requestKey);
|
|
1583
1686
|
for (const [turnId, ctx] of this.#turnIndex) {
|
|
@@ -1585,6 +1688,9 @@ export class DebugTrace {
|
|
|
1585
1688
|
}
|
|
1586
1689
|
}
|
|
1587
1690
|
} catch (err) {
|
|
1691
|
+
// Keep immutable entries until a later append/close can durably retry
|
|
1692
|
+
// them. In particular, finalize must not evict a failed base/delta.
|
|
1693
|
+
trace._failedAppend = { records: batch.records, writeMeta, evictAfterWrite };
|
|
1588
1694
|
console.warn('[Yeaft] debug trace append failed:', err?.message || err);
|
|
1589
1695
|
}
|
|
1590
1696
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -336,9 +336,10 @@ function sleepWithAbort(ms, signal) {
|
|
|
336
336
|
* - `toolCalls` on assistant turns (the LLM's function_call requests)
|
|
337
337
|
* - `toolCallId` + `isError` on tool turns (the paired tool_result)
|
|
338
338
|
*
|
|
339
|
-
* Content is kept intact for the live protocol.
|
|
340
|
-
*
|
|
341
|
-
*
|
|
339
|
+
* Content is kept intact for the live protocol. Raw provider exchanges belong
|
|
340
|
+
* to the loop-level file-backed trace, never to its message snapshots: copying
|
|
341
|
+
* historical rawRequest values here multiplies retained request bodies on every
|
|
342
|
+
* tool loop. Do not even traverse diagnostic fields on incoming messages.
|
|
342
343
|
*
|
|
343
344
|
* Pure function — no side effects on the input message.
|
|
344
345
|
*
|
|
@@ -348,7 +349,6 @@ function sleepWithAbort(ms, signal) {
|
|
|
348
349
|
export function mapDebugMessage(m) {
|
|
349
350
|
const out = { role: m.role };
|
|
350
351
|
out.content = m.content;
|
|
351
|
-
if (m.rawRequest != null) out.rawRequest = m.rawRequest;
|
|
352
352
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
353
353
|
out.toolCalls = m.toolCalls.map(tc => ({
|
|
354
354
|
id: tc.id,
|
|
@@ -3741,7 +3741,10 @@ export class Engine {
|
|
|
3741
3741
|
// yielding any post-stream diagnostics. A consumer may stop iterating at
|
|
3742
3742
|
// any yield; persistence therefore cannot wait for turn_end or even the
|
|
3743
3743
|
// debug `loop` event below.
|
|
3744
|
-
|
|
3744
|
+
// The complete exchange is already owned by the per-loop debug trace.
|
|
3745
|
+
// Keeping it on model history retains every previous request and makes
|
|
3746
|
+
// subsequent debug snapshots grow with cumulative request bodies.
|
|
3747
|
+
const assistantMsg = { role: 'assistant', content: responseText, responseKind: 'progress' };
|
|
3745
3748
|
if (toolCalls.length > 0) {
|
|
3746
3749
|
assistantMsg.toolCalls = toolCalls.map(tc => ({
|
|
3747
3750
|
id: tc.id,
|