@yeaft/webchat-agent 1.0.197 → 1.0.199
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/local-runtime/web/app.bundle.js +83 -83
- 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/conversation/persist.js +180 -36
- package/yeaft/engine.js +3 -1
- package/yeaft/tools/types.js +1 -1
- package/yeaft/web-bridge.js +469 -189
|
Binary file
|
package/package.json
CHANGED
|
@@ -57,6 +57,10 @@ let DEFAULT_RECENT_TURNS = 20;
|
|
|
57
57
|
const RECENT_SESSION_SCAN_BASE_CAP = 64;
|
|
58
58
|
const RECENT_SESSION_SCAN_PER_TURN_CAP = 4;
|
|
59
59
|
const RECENT_SESSION_SCAN_MAX_CAP = 256;
|
|
60
|
+
// A normal Engine loop executes at most 30 tools, but parallel VPs can append
|
|
61
|
+
// many rows between a call and its result. One extra normal delta page keeps the
|
|
62
|
+
// scan bounded while leaving enough room to close a legitimate interleaved arc.
|
|
63
|
+
const DELTA_TOOL_PAIR_EXTENSION_CAP = 500;
|
|
60
64
|
|
|
61
65
|
|
|
62
66
|
const SEGMENT_INDEX_FILE = 'index.json';
|
|
@@ -235,6 +239,90 @@ function recentSessionScanCap(turnsLimit) {
|
|
|
235
239
|
return Math.min(RECENT_SESSION_SCAN_MAX_CAP, RECENT_SESSION_SCAN_BASE_CAP + turns * RECENT_SESSION_SCAN_PER_TURN_CAP);
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
function askUserToolIdentity(row, toolCallId) {
|
|
243
|
+
if (!row || typeof toolCallId !== 'string' || !toolCallId) return null;
|
|
244
|
+
return [
|
|
245
|
+
row.sessionId || '',
|
|
246
|
+
row.speakerVpId || '',
|
|
247
|
+
row.turnId || '',
|
|
248
|
+
row.threadId || 'main',
|
|
249
|
+
toolCallId,
|
|
250
|
+
].join('\u0000');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseAskUserResult(toolCall, toolResult) {
|
|
254
|
+
if (!toolCall || toolCall.name !== 'AskUser' || typeof toolCall.id !== 'string' || !toolCall.id
|
|
255
|
+
|| !toolResult || toolResult.isError) return null;
|
|
256
|
+
let payload = toolResult.content;
|
|
257
|
+
if (typeof payload === 'string') {
|
|
258
|
+
try { payload = JSON.parse(payload); } catch { return null; }
|
|
259
|
+
}
|
|
260
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
261
|
+
|
|
262
|
+
const question = typeof toolCall.input?.question === 'string'
|
|
263
|
+
? toolCall.input.question
|
|
264
|
+
: (typeof payload.question === 'string' ? payload.question : '');
|
|
265
|
+
const options = Array.isArray(toolCall.input?.options)
|
|
266
|
+
? toolCall.input.options.filter(option => typeof option === 'string')
|
|
267
|
+
: [];
|
|
268
|
+
if (payload.timedOut === true) {
|
|
269
|
+
return {
|
|
270
|
+
toolCallId: toolCall.id,
|
|
271
|
+
status: 'expired',
|
|
272
|
+
question,
|
|
273
|
+
options,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (!payload.answers || typeof payload.answers !== 'object' || Array.isArray(payload.answers)) return null;
|
|
277
|
+
return {
|
|
278
|
+
toolCallId: toolCall.id,
|
|
279
|
+
status: 'answered',
|
|
280
|
+
question,
|
|
281
|
+
options,
|
|
282
|
+
answers: payload.answers,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function projectVisibleSessionMessages(messages) {
|
|
287
|
+
const rows = Array.isArray(messages) ? messages : [];
|
|
288
|
+
const toolResults = new Map();
|
|
289
|
+
for (const row of rows) {
|
|
290
|
+
if (row?.role !== 'tool') continue;
|
|
291
|
+
const identity = askUserToolIdentity(row, row.toolCallId);
|
|
292
|
+
if (identity) toolResults.set(identity, row);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const visible = [];
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
if (!row || (row.role !== 'user' && row.role !== 'assistant')) continue;
|
|
298
|
+
if (row.role !== 'assistant' || !Array.isArray(row.toolCalls) || row.toolCalls.length === 0) {
|
|
299
|
+
if (row.role === 'assistant' && !row.content && !row.attachments && !row.images
|
|
300
|
+
&& !row.toolSummaryCount && !row.askUserResults) continue;
|
|
301
|
+
visible.push(row);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const askUserResults = [];
|
|
306
|
+
let omittedToolCount = 0;
|
|
307
|
+
for (const toolCall of row.toolCalls) {
|
|
308
|
+
const identity = askUserToolIdentity(row, toolCall?.id);
|
|
309
|
+
const result = parseAskUserResult(toolCall, identity ? toolResults.get(identity) : null);
|
|
310
|
+
if (result) askUserResults.push(result);
|
|
311
|
+
else omittedToolCount += 1;
|
|
312
|
+
}
|
|
313
|
+
const { toolCalls, ...rest } = row;
|
|
314
|
+
const projected = {
|
|
315
|
+
...rest,
|
|
316
|
+
...(omittedToolCount > 0 ? { toolSummaryCount: omittedToolCount } : {}),
|
|
317
|
+
...(askUserResults.length > 0 ? { askUserResults } : {}),
|
|
318
|
+
};
|
|
319
|
+
if (!projected.content && !projected.attachments && !projected.images
|
|
320
|
+
&& !projected.toolSummaryCount && !projected.askUserResults) continue;
|
|
321
|
+
visible.push(projected);
|
|
322
|
+
}
|
|
323
|
+
return visible;
|
|
324
|
+
}
|
|
325
|
+
|
|
238
326
|
// ─── Frontmatter helpers ─────────────────────────────────────
|
|
239
327
|
|
|
240
328
|
/**
|
|
@@ -1346,14 +1434,15 @@ export class ConversationStore {
|
|
|
1346
1434
|
const cutoff = Number.isFinite(beforeSeq) ? beforeSeq : Infinity;
|
|
1347
1435
|
const page = this.#loadRecentSessionWindow(sessionId, turnsLimit, {
|
|
1348
1436
|
beforeSeq: cutoff,
|
|
1349
|
-
roles:
|
|
1350
|
-
stripAssistantToolCalls:
|
|
1437
|
+
roles: null,
|
|
1438
|
+
stripAssistantToolCalls: false,
|
|
1351
1439
|
});
|
|
1352
|
-
|
|
1440
|
+
const messages = projectVisibleSessionMessages(page.messages);
|
|
1441
|
+
if (messages.length === 0) return { messages: [], oldestSeq: null, hasMore: page.truncated };
|
|
1353
1442
|
|
|
1354
|
-
const oldestSeq =
|
|
1443
|
+
const oldestSeq = messages.length ? parseSeqFromId(messages[0].id) : null;
|
|
1355
1444
|
return {
|
|
1356
|
-
messages
|
|
1445
|
+
messages,
|
|
1357
1446
|
oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
|
|
1358
1447
|
hasMore: page.truncated,
|
|
1359
1448
|
};
|
|
@@ -1368,8 +1457,8 @@ export class ConversationStore {
|
|
|
1368
1457
|
* cursor. When no visible rows changed after `afterSeq`, keep the cursor at
|
|
1369
1458
|
* least at `afterSeq` so an empty delta still completes the in-flight sync
|
|
1370
1459
|
* without downgrading the client to a cursor-less loaded state. Hidden rows
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1460
|
+
* advance the cursor only at pair-safe boundaries; a cursor must never cross
|
|
1461
|
+
* an assistant tool call before all of that call's result rows are included.
|
|
1373
1462
|
*
|
|
1374
1463
|
* @param {string} sessionId
|
|
1375
1464
|
* @param {number|null} afterSeq — exclusive lower bound
|
|
@@ -1382,19 +1471,81 @@ export class ConversationStore {
|
|
|
1382
1471
|
const cutoff = Number.isFinite(afterSeq) && afterSeq >= 0 ? afterSeq : null;
|
|
1383
1472
|
if (cutoff === null) return { messages: [], latestSeq: null };
|
|
1384
1473
|
const after = [];
|
|
1385
|
-
|
|
1474
|
+
const pendingToolResultIds = new Set();
|
|
1475
|
+
const completedBeforeCursor = new Set();
|
|
1476
|
+
const boundaryAssistants = [];
|
|
1477
|
+
let boundaryLookbackRows = 0;
|
|
1478
|
+
for (const previous of this.#iterateSessionRows(sessionId, { beforeSeq: cutoff + 1, desc: true })) {
|
|
1479
|
+
if (!previous || previous.sessionId !== sessionId) continue;
|
|
1480
|
+
boundaryLookbackRows += 1;
|
|
1481
|
+
if (boundaryLookbackRows > DELTA_TOOL_PAIR_EXTENSION_CAP) break;
|
|
1482
|
+
if (isHiddenConversationRow(previous)) continue;
|
|
1483
|
+
if (previous.role === 'tool' && typeof previous.toolCallId === 'string') {
|
|
1484
|
+
completedBeforeCursor.add(previous.toolCallId);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (previous.role === 'assistant' && Array.isArray(previous.toolCalls) && previous.toolCalls.length > 0) {
|
|
1488
|
+
const pendingIds = previous.toolCalls
|
|
1489
|
+
.map(toolCall => toolCall?.id)
|
|
1490
|
+
.filter(toolCallId => typeof toolCallId === 'string'
|
|
1491
|
+
&& toolCallId
|
|
1492
|
+
&& !completedBeforeCursor.has(toolCallId));
|
|
1493
|
+
if (pendingIds.length > 0) boundaryAssistants.push({ message: previous, pendingIds });
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (previous.role === 'user') break;
|
|
1497
|
+
}
|
|
1498
|
+
boundaryAssistants.sort((a, b) => compareMessagesBySeq(a.message, b.message));
|
|
1499
|
+
for (const boundary of boundaryAssistants) {
|
|
1500
|
+
after.push(boundary.message);
|
|
1501
|
+
for (const toolCallId of boundary.pendingIds) pendingToolResultIds.add(toolCallId);
|
|
1502
|
+
}
|
|
1503
|
+
const earliestBoundarySeq = boundaryAssistants.length > 0
|
|
1504
|
+
? parseSeqFromId(boundaryAssistants[0].message.id)
|
|
1505
|
+
: null;
|
|
1506
|
+
let safeCursorSeq = Number.isFinite(earliestBoundarySeq)
|
|
1507
|
+
? Math.max(0, earliestBoundarySeq - 1)
|
|
1508
|
+
: cutoff;
|
|
1509
|
+
let visibleRows = after.length;
|
|
1510
|
+
let extensionRows = 0;
|
|
1386
1511
|
for (const m of this.#iterateSessionRows(sessionId, { afterSeq: cutoff, desc: false })) {
|
|
1387
1512
|
if (!m || m.sessionId !== sessionId) continue;
|
|
1388
1513
|
const seq = parseSeqFromId(m.id);
|
|
1389
|
-
|
|
1390
|
-
if (
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1514
|
+
const hidden = isHiddenConversationRow(m);
|
|
1515
|
+
if (!hidden) {
|
|
1516
|
+
// Keep every outstanding call open across interleaved VP rows. Session
|
|
1517
|
+
// persistence is globally sequenced, so a sibling VP may append visible
|
|
1518
|
+
// messages between an assistant call and that call's result.
|
|
1519
|
+
after.push(m);
|
|
1520
|
+
visibleRows += 1;
|
|
1521
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
1522
|
+
for (const toolCall of m.toolCalls) {
|
|
1523
|
+
if (typeof toolCall?.id === 'string' && toolCall.id) pendingToolResultIds.add(toolCall.id);
|
|
1524
|
+
}
|
|
1525
|
+
} else if (m.role === 'tool' && typeof m.toolCallId === 'string') {
|
|
1526
|
+
pendingToolResultIds.delete(m.toolCallId);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (pendingToolResultIds.size === 0 && Number.isFinite(seq)) safeCursorSeq = seq;
|
|
1530
|
+
if (visibleRows < limit) continue;
|
|
1531
|
+
if (pendingToolResultIds.size === 0) break;
|
|
1532
|
+
extensionRows += 1;
|
|
1533
|
+
if (extensionRows >= DELTA_TOOL_PAIR_EXTENSION_CAP) break;
|
|
1534
|
+
}
|
|
1535
|
+
// If the extension cap stopped inside a malformed arc, return only the
|
|
1536
|
+
// prefix covered by the safe cursor. Returning later rows with an earlier
|
|
1537
|
+
// cursor would make the next delta repeat visible messages unnecessarily.
|
|
1538
|
+
const pairSafeRows = pendingToolResultIds.size === 0
|
|
1539
|
+
? after
|
|
1540
|
+
: after.filter(message => {
|
|
1541
|
+
const seq = parseSeqFromId(message?.id);
|
|
1542
|
+
return Number.isFinite(seq) && seq <= safeCursorSeq;
|
|
1543
|
+
});
|
|
1544
|
+
const sliced = pairSanitize(pairSafeRows);
|
|
1545
|
+
// Never advance past a row the sanitizer had to drop. A malformed or
|
|
1546
|
+
// over-cap tool arc must be retried from its assistant call rather than
|
|
1547
|
+
// turning the following result into a permanent orphan on the next page.
|
|
1548
|
+
return { messages: projectVisibleSessionMessages(sliced), latestSeq: safeCursorSeq };
|
|
1398
1549
|
}
|
|
1399
1550
|
|
|
1400
1551
|
/**
|
|
@@ -1480,31 +1631,33 @@ export class ConversationStore {
|
|
|
1480
1631
|
|
|
1481
1632
|
const beforeTurns = Math.min(10, Math.max(1, Number.isFinite(opts.beforeTurns) ? Math.floor(opts.beforeTurns) : 3));
|
|
1482
1633
|
const afterTurns = Math.min(10, Math.max(1, Number.isFinite(opts.afterTurns) ? Math.floor(opts.afterTurns) : 3));
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1634
|
+
const beforeRaw = this.#loadRecentSessionWindow(sessionId, beforeTurns + 1, {
|
|
1635
|
+
beforeSeq: anchorSeq + 1,
|
|
1636
|
+
roles: null,
|
|
1637
|
+
stripAssistantToolCalls: false,
|
|
1638
|
+
});
|
|
1639
|
+
const messages = beforeRaw.messages.slice();
|
|
1485
1640
|
const seen = new Set(messages.map(message => message?.id).filter(Boolean));
|
|
1486
1641
|
let followingUserTurns = 0;
|
|
1487
1642
|
|
|
1488
1643
|
for (const message of this.#iterateSessionRows(sessionId, { afterSeq: anchorSeq, desc: false })) {
|
|
1489
1644
|
if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
|
|
1490
|
-
if (message.role !== 'user' && message.role !== 'assistant') continue;
|
|
1491
1645
|
if (message.role === 'user') {
|
|
1492
1646
|
followingUserTurns += 1;
|
|
1493
1647
|
if (followingUserTurns > afterTurns) break;
|
|
1494
1648
|
}
|
|
1495
1649
|
if (message.id && seen.has(message.id)) continue;
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
if (projected.id) seen.add(projected.id);
|
|
1499
|
-
messages.push(projected);
|
|
1650
|
+
if (message.id) seen.add(message.id);
|
|
1651
|
+
messages.push(message);
|
|
1500
1652
|
}
|
|
1501
1653
|
|
|
1502
1654
|
messages.sort(compareMessagesBySeq);
|
|
1503
|
-
const
|
|
1655
|
+
const visibleMessages = projectVisibleSessionMessages(messages);
|
|
1656
|
+
const oldestSeq = visibleMessages.length > 0 ? parseSeqFromId(visibleMessages[0].id) : null;
|
|
1504
1657
|
return {
|
|
1505
|
-
messages,
|
|
1658
|
+
messages: visibleMessages,
|
|
1506
1659
|
oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
|
|
1507
|
-
hasMoreBefore:
|
|
1660
|
+
hasMoreBefore: beforeRaw.truncated,
|
|
1508
1661
|
};
|
|
1509
1662
|
}
|
|
1510
1663
|
|
|
@@ -2246,15 +2399,6 @@ export class ConversationStore {
|
|
|
2246
2399
|
return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
|
|
2247
2400
|
}
|
|
2248
2401
|
|
|
2249
|
-
#projectVisibleMessage(message) {
|
|
2250
|
-
if (!message || (message.role !== 'user' && message.role !== 'assistant')) return null;
|
|
2251
|
-
if (message.role !== 'assistant' || !Array.isArray(message.toolCalls) || message.toolCalls.length === 0) {
|
|
2252
|
-
return message;
|
|
2253
|
-
}
|
|
2254
|
-
const { toolCalls, ...rest } = message;
|
|
2255
|
-
return { ...rest, toolSummaryCount: toolCalls.length };
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
2402
|
#readSegmentRows(conversationDir, opts = {}) {
|
|
2259
2403
|
return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
|
|
2260
2404
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -1242,7 +1242,9 @@ export class Engine {
|
|
|
1242
1242
|
// Null in non-VP / test contexts — tools tolerate missing slots.
|
|
1243
1243
|
getCurrentTodos: vpCtx?.getCurrentTodos || null,
|
|
1244
1244
|
setCurrentTodos: vpCtx?.setCurrentTodos || null,
|
|
1245
|
-
askUser: vpCtx?.askUser
|
|
1245
|
+
askUser: typeof vpCtx?.askUser === 'function'
|
|
1246
|
+
? input => vpCtx.askUser(input, typeof vpCtx?.currentToolCall === 'function' ? vpCtx.currentToolCall() : null)
|
|
1247
|
+
: null,
|
|
1246
1248
|
// task-707: tool-callable end-turn signal. The engine threads this
|
|
1247
1249
|
// setter when constructing toolCtx so a tool (e.g. route_forward)
|
|
1248
1250
|
// can mark "after this batch, end the turn — do NOT call adapter
|
package/yeaft/tools/types.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* @property {number} [contextWindow] — current model's context window in
|
|
33
33
|
* tokens (used by ToolRegistry.execute to cap a single tool result at a
|
|
34
34
|
* fraction of the window so one runaway grep can't blow the wire).
|
|
35
|
-
* @property {(input: {question:string, options?:string[]}) => Promise<object>} [askUser]
|
|
35
|
+
* @property {(input: {question:string, options?:string[]}, toolCall?: {id?:string, name?:string}|null) => Promise<object>} [askUser]
|
|
36
36
|
* — host-provided interactive prompt. Resolves only after the user answers.
|
|
37
37
|
* @property {(reason?: string|object) => void} [requestEndTurn]
|
|
38
38
|
* — tool-callable signal that the current engine turn should end after
|