@yeaft/webchat-agent 0.1.792 → 0.1.794
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/unify/vp/seed-defaults.js +28 -0
- package/unify/vp/stock-ids.js +53 -0
- package/unify/vp/vp-bridge.js +10 -2
- package/unify/vp/vp-crud.js +17 -7
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
|
31
31
|
import { join } from 'path';
|
|
32
32
|
import { createVp, VpCrudError } from './vp-crud.js';
|
|
33
33
|
import { DEFAULT_VP_LIB_DIR } from './vp-store.js';
|
|
34
|
+
import { STOCK_VP_IDS } from './stock-ids.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* The 33 default VPs. Each entry is a valid `createVp` payload.
|
|
@@ -855,6 +856,33 @@ Bad for: requests that require pretending to be a licensed professional, bypassi
|
|
|
855
856
|
},
|
|
856
857
|
]);
|
|
857
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Self-check: every seed persona's vpId must appear in STOCK_VP_IDS, and
|
|
861
|
+
* vice versa. The two lists live in separate modules to break a circular
|
|
862
|
+
* import (see stock-ids.js header), so the only thing keeping them in
|
|
863
|
+
* sync is this load-time assertion. If you add a new seed VP and forget
|
|
864
|
+
* to add its id to stock-ids.js#STOCK_VP_ID_LIST (or vice versa), the
|
|
865
|
+
* agent will refuse to start with a clear error.
|
|
866
|
+
*/
|
|
867
|
+
const _seedIds = new Set(DEFAULT_VPS.map(v => v.vpId));
|
|
868
|
+
{
|
|
869
|
+
const missingInStockIds = [];
|
|
870
|
+
for (const id of _seedIds) {
|
|
871
|
+
if (!STOCK_VP_IDS.has(id)) missingInStockIds.push(id);
|
|
872
|
+
}
|
|
873
|
+
const missingInSeeds = [];
|
|
874
|
+
for (const id of STOCK_VP_IDS) {
|
|
875
|
+
if (!_seedIds.has(id)) missingInSeeds.push(id);
|
|
876
|
+
}
|
|
877
|
+
if (missingInStockIds.length || missingInSeeds.length) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
'[seed-defaults] DEFAULT_VPS / STOCK_VP_IDS mismatch — '
|
|
880
|
+
+ `add to stock-ids.js: [${missingInStockIds.join(', ')}]; `
|
|
881
|
+
+ `add to DEFAULT_VPS: [${missingInSeeds.join(', ')}]`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
858
886
|
/**
|
|
859
887
|
* True iff `libDir` exists and contains at least one subdirectory that
|
|
860
888
|
* looks like a VP entry (has a `role.md` file). A stray empty directory
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stock-ids.js — single source of truth for "is this vpId a stock seed VP?".
|
|
3
|
+
*
|
|
4
|
+
* Owns the canonical list of seed vpIds and exposes `STOCK_VP_IDS` (a Set
|
|
5
|
+
* for O(1) lookup) plus `isStockVpId(vpId)`.
|
|
6
|
+
*
|
|
7
|
+
* Why this lives in its own tiny module instead of in seed-defaults.js:
|
|
8
|
+
* `seed-defaults.js` imports `createVp / VpCrudError` from `vp-crud.js`,
|
|
9
|
+
* so if `vp-crud.js` were to import `STOCK_VP_IDS` from `seed-defaults`
|
|
10
|
+
* directly, the module graph would be circular (vp-crud → seed-defaults
|
|
11
|
+
* → vp-crud). The cycle "works" today only because every consumer reads
|
|
12
|
+
* STOCK_VP_IDS inside a function body (live binding), but a future
|
|
13
|
+
* refactor that moves the check to module top level — say, to validate
|
|
14
|
+
* input on import — would crash with `STOCK_VP_IDS is undefined`. Moving
|
|
15
|
+
* the Set into a leaf module with zero inbound deps from elsewhere in
|
|
16
|
+
* the package breaks the cycle for good.
|
|
17
|
+
*
|
|
18
|
+
* Authoritative-vs-derived: this file is the SOURCE OF TRUTH for stock
|
|
19
|
+
* ids. seed-defaults.js asserts at module load that every entry in
|
|
20
|
+
* DEFAULT_VPS appears here, and vice versa — see the self-check at the
|
|
21
|
+
* bottom of seed-defaults.js. So adding a new seed VP only requires
|
|
22
|
+
* adding both the persona object *and* its id here (two-file change,
|
|
23
|
+
* caught by the assertion if you forget either).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const STOCK_VP_ID_LIST = Object.freeze([
|
|
27
|
+
// engineering
|
|
28
|
+
'steve', 'linus', 'martin', 'dieter', 'ada', 'grace', 'alice', 'ken',
|
|
29
|
+
'margaret', 'shannon', 'alan', 'norman',
|
|
30
|
+
// philosophy / psychology
|
|
31
|
+
'kongzi', 'socrates', 'nietzsche', 'kahneman', 'jung',
|
|
32
|
+
// strategy / business
|
|
33
|
+
'sunzi', 'clausewitz', 'simaqian', 'harari',
|
|
34
|
+
'buffett', 'munger', 'dalio', 'bezos', 'drucker',
|
|
35
|
+
// arts / culture
|
|
36
|
+
'luxun', 'sudongpo', 'borges', 'einstein', 'kubrick', 'miyazaki',
|
|
37
|
+
// assistant
|
|
38
|
+
'omni',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** @type {ReadonlySet<string>} */
|
|
42
|
+
export const STOCK_VP_IDS = new Set(STOCK_VP_ID_LIST);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True iff `vpId` is a stock seed VP that ships with the agent.
|
|
46
|
+
* Pure function of the string — undefined / non-string input returns false.
|
|
47
|
+
*
|
|
48
|
+
* @param {unknown} vpId
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function isStockVpId(vpId) {
|
|
52
|
+
return typeof vpId === 'string' && STOCK_VP_IDS.has(vpId);
|
|
53
|
+
}
|
package/unify/vp/vp-bridge.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { defaultRegistry } from './registry.js';
|
|
25
25
|
import { VpLoader } from './vp-loader.js';
|
|
26
|
+
import { STOCK_VP_IDS } from './stock-ids.js';
|
|
26
27
|
|
|
27
28
|
/** Process-singleton VpLoader; lazily started on first subscribe. */
|
|
28
29
|
let _loaderStarted = false;
|
|
@@ -175,8 +176,8 @@ function ensureLoader(registry = defaultRegistry) {
|
|
|
175
176
|
* Serialise a VP (entity layer shape) to the wire-format the web layer
|
|
176
177
|
* expects (spec §2.1). Pure; no IO.
|
|
177
178
|
*
|
|
178
|
-
* @param {{id:string,name:string,role:string,traits?:string[],modelHint?:string,personaHash?:string}} vp
|
|
179
|
-
* @returns {{vpId:string,displayName:string,subtitle:string,role:string,traits:string[],modelHint:?string,personaHash:?string}}
|
|
179
|
+
* @param {{id:string,name:string,role:string,nameZh?:string,aliases?:string[],traits?:string[],modelHint?:string,personaHash?:string}} vp
|
|
180
|
+
* @returns {{vpId:string,displayName:string,displayNameZh:string,aliases:string[],subtitle:string,role:string,traits:string[],modelHint:?string,personaHash:?string,isStock:boolean}}
|
|
180
181
|
*/
|
|
181
182
|
export function serializeVpForWire(vp) {
|
|
182
183
|
return {
|
|
@@ -191,6 +192,13 @@ export function serializeVpForWire(vp) {
|
|
|
191
192
|
traits: Array.isArray(vp.traits) ? vp.traits.slice() : [],
|
|
192
193
|
modelHint: vp.modelHint ?? null,
|
|
193
194
|
personaHash: vp.personaHash ?? null,
|
|
195
|
+
// task-vp-customize: mark seed VPs so the frontend can disable
|
|
196
|
+
// Edit/Delete and surface a "Stock" badge. Pure id check — see
|
|
197
|
+
// stock-ids.js#STOCK_VP_IDS for the contract. `Set.has(undefined)`
|
|
198
|
+
// is fine, but we still coerce to plain boolean so the wire field
|
|
199
|
+
// is strictly `true | false` (never `undefined`) and downstream
|
|
200
|
+
// `!!` reads can collapse cleanly.
|
|
201
|
+
isStock: STOCK_VP_IDS.has(vp.id) === true,
|
|
194
202
|
};
|
|
195
203
|
}
|
|
196
204
|
|
package/unify/vp/vp-crud.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Error codes returned to the caller (wire-visible):
|
|
15
15
|
* 'duplicate' — vpId already exists (on create)
|
|
16
16
|
* 'not_found' — vpId does not exist on disk (on update/delete)
|
|
17
|
+
* 'stock_readonly' — vpId is one of the seed/stock VPs (mutation refused)
|
|
17
18
|
* <reason from validateVpId> — invalid shape
|
|
18
19
|
*/
|
|
19
20
|
|
|
@@ -24,6 +25,7 @@ import { validateVpId } from '../groups/ids.js';
|
|
|
24
25
|
import { DEFAULT_VP_LIB_DIR, parseRoleMd } from './vp-store.js';
|
|
25
26
|
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store-v2.js';
|
|
26
27
|
import { VP_STUB_MARKER } from '../memory/seed-backfill.js';
|
|
28
|
+
import { STOCK_VP_IDS } from './stock-ids.js';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Default memory root used when callers don't pass `options.memoryRoot`.
|
|
@@ -34,13 +36,6 @@ import { VP_STUB_MARKER } from '../memory/seed-backfill.js';
|
|
|
34
36
|
*/
|
|
35
37
|
const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
|
|
36
38
|
|
|
37
|
-
/**
|
|
38
|
-
* Build the seed summary body for a freshly-created VP. Pulled into a
|
|
39
|
-
* helper so tests can pin the exact format.
|
|
40
|
-
*
|
|
41
|
-
* @param {object} payload same shape as createVp
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
39
|
/**
|
|
45
40
|
* Build the seed body for a freshly-created VP's `<root>/vp/<id>/summary.md`.
|
|
46
41
|
*
|
|
@@ -218,6 +213,11 @@ export function updateVp(payload, options = {}) {
|
|
|
218
213
|
const v = validateVpId(vpId);
|
|
219
214
|
if (!v.ok) throw new VpCrudError(v.reason, vpId);
|
|
220
215
|
|
|
216
|
+
// Stock VPs ship with the agent and are immutable from the CRUD path.
|
|
217
|
+
// The UI also disables Edit/Delete on these, but a misbehaving WS client
|
|
218
|
+
// could bypass the UI — refuse here so the file on disk stays canonical.
|
|
219
|
+
if (STOCK_VP_IDS.has(vpId)) throw new VpCrudError('stock_readonly', vpId);
|
|
220
|
+
|
|
221
221
|
const dir = vpDirFor(libDir, vpId);
|
|
222
222
|
if (!existsSync(dir) || !existsSync(vpRolePathFor(libDir, vpId))) {
|
|
223
223
|
throw new VpCrudError('not_found', vpId);
|
|
@@ -249,6 +249,10 @@ export function deleteVp(vpId, options = {}) {
|
|
|
249
249
|
if (!vpId || typeof vpId !== 'string' || vpId.includes('/') || vpId.includes('\\') || vpId === '..' || vpId === '.') {
|
|
250
250
|
throw new VpCrudError('illegal_character', vpId);
|
|
251
251
|
}
|
|
252
|
+
// Stock VPs ship with the agent. Refuse the delete server-side so a
|
|
253
|
+
// misbehaving WS client cannot wipe `~/.yeaft/virtual-persons/steve/`
|
|
254
|
+
// even if the UI's delete button is missing or disabled.
|
|
255
|
+
if (STOCK_VP_IDS.has(vpId)) throw new VpCrudError('stock_readonly', vpId);
|
|
252
256
|
const dir = vpDirFor(libDir, vpId);
|
|
253
257
|
if (!existsSync(dir)) {
|
|
254
258
|
throw new VpCrudError('not_found', vpId);
|
|
@@ -298,5 +302,11 @@ export function readVp(vpId, options = {}) {
|
|
|
298
302
|
modelHint,
|
|
299
303
|
persona: body,
|
|
300
304
|
planInstruction: typeof meta.planInstruction === 'string' ? String(meta.planInstruction) : '',
|
|
305
|
+
// Echo the authoritative stock flag on the read response so the UI's
|
|
306
|
+
// detail view doesn't have to trust the (potentially stale) list
|
|
307
|
+
// snapshot it was launched from. Defence-in-depth: same Set, two
|
|
308
|
+
// call sites; if either disagrees, the agent guard in updateVp /
|
|
309
|
+
// deleteVp is still the final word.
|
|
310
|
+
isStock: STOCK_VP_IDS.has(id) === true,
|
|
301
311
|
};
|
|
302
312
|
}
|