@defai.digital/discussion-domain 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.
@@ -8,7 +8,7 @@
8
8
  * - INV-DISC-642: Agent responses weighted by agentWeightMultiplier (default 1.5x)
9
9
  */
10
10
 
11
- import { DEFAULT_AGENT_WEIGHT_MULTIPLIER, type VotingResults, type VoteRecord } from '@defai.digital/contracts';
11
+ import { DEFAULT_AGENT_WEIGHT_MULTIPLIER, DEFAULT_VOTING_SUMMARY_TIMEOUT, type VotingResults, type VoteRecord } from '@defai.digital/contracts';
12
12
  import type { ConsensusExecutor, ConsensusExecutionContext, ConsensusExecutionResult } from '../types.js';
13
13
  import {
14
14
  VOTING_TALLY,
@@ -69,6 +69,21 @@ export class VotingConsensus implements ConsensusExecutor {
69
69
  onProgress
70
70
  );
71
71
 
72
+ const consensusDurationMs = Date.now() - startTime;
73
+
74
+ // Emit consensus_complete event for Phase 2 tracing
75
+ onProgress?.({
76
+ type: 'consensus_complete',
77
+ message: `Voting consensus reached: ${votingResults.winner} won${votingResults.unanimous ? ' unanimously' : ''}`,
78
+ timestamp: new Date().toISOString(),
79
+ success: true,
80
+ consensusMethod: 'voting',
81
+ confidence: votingResults.unanimous ? 1.0 : votingResults.margin,
82
+ winner: votingResults.winner,
83
+ votes: votingResults.votes,
84
+ durationMs: consensusDurationMs,
85
+ });
86
+
72
87
  return {
73
88
  synthesis,
74
89
  consensus: {
@@ -77,7 +92,7 @@ export class VotingConsensus implements ConsensusExecutor {
77
92
  agreementScore: votingResults.unanimous ? 1.0 : votingResults.margin,
78
93
  },
79
94
  votingResults,
80
- durationMs: Date.now() - startTime,
95
+ durationMs: consensusDurationMs,
81
96
  success: true,
82
97
  };
83
98
  }
@@ -185,7 +200,7 @@ export class VotingConsensus implements ConsensusExecutor {
185
200
  prompt,
186
201
  systemPrompt: getProviderSystemPrompt(synthesizerId),
187
202
  temperature: 0.5,
188
- timeoutMs: 60000,
203
+ timeoutMs: DEFAULT_VOTING_SUMMARY_TIMEOUT, // Vote summary is simpler (90 sec default)
189
204
  abortSignal,
190
205
  });
191
206
 
@@ -193,6 +208,10 @@ export class VotingConsensus implements ConsensusExecutor {
193
208
  type: 'synthesis_complete',
194
209
  provider: synthesizerId,
195
210
  timestamp: new Date().toISOString(),
211
+ // Extended fields for Phase 2 tracing
212
+ success: result.success,
213
+ durationMs: result.durationMs,
214
+ tokenCount: result.tokenCount,
196
215
  });
197
216
 
