@defai.digital/discussion-domain 13.0.3 → 13.1.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 (73) hide show
  1. package/dist/budget-manager.d.ts +79 -0
  2. package/dist/budget-manager.d.ts.map +1 -0
  3. package/dist/budget-manager.js +155 -0
  4. package/dist/budget-manager.js.map +1 -0
  5. package/dist/confidence-extractor.d.ts +60 -0
  6. package/dist/confidence-extractor.d.ts.map +1 -0
  7. package/dist/confidence-extractor.js +251 -0
  8. package/dist/confidence-extractor.js.map +1 -0
  9. package/dist/consensus/synthesis.d.ts.map +1 -1
  10. package/dist/consensus/synthesis.js +2 -0
  11. package/dist/consensus/synthesis.js.map +1 -1
  12. package/dist/consensus/voting.d.ts +3 -0
  13. package/dist/consensus/voting.d.ts.map +1 -1
  14. package/dist/consensus/voting.js +15 -4
  15. package/dist/consensus/voting.js.map +1 -1
  16. package/dist/context-tracker.d.ts +77 -0
  17. package/dist/context-tracker.d.ts.map +1 -0
  18. package/dist/context-tracker.js +177 -0
  19. package/dist/context-tracker.js.map +1 -0
  20. package/dist/cost-tracker.d.ts +123 -0
  21. package/dist/cost-tracker.d.ts.map +1 -0
  22. package/dist/cost-tracker.js +196 -0
  23. package/dist/cost-tracker.js.map +1 -0
  24. package/dist/executor.d.ts.map +1 -1
  25. package/dist/executor.js +9 -0
  26. package/dist/executor.js.map +1 -1
  27. package/dist/index.d.ts +7 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/participant-resolver.d.ts +111 -0
  32. package/dist/participant-resolver.d.ts.map +1 -0
  33. package/dist/participant-resolver.js +160 -0
  34. package/dist/participant-resolver.js.map +1 -0
  35. package/dist/patterns/round-robin.d.ts +3 -0
  36. package/dist/patterns/round-robin.d.ts.map +1 -1
  37. package/dist/patterns/round-robin.js +41 -2
  38. package/dist/patterns/round-robin.js.map +1 -1
  39. package/dist/patterns/synthesis.d.ts +3 -0
  40. package/dist/patterns/synthesis.d.ts.map +1 -1
  41. package/dist/patterns/synthesis.js +77 -3
  42. package/dist/patterns/synthesis.js.map +1 -1
  43. package/dist/prompts/templates.d.ts +1 -0
  44. package/dist/prompts/templates.d.ts.map +1 -1
  45. package/dist/prompts/templates.js +3 -1
  46. package/dist/prompts/templates.js.map +1 -1
  47. package/dist/provider-bridge.d.ts +3 -1
  48. package/dist/provider-bridge.d.ts.map +1 -1
  49. package/dist/provider-bridge.js +48 -32
  50. package/dist/provider-bridge.js.map +1 -1
  51. package/dist/recursive-executor.d.ts +80 -0
  52. package/dist/recursive-executor.d.ts.map +1 -0
  53. package/dist/recursive-executor.js +354 -0
  54. package/dist/recursive-executor.js.map +1 -0
  55. package/dist/types.d.ts +83 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/budget-manager.ts +272 -0
  60. package/src/confidence-extractor.ts +321 -0
  61. package/src/consensus/synthesis.ts +2 -0
  62. package/src/consensus/voting.ts +22 -6
  63. package/src/context-tracker.ts +307 -0
  64. package/src/cost-tracker.ts +363 -0
  65. package/src/executor.ts +9 -0
  66. package/src/index.ts +72 -0
  67. package/src/participant-resolver.ts +297 -0
  68. package/src/patterns/round-robin.ts +48 -2
  69. package/src/patterns/synthesis.ts +89 -3
  70. package/src/prompts/templates.ts +4 -2
  71. package/src/provider-bridge.ts +52 -31
  72. package/src/recursive-executor.ts +510 -0
  73. package/src/types.ts +120 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Discussion Context Tracker
