myshell-tools 1.0.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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/data/orchestrator.json +113 -0
- package/package.json +49 -0
- package/src/auth/recovery.mjs +328 -0
- package/src/auth/refresh.mjs +373 -0
- package/src/chef.mjs +348 -0
- package/src/cli/doctor.mjs +568 -0
- package/src/cli/reset.mjs +447 -0
- package/src/cli/status.mjs +379 -0
- package/src/cli.mjs +429 -0
- package/src/commands/doctor.mjs +375 -0
- package/src/commands/help.mjs +324 -0
- package/src/commands/status.mjs +331 -0
- package/src/monitor/health.mjs +486 -0
- package/src/monitor/performance.mjs +442 -0
- package/src/monitor/report.mjs +535 -0
- package/src/orchestrator/classify.mjs +391 -0
- package/src/orchestrator/confidence.mjs +151 -0
- package/src/orchestrator/handoffs.mjs +231 -0
- package/src/orchestrator/review.mjs +222 -0
- package/src/providers/balance.mjs +201 -0
- package/src/providers/claude.mjs +236 -0
- package/src/providers/codex.mjs +255 -0
- package/src/providers/detect.mjs +185 -0
- package/src/providers/errors.mjs +373 -0
- package/src/providers/select.mjs +162 -0
- package/src/repl-enhanced.mjs +417 -0
- package/src/repl.mjs +321 -0
- package/src/state/archive.mjs +366 -0
- package/src/state/atomic.mjs +116 -0
- package/src/state/cleanup.mjs +440 -0
- package/src/state/recovery.mjs +461 -0
- package/src/state/session.mjs +147 -0
- package/src/ui/errors.mjs +456 -0
- package/src/ui/formatter.mjs +327 -0
- package/src/ui/icons.mjs +318 -0
- package/src/ui/progress.mjs +468 -0
- package/templates/prompts/confidence-format.txt +14 -0
- package/templates/prompts/ic-with-feedback.txt +41 -0
- package/templates/prompts/ic.txt +13 -0
- package/templates/prompts/manager-review.txt +40 -0
- package/templates/prompts/manager.txt +14 -0
- package/templates/prompts/worker.txt +12 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.mjs — Comprehensive error handling and recovery for provider operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { handleAuthFailure, getRecoverySuggestions } from '../auth/recovery.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom error class for CLI operations
|
|
10
|
+
*/
|
|
11
|
+
export class CliError extends Error {
|
|
12
|
+
constructor(message, details = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'CliError';
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.isRecoverable = details.isRecoverable || false;
|
|
17
|
+
this.provider = details.provider;
|
|
18
|
+
this.command = details.command;
|
|
19
|
+
this.args = details.args;
|
|
20
|
+
this.exitCode = details.exitCode;
|
|
21
|
+
this.stderr = details.stderr;
|
|
22
|
+
this.originalError = details.originalError;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detailed error for subprocess failures
|
|
28
|
+
*/
|
|
29
|
+
export class DetailedCliError extends CliError {
|
|
30
|
+
constructor(error, command, args, provider) {
|
|
31
|
+
const details = {
|
|
32
|
+
command,
|
|
33
|
+
args: args?.slice() || [],
|
|
34
|
+
provider,
|
|
35
|
+
originalError: error,
|
|
36
|
+
isRecoverable: isRecoverableError(error),
|
|
37
|
+
exitCode: error.status || error.code || -1,
|
|
38
|
+
stderr: error.stderr || error.message || ''
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let message = `${provider?.toUpperCase() || 'CLI'} command failed`;
|
|
42
|
+
|
|
43
|
+
if (command && args) {
|
|
44
|
+
message += `: ${command} ${args.join(' ')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error.message) {
|
|
48
|
+
message += ` - ${error.message}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
super(message, details);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if an error is recoverable with retries
|
|
57
|
+
*/
|
|
58
|
+
export function isRecoverableError(error) {
|
|
59
|
+
if (!error) return false;
|
|
60
|
+
|
|
61
|
+
const errorString = error.toString().toLowerCase();
|
|
62
|
+
const stderr = error.stderr?.toLowerCase() || '';
|
|
63
|
+
const combined = errorString + ' ' + stderr;
|
|
64
|
+
|
|
65
|
+
// Network-related errors (recoverable)
|
|
66
|
+
if (combined.includes('timeout') ||
|
|
67
|
+
combined.includes('network') ||
|
|
68
|
+
combined.includes('enotfound') ||
|
|
69
|
+
combined.includes('econnreset') ||
|
|
70
|
+
combined.includes('econnrefused')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Temporary server issues (recoverable)
|
|
75
|
+
if (combined.includes('502') ||
|
|
76
|
+
combined.includes('503') ||
|
|
77
|
+
combined.includes('504') ||
|
|
78
|
+
combined.includes('internal server error')) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Rate limiting (recoverable with delay)
|
|
83
|
+
if (combined.includes('rate limit') ||
|
|
84
|
+
combined.includes('429') ||
|
|
85
|
+
combined.includes('too many requests')) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Temporary auth token issues (recoverable with refresh)
|
|
90
|
+
if (combined.includes('token expired') ||
|
|
91
|
+
combined.includes('invalid token') ||
|
|
92
|
+
combined.includes('token not found')) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process spawning issues (sometimes recoverable)
|
|
97
|
+
if (error.code === 'EAGAIN' || error.code === 'EMFILE') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sleep with jitter for backoff
|
|
106
|
+
*/
|
|
107
|
+
function sleep(ms) {
|
|
108
|
+
const jitter = Math.floor(Math.random() * (ms * 0.1)); // 10% jitter
|
|
109
|
+
return new Promise(resolve => setTimeout(resolve, ms + jitter));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Robust subprocess execution with retry and recovery
|
|
114
|
+
*/
|
|
115
|
+
export async function executeWithRecovery(command, args, options = {}) {
|
|
116
|
+
const {
|
|
117
|
+
provider,
|
|
118
|
+
maxRetries = 3,
|
|
119
|
+
timeoutMs = 120000,
|
|
120
|
+
backoffMs = [1000, 2000, 4000],
|
|
121
|
+
cwd = process.cwd(),
|
|
122
|
+
onRetry
|
|
123
|
+
} = options;
|
|
124
|
+
|
|
125
|
+
let lastError;
|
|
126
|
+
|
|
127
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const proc = spawnSync(command, args, {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
timeout: timeoutMs,
|
|
133
|
+
cwd,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Success case
|
|
137
|
+
if (proc.status === 0) {
|
|
138
|
+
return proc;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create detailed error for failed command
|
|
142
|
+
const error = new Error(`Process exited with code ${proc.status}`);
|
|
143
|
+
error.status = proc.status;
|
|
144
|
+
error.stderr = proc.stderr;
|
|
145
|
+
lastError = new DetailedCliError(error, command, args, provider);
|
|
146
|
+
|
|
147
|
+
// Check if this is an auth error
|
|
148
|
+
if (isAuthError(proc.stderr)) {
|
|
149
|
+
const recovery = await handleAuthFailure(provider, proc.stderr);
|
|
150
|
+
if (recovery.recovered) {
|
|
151
|
+
continue; // Retry after successful recovery
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if recoverable
|
|
156
|
+
if (!isRecoverableError(lastError)) {
|
|
157
|
+
throw lastError;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Backoff before retry
|
|
161
|
+
if (attempt < maxRetries - 1) {
|
|
162
|
+
const backoff = backoffMs[Math.min(attempt, backoffMs.length - 1)];
|
|
163
|
+
if (onRetry) {
|
|
164
|
+
onRetry(attempt + 1, lastError, backoff);
|
|
165
|
+
}
|
|
166
|
+
await sleep(backoff);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (spawnError) {
|
|
170
|
+
lastError = new DetailedCliError(spawnError, command, args, provider);
|
|
171
|
+
|
|
172
|
+
// Handle specific spawn errors
|
|
173
|
+
if (spawnError.code === 'ENOENT') {
|
|
174
|
+
// CLI not found - not recoverable with retries
|
|
175
|
+
throw lastError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!isRecoverableError(spawnError) || attempt === maxRetries - 1) {
|
|
179
|
+
throw lastError;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Retry for recoverable spawn errors
|
|
183
|
+
if (attempt < maxRetries - 1) {
|
|
184
|
+
const backoff = backoffMs[Math.min(attempt, backoffMs.length - 1)];
|
|
185
|
+
if (onRetry) {
|
|
186
|
+
onRetry(attempt + 1, lastError, backoff);
|
|
187
|
+
}
|
|
188
|
+
await sleep(backoff);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw lastError;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if error is authentication-related
|
|
198
|
+
*/
|
|
199
|
+
function isAuthError(stderr) {
|
|
200
|
+
if (!stderr) return false;
|
|
201
|
+
const lower = stderr.toLowerCase();
|
|
202
|
+
|
|
203
|
+
return lower.includes('authentication') ||
|
|
204
|
+
lower.includes('unauthorized') ||
|
|
205
|
+
lower.includes('401') ||
|
|
206
|
+
lower.includes('invalid credentials') ||
|
|
207
|
+
lower.includes('login required') ||
|
|
208
|
+
lower.includes('access denied') ||
|
|
209
|
+
lower.includes('forbidden') ||
|
|
210
|
+
lower.includes('token') && (lower.includes('expired') || lower.includes('invalid'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if error is a rate limit
|
|
215
|
+
*/
|
|
216
|
+
function isRateLimitError(stderr) {
|
|
217
|
+
if (!stderr) return false;
|
|
218
|
+
const lower = stderr.toLowerCase();
|
|
219
|
+
|
|
220
|
+
return lower.includes('rate limit') ||
|
|
221
|
+
lower.includes('429') ||
|
|
222
|
+
lower.includes('too many requests') ||
|
|
223
|
+
lower.includes('quota') && lower.includes('exceeded');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parse and classify CLI output for better error handling
|
|
228
|
+
*/
|
|
229
|
+
export function parseCliOutput(stdout, stderr, exitCode) {
|
|
230
|
+
const result = {
|
|
231
|
+
success: exitCode === 0,
|
|
232
|
+
output: stdout || '',
|
|
233
|
+
stderr: stderr || '',
|
|
234
|
+
exitCode,
|
|
235
|
+
error: null,
|
|
236
|
+
errorType: null,
|
|
237
|
+
isRecoverable: false,
|
|
238
|
+
suggestions: []
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (exitCode !== 0 || stderr) {
|
|
242
|
+
const errorText = stderr || 'Process failed';
|
|
243
|
+
result.error = errorText;
|
|
244
|
+
result.isRecoverable = isRecoverableError({ stderr, status: exitCode });
|
|
245
|
+
|
|
246
|
+
// Classify error type
|
|
247
|
+
if (isAuthError(stderr)) {
|
|
248
|
+
result.errorType = 'auth';
|
|
249
|
+
result.suggestions.push('Re-authenticate with the CLI');
|
|
250
|
+
} else if (isRateLimitError(stderr)) {
|
|
251
|
+
result.errorType = 'rate_limit';
|
|
252
|
+
result.suggestions.push('Wait before retrying');
|
|
253
|
+
result.suggestions.push('Consider using a different model tier');
|
|
254
|
+
} else if (stderr.toLowerCase().includes('timeout')) {
|
|
255
|
+
result.errorType = 'timeout';
|
|
256
|
+
result.suggestions.push('Increase timeout or simplify the request');
|
|
257
|
+
} else if (stderr.toLowerCase().includes('network')) {
|
|
258
|
+
result.errorType = 'network';
|
|
259
|
+
result.suggestions.push('Check internet connection');
|
|
260
|
+
} else if (exitCode === 127 || stderr.toLowerCase().includes('command not found')) {
|
|
261
|
+
result.errorType = 'cli_missing';
|
|
262
|
+
result.suggestions.push('Install the required CLI tool');
|
|
263
|
+
} else {
|
|
264
|
+
result.errorType = 'unknown';
|
|
265
|
+
result.suggestions = getRecoverySuggestions(stderr);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create user-friendly error message
|
|
274
|
+
*/
|
|
275
|
+
export function createFriendlyErrorMessage(error, provider) {
|
|
276
|
+
let message = `❌ ${provider?.toUpperCase() || 'CLI'} Error`;
|
|
277
|
+
|
|
278
|
+
if (error instanceof CliError) {
|
|
279
|
+
switch (error.details.errorType || 'unknown') {
|
|
280
|
+
case 'cli_missing':
|
|
281
|
+
message += '\n\n📦 CLI not installed or not found in PATH';
|
|
282
|
+
if (provider === 'claude') {
|
|
283
|
+
message += '\n Install: pip install anthropic-cli';
|
|
284
|
+
message += '\n Then: claude auth login';
|
|
285
|
+
} else if (provider === 'codex') {
|
|
286
|
+
message += '\n Install: npm install -g @openai/codex';
|
|
287
|
+
message += '\n Then: codex login';
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
case 'auth':
|
|
292
|
+
message += '\n\n🔐 Authentication failed';
|
|
293
|
+
message += `\n Run: ${provider === 'claude' ? 'claude auth login' : 'codex login'}`;
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'rate_limit':
|
|
297
|
+
message += '\n\n⏱️ Rate limit exceeded';
|
|
298
|
+
message += '\n Wait a few minutes before retrying';
|
|
299
|
+
message += '\n Consider using a different model tier';
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'timeout':
|
|
303
|
+
message += '\n\n⏰ Request timed out';
|
|
304
|
+
message += '\n Try simplifying your request or increasing timeout';
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case 'network':
|
|
308
|
+
message += '\n\n🌐 Network connectivity issue';
|
|
309
|
+
message += '\n Check your internet connection and try again';
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
default:
|
|
313
|
+
message += '\n\n💥 Unexpected error occurred';
|
|
314
|
+
if (error.message) {
|
|
315
|
+
message += `\n ${error.message}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (error.details.suggestions?.length > 0) {
|
|
320
|
+
message += '\n\n💡 Suggestions:';
|
|
321
|
+
for (const suggestion of error.details.suggestions) {
|
|
322
|
+
message += `\n • ${suggestion}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (error.details.isRecoverable) {
|
|
327
|
+
message += '\n\n🔄 This error might be temporary. Cortex will retry automatically.';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
} else {
|
|
331
|
+
message += `\n ${error.message || 'Unknown error occurred'}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return message;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Graceful error handling wrapper for provider functions
|
|
339
|
+
*/
|
|
340
|
+
export function withErrorHandling(fn, provider) {
|
|
341
|
+
return async (...args) => {
|
|
342
|
+
try {
|
|
343
|
+
return await fn(...args);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (error instanceof CliError) {
|
|
346
|
+
throw error; // Already handled
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Convert generic errors to CLI errors
|
|
350
|
+
const cliError = new CliError(error.message || 'Operation failed', {
|
|
351
|
+
provider,
|
|
352
|
+
originalError: error,
|
|
353
|
+
isRecoverable: isRecoverableError(error)
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
throw cliError;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Progress callback for retries
|
|
363
|
+
*/
|
|
364
|
+
export function defaultRetryCallback(attempt, error, backoffMs) {
|
|
365
|
+
const provider = error.provider?.toUpperCase() || 'CLI';
|
|
366
|
+
console.log(`⚠️ ${provider} attempt ${attempt} failed, retrying in ${backoffMs}ms...`);
|
|
367
|
+
|
|
368
|
+
if (error.details?.errorType === 'rate_limit') {
|
|
369
|
+
console.log(' Rate limit detected, backing off...');
|
|
370
|
+
} else if (error.details?.errorType === 'network') {
|
|
371
|
+
console.log(' Network issue detected, retrying...');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* select.mjs — Intelligent provider selection and load balancing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getRecentHandoffs, getHandoffStats } from '../orchestrator/handoffs.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Select the best provider for a task based on load balancing and context
|
|
9
|
+
*/
|
|
10
|
+
export function selectProvider(tier, context = {}) {
|
|
11
|
+
const { availableModels, sessionId } = context;
|
|
12
|
+
|
|
13
|
+
if (!availableModels) {
|
|
14
|
+
console.warn('No available models provided to selectProvider');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const tierModels = availableModels[tier] || [];
|
|
19
|
+
if (tierModels.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If only one provider available, use it
|
|
24
|
+
if (tierModels.length === 1) {
|
|
25
|
+
return tierModels[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Get load balancing data
|
|
29
|
+
const loadBalance = getProviderLoadBalance(sessionId);
|
|
30
|
+
const modelStrengths = getModelStrengths(tier);
|
|
31
|
+
|
|
32
|
+
// Score each available model
|
|
33
|
+
const scoredModels = tierModels.map(model => {
|
|
34
|
+
let score = 0;
|
|
35
|
+
|
|
36
|
+
// Base score from model strength for this tier
|
|
37
|
+
score += modelStrengths[model.provider] || 0.5;
|
|
38
|
+
|
|
39
|
+
// Load balancing bonus (prefer less-used providers)
|
|
40
|
+
const providerLoad = loadBalance[model.provider] || 0;
|
|
41
|
+
const avgLoad = Object.values(loadBalance).reduce((sum, load) => sum + load, 0) / Object.keys(loadBalance).length || 0;
|
|
42
|
+
|
|
43
|
+
if (providerLoad < avgLoad - 2) {
|
|
44
|
+
score += 0.3; // Bonus for underused provider
|
|
45
|
+
} else if (providerLoad > avgLoad + 2) {
|
|
46
|
+
score -= 0.2; // Penalty for overused provider
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prefer models that haven't failed recently
|
|
50
|
+
const recentFailures = getRecentFailures(model.provider, 0.5); // Last 30 min
|
|
51
|
+
score -= recentFailures * 0.1;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...model,
|
|
55
|
+
score,
|
|
56
|
+
load: providerLoad,
|
|
57
|
+
failures: recentFailures
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Sort by score (highest first) and return best
|
|
62
|
+
scoredModels.sort((a, b) => b.score - a.score);
|
|
63
|
+
|
|
64
|
+
const selected = scoredModels[0];
|
|
65
|
+
|
|
66
|
+
console.log(` 🎯 Provider selection for ${tier}: ${selected.provider}/${selected.model} (score: ${selected.score.toFixed(2)})`);
|
|
67
|
+
|
|
68
|
+
return selected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get provider load balance statistics
|
|
73
|
+
*/
|
|
74
|
+
function getProviderLoadBalance(sessionId, timeWindowHours = 1) {
|
|
75
|
+
const handoffs = getRecentHandoffs(timeWindowHours, sessionId);
|
|
76
|
+
|
|
77
|
+
const loadBalance = {};
|
|
78
|
+
|
|
79
|
+
for (const handoff of handoffs) {
|
|
80
|
+
if (handoff.provider_from) {
|
|
81
|
+
loadBalance[handoff.provider_from] = (loadBalance[handoff.provider_from] || 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
if (handoff.provider_to) {
|
|
84
|
+
loadBalance[handoff.provider_to] = (loadBalance[handoff.provider_to] || 0) + 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return loadBalance;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get model strengths by tier based on empirical performance
|
|
93
|
+
*/
|
|
94
|
+
function getModelStrengths(tier) {
|
|
95
|
+
const strengths = {
|
|
96
|
+
worker: {
|
|
97
|
+
claude: 0.8, // Excellent for search and analysis
|
|
98
|
+
codex: 0.6 // Good for quick lookups
|
|
99
|
+
},
|
|
100
|
+
ic: {
|
|
101
|
+
codex: 0.8, // Excellent for implementation
|
|
102
|
+
claude: 0.7 // Good for code generation
|
|
103
|
+
},
|
|
104
|
+
manager: {
|
|
105
|
+
claude: 0.9, // Excellent for architecture and review
|
|
106
|
+
codex: 0.7 // Good for complex reasoning
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return strengths[tier] || { claude: 0.5, codex: 0.5 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get recent failure count for a provider
|
|
115
|
+
*/
|
|
116
|
+
function getRecentFailures(provider, timeWindowHours = 0.5) {
|
|
117
|
+
const handoffs = getRecentHandoffs(timeWindowHours);
|
|
118
|
+
|
|
119
|
+
return handoffs.filter(handoff =>
|
|
120
|
+
handoff.provider_from === provider &&
|
|
121
|
+
handoff.op === 'escalate_up' &&
|
|
122
|
+
handoff.reason.includes('failure')
|
|
123
|
+
).length;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if provider is currently available
|
|
128
|
+
*/
|
|
129
|
+
export function checkProviderHealth(provider) {
|
|
130
|
+
const recentFailures = getRecentFailures(provider, 0.25); // Last 15 minutes
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
available: recentFailures < 3, // Healthy if fewer than 3 failures in 15 min
|
|
134
|
+
failures: recentFailures,
|
|
135
|
+
status: recentFailures >= 3 ? 'degraded' : 'healthy'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get provider usage statistics for debugging
|
|
141
|
+
*/
|
|
142
|
+
export function getProviderStats(timeWindowHours = 1) {
|
|
143
|
+
const stats = getHandoffStats(timeWindowHours);
|
|
144
|
+
|
|
145
|
+
const providerStats = {};
|
|
146
|
+
|
|
147
|
+
for (const [provider, count] of Object.entries(stats.by_provider)) {
|
|
148
|
+
providerStats[provider] = {
|
|
149
|
+
handoffs: count,
|
|
150
|
+
percentage: Math.round((count / stats.total) * 100),
|
|
151
|
+
health: checkProviderHealth(provider)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
total_handoffs: stats.total,
|
|
157
|
+
time_window_hours: timeWindowHours,
|
|
158
|
+
providers: providerStats,
|
|
159
|
+
avg_duration_ms: stats.avg_duration_ms,
|
|
160
|
+
success_rate: Math.round(stats.success_rate * 100)
|
|
161
|
+
};
|
|
162
|
+
}
|