@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
@@ -62,13 +62,29 @@ type CompletionResponseLike =
62
62
 
63
63
  /**
64
64
  * Minimal HealthCheckResult interface
65
+ * Note: Supports both `healthy: boolean` and `status: 'healthy'|'unhealthy'` formats
65
66
  */
66
67
  interface HealthCheckResultLike {
67
- healthy: boolean;
68
+ healthy?: boolean | undefined;
69
+ status?: 'healthy' | 'degraded' | 'unhealthy' | undefined;
68
70
  message?: string | undefined;
69
71
  latencyMs: number;
70
72
  }
71
73
 
74
+ /**
75
+ * Helper to check if a health result indicates healthy status
76
+ */
77
+ function isHealthy(result: HealthCheckResultLike): boolean {
78
+ // Support both formats: `healthy: boolean` and `status: 'healthy'`
79
+ if (result.healthy !== undefined) {
80
+ return result.healthy;
81
+ }
82
+ if (result.status !== undefined) {
83
+ return result.status === 'healthy';
84
+ }
85
+ return false;
86
+ }
87
+
72
88
  /**
73
89
  * Minimal ProviderRegistry interface
74
90
  */
@@ -218,11 +234,12 @@ export function createProviderBridge(
218
234
  if (performHealthChecks) {
219
235
  try {
220
236
  const health = await provider.checkHealth();
237
+ const healthy = isHealthy(health);
221
238
  healthCache.set(providerId, {
222
- healthy: health.healthy,
239
+ healthy,
223
240
  timestamp: Date.now(),
224
241
  });
225
- return health.healthy;
242
+ return healthy;
226
243
  } catch {
227
244
  healthCache.set(providerId, {
228
245
  healthy: false,
@@ -238,43 +255,47 @@ export function createProviderBridge(
238
255
 
239
256
  async getAvailableProviders(): Promise<string[]> {
240
257
  const allProviders = registry.getAll();
241
- const available: string[] = [];
258
+ const now = Date.now();
259
+
260
+ // Separate cached vs uncached providers
261
+ const cached: string[] = [];
262
+ const toCheck: LLMProviderLike[] = [];
242
263
 
243
- // Check each provider's availability
244
264
  for (const provider of allProviders) {
245
- // Check cache first
246
- const cached = healthCache.get(provider.providerId);
247
- if (cached && Date.now() - cached.timestamp < healthCheckCacheMs) {
248
- if (cached.healthy) {
249
- available.push(provider.providerId);
250
- }
251
- continue;
265
+ const entry = healthCache.get(provider.providerId);
266
+ if (entry && now - entry.timestamp < healthCheckCacheMs) {
267
+ if (entry.healthy) cached.push(provider.providerId);
268
+ } else if (performHealthChecks) {
269
+ toCheck.push(provider);
270
+ } else {
271
+ cached.push(provider.providerId); // No health check, assume available
252
272
  }
273
+ }
274
+
275
+ // Check uncached providers in parallel
276
+ if (toCheck.length === 0) return cached;
253
277
 
254
- // Perform health check if enabled
255
- if (performHealthChecks) {
278
+ const results = await Promise.allSettled(
279
+ toCheck.map(async (provider) => {
256
280
  try {
257
281
  const health = await provider.checkHealth();
258
- healthCache.set(provider.providerId, {
259
- healthy: health.healthy,
260
- timestamp: Date.now(),
261
- });
262
- if (health.healthy) {
263
- available.push(provider.providerId);
264
- }
282
+ const healthy = isHealthy(health);
283
+ healthCache.set(provider.providerId, { healthy, timestamp: Date.now() });
284
+ return healthy ? provider.providerId : null;
265
285
  } catch {
266
- healthCache.set(provider.providerId, {
267
- healthy: false,
268
- timestamp: Date.now(),
269
- });
286
+ healthCache.set(provider.providerId, { healthy: false, timestamp: Date.now() });
287
+ return null;
270
288
  }
271
- } else {
272
- // If health checks disabled, assume available
273
- available.push(provider.providerId);
274
- }
275
- }
289
+ })
290
+ );
291
+
292
+ // Collect successful health checks
293
+ const checked = results
294
+ .filter((r): r is PromiseFulfilledResult<string | null> => r.status === 'fulfilled')
295
+ .map((r) => r.value)
296
+ .filter((id): id is string => id !== null);
276
297
 
277
- return available;
298
+ return [...cached, ...checked];
278
299
  },
279
300
  };
280
301
  }
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Recursive Discussion Executor
3
+ *
4
+ * Extends the base discussion executor with support for recursive sub-discussions.
5
+ * Providers can spawn sub-discussions during their response.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-600: Depth never exceeds maxDepth
9
+ * - INV-DISC-601: No circular discussions
10
+ * - INV-DISC-610: Child timeout ≤ parent remaining budget
11
+ * - INV-DISC-620: Total calls ≤ maxTotalCalls
12
+ */
13
+
14
+ import {
15
+ DEFAULT_PROVIDERS,
16
+ DiscussionErrorCodes,
17
+ createFailedDiscussionResult,
18
+ type DiscussStepConfig,
19
+ type DiscussionResult,
20
+ type DiscussionRequest,
21
+ type DiscussionContext,
22
+ type SubDiscussionResult,
23
+ type RecursiveConfig,
24
+ type TimeoutConfig,
25
+ type CostControlConfig,
26
+ DEFAULT_DISCUSSION_DEPTH,
27
+ DEFAULT_TOTAL_BUDGET_MS,
28
+ DEFAULT_MAX_TOTAL_CALLS,
29
+ MIN_SYNTHESIS_TIME_MS,
30
+ } from '@defai.digital/contracts';
31
+
32
+ import type {
33
+ DiscussionProviderExecutor,
34
+ RecursiveDiscussionExecutorOptions,
35
+ DiscussionProgressEvent,
36
+ RecursivePatternExecutionResult,
37
+ } from './types.js';
38
+ import { getPatternExecutor } from './patterns/index.js';
39
+ import { getConsensusExecutor } from './consensus/index.js';
40
+ import { createContextTracker } from './context-tracker.js';
41
+ import { createBudgetManager } from './budget-manager.js';
42
+
43
+ /**
44
+ * Extended discussion result with recursive info
45
+ */
46
+ export interface RecursiveDiscussionResult extends DiscussionResult {
47
+ /** Sub-discussions that were spawned */
48
+ subDiscussions?: SubDiscussionResult[];
49
+
50
+ /** Total provider calls across all levels */
51
+ totalProviderCalls?: number;
52
+
53
+ /** Maximum depth reached */
54
+ maxDepthReached?: number;
55
+
56
+ /** Discussion context */
57
+ context?: DiscussionContext;
58
+ }
59
+
60
+ /**
61
+ * Recursive discussion executor class.
62
+ *
63
+ * Orchestrates multi-model discussions with support for nested sub-discussions.
64
+ */
65
+ export class RecursiveDiscussionExecutor {
66
+ private readonly providerExecutor: DiscussionProviderExecutor;
67
+ private readonly defaultTimeoutMs: number;
68
+ private readonly checkProviderHealth: boolean;
69
+ private readonly traceId: string | undefined;
70
+ private readonly recursiveConfig: RecursiveConfig;
71
+ private readonly timeoutConfig: TimeoutConfig;
72
+ private readonly costConfig: CostControlConfig;
73
+ private readonly parentContext: DiscussionContext | undefined;
74
+ private readonly onSubDiscussionSpawn: ((context: DiscussionContext, topic: string) => void) | undefined;
75
+ private readonly onSubDiscussionComplete: ((result: SubDiscussionResult) => void) | undefined;
76
+
77
+ constructor(options: RecursiveDiscussionExecutorOptions) {
78
+ this.providerExecutor = options.providerExecutor;
79
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? 60000;
80
+ this.checkProviderHealth = options.checkProviderHealth ?? true;
81
+ this.traceId = options.traceId;
82
+ this.parentContext = options.parentContext;
83
+ this.onSubDiscussionSpawn = options.onSubDiscussionSpawn;
84
+ this.onSubDiscussionComplete = options.onSubDiscussionComplete;
85
+
86
+ // Initialize recursive config with defaults
87
+ this.recursiveConfig = {
88
+ enabled: options.recursive?.enabled ?? false,
89
+ maxDepth: options.recursive?.maxDepth ?? DEFAULT_DISCUSSION_DEPTH,
90
+ allowedProviders: options.recursive?.allowedProviders,
91
+ allowSubDiscussions: options.recursive?.allowSubDiscussions ?? true,
92
+ };
93
+
94
+ // Initialize timeout config
95
+ this.timeoutConfig = {
96
+ strategy: options.timeout?.strategy ?? 'cascade',
97
+ totalBudgetMs: options.timeout?.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS,
98
+ minSynthesisMs: options.timeout?.minSynthesisMs ?? MIN_SYNTHESIS_TIME_MS,
99
+ levelTimeouts: options.timeout?.levelTimeouts,
100
+ };
101
+
102
+ // Initialize cost config
103
+ this.costConfig = {
104
+ maxTotalCalls: options.cost?.maxTotalCalls ?? DEFAULT_MAX_TOTAL_CALLS,
105
+ budgetUsd: options.cost?.budgetUsd,
106
+ cascadingConfidence: {
107
+ enabled: options.cost?.cascadingConfidence?.enabled ?? true,
108
+ threshold: options.cost?.cascadingConfidence?.threshold ?? 0.9,
109
+ minProviders: options.cost?.cascadingConfidence?.minProviders ?? 2,
110
+ },
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Execute a recursive discussion from a DiscussionRequest
116
+ */
117
+ async executeRequest(
118
+ request: DiscussionRequest,
119
+ options?: {
120
+ abortSignal?: AbortSignal;
121
+ onProgress?: (event: DiscussionProgressEvent) => void;
122
+ }
123
+ ): Promise<RecursiveDiscussionResult> {
124
+ // Convert request to step config
125
+ const config: DiscussStepConfig = {
126
+ pattern: request.pattern || 'synthesis',
127
+ rounds: request.rounds || 2,
128
+ providers: request.providers || [...DEFAULT_PROVIDERS],
129
+ prompt: request.topic,
130
+ consensus: {
131
+ method: request.consensusMethod || 'synthesis',
132
+ threshold: 0.5,
133
+ synthesizer: 'claude',
134
+ includeDissent: true,
135
+ },
136
+ context: request.context,
137
+ verbose: request.verbose ?? false,
138
+ providerTimeout: this.defaultTimeoutMs,
139
+ continueOnProviderFailure: true,
140
+ minProviders: 2,
141
+ temperature: 0.7,
142
+ agentWeightMultiplier: 1.5,
143
+ };
144
+
145
+ return this.execute(config, options);
146
+ }
147
+
148
+ /**
149
+ * Execute a recursive discussion with full configuration
150
+ */
151
+ async execute(
152
+ config: DiscussStepConfig,
153
+ options?: {
154
+ abortSignal?: AbortSignal;
155
+ onProgress?: (event: DiscussionProgressEvent) => void;
156
+ }
157
+ ): Promise<RecursiveDiscussionResult> {
158
+ const startedAt = new Date().toISOString();
159
+ const discussionId = crypto.randomUUID();
160
+ const { abortSignal, onProgress } = options || {};
161
+
162
+ // Check for early abort
163
+ if (abortSignal?.aborted) {
164
+ return createFailedDiscussionResult(
165
+ config.pattern,
166
+ config.prompt,
167
+ DiscussionErrorCodes.INVALID_CONFIG,
168
+ 'Discussion aborted before starting',
169
+ startedAt
170
+ ) as RecursiveDiscussionResult;
171
+ }
172
+
173
+ // Create context tracker
174
+ const contextTracker = createContextTracker(
175
+ discussionId,
176
+ {
177
+ recursive: this.recursiveConfig,
178
+ timeout: this.timeoutConfig,
179
+ cost: this.costConfig,
180
+ },
181
+ this.parentContext
182
+ );
183
+
184
+ // Create budget manager
185
+ const budgetManager = createBudgetManager(
186
+ this.timeoutConfig,
187
+ this.recursiveConfig.maxDepth
188
+ );
189
+
190
+ // Check provider availability
191
+ let availableProviders: string[];
192
+ try {
193
+ availableProviders = await this.checkProviders(config.providers);
194
+ } catch (error) {
195
+ const errorMessage = error instanceof Error ? error.message : String(error);
196
+ return createFailedDiscussionResult(
197
+ config.pattern,
198
+ config.prompt,
199
+ DiscussionErrorCodes.ALL_PROVIDERS_FAILED,
200
+ `Provider health check failed: ${errorMessage}`,
201
+ startedAt
202
+ ) as RecursiveDiscussionResult;
203
+ }
204
+
205
+ // Filter by allowed providers if recursive
206
+ if (this.recursiveConfig.enabled && this.recursiveConfig.allowedProviders) {
207
+ availableProviders = availableProviders.filter(
208
+ p => this.recursiveConfig.allowedProviders!.includes(p)
209
+ );
210
+ }
211
+
212
+ // Check minimum providers
213
+ if (availableProviders.length < config.minProviders) {
214
+ return createFailedDiscussionResult(
215
+ config.pattern,
216
+ config.prompt,
217
+ DiscussionErrorCodes.INSUFFICIENT_PROVIDERS,
218
+ `Only ${availableProviders.length} providers available, need ${config.minProviders}`,
219
+ startedAt
220
+ ) as RecursiveDiscussionResult;
221
+ }
222
+
223
+ // Track sub-discussions
224
+ const subDiscussions: SubDiscussionResult[] = [];
225
+ let totalProviderCalls = 0;
226
+ let maxDepthReached = contextTracker.getContext().depth;
227
+
228
+ // Create sub-discussion spawner
229
+ const spawnSubDiscussion = async (
230
+ topic: string,
231
+ providers?: string[]
232
+ ): Promise<SubDiscussionResult | null> => {
233
+ // Check if we can spawn
234
+ const check = contextTracker.canSpawnSubDiscussion();
235
+ if (!check.allowed) {
236
+ onProgress?.({
237
+ type: 'provider_complete',
238
+ message: `Sub-discussion blocked: ${check.reason}`,
239
+ timestamp: new Date().toISOString(),
240
+ });
241
+ return null;
242
+ }
243
+
244
+ // Create child context
245
+ const childId = crypto.randomUUID();
246
+ const childContext = contextTracker.createChildContext(childId);
247
+
248
+ // Notify spawn
249
+ this.onSubDiscussionSpawn?.(childContext, topic);
250
+
251
+ onProgress?.({
252
+ type: 'round_start',
253
+ message: `Spawning sub-discussion at depth ${childContext.depth}: ${topic.slice(0, 50)}...`,
254
+ timestamp: new Date().toISOString(),
255
+ });
256
+
257
+ // Create child executor
258
+ const childExecutor = new RecursiveDiscussionExecutor({
259
+ providerExecutor: this.providerExecutor,
260
+ defaultTimeoutMs: budgetManager.getProviderTimeout(childContext.depth),
261
+ checkProviderHealth: false, // Already checked parent providers
262
+ traceId: this.traceId,
263
+ recursive: {
264
+ ...this.recursiveConfig,
265
+ maxDepth: this.recursiveConfig.maxDepth, // Keep same max depth
266
+ },
267
+ timeout: {
268
+ ...this.timeoutConfig,
269
+ totalBudgetMs: childContext.remainingBudgetMs,
270
+ },
271
+ cost: this.costConfig,
272
+ parentContext: childContext,
273
+ });
274
+
275
+ // Execute sub-discussion
276
+ const subStart = Date.now();
277
+ const subResult = await childExecutor.execute(
278
+ {
279
+ pattern: 'synthesis',
280
+ rounds: 1, // Sub-discussions are quick
281
+ providers: providers || availableProviders.slice(0, 3),
282
+ prompt: topic,
283
+ consensus: { method: 'synthesis', synthesizer: 'claude', threshold: 0.5, includeDissent: false },
284
+ providerTimeout: budgetManager.getProviderTimeout(childContext.depth),
285
+ continueOnProviderFailure: true,
286
+ minProviders: 2,
287
+ temperature: 0.7,
288
+ verbose: false,
289
+ agentWeightMultiplier: 1.5,
290
+ },
291
+ abortSignal ? { abortSignal } : {}
292
+ );
293
+ const subDuration = Date.now() - subStart;
294
+
295
+ // Record usage
296
+ contextTracker.recordCalls(subResult.participatingProviders.length);
297
+ contextTracker.recordElapsed(subDuration);
298
+ budgetManager.recordUsage(childContext.depth, subDuration);
299
+
300
+ // Update tracking
301
+ totalProviderCalls += subResult.totalProviderCalls ?? subResult.participatingProviders.length;
302
+ maxDepthReached = Math.max(maxDepthReached, childContext.depth);
303
+
304
+ // Create sub-discussion result
305
+ const subDiscussionResult: SubDiscussionResult = {
306
+ discussionId: childId,
307
+ topic,
308
+ participatingProviders: subResult.participatingProviders,
309
+ synthesis: subResult.synthesis,
310
+ durationMs: subDuration,
311
+ depth: childContext.depth,
312
+ };
313
+
314
+ subDiscussions.push(subDiscussionResult);
315
+ this.onSubDiscussionComplete?.(subDiscussionResult);
316
+
317
+ onProgress?.({
318
+ type: 'round_complete',
319
+ message: `Sub-discussion completed at depth ${childContext.depth}`,
320
+ timestamp: new Date().toISOString(),
321
+ });
322
+
323
+ return subDiscussionResult;
324
+ };
325
+
326
+ // Get pattern executor
327
+ const patternExecutor = getPatternExecutor(config.pattern);
328
+
329
+ // Build execution context with recursion support
330
+ const patternContext = {
331
+ config: {
332
+ ...config,
333
+ providerTimeout: budgetManager.getProviderTimeout(contextTracker.getContext().depth),
334
+ },
335
+ providerExecutor: this.providerExecutor,
336
+ availableProviders,
337
+ abortSignal,
338
+ traceId: this.traceId,
339
+ onProgress,
340
+ // Cascading confidence for early exit
341
+ cascadingConfidence: this.costConfig.cascadingConfidence ? {
342
+ enabled: this.costConfig.cascadingConfidence.enabled ?? true,
343
+ threshold: this.costConfig.cascadingConfidence.threshold ?? 0.9,
344
+ minProviders: this.costConfig.cascadingConfidence.minProviders ?? 2,
345
+ } : undefined,
346
+ // Recursive extensions
347
+ discussionContext: contextTracker.getContext(),
348
+ allowSubDiscussions: this.recursiveConfig.enabled && this.recursiveConfig.allowSubDiscussions,
349
+ spawnSubDiscussion: this.recursiveConfig.enabled ? spawnSubDiscussion : undefined,
350
+ };
351
+
352
+ // Execute pattern
353
+ const patternResult = await patternExecutor.execute(patternContext) as RecursivePatternExecutionResult;
354
+
355
+ // Record calls from pattern execution
356
+ totalProviderCalls += patternResult.participatingProviders.length;
357
+ contextTracker.recordCalls(patternResult.participatingProviders.length);
358
+
359
+ if (!patternResult.success) {
360
+ return createFailedDiscussionResult(
361
+ config.pattern,
362
+ config.prompt,
363
+ DiscussionErrorCodes.ALL_PROVIDERS_FAILED,
364
+ patternResult.error || 'Pattern execution failed',
365
+ startedAt
366
+ ) as RecursiveDiscussionResult;
367
+ }
368
+
369
+ // Execute consensus mechanism
370
+ const consensusExecutor = getConsensusExecutor(config.consensus.method);
371
+
372
+ const consensusResult = await consensusExecutor.execute({
373
+ topic: config.prompt,
374
+ rounds: patternResult.rounds,
375
+ participatingProviders: patternResult.participatingProviders,
376
+ config: config.consensus,
377
+ agentWeightMultiplier: config.agentWeightMultiplier,
378
+ providerExecutor: this.providerExecutor,
379
+ abortSignal,
380
+ onProgress,
381
+ });
382
+
383
+ // Record synthesis call
384
+ totalProviderCalls += 1;
385
+ contextTracker.recordCalls(1);
386
+
387
+ if (!consensusResult.success) {
388
+ return createFailedDiscussionResult(
389
+ config.pattern,
390
+ config.prompt,
391
+ DiscussionErrorCodes.CONSENSUS_FAILED,
392
+ consensusResult.error || 'Consensus failed',
393
+ startedAt
394
+ ) as RecursiveDiscussionResult;
395
+ }
396
+
397
+ // Build final result
398
+ const result: RecursiveDiscussionResult = {
399
+ success: true,
400
+ pattern: config.pattern,
401
+ topic: config.prompt,
402
+ participatingProviders: patternResult.participatingProviders,
403
+ failedProviders: patternResult.failedProviders,
404
+ rounds: patternResult.rounds,
405
+ synthesis: consensusResult.synthesis,
406
+ consensus: consensusResult.consensus,
407
+ votingResults: consensusResult.votingResults,
408
+ totalDurationMs: patternResult.totalDurationMs + consensusResult.durationMs,
409
+ metadata: {
410
+ startedAt,
411
+ completedAt: new Date().toISOString(),
412
+ traceId: this.traceId,
413
+ // Include early exit info if triggered
414
+ ...(patternResult.earlyExit?.triggered ? {
415
+ earlyExit: patternResult.earlyExit,
416
+ } : {}),
417
+ },
418
+ // Recursive extensions
419
+ ...(subDiscussions.length > 0 ? { subDiscussions } : {}),
420
+ totalProviderCalls,
421
+ maxDepthReached,
422
+ context: contextTracker.getContext(),
423
+ };
424
+
425
+ return result;
426
+ }
427
+
428
+ /**
429
+ * Check provider availability
430
+ */
431
+ private async checkProviders(providers: string[]): Promise<string[]> {
432
+ if (!this.checkProviderHealth) {
433
+ return providers;
434
+ }
435
+
436
+ const available: string[] = [];
437
+
438
+ await Promise.all(
439
+ providers.map(async (providerId) => {
440
+ try {
441
+ const isAvailable = await this.providerExecutor.isAvailable(providerId);
442
+ if (isAvailable) {
443
+ available.push(providerId);
444
+ }
445
+ } catch {
446
+ // Provider check failed, don't include
447
+ }
448
+ })
449
+ );
450
+
451
+ return available;
452
+ }
453
+
454
+ /**
455
+ * Quick recursive synthesis discussion
456
+ *
457
+ * Creates a temporary child executor with recursion enabled to avoid
458
+ * mutating the parent config (which could cause race conditions).
459
+ */
460
+ async quickRecursiveSynthesis(
461
+ topic: string,
462
+ options?: {
463
+ providers?: string[];
464
+ maxDepth?: number;
465
+ abortSignal?: AbortSignal;
466
+ onProgress?: (event: DiscussionProgressEvent) => void;
467
+ }
468
+ ): Promise<RecursiveDiscussionResult> {
469
+ // Create a new executor with recursion enabled instead of mutating this instance
470
+ const recursiveExecutor = new RecursiveDiscussionExecutor({
471
+ providerExecutor: this.providerExecutor,
472
+ defaultTimeoutMs: this.defaultTimeoutMs,
473
+ checkProviderHealth: this.checkProviderHealth,
474
+ traceId: this.traceId,
475
+ recursive: {
476
+ ...this.recursiveConfig,
477
+ enabled: true,
478
+ maxDepth: options?.maxDepth ?? this.recursiveConfig.maxDepth,
479
+ },
480
+ timeout: this.timeoutConfig,
481
+ cost: this.costConfig,
482
+ parentContext: this.parentContext,
483
+ onSubDiscussionSpawn: this.onSubDiscussionSpawn,
484
+ onSubDiscussionComplete: this.onSubDiscussionComplete,
485
+ });
486
+
487
+ return recursiveExecutor.executeRequest(
488
+ {
489
+ topic,
490
+ pattern: 'synthesis',
491
+ providers: options?.providers,
492
+ rounds: 2,
493
+ },
494
+ options
495
+ );
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Create a recursive discussion executor with default options
501
+ */
502
+ export function createRecursiveDiscussionExecutor(
503
+ providerExecutor: DiscussionProviderExecutor,
504
+ options?: Partial<Omit<RecursiveDiscussionExecutorOptions, 'providerExecutor'>>
505
+ ): RecursiveDiscussionExecutor {
506
+ return new RecursiveDiscussionExecutor({
507
+ providerExecutor,
508
+ ...options,
509
+ });
510
+ }