@defai.digital/discussion-domain 13.0.3 → 13.1.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.
- package/dist/budget-manager.d.ts +79 -0
- package/dist/budget-manager.d.ts.map +1 -0
- package/dist/budget-manager.js +155 -0
- package/dist/budget-manager.js.map +1 -0
- package/dist/confidence-extractor.d.ts +60 -0
- package/dist/confidence-extractor.d.ts.map +1 -0
- package/dist/confidence-extractor.js +251 -0
- package/dist/confidence-extractor.js.map +1 -0
- package/dist/consensus/synthesis.d.ts.map +1 -1
- package/dist/consensus/synthesis.js +2 -0
- package/dist/consensus/synthesis.js.map +1 -1
- package/dist/consensus/voting.d.ts +3 -0
- package/dist/consensus/voting.d.ts.map +1 -1
- package/dist/consensus/voting.js +15 -4
- package/dist/consensus/voting.js.map +1 -1
- package/dist/context-tracker.d.ts +77 -0
- package/dist/context-tracker.d.ts.map +1 -0
- package/dist/context-tracker.js +177 -0
- package/dist/context-tracker.js.map +1 -0
- package/dist/cost-tracker.d.ts +123 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +195 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +9 -0
- package/dist/executor.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/participant-resolver.d.ts +111 -0
- package/dist/participant-resolver.d.ts.map +1 -0
- package/dist/participant-resolver.js +160 -0
- package/dist/participant-resolver.js.map +1 -0
- package/dist/patterns/round-robin.d.ts +3 -0
- package/dist/patterns/round-robin.d.ts.map +1 -1
- package/dist/patterns/round-robin.js +41 -2
- package/dist/patterns/round-robin.js.map +1 -1
- package/dist/patterns/synthesis.d.ts +3 -0
- package/dist/patterns/synthesis.d.ts.map +1 -1
- package/dist/patterns/synthesis.js +77 -3
- package/dist/patterns/synthesis.js.map +1 -1
- package/dist/prompts/templates.d.ts +1 -0
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/prompts/templates.js +3 -1
- package/dist/prompts/templates.js.map +1 -1
- package/dist/provider-bridge.d.ts.map +1 -1
- package/dist/provider-bridge.js +31 -30
- package/dist/provider-bridge.js.map +1 -1
- package/dist/recursive-executor.d.ts +77 -0
- package/dist/recursive-executor.d.ts.map +1 -0
- package/dist/recursive-executor.js +344 -0
- package/dist/recursive-executor.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/budget-manager.ts +272 -0
- package/src/confidence-extractor.ts +321 -0
- package/src/consensus/synthesis.ts +2 -0
- package/src/consensus/voting.ts +22 -6
- package/src/context-tracker.ts +307 -0
- package/src/cost-tracker.ts +362 -0
- package/src/executor.ts +9 -0
- package/src/index.ts +72 -0
- package/src/participant-resolver.ts +297 -0
- package/src/patterns/round-robin.ts +48 -2
- package/src/patterns/synthesis.ts +89 -3
- package/src/prompts/templates.ts +4 -2
- package/src/provider-bridge.ts +31 -28
- package/src/recursive-executor.ts +500 -0
- 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,362 @@
|
|
|
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
|
+
return `$${(costUsd * 100).toFixed(2)}¢`;
|
|
333
|
+
}
|
|
334
|
+
return `$${costUsd.toFixed(4)}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Format cost summary for CLI/logs
|
|
339
|
+
*/
|
|
340
|
+
export function formatCostSummary(summary: CostSummary): string {
|
|
341
|
+
const lines: string[] = [
|
|
342
|
+
`Cost Summary:`,
|
|
343
|
+
` Total Calls: ${summary.totalCalls}`,
|
|
344
|
+
` Total Tokens: ${summary.totalInputTokens} in / ${summary.totalOutputTokens} out`,
|
|
345
|
+
` Estimated Cost: ${formatCost(summary.totalCostUsd)}`,
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
if (summary.remainingBudgetUsd !== undefined) {
|
|
349
|
+
lines.push(` Remaining Budget: ${formatCost(summary.remainingBudgetUsd)}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
lines.push(` Remaining Calls: ${summary.remainingCalls}`);
|
|
353
|
+
|
|
354
|
+
if (Object.keys(summary.byProvider).length > 0) {
|
|
355
|
+
lines.push(` By Provider:`);
|
|
356
|
+
for (const [provider, data] of Object.entries(summary.byProvider)) {
|
|
357
|
+
lines.push(` ${provider}: ${data.calls} calls, ${formatCost(data.costUsd)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return lines.join('\n');
|
|
362
|
+
}
|
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);
|