@yeaft/webchat-agent 0.1.666 → 0.1.669
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/connection/message-router.js +24 -1
- package/package.json +3 -2
- package/scripts/check-pty.js +87 -0
- package/unify/config-api.js +151 -0
- package/unify/config.js +2 -2
- package/unify/dream-v2/apply.js +1 -1
- package/unify/dream-v2/limits.js +1 -1
- package/unify/dream-v2/merge.js +1 -1
- package/unify/dream-v2/runner.js +2 -2
- package/unify/dream-v2/schedule.js +2 -2
- package/unify/dream-v2/segment.js +1 -1
- package/unify/dream-v2/session-wiring.js +2 -3
- package/unify/dream-v2/snapshot.js +1 -1
- package/unify/dream-v2/state.js +1 -1
- package/unify/dream-v2/triage.js +1 -1
- package/unify/engine.js +3 -5
- package/unify/eval/cases/memory.js +9 -142
- package/unify/memory/ams.js +1 -1
- package/unify/memory/segment-store.js +1 -1
- package/unify/memory/store-v2.js +6 -9
- package/unify/prompts.js +5 -31
- package/unify/session.js +1 -1
- package/unify/tools/web-search.js +8 -0
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
|
-
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
|
|
38
|
+
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
39
|
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
@@ -355,6 +355,29 @@ export async function handleMessage(msg) {
|
|
|
355
355
|
break;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// Search settings (web-search backend + Tavily key) — read/write the
|
|
359
|
+
// `search` section of config.json. `get_tavily_usage` hits Tavily's
|
|
360
|
+
// /usage endpoint with the saved key and is fired from the UI only
|
|
361
|
+
// when the Search tab opens or the user clicks "Refresh" (no polling
|
|
362
|
+
// — the user explicitly asked for live read on open).
|
|
363
|
+
case 'get_search_settings': {
|
|
364
|
+
const settings = getSearchSettings(ctx.CONFIG?.yeaftDir);
|
|
365
|
+
sendToServer({ type: 'search_settings', ...settings });
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'update_search_settings': {
|
|
370
|
+
const result = updateSearchSettings(msg.settings || msg.config || {}, ctx.CONFIG?.yeaftDir);
|
|
371
|
+
sendToServer({ type: 'search_settings_updated', ...result });
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case 'get_tavily_usage': {
|
|
376
|
+
const usage = await fetchTavilyUsage(ctx.CONFIG?.yeaftDir);
|
|
377
|
+
sendToServer({ type: 'tavily_usage', ...usage });
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
358
381
|
// Unify — independent chat via Engine
|
|
359
382
|
case 'unify_chat':
|
|
360
383
|
await handleUnifyChat(msg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.669",
|
|
4
4
|
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node index.js",
|
|
13
|
-
"dev": "nodemon index.js"
|
|
13
|
+
"dev": "nodemon index.js",
|
|
14
|
+
"postinstall": "node scripts/check-pty.js"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=22.5.0"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent/scripts/check-pty.js — postinstall sanity check for node-pty.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists:
|
|
6
|
+
* `node-pty` is an optionalDependency. npm install swallows install
|
|
7
|
+
* failures silently — when the native module fails to build (e.g.
|
|
8
|
+
* Linux x64, where node-pty's tarball ships no prebuilds and the
|
|
9
|
+
* host g++ is too old to compile C++20), the agent loses its
|
|
10
|
+
* `terminal` capability with zero user-facing signal. The web UI's
|
|
11
|
+
* Terminal tab silently disappears.
|
|
12
|
+
*
|
|
13
|
+
* This postinstall script makes that failure visible: if we're on a
|
|
14
|
+
* platform where node-pty was supposed to install but didn't, print
|
|
15
|
+
* a clear warning with the recovery command. We never fail the
|
|
16
|
+
* install — pty is genuinely optional, and required-feature mode
|
|
17
|
+
* would block users who don't need a Terminal tab.
|
|
18
|
+
*
|
|
19
|
+
* Exit code is always 0 — we only print, never abort.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import { createRequire } from 'module';
|
|
24
|
+
import { platform } from 'os';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
function tryResolveNodePty() {
|
|
29
|
+
try {
|
|
30
|
+
return require.resolve('node-pty');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryLoadBinary() {
|
|
37
|
+
// node-pty's lib/index.js does `require('../build/Release/pty.node')`
|
|
38
|
+
// — if the binary is missing or ABI-incompatible, that throws. We
|
|
39
|
+
// mimic the load eagerly so the warning fires at install time, not
|
|
40
|
+
// at agent startup.
|
|
41
|
+
try {
|
|
42
|
+
require('node-pty');
|
|
43
|
+
return { ok: true };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { ok: false, error: e.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolved = tryResolveNodePty();
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
// node-pty was never installed at all — npm skipped it for the
|
|
52
|
+
// current platform/engine combination.
|
|
53
|
+
const plat = platform();
|
|
54
|
+
if (plat === 'linux' || plat === 'darwin' || plat === 'win32') {
|
|
55
|
+
console.warn('');
|
|
56
|
+
console.warn(' ⚠ node-pty was not installed on this host.');
|
|
57
|
+
console.warn(' The agent will run, but the web UI Terminal tab');
|
|
58
|
+
console.warn(' will be hidden (terminal capability missing).');
|
|
59
|
+
if (plat === 'linux') {
|
|
60
|
+
console.warn('');
|
|
61
|
+
console.warn(' Linux fix: install a C++20-capable compiler');
|
|
62
|
+
console.warn(' (g++-10 or newer) and rebuild:');
|
|
63
|
+
console.warn('');
|
|
64
|
+
console.warn(' sudo apt-get install -y g++-10');
|
|
65
|
+
console.warn(' CXX=g++-10 npm install node-pty --include=optional');
|
|
66
|
+
} else {
|
|
67
|
+
console.warn('');
|
|
68
|
+
console.warn(' Reinstall to retry: npm install node-pty');
|
|
69
|
+
}
|
|
70
|
+
console.warn('');
|
|
71
|
+
}
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const loaded = tryLoadBinary();
|
|
76
|
+
if (!loaded.ok) {
|
|
77
|
+
console.warn('');
|
|
78
|
+
console.warn(' ⚠ node-pty resolved but failed to load native binary.');
|
|
79
|
+
console.warn(` Error: ${loaded.error}`);
|
|
80
|
+
console.warn(' The agent will run without Terminal tab support.');
|
|
81
|
+
console.warn(' Recover: npm rebuild node-pty');
|
|
82
|
+
console.warn('');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Silent on success — clean install output.
|
|
87
|
+
process.exit(0);
|
package/unify/config-api.js
CHANGED
|
@@ -215,3 +215,154 @@ export function updateUnifySettings(update, dir) {
|
|
|
215
215
|
|
|
216
216
|
return merged;
|
|
217
217
|
}
|
|
218
|
+
|
|
219
|
+
// ─── Search settings (web-search backend selection + Tavily key) ────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Valid backend values. `playwright` is reserved for the upcoming
|
|
223
|
+
* playwright-service tool — its UI option is currently disabled, but we
|
|
224
|
+
* accept the literal so a hand-edited config doesn't trip validation.
|
|
225
|
+
* Anything else is rejected on write and normalized to `tavily` on read.
|
|
226
|
+
*/
|
|
227
|
+
const VALID_BACKENDS = ['tavily', 'playwright'];
|
|
228
|
+
|
|
229
|
+
function maskKey(key) {
|
|
230
|
+
if (!key || typeof key !== 'string') return null;
|
|
231
|
+
if (key.length <= 10) return '***';
|
|
232
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read the `search` section of config.json. Tavily key is returned in
|
|
237
|
+
* masked form (`tvly-d...j3dgV`) — the raw key never leaves the agent.
|
|
238
|
+
* UI uses `tavilyKeyConfigured` to decide whether the input shows a
|
|
239
|
+
* "(unchanged)" placeholder vs an empty box.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} [dir]
|
|
242
|
+
* @returns {{ backend: string, tavilyKeyConfigured: boolean, tavilyKeyMasked: string|null, disableHtmlFallback: boolean } | { error: string }}
|
|
243
|
+
*/
|
|
244
|
+
export function getSearchSettings(dir) {
|
|
245
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
246
|
+
const configPath = join(root, 'config.json');
|
|
247
|
+
const defaults = {
|
|
248
|
+
backend: 'tavily',
|
|
249
|
+
tavilyKeyConfigured: false,
|
|
250
|
+
tavilyKeyMasked: null,
|
|
251
|
+
disableHtmlFallback: false,
|
|
252
|
+
};
|
|
253
|
+
if (!existsSync(configPath)) return defaults;
|
|
254
|
+
try {
|
|
255
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
256
|
+
const s = (json && typeof json.search === 'object' && json.search) || {};
|
|
257
|
+
const backend = VALID_BACKENDS.includes(s.backend) ? s.backend : 'tavily';
|
|
258
|
+
const key = typeof s.tavilyApiKey === 'string' ? s.tavilyApiKey : '';
|
|
259
|
+
return {
|
|
260
|
+
backend,
|
|
261
|
+
tavilyKeyConfigured: !!key,
|
|
262
|
+
tavilyKeyMasked: key ? maskKey(key) : null,
|
|
263
|
+
disableHtmlFallback: !!s.disableHtmlFallback,
|
|
264
|
+
};
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update the `search` section of config.json. Update is shallow-merged:
|
|
272
|
+
* any field omitted from `update` keeps its previous value. Pass
|
|
273
|
+
* `tavilyApiKey: ''` explicitly to clear the key; pass `undefined` (or
|
|
274
|
+
* omit) to keep it unchanged — this is what the UI relies on so the
|
|
275
|
+
* "(unchanged)" placeholder doesn't accidentally wipe a saved key when
|
|
276
|
+
* the user only touches the backend radio.
|
|
277
|
+
*
|
|
278
|
+
* @param {{ backend?: string, tavilyApiKey?: string, disableHtmlFallback?: boolean }} update
|
|
279
|
+
* @param {string} [dir]
|
|
280
|
+
* @returns {ReturnType<typeof getSearchSettings>}
|
|
281
|
+
*/
|
|
282
|
+
export function updateSearchSettings(update, dir) {
|
|
283
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
284
|
+
const configPath = join(root, 'config.json');
|
|
285
|
+
|
|
286
|
+
if (!update || typeof update !== 'object') {
|
|
287
|
+
return { error: 'update payload required' };
|
|
288
|
+
}
|
|
289
|
+
if (update.backend !== undefined && !VALID_BACKENDS.includes(update.backend)) {
|
|
290
|
+
return { error: `backend must be one of: ${VALID_BACKENDS.join(', ')}` };
|
|
291
|
+
}
|
|
292
|
+
if (update.tavilyApiKey !== undefined && typeof update.tavilyApiKey !== 'string') {
|
|
293
|
+
return { error: 'tavilyApiKey must be a string' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let existing = {};
|
|
297
|
+
if (existsSync(configPath)) {
|
|
298
|
+
try {
|
|
299
|
+
existing = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
300
|
+
} catch {
|
|
301
|
+
existing = {};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
|
|
305
|
+
const merged = { ...prev };
|
|
306
|
+
if (update.backend !== undefined) merged.backend = update.backend;
|
|
307
|
+
if (update.tavilyApiKey !== undefined) merged.tavilyApiKey = update.tavilyApiKey;
|
|
308
|
+
if (update.disableHtmlFallback !== undefined) merged.disableHtmlFallback = !!update.disableHtmlFallback;
|
|
309
|
+
existing.search = merged;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return { error: `Failed to write config.json: ${e.message}` };
|
|
315
|
+
}
|
|
316
|
+
return getSearchSettings(root);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Probe Tavily's `/usage` endpoint with the currently-saved key. Returns
|
|
321
|
+
* the plan + usage fields the UI shows, or `{ error }` for any of:
|
|
322
|
+
* - no key configured
|
|
323
|
+
* - HTTP error from Tavily (401 = bad key, etc.)
|
|
324
|
+
* - network failure
|
|
325
|
+
*
|
|
326
|
+
* Called only when the user opens the Search settings tab (the user
|
|
327
|
+
* explicitly asked for "open settings → live read, don't poll"). No
|
|
328
|
+
* caching here — a stale display is more confusing than a fresh probe.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} [dir]
|
|
331
|
+
* @returns {Promise<{ plan: string, used: number, limit: number|null, paygoUsed: number, paygoLimit: number|null } | { error: string }>}
|
|
332
|
+
*/
|
|
333
|
+
export async function fetchTavilyUsage(dir) {
|
|
334
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
335
|
+
const configPath = join(root, 'config.json');
|
|
336
|
+
if (!existsSync(configPath)) return { error: 'config.json not found' };
|
|
337
|
+
let key;
|
|
338
|
+
try {
|
|
339
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
340
|
+
key = json?.search?.tavilyApiKey;
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
343
|
+
}
|
|
344
|
+
if (!key) return { error: 'Tavily API key not configured' };
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch('https://api.tavily.com/usage', {
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) {
|
|
352
|
+
const text = await res.text().catch(() => '');
|
|
353
|
+
return { error: `${res.status} ${res.statusText} ${text.slice(0, 200)}` };
|
|
354
|
+
}
|
|
355
|
+
const data = await res.json();
|
|
356
|
+
const account = data?.account || {};
|
|
357
|
+
return {
|
|
358
|
+
plan: account.current_plan || 'unknown',
|
|
359
|
+
used: Number(account.plan_usage) || 0,
|
|
360
|
+
limit: account.plan_limit ?? null,
|
|
361
|
+
paygoUsed: Number(account.paygo_usage) || 0,
|
|
362
|
+
paygoLimit: account.paygo_limit ?? null,
|
|
363
|
+
};
|
|
364
|
+
} catch (e) {
|
|
365
|
+
return { error: e.message || String(e) };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
package/unify/config.js
CHANGED
|
@@ -217,7 +217,7 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
217
217
|
maxContinueTurns: overrides.maxContinueTurns ?? fileConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
|
|
218
218
|
// task-318: legacy path never had the `unify` section — defaults.
|
|
219
219
|
unify: normaliseUnifySection(null),
|
|
220
|
-
//
|
|
220
|
+
// H2-AMS feature flag. Default true. Override wins.
|
|
221
221
|
memoryV2: overrides.memoryV2 !== undefined ? !!overrides.memoryV2 : true,
|
|
222
222
|
providers: null,
|
|
223
223
|
primaryModel: null,
|
|
@@ -315,7 +315,7 @@ export function loadConfig(overrides = {}) {
|
|
|
315
315
|
// don't pollute the flat config namespace used by chat/crew code.
|
|
316
316
|
unify: normaliseUnifySection(jsonConfig.unify),
|
|
317
317
|
|
|
318
|
-
//
|
|
318
|
+
// H2-AMS feature flag. When true the session opens the FTS5
|
|
319
319
|
// SegmentIndex (used by groups/pre-flow.js → memory/preflow.js
|
|
320
320
|
// for pre-turn recall) and wires the v2 dream pipeline
|
|
321
321
|
// (dream-v2/runner.js). When false both are skipped — no recall,
|
package/unify/dream-v2/apply.js
CHANGED
package/unify/dream-v2/limits.js
CHANGED
package/unify/dream-v2/merge.js
CHANGED
package/unify/dream-v2/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream-v2/runner.js
|
|
2
|
+
* dream-v2/runner.js.
|
|
3
3
|
*
|
|
4
4
|
* Orchestrates one full dream pass:
|
|
5
5
|
*
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* onProgress emits dream_progress events that the web bridge forwards
|
|
31
31
|
* as `unify_output` messages so the debug panel can render live state
|
|
32
|
-
*
|
|
32
|
+
* . All events flow through the same channel; no new
|
|
33
33
|
* WebSocket message type is introduced.
|
|
34
34
|
*
|
|
35
35
|
* Everything that touches a shell of the system (LLM, message store,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream-v2/schedule.js
|
|
2
|
+
* dream-v2/schedule.js.
|
|
3
3
|
*
|
|
4
4
|
* Two trigger paths:
|
|
5
5
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The scheduler is a thin wrapper around `runDream()` that prevents
|
|
12
12
|
* concurrent passes (a second tick while the previous is still running
|
|
13
|
-
* is dropped, not queued
|
|
13
|
+
* is dropped, not queued: "slow is OK, doesn't
|
|
14
14
|
* compete with user latency").
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dream-v2/session-wiring.js
|
|
2
|
+
* dream-v2/session-wiring.js wire-up for session.js.
|
|
3
3
|
*
|
|
4
4
|
* Bridges the framework-agnostic `runDream` orchestrator (dream-v2/runner.js)
|
|
5
5
|
* to the live yeaft session: groups store, conversation log, LLM adapter,
|
|
6
6
|
* and the engine's progress event sink.
|
|
7
7
|
*
|
|
8
8
|
* The dream pipeline only activates when `config.memoryV2 === true`. When
|
|
9
|
-
* the flag is off, this module is a no-op
|
|
10
|
-
* keeps running as before (session.js still wires it).
|
|
9
|
+
* the flag is off, this module is a no-op.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import { join } from 'path';
|
package/unify/dream-v2/state.js
CHANGED
package/unify/dream-v2/triage.js
CHANGED
package/unify/engine.js
CHANGED
|
@@ -557,8 +557,8 @@ export class Engine {
|
|
|
557
557
|
* @param {{ profile?: string, entries?: object[] }} [memory]
|
|
558
558
|
* @param {string} [compactSummary]
|
|
559
559
|
* @param {string} [prompt] — user prompt (for skill relevance matching)
|
|
560
|
-
* @param {string} [memoryInjection] —
|
|
561
|
-
* @param {string} [userProfile] — user profile
|
|
560
|
+
* @param {string} [memoryInjection] — prebuilt memory block from preflow
|
|
561
|
+
* @param {string} [userProfile] — user profile string
|
|
562
562
|
* @param {object} [vpPersona]
|
|
563
563
|
* @param {{user?:string, group?:string, vp?:string}} [summaries]
|
|
564
564
|
* @returns {string}
|
|
@@ -936,12 +936,10 @@ export class Engine {
|
|
|
936
936
|
// memory/preflow.js) — per-turn scoped recall
|
|
937
937
|
// (b) AMS snapshot (resident summaries + onDemand FTS hits) appended
|
|
938
938
|
// below
|
|
939
|
-
// The legacy entries-aggregate `buildMemoryInjection` (index.md / by-project /
|
|
940
|
-
// by-topic / timeline) was retired in the H2-AMS rip.
|
|
941
939
|
let memoryInjection = '';
|
|
942
940
|
let recallEntryCount = 0;
|
|
943
941
|
|
|
944
|
-
//
|
|
942
|
+
// FTS5 recall: append per-turn scoped hits to memory injection
|
|
945
943
|
const recallResult = await this.#recallMemory(prompt, {
|
|
946
944
|
groupId,
|
|
947
945
|
vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
|
|
@@ -1,134 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* eval/cases/memory.js — Memory recall eval cases
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Scope + tag filtering
|
|
7
|
-
* - LLM selection (when >7 candidates)
|
|
8
|
-
* - Fingerprint caching
|
|
9
|
-
* - Memory injection into system prompt
|
|
4
|
+
* Smoke-level evals for the H2-AMS memory pipeline. Detailed mocks live
|
|
5
|
+
* in unit tests; this file only declares the eval scenarios.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import {
|
|
13
|
-
noError,
|
|
14
|
-
containsText,
|
|
15
|
-
custom,
|
|
16
|
-
} from '../runner.js';
|
|
17
|
-
|
|
18
|
-
// ─── Memory Recall Test Helpers ──────────────────────────────
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Create an engine with pre-loaded memory entries for eval.
|
|
22
|
-
* Uses a mock MemoryStore that returns predefined entries.
|
|
23
|
-
*/
|
|
24
|
-
function createMockMemoryStore(entries) {
|
|
25
|
-
return {
|
|
26
|
-
readProfile: () => 'User is a senior TypeScript developer who prefers functional programming.',
|
|
27
|
-
readEntry: (name) => entries.find(e => e.name === name) || null,
|
|
28
|
-
readSection: () => '',
|
|
29
|
-
listEntries: () => entries,
|
|
30
|
-
findByFilter: ({ scope, tags, limit = 15 }) => {
|
|
31
|
-
// Simple scoring: scope match + tag overlap
|
|
32
|
-
return entries
|
|
33
|
-
.map(e => {
|
|
34
|
-
let score = 0;
|
|
35
|
-
if (scope && e.scope === scope) score += 3;
|
|
36
|
-
if (scope && e.scope === 'global') score += 1;
|
|
37
|
-
if (tags) {
|
|
38
|
-
for (const t of tags) {
|
|
39
|
-
if (e.tags && e.tags.includes(t)) score += 1;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return { ...e, _score: score };
|
|
43
|
-
})
|
|
44
|
-
.filter(e => e._score > 0)
|
|
45
|
-
.sort((a, b) => b._score - a._score)
|
|
46
|
-
.slice(0, limit);
|
|
47
|
-
},
|
|
48
|
-
bumpFrequency: () => {},
|
|
49
|
-
search: (keyword) => entries.filter(e =>
|
|
50
|
-
e.content.toLowerCase().includes(keyword.toLowerCase()) ||
|
|
51
|
-
e.name.toLowerCase().includes(keyword.toLowerCase()),
|
|
52
|
-
),
|
|
53
|
-
stats: () => ({ entryCount: entries.length, scopes: [], kinds: {} }),
|
|
54
|
-
writeEntry: () => 'test-entry',
|
|
55
|
-
writeEntries: () => [],
|
|
56
|
-
deleteEntry: () => true,
|
|
57
|
-
rebuildScopes: () => {},
|
|
58
|
-
addToSection: () => {},
|
|
59
|
-
writeProfile: () => {},
|
|
60
|
-
clear: () => {},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const sampleMemoryEntries = [
|
|
65
|
-
{
|
|
66
|
-
name: 'typescript-strict-mode',
|
|
67
|
-
kind: 'preference',
|
|
68
|
-
scope: 'global',
|
|
69
|
-
tags: ['typescript', 'config', 'strict'],
|
|
70
|
-
importance: 'high',
|
|
71
|
-
frequency: 5,
|
|
72
|
-
content: 'User always uses TypeScript strict mode with noImplicitAny enabled.',
|
|
73
|
-
created_at: '2026-03-01T00:00:00Z',
|
|
74
|
-
updated_at: '2026-04-01T00:00:00Z',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'prefers-vitest',
|
|
78
|
-
kind: 'preference',
|
|
79
|
-
scope: 'work/claude-web-chat',
|
|
80
|
-
tags: ['testing', 'vitest', 'framework'],
|
|
81
|
-
importance: 'normal',
|
|
82
|
-
frequency: 3,
|
|
83
|
-
content: 'User prefers vitest over jest for testing. Uses vitest for all new projects.',
|
|
84
|
-
created_at: '2026-03-15T00:00:00Z',
|
|
85
|
-
updated_at: '2026-04-01T00:00:00Z',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'error-handling-pattern',
|
|
89
|
-
kind: 'lesson',
|
|
90
|
-
scope: 'global',
|
|
91
|
-
tags: ['error-handling', 'typescript', 'patterns'],
|
|
92
|
-
importance: 'high',
|
|
93
|
-
frequency: 4,
|
|
94
|
-
content: 'Always use Result<T, E> pattern instead of throwing exceptions. Wrap external API calls in try-catch and return Result.',
|
|
95
|
-
created_at: '2026-02-01T00:00:00Z',
|
|
96
|
-
updated_at: '2026-04-01T00:00:00Z',
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'project-structure',
|
|
100
|
-
kind: 'context',
|
|
101
|
-
scope: 'work/claude-web-chat',
|
|
102
|
-
tags: ['architecture', 'project', 'monorepo'],
|
|
103
|
-
importance: 'normal',
|
|
104
|
-
frequency: 2,
|
|
105
|
-
content: 'Project uses monorepo with agent/, server/, web/ directories. Agent code is in agent/unify/.',
|
|
106
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
107
|
-
updated_at: '2026-03-01T00:00:00Z',
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: 'functional-programming',
|
|
111
|
-
kind: 'preference',
|
|
112
|
-
scope: 'global',
|
|
113
|
-
tags: ['functional', 'programming', 'style'],
|
|
114
|
-
importance: 'normal',
|
|
115
|
-
frequency: 6,
|
|
116
|
-
content: 'User prefers functional programming: pure functions, immutable data, map/filter/reduce over loops.',
|
|
117
|
-
created_at: '2026-01-15T00:00:00Z',
|
|
118
|
-
updated_at: '2026-04-05T00:00:00Z',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'api-design-rest',
|
|
122
|
-
kind: 'skill',
|
|
123
|
-
scope: 'global',
|
|
124
|
-
tags: ['api', 'rest', 'design'],
|
|
125
|
-
importance: 'normal',
|
|
126
|
-
frequency: 1,
|
|
127
|
-
content: 'REST API conventions: use plural nouns, HTTP methods for CRUD, 2xx success, 4xx client error, 5xx server error.',
|
|
128
|
-
created_at: '2026-02-15T00:00:00Z',
|
|
129
|
-
updated_at: '2026-02-15T00:00:00Z',
|
|
130
|
-
},
|
|
131
|
-
];
|
|
8
|
+
import { noError, custom } from '../runner.js';
|
|
132
9
|
|
|
133
10
|
// ─── Eval Cases ──────────────────────────────────────────────
|
|
134
11
|
|
|
@@ -139,12 +16,8 @@ export const memoryCases = [
|
|
|
139
16
|
{
|
|
140
17
|
id: 'memory-profile-injection',
|
|
141
18
|
suite: 'memory',
|
|
142
|
-
description: 'System prompt should include
|
|
19
|
+
description: 'System prompt should include memory section after preflow',
|
|
143
20
|
prompt: 'Help me with a coding task',
|
|
144
|
-
setupEngine: (engine) => {
|
|
145
|
-
// We can't directly inject memoryStore here since Engine uses private fields
|
|
146
|
-
// Instead, this eval verifies via the adapter call log that system prompt contains memory
|
|
147
|
-
},
|
|
148
21
|
criteria: [
|
|
149
22
|
noError,
|
|
150
23
|
custom('has-response', 'Model produces a response', 5, (result) => ({
|
|
@@ -154,29 +27,23 @@ export const memoryCases = [
|
|
|
154
27
|
],
|
|
155
28
|
},
|
|
156
29
|
|
|
157
|
-
// ───
|
|
30
|
+
// ─── Recall Event ──────────────────────────────────────
|
|
158
31
|
|
|
159
32
|
{
|
|
160
33
|
id: 'memory-keyword-extraction',
|
|
161
34
|
suite: 'memory',
|
|
162
|
-
description: '
|
|
35
|
+
description: 'Recall event emitted when preflow has hits',
|
|
163
36
|
prompt: 'How should I handle TypeScript errors in my Express API?',
|
|
164
37
|
criteria: [
|
|
165
38
|
noError,
|
|
166
|
-
|
|
167
|
-
custom('recall-event', 'Recall event emitted (if memory store provided)', 3, (result) => {
|
|
168
|
-
// Without a real memory store this won't emit recall, so we check gracefully
|
|
39
|
+
custom('recall-event', 'Recall event emitted (if FTS index seeded)', 3, (result) => {
|
|
169
40
|
const recallEvent = result.events.find(e => e.type === 'recall');
|
|
170
41
|
return {
|
|
171
|
-
pass: true,
|
|
42
|
+
pass: true,
|
|
172
43
|
score: recallEvent ? 1 : 0.5,
|
|
173
|
-
reason: recallEvent ? `Recalled ${recallEvent.entryCount}
|
|
44
|
+
reason: recallEvent ? `Recalled ${recallEvent.entryCount} segments` : 'No FTS hits',
|
|
174
45
|
};
|
|
175
46
|
}),
|
|
176
47
|
],
|
|
177
48
|
},
|
|
178
49
|
];
|
|
179
|
-
|
|
180
|
-
// ─── Exported for direct import in unit tests ────────────────
|
|
181
|
-
|
|
182
|
-
export { createMockMemoryStore, sampleMemoryEntries };
|
package/unify/memory/ams.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* truth is `<scope>/memory.md` + `<scope>/summary.md`. AMS itself
|
|
12
12
|
* doesn't write to disk — that's Dream's job.
|
|
13
13
|
*
|
|
14
|
-
* Privacy
|
|
14
|
+
* Privacy: `vp/<other>` scopes are ALWAYS filtered out
|
|
15
15
|
* for any worker that isn't `<other>`. The owning code passes its own
|
|
16
16
|
* vpId at construction.
|
|
17
17
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* segment blocks) and the SQLite segment index. This layer handles
|
|
6
6
|
* scope <-> file path mapping; the index layer is scope-agnostic.
|
|
7
7
|
*
|
|
8
|
-
* Path conventions
|
|
8
|
+
* Path conventions:
|
|
9
9
|
* ~/.yeaft/memory/user/memory.md
|
|
10
10
|
* ~/.yeaft/memory/vp/<id>/memory.md
|
|
11
11
|
* ~/.yeaft/memory/group/<id>/memory.md
|
package/unify/memory/store-v2.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* memory/store-v2.js —
|
|
2
|
+
* memory/store-v2.js — per-scope memory.md + summary.md (Layer-A storage).
|
|
3
3
|
*
|
|
4
4
|
* One pair of files per scope. No shards, no entries/, no index.md, no
|
|
5
5
|
* index.json. The five scope kinds — user, vp, group, feature, topic — share
|
|
@@ -29,18 +29,15 @@
|
|
|
29
29
|
* ACL:
|
|
30
30
|
* - This module enforces ONE ACL: `vp/<other>` paths are blocked when
|
|
31
31
|
* `currentVpId` is given and differs from `<other>`. Every other scope
|
|
32
|
-
* boundary is ACL-free
|
|
32
|
+
* boundary is ACL-free.
|
|
33
33
|
*
|
|
34
34
|
* What this module deliberately does NOT do:
|
|
35
35
|
* - No frontmatter parsing. memory.md and summary.md are pure markdown;
|
|
36
36
|
* the dream-state metadata block lives at the file's tail and is read
|
|
37
37
|
* by `dream-v2/state.js`, not here.
|
|
38
38
|
* - No LLM calls, no extraction, no summarisation. Pure I/O.
|
|
39
|
-
* - No legacy R6 fallback. The old MemoryStore (memory/store.js) and
|
|
40
|
-
* ScopeTree (memory/scope-tree.js) remain in service until PR-E swaps
|
|
41
|
-
* callers; this module is additive.
|
|
42
39
|
*
|
|
43
|
-
* Reference: agent/unify/memory/DESIGN-
|
|
40
|
+
* Reference: agent/unify/memory/DESIGN-H2-AMS.md.
|
|
44
41
|
*/
|
|
45
42
|
|
|
46
43
|
import {
|
|
@@ -228,8 +225,8 @@ export async function writeMemory(scope, content, opts = {}) {
|
|
|
228
225
|
}
|
|
229
226
|
|
|
230
227
|
/**
|
|
231
|
-
* Append to a scope's memory.md. Used by the rare "direct write" path
|
|
232
|
-
*
|
|
228
|
+
* Append to a scope's memory.md. Used by the rare "direct write" path;
|
|
229
|
+
* main flow is dream-driven rewrites.
|
|
233
230
|
*
|
|
234
231
|
* Append is non-atomic with concurrent readers in the strict sense, but a
|
|
235
232
|
* single appendFile of a small buffer is atomic at the kernel level on POSIX
|
|
@@ -316,7 +313,7 @@ export async function ensureScope(scope, opts = {}) {
|
|
|
316
313
|
|
|
317
314
|
/**
|
|
318
315
|
* Enumerate all scopes present on disk. Returns Scope shapes that round-trip
|
|
319
|
-
* back through `scopeDir`. Used by Triage
|
|
316
|
+
* back through `scopeDir`. Used by Triage to list candidate
|
|
320
317
|
* scopes for a group's diff.
|
|
321
318
|
*
|
|
322
319
|
* Walks shallowly:
|
package/unify/prompts.js
CHANGED
|
@@ -8,39 +8,13 @@
|
|
|
8
8
|
* and used to enrich the system prompt beyond the hardcoded fallbacks.
|
|
9
9
|
*
|
|
10
10
|
* Phase 2 additions:
|
|
11
|
-
* - Memory section (
|
|
11
|
+
* - Memory section (recalled segments via H2-AMS pre-flow)
|
|
12
12
|
* - Compact summary section (conversation history summary)
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - Legacy `memory={profile,entries}` param still supported for callers
|
|
19
|
-
* (tests, CLI) that have not migrated.
|
|
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/.
|
|
14
|
+
* Memory injection (H2-AMS):
|
|
15
|
+
* - `memoryInjection` carries prebuilt FTS-recall text from
|
|
16
|
+
* `memory/preflow.js` over the relevant scopes (user/group/vp/feature/
|
|
17
|
+
* global). Engine passes it every turn after preflow runs.
|
|
44
18
|
*
|
|
45
19
|
* Reference: yeaft-unify-system-prompt-budget.md — Static + Dynamic + Context layers
|
|
46
20
|
*/
|
package/unify/session.js
CHANGED
|
@@ -282,7 +282,7 @@ export async function loadSession(options = {}) {
|
|
|
282
282
|
yeaftDir,
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
// ─── 9a. Create dream scheduler
|
|
285
|
+
// ─── 9a. Create dream scheduler ────────────
|
|
286
286
|
// The legacy R6 dream-scheduler was retired alongside recall-r6;
|
|
287
287
|
// dream-v2 is the only active path. The `memoryV2: false` opt-out
|
|
288
288
|
// no longer leaves a usable system, so we always wire v2 here.
|
|
@@ -60,6 +60,14 @@ Guidelines:
|
|
|
60
60
|
const signal = ctx?.signal;
|
|
61
61
|
const errors = [];
|
|
62
62
|
|
|
63
|
+
// Backend preference set in UnifySettings → Search tab. When the
|
|
64
|
+
// user picks `playwright` we'd normally call the playwright-service;
|
|
65
|
+
// that service is not yet shipped (next PR), so the preference is
|
|
66
|
+
// recorded for forward-compat and we transparently fall through to
|
|
67
|
+
// Tavily / HTML scrape. Once the service lands we'll insert a
|
|
68
|
+
// tryPlaywright backend here ahead of Tavily.
|
|
69
|
+
// Anything other than 'playwright' is treated as 'tavily'.
|
|
70
|
+
|
|
63
71
|
// 1. Tavily — default, fast, structured.
|
|
64
72
|
if (search.tavilyApiKey) {
|
|
65
73
|
const r = await tryTavily(query, limit, search.tavilyApiKey, signal);
|