@yeaft/webchat-agent 0.1.751 → 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/dream-v2/apply.js +19 -11
- package/unify/dream-v2/prompts/index.js +16 -2
- package/unify/dream-v2/runner.js +2 -0
- package/unify/dream-v2/session-wiring.js +4 -1
- package/unify/dream-v2/triage.js +17 -11
- package/unify/engine.js +16 -4
- package/unify/history-compact.js +41 -21
- package/unify/prompts.js +39 -11
- package/unify/session.js +7 -0
- package/unify/tools/registry.js +73 -5
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/dream-v2/apply.js
CHANGED
|
@@ -28,7 +28,11 @@ import { snapshotScope } from './snapshot.js';
|
|
|
28
28
|
import { parseJsonSafe } from './triage.js';
|
|
29
29
|
import { render } from './prompts/index.js';
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
function applySystem(language) {
|
|
32
|
+
return String(language || '').toLowerCase().startsWith('zh')
|
|
33
|
+
? '你是梦境流水线的 Apply 阶段。你会根据最近的群组对话重写单个 scope 的 memory.md 和 summary.md。请只回复严格 JSON,不要输出说明文字或 markdown fence。memory_md 和 summary_md 的自然语言内容必须使用中文;JSON key、scope、schema 字段和代码标识符保持英文。'
|
|
34
|
+
: 'You are the Apply stage of a dream pipeline. You rewrite a single scope\'s memory.md and summary.md based on recent group conversations. Reply with strict JSON only — no prose, no fences.';
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* Build the UPDATE prompt body. Accepts the current scope state +
|
|
@@ -44,16 +48,18 @@ const SYSTEM = `You are the Apply stage of a dream pipeline. You rewrite a singl
|
|
|
44
48
|
*/
|
|
45
49
|
export function buildUpdatePrompt(ctx) {
|
|
46
50
|
const batchHeader = (ctx.batchInfo && ctx.batchInfo.total > 1)
|
|
47
|
-
?
|
|
51
|
+
? (String(ctx.language || '').toLowerCase().startsWith('zh')
|
|
52
|
+
? `这是第 ${ctx.batchInfo.index}/${ctx.batchInfo.total} 批。前面的批次已经合并进下面当前的 memory.md。\n`
|
|
53
|
+
: `This is batch ${ctx.batchInfo.index} of ${ctx.batchInfo.total}.\nEarlier batches have already been folded into the current memory.md below.\n`)
|
|
48
54
|
: '';
|
|
49
|
-
const sources = renderSourceBlocks(ctx.sources);
|
|
55
|
+
const sources = renderSourceBlocks(ctx.sources, ctx.language);
|
|
50
56
|
return render('update', {
|
|
51
57
|
target: ctx.target,
|
|
52
58
|
batchHeader,
|
|
53
59
|
memoryMd: ctx.memoryMd || '',
|
|
54
60
|
summaryMd: ctx.summaryMd || '',
|
|
55
61
|
sources,
|
|
56
|
-
});
|
|
62
|
+
}, { language: ctx.language });
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -67,10 +73,10 @@ export function buildUpdatePrompt(ctx) {
|
|
|
67
73
|
* }} ctx
|
|
68
74
|
*/
|
|
69
75
|
export function buildCreatePrompt(ctx) {
|
|
70
|
-
const sources = renderSourceBlocks(ctx.sources);
|
|
76
|
+
const sources = renderSourceBlocks(ctx.sources, ctx.language);
|
|
71
77
|
let siblingsBlock = '';
|
|
72
78
|
if (ctx.siblingTopics && ctx.siblingTopics.length > 0) {
|
|
73
|
-
const lines = ['For tone reference, sibling/parent topic summaries:'];
|
|
79
|
+
const lines = [String(ctx.language || '').toLowerCase().startsWith('zh') ? '语气参考:同级/父级 topic 摘要:' : 'For tone reference, sibling/parent topic summaries:'];
|
|
74
80
|
for (const t of ctx.siblingTopics) lines.push(` - ${t.path}: ${oneLine(t.summary)}`);
|
|
75
81
|
lines.push('');
|
|
76
82
|
siblingsBlock = lines.join('\n');
|
|
@@ -79,7 +85,7 @@ export function buildCreatePrompt(ctx) {
|
|
|
79
85
|
target: ctx.target,
|
|
80
86
|
sources,
|
|
81
87
|
siblingsBlock,
|
|
82
|
-
});
|
|
88
|
+
}, { language: ctx.language });
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/**
|
|
@@ -89,13 +95,13 @@ export function buildCreatePrompt(ctx) {
|
|
|
89
95
|
*
|
|
90
96
|
* @param {Array<{ groupId: string, diff: Array<object> }>} sources
|
|
91
97
|
*/
|
|
92
|
-
function renderSourceBlocks(sources) {
|
|
98
|
+
function renderSourceBlocks(sources, language) {
|
|
93
99
|
const out = [];
|
|
94
100
|
for (const src of (sources || [])) {
|
|
95
101
|
out.push('');
|
|
96
102
|
out.push(`[group/${src.groupId}]`);
|
|
97
103
|
for (const m of (src.diff || [])) {
|
|
98
|
-
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? ' (already processed)' : ''}]`;
|
|
104
|
+
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? (String(language || '').toLowerCase().startsWith('zh') ? '(已处理)' : ' (already processed)') : ''}]`;
|
|
99
105
|
out.push(head);
|
|
100
106
|
out.push(truncateMessage(m.body || ''));
|
|
101
107
|
}
|
|
@@ -174,9 +180,10 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
174
180
|
target: merged.target,
|
|
175
181
|
sources: merged.sources,
|
|
176
182
|
siblingTopics: siblings,
|
|
183
|
+
language: opts.language,
|
|
177
184
|
});
|
|
178
185
|
if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'llm', batch: 1, of: 1 });
|
|
179
|
-
const raw = await opts.llm({ pass: 'create', prompt, system:
|
|
186
|
+
const raw = await opts.llm({ pass: 'create', prompt, system: applySystem(opts.language) });
|
|
180
187
|
const parsed = parseJsonSafe(raw);
|
|
181
188
|
if (!parsed || typeof parsed.memory_md !== 'string') {
|
|
182
189
|
throw new Error(`apply: CREATE returned malformed JSON for ${merged.target}`);
|
|
@@ -202,9 +209,10 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
202
209
|
summaryMd,
|
|
203
210
|
sources: batch,
|
|
204
211
|
batchInfo: { index: i, total: batches.length },
|
|
212
|
+
language: opts.language,
|
|
205
213
|
});
|
|
206
214
|
if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'llm', batch: i, of: batches.length });
|
|
207
|
-
const raw = await opts.llm({ pass: 'update', prompt, system:
|
|
215
|
+
const raw = await opts.llm({ pass: 'update', prompt, system: applySystem(opts.language) });
|
|
208
216
|
const parsed = parseJsonSafe(raw);
|
|
209
217
|
if (!parsed || typeof parsed.memory_md !== 'string') {
|
|
210
218
|
throw new Error(`apply: UPDATE batch ${i} returned malformed JSON for ${merged.target}`);
|
|
@@ -48,6 +48,16 @@ export function extractTemplateForScope(scope) {
|
|
|
48
48
|
/** @type {Record<string, string>} */
|
|
49
49
|
const cache = {};
|
|
50
50
|
|
|
51
|
+
export function normalizePromptLanguage(language) {
|
|
52
|
+
return String(language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function languageDirective(language) {
|
|
56
|
+
return normalizePromptLanguage(language) === 'zh'
|
|
57
|
+
? '语言要求:请用中文生成所有自然语言内容,尤其是 memory_md 和 summary_md;工具名、scope、JSON key、schema 字段、代码标识符和枚举值必须保持英文。严格按要求输出 JSON 时,不要翻译 JSON key。'
|
|
58
|
+
: 'Language requirement: write all natural-language memory content in English. Keep tool names, scopes, JSON keys, schema fields, code identifiers, and enum values in English. When strict JSON is required, do not rename JSON keys.';
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
function load(name) {
|
|
52
62
|
if (cache[name]) return cache[name];
|
|
53
63
|
const file = FILES[name];
|
|
@@ -63,13 +73,17 @@ function load(name) {
|
|
|
63
73
|
*
|
|
64
74
|
* @param {string} name
|
|
65
75
|
* @param {Record<string, string>} vars
|
|
76
|
+
* @param {{ language?: string, includeLanguageDirective?: boolean }} [opts]
|
|
66
77
|
*/
|
|
67
|
-
export function render(name, vars) {
|
|
78
|
+
export function render(name, vars, opts = {}) {
|
|
68
79
|
const tpl = load(name);
|
|
69
|
-
|
|
80
|
+
const body = tpl.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
|
|
70
81
|
if (!(key in vars)) throw new Error(`prompts.${name}: missing var ${key}`);
|
|
71
82
|
return vars[key];
|
|
72
83
|
});
|
|
84
|
+
if (opts.includeLanguageDirective === false) return body;
|
|
85
|
+
if (!opts.language) return body;
|
|
86
|
+
return `${languageDirective(opts.language)}\n\n${body}`;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
/** Test-only: drop the in-memory cache so a fresh read re-loads from disk. */
|
package/unify/dream-v2/runner.js
CHANGED
|
@@ -143,6 +143,7 @@ export async function runDream(opts) {
|
|
|
143
143
|
topicSummaries,
|
|
144
144
|
llm: opts.llm,
|
|
145
145
|
onProgress,
|
|
146
|
+
language: opts.language,
|
|
146
147
|
});
|
|
147
148
|
} catch (err) {
|
|
148
149
|
groupsReport.push({ groupId, new: newCount, status: 'error', error: err.message });
|
|
@@ -178,6 +179,7 @@ export async function runDream(opts) {
|
|
|
178
179
|
nowIso: opts.nowIso || (() => nowIso),
|
|
179
180
|
onProgress,
|
|
180
181
|
siblingTopicsFor: opts.siblingTopicsFor,
|
|
182
|
+
language: opts.language,
|
|
181
183
|
});
|
|
182
184
|
targetsReport.push({ ...r, sources: merged.sources.length, status: 'done' });
|
|
183
185
|
} catch (err) {
|
|
@@ -31,6 +31,7 @@ export function buildRunDreamOpts(session, onProgress) {
|
|
|
31
31
|
|
|
32
32
|
return {
|
|
33
33
|
root: memoryRoot,
|
|
34
|
+
language: session.config?.language || 'en',
|
|
34
35
|
llm: makeLlm(session),
|
|
35
36
|
listGroups: async () => {
|
|
36
37
|
try { return listGroups(groupsRoot).map(g => g.id); }
|
|
@@ -110,7 +111,9 @@ function makeLlm(session) {
|
|
|
110
111
|
}
|
|
111
112
|
const r = await adapter.call({
|
|
112
113
|
model,
|
|
113
|
-
system: system ||
|
|
114
|
+
system: system || (String(session.config?.language || '').toLowerCase().startsWith('zh')
|
|
115
|
+
? `你是梦境流水线 — pass: ${pass}。请用中文生成自然语言内容;JSON key 保持英文。`
|
|
116
|
+
: `You are the dream pipeline — pass: ${pass}.`),
|
|
114
117
|
messages: [{ role: 'user', content: prompt }],
|
|
115
118
|
maxTokens: 2048,
|
|
116
119
|
});
|
package/unify/dream-v2/triage.js
CHANGED
|
@@ -39,7 +39,11 @@ import { isValidTopic } from '../memory/store-v2.js';
|
|
|
39
39
|
import { truncateMessage } from './segment.js';
|
|
40
40
|
import { render } from './prompts/index.js';
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
function triageSystem(language) {
|
|
43
|
+
return String(language || '').toLowerCase().startsWith('zh')
|
|
44
|
+
? '你是梦境流水线的 Triage 阶段,负责判断最近的群组对话会影响哪些 scope。请只回复严格 JSON,不要输出说明文字或 markdown fence。自然语言内容使用中文;JSON key、scope 和枚举值保持英文。'
|
|
45
|
+
: 'You are the Triage stage of a dream pipeline that decides which scopes a recent group conversation should affect. Reply with strict JSON only — no prose, no markdown fences.';
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* Hard rules: deterministically derive must-include scopes from the
|
|
@@ -89,11 +93,11 @@ export function applyHardRules({ groupId, messages }) {
|
|
|
89
93
|
*/
|
|
90
94
|
export function buildPass1Prompt(ctx) {
|
|
91
95
|
const topicSummaries = (!ctx.topicSummaries || ctx.topicSummaries.length === 0)
|
|
92
|
-
? ' (none)'
|
|
96
|
+
? (String(ctx.language || '').toLowerCase().startsWith('zh') ? ' (无)' : ' (none)')
|
|
93
97
|
: ctx.topicSummaries.map(t => ` - ${t.path} — ${oneLine(t.summary)}`).join('\n');
|
|
94
98
|
const conv = [];
|
|
95
99
|
for (const m of (ctx.messages || [])) {
|
|
96
|
-
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? ' (already processed)' : ''}]`;
|
|
100
|
+
const head = `[${m.role || 'message'}${m.kind === 'overlap' ? (String(ctx.language || '').toLowerCase().startsWith('zh') ? '(已处理)' : ' (already processed)') : ''}]`;
|
|
97
101
|
conv.push(head);
|
|
98
102
|
conv.push(truncateMessage(m.body || ''));
|
|
99
103
|
conv.push('');
|
|
@@ -102,7 +106,7 @@ export function buildPass1Prompt(ctx) {
|
|
|
102
106
|
groupId: ctx.groupId,
|
|
103
107
|
topicSummaries,
|
|
104
108
|
conversation: conv.join('\n').trimEnd(),
|
|
105
|
-
});
|
|
109
|
+
}, { language: ctx.language });
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
@@ -112,12 +116,12 @@ export function buildPass1Prompt(ctx) {
|
|
|
112
116
|
*/
|
|
113
117
|
export function buildPass2Prompt(ctx) {
|
|
114
118
|
const existingTopics = (!ctx.existingTopics || ctx.existingTopics.length === 0)
|
|
115
|
-
? ' (none)'
|
|
119
|
+
? (String(ctx.language || '').toLowerCase().startsWith('zh') ? ' (无)' : ' (none)')
|
|
116
120
|
: ctx.existingTopics.map(t => ` - ${t.path} — ${oneLine(t.summary)}`).join('\n');
|
|
117
121
|
return render('triagePass2', {
|
|
118
122
|
description: ctx.description,
|
|
119
123
|
existingTopics,
|
|
120
|
-
});
|
|
124
|
+
}, { language: ctx.language });
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
/**
|
|
@@ -131,10 +135,10 @@ export function buildPass2Prompt(ctx) {
|
|
|
131
135
|
* }} args
|
|
132
136
|
* @returns {Promise<Array<{ kind: 'update'|'create', scope: string }>>}
|
|
133
137
|
*/
|
|
134
|
-
export async function classifySoft({ groupId, messages, topicSummaries, llm }) {
|
|
138
|
+
export async function classifySoft({ groupId, messages, topicSummaries, llm, language }) {
|
|
135
139
|
if (!llm) throw new Error('triage.classifySoft: llm callable required');
|
|
136
|
-
const pass1Prompt = buildPass1Prompt({ groupId, messages, topicSummaries });
|
|
137
|
-
const pass1Raw = await llm({ pass: 'triage-pass1', prompt: pass1Prompt, system:
|
|
140
|
+
const pass1Prompt = buildPass1Prompt({ groupId, messages, topicSummaries, language });
|
|
141
|
+
const pass1Raw = await llm({ pass: 'triage-pass1', prompt: pass1Prompt, system: triageSystem(language) });
|
|
138
142
|
const pass1 = parseJsonSafe(pass1Raw);
|
|
139
143
|
const out = [];
|
|
140
144
|
|
|
@@ -151,8 +155,9 @@ export async function classifySoft({ groupId, messages, topicSummaries, llm }) {
|
|
|
151
155
|
const pass2Prompt = buildPass2Prompt({
|
|
152
156
|
description: description.trim(),
|
|
153
157
|
existingTopics: topicSummaries || [],
|
|
158
|
+
language,
|
|
154
159
|
});
|
|
155
|
-
const pass2Raw = await llm({ pass: 'triage-pass2', prompt: pass2Prompt, system:
|
|
160
|
+
const pass2Raw = await llm({ pass: 'triage-pass2', prompt: pass2Prompt, system: triageSystem(language) });
|
|
156
161
|
const pass2 = parseJsonSafe(pass2Raw);
|
|
157
162
|
if (!pass2 || !pass2.decision) continue;
|
|
158
163
|
if (pass2.decision === 'none') continue;
|
|
@@ -201,7 +206,7 @@ export async function triageOneSegment(args) {
|
|
|
201
206
|
* }} args
|
|
202
207
|
* @returns {Promise<Array<{ kind: 'update'|'create', scope: string }>>}
|
|
203
208
|
*/
|
|
204
|
-
export async function triageGroupSegments({ groupId, segments, topicSummaries, llm, onProgress }) {
|
|
209
|
+
export async function triageGroupSegments({ groupId, segments, topicSummaries, llm, onProgress, language }) {
|
|
205
210
|
let acc = [];
|
|
206
211
|
let i = 0;
|
|
207
212
|
for (const seg of (segments || [])) {
|
|
@@ -212,6 +217,7 @@ export async function triageGroupSegments({ groupId, segments, topicSummaries, l
|
|
|
212
217
|
messages: seg.messages,
|
|
213
218
|
topicSummaries,
|
|
214
219
|
llm,
|
|
220
|
+
language,
|
|
215
221
|
});
|
|
216
222
|
acc = dedupeActions([...acc, ...segActions]);
|
|
217
223
|
}
|
package/unify/engine.js
CHANGED
|
@@ -464,7 +464,7 @@ export class Engine {
|
|
|
464
464
|
*/
|
|
465
465
|
#getToolDefs() {
|
|
466
466
|
if (this.#toolRegistry) {
|
|
467
|
-
return this.#toolRegistry.getToolDefs();
|
|
467
|
+
return this.#toolRegistry.getToolDefs(this.#config?.language || 'en');
|
|
468
468
|
}
|
|
469
469
|
// Legacy path: no mode filtering
|
|
470
470
|
const defs = [];
|
|
@@ -645,7 +645,9 @@ export class Engine {
|
|
|
645
645
|
runLLM: async (prompt) => {
|
|
646
646
|
const out = await this.#adapter.call({
|
|
647
647
|
model: this.#fastConfig.model,
|
|
648
|
-
system:
|
|
648
|
+
system: (String(this.#config?.language || '').toLowerCase().startsWith('zh')
|
|
649
|
+
? '你是记忆管理子程序。请按要求只回复一个 JSON 对象,不要输出额外说明。'
|
|
650
|
+
: 'You are a memory-management subroutine. Reply with a single JSON object as instructed.'),
|
|
649
651
|
messages: [{ role: 'user', content: prompt }],
|
|
650
652
|
maxTokens: 1024,
|
|
651
653
|
});
|
|
@@ -973,13 +975,23 @@ export class Engine {
|
|
|
973
975
|
|
|
974
976
|
const archiveIds = [];
|
|
975
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
|
+
|
|
976
988
|
const hooks = {
|
|
977
989
|
summarise: async () => {
|
|
978
990
|
try {
|
|
979
991
|
const result = await adapter.call({
|
|
980
992
|
model: fastConfig.model,
|
|
981
|
-
system:
|
|
982
|
-
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')}` }],
|
|
983
995
|
maxTokens: 1024,
|
|
984
996
|
});
|
|
985
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/prompts.js
CHANGED
|
@@ -178,6 +178,7 @@ const PROMPTS = {
|
|
|
178
178
|
`💡 ${min}min since last summary (+${count} new messages). Consider calling \`task_summary_post\`.`,
|
|
179
179
|
// DESIGN-PROMPT §3 ④ — Active Scope header
|
|
180
180
|
activeScopeHeader: '## active_scope',
|
|
181
|
+
groupAnnouncementHeader: '[Group Announcement]',
|
|
181
182
|
vpPersonaIntro: (name, role) =>
|
|
182
183
|
`You ARE **${name}**${role ? ` (${role})` : ''}. Speak in the first person as ${name}; do not refer to yourself as "Yeaft" or as a generic AI assistant. The text below is your identity, expertise, and decision style.`,
|
|
183
184
|
},
|
|
@@ -193,6 +194,7 @@ const PROMPTS = {
|
|
|
193
194
|
`💡 距上次 summary 已过 ${min}min,新增 ${count} 条消息,建议调用 \`task_summary_post\`。`,
|
|
194
195
|
// DESIGN-PROMPT §3 ④ — Active Scope header
|
|
195
196
|
activeScopeHeader: '## active_scope',
|
|
197
|
+
groupAnnouncementHeader: '[群组公告]',
|
|
196
198
|
vpPersonaIntro: (name, role) =>
|
|
197
199
|
`你就是 **${name}**${role ? `(${role})` : ''}。请以 ${name} 的第一人称发言;不要自称 "Yeaft" 或泛指的 AI 助手。下面的文字是你的身份、专业方向与判断风格。`,
|
|
198
200
|
},
|
|
@@ -201,6 +203,28 @@ const PROMPTS = {
|
|
|
201
203
|
/** Supported language codes. */
|
|
202
204
|
export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
203
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Return true for Chinese locales. Real app config persists values like
|
|
208
|
+
* `zh-CN`; prompt templates are keyed by the base language (`zh`).
|
|
209
|
+
*
|
|
210
|
+
* @param {string} language
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
export function isZhLanguage(language) {
|
|
214
|
+
return String(language || '').toLowerCase().startsWith('zh');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Normalize app/user locale to the prompt dictionary key.
|
|
219
|
+
* Protocol identifiers stay English; this only selects visible prose.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} language
|
|
222
|
+
* @returns {'en'|'zh'}
|
|
223
|
+
*/
|
|
224
|
+
export function normalizePromptLanguage(language) {
|
|
225
|
+
return isZhLanguage(language) ? 'zh' : 'en';
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
/**
|
|
205
229
|
* Build the system prompt for a given language.
|
|
206
230
|
*
|
|
@@ -267,9 +291,9 @@ export function buildSystemPrompt({
|
|
|
267
291
|
vpPersona,
|
|
268
292
|
groupAnnouncement = '',
|
|
269
293
|
} = {}) {
|
|
270
|
-
//
|
|
271
|
-
const
|
|
272
|
-
const
|
|
294
|
+
// Normalize app locales like `zh-CN` to prompt dictionary/template keys.
|
|
295
|
+
const effectiveLang = normalizePromptLanguage(language);
|
|
296
|
+
const lang = PROMPTS[effectiveLang] || PROMPTS.en;
|
|
273
297
|
|
|
274
298
|
const parts = [];
|
|
275
299
|
|
|
@@ -299,7 +323,7 @@ export function buildSystemPrompt({
|
|
|
299
323
|
// instructions. Empty/whitespace = no block emitted.
|
|
300
324
|
const annText = (typeof groupAnnouncement === 'string') ? groupAnnouncement.trim() : '';
|
|
301
325
|
if (annText) {
|
|
302
|
-
parts.push(
|
|
326
|
+
parts.push(`${lang.groupAnnouncementHeader || '[Group Announcement]'}\n${annText}`);
|
|
303
327
|
}
|
|
304
328
|
|
|
305
329
|
// ─── 2. Date Metadata ──────────────────────────────────
|
|
@@ -674,7 +698,8 @@ const LAYER_A_HEADERS = {
|
|
|
674
698
|
*/
|
|
675
699
|
export function renderLayerASummaries(summaries, language = 'en') {
|
|
676
700
|
if (!summaries || typeof summaries !== 'object') return '';
|
|
677
|
-
const
|
|
701
|
+
const effectiveLang = normalizePromptLanguage(language);
|
|
702
|
+
const headers = LAYER_A_HEADERS[effectiveLang] || LAYER_A_HEADERS.en;
|
|
678
703
|
const out = [];
|
|
679
704
|
for (const key of ['user', 'group', 'vp']) {
|
|
680
705
|
const body = typeof summaries[key] === 'string' ? summaries[key].trim() : '';
|
|
@@ -712,17 +737,18 @@ export function buildWorkerPrompt(params = {}) {
|
|
|
712
737
|
includeShape = true,
|
|
713
738
|
...rest
|
|
714
739
|
} = params;
|
|
740
|
+
const effectiveLang = normalizePromptLanguage(language);
|
|
715
741
|
|
|
716
742
|
const parts = [];
|
|
717
743
|
|
|
718
744
|
// Optional harness — describes the layered shape.
|
|
719
745
|
if (includeShape) {
|
|
720
|
-
const shape = getTemplate('harnessWorkerShape',
|
|
746
|
+
const shape = getTemplate('harnessWorkerShape', effectiveLang);
|
|
721
747
|
if (shape) parts.push(shape);
|
|
722
748
|
}
|
|
723
749
|
|
|
724
750
|
// Identity + Rules + Memory + Active Scope (DESIGN-PROMPT §3).
|
|
725
|
-
const baseBlock = buildSystemPrompt({ ...rest, language });
|
|
751
|
+
const baseBlock = buildSystemPrompt({ ...rest, language: effectiveLang });
|
|
726
752
|
if (baseBlock) parts.push(baseBlock);
|
|
727
753
|
|
|
728
754
|
return parts.join('\n\n');
|
|
@@ -739,7 +765,8 @@ export function buildWorkerPrompt(params = {}) {
|
|
|
739
765
|
*/
|
|
740
766
|
export function renderPriorPlan(priorPlan, language = 'en') {
|
|
741
767
|
if (!priorPlan || typeof priorPlan !== 'object') return '';
|
|
742
|
-
const
|
|
768
|
+
const effectiveLang = normalizePromptLanguage(language);
|
|
769
|
+
const header = effectiveLang === 'zh' ? '## 上一轮 plan' : '## prior_plan';
|
|
743
770
|
const lines = [];
|
|
744
771
|
if (priorPlan.vpId) lines.push(`vpId: ${priorPlan.vpId}`);
|
|
745
772
|
const fq = priorPlan.forwardQuery;
|
|
@@ -780,17 +807,18 @@ export function renderPriorPlan(priorPlan, language = 'en') {
|
|
|
780
807
|
*/
|
|
781
808
|
export function buildRouterPrompt(params = {}) {
|
|
782
809
|
const { language = 'en', summaries, routerContext, priorPlan, includeShape = true } = params;
|
|
810
|
+
const effectiveLang = normalizePromptLanguage(language);
|
|
783
811
|
const parts = [];
|
|
784
812
|
|
|
785
813
|
if (includeShape) {
|
|
786
|
-
const shape = getTemplate('harnessRouterShape',
|
|
814
|
+
const shape = getTemplate('harnessRouterShape', effectiveLang);
|
|
787
815
|
if (shape) parts.push(shape);
|
|
788
816
|
}
|
|
789
817
|
|
|
790
|
-
const summaryBlock = renderLayerASummaries(summaries,
|
|
818
|
+
const summaryBlock = renderLayerASummaries(summaries, effectiveLang);
|
|
791
819
|
if (summaryBlock) parts.push(summaryBlock);
|
|
792
820
|
|
|
793
|
-
const priorBlock = renderPriorPlan(priorPlan,
|
|
821
|
+
const priorBlock = renderPriorPlan(priorPlan, effectiveLang);
|
|
794
822
|
if (priorBlock) parts.push(priorBlock);
|
|
795
823
|
|
|
796
824
|
if (typeof routerContext === 'string' && routerContext.trim()) {
|
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
|
@@ -41,6 +41,69 @@ import { DEFAULT_CONTEXT_WINDOW } from '../models.js';
|
|
|
41
41
|
const TOOL_RESULT_CAP_RATIO = 0.10;
|
|
42
42
|
const TOOL_RESULT_MIN_CAP = 8 * 1024;
|
|
43
43
|
|
|
44
|
+
function normalizeLanguage(language) {
|
|
45
|
+
return String(language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function localizeVisibleText(value, language, toolName) {
|
|
49
|
+
const lang = normalizeLanguage(language);
|
|
50
|
+
if (typeof value === 'function') return localizeVisibleText(value(lang), lang, toolName);
|
|
51
|
+
if (value && typeof value === 'object') {
|
|
52
|
+
const picked = value[lang] || value[lang === 'zh' ? 'zh-CN' : 'en-US'] || value.en || value.default;
|
|
53
|
+
if (typeof picked === 'string') return picked;
|
|
54
|
+
}
|
|
55
|
+
const text = typeof value === 'string' ? value : String(value || '');
|
|
56
|
+
if (lang !== 'zh') return text;
|
|
57
|
+
if (!text.trim()) return text;
|
|
58
|
+
return [
|
|
59
|
+
`工具说明:${toolName || '该工具'}。请严格按照 schema 调用;工具名、参数名、JSON key 和枚举值保持英文,不要翻译。`,
|
|
60
|
+
`原始协议说明(英文,供精确调用参考):${text}`,
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
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
|
+
*/
|
|
90
|
+
function localizeParameters(parameters, language, toolName) {
|
|
91
|
+
const lang = normalizeLanguage(language);
|
|
92
|
+
if (lang !== 'zh' || !parameters || typeof parameters !== 'object') return parameters;
|
|
93
|
+
if (Array.isArray(parameters)) return parameters.map(v => localizeParameters(v, lang, toolName));
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
96
|
+
if (key === 'description' && typeof value === 'string') {
|
|
97
|
+
out[key] = localizeVisibleText(value, lang, toolName);
|
|
98
|
+
} else if (value && typeof value === 'object') {
|
|
99
|
+
out[key] = localizeParameters(value, lang, toolName);
|
|
100
|
+
} else {
|
|
101
|
+
out[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
44
107
|
/**
|
|
45
108
|
* Per-tool execution timeout (ms).
|
|
46
109
|
*
|
|
@@ -99,7 +162,7 @@ export class ToolExecutionTimeoutError extends Error {
|
|
|
99
162
|
* @param {{ contextWindow?: number, toolName: string }} opts
|
|
100
163
|
* @returns {string}
|
|
101
164
|
*/
|
|
102
|
-
export function truncateToolResultIfNeeded(output, { contextWindow, toolName }) {
|
|
165
|
+
export function truncateToolResultIfNeeded(output, { contextWindow, toolName, language } = {}) {
|
|
103
166
|
let text;
|
|
104
167
|
if (typeof output === 'string') {
|
|
105
168
|
text = output;
|
|
@@ -116,7 +179,9 @@ export function truncateToolResultIfNeeded(output, { contextWindow, toolName })
|
|
|
116
179
|
const cap = Math.max(TOOL_RESULT_MIN_CAP, Math.floor(ctx * TOOL_RESULT_CAP_RATIO));
|
|
117
180
|
if (text.length <= cap) return text;
|
|
118
181
|
const head = text.slice(0, cap);
|
|
119
|
-
const marker =
|
|
182
|
+
const marker = normalizeLanguage(language) === 'zh'
|
|
183
|
+
? `\n\n[已截断:${toolName} 返回 ${formatSize(text.length)},上限为 ${formatSize(cap)};模型不会看到剩余内容]`
|
|
184
|
+
: `\n\n[truncated: ${toolName} returned ${formatSize(text.length)}, capped at ${formatSize(cap)}; the model will not see the rest of this output]`;
|
|
120
185
|
return head + marker;
|
|
121
186
|
}
|
|
122
187
|
|
|
@@ -213,13 +278,15 @@ export class ToolRegistry {
|
|
|
213
278
|
/**
|
|
214
279
|
* Get tool definitions for the LLM adapter.
|
|
215
280
|
* Returns all registered tools — mode filtering was removed in task-297.
|
|
281
|
+
* @param {string} [language='en']
|
|
216
282
|
* @returns {{ name: string, description: string, parameters: object }[]}
|
|
217
283
|
*/
|
|
218
|
-
getToolDefs() {
|
|
284
|
+
getToolDefs(language = 'en') {
|
|
285
|
+
const lang = normalizeLanguage(language);
|
|
219
286
|
return this.getAllTools().map(t => ({
|
|
220
287
|
name: t.name,
|
|
221
|
-
description: t.description,
|
|
222
|
-
parameters: t.parameters,
|
|
288
|
+
description: localizeVisibleText(t.description, lang, t.name),
|
|
289
|
+
parameters: localizeParameters(t.parameters, lang, t.name),
|
|
223
290
|
}));
|
|
224
291
|
}
|
|
225
292
|
|
|
@@ -264,6 +331,7 @@ export class ToolRegistry {
|
|
|
264
331
|
return truncateToolResultIfNeeded(output, {
|
|
265
332
|
contextWindow: ctx.contextWindow,
|
|
266
333
|
toolName: name,
|
|
334
|
+
language: ctx.config?.language,
|
|
267
335
|
});
|
|
268
336
|
}
|
|
269
337
|
|