@defai.digital/discussion-domain 13.0.3 → 13.1.1

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.
Files changed (73) hide show
  1. package/dist/budget-manager.d.ts +79 -0
  2. package/dist/budget-manager.d.ts.map +1 -0
  3. package/dist/budget-manager.js +155 -0
  4. package/dist/budget-manager.js.map +1 -0
  5. package/dist/confidence-extractor.d.ts +60 -0
  6. package/dist/confidence-extractor.d.ts.map +1 -0
  7. package/dist/confidence-extractor.js +251 -0
  8. package/dist/confidence-extractor.js.map +1 -0
  9. package/dist/consensus/synthesis.d.ts.map +1 -1
  10. package/dist/consensus/synthesis.js +2 -0
  11. package/dist/consensus/synthesis.js.map +1 -1
  12. package/dist/consensus/voting.d.ts +3 -0
  13. package/dist/consensus/voting.d.ts.map +1 -1
  14. package/dist/consensus/voting.js +15 -4
  15. package/dist/consensus/voting.js.map +1 -1
  16. package/dist/context-tracker.d.ts +77 -0
  17. package/dist/context-tracker.d.ts.map +1 -0
  18. package/dist/context-tracker.js +177 -0
  19. package/dist/context-tracker.js.map +1 -0
  20. package/dist/cost-tracker.d.ts +123 -0
  21. package/dist/cost-tracker.d.ts.map +1 -0
  22. package/dist/cost-tracker.js +196 -0
  23. package/dist/cost-tracker.js.map +1 -0
  24. package/dist/executor.d.ts.map +1 -1
  25. package/dist/executor.js +9 -0
  26. package/dist/executor.js.map +1 -1
  27. package/dist/index.d.ts +7 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/participant-resolver.d.ts +111 -0
  32. package/dist/participant-resolver.d.ts.map +1 -0
  33. package/dist/participant-resolver.js +160 -0
  34. package/dist/participant-resolver.js.map +1 -0
  35. package/dist/patterns/round-robin.d.ts +3 -0
  36. package/dist/patterns/round-robin.d.ts.map +1 -1
  37. package/dist/patterns/round-robin.js +41 -2
  38. package/dist/patterns/round-robin.js.map +1 -1
  39. package/dist/patterns/synthesis.d.ts +3 -0
  40. package/dist/patterns/synthesis.d.ts.map +1 -1
  41. package/dist/patterns/synthesis.js +77 -3
  42. package/dist/patterns/synthesis.js.map +1 -1
  43. package/dist/prompts/templates.d.ts +1 -0
  44. package/dist/prompts/templates.d.ts.map +1 -1
  45. package/dist/prompts/templates.js +3 -1
  46. package/dist/prompts/templates.js.map +1 -1
  47. package/dist/provider-bridge.d.ts +3 -1
  48. package/dist/provider-bridge.d.ts.map +1 -1
  49. package/dist/provider-bridge.js +48 -32
  50. package/dist/provider-bridge.js.map +1 -1
  51. package/dist/recursive-executor.d.ts +80 -0
  52. package/dist/recursive-executor.d.ts.map +1 -0
  53. package/dist/recursive-executor.js +354 -0
  54. package/dist/recursive-executor.js.map +1 -0
  55. package/dist/types.d.ts +83 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/budget-manager.ts +272 -0
  60. package/src/confidence-extractor.ts +321 -0
  61. package/src/consensus/synthesis.ts +2 -0
  62. package/src/consensus/voting.ts +22 -6
  63. package/src/context-tracker.ts +307 -0
  64. package/src/cost-tracker.ts +363 -0
  65. package/src/executor.ts +9 -0
  66. package/src/index.ts +72 -0
  67. package/src/participant-resolver.ts +297 -0
  68. package/src/patterns/round-robin.ts +48 -2
  69. package/src/patterns/synthesis.ts +89 -3
  70. package/src/prompts/templates.ts +4 -2
  71. package/src/provider-bridge.ts +52 -31
  72. package/src/recursive-executor.ts +510 -0
  73. package/src/types.ts +120 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Discussion Budget Manager
