@yeaft/webchat-agent 0.1.751 → 0.1.752
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/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 +4 -2
- package/unify/prompts.js +39 -11
- package/unify/tools/registry.js +45 -5
package/package.json
CHANGED
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
|
});
|
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/tools/registry.js
CHANGED
|
@@ -41,6 +41,41 @@ 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
|
+
function localizeParameters(parameters, language, toolName) {
|
|
65
|
+
const lang = normalizeLanguage(language);
|
|
66
|
+
if (lang !== 'zh' || !parameters || typeof parameters !== 'object') return parameters;
|
|
67
|
+
if (Array.isArray(parameters)) return parameters.map(v => localizeParameters(v, lang, toolName));
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
70
|
+
if (key === 'description') {
|
|
71
|
+
out[key] = localizeVisibleText(value, lang, toolName);
|
|
72
|
+
} else {
|
|
73
|
+
out[key] = localizeParameters(value, lang, toolName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
/**
|
|
45
80
|
* Per-tool execution timeout (ms).
|
|
46
81
|
*
|
|
@@ -99,7 +134,7 @@ export class ToolExecutionTimeoutError extends Error {
|
|
|
99
134
|
* @param {{ contextWindow?: number, toolName: string }} opts
|
|
100
135
|
* @returns {string}
|
|
101
136
|
*/
|
|
102
|
-
export function truncateToolResultIfNeeded(output, { contextWindow, toolName }) {
|
|
137
|
+
export function truncateToolResultIfNeeded(output, { contextWindow, toolName, language } = {}) {
|
|
103
138
|
let text;
|
|
104
139
|
if (typeof output === 'string') {
|
|
105
140
|
text = output;
|
|
@@ -116,7 +151,9 @@ export function truncateToolResultIfNeeded(output, { contextWindow, toolName })
|
|
|
116
151
|
const cap = Math.max(TOOL_RESULT_MIN_CAP, Math.floor(ctx * TOOL_RESULT_CAP_RATIO));
|
|
117
152
|
if (text.length <= cap) return text;
|
|
118
153
|
const head = text.slice(0, cap);
|
|
119
|
-
const marker =
|
|
154
|
+
const marker = normalizeLanguage(language) === 'zh'
|
|
155
|
+
? `\n\n[已截断:${toolName} 返回 ${formatSize(text.length)},上限为 ${formatSize(cap)};模型不会看到剩余内容]`
|
|
156
|
+
: `\n\n[truncated: ${toolName} returned ${formatSize(text.length)}, capped at ${formatSize(cap)}; the model will not see the rest of this output]`;
|
|
120
157
|
return head + marker;
|
|
121
158
|
}
|
|
122
159
|
|
|
@@ -213,13 +250,15 @@ export class ToolRegistry {
|
|
|
213
250
|
/**
|
|
214
251
|
* Get tool definitions for the LLM adapter.
|
|
215
252
|
* Returns all registered tools — mode filtering was removed in task-297.
|
|
253
|
+
* @param {string} [language='en']
|
|
216
254
|
* @returns {{ name: string, description: string, parameters: object }[]}
|
|
217
255
|
*/
|
|
218
|
-
getToolDefs() {
|
|
256
|
+
getToolDefs(language = 'en') {
|
|
257
|
+
const lang = normalizeLanguage(language);
|
|
219
258
|
return this.getAllTools().map(t => ({
|
|
220
259
|
name: t.name,
|
|
221
|
-
description: t.description,
|
|
222
|
-
parameters: t.parameters,
|
|
260
|
+
description: localizeVisibleText(t.description, lang, t.name),
|
|
261
|
+
parameters: localizeParameters(t.parameters, lang, t.name),
|
|
223
262
|
}));
|
|
224
263
|
}
|
|
225
264
|
|
|
@@ -264,6 +303,7 @@ export class ToolRegistry {
|
|
|
264
303
|
return truncateToolResultIfNeeded(output, {
|
|
265
304
|
contextWindow: ctx.contextWindow,
|
|
266
305
|
toolName: name,
|
|
306
|
+
language: ctx.config?.language,
|
|
267
307
|
});
|
|
268
308
|
}
|
|
269
309
|
|