@yeaft/webchat-agent 0.1.528 → 0.1.530
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/prompts.js +279 -0
package/package.json
CHANGED
package/unify/prompts.js
CHANGED
|
@@ -18,12 +18,37 @@
|
|
|
18
18
|
* - Legacy `memory={profile,entries}` param still supported for callers
|
|
19
19
|
* (tests, CLI) that have not migrated.
|
|
20
20
|
*
|
|
21
|
+
* task-334e additions (R6 §Δ24.5 / §Δ27.3 / §Δ31.4 / §Δ29.3):
|
|
22
|
+
* - `taskCtx` param → renders a `## task_ctx` block with:
|
|
23
|
+
* * task-memory top-5 bodies with semantic shard prefix `[shard]`
|
|
24
|
+
* (no sourceRef — memory_trace opens the trail on demand)
|
|
25
|
+
* * `### related tasks` sub-section — `relatedTaskIds` top-3 (sorted
|
|
26
|
+
* by updatedAt desc) + per-task top-2 memory; ACL-gated by
|
|
27
|
+
* `target.members` ∋ currentVpId (§Δ31.4)
|
|
28
|
+
* * `### summary reminder` soft nudge — condition (§Δ27.3):
|
|
29
|
+
* non-summary msgs ≥ 3 AND since-lastSummary > 15min AND
|
|
30
|
+
* currentVpId == task.initiatorVpId → emits a DYNAMIC hint
|
|
31
|
+
* - `userProfile` param → renders a `## user_profile` block. Stub
|
|
32
|
+
* implementation: when not passed explicitly, we fall back to reading
|
|
33
|
+
* `~/.yeaft/user/profile.json` ({ "content": "…string…" }) if present
|
|
34
|
+
* (§Δ29.3 placeholder until 334l wires real user-memory recall).
|
|
35
|
+
* - `coreMemory` param → renders a `## core_memory` block with recall
|
|
36
|
+
* top-7 memory bodies (no sourceRef) and a trailing meta line pointing
|
|
37
|
+
* at `memory_trace` as the way to open the original message.
|
|
38
|
+
*
|
|
39
|
+
* Hard constraints (task-334e contract):
|
|
40
|
+
* - Does NOT modify engine.js turn loop.
|
|
41
|
+
* - Does NOT implement memory_trace / open_source_message (task-334f).
|
|
42
|
+
* - Does NOT implement task_summary_post (task-334n).
|
|
43
|
+
* - Changes limited to prompts.js + templates/.
|
|
44
|
+
*
|
|
21
45
|
* Reference: yeaft-unify-system-prompt-budget.md — Static + Dynamic + Context layers
|
|
22
46
|
*/
|
|
23
47
|
|
|
24
48
|
import { readFileSync, existsSync } from 'fs';
|
|
25
49
|
import { join, dirname } from 'path';
|
|
26
50
|
import { fileURLToPath } from 'url';
|
|
51
|
+
import { homedir } from 'os';
|
|
27
52
|
|
|
28
53
|
// ─── Template Loading (one-time at startup) ──────────────────────
|
|
29
54
|
|
|
@@ -159,6 +184,14 @@ const PROMPTS = {
|
|
|
159
184
|
profileHeader: '### User Profile',
|
|
160
185
|
recalledHeader: '### Recalled Memories',
|
|
161
186
|
compactHeader: '## Conversation History Summary',
|
|
187
|
+
// task-334e — new section headers
|
|
188
|
+
taskCtxHeader: '## task_ctx',
|
|
189
|
+
taskCtxRelatedHeader: '### related tasks',
|
|
190
|
+
taskCtxSummaryReminder: (min, count) =>
|
|
191
|
+
`💡 ${min}min since last summary (+${count} new messages). Consider calling \`task_summary_post\`.`,
|
|
192
|
+
userProfileHeader: '## user_profile',
|
|
193
|
+
coreMemoryHeader: '## core_memory',
|
|
194
|
+
coreMemoryMeta: 'To open the original message behind any entry above, call `memory_trace`.',
|
|
162
195
|
},
|
|
163
196
|
zh: {
|
|
164
197
|
identity: '你是 Yeaft,一个有用的 AI 助手。',
|
|
@@ -169,6 +202,14 @@ const PROMPTS = {
|
|
|
169
202
|
profileHeader: '### 用户画像',
|
|
170
203
|
recalledHeader: '### 相关记忆',
|
|
171
204
|
compactHeader: '## 对话历史摘要',
|
|
205
|
+
// task-334e — new section headers
|
|
206
|
+
taskCtxHeader: '## task_ctx',
|
|
207
|
+
taskCtxRelatedHeader: '### 相关任务',
|
|
208
|
+
taskCtxSummaryReminder: (min, count) =>
|
|
209
|
+
`💡 距上次 summary 已过 ${min}min,新增 ${count} 条消息,建议调用 \`task_summary_post\`。`,
|
|
210
|
+
userProfileHeader: '## user_profile',
|
|
211
|
+
coreMemoryHeader: '## core_memory',
|
|
212
|
+
coreMemoryMeta: '如需原始 message,调 `memory_trace`。',
|
|
172
213
|
},
|
|
173
214
|
};
|
|
174
215
|
|
|
@@ -191,6 +232,35 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
|
191
232
|
* 5. Skills section
|
|
192
233
|
* 6. Memory section
|
|
193
234
|
* 7. Compact summary section
|
|
235
|
+
* 8. Task context section (task-334e §Δ24.5 + §Δ27.3 + §Δ31.4)
|
|
236
|
+
* 9. User profile section (task-334e §Δ29.3 stub)
|
|
237
|
+
* 10. Core memory section (task-334e §Δ24.5)
|
|
238
|
+
*
|
|
239
|
+
* task-334e params:
|
|
240
|
+
* @param {object} [taskCtx] — per-task context
|
|
241
|
+
* @param {string} [taskCtx.taskId]
|
|
242
|
+
* @param {string} [taskCtx.currentVpId] — used for ACL + initiator check
|
|
243
|
+
* @param {string} [taskCtx.initiatorVpId] — task initiator VP id
|
|
244
|
+
* @param {Array<{body:string, shard?:string}>} [taskCtx.memories] — task-memory top-5
|
|
245
|
+
* @param {Array<{id:string, title?:string, members?:string[], updatedAt?:number,
|
|
246
|
+
* memories?:Array<{body:string, shard?:string}>}>} [taskCtx.relatedTasks]
|
|
247
|
+
* — related tasks; we take top-3 by updatedAt desc, top-2 mem each,
|
|
248
|
+
* ACL-gated (members must include currentVpId)
|
|
249
|
+
* @param {object} [taskCtx.summaryReminder]
|
|
250
|
+
* @param {number} [taskCtx.summaryReminder.nonSummaryCount] — msgs since last summary
|
|
251
|
+
* @param {number} [taskCtx.summaryReminder.lastSummaryAt] — epoch ms (0/missing = never)
|
|
252
|
+
* @param {number} [taskCtx.summaryReminder.now] — override clock (tests), default Date.now()
|
|
253
|
+
*
|
|
254
|
+
* @param {string} [userProfile] — explicit profile content (334l path);
|
|
255
|
+
* when omitted we read `~/.yeaft/user/profile.json` `{ content }` as stub.
|
|
256
|
+
* @param {{ entries?: Array<{body:string, shard?:string}>, max?: number }} [coreMemory]
|
|
257
|
+
* — recalled memory entries; we render top-7 bodies + meta line.
|
|
258
|
+
*
|
|
259
|
+
* @param {boolean} [memoryTraceAvailable=false] — feature flag gating the
|
|
260
|
+
* "call memory_trace" meta line in the core_memory block. Defaults to
|
|
261
|
+
* false so we don't point VPs at an unimplemented tool (prev-3 Nit-2 /
|
|
262
|
+
* PM-approved Option A). 334f will flip this to `true` from session.js
|
|
263
|
+
* once `memory_trace` ships; this slice stays decoupled from session.js.
|
|
194
264
|
*
|
|
195
265
|
* @param {{
|
|
196
266
|
* language?: string,
|
|
@@ -200,6 +270,10 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
|
200
270
|
* memoryInjection?: string,
|
|
201
271
|
* compactSummary?: string,
|
|
202
272
|
* skillContent?: string,
|
|
273
|
+
* taskCtx?: object,
|
|
274
|
+
* userProfile?: string,
|
|
275
|
+
* coreMemory?: object,
|
|
276
|
+
* memoryTraceAvailable?: boolean,
|
|
203
277
|
* }} params
|
|
204
278
|
* @returns {string}
|
|
205
279
|
*/
|
|
@@ -211,6 +285,10 @@ export function buildSystemPrompt({
|
|
|
211
285
|
memoryInjection,
|
|
212
286
|
compactSummary,
|
|
213
287
|
skillContent,
|
|
288
|
+
taskCtx,
|
|
289
|
+
userProfile,
|
|
290
|
+
coreMemory,
|
|
291
|
+
memoryTraceAvailable = false,
|
|
214
292
|
} = {}) {
|
|
215
293
|
// Fallback to English for unknown languages
|
|
216
294
|
const lang = PROMPTS[language] || PROMPTS.en;
|
|
@@ -287,5 +365,206 @@ export function buildSystemPrompt({
|
|
|
287
365
|
parts.push(`${lang.compactHeader}\n${compactSummary}`);
|
|
288
366
|
}
|
|
289
367
|
|
|
368
|
+
// ─── 8. Task Context Section (task-334e §Δ24.5 / §Δ27.3 / §Δ31.4) ─
|
|
369
|
+
const taskCtxBlock = renderTaskCtx(taskCtx, lang);
|
|
370
|
+
if (taskCtxBlock) parts.push(taskCtxBlock);
|
|
371
|
+
|
|
372
|
+
// ─── 9. User Profile Section (task-334e §Δ29.3 stub) ───
|
|
373
|
+
const profileBlock = renderUserProfile(userProfile, lang);
|
|
374
|
+
if (profileBlock) parts.push(profileBlock);
|
|
375
|
+
|
|
376
|
+
// ─── 10. Core Memory Section (task-334e §Δ24.5) ────────
|
|
377
|
+
const coreMemBlock = renderCoreMemory(coreMemory, lang, memoryTraceAvailable);
|
|
378
|
+
if (coreMemBlock) parts.push(coreMemBlock);
|
|
379
|
+
|
|
290
380
|
return parts.join('\n\n');
|
|
291
381
|
}
|
|
382
|
+
|
|
383
|
+
// ─── task-334e helpers ───────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
const DEFAULT_TASK_MEMORY_TOP = 5;
|
|
386
|
+
const DEFAULT_RELATED_TASK_TOP = 3;
|
|
387
|
+
const DEFAULT_RELATED_TASK_MEMORY_TOP = 2;
|
|
388
|
+
const DEFAULT_CORE_MEMORY_TOP = 7;
|
|
389
|
+
const SUMMARY_REMINDER_MIN_MESSAGES = 3;
|
|
390
|
+
const SUMMARY_REMINDER_MIN_AGE_MS = 15 * 60 * 1000; // 15 minutes
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Render `## task_ctx` block. Never throws on malformed input — missing
|
|
394
|
+
* fields degrade to omission. The block is only emitted when at least one
|
|
395
|
+
* of { memories, relatedTasks (post-ACL), summaryReminder } has content.
|
|
396
|
+
*/
|
|
397
|
+
function renderTaskCtx(taskCtx, lang) {
|
|
398
|
+
if (!taskCtx || typeof taskCtx !== 'object') return '';
|
|
399
|
+
|
|
400
|
+
const memLines = renderTaskMemories(taskCtx.memories);
|
|
401
|
+
const relatedLines = renderRelatedTasks(
|
|
402
|
+
taskCtx.relatedTasks,
|
|
403
|
+
taskCtx.currentVpId,
|
|
404
|
+
lang,
|
|
405
|
+
);
|
|
406
|
+
const reminderLine = renderSummaryReminder(taskCtx, lang);
|
|
407
|
+
|
|
408
|
+
if (!memLines && !relatedLines && !reminderLine) return '';
|
|
409
|
+
|
|
410
|
+
const out = [lang.taskCtxHeader];
|
|
411
|
+
if (taskCtx.taskId) out.push(`taskId: ${taskCtx.taskId}`);
|
|
412
|
+
if (memLines) out.push(memLines);
|
|
413
|
+
if (relatedLines) out.push(relatedLines);
|
|
414
|
+
if (reminderLine) out.push(reminderLine);
|
|
415
|
+
return out.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Render task-memory top-N bodies with `[shard]` prefix, no sourceRef. */
|
|
419
|
+
function renderTaskMemories(memories) {
|
|
420
|
+
if (!Array.isArray(memories) || memories.length === 0) return '';
|
|
421
|
+
const lines = [];
|
|
422
|
+
for (const m of memories.slice(0, DEFAULT_TASK_MEMORY_TOP)) {
|
|
423
|
+
const body = typeof m?.body === 'string' ? m.body.trim() : '';
|
|
424
|
+
if (!body) continue;
|
|
425
|
+
const shard = typeof m?.shard === 'string' && m.shard.trim() ? m.shard.trim() : 'general';
|
|
426
|
+
lines.push(`- [${shard}] ${body}`);
|
|
427
|
+
}
|
|
428
|
+
return lines.join('\n');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Render `### related tasks` sub-block. §Δ31.4 ACL: a related task is only
|
|
433
|
+
* included if `task.members` contains `currentVpId`. Missing `members` is
|
|
434
|
+
* treated as private (excluded) — fail-closed.
|
|
435
|
+
*
|
|
436
|
+
* Ordering: by `updatedAt` desc (undefined treated as 0). Top-3 tasks, top-2
|
|
437
|
+
* memory each.
|
|
438
|
+
*/
|
|
439
|
+
function renderRelatedTasks(relatedTasks, currentVpId, lang) {
|
|
440
|
+
if (!Array.isArray(relatedTasks) || relatedTasks.length === 0) return '';
|
|
441
|
+
if (!currentVpId) return ''; // no ACL subject → fail-closed
|
|
442
|
+
|
|
443
|
+
const allowed = relatedTasks.filter((t) => {
|
|
444
|
+
if (!t || typeof t !== 'object') return false;
|
|
445
|
+
const members = Array.isArray(t.members) ? t.members : null;
|
|
446
|
+
if (!members) return false; // fail-closed on missing ACL
|
|
447
|
+
return members.includes(currentVpId);
|
|
448
|
+
});
|
|
449
|
+
if (allowed.length === 0) return '';
|
|
450
|
+
|
|
451
|
+
// Sort by updatedAt desc; undefined coerces to 0 (i.e. pushed to the end).
|
|
452
|
+
const sorted = allowed
|
|
453
|
+
.slice()
|
|
454
|
+
.sort((a, b) => (Number(b.updatedAt) || 0) - (Number(a.updatedAt) || 0));
|
|
455
|
+
|
|
456
|
+
const out = [lang.taskCtxRelatedHeader];
|
|
457
|
+
for (const t of sorted.slice(0, DEFAULT_RELATED_TASK_TOP)) {
|
|
458
|
+
const title = typeof t.title === 'string' && t.title.trim() ? t.title.trim() : t.id;
|
|
459
|
+
out.push(`- **${t.id}** · ${title}`);
|
|
460
|
+
const mems = Array.isArray(t.memories) ? t.memories : [];
|
|
461
|
+
for (const m of mems.slice(0, DEFAULT_RELATED_TASK_MEMORY_TOP)) {
|
|
462
|
+
const body = typeof m?.body === 'string' ? m.body.trim() : '';
|
|
463
|
+
if (!body) continue;
|
|
464
|
+
const shard = typeof m?.shard === 'string' && m.shard.trim() ? m.shard.trim() : 'general';
|
|
465
|
+
out.push(` - [${shard}] ${body}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// If every allowed task had zero usable memory, we still keep the header +
|
|
469
|
+
// task list — the related-task identifiers themselves are useful context.
|
|
470
|
+
return out.join('\n');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Render the summary-reminder line (§Δ27.3).
|
|
475
|
+
*
|
|
476
|
+
* Conditions (ALL must hold):
|
|
477
|
+
* (a) currentVpId === task.initiatorVpId
|
|
478
|
+
* (b) summaryReminder.nonSummaryCount ≥ 3
|
|
479
|
+
* (c) (now - lastSummaryAt) > 15 minutes
|
|
480
|
+
* (lastSummaryAt == 0 / missing is treated as "never summarized":
|
|
481
|
+
* only triggers if nonSummaryCount ≥ 3)
|
|
482
|
+
*/
|
|
483
|
+
function renderSummaryReminder(taskCtx, lang) {
|
|
484
|
+
const r = taskCtx && taskCtx.summaryReminder;
|
|
485
|
+
if (!r || typeof r !== 'object') return '';
|
|
486
|
+
if (!taskCtx.currentVpId || !taskCtx.initiatorVpId) return '';
|
|
487
|
+
if (taskCtx.currentVpId !== taskCtx.initiatorVpId) return '';
|
|
488
|
+
|
|
489
|
+
const count = Number(r.nonSummaryCount) || 0;
|
|
490
|
+
if (count < SUMMARY_REMINDER_MIN_MESSAGES) return '';
|
|
491
|
+
|
|
492
|
+
const now = Number(r.now) || Date.now();
|
|
493
|
+
const lastAt = Number(r.lastSummaryAt) || 0;
|
|
494
|
+
const ageMs = lastAt > 0 ? now - lastAt : Number.POSITIVE_INFINITY;
|
|
495
|
+
if (lastAt > 0 && ageMs <= SUMMARY_REMINDER_MIN_AGE_MS) return '';
|
|
496
|
+
|
|
497
|
+
// For "never summarized" (lastAt==0) we report age as nonSummaryCount's
|
|
498
|
+
// session-coarse proxy: we print a dash so the prompt does not lie about
|
|
499
|
+
// an exact minute count. The hint still carries the count of new msgs.
|
|
500
|
+
const minStr = lastAt > 0 ? String(Math.round(ageMs / 60000)) : '—';
|
|
501
|
+
return lang.taskCtxSummaryReminder(minStr, count);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Render `## user_profile` block. If the caller passed an explicit string,
|
|
506
|
+
* we use it verbatim (that's the 334l path). Otherwise we stub-read from
|
|
507
|
+
* `~/.yeaft/user/profile.json` (`{ content: "..." }`) per §Δ29.3. Any IO
|
|
508
|
+
* error is swallowed — this is best-effort context, not critical path.
|
|
509
|
+
*/
|
|
510
|
+
function renderUserProfile(userProfile, lang) {
|
|
511
|
+
let content = '';
|
|
512
|
+
if (typeof userProfile === 'string' && userProfile.trim()) {
|
|
513
|
+
content = userProfile.trim();
|
|
514
|
+
} else if (userProfile == null) {
|
|
515
|
+
content = readUserProfileStub();
|
|
516
|
+
}
|
|
517
|
+
if (!content) return '';
|
|
518
|
+
return `${lang.userProfileHeader}\n${content}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function readUserProfileStub() {
|
|
522
|
+
try {
|
|
523
|
+
const path = join(homedir(), '.yeaft', 'user', 'profile.json');
|
|
524
|
+
if (!existsSync(path)) return '';
|
|
525
|
+
const raw = readFileSync(path, 'utf8');
|
|
526
|
+
const parsed = JSON.parse(raw);
|
|
527
|
+
if (parsed && typeof parsed.content === 'string') return parsed.content.trim();
|
|
528
|
+
return '';
|
|
529
|
+
} catch {
|
|
530
|
+
// File missing, unreadable, malformed JSON, or non-string content.
|
|
531
|
+
// Stub is best-effort — fall through silently.
|
|
532
|
+
return '';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Render `## core_memory` block with recall top-7 bodies + (optional) meta line.
|
|
538
|
+
*
|
|
539
|
+
* Accepts `{ entries: [{body,shard}], max?: number }`. Never renders
|
|
540
|
+
* `sourceRef`. The "call memory_trace" meta line is gated by
|
|
541
|
+
* `memoryTraceAvailable` (prev-3 Nit-2 / PM-approved Option A): when the
|
|
542
|
+
* `memory_trace` tool is not yet implemented (334f), we omit the meta line
|
|
543
|
+
* entirely so the LLM does not try to call a non-existent tool. 334f will
|
|
544
|
+
* flip the flag to `true` when it wires session.js.
|
|
545
|
+
*/
|
|
546
|
+
function renderCoreMemory(coreMemory, lang, memoryTraceAvailable) {
|
|
547
|
+
if (!coreMemory || typeof coreMemory !== 'object') return '';
|
|
548
|
+
const entries = Array.isArray(coreMemory.entries) ? coreMemory.entries : [];
|
|
549
|
+
if (entries.length === 0) return '';
|
|
550
|
+
const max = Number.isFinite(coreMemory.max) && coreMemory.max > 0
|
|
551
|
+
? Math.floor(coreMemory.max)
|
|
552
|
+
: DEFAULT_CORE_MEMORY_TOP;
|
|
553
|
+
|
|
554
|
+
const lines = [lang.coreMemoryHeader];
|
|
555
|
+
let shown = 0;
|
|
556
|
+
for (const e of entries) {
|
|
557
|
+
if (shown >= max) break;
|
|
558
|
+
const body = typeof e?.body === 'string' ? e.body.trim() : '';
|
|
559
|
+
if (!body) continue;
|
|
560
|
+
const shard = typeof e?.shard === 'string' && e.shard.trim() ? e.shard.trim() : 'general';
|
|
561
|
+
lines.push(`- [${shard}] ${body}`);
|
|
562
|
+
shown += 1;
|
|
563
|
+
}
|
|
564
|
+
if (shown === 0) return '';
|
|
565
|
+
if (memoryTraceAvailable) {
|
|
566
|
+
lines.push('');
|
|
567
|
+
lines.push(lang.coreMemoryMeta);
|
|
568
|
+
}
|
|
569
|
+
return lines.join('\n');
|
|
570
|
+
}
|