@yeaft/webchat-agent 0.1.604 → 0.1.606
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dream-v2/refresh.js — DESIGN.md §9.14 refresh hook (Phase 8 PR-J).
|
|
3
|
+
*
|
|
4
|
+
* The PR-F wire-up plumbed `runDreamTick` into the scheduler with a v1
|
|
5
|
+
* no-op refresh placeholder. PR-J makes the refresh real but stays
|
|
6
|
+
* within the §8 v1 charter — "Thin: skip pruning/demotion; refresh-only
|
|
7
|
+
* in v1, no LLM".
|
|
8
|
+
*
|
|
9
|
+
* What "refresh" does today:
|
|
10
|
+
* 1. Read the scope's `index.md` rows (the entry catalog).
|
|
11
|
+
* 2. Select the top-N most recent rows by `updated` timestamp
|
|
12
|
+
* (default 12 — enough to surface the active context without
|
|
13
|
+
* exploding Layer A token cost).
|
|
14
|
+
* 3. Render a deterministic markdown synopsis (one bullet per row:
|
|
15
|
+
* `- <title> [<kind>; tags] (<updated>)`).
|
|
16
|
+
* 4. Write to the scope's `summary.md` via `writeSummary` (atomic
|
|
17
|
+
* rename — readers never see a partial write).
|
|
18
|
+
*
|
|
19
|
+
* If the index is empty (cold-start scope), we leave `summary.md`
|
|
20
|
+
* alone. Overwriting with an empty file would erase any human-written
|
|
21
|
+
* synopsis a future tool may have placed there.
|
|
22
|
+
*
|
|
23
|
+
* No LLM call. No pruning. No tombstones. The cursor is advanced by
|
|
24
|
+
* `runDreamTick` after this hook resolves, so a clean run of refresh
|
|
25
|
+
* counts as "scope handled" and we won't re-run until the diff-gate
|
|
26
|
+
* sees new content.
|
|
27
|
+
*
|
|
28
|
+
* Errors are propagated — `runDreamTick` already catches per-scope
|
|
29
|
+
* failures and records them under `errors[]` without aborting siblings.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readIndex, writeSummary } from '../memory/scope-tree.js';
|
|
33
|
+
|
|
34
|
+
/** Default number of recent entries surfaced on the synopsis. */
|
|
35
|
+
export const DEFAULT_TOP_N = 12;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render a deterministic markdown synopsis for a scope from its index rows.
|
|
39
|
+
* Pure function — exported for unit tests.
|
|
40
|
+
*
|
|
41
|
+
* @param {{ kind: string, id?: string, scopeDir: string }} scope
|
|
42
|
+
* @param {Array<{ path: string, title?: string, tags?: string[]|string, kind?: string, updated?: string }>} rows
|
|
43
|
+
* @param {number} [topN]
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function buildScopeSynopsis(scope, rows, topN = DEFAULT_TOP_N) {
|
|
47
|
+
if (!Array.isArray(rows) || rows.length === 0) return '';
|
|
48
|
+
|
|
49
|
+
// Sort by `updated` desc, then by path asc as a stable tiebreaker.
|
|
50
|
+
const sorted = [...rows].sort((a, b) => {
|
|
51
|
+
const u = (b.updated || '').localeCompare(a.updated || '');
|
|
52
|
+
return u !== 0 ? u : (a.path || '').localeCompare(b.path || '');
|
|
53
|
+
});
|
|
54
|
+
const top = sorted.slice(0, Math.max(1, topN | 0));
|
|
55
|
+
|
|
56
|
+
const header = `# ${scope.scopeDir} — recent context`;
|
|
57
|
+
const lines = top.map((r) => {
|
|
58
|
+
const title = (r.title || r.path || 'entry').trim();
|
|
59
|
+
const kind = r.kind ? `${r.kind}` : '';
|
|
60
|
+
const tagsRaw = Array.isArray(r.tags) ? r.tags : (typeof r.tags === 'string' ? r.tags.split(',') : []);
|
|
61
|
+
const tags = tagsRaw.map((t) => String(t).trim()).filter(Boolean);
|
|
62
|
+
const meta = [kind, ...tags].filter(Boolean).join('; ');
|
|
63
|
+
const metaPart = meta ? ` [${meta}]` : '';
|
|
64
|
+
const updated = r.updated ? ` (${r.updated})` : '';
|
|
65
|
+
return `- ${title}${metaPart}${updated}`;
|
|
66
|
+
});
|
|
67
|
+
return [header, '', ...lines, ''].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Translate a `runDreamTick` ScopeRef ({ kind, id?, scopeDir }) into a
|
|
72
|
+
* `scope-tree.js` Scope ({ kind, id }). The two surfaces share the
|
|
73
|
+
* `kind` enum but `runDreamTick` carries `scopeDir` as the canonical
|
|
74
|
+
* identifier; scope-tree derives it from `kind`+`id`.
|
|
75
|
+
*
|
|
76
|
+
* @param {{ kind: string, id?: string, scopeDir: string }} ref
|
|
77
|
+
* @returns {{ kind: string, id?: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function refToScope(ref) {
|
|
80
|
+
if (!ref || typeof ref !== 'object') {
|
|
81
|
+
throw new Error('refToScope: ref required');
|
|
82
|
+
}
|
|
83
|
+
if (ref.kind === 'user') return { kind: 'user' };
|
|
84
|
+
if (!ref.id) throw new Error(`refToScope: ${ref.kind} scope requires id`);
|
|
85
|
+
return { kind: ref.kind, id: ref.id };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a refresh hook bound to a specific memory root. The returned
|
|
90
|
+
* function is the `refresh` arg `runDreamTick` expects.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ root: string, topN?: number }} args
|
|
93
|
+
* @returns {(scope: { kind: string, id?: string, scopeDir: string }) => Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
export function createScopeRefreshHook({ root, topN = DEFAULT_TOP_N } = {}) {
|
|
96
|
+
if (!root || typeof root !== 'string') {
|
|
97
|
+
throw new Error('createScopeRefreshHook: root required');
|
|
98
|
+
}
|
|
99
|
+
return async function refreshScopeSummary(scope) {
|
|
100
|
+
const target = refToScope(scope);
|
|
101
|
+
const rows = await readIndex(target, { root });
|
|
102
|
+
const body = buildScopeSynopsis(scope, rows, topN);
|
|
103
|
+
if (!body) {
|
|
104
|
+
// Cold-start scope: leave summary.md alone (see header docs).
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await writeSummary(target, body, { root });
|
|
108
|
+
};
|
|
109
|
+
}
|
package/unify/engine.js
CHANGED
|
@@ -713,7 +713,7 @@ export class Engine {
|
|
|
713
713
|
* SCENARIO_EFFORT. Unknown values fall through to 'high'.
|
|
714
714
|
* @yields {EngineEvent}
|
|
715
715
|
*/
|
|
716
|
-
async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId } = {}) {
|
|
716
|
+
async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan } = {}) {
|
|
717
717
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
718
718
|
yield {
|
|
719
719
|
type: 'error',
|
|
@@ -764,7 +764,7 @@ export class Engine {
|
|
|
764
764
|
const runSignal = abortCtrl.signal;
|
|
765
765
|
|
|
766
766
|
try {
|
|
767
|
-
yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId });
|
|
767
|
+
yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan });
|
|
768
768
|
} finally {
|
|
769
769
|
if (signal) {
|
|
770
770
|
try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
|
|
@@ -782,7 +782,7 @@ export class Engine {
|
|
|
782
782
|
* in a try/finally without indenting the whole loop.
|
|
783
783
|
* @private
|
|
784
784
|
*/
|
|
785
|
-
async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId }) {
|
|
785
|
+
async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan }) {
|
|
786
786
|
|
|
787
787
|
// ─── Pre-query: Memory Injection (task-287) + Compact Summary ──
|
|
788
788
|
// Two-layer recall:
|
|
@@ -902,10 +902,19 @@ export class Engine {
|
|
|
902
902
|
if (vpPersona && vpPersona.vpId) {
|
|
903
903
|
const priorPlan = extractPriorPlan(conversationMessages, vpPersona.vpId);
|
|
904
904
|
const thinkingCfg = (this.#config && this.#config.thinking) || {};
|
|
905
|
+
// PR-I: live routerPlan.thinking — when the dispatcher passes
|
|
906
|
+
// `vpPlan` for this turn (per-VP plan from the V2 router) and its
|
|
907
|
+
// `vpId` matches the active persona, surface its `thinking` field
|
|
908
|
+
// to resolveThinking. Mismatched vpId means the plan addresses a
|
|
909
|
+
// different VP — ignore it for this VP's thinking decision.
|
|
910
|
+
const liveRouterThinking = (vpPlan && typeof vpPlan === 'object'
|
|
911
|
+
&& typeof vpPlan.vpId === 'string' && vpPlan.vpId === vpPersona.vpId
|
|
912
|
+
&& (vpPlan.thinking === 'high' || vpPlan.thinking === 'max'))
|
|
913
|
+
? vpPlan.thinking
|
|
914
|
+
: null;
|
|
905
915
|
const resolved = resolveThinking({
|
|
906
916
|
uiOverride: (userEffort === 'max' || userEffort === 'high') ? userEffort : null,
|
|
907
|
-
routerPlan:
|
|
908
|
-
// live router-plan thinking is a follow-up.
|
|
917
|
+
routerPlan: liveRouterThinking,
|
|
909
918
|
priorPlan: priorPlan && priorPlan.thinking ? priorPlan.thinking : null,
|
|
910
919
|
vpDefault: typeof vpPersona.thinking === 'string' ? vpPersona.thinking : null,
|
|
911
920
|
globalDefault: typeof thinkingCfg.default === 'string' ? thinkingCfg.default : null,
|
|
@@ -1106,12 +1115,29 @@ export class Engine {
|
|
|
1106
1115
|
// assistant message that produced it. Stripped at the wire by
|
|
1107
1116
|
// stripMetaForWire — pure bookkeeping for priorPlan continuity.
|
|
1108
1117
|
if (vpPersona && vpPersona.vpId) {
|
|
1118
|
+
// PR-I: when the dispatcher hands us a per-VP plan whose vpId matches
|
|
1119
|
+
// the active persona, persist its `forwardQuery`, `preselect`, and
|
|
1120
|
+
// `thinking` on the assistant message so the next turn's
|
|
1121
|
+
// priorPlan continuity (DESIGN.md §9.15) sees the live router's
|
|
1122
|
+
// decision — not a synthetic stub.
|
|
1123
|
+
const planForThisVp = (vpPlan && typeof vpPlan === 'object'
|
|
1124
|
+
&& typeof vpPlan.vpId === 'string' && vpPlan.vpId === vpPersona.vpId)
|
|
1125
|
+
? vpPlan
|
|
1126
|
+
: null;
|
|
1109
1127
|
attachRouterPlan(assistantMsg, {
|
|
1110
1128
|
vpId: vpPersona.vpId,
|
|
1111
|
-
forwardQuery:
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1129
|
+
forwardQuery: planForThisVp && planForThisVp.forwardQuery
|
|
1130
|
+
? planForThisVp.forwardQuery
|
|
1131
|
+
: { userOriginal: prompt || '', intent: '' },
|
|
1132
|
+
preselect: planForThisVp && planForThisVp.preselect
|
|
1133
|
+
? planForThisVp.preselect
|
|
1134
|
+
: undefined,
|
|
1135
|
+
thinking: planForThisVp && (planForThisVp.thinking === 'high' || planForThisVp.thinking === 'max')
|
|
1136
|
+
? planForThisVp.thinking
|
|
1137
|
+
: null,
|
|
1138
|
+
thinkingReason: planForThisVp && typeof planForThisVp.thinkingReason === 'string'
|
|
1139
|
+
? planForThisVp.thinkingReason
|
|
1140
|
+
: '',
|
|
1115
1141
|
});
|
|
1116
1142
|
}
|
|
1117
1143
|
conversationMessages.push(assistantMsg);
|
|
@@ -25,6 +25,7 @@ import { dreamShard } from './dream-shard.js';
|
|
|
25
25
|
import { checkRecompression } from './recompression.js';
|
|
26
26
|
import { runUserDreamJob } from './user-memory-store.js';
|
|
27
27
|
import { runDreamTick } from '../dream-v2/tick.js';
|
|
28
|
+
import { createScopeRefreshHook } from '../dream-v2/refresh.js';
|
|
28
29
|
|
|
29
30
|
/** Default idle timeout before dream triggers (ms). */
|
|
30
31
|
export const DREAM_IDLE_MS = 30 * 60 * 1000; // 30 min
|
|
@@ -169,16 +170,14 @@ export function createDreamScheduler(opts = {}) {
|
|
|
169
170
|
try {
|
|
170
171
|
const scopes = [{ kind: 'user', scopeDir: 'user' }];
|
|
171
172
|
if (group?.id) scopes.push({ kind: 'group', id: group.id, scopeDir: `groups/${group.id}` });
|
|
173
|
+
// PR-J: real refresh hook — read each scope's `index.md`,
|
|
174
|
+
// render a deterministic top-N synopsis, atomically write
|
|
175
|
+
// `summary.md`. No LLM, refresh-only (DESIGN.md §8 line 395).
|
|
176
|
+
const refresh = createScopeRefreshHook({ root: memoryDir });
|
|
172
177
|
const tickResult = await runDreamTick({
|
|
173
178
|
root: memoryDir,
|
|
174
179
|
scopes,
|
|
175
|
-
refresh
|
|
176
|
-
// v1 refresh hook: no-op placeholder. The actual scope
|
|
177
|
-
// summary refresh continues to flow through the legacy
|
|
178
|
-
// shard / user-dream paths above; this tick only
|
|
179
|
-
// exercises the diff-gate + cursor write so future hook
|
|
180
|
-
// implementations can plug in without re-wiring.
|
|
181
|
-
},
|
|
180
|
+
refresh,
|
|
182
181
|
});
|
|
183
182
|
result.dreamV2Tick = {
|
|
184
183
|
ran: tickResult.ran.length,
|
|
@@ -97,7 +97,7 @@ export class EngineInstance {
|
|
|
97
97
|
* @param {AbortSignal} [params.signal]
|
|
98
98
|
* @yields {object} EngineEvent with { ...event, threadId }
|
|
99
99
|
*/
|
|
100
|
-
async *query({ prompt, mode, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId } = {}) {
|
|
100
|
+
async *query({ prompt, mode, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan } = {}) {
|
|
101
101
|
if (this.#terminated) {
|
|
102
102
|
yield {
|
|
103
103
|
type: 'error',
|
|
@@ -173,7 +173,7 @@ export class EngineInstance {
|
|
|
173
173
|
curToolResults = [];
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId })) {
|
|
176
|
+
for await (const event of this.#engine.query({ prompt, mode, messages: snapshot, signal, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan })) {
|
|
177
177
|
// Re-tag every event with the bound threadId. Non-object events
|
|
178
178
|
// (shouldn't happen — all engine events are objects) are passed
|
|
179
179
|
// through untouched.
|