@defai.digital/discussion-domain 13.0.3

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 (82) hide show
  1. package/LICENSE +214 -0
  2. package/dist/consensus/index.d.ts +23 -0
  3. package/dist/consensus/index.d.ts.map +1 -0
  4. package/dist/consensus/index.js +45 -0
  5. package/dist/consensus/index.js.map +1 -0
  6. package/dist/consensus/moderator.d.ts +15 -0
  7. package/dist/consensus/moderator.d.ts.map +1 -0
  8. package/dist/consensus/moderator.js +201 -0
  9. package/dist/consensus/moderator.js.map +1 -0
  10. package/dist/consensus/synthesis.d.ts +15 -0
  11. package/dist/consensus/synthesis.d.ts.map +1 -0
  12. package/dist/consensus/synthesis.js +161 -0
  13. package/dist/consensus/synthesis.js.map +1 -0
  14. package/dist/consensus/voting.d.ts +17 -0
  15. package/dist/consensus/voting.d.ts.map +1 -0
  16. package/dist/consensus/voting.js +168 -0
  17. package/dist/consensus/voting.js.map +1 -0
  18. package/dist/executor.d.ts +73 -0
  19. package/dist/executor.d.ts.map +1 -0
  20. package/dist/executor.js +223 -0
  21. package/dist/executor.js.map +1 -0
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +21 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/patterns/critique.d.ts +20 -0
  27. package/dist/patterns/critique.d.ts.map +1 -0
  28. package/dist/patterns/critique.js +338 -0
  29. package/dist/patterns/critique.js.map +1 -0
  30. package/dist/patterns/debate.d.ts +20 -0
  31. package/dist/patterns/debate.d.ts.map +1 -0
  32. package/dist/patterns/debate.js +236 -0
  33. package/dist/patterns/debate.js.map +1 -0
  34. package/dist/patterns/index.d.ts +25 -0
  35. package/dist/patterns/index.d.ts.map +1 -0
  36. package/dist/patterns/index.js +49 -0
  37. package/dist/patterns/index.js.map +1 -0
  38. package/dist/patterns/round-robin.d.ts +13 -0
  39. package/dist/patterns/round-robin.d.ts.map +1 -0
  40. package/dist/patterns/round-robin.js +160 -0
  41. package/dist/patterns/round-robin.js.map +1 -0
  42. package/dist/patterns/synthesis.d.ts +20 -0
  43. package/dist/patterns/synthesis.d.ts.map +1 -0
  44. package/dist/patterns/synthesis.js +250 -0
  45. package/dist/patterns/synthesis.js.map +1 -0
  46. package/dist/patterns/voting.d.ts +19 -0
  47. package/dist/patterns/voting.d.ts.map +1 -0
  48. package/dist/patterns/voting.js +186 -0
  49. package/dist/patterns/voting.js.map +1 -0
  50. package/dist/prompts/index.d.ts +7 -0
  51. package/dist/prompts/index.d.ts.map +1 -0
  52. package/dist/prompts/index.js +21 -0
  53. package/dist/prompts/index.js.map +1 -0
  54. package/dist/prompts/templates.d.ts +55 -0
  55. package/dist/prompts/templates.d.ts.map +1 -0
  56. package/dist/prompts/templates.js +346 -0
  57. package/dist/prompts/templates.js.map +1 -0
  58. package/dist/provider-bridge.d.ts +115 -0
  59. package/dist/provider-bridge.d.ts.map +1 -0
  60. package/dist/provider-bridge.js +215 -0
  61. package/dist/provider-bridge.js.map +1 -0
  62. package/dist/types.d.ts +201 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +102 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +48 -0
  67. package/src/consensus/index.ts +52 -0
  68. package/src/consensus/moderator.ts +242 -0
  69. package/src/consensus/synthesis.ts +202 -0
  70. package/src/consensus/voting.ts +221 -0
  71. package/src/executor.ts +338 -0
  72. package/src/index.ts +69 -0
  73. package/src/patterns/critique.ts +465 -0
  74. package/src/patterns/debate.ts +340 -0
  75. package/src/patterns/index.ts +56 -0
  76. package/src/patterns/round-robin.ts +223 -0
  77. package/src/patterns/synthesis.ts +353 -0
  78. package/src/patterns/voting.ts +266 -0
  79. package/src/prompts/index.ts +41 -0
  80. package/src/prompts/templates.ts +381 -0
  81. package/src/provider-bridge.ts +346 -0
  82. package/src/types.ts +375 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Debate Pattern Executor
