web-agent-bridge 2.1.0 → 2.3.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,681 @@
1
+ /**
2
+ * Agent Symphony Orchestrator — Autonomous Multi-Agent Collaboration
3
+ *
4
+ * Coordinates specialized agents (Researcher, Negotiator, Analyst, Guardian)
5
+ * to execute complex tasks WITHOUT any external LLM dependency.
6
+ * Each agent has built-in rule engines and heuristics.
7
+ *
8
+ * Symphony Phases:
9
+ * 1. COMPOSE — Assign roles based on task type
10
+ * 2. DISCOVER — Researcher gathers site data via WAB schema
11
+ * 3. ANALYZE — Analyst evaluates options using learned preferences
12
+ * 4. NEGOTIATE — Negotiator pursues best deal terms
13
+ * 5. GUARD — Guardian validates safety & fairness
14
+ * 6. DECIDE — Final consensus assembly from all agent outputs
15
+ *
16
+ * All processing is local — no tokens consumed, no data shared externally.
17
+ */
18
+
19
+ const crypto = require('crypto');
20
+ const { db } = require('../models/db');
21
+
22
+ // ─── Schema ──────────────────────────────────────────────────────────
23
+
24
+ db.exec(`
25
+ CREATE TABLE IF NOT EXISTS symphony_compositions (
26
+ id TEXT PRIMARY KEY,
27
+ site_id TEXT NOT NULL,
28
+ task TEXT NOT NULL,
29
+ task_type TEXT NOT NULL,
30
+ status TEXT DEFAULT 'composing',
31
+ phases_completed TEXT DEFAULT '[]',
32
+ current_phase TEXT DEFAULT 'compose',
33
+ final_decision TEXT,
34
+ confidence REAL DEFAULT 0.0,
35
+ duration_ms INTEGER DEFAULT 0,
36
+ created_at TEXT DEFAULT (datetime('now')),
37
+ completed_at TEXT
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS symphony_roles (
41
+ id TEXT PRIMARY KEY,
42
+ composition_id TEXT NOT NULL,
43
+ role TEXT NOT NULL,
44
+ agent_id TEXT,
45
+ status TEXT DEFAULT 'waiting',
46
+ input TEXT DEFAULT '{}',
47
+ output TEXT DEFAULT '{}',
48
+ reasoning TEXT,
49
+ confidence REAL DEFAULT 0.0,
50
+ started_at TEXT,
51
+ completed_at TEXT,
52
+ FOREIGN KEY (composition_id) REFERENCES symphony_compositions(id) ON DELETE CASCADE
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS symphony_consensus (
56
+ id TEXT PRIMARY KEY,
57
+ composition_id TEXT NOT NULL,
58
+ votes TEXT DEFAULT '{}',
59
+ method TEXT DEFAULT 'weighted',
60
+ result TEXT DEFAULT '{}',
61
+ agreement_score REAL DEFAULT 0.0,
62
+ created_at TEXT DEFAULT (datetime('now')),
63
+ FOREIGN KEY (composition_id) REFERENCES symphony_compositions(id) ON DELETE CASCADE
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS symphony_templates (
67
+ id TEXT PRIMARY KEY,
68
+ name TEXT UNIQUE NOT NULL,
69
+ task_type TEXT NOT NULL,
70
+ roles TEXT NOT NULL,
71
+ phase_order TEXT NOT NULL,
72
+ description TEXT,
73
+ created_at TEXT DEFAULT (datetime('now'))
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_symphony_comp_site ON symphony_compositions(site_id);
77
+ CREATE INDEX IF NOT EXISTS idx_symphony_comp_status ON symphony_compositions(status);
78
+ CREATE INDEX IF NOT EXISTS idx_symphony_roles_comp ON symphony_roles(composition_id);
79
+ CREATE INDEX IF NOT EXISTS idx_symphony_consensus_comp ON symphony_consensus(composition_id);
80
+ `);
81
+
82
+ // ─── Default Templates ──────────────────────────────────────────────
83
+
84
+ const TEMPLATES = [
85
+ {
86
+ name: 'purchase_advisor',
87
+ task_type: 'purchase',
88
+ roles: ['researcher', 'analyst', 'negotiator', 'guardian'],
89
+ phase_order: ['discover', 'analyze', 'negotiate', 'guard', 'decide'],
90
+ description: 'End-to-end purchase advisory: discover products, analyze value, negotiate price, verify safety',
91
+ },
92
+ {
93
+ name: 'price_hunter',
94
+ task_type: 'price_comparison',
95
+ roles: ['researcher', 'analyst', 'guardian'],
96
+ phase_order: ['discover', 'analyze', 'guard', 'decide'],
97
+ description: 'Cross-site price comparison and best-deal identification',
98
+ },
99
+ {
100
+ name: 'deal_negotiator',
101
+ task_type: 'negotiation',
102
+ roles: ['researcher', 'negotiator', 'guardian'],
103
+ phase_order: ['discover', 'negotiate', 'guard', 'decide'],
104
+ description: 'Aggressive deal-seeking with safety verification',
105
+ },
106
+ {
107
+ name: 'site_scout',
108
+ task_type: 'exploration',
109
+ roles: ['researcher', 'analyst'],
110
+ phase_order: ['discover', 'analyze', 'decide'],
111
+ description: 'Explore and catalog site capabilities',
112
+ },
113
+ {
114
+ name: 'trust_auditor',
115
+ task_type: 'verification',
116
+ roles: ['researcher', 'guardian', 'analyst'],
117
+ phase_order: ['discover', 'guard', 'analyze', 'decide'],
118
+ description: 'Comprehensive trust and safety audit of a site',
119
+ },
120
+ ];
121
+
122
+ const _ensureTemplates = db.transaction(() => {
123
+ const insert = db.prepare(`INSERT OR IGNORE INTO symphony_templates (id, name, task_type, roles, phase_order, description) VALUES (?, ?, ?, ?, ?, ?)`);
124
+ for (const t of TEMPLATES) {
125
+ insert.run(crypto.randomUUID(), t.name, t.task_type, JSON.stringify(t.roles), JSON.stringify(t.phase_order), t.description);
126
+ }
127
+ });
128
+ _ensureTemplates();
129
+
130
+ // ─── Prepared Statements ─────────────────────────────────────────────
131
+
132
+ const stmts = {
133
+ insertComposition: db.prepare(`INSERT INTO symphony_compositions (id, site_id, task, task_type) VALUES (?, ?, ?, ?)`),
134
+ getComposition: db.prepare(`SELECT * FROM symphony_compositions WHERE id = ?`),
135
+ updateComposition: db.prepare(`UPDATE symphony_compositions SET status = ?, current_phase = ?, phases_completed = ?, final_decision = ?, confidence = ?, duration_ms = ?, completed_at = ? WHERE id = ?`),
136
+ getCompositionsBySite: db.prepare(`SELECT * FROM symphony_compositions WHERE site_id = ? ORDER BY created_at DESC LIMIT ?`),
137
+ getActiveCompositions: db.prepare(`SELECT * FROM symphony_compositions WHERE status IN ('composing', 'executing') ORDER BY created_at DESC`),
138
+
139
+ insertRole: db.prepare(`INSERT INTO symphony_roles (id, composition_id, role, agent_id) VALUES (?, ?, ?, ?)`),
140
+ getRoles: db.prepare(`SELECT * FROM symphony_roles WHERE composition_id = ? ORDER BY started_at ASC`),
141
+ getRole: db.prepare(`SELECT * FROM symphony_roles WHERE composition_id = ? AND role = ?`),
142
+ updateRole: db.prepare(`UPDATE symphony_roles SET status = ?, input = ?, output = ?, reasoning = ?, confidence = ?, started_at = COALESCE(started_at, datetime('now')), completed_at = ? WHERE id = ?`),
143
+
144
+ insertConsensus: db.prepare(`INSERT INTO symphony_consensus (id, composition_id, votes, method, result, agreement_score) VALUES (?, ?, ?, ?, ?, ?)`),
145
+ getConsensus: db.prepare(`SELECT * FROM symphony_consensus WHERE composition_id = ?`),
146
+
147
+ getTemplate: db.prepare(`SELECT * FROM symphony_templates WHERE name = ?`),
148
+ getTemplateByType: db.prepare(`SELECT * FROM symphony_templates WHERE task_type = ?`),
149
+ getAllTemplates: db.prepare(`SELECT * FROM symphony_templates ORDER BY name`),
150
+
151
+ getStats: db.prepare(`SELECT
152
+ (SELECT COUNT(*) FROM symphony_compositions WHERE site_id = ?) as total_compositions,
153
+ (SELECT COUNT(*) FROM symphony_compositions WHERE site_id = ? AND status = 'completed') as completed,
154
+ (SELECT AVG(confidence) FROM symphony_compositions WHERE site_id = ? AND status = 'completed') as avg_confidence,
155
+ (SELECT AVG(duration_ms) FROM symphony_compositions WHERE site_id = ? AND status = 'completed') as avg_duration_ms`),
156
+ };
157
+
158
+ // ─── Role Engines (Rule-Based AI) ────────────────────────────────────
159
+
160
+ const RoleEngines = {
161
+ /**
162
+ * Researcher — Discovers and catalogs site information
163
+ */
164
+ researcher: {
165
+ execute(input) {
166
+ const { siteData, task } = input;
167
+ const findings = [];
168
+ const capabilities = [];
169
+
170
+ // Analyze schema if available
171
+ if (siteData?.schema) {
172
+ const schema = typeof siteData.schema === 'string' ? JSON.parse(siteData.schema) : siteData.schema;
173
+ if (schema.actions) {
174
+ for (const [name, def] of Object.entries(schema.actions)) {
175
+ capabilities.push({ name, type: 'action', params: Object.keys(def.params || {}) });
176
+ }
177
+ findings.push(`Found ${Object.keys(schema.actions).length} available actions`);
178
+ }
179
+ if (schema.products) {
180
+ findings.push(`Catalog: ${schema.products.length || 'unknown'} products available`);
181
+ }
182
+ }
183
+
184
+ // Extract relevant data points from site data
185
+ if (siteData?.products) {
186
+ const products = Array.isArray(siteData.products) ? siteData.products : [];
187
+ const prices = products.filter((p) => p.price).map((p) => ({ name: p.name, price: p.price }));
188
+ if (prices.length > 0) {
189
+ findings.push(`Price range: ${Math.min(...prices.map((p) => p.price))} - ${Math.max(...prices.map((p) => p.price))}`);
190
+ }
191
+ }
192
+
193
+ if (siteData?.categories) findings.push(`Categories: ${siteData.categories.join(', ')}`);
194
+ if (siteData?.policies) {
195
+ if (siteData.policies.returns) findings.push(`Return policy: ${siteData.policies.returns}`);
196
+ if (siteData.policies.shipping) findings.push(`Shipping: ${siteData.policies.shipping}`);
197
+ }
198
+
199
+ return {
200
+ findings,
201
+ capabilities,
202
+ dataQuality: findings.length > 3 ? 'rich' : findings.length > 0 ? 'moderate' : 'sparse',
203
+ confidence: Math.min(0.95, 0.3 + findings.length * 0.1),
204
+ reasoning: `Discovered ${findings.length} data points and ${capabilities.length} capabilities`,
205
+ };
206
+ },
207
+ },
208
+
209
+ /**
210
+ * Analyst — Evaluates options using scoring heuristics
211
+ */
212
+ analyst: {
213
+ execute(input) {
214
+ const { findings, products, preferences, task } = input;
215
+ const analyses = [];
216
+
217
+ // Score products if available
218
+ if (products && Array.isArray(products)) {
219
+ const scored = products.map((product) => {
220
+ let score = 50; // base score
221
+ const reasons = [];
222
+
223
+ // Price scoring
224
+ if (product.price !== undefined && preferences?.maxPrice) {
225
+ const priceRatio = product.price / preferences.maxPrice;
226
+ if (priceRatio <= 0.5) { score += 25; reasons.push('well under budget'); }
227
+ else if (priceRatio <= 0.8) { score += 15; reasons.push('within budget'); }
228
+ else if (priceRatio <= 1.0) { score += 5; reasons.push('near budget limit'); }
229
+ else { score -= 20; reasons.push('over budget'); }
230
+ }
231
+
232
+ // Rating scoring
233
+ if (product.rating !== undefined) {
234
+ score += (product.rating - 3) * 10;
235
+ if (product.rating >= 4.5) reasons.push('highly rated');
236
+ if (product.rating < 3) reasons.push('poorly rated');
237
+ }
238
+
239
+ // Availability
240
+ if (product.inStock === false) { score -= 30; reasons.push('out of stock'); }
241
+
242
+ // Discount scoring
243
+ if (product.discount) {
244
+ score += Math.min(20, product.discount);
245
+ reasons.push(`${product.discount}% discount`);
246
+ }
247
+
248
+ // Preference matching
249
+ if (preferences?.preferredCategory && product.category === preferences.preferredCategory) {
250
+ score += 10;
251
+ reasons.push('matches preferred category');
252
+ }
253
+
254
+ return { ...product, score: Math.max(0, Math.min(100, score)), reasons };
255
+ });
256
+
257
+ scored.sort((a, b) => b.score - a.score);
258
+ analyses.push({
259
+ type: 'product_ranking',
260
+ items: scored.slice(0, 10),
261
+ bestOption: scored[0],
262
+ worstOption: scored[scored.length - 1],
263
+ });
264
+ }
265
+
266
+ // Value analysis from findings
267
+ const valueInsights = [];
268
+ if (findings) {
269
+ for (const f of findings) {
270
+ if (typeof f === 'string' && f.includes('Price range')) valueInsights.push(f);
271
+ if (typeof f === 'string' && f.includes('discount')) valueInsights.push(f);
272
+ if (typeof f === 'string' && f.includes('Return policy')) valueInsights.push(f);
273
+ }
274
+ }
275
+
276
+ return {
277
+ analyses,
278
+ valueInsights,
279
+ recommendation: analyses[0]?.items?.[0] || null,
280
+ confidence: analyses.length > 0 ? 0.7 + analyses[0].items.length * 0.02 : 0.3,
281
+ reasoning: `Analyzed ${analyses.length > 0 ? analyses[0].items.length : 0} options with ${valueInsights.length} value insights`,
282
+ };
283
+ },
284
+ },
285
+
286
+ /**
287
+ * Negotiator — Pursues optimal deal terms
288
+ */
289
+ negotiator: {
290
+ execute(input) {
291
+ const { product, siteCapabilities, preferences, marketData } = input;
292
+ const strategies = [];
293
+ const terms = {};
294
+
295
+ if (!product) {
296
+ return { strategies: [], terms: {}, confidence: 0, reasoning: 'No product to negotiate' };
297
+ }
298
+
299
+ const price = product.price || 0;
300
+
301
+ // Strategy: Volume discount
302
+ if (siteCapabilities?.some((c) => c.name === 'bulk_order' || c.name === 'quantity_discount')) {
303
+ strategies.push({ type: 'volume', description: 'Request bulk/quantity discount', priority: 2 });
304
+ terms.quantity_discount = true;
305
+ }
306
+
307
+ // Strategy: Competitor price matching
308
+ if (marketData?.competitorPrices) {
309
+ const lowest = Math.min(...marketData.competitorPrices);
310
+ if (lowest < price) {
311
+ strategies.push({ type: 'price_match', description: `Competitor offers ${lowest} (${Math.round((1 - lowest / price) * 100)}% less)`, priority: 3 });
312
+ terms.target_price = lowest;
313
+ }
314
+ }
315
+
316
+ // Strategy: Loyalty/repeat customer
317
+ if (preferences?.visitCount > 3) {
318
+ strategies.push({ type: 'loyalty', description: 'Leverage repeat customer status', priority: 1 });
319
+ terms.loyalty = true;
320
+ }
321
+
322
+ // Strategy: Bundle deal
323
+ if (siteCapabilities?.some((c) => c.name === 'bundle' || c.name === 'add_to_cart')) {
324
+ strategies.push({ type: 'bundle', description: 'Explore bundle pricing', priority: 1 });
325
+ terms.bundle = true;
326
+ }
327
+
328
+ // Calculate target price
329
+ let targetDiscount = 0;
330
+ if (strategies.length > 0) targetDiscount = Math.min(35, strategies.length * 8 + 5);
331
+ terms.target_price = terms.target_price || price * (1 - targetDiscount / 100);
332
+ terms.max_acceptable = price * 0.95; // worst case: 5% off
333
+ terms.ideal_price = price * (1 - targetDiscount / 100);
334
+
335
+ strategies.sort((a, b) => b.priority - a.priority);
336
+
337
+ return {
338
+ strategies,
339
+ terms,
340
+ confidence: Math.min(0.9, 0.3 + strategies.length * 0.15),
341
+ reasoning: `${strategies.length} negotiation strategies identified, target ${targetDiscount}% discount`,
342
+ };
343
+ },
344
+ },
345
+
346
+ /**
347
+ * Guardian — Validates safety, fairness, and trust
348
+ */
349
+ guardian: {
350
+ execute(input) {
351
+ const { product, siteData, negotiationTerms, reputationData } = input;
352
+ const warnings = [];
353
+ const approvals = [];
354
+ let riskScore = 0;
355
+
356
+ // Price manipulation check
357
+ if (product?.price && product?.originalPrice) {
358
+ const realDiscount = (1 - product.price / product.originalPrice) * 100;
359
+ if (product.discount && Math.abs(product.discount - realDiscount) > 5) {
360
+ warnings.push({ type: 'price_manipulation', severity: 'high', detail: `Claimed discount (${product.discount}%) doesn't match actual (${Math.round(realDiscount)}%)` });
361
+ riskScore += 30;
362
+ }
363
+ }
364
+
365
+ // Reputation check
366
+ if (reputationData) {
367
+ if (reputationData.trustLevel === 'emerging') {
368
+ warnings.push({ type: 'low_trust', severity: 'medium', detail: 'Site has low trust level' });
369
+ riskScore += 15;
370
+ }
371
+ if (reputationData.attestationCount < 3) {
372
+ warnings.push({ type: 'few_attestations', severity: 'low', detail: 'Limited reputation data' });
373
+ riskScore += 10;
374
+ }
375
+ if (reputationData.trustLevel === 'verified' || reputationData.trustLevel === 'exemplary') {
376
+ approvals.push('trusted_site');
377
+ }
378
+ }
379
+
380
+ // Negotiation terms safety
381
+ if (negotiationTerms?.target_price && product?.price) {
382
+ if (negotiationTerms.target_price < product.price * 0.3) {
383
+ warnings.push({ type: 'unrealistic_price', severity: 'medium', detail: 'Target price seems unrealistically low' });
384
+ riskScore += 10;
385
+ }
386
+ }
387
+
388
+ // Data quality check
389
+ if (siteData) {
390
+ if (!siteData.policies?.returns) {
391
+ warnings.push({ type: 'no_return_policy', severity: 'medium', detail: 'No return policy found' });
392
+ riskScore += 10;
393
+ }
394
+ if (!siteData.policies?.privacy) {
395
+ warnings.push({ type: 'no_privacy_policy', severity: 'low', detail: 'No privacy policy found' });
396
+ riskScore += 5;
397
+ }
398
+ }
399
+
400
+ const safe = riskScore < 40;
401
+ if (safe) approvals.push('risk_acceptable');
402
+
403
+ return {
404
+ safe,
405
+ riskScore: Math.min(100, riskScore),
406
+ warnings,
407
+ approvals,
408
+ confidence: warnings.length === 0 ? 0.9 : Math.max(0.3, 0.9 - warnings.length * 0.1),
409
+ reasoning: `Risk score: ${riskScore}/100, ${warnings.length} warnings, ${approvals.length} approvals`,
410
+ };
411
+ },
412
+ },
413
+ };
414
+
415
+ // ─── Core API ────────────────────────────────────────────────────────
416
+
417
+ /**
418
+ * Compose a new symphony — select template and assign roles.
419
+ */
420
+ function compose(siteId, task, taskType, agentIds = {}) {
421
+ const template = stmts.getTemplateByType.get(taskType) || stmts.getTemplateByType.get('exploration');
422
+ if (!template) throw new Error(`No template for task type: ${taskType}`);
423
+
424
+ const roles = JSON.parse(template.roles);
425
+ const id = crypto.randomUUID();
426
+ stmts.insertComposition.run(id, siteId, task, taskType);
427
+
428
+ // Assign roles
429
+ const roleAssignments = [];
430
+ for (const role of roles) {
431
+ const roleId = crypto.randomUUID();
432
+ const agentId = agentIds[role] || null;
433
+ stmts.insertRole.run(roleId, id, role, agentId);
434
+ roleAssignments.push({ id: roleId, role, agentId });
435
+ }
436
+
437
+ return {
438
+ compositionId: id,
439
+ template: template.name,
440
+ roles: roleAssignments,
441
+ phases: JSON.parse(template.phase_order),
442
+ status: 'composing',
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Execute a single phase of the symphony.
448
+ */
449
+ function executePhase(compositionId, phaseName, phaseInput = {}) {
450
+ const comp = stmts.getComposition.get(compositionId);
451
+ if (!comp) throw new Error('Composition not found');
452
+
453
+ const roles = stmts.getRoles.all(compositionId);
454
+ const phasesCompleted = JSON.parse(comp.phases_completed || '[]');
455
+
456
+ // Determine which role handles this phase
457
+ const phaseRoleMap = {
458
+ discover: 'researcher',
459
+ analyze: 'analyst',
460
+ negotiate: 'negotiator',
461
+ guard: 'guardian',
462
+ };
463
+
464
+ const roleName = phaseRoleMap[phaseName];
465
+ if (!roleName) {
466
+ // 'decide' phase — run consensus
467
+ return _runConsensus(compositionId, roles);
468
+ }
469
+
470
+ const role = roles.find((r) => r.role === roleName);
471
+ if (!role) {
472
+ return { phase: phaseName, skipped: true, reason: `No ${roleName} assigned` };
473
+ }
474
+
475
+ // Gather input from previous phases
476
+ const previousOutputs = {};
477
+ for (const r of roles) {
478
+ if (r.output && r.output !== '{}') {
479
+ previousOutputs[r.role] = JSON.parse(r.output);
480
+ }
481
+ }
482
+
483
+ const fullInput = { ...previousOutputs, ...phaseInput };
484
+
485
+ // Execute the role engine
486
+ const engine = RoleEngines[roleName];
487
+ if (!engine) throw new Error(`No engine for role: ${roleName}`);
488
+
489
+ const output = engine.execute(fullInput);
490
+
491
+ // Save role result
492
+ stmts.updateRole.run(
493
+ 'completed', JSON.stringify(fullInput), JSON.stringify(output),
494
+ output.reasoning || '', output.confidence || 0,
495
+ new Date().toISOString(), role.id
496
+ );
497
+
498
+ // Update composition
499
+ phasesCompleted.push(phaseName);
500
+ stmts.updateComposition.run(
501
+ 'executing', phaseName, JSON.stringify(phasesCompleted),
502
+ null, 0, 0, null, compositionId
503
+ );
504
+
505
+ return { phase: phaseName, role: roleName, output, phasesCompleted };
506
+ }
507
+
508
+ /**
509
+ * Execute the entire symphony end-to-end.
510
+ */
511
+ function perform(siteId, task, taskType, inputData = {}, agentIds = {}) {
512
+ const startTime = Date.now();
513
+ const composition = compose(siteId, task, taskType, agentIds);
514
+
515
+ // Get template phase order
516
+ const template = stmts.getTemplateByType.get(taskType);
517
+ const phases = template ? JSON.parse(template.phase_order) : ['discover', 'decide'];
518
+
519
+ const phaseResults = {};
520
+
521
+ // Execute phases sequentially, piping outputs
522
+ for (const phase of phases) {
523
+ const phaseInput = { ...inputData };
524
+
525
+ // Pipe previous phase outputs
526
+ for (const [prevPhase, prevResult] of Object.entries(phaseResults)) {
527
+ if (prevResult.output) {
528
+ Object.assign(phaseInput, prevResult.output);
529
+ }
530
+ }
531
+
532
+ const result = executePhase(composition.compositionId, phase, phaseInput);
533
+ phaseResults[phase] = result;
534
+ }
535
+
536
+ const duration = Date.now() - startTime;
537
+
538
+ // Get final decision
539
+ const consensus = stmts.getConsensus.get(composition.compositionId);
540
+ const finalDecision = consensus ? JSON.parse(consensus.result) : phaseResults;
541
+
542
+ // Calculate overall confidence
543
+ const roleOutputs = Object.values(phaseResults).filter((r) => r.output?.confidence);
544
+ const avgConfidence = roleOutputs.length > 0
545
+ ? roleOutputs.reduce((s, r) => s + r.output.confidence, 0) / roleOutputs.length
546
+ : 0;
547
+
548
+ // Finalize composition
549
+ stmts.updateComposition.run(
550
+ 'completed', 'decide', JSON.stringify(Object.keys(phaseResults)),
551
+ JSON.stringify(finalDecision), avgConfidence, duration,
552
+ new Date().toISOString(), composition.compositionId
553
+ );
554
+
555
+ return {
556
+ compositionId: composition.compositionId,
557
+ template: composition.template,
558
+ phases: phaseResults,
559
+ decision: finalDecision,
560
+ confidence: avgConfidence,
561
+ duration_ms: duration,
562
+ status: 'completed',
563
+ };
564
+ }
565
+
566
+ // ─── Consensus Engine ────────────────────────────────────────────────
567
+
568
+ function _runConsensus(compositionId, roles) {
569
+ const completedRoles = roles.filter((r) => r.output && r.output !== '{}');
570
+ const votes = {};
571
+ let totalConfidence = 0;
572
+
573
+ for (const role of completedRoles) {
574
+ const output = JSON.parse(role.output);
575
+ votes[role.role] = {
576
+ confidence: output.confidence || 0,
577
+ recommendation: output.recommendation || output.strategies?.[0] || null,
578
+ reasoning: output.reasoning || '',
579
+ safe: output.safe !== undefined ? output.safe : true,
580
+ riskScore: output.riskScore || 0,
581
+ };
582
+ totalConfidence += output.confidence || 0;
583
+ }
584
+
585
+ // Weighted consensus
586
+ const result = {
587
+ recommendation: null,
588
+ reasoning: [],
589
+ overallConfidence: completedRoles.length > 0 ? totalConfidence / completedRoles.length : 0,
590
+ safe: true,
591
+ participatingRoles: completedRoles.map((r) => r.role),
592
+ };
593
+
594
+ // Guardian veto check
595
+ if (votes.guardian && !votes.guardian.safe) {
596
+ result.safe = false;
597
+ result.recommendation = { action: 'abort', reason: 'Guardian flagged safety concerns', riskScore: votes.guardian.riskScore };
598
+ result.reasoning.push(`GUARDIAN VETO: ${votes.guardian.reasoning}`);
599
+ } else {
600
+ // Take analyst recommendation if available, otherwise researcher findings
601
+ if (votes.analyst?.recommendation) {
602
+ result.recommendation = votes.analyst.recommendation;
603
+ result.reasoning.push(`Analyst: ${votes.analyst.reasoning}`);
604
+ }
605
+ if (votes.negotiator) {
606
+ result.negotiation = votes.negotiator;
607
+ result.reasoning.push(`Negotiator: ${votes.negotiator.reasoning}`);
608
+ }
609
+ if (votes.researcher) {
610
+ result.reasoning.push(`Researcher: ${votes.researcher.reasoning}`);
611
+ }
612
+ if (votes.guardian) {
613
+ result.reasoning.push(`Guardian: ${votes.guardian.reasoning}`);
614
+ }
615
+ }
616
+
617
+ const consensusId = crypto.randomUUID();
618
+ const agreementScore = _calculateAgreement(votes);
619
+
620
+ stmts.insertConsensus.run(
621
+ consensusId, compositionId, JSON.stringify(votes),
622
+ 'weighted', JSON.stringify(result), agreementScore
623
+ );
624
+
625
+ return {
626
+ phase: 'decide',
627
+ consensus: result,
628
+ votes,
629
+ agreementScore,
630
+ };
631
+ }
632
+
633
+ function _calculateAgreement(votes) {
634
+ const confidences = Object.values(votes).map((v) => v.confidence);
635
+ if (confidences.length < 2) return 1.0;
636
+
637
+ // Agreement = inverse of confidence variance
638
+ const mean = confidences.reduce((s, c) => s + c, 0) / confidences.length;
639
+ const variance = confidences.reduce((s, c) => s + (c - mean) ** 2, 0) / confidences.length;
640
+ return Math.max(0, 1 - variance);
641
+ }
642
+
643
+ // ─── Query API ───────────────────────────────────────────────────────
644
+
645
+ function getComposition(compositionId) {
646
+ const comp = stmts.getComposition.get(compositionId);
647
+ if (!comp) return null;
648
+ const roles = stmts.getRoles.all(compositionId);
649
+ const consensus = stmts.getConsensus.get(compositionId);
650
+ return {
651
+ ...comp,
652
+ phases_completed: JSON.parse(comp.phases_completed || '[]'),
653
+ final_decision: comp.final_decision ? JSON.parse(comp.final_decision) : null,
654
+ roles: roles.map((r) => ({ ...r, input: JSON.parse(r.input || '{}'), output: JSON.parse(r.output || '{}') })),
655
+ consensus: consensus ? { ...consensus, votes: JSON.parse(consensus.votes), result: JSON.parse(consensus.result) } : null,
656
+ };
657
+ }
658
+
659
+ function getCompositions(siteId, limit = 20) {
660
+ return stmts.getCompositionsBySite.all(siteId, limit).map((c) => ({
661
+ ...c,
662
+ phases_completed: JSON.parse(c.phases_completed || '[]'),
663
+ final_decision: c.final_decision ? JSON.parse(c.final_decision) : null,
664
+ }));
665
+ }
666
+
667
+ function getTemplates() {
668
+ return stmts.getAllTemplates.all().map((t) => ({
669
+ ...t, roles: JSON.parse(t.roles), phase_order: JSON.parse(t.phase_order),
670
+ }));
671
+ }
672
+
673
+ function getStats(siteId) {
674
+ return stmts.getStats.get(siteId, siteId, siteId, siteId);
675
+ }
676
+
677
+ module.exports = {
678
+ compose, executePhase, perform,
679
+ getComposition, getCompositions, getTemplates, getStats,
680
+ RoleEngines,
681
+ };