@openviking/dsh-memory-plugin 0.5.30 → 0.5.31
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/shared/profile-inject.mjs +130 -32
package/package.json
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* viking://user/<space>/memories/entities/ (ls with abstracts)
|
|
9
9
|
* GET /api/v1/skills (own + account-shared skills)
|
|
10
10
|
*
|
|
11
|
+
* With an actorPeerId the same three paths under
|
|
12
|
+
* viking://user/<space>/peers/<peer>/memories/ are read too, and each section
|
|
13
|
+
* lists the user root first, then the peer root, under one shared budget.
|
|
14
|
+
*
|
|
11
15
|
* Budget enforced via the CJK-aware estimateTokens() below — codepoint >=
|
|
12
16
|
* 0x3000 counts at 1.5 tokens, else chars/4. The estimator is exported so
|
|
13
17
|
* callers (e.g. session-start.mjs) can log token counts that match what
|
|
@@ -240,6 +244,57 @@ function formatListing(headerUri, entries, budgetTokens, moreHint = MEMORY_MORE_
|
|
|
240
244
|
return { lines, used, dropped: 0 };
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Preferences and entities of each root in order, user root first. A root
|
|
249
|
+
* with any entry takes its fair share of what is left, and within a root the
|
|
250
|
+
* preferences get half and the entities the rest. With one root this is the
|
|
251
|
+
* single-scope split unchanged.
|
|
252
|
+
*/
|
|
253
|
+
function formatListingGroups(scopes, budgetTokens) {
|
|
254
|
+
const populated = scopes.filter((s) => s.prefs.length > 0 || s.ents.length > 0);
|
|
255
|
+
const lines = [];
|
|
256
|
+
let used = 0;
|
|
257
|
+
let droppedPref = 0;
|
|
258
|
+
let droppedEnt = 0;
|
|
259
|
+
for (let i = 0; i < populated.length; i++) {
|
|
260
|
+
const s = populated[i];
|
|
261
|
+
const share = Math.floor(Math.max(0, budgetTokens - used) / (populated.length - i));
|
|
262
|
+
const prefBlock = formatListing(s.prefUri, s.prefs, Math.floor(share / 2));
|
|
263
|
+
const entBlock = formatListing(s.entUri, s.ents, Math.max(0, share - prefBlock.used));
|
|
264
|
+
lines.push(...prefBlock.lines, ...entBlock.lines);
|
|
265
|
+
used += prefBlock.used + entBlock.used;
|
|
266
|
+
droppedPref += prefBlock.dropped;
|
|
267
|
+
droppedEnt += entBlock.dropped;
|
|
268
|
+
}
|
|
269
|
+
return { lines, used, droppedPref, droppedEnt };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Below this a trimmed peer profile is mostly elision markers; point at it instead.
|
|
273
|
+
const PEER_PROFILE_MIN_TOKENS = 200;
|
|
274
|
+
const PEER_PROFILE_POINTER = "Omitted for length; read this uri with the read tool.";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Drop peer-profile lines that already appear in the user profile, so the
|
|
278
|
+
* shared budget is not spent twice on the same fact. Returns null when no
|
|
279
|
+
* non-empty line is left.
|
|
280
|
+
*/
|
|
281
|
+
function dedupeProfileLines(peerProfile, userProfile) {
|
|
282
|
+
const seen = new Set(
|
|
283
|
+
String(userProfile || "").split("\n").map((l) => l.trim()).filter(Boolean),
|
|
284
|
+
);
|
|
285
|
+
const kept = [];
|
|
286
|
+
for (const line of peerProfile.split("\n")) {
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (trimmed && seen.has(trimmed)) continue;
|
|
289
|
+
// Removed lines must not leave runs of blank lines behind.
|
|
290
|
+
if (!trimmed && (kept.length === 0 || !kept[kept.length - 1].trim())) continue;
|
|
291
|
+
kept.push(line);
|
|
292
|
+
}
|
|
293
|
+
// Headings alone carry nothing worth a second <user-profile> element.
|
|
294
|
+
const hasContent = kept.some((l) => l.trim() && !/^#{1,6}\s/.test(l.trim()));
|
|
295
|
+
return hasContent ? kept.join("\n").trim() : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
243
298
|
const AGENT_SKILLS_ROOT = "viking://agent/skills";
|
|
244
299
|
// The server caps each skill root at node_limit, so the two roots together
|
|
245
300
|
// can return twice this many; the token budget below decides what fits.
|
|
@@ -349,24 +404,26 @@ function formatSkillCatalog(groups, budgetTokens) {
|
|
|
349
404
|
/**
|
|
350
405
|
* Build the profile injection block.
|
|
351
406
|
*
|
|
352
|
-
* Returns null when
|
|
353
|
-
*
|
|
407
|
+
* Returns null when no profile, no listing, and no skill catalog has any
|
|
408
|
+
* content. The returned `block` is just the inner
|
|
354
409
|
* <user-profile>/<available-memories>/<available-skills> payload — the caller
|
|
355
410
|
* wraps it in <openviking-context source="...">.
|
|
356
411
|
*
|
|
357
412
|
* @param {Function} fetchJSON ov-session.mjs:makeFetchJSON closure
|
|
358
|
-
* @param {number} totalBudgetTokens budget for the
|
|
359
|
-
* @param {string} [actorPeerId]
|
|
413
|
+
* @param {number} totalBudgetTokens budget for the profiles and memory listings
|
|
414
|
+
* @param {string} [actorPeerId] adds the peer root alongside the user root
|
|
360
415
|
* @param {{ skillCatalog?: boolean, skillCatalogTokenBudget?: number, sessionStartMaxBytes?: number }} [options]
|
|
361
416
|
* callers pass their resolved plugin config: its skillCatalog knob adds
|
|
362
417
|
* <available-skills>, budgeted separately by skillCatalogTokenBudget.
|
|
363
418
|
* Without it the block is unchanged.
|
|
364
419
|
* @returns {Promise<null | {
|
|
365
420
|
* block: string, chars: number, tokens: number, profileUri: string,
|
|
366
|
-
* profileChars: number,
|
|
421
|
+
* profileChars: number, peerProfileUri: string, peerProfileChars: number,
|
|
422
|
+
* prefCount: number, entCount: number,
|
|
367
423
|
* droppedPref: number, droppedEnt: number,
|
|
368
424
|
* skillCount: number, droppedSkill: number, skillTokens: number,
|
|
369
425
|
* }>}
|
|
426
|
+
* Counts are summed across both roots; profileChars covers both raw profiles.
|
|
370
427
|
*/
|
|
371
428
|
export async function buildProfileBlock(fetchJSON, totalBudgetTokens, actorPeerId = "", options = {}) {
|
|
372
429
|
const { skillCatalog = false, skillCatalogTokenBudget = 0, sessionStartMaxBytes = 0 } = options;
|
|
@@ -378,42 +435,79 @@ export async function buildProfileBlock(fetchJSON, totalBudgetTokens, actorPeerI
|
|
|
378
435
|
: Infinity;
|
|
379
436
|
const skillBudget = Math.min(skillCatalogTokenBudget, Math.floor(capTokens / 4));
|
|
380
437
|
const space = await resolveUserSpace(fetchJSON, actorPeerId);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
438
|
+
|
|
439
|
+
// Profile, preferences and entities carry no message range, so extraction
|
|
440
|
+
// writes the user's own facts to the user root even inside a peer session:
|
|
441
|
+
// with an actor peer both roots hold memories, and both are read.
|
|
442
|
+
const roots = [`viking://user/${space}/memories`];
|
|
443
|
+
if (actorPeerId) roots.push(`viking://user/${space}/peers/${actorPeerId}/memories`);
|
|
444
|
+
const scopes = roots.map((root) => ({
|
|
445
|
+
profileUri: `${root}/profile.md`,
|
|
446
|
+
prefUri: `${root}/preferences`,
|
|
447
|
+
entUri: `${root}/entities`,
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
// Every read is independent, and a hook on the SessionStart path pays for
|
|
451
|
+
// every round trip.
|
|
452
|
+
const reads = await Promise.all([
|
|
453
|
+
...scopes.flatMap((s) => [
|
|
454
|
+
readProfile(fetchJSON, s.profileUri, actorPeerId),
|
|
455
|
+
lsDir(fetchJSON, s.prefUri, actorPeerId),
|
|
456
|
+
lsDir(fetchJSON, s.entUri, actorPeerId),
|
|
457
|
+
]),
|
|
389
458
|
skillCatalog && skillBudget > 0
|
|
390
459
|
? fetchSkillCatalog(fetchJSON, actorPeerId)
|
|
391
460
|
: [],
|
|
392
461
|
]);
|
|
462
|
+
const skillGroups = reads.pop();
|
|
463
|
+
scopes.forEach((s, i) => {
|
|
464
|
+
[s.profile, s.prefs, s.ents] = reads.slice(i * 3, i * 3 + 3);
|
|
465
|
+
});
|
|
466
|
+
const [user, peer] = scopes;
|
|
393
467
|
const skills = formatSkillCatalog(skillGroups, skillBudget);
|
|
394
468
|
|
|
395
|
-
|
|
469
|
+
const peerProfile = peer?.profile ? dedupeProfileLines(peer.profile, user.profile) : null;
|
|
470
|
+
if (
|
|
471
|
+
!user.profile && !peerProfile
|
|
472
|
+
&& scopes.every((s) => s.prefs.length === 0 && s.ents.length === 0)
|
|
473
|
+
&& skills.lines.length === 0
|
|
474
|
+
) {
|
|
396
475
|
return null;
|
|
397
476
|
}
|
|
398
477
|
|
|
399
478
|
const memoryBudget = Math.min(totalBudgetTokens, Math.max(0, capTokens - skills.used));
|
|
400
|
-
//
|
|
479
|
+
// The profiles get up to half the total budget; listings split the rest.
|
|
401
480
|
// Sub-cap protects against a runaway profile blowing the listing budget.
|
|
481
|
+
// The user profile comes first; the peer profile only gets what it leaves.
|
|
402
482
|
const profileBudget = Math.floor(memoryBudget / 2);
|
|
403
|
-
const profileTrunc = profile ? elideProfile(profile, profileBudget) : null;
|
|
483
|
+
const profileTrunc = user.profile ? elideProfile(user.profile, profileBudget) : null;
|
|
404
484
|
const profileTokens = estimateTokens(profileTrunc || "");
|
|
405
|
-
|
|
406
|
-
const listingBudget = Math.max(0, memoryBudget - profileTokens);
|
|
407
|
-
const halfListing = Math.floor(listingBudget / 2);
|
|
408
|
-
const prefBlock = formatListing(prefUri, prefs, halfListing);
|
|
409
|
-
const entBudget = Math.max(0, listingBudget - prefBlock.used);
|
|
410
|
-
const entBlock = formatListing(entUri, ents, entBudget);
|
|
411
|
-
|
|
412
485
|
const profileLines = profileTrunc
|
|
413
|
-
? [`<user-profile uri="${profileUri}">`, profileTrunc, `</user-profile>`]
|
|
486
|
+
? [`<user-profile uri="${user.profileUri}">`, profileTrunc, `</user-profile>`]
|
|
414
487
|
: [];
|
|
415
|
-
let
|
|
416
|
-
|
|
488
|
+
let peerProfileTokens = 0;
|
|
489
|
+
if (peerProfile) {
|
|
490
|
+
const peerBudget = profileBudget - profileTokens;
|
|
491
|
+
// elideProfile keeps at least 400 chars (~600 tokens of CJK), so a trim
|
|
492
|
+
// that still overruns the peer's share falls back to the pointer.
|
|
493
|
+
const peerTrunc = estimateTokens(peerProfile) <= peerBudget || peerBudget >= PEER_PROFILE_MIN_TOKENS
|
|
494
|
+
? elideProfile(peerProfile, peerBudget)
|
|
495
|
+
: null;
|
|
496
|
+
if (peerTrunc !== null && estimateTokens(peerTrunc) <= peerBudget) {
|
|
497
|
+
profileLines.push(`<user-profile uri="${peer.profileUri}">`, peerTrunc, `</user-profile>`);
|
|
498
|
+
peerProfileTokens = estimateTokens(peerTrunc);
|
|
499
|
+
} else {
|
|
500
|
+
const pointer = `<user-profile uri="${peer.profileUri}">${PEER_PROFILE_POINTER}</user-profile>`;
|
|
501
|
+
profileLines.push(pointer);
|
|
502
|
+
peerProfileTokens = estimateTokens(pointer);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const listingBudget = Math.max(0, memoryBudget - profileTokens - peerProfileTokens);
|
|
507
|
+
const listings = formatListingGroups(scopes, listingBudget);
|
|
508
|
+
|
|
509
|
+
let memoryLines = listings.lines.length > 0
|
|
510
|
+
? [`<available-memories>`, ...listings.lines, `</available-memories>`]
|
|
417
511
|
: [];
|
|
418
512
|
let skillLines = skills.lines;
|
|
419
513
|
const render = () => [...profileLines, ...memoryLines, ...skillLines].join("\n");
|
|
@@ -430,16 +524,20 @@ export async function buildProfileBlock(fetchJSON, totalBudgetTokens, actorPeerI
|
|
|
430
524
|
}
|
|
431
525
|
if (!block) return null;
|
|
432
526
|
|
|
527
|
+
const prefCount = scopes.reduce((n, s) => n + s.prefs.length, 0);
|
|
528
|
+
const entCount = scopes.reduce((n, s) => n + s.ents.length, 0);
|
|
433
529
|
return {
|
|
434
530
|
block,
|
|
435
531
|
chars: block.length,
|
|
436
532
|
tokens: estimateTokens(block),
|
|
437
|
-
profileUri,
|
|
438
|
-
profileChars: profile?.length ?? 0,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
533
|
+
profileUri: user.profileUri,
|
|
534
|
+
profileChars: (user.profile?.length ?? 0) + (peer?.profile?.length ?? 0),
|
|
535
|
+
peerProfileUri: peer?.profileUri ?? "",
|
|
536
|
+
peerProfileChars: peer?.profile?.length ?? 0,
|
|
537
|
+
prefCount,
|
|
538
|
+
entCount,
|
|
539
|
+
droppedPref: memoryLines.length ? listings.droppedPref : prefCount,
|
|
540
|
+
droppedEnt: memoryLines.length ? listings.droppedEnt : entCount,
|
|
443
541
|
skillCount: skills.count,
|
|
444
542
|
droppedSkill: skillLines.length ? skills.dropped : skills.count,
|
|
445
543
|
skillTokens: skillLines.length ? skills.used : 0,
|