3
+ *
4
+ * Models argue opposing positions with an impartial judge.
5
+ * Requires role assignments: proponent, opponent, judge.
6
+ *
7
+ * Flow:
8
+ * 1. Round 1: Proponent presents arguments
9
+ * 2. Round 2: Opponent responds with counter-arguments
10
+ * 3. Round 3+: Rebuttals (alternating)
11
+ * 4. Final: Judge evaluates and declares winner
12
+ */
13
+
14
+ import type { DiscussionRound, DebateRole } from '@defai.digital/contracts';
15
+ import type {
16
+ PatternExecutor,
17
+ PatternExecutionContext,
18
+ PatternExecutionResult,
19
+ } from '../types.js';
20
+ import {
21
+ DEBATE_PROPONENT,
22
+ DEBATE_OPPONENT,
23
+ DEBATE_REBUTTAL,
24
+ interpolate,
25
+ getProviderSystemPrompt,
26
+ } from '../prompts/templates.js';
27
+
28
+ // Local type for discussion DiscussionProviderResponse (avoids conflict with provider/v1 DiscussionProviderResponse)
29
+ interface DiscussionProviderResponse {
30
+ provider: string;
31
+ content: string;
32
+ round: number;
33
+ role?: DebateRole | undefined;
34
+ confidence?: number | undefined;
35
+ vote?: string | undefined;
36
+ timestamp: string;
37
+ durationMs: number;
38
+ tokenCount?: number | undefined;
39
+ truncated?: boolean | undefined;
40
+ error?: string | undefined;
41
+ }
42
+
43
+ export class DebatePattern implements PatternExecutor {
44
+ readonly pattern = 'debate' as const;
45
+
46
+ async execute(context: PatternExecutionContext): Promise<PatternExecutionResult> {
47
+ const startTime = Date.now();
48
+ const { config, providerExecutor, availableProviders, abortSignal, onProgress } = context;
49
+
50
+ // Validate roles are assigned
51
+ if (!config.roles) {
52
+ return {
53
+ rounds: [],
54
+ participatingProviders: [],
55
+ failedProviders: [],
56
+ totalDurationMs: Date.now() - startTime,
57
+ success: false,
58
+ error: 'Debate pattern requires role assignments',
59
+ };
60
+ }
61
+
62
+ const rounds: DiscussionRound[] = [];
63
+ const participatingProviders = new Set<string>();
64
+ const failedProviders = new Set<string>();
65
+
66
+ // Get role assignments
67
+ const proponentId = this.findProviderByRole(config.roles, 'proponent');
68
+ const opponentId = this.findProviderByRole(config.roles, 'opponent');
69
+ const judgeId = this.findProviderByRole(config.roles, 'judge');
70
+
71
+ if (!proponentId || !opponentId) {
72
+ return {
73
+ rounds: [],
74
+ participatingProviders: [],
75
+ failedProviders: [],
76
+ totalDurationMs: Date.now() - startTime,
77
+ success: false,
78
+ error: 'Debate requires proponent and opponent roles',
79
+ };
80
+ }
81
+
82
+ // Check availability
83
+ const requiredProviders = [proponentId, opponentId, judgeId].filter(Boolean) as string[];
84
+ const unavailable = requiredProviders.filter(p => !availableProviders.includes(p));
85
+ if (unavailable.length > 0) {
86
+ return {
87
+ rounds: [],
88
+ participatingProviders: [],
89
+ failedProviders: unavailable,
90
+ totalDurationMs: Date.now() - startTime,
91
+ success: false,
92
+ error: `Required providers unavailable: ${unavailable.join(', ')}`,
93
+ };
94
+ }
95
+
96
+ let proponentArguments = '';
97
+ let opponentArguments = '';
98
+
99
+ // Round 1: Proponent's opening arguments
100
+ onProgress?.({
101
+ type: 'round_start',
102
+ round: 1,
103
+ message: 'Proponent presenting opening arguments',
104
+ timestamp: new Date().toISOString(),
105
+ });
106
+
107
+ const round1 = await this.executeDebateRound(
108
+ 1,
109
+ proponentId,
110
+ 'proponent',
111
+ config,
112
+ providerExecutor,
113
+ interpolate(DEBATE_PROPONENT, {
114
+ topic: config.prompt,
115
+ context: config.context || '',
116
+ }),
117
+ abortSignal,
118
+ onProgress
119
+ );
120
+
121
+ rounds.push(round1.round);
122
+ if (round1.success) {
123
+ participatingProviders.add(proponentId);
124
+ proponentArguments = round1.content;
125
+ } else {
126
+ failedProviders.add(proponentId);
127
+ return {
128
+ rounds,
129
+ participatingProviders: Array.from(participatingProviders),
130
+ failedProviders: Array.from(failedProviders),
131
+ totalDurationMs: Date.now() - startTime,
132
+ success: false,
133
+ error: 'Proponent failed to present arguments',
134
+ };
135
+ }
136
+
137
+ // Round 2: Opponent's counter-arguments
138
+ onProgress?.({
139
+ type: 'round_start',
140
+ round: 2,
141
+ message: 'Opponent presenting counter-arguments',
142
+ timestamp: new Date().toISOString(),
143
+ });
144
+
145
+ const round2 = await this.executeDebateRound(
146
+ 2,
147
+ opponentId,
148
+ 'opponent',
149
+ config,
150
+ providerExecutor,
151
+ interpolate(DEBATE_OPPONENT, {
152
+ topic: config.prompt,
153
+ context: config.context || '',
154
+ proponentArguments,
155
+ }),
156
+ abortSignal,
157
+ onProgress
158
+ );
159
+
160
+ rounds.push(round2.round);
161
+ if (round2.success) {
162
+ participatingProviders.add(opponentId);
163
+ opponentArguments = round2.content;
164
+ } else {
165
+ failedProviders.add(opponentId);
166
+ return {
167
+ rounds,
168
+ participatingProviders: Array.from(participatingProviders),
169
+ failedProviders: Array.from(failedProviders),
170
+ totalDurationMs: Date.now() - startTime,
171
+ success: false,
172
+ error: 'Opponent failed to present counter-arguments',
173
+ };
174
+ }
175
+
176
+ // Additional rounds for rebuttals
177
+ if (config.rounds > 2) {
178
+ // Proponent rebuttal
179
+ onProgress?.({
180
+ type: 'round_start',
181
+ round: 3,
182
+ message: 'Proponent rebuttal',
183
+ timestamp: new Date().toISOString(),
184
+ });
185
+
186
+ const round3 = await this.executeDebateRound(
187
+ 3,
188
+ proponentId,
189
+ 'proponent',
190
+ config,
191
+ providerExecutor,
192
+ interpolate(DEBATE_REBUTTAL, {
193
+ topic: config.prompt,
194
+ proponentArguments,
195
+ opponentArguments,
196
+ role: 'PROPONENT',
197
+ }),
198
+ abortSignal,
199
+ onProgress
200
+ );
201
+
202
+ rounds.push(round3.round);
203
+
204
+ // Opponent rebuttal
205
+ if (config.rounds > 3) {
206
+ onProgress?.({
207
+ type: 'round_start',
208
+ round: 4,
209
+ message: 'Opponent rebuttal',
210
+ timestamp: new Date().toISOString(),
211
+ });
212
+
213
+ const round4 = await this.executeDebateRound(
214
+ 4,
215
+ opponentId,
216
+ 'opponent',
217
+ config,
218
+ providerExecutor,
219
+ interpolate(DEBATE_REBUTTAL, {
220
+ topic: config.prompt,
221
+ proponentArguments,
222
+ opponentArguments,
223
+ role: 'OPPONENT',
224
+ }),
225
+ abortSignal,
226
+ onProgress
227
+ );
228
+
229
+ rounds.push(round4.round);
230
+ }
231
+ }
232
+
233
+ // Add judge if present (they participate but don't contribute rounds until consensus)
234
+ if (judgeId && availableProviders.includes(judgeId)) {
235
+ participatingProviders.add(judgeId);
236
+ }
237
+
238
+ return {
239
+ rounds,
240
+ participatingProviders: Array.from(participatingProviders),
241
+ failedProviders: Array.from(failedProviders),
242
+ totalDurationMs: Date.now() - startTime,
243
+ success: participatingProviders.size >= 2,
244
+ };
245
+ }
246
+
247
+ private findProviderByRole(
248
+ roles: Record<string, DebateRole>,
249
+ targetRole: DebateRole
250
+ ): string | undefined {
251
+ return Object.entries(roles).find(([_, role]) => role === targetRole)?.[0];
252
+ }
253
+
254
+ private async executeDebateRound(
255
+ roundNum: number,
256
+ providerId: string,
257
+ role: DebateRole,
258
+ config: PatternExecutionContext['config'],
259
+ providerExecutor: PatternExecutionContext['providerExecutor'],
260
+ prompt: string,
261
+ abortSignal?: AbortSignal,
262
+ onProgress?: PatternExecutionContext['onProgress']
263
+ ): Promise<{ round: DiscussionRound; success: boolean; content: string }> {
264
+ const roundStart = Date.now();
265
+
266
+ onProgress?.({
267
+ type: 'provider_start',
268
+ round: roundNum,
269
+ provider: providerId,
270
+ timestamp: new Date().toISOString(),
271
+ });
272
+
273
+ try {
274
+ const result = await providerExecutor.execute({
275
+ providerId,
276
+ prompt,
277
+ systemPrompt: getProviderSystemPrompt(providerId),
278
+ temperature: config.temperature,
279
+ timeoutMs: config.providerTimeout,
280
+ abortSignal,
281
+ });
282
+
283
+ const response: DiscussionProviderResponse = {
284
+ provider: providerId,
285
+ content: result.success ? result.content || '' : '',
286
+ round: roundNum,
287
+ role,
288
+ timestamp: new Date().toISOString(),
289
+ durationMs: result.durationMs,
290
+ tokenCount: result.tokenCount,
291
+ error: result.success ? undefined : result.error,
292
+ };
293
+
294
+ onProgress?.({
295
+ type: 'provider_complete',
296
+ round: roundNum,
297
+ provider: providerId,
298
+ message: result.success ? 'completed' : `failed: ${result.error}`,
299
+ timestamp: new Date().toISOString(),
300
+ });
301
+
302
+ onProgress?.({
303
+ type: 'round_complete',
304
+ round: roundNum,
305
+ timestamp: new Date().toISOString(),
306
+ });
307
+
308
+ return {
309
+ round: {
310
+ roundNumber: roundNum,
311
+ responses: [response],
312
+ durationMs: Date.now() - roundStart,
313
+ },
314
+ success: result.success,
315
+ content: result.content || '',
316
+ };
317
+
318
+ } catch (error) {
319
+ const errorMessage = error instanceof Error ? error.message : String(error);
320
+
321
+ return {
322
+ round: {
323
+ roundNumber: roundNum,
324
+ responses: [{
325
+ provider: providerId,
326
+ content: '',
327
+ round: roundNum,
328
+ role,
329
+ timestamp: new Date().toISOString(),
330
+ durationMs: Date.now() - roundStart,
331
+ error: errorMessage,
332
+ }],
333
+ durationMs: Date.now() - roundStart,
334
+ },
335
+ success: false,
336
+ content: '',
337
+ };
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Discussion Pattern Executors
3
+ *
4
+ * Exports all pattern implementations and a factory for getting the right executor.
5
+ */
6
+
7
+ import type { DiscussionPattern } from '@defai.digital/contracts';
8
+ import type { PatternExecutor } from '../types.js';
9
+ import { RoundRobinPattern } from './round-robin.js';
10
+ import { SynthesisPattern } from './synthesis.js';
11
+ import { DebatePattern } from './debate.js';
12
+ import { CritiquePattern } from './critique.js';
13
+ import { VotingPattern } from './voting.js';
14
+
15
+ // Export all pattern classes
16
+ export { RoundRobinPattern } from './round-robin.js';
17
+ export { SynthesisPattern } from './synthesis.js';
18
+ export { DebatePattern } from './debate.js';
19
+ export { CritiquePattern } from './critique.js';
20
+ export { VotingPattern } from './voting.js';
21
+
22
+ /**
23
+ * Map of pattern types to their executors
24
+ */
25
+ const patternExecutors: Record<DiscussionPattern, () => PatternExecutor> = {
26
+ 'round-robin': () => new RoundRobinPattern(),
27
+ synthesis: () => new SynthesisPattern(),
28
+ debate: () => new DebatePattern(),
29
+ critique: () => new CritiquePattern(),
30
+ voting: () => new VotingPattern(),
31
+ };
32
+
33
+ /**
34
+ * Get a pattern executor for the given pattern type
35
+ */
36
+ export function getPatternExecutor(pattern: DiscussionPattern): PatternExecutor {
37
+ const factory = patternExecutors[pattern];
38
+ if (!factory) {
39
+ throw new Error(`Unknown discussion pattern: ${pattern}`);
40
+ }
41
+ return factory();
42
+ }
43
+
44
+ /**
45
+ * Check if a pattern type is supported
46
+ */
47
+ export function isPatternSupported(pattern: string): pattern is DiscussionPattern {
48
+ return pattern in patternExecutors;
49
+ }
50
+
51
+ /**
52
+ * Get all supported pattern types
53
+ */
54
+ export function getSupportedPatterns(): DiscussionPattern[] {
55
+ return Object.keys(patternExecutors) as DiscussionPattern[];
56
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Round-Robin Pattern Executor
3
+ *
4
+ * Models respond sequentially, each building on previous responses.
5
+ * Good for: brainstorming, iterative refinement, building consensus.
6
+ */
7
+
8
+ import type { DiscussionRound, DebateRole } from '@defai.digital/contracts';
9
+ import type {
10
+ PatternExecutor,
11
+ PatternExecutionContext,
12
+ PatternExecutionResult,
13
+ } from '../types.js';
14
+
15
+ // Local type for discussion ProviderResponse (avoids conflict with provider/v1 ProviderResponse)
16
+ interface DiscussionProviderResponse {
17
+ provider: string;
18
+ content: string;
19
+ round: number;
20
+ role?: DebateRole | undefined;
21
+ confidence?: number | undefined;
22
+ vote?: string | undefined;
23
+ timestamp: string;
24
+ durationMs: number;
25
+ tokenCount?: number | undefined;
26
+ truncated?: boolean | undefined;
27
+ error?: string | undefined;
28
+ }
29
+ import {
30
+ ROUND_ROBIN_INITIAL,
31
+ ROUND_ROBIN_FOLLOWUP,
32
+ interpolate,
33
+ formatPreviousResponses,
34
+ getProviderSystemPrompt,
35
+ } from '../prompts/templates.js';
36
+
37
+ export class RoundRobinPattern implements PatternExecutor {
38
+ readonly pattern = 'round-robin' as const;
39
+
40
+ async execute(context: PatternExecutionContext): Promise<PatternExecutionResult> {
41
+ const startTime = Date.now();
42
+ const { config, providerExecutor, availableProviders, abortSignal, onProgress } = context;
43
+
44
+ const rounds: DiscussionRound[] = [];
45
+ const participatingProviders = new Set<string>();
46
+ const failedProviders = new Set<string>();
47
+ const allResponses: DiscussionProviderResponse[] = [];
48
+
49
+ // Filter to available providers
50
+ const providers = config.providers.filter(p => availableProviders.includes(p));
51
+
52
+ if (providers.length < config.minProviders) {
53
+ return {
54
+ rounds: [],
55
+ participatingProviders: [],
56
+ failedProviders: config.providers.filter(p => !availableProviders.includes(p)),
57
+ totalDurationMs: Date.now() - startTime,
58
+ success: false,
59
+ error: `Only ${providers.length} providers available, need ${config.minProviders}`,
60
+ };
61
+ }
62
+
63
+ // Execute rounds
64
+ for (let roundNum = 1; roundNum <= config.rounds; roundNum++) {
65
+ if (abortSignal?.aborted) {
66
+ break;
67
+ }
68
+
69
+ onProgress?.({
70
+ type: 'round_start',
71
+ round: roundNum,
72
+ message: `Starting round ${roundNum} of ${config.rounds}`,
73
+ timestamp: new Date().toISOString(),
74
+ });
75
+
76
+ const roundStart = Date.now();
77
+ const roundResponses: DiscussionProviderResponse[] = [];
78
+
79
+ // Each provider responds sequentially
80
+ for (const providerId of providers) {
81
+ if (abortSignal?.aborted) {
82
+ break;
83
+ }
84
+
85
+ onProgress?.({
86
+ type: 'provider_start',
87
+ round: roundNum,
88
+ provider: providerId,
89
+ message: `${providerId} is responding...`,
90
+ timestamp: new Date().toISOString(),
91
+ });
92
+
93
+ const responseStart = Date.now();
94
+
95
+ try {
96
+ // Build prompt based on round
97
+ const prompt = this.buildPrompt(
98
+ config.prompt,
99
+ config.context || '',
100
+ allResponses,
101
+ roundNum === 1
102
+ );
103
+
104
+ const result = await providerExecutor.execute({
105
+ providerId,
106
+ prompt,
107
+ systemPrompt: getProviderSystemPrompt(providerId),
108
+ temperature: config.temperature,
109
+ timeoutMs: config.providerTimeout,
110
+ abortSignal,
111
+ });
112
+
113
+ const response: DiscussionProviderResponse = {
114
+ provider: providerId,
115
+ content: result.success ? result.content || '' : '',
116
+ round: roundNum,
117
+ timestamp: new Date().toISOString(),
118
+ durationMs: result.durationMs,
119
+ tokenCount: result.tokenCount,
120
+ truncated: result.truncated,
121
+ error: result.success ? undefined : result.error,
122
+ };
123
+
124
+ roundResponses.push(response);
125
+ allResponses.push(response);
126
+
127
+ if (result.success) {
128
+ participatingProviders.add(providerId);
129
+ } else if (!config.continueOnProviderFailure) {
130
+ failedProviders.add(providerId);
131
+ break;
132
+ } else {
133
+ failedProviders.add(providerId);
134
+ }
135
+
136
+ onProgress?.({
137
+ type: 'provider_complete',
138
+ round: roundNum,
139
+ provider: providerId,
140
+ message: result.success ? `${providerId} completed` : `${providerId} failed: ${result.error}`,
141
+ timestamp: new Date().toISOString(),
142
+ });
143
+
144
+ } catch (error) {
145
+ const errorMessage = error instanceof Error ? error.message : String(error);
146
+ failedProviders.add(providerId);
147
+
148
+ roundResponses.push({
149
+ provider: providerId,
150
+ content: '',
151
+ round: roundNum,
152
+ timestamp: new Date().toISOString(),
153
+ durationMs: Date.now() - responseStart,
154
+ error: errorMessage,
155
+ });
156
+
157
+ if (!config.continueOnProviderFailure) {
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ rounds.push({
164
+ roundNumber: roundNum,
165
+ responses: roundResponses,
166
+ durationMs: Date.now() - roundStart,
167
+ });
168
+
169
+ onProgress?.({
170
+ type: 'round_complete',
171
+ round: roundNum,
172
+ message: `Round ${roundNum} complete`,
173
+ timestamp: new Date().toISOString(),
174
+ });
175
+
176
+ // Check if we have enough participating providers
177
+ if (participatingProviders.size < config.minProviders) {
178
+ return {
179
+ rounds,
180
+ participatingProviders: Array.from(participatingProviders),
181
+ failedProviders: Array.from(failedProviders),
182
+ totalDurationMs: Date.now() - startTime,
183
+ success: false,
184
+ error: `Only ${participatingProviders.size} providers succeeded, need ${config.minProviders}`,
185
+ };
186
+ }
187
+ }
188
+
189
+ return {
190
+ rounds,
191
+ participatingProviders: Array.from(participatingProviders),
192
+ failedProviders: Array.from(failedProviders),
193
+ totalDurationMs: Date.now() - startTime,
194
+ success: participatingProviders.size >= config.minProviders,
195
+ };
196
+ }
197
+
198
+ private buildPrompt(
199
+ topic: string,
200
+ context: string,
201
+ previousResponses: DiscussionProviderResponse[],
202
+ isInitial: boolean
203
+ ): string {
204
+ if (isInitial || previousResponses.length === 0) {
205
+ return interpolate(ROUND_ROBIN_INITIAL, {
206
+ topic,
207
+ context: context ? `\n## Additional Context\n${context}` : '',
208
+ });
209
+ }
210
+
211
+ const formattedResponses = formatPreviousResponses(
212
+ previousResponses.map(r => ({
213
+ provider: r.provider,
214
+ content: r.content,
215
+ }))
216
+ );
217
+
218
+ return interpolate(ROUND_ROBIN_FOLLOWUP, {
219
+ topic,
220
+ previousResponses: formattedResponses,
221
+ });
222
+ }
223
+ }