@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
@@ -238,43 +238,46 @@ export function createProviderBridge(
238
238
 
239
239
  async getAvailableProviders(): Promise<string[]> {
240
240
  const allProviders = registry.getAll();
241
- const available: string[] = [];
241
+ const now = Date.now();
242
+
243
+ // Separate cached vs uncached providers
244
+ const cached: string[] = [];
245
+ const toCheck: LLMProviderLike[] = [];
242
246
 
243
- // Check each provider's availability
244
247
  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;
248
+ const entry = healthCache.get(provider.providerId);
249
+ if (entry && now - entry.timestamp < healthCheckCacheMs) {
250
+ if (entry.healthy) cached.push(provider.providerId);
251
+ } else if (performHealthChecks) {
252
+ toCheck.push(provider);
253
+ } else {
254
+ cached.push(provider.providerId); // No health check, assume available
252
255
  }
256
+ }
257
+
258
+ // Check uncached providers in parallel
259
+ if (toCheck.length === 0) return cached;
253
260
 
254
- // Perform health check if enabled
255
- if (performHealthChecks) {
261
+ const results = await Promise.allSettled(
262
+ toCheck.map(async (provider) => {
256
263
  try {
257
264
  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
- }
265
+ healthCache.set(provider.providerId, { healthy: health.healthy, timestamp: Date.now() });
266
+ return health.healthy ? provider.providerId : null;
265
267
  } catch {
266
- healthCache.set(provider.providerId, {
267
- healthy: false,
268
- timestamp: Date.now(),
269
- });
268
+ healthCache.set(provider.providerId, { healthy: false, timestamp: Date.now() });
269
+ return null;
270
270
  }
271
- } else {
272
- // If health checks disabled, assume available
273
- available.push(provider.providerId);
274
- }
275
- }
271
+ })
272
+ );
273
+
274
+ // Collect successful health checks
275
+ const checked = results
276
+ .filter((r): r is PromiseFulfilledResult<string | null> => r.status === 'fulfilled')
277
+ .map((r) => r.value)
278
+ .filter((id): id is string => id !== null);
276
279
 
277
- return available;
280
+ return [...cached, ...checked];
278
281
  },
279
282
  };
280
283
  }
@@ -0,0 +1,500 @@
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
+ providerExecutor: this.providerExecutor,
378
+ abortSignal,
379
+ onProgress,
380
+ });
381
+
382
+ // Record synthesis call
383
+ totalProviderCalls += 1;
384
+ contextTracker.recordCalls(1);
385
+
386
+ if (!consensusResult.success) {
387
+ return createFailedDiscussionResult(
388
+ config.pattern,
389
+ config.prompt,
390
+ DiscussionErrorCodes.CONSENSUS_FAILED,
391
+ consensusResult.error || 'Consensus failed',
392
+ startedAt
393
+ ) as RecursiveDiscussionResult;
394
+ }
395
+
396
+ // Build final result
397
+ const result: RecursiveDiscussionResult = {
398
+ success: true,
399
+ pattern: config.pattern,
400
+ topic: config.prompt,
401
+ participatingProviders: patternResult.participatingProviders,
402
+ failedProviders: patternResult.failedProviders,
403
+ rounds: patternResult.rounds,
404
+ synthesis: consensusResult.synthesis,
405
+ consensus: consensusResult.consensus,
406
+ votingResults: consensusResult.votingResults,
407
+ totalDurationMs: patternResult.totalDurationMs + consensusResult.durationMs,
408
+ metadata: {
409
+ startedAt,
410
+ completedAt: new Date().toISOString(),
411
+ traceId: this.traceId,
412
+ // Include early exit info if triggered
413
+ ...(patternResult.earlyExit?.triggered ? {
414
+ earlyExit: patternResult.earlyExit,
415
+ } : {}),
416
+ },
417
+ // Recursive extensions
418
+ ...(subDiscussions.length > 0 ? { subDiscussions } : {}),
419
+ totalProviderCalls,
420
+ maxDepthReached,
421
+ context: contextTracker.getContext(),
422
+ };
423
+
424
+ return result;
425
+ }
426
+
427
+ /**
428
+ * Check provider availability
429
+ */
430
+ private async checkProviders(providers: string[]): Promise<string[]> {
431
+ if (!this.checkProviderHealth) {
432
+ return providers;
433
+ }
434
+
435
+ const available: string[] = [];
436
+
437
+ await Promise.all(
438
+ providers.map(async (providerId) => {
439
+ try {
440
+ const isAvailable = await this.providerExecutor.isAvailable(providerId);
441
+ if (isAvailable) {
442
+ available.push(providerId);
443
+ }
444
+ } catch {
445
+ // Provider check failed, don't include
446
+ }
447
+ })
448
+ );
449
+
450
+ return available;
451
+ }
452
+
453
+ /**
454
+ * Quick recursive synthesis discussion
455
+ */
456
+ async quickRecursiveSynthesis(
457
+ topic: string,
458
+ options?: {
459
+ providers?: string[];
460
+ maxDepth?: number;
461
+ abortSignal?: AbortSignal;
462
+ onProgress?: (event: DiscussionProgressEvent) => void;
463
+ }
464
+ ): Promise<RecursiveDiscussionResult> {
465
+ // Temporarily enable recursion for this call
466
+ const originalEnabled = this.recursiveConfig.enabled;
467
+ this.recursiveConfig.enabled = true;
468
+
469
+ if (options?.maxDepth) {
470
+ this.recursiveConfig.maxDepth = options.maxDepth;
471
+ }
472
+
473
+ try {
474
+ return await this.executeRequest(
475
+ {
476
+ topic,
477
+ pattern: 'synthesis',
478
+ providers: options?.providers,
479
+ rounds: 2,
480
+ },
481
+ options
482
+ );
483
+ } finally {
484
+ this.recursiveConfig.enabled = originalEnabled;
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Create a recursive discussion executor with default options
491
+ */
492
+ export function createRecursiveDiscussionExecutor(
493
+ providerExecutor: DiscussionProviderExecutor,
494
+ options?: Partial<Omit<RecursiveDiscussionExecutorOptions, 'providerExecutor'>>
495
+ ): RecursiveDiscussionExecutor {
496
+ return new RecursiveDiscussionExecutor({
497
+ providerExecutor,
498
+ ...options,
499
+ });
500
+ }