@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.752",
3
+ "version": "0.1.753",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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, { summarize, maxContextTokens });
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: 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.',
984
- messages: [{ role: 'user', content: `Summarize:\n\n${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
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();
@@ -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 content =
336
- 'This session is being continued from a previous conversation. ' +
337
- 'The earlier context has been summarized for efficiency.\n\n' +
338
- 'Summary of conversation so far:\n' +
339
- body +
340
- '\n\nContinue the conversation from where it left off without asking the user any further questions.';
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 system =
360
- 'You are a conversation summarizer for a multi-agent group chat. ' +
361
- 'Produce a concise (4–8 short bullet points) summary of the conversation ' +
362
- 'so far. Preserve: (1) decisions made, (2) facts learned, (3) the user\'s ' +
363
- 'current goal, (4) any open questions or pending actions, (5) which VPs ' +
364
- 'are participating and what each contributed. Do NOT include raw tool ' +
365
- 'output. Do NOT speculate. Be specific.';
366
- const prompt =
367
- 'Summarize the following conversation. Output ONLY the summary, no ' +
368
- 'preamble.\n\n' +
369
- transcript;
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 ────────────
@@ -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;