mcp-rubber-duck 1.2.4 → 1.3.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 (48) hide show
  1. package/.eslintrc.json +1 -0
  2. package/.github/workflows/security.yml +4 -2
  3. package/.github/workflows/semantic-release.yml +4 -2
  4. package/CHANGELOG.md +20 -0
  5. package/README.md +116 -2
  6. package/audit-ci.json +3 -1
  7. package/dist/config/types.d.ts +78 -0
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/server.d.ts.map +1 -1
  10. package/dist/server.js +150 -0
  11. package/dist/server.js.map +1 -1
  12. package/dist/services/consensus.d.ts +28 -0
  13. package/dist/services/consensus.d.ts.map +1 -0
  14. package/dist/services/consensus.js +257 -0
  15. package/dist/services/consensus.js.map +1 -0
  16. package/dist/services/mcp-client-manager.d.ts.map +1 -1
  17. package/dist/services/mcp-client-manager.js +1 -3
  18. package/dist/services/mcp-client-manager.js.map +1 -1
  19. package/dist/tools/duck-debate.d.ts +16 -0
  20. package/dist/tools/duck-debate.d.ts.map +1 -0
  21. package/dist/tools/duck-debate.js +272 -0
  22. package/dist/tools/duck-debate.js.map +1 -0
  23. package/dist/tools/duck-iterate.d.ts +14 -0
  24. package/dist/tools/duck-iterate.d.ts.map +1 -0
  25. package/dist/tools/duck-iterate.js +195 -0
  26. package/dist/tools/duck-iterate.js.map +1 -0
  27. package/dist/tools/duck-judge.d.ts +15 -0
  28. package/dist/tools/duck-judge.d.ts.map +1 -0
  29. package/dist/tools/duck-judge.js +208 -0
  30. package/dist/tools/duck-judge.js.map +1 -0
  31. package/dist/tools/duck-vote.d.ts +14 -0
  32. package/dist/tools/duck-vote.d.ts.map +1 -0
  33. package/dist/tools/duck-vote.js +46 -0
  34. package/dist/tools/duck-vote.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/config/types.ts +92 -0
  37. package/src/server.ts +154 -0
  38. package/src/services/consensus.ts +324 -0
  39. package/src/services/mcp-client-manager.ts +1 -3
  40. package/src/tools/duck-debate.ts +383 -0
  41. package/src/tools/duck-iterate.ts +253 -0
  42. package/src/tools/duck-judge.ts +301 -0
  43. package/src/tools/duck-vote.ts +87 -0
  44. package/tests/consensus.test.ts +282 -0
  45. package/tests/duck-debate.test.ts +286 -0
  46. package/tests/duck-iterate.test.ts +249 -0
  47. package/tests/duck-judge.test.ts +296 -0
  48. package/tests/duck-vote.test.ts +250 -0
