@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
package/src/provider-bridge.ts
CHANGED
|
@@ -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
|
|
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
|
|
239
|
+
healthy,
|
|
223
240
|
timestamp: Date.now(),
|
|
224
241
|
});
|
|
225
|
-
return
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
278
|
+
const results = await Promise.allSettled(
|
|
279
|
+
toCheck.map(async (provider) => {
|
|
256
280
|
try {
|
|
257
281
|
const health = await provider.checkHealth();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
268
|
-
timestamp: Date.now(),
|
|
269
|
-
});
|
|
286
|
+
healthCache.set(provider.providerId, { healthy: false, timestamp: Date.now() });
|
|
287
|
+
return null;
|
|
270
288
|
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
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
|
+
}
|