@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.
- package/dist/budget-manager.d.ts +79 -0
- package/dist/budget-manager.d.ts.map +1 -0
- package/dist/budget-manager.js +155 -0
- package/dist/budget-manager.js.map +1 -0
- package/dist/confidence-extractor.d.ts +60 -0
- package/dist/confidence-extractor.d.ts.map +1 -0
- package/dist/confidence-extractor.js +251 -0
- package/dist/confidence-extractor.js.map +1 -0
- package/dist/consensus/synthesis.d.ts.map +1 -1
- package/dist/consensus/synthesis.js +2 -0
- package/dist/consensus/synthesis.js.map +1 -1
- package/dist/consensus/voting.d.ts +3 -0
- package/dist/consensus/voting.d.ts.map +1 -1
- package/dist/consensus/voting.js +15 -4
- package/dist/consensus/voting.js.map +1 -1
- package/dist/context-tracker.d.ts +77 -0
- package/dist/context-tracker.d.ts.map +1 -0
- package/dist/context-tracker.js +177 -0
- package/dist/context-tracker.js.map +1 -0
- package/dist/cost-tracker.d.ts +123 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +196 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +9 -0
- package/dist/executor.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/participant-resolver.d.ts +111 -0
- package/dist/participant-resolver.d.ts.map +1 -0
- package/dist/participant-resolver.js +160 -0
- package/dist/participant-resolver.js.map +1 -0
- package/dist/patterns/round-robin.d.ts +3 -0
- package/dist/patterns/round-robin.d.ts.map +1 -1
- package/dist/patterns/round-robin.js +41 -2
- package/dist/patterns/round-robin.js.map +1 -1
- package/dist/patterns/synthesis.d.ts +3 -0
- package/dist/patterns/synthesis.d.ts.map +1 -1
- package/dist/patterns/synthesis.js +77 -3
- package/dist/patterns/synthesis.js.map +1 -1
- package/dist/prompts/templates.d.ts +1 -0
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/prompts/templates.js +3 -1
- package/dist/prompts/templates.js.map +1 -1
- package/dist/provider-bridge.d.ts +3 -1
- package/dist/provider-bridge.d.ts.map +1 -1
- package/dist/provider-bridge.js +48 -32
- package/dist/provider-bridge.js.map +1 -1
- package/dist/recursive-executor.d.ts +80 -0
- package/dist/recursive-executor.d.ts.map +1 -0
- package/dist/recursive-executor.js +354 -0
- package/dist/recursive-executor.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/budget-manager.ts +272 -0
- package/src/confidence-extractor.ts +321 -0
- package/src/consensus/synthesis.ts +2 -0
- package/src/consensus/voting.ts +22 -6
- package/src/context-tracker.ts +307 -0
- package/src/cost-tracker.ts +363 -0
- package/src/executor.ts +9 -0
- package/src/index.ts +72 -0
- package/src/participant-resolver.ts +297 -0
- package/src/patterns/round-robin.ts +48 -2
- package/src/patterns/synthesis.ts +89 -3
- package/src/prompts/templates.ts +4 -2
- package/src/provider-bridge.ts +52 -31
- package/src/recursive-executor.ts +510 -0
- 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
|
+
}
|
package/src/consensus/voting.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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) +
|
|
123
|
+
weightedVotes[vote.choice] = (weightedVotes[vote.choice] || 0) + weightedConfidence;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
// Determine winner (by weighted votes)
|