@defai.digital/cli 13.3.0 → 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/bootstrap.d.ts +12 -0
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +23 -0
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +5 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/agent.d.ts +1 -0
  9. package/dist/commands/agent.d.ts.map +1 -1
  10. package/dist/commands/agent.js +211 -27
  11. package/dist/commands/agent.js.map +1 -1
  12. package/dist/commands/call.d.ts +2 -1
  13. package/dist/commands/call.d.ts.map +1 -1
  14. package/dist/commands/call.js +199 -10
  15. package/dist/commands/call.js.map +1 -1
  16. package/dist/commands/cleanup.d.ts +3 -1
  17. package/dist/commands/cleanup.d.ts.map +1 -1
  18. package/dist/commands/cleanup.js +123 -11
  19. package/dist/commands/cleanup.js.map +1 -1
  20. package/dist/commands/discuss.d.ts +2 -1
  21. package/dist/commands/discuss.d.ts.map +1 -1
  22. package/dist/commands/discuss.js +604 -105
  23. package/dist/commands/discuss.js.map +1 -1
  24. package/dist/commands/doctor.d.ts +11 -2
  25. package/dist/commands/doctor.d.ts.map +1 -1
  26. package/dist/commands/doctor.js +272 -38
  27. package/dist/commands/doctor.js.map +1 -1
  28. package/dist/commands/index.d.ts +1 -0
  29. package/dist/commands/index.d.ts.map +1 -1
  30. package/dist/commands/index.js +2 -0
  31. package/dist/commands/index.js.map +1 -1
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +136 -122
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/monitor.d.ts +22 -0
  36. package/dist/commands/monitor.d.ts.map +1 -0
  37. package/dist/commands/monitor.js +255 -0
  38. package/dist/commands/monitor.js.map +1 -0
  39. package/dist/commands/setup.d.ts.map +1 -1
  40. package/dist/commands/setup.js +107 -2
  41. package/dist/commands/setup.js.map +1 -1
  42. package/dist/commands/status.d.ts +6 -5
  43. package/dist/commands/status.d.ts.map +1 -1
  44. package/dist/commands/status.js +260 -75
  45. package/dist/commands/status.js.map +1 -1
  46. package/dist/parser.d.ts.map +1 -1
  47. package/dist/parser.js +30 -0
  48. package/dist/parser.js.map +1 -1
  49. package/dist/types.d.ts +4 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +1 -0
  52. package/dist/types.js.map +1 -1
  53. package/dist/utils/database.d.ts.map +1 -1
  54. package/dist/utils/database.js +5 -0
  55. package/dist/utils/database.js.map +1 -1
  56. package/dist/web/api.d.ts +12 -0
  57. package/dist/web/api.d.ts.map +1 -0
  58. package/dist/web/api.js +1189 -0
  59. package/dist/web/api.js.map +1 -0
  60. package/dist/web/dashboard.d.ts +13 -0
  61. package/dist/web/dashboard.d.ts.map +1 -0
  62. package/dist/web/dashboard.js +5290 -0
  63. package/dist/web/dashboard.js.map +1 -0
  64. package/package.json +21 -21
@@ -7,13 +7,15 @@
7
7
  *
8
8
  * Enables multiple AI models to discuss a topic from their unique perspectives,
9
9
  * building consensus through various patterns and mechanisms.
10
+ * Traces are emitted to SQLite for dashboard visibility.
10
11
  */
12
+ import { randomUUID } from 'node:crypto';
11
13
  // Bootstrap imports - composition root provides adapter access
12
- import { createProvider, PROVIDER_CONFIGS, } from '../bootstrap.js';
14
+ import { bootstrap, createProvider, getTraceStore, PROVIDER_CONFIGS, } from '../bootstrap.js';
13
15
  // Discussion domain imports
14
16
  import { DiscussionExecutor, RecursiveDiscussionExecutor, parseParticipantList, } from '@defai.digital/discussion-domain';
15
17
  // Contract types
16
- import { DEFAULT_PROVIDERS, DEFAULT_PROVIDER_TIMEOUT, DEFAULT_TOTAL_BUDGET_MS, DEFAULT_ROUNDS, DEFAULT_DISCUSSION_DEPTH, DEFAULT_MAX_TOTAL_CALLS, DEFAULT_CONFIDENCE_THRESHOLD, DEFAULT_AGENT_WEIGHT_MULTIPLIER, getErrorMessage, } from '@defai.digital/contracts';
18
+ import { DEFAULT_PROVIDER_TIMEOUT, DEFAULT_TOTAL_BUDGET_MS, DEFAULT_ROUNDS, DEFAULT_DISCUSSION_DEPTH, MAX_DISCUSSION_DEPTH, DEFAULT_MAX_TOTAL_CALLS, DEFAULT_CONFIDENCE_THRESHOLD, DEFAULT_AGENT_WEIGHT_MULTIPLIER, getErrorMessage, } from '@defai.digital/contracts';
17
19
  import { COLORS, ICONS } from '../utils/terminal.js';
18
20
  // Pattern display names