@@ -0,0 +1,383 @@
1
+ import { ProviderManager } from '../providers/manager.js';
2
+ import {
3
+ DebateFormat,
4
+ DebatePosition,
5
+ DebateParticipant,
6
+ DebateArgument,
7
+ DebateResult,
8
+ } from '../config/types.js';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ export interface DuckDebateArgs {
12
+ prompt: string;
13
+ rounds?: number;
14
+ providers?: string[];
15
+ format: DebateFormat;
16
+ synthesizer?: string;
17
+ }
18
+
19
+ const DEFAULT_ROUNDS = 3;
20
+
21
+ export async function duckDebateTool(
22
+ providerManager: ProviderManager,
23
+ args: Record<string, unknown>
24
+ ) {
25
+ const {
26
+ prompt,
27
+ rounds = DEFAULT_ROUNDS,
28
+ providers,
29
+ format,
30
+ synthesizer,
31
+ } = args as unknown as DuckDebateArgs;
32
+
33
+ // Validate inputs
34
+ if (!prompt || typeof prompt !== 'string') {
35
+ throw new Error('Prompt/topic is required');
36
+ }
37
+
38
+ if (!format || !['oxford', 'socratic', 'adversarial'].includes(format)) {
39
+ throw new Error('Format must be "oxford", "socratic", or "adversarial"');
40
+ }
41
+
42
+ if (rounds < 1 || rounds > 10) {
43
+ throw new Error('Rounds must be between 1 and 10');
44
+ }
45
+
46
+ // Get providers
47
+ const allProviders = providerManager.getProviderNames();
48
+
49
+ // If providers explicitly specified but less than 2, error
50
+ if (providers && providers.length < 2) {
51
+ throw new Error('At least 2 providers are required for a debate');
52
+ }
53
+
54
+ const debateProviders = providers && providers.length >= 2
55
+ ? providers
56
+ : allProviders;
57
+
58
+ if (debateProviders.length < 2) {
59
+ throw new Error('At least 2 providers are required for a debate');
60
+ }
61
+
62
+ // Validate providers exist
63
+ for (const p of debateProviders) {
64
+ if (!allProviders.includes(p)) {
65
+ throw new Error(`Provider "${p}" not found`);
66
+ }
67
+ }
68
+
69
+ logger.info(`Starting ${format} debate with ${debateProviders.length} participants for ${rounds} rounds`);
70
+
71
+ // Assign positions based on format
72
+ const participants = assignPositions(debateProviders, format, providerManager);
73
+
74
+ // Run debate rounds
75
+ const debateRounds: DebateArgument[][] = [];
76
+
77
+ for (let roundNum = 1; roundNum <= rounds; roundNum++) {
78
+ logger.info(`Debate round ${roundNum}/${rounds}`);
79
+ const roundArguments: DebateArgument[] = [];
80
+
81
+ // Each participant argues in this round
82
+ for (const participant of participants) {
83
+ const argumentPrompt = buildArgumentPrompt(
84
+ prompt,
85
+ format,
86
+ participant,
87
+ roundNum,
88
+ debateRounds,
89
+ participants
90
+ );
91
+
92
+ const response = await providerManager.askDuck(participant.provider, argumentPrompt);
93
+
94
+ roundArguments.push({
95
+ round: roundNum,
96
+ provider: participant.provider,
97
+ nickname: participant.nickname,
98
+ position: participant.position,
99
+ content: response.content,
100
+ timestamp: new Date(),
101
+ });
102
+ }
103
+
104
+ debateRounds.push(roundArguments);
105
+ }
106
+
107
+ // Generate synthesis
108
+ const synthesizerProvider = synthesizer || debateProviders[0];
109
+ const synthesisPrompt = buildSynthesisPrompt(prompt, format, debateRounds, participants);
110
+ const synthesisResponse = await providerManager.askDuck(synthesizerProvider, synthesisPrompt);
111
+
112
+ const result: DebateResult = {
113
+ topic: prompt,
114
+ format,
115
+ participants,
116
+ rounds: debateRounds,
117
+ synthesis: synthesisResponse.content,
118
+ synthesizer: synthesizerProvider,
119
+ totalRounds: rounds,
120
+ };
121
+
122
+ // Format output
123
+ const formattedOutput = formatDebateResult(result);
124
+
125
+ logger.info(`Debate completed: ${rounds} rounds, synthesized by ${synthesizerProvider}`);
126
+
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text',
131
+ text: formattedOutput,
132
+ },
133
+ ],
134
+ };
135
+ }
136
+
137
+ function assignPositions(
138
+ providers: string[],
139
+ format: DebateFormat,
140
+ providerManager: ProviderManager
141
+ ): DebateParticipant[] {
142
+ const participants: DebateParticipant[] = [];
143
+
144
+ for (let i = 0; i < providers.length; i++) {
145
+ const provider = providers[i];
146
+ const providerInfo = providerManager.getProvider(provider);
147
+
148
+ let position: DebatePosition;
149
+
150
+ if (format === 'oxford') {
151
+ // Oxford: alternating pro/con
152
+ position = i % 2 === 0 ? 'pro' : 'con';
153
+ } else if (format === 'adversarial') {
154
+ // Adversarial: first is defender, rest are challengers (con)
155
+ position = i === 0 ? 'pro' : 'con';
156
+ } else {
157
+ // Socratic: all neutral, questioning each other
158
+ position = 'neutral';
159
+ }
160
+
161
+ participants.push({
162
+ provider,
163
+ nickname: providerInfo.nickname,
164
+ position,
165
+ });
166
+ }
167
+
168
+ return participants;
169
+ }
170
+
171
+ function buildArgumentPrompt(
172
+ topic: string,
173
+ format: DebateFormat,
174
+ participant: DebateParticipant,
175
+ round: number,
176
+ previousRounds: DebateArgument[][],
177
+ allParticipants: DebateParticipant[]
178
+ ): string {
179
+ const previousContext = buildPreviousContext(previousRounds);
180
+
181
+ if (format === 'oxford') {
182
+ return buildOxfordPrompt(topic, participant, round, previousContext);
183
+ } else if (format === 'socratic') {
184
+ return buildSocraticPrompt(topic, participant, round, previousContext, allParticipants);
185
+ } else {
186
+ return buildAdversarialPrompt(topic, participant, round, previousContext);
187
+ }
188
+ }
189
+
190
+ function buildPreviousContext(previousRounds: DebateArgument[][]): string {
191
+ if (previousRounds.length === 0) {
192
+ return '';
193
+ }
194
+
195
+ let context = '\n\nPREVIOUS ARGUMENTS:\n';
196
+ for (const round of previousRounds) {
197
+ for (const arg of round) {
198
+ const posLabel = arg.position === 'pro' ? '[PRO]' :
199
+ arg.position === 'con' ? '[CON]' : '[NEUTRAL]';
200
+ context += `\n--- Round ${arg.round} - ${arg.nickname} ${posLabel} ---\n`;
201
+ context += `${arg.content}\n`;
202
+ }
203
+ }
204
+ return context;
205
+ }
206
+
207
+ function buildOxfordPrompt(
208
+ topic: string,
209
+ participant: DebateParticipant,
210
+ round: number,
211
+ previousContext: string
212
+ ): string {
213
+ const position = participant.position === 'pro' ? 'IN FAVOR OF' : 'AGAINST';
214
+ const positionLabel = participant.position === 'pro' ? 'PRO' : 'CON';
215
+
216
+ return `You are participating in an Oxford-style debate.
217
+
218
+ TOPIC: "${topic}"
219
+
220
+ YOUR POSITION: ${position} (${positionLabel})
221
+ ROUND: ${round}
222
+
223
+ ${previousContext}
224
+
225
+ INSTRUCTIONS:
226
+ 1. Present clear, logical arguments ${position.toLowerCase()} the topic
227
+ 2. ${round > 1 ? 'Address and rebut opposing arguments from previous rounds' : 'Establish your core thesis and supporting points'}
228
+ 3. Use evidence, examples, and reasoning
229
+ 4. Be persuasive but intellectually honest
230
+ 5. Keep your argument focused and structured
231
+
232
+ Present your argument for Round ${round}:`;
233
+ }
234
+
235
+ function buildSocraticPrompt(
236
+ topic: string,
237
+ participant: DebateParticipant,
238
+ round: number,
239
+ previousContext: string,
240
+ allParticipants: DebateParticipant[]
241
+ ): string {
242
+ const otherParticipants = allParticipants
243
+ .filter(p => p.provider !== participant.provider)
244
+ .map(p => p.nickname)
245
+ .join(', ');
246
+
247
+ return `You are participating in a Socratic dialogue.
248
+
249
+ TOPIC: "${topic}"
250
+
251
+ YOUR ROLE: Philosophical inquirer exploring the topic through questions and reasoning
252
+ OTHER PARTICIPANTS: ${otherParticipants}
253
+ ROUND: ${round}
254
+
255
+ ${previousContext}
256
+
257
+ INSTRUCTIONS:
258
+ 1. ${round === 1 ? 'Begin by questioning assumptions about the topic' : 'Build on previous responses with deeper questions'}
259
+ 2. Use the Socratic method: ask probing questions that reveal underlying assumptions
260
+ 3. Offer your own perspective while remaining open to other views
261
+ 4. Seek to understand the truth through dialogue, not to "win"
262
+ 5. Challenge ideas respectfully and constructively
263
+
264
+ Present your contribution to Round ${round}:`;
265
+ }
266
+
267
+ function buildAdversarialPrompt(
268
+ topic: string,
269
+ participant: DebateParticipant,
270
+ round: number,
271
+ previousContext: string
272
+ ): string {
273
+ const role = participant.position === 'pro' ? 'DEFENDER' : 'CHALLENGER';
274
+ const instruction = participant.position === 'pro'
275
+ ? 'Defend the proposition and address all critiques raised'
276
+ : 'Attack weaknesses in the arguments, find flaws, and present counter-examples';
277
+
278
+ return `You are participating in an adversarial debate.
279
+
280
+ TOPIC: "${topic}"
281
+
282
+ YOUR ROLE: ${role}
283
+ ROUND: ${round}
284
+
285
+ ${previousContext}
286
+
287
+ INSTRUCTIONS:
288
+ 1. ${instruction}
289
+ 2. Be rigorous and thorough in your analysis
290
+ 3. ${participant.position === 'con' ? 'Identify logical fallacies, weak evidence, or missing considerations' : 'Strengthen your position against attacks'}
291
+ 4. Use concrete examples and evidence
292
+ 5. Be intellectually aggressive but fair
293
+
294
+ Present your ${role.toLowerCase()} argument for Round ${round}:`;
295
+ }
296
+
297
+ function buildSynthesisPrompt(
298
+ topic: string,
299
+ format: DebateFormat,
300
+ rounds: DebateArgument[][],
301
+ participants: DebateParticipant[]
302
+ ): string {
303
+ let transcript = '';
304
+ for (const round of rounds) {
305
+ for (const arg of round) {
306
+ const posLabel = arg.position === 'pro' ? '[PRO]' :
307
+ arg.position === 'con' ? '[CON]' : '[NEUTRAL]';
308
+ transcript += `\n--- Round ${arg.round} - ${arg.nickname} ${posLabel} ---\n`;
309
+ transcript += `${arg.content}\n`;
310
+ }
311
+ }
312
+
313
+ const participantList = participants
314
+ .map(p => `${p.nickname} (${p.position})`)
315
+ .join(', ');
316
+
317
+ return `You are the moderator synthesizing a ${format} debate.
318
+
319
+ TOPIC: "${topic}"
320
+ PARTICIPANTS: ${participantList}
321
+
322
+ DEBATE TRANSCRIPT:
323
+ ${transcript}
324
+
325
+ YOUR TASK:
326
+ 1. Summarize the key arguments from each side
327
+ 2. Identify the strongest points made
328
+ 3. Note where participants agreed or found common ground
329
+ 4. Highlight unresolved tensions or questions
330
+ 5. Provide a balanced conclusion (who had stronger arguments, or if it was a draw)
331
+ 6. Suggest what additional considerations might be valuable
332
+
333
+ Provide your synthesis:`;
334
+ }
335
+
336
+ function formatDebateResult(result: DebateResult): string {
337
+ const formatEmoji = result.format === 'oxford' ? 'šŸŽ“' :
338
+ result.format === 'socratic' ? 'šŸ›ļø' : 'āš”ļø';
339
+
340
+ let output = `${formatEmoji} **${result.format.charAt(0).toUpperCase() + result.format.slice(1)} Debate**\n`;
341
+ output += `═══════════════════════════════════════\n\n`;
342
+ output += `**Topic:** "${result.topic}"\n`;
343
+ output += `**Format:** ${result.format}\n`;
344
+ output += `**Rounds:** ${result.totalRounds}\n\n`;
345
+
346
+ // Participants
347
+ output += `**Participants:**\n`;
348
+ for (const p of result.participants) {
349
+ const posEmoji = p.position === 'pro' ? 'āœ…' : p.position === 'con' ? 'āŒ' : 'šŸ”';
350
+ output += ` ${posEmoji} ${p.nickname} (${p.position})\n`;
351
+ }
352
+ output += `\n`;
353
+
354
+ // Debate rounds
355
+ output += `**Debate Transcript:**\n`;
356
+ output += `─────────────────────────────────────\n`;
357
+
358
+ for (let i = 0; i < result.rounds.length; i++) {
359
+ output += `\nšŸ“¢ **ROUND ${i + 1}**\n`;
360
+
361
+ for (const arg of result.rounds[i]) {
362
+ const posEmoji = arg.position === 'pro' ? 'āœ…' : arg.position === 'con' ? 'āŒ' : 'šŸ”';
363
+ output += `\n${posEmoji} **${arg.nickname}** [${arg.position.toUpperCase()}]:\n`;
364
+
365
+ // Truncate long arguments
366
+ const displayContent = arg.content.length > 800
367
+ ? arg.content.substring(0, 800) + '...[truncated]'
368
+ : arg.content;
369
+ output += `${displayContent}\n`;
370
+ }
371
+ }
372
+
373
+ // Synthesis
374
+ output += `\n═══════════════════════════════════════\n`;
375
+ output += `šŸŽÆ **Synthesis** (by ${result.synthesizer})\n`;
376
+ output += `─────────────────────────────────────\n`;
377
+ output += `${result.synthesis}\n`;
378
+
379
+ output += `\n═══════════════════════════════════════\n`;
380
+ output += `šŸ“Š ${result.totalRounds} rounds completed with ${result.participants.length} participants\n`;
381
+
382
+ return output;
383
+ }
@@ -0,0 +1,253 @@
1
+ import { ProviderManager } from '../providers/manager.js';
2
+ import { IterationRound, IterationResult } from '../config/types.js';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ export interface DuckIterateArgs {
6
+ prompt: string;
7
+ iterations?: number;
8
+ providers: [string, string];
9
+ mode: 'refine' | 'critique-improve';
10
+ }
11
+
12
+ const DEFAULT_ITERATIONS = 3;
13
+ const CONVERGENCE_THRESHOLD = 0.8; // 80% similarity indicates convergence
14
+
15
+ export async function duckIterateTool(
16
+ providerManager: ProviderManager,
17
+ args: Record<string, unknown>
18
+ ) {
19
+ const {
20
+ prompt,
21
+ iterations = DEFAULT_ITERATIONS,
22
+ providers,
23
+ mode,
24
+ } = args as unknown as DuckIterateArgs;
25
+
26
+ // Validate inputs
27
+ if (!prompt || typeof prompt !== 'string') {
28
+ throw new Error('Prompt is required');
29
+ }
30
+
31
+ if (!providers || !Array.isArray(providers) || providers.length !== 2) {
32
+ throw new Error('Exactly 2 providers are required for iteration');
33
+ }
34
+
35
+ if (!mode || !['refine', 'critique-improve'].includes(mode)) {
36
+ throw new Error('Mode must be either "refine" or "critique-improve"');
37
+ }
38
+
39
+ if (iterations < 1 || iterations > 10) {
40
+ throw new Error('Iterations must be between 1 and 10');
41
+ }
42
+
43
+ // Validate providers exist
44
+ const providerNames = providerManager.getProviderNames();
45
+ for (const p of providers) {
46
+ if (!providerNames.includes(p)) {
47
+ throw new Error(`Provider "${p}" not found`);
48
+ }
49
+ }
50
+
51
+ logger.info(`Starting ${mode} iteration with ${providers.join(' <-> ')} for ${iterations} rounds`);
52
+
53
+ const rounds: IterationRound[] = [];
54
+ let lastResponse = '';
55
+ let converged = false;
56
+
57
+ // Round 1: Initial generation by provider A
58
+ const initialResponse = await providerManager.askDuck(providers[0], prompt);
59
+ const providerAInfo = providerManager.getProvider(providers[0]);
60
+
61
+ rounds.push({
62
+ round: 1,
63
+ provider: providers[0],
64
+ nickname: providerAInfo.nickname,
65
+ role: 'generator',
66
+ content: initialResponse.content,
67
+ timestamp: new Date(),
68
+ });
69
+
70
+ lastResponse = initialResponse.content;
71
+ logger.info(`Round 1: ${providers[0]} generated initial response`);
72
+
73
+ // Subsequent rounds: Alternate between providers
74
+ for (let i = 2; i <= iterations; i++) {
75
+ const isProviderA = i % 2 === 1;
76
+ const currentProvider = isProviderA ? providers[0] : providers[1];
77
+ const providerInfo = providerManager.getProvider(currentProvider);
78
+
79
+ const iterationPrompt = buildIterationPrompt(prompt, lastResponse, mode, i, rounds);
80
+
81
+ const response = await providerManager.askDuck(currentProvider, iterationPrompt);
82
+
83
+ // Check for convergence
84
+ if (checkConvergence(lastResponse, response.content)) {
85
+ converged = true;
86
+ logger.info(`Convergence detected at round ${i}`);
87
+ }
88
+
89
+ const role = mode === 'refine' ? 'refiner' : (i % 2 === 0 ? 'critic' : 'refiner');
90
+
91
+ rounds.push({
92
+ round: i,
93
+ provider: currentProvider,
94
+ nickname: providerInfo.nickname,
95
+ role,
96
+ content: response.content,
97
+ timestamp: new Date(),
98
+ });
99
+
100
+ lastResponse = response.content;
101
+ logger.info(`Round ${i}: ${currentProvider} ${role === 'critic' ? 'critiqued' : 'refined'}`);
102
+
103
+ if (converged) {
104
+ break;
105
+ }
106
+ }
107
+
108
+ const result: IterationResult = {
109
+ prompt,
110
+ mode,
111
+ providers,
112
+ rounds,
113
+ finalResponse: lastResponse,
114
+ totalIterations: rounds.length,
115
+ converged,
116
+ };
117
+
118
+ // Format output
119
+ const formattedOutput = formatIterationResult(result);
120
+
121
+ logger.info(`Iteration completed: ${rounds.length} rounds, converged: ${converged}`);
122
+
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: formattedOutput,
128
+ },
129
+ ],
130
+ };
131
+ }
132
+
133
+ function buildIterationPrompt(
134
+ originalPrompt: string,
135
+ previousResponse: string,
136
+ mode: 'refine' | 'critique-improve',
137
+ round: number,
138
+ previousRounds: IterationRound[]
139
+ ): string {
140
+ if (mode === 'refine') {
141
+ return `You are refining a response through iterative improvement.
142
+
143
+ ORIGINAL TASK:
144
+ ${originalPrompt}
145
+
146
+ PREVIOUS RESPONSE (Round ${round - 1}):
147
+ ${previousResponse}
148
+
149
+ YOUR TASK:
150
+ Improve upon the previous response. Make it:
151
+ - More accurate
152
+ - More complete
153
+ - Clearer and better structured
154
+ - More practical and actionable
155
+
156
+ Provide your improved version directly. Do not explain what you changed - just give the improved response.`;
157
+ } else {
158
+ // critique-improve mode
159
+ const isEvenRound = round % 2 === 0;
160
+
161
+ if (isEvenRound) {
162
+ // Critic round
163
+ return `You are a critical reviewer evaluating a response.
164
+
165
+ ORIGINAL TASK:
166
+ ${originalPrompt}
167
+
168
+ RESPONSE TO CRITIQUE:
169
+ ${previousResponse}
170
+
171
+ YOUR TASK:
172
+ Provide a thorough critique of this response:
173
+ 1. Identify specific weaknesses, errors, or gaps
174
+ 2. Point out unclear or confusing parts
175
+ 3. Suggest concrete improvements
176
+ 4. Note any missing considerations
177
+
178
+ Be constructive but thorough. Format as a bulleted critique.`;
179
+ } else {
180
+ // Improvement round based on critique
181
+ const lastCritique = previousRounds[previousRounds.length - 1]?.content || '';
182
+ const lastGoodResponse = previousRounds[previousRounds.length - 2]?.content || previousResponse;
183
+
184
+ return `You are improving a response based on critical feedback.
185
+
186
+ ORIGINAL TASK:
187
+ ${originalPrompt}
188
+
189
+ PREVIOUS RESPONSE:
190
+ ${lastGoodResponse}
191
+
192
+ CRITIQUE RECEIVED:
193
+ ${lastCritique}
194
+
195
+ YOUR TASK:
196
+ Create an improved response that addresses the critique points while maintaining the strengths of the original. Provide only the improved response, not meta-commentary.`;
197
+ }
198
+ }
199
+ }
200
+
201
+ function checkConvergence(previous: string, current: string): boolean {
202
+ // Simple similarity check based on length and common words
203
+ const prevWords = new Set(previous.toLowerCase().split(/\s+/));
204
+ const currWords = new Set(current.toLowerCase().split(/\s+/));
205
+
206
+ const intersection = new Set([...prevWords].filter(x => currWords.has(x)));
207
+ const union = new Set([...prevWords, ...currWords]);
208
+
209
+ const similarity = intersection.size / union.size;
210
+
211
+ // Also check if lengths are similar
212
+ const lengthRatio = Math.min(previous.length, current.length) / Math.max(previous.length, current.length);
213
+
214
+ return similarity > CONVERGENCE_THRESHOLD && lengthRatio > 0.8;
215
+ }
216
+
217
+ function formatIterationResult(result: IterationResult): string {
218
+ let output = `šŸ”„ **Iterative Refinement Results**\n`;
219
+ output += `═══════════════════════════════════════\n\n`;
220
+ output += `**Mode:** ${result.mode}\n`;
221
+ output += `**Providers:** ${result.providers.join(' ↔ ')}\n`;
222
+ output += `**Iterations:** ${result.totalIterations}`;
223
+ if (result.converged) {
224
+ output += ` (converged early āœ“)`;
225
+ }
226
+ output += `\n\n`;
227
+
228
+ // Show each round
229
+ output += `**Iteration History:**\n`;
230
+ output += `─────────────────────────────────────\n`;
231
+
232
+ for (const round of result.rounds) {
233
+ const roleEmoji = round.role === 'generator' ? 'šŸŽÆ' :
234
+ round.role === 'critic' ? 'šŸ”' : '✨';
235
+ output += `\n${roleEmoji} **Round ${round.round}: ${round.nickname}** (${round.role})\n`;
236
+
237
+ // Truncate long content for display
238
+ const displayContent = round.content.length > 500
239
+ ? round.content.substring(0, 500) + '...[truncated]'
240
+ : round.content;
241
+ output += `${displayContent}\n`;
242
+ }
243
+
244
+ // Final response
245
+ output += `\n═══════════════════════════════════════\n`;
246
+ output += `šŸ **Final Response:**\n`;
247
+ output += `─────────────────────────────────────\n`;
248
+ output += `${result.finalResponse}\n`;
249
+ output += `\n═══════════════════════════════════════\n`;
250
+ output += `šŸ“Š ${result.totalIterations} rounds completed\n`;
251
+
252
+ return output;
253
+ }