@yeaft/webchat-agent 0.1.763 → 0.1.766

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/unify/prompts.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * ③ Memory — single block produced upstream by the AMS render
15
15
  * outlet and threaded through here as `memoryInjection`
16
16
  * ④ Active Scope — structured per-turn scope summary
17
- * (feature / group / vp / envelope IDs)
17
+ * (group / vp / envelope IDs)
18
18
  *
19
19
  * The compact summary, user_profile, and core_memory blocks that used to
20
20
  * live inside the system prompt are GONE. Compact summary is now part of
@@ -149,6 +149,10 @@ const RAW_TEMPLATES = {
149
149
  harnessRouterShape: readTemplate('harness/router-shape.md', { required: false }),
150
150
  // Phase 3b — coordinator harness rule for inter-VP forwarding.
151
151
  harnessRouterHandoff: readTemplate('harness/router-handoff.md', { required: false }),
152
+ // task — StartPlan tool fallback. The `StartPlan` tool reads this when
153
+ // a VP has no `planInstruction` of its own. Required so a misconfigured
154
+ // install fails fast instead of injecting an empty plan instruction.
155
+ planInstruction: readTemplate('plan-instruction.md'),
152
156
  };
153
157
 
154
158
  /**
@@ -163,6 +167,23 @@ function getTemplate(key, language) {
163
167
  return extractLangSection(raw, language);
164
168
  }
165
169
 
170
+ /**
171
+ * Default planning-instruction text returned by the `StartPlan` tool when
172
+ * the active VP has no `planInstruction` override on its role.md frontmatter.
173
+ *
174
+ * Pulled from `templates/plan-instruction.md`. Marked required at load time
175
+ * so a missing template fails fast on agent boot — preferable to silently
176
+ * shipping an empty plan instruction to the LLM.
177
+ *
178
+ * @param {string} [language='en'] — 'en' / 'zh' (uses lang-section markers
179
+ * if the template carries them; falls
180
+ * back to the whole body otherwise).
181
+ * @returns {string}
182
+ */
183
+ export function getDefaultPlanInstruction(language = 'en') {
184
+ return getTemplate('planInstruction', language);
185
+ }
186
+
166
187
  // ─── Prompt Templates (hardcoded fallbacks) ──────────────────────
167
188
 
