@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.444",
3
+ "version": "0.1.446",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
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
- // ─── Prompt Templates ─────────────────────────────────────────
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
- lang.identity,
71
- lang.mode(mode),
72
- lang.date(new Date().toISOString().split('T')[0]),
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
- parts.push(lang.work);
192
+ const workerTemplate = getTemplate('modeWorker', effectiveLang);
193
+ parts.push(workerTemplate || lang.work);
77
194
  } else if (mode === 'dream') {
78
- parts.push(lang.dream);
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
  }
@@ -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 — signal turn end ──
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
  }