@yeaft/webchat-agent 1.0.517 → 1.0.519
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/index.js +1 -1
- package/local-runtime/server/handlers/client-workbench.js +11 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +138 -124
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/workbench/file-ops.js +18 -3
- package/workbench/file-reference-resolver.js +38 -19
- package/yeaft/conversation/persist.js +7 -0
- package/yeaft/debug-trace.js +29 -1
- package/yeaft/engine.js +78 -10
- package/yeaft/web-bridge.js +10 -1
|
Binary file
|
package/package.json
CHANGED
package/workbench/file-ops.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFile, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
|
|
1
|
+
import { readFile, realpath, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { join, basename, dirname, extname } from 'path';
|
|
3
|
+
import { join, basename, dirname, extname, isAbsolute, relative, resolve } from 'path';
|
|
4
4
|
import { platform } from 'os';
|
|
5
5
|
import ctx from '../context.js';
|
|
6
6
|
import { resolveAndValidatePath, BINARY_EXTENSIONS } from './utils.js';
|
|
@@ -9,6 +9,19 @@ import { sendWorkbenchResult } from './request-routing.js';
|
|
|
9
9
|
export const MAX_WORKBENCH_PREVIEW_BYTES = 20 * 1024 * 1024;
|
|
10
10
|
export const WORKBENCH_FILE_CHUNK_BYTES = 1024 * 1024;
|
|
11
11
|
|
|
12
|
+
async function validateResponseImagePath(filePath, workDir) {
|
|
13
|
+
const canonicalRoot = await realpath(resolve(workDir));
|
|
14
|
+
const canonicalFile = await realpath(resolveAndValidatePath(filePath, canonicalRoot));
|
|
15
|
+
const relativePath = relative(canonicalRoot, canonicalFile);
|
|
16
|
+
const outside = relativePath === '..'
|
|
17
|
+
|| relativePath.startsWith(`..${platform() === 'win32' ? '\\' : '/'}`)
|
|
18
|
+
|| isAbsolute(relativePath);
|
|
19
|
+
if (outside) throw new Error('Response image is outside the active workspace.');
|
|
20
|
+
const mimeType = BINARY_EXTENSIONS[extname(canonicalFile).toLowerCase()];
|
|
21
|
+
if (!mimeType?.startsWith('image/')) throw new Error('Response preview only supports image files.');
|
|
22
|
+
return canonicalFile;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
async function reportFileTransferDropped(msg, base) {
|
|
13
26
|
const errorResult = {
|
|
14
27
|
...base,
|
|
@@ -64,7 +77,9 @@ export async function handleReadFile(msg) {
|
|
|
64
77
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
65
78
|
|
|
66
79
|
try {
|
|
67
|
-
const resolved =
|
|
80
|
+
const resolved = msg.responseImagePreview
|
|
81
|
+
? await validateResponseImagePath(filePath, workDir)
|
|
82
|
+
: resolveAndValidatePath(filePath, workDir);
|
|
68
83
|
const ext = extname(resolved).toLowerCase();
|
|
69
84
|
const mimeType = BINARY_EXTENSIONS[ext];
|
|
70
85
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { basename, join, relative, resolve } from 'node:path';
|
|
1
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
import ctx from '../context.js';
|
|
5
5
|
import { resolveAndValidatePath } from './utils.js';
|
|
@@ -10,15 +10,28 @@ const MAX_SCANNED_ENTRIES = 5000;
|
|
|
10
10
|
const MAX_DEPTH = 10;
|
|
11
11
|
const SKIP_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'dist', 'build', '.cache', 'bin', 'obj']);
|
|
12
12
|
|
|
13
|
-
async function isFile(path) {
|
|
14
|
-
try { return (await stat(path)).isFile(); } catch { return false; }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
function comparable(value) {
|
|
18
14
|
const normalized = String(value || '').replaceAll('\\', '/');
|
|
19
15
|
return platform() === 'win32' ? normalized.toLowerCase() : normalized;
|
|
20
16
|
}
|
|
21
17
|
|
|
18
|
+
function isInsideWorkspace(root, candidate) {
|
|
19
|
+
const relativePath = relative(root, candidate);
|
|
20
|
+
return relativePath === '' || (relativePath !== '..'
|
|
21
|
+
&& !relativePath.startsWith(`..${platform() === 'win32' ? '\\' : '/'}`)
|
|
22
|
+
&& !isAbsolute(relativePath));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function canonicalWorkspaceFile(candidate, canonicalRoot) {
|
|
26
|
+
try {
|
|
27
|
+
const canonicalPath = await realpath(candidate);
|
|
28
|
+
if (!isInsideWorkspace(canonicalRoot, canonicalPath) || !(await stat(canonicalPath)).isFile()) return null;
|
|
29
|
+
return canonicalPath;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
async function findUniqueBasenames(workDir, requestedPaths, {
|
|
23
36
|
maxScannedEntries = MAX_SCANNED_ENTRIES,
|
|
24
37
|
maxDepth = MAX_DEPTH,
|
|
@@ -67,24 +80,30 @@ export async function resolveFileReferences(references, workDir, scanOptions) {
|
|
|
67
80
|
const unique = [...new Set((Array.isArray(references) ? references : [])
|
|
68
81
|
.map(value => typeof value === 'string' ? value.trim() : '')
|
|
69
82
|
.filter(Boolean))].slice(0, MAX_REFERENCES);
|
|
70
|
-
const exactMatches = await Promise.all(unique.map(async requestedPath => {
|
|
71
|
-
const exactPath = resolveAndValidatePath(requestedPath, workDir);
|
|
72
|
-
return await isFile(exactPath) ? exactPath : null;
|
|
73
|
-
}));
|
|
74
|
-
const unresolved = unique.filter((_path, index) => !exactMatches[index]);
|
|
75
|
-
const basenameScan = await findUniqueBasenames(workDir, unresolved, scanOptions);
|
|
76
83
|
const root = resolve(workDir);
|
|
84
|
+
const canonicalRoot = await realpath(root);
|
|
85
|
+
const exactCandidates = unique.map(requestedPath => resolveAndValidatePath(requestedPath, root));
|
|
86
|
+
const exactMatches = await Promise.all(exactCandidates.map(exactPath => (
|
|
87
|
+
canonicalWorkspaceFile(exactPath, canonicalRoot)
|
|
88
|
+
)));
|
|
89
|
+
const unresolved = unique.filter((_path, index) => (
|
|
90
|
+
!exactMatches[index] && isInsideWorkspace(root, exactCandidates[index])
|
|
91
|
+
));
|
|
92
|
+
const basenameScan = await findUniqueBasenames(root, unresolved, scanOptions);
|
|
77
93
|
|
|
78
|
-
|
|
94
|
+
const resolvedEntries = await Promise.all(unique.map(async (requestedPath, index) => {
|
|
79
95
|
const matches = basenameScan.matches.get(comparable(basename(requestedPath))) || [];
|
|
80
|
-
const fallbackPath = basenameScan.complete && matches.length === 1
|
|
96
|
+
const fallbackPath = basenameScan.complete && matches.length === 1
|
|
97
|
+
? await canonicalWorkspaceFile(matches[0], canonicalRoot)
|
|
98
|
+
: null;
|
|
81
99
|
const matchedPath = exactMatches[index] || fallbackPath;
|
|
82
|
-
if (!matchedPath) return
|
|
83
|
-
return
|
|
100
|
+
if (!matchedPath) return null;
|
|
101
|
+
return {
|
|
84
102
|
requestedPath,
|
|
85
|
-
resolvedPath: relative(
|
|
86
|
-
}
|
|
87
|
-
});
|
|
103
|
+
resolvedPath: relative(canonicalRoot, matchedPath).replaceAll('\\', '/') || basename(matchedPath),
|
|
104
|
+
};
|
|
105
|
+
}));
|
|
106
|
+
return resolvedEntries.filter(Boolean);
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
export async function handleResolveFileReferences(msg) {
|
|
@@ -413,6 +413,9 @@ function serializeMessage(msg) {
|
|
|
413
413
|
if (msg.model) fm.push(`model: ${msg.model}`);
|
|
414
414
|
if (msg.effort) fm.push(`effort: ${msg.effort}`);
|
|
415
415
|
if (Number.isInteger(msg.llmCallCount) && msg.llmCallCount > 0) fm.push(`llmCallCount: ${msg.llmCallCount}`);
|
|
416
|
+
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'totalMs']) {
|
|
417
|
+
if (Number.isFinite(msg[key]) && msg[key] >= 0) fm.push(`${key}: ${Math.round(msg[key])}`);
|
|
418
|
+
}
|
|
416
419
|
if (msg.turnNumber != null) fm.push(`turnNumber: ${msg.turnNumber}`);
|
|
417
420
|
if (msg.toolCallId) fm.push(`toolCallId: ${msg.toolCallId}`);
|
|
418
421
|
if (msg.eventType) fm.push(`eventType: ${msg.eventType}`);
|
|
@@ -564,6 +567,10 @@ export function parseMessage(raw) {
|
|
|
564
567
|
case 'model': msg.model = value; break;
|
|
565
568
|
case 'effort': msg.effort = value; break;
|
|
566
569
|
case 'llmCallCount': msg.llmCallCount = parseInt(value, 10); break;
|
|
570
|
+
case 'inputTokens': msg.inputTokens = parseInt(value, 10); break;
|
|
571
|
+
case 'outputTokens': msg.outputTokens = parseInt(value, 10); break;
|
|
572
|
+
case 'totalTokens': msg.totalTokens = parseInt(value, 10); break;
|
|
573
|
+
case 'totalMs': msg.totalMs = parseInt(value, 10); break;
|
|
567
574
|
case 'turnNumber': msg.turnNumber = parseInt(value, 10); break;
|
|
568
575
|
case 'toolCallId': msg.toolCallId = value; break;
|
|
569
576
|
case 'eventType': msg.eventType = value; break;
|
package/yeaft/debug-trace.js
CHANGED
|
@@ -332,10 +332,27 @@ function messagesPrefixLength(prevMessages, nextMessages) {
|
|
|
332
332
|
return i;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
function normalizeRequestInputBreakdown(value) {
|
|
336
|
+
if (!value || typeof value !== 'object') return null;
|
|
337
|
+
const normalized = {};
|
|
338
|
+
for (const key of [
|
|
339
|
+
'systemPromptTokens',
|
|
340
|
+
'historyMessageTokens',
|
|
341
|
+
'toolDefinitionTokens',
|
|
342
|
+
'currentTurnTokens',
|
|
343
|
+
'totalEstimatedTokens',
|
|
344
|
+
]) {
|
|
345
|
+
const number = Number(value[key]);
|
|
346
|
+
normalized[key] = Number.isFinite(number) && number >= 0 ? Math.floor(number) : 0;
|
|
347
|
+
}
|
|
348
|
+
return normalized;
|
|
349
|
+
}
|
|
350
|
+
|
|
335
351
|
function buildRequestSnapshot(info = {}) {
|
|
336
352
|
return {
|
|
337
353
|
systemPrompt: String(info.systemPrompt || ''),
|
|
338
354
|
messages: Array.isArray(info.messages) ? cloneJsonValue(info.messages) : [],
|
|
355
|
+
requestInputBreakdown: normalizeRequestInputBreakdown(info.requestInputBreakdown),
|
|
339
356
|
rawRequest: info.rawRequest ?? null,
|
|
340
357
|
};
|
|
341
358
|
}
|
|
@@ -346,6 +363,7 @@ function buildRequestDelta(previous, next) {
|
|
|
346
363
|
base: true,
|
|
347
364
|
systemPrompt: next.systemPrompt || '',
|
|
348
365
|
messages: Array.isArray(next.messages) ? next.messages : [],
|
|
366
|
+
requestInputBreakdown: next.requestInputBreakdown || null,
|
|
349
367
|
};
|
|
350
368
|
const rawRequestDelta = buildRawRequestDelta(null, next.rawRequest);
|
|
351
369
|
if (rawRequestDelta) delta.rawRequestDelta = rawRequestDelta;
|
|
@@ -355,6 +373,9 @@ function buildRequestDelta(previous, next) {
|
|
|
355
373
|
if ((next.systemPrompt || '') !== (previous.systemPrompt || '')) {
|
|
356
374
|
delta.systemPrompt = next.systemPrompt || '';
|
|
357
375
|
}
|
|
376
|
+
if (!stableEqual(next.requestInputBreakdown || null, previous.requestInputBreakdown || null)) {
|
|
377
|
+
delta.requestInputBreakdown = next.requestInputBreakdown || null;
|
|
378
|
+
}
|
|
358
379
|
const prevMessages = Array.isArray(previous.messages) ? previous.messages : [];
|
|
359
380
|
const nextMessages = Array.isArray(next.messages) ? next.messages : [];
|
|
360
381
|
const prefix = messagesPrefixLength(prevMessages, nextMessages);
|
|
@@ -371,16 +392,18 @@ function buildRequestDelta(previous, next) {
|
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
function applyRequestDelta(previous, delta = {}) {
|
|
374
|
-
const base = previous || { systemPrompt: '', messages: [], rawRequest: null };
|
|
395
|
+
const base = previous || { systemPrompt: '', messages: [], requestInputBreakdown: null, rawRequest: null };
|
|
375
396
|
const next = {
|
|
376
397
|
systemPrompt: base.systemPrompt || '',
|
|
377
398
|
messages: Array.isArray(base.messages) ? [...base.messages] : [],
|
|
399
|
+
requestInputBreakdown: normalizeRequestInputBreakdown(base.requestInputBreakdown),
|
|
378
400
|
rawRequest: base.rawRequest ?? null,
|
|
379
401
|
};
|
|
380
402
|
if (delta.base) {
|
|
381
403
|
const nextBase = {
|
|
382
404
|
systemPrompt: delta.systemPrompt || '',
|
|
383
405
|
messages: Array.isArray(delta.messages) ? delta.messages : [],
|
|
406
|
+
requestInputBreakdown: normalizeRequestInputBreakdown(delta.requestInputBreakdown),
|
|
384
407
|
rawRequest: Object.prototype.hasOwnProperty.call(delta, 'rawRequest') ? delta.rawRequest : null,
|
|
385
408
|
};
|
|
386
409
|
if (Object.prototype.hasOwnProperty.call(delta, 'rawRequestDelta')) {
|
|
@@ -389,6 +412,9 @@ function applyRequestDelta(previous, delta = {}) {
|
|
|
389
412
|
return nextBase;
|
|
390
413
|
}
|
|
391
414
|
if (typeof delta.systemPrompt === 'string') next.systemPrompt = delta.systemPrompt;
|
|
415
|
+
if (Object.prototype.hasOwnProperty.call(delta, 'requestInputBreakdown')) {
|
|
416
|
+
next.requestInputBreakdown = normalizeRequestInputBreakdown(delta.requestInputBreakdown);
|
|
417
|
+
}
|
|
392
418
|
if (Array.isArray(delta.messages)) {
|
|
393
419
|
next.messages = delta.messages;
|
|
394
420
|
} else if (Array.isArray(delta.messagesAppend)) {
|
|
@@ -639,6 +665,7 @@ function expandTrace(trace) {
|
|
|
639
665
|
// its actual source loop. Messages remain limited to the final loop.
|
|
640
666
|
systemPrompt: '',
|
|
641
667
|
messages: [],
|
|
668
|
+
requestInputBreakdown: snapshot.requestInputBreakdown || null,
|
|
642
669
|
response: loop.response || '',
|
|
643
670
|
toolCalls: Array.isArray(loop.toolCalls) ? loop.toolCalls : [],
|
|
644
671
|
usage,
|
|
@@ -966,6 +993,7 @@ export class DebugTrace {
|
|
|
966
993
|
trace.baseRequest = {
|
|
967
994
|
systemPrompt: snapshot.systemPrompt,
|
|
968
995
|
messages: Array.isArray(snapshot.messages) ? cloneJsonValue(snapshot.messages) : [],
|
|
996
|
+
requestInputBreakdown: snapshot.requestInputBreakdown,
|
|
969
997
|
rawRequest: snapshot.rawRequest ?? null,
|
|
970
998
|
};
|
|
971
999
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -33,7 +33,13 @@ import {
|
|
|
33
33
|
DEFAULT_PROJECT_DOC_MAX_BYTES,
|
|
34
34
|
} from './sessions/project-doc.js';
|
|
35
35
|
import { archiveToolResults } from './archive/tool-results.js';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
trimSnapshotForBudget,
|
|
38
|
+
estimateMessageTokens,
|
|
39
|
+
estimateMessagesTokens as estimateHistoryMessagesTokens,
|
|
40
|
+
buildHistoryBuckets,
|
|
41
|
+
fitProviderRequestToContext,
|
|
42
|
+
} from './history-window.js';
|
|
37
43
|
import { recallConversationTurns } from './conversation/history-index.js';
|
|
38
44
|
import { parseSeqFromId } from './conversation/persist.js';
|
|
39
45
|
import { isVpForeign, readContent as readScopeContent } from './memory/store.js';
|
|
@@ -413,6 +419,41 @@ export function estimateMessagesTokens(system, messages) {
|
|
|
413
419
|
return total;
|
|
414
420
|
}
|
|
415
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Estimate the input composition of the exact provider request copy. These are
|
|
424
|
+
* diagnostic estimates only; provider usage remains authoritative for billing.
|
|
425
|
+
* `historyMessageCount` is the final post-fit boundary between historical rows
|
|
426
|
+
* and current-turn rows.
|
|
427
|
+
*
|
|
428
|
+
* @param {{ systemPrompt?:string, messages?:Array<object>, historyMessageCount?:number, toolDefs?:Array<object> }} input
|
|
429
|
+
* @returns {{systemPromptTokens:number, historyMessageTokens:number, toolDefinitionTokens:number, currentTurnTokens:number, totalEstimatedTokens:number}}
|
|
430
|
+
*/
|
|
431
|
+
export function estimateProviderInputBreakdown({
|
|
432
|
+
systemPrompt = '',
|
|
433
|
+
messages = [],
|
|
434
|
+
historyMessageCount = 0,
|
|
435
|
+
toolDefs = [],
|
|
436
|
+
} = {}) {
|
|
437
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
438
|
+
const split = Math.max(0, Math.min(
|
|
439
|
+
source.length,
|
|
440
|
+
Number.isInteger(historyMessageCount) ? historyMessageCount : 0,
|
|
441
|
+
));
|
|
442
|
+
const systemPromptTokens = approxTokens(typeof systemPrompt === 'string' ? systemPrompt : '');
|
|
443
|
+
const historyMessageTokens = estimateHistoryMessagesTokens(source.slice(0, split));
|
|
444
|
+
const currentTurnTokens = estimateHistoryMessagesTokens(source.slice(split));
|
|
445
|
+
const toolDefinitionTokens = Array.isArray(toolDefs) && toolDefs.length > 0
|
|
446
|
+
? approxTokens(JSON.stringify(toolDefs))
|
|
447
|
+
: 0;
|
|
448
|
+
return {
|
|
449
|
+
systemPromptTokens,
|
|
450
|
+
historyMessageTokens,
|
|
451
|
+
toolDefinitionTokens,
|
|
452
|
+
currentTurnTokens,
|
|
453
|
+
totalEstimatedTokens: systemPromptTokens + historyMessageTokens + toolDefinitionTokens + currentTurnTokens,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
416
457
|
export const GROUP_CONTEXT_PRESSURE_RATIO = 0.8;
|
|
417
458
|
|
|
418
459
|
export function shouldAllowGroupReflection({
|
|
@@ -2814,6 +2855,8 @@ export class Engine {
|
|
|
2814
2855
|
reportedSkillError = skillResolutionError;
|
|
2815
2856
|
systemPrompt = buildCurrentSystemPrompt();
|
|
2816
2857
|
if (executionPolicy?.prompt) systemPrompt += `\n\n${executionPolicy.prompt}`;
|
|
2858
|
+
let requestDebugMessages = [];
|
|
2859
|
+
let requestInputBreakdown = null;
|
|
2817
2860
|
|
|
2818
2861
|
try {
|
|
2819
2862
|
// Resolve effort per provider request so a saved Session effort takes
|
|
@@ -2941,6 +2984,7 @@ export class Engine {
|
|
|
2941
2984
|
language: requestConfig.language,
|
|
2942
2985
|
});
|
|
2943
2986
|
wireMessages = fittedRequest.messages;
|
|
2987
|
+
historyMessageCount = fittedRequest.meta.historyMessagesAfter;
|
|
2944
2988
|
if (fittedRequest.meta.droppedHistoryMessages > 0
|
|
2945
2989
|
|| fittedRequest.meta.droppedCurrentMessages > 0
|
|
2946
2990
|
|| providerContextScale < 1) {
|
|
@@ -2996,6 +3040,17 @@ export class Engine {
|
|
|
2996
3040
|
}
|
|
2997
3041
|
}
|
|
2998
3042
|
|
|
3043
|
+
// Freeze diagnostics only after every provider-only trim/sweep has
|
|
3044
|
+
// completed, so debug describes the exact request copy rather than the
|
|
3045
|
+
// durable transcript or an earlier preflight candidate.
|
|
3046
|
+
requestDebugMessages = wireMessages.map(mapDebugMessage);
|
|
3047
|
+
requestInputBreakdown = estimateProviderInputBreakdown({
|
|
3048
|
+
systemPrompt,
|
|
3049
|
+
messages: wireMessages,
|
|
3050
|
+
historyMessageCount: Math.min(historyMessageCount, wireMessages.length),
|
|
3051
|
+
toolDefs,
|
|
3052
|
+
});
|
|
3053
|
+
|
|
2999
3054
|
// Capture only the provider route before the visible boundary. The
|
|
3000
3055
|
// returned async iterable must not make a request or write durable
|
|
3001
3056
|
// state; both the retry continuation and Work Center EngineTurn remain
|
|
@@ -3281,7 +3336,8 @@ export class Engine {
|
|
|
3281
3336
|
latencyMs,
|
|
3282
3337
|
responseText,
|
|
3283
3338
|
systemPrompt,
|
|
3284
|
-
messages:
|
|
3339
|
+
messages: requestDebugMessages,
|
|
3340
|
+
requestInputBreakdown,
|
|
3285
3341
|
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
3286
3342
|
usage: {
|
|
3287
3343
|
inputTokens: totalUsage.inputTokens || 0,
|
|
@@ -3549,7 +3605,8 @@ export class Engine {
|
|
|
3549
3605
|
// error path too — failure traces are the most valuable for
|
|
3550
3606
|
// hydration.
|
|
3551
3607
|
systemPrompt,
|
|
3552
|
-
messages:
|
|
3608
|
+
messages: requestDebugMessages,
|
|
3609
|
+
requestInputBreakdown,
|
|
3553
3610
|
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
3554
3611
|
usage: {
|
|
3555
3612
|
inputTokens: totalUsage.inputTokens || 0,
|
|
@@ -3575,7 +3632,8 @@ export class Engine {
|
|
|
3575
3632
|
loopNumber: turnNumber,
|
|
3576
3633
|
model: currentModel,
|
|
3577
3634
|
systemPrompt,
|
|
3578
|
-
messages:
|
|
3635
|
+
messages: requestDebugMessages,
|
|
3636
|
+
requestInputBreakdown,
|
|
3579
3637
|
response: responseText || `Error: ${err.message}`,
|
|
3580
3638
|
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
3581
3639
|
usage: {
|
|
@@ -3654,7 +3712,8 @@ export class Engine {
|
|
|
3654
3712
|
// in-memory — without these columns the user can never see
|
|
3655
3713
|
// history from before the panel was opened.
|
|
3656
3714
|
systemPrompt,
|
|
3657
|
-
messages:
|
|
3715
|
+
messages: requestDebugMessages,
|
|
3716
|
+
requestInputBreakdown,
|
|
3658
3717
|
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
3659
3718
|
usage: {
|
|
3660
3719
|
inputTokens: totalUsage.inputTokens || 0,
|
|
@@ -3749,7 +3808,8 @@ export class Engine {
|
|
|
3749
3808
|
loopNumber: turnNumber,
|
|
3750
3809
|
model: currentModel,
|
|
3751
3810
|
systemPrompt,
|
|
3752
|
-
messages:
|
|
3811
|
+
messages: requestDebugMessages,
|
|
3812
|
+
requestInputBreakdown,
|
|
3753
3813
|
response: responseText,
|
|
3754
3814
|
toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
|
|
3755
3815
|
usage: {
|
|
@@ -4926,13 +4986,19 @@ export class Engine {
|
|
|
4926
4986
|
// Loop back to call adapter again with tool results
|
|
4927
4987
|
}
|
|
4928
4988
|
|
|
4989
|
+
const totalMs = Date.now() - queryStartedAt;
|
|
4990
|
+
const totalTokens = cumulativeInputTokens + cumulativeOutputTokens;
|
|
4929
4991
|
// Store final provider metadata on the response row itself. Debug events
|
|
4930
|
-
// are transient; the response card must retain
|
|
4931
|
-
//
|
|
4992
|
+
// are transient; the response card must retain model, call count, token
|
|
4993
|
+
// usage, and duration after a reload.
|
|
4932
4994
|
const llmCountMessage = lastPersistedAssistantTextMessage || lastPersistedAssistantMessage;
|
|
4933
4995
|
if (llmCountMessage && typeof this.#conversationStore?.update === 'function') {
|
|
4934
4996
|
const updated = this.#conversationStore.update(llmCountMessage, {
|
|
4935
4997
|
llmCallCount: turnNumber,
|
|
4998
|
+
inputTokens: cumulativeInputTokens,
|
|
4999
|
+
outputTokens: cumulativeOutputTokens,
|
|
5000
|
+
totalTokens,
|
|
5001
|
+
totalMs,
|
|
4936
5002
|
...(lastSuccessfulModel ? { model: lastSuccessfulModel } : {}),
|
|
4937
5003
|
...(lastSuccessfulEffort ? { effort: lastSuccessfulEffort } : {}),
|
|
4938
5004
|
});
|
|
@@ -4948,8 +5014,10 @@ export class Engine {
|
|
|
4948
5014
|
type: 'turn_close',
|
|
4949
5015
|
turnId: queryTurnId,
|
|
4950
5016
|
threadId,
|
|
4951
|
-
totalMs
|
|
4952
|
-
|
|
5017
|
+
totalMs,
|
|
5018
|
+
inputTokens: cumulativeInputTokens,
|
|
5019
|
+
outputTokens: cumulativeOutputTokens,
|
|
5020
|
+
totalTokens,
|
|
4953
5021
|
loopCount: turnNumber,
|
|
4954
5022
|
...(lastSuccessfulModel ? { model: lastSuccessfulModel } : {}),
|
|
4955
5023
|
...(lastSuccessfulEffort ? { effort: lastSuccessfulEffort } : {}),
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1408,6 +1408,9 @@ function projectPersistedToHistoryEntry(m, { includeReflections = false } = {})
|
|
|
1408
1408
|
if (m.imageAssetAnchor) entry.imageAssetAnchor = true;
|
|
1409
1409
|
if (m.responseKind === 'progress' || m.responseKind === 'result') entry.responseKind = m.responseKind;
|
|
1410
1410
|
if (Number.isInteger(m.llmCallCount) && m.llmCallCount > 0) entry.llmCallCount = m.llmCallCount;
|
|
1411
|
+
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'totalMs']) {
|
|
1412
|
+
if (Number.isFinite(m[key]) && m[key] >= 0) entry[key] = m[key];
|
|
1413
|
+
}
|
|
1411
1414
|
if (typeof m.model === 'string' && m.model) entry.model = m.model;
|
|
1412
1415
|
if (typeof m.effort === 'string' && m.effort) entry.effort = m.effort;
|
|
1413
1416
|
if (m.incomplete === true) entry.incomplete = true;
|
|
@@ -1551,6 +1554,10 @@ function projectVisibleHistoryChunkMessages(messages = []) {
|
|
|
1551
1554
|
...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
|
|
1552
1555
|
...(m.responseKind === 'progress' || m.responseKind === 'result' ? { responseKind: m.responseKind } : {}),
|
|
1553
1556
|
...(Number.isInteger(m.llmCallCount) && m.llmCallCount > 0 ? { llmCallCount: m.llmCallCount } : {}),
|
|
1557
|
+
...(Number.isFinite(m.inputTokens) && m.inputTokens >= 0 ? { inputTokens: m.inputTokens } : {}),
|
|
1558
|
+
...(Number.isFinite(m.outputTokens) && m.outputTokens >= 0 ? { outputTokens: m.outputTokens } : {}),
|
|
1559
|
+
...(Number.isFinite(m.totalTokens) && m.totalTokens >= 0 ? { totalTokens: m.totalTokens } : {}),
|
|
1560
|
+
...(Number.isFinite(m.totalMs) && m.totalMs >= 0 ? { totalMs: m.totalMs } : {}),
|
|
1554
1561
|
...(typeof m.model === 'string' && m.model ? { model: m.model } : {}),
|
|
1555
1562
|
...(typeof m.effort === 'string' && m.effort ? { effort: m.effort } : {}),
|
|
1556
1563
|
...(m.incomplete === true ? { incomplete: true } : {}),
|
|
@@ -4568,6 +4575,8 @@ function handleEngineEvent(event, hctx) {
|
|
|
4568
4575
|
turnId: event.turnId,
|
|
4569
4576
|
threadId: event.threadId,
|
|
4570
4577
|
totalMs: event.totalMs,
|
|
4578
|
+
inputTokens: event.inputTokens,
|
|
4579
|
+
outputTokens: event.outputTokens,
|
|
4571
4580
|
totalTokens: event.totalTokens,
|
|
4572
4581
|
loopCount: event.loopCount,
|
|
4573
4582
|
...(typeof event.model === 'string' && event.model ? { model: event.model } : {}),
|
|
@@ -5660,7 +5669,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5660
5669
|
resetQueryTimer();
|
|
5661
5670
|
|
|
5662
5671
|
// Emit turn_start so frontend can create the message block.
|
|
5663
|
-
sendSessionEvent({ type: 'vp_turn_start', vpId, threadId, turnId, sessionId, title: thread?.title || '' }, envelope);
|
|
5672
|
+
sendSessionEvent({ type: 'vp_turn_start', vpId, threadId, turnId, sessionId, title: thread?.title || '', ts: turnStartAt }, envelope);
|
|
5664
5673
|
// vp-status: LLM call about to start, no text/tool yet → 'thinking'.
|
|
5665
5674
|
try {
|
|
5666
5675
|
getVpStatusBroker().transition({ sessionId, vpId, threadId, title: thread?.title || '', state: 'thinking', turnId, messageCount: thread?.messageIds?.length || 0 });
|