3
+ *
4
+ * Manages timeout budgets across recursive discussions using configurable strategies.
5
+ * Supports fixed, cascade, and budget allocation strategies.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-610: Child timeout ≤ parent remaining budget
9
+ * - INV-DISC-611: Minimum time reserved for synthesis
10
+ * - INV-DISC-612: Total timeout includes all nested calls
11
+ * - INV-DISC-613: Strategy applied consistently
12
+ */
13
+
14
+ import {
15
+ type TimeoutConfig,
16
+ type TimeoutStrategy,
17
+ getTimeoutForLevel,
18
+ DEFAULT_TOTAL_BUDGET_MS,
19
+ MIN_SYNTHESIS_TIME_MS,
20
+ MAX_DISCUSSION_DEPTH,
21
+ } from '@defai.digital/contracts';
22
+
23
+ /**
24
+ * Budget allocation result for a discussion level
25
+ */
26
+ export interface BudgetAllocation {
27
+ /** Timeout for provider calls at this level */
28
+ providerTimeoutMs: number;
29
+
30
+ /** Time reserved for synthesis at this level */
31
+ synthesisTimeMs: number;
32
+
33
+ /** Total budget for this level */
34
+ totalLevelBudgetMs: number;
35
+
36
+ /** Budget available for sub-discussions */
37
+ subDiscussionBudgetMs: number;
38
+ }
39
+
40
+ /**
41
+ * Budget status snapshot
42
+ */
43
+ export interface BudgetStatus {
44
+ /** Total budget configured */
45
+ totalBudgetMs: number;
46
+
47
+ /** Time elapsed since start */
48
+ elapsedMs: number;
49
+
50
+ /** Remaining budget */
51
+ remainingMs: number;
52
+
53
+ /** Current depth */
54
+ currentDepth: number;
55
+
56
+ /** Budget used per level */
57
+ usageByLevel: Map<number, number>;
58
+
59
+ /** Whether budget is exhausted */
60
+ exhausted: boolean;
61
+
62
+ /** Utilization percentage */
63
+ utilizationPercent: number;
64
+ }
65
+
66
+ /**
67
+ * Discussion budget manager interface
68
+ */
69
+ export interface DiscussionBudgetManager {
70
+ /** Get budget allocation for a specific depth */
71
+ getAllocation(depth: number): BudgetAllocation;
72
+
73
+ /** Get current budget status */
74
+ getStatus(): BudgetStatus;
75
+
76
+ /** Record time spent at a level */
77
+ recordUsage(depth: number, elapsedMs: number): void;
78
+
79
+ /** Get remaining budget for a depth */
80
+ getRemainingBudget(depth: number): number;
81
+
82
+ /** Check if budget allows sub-discussion at depth */
83
+ canAllocateSubDiscussion(depth: number): boolean;
84
+
85
+ /** Get timeout for provider call at depth */
86
+ getProviderTimeout(depth: number): number;
87
+
88
+ /** Get the configured strategy */
89
+ getStrategy(): TimeoutStrategy;
90
+
91
+ /** Create a child budget manager for sub-discussion */
92
+ createChildManager(startingBudgetMs: number): DiscussionBudgetManager;
93
+ }
94
+
95
+ /**
96
+ * Creates a discussion budget manager
97
+ */
98
+ export function createBudgetManager(
99
+ config: Partial<TimeoutConfig> = {},
100
+ maxDepth = MAX_DISCUSSION_DEPTH
101
+ ): DiscussionBudgetManager {
102
+ // Apply defaults
103
+ const timeoutConfig: TimeoutConfig = {
104
+ strategy: config.strategy ?? 'cascade',
105
+ totalBudgetMs: config.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS,
106
+ minSynthesisMs: config.minSynthesisMs ?? MIN_SYNTHESIS_TIME_MS,
107
+ levelTimeouts: config.levelTimeouts,
108
+ };
109
+
110
+ const startTime = Date.now();
111
+ const usageByLevel = new Map<number, number>();
112
+
113
+ // Pre-calculate allocations for each level
114
+ const allocations = new Map<number, BudgetAllocation>();
115
+
116
+ for (let depth = 0; depth <= maxDepth; depth++) {
117
+ const levelTimeout = getTimeoutForLevel(timeoutConfig, depth, maxDepth);
118
+ const synthesisTime = timeoutConfig.minSynthesisMs;
119
+ const providerTime = Math.max(synthesisTime, levelTimeout - synthesisTime);
120
+
121
+ // Calculate sub-discussion budget (for next level)
122
+ let subBudget = 0;
123
+ if (depth < maxDepth) {
124
+ subBudget = getTimeoutForLevel(timeoutConfig, depth + 1, maxDepth);
125
+ }
126
+
127
+ allocations.set(depth, {
128
+ providerTimeoutMs: providerTime,
129
+ synthesisTimeMs: synthesisTime,
130
+ totalLevelBudgetMs: levelTimeout,
131
+ subDiscussionBudgetMs: subBudget,
132
+ });
133
+ }
134
+
135
+ return {
136
+ getAllocation(depth: number): BudgetAllocation {
137
+ const allocation = allocations.get(Math.min(depth, maxDepth));
138
+ if (!allocation) {
139
+ // Fallback for unexpected depth
140
+ return {
141
+ providerTimeoutMs: timeoutConfig.minSynthesisMs,
142
+ synthesisTimeMs: timeoutConfig.minSynthesisMs,
143
+ totalLevelBudgetMs: timeoutConfig.minSynthesisMs * 2,
144
+ subDiscussionBudgetMs: 0,
145
+ };
146
+ }
147
+ return { ...allocation };
148
+ },
149
+
150
+ getStatus(): BudgetStatus {
151
+ const elapsedMs = Date.now() - startTime;
152
+ const remainingMs = Math.max(0, timeoutConfig.totalBudgetMs - elapsedMs);
153
+
154
+ // Find current depth (highest level with usage)
155
+ let currentDepth = 0;
156
+ for (const [depth] of usageByLevel) {
157
+ if (depth > currentDepth) {
158
+ currentDepth = depth;
159
+ }
160
+ }
161
+
162
+ return {
163
+ totalBudgetMs: timeoutConfig.totalBudgetMs,
164
+ elapsedMs,
165
+ remainingMs,
166
+ currentDepth,
167
+ usageByLevel: new Map(usageByLevel),
168
+ exhausted: remainingMs <= 0,
169
+ utilizationPercent: (elapsedMs / timeoutConfig.totalBudgetMs) * 100,
170
+ };
171
+ },
172
+
173
+ recordUsage(depth: number, elapsedMs: number): void {
174
+ const current = usageByLevel.get(depth) ?? 0;
175
+ usageByLevel.set(depth, current + elapsedMs);
176
+ },
177
+
178
+ getRemainingBudget(depth: number): number {
179
+ const elapsedMs = Date.now() - startTime;
180
+ const totalRemaining = Math.max(0, timeoutConfig.totalBudgetMs - elapsedMs);
181
+
182
+ // Get level allocation
183
+ const allocation = allocations.get(depth);
184
+ if (!allocation) {
185
+ return 0;
186
+ }
187
+
188
+ // Usage at this level
189
+ const levelUsage = usageByLevel.get(depth) ?? 0;
190
+ const levelRemaining = Math.max(0, allocation.totalLevelBudgetMs - levelUsage);
191
+
192
+ // Return minimum of total remaining and level remaining
193
+ return Math.min(totalRemaining, levelRemaining);
194
+ },
195
+
196
+ canAllocateSubDiscussion(depth: number): boolean {
197
+ // Check if we can go deeper
198
+ if (depth >= maxDepth) {
199
+ return false;
200
+ }
201
+
202
+ // Check remaining budget
203
+ const remaining = this.getRemainingBudget(depth);
204
+ const minRequired = timeoutConfig.minSynthesisMs * 2; // Min for sub-discussion
205
+
206
+ return remaining >= minRequired;
207
+ },
208
+
209
+ getProviderTimeout(depth: number): number {
210
+ const allocation = this.getAllocation(depth);
211
+ const remaining = this.getRemainingBudget(depth);
212
+
213
+ // Don't exceed remaining budget
214
+ return Math.min(allocation.providerTimeoutMs, remaining);
215
+ },
216
+
217
+ getStrategy(): TimeoutStrategy {
218
+ return timeoutConfig.strategy;
219
+ },
220
+
221
+ createChildManager(startingBudgetMs: number): DiscussionBudgetManager {
222
+ // Create a new manager with reduced budget
223
+ return createBudgetManager(
224
+ {
225
+ ...timeoutConfig,
226
+ totalBudgetMs: startingBudgetMs,
227
+ },
228
+ maxDepth
229
+ );
230
+ },
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Utility to format budget status for logging
236
+ */
237
+ export function formatBudgetStatus(status: BudgetStatus): string {
238
+ const lines = [
239
+ `Budget Status:`,
240
+ ` Total: ${status.totalBudgetMs}ms`,
241
+ ` Elapsed: ${status.elapsedMs}ms`,
242
+ ` Remaining: ${status.remainingMs}ms`,
243
+ ` Utilization: ${status.utilizationPercent.toFixed(1)}%`,
244
+ ` Current Depth: ${status.currentDepth}`,
245
+ ` Exhausted: ${status.exhausted}`,
246
+ ];
247
+
248
+ if (status.usageByLevel.size > 0) {
249
+ lines.push(` Usage by Level:`);
250
+ for (const [depth, usage] of status.usageByLevel) {
251
+ lines.push(` Level ${depth}: ${usage}ms`);
252
+ }
253
+ }
254
+
255
+ return lines.join('\n');
256
+ }
257
+
258
+ /**
259
+ * Calculate recommended providers based on remaining budget
260
+ */
261
+ export function recommendProvidersForBudget(
262
+ remainingMs: number,
263
+ perProviderEstimateMs: number,
264
+ minProviders = 2,
265
+ maxProviders = 6
266
+ ): number {
267
+ // Calculate how many providers we can fit
268
+ const possibleProviders = Math.floor(remainingMs / perProviderEstimateMs);
269
+
270
+ // Clamp to valid range
271
+ return Math.max(minProviders, Math.min(maxProviders, possibleProviders));
272
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Confidence Extractor
3
+ *
4
+ * Extracts confidence scores from provider responses for early exit decisions.
5
+ * Supports cascading confidence where high-confidence first responses can
6
+ * short-circuit the discussion.
7
+ *
8
+ * Invariants:
9
+ * - INV-DISC-622: Confidence threshold configurable (default 0.9)
10
+ * - INV-DISC-623: Minimum 2 providers for quality
11
+ */
12
+
13
+ import type { CascadingConfidenceConfig } from '@defai.digital/contracts';
14
+
15
+ /**
16
+ * Result of confidence extraction from a response
17
+ */
18
+ export interface ExtractedConfidence {
19
+ /** Confidence score 0-1, or null if not extractable */
20
+ score: number | null;
21
+
22
+ /** Method used to extract confidence */
23
+ method: 'explicit' | 'heuristic' | 'none';
24
+
25
+ /** Explanation of confidence assessment */
26
+ explanation?: string;
27
+ }
28
+
29
+ /**
30
+ * Result of early exit evaluation
31
+ */
32
+ export interface EarlyExitDecision {
33
+ /** Whether to exit early */
34
+ shouldExit: boolean;
35
+
36
+ /** Reason for decision */
37
+ reason: string;
38
+
39
+ /** Confidence score that triggered decision */
40
+ confidence?: number;
41
+
42
+ /** Number of providers that responded */
43
+ providerCount: number;
44
+ }
45
+
46
+ /**
47
+ * Default cascading confidence config
48
+ */
49
+ export const DEFAULT_CASCADING_CONFIDENCE: Required<CascadingConfidenceConfig> = {
50
+ enabled: true,
51
+ threshold: 0.9,
52
+ minProviders: 2,
53
+ };
54
+
55
+ /**
56
+ * Patterns for extracting explicit confidence from responses
57
+ */
58
+ const CONFIDENCE_PATTERNS = [
59
+ // "Confidence: 95%" or "Confidence: 0.95"
60
+ /confidence[:\s]+(\d+(?:\.\d+)?)\s*%?/i,
61
+ // "I am 95% confident" or "95% certain"
62
+ /(?:am|is)\s+(\d+(?:\.\d+)?)\s*%?\s*(?:confident|certain|sure)/i,
63
+ // "[HIGH CONFIDENCE]" markers
64
+ /\[(?:very\s+)?high\s+confidence\]/i,
65
+ /\[medium\s+confidence\]/i,
66
+ /\[low\s+confidence\]/i,
67
+ ];
68
+
69
+ /**
70
+ * Heuristic confidence indicators
71
+ */
72
+ const HIGH_CONFIDENCE_PHRASES = [
73
+ 'definitely',
74
+ 'certainly',
75
+ 'absolutely',
76
+ 'without a doubt',
77
+ 'clearly',
78
+ 'obviously',
79
+ 'undoubtedly',
80
+ 'i am confident',
81
+ 'i am certain',
82
+ 'i strongly believe',
83
+ ];
84
+
85
+ const LOW_CONFIDENCE_PHRASES = [
86
+ 'perhaps',
87
+ 'maybe',
88
+ 'might',
89
+ 'could be',
90
+ 'not sure',
91
+ 'uncertain',
92
+ 'possibly',
93
+ 'it depends',
94
+ 'hard to say',
95
+ 'difficult to determine',
96
+ 'i think',
97
+ 'it seems',
98
+ ];
99
+
100
+ /**
101
+ * Extract confidence score from a provider response
102
+ */
103
+ export function extractConfidence(content: string): ExtractedConfidence {
104
+ if (!content || content.trim().length === 0) {
105
+ return { score: null, method: 'none' };
106
+ }
107
+
108
+ const normalizedContent = content.toLowerCase();
109
+
110
+ // Try explicit patterns first
111
+ for (const pattern of CONFIDENCE_PATTERNS) {
112
+ const match = pattern.exec(content);
113
+ if (match) {
114
+ // Check for marker-based confidence
115
+ if (match[0].includes('high confidence')) {
116
+ return {
117
+ score: 0.9,
118
+ method: 'explicit',
119
+ explanation: 'Explicit high confidence marker found',
120
+ };
121
+ }
122
+ if (match[0].includes('medium confidence')) {
123
+ return {
124
+ score: 0.6,
125
+ method: 'explicit',
126
+ explanation: 'Explicit medium confidence marker found',
127
+ };
128
+ }
129
+ if (match[0].includes('low confidence')) {
130
+ return {
131
+ score: 0.3,
132
+ method: 'explicit',
133
+ explanation: 'Explicit low confidence marker found',
134
+ };
135
+ }
136
+
137
+ // Numeric confidence
138
+ if (match[1]) {
139
+ let score = parseFloat(match[1]);
140
+ // Normalize percentage to 0-1
141
+ if (score > 1) {
142
+ score = score / 100;
143
+ }
144
+ // Clamp to valid range
145
+ score = Math.max(0, Math.min(1, score));
146
+ return {
147
+ score,
148
+ method: 'explicit',
149
+ explanation: `Explicit confidence: ${(score * 100).toFixed(0)}%`,
150
+ };
151
+ }
152
+ }
153
+ }
154
+
155
+ // Fall back to heuristic analysis
156
+ const highCount = HIGH_CONFIDENCE_PHRASES.filter(phrase =>
157
+ normalizedContent.includes(phrase)
158
+ ).length;
159
+
160
+ const lowCount = LOW_CONFIDENCE_PHRASES.filter(phrase =>
161
+ normalizedContent.includes(phrase)
162
+ ).length;
163
+
164
+ if (highCount > 0 || lowCount > 0) {
165
+ // Calculate heuristic score based on phrase counts
166
+ const netConfidence = highCount - lowCount;
167
+ let score: number;
168
+
169
+ if (netConfidence >= 2) {
170
+ score = 0.85;
171
+ } else if (netConfidence === 1) {
172
+ score = 0.75;
173
+ } else if (netConfidence === 0) {
174
+ score = 0.6;
175
+ } else if (netConfidence === -1) {
176
+ score = 0.45;
177
+ } else {
178
+ score = 0.3;
179
+ }
180
+
181
+ return {
182
+ score,
183
+ method: 'heuristic',
184
+ explanation: `Heuristic: ${highCount} high-confidence phrases, ${lowCount} low-confidence phrases`,
185
+ };
186
+ }
187
+
188
+ // Response length heuristic - longer, more detailed responses often indicate confidence
189
+ const wordCount = content.split(/\s+/).length;
190
+ if (wordCount > 200) {
191
+ return {
192
+ score: 0.7,
193
+ method: 'heuristic',
194
+ explanation: 'Detailed response suggests moderate-high confidence',
195
+ };
196
+ }
197
+
198
+ return { score: null, method: 'none' };
199
+ }
200
+
201
+ /**
202
+ * Evaluate whether to exit early based on provider responses
203
+ */
204
+ export function evaluateEarlyExit(
205
+ responses: Array<{ provider: string; content: string; confidence: number | undefined }>,
206
+ config: CascadingConfidenceConfig = DEFAULT_CASCADING_CONFIDENCE
207
+ ): EarlyExitDecision {
208
+ // Early exit disabled
209
+ if (!config.enabled) {
210
+ return {
211
+ shouldExit: false,
212
+ reason: 'Cascading confidence disabled',
213
+ providerCount: responses.length,
214
+ };
215
+ }
216
+
217
+ // Not enough providers yet
218
+ if (responses.length < config.minProviders) {
219
+ return {
220
+ shouldExit: false,
221
+ reason: `Need at least ${config.minProviders} providers, have ${responses.length}`,
222
+ providerCount: responses.length,
223
+ };
224
+ }
225
+
226
+ // Check if all responses have high confidence
227
+ const confidences: number[] = [];
228
+
229
+ for (const response of responses) {
230
+ // Use provided confidence or extract it
231
+ const confidence = response.confidence ?? extractConfidence(response.content).score;
232
+ if (confidence !== null) {
233
+ confidences.push(confidence);
234
+ }
235
+ }
236
+
237
+ if (confidences.length === 0) {
238
+ return {
239
+ shouldExit: false,
240
+ reason: 'No confidence scores available',
241
+ providerCount: responses.length,
242
+ };
243
+ }
244
+
245
+ // Calculate average confidence
246
+ const avgConfidence = confidences.reduce((a, b) => a + b, 0) / confidences.length;
247
+
248
+ // Check if average exceeds threshold
249
+ if (avgConfidence >= config.threshold) {
250
+ return {
251
+ shouldExit: true,
252
+ reason: `Average confidence ${(avgConfidence * 100).toFixed(0)}% exceeds threshold ${(config.threshold * 100).toFixed(0)}%`,
253
+ confidence: avgConfidence,
254
+ providerCount: responses.length,
255
+ };
256
+ }
257
+
258
+ // Check if first provider has high confidence (cascading pattern)
259
+ const firstConfidence = confidences[0];
260
+ if (firstConfidence !== undefined && firstConfidence >= config.threshold && responses.length >= config.minProviders) {
261
+ return {
262
+ shouldExit: true,
263
+ reason: `First provider confidence ${(firstConfidence * 100).toFixed(0)}% exceeds threshold`,
264
+ confidence: firstConfidence,
265
+ providerCount: responses.length,
266
+ };
267
+ }
268
+
269
+ return {
270
+ shouldExit: false,
271
+ reason: `Average confidence ${(avgConfidence * 100).toFixed(0)}% below threshold ${(config.threshold * 100).toFixed(0)}%`,
272
+ confidence: avgConfidence,
273
+ providerCount: responses.length,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Calculate agreement score between provider responses
279
+ * Higher score = more agreement = higher effective confidence
280
+ */
281
+ export function calculateAgreementScore(
282
+ responses: Array<{ content: string }>
283
+ ): number {
284
+ if (responses.length < 2) {
285
+ return 1.0; // Single response has perfect "agreement"
286
+ }
287
+
288
+ // Simple heuristic: check for common key phrases/conclusions
289
+ const keywords = new Map<string, number>();
290
+
291
+ for (const response of responses) {
292
+ const words = response.content
293
+ .toLowerCase()
294
+ .split(/\s+/)
295
+ .filter(w => w.length > 4); // Only meaningful words
296
+
297
+ for (const word of words) {
298
+ keywords.set(word, (keywords.get(word) || 0) + 1);
299
+ }
300
+ }
301
+
302
+ // Count words that appear in multiple responses
303
+ let sharedCount = 0;
304
+ const totalUniqueWords = keywords.size;
305
+
306
+ for (const count of keywords.values()) {
307
+ if (count > 1) {
308
+ sharedCount++;
309
+ }
310
+ }
311
+
312
+ if (totalUniqueWords === 0) {
313
+ return 0.5; // Default for empty responses
314
+ }
315
+
316
+ // Calculate agreement ratio (0-1)
317
+ const agreementRatio = sharedCount / totalUniqueWords;
318
+
319
+ // Scale to reasonable range (0.3 - 1.0)
320
+ return 0.3 + agreementRatio * 0.7;
321
+ }
@@ -124,6 +124,8 @@ export class SynthesisConsensus implements ConsensusExecutor {
124
124
  provider: r.provider,
125
125
  content: r.content,
126
126
  role: r.role,
127
+ // Indicate if this is an agent with domain expertise (INV-DISC-642)
128
+ isAgent: r.isAgent,
127
129
  }))
128
130
  );
129
131
  }
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Tallies votes from all providers and determines winner based on
5
5
  * raw counts or confidence-weighted scores.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-642: Agent responses weighted by agentWeightMultiplier (default 1.5x)
6
9
  */
7
10
 
8
11
  import type { VotingResults, VoteRecord } from '@defai.digital/contracts';
@@ -14,6 +17,11 @@ import {
14
17
  getProviderSystemPrompt,
15
18
  } from '../prompts/templates.js';
16
19
 
20
+ /**
21
+ * Default agent weight multiplier (INV-DISC-642)
22
+ */
23
+ const DEFAULT_AGENT_WEIGHT = 1.5;
24
+
17
25
  export class VotingConsensus implements ConsensusExecutor {
18
26
  readonly method = 'voting' as const;
19
27
 
@@ -36,8 +44,8 @@ export class VotingConsensus implements ConsensusExecutor {
36
44
  };
37
45
  }
38
46
 
39
- // Tally votes
40
- const votingResults = this.tallyVotes(votes, config.threshold);
47
+ // Tally votes with agent weight multiplier (INV-DISC-642)
48
+ const votingResults = this.tallyVotes(votes, context.agentWeightMultiplier);
41
49
 
42
50
  // Generate synthesis summary
43
51
  const synthesizerId = config.synthesizer ||
@@ -73,8 +81,8 @@ export class VotingConsensus implements ConsensusExecutor {
73
81
  };
74
82
  }
