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
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roundtable Mode Engine
|
|
3
|
+
*
|
|
4
|
+
* Turn-based multi-model round-table. Models respond SEQUENTIALLY in the order
|
|
5
|
+
* given; each model sees the full running transcript (prior laps + earlier turns
|
|
6
|
+
* in the current lap) and builds on it. One tool call runs exactly one lap (one
|
|
7
|
+
* turn per model); the caller drives more laps by passing back the continuation_id.
|
|
8
|
+
*
|
|
9
|
+
* This is the sequential counterpart to the parallel engine. It is an execution
|
|
10
|
+
* core: it resolves the turn plan, runs the lap loop, and RETURNS the lap data
|
|
11
|
+
* (turns, transcript, counts). Persistence, export, and MCP-response construction
|
|
12
|
+
* live in the unified chat tool's shared shell.
|
|
13
|
+
*
|
|
14
|
+
* CRITICAL provider constraint: SDK providers (codex, claude, copilot) reduce the
|
|
15
|
+
* message array to ONLY the last `user` message. Therefore each turn's entire
|
|
16
|
+
* context (prior-lap transcript + lap prompt + same-lap turns + framing) is packed
|
|
17
|
+
* into a SINGLE self-contained final user message ("turn packet"). Do not spread
|
|
18
|
+
* turn context across multiple messages.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { debugLog } from '../../utils/console.js';
|
|
22
|
+
import { acquireProviderStream } from './streamShared.js';
|
|
23
|
+
import {
|
|
24
|
+
getDefaultModelForProvider,
|
|
25
|
+
getProviderUnavailableMessage,
|
|
26
|
+
getAvailableProviders,
|
|
27
|
+
resolveModelSpec,
|
|
28
|
+
} from '../../utils/modelRouting.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a stored transcript (from prior laps or a prior chat/consensus thread)
|
|
32
|
+
* into labeled text that can be embedded in the next turn's packet. Stored state
|
|
33
|
+
* pairs user (prompt) and assistant (response) messages; we re-render those as
|
|
34
|
+
* readable context so last-user-only SDK providers still see the history (and so
|
|
35
|
+
* a provider does not mistake prior multi-speaker transcript for its own previous
|
|
36
|
+
* output). Also used by the parallel engine's shell to pack prior history into a
|
|
37
|
+
* resumed chat/consensus turn.
|
|
38
|
+
* @param {Array} storedMessages - Stored messages from a prior conversation state
|
|
39
|
+
* @returns {string} Labeled prior-transcript text ('' for a new conversation)
|
|
40
|
+
*/
|
|
41
|
+
export function renderStoredTranscriptToText(storedMessages = []) {
|
|
42
|
+
if (!Array.isArray(storedMessages) || storedMessages.length === 0) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const blocks = [];
|
|
47
|
+
let lapNumber = 0;
|
|
48
|
+
let pendingPrompt = null;
|
|
49
|
+
|
|
50
|
+
const toText = (content) => {
|
|
51
|
+
if (typeof content === 'string') {
|
|
52
|
+
return content;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(content)) {
|
|
55
|
+
// Complex content array (files/images + text) — extract text parts only
|
|
56
|
+
return content
|
|
57
|
+
.filter((part) => part && part.type === 'text' && part.text)
|
|
58
|
+
.map((part) => part.text)
|
|
59
|
+
.join('\n');
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const message of storedMessages) {
|
|
65
|
+
if (!message || message.role === 'system') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (message.role === 'user') {
|
|
69
|
+
pendingPrompt = toText(message.content);
|
|
70
|
+
} else if (message.role === 'assistant') {
|
|
71
|
+
lapNumber += 1;
|
|
72
|
+
const promptText = pendingPrompt ? `${pendingPrompt}\n\n` : '';
|
|
73
|
+
const assistantText = toText(message.content);
|
|
74
|
+
blocks.push(
|
|
75
|
+
`## Earlier in this round-table (lap ${lapNumber}):\n${promptText}${assistantText}`,
|
|
76
|
+
);
|
|
77
|
+
pendingPrompt = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return blocks.join('\n\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the per-turn framing text for the model at position `i`.
|
|
86
|
+
* @param {object} params
|
|
87
|
+
* @returns {string} Framing text appended to the turn packet
|
|
88
|
+
*/
|
|
89
|
+
function buildFramingText({ i, models }) {
|
|
90
|
+
const total = models.length;
|
|
91
|
+
const selfModel = models[i];
|
|
92
|
+
const prevModel = i > 0 ? models[i - 1] : null;
|
|
93
|
+
const nextModel = i < total - 1 ? models[i + 1] : null;
|
|
94
|
+
|
|
95
|
+
const order = models.join(', ');
|
|
96
|
+
const prevText = prevModel || 'no one (you open the round)';
|
|
97
|
+
const nextText = nextModel || 'no one (you close this round)';
|
|
98
|
+
const handoffText = nextModel
|
|
99
|
+
? `Your response will be passed to the next participant (${nextModel}).`
|
|
100
|
+
: 'Your response will be returned to the user, as you are the last participant this round.';
|
|
101
|
+
|
|
102
|
+
const lines = [
|
|
103
|
+
`You are participant "${selfModel}" in a multi-model round-table conversation.`,
|
|
104
|
+
`Participants, in speaking order: ${order}.`,
|
|
105
|
+
`You are speaking in position ${i + 1} of ${total}, after ${prevText}, before ${nextText}.`,
|
|
106
|
+
'The original topic/prompt for this round is shown above, followed by any responses already given this round.',
|
|
107
|
+
'Respond to the whole conversation so far — build on, challenge, or refine what others have said; do not merely repeat them.',
|
|
108
|
+
handoffText,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the single self-contained turn packet TEXT for the model at position `i`.
|
|
116
|
+
* Order: prior-transcript section, lap prompt, same-lap turns, framing.
|
|
117
|
+
* This is the LAST user message — the only thing last-user-only SDK providers see.
|
|
118
|
+
* @param {object} params
|
|
119
|
+
* @returns {string} Turn packet text
|
|
120
|
+
*/
|
|
121
|
+
function buildTurnPacket({ priorTranscriptText, prompt, sameLapTurns, i, models }) {
|
|
122
|
+
const parts = [];
|
|
123
|
+
|
|
124
|
+
if (priorTranscriptText && priorTranscriptText.trim()) {
|
|
125
|
+
parts.push(priorTranscriptText.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
parts.push(`Original topic for this round:\n${prompt}`);
|
|
129
|
+
|
|
130
|
+
// Same-lap turns from models 0..i-1 (omitted for the opener, i=0)
|
|
131
|
+
if (i > 0 && sameLapTurns.length > 0) {
|
|
132
|
+
const turnBlocks = sameLapTurns.map((turn) => {
|
|
133
|
+
if (turn.status === 'success') {
|
|
134
|
+
return `### ${turn.model} said:\n${turn.response}`;
|
|
135
|
+
}
|
|
136
|
+
return `### ${turn.model} did not respond (error: ${turn.error})`;
|
|
137
|
+
});
|
|
138
|
+
parts.push(turnBlocks.join('\n\n'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
parts.push(buildFramingText({ i, models }));
|
|
142
|
+
|
|
143
|
+
return parts.join('\n\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format the full lap transcript for storage/display.
|
|
148
|
+
* @param {Array} lapTurns - Turns from the current lap
|
|
149
|
+
* @returns {string} Formatted transcript
|
|
150
|
+
*/
|
|
151
|
+
export function formatLapTranscript(lapTurns) {
|
|
152
|
+
let content = '';
|
|
153
|
+
let successful = 0;
|
|
154
|
+
|
|
155
|
+
lapTurns.forEach((turn, index) => {
|
|
156
|
+
if (turn.status === 'success') {
|
|
157
|
+
successful += 1;
|
|
158
|
+
content += `### ${turn.model} (turn ${index + 1}):\n${turn.response}\n\n---\n\n`;
|
|
159
|
+
} else {
|
|
160
|
+
content += `### ${turn.model} (turn ${index + 1}, did not respond):\nError: ${turn.error}\n\n---\n\n`;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
content += `\n**Summary:** Conversation lap completed with ${successful}/${lapTurns.length} successful turns.`;
|
|
165
|
+
return content;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the ordered model list into a turn plan. Unlike the parallel engine,
|
|
170
|
+
* unknown or unavailable models are NOT dropped — they are recorded with a
|
|
171
|
+
* preFailReason so they keep their position in the order (and produce a failed
|
|
172
|
+
* turn).
|
|
173
|
+
* @param {Array<string>} models - Ordered model list
|
|
174
|
+
* @param {object} providers - Provider instances
|
|
175
|
+
* @param {object} config - Configuration
|
|
176
|
+
* @param {boolean} hasImages - Whether the request includes images
|
|
177
|
+
* @returns {Array<object>} Ordered turn plan entries
|
|
178
|
+
*/
|
|
179
|
+
export function resolveTurnPlan(models, providers, config, hasImages = false) {
|
|
180
|
+
// Single "auto" expands to the first available provider's default model only
|
|
181
|
+
// (a single-model round-table is valid). Multiple explicit models resolve per-entry.
|
|
182
|
+
let modelsToProcess = models;
|
|
183
|
+
if (models.length === 1 && String(models[0]).toLowerCase() === 'auto') {
|
|
184
|
+
const [firstAvailable] = getAvailableProviders(providers, config, {
|
|
185
|
+
hasImages,
|
|
186
|
+
limit: 1,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// If a provider is available, use its default model. Otherwise keep "auto"
|
|
190
|
+
// so it resolves to a turn that fails cleanly (all-fail laps must complete).
|
|
191
|
+
modelsToProcess = firstAvailable
|
|
192
|
+
? [getDefaultModelForProvider(firstAvailable)]
|
|
193
|
+
: ['auto'];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return modelsToProcess.map((modelName) => {
|
|
197
|
+
if (!modelName || typeof modelName !== 'string') {
|
|
198
|
+
return {
|
|
199
|
+
model: modelName || 'unknown',
|
|
200
|
+
provider: null,
|
|
201
|
+
providerInstance: null,
|
|
202
|
+
resolvedModel: null,
|
|
203
|
+
preFailReason: 'Invalid model specification',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { providerName, provider, resolvedModel, status, options } =
|
|
208
|
+
resolveModelSpec(modelName, providers, config);
|
|
209
|
+
|
|
210
|
+
if (status === 'not_found') {
|
|
211
|
+
return {
|
|
212
|
+
model: modelName,
|
|
213
|
+
provider: providerName,
|
|
214
|
+
providerInstance: null,
|
|
215
|
+
resolvedModel,
|
|
216
|
+
preFailReason: `Provider not found: ${providerName}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (status === 'unavailable') {
|
|
221
|
+
return {
|
|
222
|
+
model: modelName,
|
|
223
|
+
provider: providerName,
|
|
224
|
+
providerInstance: null,
|
|
225
|
+
resolvedModel,
|
|
226
|
+
preFailReason: getProviderUnavailableMessage(providerName),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
model: modelName,
|
|
232
|
+
provider: providerName,
|
|
233
|
+
providerInstance: provider,
|
|
234
|
+
resolvedModel,
|
|
235
|
+
resolveOptions: options,
|
|
236
|
+
preFailReason: null,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build the final user message content for a turn. Files/images are attached to
|
|
243
|
+
* THIS message so multimodal providers see them, with the packet text appended.
|
|
244
|
+
* @returns {string|Array} User message content
|
|
245
|
+
*/
|
|
246
|
+
function buildTurnUserContent(packetText, contextMessage) {
|
|
247
|
+
if (contextMessage && contextMessage.content) {
|
|
248
|
+
return [...contextMessage.content, { type: 'text', text: packetText }];
|
|
249
|
+
}
|
|
250
|
+
return packetText;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Execute a single turn. Streams (updating job progress) when a job context is
|
|
255
|
+
* present; otherwise performs a plain invoke. Cancellation propagates by throwing
|
|
256
|
+
* so the lap aborts rather than demoting to a failed turn.
|
|
257
|
+
* @returns {Promise<object>} Turn result { model, provider, status, response|error }
|
|
258
|
+
*/
|
|
259
|
+
async function executeTurn(
|
|
260
|
+
plan,
|
|
261
|
+
messages,
|
|
262
|
+
options,
|
|
263
|
+
context,
|
|
264
|
+
streamNormalizer,
|
|
265
|
+
turnIndex,
|
|
266
|
+
) {
|
|
267
|
+
// `options.signal` is the active signal for both sync (request signal) and
|
|
268
|
+
// async (job context signal) paths.
|
|
269
|
+
const activeSignal = options.signal;
|
|
270
|
+
try {
|
|
271
|
+
if (activeSignal?.aborted) {
|
|
272
|
+
throw new Error('Roundtable execution was cancelled');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Synchronous path (no job context): plain invoke.
|
|
276
|
+
if (!context) {
|
|
277
|
+
const response = await plan.providerInstance.invoke(messages, options);
|
|
278
|
+
return {
|
|
279
|
+
model: plan.model,
|
|
280
|
+
provider: plan.provider,
|
|
281
|
+
status: 'success',
|
|
282
|
+
response: response.content,
|
|
283
|
+
metadata: response.metadata || {},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Async path: stream and surface progress.
|
|
288
|
+
const { stream, response: acquiredResponse } = await acquireProviderStream(
|
|
289
|
+
plan.providerInstance,
|
|
290
|
+
messages,
|
|
291
|
+
options,
|
|
292
|
+
);
|
|
293
|
+
let response = acquiredResponse;
|
|
294
|
+
|
|
295
|
+
if (stream) {
|
|
296
|
+
const normalizedStream = streamNormalizer.normalize(plan.provider, stream, {
|
|
297
|
+
provider: plan.provider,
|
|
298
|
+
model: options.model,
|
|
299
|
+
requestId: `${context.jobId}-turn-${turnIndex}`,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
let accumulatedContent = '';
|
|
303
|
+
let finalUsage = null;
|
|
304
|
+
let finalMetadata = {};
|
|
305
|
+
|
|
306
|
+
for await (const event of normalizedStream) {
|
|
307
|
+
if (context.signal?.aborted) {
|
|
308
|
+
throw new Error('Roundtable execution was cancelled');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
switch (event.type) {
|
|
312
|
+
case 'delta':
|
|
313
|
+
accumulatedContent += event.data.textDelta;
|
|
314
|
+
break;
|
|
315
|
+
case 'usage':
|
|
316
|
+
finalUsage = event.data.usage;
|
|
317
|
+
break;
|
|
318
|
+
case 'end':
|
|
319
|
+
accumulatedContent = event.data.content || accumulatedContent;
|
|
320
|
+
finalUsage = event.data.usage || finalUsage;
|
|
321
|
+
finalMetadata = event.data.metadata || finalMetadata;
|
|
322
|
+
break;
|
|
323
|
+
case 'error':
|
|
324
|
+
throw new Error(`Streaming error: ${event.data.error.message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
response = {
|
|
329
|
+
content: accumulatedContent,
|
|
330
|
+
metadata: { ...finalMetadata, usage: finalUsage, streaming: true },
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!stream && !response) {
|
|
335
|
+
response = await plan.providerInstance.invoke(messages, options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
model: plan.model,
|
|
340
|
+
provider: plan.provider,
|
|
341
|
+
status: 'success',
|
|
342
|
+
response: response.content,
|
|
343
|
+
metadata: response.metadata || {},
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Cancellation must abort the whole lap, not be demoted to a failed turn.
|
|
347
|
+
// Rethrow so it propagates to the caller (sync shell or job runner).
|
|
348
|
+
if (activeSignal?.aborted || error.name === 'AbortError') {
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
model: plan.model,
|
|
353
|
+
provider: plan.provider,
|
|
354
|
+
status: 'failed',
|
|
355
|
+
error: error.message,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Run one roundtable lap: one turn per model, in order, each seeing the full
|
|
362
|
+
* running transcript. Returns the lap data for the shell to persist/format.
|
|
363
|
+
*
|
|
364
|
+
* @param {object} params
|
|
365
|
+
* @param {Array<string>} params.models - Ordered model list
|
|
366
|
+
* @param {string} params.prompt - Lap prompt
|
|
367
|
+
* @param {Array} params.priorHistory - Loaded stored messages (may include a leading system msg)
|
|
368
|
+
* @param {object|null} params.contextMessage - Files/images context message
|
|
369
|
+
* @param {string} params.systemPrompt - System prompt for the roundtable mode
|
|
370
|
+
* @param {object} params.providers - Provider instances
|
|
371
|
+
* @param {object} params.config - Configuration
|
|
372
|
+
* @param {AbortSignal} [params.signal] - Sync-path abort signal
|
|
373
|
+
* @param {string} params.reasoning_effort - Reasoning depth
|
|
374
|
+
* @param {boolean} params.hasImages - Whether the request includes images
|
|
375
|
+
* @param {object|null} [params.context] - Job context (async) or null (sync)
|
|
376
|
+
* @param {object} [params.providerStreamNormalizer] - Stream normalizer (async)
|
|
377
|
+
* @returns {Promise<object>} { lapTurns, transcript, turnsSuccessful, turnsFailed, lapUserMessage }
|
|
378
|
+
*/
|
|
379
|
+
export async function runRoundtableLap({
|
|
380
|
+
models,
|
|
381
|
+
prompt,
|
|
382
|
+
priorHistory,
|
|
383
|
+
contextMessage,
|
|
384
|
+
systemPrompt,
|
|
385
|
+
providers,
|
|
386
|
+
config,
|
|
387
|
+
signal,
|
|
388
|
+
reasoning_effort,
|
|
389
|
+
hasImages,
|
|
390
|
+
context = null,
|
|
391
|
+
providerStreamNormalizer,
|
|
392
|
+
}) {
|
|
393
|
+
const priorTranscriptText = renderStoredTranscriptToText(priorHistory);
|
|
394
|
+
const turnPlan = resolveTurnPlan(models, providers, config, hasImages);
|
|
395
|
+
const activeSignal = context ? context.signal : signal;
|
|
396
|
+
|
|
397
|
+
if (context) {
|
|
398
|
+
await context.updateJob({
|
|
399
|
+
models_list: models.join(', '),
|
|
400
|
+
roundtable_progress: `0/${turnPlan.length}`,
|
|
401
|
+
total_turns: turnPlan.length,
|
|
402
|
+
completed_turns: 0,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const lapTurns = [];
|
|
407
|
+
|
|
408
|
+
for (let i = 0; i < turnPlan.length; i++) {
|
|
409
|
+
if (activeSignal?.aborted) {
|
|
410
|
+
throw new Error('Roundtable execution was cancelled');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const plan = turnPlan[i];
|
|
414
|
+
|
|
415
|
+
if (plan.preFailReason) {
|
|
416
|
+
lapTurns.push({
|
|
417
|
+
model: plan.model,
|
|
418
|
+
provider: plan.provider,
|
|
419
|
+
status: 'failed',
|
|
420
|
+
error: plan.preFailReason,
|
|
421
|
+
position: i,
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
const packetText = buildTurnPacket({
|
|
425
|
+
priorTranscriptText,
|
|
426
|
+
prompt,
|
|
427
|
+
sameLapTurns: lapTurns,
|
|
428
|
+
i,
|
|
429
|
+
models,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const finalUserContent = buildTurnUserContent(packetText, contextMessage);
|
|
433
|
+
|
|
434
|
+
const messages = [
|
|
435
|
+
{ role: 'system', content: systemPrompt },
|
|
436
|
+
{ role: 'user', content: finalUserContent },
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const turnResult = await executeTurn(
|
|
440
|
+
plan,
|
|
441
|
+
messages,
|
|
442
|
+
{
|
|
443
|
+
reasoning_effort,
|
|
444
|
+
signal: activeSignal,
|
|
445
|
+
config,
|
|
446
|
+
model: plan.resolvedModel,
|
|
447
|
+
// Web search opt-in from an OpenRouter `:online` decoration; only ever
|
|
448
|
+
// set for OpenRouter turns.
|
|
449
|
+
...(plan.resolveOptions?.web_search && { web_search: true }),
|
|
450
|
+
},
|
|
451
|
+
context,
|
|
452
|
+
providerStreamNormalizer,
|
|
453
|
+
i,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
lapTurns.push({ ...turnResult, position: i });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (context) {
|
|
460
|
+
// Report per-turn progress with the running transcript. Use flat keys (not
|
|
461
|
+
// a `progress` object) — asyncJobStore.update() treats the reserved
|
|
462
|
+
// `progress` key as a numeric 0..1 value. Numeric overall progress is
|
|
463
|
+
// supplied separately as a fraction.
|
|
464
|
+
await context.updateJob({
|
|
465
|
+
roundtable_progress: `${i + 1}/${turnPlan.length}`,
|
|
466
|
+
accumulated_content: formatLapTranscript(lapTurns),
|
|
467
|
+
progress: (i + 1) / turnPlan.length,
|
|
468
|
+
total_turns: turnPlan.length,
|
|
469
|
+
completed_turns: i + 1,
|
|
470
|
+
current_model: plan.model,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const turnsSuccessful = lapTurns.filter((t) => t.status === 'success').length;
|
|
476
|
+
const turnsFailed = lapTurns.length - turnsSuccessful;
|
|
477
|
+
const transcript = formatLapTranscript(lapTurns);
|
|
478
|
+
|
|
479
|
+
const lapUserMessage = {
|
|
480
|
+
role: 'user',
|
|
481
|
+
content: buildTurnUserContent(prompt, contextMessage),
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
debugLog(
|
|
485
|
+
`[Roundtable] Lap completed: ${turnsSuccessful}/${lapTurns.length} turns succeeded`,
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
lapTurns,
|
|
490
|
+
transcript,
|
|
491
|
+
turnsSuccessful,
|
|
492
|
+
turnsFailed,
|
|
493
|
+
lapUserMessage,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared streaming helper for the mode engines.
|
|
3
|
+
*
|
|
4
|
+
* Both the parallel and roundtable engines acquire a stream from a provider the
|
|
5
|
+
* same way: providers with a native `stream(messages, options)` method return an
|
|
6
|
+
* async iterator directly; SDK providers (copilot, codex, claude, gemini-cli)
|
|
7
|
+
* instead stream via `invoke(..., { stream: true })`, which may yield an async
|
|
8
|
+
* iterator or, when streaming is unavailable, a plain response object.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Acquire a stream (or a plain response) from a provider for the async path.
|
|
13
|
+
* @param {object} providerInstance - Provider implementation
|
|
14
|
+
* @param {Array} messages - Message array
|
|
15
|
+
* @param {object} options - Invocation options
|
|
16
|
+
* @returns {Promise<{ stream: (AsyncIterable|null), response: (object|undefined) }>}
|
|
17
|
+
*/
|
|
18
|
+
export async function acquireProviderStream(providerInstance, messages, options) {
|
|
19
|
+
if (providerInstance.stream && typeof providerInstance.stream === 'function') {
|
|
20
|
+
return { stream: providerInstance.stream(messages, options), response: undefined };
|
|
21
|
+
}
|
|
22
|
+
// SDK providers (copilot, codex, claude, gemini-cli) stream via invoke
|
|
23
|
+
const streamResult = await providerInstance.invoke(messages, {
|
|
24
|
+
...options,
|
|
25
|
+
stream: true,
|
|
26
|
+
});
|
|
27
|
+
if (streamResult && typeof streamResult[Symbol.asyncIterator] === 'function') {
|
|
28
|
+
return { stream: streamResult, response: undefined };
|
|
29
|
+
}
|
|
30
|
+
return { stream: null, response: streamResult };
|
|
31
|
+
}
|
|
@@ -265,35 +265,10 @@ export async function processMultipleFiles(filePaths, options = {}) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
|
-
*
|
|
269
|
-
* @param {string} query - Search query
|
|
270
|
-
* @param {object} options - Search options
|
|
271
|
-
* @returns {Promise<object>} Web search results for context
|
|
272
|
-
*/
|
|
273
|
-
export async function processWebSearchContext(query, options = {}) {
|
|
274
|
-
// Placeholder implementation - can be enhanced with actual web search API
|
|
275
|
-
return {
|
|
276
|
-
type: 'web_search',
|
|
277
|
-
query,
|
|
278
|
-
results: [],
|
|
279
|
-
error: null,
|
|
280
|
-
timestamp: new Date().toISOString(),
|
|
281
|
-
// Placeholder: Future implementation could integrate with:
|
|
282
|
-
// - Google Search API
|
|
283
|
-
// - Bing Search API
|
|
284
|
-
// - DuckDuckGo API
|
|
285
|
-
// - Custom search engines
|
|
286
|
-
placeholder: true,
|
|
287
|
-
message: 'Web search integration placeholder - not yet implemented',
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Unified context processor - handles all context types
|
|
268
|
+
* Unified context processor - handles files and images
|
|
293
269
|
* @param {object} contextRequest - Context processing request
|
|
294
270
|
* @param {string[]} contextRequest.files - Array of file paths
|
|
295
271
|
* @param {string[]} contextRequest.images - Array of image paths (for explicit image processing)
|
|
296
|
-
* @param {string} contextRequest.webSearch - Web search query
|
|
297
272
|
* @param {object} options - Processing options
|
|
298
273
|
* @returns {Promise<object>} Unified context result
|
|
299
274
|
*/
|
|
@@ -301,7 +276,6 @@ export async function processUnifiedContext(contextRequest, options = {}) {
|
|
|
301
276
|
const result = {
|
|
302
277
|
files: [],
|
|
303
278
|
images: [],
|
|
304
|
-
webSearch: null,
|
|
305
279
|
errors: [],
|
|
306
280
|
timestamp: new Date().toISOString(),
|
|
307
281
|
};
|
|
@@ -319,17 +293,6 @@ export async function processUnifiedContext(contextRequest, options = {}) {
|
|
|
319
293
|
imageProcessingMode: true, // Placeholder for future image-specific processing
|
|
320
294
|
});
|
|
321
295
|
}
|
|
322
|
-
|
|
323
|
-
// Process web search if provided
|
|
324
|
-
if (
|
|
325
|
-
contextRequest.webSearch &&
|
|
326
|
-
typeof contextRequest.webSearch === 'string'
|
|
327
|
-
) {
|
|
328
|
-
result.webSearch = await processWebSearchContext(
|
|
329
|
-
contextRequest.webSearch,
|
|
330
|
-
options,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
296
|
} catch (error) {
|
|
334
297
|
result.errors.push({
|
|
335
298
|
type: 'unified_processing_error',
|
|
@@ -143,9 +143,7 @@ function extractMessageContent(message) {
|
|
|
143
143
|
function generateMetadata(conversationState, totalTurns, params) {
|
|
144
144
|
const metadata = {
|
|
145
145
|
continuation_id: params.continuation_id,
|
|
146
|
-
|
|
147
|
-
provider: conversationState.provider,
|
|
148
|
-
temperature: params.temperature || 0.5,
|
|
146
|
+
mode: params.mode || conversationState.mode || 'chat',
|
|
149
147
|
total_turns: totalTurns,
|
|
150
148
|
created_at: conversationState.createdAt
|
|
151
149
|
? new Date(conversationState.createdAt).toISOString()
|
|
@@ -156,15 +154,12 @@ function generateMetadata(conversationState, totalTurns, params) {
|
|
|
156
154
|
};
|
|
157
155
|
|
|
158
156
|
// Add optional parameters if present
|
|
157
|
+
if (params.models && params.models.length > 0) {
|
|
158
|
+
metadata.models = params.models;
|
|
159
|
+
}
|
|
159
160
|
if (params.reasoning_effort) {
|
|
160
161
|
metadata.reasoning_effort = params.reasoning_effort;
|
|
161
162
|
}
|
|
162
|
-
if (params.verbosity) {
|
|
163
|
-
metadata.verbosity = params.verbosity;
|
|
164
|
-
}
|
|
165
|
-
if (params.use_websearch !== undefined) {
|
|
166
|
-
metadata.use_websearch = params.use_websearch;
|
|
167
|
-
}
|
|
168
163
|
if (params.files && params.files.length > 0) {
|
|
169
164
|
metadata.files = params.files;
|
|
170
165
|
}
|
|
@@ -174,13 +169,6 @@ function generateMetadata(conversationState, totalTurns, params) {
|
|
|
174
169
|
img.startsWith('data:') ? '[base64 image]' : img,
|
|
175
170
|
);
|
|
176
171
|
}
|
|
177
|
-
// Consensus-specific metadata
|
|
178
|
-
if (params.models) {
|
|
179
|
-
metadata.models = params.models;
|
|
180
|
-
}
|
|
181
|
-
if (params.enable_cross_feedback !== undefined) {
|
|
182
|
-
metadata.enable_cross_feedback = params.enable_cross_feedback;
|
|
183
|
-
}
|
|
184
172
|
|
|
185
173
|
return metadata;
|
|
186
174
|
}
|
|
@@ -195,15 +183,11 @@ export async function exportConversation(conversationState, options = {}) {
|
|
|
195
183
|
const {
|
|
196
184
|
clientCwd,
|
|
197
185
|
continuation_id,
|
|
198
|
-
|
|
199
|
-
temperature,
|
|
186
|
+
mode,
|
|
200
187
|
reasoning_effort,
|
|
201
|
-
verbosity,
|
|
202
|
-
use_websearch,
|
|
203
188
|
files,
|
|
204
189
|
images,
|
|
205
190
|
models,
|
|
206
|
-
enable_cross_feedback,
|
|
207
191
|
} = options;
|
|
208
192
|
|
|
209
193
|
if (!continuation_id) {
|
|
@@ -253,15 +237,11 @@ export async function exportConversation(conversationState, options = {}) {
|
|
|
253
237
|
// 5. Always update metadata atomically
|
|
254
238
|
const metadata = generateMetadata(conversationState, turns.length, {
|
|
255
239
|
continuation_id,
|
|
256
|
-
|
|
257
|
-
temperature,
|
|
240
|
+
mode,
|
|
258
241
|
reasoning_effort,
|
|
259
|
-
verbosity,
|
|
260
|
-
use_websearch,
|
|
261
242
|
files,
|
|
262
243
|
images,
|
|
263
244
|
models,
|
|
264
|
-
enable_cross_feedback,
|
|
265
245
|
});
|
|
266
246
|
|
|
267
247
|
const metadataPath = path.join(exportDir, 'metadata.json');
|