@yeaft/webchat-agent 0.1.752 → 0.1.753
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 +1 -1
- package/unify/compact/compactor.js +14 -2
- package/unify/engine.js +12 -2
- package/unify/history-compact.js +41 -21
- package/unify/session.js +7 -0
- package/unify/tools/registry.js +30 -2
package/package.json
CHANGED
|
@@ -62,12 +62,17 @@ export class Compactor {
|
|
|
62
62
|
* treats that as a soft failure).
|
|
63
63
|
* @param {() => number|undefined} [opts.getMaxContextTokens]
|
|
64
64
|
* Returns `config.maxContextTokens` for `shouldCompactHistory`.
|
|
65
|
+
* @param {() => string|undefined} [opts.getLanguage]
|
|
66
|
+
* Returns the live `config.language`. Threaded into
|
|
67
|
+
* `compactHistory` so the compactor's summary prompt + the
|
|
68
|
+
* "session continued" wrapper render in the user's preferred
|
|
69
|
+
* locale instead of always English.
|
|
65
70
|
* @param {(groupId: string, result: CompactedResult) => void} [opts.onCompacted]
|
|
66
71
|
* Optional sink. Bridge wires this to send the
|
|
67
72
|
* `unify_history_compacted` WS event. Default: no-op. Can be
|
|
68
73
|
* replaced post-construction via `setOnCompacted`.
|
|
69
74
|
*/
|
|
70
|
-
constructor({ summarize, getMaxContextTokens, onCompacted } = {}) {
|
|
75
|
+
constructor({ summarize, getMaxContextTokens, getLanguage, onCompacted } = {}) {
|
|
71
76
|
if (typeof summarize !== 'function') {
|
|
72
77
|
throw new TypeError('Compactor: summarize is required');
|
|
73
78
|
}
|
|
@@ -75,6 +80,9 @@ export class Compactor {
|
|
|
75
80
|
this._getMaxContextTokens = typeof getMaxContextTokens === 'function'
|
|
76
81
|
? getMaxContextTokens
|
|
77
82
|
: () => undefined;
|
|
83
|
+
this._getLanguage = typeof getLanguage === 'function'
|
|
84
|
+
? getLanguage
|
|
85
|
+
: () => undefined;
|
|
78
86
|
this._onCompacted = typeof onCompacted === 'function' ? onCompacted : () => {};
|
|
79
87
|
/** @type {Map<string, { inFlight: Promise<void>|null, pending: boolean }>} */
|
|
80
88
|
this._states = new Map();
|
|
@@ -186,7 +194,11 @@ export class Compactor {
|
|
|
186
194
|
const summarize = ({ system, prompt }) =>
|
|
187
195
|
this._summarize({ system, prompt, maxTokens: SUMMARIZER_MAX_TOKENS });
|
|
188
196
|
|
|
189
|
-
const result = await compactHistory(snapshot, {
|
|
197
|
+
const result = await compactHistory(snapshot, {
|
|
198
|
+
summarize,
|
|
199
|
+
maxContextTokens,
|
|
200
|
+
language: this._getLanguage(),
|
|
201
|
+
});
|
|
190
202
|
if (!result || !result.compacted) {
|
|
191
203
|
if (result && result.error) {
|
|
192
204
|
console.warn(
|
package/unify/engine.js
CHANGED
|
@@ -975,13 +975,23 @@ export class Engine {
|
|
|
975
975
|
|
|
976
976
|
const archiveIds = [];
|
|
977
977
|
|
|
978
|
+
// Language-aware summarizer prompts. The orchestrator-track summary
|
|
979
|
+
// ends up in the system prompt as a "previous conversation summary"
|
|
980
|
+
// block, so it needs to match the user's preferred language to avoid
|
|
981
|
+
// a jarring locale flip mid-context.
|
|
982
|
+
const isZh = String(this.#config.language || '').toLowerCase().startsWith('zh');
|
|
983
|
+
const summariserSystem = isZh
|
|
984
|
+
? '你是对话摘要器。请用中文写出 2–3 段简明摘要,保留决策、事实与上下文。'
|
|
985
|
+
: 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.';
|
|
986
|
+
const summariserPromptPrefix = isZh ? '请概括:\n\n' : 'Summarize:\n\n';
|
|
987
|
+
|
|
978
988
|
const hooks = {
|
|
979
989
|
summarise: async () => {
|
|
980
990
|
try {
|
|
981
991
|
const result = await adapter.call({
|
|
982
992
|
model: fastConfig.model,
|
|
983
|
-
system:
|
|
984
|
-
messages: [{ role: 'user', content:
|
|
993
|
+
system: summariserSystem,
|
|
994
|
+
messages: [{ role: 'user', content: `${summariserPromptPrefix}${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
|
|
985
995
|
maxTokens: 1024,
|
|
986
996
|
});
|
|
987
997
|
return (result.text || '').trim();
|
package/unify/history-compact.js
CHANGED
|
@@ -328,16 +328,22 @@ export function findCutIndex(messages, keepRecent) {
|
|
|
328
328
|
* `server/db/message-db.js`) recognise it.
|
|
329
329
|
*
|
|
330
330
|
* @param {string} summary
|
|
331
|
+
* @param {{ language?: string }} [opts]
|
|
331
332
|
* @returns {{role:'user', content:string, _compactSummary: true}}
|
|
332
333
|
*/
|
|
333
|
-
export function wrapSummaryAsUserMessage(summary) {
|
|
334
|
+
export function wrapSummaryAsUserMessage(summary, opts = {}) {
|
|
334
335
|
const body = (summary || '').trim() || '(no summary produced)';
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
const isZh = String(opts.language || '').toLowerCase().startsWith('zh');
|
|
337
|
+
const content = isZh
|
|
338
|
+
? '本会话延续自之前的对话。早期上下文已经被概括以节省空间。\n\n' +
|
|
339
|
+
'至此为止的对话摘要:\n' +
|
|
340
|
+
body +
|
|
341
|
+
'\n\n请从中断处继续对话,不要再向用户重复确认。'
|
|
342
|
+
: 'This session is being continued from a previous conversation. ' +
|
|
343
|
+
'The earlier context has been summarized for efficiency.\n\n' +
|
|
344
|
+
'Summary of conversation so far:\n' +
|
|
345
|
+
body +
|
|
346
|
+
'\n\nContinue the conversation from where it left off without asking the user any further questions.';
|
|
341
347
|
return {
|
|
342
348
|
role: 'user',
|
|
343
349
|
content,
|
|
@@ -349,24 +355,36 @@ export function wrapSummaryAsUserMessage(summary) {
|
|
|
349
355
|
* Build the prompt fed to the fast-model summarizer. Kept in code (not in
|
|
350
356
|
* a template file) because it's small and lives alongside the call site.
|
|
351
357
|
*
|
|
358
|
+
* The summarizer prompt itself is language-aware: callers pass the live
|
|
359
|
+
* `config.language` so the produced summary is written in the user's
|
|
360
|
+
* preferred language. JSON-style structural cues stay English so the
|
|
361
|
+
* summary remains easy to splice into the next turn regardless of locale.
|
|
362
|
+
*
|
|
352
363
|
* @param {Array<{role:string, content:string}>} cleanedMessages
|
|
364
|
+
* @param {{ language?: string }} [opts]
|
|
353
365
|
* @returns {{system: string, prompt: string}}
|
|
354
366
|
*/
|
|
355
|
-
export function buildSummaryPrompt(cleanedMessages) {
|
|
367
|
+
export function buildSummaryPrompt(cleanedMessages, opts = {}) {
|
|
356
368
|
const transcript = cleanedMessages
|
|
357
369
|
.map(m => `[${m.role}]\n${m.content}`)
|
|
358
370
|
.join('\n\n---\n\n');
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
'
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
'
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
371
|
+
const isZh = String(opts.language || '').toLowerCase().startsWith('zh');
|
|
372
|
+
const system = isZh
|
|
373
|
+
? '你是多 agent 群聊的对话摘要器。请用中文写出 4–8 条简明 bullet 摘要。' +
|
|
374
|
+
'保留:(1) 已做的决策,(2) 已学到的事实,(3) 用户当前目标,' +
|
|
375
|
+
'(4) 任何未解决的问题或待办事项,(5) 哪些 VP 参与了对话以及各自贡献。' +
|
|
376
|
+
'不要包含原始工具输出。不要臆测。要具体。'
|
|
377
|
+
: 'You are a conversation summarizer for a multi-agent group chat. ' +
|
|
378
|
+
'Produce a concise (4–8 short bullet points) summary of the conversation ' +
|
|
379
|
+
'so far. Preserve: (1) decisions made, (2) facts learned, (3) the user\'s ' +
|
|
380
|
+
'current goal, (4) any open questions or pending actions, (5) which VPs ' +
|
|
381
|
+
'are participating and what each contributed. Do NOT include raw tool ' +
|
|
382
|
+
'output. Do NOT speculate. Be specific.';
|
|
383
|
+
const prompt = isZh
|
|
384
|
+
? '请概括下面的对话。只输出摘要正文,不要前言。\n\n' + transcript
|
|
385
|
+
: 'Summarize the following conversation. Output ONLY the summary, no ' +
|
|
386
|
+
'preamble.\n\n' +
|
|
387
|
+
transcript;
|
|
370
388
|
return { system, prompt };
|
|
371
389
|
}
|
|
372
390
|
|
|
@@ -384,6 +402,7 @@ export function buildSummaryPrompt(cleanedMessages) {
|
|
|
384
402
|
* maxContextTokens?: number,
|
|
385
403
|
* tokenFraction?: number,
|
|
386
404
|
* hardTokenCeiling?: number,
|
|
405
|
+
* language?: string,
|
|
387
406
|
* }} options
|
|
388
407
|
* @returns {Promise<{
|
|
389
408
|
* messages: Array<object>,
|
|
@@ -407,6 +426,7 @@ export async function compactHistory(messages, options) {
|
|
|
407
426
|
maxContextTokens,
|
|
408
427
|
tokenFraction,
|
|
409
428
|
hardTokenCeiling,
|
|
429
|
+
language,
|
|
410
430
|
} = options || {};
|
|
411
431
|
|
|
412
432
|
if (typeof summarize !== 'function') {
|
|
@@ -460,7 +480,7 @@ export async function compactHistory(messages, options) {
|
|
|
460
480
|
|
|
461
481
|
let summaryText = '';
|
|
462
482
|
if (cleaned.length > 0) {
|
|
463
|
-
const { system, prompt } = buildSummaryPrompt(cleaned);
|
|
483
|
+
const { system, prompt } = buildSummaryPrompt(cleaned, { language });
|
|
464
484
|
try {
|
|
465
485
|
summaryText = (await summarize({ system, prompt })) || '';
|
|
466
486
|
} catch (err) {
|
|
@@ -498,7 +518,7 @@ export async function compactHistory(messages, options) {
|
|
|
498
518
|
}
|
|
499
519
|
}
|
|
500
520
|
|
|
501
|
-
const summaryMsg = wrapSummaryAsUserMessage(summaryText);
|
|
521
|
+
const summaryMsg = wrapSummaryAsUserMessage(summaryText, { language });
|
|
502
522
|
|
|
503
523
|
// Defensive pair-sanitize: the cut at `cutIdx` lands at a user-message
|
|
504
524
|
// boundary so an `[assistant(toolCalls), tool…]` arc is not split, but
|
package/unify/session.js
CHANGED
|
@@ -315,6 +315,13 @@ export async function loadSession(options = {}) {
|
|
|
315
315
|
engine.summarizeForCompact({ system, prompt, maxTokens }),
|
|
316
316
|
getMaxContextTokens: () =>
|
|
317
317
|
typeof config.maxContextTokens === 'number' ? config.maxContextTokens : undefined,
|
|
318
|
+
// Live-read: `config.language` is mutated in place by
|
|
319
|
+
// `engine.setLanguage()` (which broadcastLanguageChange fans out to
|
|
320
|
+
// every per-VP engine). The compactor must see the post-broadcast
|
|
321
|
+
// value, not a boot-time snapshot, so the summary prompt + the
|
|
322
|
+
// "session continued" wrapper render in the user's current locale.
|
|
323
|
+
getLanguage: () =>
|
|
324
|
+
typeof config.language === 'string' ? config.language : undefined,
|
|
318
325
|
});
|
|
319
326
|
|
|
320
327
|
// ─── 9a. Create dream scheduler ────────────
|
package/unify/tools/registry.js
CHANGED
|
@@ -61,16 +61,44 @@ function localizeVisibleText(value, language, toolName) {
|
|
|
61
61
|
].join('\n');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Walk a JSON Schema `parameters` object and localize the human-readable
|
|
66
|
+
* `description` strings to the requested language. All other schema bits
|
|
67
|
+
* (`type`, `enum`, `required`, `items`, nested `properties`, etc.) are
|
|
68
|
+
* preserved by value.
|
|
69
|
+
*
|
|
70
|
+
* Critical correctness rule: a JSON Schema can have a *property named*
|
|
71
|
+
* `description` whose value is itself a sub-schema (e.g.
|
|
72
|
+
* `properties: { description: { type: 'string', description: 'Detailed...' }}`).
|
|
73
|
+
* In that case the OUTER `description` key holds the sub-schema and must
|
|
74
|
+
* be recursed into; only the INNER `description: 'Detailed...'` value
|
|
75
|
+
* (which is a string) should be localized.
|
|
76
|
+
*
|
|
77
|
+
* Previous bug: this walker localized ANY value under a `description`
|
|
78
|
+
* key, including sub-schema objects. `localizeVisibleText` then ran
|
|
79
|
+
* `String(value)` on the object, producing the literal string
|
|
80
|
+
* `'[object Object]'`. GPT-5's strict schema validator rejected the
|
|
81
|
+
* resulting `{ description: '[object Object]' }` with
|
|
82
|
+
* `"'[object Object]' is not of type 'object', 'boolean'"`. This made
|
|
83
|
+
* FeatureCreate (and any tool whose schema contains a property named
|
|
84
|
+
* `description`) unusable in zh locale on strict providers.
|
|
85
|
+
*
|
|
86
|
+
* The fix: only treat a `description` value as localizable text when it
|
|
87
|
+
* is actually a string. Object/array values under a `description` key
|
|
88
|
+
* are sub-schemas and must be recursed into normally.
|
|
89
|
+
*/
|
|
64
90
|
function localizeParameters(parameters, language, toolName) {
|
|
65
91
|
const lang = normalizeLanguage(language);
|
|
66
92
|
if (lang !== 'zh' || !parameters || typeof parameters !== 'object') return parameters;
|
|
67
93
|
if (Array.isArray(parameters)) return parameters.map(v => localizeParameters(v, lang, toolName));
|
|
68
94
|
const out = {};
|
|
69
95
|
for (const [key, value] of Object.entries(parameters)) {
|
|
70
|
-
if (key === 'description') {
|
|
96
|
+
if (key === 'description' && typeof value === 'string') {
|
|
71
97
|
out[key] = localizeVisibleText(value, lang, toolName);
|
|
72
|
-
} else {
|
|
98
|
+
} else if (value && typeof value === 'object') {
|
|
73
99
|
out[key] = localizeParameters(value, lang, toolName);
|
|
100
|
+
} else {
|
|
101
|
+
out[key] = value;
|
|
74
102
|
}
|
|
75
103
|
}
|
|
76
104
|
return out;
|