@yeaft/webchat-agent 0.1.762 → 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/bin/yeaft-stats.js +165 -0
- package/connection/message-router.js +6 -9
- package/package.json +3 -2
- package/unify/cli.js +16 -0
- package/unify/dream-v2/prompts/index.js +4 -2
- package/unify/dream-v2/session-wiring.js +5 -4
- package/unify/dream-v2/triage.js +7 -10
- package/unify/engine.js +42 -88
- package/unify/groups/pre-flow.js +5 -5
- package/unify/prompts.js +35 -202
- package/unify/session.js +18 -3
- package/unify/stats/format.js +31 -0
- package/unify/stats/tool-usage.js +324 -0
- package/unify/sub-agent/runner.js +4 -19
- package/unify/templates/plan-instruction.md +42 -0
- package/unify/templates/tool-guidance.md +41 -0
- package/unify/tools/index.js +13 -22
- package/unify/tools/start-plan.js +133 -0
- package/unify/tools/todo-write.js +125 -0
- package/unify/vp/vp-crud.js +1 -0
- package/unify/vp/vp-loader.js +2 -1
- package/unify/vp/vp-store.js +8 -0
- package/unify/web-bridge.js +93 -145
- package/unify/features/store.js +0 -583
- package/unify/features/summary.js +0 -250
- package/unify/tools/feature-tools.js +0 -713
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
|
-
* (
|
|
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
|
-
* (
|
|
243
|
-
* (
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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.
|
|
374
|
-
//
|
|
375
|
-
//
|
|
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
|
-
// ───
|
|
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
|
|
617
|
-
*
|
|
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: {
|
|
759
|
-
*
|
|
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.
|
|
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
|
+
}
|