@yeaft/webchat-agent 0.1.633 → 0.1.634

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.633",
3
+ "version": "0.1.634",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@ import { withDreamMarker } from './state.js';
26
26
  import { batchSourcesForApply, needsBatchedApply, truncateMessage } from './segment.js';
27
27
  import { snapshotScope } from './snapshot.js';
28
28
  import { parseJsonSafe } from './triage.js';
29
+ import { render } from './prompts/index.js';
29
30
 
30
31
  const SYSTEM = `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.`;
31
32
 
@@ -42,53 +43,17 @@ const SYSTEM = `You are the Apply stage of a dream pipeline. You rewrite a singl
42
43
  * }} ctx
43
44
  */
44
45
  export function buildUpdatePrompt(ctx) {
45
- const lines = [];
46
- lines.push('You are updating an existing memory scope.');
47
- lines.push('');
48
- lines.push(`Scope: ${ctx.target}`);
49
- if (ctx.batchInfo && ctx.batchInfo.total > 1) {
50
- lines.push(`This is batch ${ctx.batchInfo.index} of ${ctx.batchInfo.total}.`);
51
- lines.push('Earlier batches have already been folded into the current memory.md below.');
52
- }
53
- lines.push('');
54
- lines.push('Current memory.md:');
55
- lines.push('"""');
56
- lines.push(ctx.memoryMd || '');
57
- lines.push('"""');
58
- lines.push('');
59
- lines.push('Current summary.md:');
60
- lines.push('"""');
61
- lines.push(ctx.summaryMd || '');
62
- lines.push('"""');
63
- lines.push('');
64
- lines.push('Recent conversations:');
65
- for (const src of (ctx.sources || [])) {
66
- lines.push('');
67
- lines.push(`[group/${src.groupId}]`);
68
- for (const m of (src.diff || [])) {
69
- const head = `[${m.role || 'message'}${m.kind === 'overlap' ? ' (already processed)' : ''}]`;
70
- lines.push(head);
71
- lines.push(truncateMessage(m.body || ''));
72
- }
73
- }
74
- lines.push('');
75
- lines.push('Task:');
76
- lines.push('- Extract from these conversations what is relevant to THIS scope.');
77
- lines.push('- Integrate it into memory.md (reorganize sections if needed).');
78
- lines.push('- Drop stale or contradicted entries.');
79
- lines.push('- Rewrite summary.md (1–3 sentences).');
80
- lines.push('- The same conversations are being processed for OTHER scopes too.');
81
- lines.push(' Only handle what is relevant here. Ignore the rest.');
82
- lines.push('');
83
- lines.push('Hard rules:');
84
- lines.push('- Never read or reference any other scope\'s files.');
85
- lines.push('- Never modify VP system-prompt, group charter, or user preferences.');
86
- lines.push('- If something contradicts a charter, annotate with');
87
- lines.push(' "⚠️ contradicts charter — verify which is current" and continue.');
88
- lines.push('');
89
- lines.push('Reply with strict JSON of the shape:');
90
- lines.push('{ "memory_md": "...", "summary_md": "..." }');
91
- return lines.join('\n');
46
+ const batchHeader = (ctx.batchInfo && ctx.batchInfo.total > 1)
47
+ ? `This is batch ${ctx.batchInfo.index} of ${ctx.batchInfo.total}.\nEarlier batches have already been folded into the current memory.md below.\n`
48
+ : '';
49
+ const sources = renderSourceBlocks(ctx.sources);
50
+ return render('update', {
51
+ target: ctx.target,
52
+ batchHeader,
53
+ memoryMd: ctx.memoryMd || '',
54
+ summaryMd: ctx.summaryMd || '',
55
+ sources,
56
+ });
92
57
  }
93
58
 
94
59
  /**
@@ -102,36 +67,40 @@ export function buildUpdatePrompt(ctx) {
102
67
  * }} ctx
103
68
  */
104
69
  export function buildCreatePrompt(ctx) {
105
- const lines = [];
106
- lines.push('You are creating a new memory scope from scratch.');
107
- lines.push('');
108
- lines.push(`Scope path: ${ctx.target} (must be ≤2 levels)`);
109
- lines.push('');
110
- lines.push('Source conversations:');
111
- for (const src of (ctx.sources || [])) {
70
+ const sources = renderSourceBlocks(ctx.sources);
71
+ let siblingsBlock = '';
72
+ if (ctx.siblingTopics && ctx.siblingTopics.length > 0) {
73
+ const lines = ['For tone reference, sibling/parent topic summaries:'];
74
+ for (const t of ctx.siblingTopics) lines.push(` - ${t.path}: ${oneLine(t.summary)}`);
112
75
  lines.push('');
113
- lines.push(`[group/${src.groupId}]`);
76
+ siblingsBlock = lines.join('\n');
77
+ }
78
+ return render('create', {
79
+ target: ctx.target,
80
+ sources,
81
+ siblingsBlock,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Render a list of `(groupId, diff)` source blocks for inclusion in the
87
+ * update / create prompts. Single-source aware: omits leading blank line
88
+ * if there's only one source, to keep small prompts compact.
89
+ *
90
+ * @param {Array<{ groupId: string, diff: Array<object> }>} sources
91
+ */
92
+ function renderSourceBlocks(sources) {
93
+ const out = [];
94
+ for (const src of (sources || [])) {
95
+ out.push('');
96
+ out.push(`[group/${src.groupId}]`);
114
97
  for (const m of (src.diff || [])) {
115
98
  const head = `[${m.role || 'message'}${m.kind === 'overlap' ? ' (already processed)' : ''}]`;
116
- lines.push(head);
117
- lines.push(truncateMessage(m.body || ''));
99
+ out.push(head);
100
+ out.push(truncateMessage(m.body || ''));
118
101
  }
119
102
  }
120
- lines.push('');
121
- if (ctx.siblingTopics && ctx.siblingTopics.length > 0) {
122
- lines.push('For tone reference, sibling/parent topic summaries:');
123
- for (const t of ctx.siblingTopics) {
124
- lines.push(` - ${t.path}: ${oneLine(t.summary)}`);
125
- }
126
- lines.push('');
127
- }
128
- lines.push('Task:');
129
- lines.push('1. Write memory.md from scratch with reasonable section structure.');
130
- lines.push('2. Write summary.md (1–3 sentences).');
131
- lines.push('');
132
- lines.push('Reply with strict JSON of the shape:');
133
- lines.push('{ "memory_md": "...", "summary_md": "..." }');
134
- return lines.join('\n');
103
+ return out.join('\n').replace(/^\n/, '');
135
104
  }
136
105
 
137
106
  /**
@@ -0,0 +1,52 @@
1
+ /**
2
+ * dream-v2/prompts/index.js — load + render the four dream prompts.
3
+ *
4
+ * Prompts live as `.md` files in this directory. We read once and cache.
5
+ * Rendering is plain `{{name}}` substitution; no nested logic, no escaping —
6
+ * the prompts are inputs to an LLM and the templates are author-controlled.
7
+ */
8
+
9
+ import { readFileSync } from 'fs';
10
+ import { dirname, join } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ const FILES = {
16
+ triagePass1: 'triage-pass1.md',
17
+ triagePass2: 'triage-pass2.md',
18
+ update: 'update.md',
19
+ create: 'create.md',
20
+ };
21
+
22
+ /** @type {Record<string, string>} */
23
+ const cache = {};
24
+
25
+ function load(name) {
26
+ if (cache[name]) return cache[name];
27
+ const file = FILES[name];
28
+ if (!file) throw new Error(`prompts: unknown template ${name}`);
29
+ const txt = readFileSync(join(__dirname, file), 'utf8');
30
+ cache[name] = txt;
31
+ return txt;
32
+ }
33
+
34
+ /**
35
+ * Render a template with `{{name}}` substitution. Missing keys throw —
36
+ * silent rendering of half-built prompts has bitten us before.
37
+ *
38
+ * @param {string} name
39
+ * @param {Record<string, string>} vars
40
+ */
41
+ export function render(name, vars) {
42
+ const tpl = load(name);
43
+ return tpl.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
44
+ if (!(key in vars)) throw new Error(`prompts.${name}: missing var ${key}`);
45
+ return vars[key];
46
+ });
47
+ }
48
+
49
+ /** Test-only: drop the in-memory cache so a fresh read re-loads from disk. */
50
+ export function _resetCache() {
51
+ for (const k of Object.keys(cache)) delete cache[k];
52
+ }
@@ -37,6 +37,7 @@
37
37
 
38
38
  import { isValidTopic } from '../memory/store-v2.js';
39
39
  import { truncateMessage } from './segment.js';
40
+ import { render } from './prompts/index.js';
40
41
 
41
42
  const SYSTEM = `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.`;
42
43
 
@@ -87,38 +88,21 @@ export function applyHardRules({ groupId, messages }) {
87
88
  * @param {{ groupId: string, messages: Array<object>, topicSummaries: Array<{ path: string, summary: string }> }} ctx
88
89
  */
89
90
  export function buildPass1Prompt(ctx) {
90
- const lines = [
91
- 'You are deciding whether a recent group conversation carries:',
92
- ' - signals that should update the USER profile, and/or',
93
- ' - signals that should update one or more TOPIC scopes.',
94
- '',
95
- 'Do NOT mention vp/, group/, or feature/ scopes — those are handled by hard rules.',
96
- '',
97
- `Group: ${ctx.groupId}`,
98
- '',
99
- 'Existing topic scopes (path — summary):',
100
- ];
101
- if (!ctx.topicSummaries || ctx.topicSummaries.length === 0) {
102
- lines.push(' (none)');
103
- } else {
104
- for (const t of ctx.topicSummaries) {
105
- lines.push(` - ${t.path} — ${oneLine(t.summary)}`);
106
- }
107
- }
108
- lines.push('', 'Conversation:');
91
+ const topicSummaries = (!ctx.topicSummaries || ctx.topicSummaries.length === 0)
92
+ ? ' (none)'
93
+ : ctx.topicSummaries.map(t => ` - ${t.path} ${oneLine(t.summary)}`).join('\n');
94
+ const conv = [];
109
95
  for (const m of (ctx.messages || [])) {
110
96
  const head = `[${m.role || 'message'}${m.kind === 'overlap' ? ' (already processed)' : ''}]`;
111
- lines.push(head);
112
- lines.push(truncateMessage(m.body || ''));
113
- lines.push('');
97
+ conv.push(head);
98
+ conv.push(truncateMessage(m.body || ''));
99
+ conv.push('');
114
100
  }
115
- lines.push('Respond with strict JSON of the shape:');
116
- lines.push('{');
117
- lines.push(' "user_profile_signals": boolean,');
118
- lines.push(' "topics": [ "<short category description>", ... ],');
119
- lines.push(' "trivial_only": boolean');
120
- lines.push('}');
121
- return lines.join('\n');
101
+ return render('triagePass1', {
102
+ groupId: ctx.groupId,
103
+ topicSummaries,
104
+ conversation: conv.join('\n').trimEnd(),
105
+ });
122
106
  }
123
107
 
124
108
  /**
@@ -127,30 +111,13 @@ export function buildPass1Prompt(ctx) {
127
111
  * @param {{ description: string, existingTopics: Array<{ path: string, summary: string }> }} ctx
128
112
  */
129
113
  export function buildPass2Prompt(ctx) {
130
- const lines = [
131
- 'Bind a free-form topic description to an exact path under topic/.',
132
- 'Rules:',
133
- ' - At most TWO path segments. Reject any third level.',
134
- ' - Segments may contain letters, digits, dashes, underscores, dots, CJK.',
135
- ' - Prefer matching an existing path if the description fits.',
136
- '',
137
- `Description: ${ctx.description}`,
138
- '',
139
- 'Existing topics:',
140
- ];
141
- if (!ctx.existingTopics || ctx.existingTopics.length === 0) {
142
- lines.push(' (none)');
143
- } else {
144
- for (const t of ctx.existingTopics) {
145
- lines.push(` - ${t.path} — ${oneLine(t.summary)}`);
146
- }
147
- }
148
- lines.push('');
149
- lines.push('Reply with strict JSON, exactly one of:');
150
- lines.push(' { "decision": "match", "path": "<existing path>" }');
151
- lines.push(' { "decision": "new", "path": "<new ≤2-segment path>" }');
152
- lines.push(' { "decision": "none" }');
153
- return lines.join('\n');
114
+ const existingTopics = (!ctx.existingTopics || ctx.existingTopics.length === 0)
115
+ ? ' (none)'
116
+ : ctx.existingTopics.map(t => ` - ${t.path} — ${oneLine(t.summary)}`).join('\n');
117
+ return render('triagePass2', {
118
+ description: ctx.description,
119
+ existingTopics,
120
+ });
154
121
  }
155
122
 
156
123
  /**