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/checkStatus.js
CHANGED
|
@@ -245,6 +245,7 @@ async function listAllJobs(asyncJobStore, fileCache, options = {}) {
|
|
|
245
245
|
jobId: cachedJob.jobId,
|
|
246
246
|
status: cachedJob.status || 'completed',
|
|
247
247
|
tool: cachedJob.tool || 'unknown',
|
|
248
|
+
mode: cachedJob.mode || cachedJob.tool,
|
|
248
249
|
createdAt: cachedJob.createdAt || cachedJob.startedAt,
|
|
249
250
|
updatedAt:
|
|
250
251
|
cachedJob.completedAt || cachedJob.updatedAt || Date.now(),
|
package/src/tools/index.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
// Import individual tools
|
|
9
9
|
import { chatTool } from './chat.js';
|
|
10
|
-
import { consensusTool } from './consensus.js';
|
|
11
|
-
import { conversationTool } from './conversation.js';
|
|
12
10
|
import { checkStatusTool } from './checkStatus.js';
|
|
13
11
|
import { cancelJobTool } from './cancelJob.js';
|
|
14
12
|
|
|
@@ -19,8 +17,6 @@ import { cancelJobTool } from './cancelJob.js';
|
|
|
19
17
|
*/
|
|
20
18
|
const tools = {
|
|
21
19
|
chat: chatTool,
|
|
22
|
-
consensus: consensusTool,
|
|
23
|
-
conversation: conversationTool,
|
|
24
20
|
check_status: checkStatusTool,
|
|
25
21
|
cancel_job: cancelJobTool,
|
|
26
22
|
};
|
|
@@ -47,8 +43,8 @@ export function getTools(config = null) {
|
|
|
47
43
|
// Clone the tool to avoid mutating the original
|
|
48
44
|
const clonedTool = tool;
|
|
49
45
|
|
|
50
|
-
// For chat
|
|
51
|
-
if (name === 'chat'
|
|
46
|
+
// For the chat tool, remove the 'async' parameter from its schema
|
|
47
|
+
if (name === 'chat') {
|
|
52
48
|
// Create a modified inputSchema without the async parameter
|
|
53
49
|
const modifiedSchema = {
|
|
54
50
|
...tool.inputSchema,
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Mode Engine
|
|
3
|
+
*
|
|
4
|
+
* Serves the `chat` and `consensus` modes. Both fan out a set of call plans and
|
|
5
|
+
* invoke providers concurrently; consensus additionally runs a cross-feedback
|
|
6
|
+
* refinement phase. This is an execution core: it resolves candidates, invokes,
|
|
7
|
+
* streams per-call progress, and RETURNS invocation data (per-call responses and
|
|
8
|
+
* failures, selected candidate, metadata/thread IDs). Persistence, export, and
|
|
9
|
+
* MCP-response construction live in the unified chat tool's shared shell.
|
|
10
|
+
*
|
|
11
|
+
* A call plan is `{ modelSpec, displayModel, threadKey, candidates: [{ name,
|
|
12
|
+
* providerInstance, resolvedModel }, ...] }`. In `chat` mode the "auto" spec
|
|
13
|
+
* yields one call plan with the full provider-priority candidate list and the
|
|
14
|
+
* engine fails over across candidates in order; an explicit model yields one
|
|
15
|
+
* candidate (no failover). In `consensus` mode every call plan has a single
|
|
16
|
+
* candidate and there is no failover.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createLogger } from '../../utils/logger.js';
|
|
20
|
+
import {
|
|
21
|
+
isRecoverableError,
|
|
22
|
+
retryWithBackoff,
|
|
23
|
+
} from '../../utils/errorHandler.js';
|
|
24
|
+
import { acquireProviderStream } from './streamShared.js';
|
|
25
|
+
|
|
26
|
+
const logger = createLogger('parallel');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decide whether a provider error should advance auto-mode to the next candidate.
|
|
30
|
+
* @param {Error} error
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function shouldFailoverToNextProvider(error) {
|
|
34
|
+
if (isRecoverableError(error)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const message = (error && error.message) || '';
|
|
39
|
+
return /(api key|authentication|unauthorized|forbidden|invalid|not available)/i.test(
|
|
40
|
+
message,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-provider retry options, matching the chat tool's historical behavior.
|
|
46
|
+
* @param {object} config
|
|
47
|
+
* @param {string} providerName
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
export function getProviderRetryOptions(config, providerName) {
|
|
51
|
+
const nodeEnv = config?.environment?.nodeEnv || process.env.NODE_ENV;
|
|
52
|
+
const isTest = nodeEnv === 'test';
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
retries: isTest ? 1 : 3,
|
|
56
|
+
delay: isTest ? 0 : 500,
|
|
57
|
+
maxDelay: isTest ? 0 : 10000,
|
|
58
|
+
operation: `provider-invoke:${providerName}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The single cross-feedback refinement prompt template. This is now the only
|
|
64
|
+
* template — there is no per-call override.
|
|
65
|
+
* @param {string} prompt - Original question
|
|
66
|
+
* @param {Array} successful - Successful phase-1 results
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function buildFeedbackPrompt(prompt, successful) {
|
|
70
|
+
return `Based on the other AI responses below, please refine your answer to the original question. Consider different perspectives and provide your final response:
|
|
71
|
+
|
|
72
|
+
Original Question: ${prompt}
|
|
73
|
+
|
|
74
|
+
Other AI Responses:
|
|
75
|
+
${successful.map((r, i) => `${i + 1}. ${r.model}: ${r.response}`).join('\n\n')}
|
|
76
|
+
|
|
77
|
+
Please provide your refined response:`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Join every call's accumulated streaming content for the unified
|
|
82
|
+
* `accumulated_content` field shown by check_status.
|
|
83
|
+
* @param {object} contents - Map of index -> accumulated text
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function combineContents(contents) {
|
|
87
|
+
return Object.values(contents)
|
|
88
|
+
.filter((content) => content && content.length > 0)
|
|
89
|
+
.join('\n\n---\n\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Invoke a single candidate, streaming through the normalizer when a job context
|
|
94
|
+
* is present (updating flat `provider_${index}_*` progress keys) or performing a
|
|
95
|
+
* plain invoke otherwise.
|
|
96
|
+
* @returns {Promise<object>} Provider response { content, metadata }
|
|
97
|
+
*/
|
|
98
|
+
async function invokeCandidate({
|
|
99
|
+
candidate,
|
|
100
|
+
messages,
|
|
101
|
+
options,
|
|
102
|
+
index,
|
|
103
|
+
phaseLabel,
|
|
104
|
+
context,
|
|
105
|
+
streamNormalizer,
|
|
106
|
+
providerContents,
|
|
107
|
+
}) {
|
|
108
|
+
// Reset this call's partial content at the start of every attempt so a retried
|
|
109
|
+
// or failed-over attempt never leaves stale streamed text behind.
|
|
110
|
+
providerContents[index] = '';
|
|
111
|
+
|
|
112
|
+
if (!context) {
|
|
113
|
+
const response = await candidate.providerInstance.invoke(messages, options);
|
|
114
|
+
if (response?.content) {
|
|
115
|
+
providerContents[index] = response.content;
|
|
116
|
+
}
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await context.updateJob({
|
|
121
|
+
[`provider_${index}_status`]: 'prompting',
|
|
122
|
+
[`provider_${index}_model`]: candidate.displayModel,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { stream, response: acquiredResponse } = await acquireProviderStream(
|
|
126
|
+
candidate.providerInstance,
|
|
127
|
+
messages,
|
|
128
|
+
options,
|
|
129
|
+
);
|
|
130
|
+
let response = acquiredResponse;
|
|
131
|
+
|
|
132
|
+
if (stream) {
|
|
133
|
+
const normalizedStream = streamNormalizer.normalize(candidate.name, stream, {
|
|
134
|
+
provider: candidate.name,
|
|
135
|
+
model: options.model,
|
|
136
|
+
requestId: `${context.jobId}-${phaseLabel}-${index}`,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let accumulatedContent = '';
|
|
140
|
+
let finalUsage = null;
|
|
141
|
+
let finalMetadata = {};
|
|
142
|
+
|
|
143
|
+
await context.updateJob({ [`provider_${index}_status`]: 'streaming' });
|
|
144
|
+
|
|
145
|
+
for await (const event of normalizedStream) {
|
|
146
|
+
if (context.signal?.aborted) {
|
|
147
|
+
throw new Error('Execution was cancelled');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
switch (event.type) {
|
|
151
|
+
case 'delta':
|
|
152
|
+
accumulatedContent += event.data.textDelta;
|
|
153
|
+
providerContents[index] = accumulatedContent;
|
|
154
|
+
await context.updateJob({
|
|
155
|
+
[`provider_${index}_preview`]:
|
|
156
|
+
accumulatedContent.length > 150
|
|
157
|
+
? accumulatedContent.substring(0, 150) + '...'
|
|
158
|
+
: accumulatedContent,
|
|
159
|
+
accumulated_content: combineContents(providerContents),
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
case 'usage':
|
|
163
|
+
finalUsage = event.data.usage;
|
|
164
|
+
break;
|
|
165
|
+
case 'end':
|
|
166
|
+
accumulatedContent = event.data.content || accumulatedContent;
|
|
167
|
+
finalUsage = event.data.usage || finalUsage;
|
|
168
|
+
finalMetadata = event.data.metadata || finalMetadata;
|
|
169
|
+
break;
|
|
170
|
+
case 'error':
|
|
171
|
+
throw new Error(`Streaming error: ${event.data.error.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
response = {
|
|
176
|
+
content: accumulatedContent,
|
|
177
|
+
metadata: { ...finalMetadata, usage: finalUsage, streaming: true },
|
|
178
|
+
};
|
|
179
|
+
providerContents[index] = accumulatedContent;
|
|
180
|
+
} else {
|
|
181
|
+
if (!response) {
|
|
182
|
+
response = await candidate.providerInstance.invoke(messages, options);
|
|
183
|
+
}
|
|
184
|
+
if (response?.content) {
|
|
185
|
+
providerContents[index] = response.content;
|
|
186
|
+
await context.updateJob({
|
|
187
|
+
accumulated_content: combineContents(providerContents),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return response;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run one fan-out phase over the given call plans.
|
|
197
|
+
* @param {object} params
|
|
198
|
+
* @returns {Promise<Array>} Per-call-plan results
|
|
199
|
+
*/
|
|
200
|
+
async function runPhase({
|
|
201
|
+
callPlans,
|
|
202
|
+
buildMessagesForCandidate,
|
|
203
|
+
optionsForCandidate,
|
|
204
|
+
activeSignal,
|
|
205
|
+
context,
|
|
206
|
+
streamNormalizer,
|
|
207
|
+
progressKey,
|
|
208
|
+
phaseWord,
|
|
209
|
+
phaseLabel,
|
|
210
|
+
retryOptionsFor,
|
|
211
|
+
}) {
|
|
212
|
+
const providerContents = {};
|
|
213
|
+
const totalCount = callPlans.length;
|
|
214
|
+
let completedCount = 0;
|
|
215
|
+
|
|
216
|
+
if (context) {
|
|
217
|
+
await context.updateJob({
|
|
218
|
+
[progressKey]: `0/${totalCount}${phaseWord ? ` ${phaseWord}` : ''}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const settled = await Promise.allSettled(
|
|
223
|
+
callPlans.map(async (callPlan, index) => {
|
|
224
|
+
let lastError = null;
|
|
225
|
+
|
|
226
|
+
for (let ci = 0; ci < callPlan.candidates.length; ci++) {
|
|
227
|
+
const candidate = callPlan.candidates[ci];
|
|
228
|
+
|
|
229
|
+
// Never fail over (or start a new candidate) once aborted.
|
|
230
|
+
if (activeSignal?.aborted) {
|
|
231
|
+
throw new Error('Execution was cancelled');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const messages = buildMessagesForCandidate(candidate, callPlan);
|
|
235
|
+
const options = {
|
|
236
|
+
...optionsForCandidate(candidate, callPlan),
|
|
237
|
+
signal: activeSignal,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const attempt = () =>
|
|
242
|
+
invokeCandidate({
|
|
243
|
+
candidate,
|
|
244
|
+
messages,
|
|
245
|
+
options,
|
|
246
|
+
index,
|
|
247
|
+
phaseLabel,
|
|
248
|
+
context,
|
|
249
|
+
streamNormalizer,
|
|
250
|
+
providerContents,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const retryOpts = retryOptionsFor
|
|
254
|
+
? retryOptionsFor(candidate.name)
|
|
255
|
+
: null;
|
|
256
|
+
const response = retryOpts
|
|
257
|
+
? await retryWithBackoff(attempt, retryOpts)
|
|
258
|
+
: await attempt();
|
|
259
|
+
|
|
260
|
+
if (!response || !response.content) {
|
|
261
|
+
throw new Error('Provider returned invalid response');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
completedCount++;
|
|
265
|
+
if (context) {
|
|
266
|
+
await context.updateJob({
|
|
267
|
+
[progressKey]: `${completedCount}/${totalCount}${phaseWord ? ` ${phaseWord}` : ''}`,
|
|
268
|
+
[`provider_${index}_status`]: 'finished',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
modelSpec: callPlan.modelSpec,
|
|
274
|
+
model: callPlan.displayModel,
|
|
275
|
+
provider: candidate.name,
|
|
276
|
+
providerInstance: candidate.providerInstance,
|
|
277
|
+
resolvedModel: candidate.resolvedModel,
|
|
278
|
+
threadKey: callPlan.threadKey,
|
|
279
|
+
status: 'success',
|
|
280
|
+
response: response.content,
|
|
281
|
+
metadata: response.metadata || {},
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
lastError = error;
|
|
285
|
+
|
|
286
|
+
// Cancellation aborts the whole phase — never demote to failed or fail over.
|
|
287
|
+
if (activeSignal?.aborted || error.name === 'AbortError') {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const isLastCandidate = ci === callPlan.candidates.length - 1;
|
|
292
|
+
const terminal =
|
|
293
|
+
isLastCandidate || !shouldFailoverToNextProvider(error);
|
|
294
|
+
|
|
295
|
+
// Partial text streamed before the error. On a terminal failure we
|
|
296
|
+
// RETAIN it (design: partial text received before an in-band stream
|
|
297
|
+
// error is kept, with the operation ultimately marked failed rather
|
|
298
|
+
// than successfully completed); on failover we clear it so the next
|
|
299
|
+
// candidate starts clean — it may later overwrite the preview as it
|
|
300
|
+
// streams.
|
|
301
|
+
const partial = providerContents[index] || '';
|
|
302
|
+
|
|
303
|
+
if (context) {
|
|
304
|
+
if (terminal) {
|
|
305
|
+
await context.updateJob({
|
|
306
|
+
[`provider_${index}_status`]: 'failed',
|
|
307
|
+
[`provider_${index}_error`]: error.message,
|
|
308
|
+
accumulated_content: combineContents(providerContents),
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
providerContents[index] = '';
|
|
312
|
+
await context.updateJob({
|
|
313
|
+
[`provider_${index}_status`]: 'failed',
|
|
314
|
+
[`provider_${index}_error`]: error.message,
|
|
315
|
+
[`provider_${index}_preview`]: null,
|
|
316
|
+
accumulated_content: combineContents(providerContents),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (terminal) {
|
|
322
|
+
const failure = {
|
|
323
|
+
modelSpec: callPlan.modelSpec,
|
|
324
|
+
model: callPlan.displayModel,
|
|
325
|
+
provider: candidate.name,
|
|
326
|
+
resolvedModel: candidate.resolvedModel,
|
|
327
|
+
threadKey: callPlan.threadKey,
|
|
328
|
+
status: 'failed',
|
|
329
|
+
error: error.message,
|
|
330
|
+
metadata: {},
|
|
331
|
+
};
|
|
332
|
+
if (partial) {
|
|
333
|
+
failure.partial_content = partial.slice(0, 2000);
|
|
334
|
+
}
|
|
335
|
+
return failure;
|
|
336
|
+
}
|
|
337
|
+
// otherwise continue to the next candidate
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
modelSpec: callPlan.modelSpec,
|
|
343
|
+
model: callPlan.displayModel,
|
|
344
|
+
threadKey: callPlan.threadKey,
|
|
345
|
+
status: 'failed',
|
|
346
|
+
error: (lastError && lastError.message) || 'Unknown error',
|
|
347
|
+
metadata: {},
|
|
348
|
+
};
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return settled.map((result, index) => {
|
|
353
|
+
if (result.status === 'fulfilled') {
|
|
354
|
+
return result.value;
|
|
355
|
+
}
|
|
356
|
+
// A rejected settle means cancellation propagated; re-throw to the caller so
|
|
357
|
+
// the shell/job runner treats the whole operation as cancelled.
|
|
358
|
+
throw result.reason instanceof Error
|
|
359
|
+
? result.reason
|
|
360
|
+
: new Error(callPlans[index] ? 'Execution was cancelled' : 'Execution failed');
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Run `chat` mode: a single fan-out phase with per-call candidate failover.
|
|
366
|
+
* @param {object} params
|
|
367
|
+
* @returns {Promise<{results: Array}>}
|
|
368
|
+
*/
|
|
369
|
+
export async function runChatMode({
|
|
370
|
+
callPlans,
|
|
371
|
+
buildMessagesForCandidate,
|
|
372
|
+
optionsForCandidate,
|
|
373
|
+
signal,
|
|
374
|
+
context = null,
|
|
375
|
+
providerStreamNormalizer,
|
|
376
|
+
retryOptionsFor,
|
|
377
|
+
}) {
|
|
378
|
+
const activeSignal = context ? context.signal : signal;
|
|
379
|
+
|
|
380
|
+
const results = await runPhase({
|
|
381
|
+
callPlans,
|
|
382
|
+
buildMessagesForCandidate,
|
|
383
|
+
optionsForCandidate,
|
|
384
|
+
activeSignal,
|
|
385
|
+
context,
|
|
386
|
+
streamNormalizer: providerStreamNormalizer,
|
|
387
|
+
progressKey: 'chat_progress',
|
|
388
|
+
phaseWord: '',
|
|
389
|
+
phaseLabel: 'chat',
|
|
390
|
+
retryOptionsFor,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return { results };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Run `consensus` mode: an initial fan-out phase, then (when ≥2 phase-1 responses
|
|
398
|
+
* succeeded and the request was not aborted) a cross-feedback refinement phase.
|
|
399
|
+
* @param {object} params
|
|
400
|
+
* @returns {Promise<{initial: Array, refined: Array|null}>}
|
|
401
|
+
*/
|
|
402
|
+
export async function runConsensusMode({
|
|
403
|
+
callPlans,
|
|
404
|
+
buildMessagesForCandidate,
|
|
405
|
+
optionsForCandidate,
|
|
406
|
+
prompt,
|
|
407
|
+
signal,
|
|
408
|
+
context = null,
|
|
409
|
+
providerStreamNormalizer,
|
|
410
|
+
}) {
|
|
411
|
+
const activeSignal = context ? context.signal : signal;
|
|
412
|
+
|
|
413
|
+
const initial = await runPhase({
|
|
414
|
+
callPlans,
|
|
415
|
+
buildMessagesForCandidate,
|
|
416
|
+
optionsForCandidate,
|
|
417
|
+
activeSignal,
|
|
418
|
+
context,
|
|
419
|
+
streamNormalizer: providerStreamNormalizer,
|
|
420
|
+
progressKey: 'consensus_progress',
|
|
421
|
+
phaseWord: 'initial',
|
|
422
|
+
phaseLabel: 'initial',
|
|
423
|
+
retryOptionsFor: null,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const successful = initial.filter((r) => r.status === 'success');
|
|
427
|
+
|
|
428
|
+
let refined = null;
|
|
429
|
+
|
|
430
|
+
// Refinement runs whenever ≥2 phase-1 responses succeeded (no gating flag).
|
|
431
|
+
if (successful.length > 1 && !activeSignal?.aborted) {
|
|
432
|
+
logger.debug('Running cross-feedback refinement phase', {
|
|
433
|
+
data: { responseCount: successful.length },
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const feedbackPrompt = buildFeedbackPrompt(prompt, successful);
|
|
437
|
+
|
|
438
|
+
const refineCallPlans = successful.map((r) => ({
|
|
439
|
+
modelSpec: r.modelSpec,
|
|
440
|
+
displayModel: r.model,
|
|
441
|
+
threadKey: r.threadKey,
|
|
442
|
+
initialResponse: r.response,
|
|
443
|
+
candidates: [
|
|
444
|
+
{
|
|
445
|
+
name: r.provider,
|
|
446
|
+
providerInstance: r.providerInstance,
|
|
447
|
+
resolvedModel: r.resolvedModel,
|
|
448
|
+
displayModel: r.model,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
}));
|
|
452
|
+
|
|
453
|
+
const refinedResults = await runPhase({
|
|
454
|
+
callPlans: refineCallPlans,
|
|
455
|
+
buildMessagesForCandidate: (candidate, callPlan) => [
|
|
456
|
+
...buildMessagesForCandidate(candidate, callPlan),
|
|
457
|
+
{ role: 'assistant', content: callPlan.initialResponse },
|
|
458
|
+
{ role: 'user', content: feedbackPrompt },
|
|
459
|
+
],
|
|
460
|
+
optionsForCandidate,
|
|
461
|
+
activeSignal,
|
|
462
|
+
context,
|
|
463
|
+
streamNormalizer: providerStreamNormalizer,
|
|
464
|
+
progressKey: 'consensus_progress',
|
|
465
|
+
phaseWord: 'refined',
|
|
466
|
+
phaseLabel: 'refinement',
|
|
467
|
+
retryOptionsFor: null,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Map refinement outcomes back onto the phase-1 successes.
|
|
471
|
+
refined = refinedResults.map((result) => {
|
|
472
|
+
const initialResult = successful.find(
|
|
473
|
+
(s) => s.modelSpec === result.modelSpec,
|
|
474
|
+
);
|
|
475
|
+
return {
|
|
476
|
+
model: result.model,
|
|
477
|
+
provider: result.provider,
|
|
478
|
+
initial_response: initialResult ? initialResult.response : null,
|
|
479
|
+
refined_response: result.status === 'success' ? result.response : null,
|
|
480
|
+
refined_metadata: result.status === 'success' ? result.metadata : {},
|
|
481
|
+
refined_error: result.status === 'failed' ? result.error : null,
|
|
482
|
+
status: result.status === 'success' ? 'success' : 'partial',
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return { initial, refined };
|
|
488
|
+
}
|