elsabro 6.0.0 → 7.0.0

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.
@@ -0,0 +1,414 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PartyEngine — Multi-agent collaborative discussion engine.
5
+ *
6
+ * Selects relevant agents based on topic keywords, runs round-based
7
+ * discussions with callbacks, and produces synthesis summaries.
8
+ * Reuses CheckpointManager for session persistence.
9
+ *
10
+ * Zero external dependencies. CommonJS, Node 18+.
11
+ */
12
+
13
+ const path = require('path');
14
+ const { CheckpointManager } = require('./checkpoint');
15
+
16
+ // ── Agent Personality Cards ──────────────────────────────────────────────────
17
+
18
+ const CARDS = require('./agent-cards.json');
19
+
20
+ // ── Relevance Map ────────────────────────────────────────────────────────────
21
+ // Maps keywords to agent scores. Each bucket awards points to 1-3 agents.
22
+ // Higher score = more relevant to the topic.
23
+
24
+ const RELEVANCE_MAP = {
25
+ architecture: { planner: 3, executor: 2, debugger: 1 },
26
+ testing: { qa: 3, executor: 2, verifier: 1 },
27
+ design: { 'ux-designer': 3, planner: 2, analyst: 1 },
28
+ ux: { 'ux-designer': 3, analyst: 2, 'tech-writer': 1 },
29
+ ui: { 'ux-designer': 3, executor: 2, analyst: 1 },
30
+ bug: { debugger: 3, qa: 2, executor: 1 },
31
+ debug: { debugger: 3, qa: 2, verifier: 1 },
32
+ performance: { debugger: 3, executor: 2, qa: 1 },
33
+ documentation: { 'tech-writer': 3, planner: 2, analyst: 1 },
34
+ docs: { 'tech-writer': 3, 'ux-designer': 1, planner: 1 },
35
+ planning: { planner: 3, 'scrum-master': 2, analyst: 1 },
36
+ sprint: { 'scrum-master': 3, planner: 2, qa: 1 },
37
+ scope: { analyst: 3, 'scrum-master': 2, planner: 1 },
38
+ requirements: { analyst: 3, planner: 2, 'ux-designer': 1 },
39
+ deploy: { executor: 3, qa: 2, verifier: 1 },
40
+ refactor: { executor: 3, debugger: 2, verifier: 1 },
41
+ security: { verifier: 3, debugger: 2, qa: 1 },
42
+ quality: { verifier: 3, qa: 2, 'tech-writer': 1 }
43
+ };
44
+
45
+ // Default scores when no keywords match — ensures every agent is reachable.
46
+ const DEFAULT_SCORES = {
47
+ analyst: 2, planner: 2, executor: 1,
48
+ qa: 1, debugger: 1, 'ux-designer': 1,
49
+ 'scrum-master': 1, 'tech-writer': 1, verifier: 1
50
+ };
51
+
52
+ // ── Helpers ──────────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Slugify a topic string for use as a checkpoint flowId component.
56
+ * @param {string} text
57
+ * @returns {string}
58
+ */
59
+ function slugify(text) {
60
+ return text
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, '-')
63
+ .replace(/^-+|-+$/g, '')
64
+ .slice(0, 40);
65
+ }
66
+
67
+ /**
68
+ * Detect agent names mentioned directly in topic text.
69
+ * Returns array of matching agent IDs.
70
+ * @param {string} topic
71
+ * @returns {string[]}
72
+ */
73
+ function detectAgentNames(topic) {
74
+ const lower = topic.toLowerCase();
75
+ const found = [];
76
+ for (const card of Object.values(CARDS)) {
77
+ if (lower.includes(card.name.toLowerCase())) {
78
+ found.push(card.id);
79
+ }
80
+ }
81
+ return found;
82
+ }
83
+
84
+ // ── PartyEngine ──────────────────────────────────────────────────────────────
85
+
86
+ class PartyEngine {
87
+ /**
88
+ * @param {object} [options]
89
+ * @param {string} [options.checkpointDir] – checkpoint directory
90
+ * @param {number} [options.maxRounds] – max discussion rounds (default: 3)
91
+ * @param {number} [options.maxAgents] – max agents per party (default: 3)
92
+ */
93
+ constructor(options = {}) {
94
+ this.maxRounds = options.maxRounds || 3;
95
+ this.maxAgents = options.maxAgents || 3;
96
+ this.cards = { ...CARDS };
97
+ this.checkpoint = new CheckpointManager({
98
+ dir: options.checkpointDir
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Select the most relevant agents for a topic.
104
+ *
105
+ * Algorithm:
106
+ * 1. Tokenize topic (lowercase, split on whitespace)
107
+ * 2. Match tokens against RELEVANCE_MAP keys
108
+ * 3. Accumulate scores (each keyword scores once per agent)
109
+ * 4. Break ties by first matching keyword position
110
+ * 5. Respect manual overrides (options.agents)
111
+ * 6. Detect agent names in topic → force inclusion
112
+ *
113
+ * @param {string} topic
114
+ * @param {object} [options]
115
+ * @param {string[]} [options.agents] – manual agent IDs (bypass scoring)
116
+ * @param {number} [options.maxAgents] – override instance maxAgents
117
+ * @returns {object[]} array of agent card objects
118
+ */
119
+ selectAgents(topic, options = {}) {
120
+ const max = options.maxAgents || this.maxAgents;
121
+
122
+ // Manual override: bypass scoring entirely
123
+ if (options.agents && options.agents.length > 0) {
124
+ return options.agents
125
+ .map(id => this.cards[id])
126
+ .filter(Boolean)
127
+ .slice(0, max);
128
+ }
129
+
130
+ // Detect agent names mentioned in topic
131
+ const forced = detectAgentNames(topic || '');
132
+
133
+ // Tokenize
134
+ const tokens = (topic || '').toLowerCase().split(/\s+/).filter(Boolean);
135
+
136
+ // Score accumulation
137
+ const scores = {};
138
+ const firstMatch = {}; // track first keyword position for tie-breaking
139
+
140
+ for (let i = 0; i < tokens.length; i++) {
141
+ const token = tokens[i];
142
+ const bucket = RELEVANCE_MAP[token];
143
+ if (!bucket) continue;
144
+
145
+ for (const [agentId, score] of Object.entries(bucket)) {
146
+ // Each keyword scores once per agent (no duplicates from repeated tokens)
147
+ const key = `${agentId}:${token}`;
148
+ if (!scores[`_seen_${key}`]) {
149
+ scores[`_seen_${key}`] = true;
150
+ scores[agentId] = (scores[agentId] || 0) + score;
151
+ if (firstMatch[agentId] === undefined) {
152
+ firstMatch[agentId] = i;
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ // If no keywords matched, use defaults
159
+ const hasMatches = Object.keys(scores).some(k => !k.startsWith('_seen_'));
160
+ if (!hasMatches) {
161
+ for (const [id, score] of Object.entries(DEFAULT_SCORES)) {
162
+ scores[id] = score;
163
+ firstMatch[id] = 999;
164
+ }
165
+ }
166
+
167
+ // Build sorted agent list
168
+ const agentIds = Object.keys(this.cards);
169
+ const ranked = agentIds
170
+ .filter(id => scores[id] > 0)
171
+ .sort((a, b) => {
172
+ // Primary: higher score first
173
+ const scoreDiff = (scores[b] || 0) - (scores[a] || 0);
174
+ if (scoreDiff !== 0) return scoreDiff;
175
+ // Tiebreak: earlier first-match position
176
+ return (firstMatch[a] || 999) - (firstMatch[b] || 999);
177
+ });
178
+
179
+ // Merge forced agents (from name detection) with scored ranking
180
+ const result = [];
181
+ const added = new Set();
182
+
183
+ // Forced agents first
184
+ for (const id of forced) {
185
+ if (this.cards[id] && !added.has(id)) {
186
+ result.push(this.cards[id]);
187
+ added.add(id);
188
+ }
189
+ }
190
+
191
+ // Fill remaining from ranked
192
+ for (const id of ranked) {
193
+ if (added.has(id)) continue;
194
+ if (result.length >= max) break;
195
+ result.push(this.cards[id]);
196
+ added.add(id);
197
+ }
198
+
199
+ return result.slice(0, max);
200
+ }
201
+
202
+ /**
203
+ * Run a full party discussion session.
204
+ *
205
+ * @param {string} topic – discussion topic
206
+ * @param {object} [options] – { agents, maxRounds, maxAgents, projectContext }
207
+ * @param {object} callbacks
208
+ * @param {function} callbacks.onAgentTurn – (params) => string response
209
+ * @param {function} [callbacks.onSynthesize] – (params) => { consensus, debates, actionItems, keyInsights, suggestedNext }
210
+ * @param {function} [callbacks.onRoundComplete] – (params) => "continue"|"stop"|"add_round"
211
+ * @param {function} [callbacks.onFormatSummary] – (template, data) => string
212
+ * @returns {Promise<object>} { topic, agents, rounds, history, synthesis, sessionId }
213
+ */
214
+ async run(topic, options = {}, callbacks = {}) {
215
+ if (!callbacks.onAgentTurn) {
216
+ throw new Error('PartyEngine.run() requires callbacks.onAgentTurn');
217
+ }
218
+
219
+ const agents = this.selectAgents(topic, options);
220
+ let totalRounds = options.maxRounds || this.maxRounds;
221
+ const sessionId = `party-${slugify(topic)}-${Date.now()}`;
222
+ const history = [];
223
+
224
+ for (let round = 1; round <= totalRounds; round++) {
225
+ const roundResponses = [];
226
+
227
+ for (const agent of agents) {
228
+ const instruction = round === 1
229
+ ? 'Share your initial perspective on this topic.'
230
+ : 'React to previous perspectives and refine your position.';
231
+
232
+ const response = await callbacks.onAgentTurn({
233
+ agent,
234
+ topic,
235
+ round,
236
+ totalRounds,
237
+ history: [...history],
238
+ instruction,
239
+ projectContext: options.projectContext || null
240
+ });
241
+
242
+ const entry = {
243
+ round,
244
+ agent: { id: agent.id, name: agent.name },
245
+ response: response || '',
246
+ timestamp: new Date().toISOString()
247
+ };
248
+
249
+ history.push(entry);
250
+ roundResponses.push(entry);
251
+ }
252
+
253
+ // Checkpoint after each round
254
+ this.checkpoint.save(sessionId, {
255
+ topic,
256
+ agents: agents.map(a => a.id),
257
+ currentRound: round,
258
+ totalRounds,
259
+ history,
260
+ options
261
+ });
262
+
263
+ // Round complete callback
264
+ if (callbacks.onRoundComplete) {
265
+ const decision = await callbacks.onRoundComplete({
266
+ completedRound: round,
267
+ totalRounds,
268
+ lastResponses: roundResponses
269
+ });
270
+
271
+ if (decision === 'stop') break;
272
+ if (decision === 'add_round') totalRounds++;
273
+ }
274
+ }
275
+
276
+ // Synthesis
277
+ let synthesis = null;
278
+ if (callbacks.onSynthesize) {
279
+ synthesis = await callbacks.onSynthesize({
280
+ history,
281
+ topic,
282
+ agents
283
+ });
284
+ } else {
285
+ synthesis = { consensus: [], debates: [], actionItems: [], keyInsights: [], suggestedNext: null };
286
+ }
287
+
288
+ // Clean old checkpoints (keep last 2)
289
+ this.checkpoint.clean(sessionId, 2);
290
+
291
+ return {
292
+ topic,
293
+ agents: agents.map(a => ({ id: a.id, name: a.name })),
294
+ rounds: history.length > 0 ? history[history.length - 1].round : 0,
295
+ history,
296
+ synthesis,
297
+ sessionId
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Resume a party session from checkpoint.
303
+ *
304
+ * @param {string} sessionId – checkpoint session ID
305
+ * @param {object} callbacks – same as run()
306
+ * @returns {Promise<object>} same as run()
307
+ */
308
+ async resume(sessionId, callbacks = {}) {
309
+ if (!callbacks.onAgentTurn) {
310
+ throw new Error('PartyEngine.resume() requires callbacks.onAgentTurn');
311
+ }
312
+
313
+ const checkpoint = this.checkpoint.load(sessionId);
314
+ if (!checkpoint) {
315
+ throw new Error(`No checkpoint found for session: ${sessionId}`);
316
+ }
317
+
318
+ const { topic, agents: agentIds, currentRound, totalRounds, history, options } = checkpoint;
319
+ const agents = agentIds.map(id => this.cards[id]).filter(Boolean);
320
+ let rounds = totalRounds;
321
+ const startRound = currentRound + 1;
322
+
323
+ for (let round = startRound; round <= rounds; round++) {
324
+ const roundResponses = [];
325
+
326
+ for (const agent of agents) {
327
+ const instruction = round === 1
328
+ ? 'Share your initial perspective on this topic.'
329
+ : 'React to previous perspectives and refine your position.';
330
+
331
+ const response = await callbacks.onAgentTurn({
332
+ agent,
333
+ topic,
334
+ round,
335
+ totalRounds: rounds,
336
+ history: [...history],
337
+ instruction,
338
+ projectContext: (options && options.projectContext) || null
339
+ });
340
+
341
+ const entry = {
342
+ round,
343
+ agent: { id: agent.id, name: agent.name },
344
+ response: response || '',
345
+ timestamp: new Date().toISOString()
346
+ };
347
+
348
+ history.push(entry);
349
+ roundResponses.push(entry);
350
+ }
351
+
352
+ this.checkpoint.save(sessionId, {
353
+ topic,
354
+ agents: agentIds,
355
+ currentRound: round,
356
+ totalRounds: rounds,
357
+ history,
358
+ options
359
+ });
360
+
361
+ if (callbacks.onRoundComplete) {
362
+ const decision = await callbacks.onRoundComplete({
363
+ completedRound: round,
364
+ totalRounds: rounds,
365
+ lastResponses: roundResponses
366
+ });
367
+
368
+ if (decision === 'stop') break;
369
+ if (decision === 'add_round') rounds++;
370
+ }
371
+ }
372
+
373
+ let synthesis = null;
374
+ if (callbacks.onSynthesize) {
375
+ synthesis = await callbacks.onSynthesize({
376
+ history,
377
+ topic,
378
+ agents
379
+ });
380
+ } else {
381
+ synthesis = { consensus: [], debates: [], actionItems: [], keyInsights: [], suggestedNext: null };
382
+ }
383
+
384
+ this.checkpoint.clean(sessionId, 2);
385
+
386
+ return {
387
+ topic,
388
+ agents: agents.map(a => ({ id: a.id, name: a.name })),
389
+ rounds: history.length > 0 ? history[history.length - 1].round : 0,
390
+ history,
391
+ synthesis,
392
+ sessionId
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Get all loaded personality cards.
398
+ * @returns {object} map of agentId → card
399
+ */
400
+ getCards() {
401
+ return { ...this.cards };
402
+ }
403
+
404
+ /**
405
+ * Get a single personality card by agent ID.
406
+ * @param {string} agentId
407
+ * @returns {object|undefined}
408
+ */
409
+ getCard(agentId) {
410
+ return this.cards[agentId];
411
+ }
412
+ }
413
+
414
+ module.exports = { PartyEngine, RELEVANCE_MAP, slugify, detectAgentNames };
@@ -8,7 +8,7 @@
8
8
  * not_implemented nodes gracefully.
9
9
  */
10
10
 
11
- const { getExecutor, checkRuntimeStatus, NotImplementedError } = require('./executors');
11
+ const { getExecutor, checkRuntimeStatus, NotImplementedError, DeprecatedNodeError } = require('./executors');
12
12
 
13
13
  class RunnerError extends Error {
14
14
  constructor(message, nodeId) {
@@ -81,8 +81,8 @@ async function runFlow(engine, inputs, callbacks, checkpoint) {
81
81
  checkRuntimeStatus(node);
82
82
  result = await executor(node, context, callbacks);
83
83
  } catch (err) {
84
- // Let NotImplementedError propagate cleanly
85
- if (err instanceof NotImplementedError) {
84
+ // Let NotImplementedError and DeprecatedNodeError propagate cleanly
85
+ if (err instanceof NotImplementedError || err instanceof DeprecatedNodeError) {
86
86
  // Save a checkpoint before stopping
87
87
  if (callbacks.onCheckpoint) {
88
88
  await callbacks.onCheckpoint({
@@ -121,9 +121,9 @@ async function runFlow(engine, inputs, callbacks, checkpoint) {
121
121
  });
122
122
  }
123
123
 
124
- // 6. Notify: node completed
124
+ // 6. Notify: node completed (context passed for post-processing, e.g. dry-run fixups)
125
125
  if (callbacks.onNodeComplete) {
126
- await callbacks.onNodeComplete(node.id, result);
126
+ await callbacks.onNodeComplete(node.id, result, context);
127
127
  }
128
128
 
129
129
  // 7. Advance