168
189
  const PROMPTS = {
@@ -171,11 +192,6 @@ const PROMPTS = {
171
192
  date: (d) => `Date: ${d}`,
172
193
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
173
194
  tools: (names) => `Available tools: ${names}`,
174
- // task-334e — task-context section header (sub-block of Active Scope)
175
- taskCtxHeader: '## task_ctx',
176
- taskCtxRelatedHeader: '### related tasks',
177
- taskCtxSummaryReminder: (min, count) =>
178
- `💡 ${min}min since last summary (+${count} new messages). Consider calling \`task_summary_post\`.`,
179
195
  // DESIGN-PROMPT §3 ④ — Active Scope header
180
196
  activeScopeHeader: '## active_scope',
181
197
  groupAnnouncementHeader: '[Group Announcement]',
@@ -187,11 +203,6 @@ const PROMPTS = {
187
203
  date: (d) => `日期:${d}`,
188
204
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
189
205
  tools: (names) => `可用工具:${names}`,
190
- // task-334e — task-context section header (sub-block of Active Scope)
191
- taskCtxHeader: '## task_ctx',
192
- taskCtxRelatedHeader: '### 相关任务',
193
- taskCtxSummaryReminder: (min, count) =>
194
- `💡 距上次 summary 已过 ${min}min,新增 ${count} 条消息,建议调用 \`task_summary_post\`。`,
195
206
  // DESIGN-PROMPT §3 ④ — Active Scope header
196
207
  activeScopeHeader: '## active_scope',
197
208
  groupAnnouncementHeader: '[群组公告]',
@@ -239,30 +250,14 @@ export function normalizePromptLanguage(language) {
239
250
  * ③ Memory — Single block produced by the AMS render outlet
240
251
  * (callers pass it as `memoryInjection`).
241
252
  * ④ Active Scope — Structured per-turn scope summary
242
- * (feature / group / vp / envelope IDs).
243
- * (Task context lives inside Active Scope; the previous standalone
244
- * user_profile / core_memory blocks are gone those signals now
245
- * arrive through AMS Resident.)
246
- *
247
- * task-334e taskCtx is preserved as a sub-block of Active Scope:
248
- * @param {object} [taskCtx] — per-task context
249
- * @param {string} [taskCtx.taskId]
250
- * @param {string} [taskCtx.currentVpId] — used for ACL + initiator check
251
- * @param {string} [taskCtx.initiatorVpId] — task initiator VP id
252
- * @param {Array<{body:string, shard?:string}>} [taskCtx.memories] — task-memory top-5
253
- * @param {Array<{id:string, title?:string, members?:string[], updatedAt?:number,
254
- * memories?:Array<{body:string, shard?:string}>}>} [taskCtx.relatedTasks]
255
- * — related tasks; we take top-3 by updatedAt desc, top-2 mem each,
256
- * ACL-gated (members must include currentVpId)
257
- * @param {object} [taskCtx.summaryReminder]
258
- * @param {number} [taskCtx.summaryReminder.nonSummaryCount] — msgs since last summary
259
- * @param {number} [taskCtx.summaryReminder.lastSummaryAt] — epoch ms (0/missing = never)
260
- * @param {number} [taskCtx.summaryReminder.now] — override clock (tests), default Date.now()
253
+ * (group / vp / envelope IDs).
254
+ * (The previous standalone user_profile / core_memory blocks are
255
+ * gone those signals now arrive through AMS Resident. Task
256
+ * context (`taskCtx`) was wired into Active Scope by task-334e
257
+ * but never actually populated by the engine; removed 2026-05-13.)
261
258
  *
262
259
  * Active Scope params (DESIGN-PROMPT §3 ④):
263
260
  * @param {object} [activeScope] — structured scope summary for this turn
264
- * @param {string|null} [activeScope.featureId] currently active feature, or null
265
- * @param {string} [activeScope.featureTitle] short title for human display
266
261
  * @param {string} [activeScope.groupId]
267
262
  * @param {string} [activeScope.vpId]
268
263
  * @param {object} [activeScope.envelope] inbound routing info (sender, intent)
@@ -273,7 +268,6 @@ export function normalizePromptLanguage(language) {
273
268
  * toolNames?: string[],
274
269
  * memoryInjection?: string,
275
270
  * skillContent?: string,
276
- * taskCtx?: object,
277
271
  * activeScope?: object,
278
272
  * vpPersona?: object,
279
273
  * groupAnnouncement?: string,
@@ -286,7 +280,6 @@ export function buildSystemPrompt({
286
280
  toolNames = [],
287
281
  memoryInjection,
288
282
  skillContent,
289
- taskCtx,
290
283
  activeScope,
291
284
  vpPersona,
292
285
  groupAnnouncement = '',
@@ -370,20 +363,16 @@ export function buildSystemPrompt({
370
363
  }
371
364
 
372
365
  // ─── 7. Active Scope (DESIGN-PROMPT §3 ④) ──────────────
373
- // Structured per-turn scope summary. taskCtx is rendered as a
374
- // sub-block of Active Scope (when supplied), and the new
375
- // feature/group/vp/envelope identifiers are rendered as a leading
376
- // line.
366
+ // Structured per-turn scope summary. The group/vp/envelope identifiers
367
+ // are rendered as a leading line. (Per-task taskCtx sub-block was
368
+ // never wired and is removed 2026-05-13.)
377
369
  const activeScopeBlock = renderActiveScope(activeScope, lang);
378
370
  if (activeScopeBlock) parts.push(activeScopeBlock);
379
371
 
380
- const taskCtxBlock = renderTaskCtx(taskCtx, lang);
381
- if (taskCtxBlock) parts.push(taskCtxBlock);
382
-
383
372
  return parts.join('\n\n');
384
373
  }
385
374
 
386
- // ─── task-334e helpers ───────────────────────────────────────────
375
+ // ─── helpers ─────────────────────────────────────────────────────
387
376
 
388
377
  /**
389
378
  * Render the `## active_persona` block when the engine is running on
@@ -455,147 +444,6 @@ function hasCjk(text) {
455
444
  return /[\u3400-\u9fff\uf900-\ufaff]/u.test(String(text || ''));
456
445
  }
457
446
 
458
- const DEFAULT_TASK_MEMORY_TOP = 5;
459
- const DEFAULT_RELATED_TASK_TOP = 3;
460
- const DEFAULT_RELATED_TASK_MEMORY_TOP = 2;
461
- // task-334n §Δ31.4 — tightened reminder gate:
462
- // (a) currentVpId === initiatorVpId
463
- // (b) task.members.length >= 2 (multi-VP only)
464
- // (c) nonSummaryCount >= 10 OR (now - lastSummaryAt) >= 20 min
465
- // 334e's earlier looser gate (3 msgs / 15 min) is preserved as a legacy
466
- // fallback path for callers that never set `summaryReminder.members`.
467
- const SUMMARY_REMINDER_MIN_MESSAGES = 3;
468
- const SUMMARY_REMINDER_MIN_AGE_MS = 15 * 60 * 1000; // 15 minutes (legacy)
469
- const SUMMARY_REMINDER_MIN_TURNS_334N = 10;
470
- const SUMMARY_REMINDER_MIN_AGE_MS_334N = 20 * 60 * 1000; // 20 minutes
471
- const SUMMARY_REMINDER_MIN_MEMBERS_334N = 2;
472
-
473
- /**
474
- * Render `## task_ctx` block. Never throws on malformed input — missing
475
- * fields degrade to omission. The block is only emitted when at least one
476
- * of { memories, relatedTasks (post-ACL), summaryReminder } has content.
477
- */
478
- function renderTaskCtx(taskCtx, lang) {
479
- if (!taskCtx || typeof taskCtx !== 'object') return '';
480
-
481
- const memLines = renderTaskMemories(taskCtx.memories);
482
- const relatedLines = renderRelatedTasks(
483
- taskCtx.relatedTasks,
484
- taskCtx.currentVpId,
485
- lang,
486
- taskCtx.groupId,
487
- );
488
- const reminderLine = renderSummaryReminder(taskCtx, lang);
489
-
490
- if (!memLines && !relatedLines && !reminderLine) return '';
491
-
492
- const out = [lang.taskCtxHeader];
493
- if (taskCtx.taskId) out.push(`taskId: ${taskCtx.taskId}`);
494
- if (memLines) out.push(memLines);
495
- if (relatedLines) out.push(relatedLines);
496
- if (reminderLine) out.push(reminderLine);
497
- return out.join('\n');
498
- }
499
-
500
- /** Render task-memory top-N bodies with `[shard]` prefix, no sourceRef. */
501
- function renderTaskMemories(memories) {
502
- if (!Array.isArray(memories) || memories.length === 0) return '';
503
- const lines = [];
504
- for (const m of memories.slice(0, DEFAULT_TASK_MEMORY_TOP)) {
505
- const body = typeof m?.body === 'string' ? m.body.trim() : '';
506
- if (!body) continue;
507
- const shard = typeof m?.shard === 'string' && m.shard.trim() ? m.shard.trim() : 'general';
508
- lines.push(`- [${shard}] ${body}`);
509
- }
510
- return lines.join('\n');
511
- }
512
-
513
- /**
514
- * Render `### related tasks` sub-block. §Δ31.4 ACL: a related task is only
515
- * included if `task.members` contains `currentVpId`. Missing `members` is
516
- * treated as private (excluded) — fail-closed.
517
- *
518
- * Ordering: by `updatedAt` desc (undefined treated as 0). Top-3 tasks, top-2
519
- * memory each.
520
- */
521
- function renderRelatedTasks(relatedTasks, currentVpId, lang, currentTaskGroupId) {
522
- if (!Array.isArray(relatedTasks) || relatedTasks.length === 0) return '';
523
- if (!currentVpId) return ''; // no ACL subject → fail-closed
524
-
525
- const allowed = relatedTasks.filter((t) => {
526
- if (!t || typeof t !== 'object') return false;
527
- const members = Array.isArray(t.members) ? t.members : null;
528
- // task-334n §Δ27.3 — either same-group OR members-intersection grants.
529
- if (currentTaskGroupId && t.groupId && t.groupId === currentTaskGroupId) {
530
- return true;
531
- }
532
- if (!members) return false; // fail-closed on missing ACL
533
- return members.includes(currentVpId);
534
- });
535
- if (allowed.length === 0) return '';
536
-
537
- // Sort by updatedAt desc; undefined coerces to 0 (i.e. pushed to the end).
538
- const sorted = allowed
539
- .slice()
540
- .sort((a, b) => (Number(b.updatedAt) || 0) - (Number(a.updatedAt) || 0));
541
-
542
- const out = [lang.taskCtxRelatedHeader];
543
- for (const t of sorted.slice(0, DEFAULT_RELATED_TASK_TOP)) {
544
- const title = typeof t.title === 'string' && t.title.trim() ? t.title.trim() : t.id;
545
- out.push(`- **${t.id}** · ${title}`);
546
- const mems = Array.isArray(t.memories) ? t.memories : [];
547
- for (const m of mems.slice(0, DEFAULT_RELATED_TASK_MEMORY_TOP)) {
548
- const body = typeof m?.body === 'string' ? m.body.trim() : '';
549
- if (!body) continue;
550
- const shard = typeof m?.shard === 'string' && m.shard.trim() ? m.shard.trim() : 'general';
551
- out.push(` - [${shard}] ${body}`);
552
- }
553
- }
554
- // If every allowed task had zero usable memory, we still keep the header +
555
- // task list — the related-task identifiers themselves are useful context.
556
- return out.join('\n');
557
- }
558
-
559
- /**
560
- * Render the summary-reminder line (§Δ27.3).
561
- *
562
- * Conditions (ALL must hold):
563
- * (a) currentVpId === task.initiatorVpId
564
- * (b) summaryReminder.nonSummaryCount ≥ 3
565
- * (c) (now - lastSummaryAt) > 15 minutes
566
- * (lastSummaryAt == 0 / missing is treated as "never summarized":
567
- * only triggers if nonSummaryCount ≥ 3)
568
- */
569
- function renderSummaryReminder(taskCtx, lang) {
570
- const r = taskCtx && taskCtx.summaryReminder;
571
- if (!r || typeof r !== 'object') return '';
572
- if (!taskCtx.currentVpId || !taskCtx.initiatorVpId) return '';
573
- if (taskCtx.currentVpId !== taskCtx.initiatorVpId) return '';
574
-
575
- const count = Number(r.nonSummaryCount) || 0;
576
- const now = Number(r.now) || Date.now();
577
- const lastAt = Number(r.lastSummaryAt) || 0;
578
- const ageMs = lastAt > 0 ? now - lastAt : Number.POSITIVE_INFINITY;
579
-
580
- // task-334n §Δ31.4 gate: when `members` is supplied, apply the strict
581
- // multi-VP / 20min-or-10turn rule. Otherwise keep the legacy 334e gate
582
- // so pre-334n callers still see reminders under the old thresholds.
583
- const members = Array.isArray(r.members) ? r.members : null;
584
- if (members) {
585
- if (members.length < SUMMARY_REMINDER_MIN_MEMBERS_334N) return '';
586
- const ageOk = lastAt > 0 && ageMs >= SUMMARY_REMINDER_MIN_AGE_MS_334N;
587
- const turnsOk = count >= SUMMARY_REMINDER_MIN_TURNS_334N;
588
- // `never summarised` (lastAt=0) only counts when turnsOk, otherwise we
589
- // silently wait — aligns with §Δ31.4 "too-soon" reason code.
590
- if (!ageOk && !turnsOk) return '';
591
- } else {
592
- if (count < SUMMARY_REMINDER_MIN_MESSAGES) return '';
593
- if (lastAt > 0 && ageMs <= SUMMARY_REMINDER_MIN_AGE_MS) return '';
594
- }
595
-
596
- const minStr = lastAt > 0 ? String(Math.round(ageMs / 60000)) : '—';
597
- return lang.taskCtxSummaryReminder(minStr, count);
598
- }
599
447
 
600
448
  /**
601
449
  * Render `## active_scope` block (DESIGN-PROMPT §3 ④).
@@ -607,18 +455,15 @@ function renderSummaryReminder(taskCtx, lang) {
607
455
  *
608
456
  * Schema:
609
457
  * ## active_scope
610
- * feature: <featureId> "<title>" (omitted when null/empty)
611
458
  * group: <groupId> (omitted when missing)
612
459
  * vp: <vpId> (omitted when missing)
613
460
  * envelope: from=<sender> intent=<intent> (omitted when no envelope)
614
461
  *
615
462
  * Returns '' when the input has no useful field — we don't emit an empty
616
- * header. featureId is allowed to be `null` (DESIGN-PROMPT §5.1 — T4
617
- * Scope Tagging is a placeholder; not every turn lives in a feature).
463
+ * header. (`featureId`/`featureTitle` fields were removed 2026-05-13 along
464
+ * with the rest of the Feature system; the JSDoc once described them.)
618
465
  *
619
466
  * @param {object} [activeScope]
620
- * @param {string|null} [activeScope.featureId]
621
- * @param {string} [activeScope.featureTitle]
622
467
  * @param {string} [activeScope.groupId]
623
468
  * @param {string} [activeScope.vpId]
624
469
  * @param {object} [activeScope.envelope] inbound routing summary
@@ -629,18 +474,6 @@ function renderActiveScope(activeScope, lang) {
629
474
  if (!activeScope || typeof activeScope !== 'object') return '';
630
475
 
631
476
  const lines = [];
632
- const feature = typeof activeScope.featureId === 'string' && activeScope.featureId.trim()
633
- ? activeScope.featureId.trim()
634
- : null;
635
- if (feature) {
636
- // Escape embedded `"` in featureTitle so a title like `Onboard "v2"` does
637
- // not produce a malformed `feature: f1 "Onboard "v2""` line. Titles come
638
- // from user / agent input — assume nothing.
639
- const title = typeof activeScope.featureTitle === 'string' && activeScope.featureTitle.trim()
640
- ? ` "${activeScope.featureTitle.trim().replace(/"/g, '\\"')}"`
641
- : '';
642
- lines.push(`feature: ${feature}${title}`);
643
- }
644
477
  const group = typeof activeScope.groupId === 'string' && activeScope.groupId.trim()
645
478
  ? activeScope.groupId.trim()
646
479
  : '';
@@ -755,8 +588,8 @@ export function renderLayerASummaries(summaries, language = 'en') {
755
588
  * Earlier task-322 / task-334e variants accepted `taskScope` and
756
589
  * `turnScope` pass-through strings so callers could append their own
757
590
  * scope blocks. DESIGN-PROMPT v1 retired that surface — Active Scope is
758
- * now structured (`activeScope: { featureId, groupId, vpId, envelope }`)
759
- * and rendered by `buildSystemPrompt` itself. Both pass-through params
591
+ * now structured (`activeScope: { groupId, vpId, envelope }`) and
592
+ * rendered by `buildSystemPrompt` itself. Both pass-through params
760
593
  * had zero remaining callers when v1 landed; removing them prevents the
761
594
  * "two ways to describe scope" drift §1 set out to eliminate.
762
595
  *
package/unify/session.js CHANGED
@@ -21,9 +21,9 @@ import { ConversationStore } from './conversation/persist.js';
21
21
  import { SkillManager, createSkillManager } from './skills.js';
22
22
  import { MCPManager } from './mcp.js';
23
23
  import { createFullRegistry } from './tools/index.js';
24
- import { initFeatureStore } from './tools/feature-tools.js';
25
24
  import { Engine } from './engine.js';
26
25
  import { Compactor } from './compact/compactor.js';
26
+ import { ToolUsageStats } from './stats/tool-usage.js';
27
27
  // H2.f.5: threads/, pipeline/dispatcher and input-queue retired. The
28
28
  // session now exposes a single Engine.
29
29
  //
@@ -221,8 +221,7 @@ export async function loadSession(options = {}) {
221
221
  }
222
222
  }
223
223
 
224
- // ─── 5a. Initialize feature store ──────────────────────
225
- initFeatureStore(yeaftDir, { readOnly: config._readOnly || false });
224
+ // ─── 5a. (removed 2026-05-13) Feature store init — Feature system retired.
226
225
 
227
226
  // ─── 5b. (H2.f.5) thread store retired. Single conversation. ───
228
227
 
@@ -318,6 +317,13 @@ export async function loadSession(options = {}) {
318
317
  }
319
318
 
320
319
  // ─── 9. Create engine (wires everything) ───────────────
320
+ // Tool-call usage statistics: persisted to <yeaftDir>/stats/tool-usage.json.
321
+ // Loaded synchronously at boot so the first turn already sees prior counts.
322
+ // Threaded into the engine so it can `record` each tool_exec event.
323
+ const toolStats = new ToolUsageStats({
324
+ path: join(yeaftDir, 'stats', 'tool-usage.json'),
325
+ });
326
+ toolStats.loadSync();
321
327
  const engine = new Engine({
322
328
  adapter,
323
329
  trace,
@@ -329,6 +335,7 @@ export async function loadSession(options = {}) {
329
335
  skillManager,
330
336
  mcpManager,
331
337
  yeaftDir,
338
+ toolStats,
332
339
  });
333
340
 
334
341
  // ─── 9a-pre. Create per-group history Compactor ────────
@@ -435,6 +442,13 @@ export async function loadSession(options = {}) {
435
442
  } catch {
436
443
  // Best-effort cleanup
437
444
  }
445
+ try {
446
+ if (toolStats && typeof toolStats.flush === 'function') {
447
+ await toolStats.flush();
448
+ }
449
+ } catch {
450
+ // Best-effort cleanup
451
+ }
438
452
  }
439
453
 
440
454
  return {
@@ -451,6 +465,7 @@ export async function loadSession(options = {}) {
451
465
  yeaftDir,
452
466
  status,
453
467
  amsRegistry,
468
+ toolStats,
454
469
  shutdown,
455
470
  // task-325c: user-initiated abort API. Delegates to web-bridge which
456
471
  // owns the single AbortController. Lazy-imported to avoid a hard cycle
@@ -0,0 +1,31 @@
1
+ /**
2
+ * format.js — human-readable formatters for tool-usage stats.
3
+ *
4
+ * Single source of truth shared by the agent-side CLI (`bin/yeaft-stats.js`)
5
+ * and the REPL `/stats` command. The frontend `UnifyToolStatsDrawer.js`
6
+ * copies the same logic verbatim because the no-build-step web layer can't
7
+ * import from `agent/`; keep the two definitions byte-identical when
8
+ * tweaking either side.
9
+ */
10
+
11
+ export function formatMs(ms) {
12
+ if (!Number.isFinite(ms)) return '-';
13
+ if (ms < 1000) return `${ms}ms`;
14
+ return `${(ms / 1000).toFixed(2)}s`;
15
+ }
16
+
17
+ export function formatPct(rate) {
18
+ if (!Number.isFinite(rate) || rate === 0) return '0%';
19
+ return `${(rate * 100).toFixed(1)}%`;
20
+ }
21
+
22
+ export function formatLastCalled(iso, now = Date.now()) {
23
+ if (typeof iso !== 'string' || !iso) return 'never';
24
+ const t = Date.parse(iso);
25
+ if (Number.isNaN(t)) return iso;
26
+ const ageMs = now - t;
27
+ if (ageMs < 60_000) return 'just now';
28
+ if (ageMs < 3_600_000) return `${Math.floor(ageMs / 60_000)}m ago`;
29
+ if (ageMs < 86_400_000) return `${Math.floor(ageMs / 3_600_000)}h ago`;
30
+ return `${Math.floor(ageMs / 86_400_000)}d ago`;
31
+ }