@yeaft/webchat-agent 0.1.444 → 0.1.446
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/prompts.js +142 -12
- package/unify/web-bridge.js +23 -1
package/package.json
CHANGED
package/unify/prompts.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Single source of truth for system prompts. Both engine.js and cli.js
|
|
5
5
|
* import buildSystemPrompt() from here. Supports 'en' and 'zh'.
|
|
6
6
|
*
|
|
7
|
+
* Template files from agent/unify/templates/ are loaded once at startup
|
|
8
|
+
* and used to enrich the system prompt beyond the hardcoded fallbacks.
|
|
9
|
+
*
|
|
7
10
|
* Phase 2 additions:
|
|
8
11
|
* - Memory section (user profile + recalled entries)
|
|
9
12
|
* - Compact summary section (conversation history summary)
|
|
@@ -11,7 +14,99 @@
|
|
|
11
14
|
* Reference: yeaft-unify-system-prompt-budget.md — Static + Dynamic + Context layers
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
import { readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { join, dirname } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
// ─── Template Loading (one-time at startup) ──────────────────────
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const TEMPLATES_DIR = join(__dirname, 'templates');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a template file from the templates/ directory.
|
|
28
|
+
* Returns empty string if file doesn't exist or can't be read.
|
|
29
|
+
* @param {string} name — filename (e.g. 'base.md')
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function readTemplate(name) {
|
|
33
|
+
const path = join(TEMPLATES_DIR, name);
|
|
34
|
+
if (!existsSync(path)) return '';
|
|
35
|
+
try {
|
|
36
|
+
return readFileSync(path, 'utf8').trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract the section for a given language from a bilingual template.
|
|
44
|
+
* Templates use `<!-- lang:en -->` / `<!-- lang:zh -->` HTML comment markers
|
|
45
|
+
* to delimit language sections. Returns the content between the matching
|
|
46
|
+
* marker and the next marker (or EOF).
|
|
47
|
+
*
|
|
48
|
+
* If no markers exist, returns the full content regardless of language.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} content — full template content
|
|
51
|
+
* @param {string} language — 'en' or 'zh'
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function extractLangSection(content, language) {
|
|
55
|
+
if (!content) return '';
|
|
56
|
+
|
|
57
|
+
const marker = `<!-- lang:${language} -->`;
|
|
58
|
+
const markerIdx = content.indexOf(marker);
|
|
59
|
+
|
|
60
|
+
if (markerIdx === -1) {
|
|
61
|
+
// No marker for this language — if language is 'zh', try 'en' fallback
|
|
62
|
+
if (language === 'zh') {
|
|
63
|
+
const enMarker = '<!-- lang:en -->';
|
|
64
|
+
const enIdx = content.indexOf(enMarker);
|
|
65
|
+
if (enIdx !== -1) {
|
|
66
|
+
// Has en marker but no zh — return en section as fallback
|
|
67
|
+
return extractLangSection(content, 'en');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// No markers at all — return full content
|
|
71
|
+
if (!content.includes('<!-- lang:')) return content;
|
|
72
|
+
// Has markers but not for this language — fallback to en
|
|
73
|
+
return extractLangSection(content, 'en');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract from after the marker to the next <!-- lang: marker or EOF
|
|
77
|
+
const sectionStart = markerIdx + marker.length;
|
|
78
|
+
const nextMarkerIdx = content.indexOf('<!-- lang:', sectionStart);
|
|
79
|
+
|
|
80
|
+
if (nextMarkerIdx === -1) {
|
|
81
|
+
// This is the last section — take everything after the marker
|
|
82
|
+
return content.slice(sectionStart).trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return content.slice(sectionStart, nextMarkerIdx).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Loaded templates — read once at module load time. */
|
|
89
|
+
const RAW_TEMPLATES = {
|
|
90
|
+
base: readTemplate('base.md'),
|
|
91
|
+
modeChat: readTemplate('mode-chat.md'),
|
|
92
|
+
modeWorker: readTemplate('mode-worker.md'),
|
|
93
|
+
modeDream: readTemplate('mode-dream.md'),
|
|
94
|
+
toolGuidance: readTemplate('tool-guidance.md'),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a template section for the given language.
|
|
99
|
+
* @param {string} key — template key (e.g. 'base', 'modeChat')
|
|
100
|
+
* @param {string} language — 'en' or 'zh'
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function getTemplate(key, language) {
|
|
104
|
+
const raw = RAW_TEMPLATES[key];
|
|
105
|
+
if (!raw) return '';
|
|
106
|
+
return extractLangSection(raw, language);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Prompt Templates (hardcoded fallbacks) ──────────────────────
|
|
15
110
|
|
|
16
111
|
const PROMPTS = {
|
|
17
112
|
en: {
|
|
@@ -46,12 +141,22 @@ export const SUPPORTED_LANGUAGES = Object.keys(PROMPTS);
|
|
|
46
141
|
/**
|
|
47
142
|
* Build the system prompt for a given language and mode.
|
|
48
143
|
*
|
|
144
|
+
* Prompt structure:
|
|
145
|
+
* 1. Core identity (from template or fallback)
|
|
146
|
+
* 2. Mode + date metadata
|
|
147
|
+
* 3. Mode-specific behavioral instructions (from template or fallback)
|
|
148
|
+
* 4. Tool list + tool guidance (from template)
|
|
149
|
+
* 5. Skills section
|
|
150
|
+
* 6. Memory section
|
|
151
|
+
* 7. Compact summary section
|
|
152
|
+
*
|
|
49
153
|
* @param {{
|
|
50
154
|
* language?: string,
|
|
51
155
|
* mode?: string,
|
|
52
156
|
* toolNames?: string[],
|
|
53
157
|
* memory?: { profile?: string, entries?: object[] },
|
|
54
|
-
* compactSummary?: string
|
|
158
|
+
* compactSummary?: string,
|
|
159
|
+
* skillContent?: string,
|
|
55
160
|
* }} params
|
|
56
161
|
* @returns {string}
|
|
57
162
|
*/
|
|
@@ -65,29 +170,54 @@ export function buildSystemPrompt({
|
|
|
65
170
|
} = {}) {
|
|
66
171
|
// Fallback to English for unknown languages
|
|
67
172
|
const lang = PROMPTS[language] || PROMPTS.en;
|
|
173
|
+
const effectiveLang = PROMPTS[language] ? language : 'en';
|
|
68
174
|
|
|
69
|
-
const parts = [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
175
|
+
const parts = [];
|
|
176
|
+
|
|
177
|
+
// ─── 1. Core Identity ──────────────────────────────────
|
|
178
|
+
// Use template if available, otherwise fallback to hardcoded one-liner
|
|
179
|
+
const baseTemplate = getTemplate('base', effectiveLang);
|
|
180
|
+
if (baseTemplate) {
|
|
181
|
+
parts.push(baseTemplate);
|
|
182
|
+
} else {
|
|
183
|
+
parts.push(lang.identity);
|
|
184
|
+
}
|
|
74
185
|
|
|
186
|
+
// ─── 2. Mode + Date Metadata ───────────────────────────
|
|
187
|
+
parts.push(lang.mode(mode));
|
|
188
|
+
parts.push(lang.date(new Date().toISOString().split('T')[0]));
|
|
189
|
+
|
|
190
|
+
// ─── 3. Mode-Specific Instructions ─────────────────────
|
|
75
191
|
if (mode === 'work') {
|
|
76
|
-
|
|
192
|
+
const workerTemplate = getTemplate('modeWorker', effectiveLang);
|
|
193
|
+
parts.push(workerTemplate || lang.work);
|
|
77
194
|
} else if (mode === 'dream') {
|
|
78
|
-
|
|
195
|
+
const dreamTemplate = getTemplate('modeDream', effectiveLang);
|
|
196
|
+
parts.push(dreamTemplate || lang.dream);
|
|
197
|
+
} else if (mode === 'chat') {
|
|
198
|
+
const chatTemplate = getTemplate('modeChat', effectiveLang);
|
|
199
|
+
if (chatTemplate) {
|
|
200
|
+
parts.push(chatTemplate);
|
|
201
|
+
}
|
|
202
|
+
// No fallback needed — chat mode previously had no instructions
|
|
79
203
|
}
|
|
80
204
|
|
|
205
|
+
// ─── 4. Tools + Tool Guidance ──────────────────────────
|
|
81
206
|
if (toolNames.length > 0) {
|
|
82
207
|
parts.push(lang.tools(toolNames.join(', ')));
|
|
208
|
+
|
|
209
|
+
const toolGuidanceTemplate = getTemplate('toolGuidance', effectiveLang);
|
|
210
|
+
if (toolGuidanceTemplate) {
|
|
211
|
+
parts.push(toolGuidanceTemplate);
|
|
212
|
+
}
|
|
83
213
|
}
|
|
84
214
|
|
|
85
|
-
// ─── Skills Section
|
|
215
|
+
// ─── 5. Skills Section ─────────────────────────────────
|
|
86
216
|
if (skillContent) {
|
|
87
217
|
parts.push(skillContent);
|
|
88
218
|
}
|
|
89
219
|
|
|
90
|
-
// ─── Memory Section
|
|
220
|
+
// ─── 6. Memory Section ─────────────────────────────────
|
|
91
221
|
if (memory && (memory.profile || (memory.entries && memory.entries.length > 0))) {
|
|
92
222
|
const memoryParts = [lang.memoryHeader];
|
|
93
223
|
|
|
@@ -106,7 +236,7 @@ export function buildSystemPrompt({
|
|
|
106
236
|
parts.push(memoryParts.join('\n\n'));
|
|
107
237
|
}
|
|
108
238
|
|
|
109
|
-
// ─── Compact Summary Section
|
|
239
|
+
// ─── 7. Compact Summary Section ────────────────────────
|
|
110
240
|
if (compactSummary) {
|
|
111
241
|
parts.push(`${lang.compactHeader}\n${compactSummary}`);
|
|
112
242
|
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -32,6 +32,11 @@ let unifyConversationId = null;
|
|
|
32
32
|
/** Current query mode: 'chat' or 'work' */
|
|
33
33
|
let currentMode = 'chat';
|
|
34
34
|
|
|
35
|
+
/** Accumulated conversation messages for context continuity across queries.
|
|
36
|
+
* Each entry is { role: 'user'|'assistant', content: string|Array }.
|
|
37
|
+
* Cleared on session reset or consolidation. */
|
|
38
|
+
let conversationMessages = [];
|
|
39
|
+
|
|
35
40
|
/** Whether we've already sent a permission warning to the UI */
|
|
36
41
|
let _permissionDiagnosticSent = false;
|
|
37
42
|
|
|
@@ -133,10 +138,14 @@ export async function handleUnifyChat(msg) {
|
|
|
133
138
|
resetQueryTimer();
|
|
134
139
|
|
|
135
140
|
try {
|
|
141
|
+
// ─── Collect assistant response for conversation history ──
|
|
142
|
+
let assistantTextParts = [];
|
|
143
|
+
|
|
136
144
|
// ─── Stream Engine events → claude_output format ──
|
|
137
145
|
for await (const event of session.engine.query({
|
|
138
146
|
prompt,
|
|
139
147
|
mode: currentMode,
|
|
148
|
+
messages: conversationMessages,
|
|
140
149
|
signal: currentAbort.signal,
|
|
141
150
|
})) {
|
|
142
151
|
// Reset timeout on every event — activity means the query is alive
|
|
@@ -144,6 +153,7 @@ export async function handleUnifyChat(msg) {
|
|
|
144
153
|
switch (event.type) {
|
|
145
154
|
// ── Text streaming ──
|
|
146
155
|
case 'text_delta':
|
|
156
|
+
assistantTextParts.push(event.text);
|
|
147
157
|
sendUnifyOutput({
|
|
148
158
|
type: 'assistant',
|
|
149
159
|
message: {
|
|
@@ -232,6 +242,9 @@ export async function handleUnifyChat(msg) {
|
|
|
232
242
|
|
|
233
243
|
// ── Context consolidation ──
|
|
234
244
|
case 'consolidate':
|
|
245
|
+
// Engine has compressed the context — clear our accumulated history.
|
|
246
|
+
// The engine's compactSummary will provide context on next query.
|
|
247
|
+
conversationMessages = [];
|
|
235
248
|
sendUnifyEvent({
|
|
236
249
|
type: 'consolidate',
|
|
237
250
|
archivedCount: event.archivedCount,
|
|
@@ -303,7 +316,15 @@ export async function handleUnifyChat(msg) {
|
|
|
303
316
|
}
|
|
304
317
|
}
|
|
305
318
|
|
|
306
|
-
// ─── Query complete —
|
|
319
|
+
// ─── Query complete — accumulate messages for context continuity ──
|
|
320
|
+
conversationMessages.push({ role: 'user', content: prompt });
|
|
321
|
+
|
|
322
|
+
const fullText = assistantTextParts.join('');
|
|
323
|
+
if (fullText) {
|
|
324
|
+
conversationMessages.push({ role: 'assistant', content: fullText });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Signal turn end to UI ──
|
|
307
328
|
// Finish any streaming text
|
|
308
329
|
sendUnifyOutput({
|
|
309
330
|
type: 'assistant',
|
|
@@ -426,4 +447,5 @@ export async function resetUnifySession() {
|
|
|
426
447
|
}
|
|
427
448
|
unifyConversationId = null;
|
|
428
449
|
currentMode = 'chat';
|
|
450
|
+
conversationMessages = [];
|
|
429
451
|
}
|