@yeaft/webchat-agent 0.1.586 → 0.1.587

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.586",
3
+ "version": "0.1.587",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -297,7 +297,7 @@ export class Engine {
297
297
  * @param {string} [userProfile] — user profile from user-memory shard store
298
298
  * @returns {string}
299
299
  */
300
- #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile) {
300
+ #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona) {
301
301
  // Get relevant skill content if SkillManager is wired
302
302
  let skillContent = '';
303
303
  if (this.#skillManager && prompt) {
@@ -317,6 +317,7 @@ export class Engine {
317
317
  compactSummary,
318
318
  skillContent,
319
319
  userProfile,
320
+ vpPersona,
320
321
  // task-334f: memory_trace tool is now registered (49 → 51 tools), so
321
322
  // unlock the core_memory meta-line behind 334e's feature flag.
322
323
  memoryTraceAvailable: true,
@@ -329,7 +330,7 @@ export class Engine {
329
330
  * @param {AbortSignal} [signal]
330
331
  * @returns {object}
331
332
  */
332
- #buildToolContext(signal) {
333
+ #buildToolContext(signal, vpCtx) {
333
334
  return {
334
335
  signal,
335
336
  yeaftDir: this.#yeaftDir,
@@ -348,6 +349,13 @@ export class Engine {
348
349
  imageAllowlist: Array.isArray(this.#config?.unify?.imageAllowlist)
349
350
  ? this.#config.unify.imageAllowlist
350
351
  : [],
352
+ // Bug 4 fix — VP / routing context for RouteForward (and any other
353
+ // VP-aware tool). Undefined when running in non-group / no-VP flows.
354
+ router: vpCtx?.router,
355
+ senderVpId: vpCtx?.senderVpId,
356
+ inboundEnvelope: vpCtx?.inboundEnvelope,
357
+ taskId: vpCtx?.taskId,
358
+ taskMembers: vpCtx?.taskMembers,
351
359
  };
352
360
  }
353
361
 
@@ -510,7 +518,7 @@ export class Engine {
510
518
  * SCENARIO_EFFORT. Unknown values fall through to 'high'.
511
519
  * @yields {EngineEvent}
512
520
  */
513
- async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat' }) {
521
+ async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers } = {}) {
514
522
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
515
523
  yield {
516
524
  type: 'error',
@@ -561,7 +569,7 @@ export class Engine {
561
569
  const runSignal = abortCtrl.signal;
562
570
 
563
571
  try {
564
- yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario });
572
+ yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers });
565
573
  } finally {
566
574
  if (signal) {
567
575
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -579,7 +587,7 @@ export class Engine {
579
587
  * in a try/finally without indenting the whole loop.
580
588
  * @private
581
589
  */
582
- async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat' }) {
590
+ async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers }) {
583
591
 
584
592
  // ─── Pre-query: Memory Injection (task-287) + Compact Summary ──
585
593
  // Two-layer recall:
@@ -618,7 +626,7 @@ export class Engine {
618
626
 
619
627
  const compactSummary = this.#getCompactSummary();
620
628
  const userProfile = recallResult?.profile || '';
621
- const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile);
629
+ const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona);
622
630
 
623
631
  // Build conversation: existing messages + new user message
624
632
  const conversationMessages = [
@@ -883,7 +891,7 @@ export class Engine {
883
891
  }
884
892
 
885
893
  // Execute tool calls and feed results back
886
- const toolCtx = this.#buildToolContext(signal);
894
+ const toolCtx = this.#buildToolContext(signal, { router, senderVpId, inboundEnvelope, taskId, taskMembers });
887
895
 
888
896
  // task-325a: track whether we aborted mid tool-loop so we can
889
897
  // break out of the outer while-loop cleanly once the current
@@ -164,6 +164,7 @@ export class Dispatcher {
164
164
  transientMeta.set(entry, {
165
165
  messageId: opts.messageId || undefined,
166
166
  override: opts.override || undefined,
167
+ queryOpts: opts.queryOpts || undefined,
167
168
  });
168
169
  const snapshot = this.#queueSnapshot();
169
170
  return { entry, snapshot };
@@ -299,7 +300,8 @@ export class Dispatcher {
299
300
  }
300
301
 
301
302
  try {
302
- for await (const event of instance.query({ prompt: claimed.text, signal })) {
303
+ const queryOpts = (transientMeta.get(claimed) || {}).queryOpts || {};
304
+ for await (const event of instance.query({ prompt: claimed.text, signal, ...queryOpts })) {
303
305
  yield { type: 'engine_event', threadId: targetThreadId, event };
304
306
  }
305
307
  inputQueue.markRouted(claimed.id, targetThreadId);
package/unify/prompts.js CHANGED
@@ -192,6 +192,9 @@ const PROMPTS = {
192
192
  userProfileHeader: '## user_profile',
193
193
  coreMemoryHeader: '## core_memory',
194
194
  coreMemoryMeta: 'To open the original message behind any entry above, call `memory_trace`.',
195
+ vpPersonaHeader: '## active_persona',
196
+ vpPersonaIntro: (name, role) =>
197
+ `For this turn you are speaking as **${name}**${role ? ` (${role})` : ''}. Stay in character; the persona below overrides the generic Yeaft identity for tone, expertise, and decision style.`,
195
198
  },
196
199
  zh: {
197
200
  identity: '你是 Yeaft,一个有用的 AI 助手。',
@@ -210,6 +213,9 @@ const PROMPTS = {
210
213
  userProfileHeader: '## user_profile',
211
214
  coreMemoryHeader: '## core_memory',
212
215
  coreMemoryMeta: '如需原始 message,调 `memory_trace`。',
216
+ vpPersonaHeader: '## active_persona',
217
+ vpPersonaIntro: (name, role) =>
218
+ `本轮你以 **${name}**${role ? `(${role})` : ''} 的身份说话。请保持人设:以下 persona 在语气、专业方向与判断风格上覆盖默认的 Yeaft 身份。`,
213
219
  },
214
220
  };
215
221
 
@@ -289,6 +295,7 @@ export function buildSystemPrompt({
289
295
  userProfile,
290
296
  coreMemory,
291
297
  memoryTraceAvailable = false,
298
+ vpPersona,
292
299
  } = {}) {
293
300
  // Fallback to English for unknown languages
294
301
  const lang = PROMPTS[language] || PROMPTS.en;
@@ -308,6 +315,14 @@ export function buildSystemPrompt({
308
315
  // ─── 2. Date Metadata ──────────────────────────────────
309
316
  parts.push(lang.date(new Date().toISOString().split('T')[0]));
310
317
 
318
+ // ─── 2.5 VP Persona Override (Bug 3 fix) ───────────────
319
+ // When the caller (web-bridge / dispatcher) addressed a specific VP via
320
+ // @-mention, inject that VP's persona body so the LLM stops speaking as
321
+ // generic Yeaft and adopts the VP's voice. Placed AFTER base identity so
322
+ // the persona section's directive ("override generic Yeaft") wins.
323
+ const vpBlock = renderVpPersona(vpPersona, lang);
324
+ if (vpBlock) parts.push(vpBlock);
325
+
311
326
  // ─── 3. Mode-Specific Instructions ─────────────────────
312
327
  // task-297: single unified mode for all normal operation.
313
328
  // `dream` is retained for background memory maintenance.
@@ -382,6 +397,36 @@ export function buildSystemPrompt({
382
397
 
383
398
  // ─── task-334e helpers ───────────────────────────────────────────
384
399
 
400
+ /**
401
+ * Render the `## active_persona` block when the engine is running on
402
+ * behalf of an addressed VP. Accepts `{ displayName, role?, persona }` —
403
+ * `persona` is the body text from the VP's role.md (loaded by the engine
404
+ * via readVp). When `persona` is empty we still emit the intro line so
405
+ * the LLM at least knows whose voice to adopt; if even displayName is
406
+ * missing we omit the whole block (no useful signal).
407
+ *
408
+ * @param {object} vpPersona
409
+ * @param {string} vpPersona.displayName
410
+ * @param {string} [vpPersona.role]
411
+ * @param {string} [vpPersona.persona]
412
+ * @param {object} lang
413
+ * @returns {string}
414
+ */
415
+ function renderVpPersona(vpPersona, lang) {
416
+ if (!vpPersona || typeof vpPersona !== 'object') return '';
417
+ const name = typeof vpPersona.displayName === 'string'
418
+ ? vpPersona.displayName.trim() : '';
419
+ if (!name) return '';
420
+ const role = typeof vpPersona.role === 'string' ? vpPersona.role.trim() : '';
421
+ const body = typeof vpPersona.persona === 'string' ? vpPersona.persona.trim() : '';
422
+ const lines = [
423
+ lang.vpPersonaHeader,
424
+ lang.vpPersonaIntro(name, role),
425
+ ];
426
+ if (body) lines.push('', body);
427
+ return lines.join('\n');
428
+ }
429
+
385
430
  const DEFAULT_TASK_MEMORY_TOP = 5;
386
431
  const DEFAULT_RELATED_TASK_TOP = 3;
387
432
  const DEFAULT_RELATED_TASK_MEMORY_TOP = 2;
@@ -97,7 +97,7 @@ export class EngineInstance {
97
97
  * @param {AbortSignal} [params.signal]
98
98
  * @yields {object} EngineEvent with { ...event, threadId }
99
99
  */
100
- async *query({ prompt, mode, signal }) {
100
+ async *query({ prompt, mode, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers } = {}) {
101
101
  if (this.#terminated) {
102
102
  yield {
103
103
  type: 'error',
@@ -173,7 +173,7 @@ export class EngineInstance {
173
173
  curToolResults = [];
174
174
  }
175
175
 
176
- for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal })) {
176
+ for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers })) {
177
177
  // Re-tag every event with the bound threadId. Non-object events
178
178
  // (shouldn't happen — all engine events are objects) are passed
179
179
  // through untouched.
@@ -28,6 +28,7 @@ import ctx from '../context.js';
28
28
  import { getThreadStore, MAIN_THREAD_ID } from './threads/store.js';
29
29
  import { handleVpSubscribe } from './vp/vp-bridge.js';
30
30
  import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
31
+ import { createRouter } from './routing/router.js';
31
32
  import { handleUnifyTaskMessage as _handleUnifyTaskMessage } from './task-message.js';
32
33
  import {
33
34
  handleUnifyUserMemoryWrite as _handleUnifyUserMemoryWrite,
@@ -1165,6 +1166,10 @@ export async function handleUnifyGroupChat(msg) {
1165
1166
  groupId,
1166
1167
  vpId,
1167
1168
  speakerVpId: vpId,
1169
+ // Bug 4: hand the coordinator down so handleUnifyChat can build a
1170
+ // Router for the RouteForward tool. Field is namespaced with `_`
1171
+ // to mark it as an internal hop, never sent over WS.
1172
+ _groupCoordinator: coord,
1168
1173
  });
1169
1174
  } catch (err) {
1170
1175
  console.warn('[Unify] unify_group_chat: per-vp dispatch failed', vpId, err?.message || err);
@@ -1181,6 +1186,42 @@ export async function handleUnifyGroupChat(msg) {
1181
1186
  }
1182
1187
  }
1183
1188
 
1189
+ /**
1190
+ * Build the per-query VP context for the Engine.
1191
+ *
1192
+ * - Loads the addressed VP's persona via readVp() so the system prompt
1193
+ * speaks in that VP's voice (Bug 3 fix).
1194
+ * - When a GroupCoordinator handle is supplied, wraps it in a Router so
1195
+ * the RouteForward tool actually forwards instead of returning
1196
+ * `router_unavailable` (Bug 4 fix).
1197
+ *
1198
+ * Returns `undefined` when we have nothing to inject — keeps the legacy
1199
+ * single-agent path identical to before.
1200
+ */
1201
+ function buildVpQueryOpts({ vpId, groupCoordinator }) {
1202
+ if (!vpId) return undefined;
1203
+ const out = { senderVpId: vpId };
1204
+ try {
1205
+ const vp = readVp(vpId);
1206
+ if (vp) {
1207
+ out.vpPersona = {
1208
+ displayName: vp.displayName || vpId,
1209
+ role: vp.role || '',
1210
+ persona: vp.persona || '',
1211
+ };
1212
+ }
1213
+ } catch { /* persona load is best-effort */ }
1214
+ if (groupCoordinator && typeof groupCoordinator.ingest === 'function') {
1215
+ try {
1216
+ out.router = createRouter({ coordinator: groupCoordinator });
1217
+ } catch {
1218
+ // Router build failure is non-fatal — RouteForward will report
1219
+ // router_unavailable and the VP can pivot.
1220
+ }
1221
+ }
1222
+ return out;
1223
+ }
1224
+
1184
1225
  /**
1185
1226
  * Handle a unify_chat message from the web UI.
1186
1227
  *
@@ -1191,6 +1232,12 @@ export async function handleUnifyGroupChat(msg) {
1191
1232
  export async function handleUnifyChat(msg) {
1192
1233
  const { prompt, mode } = msg;
1193
1234
  if (!prompt?.trim()) return;
1235
+ // Bug 3 / Bug 4 — when the upstream group dispatcher addresses a specific
1236
+ // VP, it stamps `vpId` (and optionally a coordinator handle in
1237
+ // `_groupCoordinator`). Hoist them now so they survive the lazy-init
1238
+ // branch and reach the dispatcher.submit() queryOpts.
1239
+ const vpId = typeof msg.vpId === 'string' && msg.vpId.trim() ? msg.vpId.trim() : null;
1240
+ const groupCoordinator = msg._groupCoordinator || null;
1194
1241
 
1195
1242
  // Deprecation warning — task-297 removed chat/work mode distinction
1196
1243
  if (mode !== undefined && mode !== null) {
@@ -1302,6 +1349,7 @@ export async function handleUnifyChat(msg) {
1302
1349
  const { entry } = session.dispatcher.submit(cleanedPrompt, {
1303
1350
  messageId: msg.messageId,
1304
1351
  override: override || undefined,
1352
+ queryOpts: buildVpQueryOpts({ vpId, groupCoordinator }),
1305
1353
  });
1306
1354
  sendUnifyEvent({
1307
1355
  type: 'input_queue_updated',