@defai.digital/discussion-domain 13.0.3 → 13.1.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.
Files changed (72) 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 +195 -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.map +1 -1
  48. package/dist/provider-bridge.js +31 -30
  49. package/dist/provider-bridge.js.map +1 -1
  50. package/dist/recursive-executor.d.ts +77 -0
  51. package/dist/recursive-executor.d.ts.map +1 -0
  52. package/dist/recursive-executor.js +344 -0
  53. package/dist/recursive-executor.js.map +1 -0
  54. package/dist/types.d.ts +83 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/budget-manager.ts +272 -0
  59. package/src/confidence-extractor.ts +321 -0
  60. package/src/consensus/synthesis.ts +2 -0
  61. package/src/consensus/voting.ts +22 -6
  62. package/src/context-tracker.ts +307 -0
  63. package/src/cost-tracker.ts +362 -0
  64. package/src/executor.ts +9 -0
  65. package/src/index.ts +72 -0
  66. package/src/participant-resolver.ts +297 -0
  67. package/src/patterns/round-robin.ts +48 -2
  68. package/src/patterns/synthesis.ts +89 -3
  69. package/src/prompts/templates.ts +4 -2
  70. package/src/provider-bridge.ts +31 -28
  71. package/src/recursive-executor.ts +500 -0
  72. package/src/types.ts +120 -0
package/src/index.ts CHANGED
@@ -10,6 +10,32 @@
10
10
  // Main executor
11
11
  export { DiscussionExecutor, createDiscussionExecutor } from './executor.js';
12
12
 
13
+ // Recursive executor
14
+ export {
15
+ RecursiveDiscussionExecutor,
16
+ createRecursiveDiscussionExecutor,
17
+ type RecursiveDiscussionResult,
18
+ } from './recursive-executor.js';
19
+
20
+ // Context tracker for recursion management
21
+ export {
22
+ createContextTracker,
23
+ DiscussionContextError,
24
+ type DiscussionContextTracker,
25
+ type SubDiscussionCheck,
26
+ type ContextTrackerConfig,
27
+ } from './context-tracker.js';
28
+
29
+ // Budget manager for timeout cascade
30
+ export {
31
+ createBudgetManager,
32
+ formatBudgetStatus,
33
+ recommendProvidersForBudget,
34
+ type DiscussionBudgetManager,
35
+ type BudgetAllocation,
36
+ type BudgetStatus,
37
+ } from './budget-manager.js';
38
+
13
39
  // Types and interfaces
