@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/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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yeaft-stats — CLI entry for tool-call usage statistics.
|
|
4
|
+
*
|
|
5
|
+
* Reads `~/.yeaft/stats/tool-usage.json` and renders a table (default),
|
|
6
|
+
* lists tools that have never been called (`--unused`), or dumps raw
|
|
7
|
+
* JSON (`--json`).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* yeaft-stats # ranked table by call count (desc)
|
|
11
|
+
* yeaft-stats --unused # tools registered but never called
|
|
12
|
+
* yeaft-stats --json # raw snapshot (pipe-friendly)
|
|
13
|
+
* yeaft-stats --reset # delete the stats file
|
|
14
|
+
* yeaft-stats --yeaft-dir=/path
|
|
15
|
+
* yeaft-stats --help
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { ToolUsageStats } from '../unify/stats/tool-usage.js';
|
|
21
|
+
import { allTools } from '../unify/tools/index.js';
|
|
22
|
+
import { formatMs, formatPct, formatLastCalled } from '../unify/stats/format.js';
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const opts = {
|
|
26
|
+
unused: false,
|
|
27
|
+
json: false,
|
|
28
|
+
reset: false,
|
|
29
|
+
yeaftDir: null,
|
|
30
|
+
help: false,
|
|
31
|
+
};
|
|
32
|
+
for (const arg of argv.slice(2)) {
|
|
33
|
+
if (arg === '--unused') opts.unused = true;
|
|
34
|
+
else if (arg === '--json') opts.json = true;
|
|
35
|
+
else if (arg === '--reset') opts.reset = true;
|
|
36
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
37
|
+
else if (arg.startsWith('--yeaft-dir=')) opts.yeaftDir = arg.slice('--yeaft-dir='.length);
|
|
38
|
+
}
|
|
39
|
+
return opts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
console.log(`yeaft-stats — tool-call usage statistics
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
yeaft-stats Ranked table by call count (desc)
|
|
47
|
+
yeaft-stats --unused Tools registered but never called
|
|
48
|
+
yeaft-stats --json Raw JSON snapshot (pipe-friendly)
|
|
49
|
+
yeaft-stats --reset Delete the stats file
|
|
50
|
+
yeaft-stats --yeaft-dir=/path
|
|
51
|
+
yeaft-stats --help Show this message`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function padRight(s, n) {
|
|
55
|
+
const str = String(s);
|
|
56
|
+
return str.length >= n ? str : str + ' '.repeat(n - str.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function padLeft(s, n) {
|
|
60
|
+
const str = String(s);
|
|
61
|
+
return str.length >= n ? str : ' '.repeat(n - str.length) + str;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printTable(snapshot) {
|
|
65
|
+
const entries = Object.entries(snapshot)
|
|
66
|
+
.sort((a, b) => b[1].callCount - a[1].callCount);
|
|
67
|
+
if (entries.length === 0) {
|
|
68
|
+
console.log('(no tool calls recorded yet)');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const cols = [
|
|
72
|
+
{ key: 'name', label: 'name', width: 28, align: 'l' },
|
|
73
|
+
{ key: 'calls', label: 'calls', width: 7, align: 'r' },
|
|
74
|
+
{ key: 'errors', label: 'errors', width: 7, align: 'r' },
|
|
75
|
+
{ key: 'errRate', label: 'err%', width: 6, align: 'r' },
|
|
76
|
+
{ key: 'p50', label: 'p50', width: 8, align: 'r' },
|
|
77
|
+
{ key: 'p95', label: 'p95', width: 8, align: 'r' },
|
|
78
|
+
{ key: 'avg', label: 'avg', width: 8, align: 'r' },
|
|
79
|
+
{ key: 'last', label: 'last', width: 12, align: 'l' },
|
|
80
|
+
];
|
|
81
|
+
const header = cols.map(c => (c.align === 'r' ? padLeft(c.label, c.width) : padRight(c.label, c.width))).join(' ');
|
|
82
|
+
console.log(header);
|
|
83
|
+
console.log('-'.repeat(header.length));
|
|
84
|
+
for (const [name, rec] of entries) {
|
|
85
|
+
const row = [
|
|
86
|
+
padRight(name.slice(0, cols[0].width), cols[0].width),
|
|
87
|
+
padLeft(rec.callCount, cols[1].width),
|
|
88
|
+
padLeft(rec.errorCount, cols[2].width),
|
|
89
|
+
padLeft(formatPct(rec.errorRate), cols[3].width),
|
|
90
|
+
padLeft(formatMs(rec.p50Ms), cols[4].width),
|
|
91
|
+
padLeft(formatMs(rec.p95Ms), cols[5].width),
|
|
92
|
+
padLeft(formatMs(rec.avgMs), cols[6].width),
|
|
93
|
+
padRight(formatLastCalled(rec.lastCalledAt), cols[7].width),
|
|
94
|
+
].join(' ');
|
|
95
|
+
console.log(row);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getRegisteredToolNames() {
|
|
100
|
+
// Pull names directly from the static `allTools` export. MCP/skill
|
|
101
|
+
// dynamic tools aren't covered here — the unused list only flags
|
|
102
|
+
// built-in tools that have a definition in the source tree.
|
|
103
|
+
try {
|
|
104
|
+
if (Array.isArray(allTools)) {
|
|
105
|
+
return allTools
|
|
106
|
+
.filter(t => t && typeof t.name === 'string' && t.name)
|
|
107
|
+
.map(t => t.name);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// fall through
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const opts = parseArgs(process.argv);
|
|
117
|
+
if (opts.help) {
|
|
118
|
+
printHelp();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const yeaftDir = opts.yeaftDir || join(homedir(), '.yeaft');
|
|
123
|
+
const statsPath = join(yeaftDir, 'stats', 'tool-usage.json');
|
|
124
|
+
const stats = new ToolUsageStats({ path: statsPath });
|
|
125
|
+
|
|
126
|
+
if (opts.reset) {
|
|
127
|
+
await stats.reset();
|
|
128
|
+
console.log(`reset: ${statsPath}`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stats.loadSync();
|
|
133
|
+
const snapshot = stats.snapshot();
|
|
134
|
+
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
console.log(JSON.stringify({ path: statsPath, tools: snapshot }, null, 2));
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (opts.unused) {
|
|
141
|
+
const registered = await getRegisteredToolNames();
|
|
142
|
+
if (registered.length === 0) {
|
|
143
|
+
console.error('(could not enumerate registered tools — pass --json for raw stats)');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const unused = stats.getRegisteredButUncalled(registered);
|
|
147
|
+
console.log(`Registered: ${registered.length} Called at least once: ${registered.length - unused.length} Unused: ${unused.length}`);
|
|
148
|
+
console.log('-'.repeat(60));
|
|
149
|
+
if (unused.length === 0) {
|
|
150
|
+
console.log('(every registered tool has been called at least once)');
|
|
151
|
+
} else {
|
|
152
|
+
for (const name of unused) console.log(name);
|
|
153
|
+
}
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`stats: ${statsPath}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
printTable(snapshot);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch(err => {
|
|
163
|
+
console.error(`yeaft-stats failed: ${err && err.message ? err.message : err}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead,
|
|
39
|
+
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, handleUnifyFetchToolStats, broadcastLanguageChange } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -460,14 +460,6 @@ export async function handleMessage(msg) {
|
|
|
460
460
|
handleUnifyVpRead(msg);
|
|
461
461
|
break;
|
|
462
462
|
|
|
463
|
-
// R6 G1a — feature summary history + feature affiliation CRUD.
|
|
464
|
-
case 'unify_fetch_summary_history':
|
|
465
|
-
await handleUnifyFetchSummaryHistory(msg);
|
|
466
|
-
break;
|
|
467
|
-
case 'unify_feature_crud':
|
|
468
|
-
await handleUnifyFeatureCrud(msg);
|
|
469
|
-
break;
|
|
470
|
-
|
|
471
463
|
// task-334m: Group CRUD + D1 seed wiring (§Δ10 334m + R6 §Δ31.2).
|
|
472
464
|
// All handlers reply via `group_crud_result`; mutating ops additionally
|
|
473
465
|
// emit `group_roster_changed` (add/remove/default) or
|
|
@@ -505,6 +497,11 @@ export async function handleMessage(msg) {
|
|
|
505
497
|
await handleUnifyDreamTrigger(msg);
|
|
506
498
|
break;
|
|
507
499
|
|
|
500
|
+
// 2026-05-13: per-tool call counters for the Unify debug drawer.
|
|
501
|
+
case 'unify_fetch_tool_stats':
|
|
502
|
+
await handleUnifyFetchToolStats(msg);
|
|
503
|
+
break;
|
|
504
|
+
|
|
508
505
|
// Expert roles definition (for ExpertPanel detail view)
|
|
509
506
|
case 'get_expert_roles': {
|
|
510
507
|
const { getExpertRolesDefinition } = await import('../expert-roles.js');
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.766",
|
|
4
4
|
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"yeaft-agent": "./cli.js",
|
|
9
|
-
"yeaft": "./unify/cli.js"
|
|
9
|
+
"yeaft": "./unify/cli.js",
|
|
10
|
+
"yeaft-stats": "./bin/yeaft-stats.js"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
13
|
"start": "node index.js",
|
package/unify/cli.js
CHANGED
|
@@ -454,6 +454,22 @@ async function runREPL(config, args) {
|
|
|
454
454
|
console.log(` Cold messages: ${conversationStore.countCold()}`);
|
|
455
455
|
console.log(` Registered tools: ${toolRegistry.size}`);
|
|
456
456
|
console.log(` Loaded skills: ${skillManager.size}`);
|
|
457
|
+
// 2026-05-13: per-tool call counters. Rendered inline so the
|
|
458
|
+
// REPL gives a one-shot snapshot — the `yeaft-stats` CLI is
|
|
459
|
+
// the dedicated entry for richer rendering / --unused mode.
|
|
460
|
+
if (session.toolStats && typeof session.toolStats.snapshot === 'function') {
|
|
461
|
+
const snap = session.toolStats.snapshot();
|
|
462
|
+
const entries = Object.entries(snap).sort((a, b) => b[1].callCount - a[1].callCount);
|
|
463
|
+
if (entries.length === 0) {
|
|
464
|
+
console.log(` Tool calls: (none recorded yet)`);
|
|
465
|
+
} else {
|
|
466
|
+
console.log(` Tool calls (top 10 by count):`);
|
|
467
|
+
for (const [name, rec] of entries.slice(0, 10)) {
|
|
468
|
+
const errPct = rec.callCount > 0 ? ((rec.errorCount / rec.callCount) * 100).toFixed(1) : '0';
|
|
469
|
+
console.log(` ${name.padEnd(28)} ${String(rec.callCount).padStart(5)} calls ${errPct.padStart(5)}% err p50=${rec.p50Ms}ms p95=${rec.p95Ms}ms`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
457
473
|
break;
|
|
458
474
|
}
|
|
459
475
|
|
|
@@ -21,7 +21,6 @@ const FILES = {
|
|
|
21
21
|
extractUser: 'extract-user.md',
|
|
22
22
|
extractVp: 'extract-vp.md',
|
|
23
23
|
extractGroup: 'extract-group.md',
|
|
24
|
-
extractFeature: 'extract-feature.md',
|
|
25
24
|
extractTopic: 'extract-topic.md',
|
|
26
25
|
// H2.e — per-scope summary compression
|
|
27
26
|
summarizeScope: 'summarize-scope.md',
|
|
@@ -32,6 +31,10 @@ const FILES = {
|
|
|
32
31
|
* extraction template name. Unknown scopes fall back to `extractTopic`
|
|
33
32
|
* (the most generic template) so we never throw at extraction time.
|
|
34
33
|
*
|
|
34
|
+
* (2026-05-13: `extractFeature` was removed along with the Feature
|
|
35
|
+
* system; legacy `feature/*` scopes — if any remain in old data — fall
|
|
36
|
+
* through to `extractTopic` here, which is harmless.)
|
|
37
|
+
*
|
|
35
38
|
* @param {string} scope
|
|
36
39
|
* @returns {keyof typeof FILES}
|
|
37
40
|
*/
|
|
@@ -40,7 +43,6 @@ export function extractTemplateForScope(scope) {
|
|
|
40
43
|
if (scope === 'user') return 'extractUser';
|
|
41
44
|
if (scope.startsWith('vp/')) return 'extractVp';
|
|
42
45
|
if (scope.startsWith('group/')) return 'extractGroup';
|
|
43
|
-
if (scope.startsWith('feature/')) return 'extractFeature';
|
|
44
46
|
if (scope.startsWith('topic/')) return 'extractTopic';
|
|
45
47
|
return 'extractTopic';
|
|
46
48
|
}
|
|
@@ -77,7 +77,11 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Translate a group-store message record (id, from, role, text, ...) into
|
|
80
|
-
* the shape runDream expects (id, role, body, vpId, author
|
|
80
|
+
* the shape runDream expects (id, role, body, vpId, author).
|
|
81
|
+
*
|
|
82
|
+
* (2026-05-13: legacy `m.meta.featureId` propagation was dropped along
|
|
83
|
+
* with the Feature system. Historical messages on disk may still carry
|
|
84
|
+
* it, but it no longer influences dream scoping.)
|
|
81
85
|
*
|
|
82
86
|
* @param {Object} m
|
|
83
87
|
*/
|
|
@@ -91,9 +95,6 @@ function translateGroupMessage(m) {
|
|
|
91
95
|
if (role === 'assistant' && m.from && m.from !== 'user') {
|
|
92
96
|
out.vpId = m.from;
|
|
93
97
|
}
|
|
94
|
-
if (m.meta && typeof m.meta === 'object' && m.meta.featureId) {
|
|
95
|
-
out.featureId = m.meta.featureId;
|
|
96
|
-
}
|
|
97
98
|
return out;
|
|
98
99
|
}
|
|
99
100
|
|
package/unify/dream-v2/triage.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1. **Hard rules** (this module, no LLM): everything we can determine
|
|
8
8
|
* from message metadata. Always include the active group, every VP
|
|
9
|
-
* that spoke as an assistant in the diff,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* that spoke as an assistant in the diff, and `user` (so painted-over
|
|
10
|
+
* user-profile signals can't be missed). (Feature scope was dropped
|
|
11
|
+
* 2026-05-13 along with the rest of the Feature system.)
|
|
12
12
|
*
|
|
13
13
|
* 2. **Soft classification** (LLM, two passes):
|
|
14
14
|
* Pass-1: high-recall — does the diff carry user-profile signal?
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* it to an exact existing path or propose
|
|
18
18
|
* a new ≤2-level path.
|
|
19
19
|
*
|
|
20
|
-
* VP / group
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* VP / group are deliberately NOT asked of the LLM — Hard Rules
|
|
21
|
+
* already cover them, and giving the LLM a chance to drop a
|
|
22
|
+
* structurally-required scope would weaken the contract.
|
|
23
23
|
*
|
|
24
24
|
* `user_profile_signals === true` does not need Pass-2 either: the
|
|
25
25
|
* Hard Rule already added `user` and Apply itself decides whether
|
|
@@ -75,10 +75,7 @@ export function applyHardRules({ groupId, messages }) {
|
|
|
75
75
|
const vp = m.vpId || (m.author && /^vp:(.+)$/.exec(m.author)?.[1]);
|
|
76
76
|
if (vp && /^[A-Za-z0-9_\-.一-鿿]+$/.test(vp)) add(`vp/${vp}`);
|
|
77
77
|
}
|
|
78
|
-
// Active feature
|
|
79
|
-
if (m.featureId && /^[A-Za-z0-9_\-.一-鿿]+$/.test(m.featureId)) {
|
|
80
|
-
add(`feature/${m.featureId}`);
|
|
81
|
-
}
|
|
78
|
+
// (Active feature scope was dropped 2026-05-13 with the Feature system.)
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
return Array.from(out.values());
|
package/unify/engine.js
CHANGED
|
@@ -253,32 +253,17 @@ export class Engine {
|
|
|
253
253
|
/** @type {string|null} */
|
|
254
254
|
#yeaftDir;
|
|
255
255
|
|
|
256
|
+
/** @type {import('./stats/tool-usage.js').ToolUsageStats|null} — per-tool call/latency counters */
|
|
257
|
+
#toolStats = null;
|
|
258
|
+
|
|
256
259
|
/** @type {object|null} — Config override for internal tasks (recall, consolidation, dream) using fastModel */
|
|
257
260
|
#fastConfig;
|
|
258
261
|
|
|
259
262
|
/** @type {((agentId: string, evt: object) => void) | null} */
|
|
260
263
|
#subAgentEventSink = null;
|
|
261
264
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*
|
|
265
|
-
* The feature arc lives in the per-VP web-bridge (`runVpTurn` creates
|
|
266
|
-
* one per turn) so the engine itself does NOT own the featureId. To
|
|
267
|
-
* let sub-agents inherit their parent's featureId without coupling the
|
|
268
|
-
* engine to FeatureArc, the bridge plugs in a thin accessor right
|
|
269
|
-
* after creating the arc — `engine.setCurrentFeatureIdAccessor(() =>
|
|
270
|
-
* arc.getFeatureId() || null)`. The engine surfaces the result via
|
|
271
|
-
* `parentEngineDeps.getCurrentFeatureId` (read lazily at sub-agent
|
|
272
|
-
* event-emit time, so a feature that opens mid-turn still tags the
|
|
273
|
-
* sub-agent's later events).
|
|
274
|
-
*
|
|
275
|
-
* The accessor is cleared (set to null) by the bridge in the same
|
|
276
|
-
* `finally` block that clears the per-turn AbortController, so a
|
|
277
|
-
* stale arc reference can't leak into the next turn.
|
|
278
|
-
*
|
|
279
|
-
* @type {(() => (string|null)) | null}
|
|
280
|
-
*/
|
|
281
|
-
#currentFeatureIdAccessor = null;
|
|
265
|
+
// (removed 2026-05-13) `#currentFeatureIdAccessor` — sub-agent
|
|
266
|
+
// feature-inheritance plumbing that went with the Feature system.
|
|
282
267
|
|
|
283
268
|
/**
|
|
284
269
|
* task-325a — abort state.
|
|
@@ -347,7 +332,7 @@ export class Engine {
|
|
|
347
332
|
* yeaftDir?: string,
|
|
348
333
|
* }} params
|
|
349
334
|
*/
|
|
350
|
-
constructor({ adapter, trace, config, conversationStore, memoryIndex, amsRegistry, toolRegistry, skillManager, mcpManager, yeaftDir }) {
|
|
335
|
+
constructor({ adapter, trace, config, conversationStore, memoryIndex, amsRegistry, toolRegistry, skillManager, mcpManager, yeaftDir, toolStats = null }) {
|
|
351
336
|
this.#adapter = adapter;
|
|
352
337
|
this.#trace = trace;
|
|
353
338
|
this.#config = config;
|
|
@@ -360,6 +345,7 @@ export class Engine {
|
|
|
360
345
|
this.#skillManager = skillManager || null;
|
|
361
346
|
this.#mcpManager = mcpManager || null;
|
|
362
347
|
this.#yeaftDir = yeaftDir || null;
|
|
348
|
+
this.#toolStats = toolStats || null;
|
|
363
349
|
|
|
364
350
|
// PR-L: tool history reflection log. Keyed by traceId so distinct
|
|
365
351
|
// engine instances don't stomp on each other's jsonl files. When
|
|
@@ -516,7 +502,6 @@ export class Engine {
|
|
|
516
502
|
* @param {{
|
|
517
503
|
* groupId?: string,
|
|
518
504
|
* ownVpId?: string|null,
|
|
519
|
-
* featureId?: string,
|
|
520
505
|
* summaries: { user?: string, group?: string, vp?: string },
|
|
521
506
|
* recallEntries: object[],
|
|
522
507
|
* }} args
|
|
@@ -562,7 +547,6 @@ export class Engine {
|
|
|
562
547
|
const scopes = buildRelevantScopes({
|
|
563
548
|
groupId: args.groupId,
|
|
564
549
|
vpId: ownVpId,
|
|
565
|
-
featureId: args.featureId,
|
|
566
550
|
});
|
|
567
551
|
|
|
568
552
|
return { ams, groupKey, ownVpId, scopes, snapshotBlock };
|
|
@@ -753,9 +737,21 @@ export class Engine {
|
|
|
753
737
|
// VP-aware tool). Undefined when running in non-group / no-VP flows.
|
|
754
738
|
router: vpCtx?.router,
|
|
755
739
|
senderVpId: vpCtx?.senderVpId,
|
|
740
|
+
// Active VP persona — surfaced so tools like `StartPlan` can read
|
|
741
|
+
// the optional `planInstruction` override without re-reading
|
|
742
|
+
// role.md. Mirrors the symmetry already present in
|
|
743
|
+
// `parentEngineDeps.parentVpPersona` below — sub-agents inherit it
|
|
744
|
+
// through the parent deps; tools at this level read it directly.
|
|
745
|
+
// Null in non-VP / test contexts.
|
|
746
|
+
vpPersona: vpCtx?.vpPersona || null,
|
|
756
747
|
inboundEnvelope: vpCtx?.inboundEnvelope,
|
|
757
748
|
taskId: vpCtx?.taskId,
|
|
758
749
|
taskMembers: vpCtx?.taskMembers,
|
|
750
|
+
// TodoWrite per-VP cache hooks. Threaded from web-bridge so each
|
|
751
|
+
// VP keeps its own todo list (see todo-write.js, web-bridge.js).
|
|
752
|
+
// Null in non-VP / test contexts — tools tolerate missing slots.
|
|
753
|
+
getCurrentTodos: vpCtx?.getCurrentTodos || null,
|
|
754
|
+
setCurrentTodos: vpCtx?.setCurrentTodos || null,
|
|
759
755
|
// task-707: tool-callable end-turn signal. The engine threads this
|
|
760
756
|
// setter when constructing toolCtx so a tool (e.g. route_forward)
|
|
761
757
|
// can mark "after this batch, end the turn — do NOT call adapter
|
|
@@ -776,13 +772,6 @@ export class Engine {
|
|
|
776
772
|
parentVpPersona: vpCtx?.vpPersona || null,
|
|
777
773
|
onEvent: this.#subAgentEventSink || null,
|
|
778
774
|
language: this.#config?.language || 'en',
|
|
779
|
-
// PR-4: lazy accessor so the sub-agent runner can stamp the
|
|
780
|
-
// parent's active featureId on every forwarded event. Read at
|
|
781
|
-
// emit-time (NOT at spawn-time) so a feature that opens AFTER
|
|
782
|
-
// the sub-agent starts still tags later events.
|
|
783
|
-
getCurrentFeatureId: () => {
|
|
784
|
-
try { return this.#currentFeatureIdAccessor?.() || null; } catch { return null; }
|
|
785
|
-
},
|
|
786
775
|
},
|
|
787
776
|
};
|
|
788
777
|
}
|
|
@@ -799,36 +788,6 @@ export class Engine {
|
|
|
799
788
|
this.#subAgentEventSink = typeof sink === 'function' ? sink : null;
|
|
800
789
|
}
|
|
801
790
|
|
|
802
|
-
/**
|
|
803
|
-
* PR-4 — install / clear the per-turn current-featureId accessor.
|
|
804
|
-
*
|
|
805
|
-
* Called by `runVpTurn` in web-bridge.js: right after `arc =
|
|
806
|
-
* createFeatureArc(...)` it passes `() => arc.getFeatureId() || null`,
|
|
807
|
-
* and clears it (passes null) in the `finally` so a stale arc
|
|
808
|
-
* reference can't leak into the next turn. The engine reads it
|
|
809
|
-
* lazily via `parentEngineDeps.getCurrentFeatureId` so sub-agents
|
|
810
|
-
* inherit whatever featureId is live at the moment they emit.
|
|
811
|
-
*
|
|
812
|
-
* Concurrency note: callers are expected to serialize per-VP turns
|
|
813
|
-
* (web-bridge does — `runVpTurn` runs one turn at a time per VP and
|
|
814
|
-
* its `finally` clears the accessor before the next turn lands).
|
|
815
|
-
* If two installs collide (which today would be a bug, not by
|
|
816
|
-
* design), we warn so it's visible in logs.
|
|
817
|
-
*
|
|
818
|
-
* @param {(() => (string|null)) | null} fn
|
|
819
|
-
*/
|
|
820
|
-
setCurrentFeatureIdAccessor(fn) {
|
|
821
|
-
const next = typeof fn === 'function' ? fn : null;
|
|
822
|
-
if (next && this.#currentFeatureIdAccessor) {
|
|
823
|
-
// Canary: a prior accessor is still installed when we're about
|
|
824
|
-
// to overwrite it. Today's lifecycle (per-turn install + finally
|
|
825
|
-
// clear) means this should never fire; if it does, two turns
|
|
826
|
-
// are racing on the same engine.
|
|
827
|
-
console.warn('[Engine] setCurrentFeatureIdAccessor: overwriting non-null accessor — concurrent turns on the same VP engine?');
|
|
828
|
-
}
|
|
829
|
-
this.#currentFeatureIdAccessor = next;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
791
|
/**
|
|
833
792
|
* Perform memory recall for a given prompt.
|
|
834
793
|
*
|
|
@@ -839,7 +798,7 @@ export class Engine {
|
|
|
839
798
|
* without injection.
|
|
840
799
|
*
|
|
841
800
|
* @param {string} prompt
|
|
842
|
-
* @param {{ groupId?: string, vpId?: string
|
|
801
|
+
* @param {{ groupId?: string, vpId?: string }} [ctx]
|
|
843
802
|
* @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
|
|
844
803
|
*/
|
|
845
804
|
async #recallMemory(prompt, ctx = {}) {
|
|
@@ -850,7 +809,6 @@ export class Engine {
|
|
|
850
809
|
userMsg: prompt,
|
|
851
810
|
groupId: ctx.groupId,
|
|
852
811
|
vpId: ctx.vpId,
|
|
853
|
-
featureId: ctx.featureId,
|
|
854
812
|
});
|
|
855
813
|
memory.profile = result.profile || '';
|
|
856
814
|
memory.entries = result.entries || [];
|
|
@@ -1063,7 +1021,7 @@ export class Engine {
|
|
|
1063
1021
|
* string-prompt shape (no regression for existing callers).
|
|
1064
1022
|
* @yields {EngineEvent}
|
|
1065
1023
|
*/
|
|
1066
|
-
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted = false } = {}) {
|
|
1024
|
+
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null } = {}) {
|
|
1067
1025
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1068
1026
|
yield {
|
|
1069
1027
|
type: 'error',
|
|
@@ -1122,7 +1080,7 @@ export class Engine {
|
|
|
1122
1080
|
const runSignal = abortCtrl.signal;
|
|
1123
1081
|
|
|
1124
1082
|
try {
|
|
1125
|
-
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted });
|
|
1083
|
+
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted, getCurrentTodos, setCurrentTodos });
|
|
1126
1084
|
} finally {
|
|
1127
1085
|
if (signal) {
|
|
1128
1086
|
try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
|
|
@@ -1140,7 +1098,7 @@ export class Engine {
|
|
|
1140
1098
|
* in a try/finally without indenting the whole loop.
|
|
1141
1099
|
* @private
|
|
1142
1100
|
*/
|
|
1143
|
-
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted = false }) {
|
|
1101
|
+
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null }) {
|
|
1144
1102
|
|
|
1145
1103
|
// ─── Pre-query: FTS5 Memory Recall + AMS snapshot ─────
|
|
1146
1104
|
// Memory has a SINGLE render outlet now (DESIGN-PROMPT §3 ③):
|
|
@@ -1159,9 +1117,6 @@ export class Engine {
|
|
|
1159
1117
|
vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
|
|
1160
1118
|
? vpPersona.vpId
|
|
1161
1119
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
1162
|
-
featureId: typeof inboundEnvelope === 'object' && inboundEnvelope
|
|
1163
|
-
? inboundEnvelope.featureId
|
|
1164
|
-
: undefined,
|
|
1165
1120
|
});
|
|
1166
1121
|
recallEntryCount = recallResult && Array.isArray(recallResult.entries)
|
|
1167
1122
|
? recallResult.entries.length
|
|
@@ -1191,13 +1146,9 @@ export class Engine {
|
|
|
1191
1146
|
&& typeof vpPersona.vpId === 'string'
|
|
1192
1147
|
? vpPersona.vpId
|
|
1193
1148
|
: (typeof senderVpId === 'string' ? senderVpId : null);
|
|
1194
|
-
const featureIdForAms = typeof inboundEnvelope === 'object' && inboundEnvelope
|
|
1195
|
-
? inboundEnvelope.featureId
|
|
1196
|
-
: undefined;
|
|
1197
1149
|
const amsContext = this.#prepareAms({
|
|
1198
1150
|
groupId,
|
|
1199
1151
|
ownVpId: ownVpIdForAms,
|
|
1200
|
-
featureId: featureIdForAms,
|
|
1201
1152
|
summaries,
|
|
1202
1153
|
recallEntries: recallResult ? (recallResult.entries || []) : [],
|
|
1203
1154
|
});
|
|
@@ -1206,17 +1157,10 @@ export class Engine {
|
|
|
1206
1157
|
}
|
|
1207
1158
|
|
|
1208
1159
|
// ─── Active Scope (DESIGN-PROMPT §3 ④) ──────────────────────
|
|
1209
|
-
// Structured per-turn scope summary:
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
// (T4 Scope Tagging is a placeholder; not every turn lives in a
|
|
1213
|
-
// feature — DESIGN-PROMPT §5.1).
|
|
1160
|
+
// Structured per-turn scope summary: group + vp + envelope routing
|
|
1161
|
+
// info. Long-form scope content lives in AMS — this block carries
|
|
1162
|
+
// only IDs + tiny labels. (Feature scope retired 2026-05-13.)
|
|
1214
1163
|
const activeScope = {
|
|
1215
|
-
featureId: featureIdForAms || null,
|
|
1216
|
-
featureTitle: typeof inboundEnvelope === 'object' && inboundEnvelope
|
|
1217
|
-
&& typeof inboundEnvelope.featureTitle === 'string'
|
|
1218
|
-
? inboundEnvelope.featureTitle
|
|
1219
|
-
: '',
|
|
1220
1164
|
groupId: groupId || '',
|
|
1221
1165
|
vpId: ownVpIdForAms || '',
|
|
1222
1166
|
envelope: inboundEnvelope || null,
|
|
@@ -1228,12 +1172,6 @@ export class Engine {
|
|
|
1228
1172
|
vpPersona,
|
|
1229
1173
|
activeScope,
|
|
1230
1174
|
groupAnnouncement,
|
|
1231
|
-
// taskCtx is not currently wired by the query loop. The legacy
|
|
1232
|
-
// task-context sub-block (renderTaskCtx, task-334e/334n contract) is
|
|
1233
|
-
// retained behind a feature flag for callers that still build it
|
|
1234
|
-
// upstream — it'll be folded into Active Scope or retired in a
|
|
1235
|
-
// dedicated PR alongside the task-334n cleanup.
|
|
1236
|
-
taskCtx: undefined,
|
|
1237
1175
|
});
|
|
1238
1176
|
|
|
1239
1177
|
// ─── Compact summary as messages-array head (DESIGN-PROMPT §4.3) ─
|
|
@@ -1901,6 +1839,8 @@ export class Engine {
|
|
|
1901
1839
|
taskMembers,
|
|
1902
1840
|
vpPersona,
|
|
1903
1841
|
contextWindow: currentContextWindow,
|
|
1842
|
+
getCurrentTodos,
|
|
1843
|
+
setCurrentTodos,
|
|
1904
1844
|
requestEndTurn: (reason) => {
|
|
1905
1845
|
// First call wins — preserve the kind/reason of the first tool
|
|
1906
1846
|
// that asked to end the turn. Late callers (a second
|
|
@@ -2012,6 +1952,20 @@ export class Engine {
|
|
|
2012
1952
|
isError,
|
|
2013
1953
|
};
|
|
2014
1954
|
|
|
1955
|
+
// 2026-05-13: feed the per-tool counters. Stays best-effort — a
|
|
1956
|
+
// stats sink that throws shouldn't crash the engine. `record`
|
|
1957
|
+
// already swallows internal write errors.
|
|
1958
|
+
if (this.#toolStats && typeof this.#toolStats.record === 'function') {
|
|
1959
|
+
try {
|
|
1960
|
+
this.#toolStats.record({
|
|
1961
|
+
name: tc.name,
|
|
1962
|
+
durationMs: toolDurationMs,
|
|
1963
|
+
isError,
|
|
1964
|
+
errorMessage: isError && typeof output === 'string' ? output.slice(0, 500) : null,
|
|
1965
|
+
});
|
|
1966
|
+
} catch { /* swallow */ }
|
|
1967
|
+
}
|
|
1968
|
+
|
|
2015
1969
|
// Log tool to debug trace
|
|
2016
1970
|
this.#trace.logTool(turnId, {
|
|
2017
1971
|
toolName: tc.name,
|
package/unify/groups/pre-flow.js
CHANGED
|
@@ -225,19 +225,20 @@ export function formatPickedForInjection(picked) {
|
|
|
225
225
|
*/
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* Build the canonical scope list for a given (groupId, vpId
|
|
228
|
+
* Build the canonical scope list for a given (groupId, vpId).
|
|
229
229
|
* Always includes 'user'. The order is significant — preflow.js's scope
|
|
230
230
|
* filter accepts/rejects by membership, and the formatter renders in
|
|
231
231
|
* order.
|
|
232
232
|
*
|
|
233
|
-
*
|
|
233
|
+
* (2026-05-13: `featureId` scope dropped along with the Feature system.)
|
|
234
|
+
*
|
|
235
|
+
* @param {{groupId?: string, vpId?: string, extra?: string[]}} ctx
|
|
234
236
|
* @returns {string[]}
|
|
235
237
|
*/
|
|
236
|
-
export function buildRelevantScopes({ groupId, vpId,
|
|
238
|
+
export function buildRelevantScopes({ groupId, vpId, extra } = {}) {
|
|
237
239
|
const scopes = ['user'];
|
|
238
240
|
if (groupId) scopes.push(`group/${groupId}`);
|
|
239
241
|
if (vpId) scopes.push(`vp/${vpId}`);
|
|
240
|
-
if (featureId) scopes.push(`feature/${featureId}`);
|
|
241
242
|
if (Array.isArray(extra)) {
|
|
242
243
|
for (const s of extra) {
|
|
243
244
|
if (s && !scopes.includes(s)) scopes.push(s);
|
|
@@ -273,7 +274,6 @@ export function runMemoryPreflow(index, opts) {
|
|
|
273
274
|
const relevantScopes = buildRelevantScopes({
|
|
274
275
|
groupId: opts.groupId,
|
|
275
276
|
vpId: opts.vpId,
|
|
276
|
-
featureId: opts.featureId,
|
|
277
277
|
extra: opts.extraScopes,
|
|
278
278
|
});
|
|
279
279
|
|