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.
Files changed (42) hide show
  1. package/.env.example +6 -3
  2. package/README.md +96 -94
  3. package/docs/API.md +703 -1562
  4. package/docs/ARCHITECTURE.md +13 -11
  5. package/docs/EXAMPLES.md +241 -667
  6. package/docs/PROVIDERS.md +104 -79
  7. package/package.json +1 -1
  8. package/src/async/asyncJobStore.js +2 -2
  9. package/src/async/jobRunner.js +6 -1
  10. package/src/async/providerStreamNormalizer.js +59 -1
  11. package/src/config.js +4 -3
  12. package/src/prompts/helpPrompt.js +43 -61
  13. package/src/providers/anthropic.js +0 -31
  14. package/src/providers/claude.js +0 -14
  15. package/src/providers/codex.js +15 -21
  16. package/src/providers/copilot.js +36 -202
  17. package/src/providers/deepseek.js +73 -53
  18. package/src/providers/gemini-cli.js +0 -3
  19. package/src/providers/google.js +10 -27
  20. package/src/providers/interface.js +0 -3
  21. package/src/providers/mistral.js +169 -63
  22. package/src/providers/openai-compatible.js +113 -28
  23. package/src/providers/openai.js +14 -66
  24. package/src/providers/openrouter-discovery.js +308 -0
  25. package/src/providers/openrouter.js +452 -282
  26. package/src/providers/xai.js +298 -280
  27. package/src/services/summarizationService.js +4 -14
  28. package/src/systemPrompts.js +19 -2
  29. package/src/tools/cancelJob.js +7 -1
  30. package/src/tools/chat.js +957 -995
  31. package/src/tools/checkStatus.js +1 -0
  32. package/src/tools/index.js +2 -6
  33. package/src/tools/modes/parallel.js +488 -0
  34. package/src/tools/modes/roundtable.js +495 -0
  35. package/src/tools/modes/streamShared.js +31 -0
  36. package/src/utils/contextProcessor.js +1 -38
  37. package/src/utils/conversationExporter.js +6 -26
  38. package/src/utils/formatStatus.js +46 -19
  39. package/src/utils/modelRouting.js +207 -4
  40. package/src/providers/openrouter-endpoints-client.js +0 -220
  41. package/src/tools/consensus.js +0 -1813
  42. 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
- * Web search context integration (placeholder)
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
- model: params.model || 'auto',
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
- model,
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
- model,
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');