converse-mcp-server 2.29.2 → 3.0.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/.env.example +6 -3
- package/README.md +96 -94
- package/docs/API.md +703 -1562
- package/docs/ARCHITECTURE.md +13 -11
- package/docs/EXAMPLES.md +241 -667
- package/docs/PROVIDERS.md +104 -79
- package/package.json +1 -1
- package/src/async/asyncJobStore.js +2 -2
- package/src/async/jobRunner.js +6 -1
- package/src/async/providerStreamNormalizer.js +59 -1
- package/src/config.js +4 -3
- package/src/prompts/helpPrompt.js +43 -61
- package/src/providers/anthropic.js +0 -31
- package/src/providers/claude.js +0 -14
- package/src/providers/codex.js +15 -21
- package/src/providers/copilot.js +36 -202
- package/src/providers/deepseek.js +73 -53
- package/src/providers/gemini-cli.js +0 -3
- package/src/providers/google.js +10 -27
- package/src/providers/interface.js +0 -3
- package/src/providers/mistral.js +169 -63
- package/src/providers/openai-compatible.js +113 -28
- package/src/providers/openai.js +14 -66
- package/src/providers/openrouter-discovery.js +308 -0
- package/src/providers/openrouter.js +452 -282
- package/src/providers/xai.js +298 -280
- package/src/services/summarizationService.js +4 -14
- package/src/systemPrompts.js +19 -2
- package/src/tools/cancelJob.js +7 -1
- package/src/tools/chat.js +957 -995
- package/src/tools/checkStatus.js +1 -0
- package/src/tools/index.js +2 -6
- package/src/tools/modes/parallel.js +488 -0
- package/src/tools/modes/roundtable.js +495 -0
- package/src/tools/modes/streamShared.js +31 -0
- package/src/utils/contextProcessor.js +1 -38
- package/src/utils/conversationExporter.js +6 -26
- package/src/utils/formatStatus.js +46 -19
- package/src/utils/modelRouting.js +207 -4
- package/src/providers/openrouter-endpoints-client.js +0 -220
- package/src/tools/consensus.js +0 -1813
- package/src/tools/conversation.js +0 -1233
package/src/tools/consensus.js
DELETED
|
@@ -1,1813 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Consensus Tool
|
|
3
|
-
*
|
|
4
|
-
* Multi-provider parallel execution with response aggregation.
|
|
5
|
-
* Calls all available providers simultaneously and aggregates responses.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
createToolResponse,
|
|
10
|
-
createToolError,
|
|
11
|
-
formatFailureDetails,
|
|
12
|
-
} from './index.js';
|
|
13
|
-
import {
|
|
14
|
-
processUnifiedContext,
|
|
15
|
-
createFileContext,
|
|
16
|
-
} from '../utils/contextProcessor.js';
|
|
17
|
-
import {
|
|
18
|
-
generateContinuationId,
|
|
19
|
-
addMessageToHistory,
|
|
20
|
-
isValidContinuationId,
|
|
21
|
-
} from '../continuationStore.js';
|
|
22
|
-
import { isSafeIdSegment } from '../utils/idValidation.js';
|
|
23
|
-
import { debugLog, debugError } from '../utils/console.js';
|
|
24
|
-
import { createLogger } from '../utils/logger.js';
|
|
25
|
-
import { CONSENSUS_PROMPT } from '../systemPrompts.js';
|
|
26
|
-
import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
|
|
27
|
-
import { validateAllPaths } from '../utils/fileValidator.js';
|
|
28
|
-
import { SummarizationService } from '../services/summarizationService.js';
|
|
29
|
-
import { exportConversation } from '../utils/conversationExporter.js';
|
|
30
|
-
import {
|
|
31
|
-
providerSupportsImages,
|
|
32
|
-
getProviderUnavailableMessage,
|
|
33
|
-
} from '../utils/modelRouting.js';
|
|
34
|
-
|
|
35
|
-
const logger = createLogger('consensus');
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Consensus tool implementation
|
|
39
|
-
* @param {object} args - Tool arguments
|
|
40
|
-
* @param {object} dependencies - Injected dependencies (config, providers, continuationStore)
|
|
41
|
-
* @returns {object} MCP tool response
|
|
42
|
-
*/
|
|
43
|
-
export async function consensusTool(args, dependencies) {
|
|
44
|
-
try {
|
|
45
|
-
const {
|
|
46
|
-
config,
|
|
47
|
-
providers,
|
|
48
|
-
continuationStore,
|
|
49
|
-
contextProcessor,
|
|
50
|
-
jobRunner,
|
|
51
|
-
providerStreamNormalizer,
|
|
52
|
-
signal,
|
|
53
|
-
} = dependencies;
|
|
54
|
-
|
|
55
|
-
// Validate required arguments
|
|
56
|
-
if (!args.prompt || typeof args.prompt !== 'string') {
|
|
57
|
-
return createToolError('Prompt is required and must be a string');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
!args.models ||
|
|
62
|
-
!Array.isArray(args.models) ||
|
|
63
|
-
args.models.length === 0
|
|
64
|
-
) {
|
|
65
|
-
return createToolError(
|
|
66
|
-
'Models array is required and must contain at least one model',
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Extract and validate arguments
|
|
71
|
-
const {
|
|
72
|
-
prompt,
|
|
73
|
-
models,
|
|
74
|
-
files = [],
|
|
75
|
-
images = [],
|
|
76
|
-
continuation_id,
|
|
77
|
-
enable_cross_feedback = true,
|
|
78
|
-
cross_feedback_prompt,
|
|
79
|
-
temperature = 0.2,
|
|
80
|
-
reasoning_effort = 'medium',
|
|
81
|
-
use_websearch = false,
|
|
82
|
-
async = false,
|
|
83
|
-
export: shouldExport = false,
|
|
84
|
-
} = args;
|
|
85
|
-
|
|
86
|
-
// Handle async execution mode
|
|
87
|
-
if (async) {
|
|
88
|
-
// Validate async dependencies are available
|
|
89
|
-
if (!jobRunner || !providerStreamNormalizer) {
|
|
90
|
-
return createToolError(
|
|
91
|
-
'Async execution not available - missing async dependencies',
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Validate custom continuation ID for async safety (used as filesystem path segment)
|
|
96
|
-
if (continuation_id && !isSafeIdSegment(continuation_id)) {
|
|
97
|
-
return createToolError(
|
|
98
|
-
`Invalid continuation_id for async mode: "${continuation_id}". Async IDs must contain only letters, numbers, hyphens, and underscores (max 128 chars).`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Generate continuation ID for background execution result
|
|
103
|
-
const bgContinuationId = continuation_id || generateContinuationId();
|
|
104
|
-
|
|
105
|
-
// Determine if this is a custom ID (non-standard format AND not found in store)
|
|
106
|
-
let isCustomId = false;
|
|
107
|
-
if (continuation_id && !isValidContinuationId(continuation_id)) {
|
|
108
|
-
try {
|
|
109
|
-
const existing = await continuationStore.get(continuation_id);
|
|
110
|
-
isCustomId = !existing;
|
|
111
|
-
} catch {
|
|
112
|
-
isCustomId = true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Create models list for status display
|
|
117
|
-
const modelsList = args.models.join(', ');
|
|
118
|
-
|
|
119
|
-
// Generate title early for initial response
|
|
120
|
-
const summarizationService = new SummarizationService(providers, config);
|
|
121
|
-
let title = null;
|
|
122
|
-
try {
|
|
123
|
-
title = await summarizationService.generateTitle(prompt);
|
|
124
|
-
debugLog(
|
|
125
|
-
`Consensus: Generated title for initial response - "${title}"`,
|
|
126
|
-
);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
debugError(
|
|
129
|
-
'Consensus: Failed to generate title for initial response',
|
|
130
|
-
error,
|
|
131
|
-
);
|
|
132
|
-
title = prompt.substring(0, 50);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
// Submit background job
|
|
137
|
-
const jobId = await jobRunner.submit(
|
|
138
|
-
{
|
|
139
|
-
tool: 'consensus',
|
|
140
|
-
sessionId: bgContinuationId, // Use continuation_id as sessionId for consistency
|
|
141
|
-
options: {
|
|
142
|
-
...args,
|
|
143
|
-
jobId: bgContinuationId, // Use continuation ID as job ID
|
|
144
|
-
models_list: modelsList, // Add models list for status display
|
|
145
|
-
title, // Pass the generated title
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
async (context) => {
|
|
149
|
-
// Execute consensus in background using stream normalizer
|
|
150
|
-
return await executeConsensusWithStreaming(
|
|
151
|
-
args,
|
|
152
|
-
{
|
|
153
|
-
...dependencies,
|
|
154
|
-
continuationId: bgContinuationId,
|
|
155
|
-
isCustomId,
|
|
156
|
-
title, // Pass title to execution context
|
|
157
|
-
},
|
|
158
|
-
context,
|
|
159
|
-
);
|
|
160
|
-
},
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// Format initial response like check_status output
|
|
164
|
-
const startTime = new Date()
|
|
165
|
-
.toLocaleString('en-GB', {
|
|
166
|
-
day: '2-digit',
|
|
167
|
-
month: '2-digit',
|
|
168
|
-
year: 'numeric',
|
|
169
|
-
hour: '2-digit',
|
|
170
|
-
minute: '2-digit',
|
|
171
|
-
second: '2-digit',
|
|
172
|
-
hour12: false,
|
|
173
|
-
})
|
|
174
|
-
.replace(',', '');
|
|
175
|
-
|
|
176
|
-
const statusLine = `⏳ SUBMITTED | CONSENSUS | ${bgContinuationId} | 1/1 | Started: ${startTime} | "${title || 'Processing...'}" | ${modelsList}`;
|
|
177
|
-
|
|
178
|
-
// Return formatted response with status line and continuation_id
|
|
179
|
-
return createToolResponse({
|
|
180
|
-
content: `${statusLine}\ncontinuation_id: ${bgContinuationId}`,
|
|
181
|
-
continuation: {
|
|
182
|
-
id: bgContinuationId, // Use continuation_id as the primary ID
|
|
183
|
-
status: 'processing',
|
|
184
|
-
...(isCustomId && { custom_id: true }),
|
|
185
|
-
},
|
|
186
|
-
async_execution: true,
|
|
187
|
-
});
|
|
188
|
-
} catch (error) {
|
|
189
|
-
logger.error('Failed to submit async consensus job', { error });
|
|
190
|
-
return createToolError(`Async execution failed: ${error.message}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let conversationHistory = [];
|
|
195
|
-
let continuationId = continuation_id;
|
|
196
|
-
let isCustomId = false;
|
|
197
|
-
|
|
198
|
-
// Load existing conversation if continuation_id provided
|
|
199
|
-
if (continuationId) {
|
|
200
|
-
try {
|
|
201
|
-
const existingState =
|
|
202
|
-
await dependencies.continuationStore.get(continuationId);
|
|
203
|
-
if (existingState) {
|
|
204
|
-
conversationHistory = existingState.messages || [];
|
|
205
|
-
} else {
|
|
206
|
-
// Preserve user-provided ID and start fresh conversation
|
|
207
|
-
isCustomId = !isValidContinuationId(continuationId);
|
|
208
|
-
}
|
|
209
|
-
} catch (error) {
|
|
210
|
-
logger.error('Error loading conversation', { error });
|
|
211
|
-
// Preserve user-provided ID on error
|
|
212
|
-
isCustomId = !isValidContinuationId(continuationId);
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
// Generate new continuation ID for new conversation
|
|
216
|
-
continuationId = generateContinuationId();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Validate file paths before processing
|
|
220
|
-
if (files.length > 0 || images.length > 0) {
|
|
221
|
-
const validation = await validateAllPaths(
|
|
222
|
-
{
|
|
223
|
-
files,
|
|
224
|
-
images,
|
|
225
|
-
},
|
|
226
|
-
{ clientCwd: config.server?.client_cwd },
|
|
227
|
-
);
|
|
228
|
-
if (!validation.valid) {
|
|
229
|
-
logger.error('File validation failed', { errors: validation.errors });
|
|
230
|
-
return validation.errorResponse;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Process context (files and images)
|
|
235
|
-
let contextMessage = null;
|
|
236
|
-
if (files.length > 0 || images.length > 0) {
|
|
237
|
-
try {
|
|
238
|
-
const contextRequest = {
|
|
239
|
-
files: Array.isArray(files) ? files : [],
|
|
240
|
-
images: Array.isArray(images) ? images : [],
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const contextResult = await contextProcessor.processUnifiedContext(
|
|
244
|
-
contextRequest,
|
|
245
|
-
{
|
|
246
|
-
enforceSecurityCheck: false, // Allow files from any location
|
|
247
|
-
skipSecurityCheck: true, // Legacy flag for backward compatibility
|
|
248
|
-
clientCwd: config.server?.client_cwd, // Use auto-detected client working directory
|
|
249
|
-
},
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
// Create context message from files and images
|
|
253
|
-
const allProcessedFiles = [
|
|
254
|
-
...contextResult.files,
|
|
255
|
-
...contextResult.images,
|
|
256
|
-
];
|
|
257
|
-
if (allProcessedFiles.length > 0) {
|
|
258
|
-
contextMessage = createFileContext(allProcessedFiles, {
|
|
259
|
-
includeMetadata: true,
|
|
260
|
-
includeErrors: true,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
} catch (error) {
|
|
264
|
-
logger.error('Error processing context', { error });
|
|
265
|
-
// Continue without context if processing fails
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Build message array for providers
|
|
270
|
-
const messages = [];
|
|
271
|
-
|
|
272
|
-
// Add system prompt
|
|
273
|
-
messages.push({
|
|
274
|
-
role: 'system',
|
|
275
|
-
content: CONSENSUS_PROMPT,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Add conversation history
|
|
279
|
-
messages.push(...conversationHistory);
|
|
280
|
-
|
|
281
|
-
// Add user prompt with context
|
|
282
|
-
const userMessage = {
|
|
283
|
-
role: 'user',
|
|
284
|
-
content: prompt, // default to simple string content
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// If we have context (files/images), create complex content array
|
|
288
|
-
if (contextMessage && contextMessage.content) {
|
|
289
|
-
// Create complex content array
|
|
290
|
-
userMessage.content = [
|
|
291
|
-
...contextMessage.content, // Include all file/image parts
|
|
292
|
-
{ type: 'text', text: prompt }, // Add the user prompt as text
|
|
293
|
-
];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
messages.push(userMessage);
|
|
297
|
-
|
|
298
|
-
// Resolve model specifications to provider calls
|
|
299
|
-
const providerCalls = [];
|
|
300
|
-
const failedModels = [];
|
|
301
|
-
|
|
302
|
-
// Special handling for single "auto" model - expand to first 3 available providers
|
|
303
|
-
let modelsToProcess = models;
|
|
304
|
-
if (models.length === 1 && models[0].toLowerCase() === 'auto') {
|
|
305
|
-
// Find first 3 available providers
|
|
306
|
-
// Prioritize subscription-based providers (codex, gemini-cli, claude, copilot) over API-key providers
|
|
307
|
-
const availableProviders = [];
|
|
308
|
-
const providerOrder = [
|
|
309
|
-
'codex',
|
|
310
|
-
'gemini-cli',
|
|
311
|
-
'claude',
|
|
312
|
-
'copilot',
|
|
313
|
-
'openai',
|
|
314
|
-
'google',
|
|
315
|
-
'xai',
|
|
316
|
-
'anthropic',
|
|
317
|
-
'mistral',
|
|
318
|
-
'deepseek',
|
|
319
|
-
'openrouter',
|
|
320
|
-
];
|
|
321
|
-
|
|
322
|
-
const requestHasImages = Array.isArray(images) && images.length > 0;
|
|
323
|
-
|
|
324
|
-
for (const providerName of providerOrder) {
|
|
325
|
-
if (availableProviders.length >= 3) break;
|
|
326
|
-
const provider = providers[providerName];
|
|
327
|
-
if (provider && provider.isAvailable(config)) {
|
|
328
|
-
// Skip text-only providers when the request includes images.
|
|
329
|
-
if (
|
|
330
|
-
requestHasImages &&
|
|
331
|
-
!providerSupportsImages(provider, providerName)
|
|
332
|
-
) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
availableProviders.push(providerName);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (availableProviders.length === 0) {
|
|
340
|
-
return createToolError(
|
|
341
|
-
'No providers available. Please configure at least one API key.',
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Create model names for each available provider with their default model
|
|
346
|
-
modelsToProcess = availableProviders.map((providerName) =>
|
|
347
|
-
getDefaultModelForProvider(providerName),
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
logger.debug('Auto-expanded to providers', {
|
|
351
|
-
data: {
|
|
352
|
-
providers: availableProviders,
|
|
353
|
-
models: modelsToProcess,
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
for (const modelName of modelsToProcess) {
|
|
359
|
-
if (!modelName || typeof modelName !== 'string') {
|
|
360
|
-
failedModels.push({
|
|
361
|
-
model: modelName || 'unknown',
|
|
362
|
-
error: 'Invalid model specification',
|
|
363
|
-
status: 'failed',
|
|
364
|
-
});
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const providerName = mapModelToProvider(modelName, providers);
|
|
368
|
-
const resolvedModelName = resolveAutoModel(modelName, providerName);
|
|
369
|
-
const provider = providers[providerName];
|
|
370
|
-
|
|
371
|
-
if (!provider) {
|
|
372
|
-
failedModels.push({
|
|
373
|
-
model: modelName,
|
|
374
|
-
provider: providerName,
|
|
375
|
-
error: `Provider not found: ${providerName}`,
|
|
376
|
-
status: 'failed',
|
|
377
|
-
});
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (!provider.isAvailable(config)) {
|
|
382
|
-
failedModels.push({
|
|
383
|
-
model: modelName,
|
|
384
|
-
provider: providerName,
|
|
385
|
-
error: getProviderUnavailableMessage(providerName),
|
|
386
|
-
status: 'failed',
|
|
387
|
-
});
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
providerCalls.push({
|
|
392
|
-
model: modelName, // Keep original model name for display
|
|
393
|
-
provider: providerName,
|
|
394
|
-
providerInstance: provider,
|
|
395
|
-
options: {
|
|
396
|
-
temperature,
|
|
397
|
-
reasoning_effort,
|
|
398
|
-
use_websearch,
|
|
399
|
-
signal,
|
|
400
|
-
config,
|
|
401
|
-
model: resolvedModelName, // Use resolved model name for API call
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (providerCalls.length === 0) {
|
|
407
|
-
return createToolError(
|
|
408
|
-
`No valid providers available for the specified models. Failed models: ${failedModels.map((f) => f.model).join(', ')}`,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Phase 1: Initial parallel provider calls
|
|
413
|
-
logger.debug('Calling providers in parallel', {
|
|
414
|
-
data: { providerCount: providerCalls.length },
|
|
415
|
-
});
|
|
416
|
-
const consensusStartTime = Date.now();
|
|
417
|
-
const initialResults = await Promise.allSettled(
|
|
418
|
-
providerCalls.map(async (call) => {
|
|
419
|
-
try {
|
|
420
|
-
const response = await call.providerInstance.invoke(
|
|
421
|
-
messages,
|
|
422
|
-
call.options,
|
|
423
|
-
);
|
|
424
|
-
return {
|
|
425
|
-
model: call.model,
|
|
426
|
-
provider: call.provider,
|
|
427
|
-
status: 'success',
|
|
428
|
-
response: response.content,
|
|
429
|
-
metadata: response.metadata || {},
|
|
430
|
-
};
|
|
431
|
-
} catch (error) {
|
|
432
|
-
return {
|
|
433
|
-
model: call.model,
|
|
434
|
-
provider: call.provider,
|
|
435
|
-
status: 'failed',
|
|
436
|
-
error: error.message,
|
|
437
|
-
metadata: {},
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}),
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
// Process initial results
|
|
444
|
-
const initialPhase = {
|
|
445
|
-
successful: [],
|
|
446
|
-
failed: [],
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
initialResults.forEach((result, index) => {
|
|
450
|
-
if (result.status === 'fulfilled') {
|
|
451
|
-
if (result.value.status === 'success') {
|
|
452
|
-
initialPhase.successful.push(result.value);
|
|
453
|
-
} else {
|
|
454
|
-
initialPhase.failed.push(result.value);
|
|
455
|
-
}
|
|
456
|
-
} else {
|
|
457
|
-
initialPhase.failed.push({
|
|
458
|
-
model: providerCalls[index].model,
|
|
459
|
-
provider: providerCalls[index].provider,
|
|
460
|
-
status: 'failed',
|
|
461
|
-
error: result.reason.message || 'Unknown error',
|
|
462
|
-
metadata: {},
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// Add pre-failed models to failed list
|
|
468
|
-
initialPhase.failed.push(...failedModels);
|
|
469
|
-
|
|
470
|
-
let refinedPhase = null;
|
|
471
|
-
|
|
472
|
-
// Phase 2: Cross-feedback (if enabled and we have multiple successful responses)
|
|
473
|
-
// Skip if signal is already aborted after Phase 1
|
|
474
|
-
if (enable_cross_feedback && initialPhase.successful.length > 1 && !signal?.aborted) {
|
|
475
|
-
logger.debug('Running cross-feedback phase', {
|
|
476
|
-
data: { responseCount: initialPhase.successful.length },
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// Create cross-feedback prompt
|
|
480
|
-
const feedbackPrompt =
|
|
481
|
-
cross_feedback_prompt ||
|
|
482
|
-
`Based on the other AI responses below, please refine your answer to the original question. Consider different perspectives and provide your final response:
|
|
483
|
-
|
|
484
|
-
Original Question: ${prompt}
|
|
485
|
-
|
|
486
|
-
Other AI Responses:
|
|
487
|
-
${initialPhase.successful.map((r, i) => `${i + 1}. ${r.model}: ${r.response}`).join('\n\n')}
|
|
488
|
-
|
|
489
|
-
Please provide your refined response:`;
|
|
490
|
-
|
|
491
|
-
// Build feedback messages - need to add the assistant's initial response first
|
|
492
|
-
const feedbackMessages = [...messages];
|
|
493
|
-
|
|
494
|
-
// Run refinement calls in parallel
|
|
495
|
-
const refinementResults = await Promise.allSettled(
|
|
496
|
-
initialPhase.successful.map(async (initialResult) => {
|
|
497
|
-
try {
|
|
498
|
-
const call = providerCalls.find(
|
|
499
|
-
(c) => c.model === initialResult.model,
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
// Build model-specific feedback messages with the assistant's initial response
|
|
503
|
-
const modelFeedbackMessages = [...messages];
|
|
504
|
-
// Add the assistant's initial response
|
|
505
|
-
modelFeedbackMessages.push({
|
|
506
|
-
role: 'assistant',
|
|
507
|
-
content: initialResult.response,
|
|
508
|
-
});
|
|
509
|
-
// Now add the feedback prompt
|
|
510
|
-
modelFeedbackMessages.push({
|
|
511
|
-
role: 'user',
|
|
512
|
-
content: feedbackPrompt,
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const response = await call.providerInstance.invoke(
|
|
516
|
-
modelFeedbackMessages,
|
|
517
|
-
call.options,
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
return {
|
|
521
|
-
...initialResult,
|
|
522
|
-
refined_response: response.content,
|
|
523
|
-
refined_metadata: response.metadata || {},
|
|
524
|
-
initial_response: initialResult.response,
|
|
525
|
-
status: 'success',
|
|
526
|
-
};
|
|
527
|
-
} catch (error) {
|
|
528
|
-
return {
|
|
529
|
-
...initialResult,
|
|
530
|
-
refined_response: null,
|
|
531
|
-
refined_error: error.message,
|
|
532
|
-
initial_response: initialResult.response,
|
|
533
|
-
status: 'partial', // Had initial success but refinement failed
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
}),
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
// Process refinement results
|
|
540
|
-
refinedPhase = [];
|
|
541
|
-
refinementResults.forEach((result) => {
|
|
542
|
-
if (result.status === 'fulfilled') {
|
|
543
|
-
refinedPhase.push(result.value);
|
|
544
|
-
} else {
|
|
545
|
-
// This shouldn't happen with our error handling, but just in case
|
|
546
|
-
const originalResult = result.value || {};
|
|
547
|
-
refinedPhase.push({
|
|
548
|
-
...originalResult,
|
|
549
|
-
refined_response: null,
|
|
550
|
-
refined_error: 'Refinement phase failed unexpectedly',
|
|
551
|
-
status: 'partial',
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Save conversation state (skip on abort to avoid persisting incomplete history)
|
|
558
|
-
let conversationState;
|
|
559
|
-
if (!signal?.aborted) {
|
|
560
|
-
try {
|
|
561
|
-
// Build formatted consensus response with actual model responses
|
|
562
|
-
let formattedContent = '';
|
|
563
|
-
|
|
564
|
-
// Add initial responses
|
|
565
|
-
formattedContent += '## Initial Responses\n\n';
|
|
566
|
-
for (const result of initialPhase.successful) {
|
|
567
|
-
formattedContent += `### ${result.model} (initial response):\n${result.response}\n\n---\n\n`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Add refined responses if cross-feedback was enabled
|
|
571
|
-
if (refinedPhase) {
|
|
572
|
-
formattedContent += '## Refined Responses\n\n';
|
|
573
|
-
for (const result of refinedPhase) {
|
|
574
|
-
if (result.status === 'success' && result.refined_response) {
|
|
575
|
-
formattedContent += `### ${result.model} (refined response):\n${result.refined_response}\n\n---\n\n`;
|
|
576
|
-
} else if (result.status === 'partial') {
|
|
577
|
-
// Show initial response for models that failed refinement
|
|
578
|
-
formattedContent += `### ${result.model} (refinement failed, showing initial):\n${result.initial_response}\n\n---\n\n`;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Add summary at the end
|
|
584
|
-
formattedContent += `\n**Summary:** Consensus completed with ${initialPhase.successful.length} successful initial responses`;
|
|
585
|
-
if (refinedPhase) {
|
|
586
|
-
const successfulRefinements = refinedPhase.filter(
|
|
587
|
-
(r) => r.status === 'success',
|
|
588
|
-
).length;
|
|
589
|
-
formattedContent += ` and ${successfulRefinements} successful refined responses`;
|
|
590
|
-
}
|
|
591
|
-
formattedContent += '.';
|
|
592
|
-
const consensusMessage = {
|
|
593
|
-
role: 'assistant',
|
|
594
|
-
content: formattedContent,
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
conversationState = {
|
|
598
|
-
messages: [...messages, consensusMessage],
|
|
599
|
-
type: 'consensus',
|
|
600
|
-
lastUpdated: Date.now(),
|
|
601
|
-
consensusData: {
|
|
602
|
-
modelsRequested: models.length,
|
|
603
|
-
providersSuccessful: initialPhase.successful.length,
|
|
604
|
-
providersFailed: initialPhase.failed.length,
|
|
605
|
-
crossFeedbackEnabled: enable_cross_feedback,
|
|
606
|
-
},
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
await dependencies.continuationStore.set(
|
|
610
|
-
continuationId,
|
|
611
|
-
conversationState,
|
|
612
|
-
);
|
|
613
|
-
} catch (error) {
|
|
614
|
-
logger.error('Error saving consensus conversation', { error });
|
|
615
|
-
// Continue even if save fails
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Export conversation if requested
|
|
620
|
-
if (shouldExport && conversationState) {
|
|
621
|
-
await exportConversation(conversationState, {
|
|
622
|
-
clientCwd: dependencies.config.server?.client_cwd,
|
|
623
|
-
continuation_id: continuationId,
|
|
624
|
-
models,
|
|
625
|
-
temperature,
|
|
626
|
-
reasoning_effort,
|
|
627
|
-
use_websearch,
|
|
628
|
-
files,
|
|
629
|
-
images,
|
|
630
|
-
enable_cross_feedback,
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const consensusExecutionTime = (Date.now() - consensusStartTime) / 1000; // Convert to seconds
|
|
635
|
-
|
|
636
|
-
// Calculate final success count and collect failure details
|
|
637
|
-
let finalSuccessCount;
|
|
638
|
-
const failureDetails = [];
|
|
639
|
-
|
|
640
|
-
if (enable_cross_feedback && refinedPhase) {
|
|
641
|
-
// When cross-feedback is enabled, count only models that succeeded in both phases
|
|
642
|
-
finalSuccessCount = refinedPhase.filter(
|
|
643
|
-
(r) => r.status === 'success',
|
|
644
|
-
).length;
|
|
645
|
-
|
|
646
|
-
// Collect detailed failure information
|
|
647
|
-
refinedPhase.forEach((result) => {
|
|
648
|
-
if (result.status === 'partial') {
|
|
649
|
-
failureDetails.push(`${result.model} (refinement failed)`);
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// Add models that failed in initial phase
|
|
654
|
-
initialPhase.failed.forEach((failure) => {
|
|
655
|
-
failureDetails.push(`${failure.model} (initial failed)`);
|
|
656
|
-
});
|
|
657
|
-
} else {
|
|
658
|
-
// When cross-feedback is disabled, count initial successes
|
|
659
|
-
finalSuccessCount = initialPhase.successful.length;
|
|
660
|
-
|
|
661
|
-
// Collect initial failure information
|
|
662
|
-
initialPhase.failed.forEach((failure) => {
|
|
663
|
-
failureDetails.push(`${failure.model} (${failure.error})`);
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Create models list string for display
|
|
668
|
-
const modelsList = providerCalls.map((call) => call.model).join(', ');
|
|
669
|
-
|
|
670
|
-
// Create unified status line (similar to async status display)
|
|
671
|
-
const finalCount = refinedPhase
|
|
672
|
-
? refinedPhase.filter((r) => r.status === 'success').length
|
|
673
|
-
: initialPhase.successful.length;
|
|
674
|
-
const totalCount = providerCalls.length;
|
|
675
|
-
const statusLine =
|
|
676
|
-
config.environment?.nodeEnv !== 'test'
|
|
677
|
-
? `✅ COMPLETED | CONSENSUS | ${continuationId} | ${consensusExecutionTime.toFixed(1)}s elapsed | ${finalCount}/${totalCount} succeeded | ${modelsList}\n`
|
|
678
|
-
: '';
|
|
679
|
-
|
|
680
|
-
// Always include continuation_id line for clarity
|
|
681
|
-
const continuationIdLine = `continuation_id: ${continuationId}\n\n`;
|
|
682
|
-
|
|
683
|
-
// Build result object keeping backward compatibility but removing rawResponse
|
|
684
|
-
const result = {
|
|
685
|
-
status: 'consensus_complete',
|
|
686
|
-
models_consulted: models.length,
|
|
687
|
-
successful_initial_responses: initialPhase.successful.length,
|
|
688
|
-
failed_responses: initialPhase.failed.length,
|
|
689
|
-
refined_responses: refinedPhase
|
|
690
|
-
? refinedPhase.filter((r) => r.status === 'success').length
|
|
691
|
-
: 0,
|
|
692
|
-
phases: {
|
|
693
|
-
initial: initialPhase.successful,
|
|
694
|
-
...(refinedPhase !== null && { refined: refinedPhase }),
|
|
695
|
-
failed: initialPhase.failed,
|
|
696
|
-
},
|
|
697
|
-
continuation: {
|
|
698
|
-
id: continuationId,
|
|
699
|
-
messageCount: messages.length + 1,
|
|
700
|
-
...(isCustomId && { custom_id: true }),
|
|
701
|
-
},
|
|
702
|
-
settings: {
|
|
703
|
-
enable_cross_feedback,
|
|
704
|
-
temperature,
|
|
705
|
-
models_requested: models,
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
// Apply token limiting to the final response
|
|
710
|
-
const tokenLimit = getTokenLimit(config);
|
|
711
|
-
const resultStr = JSON.stringify(result, null, 2);
|
|
712
|
-
const limitedResult = applyTokenLimit(resultStr, tokenLimit);
|
|
713
|
-
|
|
714
|
-
// Add failure details to the response content if there are failures
|
|
715
|
-
let finalContent = limitedResult.content;
|
|
716
|
-
if (failureDetails.length > 0) {
|
|
717
|
-
const failureInfo = formatFailureDetails(failureDetails);
|
|
718
|
-
finalContent = limitedResult.content + failureInfo;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Prepend status line and continuation_id line when appropriate
|
|
722
|
-
finalContent = statusLine + continuationIdLine + finalContent;
|
|
723
|
-
|
|
724
|
-
// Return with continuation at top level for test compatibility
|
|
725
|
-
return createToolResponse({
|
|
726
|
-
content: finalContent,
|
|
727
|
-
continuation: {
|
|
728
|
-
id: continuationId,
|
|
729
|
-
messageCount: messages.length + 1,
|
|
730
|
-
...(isCustomId && { custom_id: true }),
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
} catch (error) {
|
|
734
|
-
if (dependencies?.signal?.aborted || error.name === 'AbortError') {
|
|
735
|
-
logger.debug('Consensus tool cancelled by client');
|
|
736
|
-
return createToolError('Consensus request cancelled');
|
|
737
|
-
}
|
|
738
|
-
logger.error('Consensus tool error', { error });
|
|
739
|
-
return createToolError('Consensus tool failed', error);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Map model name to provider name (same as chat tool)
|
|
745
|
-
* @param {string} model - Model name
|
|
746
|
-
* @returns {string} Provider name
|
|
747
|
-
*/
|
|
748
|
-
/**
|
|
749
|
-
* Get default model for a provider
|
|
750
|
-
*/
|
|
751
|
-
function getDefaultModelForProvider(providerName) {
|
|
752
|
-
const defaults = {
|
|
753
|
-
codex: 'codex',
|
|
754
|
-
'gemini-cli': 'gemini',
|
|
755
|
-
claude: 'claude',
|
|
756
|
-
copilot: 'copilot',
|
|
757
|
-
openai: 'gpt-5',
|
|
758
|
-
xai: 'grok-4-0709',
|
|
759
|
-
google: 'gemini-pro',
|
|
760
|
-
anthropic: 'claude-sonnet-4-20250514',
|
|
761
|
-
mistral: 'magistral-medium-2506',
|
|
762
|
-
deepseek: 'deepseek-reasoner',
|
|
763
|
-
openrouter: 'qwen/qwen3-coder',
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
return defaults[providerName] || 'gpt-5';
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Resolve "auto" model to default model for the provider
|
|
771
|
-
*/
|
|
772
|
-
function resolveAutoModel(model, providerName) {
|
|
773
|
-
if (model.toLowerCase() !== 'auto') {
|
|
774
|
-
return model;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return getDefaultModelForProvider(providerName);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function mapModelToProvider(model, providers) {
|
|
781
|
-
const modelLower = model.toLowerCase();
|
|
782
|
-
|
|
783
|
-
// Handle "auto" - prioritize: codex > gemini-cli > claude > copilot > openai
|
|
784
|
-
if (modelLower === 'auto') {
|
|
785
|
-
if (providers['codex']) {
|
|
786
|
-
return 'codex';
|
|
787
|
-
}
|
|
788
|
-
if (providers['gemini-cli']) {
|
|
789
|
-
return 'gemini-cli';
|
|
790
|
-
}
|
|
791
|
-
if (providers['claude']) {
|
|
792
|
-
return 'claude';
|
|
793
|
-
}
|
|
794
|
-
if (providers['copilot']) {
|
|
795
|
-
return 'copilot';
|
|
796
|
-
}
|
|
797
|
-
return 'openai';
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Check Codex (exact match only - don't route "gpt-5-codex" etc to Codex provider)
|
|
801
|
-
if (modelLower === 'codex') {
|
|
802
|
-
return 'codex';
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// Check Gemini CLI (exact match only - routes to CLI provider instead of Google API)
|
|
806
|
-
if (modelLower === 'gemini' || modelLower === 'gemini-cli') {
|
|
807
|
-
return 'gemini-cli';
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Check gemini: prefix (e.g., gemini:flash, gemini:pro) - routes to Antigravity
|
|
811
|
-
// CLI provider. Must be before the google flash/pro keyword rule so it wins.
|
|
812
|
-
if (modelLower.startsWith('gemini:')) {
|
|
813
|
-
return 'gemini-cli';
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
|
|
817
|
-
if (
|
|
818
|
-
modelLower === 'claude' ||
|
|
819
|
-
modelLower === 'claude-sdk' ||
|
|
820
|
-
modelLower === 'claude-code'
|
|
821
|
-
) {
|
|
822
|
-
return 'claude';
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Check claude: prefix (e.g., claude:fable, claude:opus) - routes to SDK provider
|
|
826
|
-
// Must be before keyword matching to prevent misrouting to Anthropic API
|
|
827
|
-
if (modelLower.startsWith('claude:')) {
|
|
828
|
-
return 'claude';
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Check Copilot SDK (exact match only - routes to SDK provider)
|
|
832
|
-
if (
|
|
833
|
-
modelLower === 'copilot' ||
|
|
834
|
-
modelLower === 'copilot-sdk' ||
|
|
835
|
-
modelLower === 'github-copilot'
|
|
836
|
-
) {
|
|
837
|
-
return 'copilot';
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Check copilot: prefix (e.g., copilot:gpt-5.2, copilot:claude-sonnet-4.6)
|
|
841
|
-
// Must be before slash-format and keyword matching to prevent misrouting
|
|
842
|
-
if (modelLower.startsWith('copilot:')) {
|
|
843
|
-
return 'copilot';
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Check OpenRouter-specific patterns first
|
|
847
|
-
if (
|
|
848
|
-
modelLower === 'openrouter auto' ||
|
|
849
|
-
modelLower === 'auto router' ||
|
|
850
|
-
modelLower === 'auto-router' ||
|
|
851
|
-
modelLower === 'openrouter-auto'
|
|
852
|
-
) {
|
|
853
|
-
return 'openrouter';
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// If model contains "/", check if native provider supports it
|
|
857
|
-
if (modelLower.includes('/')) {
|
|
858
|
-
// Check each provider to see if they have this exact model
|
|
859
|
-
for (const [providerName, provider] of Object.entries(providers)) {
|
|
860
|
-
if (provider && provider.getModelConfig) {
|
|
861
|
-
const modelConfig = provider.getModelConfig(model);
|
|
862
|
-
if (
|
|
863
|
-
modelConfig &&
|
|
864
|
-
!modelConfig.isDynamic &&
|
|
865
|
-
!modelConfig.needsApiUpdate
|
|
866
|
-
) {
|
|
867
|
-
// Model exists in this provider's static list
|
|
868
|
-
return providerName;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
// No native provider has this model, route to OpenRouter
|
|
873
|
-
return 'openrouter';
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// For non-slash models, use keyword matching as before
|
|
877
|
-
|
|
878
|
-
// OpenAI models
|
|
879
|
-
if (
|
|
880
|
-
modelLower.includes('gpt') ||
|
|
881
|
-
modelLower.includes('o1') ||
|
|
882
|
-
modelLower.includes('o3') ||
|
|
883
|
-
modelLower.includes('o4')
|
|
884
|
-
) {
|
|
885
|
-
return 'openai';
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// XAI models
|
|
889
|
-
if (modelLower.includes('grok')) {
|
|
890
|
-
return 'xai';
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Google models
|
|
894
|
-
if (
|
|
895
|
-
modelLower.includes('flash') ||
|
|
896
|
-
modelLower.includes('pro') ||
|
|
897
|
-
modelLower === 'google'
|
|
898
|
-
) {
|
|
899
|
-
return 'google';
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// Anthropic models
|
|
903
|
-
if (
|
|
904
|
-
modelLower.includes('claude') ||
|
|
905
|
-
modelLower.includes('fable') ||
|
|
906
|
-
modelLower.includes('opus') ||
|
|
907
|
-
modelLower.includes('sonnet') ||
|
|
908
|
-
modelLower.includes('haiku')
|
|
909
|
-
) {
|
|
910
|
-
return 'anthropic';
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Mistral models
|
|
914
|
-
if (modelLower.includes('mistral') || modelLower.includes('magistral')) {
|
|
915
|
-
return 'mistral';
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// DeepSeek models
|
|
919
|
-
if (
|
|
920
|
-
modelLower.includes('deepseek') ||
|
|
921
|
-
modelLower === 'reasoner' ||
|
|
922
|
-
modelLower === 'r1' ||
|
|
923
|
-
modelLower === 'chat'
|
|
924
|
-
) {
|
|
925
|
-
return 'deepseek';
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// OpenRouter models (specific model patterns)
|
|
929
|
-
if (
|
|
930
|
-
modelLower.includes('qwen') ||
|
|
931
|
-
modelLower.includes('kimi') ||
|
|
932
|
-
modelLower.includes('moonshot') ||
|
|
933
|
-
modelLower === 'k2'
|
|
934
|
-
) {
|
|
935
|
-
return 'openrouter';
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Default fallback
|
|
939
|
-
return 'openai';
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Execute consensus with streaming normalization for async execution
|
|
944
|
-
* @param {object} args - Original consensus arguments
|
|
945
|
-
* @param {object} dependencies - Dependencies with continuationId
|
|
946
|
-
* @param {object} context - Job execution context
|
|
947
|
-
* @returns {Promise<object>} Complete consensus result
|
|
948
|
-
*/
|
|
949
|
-
async function executeConsensusWithStreaming(args, dependencies, context) {
|
|
950
|
-
const {
|
|
951
|
-
config,
|
|
952
|
-
providers,
|
|
953
|
-
continuationStore,
|
|
954
|
-
contextProcessor,
|
|
955
|
-
providerStreamNormalizer,
|
|
956
|
-
continuationId,
|
|
957
|
-
isCustomId,
|
|
958
|
-
title: passedTitle, // Title passed from initial submission
|
|
959
|
-
} = dependencies;
|
|
960
|
-
|
|
961
|
-
const {
|
|
962
|
-
prompt,
|
|
963
|
-
models,
|
|
964
|
-
files = [],
|
|
965
|
-
images = [],
|
|
966
|
-
enable_cross_feedback = true,
|
|
967
|
-
cross_feedback_prompt,
|
|
968
|
-
temperature = 0.2,
|
|
969
|
-
reasoning_effort = 'medium',
|
|
970
|
-
use_websearch = false,
|
|
971
|
-
export: shouldExport = false,
|
|
972
|
-
} = args;
|
|
973
|
-
|
|
974
|
-
let conversationHistory = [];
|
|
975
|
-
|
|
976
|
-
// Load existing conversation if continuation_id provided
|
|
977
|
-
if (continuationId) {
|
|
978
|
-
try {
|
|
979
|
-
const existingState = await continuationStore.get(continuationId);
|
|
980
|
-
if (existingState) {
|
|
981
|
-
conversationHistory = existingState.messages || [];
|
|
982
|
-
}
|
|
983
|
-
} catch (error) {
|
|
984
|
-
logger.error('Error loading conversation', { error });
|
|
985
|
-
// Continue with fresh conversation on error
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Validate file paths before processing
|
|
990
|
-
if (files.length > 0 || images.length > 0) {
|
|
991
|
-
const validation = await validateAllPaths(
|
|
992
|
-
{
|
|
993
|
-
files,
|
|
994
|
-
images,
|
|
995
|
-
},
|
|
996
|
-
{ clientCwd: config.server?.client_cwd },
|
|
997
|
-
);
|
|
998
|
-
if (!validation.valid) {
|
|
999
|
-
logger.error('File validation failed', { errors: validation.errors });
|
|
1000
|
-
throw new Error(
|
|
1001
|
-
`File validation failed: ${validation.errors.join(', ')}`,
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// Process context (files and images)
|
|
1007
|
-
let contextMessage = null;
|
|
1008
|
-
if (files.length > 0 || images.length > 0) {
|
|
1009
|
-
try {
|
|
1010
|
-
const contextRequest = {
|
|
1011
|
-
files: Array.isArray(files) ? files : [],
|
|
1012
|
-
images: Array.isArray(images) ? images : [],
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
const contextResult = await contextProcessor.processUnifiedContext(
|
|
1016
|
-
contextRequest,
|
|
1017
|
-
{
|
|
1018
|
-
enforceSecurityCheck: false,
|
|
1019
|
-
skipSecurityCheck: true,
|
|
1020
|
-
clientCwd: config.server?.client_cwd,
|
|
1021
|
-
},
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
// Create context message from files and images
|
|
1025
|
-
const allProcessedFiles = [
|
|
1026
|
-
...contextResult.files,
|
|
1027
|
-
...contextResult.images,
|
|
1028
|
-
];
|
|
1029
|
-
if (allProcessedFiles.length > 0) {
|
|
1030
|
-
contextMessage = createFileContext(allProcessedFiles, {
|
|
1031
|
-
includeMetadata: true,
|
|
1032
|
-
includeErrors: true,
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
logger.error('Error processing context', { error });
|
|
1037
|
-
// Continue without context if processing fails
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Build message array for providers
|
|
1042
|
-
const messages = [];
|
|
1043
|
-
|
|
1044
|
-
// Add system prompt
|
|
1045
|
-
messages.push({
|
|
1046
|
-
role: 'system',
|
|
1047
|
-
content: CONSENSUS_PROMPT,
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// Add conversation history
|
|
1051
|
-
messages.push(...conversationHistory);
|
|
1052
|
-
|
|
1053
|
-
// Add user prompt with context
|
|
1054
|
-
const userMessage = {
|
|
1055
|
-
role: 'user',
|
|
1056
|
-
content: prompt,
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
|
-
// If we have context (files/images), create complex content array
|
|
1060
|
-
if (contextMessage && contextMessage.content) {
|
|
1061
|
-
userMessage.content = [
|
|
1062
|
-
...contextMessage.content,
|
|
1063
|
-
{ type: 'text', text: prompt },
|
|
1064
|
-
];
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
messages.push(userMessage);
|
|
1068
|
-
|
|
1069
|
-
// Resolve model specifications to provider calls
|
|
1070
|
-
const providerCalls = [];
|
|
1071
|
-
const failedModels = [];
|
|
1072
|
-
|
|
1073
|
-
// Special handling for single "auto" model - expand to first 3 available providers
|
|
1074
|
-
let modelsToProcess = models;
|
|
1075
|
-
if (models.length === 1 && models[0].toLowerCase() === 'auto') {
|
|
1076
|
-
// Prioritize subscription-based providers (codex, gemini-cli, claude, copilot) over API-key providers
|
|
1077
|
-
const availableProviders = [];
|
|
1078
|
-
const providerOrder = [
|
|
1079
|
-
'codex',
|
|
1080
|
-
'gemini-cli',
|
|
1081
|
-
'claude',
|
|
1082
|
-
'copilot',
|
|
1083
|
-
'openai',
|
|
1084
|
-
'google',
|
|
1085
|
-
'xai',
|
|
1086
|
-
'anthropic',
|
|
1087
|
-
'mistral',
|
|
1088
|
-
'deepseek',
|
|
1089
|
-
'openrouter',
|
|
1090
|
-
];
|
|
1091
|
-
|
|
1092
|
-
const requestHasImages = Array.isArray(images) && images.length > 0;
|
|
1093
|
-
|
|
1094
|
-
for (const providerName of providerOrder) {
|
|
1095
|
-
if (availableProviders.length >= 3) break;
|
|
1096
|
-
const provider = providers[providerName];
|
|
1097
|
-
if (provider && provider.isAvailable(config)) {
|
|
1098
|
-
// Skip text-only providers when the request includes images.
|
|
1099
|
-
if (
|
|
1100
|
-
requestHasImages &&
|
|
1101
|
-
!providerSupportsImages(provider, providerName)
|
|
1102
|
-
) {
|
|
1103
|
-
continue;
|
|
1104
|
-
}
|
|
1105
|
-
availableProviders.push(providerName);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (availableProviders.length === 0) {
|
|
1110
|
-
throw new Error(
|
|
1111
|
-
'No providers available. Please configure at least one API key.',
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
modelsToProcess = availableProviders.map((providerName) =>
|
|
1116
|
-
getDefaultModelForProvider(providerName),
|
|
1117
|
-
);
|
|
1118
|
-
|
|
1119
|
-
logger.debug('Auto-expanded to providers', {
|
|
1120
|
-
data: {
|
|
1121
|
-
providers: availableProviders,
|
|
1122
|
-
models: modelsToProcess,
|
|
1123
|
-
},
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Build provider calls array
|
|
1128
|
-
for (const modelName of modelsToProcess) {
|
|
1129
|
-
if (!modelName || typeof modelName !== 'string') {
|
|
1130
|
-
failedModels.push({
|
|
1131
|
-
model: modelName || 'unknown',
|
|
1132
|
-
error: 'Invalid model specification',
|
|
1133
|
-
status: 'failed',
|
|
1134
|
-
});
|
|
1135
|
-
continue;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
const providerName = mapModelToProvider(modelName, providers);
|
|
1139
|
-
const resolvedModelName = resolveAutoModel(modelName, providerName);
|
|
1140
|
-
const provider = providers[providerName];
|
|
1141
|
-
|
|
1142
|
-
if (!provider) {
|
|
1143
|
-
failedModels.push({
|
|
1144
|
-
model: modelName,
|
|
1145
|
-
provider: providerName,
|
|
1146
|
-
error: `Provider not found: ${providerName}`,
|
|
1147
|
-
status: 'failed',
|
|
1148
|
-
});
|
|
1149
|
-
continue;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
if (!provider.isAvailable(config)) {
|
|
1153
|
-
failedModels.push({
|
|
1154
|
-
model: modelName,
|
|
1155
|
-
provider: providerName,
|
|
1156
|
-
error: getProviderUnavailableMessage(providerName),
|
|
1157
|
-
status: 'failed',
|
|
1158
|
-
});
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
providerCalls.push({
|
|
1163
|
-
model: modelName,
|
|
1164
|
-
provider: providerName,
|
|
1165
|
-
providerInstance: provider,
|
|
1166
|
-
options: {
|
|
1167
|
-
temperature,
|
|
1168
|
-
reasoning_effort,
|
|
1169
|
-
use_websearch,
|
|
1170
|
-
signal: context?.signal, // Pass AbortSignal for cancellation support
|
|
1171
|
-
config,
|
|
1172
|
-
model: resolvedModelName,
|
|
1173
|
-
},
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
if (providerCalls.length === 0) {
|
|
1178
|
-
throw new Error(
|
|
1179
|
-
`No valid providers available for the specified models. Failed models: ${failedModels.map((f) => f.model).join(', ')}`,
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Create models list string for display
|
|
1184
|
-
const modelsList = providerCalls.map((call) => call.model).join(', ');
|
|
1185
|
-
|
|
1186
|
-
// Initialize SummarizationService
|
|
1187
|
-
const summarizationService = new SummarizationService(providers, config);
|
|
1188
|
-
|
|
1189
|
-
// Use passed title or generate if not provided
|
|
1190
|
-
let title = passedTitle;
|
|
1191
|
-
if (!title) {
|
|
1192
|
-
try {
|
|
1193
|
-
title = await summarizationService.generateTitle(prompt);
|
|
1194
|
-
debugLog(`Consensus: Generated title - "${title}"`);
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
debugError('Consensus: Error generating title', error);
|
|
1197
|
-
// Continue without title if generation fails
|
|
1198
|
-
title = prompt.substring(0, 50);
|
|
1199
|
-
}
|
|
1200
|
-
} else {
|
|
1201
|
-
debugLog(`Consensus: Using passed title - "${title}"`);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Update job status for phase 1 with title
|
|
1205
|
-
await context.updateJob({
|
|
1206
|
-
models_list: modelsList,
|
|
1207
|
-
title,
|
|
1208
|
-
consensus_progress: `0/${providerCalls.length} initial`,
|
|
1209
|
-
progress: {
|
|
1210
|
-
phase: 'initial_consensus',
|
|
1211
|
-
total_providers: providerCalls.length,
|
|
1212
|
-
completed_providers: 0,
|
|
1213
|
-
failed_providers: failedModels.length,
|
|
1214
|
-
provider_status: {},
|
|
1215
|
-
},
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
const consensusStartTime = Date.now();
|
|
1219
|
-
|
|
1220
|
-
// Phase 1: Initial parallel provider calls with streaming
|
|
1221
|
-
logger.debug('Calling providers in parallel with streaming', {
|
|
1222
|
-
data: { providerCount: providerCalls.length },
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
const initialResults = await executeConsensusPhaseWithStreaming(
|
|
1226
|
-
providerCalls,
|
|
1227
|
-
messages,
|
|
1228
|
-
'initial',
|
|
1229
|
-
context,
|
|
1230
|
-
providerStreamNormalizer,
|
|
1231
|
-
);
|
|
1232
|
-
|
|
1233
|
-
// Process initial results
|
|
1234
|
-
const initialPhase = {
|
|
1235
|
-
successful: [],
|
|
1236
|
-
failed: [],
|
|
1237
|
-
};
|
|
1238
|
-
|
|
1239
|
-
initialResults.forEach((result) => {
|
|
1240
|
-
if (result.status === 'success') {
|
|
1241
|
-
initialPhase.successful.push(result);
|
|
1242
|
-
} else {
|
|
1243
|
-
initialPhase.failed.push(result);
|
|
1244
|
-
}
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
// Add pre-failed models to failed list
|
|
1248
|
-
initialPhase.failed.push(...failedModels);
|
|
1249
|
-
|
|
1250
|
-
let refinedPhase = null;
|
|
1251
|
-
|
|
1252
|
-
// Phase 2: Cross-feedback (if enabled and we have multiple successful responses)
|
|
1253
|
-
if (enable_cross_feedback && initialPhase.successful.length > 1) {
|
|
1254
|
-
logger.debug('Running cross-feedback phase with streaming', {
|
|
1255
|
-
data: { responseCount: initialPhase.successful.length },
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
// Update job status for phase 2
|
|
1259
|
-
await context.updateJob({
|
|
1260
|
-
progress: {
|
|
1261
|
-
phase: 'cross_feedback',
|
|
1262
|
-
total_providers: initialPhase.successful.length,
|
|
1263
|
-
completed_providers: 0,
|
|
1264
|
-
provider_status: {},
|
|
1265
|
-
},
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
// Create cross-feedback prompt
|
|
1269
|
-
const feedbackPrompt =
|
|
1270
|
-
cross_feedback_prompt ||
|
|
1271
|
-
`Based on the other AI responses below, please refine your answer to the original question. Consider different perspectives and provide your final response:
|
|
1272
|
-
|
|
1273
|
-
Original Question: ${prompt}
|
|
1274
|
-
|
|
1275
|
-
Other AI Responses:
|
|
1276
|
-
${initialPhase.successful.map((r, i) => `${i + 1}. ${r.model}: ${r.response}`).join('\n\n')}
|
|
1277
|
-
|
|
1278
|
-
Please provide your refined response:`;
|
|
1279
|
-
|
|
1280
|
-
// Build model-specific feedback calls
|
|
1281
|
-
const feedbackCalls = initialPhase.successful.map((initialResult) => {
|
|
1282
|
-
const call = providerCalls.find((c) => c.model === initialResult.model);
|
|
1283
|
-
|
|
1284
|
-
// Build model-specific feedback messages with the assistant's initial response
|
|
1285
|
-
const modelFeedbackMessages = [...messages];
|
|
1286
|
-
modelFeedbackMessages.push({
|
|
1287
|
-
role: 'assistant',
|
|
1288
|
-
content: initialResult.response,
|
|
1289
|
-
});
|
|
1290
|
-
modelFeedbackMessages.push({
|
|
1291
|
-
role: 'user',
|
|
1292
|
-
content: feedbackPrompt,
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
return {
|
|
1296
|
-
...call,
|
|
1297
|
-
messages: modelFeedbackMessages,
|
|
1298
|
-
initialResult,
|
|
1299
|
-
};
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// Execute refinement phase with streaming
|
|
1303
|
-
const refinementResults = await executeConsensusPhaseWithStreaming(
|
|
1304
|
-
feedbackCalls,
|
|
1305
|
-
null, // messages already embedded in feedbackCalls
|
|
1306
|
-
'refinement',
|
|
1307
|
-
context,
|
|
1308
|
-
providerStreamNormalizer,
|
|
1309
|
-
);
|
|
1310
|
-
|
|
1311
|
-
// Process refinement results
|
|
1312
|
-
refinedPhase = refinementResults.map((result, index) => {
|
|
1313
|
-
const initialResult = feedbackCalls[index].initialResult;
|
|
1314
|
-
return {
|
|
1315
|
-
...initialResult,
|
|
1316
|
-
refined_response: result.status === 'success' ? result.response : null,
|
|
1317
|
-
refined_metadata: result.status === 'success' ? result.metadata : {},
|
|
1318
|
-
refined_error: result.status === 'failed' ? result.error : null,
|
|
1319
|
-
initial_response: initialResult.response,
|
|
1320
|
-
status: result.status === 'success' ? 'success' : 'partial',
|
|
1321
|
-
};
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
// Save conversation state
|
|
1326
|
-
let conversationState;
|
|
1327
|
-
try {
|
|
1328
|
-
// Build formatted consensus response with actual model responses
|
|
1329
|
-
let formattedContent = '';
|
|
1330
|
-
|
|
1331
|
-
// Add initial responses
|
|
1332
|
-
formattedContent += '## Initial Responses\n\n';
|
|
1333
|
-
for (const result of initialPhase.successful) {
|
|
1334
|
-
formattedContent += `### ${result.model} (initial response):\n${result.response}\n\n---\n\n`;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// Add refined responses if cross-feedback was enabled
|
|
1338
|
-
if (refinedPhase) {
|
|
1339
|
-
formattedContent += '## Refined Responses\n\n';
|
|
1340
|
-
for (const result of refinedPhase) {
|
|
1341
|
-
if (result.status === 'success' && result.refined_response) {
|
|
1342
|
-
formattedContent += `### ${result.model} (refined response):\n${result.refined_response}\n\n---\n\n`;
|
|
1343
|
-
} else if (result.status === 'partial') {
|
|
1344
|
-
// Show initial response for models that failed refinement
|
|
1345
|
-
formattedContent += `### ${result.model} (refinement failed, showing initial):\n${result.initial_response}\n\n---\n\n`;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// Add summary at the end
|
|
1351
|
-
formattedContent += `\n**Summary:** Consensus completed with ${initialPhase.successful.length} successful initial responses`;
|
|
1352
|
-
if (refinedPhase) {
|
|
1353
|
-
const successfulRefinements = refinedPhase.filter(
|
|
1354
|
-
(r) => r.status === 'success',
|
|
1355
|
-
).length;
|
|
1356
|
-
formattedContent += ` and ${successfulRefinements} successful refined responses`;
|
|
1357
|
-
}
|
|
1358
|
-
formattedContent += '.';
|
|
1359
|
-
const consensusMessage = {
|
|
1360
|
-
role: 'assistant',
|
|
1361
|
-
content: formattedContent,
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
|
-
conversationState = {
|
|
1365
|
-
messages: [...messages, consensusMessage],
|
|
1366
|
-
type: 'consensus',
|
|
1367
|
-
lastUpdated: Date.now(),
|
|
1368
|
-
consensusData: {
|
|
1369
|
-
modelsRequested: models.length,
|
|
1370
|
-
providersSuccessful: initialPhase.successful.length,
|
|
1371
|
-
providersFailed: initialPhase.failed.length,
|
|
1372
|
-
crossFeedbackEnabled: enable_cross_feedback,
|
|
1373
|
-
},
|
|
1374
|
-
};
|
|
1375
|
-
|
|
1376
|
-
await continuationStore.set(continuationId, conversationState);
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
logger.error('Error saving consensus conversation', { error });
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Export conversation if requested
|
|
1382
|
-
if (shouldExport && conversationState) {
|
|
1383
|
-
await exportConversation(conversationState, {
|
|
1384
|
-
clientCwd: config.server?.client_cwd,
|
|
1385
|
-
continuation_id: continuationId,
|
|
1386
|
-
models,
|
|
1387
|
-
temperature,
|
|
1388
|
-
reasoning_effort,
|
|
1389
|
-
use_websearch,
|
|
1390
|
-
files,
|
|
1391
|
-
images,
|
|
1392
|
-
enable_cross_feedback,
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const consensusExecutionTime = (Date.now() - consensusStartTime) / 1000;
|
|
1397
|
-
|
|
1398
|
-
// Generate final summary from combined responses
|
|
1399
|
-
let finalSummary = null;
|
|
1400
|
-
const combinedResponses = [];
|
|
1401
|
-
|
|
1402
|
-
// Collect all successful responses for summary generation
|
|
1403
|
-
if (refinedPhase) {
|
|
1404
|
-
// Use refined responses when available
|
|
1405
|
-
refinedPhase.forEach((result) => {
|
|
1406
|
-
if (result.status === 'success' && result.refined_response) {
|
|
1407
|
-
combinedResponses.push(`${result.model}:\n${result.refined_response}`);
|
|
1408
|
-
} else if (result.initial_response) {
|
|
1409
|
-
// Fall back to initial response if refinement failed
|
|
1410
|
-
combinedResponses.push(`${result.model}:\n${result.initial_response}`);
|
|
1411
|
-
}
|
|
1412
|
-
});
|
|
1413
|
-
} else {
|
|
1414
|
-
// Use initial responses
|
|
1415
|
-
initialPhase.successful.forEach((result) => {
|
|
1416
|
-
if (result.response) {
|
|
1417
|
-
combinedResponses.push(`${result.model}:\n${result.response}`);
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// Generate summary if we have responses
|
|
1423
|
-
if (combinedResponses.length > 0) {
|
|
1424
|
-
const combinedContent = combinedResponses.join('\n\n---\n\n');
|
|
1425
|
-
if (combinedContent.length > 100) {
|
|
1426
|
-
try {
|
|
1427
|
-
finalSummary =
|
|
1428
|
-
await summarizationService.generateFinalSummary(combinedContent);
|
|
1429
|
-
debugLog(`Consensus: Generated final summary - "${finalSummary}"`);
|
|
1430
|
-
|
|
1431
|
-
// Update job with final summary
|
|
1432
|
-
await context.updateJob({
|
|
1433
|
-
final_summary: finalSummary,
|
|
1434
|
-
});
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
debugError('Consensus: Error generating final summary', error);
|
|
1437
|
-
// Continue without summary if generation fails
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// Calculate final success count and collect failure details
|
|
1443
|
-
let finalSuccessCount;
|
|
1444
|
-
const failureDetails = [];
|
|
1445
|
-
|
|
1446
|
-
if (enable_cross_feedback && refinedPhase) {
|
|
1447
|
-
finalSuccessCount = refinedPhase.filter(
|
|
1448
|
-
(r) => r.status === 'success',
|
|
1449
|
-
).length;
|
|
1450
|
-
|
|
1451
|
-
refinedPhase.forEach((result) => {
|
|
1452
|
-
if (result.status === 'partial') {
|
|
1453
|
-
failureDetails.push(`${result.model} (refinement failed)`);
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
|
-
initialPhase.failed.forEach((failure) => {
|
|
1458
|
-
failureDetails.push(`${failure.model} (initial failed)`);
|
|
1459
|
-
});
|
|
1460
|
-
} else {
|
|
1461
|
-
finalSuccessCount = initialPhase.successful.length;
|
|
1462
|
-
initialPhase.failed.forEach((failure) => {
|
|
1463
|
-
failureDetails.push(`${failure.model} (${failure.error})`);
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// Return complete consensus result for job completion
|
|
1468
|
-
return {
|
|
1469
|
-
status: 'consensus_complete',
|
|
1470
|
-
models_consulted: models.length,
|
|
1471
|
-
successful_initial_responses: initialPhase.successful.length,
|
|
1472
|
-
failed_responses: initialPhase.failed.length,
|
|
1473
|
-
refined_responses: refinedPhase
|
|
1474
|
-
? refinedPhase.filter((r) => r.status === 'success').length
|
|
1475
|
-
: 0,
|
|
1476
|
-
phases: {
|
|
1477
|
-
initial: initialPhase.successful,
|
|
1478
|
-
...(refinedPhase !== null && { refined: refinedPhase }),
|
|
1479
|
-
failed: initialPhase.failed,
|
|
1480
|
-
},
|
|
1481
|
-
continuation: {
|
|
1482
|
-
id: continuationId,
|
|
1483
|
-
messageCount: messages.length + 1,
|
|
1484
|
-
...(isCustomId && { custom_id: true }),
|
|
1485
|
-
},
|
|
1486
|
-
settings: {
|
|
1487
|
-
enable_cross_feedback,
|
|
1488
|
-
temperature,
|
|
1489
|
-
models_requested: models,
|
|
1490
|
-
},
|
|
1491
|
-
metadata: {
|
|
1492
|
-
execution_time: consensusExecutionTime,
|
|
1493
|
-
async_execution: true,
|
|
1494
|
-
successful_models: finalSuccessCount,
|
|
1495
|
-
total_models: models.length,
|
|
1496
|
-
failure_details: failureDetails,
|
|
1497
|
-
title,
|
|
1498
|
-
final_summary: finalSummary,
|
|
1499
|
-
},
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
/**
|
|
1504
|
-
* Execute a consensus phase (initial or refinement) with streaming support
|
|
1505
|
-
* @param {Array} providerCalls - Provider calls to execute
|
|
1506
|
-
* @param {Array} messages - Messages to send (null if already embedded in calls)
|
|
1507
|
-
* @param {string} phase - Phase name ('initial' or 'refinement')
|
|
1508
|
-
* @param {object} context - Job execution context
|
|
1509
|
-
* @param {object} streamNormalizer - Stream normalizer instance
|
|
1510
|
-
* @returns {Promise<Array>} Results from all providers
|
|
1511
|
-
*/
|
|
1512
|
-
async function executeConsensusPhaseWithStreaming(
|
|
1513
|
-
providerCalls,
|
|
1514
|
-
messages,
|
|
1515
|
-
phase,
|
|
1516
|
-
context,
|
|
1517
|
-
streamNormalizer,
|
|
1518
|
-
) {
|
|
1519
|
-
let completedCount = 0;
|
|
1520
|
-
const totalCount = providerCalls.length;
|
|
1521
|
-
const providerContents = {}; // Store accumulated content per provider
|
|
1522
|
-
|
|
1523
|
-
const results = await Promise.allSettled(
|
|
1524
|
-
providerCalls.map(async (call, index) => {
|
|
1525
|
-
try {
|
|
1526
|
-
// Check for cancellation before starting
|
|
1527
|
-
if (context.signal.aborted) {
|
|
1528
|
-
throw new Error('Consensus execution was cancelled');
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// Update provider status to 'prompting'
|
|
1532
|
-
await context.updateJob({
|
|
1533
|
-
progress: {
|
|
1534
|
-
[`provider_${index}_status`]: 'prompting',
|
|
1535
|
-
[`provider_${index}_model`]: call.model,
|
|
1536
|
-
},
|
|
1537
|
-
});
|
|
1538
|
-
|
|
1539
|
-
const messagesToSend = call.messages || messages;
|
|
1540
|
-
let response;
|
|
1541
|
-
|
|
1542
|
-
// Try streaming: .stream() method first, then invoke({stream:true}) for SDK providers
|
|
1543
|
-
let stream = null;
|
|
1544
|
-
if (
|
|
1545
|
-
call.providerInstance.stream &&
|
|
1546
|
-
typeof call.providerInstance.stream === 'function'
|
|
1547
|
-
) {
|
|
1548
|
-
stream = call.providerInstance.stream(
|
|
1549
|
-
messagesToSend,
|
|
1550
|
-
call.options,
|
|
1551
|
-
);
|
|
1552
|
-
} else {
|
|
1553
|
-
// SDK providers (copilot, codex, claude, gemini-cli) stream via invoke
|
|
1554
|
-
const streamResult = await call.providerInstance.invoke(
|
|
1555
|
-
messagesToSend,
|
|
1556
|
-
{ ...call.options, stream: true },
|
|
1557
|
-
);
|
|
1558
|
-
// Check if result is an async iterable (streaming) vs a plain response
|
|
1559
|
-
if (streamResult && typeof streamResult[Symbol.asyncIterator] === 'function') {
|
|
1560
|
-
stream = streamResult;
|
|
1561
|
-
} else {
|
|
1562
|
-
// Provider returned a non-streaming response — use it directly
|
|
1563
|
-
response = streamResult;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
if (stream) {
|
|
1568
|
-
const normalizedStream = streamNormalizer.normalize(
|
|
1569
|
-
call.provider,
|
|
1570
|
-
stream,
|
|
1571
|
-
{
|
|
1572
|
-
provider: call.provider,
|
|
1573
|
-
model: call.options.model,
|
|
1574
|
-
requestId: `${context.jobId}-${phase}-${index}`,
|
|
1575
|
-
},
|
|
1576
|
-
);
|
|
1577
|
-
|
|
1578
|
-
// Process normalized stream
|
|
1579
|
-
let accumulatedContent = '';
|
|
1580
|
-
let finalUsage = null;
|
|
1581
|
-
let finalMetadata = {};
|
|
1582
|
-
|
|
1583
|
-
await context.updateJob({
|
|
1584
|
-
progress: { [`provider_${index}_status`]: 'streaming' },
|
|
1585
|
-
});
|
|
1586
|
-
|
|
1587
|
-
for await (const event of normalizedStream) {
|
|
1588
|
-
if (context.signal.aborted) {
|
|
1589
|
-
throw new Error('Consensus execution was cancelled');
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
switch (event.type) {
|
|
1593
|
-
case 'delta':
|
|
1594
|
-
accumulatedContent += event.data.textDelta;
|
|
1595
|
-
// Store provider's accumulated content
|
|
1596
|
-
providerContents[index] = accumulatedContent;
|
|
1597
|
-
|
|
1598
|
-
// Combine all provider contents for unified accumulated_content
|
|
1599
|
-
const combinedContent = Object.values(providerContents)
|
|
1600
|
-
.filter((content) => content && content.length > 0)
|
|
1601
|
-
.join('\n\n---\n\n');
|
|
1602
|
-
|
|
1603
|
-
// Update with both provider preview and combined accumulated content
|
|
1604
|
-
await context.updateJob({
|
|
1605
|
-
[`provider_${index}_preview`]:
|
|
1606
|
-
accumulatedContent.length > 150
|
|
1607
|
-
? accumulatedContent.substring(0, 150) + '...'
|
|
1608
|
-
: accumulatedContent,
|
|
1609
|
-
accumulated_content: combinedContent,
|
|
1610
|
-
});
|
|
1611
|
-
break;
|
|
1612
|
-
case 'usage':
|
|
1613
|
-
finalUsage = event.data.usage;
|
|
1614
|
-
break;
|
|
1615
|
-
case 'end':
|
|
1616
|
-
accumulatedContent = event.data.content || accumulatedContent;
|
|
1617
|
-
finalUsage = event.data.usage || finalUsage;
|
|
1618
|
-
finalMetadata = event.data.metadata || finalMetadata;
|
|
1619
|
-
break;
|
|
1620
|
-
case 'error':
|
|
1621
|
-
throw new Error(`Streaming error: ${event.data.error.message}`);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
response = {
|
|
1626
|
-
content: accumulatedContent,
|
|
1627
|
-
metadata: {
|
|
1628
|
-
...finalMetadata,
|
|
1629
|
-
usage: finalUsage,
|
|
1630
|
-
streaming: true,
|
|
1631
|
-
},
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
// Store final provider content
|
|
1635
|
-
providerContents[index] = accumulatedContent;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
if (!stream) {
|
|
1639
|
-
// Non-streaming path (provider doesn't support streaming at all)
|
|
1640
|
-
if (!response) {
|
|
1641
|
-
response = await call.providerInstance.invoke(
|
|
1642
|
-
messagesToSend,
|
|
1643
|
-
call.options,
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// Store provider content for non-streaming response
|
|
1648
|
-
if (response && response.content) {
|
|
1649
|
-
providerContents[index] = response.content;
|
|
1650
|
-
|
|
1651
|
-
// Update accumulated content for non-streaming provider
|
|
1652
|
-
const combinedContent = Object.values(providerContents)
|
|
1653
|
-
.filter((content) => content && content.length > 0)
|
|
1654
|
-
.join('\n\n---\n\n');
|
|
1655
|
-
|
|
1656
|
-
await context.updateJob({
|
|
1657
|
-
accumulated_content: combinedContent,
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// Update provider status to 'finished'
|
|
1663
|
-
completedCount++;
|
|
1664
|
-
const progressText =
|
|
1665
|
-
phase === 'initial'
|
|
1666
|
-
? `${completedCount}/${totalCount} initial`
|
|
1667
|
-
: phase === 'refinement'
|
|
1668
|
-
? `${completedCount}/${totalCount} refined`
|
|
1669
|
-
: `${completedCount}/${totalCount} responded`;
|
|
1670
|
-
|
|
1671
|
-
await context.updateJob({
|
|
1672
|
-
consensus_progress: progressText,
|
|
1673
|
-
progress: {
|
|
1674
|
-
[`provider_${index}_status`]: 'finished',
|
|
1675
|
-
completed_providers: completedCount,
|
|
1676
|
-
},
|
|
1677
|
-
});
|
|
1678
|
-
|
|
1679
|
-
return {
|
|
1680
|
-
model: call.model,
|
|
1681
|
-
provider: call.provider,
|
|
1682
|
-
status: 'success',
|
|
1683
|
-
response: response.content,
|
|
1684
|
-
metadata: response.metadata || {},
|
|
1685
|
-
};
|
|
1686
|
-
} catch (error) {
|
|
1687
|
-
// Update provider status to 'failed'
|
|
1688
|
-
await context.updateJob({
|
|
1689
|
-
progress: {
|
|
1690
|
-
[`provider_${index}_status`]: 'failed',
|
|
1691
|
-
[`provider_${index}_error`]: error.message,
|
|
1692
|
-
},
|
|
1693
|
-
});
|
|
1694
|
-
|
|
1695
|
-
return {
|
|
1696
|
-
model: call.model,
|
|
1697
|
-
provider: call.provider,
|
|
1698
|
-
status: 'failed',
|
|
1699
|
-
error: error.message,
|
|
1700
|
-
metadata: {},
|
|
1701
|
-
};
|
|
1702
|
-
}
|
|
1703
|
-
}),
|
|
1704
|
-
);
|
|
1705
|
-
|
|
1706
|
-
// After all providers complete, update with final combined content
|
|
1707
|
-
const finalCombinedContent = Object.values(providerContents)
|
|
1708
|
-
.filter((content) => content && content.length > 0)
|
|
1709
|
-
.join('\n\n---\n\n');
|
|
1710
|
-
|
|
1711
|
-
if (finalCombinedContent) {
|
|
1712
|
-
await context.updateJob({
|
|
1713
|
-
accumulated_content: finalCombinedContent,
|
|
1714
|
-
});
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
return results.map((result, index) => {
|
|
1718
|
-
if (result.status === 'fulfilled') {
|
|
1719
|
-
return result.value;
|
|
1720
|
-
} else {
|
|
1721
|
-
return {
|
|
1722
|
-
model: providerCalls[index].model,
|
|
1723
|
-
provider: providerCalls[index].provider,
|
|
1724
|
-
status: 'failed',
|
|
1725
|
-
error: result.reason.message || 'Unknown error',
|
|
1726
|
-
metadata: {},
|
|
1727
|
-
};
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// Tool metadata
|
|
1733
|
-
consensusTool.description =
|
|
1734
|
-
'PARALLEL CONSENSUS WITH CROSS-MODEL FEEDBACK - Query multiple models simultaneously, then optionally refine responses based on cross-feedback. For complex decisions, architectural choices, technical evaluations. Use models: ["auto"] for automatic selection. IMPORTANT: Use the "files" parameter to share code/file content instead of pasting into the prompt.';
|
|
1735
|
-
consensusTool.inputSchema = {
|
|
1736
|
-
type: 'object',
|
|
1737
|
-
properties: {
|
|
1738
|
-
models: {
|
|
1739
|
-
type: 'array',
|
|
1740
|
-
items: { type: 'string' },
|
|
1741
|
-
minItems: 1,
|
|
1742
|
-
description:
|
|
1743
|
-
'List of models to consult. Examples: ["codex", "gemini", "gemini:flash", "claude", "claude:opus", "copilot", "copilot:codex"]',
|
|
1744
|
-
},
|
|
1745
|
-
files: {
|
|
1746
|
-
type: 'array',
|
|
1747
|
-
items: { type: 'string' },
|
|
1748
|
-
description:
|
|
1749
|
-
'File paths for additional context (absolute or relative paths). Supports line ranges: file.txt{10:50}, file.txt{100:}. Example: ["./docs/architecture.md{1:100}", "./requirements.txt"]. IMPORTANT: Always use this parameter to share file content instead of copying code into the prompt - it provides better formatting, line numbers, and preserves context.',
|
|
1750
|
-
},
|
|
1751
|
-
images: {
|
|
1752
|
-
type: 'array',
|
|
1753
|
-
items: { type: 'string' },
|
|
1754
|
-
description:
|
|
1755
|
-
'Image paths for visual context (absolute or relative paths, or base64). Example: ["C:\\Users\\username\\current_architecture.png", "./user_flow.jpg"]',
|
|
1756
|
-
},
|
|
1757
|
-
continuation_id: {
|
|
1758
|
-
type: 'string',
|
|
1759
|
-
description:
|
|
1760
|
-
'Thread continuation ID for multi-turn conversations. Auto-generated in the first response; pass it back to continue the conversation.',
|
|
1761
|
-
},
|
|
1762
|
-
enable_cross_feedback: {
|
|
1763
|
-
type: 'boolean',
|
|
1764
|
-
description:
|
|
1765
|
-
'Enable refinement phase where models see others\' responses and can improve their answers. Example: true (recommended), false (faster single-phase only). Default: true',
|
|
1766
|
-
default: true,
|
|
1767
|
-
},
|
|
1768
|
-
cross_feedback_prompt: {
|
|
1769
|
-
type: 'string',
|
|
1770
|
-
description:
|
|
1771
|
-
'Custom prompt for refinement phase. Example: "Focus on scalability trade-offs in your refinement" or leave empty for default cross-feedback prompt',
|
|
1772
|
-
},
|
|
1773
|
-
temperature: {
|
|
1774
|
-
type: 'number',
|
|
1775
|
-
description:
|
|
1776
|
-
'Response randomness (0.0-1.0). Examples: 0.1 (very focused), 0.2 (analytical - default), 0.5 (balanced). Default: 0.2',
|
|
1777
|
-
minimum: 0.0,
|
|
1778
|
-
maximum: 1.0,
|
|
1779
|
-
default: 0.2,
|
|
1780
|
-
},
|
|
1781
|
-
reasoning_effort: {
|
|
1782
|
-
type: 'string',
|
|
1783
|
-
enum: ['none', 'minimal', 'low', 'medium', 'high', 'max'],
|
|
1784
|
-
description:
|
|
1785
|
-
'Reasoning depth for thinking models. Examples: "none" (no reasoning, fastest), "low" (light analysis), "medium" (balanced), "high" (complex analysis). Default: "medium"',
|
|
1786
|
-
default: 'medium',
|
|
1787
|
-
},
|
|
1788
|
-
use_websearch: {
|
|
1789
|
-
type: 'boolean',
|
|
1790
|
-
description:
|
|
1791
|
-
'Enable web search for current information. Only works with models that support web search (OpenAI, XAI, Google). Example: true for recent developments or up to date documentation. Default: false',
|
|
1792
|
-
default: false,
|
|
1793
|
-
},
|
|
1794
|
-
async: {
|
|
1795
|
-
type: 'boolean',
|
|
1796
|
-
description:
|
|
1797
|
-
'Execute consensus in background with detailed progress tracking. When true, returns continuation_id immediately and processes request asynchronously with per-provider status updates. Default: false',
|
|
1798
|
-
default: false,
|
|
1799
|
-
},
|
|
1800
|
-
export: {
|
|
1801
|
-
type: 'boolean',
|
|
1802
|
-
description:
|
|
1803
|
-
'Export conversation to disk. Creates folder with continuation_id name containing numbered request/response files and metadata. Default: false',
|
|
1804
|
-
default: false,
|
|
1805
|
-
},
|
|
1806
|
-
prompt: {
|
|
1807
|
-
type: 'string',
|
|
1808
|
-
description:
|
|
1809
|
-
'The problem or proposal to gather consensus on. Include context and specific questions. Example: "Should we use microservices or monolith architecture for our e-commerce platform with 100k users?"',
|
|
1810
|
-
},
|
|
1811
|
-
},
|
|
1812
|
-
required: ['prompt', 'models'],
|
|
1813
|
-
};
|