19
21
  const PATTERN_NAMES = {
@@ -32,6 +34,248 @@ const CONSENSUS_NAMES = {
32
34
  majority: 'Majority',
33
35
  };
34
36
  // ============================================================================
37
+ // Model Selection
38
+ // ============================================================================
39
+ /**
40
+ * Safely gets the default model for a provider
41
+ * Returns the provider's default model, or first available, or 'default' placeholder
42
+ */
43
+ function getModelForProviderConfig(config) {
44
+ if (!config?.models || config.models.length === 0) {
45
+ return 'default';
46
+ }
47
+ const defaultModel = config.models.find(m => m.isDefault);
48
+ if (defaultModel !== undefined) {
49
+ return defaultModel.modelId;
50
+ }
51
+ return config.models[0]?.modelId ?? 'default';
52
+ }
53
+ // ============================================================================
54
+ // Smart Provider Selection
55
+ // ============================================================================
56
+ /**
57
+ * Randomly selects N items from an array (Fisher-Yates shuffle)
58
+ */
59
+ function randomSelect(items, count) {
60
+ if (items.length <= count)
61
+ return [...items];
62
+ // Fisher-Yates shuffle
63
+ const shuffled = [...items];
64
+ for (let i = shuffled.length - 1; i > 0; i--) {
65
+ const j = Math.floor(Math.random() * (i + 1));
66
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
67
+ }
68
+ return shuffled.slice(0, count);
69
+ }
70
+ /**
71
+ * Smart provider selection:
72
+ * - If providers specified: use them (checking availability)
73
+ * - If not specified and ≥3 available: randomly pick 3
74
+ * - If not specified and exactly 2 available: use both
75
+ * - If not specified and exactly 1 available: skip discussion, use single provider
76
+ * - If not specified and 0 available: error
77
+ */
78
+ async function selectProvidersForCLI(specifiedProviders, providersExplicitlySpecified, providerBridge, verbose) {
79
+ // If providers were explicitly specified, check their availability
80
+ if (providersExplicitlySpecified && specifiedProviders.length > 0) {
81
+ const availableProviders = [];
82
+ for (const providerId of specifiedProviders) {
83
+ const isAvailable = await providerBridge.isAvailable(providerId);
84
+ if (isAvailable) {
85
+ availableProviders.push(providerId);
86
+ if (verbose) {
87
+ console.log(` ${ICONS.check} ${providerId}`);
88
+ }
89
+ }
90
+ else if (verbose) {
91
+ console.log(` ${ICONS.cross} ${providerId} ${COLORS.dim}(not available)${COLORS.reset}`);
92
+ }
93
+ }
94
+ if (availableProviders.length === 0) {
95
+ return {
96
+ providers: [],
97
+ skipDiscussion: false,
98
+ error: 'No specified providers are available. Run "ax doctor" to check provider status.',
99
+ };
100
+ }
101
+ if (availableProviders.length === 1) {
102
+ const singleProvider = availableProviders[0];
103
+ if (singleProvider === undefined) {
104
+ return {
105
+ providers: [],
106
+ skipDiscussion: false,
107
+ error: 'No specified providers are available.',
108
+ };
109
+ }
110
+ return {
111
+ providers: [],
112
+ skipDiscussion: true,
113
+ singleProvider,
114
+ };
115
+ }
116
+ return {
117
+ providers: availableProviders,
118
+ skipDiscussion: false,
119
+ };
120
+ }
121
+ // No providers specified - get all available and use smart selection
122
+ const allAvailable = await providerBridge.getAvailableProviders();
123
+ if (allAvailable.length === 0) {
124
+ return {
125
+ providers: [],
126
+ skipDiscussion: false,
127
+ error: 'No providers available. Run "ax doctor" to check provider status.',
128
+ };
129
+ }
130
+ if (allAvailable.length === 1) {
131
+ const singleProvider = allAvailable[0];
132
+ if (singleProvider === undefined) {
133
+ return {
134
+ providers: [],
135
+ skipDiscussion: false,
136
+ error: 'No providers available.',
137
+ };
138
+ }
139
+ if (verbose) {
140
+ console.log(` ${ICONS.bullet} Only ${singleProvider} is available - using direct mode`);
141
+ }
142
+ return {
143
+ providers: [],
144
+ skipDiscussion: true,
145
+ singleProvider,
146
+ };
147
+ }
148
+ if (allAvailable.length === 2) {
149
+ if (verbose) {
150
+ console.log(` ${ICONS.bullet} Using both available providers: ${allAvailable.join(', ')}`);
151
+ }
152
+ return {
153
+ providers: allAvailable,
154
+ skipDiscussion: false,
155
+ };
156
+ }
157
+ // 3 or more available - randomly pick 3
158
+ const selected = randomSelect(allAvailable, 3);
159
+ if (verbose) {
160
+ console.log(` ${ICONS.bullet} Randomly selected from ${allAvailable.length} available: ${selected.join(', ')}`);
161
+ }
162
+ return {
163
+ providers: selected,
164
+ skipDiscussion: false,
165
+ };
166
+ }
167
+ /**
168
+ * Call a single provider directly (when only 1 provider available)
169
+ */
170
+ async function callSingleProviderCLI(topic, providerId, providerBridge, timeout, options, traceStore, traceId, _startTime // Kept for API consistency, used in caller for trace start event
171
+ ) {
172
+ const callStartTime = Date.now();
173
+ if (!options.verbose) {
174
+ process.stdout.write(`${ICONS.discuss} Asking ${providerId} directly... `);
175
+ }
176
+ try {
177
+ const result = await providerBridge.execute({
178
+ providerId,
179
+ prompt: topic,
180
+ timeoutMs: timeout,
181
+ });
182
+ if (!options.verbose) {
183
+ console.log(result.success ? ICONS.check : ICONS.cross);
184
+ }
185
+ const durationMs = Date.now() - callStartTime;
186
+ // Emit trace event
187
+ await traceStore.write({
188
+ eventId: randomUUID(),
189
+ traceId,
190
+ type: 'discussion.end',
191
+ timestamp: new Date().toISOString(),
192
+ durationMs,
193
+ status: result.success ? 'success' : 'failure',
194
+ context: { providerId },
195
+ payload: {
196
+ success: result.success,
197
+ command: 'ax discuss',
198
+ pattern: 'direct',
199
+ providers: [providerId],
200
+ singleProviderMode: true,
201
+ durationMs,
202
+ },
203
+ });
204
+ if (!result.success) {
205
+ return {
206
+ success: false,
207
+ message: `Provider ${providerId} failed: ${result.error}`,
208
+ data: undefined,
209
+ exitCode: 1,
210
+ };
211
+ }
212
+ const lines = [];
213
+ lines.push('');
214
+ lines.push(`${COLORS.bold}${ICONS.discuss} Direct Response${COLORS.reset}`);
215
+ lines.push('─'.repeat(50));
216
+ lines.push(`Provider: ${COLORS.cyan}${providerId}${COLORS.reset}`);
217
+ lines.push(`Duration: ${(durationMs / 1000).toFixed(1)}s`);
218
+ lines.push(`${COLORS.dim}(Only 1 provider available - discussion skipped)${COLORS.reset}`);
219
+ lines.push('');
220
+ lines.push(`${COLORS.bold}Response${COLORS.reset}`);
221
+ lines.push('─'.repeat(50));
222
+ lines.push(result.content ?? '');
223
+ if (options.format === 'json') {
224
+ const response = {
225
+ success: true,
226
+ pattern: 'direct',
227
+ topic,
228
+ participatingProviders: [providerId],
229
+ failedProviders: [],
230
+ synthesis: result.content ?? '',
231
+ totalDurationMs: durationMs,
232
+ consensus: { method: 'direct', synthesizer: providerId, agreementScore: 1.0 },
233
+ metadata: { singleProviderMode: true },
234
+ };
235
+ return {
236
+ success: true,
237
+ message: undefined,
238
+ data: response,
239
+ exitCode: 0,
240
+ };
241
+ }
242
+ return {
243
+ success: true,
244
+ message: lines.join('\n'),
245
+ data: { success: true, synthesis: result.content, provider: providerId, durationMs },
246
+ exitCode: 0,
247
+ };
248
+ }
249
+ catch (error) {
250
+ const errorMessage = getErrorMessage(error);
251
+ if (!options.verbose) {
252
+ console.log(ICONS.cross);
253
+ }
254
+ await traceStore.write({
255
+ eventId: randomUUID(),
256
+ traceId,
257
+ type: 'discussion.end',
258
+ timestamp: new Date().toISOString(),
259
+ durationMs: Date.now() - callStartTime,
260
+ status: 'failure',
261
+ context: { providerId },
262
+ payload: {
263
+ success: false,
264
+ command: 'ax discuss',
265
+ providers: [providerId],
266
+ singleProviderMode: true,
267
+ error: errorMessage,
268
+ },
269
+ });
270
+ return {
271
+ success: false,
272
+ message: `Error calling ${providerId}: ${errorMessage}`,
273
+ data: undefined,
274
+ exitCode: 1,
275
+ };
276
+ }
277
+ }
278
+ // ============================================================================
35
279
  // Provider Bridge
36
280
  // ============================================================================
37
281
  /**
@@ -67,9 +311,9 @@ function createProviderBridge(providerConfigs) {
67
311
  try {
68
312
  const config = providerConfigs[request.providerId];
69
313
  const completionRequest = {
70
- requestId: crypto.randomUUID(),
314
+ requestId: randomUUID(),
71
315
  messages: [{ role: 'user', content: request.prompt }],
72
- model: config?.models.find(m => m.isDefault)?.modelId ?? config?.models[0]?.modelId ?? 'default',
316
+ model: getModelForProviderConfig(config),
73
317
  systemPrompt: request.systemPrompt,
74
318
  };
75
319
  const response = await adapter.complete(completionRequest);
@@ -132,12 +376,39 @@ function createProviderBridge(providerConfigs) {
132
376
  // ============================================================================
133
377
  // Argument Parsing
134
378
  // ============================================================================
379
+ /**
380
+ * Helper to parse integer with warning on invalid input
381
+ */
382
+ function parseIntWithWarning(value, defaultValue, argName) {
383
+ if (value === undefined)
384
+ return defaultValue;
385
+ const parsed = parseInt(value, 10);
386
+ if (isNaN(parsed)) {
387
+ console.warn(`Warning: Invalid value "${value}" for ${argName}, using default ${defaultValue}`);
388
+ return defaultValue;
389
+ }
390
+ return parsed;
391
+ }
392
+ /**
393
+ * Helper to parse float with warning on invalid input
394
+ */
395
+ function parseFloatWithWarning(value, defaultValue, argName) {
396
+ if (value === undefined)
397
+ return defaultValue;
398
+ const parsed = parseFloat(value);
399
+ if (isNaN(parsed)) {
400
+ console.warn(`Warning: Invalid value "${value}" for ${argName}, using default ${defaultValue}`);
401
+ return defaultValue;
402
+ }
403
+ return parsed;
404
+ }
135
405
  /**
136
406
  * Parses discuss command arguments
137
407
  */
138
408
  function parseDiscussArgs(args, _options) {
139
409
  let topic;
140
410
  let providers = [];
411
+ let providersExplicitlySpecified = false;
141
412
  let pattern = 'synthesis';
142
413
  let rounds = DEFAULT_ROUNDS;
143
414
  let consensus = 'synthesis';
@@ -147,6 +418,10 @@ function parseDiscussArgs(args, _options) {
147
418
  // Participant options
148
419
  let participants;
149
420
  let agentWeight = DEFAULT_AGENT_WEIGHT_MULTIPLIER; // (INV-DISC-642)
421
+ // Performance optimization options
422
+ let fastMode = false;
423
+ let roundEarlyExit = true; // Enabled by default
424
+ let roundAgreementThreshold = 0.85;
150
425
  // Recursive options
151
426
  let recursive = false;
152
427
  let maxDepth = DEFAULT_DISCUSSION_DEPTH;
@@ -160,14 +435,14 @@ function parseDiscussArgs(args, _options) {
160
435
  if (arg === '--providers' && i + 1 < args.length) {
161
436
  const providerStr = args[++i];
162
437
  providers = providerStr?.split(',').map(p => p.trim()) ?? [];
438
+ providersExplicitlySpecified = true;
163
439
  }
164
440
  else if (arg === '--pattern' && i + 1 < args.length) {
165
441
  pattern = args[++i];
166
442
  }
167
443
  else if (arg === '--rounds' && i + 1 < args.length) {
168
- const parsed = parseInt(args[++i] ?? '2', 10);
169
- if (!isNaN(parsed))
170
- rounds = parsed;
444
+ const rawValue = args[++i];
445
+ rounds = parseIntWithWarning(rawValue, DEFAULT_ROUNDS, '--rounds');
171
446
  }
172
447
  else if (arg === '--consensus' && i + 1 < args.length) {
173
448
  consensus = args[++i];
@@ -179,9 +454,8 @@ function parseDiscussArgs(args, _options) {
179
454
  context = args[++i];
180
455
  }
181
456
  else if (arg === '--timeout' && i + 1 < args.length) {
182
- const parsed = parseInt(args[++i] ?? '60000', 10);
183
- if (!isNaN(parsed))
184
- timeout = parsed;
457
+ const rawValue = args[++i];
458
+ timeout = parseIntWithWarning(rawValue, DEFAULT_PROVIDER_TIMEOUT, '--timeout');
185
459
  }
186
460
  // Participant options (agents and providers)
187
461
  else if (arg === '--participants' && i + 1 < args.length) {
@@ -191,43 +465,75 @@ function parseDiscussArgs(args, _options) {
191
465
  }
192
466
  else if (arg === '--agent-weight' && i + 1 < args.length) {
193
467
  // Agent weight multiplier (0.5 - 3.0)
194
- const parsed = parseFloat(args[++i] ?? String(DEFAULT_AGENT_WEIGHT_MULTIPLIER));
195
- if (!isNaN(parsed))
196
- agentWeight = Math.max(0.5, Math.min(3.0, parsed));
468
+ const rawValue = args[++i];
469
+ const parsed = parseFloatWithWarning(rawValue, DEFAULT_AGENT_WEIGHT_MULTIPLIER, '--agent-weight');
470
+ agentWeight = Math.max(0.5, Math.min(3.0, parsed));
471
+ }
472
+ // Performance optimization options
473
+ else if (arg === '--fast' || arg === '-f') {
474
+ // Fast mode: single round, skip cross-discussion
475
+ fastMode = true;
476
+ rounds = 1; // Override to single round
477
+ }
478
+ else if (arg === '--no-round-early-exit') {
479
+ // Disable round-level early exit
480
+ roundEarlyExit = false;
481
+ }
482
+ else if (arg === '--round-threshold' && i + 1 < args.length) {
483
+ // Agreement threshold for round early exit (0.5-1.0)
484
+ const rawValue = args[++i];
485
+ const parsed = parseFloatWithWarning(rawValue, 0.85, '--round-threshold');
486
+ roundAgreementThreshold = Math.max(0.5, Math.min(1.0, parsed));
197
487
  }
198
488
  // Recursive discussion options
199
489
  else if (arg === '--recursive' || arg === '-r') {
200
490
  recursive = true;
201
491
  }
202
492
  else if (arg === '--max-depth' && i + 1 < args.length) {
203
- const parsed = parseInt(args[++i] ?? '2', 10);
204
- if (!isNaN(parsed))
205
- maxDepth = parsed;
493
+ const rawValue = args[++i];
494
+ maxDepth = parseIntWithWarning(rawValue, DEFAULT_DISCUSSION_DEPTH, '--max-depth');
495
+ // Validate max-depth is within bounds (1-4)
496
+ if (maxDepth < 1 || maxDepth > MAX_DISCUSSION_DEPTH) {
497
+ console.warn(`Warning: --max-depth must be between 1 and ${MAX_DISCUSSION_DEPTH}. Clamping to valid range.`);
498
+ maxDepth = Math.max(1, Math.min(maxDepth, MAX_DISCUSSION_DEPTH));
499
+ }
206
500
  recursive = true; // Implies recursive
207
501
  }
208
502
  else if (arg === '--timeout-strategy' && i + 1 < args.length) {
209
503
  timeoutStrategy = args[++i];
210
504
  }
211
505
  else if (arg === '--budget' && i + 1 < args.length) {
212
- // Parse budget like "180s" or "180000"
213
- const budgetStr = args[++i] ?? '180000';
506
+ // Parse budget like "180s", "3m", or "180000"
507
+ const budgetStr = args[++i] ?? '';
214
508
  let parsed;
215
509
  if (budgetStr.endsWith('s')) {
216
- parsed = parseInt(budgetStr.slice(0, -1), 10) * 1000;
510
+ const numPart = budgetStr.slice(0, -1);
511
+ parsed = parseInt(numPart, 10) * 1000;
512
+ if (isNaN(parsed)) {
513
+ console.warn(`Warning: Invalid budget format "${budgetStr}", expected format like "180s", "3m", or "180000". Using default.`);
514
+ parsed = DEFAULT_TOTAL_BUDGET_MS;
515
+ }
217
516
  }
218
517
  else if (budgetStr.endsWith('m')) {
219
- parsed = parseInt(budgetStr.slice(0, -1), 10) * 60000;
518
+ const numPart = budgetStr.slice(0, -1);
519
+ parsed = parseInt(numPart, 10) * 60000;
520
+ if (isNaN(parsed)) {
521
+ console.warn(`Warning: Invalid budget format "${budgetStr}", expected format like "180s", "3m", or "180000". Using default.`);
522
+ parsed = DEFAULT_TOTAL_BUDGET_MS;
523
+ }
220
524
  }
221
525
  else {
222
526
  parsed = parseInt(budgetStr, 10);
527
+ if (isNaN(parsed)) {
528
+ console.warn(`Warning: Invalid budget format "${budgetStr}", expected format like "180s", "3m", or "180000". Using default.`);
529
+ parsed = DEFAULT_TOTAL_BUDGET_MS;
530
+ }
223
531
  }
224
- if (!isNaN(parsed))
225
- totalBudget = parsed;
532
+ totalBudget = parsed;
226
533
  }
227
534
  else if (arg === '--max-calls' && i + 1 < args.length) {
228
- const parsed = parseInt(args[++i] ?? '20', 10);
229
- if (!isNaN(parsed))
230
- maxCalls = parsed;
535
+ const rawValue = args[++i];
536
+ maxCalls = parseIntWithWarning(rawValue, DEFAULT_MAX_TOTAL_CALLS, '--max-calls');
231
537
  }
232
538
  else if (arg === '--early-exit') {
233
539
  earlyExit = true;
@@ -236,33 +542,32 @@ function parseDiscussArgs(args, _options) {
236
542
  earlyExit = false;
237
543
  }
238
544
  else if (arg === '--confidence-threshold' && i + 1 < args.length) {
239
- const parsed = parseFloat(args[++i] ?? '0.9');
240
- if (!isNaN(parsed))
241
- confidenceThreshold = parsed;
545
+ const rawValue = args[++i];
546
+ confidenceThreshold = parseFloatWithWarning(rawValue, DEFAULT_CONFIDENCE_THRESHOLD, '--confidence-threshold');
242
547
  }
243
548
  else if (arg !== undefined && !arg.startsWith('-')) {
244
549
  // Positional argument is the topic
245
550
  if (topic === undefined) {
246
- // Collect remaining non-flag args as topic
551
+ // Collect consecutive non-flag args as topic, then continue parsing remaining flags
247
552
  const topicParts = [];
248
- for (let j = i; j < args.length; j++) {
553
+ let j = i;
554
+ for (; j < args.length; j++) {
249
555
  const part = args[j];
250
556
  if (part?.startsWith('-'))
251
557
  break;
252
558
  topicParts.push(part ?? '');
253
559
  }
254
560
  topic = topicParts.join(' ');
255
- break;
561
+ // Skip over the topic parts we just consumed (minus 1 because the for loop will increment i)
562
+ i = j - 1;
256
563
  }
257
564
  }
258
565
  }
259
- // Default providers if none specified
260
- if (providers.length === 0) {
261
- providers = [...DEFAULT_PROVIDERS];
262
- }
566
+ // Note: providers are NOT defaulted here - smart selection happens in the command handler
263
567
  return {
264
568
  topic,
265
569
  providers,
570
+ providersExplicitlySpecified,
266
571
  pattern,
267
572
  rounds,
268
573
  consensus,
@@ -271,6 +576,9 @@ function parseDiscussArgs(args, _options) {
271
576
  timeout,
272
577
  participants,
273
578
  agentWeight,
579
+ fastMode,
580
+ roundEarlyExit,
581
+ roundAgreementThreshold,
274
582
  recursive,
275
583
  maxDepth,
276
584
  timeoutStrategy,
@@ -280,36 +588,112 @@ function parseDiscussArgs(args, _options) {
280
588
  confidenceThreshold,
281
589
  };
282
590
  }
283
- // ============================================================================
284
- // Progress Display
285
- // ============================================================================
286
591
  /**
287
- * Creates a progress handler for verbose output
592
+ * Creates a progress handler for verbose output and trace event emission (Phase 2)
593
+ * Emits granular trace events: discussion.provider, discussion.round, discussion.consensus
288
594
  */
289
- function createProgressHandler(verbose) {
290
- if (!verbose) {
291
- return () => { }; // No-op
292
- }
595
+ function createProgressHandler(verbose, traceContext) {
293
596
  return (event) => {
294
- switch (event.type) {
295
- case 'round_start':
296
- console.log(`\n${COLORS.bold}Round ${event.round}${COLORS.reset}`);
297
- break;
298
- case 'provider_start':
299
- process.stdout.write(` ${ICONS.bullet} ${event.provider}: `);
300
- break;
301
- case 'provider_complete':
302
- console.log(`${ICONS.check} ${COLORS.dim}(${event.message ?? 'done'})${COLORS.reset}`);
303
- break;
304
- case 'round_complete':
305
- console.log(` ${ICONS.arrow} Round ${event.round} complete`);
306
- break;
307
- case 'synthesis_start':
308
- console.log(`\n${COLORS.bold}Synthesizing...${COLORS.reset}`);
309
- break;
310
- case 'synthesis_complete':
311
- console.log(`${ICONS.check} Synthesis complete`);
312
- break;
597
+ // Console output for verbose mode
598
+ if (verbose) {
599
+ switch (event.type) {
600
+ case 'round_start':
601
+ console.log(`\n${COLORS.bold}Round ${event.round}${COLORS.reset}`);
602
+ break;
603
+ case 'provider_start':
604
+ process.stdout.write(` ${ICONS.bullet} ${event.provider}: `);
605
+ break;
606
+ case 'provider_complete':
607
+ console.log(`${ICONS.check} ${COLORS.dim}(${event.message ?? 'done'})${COLORS.reset}`);
608
+ break;
609
+ case 'round_complete':
610
+ console.log(` ${ICONS.arrow} Round ${event.round} complete`);
611
+ break;
612
+ case 'synthesis_start':
613
+ console.log(`\n${COLORS.bold}Synthesizing...${COLORS.reset}`);
614
+ break;
615
+ case 'synthesis_complete':
616
+ console.log(`${ICONS.check} Synthesis complete`);
617
+ break;
618
+ }
619
+ }
620
+ // Emit trace events for dashboard visibility (Phase 2: Granular Events)
621
+ if (traceContext) {
622
+ const { traceStore, traceId } = traceContext;
623
+ switch (event.type) {
624
+ case 'provider_complete':
625
+ // Emit discussion.provider trace event
626
+ if (event.provider && event.round !== undefined) {
627
+ traceStore.write({
628
+ eventId: randomUUID(),
629
+ traceId,
630
+ type: 'discussion.provider',
631
+ timestamp: event.timestamp,
632
+ durationMs: event.durationMs,
633
+ status: event.success ? 'success' : 'failure',
634
+ context: {
635
+ providerId: event.provider,
636
+ },
637
+ payload: {
638
+ providerId: event.provider,
639
+ roundNumber: event.round,
640
+ success: event.success ?? false,
641
+ durationMs: event.durationMs ?? 0,
642
+ tokenCount: event.tokenCount,
643
+ role: event.role,
644
+ error: event.error,
645
+ },
646
+ }).catch((err) => {
647
+ // Fire-and-forget with error logging
648
+ console.error('[discuss] Failed to write provider trace event:', err);
649
+ });
650
+ }
651
+ break;
652
+ case 'round_complete':
653
+ // Emit discussion.round trace event
654
+ if (event.round !== undefined) {
655
+ traceStore.write({
656
+ eventId: randomUUID(),
657
+ traceId,
658
+ type: 'discussion.round',
659
+ timestamp: event.timestamp,
660
+ durationMs: event.durationMs,
661
+ context: {},
662
+ payload: {
663
+ roundNumber: event.round,
664
+ participatingProviders: event.participatingProviders ?? [],
665
+ failedProviders: event.failedProviders,
666
+ responseCount: event.responseCount ?? 0,
667
+ durationMs: event.durationMs ?? 0,
668
+ },
669
+ }).catch((err) => {
670
+ console.error('[discuss] Failed to write round trace event:', err);
671
+ });
672
+ }
673
+ break;
674
+ case 'consensus_complete':
675
+ // Emit discussion.consensus trace event
676
+ traceStore.write({
677
+ eventId: randomUUID(),
678
+ traceId,
679
+ type: 'discussion.consensus',
680
+ timestamp: event.timestamp,
681
+ durationMs: event.durationMs,
682
+ status: event.success ? 'success' : 'failure',
683
+ context: {},
684
+ payload: {
685
+ method: event.consensusMethod ?? 'synthesis',
686
+ success: event.success ?? false,
687
+ winner: event.winner,
688
+ confidence: event.confidence,
689
+ votes: event.votes,
690
+ durationMs: event.durationMs ?? 0,
691
+ },
692
+ }).catch((err) => {
693
+ console.error('[discuss] Failed to write consensus trace event:', err);
694
+ });
695
+ break;
696
+ }
313
697
  }
314
698
  };
315
699
  }
@@ -426,10 +810,15 @@ ${COLORS.bold}Basic Options:${COLORS.reset}
426
810
  --consensus Consensus method: ${consensusMethods}
427
811
  --synthesizer Provider for synthesis (default: claude)
428
812
  --context Additional context for the discussion
429
- --timeout Per-provider timeout in ms (default: 180000, max: 30 min)
813
+ --timeout Per-provider timeout in ms (default: 600000/10min, max: 30 min)
430
814
  --verbose, -v Show detailed progress
431
815
  --format Output format: text (default) or json
432
816
 
817
+ ${COLORS.bold}Performance Options:${COLORS.reset}
818
+ --fast, -f Fast mode: single round, skip cross-discussion (~50% faster)
819
+ --no-round-early-exit Disable round-level early exit on high agreement
820
+ --round-threshold Agreement threshold for round early exit (default: 0.85)
821
+
433
822
  ${COLORS.bold}Recursive Discussion Options:${COLORS.reset}
434
823
  --recursive, -r Enable recursive sub-discussions
435
824
  --max-depth Maximum discussion depth (default: 2, max: 4)
@@ -462,6 +851,10 @@ ${COLORS.bold}Examples:${COLORS.reset}
462
851
  ax discuss --pattern voting "Which framework: React, Vue, or Angular?"
463
852
  ax discuss --verbose --rounds 3 "Optimize database queries"
464
853
 
854
+ ${COLORS.cyan}# Fast mode (single round, ~50% faster)${COLORS.reset}
855
+ ax discuss --fast "What is TypeScript?"
856
+ ax discuss -f "Explain microservices"
857
+
465
858
  ${COLORS.cyan}# Recursive discussions${COLORS.reset}
466
859
  ax discuss --recursive "Complex architectural decision"
467
860
  ax discuss --recursive --max-depth 3 "Design a distributed system"
@@ -478,13 +871,15 @@ ${COLORS.bold}Examples:${COLORS.reset}
478
871
  // Main Command Handler
479
872
  // ============================================================================
480
873
  /**
481
- * Discuss command handler
874
+ * Discuss command handler with trace integration
482
875
  */
483
876
  export async function discussCommand(args, options) {
484
877
  // Show help if requested
485
878
  if (args.length === 0 || args[0] === 'help' || options.help) {
486
879
  return showDiscussHelp();
487
880
  }
881
+ // Initialize bootstrap to get SQLite trace store
882
+ await bootstrap();
488
883
  // Handle 'quick' subcommand - use quick synthesis with 2-3 providers
489
884
  if (args[0] === 'quick') {
490
885
  const quickArgs = args.slice(1);
@@ -515,55 +910,59 @@ export async function discussCommand(args, options) {
515
910
  exitCode: 1,
516
911
  };
517
912
  }
518
- // Validate providers
519
- const invalidProviders = parsed.providers.filter(p => PROVIDER_CONFIGS[p] === undefined);
520
- if (invalidProviders.length > 0) {
521
- const available = Object.keys(PROVIDER_CONFIGS).join(', ');
522
- return {
523
- success: false,
524
- message: `Error: Unknown provider(s): ${invalidProviders.join(', ')}\nAvailable: ${available}`,
525
- data: undefined,
526
- exitCode: 1,
527
- };
528
- }
529
- // Validate minimum providers
530
- if (parsed.providers.length < 2) {
531
- return {
532
- success: false,
533
- message: 'Error: At least 2 providers are required for discussion.',
534
- data: undefined,
535
- exitCode: 1,
536
- };
913
+ // Validate explicitly specified providers
914
+ if (parsed.providersExplicitlySpecified) {
915
+ const invalidProviders = parsed.providers.filter(p => PROVIDER_CONFIGS[p] === undefined);
916
+ if (invalidProviders.length > 0) {
917
+ const available = Object.keys(PROVIDER_CONFIGS).join(', ');
918
+ return {
919
+ success: false,
920
+ message: `Error: Unknown provider(s): ${invalidProviders.join(', ')}\nAvailable: ${available}`,
921
+ data: undefined,
922
+ exitCode: 1,
923
+ };
924
+ }
537
925
  }
538
926
  // Create provider bridge
539
927
  const providerBridge = createProviderBridge(PROVIDER_CONFIGS);
540
- // Check provider availability
928
+ // Create trace for dashboard visibility
929
+ const traceStore = getTraceStore();
930
+ const traceId = randomUUID();
931
+ const startTime = new Date().toISOString();
932
+ // Smart provider selection
541
933
  if (options.verbose) {
542
- console.log(`${COLORS.bold}Checking provider availability...${COLORS.reset}`);
543
- }
544
- const availableProviders = [];
545
- for (const providerId of parsed.providers) {
546
- const isAvailable = await providerBridge.isAvailable(providerId);
547
- if (isAvailable) {
548
- availableProviders.push(providerId);
549
- if (options.verbose) {
550
- console.log(` ${ICONS.check} ${providerId}`);
551
- }
552
- }
553
- else {
554
- if (options.verbose) {
555
- console.log(` ${ICONS.cross} ${providerId} ${COLORS.dim}(not available)${COLORS.reset}`);
556
- }
557
- }
934
+ console.log(`${COLORS.bold}Selecting providers...${COLORS.reset}`);
558
935
  }
559
- if (availableProviders.length < 2) {
936
+ const selection = await selectProvidersForCLI(parsed.providers, parsed.providersExplicitlySpecified, providerBridge, options.verbose);
937
+ // Handle selection errors
938
+ if (selection.error) {
560
939
  return {
561
940
  success: false,
562
- message: `Error: Only ${availableProviders.length} providers available. Need at least 2.\nRun "ax doctor" to check provider status.`,
941
+ message: `Error: ${selection.error}`,
563
942
  data: undefined,
564
943
  exitCode: 1,
565
944
  };
566
945
  }
946
+ // If only 1 provider available, skip discussion and call directly
947
+ if (selection.skipDiscussion && selection.singleProvider) {
948
+ // Emit run.start trace event
949
+ await traceStore.write({
950
+ eventId: randomUUID(),
951
+ traceId,
952
+ type: 'discussion.start',
953
+ timestamp: startTime,
954
+ context: { providerId: selection.singleProvider },
955
+ payload: {
956
+ command: 'ax discuss',
957
+ topic: parsed.topic,
958
+ pattern: 'direct',
959
+ providers: [selection.singleProvider],
960
+ singleProviderMode: true,
961
+ },
962
+ });
963
+ return callSingleProviderCLI(parsed.topic, selection.singleProvider, providerBridge, parsed.timeout, options, traceStore, traceId, startTime);
964
+ }
965
+ const availableProviders = selection.providers;
567
966
  // Build discussion config
568
967
  const config = {
569
968
  pattern: parsed.pattern,
@@ -583,6 +982,13 @@ export async function discussCommand(args, options) {
583
982
  minProviders: 2,
584
983
  temperature: 0.7,
585
984
  agentWeightMultiplier: parsed.agentWeight,
985
+ // Performance optimization options
986
+ fastMode: parsed.fastMode,
987
+ roundEarlyExit: {
988
+ enabled: parsed.roundEarlyExit,
989
+ agreementThreshold: parsed.roundAgreementThreshold,
990
+ minRounds: 1,
991
+ },
586
992
  // Include participants if specified via --participants option
587
993
  ...(parsed.participants !== undefined && { participants: parsed.participants }),
588
994
  };
@@ -594,6 +1000,9 @@ export async function discussCommand(args, options) {
594
1000
  console.log(` Pattern: ${PATTERN_NAMES[parsed.pattern] ?? parsed.pattern}`);
595
1001
  console.log(` Providers: ${availableProviders.join(', ')}`);
596
1002
  console.log(` Rounds: ${parsed.rounds}`);
1003
+ if (parsed.fastMode) {
1004
+ console.log(` ${COLORS.cyan}Fast Mode: enabled (single round)${COLORS.reset}`);
1005
+ }
597
1006
  if (parsed.participants !== undefined && parsed.participants.length > 0) {
598
1007
  const agentCount = parsed.participants.filter(p => p.type === 'agent').length;
599
1008
  const providerCount = parsed.participants.filter(p => p.type === 'provider').length;
@@ -610,9 +1019,37 @@ export async function discussCommand(args, options) {
610
1019
  }
611
1020
  else {
612
1021
  // Simple progress indicator for non-verbose mode
1022
+ const fastLabel = parsed.fastMode ? ' (fast)' : '';
613
1023
  const recursiveLabel = parsed.recursive ? ' (recursive)' : '';
614
- process.stdout.write(`${ICONS.discuss} Discussing with ${availableProviders.length} providers${recursiveLabel}... `);
1024
+ process.stdout.write(`${ICONS.discuss} Discussing with ${availableProviders.length} providers${fastLabel}${recursiveLabel}... `);
615
1025
  }
1026
+ // Emit run.start trace event with full topic
1027
+ // INV-TR-010: Include participating providers for drill-down
1028
+ const startEvent = {
1029
+ eventId: randomUUID(),
1030
+ traceId,
1031
+ type: 'discussion.start',
1032
+ timestamp: startTime,
1033
+ context: {
1034
+ // For discussions with multiple providers, store first provider as primary
1035
+ // All providers are listed in payload.providers
1036
+ providerId: availableProviders[0], // Primary provider for filtering
1037
+ },
1038
+ payload: {
1039
+ command: 'ax discuss',
1040
+ topic: parsed.topic, // Full topic for dashboard
1041
+ topicLength: parsed.topic.length,
1042
+ pattern: parsed.pattern,
1043
+ consensus: parsed.consensus,
1044
+ providers: availableProviders,
1045
+ rounds: parsed.rounds,
1046
+ recursive: parsed.recursive,
1047
+ maxDepth: parsed.recursive ? parsed.maxDepth : undefined,
1048
+ context: parsed.context,
1049
+ },
1050
+ };
1051
+ await traceStore.write(startEvent);
1052
+ const discussStartTime = Date.now();
616
1053
  try {
617
1054
  // Choose executor based on recursive flag
618
1055
  let result;
@@ -642,7 +1079,7 @@ export async function discussCommand(args, options) {
642
1079
  },
643
1080
  });
644
1081
  result = await recursiveExecutor.execute(config, {
645
- onProgress: createProgressHandler(options.verbose),
1082
+ onProgress: createProgressHandler(options.verbose, { traceStore, traceId }),
646
1083
  });
647
1084
  }
648
1085
  else {
@@ -653,13 +1090,55 @@ export async function discussCommand(args, options) {
653
1090
  checkProviderHealth: false, // Already checked above
654
1091
  });
655
1092
  result = await discussionExecutor.execute(config, {
656
- onProgress: createProgressHandler(options.verbose),
1093
+ onProgress: createProgressHandler(options.verbose, { traceStore, traceId }),
657
1094
  });
658
1095
  }
659
1096
  // Clear simple progress indicator if not verbose
660
1097
  if (!options.verbose) {
661
1098
  console.log(result.success ? ICONS.check : ICONS.cross);
662
1099
  }
1100
+ // Emit discussion.end trace event with full details
1101
+ const durationMs = Date.now() - discussStartTime;
1102
+ // Extract provider responses from rounds for dashboard
1103
+ const providerResponses = {};
1104
+ if (result.rounds) {
1105
+ for (const round of result.rounds) {
1106
+ for (const response of round.responses ?? []) {
1107
+ const providerId = response.provider ?? 'unknown';
1108
+ if (!providerResponses[providerId]) {
1109
+ providerResponses[providerId] = [];
1110
+ }
1111
+ providerResponses[providerId].push(response.content ?? '');
1112
+ }
1113
+ }
1114
+ }
1115
+ // INV-TR-010: Include participating providers for drill-down
1116
+ const endEvent = {
1117
+ eventId: randomUUID(),
1118
+ traceId,
1119
+ type: 'discussion.end',
1120
+ timestamp: new Date().toISOString(),
1121
+ durationMs,
1122
+ status: result.success ? 'success' : 'failure',
1123
+ context: {
1124
+ providerId: availableProviders[0], // Primary provider for filtering
1125
+ },
1126
+ payload: {
1127
+ success: result.success,
1128
+ command: 'ax discuss',
1129
+ pattern: parsed.pattern,
1130
+ providers: availableProviders,
1131
+ roundCount: result.rounds?.length ?? parsed.rounds,
1132
+ durationMs,
1133
+ consensusReached: result.consensus !== undefined,
1134
+ consensus: result.consensus, // Consensus metadata (method, agreementScore, etc.)
1135
+ synthesis: result.synthesis, // Final synthesized text for dashboard
1136
+ responses: providerResponses, // Provider responses by provider ID
1137
+ // Summary for quick view
1138
+ totalResponses: Object.values(providerResponses).flat().length,
1139
+ },
1140
+ };
1141
+ await traceStore.write(endEvent);
663
1142
  // Handle JSON output
664
1143
  if (options.format === 'json') {
665
1144
  return {
@@ -683,7 +1162,27 @@ export async function discussCommand(args, options) {
683
1162
  if (!options.verbose) {
684
1163
  console.log(ICONS.cross);
685
1164
  }
1165
+ // Emit error trace event
1166
+ // INV-TR-010: Include participating providers for drill-down
1167
+ const durationMs = Date.now() - discussStartTime;
686
1168
  const errorMessage = getErrorMessage(error);
1169
+ await traceStore.write({
1170
+ eventId: randomUUID(),
1171
+ traceId,
1172
+ type: 'discussion.end',
1173
+ timestamp: new Date().toISOString(),
1174
+ durationMs,
1175
+ status: 'failure',
1176
+ context: {
1177
+ providerId: availableProviders[0], // Primary provider for filtering
1178
+ },
1179
+ payload: {
1180
+ success: false,
1181
+ command: 'ax discuss',
1182
+ providers: availableProviders,
1183
+ error: errorMessage,
1184
+ },
1185
+ });
687
1186
  return {
688
1187
  success: false,
689
1188
  message: `Error during discussion: ${errorMessage}`,