@yeaft/webchat-agent 1.0.289 → 1.0.292
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +94 -90
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +213 -0
- package/yeaft/cli.js +75 -13
- package/yeaft/engine.js +64 -17
- package/yeaft/memory/preflow.js +8 -2
- package/yeaft/sessions/pre-flow.js +5 -0
- package/yeaft/stdio-protocol.js +164 -0
- package/yeaft/web-bridge.js +1 -0
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Engine } from './engine.js';
|
|
4
|
+
import { createRouter } from './routing/router.js';
|
|
5
|
+
import { createCoordinator } from './sessions/coordinator.js';
|
|
6
|
+
import { sessionsRoot } from './sessions/session-crud.js';
|
|
7
|
+
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
8
|
+
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
9
|
+
import { readVp } from './vp/vp-crud.js';
|
|
10
|
+
import { COLLAB_TOOL_POLICY } from './tools/registry.js';
|
|
11
|
+
|
|
12
|
+
function buildVpPersona(vpId, loaded) {
|
|
13
|
+
const vp = readVp(vpId, { libDir: join(loaded.yeaftDir, 'virtual-persons') });
|
|
14
|
+
if (!vp) return null;
|
|
15
|
+
return {
|
|
16
|
+
vpId,
|
|
17
|
+
displayName: vp.displayName || vpId,
|
|
18
|
+
displayNameZh: vp.displayNameZh || '',
|
|
19
|
+
role: vp.role || '',
|
|
20
|
+
roleZh: vp.roleZh || '',
|
|
21
|
+
persona: vp.persona || '',
|
|
22
|
+
planInstruction: vp.planInstruction || '',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createEngine(loaded, sessionId, vpId) {
|
|
27
|
+
const effectiveConfig = resolveSessionConfig(
|
|
28
|
+
loaded.config,
|
|
29
|
+
loadSessionConfig(loaded.yeaftDir, sessionId),
|
|
30
|
+
);
|
|
31
|
+
return new Engine({
|
|
32
|
+
adapter: loaded.adapter,
|
|
33
|
+
trace: loaded.trace,
|
|
34
|
+
config: effectiveConfig,
|
|
35
|
+
conversationStore: loaded.conversationStore,
|
|
36
|
+
memoryIndex: loaded.memoryIndex || null,
|
|
37
|
+
amsRegistry: loaded.amsRegistry || null,
|
|
38
|
+
toolRegistry: loaded.toolRegistry,
|
|
39
|
+
skillManager: loaded.skillManager,
|
|
40
|
+
mcpManager: loaded.mcpManager,
|
|
41
|
+
yeaftDir: loaded.yeaftDir,
|
|
42
|
+
toolStats: loaded.toolStats || null,
|
|
43
|
+
taskManager: loaded.taskManager || null,
|
|
44
|
+
sessionId,
|
|
45
|
+
vpId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open a transport-neutral multi-VP runtime for an existing Yeaft Session.
|
|
51
|
+
* Different VPs run concurrently; each VP owns one serial promise tail and
|
|
52
|
+
* one Engine, matching the WebSocket runtime's state-isolation rule.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when session metadata does not exist so legacy one-engine CLI
|
|
55
|
+
* invocations can keep their existing behavior.
|
|
56
|
+
*/
|
|
57
|
+
export function createCliSessionRunner({
|
|
58
|
+
loaded,
|
|
59
|
+
sessionId,
|
|
60
|
+
workDir = process.cwd(),
|
|
61
|
+
engineFactory = createEngine,
|
|
62
|
+
personaFactory = buildVpPersona,
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (!loaded || !sessionId) return null;
|
|
65
|
+
const sessionDir = join(sessionsRoot(loaded.yeaftDir), sessionId);
|
|
66
|
+
if (!loadSessionMeta(sessionDir)) return null;
|
|
67
|
+
|
|
68
|
+
const handle = openSession(sessionsRoot(loaded.yeaftDir), sessionId);
|
|
69
|
+
const engines = new Map();
|
|
70
|
+
const tails = new Map();
|
|
71
|
+
const pending = new Set();
|
|
72
|
+
let closed = false;
|
|
73
|
+
|
|
74
|
+
const engineFor = (vpId) => {
|
|
75
|
+
let engine = engines.get(vpId);
|
|
76
|
+
if (!engine) {
|
|
77
|
+
engine = engineFactory(loaded, sessionId, vpId);
|
|
78
|
+
engines.set(vpId, engine);
|
|
79
|
+
}
|
|
80
|
+
return engine;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let coordinator;
|
|
84
|
+
|
|
85
|
+
const runEnvelope = async (vpId, envelope, options) => {
|
|
86
|
+
const meta = handle.getMeta();
|
|
87
|
+
const engine = engineFor(vpId);
|
|
88
|
+
const promptText = envelope?.msg?.text || '';
|
|
89
|
+
const prompt = `@vp-${vpId} ${promptText}`;
|
|
90
|
+
const messages = loaded.conversationStore.loadSessionHistoryForVp(sessionId, vpId);
|
|
91
|
+
const todos = [];
|
|
92
|
+
let resultText = '';
|
|
93
|
+
let failed = null;
|
|
94
|
+
const scopedCoordinator = {
|
|
95
|
+
group: coordinator.group,
|
|
96
|
+
ingest(input, opts) {
|
|
97
|
+
return coordinator.ingest({ ...input, _cliTurnContext: envelope._cliTurnContext }, opts);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const queryOptions = {
|
|
101
|
+
prompt,
|
|
102
|
+
messages,
|
|
103
|
+
sessionId,
|
|
104
|
+
workDir: meta.workDir || workDir,
|
|
105
|
+
senderVpId: vpId,
|
|
106
|
+
sessionMembers: meta.roster.slice(),
|
|
107
|
+
sessionAnnouncement: meta.announcement || '',
|
|
108
|
+
vpPersona: personaFactory(vpId, loaded),
|
|
109
|
+
router: createRouter({ coordinator: scopedCoordinator }),
|
|
110
|
+
inboundEnvelope: envelope,
|
|
111
|
+
userAlreadyPersisted: true,
|
|
112
|
+
threadId: 'main',
|
|
113
|
+
vpTurnId: envelope?.msg?.id || randomUUID(),
|
|
114
|
+
collabToolPolicy: meta.roster.length > 1
|
|
115
|
+
? COLLAB_TOOL_POLICY.MULTI_VP
|
|
116
|
+
: COLLAB_TOOL_POLICY.SINGLE_VP,
|
|
117
|
+
getCurrentTodos: () => todos.slice(),
|
|
118
|
+
setCurrentTodos: (next) => {
|
|
119
|
+
todos.splice(0, todos.length, ...(Array.isArray(next) ? next : []));
|
|
120
|
+
},
|
|
121
|
+
askUser: options.askUser
|
|
122
|
+
? request => options.askUser(request, vpId, queryOptions.vpTurnId)
|
|
123
|
+
: null,
|
|
124
|
+
userEffort: options.modelEffort || null,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
for await (const event of engine.query(queryOptions)) {
|
|
129
|
+
if (event.type === 'text_delta') resultText += event.text || '';
|
|
130
|
+
await options.onEvent?.({ vpId, event, sessionId, turnId: queryOptions.vpTurnId });
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
failed = error;
|
|
134
|
+
await options.onEvent?.({
|
|
135
|
+
vpId,
|
|
136
|
+
sessionId,
|
|
137
|
+
turnId: queryOptions.vpTurnId,
|
|
138
|
+
event: { type: 'error', error, retryable: false },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { vpId, result: resultText, error: failed };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const enqueue = (vpId, envelope) => {
|
|
145
|
+
if (closed) throw new Error('CLI Session runner is closed');
|
|
146
|
+
const turnContext = envelope?._cliTurnContext;
|
|
147
|
+
if (!turnContext) throw new Error('CLI Session envelope is missing its turn context');
|
|
148
|
+
const previous = tails.get(vpId) || Promise.resolve();
|
|
149
|
+
const task = previous.catch(() => {}).then(() => runEnvelope(vpId, envelope, turnContext.options));
|
|
150
|
+
tails.set(vpId, task);
|
|
151
|
+
pending.add(task);
|
|
152
|
+
turnContext.pending.add(task);
|
|
153
|
+
task.finally(() => {
|
|
154
|
+
pending.delete(task);
|
|
155
|
+
turnContext.pending.delete(task);
|
|
156
|
+
if (tails.get(vpId) === task) tails.delete(vpId);
|
|
157
|
+
}).catch(() => {});
|
|
158
|
+
return task;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
coordinator = createCoordinator(handle, { deliver: enqueue });
|
|
162
|
+
|
|
163
|
+
async function drain(tasks = pending) {
|
|
164
|
+
const results = [];
|
|
165
|
+
while (tasks.size > 0) {
|
|
166
|
+
const batch = Array.from(tasks);
|
|
167
|
+
results.push(...await Promise.all(batch));
|
|
168
|
+
await Promise.resolve();
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
sessionId,
|
|
175
|
+
get meta() { return handle.getMeta(); },
|
|
176
|
+
async run(prompt, options = {}) {
|
|
177
|
+
if (closed) throw new Error('CLI Session runner is closed');
|
|
178
|
+
const turnContext = Object.freeze({ options: Object.freeze({ ...options }), pending: new Set() });
|
|
179
|
+
const messageId = randomUUID();
|
|
180
|
+
// The shared user row is the durability boundary. Every VP Engine skips
|
|
181
|
+
// its own user append, preventing @all from duplicating the prompt N times.
|
|
182
|
+
loaded.conversationStore.append({
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: prompt,
|
|
185
|
+
sessionId,
|
|
186
|
+
threadId: 'main',
|
|
187
|
+
clientMessageId: messageId,
|
|
188
|
+
userAuthored: true,
|
|
189
|
+
});
|
|
190
|
+
const report = coordinator.ingest({
|
|
191
|
+
id: messageId,
|
|
192
|
+
from: 'user',
|
|
193
|
+
role: 'user',
|
|
194
|
+
text: prompt,
|
|
195
|
+
_cliTurnContext: turnContext,
|
|
196
|
+
});
|
|
197
|
+
const results = await drain(turnContext.pending);
|
|
198
|
+
return { report, results };
|
|
199
|
+
},
|
|
200
|
+
abort(reason = 'user') {
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const engine of engines.values()) {
|
|
203
|
+
if (engine.abort?.(reason)) count += 1;
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
},
|
|
207
|
+
async close() {
|
|
208
|
+
closed = true;
|
|
209
|
+
await drain();
|
|
210
|
+
handle.close();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
package/yeaft/cli.js
CHANGED
|
@@ -38,7 +38,8 @@ import { ConversationStore } from './conversation/persist.js';
|
|
|
38
38
|
import { snapshotSessions } from './sessions/session-crud.js';
|
|
39
39
|
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
40
40
|
import { validateSessionId } from './sessions/ids.js';
|
|
41
|
-
import { createJsonlWriter, JsonlInput, runStreamTurn } from './stdio-protocol.js';
|
|
41
|
+
import { createJsonlWriter, JsonlInput, runStreamTurn, runStreamSessionTurn } from './stdio-protocol.js';
|
|
42
|
+
import { createCliSessionRunner } from './cli-session-runner.js';
|
|
42
43
|
|
|
43
44
|
// ─── Argument parsing ──────────────────────────────────────────
|
|
44
45
|
|
|
@@ -333,16 +334,28 @@ function handleDeleteGroup(config, sessionId) {
|
|
|
333
334
|
// ─── REPL ──────────────────────────────────────────────────────
|
|
334
335
|
|
|
335
336
|
async function runREPL(config, args) {
|
|
336
|
-
|
|
337
|
+
const workDir = resolve(args.workDir || process.cwd());
|
|
338
|
+
const persisted = args.sessionId ? loadSessionConfig(config.dir, args.sessionId) : {};
|
|
339
|
+
const effectiveConfig = resolveSessionConfig(config, persisted);
|
|
337
340
|
const session = await loadSession({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
dir: config.dir,
|
|
342
|
+
workDir,
|
|
343
|
+
model: args.model || effectiveConfig.model,
|
|
344
|
+
language: args.language || effectiveConfig.language,
|
|
345
|
+
debug: args.debug || effectiveConfig.debug,
|
|
341
346
|
skipMCP: args.skipMCP,
|
|
342
347
|
skipSkills: args.skipSkills,
|
|
348
|
+
configOverrides: effectiveConfig,
|
|
343
349
|
});
|
|
344
350
|
|
|
345
351
|
const { engine, conversationStore, trace, skillManager, mcpManager, toolRegistry } = session;
|
|
352
|
+
const sessionRunner = args.sessionId
|
|
353
|
+
? createCliSessionRunner({ loaded: session, sessionId: args.sessionId, workDir })
|
|
354
|
+
: null;
|
|
355
|
+
if (args.sessionId && !sessionRunner) {
|
|
356
|
+
await session.shutdown();
|
|
357
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
358
|
+
}
|
|
346
359
|
|
|
347
360
|
// Load persisted conversation as initial messages. `loadRecent` is now
|
|
348
361
|
// turn-based (one user round-trip = one turn; multi-VP fan-out collapses
|
|
@@ -655,9 +668,28 @@ async function runREPL(config, args) {
|
|
|
655
668
|
return;
|
|
656
669
|
}
|
|
657
670
|
|
|
658
|
-
// Regular input →
|
|
671
|
+
// Regular input → Session fan-out or legacy single Engine.
|
|
659
672
|
try {
|
|
660
673
|
let responseText = '';
|
|
674
|
+
if (sessionRunner) {
|
|
675
|
+
const outcome = await sessionRunner.run(input, {
|
|
676
|
+
modelEffort: args.modelEffort || session.config.modelEffort || null,
|
|
677
|
+
onEvent: ({ vpId, event }) => {
|
|
678
|
+
if (event.type === 'text_delta') {
|
|
679
|
+
responseText += event.text || '';
|
|
680
|
+
process.stdout.write(`\n[${vpId}] ${event.text || ''}`);
|
|
681
|
+
} else if (event.type === 'tool_start') {
|
|
682
|
+
process.stderr.write(`\n[${vpId}] ${event.name}...\n`);
|
|
683
|
+
} else if (event.type === 'error') {
|
|
684
|
+
process.stderr.write(`\n[${vpId}] Error: ${event.error?.message || event.error}\n`);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
console.log();
|
|
689
|
+
if (outcome.results.some(result => result.error)) process.exitCode = 1;
|
|
690
|
+
rl.prompt();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
661
693
|
|
|
662
694
|
for await (const event of engine.query({
|
|
663
695
|
prompt: input,
|
|
@@ -721,6 +753,7 @@ async function runREPL(config, args) {
|
|
|
721
753
|
});
|
|
722
754
|
|
|
723
755
|
rl.on('close', async () => {
|
|
756
|
+
await sessionRunner?.close();
|
|
724
757
|
await session.shutdown();
|
|
725
758
|
console.log('\nBye!');
|
|
726
759
|
process.exit(0);
|
|
@@ -766,6 +799,7 @@ async function runStreamJson(config, args) {
|
|
|
766
799
|
},
|
|
767
800
|
});
|
|
768
801
|
const { engine, conversationStore, skillManager, toolRegistry } = loaded;
|
|
802
|
+
const sessionRunner = createCliSessionRunner({ loaded, sessionId, workDir });
|
|
769
803
|
const todoState = { value: [] };
|
|
770
804
|
|
|
771
805
|
write({
|
|
@@ -788,8 +822,7 @@ async function runStreamJson(config, args) {
|
|
|
788
822
|
...(message.toolCallId && { toolCallId: message.toolCallId }),
|
|
789
823
|
...(message.toolCalls && { toolCalls: message.toolCalls }),
|
|
790
824
|
}));
|
|
791
|
-
|
|
792
|
-
engine,
|
|
825
|
+
const turnOptions = {
|
|
793
826
|
prompt,
|
|
794
827
|
messages: priorMessages,
|
|
795
828
|
sessionId,
|
|
@@ -800,7 +833,10 @@ async function runStreamJson(config, args) {
|
|
|
800
833
|
write,
|
|
801
834
|
getCurrentTodos: () => todoState.value.slice(),
|
|
802
835
|
setCurrentTodos: todos => { todoState.value = Array.isArray(todos) ? todos.slice() : []; },
|
|
803
|
-
}
|
|
836
|
+
};
|
|
837
|
+
return sessionRunner
|
|
838
|
+
? runStreamSessionTurn({ runner: sessionRunner, ...turnOptions })
|
|
839
|
+
: runStreamTurn({ engine, ...turnOptions });
|
|
804
840
|
};
|
|
805
841
|
|
|
806
842
|
let lastResult = null;
|
|
@@ -820,6 +856,7 @@ async function runStreamJson(config, args) {
|
|
|
820
856
|
}
|
|
821
857
|
} finally {
|
|
822
858
|
input?.close();
|
|
859
|
+
await sessionRunner?.close();
|
|
823
860
|
await loaded.shutdown();
|
|
824
861
|
console.log = originalConsole.log;
|
|
825
862
|
console.info = originalConsole.info;
|
|
@@ -836,18 +873,42 @@ async function runOnce(config, args) {
|
|
|
836
873
|
return;
|
|
837
874
|
}
|
|
838
875
|
|
|
839
|
-
|
|
876
|
+
const workDir = resolve(args.workDir || process.cwd());
|
|
877
|
+
const persisted = args.sessionId ? loadSessionConfig(config.dir, args.sessionId) : {};
|
|
878
|
+
const effectiveConfig = resolveSessionConfig(config, persisted);
|
|
840
879
|
const session = await loadSession({
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
880
|
+
dir: config.dir,
|
|
881
|
+
workDir,
|
|
882
|
+
model: args.model || effectiveConfig.model,
|
|
883
|
+
language: args.language || effectiveConfig.language,
|
|
884
|
+
debug: args.debug || effectiveConfig.debug,
|
|
844
885
|
skipMCP: args.skipMCP,
|
|
845
886
|
skipSkills: args.skipSkills,
|
|
887
|
+
configOverrides: effectiveConfig,
|
|
846
888
|
});
|
|
847
889
|
|
|
848
890
|
const { engine, conversationStore } = session;
|
|
891
|
+
const sessionRunner = args.sessionId
|
|
892
|
+
? createCliSessionRunner({ loaded: session, sessionId: args.sessionId, workDir })
|
|
893
|
+
: null;
|
|
849
894
|
|
|
850
895
|
try {
|
|
896
|
+
if (args.sessionId && !sessionRunner) {
|
|
897
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
898
|
+
}
|
|
899
|
+
if (sessionRunner) {
|
|
900
|
+
const outcome = await sessionRunner.run(args.prompt, {
|
|
901
|
+
modelEffort: args.modelEffort || session.config.modelEffort || null,
|
|
902
|
+
onEvent: ({ vpId, event }) => {
|
|
903
|
+
if (event.type === 'text_delta') process.stdout.write(`\n[${vpId}] ${event.text || ''}`);
|
|
904
|
+
else if (event.type === 'tool_start') process.stderr.write(`\n[${vpId}] ${event.name}...\n`);
|
|
905
|
+
else if (event.type === 'error') process.stderr.write(`\n[${vpId}] Error: ${event.error?.message || event.error}\n`);
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
console.log();
|
|
909
|
+
if (outcome.results.some(result => result.error)) process.exitCode = 1;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
851
912
|
// Load recent conversation as context
|
|
852
913
|
const priorMessages = conversationStore.loadRecent(20).map(m => ({
|
|
853
914
|
role: m.role,
|
|
@@ -903,6 +964,7 @@ async function runOnce(config, args) {
|
|
|
903
964
|
// Final newline after streaming text
|
|
904
965
|
console.log();
|
|
905
966
|
} finally {
|
|
967
|
+
await sessionRunner?.close();
|
|
906
968
|
await session.shutdown();
|
|
907
969
|
}
|
|
908
970
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -84,6 +84,8 @@ const AMS_ADJUST_TIMEOUT_MS = 30_000;
|
|
|
84
84
|
/** Maximum silence while a visible turn waits for a result-producing task. */
|
|
85
85
|
const DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS = 120_000;
|
|
86
86
|
|
|
87
|
+
const DEFAULT_MEMORY_RECALL_LIMIT = 8;
|
|
88
|
+
|
|
87
89
|
// ─── LLM retry policy defaults ──────────────────────────────────
|
|
88
90
|
// Hard-coded floor / ceiling for retry behaviour. The engine reads the
|
|
89
91
|
// effective policy from `config.llmRetry` so users can dial these via
|
|
@@ -442,6 +444,47 @@ function isZhRuntimeLanguage(language) {
|
|
|
442
444
|
return String(language || '').toLowerCase().startsWith('zh');
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
function resolveMemoryRecallLimit(config) {
|
|
448
|
+
const raw = config?.memoryRecallLimit ?? config?.dreamMemoryRecallLimit;
|
|
449
|
+
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MEMORY_RECALL_LIMIT;
|
|
450
|
+
return Math.max(1, Math.floor(raw));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function loadedMemoryDebugEntries(snapshot) {
|
|
454
|
+
const snap = snapshot || {};
|
|
455
|
+
return [
|
|
456
|
+
...loadedResidentDebugEntries(snap.resident || []),
|
|
457
|
+
...loadedSegmentDebugEntries(snap.recent || [], 'recent'),
|
|
458
|
+
...loadedSegmentDebugEntries(snap.onDemand || [], 'onDemand'),
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function loadedResidentDebugEntries(entries) {
|
|
463
|
+
return (entries || []).map((entry, index) => ({
|
|
464
|
+
id: `resident:${entry.scope || index}`,
|
|
465
|
+
layer: 'resident',
|
|
466
|
+
scope: entry.scope || null,
|
|
467
|
+
label: memoryScopeLabel(entry.scope || ''),
|
|
468
|
+
kind: 'summary',
|
|
469
|
+
score: null,
|
|
470
|
+
tags: [],
|
|
471
|
+
body: entry.summary || '',
|
|
472
|
+
})).filter(entry => entry.body);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function loadedSegmentDebugEntries(segments, layer) {
|
|
476
|
+
return (segments || []).map((seg, index) => ({
|
|
477
|
+
id: seg.id || `${layer}:${index}`,
|
|
478
|
+
layer,
|
|
479
|
+
scope: seg.scope || null,
|
|
480
|
+
label: memoryScopeLabel(seg.scope || ''),
|
|
481
|
+
kind: seg.kind || null,
|
|
482
|
+
score: typeof seg.score === 'number' ? seg.score : null,
|
|
483
|
+
tags: Array.isArray(seg.tags) ? seg.tags : [],
|
|
484
|
+
body: seg.body || '',
|
|
485
|
+
})).filter(entry => entry.body);
|
|
486
|
+
}
|
|
487
|
+
|
|
445
488
|
export class Engine {
|
|
446
489
|
/** @type {import('./llm/adapter.js').LLMAdapter} */
|
|
447
490
|
#adapter;
|
|
@@ -906,6 +949,7 @@ export class Engine {
|
|
|
906
949
|
* ownVpId: string|null,
|
|
907
950
|
* scopes: string[],
|
|
908
951
|
* snapshotBlock: string,
|
|
952
|
+
* snapshot: import('./memory/ams.js').AmsSnapshot,
|
|
909
953
|
* residentEntries: Array<{scope:string, summary:string}>,
|
|
910
954
|
* } | null}
|
|
911
955
|
*/
|
|
@@ -938,14 +982,15 @@ export class Engine {
|
|
|
938
982
|
ams.setOnDemand(segs);
|
|
939
983
|
|
|
940
984
|
// (c) Snapshot — render the AMS layers as a single prompt block.
|
|
941
|
-
const
|
|
985
|
+
const snapshot = ams.snapshot({ userMsg: args.userMsg || '' });
|
|
986
|
+
const snapshotBlock = this.#renderAmsSnapshot(snapshot, this.#config.language || 'en');
|
|
942
987
|
|
|
943
988
|
const scopes = buildRelevantScopes({
|
|
944
989
|
sessionId: args.sessionId,
|
|
945
990
|
vpId: ownVpId,
|
|
946
991
|
});
|
|
947
992
|
|
|
948
|
-
return { ams, sessionKey, ownVpId, scopes, snapshotBlock, residentEntries };
|
|
993
|
+
return { ams, sessionKey, ownVpId, scopes, snapshotBlock, snapshot, residentEntries };
|
|
949
994
|
}
|
|
950
995
|
|
|
951
996
|
/**
|
|
@@ -953,13 +998,11 @@ export class Engine {
|
|
|
953
998
|
* injection. Mirrors the heading style of the existing memory blocks
|
|
954
999
|
* so the LLM sees a consistent layout.
|
|
955
1000
|
*
|
|
956
|
-
* @param {import('./memory/ams.js').
|
|
1001
|
+
* @param {import('./memory/ams.js').AmsSnapshot} snap
|
|
957
1002
|
* @param {string} [language]
|
|
958
|
-
* @param {string} [userMsg]
|
|
959
1003
|
* @returns {string}
|
|
960
1004
|
*/
|
|
961
|
-
#renderAmsSnapshot(
|
|
962
|
-
const snap = ams.snapshot({ userMsg });
|
|
1005
|
+
#renderAmsSnapshot(snap, language = 'en') {
|
|
963
1006
|
if (!snap) return '';
|
|
964
1007
|
const parts = [];
|
|
965
1008
|
if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
|
|
@@ -1342,7 +1385,7 @@ export class Engine {
|
|
|
1342
1385
|
* @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
|
|
1343
1386
|
*/
|
|
1344
1387
|
async #recallMemory(prompt, ctx = {}) {
|
|
1345
|
-
const memory = { profile: '', entries: [], formatted: '' };
|
|
1388
|
+
const memory = { profile: '', entries: [], formatted: '', meta: {} };
|
|
1346
1389
|
if (!this.#memoryIndex) return memory;
|
|
1347
1390
|
try {
|
|
1348
1391
|
const result = runMemoryPreflow(this.#memoryIndex, {
|
|
@@ -1351,11 +1394,13 @@ export class Engine {
|
|
|
1351
1394
|
chatId: ctx.chatId || this.#chatId,
|
|
1352
1395
|
vpId: ctx.vpId,
|
|
1353
1396
|
extraScopes: ctx.extraScopes,
|
|
1397
|
+
pickLimit: resolveMemoryRecallLimit(this.#config),
|
|
1354
1398
|
fallbackOnEmpty: true,
|
|
1355
1399
|
});
|
|
1356
1400
|
memory.profile = result.profile || '';
|
|
1357
1401
|
memory.entries = result.entries || [];
|
|
1358
1402
|
memory.formatted = result.formatted || '';
|
|
1403
|
+
memory.meta = result.meta || {};
|
|
1359
1404
|
} catch {
|
|
1360
1405
|
// Fail soft — empty injection.
|
|
1361
1406
|
}
|
|
@@ -2029,6 +2074,7 @@ export class Engine {
|
|
|
2029
2074
|
if (amsContext && amsContext.snapshotBlock) {
|
|
2030
2075
|
memoryInjection = amsContext.snapshotBlock;
|
|
2031
2076
|
}
|
|
2077
|
+
const loadedMemoryForDebug = loadedMemoryDebugEntries(amsContext?.snapshot);
|
|
2032
2078
|
|
|
2033
2079
|
// Diagnostic payload for the Dream debug panel. The full AMS Resident
|
|
2034
2080
|
// layer can include user and per-VP summaries, but the browser-facing
|
|
@@ -2282,19 +2328,20 @@ export class Engine {
|
|
|
2282
2328
|
yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
|
|
2283
2329
|
}
|
|
2284
2330
|
|
|
2285
|
-
// Surface memory
|
|
2286
|
-
//
|
|
2287
|
-
//
|
|
2288
|
-
|
|
2289
|
-
if (recallResult && Array.isArray(recallResult.entries) && recallResult.entries.length > 0) {
|
|
2331
|
+
// Surface the exact memory that entered the prompt. This must be based on
|
|
2332
|
+
// the AMS snapshot, not raw FTS candidates, otherwise debug can claim memory
|
|
2333
|
+
// was loaded even when prompt cleanup, dedupe, or token budget dropped it.
|
|
2334
|
+
if (loadedMemoryForDebug.length > 0) {
|
|
2290
2335
|
yield {
|
|
2291
2336
|
type: 'memory_used',
|
|
2292
2337
|
turnId: queryTurnId,
|
|
2293
|
-
loaded:
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2338
|
+
loaded: loadedMemoryForDebug,
|
|
2339
|
+
meta: {
|
|
2340
|
+
recallLimit: resolveMemoryRecallLimit(this.#config),
|
|
2341
|
+
recallCandidates: Number.isFinite(recallResult?.meta?.hitCount)
|
|
2342
|
+
? recallResult.meta.hitCount
|
|
2343
|
+
: (recallResult && Array.isArray(recallResult.entries) ? recallResult.entries.length : 0),
|
|
2344
|
+
},
|
|
2298
2345
|
};
|
|
2299
2346
|
}
|
|
2300
2347
|
|
package/yeaft/memory/preflow.js
CHANGED
|
@@ -19,6 +19,8 @@ import { extractKeywords } from './keywords.js';
|
|
|
19
19
|
import { approxTokens } from './budget.js';
|
|
20
20
|
import { isVpForeign } from './store.js';
|
|
21
21
|
|
|
22
|
+
export const DEFAULT_PICK_LIMIT = 8;
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* @typedef {object} PreflowOptions
|
|
24
26
|
* @property {string} userMsg
|
|
@@ -27,6 +29,7 @@ import { isVpForeign } from './store.js';
|
|
|
27
29
|
* @property {string[]} [currentTags] tags from the current group/feature context
|
|
28
30
|
* @property {number} [topK] max FTS rows to fetch (default 50)
|
|
29
31
|
* @property {number} [budgetTokens] onDemand budget (caller-supplied)
|
|
32
|
+
* @property {number} [pickLimit] max picked segments (default 8)
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -54,6 +57,8 @@ export function runPreflow(index, opts) {
|
|
|
54
57
|
const topK = Number.isFinite(opts.topK) && opts.topK > 0 ? opts.topK : 50;
|
|
55
58
|
const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0
|
|
56
59
|
? opts.budgetTokens : Infinity;
|
|
60
|
+
const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0
|
|
61
|
+
? Math.floor(opts.pickLimit) : DEFAULT_PICK_LIMIT;
|
|
57
62
|
|
|
58
63
|
const keywords = extractKeywords(userMsg);
|
|
59
64
|
if (keywords.length === 0) {
|
|
@@ -80,7 +85,7 @@ export function runPreflow(index, opts) {
|
|
|
80
85
|
let dropped = 0;
|
|
81
86
|
for (const h of reranked) {
|
|
82
87
|
const tk = approxTokens(h.body);
|
|
83
|
-
if (cost + tk <= budgetTokens) {
|
|
88
|
+
if (picked.length < pickLimit && cost + tk <= budgetTokens) {
|
|
84
89
|
picked.push(toSegment(h));
|
|
85
90
|
cost += tk;
|
|
86
91
|
} else {
|
|
@@ -152,7 +157,7 @@ export function rerank(hits, ctx) {
|
|
|
152
157
|
return { ...h, _score: score };
|
|
153
158
|
})
|
|
154
159
|
.sort((a, b) => a._score - b._score)
|
|
155
|
-
.map(({ _score, ...rest }) => rest);
|
|
160
|
+
.map(({ _score, ...rest }) => ({ ...rest, score: _score }));
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
function toSegment(h) {
|
|
@@ -163,6 +168,7 @@ function toSegment(h) {
|
|
|
163
168
|
tags: h.tags,
|
|
164
169
|
sourceMessages: h.sourceMessages,
|
|
165
170
|
body: h.body,
|
|
171
|
+
score: typeof h.score === 'number' ? h.score : (typeof h.rank === 'number' ? h.rank : undefined),
|
|
166
172
|
createdAt: h.createdAt,
|
|
167
173
|
updatedAt: h.updatedAt,
|
|
168
174
|
};
|
|
@@ -259,6 +259,7 @@ export function formatPickedForInjection(picked) {
|
|
|
259
259
|
* @property {string[]} [currentTags] Contextual tags for rerank
|
|
260
260
|
* @property {number} [topK] Max FTS rows fetched (default 50)
|
|
261
261
|
* @property {number} [budgetTokens] Token budget for picked segments
|
|
262
|
+
* @property {number} [pickLimit] Max picked segments (default 8)
|
|
262
263
|
* @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
|
|
263
264
|
* @property {number} [fallbackPerScope] Max fallback segments per scope
|
|
264
265
|
*/
|
|
@@ -353,6 +354,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
353
354
|
currentTags: opts.currentTags || [],
|
|
354
355
|
topK: opts.topK,
|
|
355
356
|
budgetTokens: opts.budgetTokens,
|
|
357
|
+
pickLimit: opts.pickLimit,
|
|
356
358
|
});
|
|
357
359
|
|
|
358
360
|
let fallbackUsed = false;
|
|
@@ -362,6 +364,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
362
364
|
ownVpId: opts.vpId || null,
|
|
363
365
|
budgetTokens: opts.budgetTokens,
|
|
364
366
|
perScope: opts.fallbackPerScope,
|
|
367
|
+
pickLimit: opts.pickLimit,
|
|
365
368
|
});
|
|
366
369
|
if (fallback.length > 0) {
|
|
367
370
|
fallbackUsed = true;
|
|
@@ -400,6 +403,7 @@ function fallbackScopedSegments(index, opts) {
|
|
|
400
403
|
const scopes = prioritizeFallbackScopes(filterScopes(opts.relevantScopes || [], opts.ownVpId || null));
|
|
401
404
|
const perScope = Number.isFinite(opts.perScope) && opts.perScope > 0 ? Math.floor(opts.perScope) : 2;
|
|
402
405
|
const budgetTokens = Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0 ? opts.budgetTokens : 1200;
|
|
406
|
+
const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0 ? Math.floor(opts.pickLimit) : 8;
|
|
403
407
|
const buckets = [];
|
|
404
408
|
for (const scope of scopes) {
|
|
405
409
|
let segs = [];
|
|
@@ -419,6 +423,7 @@ function fallbackScopedSegments(index, opts) {
|
|
|
419
423
|
if (!seg) continue;
|
|
420
424
|
const tk = approxTokens(seg.body || '');
|
|
421
425
|
if (tk <= 0 || cost + tk > budgetTokens) continue;
|
|
426
|
+
if (out.length >= pickLimit) return out;
|
|
422
427
|
out.push(seg);
|
|
423
428
|
cost += tk;
|
|
424
429
|
}
|