@yeaft/webchat-agent 1.0.457 → 1.0.458
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.458"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dream/segment-extract.js.
|
|
3
3
|
*
|
|
4
|
-
* Extract atomic evidence segments for scopes touched by a
|
|
5
|
-
* persist them through memory/segment-store. Dream Apply
|
|
6
|
-
* canonical content.md and the short catalog summary.md.
|
|
4
|
+
* Extract atomic evidence segments only for scopes successfully touched by a
|
|
5
|
+
* Dream pass and persist them through memory/segment-store. Dream Apply
|
|
6
|
+
* separately maintains canonical content.md and the short catalog summary.md.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readScope, writeScope } from '../memory/segment-store.js';
|
|
@@ -144,7 +144,7 @@ function templateVarsForScope(scope, sessionId) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function normalizeTargetScopes(sessionId, targets) {
|
|
147
|
-
const out = new Set(
|
|
147
|
+
const out = new Set();
|
|
148
148
|
for (const target of targets) {
|
|
149
149
|
if (typeof target !== 'string') continue;
|
|
150
150
|
const clean = target.trim();
|
package/yeaft/dream/triage.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'util';
|
|
2
|
+
|
|
1
3
|
import { isValidTopic } from '../memory/store.js';
|
|
2
4
|
/**
|
|
3
5
|
* dream/triage.js.
|
|
@@ -6,9 +8,10 @@ import { isValidTopic } from '../memory/store.js';
|
|
|
6
8
|
* The decision is two-staged on purpose:
|
|
7
9
|
*
|
|
8
10
|
* 1. **Hard rules** (this module, no LLM): everything we can determine
|
|
9
|
-
* from message metadata. Always include the active session
|
|
10
|
-
* that spoke as an assistant in the diff
|
|
11
|
-
*
|
|
11
|
+
* from message metadata. Always include the active session and every VP
|
|
12
|
+
* that spoke as an assistant in the diff. User-profile scopes are not a
|
|
13
|
+
* structural property, so they are added only when soft classification
|
|
14
|
+
* confirms a durable profile signal. (Feature scope was dropped
|
|
12
15
|
* 2026-05-13 along with the rest of the Feature system.)
|
|
13
16
|
*
|
|
14
17
|
* 2. **Soft classification** (LLM, two passes):
|
|
@@ -18,14 +21,15 @@ import { isValidTopic } from '../memory/store.js';
|
|
|
18
21
|
* it to an exact existing path or propose
|
|
19
22
|
* a new ≤2-level path.
|
|
20
23
|
*
|
|
21
|
-
* VP /
|
|
24
|
+
* VP / Session are deliberately NOT asked of the LLM — Hard Rules
|
|
22
25
|
* already cover them, and giving the LLM a chance to drop a
|
|
23
26
|
* structurally-required scope would weaken the contract.
|
|
24
27
|
*
|
|
25
|
-
* `user_profile_signals === true` does not need Pass-2
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* `user_profile_signals === true` does not need Pass-2: it directly
|
|
29
|
+
* adds the global `user` scope and, for a real collaborative Session,
|
|
30
|
+
* the Session-local `sessions/<id>/user` scope. Keeping those scopes
|
|
31
|
+
* out when the classifier says false avoids two Apply rewrites and two
|
|
32
|
+
* extraction requests for ordinary task-focused conversation.
|
|
29
33
|
*
|
|
30
34
|
* The LLM is injected as a callable: `llm({ pass, prompt, system })` →
|
|
31
35
|
* Promise<string>. Tests pass a stub; runner injects the real adapter.
|
|
@@ -63,8 +67,8 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
63
67
|
const out = new Map();
|
|
64
68
|
const add = (scope) => { if (!out.has(scope)) out.set(scope, { kind: 'update', scope }); };
|
|
65
69
|
|
|
66
|
-
//
|
|
67
|
-
|
|
70
|
+
// User-profile scopes are content-dependent and are added by soft
|
|
71
|
+
// classification only when the diff contains a durable profile signal.
|
|
68
72
|
|
|
69
73
|
// chat path takes precedence: chat sessions have no collaborative session context.
|
|
70
74
|
if (chatId) {
|
|
@@ -84,7 +88,6 @@ export function applyHardRules({ sessionId, chatId, messages }) {
|
|
|
84
88
|
// active collaborative session, except the virtual _no-session bucket.
|
|
85
89
|
if (sessionId && sessionId !== '_no-session') {
|
|
86
90
|
add(`sessions/${sessionId}`);
|
|
87
|
-
add(`sessions/${sessionId}/user`);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
for (const m of (messages || [])) {
|
|
@@ -159,18 +162,21 @@ export async function classifySoft({ root, sessionId, messages, topicSummaries,
|
|
|
159
162
|
const pass1Prompt = buildPass1Prompt({ sessionId, messages, topicSummaries, language, maxPromptChars });
|
|
160
163
|
const pass1Raw = await llm({ pass: 'triage-pass1', prompt: pass1Prompt, system: triageSystem(language) });
|
|
161
164
|
const pass1 = parseJsonSafe(pass1Raw);
|
|
165
|
+
if (!isValidPass1(pass1)) {
|
|
166
|
+
throw triageContractError('Pass-1', pass1Raw);
|
|
167
|
+
}
|
|
162
168
|
const out = [];
|
|
163
169
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
if (pass1 && pass1.user_profile_signals === true) {
|
|
170
|
+
// User-profile scopes are expensive shared-memory rewrites, not structural
|
|
171
|
+
// Session scopes. Route to them only when Pass-1 finds a durable signal.
|
|
172
|
+
if (pass1.user_profile_signals) {
|
|
168
173
|
out.push({ kind: 'update', scope: 'user' });
|
|
174
|
+
if (sessionId && sessionId !== '_no-session') {
|
|
175
|
+
out.push({ kind: 'update', scope: `sessions/${sessionId}/user` });
|
|
176
|
+
}
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
const
|
|
172
|
-
for (const description of topicDescriptions) {
|
|
173
|
-
if (typeof description !== 'string' || !description.trim()) continue;
|
|
179
|
+
for (const description of pass1.topics) {
|
|
174
180
|
const pass2Prompt = buildPass2Prompt({
|
|
175
181
|
description: description.trim(),
|
|
176
182
|
existingTopics: topicSummaries || [],
|
|
@@ -179,20 +185,18 @@ export async function classifySoft({ root, sessionId, messages, topicSummaries,
|
|
|
179
185
|
});
|
|
180
186
|
const pass2Raw = await llm({ pass: 'triage-pass2', prompt: pass2Prompt, system: triageSystem(language) });
|
|
181
187
|
const pass2 = parseJsonSafe(pass2Raw);
|
|
182
|
-
if (!pass2
|
|
188
|
+
if (!isValidPass2(pass2, { sessionId, topicSummaries })) {
|
|
189
|
+
throw triageContractError('Pass-2', pass2Raw);
|
|
190
|
+
}
|
|
183
191
|
if (pass2.decision === 'none') continue;
|
|
184
|
-
const path =
|
|
185
|
-
if (!path) continue;
|
|
186
|
-
const segs = path.split('/').filter(Boolean);
|
|
187
|
-
if (!sessionId || sessionId === '_no-session') continue;
|
|
188
|
-
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segs })) continue;
|
|
192
|
+
const path = pass2.path;
|
|
189
193
|
const redirected = root
|
|
190
|
-
? resolveTopicRedirect(root, sessionId,
|
|
191
|
-
:
|
|
194
|
+
? resolveTopicRedirect(root, sessionId, path)
|
|
195
|
+
: path;
|
|
192
196
|
const scope = `sessions/${sessionId}/topic/${redirected}`;
|
|
193
197
|
if (pass2.decision === 'match') {
|
|
194
198
|
out.push({ kind: 'update', scope });
|
|
195
|
-
} else
|
|
199
|
+
} else {
|
|
196
200
|
out.push({ kind: 'create', scope });
|
|
197
201
|
}
|
|
198
202
|
}
|
|
@@ -268,6 +272,58 @@ function oneLine(s) {
|
|
|
268
272
|
return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
function isPlainObject(value) {
|
|
276
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function hasExactKeys(value, expected) {
|
|
280
|
+
const keys = Object.keys(value).sort();
|
|
281
|
+
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isValidPass1(value) {
|
|
285
|
+
if (!isPlainObject(value)
|
|
286
|
+
|| !hasExactKeys(value, ['topics', 'trivial_only', 'user_profile_signals'])
|
|
287
|
+
|| typeof value.user_profile_signals !== 'boolean'
|
|
288
|
+
|| typeof value.trivial_only !== 'boolean'
|
|
289
|
+
|| !Array.isArray(value.topics)
|
|
290
|
+
|| !value.topics.every(topic => typeof topic === 'string' && topic.trim().length > 0)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return !value.trivial_only || (!value.user_profile_signals && value.topics.length === 0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isValidPass2(value, { sessionId, topicSummaries }) {
|
|
297
|
+
if (!isPlainObject(value) || !['match', 'new', 'none'].includes(value.decision)) return false;
|
|
298
|
+
if (value.decision === 'none') {
|
|
299
|
+
return hasExactKeys(value, ['decision']);
|
|
300
|
+
}
|
|
301
|
+
if (!hasExactKeys(value, ['decision', 'path'])
|
|
302
|
+
|| !sessionId
|
|
303
|
+
|| sessionId === '_no-session'
|
|
304
|
+
|| typeof value.path !== 'string'
|
|
305
|
+
|| value.path !== value.path.trim()) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const path = value.path;
|
|
309
|
+
const segments = path.split('/');
|
|
310
|
+
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segments })) return false;
|
|
311
|
+
const pathExists = (topicSummaries || []).some(topic => topic?.path === path);
|
|
312
|
+
return value.decision === 'match' ? pathExists : !pathExists;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function triageContractError(pass, raw) {
|
|
316
|
+
const err = new Error(`triage: ${pass} returned malformed JSON`);
|
|
317
|
+
err.rawSnippet = rawResponseSnippet(raw);
|
|
318
|
+
return err;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function rawResponseSnippet(raw) {
|
|
322
|
+
if (typeof raw === 'string') return raw.slice(0, 1000);
|
|
323
|
+
if (raw == null) return String(raw);
|
|
324
|
+
return inspect(raw, { depth: 2, maxArrayLength: 10, breakLength: 120 }).slice(0, 1000);
|
|
325
|
+
}
|
|
326
|
+
|
|
271
327
|
/** Lenient JSON parse: tolerate fenced ```json blocks. Returns null on failure. */
|
|
272
328
|
export function parseJsonSafe(raw) {
|
|
273
329
|
if (typeof raw !== 'string') return null;
|