198
217
  if (result.success && result.content) {
package/src/executor.ts CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import {
15
15
  DEFAULT_PROVIDERS,
16
+ DEFAULT_PROVIDER_TIMEOUT,
16
17
  DiscussionErrorCodes,
17
18
  createFailedDiscussionResult,
18
19
  DEFAULT_AGENT_WEIGHT_MULTIPLIER,
@@ -26,9 +27,14 @@ import type {
26
27
  DiscussionExecutorOptions,
27
28
  PatternExecutionContext,
28
29
  DiscussionProgressEvent,
30
+ ResolvedParticipantLike,
29
31
  } from './types.js';
30
32
  import { getPatternExecutor } from './patterns/index.js';
31
33
  import { getConsensusExecutor } from './consensus/index.js';
34
+ import {
35
+ resolveParticipants,
36
+ getProviderIds,
37
+ } from './participant-resolver.js';
32
38
 
33
39
  /**
34
40
  * Main discussion executor class.
@@ -40,12 +46,16 @@ export class DiscussionExecutor {
40
46
  private readonly defaultTimeoutMs: number;
41
47
  private readonly checkProviderHealth: boolean;
42
48
  private readonly traceId: string | undefined;
49
+ private readonly participantResolverOptions: DiscussionExecutorOptions['participantResolverOptions'];
50
+ private readonly cascadingConfidence: DiscussionExecutorOptions['cascadingConfidence'];
43
51
 
44
52
  constructor(options: DiscussionExecutorOptions) {
45
53
  this.providerExecutor = options.providerExecutor;
46
- this.defaultTimeoutMs = options.defaultTimeoutMs ?? 60000;
54
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT;
47
55
  this.checkProviderHealth = options.checkProviderHealth ?? true;
48
56
  this.traceId = options.traceId;
57
+ this.participantResolverOptions = options.participantResolverOptions ?? {};
58
+ this.cascadingConfidence = options.cascadingConfidence;
49
59
  }
50
60
 
51
61
  /**
@@ -77,6 +87,7 @@ export class DiscussionExecutor {
77
87
  minProviders: 2,
78
88
  temperature: 0.7,
79
89
  agentWeightMultiplier: DEFAULT_AGENT_WEIGHT_MULTIPLIER,
90
+ fastMode: false,
80
91
  };
81
92
 
82
93
  return this.execute(config, options);
@@ -106,10 +117,39 @@ export class DiscussionExecutor {
106
117
  );
107
118
  }
108
119
 
120
+ // Resolve participants if provided (INV-DISC-640, INV-DISC-641)
121
+ let resolvedParticipants: ResolvedParticipantLike[] | undefined;
122
+ let providersToCheck: string[];
123
+
124
+ if (config.participants && config.participants.length > 0) {
125
+ // Resolve participants (agents + providers) to execution config
126
+ try {
127
+ resolvedParticipants = await resolveParticipants(config.participants, {
128
+ ...this.participantResolverOptions,
129
+ topic: config.prompt,
130
+ agentWeightMultiplier: config.agentWeightMultiplier,
131
+ });
132
+ // Extract unique provider IDs from resolved participants
133
+ providersToCheck = getProviderIds(resolvedParticipants);
134
+ } catch (error) {
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ return createFailedDiscussionResult(
137
+ config.pattern,
138
+ config.prompt,
139
+ DiscussionErrorCodes.INVALID_CONFIG,
140
+ `Failed to resolve participants: ${errorMessage}`,
141
+ startedAt
142
+ );
143
+ }
144
+ } else {
145
+ // Use legacy providers array
146
+ providersToCheck = config.providers;
147
+ }
148
+
109
149
  // INV-DISC-100: Check provider availability
110
150
  let availableProviders: string[];
111
151
  try {
112
- availableProviders = await this.checkProviders(config.providers);
152
+ availableProviders = await this.checkProviders(providersToCheck);
113
153
  } catch (error) {
114
154
  const errorMessage = error instanceof Error ? error.message : String(error);
115
155
  return createFailedDiscussionResult(
@@ -143,7 +183,10 @@ export class DiscussionExecutor {
143
183
  abortSignal,
144
184
  traceId: this.traceId,
145
185
  onProgress,
146
- // No cascading confidence for base executor (recursive executor handles this)
186
+ // Pass resolved participants for agent-aware execution
187
+ resolvedParticipants,
188
+ // Cascading confidence for early exit (INV-DISC-622, INV-DISC-623)
189
+ cascadingConfidence: this.cascadingConfidence,
147
190
  };
148
191
 
149
192
  // Execute pattern
@@ -293,6 +336,7 @@ export class DiscussionExecutor {
293
336
  minProviders: 3,
294
337
  temperature: 0.7,
295
338
  agentWeightMultiplier: DEFAULT_AGENT_WEIGHT_MULTIPLIER,
339
+ fastMode: false,
296
340
  };
297
341
 
298
342
  return this.execute(config, options);
@@ -328,6 +372,7 @@ export class DiscussionExecutor {
328
372
  minProviders: 2,
329
373
  temperature: 0.5, // Lower temperature for more consistent voting
330
374
  agentWeightMultiplier: DEFAULT_AGENT_WEIGHT_MULTIPLIER,
375
+ fastMode: false,
331
376
  };
332
377
 
333
378
  return this.execute(config, options);
@@ -203,8 +203,12 @@ export async function resolveParticipant(
203
203
  if (injection.combinedContent) {
204
204
  resolved.abilityContent = injection.combinedContent;
205
205
  }
206
- } catch {
207
- // Ability injection failed, continue without abilities
206
+ } catch (error) {
207
+ // Log warning for diagnostics, but don't fail discussion
208
+ console.warn(
209
+ `[participant-resolver] Ability injection failed for agent ${agentId}:`,
210
+ error instanceof Error ? error.message : String(error)
211
+ );
208
212
  }
209
213
  }
210
214
 
@@ -236,13 +240,23 @@ export function providersToParticipants(providers: string[]): DiscussionParticip
236
240
  * Parse participant string (e.g., "claude" or "reviewer:agent")
237
241
  */
238
242
  export function parseParticipantString(input: string): DiscussionParticipant {
239
- const parts = input.split(':');
243
+ const trimmed = input.trim();
244
+ if (!trimmed) {
245
+ throw new Error('Participant string cannot be empty');
246
+ }
247
+
248
+ const parts = trimmed.split(':');
249
+ const id = parts[0]!.trim();
250
+
251
+ if (!id) {
252
+ throw new Error('Participant ID cannot be empty');
253
+ }
240
254
 
241
255
  if (parts.length === 2 && parts[1] === 'agent') {
242
- return { type: 'agent', id: parts[0]! };
256
+ return { type: 'agent', id };
243
257
  }
244
258
 
245
- return { type: 'provider', id: parts[0]! };
259
+ return { type: 'provider', id };
246
260
  }
247
261
 
248
262
  /**
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { DiscussionRound, DebateRole } from '@defai.digital/contracts';
18
+ import { DEFAULT_ROUND_AGREEMENT_THRESHOLD } from '@defai.digital/contracts';
18
19
  import type {
19
20
  PatternExecutor,
20
21
  PatternExecutionContext,
@@ -28,7 +29,7 @@ import {
28
29
  formatPreviousResponses,
29
30
  getProviderSystemPrompt,
30
31
  } from '../prompts/templates.js';
31
- import { extractConfidence, evaluateEarlyExit } from '../confidence-extractor.js';
32
+ import { extractConfidence, evaluateEarlyExit, calculateAgreementScore } from '../confidence-extractor.js';
32
33
 
33
34
  // Local type for discussion DiscussionProviderResponse (avoids conflict with provider/v1 DiscussionProviderResponse)
34
35
  interface DiscussionProviderResponse {
@@ -57,6 +58,9 @@ export class SynthesisPattern implements PatternExecutor {
57
58
  const failedProviders = new Set<string>();
58
59
  let earlyExit: EarlyExitInfo | undefined;
59
60
 
61
+ // Extract round early exit config
62
+ const roundEarlyExit = config.roundEarlyExit ?? { enabled: true, agreementThreshold: DEFAULT_ROUND_AGREEMENT_THRESHOLD, minRounds: 1 };
63
+
60
64
  // Filter to available providers
61
65
  const providers = config.providers.filter(p => availableProviders.includes(p));
62
66
 
@@ -75,7 +79,9 @@ export class SynthesisPattern implements PatternExecutor {
75
79
  onProgress?.({
76
80
  type: 'round_start',
77
81
  round: 1,
78
- message: 'Gathering initial perspectives in parallel',
82
+ message: config.fastMode
83
+ ? 'Fast mode: Gathering perspectives (single round)'
84
+ : 'Gathering initial perspectives in parallel',
79
85
  timestamp: new Date().toISOString(),
80
86
  });
81
87
 
@@ -98,6 +104,11 @@ export class SynthesisPattern implements PatternExecutor {
98
104
  round: 1,
99
105
  message: `Initial perspectives gathered from ${initialRound.succeeded.length} providers`,
100
106
  timestamp: new Date().toISOString(),
107
+ // Extended fields for Phase 2 tracing
108
+ participatingProviders: initialRound.succeeded,
109
+ failedProviders: initialRound.failed,
110
+ responseCount: initialRound.round.responses.length,
111
+ durationMs: initialRound.round.durationMs,
101
112
  });
102
113
 
103
114
  // Check if enough providers participated
@@ -112,6 +123,28 @@ export class SynthesisPattern implements PatternExecutor {
112
123
  };
113
124
  }
114
125
 
126
+ // FAST MODE: Skip cross-discussion rounds entirely
127
+ if (config.fastMode) {
128
+ onProgress?.({
129
+ type: 'round_complete',
130
+ message: 'Fast mode: Skipping cross-discussion, proceeding to synthesis',
131
+ timestamp: new Date().toISOString(),
132
+ });
133
+
134
+ return {
135
+ rounds,
136
+ participatingProviders: Array.from(participatingProviders),
137
+ failedProviders: Array.from(failedProviders),
138
+ totalDurationMs: Date.now() - startTime,
139
+ success: true,
140
+ earlyExit: {
141
+ triggered: true,
142
+ reason: 'Fast mode enabled - single round',
143
+ atProviderCount: participatingProviders.size,
144
+ },
145
+ };
146
+ }
147
+
115
148
  // Check for early exit after round 1 (INV-DISC-643)
116
149
  if (cascadingConfidence?.enabled) {
117
150
  const responsesWithConfidence = initialRound.round.responses.map(r => ({
@@ -149,6 +182,34 @@ export class SynthesisPattern implements PatternExecutor {
149
182
  }
150
183
  }
151
184
 
185
+ // ROUND-LEVEL EARLY EXIT: Check agreement after round 1
186
+ if (roundEarlyExit.enabled && rounds.length >= (roundEarlyExit.minRounds ?? 1)) {
187
+ const agreementScore = this.calculateRoundAgreement(initialRound.round.responses);
188
+ const threshold = roundEarlyExit.agreementThreshold ?? DEFAULT_ROUND_AGREEMENT_THRESHOLD;
189
+
190
+ if (agreementScore >= threshold) {
191
+ onProgress?.({
192
+ type: 'round_complete',
193
+ message: `Round early exit: High agreement detected (${(agreementScore * 100).toFixed(0)}% >= ${(threshold * 100).toFixed(0)}%)`,
194
+ timestamp: new Date().toISOString(),
195
+ });
196
+
197
+ return {
198
+ rounds,
199
+ participatingProviders: Array.from(participatingProviders),
200
+ failedProviders: Array.from(failedProviders),
201
+ totalDurationMs: Date.now() - startTime,
202
+ success: true,
203
+ earlyExit: {
204
+ triggered: true,
205
+ reason: `High agreement after round 1 (${(agreementScore * 100).toFixed(0)}%)`,
206
+ atProviderCount: participatingProviders.size,
207
+ confidenceScore: agreementScore,
208
+ },
209
+ };
210
+ }
211
+ }
212
+
152
213
  // Additional rounds: Cross-discussion
153
214
  for (let roundNum = 2; roundNum <= config.rounds; roundNum++) {
154
215
  if (abortSignal?.aborted) {
@@ -180,6 +241,11 @@ export class SynthesisPattern implements PatternExecutor {
180
241
  round: roundNum,
181
242
  message: `Cross-discussion round ${roundNum} complete`,
182
243
  timestamp: new Date().toISOString(),
244
+ // Extended fields for Phase 2 tracing
245
+ participatingProviders: providers.filter(p => !crossRound.failed.includes(p)),
246
+ failedProviders: crossRound.failed,
247
+ responseCount: crossRound.round.responses.length,
248
+ durationMs: crossRound.round.durationMs,
183
249
  });
184
250
 
185
251
  // Check for early exit after each round
@@ -297,6 +363,11 @@ export class SynthesisPattern implements PatternExecutor {
297
363
  provider: providerId,
298
364
  message: result.success ? 'completed' : `failed: ${result.error}`,
299
365
  timestamp: new Date().toISOString(),
366
+ // Extended fields for Phase 2 tracing
367
+ success: result.success,
368
+ durationMs: result.durationMs,
369
+ tokenCount: result.tokenCount,
370
+ error: result.success ? undefined : result.error,
300
371
  });
301
372
 
302
373
  return response;
@@ -405,6 +476,11 @@ export class SynthesisPattern implements PatternExecutor {
405
476
  round: roundNum,
406
477
  provider: providerId,
407
478
  timestamp: new Date().toISOString(),
479
+ // Extended fields for Phase 2 tracing
480
+ success: result.success,
481
+ durationMs: result.durationMs,
482
+ tokenCount: result.tokenCount,
483
+ error: result.success ? undefined : result.error,
408
484
  });
409
485
 
410
486
  return response;
@@ -436,4 +512,28 @@ export class SynthesisPattern implements PatternExecutor {
436
512
  failed,
437
513
  };
438
514
  }
515
+
516
+ /**
517
+ * Calculate agreement score for responses in a round.
518
+ * Uses semantic similarity and key phrase overlap to detect consensus.
519
+ */
520
+ private calculateRoundAgreement(responses: DiscussionProviderResponse[]): number {
521
+ if (responses.length < 2) {
522
+ return 1.0; // Single response = full agreement
523
+ }
524
+
525
+ const successfulResponses = responses.filter(r => !r.error && r.content.length > 0);
526
+ if (successfulResponses.length < 2) {
527
+ return 0.5; // Not enough responses to compare
528
+ }
529
+
530
+ // Use the calculateAgreementScore function from confidence-extractor
531
+ const responsesForScoring = successfulResponses.map(r => ({
532
+ provider: r.provider,
533
+ content: r.content,
534
+ confidence: r.confidence,
535
+ }));
536
+
537
+ return calculateAgreementScore(responsesForScoring);
538
+ }
439
539
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import {
15
15
  DEFAULT_PROVIDERS,
16
+ DEFAULT_PROVIDER_TIMEOUT,
16
17
  DiscussionErrorCodes,
17
18
  createFailedDiscussionResult,
18
19
  type DiscussStepConfig,
@@ -77,7 +78,7 @@ export class RecursiveDiscussionExecutor {
77
78
 
78
79
  constructor(options: RecursiveDiscussionExecutorOptions) {
79
80
  this.providerExecutor = options.providerExecutor;
80
- this.defaultTimeoutMs = options.defaultTimeoutMs ?? 60000;
81
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT;
81
82
  this.checkProviderHealth = options.checkProviderHealth ?? true;
82
83
  this.traceId = options.traceId;
83
84
  this.parentContext = options.parentContext;
@@ -141,6 +142,7 @@ export class RecursiveDiscussionExecutor {
141
142
  minProviders: 2,
142
143
  temperature: 0.7,
143
144
  agentWeightMultiplier: DEFAULT_AGENT_WEIGHT_MULTIPLIER,
145
+ fastMode: false,
144
146
  };
145
147
 
146
148
  return this.execute(config, options);
@@ -288,6 +290,7 @@ export class RecursiveDiscussionExecutor {
288
290
  temperature: 0.7,
289
291
  verbose: false,
290
292
  agentWeightMultiplier: DEFAULT_AGENT_WEIGHT_MULTIPLIER,
293
+ fastMode: true, // Sub-discussions use fast mode
291
294
  },
292
295
  abortSignal ? { abortSignal } : {}
293
296
  );
package/src/types.ts CHANGED
@@ -117,6 +117,27 @@ export interface CascadingConfidenceOptions {
117
117
  /**
118
118
  * Context passed to pattern executors
119
119
  */
120
+ /**
121
+ * Resolved participant for pattern execution (subset of full type)
122
+ * Full type is in participant-resolver.ts
123
+ */
124
+ export interface ResolvedParticipantLike {
125
+ /** Original participant identifier */
126
+ id: string;
127
+ /** Whether this is an agent (vs raw provider) */
128
+ isAgent: boolean;
129
+ /** Provider ID to use for LLM calls */
130
+ providerId: string;
131
+ /** Agent ID (if isAgent) */
132
+ agentId?: string | undefined;
133
+ /** System prompt override (from agent config) */
134
+ systemPromptOverride?: string | undefined;
135
+ /** Injected ability content */
136
+ abilityContent?: string | undefined;
137
+ /** Weight multiplier for consensus */
138
+ weightMultiplier: number;
139
+ }
140
+
120
141
  export interface PatternExecutionContext {
121
142
  /** The discussion configuration */
122
143
  config: DiscussStepConfig;
@@ -138,17 +159,46 @@ export interface PatternExecutionContext {
138
159
 
139
160
  /** Cascading confidence configuration for early exit */
140
161
  cascadingConfidence?: CascadingConfidenceOptions | undefined;
162
+
163
+ /** Resolved participants including agents (when participants array is used) */
164
+ resolvedParticipants?: ResolvedParticipantLike[] | undefined;
141
165
  }
142
166
 
143
167
  /**
144
168
  * Progress event during discussion execution
169
+ * Extended with detailed data for Phase 2 trace event emission
145
170
  */
146
171
  export interface DiscussionProgressEvent {
147
- type: 'round_start' | 'provider_start' | 'provider_complete' | 'round_complete' | 'synthesis_start' | 'synthesis_complete';
172
+ type: 'round_start' | 'provider_start' | 'provider_complete' | 'round_complete' | 'synthesis_start' | 'synthesis_complete' | 'consensus_complete';
148
173
  round?: number | undefined;
149
174
  provider?: string | undefined;
150
175
  message?: string | undefined;
151
176
  timestamp: string;
177
+ // Extended fields for granular tracing (Phase 2)
178
+ /** Provider response success status */
179
+ success?: boolean | undefined;
180
+ /** Duration in milliseconds */
181
+ durationMs?: number | undefined;
182
+ /** Token count for provider response */
183
+ tokenCount?: number | undefined;
184
+ /** Error message for failed responses */
185
+ error?: string | undefined;
186
+ /** Role in debate pattern */
187
+ role?: string | undefined;
188
+ /** Participating providers (for round_complete) */
189
+ participatingProviders?: string[] | undefined;
190
+ /** Failed providers (for round_complete) */
191
+ failedProviders?: string[] | undefined;
192
+ /** Response count (for round_complete) */
193
+ responseCount?: number | undefined;
194
+ /** Consensus method (for consensus_complete) */
195
+ consensusMethod?: string | undefined;
196
+ /** Agreement/confidence score (for consensus_complete) */
197
+ confidence?: number | undefined;
198
+ /** Vote counts (for voting consensus) */
199
+ votes?: Record<string, number> | undefined;
200
+ /** Winner (for voting consensus) */
201
+ winner?: string | undefined;
152
202
  }
153
203
 
154
204
  /**
@@ -283,6 +333,19 @@ export interface ConsensusExecutor {
283
333
  /**
284
334
  * Options for creating a discussion executor
285
335
  */
336
+ /**
337
+ * Options for participant resolution (subset of full type)
338
+ * Full type is in participant-resolver.ts
339
+ */
340
+ export interface ParticipantResolverOptionsLike {
341
+ /** Agent registry for looking up agents */
342
+ agentRegistry?: { get(agentId: string): Promise<{ agentId: string; systemPrompt?: string } | null> } | undefined;
343
+ /** Ability manager for injecting abilities */
344
+ abilityManager?: { injectAbilities(agentId: string, task: string, coreAbilities: string[], options: { maxAbilities: number; maxTokens: number }): Promise<{ combinedContent: string; injectedAbilities: string[] }> } | undefined;
345
+ /** Default provider to use when agent has no preference */
346
+ defaultProvider?: string | undefined;
347
+ }
348
+
286
349
  export interface DiscussionExecutorOptions {
287
350
  /** Provider executor implementation */
288
351
  providerExecutor: DiscussionProviderExecutor;
@@ -295,6 +358,12 @@ export interface DiscussionExecutorOptions {
295
358
 
296
359
  /** Trace ID for debugging */
297
360
  traceId?: string | undefined;
361
+
362
+ /** Options for resolving agent participants (INV-DISC-640, INV-DISC-641) */
363
+ participantResolverOptions?: ParticipantResolverOptionsLike | undefined;
364
+
365
+ /** Cascading confidence config for early exit (INV-DISC-622, INV-DISC-623) */
366
+ cascadingConfidence?: CascadingConfidenceOptions | undefined;
298
367
  }
299
368
 
300
369
  // ============================================================================