@yeaft/webchat-agent 1.0.115 → 1.0.117
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/package.json +1 -1
- package/yeaft/engine.js +45 -11
- package/yeaft/memory/store.js +1 -1
- package/yeaft/sessions/pre-flow.js +89 -7
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -379,7 +379,7 @@ export function shouldAllowGroupReflection({
|
|
|
379
379
|
* @param {{
|
|
380
380
|
* sessionId?: string|null,
|
|
381
381
|
* ownVpId?: string|null,
|
|
382
|
-
* summaries: { user?: string, session?: string, vp?: string }
|
|
382
|
+
* summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}> }
|
|
383
383
|
* }} args
|
|
384
384
|
* @returns {Array<{scope: string, summary: string}>}
|
|
385
385
|
*/
|
|
@@ -390,6 +390,11 @@ export function buildResidentEntries(args) {
|
|
|
390
390
|
if (args.sessionId && summaries.session) {
|
|
391
391
|
out.push({ scope: `sessions/${args.sessionId}`, summary: summaries.session });
|
|
392
392
|
}
|
|
393
|
+
if (args.sessionId && Array.isArray(summaries.topics)) {
|
|
394
|
+
for (const topic of summaries.topics) {
|
|
395
|
+
if (topic?.scope && topic?.summary) out.push({ scope: topic.scope, summary: topic.summary });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
393
398
|
// VP per-session isolation (2026-06-09): the VP summary scope MUST be
|
|
394
399
|
// session-qualified. The legacy bare `vp/<id>` scope was a structural
|
|
395
400
|
// (see #loadLayerASummaries, kind:'group-vp'), so labelling it `vp/<id>`
|
|
@@ -783,12 +788,13 @@ export class Engine {
|
|
|
783
788
|
* dream tick (Phase 6) is what populates these; on a fresh install they
|
|
784
789
|
* all return ''.
|
|
785
790
|
*
|
|
786
|
-
* @param {{sessionId?: string, vpId?: string, language?: string}} ctx
|
|
787
|
-
* @returns {Promise<{user:string, session:string, vp:string}>}
|
|
791
|
+
* @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[]}} ctx
|
|
792
|
+
* @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>}>}
|
|
788
793
|
*/
|
|
789
|
-
async #loadLayerASummaries({ sessionId, vpId, language } = {}) {
|
|
790
|
-
if (!this.#yeaftDir) return { user: '', session: '', vp: '' };
|
|
794
|
+
async #loadLayerASummaries({ sessionId, vpId, language, topicScopes } = {}) {
|
|
795
|
+
if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [] };
|
|
791
796
|
const memoryRoot = `${this.#yeaftDir}/memory`;
|
|
797
|
+
const topicScopeList = Array.isArray(topicScopes) ? topicScopes.slice(0, 12) : [];
|
|
792
798
|
const tasks = [
|
|
793
799
|
readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
|
|
794
800
|
sessionId
|
|
@@ -797,17 +803,24 @@ export class Engine {
|
|
|
797
803
|
vpId && sessionId
|
|
798
804
|
? readScopeSummary({ kind: 'session-vp', sessionId, id: vpId }, { root: memoryRoot, language }).catch(() => '')
|
|
799
805
|
: Promise.resolve(''),
|
|
806
|
+
Promise.all(topicScopeList.map(scope => readTopicSummary(scope, { root: memoryRoot, language }))),
|
|
800
807
|
];
|
|
801
|
-
const [user, session, vp] = await Promise.all(tasks);
|
|
802
|
-
|
|
808
|
+
const [user, session, vp, topicsRaw] = await Promise.all(tasks);
|
|
809
|
+
const topics = (topicsRaw || []).filter(t => t && t.summary);
|
|
810
|
+
return { user: user || '', session: session || '', vp: vp || '', topics };
|
|
803
811
|
}
|
|
804
812
|
|
|
805
813
|
async #loadSessionTopicLabels(sessionId, limit = 8) {
|
|
814
|
+
return (await this.#loadSessionTopicScopes(sessionId, limit))
|
|
815
|
+
.map(scope => scope.replace(/^sessions\/[^/]+\/topic\//, ''));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async #loadSessionTopicScopes(sessionId, limit = 24) {
|
|
806
819
|
if (!this.#yeaftDir || !sessionId) return [];
|
|
807
820
|
const topicRoot = join(this.#yeaftDir, 'memory', 'sessions', sessionId, 'topic');
|
|
808
821
|
const labels = [];
|
|
809
822
|
await collectTopicLabels(topicRoot, '', labels, limit).catch(() => {});
|
|
810
|
-
return labels;
|
|
823
|
+
return labels.map(label => `sessions/${sessionId}/topic/${label}`);
|
|
811
824
|
}
|
|
812
825
|
|
|
813
826
|
/**
|
|
@@ -1240,7 +1253,7 @@ export class Engine {
|
|
|
1240
1253
|
* without injection.
|
|
1241
1254
|
*
|
|
1242
1255
|
* @param {string} prompt
|
|
1243
|
-
* @param {{ sessionId?: string, vpId?: string }} [ctx]
|
|
1256
|
+
* @param {{ sessionId?: string, vpId?: string, extraScopes?: string[] }} [ctx]
|
|
1244
1257
|
* @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
|
|
1245
1258
|
*/
|
|
1246
1259
|
async #recallMemory(prompt, ctx = {}) {
|
|
@@ -1252,6 +1265,8 @@ export class Engine {
|
|
|
1252
1265
|
sessionId: ctx.sessionId,
|
|
1253
1266
|
chatId: ctx.chatId || this.#chatId,
|
|
1254
1267
|
vpId: ctx.vpId,
|
|
1268
|
+
extraScopes: ctx.extraScopes,
|
|
1269
|
+
fallbackOnEmpty: true,
|
|
1255
1270
|
});
|
|
1256
1271
|
memory.profile = result.profile || '';
|
|
1257
1272
|
memory.entries = result.entries || [];
|
|
@@ -1784,11 +1799,13 @@ export class Engine {
|
|
|
1784
1799
|
let memoryInjection = '';
|
|
1785
1800
|
let recallEntryCount = 0;
|
|
1786
1801
|
|
|
1802
|
+
const topicScopesForMemory = await this.#loadSessionTopicScopes(sessionId);
|
|
1787
1803
|
const recallResult = await this.#recallMemory(prompt, {
|
|
1788
1804
|
sessionId,
|
|
1789
1805
|
vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
|
|
1790
1806
|
? vpPersona.vpId
|
|
1791
1807
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
1808
|
+
extraScopes: topicScopesForMemory,
|
|
1792
1809
|
});
|
|
1793
1810
|
recallEntryCount = recallResult && Array.isArray(recallResult.entries)
|
|
1794
1811
|
? recallResult.entries.length
|
|
@@ -1806,6 +1823,7 @@ export class Engine {
|
|
|
1806
1823
|
? vpPersona.vpId
|
|
1807
1824
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
1808
1825
|
language: this.#config.language || 'en',
|
|
1826
|
+
topicScopes: topicScopesForMemory,
|
|
1809
1827
|
});
|
|
1810
1828
|
|
|
1811
1829
|
// ─── AMS: populate + snapshot ───────────────────────────────
|
|
@@ -1837,9 +1855,13 @@ export class Engine {
|
|
|
1837
1855
|
// frontend state. The full system prompt remains visible in the existing
|
|
1838
1856
|
// debug-only system-prompt panel.
|
|
1839
1857
|
const activeGroupDreamScope = sessionId ? `sessions/${sessionId}` : null;
|
|
1858
|
+
const activeTopicDreamPrefix = sessionId ? `sessions/${sessionId}/topic/` : null;
|
|
1840
1859
|
const dreamResidentLoaded = amsContext && Array.isArray(amsContext.residentEntries)
|
|
1841
1860
|
? amsContext.residentEntries
|
|
1842
|
-
.filter(e => e && e.
|
|
1861
|
+
.filter(e => e && e.summary && (
|
|
1862
|
+
e.scope === activeGroupDreamScope
|
|
1863
|
+
|| (activeTopicDreamPrefix && e.scope?.startsWith(activeTopicDreamPrefix))
|
|
1864
|
+
))
|
|
1843
1865
|
.map(e => ({
|
|
1844
1866
|
scope: e.scope,
|
|
1845
1867
|
summary: String(e.summary).slice(0, 4000),
|
|
@@ -1854,7 +1876,7 @@ export class Engine {
|
|
|
1854
1876
|
// only IDs + tiny labels. (Feature scope retired 2026-05-13.)
|
|
1855
1877
|
const activeSessionTopics = Array.isArray(sessionTopics)
|
|
1856
1878
|
? sessionTopics
|
|
1857
|
-
:
|
|
1879
|
+
: topicScopesForMemory.slice(0, 8).map(scope => scope.replace(/^sessions\/[^/]+\/topic\//, ''));
|
|
1858
1880
|
const activeScope = {
|
|
1859
1881
|
sessionId: sessionId || '',
|
|
1860
1882
|
sessionMember: ownVpIdForAms || '',
|
|
@@ -3738,6 +3760,18 @@ export class Engine {
|
|
|
3738
3760
|
}
|
|
3739
3761
|
}
|
|
3740
3762
|
|
|
3763
|
+
async function readTopicSummary(scope, opts) {
|
|
3764
|
+
const m = /^sessions\/([^/]+)\/topic\/(.+)$/.exec(String(scope || ''));
|
|
3765
|
+
if (!m) return null;
|
|
3766
|
+
const path = m[2].split('/').filter(Boolean);
|
|
3767
|
+
if (path.length === 0) return null;
|
|
3768
|
+
const summary = await readScopeSummary(
|
|
3769
|
+
{ kind: 'session-topic', sessionId: m[1], path },
|
|
3770
|
+
opts,
|
|
3771
|
+
).catch(() => '');
|
|
3772
|
+
return summary ? { scope, summary } : null;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3741
3775
|
async function collectTopicLabels(dir, prefix, labels, limit) {
|
|
3742
3776
|
if (labels.length >= limit) return;
|
|
3743
3777
|
let entries;
|
package/yeaft/memory/store.js
CHANGED
|
@@ -240,7 +240,7 @@ export function isValidTopic(scope) {
|
|
|
240
240
|
*/
|
|
241
241
|
export function isVpForeign(relPath, currentVpId) {
|
|
242
242
|
if (!relPath || !currentVpId) return false;
|
|
243
|
-
const m = /^(?:group|chat|
|
|
243
|
+
const m = /^(?:group|chat|sessions?)\/[^/]+\/vp\/([^/]+)(?:\/|$)/.exec(relPath);
|
|
244
244
|
if (!m) return false;
|
|
245
245
|
return m[1] !== currentVpId;
|
|
246
246
|
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* {profile, entries, formatted} shape the engine already consumes.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { runPreflow as runFtsPreflow } from '../memory/preflow.js';
|
|
25
|
+
import { runPreflow as runFtsPreflow, filterScopes } from '../memory/preflow.js';
|
|
26
|
+
import { approxTokens } from '../memory/budget.js';
|
|
26
27
|
import { resolveFallbackVp, resolveMemberId } from './roster.js';
|
|
27
28
|
|
|
28
29
|
/** Matches `@vp-id` where id is [A-Za-z0-9_-]+. Captures the id. */
|
|
@@ -179,6 +180,8 @@ function scopeHeading(scope) {
|
|
|
179
180
|
if (m) return `## Memory: Session ${m[1]} (user)`;
|
|
180
181
|
m = /^session\/([^/]+)\/feature\/(.+)$/.exec(scope);
|
|
181
182
|
if (m) return `## Memory: Feature ${m[2]}`;
|
|
183
|
+
m = /^sessions\/([^/]+)\/topic\/(.+)$/.exec(scope);
|
|
184
|
+
if (m) return `## Memory: Topic ${m[2]}`;
|
|
182
185
|
m = /^session\/([^/]+)\/topic\/(.+)$/.exec(scope);
|
|
183
186
|
if (m) return `## Memory: Topic ${m[2]}`;
|
|
184
187
|
// Legacy nested group scopes (un-migrated data).
|
|
@@ -239,6 +242,8 @@ export function formatPickedForInjection(picked) {
|
|
|
239
242
|
* @property {string[]} [currentTags] Contextual tags for rerank
|
|
240
243
|
* @property {number} [topK] Max FTS rows fetched (default 50)
|
|
241
244
|
* @property {number} [budgetTokens] Token budget for picked segments
|
|
245
|
+
* @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
|
|
246
|
+
* @property {number} [fallbackPerScope] Max fallback segments per scope
|
|
242
247
|
*/
|
|
243
248
|
|
|
244
249
|
/**
|
|
@@ -282,14 +287,17 @@ export function buildRelevantScopes({ sessionId, chatId, vpId, extra } = {}) {
|
|
|
282
287
|
scopes.push(`group/${sessionId}/vp/${vpId}`);
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
|
-
|
|
286
|
-
for (const s of extra) {
|
|
287
|
-
if (s && !scopes.includes(s)) scopes.push(s);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
+
appendUniqueScopes(scopes, extra);
|
|
290
291
|
return scopes;
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
function appendUniqueScopes(scopes, extra) {
|
|
295
|
+
if (!Array.isArray(extra)) return;
|
|
296
|
+
for (const s of extra) {
|
|
297
|
+
if (s && !scopes.includes(s)) scopes.push(s);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
293
301
|
/**
|
|
294
302
|
* Run memory pre-flow for one VP turn. Thin wrapper around
|
|
295
303
|
* `memory/preflow.js::runPreflow` that:
|
|
@@ -321,7 +329,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
321
329
|
extra: opts.extraScopes,
|
|
322
330
|
});
|
|
323
331
|
|
|
324
|
-
|
|
332
|
+
let result = runFtsPreflow(index, {
|
|
325
333
|
userMsg,
|
|
326
334
|
relevantScopes,
|
|
327
335
|
ownVpId: opts.vpId || null,
|
|
@@ -330,6 +338,25 @@ export function runMemoryPreflow(index, opts) {
|
|
|
330
338
|
budgetTokens: opts.budgetTokens,
|
|
331
339
|
});
|
|
332
340
|
|
|
341
|
+
let fallbackUsed = false;
|
|
342
|
+
if ((result.picked || []).length === 0 && opts.fallbackOnEmpty) {
|
|
343
|
+
const fallback = fallbackScopedSegments(index, {
|
|
344
|
+
relevantScopes,
|
|
345
|
+
ownVpId: opts.vpId || null,
|
|
346
|
+
budgetTokens: opts.budgetTokens,
|
|
347
|
+
perScope: opts.fallbackPerScope,
|
|
348
|
+
});
|
|
349
|
+
if (fallback.length > 0) {
|
|
350
|
+
fallbackUsed = true;
|
|
351
|
+
result = {
|
|
352
|
+
...result,
|
|
353
|
+
picked: fallback,
|
|
354
|
+
pickedTokens: estimatePickedTokens(fallback),
|
|
355
|
+
droppedCount: result.droppedCount || 0,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
333
360
|
// Best-effort profile: pick any user-scope segment body.
|
|
334
361
|
const userSeg = (result.picked || []).find(p => p.scope === 'user');
|
|
335
362
|
const profile = userSeg ? (userSeg.body || '').trim() : '';
|
|
@@ -346,6 +373,61 @@ export function runMemoryPreflow(index, opts) {
|
|
|
346
373
|
pickedTokens: result.pickedTokens,
|
|
347
374
|
droppedCount: result.droppedCount,
|
|
348
375
|
hitCount: (result.hits || []).length,
|
|
376
|
+
fallbackUsed,
|
|
349
377
|
},
|
|
350
378
|
};
|
|
351
379
|
}
|
|
380
|
+
|
|
381
|
+
function fallbackScopedSegments(index, opts) {
|
|
382
|
+
if (!index || typeof index.listByScope !== 'function') return [];
|
|
383
|
+
const scopes = prioritizeFallbackScopes(filterScopes(opts.relevantScopes || [], opts.ownVpId || null));
|
|
384
|
+
const perScope = Number.isFinite(opts.perScope) && opts.perScope > 0 ? Math.floor(opts.perScope) : 2;
|
|
385
|
+
const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0 ? opts.budgetTokens : 1200;
|
|
386
|
+
const buckets = [];
|
|
387
|
+
for (const scope of scopes) {
|
|
388
|
+
let segs = [];
|
|
389
|
+
try { segs = index.listByScope(scope) || []; } catch { continue; }
|
|
390
|
+
segs = [...segs]
|
|
391
|
+
.filter(seg => (seg?.body || '').trim())
|
|
392
|
+
.sort(compareSegmentRecency)
|
|
393
|
+
.slice(0, perScope);
|
|
394
|
+
if (segs.length > 0) buckets.push(segs);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const out = [];
|
|
398
|
+
let cost = 0;
|
|
399
|
+
for (let i = 0; i < perScope; i += 1) {
|
|
400
|
+
for (const bucket of buckets) {
|
|
401
|
+
const seg = bucket[i];
|
|
402
|
+
if (!seg) continue;
|
|
403
|
+
const tk = approxTokens(seg.body || '');
|
|
404
|
+
if (tk <= 0 || cost + tk > budgetTokens) continue;
|
|
405
|
+
out.push(seg);
|
|
406
|
+
cost += tk;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function prioritizeFallbackScopes(scopes) {
|
|
413
|
+
const topic = [];
|
|
414
|
+
const rest = [];
|
|
415
|
+
for (const scope of scopes) {
|
|
416
|
+
if (/^(?:sessions|session|group)\/[^/]+\/topic\//.test(scope)) topic.push(scope);
|
|
417
|
+
else rest.push(scope);
|
|
418
|
+
}
|
|
419
|
+
return [...topic, ...rest];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function compareSegmentRecency(a, b) {
|
|
423
|
+
return timestampOf(b) - timestampOf(a);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function timestampOf(seg) {
|
|
427
|
+
const t = Date.parse(seg?.updatedAt || seg?.createdAt || '');
|
|
428
|
+
return Number.isFinite(t) ? t : 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function estimatePickedTokens(segments) {
|
|
432
|
+
return (segments || []).reduce((sum, seg) => sum + approxTokens(seg?.body || ''), 0);
|
|
433
|
+
}
|