3
+ *
4
+ * Tracks recursive discussion state including depth, budget, and call counts.
5
+ * Prevents infinite recursion and enforces resource limits.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-600: Depth never exceeds maxDepth
9
+ * - INV-DISC-601: No circular discussions
10
+ * - INV-DISC-610: Child timeout ≤ parent remaining budget
11
+ * - INV-DISC-620: Total calls ≤ maxTotalCalls
12
+ */
13
+
14
+ import {
15
+ type DiscussionContext,
16
+ type TimeoutConfig,
17
+ type RecursiveConfig,
18
+ type CostControlConfig,
19
+ DiscussionErrorCodes,
20
+ createRootDiscussionContext,
21
+ createChildDiscussionContext,
22
+ canSpawnSubDiscussion,
23
+ getTimeoutForLevel,
24
+ DEFAULT_DISCUSSION_DEPTH,
25
+ DEFAULT_TOTAL_BUDGET_MS,
26
+ DEFAULT_MAX_TOTAL_CALLS,
27
+ MIN_SYNTHESIS_TIME_MS,
28
+ } from '@defai.digital/contracts';
29
+
30
+ /**
31
+ * Result of checking if sub-discussion can be spawned
32
+ */
33
+ export interface SubDiscussionCheck {
34
+ allowed: boolean;
35
+ reason?: string;
36
+ errorCode?: string;
37
+ availableBudgetMs?: number;
38
+ remainingCalls?: number;
39
+ }
40
+
41
+ /**
42
+ * Configuration for the context tracker
43
+ */
44
+ export interface ContextTrackerConfig {
45
+ recursive: RecursiveConfig;
46
+ timeout: TimeoutConfig;
47
+ cost: CostControlConfig;
48
+ }
49
+
50
+ /**
51
+ * Discussion context tracker for managing recursive discussion state
52
+ */
53
+ export interface DiscussionContextTracker {
54
+ /** Get current context */
55
+ getContext(): DiscussionContext;
56
+
57
+ /** Check if sub-discussion can be spawned */
58
+ canSpawnSubDiscussion(estimatedDurationMs?: number): SubDiscussionCheck;
59
+
60
+ /** Create child context for sub-discussion */
61
+ createChildContext(childDiscussionId: string): DiscussionContext;
62
+
63
+ /** Record provider calls made */
64
+ recordCalls(count: number): void;
65
+
66
+ /** Record time elapsed */
67
+ recordElapsed(elapsedMs: number): void;
68
+
69
+ /** Get timeout for current depth level */
70
+ getTimeoutForCurrentLevel(): number;
71
+
72
+ /** Get timeout for a specific depth level */
73
+ getTimeoutForLevel(depth: number): number;
74
+
75
+ /** Check if at root level */
76
+ isRoot(): boolean;
77
+
78
+ /** Get remaining depth capacity */
79
+ getRemainingDepth(): number;
80
+
81
+ /** Get remaining call capacity */
82
+ getRemainingCalls(): number;
83
+
84
+ /** Get remaining budget in milliseconds */
85
+ getRemainingBudgetMs(): number;
86
+
87
+ /** Get elapsed time since start */
88
+ getElapsedMs(): number;
89
+ }
90
+
91
+ /**
92
+ * Creates a discussion context tracker
93
+ */
94
+ export function createContextTracker(
95
+ discussionId: string,
96
+ config: Partial<ContextTrackerConfig> = {},
97
+ parentContext?: DiscussionContext
98
+ ): DiscussionContextTracker {
99
+ // Merge config with defaults
100
+ const recursiveConfig: RecursiveConfig = {
101
+ enabled: config.recursive?.enabled ?? false,
102
+ maxDepth: config.recursive?.maxDepth ?? DEFAULT_DISCUSSION_DEPTH,
103
+ allowedProviders: config.recursive?.allowedProviders,
104
+ allowSubDiscussions: config.recursive?.allowSubDiscussions ?? true,
105
+ };
106
+
107
+ const timeoutConfig: TimeoutConfig = {
108
+ strategy: config.timeout?.strategy ?? 'cascade',
109
+ totalBudgetMs: config.timeout?.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS,
110
+ minSynthesisMs: config.timeout?.minSynthesisMs ?? MIN_SYNTHESIS_TIME_MS,
111
+ levelTimeouts: config.timeout?.levelTimeouts,
112
+ };
113
+
114
+ const costConfig: CostControlConfig = {
115
+ maxTotalCalls: config.cost?.maxTotalCalls ?? DEFAULT_MAX_TOTAL_CALLS,
116
+ budgetUsd: config.cost?.budgetUsd,
117
+ cascadingConfidence: {
118
+ enabled: config.cost?.cascadingConfidence?.enabled ?? true,
119
+ threshold: config.cost?.cascadingConfidence?.threshold ?? 0.9,
120
+ minProviders: config.cost?.cascadingConfidence?.minProviders ?? 2,
121
+ },
122
+ };
123
+
124
+ // Initialize context
125
+ let context: DiscussionContext;
126
+
127
+ if (parentContext) {
128
+ // Create child context
129
+ const elapsed = Date.now() - new Date(parentContext.startedAt).getTime();
130
+ context = createChildDiscussionContext(parentContext, discussionId, elapsed, 0);
131
+ } else {
132
+ // Create root context
133
+ context = createRootDiscussionContext(discussionId, {
134
+ maxDepth: recursiveConfig.maxDepth,
135
+ totalBudgetMs: timeoutConfig.totalBudgetMs,
136
+ maxTotalCalls: costConfig.maxTotalCalls,
137
+ });
138
+ }
139
+
140
+ // Track mutable state
141
+ let totalCalls = context.totalCalls;
142
+ let elapsedSinceContextCreation = 0;
143
+
144
+ return {
145
+ getContext(): DiscussionContext {
146
+ return {
147
+ ...context,
148
+ totalCalls,
149
+ remainingBudgetMs: Math.max(0, context.remainingBudgetMs - elapsedSinceContextCreation),
150
+ };
151
+ },
152
+
153
+ canSpawnSubDiscussion(estimatedDurationMs?: number): SubDiscussionCheck {
154
+ // Check if recursion is enabled
155
+ if (!recursiveConfig.enabled) {
156
+ return {
157
+ allowed: false,
158
+ reason: 'Recursive discussions not enabled',
159
+ errorCode: DiscussionErrorCodes.INVALID_CONFIG,
160
+ };
161
+ }
162
+
163
+ // Check if sub-discussions allowed
164
+ if (!recursiveConfig.allowSubDiscussions) {
165
+ return {
166
+ allowed: false,
167
+ reason: 'Sub-discussions not allowed in configuration',
168
+ errorCode: DiscussionErrorCodes.INVALID_CONFIG,
169
+ };
170
+ }
171
+
172
+ // Use contract function for basic checks
173
+ const remainingBudget = context.remainingBudgetMs - elapsedSinceContextCreation;
174
+ const minBudget = estimatedDurationMs ?? timeoutConfig.minSynthesisMs * 2;
175
+
176
+ const checkContext: DiscussionContext = {
177
+ ...context,
178
+ totalCalls,
179
+ remainingBudgetMs: remainingBudget,
180
+ };
181
+
182
+ const check = canSpawnSubDiscussion(checkContext, minBudget);
183
+
184
+ if (!check.allowed) {
185
+ // Determine error code based on reason
186
+ const reason = check.reason ?? 'Sub-discussion not allowed';
187
+ let errorCode: string = DiscussionErrorCodes.INVALID_CONFIG;
188
+ if (reason.includes('depth')) {
189
+ errorCode = DiscussionErrorCodes.MAX_DEPTH_EXCEEDED;
190
+ } else if (reason.includes('budget')) {
191
+ errorCode = DiscussionErrorCodes.BUDGET_EXHAUSTED;
192
+ } else if (reason.includes('calls')) {
193
+ errorCode = DiscussionErrorCodes.MAX_CALLS_EXCEEDED;
194
+ }
195
+
196
+ return {
197
+ allowed: false,
198
+ reason,
199
+ errorCode,
200
+ availableBudgetMs: remainingBudget,
201
+ remainingCalls: context.maxTotalCalls - totalCalls,
202
+ };
203
+ }
204
+
205
+ return {
206
+ allowed: true,
207
+ availableBudgetMs: remainingBudget,
208
+ remainingCalls: context.maxTotalCalls - totalCalls,
209
+ };
210
+ },
211
+
212
+ createChildContext(childDiscussionId: string): DiscussionContext {
213
+ const elapsed = Date.now() - new Date(context.startedAt).getTime();
214
+ return createChildDiscussionContext(
215
+ { ...context, totalCalls },
216
+ childDiscussionId,
217
+ elapsed,
218
+ 0
219
+ );
220
+ },
221
+
222
+ recordCalls(count: number): void {
223
+ totalCalls += count;
224
+ },
225
+
226
+ recordElapsed(elapsedMs: number): void {
227
+ elapsedSinceContextCreation += elapsedMs;
228
+ },
229
+
230
+ getTimeoutForCurrentLevel(): number {
231
+ return getTimeoutForLevel(timeoutConfig, context.depth, context.maxDepth);
232
+ },
233
+
234
+ getTimeoutForLevel(depth: number): number {
235
+ return getTimeoutForLevel(timeoutConfig, depth, context.maxDepth);
236
+ },
237
+
238
+ isRoot(): boolean {
239
+ return context.depth === 0;
240
+ },
241
+
242
+ getRemainingDepth(): number {
243
+ return context.maxDepth - context.depth;
244
+ },
245
+
246
+ getRemainingCalls(): number {
247
+ return Math.max(0, context.maxTotalCalls - totalCalls);
248
+ },
249
+
250
+ getRemainingBudgetMs(): number {
251
+ return Math.max(0, context.remainingBudgetMs - elapsedSinceContextCreation);
252
+ },
253
+
254
+ getElapsedMs(): number {
255
+ return Date.now() - new Date(context.startedAt).getTime();
256
+ },
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Discussion context error
262
+ */
263
+ export class DiscussionContextError extends Error {
264
+ constructor(
265
+ public readonly code: string,
266
+ message: string,
267
+ public readonly context?: DiscussionContext
268
+ ) {
269
+ super(message);
270
+ this.name = 'DiscussionContextError';
271
+ }
272
+
273
+ static maxDepthExceeded(context: DiscussionContext): DiscussionContextError {
274
+ return new DiscussionContextError(
275
+ DiscussionErrorCodes.MAX_DEPTH_EXCEEDED,
276
+ `Maximum discussion depth ${context.maxDepth} exceeded at depth ${context.depth}`,
277
+ context
278
+ );
279
+ }
280
+
281
+ static circularDiscussion(
282
+ discussionId: string,
283
+ context: DiscussionContext
284
+ ): DiscussionContextError {
285
+ return new DiscussionContextError(
286
+ DiscussionErrorCodes.CIRCULAR_DISCUSSION,
287
+ `Circular discussion detected: ${discussionId} already in chain`,
288
+ context
289
+ );
290
+ }
291
+
292
+ static budgetExhausted(context: DiscussionContext): DiscussionContextError {
293
+ return new DiscussionContextError(
294
+ DiscussionErrorCodes.BUDGET_EXHAUSTED,
295
+ `Timeout budget exhausted: ${context.remainingBudgetMs}ms remaining`,
296
+ context
297
+ );
298
+ }
299
+
300
+ static maxCallsExceeded(context: DiscussionContext): DiscussionContextError {
301
+ return new DiscussionContextError(
302
+ DiscussionErrorCodes.MAX_CALLS_EXCEEDED,
303
+ `Maximum calls ${context.maxTotalCalls} exceeded`,
304
+ context
305
+ );
306
+ }
307
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Discussion Cost Tracker
3
+ *
4
+ * Tracks costs and resource usage across discussion execution.
5
+ * Enforces budget limits and provides visibility into spending.
6
+ *
7
+ * Invariants:
8
+ * - INV-DISC-620: Total calls ≤ maxTotalCalls
9
+ * - INV-DISC-621: Abort if cost budget exceeded
10
+ */
11
+
12
+ import type { CostControlConfig } from '@defai.digital/contracts';
13
+ import { DEFAULT_MAX_TOTAL_CALLS } from '@defai.digital/contracts';
14
+
15
+ /**
16
+ * Cost per provider (approximate, in USD per 1K tokens)
17
+ * These are rough estimates for planning purposes
18
+ */
19
+ export const PROVIDER_COSTS: Record<string, { input: number; output: number }> = {
20
+ claude: { input: 0.003, output: 0.015 },
21
+ gemini: { input: 0.0005, output: 0.0015 },
22
+ codex: { input: 0.003, output: 0.006 },
23
+ qwen: { input: 0.001, output: 0.002 },
24
+ glm: { input: 0.001, output: 0.002 },
25
+ grok: { input: 0.002, output: 0.004 },
26
+ };
27
+
28
+ /**
29
+ * Default cost per provider if not in the map
30
+ */
31
+ const DEFAULT_COST = { input: 0.002, output: 0.004 };
32
+
33
+ /**
34
+ * Provider call record
35
+ */
36
+ export interface ProviderCallRecord {
37
+ /** Provider ID */
38
+ providerId: string;
39
+
40
+ /** Input tokens */
41
+ inputTokens: number;
42
+
43
+ /** Output tokens */
44
+ outputTokens: number;
45
+
46
+ /** Duration in milliseconds */
47
+ durationMs: number;
48
+
49
+ /** Estimated cost in USD */
50
+ estimatedCostUsd: number;
51
+
52
+ /** Discussion depth where call occurred */
53
+ depth: number;
54
+
55
+ /** Timestamp */
56
+ timestamp: string;
57
+
58
+ /** Whether call succeeded */
59
+ success: boolean;
60
+ }
61
+
62
+ /**
63
+ * Cost tracking summary
64
+ */
65
+ export interface CostSummary {
66
+ /** Total provider calls made */
67
+ totalCalls: number;
68
+
69
+ /** Total input tokens */
70
+ totalInputTokens: number;
71
+
72
+ /** Total output tokens */
73
+ totalOutputTokens: number;
74
+
75
+ /** Total estimated cost in USD */
76
+ totalCostUsd: number;
77
+
78
+ /** Cost breakdown by provider */
79
+ byProvider: Record<string, {
80
+ calls: number;
81
+ inputTokens: number;
82
+ outputTokens: number;
83
+ costUsd: number;
84
+ }>;
85
+
86
+ /** Cost breakdown by depth level */
87
+ byDepth: Record<number, {
88
+ calls: number;
89
+ costUsd: number;
90
+ }>;
91
+
92
+ /** Whether budget limit was exceeded */
93
+ budgetExceeded: boolean;
94
+
95
+ /** Whether call limit was exceeded */
96
+ callLimitExceeded: boolean;
97
+
98
+ /** Remaining budget (if configured) */
99
+ remainingBudgetUsd: number | undefined;
100
+
101
+ /** Remaining calls */
102
+ remainingCalls: number;
103
+ }
104
+
105
+ /**
106
+ * Budget check result
107
+ */
108
+ export interface BudgetCheckResult {
109
+ /** Whether operation is allowed */
110
+ allowed: boolean;
111
+
112
+ /** Reason if not allowed */
113
+ reason?: string;
114
+
115
+ /** Current usage metrics */
116
+ currentCalls: number;
117
+ currentCostUsd: number;
118
+
119
+ /** Estimated cost of proposed operation */
120
+ estimatedCostUsd?: number;
121
+ }
122
+
123
+ /**
124
+ * Discussion cost tracker interface
125
+ */
126
+ export interface DiscussionCostTracker {
127
+ /** Record a provider call */
128
+ recordCall(record: Omit<ProviderCallRecord, 'estimatedCostUsd' | 'timestamp'>): void;
129
+
130
+ /** Check if a call is within budget */
131
+ checkBudget(providerId: string, estimatedTokens?: number): BudgetCheckResult;
132
+
133
+ /** Get current cost summary */
134
+ getSummary(): CostSummary;
135
+
136
+ /** Get total calls made */
137
+ getTotalCalls(): number;
138
+
139
+ /** Get remaining calls */
140
+ getRemainingCalls(): number;
141
+
142
+ /** Check if budget exceeded */
143
+ isBudgetExceeded(): boolean;
144
+
145
+ /** Check if call limit exceeded */
146
+ isCallLimitExceeded(): boolean;
147
+
148
+ /** Estimate cost for a provider call */
149
+ estimateCost(providerId: string, inputTokens: number, outputTokens: number): number;
150
+
151
+ /** Create child tracker for sub-discussion */
152
+ createChildTracker(depth: number): DiscussionCostTracker;
153
+ }
154
+
155
+ /**
156
+ * Create a cost tracker for a discussion
157
+ */
158
+ export function createCostTracker(
159
+ config: CostControlConfig,
160
+ parentTracker?: DiscussionCostTracker
161
+ ): DiscussionCostTracker {
162
+ const calls: ProviderCallRecord[] = [];
163
+ const maxTotalCalls = config.maxTotalCalls ?? DEFAULT_MAX_TOTAL_CALLS;
164
+ const budgetUsd = config.budgetUsd;
165
+
166
+ // Track inherited calls from parent
167
+ const parentCalls = parentTracker?.getTotalCalls() ?? 0;
168
+
169
+ return {
170
+ recordCall(record): void {
171
+ const cost = estimateCallCost(
172
+ record.providerId,
173
+ record.inputTokens,
174
+ record.outputTokens
175
+ );
176
+
177
+ calls.push({
178
+ ...record,
179
+ estimatedCostUsd: cost,
180
+ timestamp: new Date().toISOString(),
181
+ });
182
+ },
183
+
184
+ checkBudget(providerId, estimatedTokens = 1000): BudgetCheckResult {
185
+ const currentCalls = calls.length + parentCalls;
186
+ const currentCost = calls.reduce((sum, c) => sum + c.estimatedCostUsd, 0);
187
+
188
+ // Check call limit
189
+ if (currentCalls >= maxTotalCalls) {
190
+ return {
191
+ allowed: false,
192
+ reason: `Call limit ${maxTotalCalls} reached`,
193
+ currentCalls,
194
+ currentCostUsd: currentCost,
195
+ };
196
+ }
197
+
198
+ // Check budget limit if configured
199
+ if (budgetUsd !== undefined) {
200
+ const estimatedCost = estimateCallCost(providerId, estimatedTokens, estimatedTokens);
201
+ if (currentCost + estimatedCost > budgetUsd) {
202
+ return {
203
+ allowed: false,
204
+ reason: `Budget limit $${budgetUsd.toFixed(2)} would be exceeded`,
205
+ currentCalls,
206
+ currentCostUsd: currentCost,
207
+ estimatedCostUsd: estimatedCost,
208
+ };
209
+ }
210
+ }
211
+
212
+ return {
213
+ allowed: true,
214
+ currentCalls,
215
+ currentCostUsd: currentCost,
216
+ };
217
+ },
218
+
219
+ getSummary(): CostSummary {
220
+ const byProvider: CostSummary['byProvider'] = {};
221
+ const byDepth: CostSummary['byDepth'] = {};
222
+
223
+ let totalInputTokens = 0;
224
+ let totalOutputTokens = 0;
225
+ let totalCostUsd = 0;
226
+
227
+ for (const call of calls) {
228
+ // Aggregate by provider
229
+ if (!byProvider[call.providerId]) {
230
+ byProvider[call.providerId] = {
231
+ calls: 0,
232
+ inputTokens: 0,
233
+ outputTokens: 0,
234
+ costUsd: 0,
235
+ };
236
+ }
237
+ const providerStats = byProvider[call.providerId]!;
238
+ providerStats.calls++;
239
+ providerStats.inputTokens += call.inputTokens;
240
+ providerStats.outputTokens += call.outputTokens;
241
+ providerStats.costUsd += call.estimatedCostUsd;
242
+
243
+ // Aggregate by depth
244
+ if (!byDepth[call.depth]) {
245
+ byDepth[call.depth] = { calls: 0, costUsd: 0 };
246
+ }
247
+ const depthStats = byDepth[call.depth]!;
248
+ depthStats.calls++;
249
+ depthStats.costUsd += call.estimatedCostUsd;
250
+
251
+ // Totals
252
+ totalInputTokens += call.inputTokens;
253
+ totalOutputTokens += call.outputTokens;
254
+ totalCostUsd += call.estimatedCostUsd;
255
+ }
256
+
257
+ const totalCalls = calls.length + parentCalls;
258
+
259
+ return {
260
+ totalCalls,
261
+ totalInputTokens,
262
+ totalOutputTokens,
263
+ totalCostUsd,
264
+ byProvider,
265
+ byDepth,
266
+ budgetExceeded: budgetUsd !== undefined && totalCostUsd > budgetUsd,
267
+ callLimitExceeded: totalCalls >= maxTotalCalls,
268
+ remainingBudgetUsd: budgetUsd !== undefined ? Math.max(0, budgetUsd - totalCostUsd) : undefined,
269
+ remainingCalls: Math.max(0, maxTotalCalls - totalCalls),
270
+ };
271
+ },
272
+
273
+ getTotalCalls(): number {
274
+ return calls.length + parentCalls;
275
+ },
276
+
277
+ getRemainingCalls(): number {
278
+ return Math.max(0, maxTotalCalls - (calls.length + parentCalls));
279
+ },
280
+
281
+ isBudgetExceeded(): boolean {
282
+ if (budgetUsd === undefined) return false;
283
+ const totalCost = calls.reduce((sum, c) => sum + c.estimatedCostUsd, 0);
284
+ return totalCost > budgetUsd;
285
+ },
286
+
287
+ isCallLimitExceeded(): boolean {
288
+ return calls.length + parentCalls > maxTotalCalls;
289
+ },
290
+
291
+ estimateCost(providerId, inputTokens, outputTokens): number {
292
+ return estimateCallCost(providerId, inputTokens, outputTokens);
293
+ },
294
+
295
+ createChildTracker(depth: number): DiscussionCostTracker {
296
+ // Child tracker inherits call count from parent
297
+ const childConfig = { ...config };
298
+ const childTracker = createCostTracker(childConfig, this);
299
+
300
+ // Wrapper to set depth on all recorded calls
301
+ const originalRecord = childTracker.recordCall.bind(childTracker);
302
+ childTracker.recordCall = (record) => {
303
+ originalRecord({ ...record, depth });
304
+ };
305
+
306
+ return childTracker;
307
+ },
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Estimate cost for a provider call
313
+ */
314
+ function estimateCallCost(
315
+ providerId: string,
316
+ inputTokens: number,
317
+ outputTokens: number
318
+ ): number {
319
+ const costs = PROVIDER_COSTS[providerId] || DEFAULT_COST;
320
+
321
+ const inputCost = (inputTokens / 1000) * costs.input;
322
+ const outputCost = (outputTokens / 1000) * costs.output;
323
+
324
+ return inputCost + outputCost;
325
+ }
326
+
327
+ /**
328
+ * Format cost for display
329
+ */
330
+ export function formatCost(costUsd: number): string {
331
+ if (costUsd < 0.01) {
332
+ // Display in cents without dollar sign
333
+ return `${(costUsd * 100).toFixed(2)}¢`;
334
+ }
335
+ return `$${costUsd.toFixed(4)}`;
336
+ }
337
+
338
+ /**
339
+ * Format cost summary for CLI/logs
340
+ */
341
+ export function formatCostSummary(summary: CostSummary): string {
342
+ const lines: string[] = [
343
+ `Cost Summary:`,
344
+ ` Total Calls: ${summary.totalCalls}`,
345
+ ` Total Tokens: ${summary.totalInputTokens} in / ${summary.totalOutputTokens} out`,
346
+ ` Estimated Cost: ${formatCost(summary.totalCostUsd)}`,
347
+ ];
348
+
349
+ if (summary.remainingBudgetUsd !== undefined) {
350
+ lines.push(` Remaining Budget: ${formatCost(summary.remainingBudgetUsd)}`);
351
+ }
352
+
353
+ lines.push(` Remaining Calls: ${summary.remainingCalls}`);
354
+
355
+ if (Object.keys(summary.byProvider).length > 0) {
356
+ lines.push(` By Provider:`);
357
+ for (const [provider, data] of Object.entries(summary.byProvider)) {
358
+ lines.push(` ${provider}: ${data.calls} calls, ${formatCost(data.costUsd)}`);
359
+ }
360
+ }
361
+
362
+ return lines.join('\n');
363
+ }
package/src/executor.ts CHANGED
@@ -75,6 +75,7 @@ export class DiscussionExecutor {
75
75
  continueOnProviderFailure: true,
76
76
  minProviders: 2,
77
77
  temperature: 0.7,
78
+ agentWeightMultiplier: 1.5,
78
79
  };
79
80
 
80
81
  return this.execute(config, options);
@@ -141,6 +142,7 @@ export class DiscussionExecutor {
141
142
  abortSignal,
142
143
  traceId: this.traceId,
143
144
  onProgress,
145
+ // No cascading confidence for base executor (recursive executor handles this)
144
146
  };
145
147
 
146
148
  // Execute pattern
@@ -164,6 +166,7 @@ export class DiscussionExecutor {
164
166
  rounds: patternResult.rounds,
165
167
  participatingProviders: patternResult.participatingProviders,
166
168
  config: config.consensus,
169
+ agentWeightMultiplier: config.agentWeightMultiplier,
167
170
  providerExecutor: this.providerExecutor,
168
171
  abortSignal,
169
172
  onProgress,
@@ -195,6 +198,10 @@ export class DiscussionExecutor {
195
198
  startedAt,
196
199
  completedAt: new Date().toISOString(),
197
200
  traceId: this.traceId,
201
+ // Include early exit info if pattern triggered it
202
+ ...(patternResult.earlyExit?.triggered ? {
203
+ earlyExit: patternResult.earlyExit,
204
+ } : {}),
198
205
  },
199
206
  };
200
207
 
@@ -284,6 +291,7 @@ export class DiscussionExecutor {
284
291
  continueOnProviderFailure: false, // Debates require all participants
285
292
  minProviders: 3,
286
293
  temperature: 0.7,
294
+ agentWeightMultiplier: 1.5,
287
295
  };
288
296
 
289
297
  return this.execute(config, options);
@@ -318,6 +326,7 @@ export class DiscussionExecutor {
318
326
  continueOnProviderFailure: true,
319
327
  minProviders: 2,
320
328
  temperature: 0.5, // Lower temperature for more consistent voting
329
+ agentWeightMultiplier: 1.5,
321
330
  };
322
331
 
323
332
  return this.execute(config, options);