14
40
  export type {
15
41
  DiscussionProviderExecutor,
@@ -23,11 +49,41 @@ export type {
23
49
  ConsensusExecutionResult,
24
50
  DiscussionExecutorOptions,
25
51
  DiscussionProgressEvent,
52
+ // Cascading confidence types
53
+ CascadingConfidenceOptions,
54
+ EarlyExitInfo,
55
+ // Recursive types
56
+ RecursiveDiscussionExecutorOptions,
57
+ RecursivePatternExecutionContext,
58
+ RecursivePatternExecutionResult,
59
+ SubDiscussionRequest,
26
60
  } from './types.js';
27
61
 
28
62
  // Stub implementation for testing
29
63
  export { StubProviderExecutor } from './types.js';
30
64
 
65
+ // Confidence extraction and early exit
66
+ export {
67
+ extractConfidence,
68
+ evaluateEarlyExit,
69
+ calculateAgreementScore,
70
+ DEFAULT_CASCADING_CONFIDENCE,
71
+ type ExtractedConfidence,
72
+ type EarlyExitDecision,
73
+ } from './confidence-extractor.js';
74
+
75
+ // Cost tracking
76
+ export {
77
+ createCostTracker,
78
+ formatCost,
79
+ formatCostSummary,
80
+ PROVIDER_COSTS,
81
+ type DiscussionCostTracker,
82
+ type ProviderCallRecord,
83
+ type CostSummary,
84
+ type BudgetCheckResult,
85
+ } from './cost-tracker.js';
86
+
31
87
  // Pattern executors
32
88
  export {
33
89
  RoundRobinPattern,
@@ -67,3 +123,19 @@ export {
67
123
  type ProviderRegistryLike,
68
124
  type ProviderBridgeOptions,
69
125
  } from './provider-bridge.js';
126
+
127
+ // Participant resolution (for agent/provider mixing)
128
+ export {
129
+ resolveParticipant,
130
+ resolveParticipants,
131
+ providersToParticipants,
132
+ parseParticipantString,
133
+ parseParticipantList,
134
+ getProviderIds,
135
+ buildEnhancedSystemPrompt,
136
+ type ResolvedParticipant,
137
+ type AgentProfileLike,
138
+ type AgentRegistryLike,
139
+ type AbilityManagerLike,
140
+ type ParticipantResolverOptions,
141
+ } from './participant-resolver.js';
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Participant Resolver
3
+ *
4
+ * Resolves discussion participants (providers and agents) to their execution config.
5
+ * Handles agent-to-provider mapping using providerAffinity.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-640: Agent uses providerAffinity.preferred[0] for provider selection
9
+ * - INV-DISC-641: Agent abilities injected with max 10K tokens
10
+ * - INV-DISC-642: Agent weight multiplier between 0.5-3.0 (default 1.5)
11
+ */
12
+
13
+ import type { DiscussionParticipant } from '@defai.digital/contracts';
14
+
15
+ /**
16
+ * Resolved participant ready for discussion execution
17
+ */
18
+ export interface ResolvedParticipant {
19
+ /** Original participant identifier */
20
+ id: string;
21
+
22
+ /** Whether this is an agent (vs raw provider) */
23
+ isAgent: boolean;
24
+
25
+ /** Provider ID to use for LLM calls */
26
+ providerId: string;
27
+
28
+ /** Agent ID (if isAgent) */
29
+ agentId?: string | undefined;
30
+
31
+ /** System prompt override (from agent config) */
32
+ systemPromptOverride?: string | undefined;
33
+
34
+ /** Injected ability content */
35
+ abilityContent?: string | undefined;
36
+
37
+ /** Temperature override (from agent config) */
38
+ temperatureOverride?: number | undefined;
39
+
40
+ /** Weight multiplier for consensus (agents typically get higher weight) */
41
+ weightMultiplier: number;
42
+ }
43
+
44
+ /**
45
+ * Agent profile interface (subset needed for resolution)
46
+ */
47
+ export interface AgentProfileLike {
48
+ agentId: string;
49
+ systemPrompt?: string | undefined;
50
+ providerAffinity?: {
51
+ preferred?: string[] | undefined;
52
+ defaultSynthesizer?: string | undefined;
53
+ temperatureOverrides?: Record<string, number> | undefined;
54
+ } | undefined;
55
+ temperature?: number | undefined;
56
+ }
57
+
58
+ /**
59
+ * Agent registry interface for looking up agents
60
+ */
61
+ export interface AgentRegistryLike {
62
+ get(agentId: string): Promise<AgentProfileLike | null>;
63
+ }
64
+
65
+ /**
66
+ * Ability manager interface for injecting abilities
67
+ */
68
+ export interface AbilityManagerLike {
69
+ injectAbilities(
70
+ agentId: string,
71
+ task: string,
72
+ coreAbilities: string[],
73
+ options: { maxAbilities: number; maxTokens: number }
74
+ ): Promise<{ combinedContent: string; injectedAbilities: string[] }>;
75
+ }
76
+
77
+ /**
78
+ * Options for participant resolution
79
+ */
80
+ export interface ParticipantResolverOptions {
81
+ /** Agent registry for looking up agents */
82
+ agentRegistry?: AgentRegistryLike | undefined;
83
+
84
+ /** Ability manager for injecting abilities */
85
+ abilityManager?: AbilityManagerLike | undefined;
86
+
87
+ /** Default provider to use when agent has no preference */
88
+ defaultProvider?: string | undefined;
89
+
90
+ /** Default weight multiplier for agent participants */
91
+ agentWeightMultiplier?: number | undefined;
92
+
93
+ /** Maximum tokens for ability injection */
94
+ maxAbilityTokens?: number | undefined;
95
+
96
+ /** Topic for ability matching */
97
+ topic?: string | undefined;
98
+ }
99
+
100
+ /**
101
+ * Default provider when agent has no preference
102
+ */
103
+ const DEFAULT_PROVIDER = 'claude';
104
+
105
+ /**
106
+ * Default agent weight multiplier (INV-DISC-642)
107
+ */
108
+ const DEFAULT_AGENT_WEIGHT = 1.5;
109
+
110
+ /**
111
+ * Maximum tokens for ability injection (INV-DISC-641)
112
+ */
113
+ const DEFAULT_MAX_ABILITY_TOKENS = 10000;
114
+
115
+ /**
116
+ * Resolve a single participant to execution config
117
+ */
118
+ export async function resolveParticipant(
119
+ participant: DiscussionParticipant,
120
+ options: ParticipantResolverOptions = {}
121
+ ): Promise<ResolvedParticipant> {
122
+ const {
123
+ agentRegistry,
124
+ abilityManager,
125
+ defaultProvider = DEFAULT_PROVIDER,
126
+ agentWeightMultiplier = DEFAULT_AGENT_WEIGHT,
127
+ maxAbilityTokens = DEFAULT_MAX_ABILITY_TOKENS,
128
+ topic = '',
129
+ } = options;
130
+
131
+ // Provider participant - simple resolution
132
+ if (participant.type === 'provider') {
133
+ return {
134
+ id: participant.id,
135
+ isAgent: false,
136
+ providerId: participant.id,
137
+ weightMultiplier: 1.0, // Base weight for providers
138
+ };
139
+ }
140
+
141
+ // Agent participant - resolve via registry
142
+ const agentId = participant.id;
143
+
144
+ // If no registry, fall back to default provider with agent weight
145
+ if (!agentRegistry) {
146
+ return {
147
+ id: agentId,
148
+ isAgent: true,
149
+ providerId: defaultProvider,
150
+ agentId,
151
+ weightMultiplier: agentWeightMultiplier,
152
+ };
153
+ }
154
+
155
+ // Look up agent
156
+ const agent = await agentRegistry.get(agentId);
157
+
158
+ if (!agent) {
159
+ // Agent not found, fall back to default provider
160
+ return {
161
+ id: agentId,
162
+ isAgent: true,
163
+ providerId: defaultProvider,
164
+ agentId,
165
+ weightMultiplier: agentWeightMultiplier,
166
+ };
167
+ }
168
+
169
+ // INV-DISC-640: Use providerAffinity.preferred[0] or fallback to default
170
+ const providerId = agent.providerAffinity?.preferred?.[0] ?? defaultProvider;
171
+
172
+ // Get temperature override if configured
173
+ const temperatureOverride = agent.providerAffinity?.temperatureOverrides?.[providerId]
174
+ ?? agent.temperature;
175
+
176
+ // Build resolved participant
177
+ const resolved: ResolvedParticipant = {
178
+ id: agentId,
179
+ isAgent: true,
180
+ providerId,
181
+ agentId,
182
+ systemPromptOverride: agent.systemPrompt,
183
+ temperatureOverride,
184
+ weightMultiplier: agentWeightMultiplier,
185
+ };
186
+
187
+ // INV-DISC-641: Inject abilities if ability manager available
188
+ if (abilityManager && topic) {
189
+ try {
190
+ const injection = await abilityManager.injectAbilities(
191
+ agentId,
192
+ topic,
193
+ [], // coreAbilities
194
+ { maxAbilities: 5, maxTokens: maxAbilityTokens }
195
+ );
196
+
197
+ if (injection.combinedContent) {
198
+ resolved.abilityContent = injection.combinedContent;
199
+ }
200
+ } catch {
201
+ // Ability injection failed, continue without abilities
202
+ }
203
+ }
204
+
205
+ return resolved;
206
+ }
207
+
208
+ /**
209
+ * Resolve multiple participants
210
+ */
211
+ export async function resolveParticipants(
212
+ participants: DiscussionParticipant[],
213
+ options: ParticipantResolverOptions = {}
214
+ ): Promise<ResolvedParticipant[]> {
215
+ const resolved = await Promise.all(
216
+ participants.map(p => resolveParticipant(p, options))
217
+ );
218
+
219
+ return resolved;
220
+ }
221
+
222
+ /**
223
+ * Convert legacy string provider list to participant array
224
+ */
225
+ export function providersToParticipants(providers: string[]): DiscussionParticipant[] {
226
+ return providers.map(id => ({ type: 'provider' as const, id }));
227
+ }
228
+
229
+ /**
230
+ * Parse participant string (e.g., "claude" or "reviewer:agent")
231
+ */
232
+ export function parseParticipantString(input: string): DiscussionParticipant {
233
+ const parts = input.split(':');
234
+
235
+ if (parts.length === 2 && parts[1] === 'agent') {
236
+ return { type: 'agent', id: parts[0]! };
237
+ }
238
+
239
+ return { type: 'provider', id: parts[0]! };
240
+ }
241
+
242
+ /**
243
+ * Parse participant list from CLI input
244
+ * Format: "claude,glm,reviewer:agent,security:agent"
245
+ */
246
+ export function parseParticipantList(input: string): DiscussionParticipant[] {
247
+ return input
248
+ .split(',')
249
+ .map(s => s.trim())
250
+ .filter(s => s.length > 0)
251
+ .map(parseParticipantString);
252
+ }
253
+
254
+ /**
255
+ * Get provider IDs from resolved participants
256
+ */
257
+ export function getProviderIds(participants: ResolvedParticipant[]): string[] {
258
+ // Unique provider IDs preserving order
259
+ const seen = new Set<string>();
260
+ const result: string[] = [];
261
+
262
+ for (const p of participants) {
263
+ if (!seen.has(p.providerId)) {
264
+ seen.add(p.providerId);
265
+ result.push(p.providerId);
266
+ }
267
+ }
268
+
269
+ return result;
270
+ }
271
+
272
+ /**
273
+ * Build enhanced system prompt with agent context
274
+ */
275
+ export function buildEnhancedSystemPrompt(
276
+ basePrompt: string,
277
+ participant: ResolvedParticipant
278
+ ): string {
279
+ const parts: string[] = [];
280
+
281
+ // Add ability content first (provides domain knowledge)
282
+ if (participant.abilityContent) {
283
+ parts.push(participant.abilityContent);
284
+ parts.push('---');
285
+ }
286
+
287
+ // Add agent system prompt
288
+ if (participant.systemPromptOverride) {
289
+ parts.push(participant.systemPromptOverride);
290
+ parts.push('---');
291
+ }
292
+
293
+ // Add base system prompt
294
+ parts.push(basePrompt);
295
+
296
+ return parts.join('\n\n');
297
+ }
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Models respond sequentially, each building on previous responses.
5
5
  * Good for: brainstorming, iterative refinement, building consensus.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-643: Early exit only after minProviders responded
6
9
  */
7
10
 
8
11
  import type { DiscussionRound, DebateRole } from '@defai.digital/contracts';
@@ -10,7 +13,9 @@ import type {
10
13
  PatternExecutor,
11
14
  PatternExecutionContext,
12
15
  PatternExecutionResult,
16
+ EarlyExitInfo,
13
17
  } from '../types.js';
18
+ import { extractConfidence, evaluateEarlyExit } from '../confidence-extractor.js';
14
19
 
15
20
  // Local type for discussion ProviderResponse (avoids conflict with provider/v1 ProviderResponse)
16
21
  interface DiscussionProviderResponse {
@@ -39,12 +44,13 @@ export class RoundRobinPattern implements PatternExecutor {
39
44
 
40
45
  async execute(context: PatternExecutionContext): Promise<PatternExecutionResult> {
41
46
  const startTime = Date.now();
42
- const { config, providerExecutor, availableProviders, abortSignal, onProgress } = context;
47
+ const { config, providerExecutor, availableProviders, abortSignal, onProgress, cascadingConfidence } = context;
43
48
 
44
49
  const rounds: DiscussionRound[] = [];
45
50
  const participatingProviders = new Set<string>();
46
51
  const failedProviders = new Set<string>();
47
52
  const allResponses: DiscussionProviderResponse[] = [];
53
+ let earlyExit: EarlyExitInfo | undefined;
48
54
 
49
55
  // Filter to available providers
50
56
  const providers = config.providers.filter(p => availableProviders.includes(p));
@@ -110,15 +116,20 @@ export class RoundRobinPattern implements PatternExecutor {
110
116
  abortSignal,
111
117
  });
112
118
 
119
+ // Extract confidence from response content
120
+ const content = result.success ? result.content || '' : '';
121
+ const confidenceResult = result.success ? extractConfidence(content) : null;
122
+
113
123
  const response: DiscussionProviderResponse = {
114
124
  provider: providerId,
115
- content: result.success ? result.content || '' : '',
125
+ content,
116
126
  round: roundNum,
117
127
  timestamp: new Date().toISOString(),
118
128
  durationMs: result.durationMs,
119
129
  tokenCount: result.tokenCount,
120
130
  truncated: result.truncated,
121
131
  error: result.success ? undefined : result.error,
132
+ confidence: confidenceResult?.score ?? undefined,
122
133
  };
123
134
 
124
135
  roundResponses.push(response);
@@ -184,6 +195,40 @@ export class RoundRobinPattern implements PatternExecutor {
184
195
  error: `Only ${participatingProviders.size} providers succeeded, need ${config.minProviders}`,
185
196
  };
186
197
  }
198
+
199
+ // Check for early exit after each round (INV-DISC-643)
200
+ if (cascadingConfidence?.enabled && roundNum < config.rounds) {
201
+ const responsesWithConfidence = roundResponses
202
+ .filter(r => !r.error)
203
+ .map(r => ({
204
+ provider: r.provider,
205
+ content: r.content,
206
+ confidence: r.confidence,
207
+ }));
208
+
209
+ const exitDecision = evaluateEarlyExit(responsesWithConfidence, {
210
+ enabled: cascadingConfidence.enabled,
211
+ threshold: cascadingConfidence.threshold,
212
+ minProviders: cascadingConfidence.minProviders,
213
+ });
214
+
215
+ if (exitDecision.shouldExit) {
216
+ earlyExit = {
217
+ triggered: true,
218
+ reason: exitDecision.reason,
219
+ atProviderCount: exitDecision.providerCount,
220
+ confidenceScore: exitDecision.confidence,
221
+ };
222
+
223
+ onProgress?.({
224
+ type: 'round_complete',
225
+ message: `Early exit after round ${roundNum}: ${exitDecision.reason}`,
226
+ timestamp: new Date().toISOString(),
227
+ });
228
+
229
+ break; // Exit the rounds loop
230
+ }
231
+ }
187
232
  }
188
233
 
189
234
  return {
@@ -192,6 +237,7 @@ export class RoundRobinPattern implements PatternExecutor {
192
237
  failedProviders: Array.from(failedProviders),
193
238
  totalDurationMs: Date.now() - startTime,
194
239
  success: participatingProviders.size >= config.minProviders,
240
+ earlyExit: earlyExit ?? { triggered: false },
195
241
  };
196
242
  }
197
243
 
@@ -9,6 +9,9 @@
9
9
  * 2. Round 2: Each provider responds to others' perspectives
10
10
  * 3. (Optional) Additional cross-discussion rounds
11
11
  * 4. Final synthesis by designated synthesizer (default: claude)
12
+ *
13
+ * Invariants:
14
+ * - INV-DISC-643: Early exit only after minProviders responded
12
15
  */
13
16
 
14
17
  import type { DiscussionRound, DebateRole } from '@defai.digital/contracts';
@@ -16,6 +19,7 @@ import type {
16
19
  PatternExecutor,
17
20
  PatternExecutionContext,
18
21
  PatternExecutionResult,
22
+ EarlyExitInfo,
19
23
  } from '../types.js';
20
24
  import {
21
25
  SYNTHESIS_INITIAL,
@@ -24,6 +28,7 @@ import {
24
28
  formatPreviousResponses,
25
29
  getProviderSystemPrompt,
26
30
  } from '../prompts/templates.js';
31
+ import { extractConfidence, evaluateEarlyExit } from '../confidence-extractor.js';
27
32
 
28
33
  // Local type for discussion DiscussionProviderResponse (avoids conflict with provider/v1 DiscussionProviderResponse)
29
34
  interface DiscussionProviderResponse {
@@ -45,11 +50,12 @@ export class SynthesisPattern implements PatternExecutor {
45
50
 
46
51
  async execute(context: PatternExecutionContext): Promise<PatternExecutionResult> {
47
52
  const startTime = Date.now();
48
- const { config, providerExecutor, availableProviders, abortSignal, onProgress } = context;
53
+ const { config, providerExecutor, availableProviders, abortSignal, onProgress, cascadingConfidence } = context;
49
54
 
50
55
  const rounds: DiscussionRound[] = [];
51
56
  const participatingProviders = new Set<string>();
52
57
  const failedProviders = new Set<string>();
58
+ let earlyExit: EarlyExitInfo | undefined;
53
59
 
54
60
  // Filter to available providers
55
61
  const providers = config.providers.filter(p => availableProviders.includes(p));
@@ -106,6 +112,43 @@ export class SynthesisPattern implements PatternExecutor {
106
112
  };
107
113
  }
108
114
 
115
+ // Check for early exit after round 1 (INV-DISC-643)
116
+ if (cascadingConfidence?.enabled) {
117
+ const responsesWithConfidence = initialRound.round.responses.map(r => ({
118
+ provider: r.provider,
119
+ content: r.content,
120
+ confidence: r.confidence,
121
+ }));
122
+
123
+ const exitDecision = evaluateEarlyExit(responsesWithConfidence, {
124
+ enabled: cascadingConfidence.enabled,
125
+ threshold: cascadingConfidence.threshold,
126
+ minProviders: cascadingConfidence.minProviders,
127
+ });
128
+
129
+ if (exitDecision.shouldExit) {
130
+ onProgress?.({
131
+ type: 'round_complete',
132
+ message: `Early exit triggered: ${exitDecision.reason}`,
133
+ timestamp: new Date().toISOString(),
134
+ });
135
+
136
+ return {
137
+ rounds,
138
+ participatingProviders: Array.from(participatingProviders),
139
+ failedProviders: Array.from(failedProviders),
140
+ totalDurationMs: Date.now() - startTime,
141
+ success: true,
142
+ earlyExit: {
143
+ triggered: true,
144
+ reason: exitDecision.reason,
145
+ atProviderCount: exitDecision.providerCount,
146
+ confidenceScore: exitDecision.confidence,
147
+ },
148
+ };
149
+ }
150
+ }
151
+
109
152
  // Additional rounds: Cross-discussion
110
153
  for (let roundNum = 2; roundNum <= config.rounds; roundNum++) {
111
154
  if (abortSignal?.aborted) {
@@ -138,6 +181,38 @@ export class SynthesisPattern implements PatternExecutor {
138
181
  message: `Cross-discussion round ${roundNum} complete`,
139
182
  timestamp: new Date().toISOString(),
140
183
  });
184
+
185
+ // Check for early exit after each round
186
+ if (cascadingConfidence?.enabled && roundNum < config.rounds) {
187
+ const allResponses = crossRound.round.responses.map(r => ({
188
+ provider: r.provider,
189
+ content: r.content,
190
+ confidence: r.confidence,
191
+ }));
192
+
193
+ const exitDecision = evaluateEarlyExit(allResponses, {
194
+ enabled: cascadingConfidence.enabled,
195
+ threshold: cascadingConfidence.threshold,
196
+ minProviders: cascadingConfidence.minProviders,
197
+ });
198
+
199
+ if (exitDecision.shouldExit) {
200
+ earlyExit = {
201
+ triggered: true,
202
+ reason: exitDecision.reason,
203
+ atProviderCount: exitDecision.providerCount,
204
+ confidenceScore: exitDecision.confidence,
205
+ };
206
+
207
+ onProgress?.({
208
+ type: 'round_complete',
209
+ message: `Early exit after round ${roundNum}: ${exitDecision.reason}`,
210
+ timestamp: new Date().toISOString(),
211
+ });
212
+
213
+ break; // Exit the rounds loop
214
+ }
215
+ }
141
216
  }
142
217
 
143
218
  return {
@@ -146,6 +221,7 @@ export class SynthesisPattern implements PatternExecutor {
146
221
  failedProviders: Array.from(failedProviders),
147
222
  totalDurationMs: Date.now() - startTime,
148
223
  success: participatingProviders.size >= config.minProviders,
224
+ earlyExit: earlyExit ?? { triggered: false },
149
225
  };
150
226
  }
151
227
 
@@ -193,15 +269,20 @@ export class SynthesisPattern implements PatternExecutor {
193
269
  abortSignal,
194
270
  });
195
271
 
272
+ // Extract confidence from response content
273
+ const content = result.success ? result.content || '' : '';
274
+ const confidenceResult = result.success ? extractConfidence(content) : null;
275
+
196
276
  const response: DiscussionProviderResponse = {
197
277
  provider: providerId,
198
- content: result.success ? result.content || '' : '',
278
+ content,
199
279
  round: roundNum,
200
280
  timestamp: new Date().toISOString(),
201
281
  durationMs: result.durationMs,
202
282
  tokenCount: result.tokenCount,
203
283
  truncated: result.truncated,
204
284
  error: result.success ? undefined : result.error,
285
+ confidence: confidenceResult?.score ?? undefined,
205
286
  };
206
287
 
207
288
  if (result.success) {
@@ -300,14 +381,19 @@ export class SynthesisPattern implements PatternExecutor {
300
381
  abortSignal,
301
382
  });
302
383
 
384
+ // Extract confidence from response content
385
+ const content = result.success ? result.content || '' : '';
386
+ const confidenceResult = result.success ? extractConfidence(content) : null;
387
+
303
388
  const response: DiscussionProviderResponse = {
304
389
  provider: providerId,
305
- content: result.success ? result.content || '' : '',
390
+ content,
306
391
  round: roundNum,
307
392
  timestamp: new Date().toISOString(),
308
393
  durationMs: result.durationMs,
309
394
  tokenCount: result.tokenCount,
310
395
  error: result.success ? undefined : result.error,
396
+ confidence: confidenceResult?.score ?? undefined,
311
397
  };
312
398
 
313
399
  if (!result.success) {
@@ -335,12 +335,14 @@ export function interpolate(template: string, variables: Record<string, string>)
335
335
  * Format previous responses for inclusion in prompts
336
336
  */
337
337
  export function formatPreviousResponses(
338
- responses: { provider: string; content: string; role?: DebateRole | undefined }[]
338
+ responses: { provider: string; content: string; role?: DebateRole | undefined; isAgent?: boolean | undefined }[]
339
339
  ): string {
340
340
  return responses
341
341
  .map(r => {
342
342
  const roleLabel = r.role ? ` (${r.role})` : '';
343
- return `### ${r.provider}${roleLabel}\n${r.content}`;
343
+ // INV-DISC-642: Indicate agent responses for synthesis weighting
344
+ const agentLabel = r.isAgent ? ' [Agent - Domain Expert]' : '';
345
+ return `### ${r.provider}${roleLabel}${agentLabel}\n${r.content}`;
344
346
  })
345
347
  .join('\n\n');
346
348
  }