75
83
 
76
- private extractVotes(rounds: ConsensusExecutionContext['rounds']): VoteRecord[] {
77
- const votes: VoteRecord[] = [];
84
+ private extractVotes(rounds: ConsensusExecutionContext['rounds']): Array<VoteRecord & { isAgent: boolean }> {
85
+ const votes: Array<VoteRecord & { isAgent: boolean }> = [];
78
86
 
79
87
  for (const round of rounds) {
80
88
  for (const response of round.responses) {
@@ -84,6 +92,7 @@ export class VotingConsensus implements ConsensusExecutor {
84
92
  choice: response.vote,
85
93
  confidence: response.confidence,
86
94
  reasoning: this.extractReasoning(response.content),
95
+ isAgent: response.isAgent ?? false,
87
96
  });
88
97
  }
89
98
  }
@@ -97,14 +106,21 @@ export class VotingConsensus implements ConsensusExecutor {
97
106
  return reasoningMatch?.[1] ? reasoningMatch[1].trim() : '';
98
107
  }
99
108
 
100
- private tallyVotes(votes: VoteRecord[], _threshold: number): VotingResults {
109
+ private tallyVotes(
110
+ votes: Array<VoteRecord & { isAgent: boolean }>,
111
+ agentWeightMultiplier: number = DEFAULT_AGENT_WEIGHT
112
+ ): VotingResults {
101
113
  // Count raw votes
102
114
  const rawVotes: Record<string, number> = {};
103
115
  const weightedVotes: Record<string, number> = {};
104
116
 
105
117
  for (const vote of votes) {
118
+ // Apply agent weight multiplier (INV-DISC-642)
119
+ const weight = vote.isAgent ? agentWeightMultiplier : 1.0;
120
+ const weightedConfidence = vote.confidence * weight;
121
+
106
122
  rawVotes[vote.choice] = (rawVotes[vote.choice] || 0) + 1;
107
- weightedVotes[vote.choice] = (weightedVotes[vote.choice] || 0) + vote.confidence;
123
+ weightedVotes[vote.choice] = (weightedVotes[vote.choice] || 0) + weightedConfidence;
108
124
  }
109
125
 
110
126
  // Determine winner (by weighted votes)