@defai.digital/automatosx 5.12.2 → 5.12.3

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +724 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,13 +7,13 @@ AutomatosX is a local-first CLI that transforms stateless AI assistants into a p
7
7
  [![npm version](https://img.shields.io/npm/v/@defai.digital/automatosx.svg)](https://www.npmjs.com/package/@defai.digital/automatosx)
8
8
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/)
10
- [![Tests](https://img.shields.io/badge/tests-2,405%20passing-brightgreen.svg)](#)
10
+ [![Tests](https://img.shields.io/badge/tests-2,458%20passing-brightgreen.svg)](#)
11
11
  [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/defai-digital/automatosx/ci.yml?branch=main&label=CI)](https://github.com/defai-digital/automatosx/actions)
12
12
  [![macOS](https://img.shields.io/badge/macOS-26.0-blue.svg)](https://www.apple.com/macos)
13
13
  [![Windows](https://img.shields.io/badge/Windows-10+-blue.svg)](https://www.microsoft.com/windows)
14
14
  [![Ubuntu](https://img.shields.io/badge/Ubuntu-24.04-orange.svg)](https://ubuntu.com)
15
15
 
16
- **Status**: ✅ Production Ready · **v5.12.2** · October 2025 · 23 Specialized Agents · 100% Resource Leak Free · Spec-Driven Development
16
+ **Status**: ✅ Production Ready · **v5.12.3** · October 2025 · 23 Specialized Agents · 100% Resource Leak Free · Spec-Driven Development
17
17
 
18
18
  ---
19
19
 
package/dist/index.js CHANGED
@@ -9000,6 +9000,646 @@ var PerformanceTimer = class {
9000
9000
  }
9001
9001
  };
9002
9002
 
9003
+ // src/core/routing-strategy.ts
9004
+ init_esm_shims();
9005
+
9006
+ // src/types/routing.ts
9007
+ init_esm_shims();
9008
+ var ROUTING_STRATEGIES = {
9009
+ /**
9010
+ * Fast Strategy - Minimize latency
9011
+ * Best for: Real-time applications, user-facing features
9012
+ */
9013
+ fast: {
9014
+ name: "fast",
9015
+ weights: {
9016
+ latency: 0.7,
9017
+ quality: 0.2,
9018
+ cost: 0.1,
9019
+ availability: 0
9020
+ }
9021
+ },
9022
+ /**
9023
+ * Cheap Strategy - Minimize cost
9024
+ * Best for: Batch processing, non-critical tasks
9025
+ */
9026
+ cheap: {
9027
+ name: "cheap",
9028
+ weights: {
9029
+ cost: 0.7,
9030
+ quality: 0.2,
9031
+ latency: 0.1,
9032
+ availability: 0
9033
+ }
9034
+ },
9035
+ /**
9036
+ * Balanced Strategy - Balance all factors equally
9037
+ * Best for: General-purpose use
9038
+ */
9039
+ balanced: {
9040
+ name: "balanced",
9041
+ weights: {
9042
+ cost: 0.33,
9043
+ latency: 0.33,
9044
+ quality: 0.33,
9045
+ availability: 0.01
9046
+ }
9047
+ },
9048
+ /**
9049
+ * Quality Strategy - Maximize quality and reliability
9050
+ * Best for: Production systems, critical tasks
9051
+ */
9052
+ quality: {
9053
+ name: "quality",
9054
+ weights: {
9055
+ quality: 0.6,
9056
+ latency: 0.3,
9057
+ cost: 0.1,
9058
+ availability: 0
9059
+ }
9060
+ },
9061
+ /**
9062
+ * Custom Strategy - User-defined weights
9063
+ * Weights must be provided in config
9064
+ */
9065
+ custom: {
9066
+ name: "custom",
9067
+ weights: {
9068
+ cost: 0.25,
9069
+ latency: 0.25,
9070
+ quality: 0.25,
9071
+ availability: 0.25
9072
+ }
9073
+ }
9074
+ };
9075
+
9076
+ // src/core/provider-metrics-tracker.ts
9077
+ init_esm_shims();
9078
+ init_logger();
9079
+ var ProviderMetricsTracker = class extends EventEmitter {
9080
+ metrics = /* @__PURE__ */ new Map();
9081
+ windowSize;
9082
+ minRequests;
9083
+ constructor(options = {}) {
9084
+ super();
9085
+ this.windowSize = options.windowSize || 100;
9086
+ this.minRequests = options.minRequests || 10;
9087
+ logger.debug("ProviderMetricsTracker initialized", {
9088
+ windowSize: this.windowSize,
9089
+ minRequests: this.minRequests
9090
+ });
9091
+ }
9092
+ /**
9093
+ * Record a request for metrics tracking
9094
+ */
9095
+ async recordRequest(provider, latencyMs, success, finishReason, tokenUsage, costUsd, model) {
9096
+ if (!this.metrics.has(provider)) {
9097
+ this.metrics.set(provider, []);
9098
+ }
9099
+ const records = this.metrics.get(provider);
9100
+ records.push({
9101
+ timestamp: Date.now(),
9102
+ latencyMs,
9103
+ success,
9104
+ finishReason,
9105
+ promptTokens: tokenUsage.prompt,
9106
+ completionTokens: tokenUsage.completion,
9107
+ totalTokens: tokenUsage.total,
9108
+ costUsd,
9109
+ model
9110
+ });
9111
+ if (records.length > this.windowSize) {
9112
+ records.splice(0, records.length - this.windowSize);
9113
+ }
9114
+ logger.debug("Request recorded", {
9115
+ provider,
9116
+ latencyMs,
9117
+ success,
9118
+ totalRequests: records.length
9119
+ });
9120
+ this.emit("metrics-updated", provider);
9121
+ }
9122
+ /**
9123
+ * Get current metrics for a provider
9124
+ */
9125
+ async getMetrics(provider) {
9126
+ const records = this.metrics.get(provider);
9127
+ if (!records || records.length === 0) {
9128
+ return null;
9129
+ }
9130
+ const now = Date.now();
9131
+ const firstTimestamp = records[0].timestamp;
9132
+ const lastTimestamp = records[records.length - 1].timestamp;
9133
+ const latencies = records.map((r) => r.latencyMs).sort((a, b) => a - b);
9134
+ const avgLatency = latencies.reduce((sum, l) => sum + l, 0) / latencies.length;
9135
+ const p50Index = Math.floor(latencies.length * 0.5);
9136
+ const p95Index = Math.floor(latencies.length * 0.95);
9137
+ const p99Index = Math.floor(latencies.length * 0.99);
9138
+ const successful = records.filter((r) => r.success);
9139
+ const failed = records.filter((r) => !r.success);
9140
+ const stopFinishes = successful.filter((r) => r.finishReason === "stop");
9141
+ const lengthFinishes = successful.filter((r) => r.finishReason === "length");
9142
+ const errorFinishes = records.filter((r) => r.finishReason === "error");
9143
+ const successRate = successful.length / records.length;
9144
+ const properStopRate = successful.length > 0 ? stopFinishes.length / successful.length : 0;
9145
+ const totalCost = records.reduce((sum, r) => sum + r.costUsd, 0);
9146
+ const avgCostPerRequest = totalCost / records.length;
9147
+ const totalTokens = records.reduce((sum, r) => sum + r.totalTokens, 0);
9148
+ const avgCostPer1M = totalTokens > 0 ? totalCost / totalTokens * 1e6 : 0;
9149
+ const lastSuccess = successful.length > 0 ? successful[successful.length - 1].timestamp : 0;
9150
+ const lastFailure = failed.length > 0 ? failed[failed.length - 1].timestamp : 0;
9151
+ let consecutiveFailures = 0;
9152
+ for (let i = records.length - 1; i >= 0; i--) {
9153
+ if (!records[i].success) {
9154
+ consecutiveFailures++;
9155
+ } else {
9156
+ break;
9157
+ }
9158
+ }
9159
+ const uptime = successRate;
9160
+ return {
9161
+ provider,
9162
+ window: records.length,
9163
+ latency: {
9164
+ avg: avgLatency,
9165
+ p50: latencies[p50Index] ?? avgLatency,
9166
+ p95: latencies[p95Index] ?? latencies[latencies.length - 1],
9167
+ p99: latencies[p99Index] ?? latencies[latencies.length - 1],
9168
+ min: latencies[0],
9169
+ max: latencies[latencies.length - 1]
9170
+ },
9171
+ quality: {
9172
+ totalRequests: records.length,
9173
+ successfulRequests: successful.length,
9174
+ failedRequests: failed.length,
9175
+ successRate,
9176
+ stopFinishes: stopFinishes.length,
9177
+ lengthFinishes: lengthFinishes.length,
9178
+ errorFinishes: errorFinishes.length,
9179
+ properStopRate
9180
+ },
9181
+ cost: {
9182
+ totalCostUsd: totalCost,
9183
+ avgCostPerRequest,
9184
+ avgCostPer1MTokens: avgCostPer1M
9185
+ },
9186
+ availability: {
9187
+ uptime,
9188
+ lastSuccess,
9189
+ lastFailure,
9190
+ consecutiveFailures
9191
+ },
9192
+ firstRequest: firstTimestamp,
9193
+ lastRequest: lastTimestamp,
9194
+ lastUpdated: now
9195
+ };
9196
+ }
9197
+ /**
9198
+ * Calculate latency score (0-1, 1 = best)
9199
+ * Based on P95 latency compared to threshold
9200
+ */
9201
+ async getLatencyScore(provider) {
9202
+ const metrics = await this.getMetrics(provider);
9203
+ if (!metrics || metrics.window < this.minRequests) {
9204
+ return 0.5;
9205
+ }
9206
+ const p95 = metrics.latency.p95;
9207
+ if (p95 < 1e3) return 1;
9208
+ if (p95 < 2e3) return 1 - (p95 - 1e3) / 1e3 * 0.1;
9209
+ if (p95 < 3e3) return 0.9 - (p95 - 2e3) / 1e3 * 0.2;
9210
+ if (p95 < 4e3) return 0.7 - (p95 - 3e3) / 1e3 * 0.2;
9211
+ if (p95 < 5e3) return 0.5 - (p95 - 4e3) / 1e3 * 0.2;
9212
+ return Math.max(0.1, 0.3 - (p95 - 5e3) / 5e3 * 0.2);
9213
+ }
9214
+ /**
9215
+ * Calculate quality score (0-1, 1 = best)
9216
+ * Based on success rate and proper stop rate
9217
+ */
9218
+ async getQualityScore(provider) {
9219
+ const metrics = await this.getMetrics(provider);
9220
+ if (!metrics || metrics.window < this.minRequests) {
9221
+ return 0.5;
9222
+ }
9223
+ const successScore = metrics.quality.successRate;
9224
+ const stopScore = metrics.quality.properStopRate;
9225
+ if (successScore < 0.5) {
9226
+ return successScore * 0.9 + stopScore * 0.1;
9227
+ } else {
9228
+ return successScore * 0.7 + stopScore * 0.3;
9229
+ }
9230
+ }
9231
+ /**
9232
+ * Calculate cost score (0-1, 1 = cheapest)
9233
+ * Relative to other providers
9234
+ */
9235
+ async getCostScore(provider, allProviders) {
9236
+ const metrics = await this.getMetrics(provider);
9237
+ if (!metrics || metrics.window < this.minRequests) {
9238
+ return 0.5;
9239
+ }
9240
+ const costs = [];
9241
+ for (const p of allProviders) {
9242
+ const m = await this.getMetrics(p);
9243
+ if (m && m.window >= this.minRequests) {
9244
+ costs.push(m.cost.avgCostPer1MTokens);
9245
+ }
9246
+ }
9247
+ if (costs.length === 0) {
9248
+ return 0.5;
9249
+ }
9250
+ const minCost = Math.min(...costs);
9251
+ const maxCost = Math.max(...costs);
9252
+ const providerCost = metrics.cost.avgCostPer1MTokens;
9253
+ if (maxCost === minCost) {
9254
+ return 1;
9255
+ }
9256
+ return 1 - (providerCost - minCost) / (maxCost - minCost);
9257
+ }
9258
+ /**
9259
+ * Calculate availability score (0-1, 1 = most available)
9260
+ */
9261
+ async getAvailabilityScore(provider) {
9262
+ const metrics = await this.getMetrics(provider);
9263
+ if (!metrics || metrics.window < this.minRequests) {
9264
+ return 0.5;
9265
+ }
9266
+ const uptimeScore = metrics.availability.uptime;
9267
+ const failurePenalty = Math.min(metrics.availability.consecutiveFailures * 0.1, 0.5);
9268
+ return Math.max(0, uptimeScore - failurePenalty);
9269
+ }
9270
+ /**
9271
+ * Calculate overall provider score based on weights
9272
+ */
9273
+ async calculateScore(provider, weights, allProviders, healthMultiplier = 1) {
9274
+ const costScore = await this.getCostScore(provider, allProviders);
9275
+ const latencyScore = await this.getLatencyScore(provider);
9276
+ const qualityScore = await this.getQualityScore(provider);
9277
+ const availabilityScore = await this.getAvailabilityScore(provider);
9278
+ const totalScore = (weights.cost * costScore + weights.latency * latencyScore + weights.quality * qualityScore + weights.availability * availabilityScore) * healthMultiplier;
9279
+ const metrics = await this.getMetrics(provider);
9280
+ return {
9281
+ provider,
9282
+ totalScore,
9283
+ breakdown: {
9284
+ costScore,
9285
+ latencyScore,
9286
+ qualityScore,
9287
+ availabilityScore
9288
+ },
9289
+ healthMultiplier,
9290
+ metadata: metrics ? {
9291
+ avgLatencyMs: metrics.latency.avg,
9292
+ avgCostPer1M: metrics.cost.avgCostPer1MTokens,
9293
+ successRate: metrics.quality.successRate,
9294
+ lastUsed: metrics.lastRequest
9295
+ } : void 0
9296
+ };
9297
+ }
9298
+ /**
9299
+ * Get all provider scores sorted by total score (descending)
9300
+ */
9301
+ async getAllScores(providers, weights, healthMultipliers) {
9302
+ const scores = [];
9303
+ for (const provider of providers) {
9304
+ const health = healthMultipliers.get(provider) || 1;
9305
+ const score = await this.calculateScore(provider, weights, providers, health);
9306
+ scores.push(score);
9307
+ }
9308
+ scores.sort((a, b) => b.totalScore - a.totalScore);
9309
+ return scores;
9310
+ }
9311
+ /**
9312
+ * Get number of requests tracked for a provider
9313
+ */
9314
+ getRequestCount(provider) {
9315
+ const records = this.metrics.get(provider);
9316
+ return records ? records.length : 0;
9317
+ }
9318
+ /**
9319
+ * Check if provider has sufficient data for scoring
9320
+ */
9321
+ hasSufficientData(provider) {
9322
+ return this.getRequestCount(provider) >= this.minRequests;
9323
+ }
9324
+ /**
9325
+ * Clear metrics for a provider
9326
+ */
9327
+ clearMetrics(provider) {
9328
+ this.metrics.delete(provider);
9329
+ logger.debug("Metrics cleared", { provider });
9330
+ }
9331
+ /**
9332
+ * Clear all metrics
9333
+ */
9334
+ clearAllMetrics() {
9335
+ this.metrics.clear();
9336
+ logger.debug("All metrics cleared");
9337
+ }
9338
+ /**
9339
+ * Get summary of all tracked providers
9340
+ */
9341
+ getSummary() {
9342
+ const summary = {};
9343
+ for (const [provider, records] of this.metrics.entries()) {
9344
+ summary[provider] = {
9345
+ requests: records.length,
9346
+ hasSufficientData: records.length >= this.minRequests
9347
+ };
9348
+ }
9349
+ return summary;
9350
+ }
9351
+ /**
9352
+ * Export metrics for debugging/analysis
9353
+ */
9354
+ async exportMetrics(provider) {
9355
+ const records = this.metrics.get(provider);
9356
+ return records ? [...records] : null;
9357
+ }
9358
+ };
9359
+ var globalMetricsTracker = null;
9360
+ function getProviderMetricsTracker() {
9361
+ if (!globalMetricsTracker) {
9362
+ globalMetricsTracker = new ProviderMetricsTracker();
9363
+ }
9364
+ return globalMetricsTracker;
9365
+ }
9366
+
9367
+ // src/core/routing-strategy.ts
9368
+ init_logger();
9369
+ var RoutingStrategyManager = class extends EventEmitter {
9370
+ strategy;
9371
+ metricsTracker;
9372
+ minRequestsForScoring;
9373
+ enableLogging;
9374
+ decisionHistory = [];
9375
+ maxHistorySize = 100;
9376
+ constructor(config = {}) {
9377
+ super();
9378
+ this.metricsTracker = getProviderMetricsTracker();
9379
+ const strategyName = config.strategy || "balanced";
9380
+ this.strategy = this.loadStrategy(strategyName, config.customWeights);
9381
+ this.minRequestsForScoring = config.minRequestsForScoring || 10;
9382
+ this.enableLogging = config.enableLogging || false;
9383
+ if (config.metricsWindow) {
9384
+ this.metricsTracker = new ProviderMetricsTracker({
9385
+ windowSize: config.metricsWindow,
9386
+ minRequests: this.minRequestsForScoring
9387
+ });
9388
+ logger.info("RoutingStrategyManager using isolated metrics tracker", {
9389
+ windowSize: config.metricsWindow,
9390
+ minRequests: this.minRequestsForScoring
9391
+ });
9392
+ }
9393
+ logger.info("RoutingStrategyManager initialized", {
9394
+ strategy: this.strategy.name,
9395
+ weights: this.strategy.weights,
9396
+ minRequestsForScoring: this.minRequestsForScoring
9397
+ });
9398
+ }
9399
+ /**
9400
+ * Load strategy configuration by name
9401
+ */
9402
+ loadStrategy(name, customWeights) {
9403
+ if (name === "custom" && customWeights) {
9404
+ const total = Object.values(customWeights).reduce((sum, w) => sum + w, 0);
9405
+ if (Math.abs(total - 1) > 0.01) {
9406
+ logger.warn("Custom routing weights do not sum to 1.0", {
9407
+ weights: customWeights,
9408
+ total
9409
+ });
9410
+ }
9411
+ return {
9412
+ name: "custom",
9413
+ weights: customWeights
9414
+ };
9415
+ }
9416
+ const strategy = ROUTING_STRATEGIES[name];
9417
+ if (!strategy) {
9418
+ logger.warn("Unknown routing strategy, falling back to balanced", { name });
9419
+ return ROUTING_STRATEGIES.balanced;
9420
+ }
9421
+ return strategy;
9422
+ }
9423
+ /**
9424
+ * Select best provider based on current strategy
9425
+ *
9426
+ * @param providers - Available providers
9427
+ * @param healthMultipliers - Health scores from circuit breaker (0-1)
9428
+ * @param fallbackToPriority - Use priority-based selection if insufficient metrics
9429
+ * @returns Selected provider name or null if none available
9430
+ */
9431
+ async selectProvider(providers, healthMultipliers, fallbackToPriority = true) {
9432
+ if (providers.length === 0) {
9433
+ return null;
9434
+ }
9435
+ const sufficientData = providers.some((p) => this.metricsTracker.getRequestCount(p) >= this.minRequestsForScoring);
9436
+ if (!sufficientData) {
9437
+ if (fallbackToPriority) {
9438
+ const healthyProviders = providers.filter((p) => {
9439
+ const health = healthMultipliers.get(p) ?? 1;
9440
+ return health >= 0.1;
9441
+ });
9442
+ if (healthyProviders.length === 0) {
9443
+ return null;
9444
+ }
9445
+ const selected = healthyProviders[0];
9446
+ if (!selected) {
9447
+ return null;
9448
+ }
9449
+ const decision2 = {
9450
+ selectedProvider: selected,
9451
+ strategy: this.strategy.name,
9452
+ scores: [],
9453
+ reason: "Insufficient metrics data, using priority-based selection with health check",
9454
+ timestamp: Date.now()
9455
+ };
9456
+ this.recordDecision(decision2);
9457
+ return decision2;
9458
+ } else {
9459
+ return null;
9460
+ }
9461
+ }
9462
+ const scores = await this.metricsTracker.getAllScores(
9463
+ providers,
9464
+ this.strategy.weights,
9465
+ healthMultipliers
9466
+ );
9467
+ if (scores.length === 0) {
9468
+ return null;
9469
+ }
9470
+ const best = scores[0];
9471
+ const reason = this.generateReason(best, scores);
9472
+ const decision = {
9473
+ selectedProvider: best.provider,
9474
+ strategy: this.strategy.name,
9475
+ scores,
9476
+ reason,
9477
+ timestamp: Date.now()
9478
+ };
9479
+ this.recordDecision(decision);
9480
+ if (this.enableLogging) {
9481
+ logger.info("Routing decision made", {
9482
+ selected: best.provider,
9483
+ strategy: this.strategy.name,
9484
+ totalScore: best.totalScore.toFixed(3),
9485
+ breakdown: {
9486
+ cost: best.breakdown.costScore.toFixed(3),
9487
+ latency: best.breakdown.latencyScore.toFixed(3),
9488
+ quality: best.breakdown.qualityScore.toFixed(3),
9489
+ availability: best.breakdown.availabilityScore.toFixed(3)
9490
+ },
9491
+ reason
9492
+ });
9493
+ }
9494
+ this.emit("decision", decision);
9495
+ return decision;
9496
+ }
9497
+ /**
9498
+ * Generate human-readable reason for provider selection
9499
+ */
9500
+ generateReason(best, allScores) {
9501
+ const reasons = [];
9502
+ const breakdown = best.breakdown;
9503
+ const weights = this.strategy.weights;
9504
+ const weightedScores = {
9505
+ cost: breakdown.costScore * weights.cost,
9506
+ latency: breakdown.latencyScore * weights.latency,
9507
+ quality: breakdown.qualityScore * weights.quality,
9508
+ availability: breakdown.availabilityScore * weights.availability
9509
+ };
9510
+ const topFactor = Object.entries(weightedScores).sort(([, a], [, b]) => b - a)[0];
9511
+ if (!topFactor) {
9512
+ return `Selected ${best.provider}`;
9513
+ }
9514
+ const [factor, score] = topFactor;
9515
+ switch (this.strategy.name) {
9516
+ case "fast":
9517
+ reasons.push(`Lowest latency (${best.metadata?.avgLatencyMs?.toFixed(0)}ms avg)`);
9518
+ break;
9519
+ case "cheap":
9520
+ reasons.push(`Lowest cost ($${best.metadata?.avgCostPer1M?.toFixed(2)}/1M tokens)`);
9521
+ break;
9522
+ case "quality":
9523
+ reasons.push(`Highest quality (${(best.breakdown.qualityScore * 100).toFixed(1)}% score)`);
9524
+ break;
9525
+ case "balanced":
9526
+ reasons.push(`Best overall balance (score: ${best.totalScore.toFixed(3)})`);
9527
+ break;
9528
+ default:
9529
+ reasons.push(`Custom strategy (${factor} weighted ${(score * 100).toFixed(1)}%)`);
9530
+ }
9531
+ if (best.healthMultiplier < 1) {
9532
+ reasons.push(`adjusted for health (${(best.healthMultiplier * 100).toFixed(0)}%)`);
9533
+ }
9534
+ return reasons.join(", ");
9535
+ }
9536
+ /**
9537
+ * Record routing decision in history
9538
+ */
9539
+ recordDecision(decision) {
9540
+ this.decisionHistory.push(decision);
9541
+ if (this.decisionHistory.length > this.maxHistorySize) {
9542
+ this.decisionHistory.shift();
9543
+ }
9544
+ }
9545
+ /**
9546
+ * Get recent routing decisions
9547
+ */
9548
+ getDecisionHistory(limit = 10) {
9549
+ return this.decisionHistory.slice(-limit);
9550
+ }
9551
+ /**
9552
+ * Get routing statistics
9553
+ */
9554
+ getStats() {
9555
+ const stats = {
9556
+ strategy: this.strategy.name,
9557
+ totalDecisions: this.decisionHistory.length,
9558
+ providerUsage: {},
9559
+ avgScores: {}
9560
+ };
9561
+ for (const decision of this.decisionHistory) {
9562
+ const provider = decision.selectedProvider;
9563
+ stats.providerUsage[provider] = (stats.providerUsage[provider] || 0) + 1;
9564
+ }
9565
+ const scoresByProvider = {};
9566
+ for (const decision of this.decisionHistory) {
9567
+ for (const score of decision.scores) {
9568
+ if (!scoresByProvider[score.provider]) {
9569
+ scoresByProvider[score.provider] = [];
9570
+ }
9571
+ scoresByProvider[score.provider].push(score.totalScore);
9572
+ }
9573
+ }
9574
+ for (const [provider, scores] of Object.entries(scoresByProvider)) {
9575
+ const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
9576
+ stats.avgScores[provider] = avg;
9577
+ }
9578
+ return stats;
9579
+ }
9580
+ /**
9581
+ * Get routing statistics (alias for getStats)
9582
+ */
9583
+ getRoutingStats() {
9584
+ return this.getStats();
9585
+ }
9586
+ /**
9587
+ * Update strategy
9588
+ */
9589
+ setStrategy(name, customWeights) {
9590
+ this.strategy = this.loadStrategy(name, customWeights);
9591
+ logger.info("Routing strategy updated", {
9592
+ strategy: this.strategy.name,
9593
+ weights: this.strategy.weights
9594
+ });
9595
+ this.emit("strategy-changed", this.strategy);
9596
+ }
9597
+ /**
9598
+ * Get current strategy
9599
+ */
9600
+ getStrategy() {
9601
+ return { ...this.strategy };
9602
+ }
9603
+ /**
9604
+ * Get current weights
9605
+ */
9606
+ getWeights() {
9607
+ return { ...this.strategy.weights };
9608
+ }
9609
+ /**
9610
+ * Enable/disable logging
9611
+ */
9612
+ setLogging(enabled) {
9613
+ this.enableLogging = enabled;
9614
+ }
9615
+ /**
9616
+ * Get metrics tracker (for advanced use)
9617
+ */
9618
+ getMetricsTracker() {
9619
+ return this.metricsTracker;
9620
+ }
9621
+ /**
9622
+ * Clear decision history
9623
+ */
9624
+ clearHistory() {
9625
+ this.decisionHistory = [];
9626
+ logger.debug("Routing decision history cleared");
9627
+ }
9628
+ /**
9629
+ * Export decision history for analysis
9630
+ */
9631
+ exportHistory() {
9632
+ return [...this.decisionHistory];
9633
+ }
9634
+ };
9635
+ var globalRoutingStrategy = null;
9636
+ function getRoutingStrategyManager(config) {
9637
+ if (!globalRoutingStrategy) {
9638
+ globalRoutingStrategy = new RoutingStrategyManager(config);
9639
+ }
9640
+ return globalRoutingStrategy;
9641
+ }
9642
+
9003
9643
  // src/core/router.ts
9004
9644
  var Router = class {
9005
9645
  providers;
@@ -9017,6 +9657,8 @@ var Router = class {
9017
9657
  totalDuration: 0,
9018
9658
  failures: 0
9019
9659
  };
9660
+ // Phase 3: Multi-factor routing
9661
+ useMultiFactorRouting = false;
9020
9662
  constructor(config) {
9021
9663
  this.providers = [...config.providers].sort((a, b) => {
9022
9664
  return a.priority - b.priority;
@@ -9029,6 +9671,14 @@ var Router = class {
9029
9671
  void limitManager.initialize().catch((err) => {
9030
9672
  logger.warn("Failed to initialize ProviderLimitManager", { error: err.message });
9031
9673
  });
9674
+ if (config.strategy) {
9675
+ this.useMultiFactorRouting = true;
9676
+ const strategyManager = getRoutingStrategyManager(config.strategy);
9677
+ logger.info("Multi-factor routing enabled", {
9678
+ strategy: strategyManager.getStrategy().name,
9679
+ weights: strategyManager.getWeights()
9680
+ });
9681
+ }
9032
9682
  this.healthCheckIntervalMs = config.healthCheckInterval;
9033
9683
  if (config.healthCheckInterval) {
9034
9684
  this.startHealthChecks(config.healthCheckInterval);
@@ -9104,9 +9754,37 @@ var Router = class {
9104
9754
  }
9105
9755
  throw ProviderError.noAvailableProviders();
9106
9756
  }
9757
+ let providersToTry = availableProviders;
9758
+ if (this.useMultiFactorRouting) {
9759
+ const strategyManager = getRoutingStrategyManager();
9760
+ const providerNames = availableProviders.map((p) => p.name);
9761
+ const healthMultipliers = /* @__PURE__ */ new Map();
9762
+ for (const provider of availableProviders) {
9763
+ const isPenalized = this.penalizedProviders.has(provider.name);
9764
+ healthMultipliers.set(provider.name, isPenalized ? 0.5 : 1);
9765
+ }
9766
+ const scores = await getProviderMetricsTracker().getAllScores(
9767
+ providerNames,
9768
+ strategyManager.getWeights(),
9769
+ healthMultipliers
9770
+ );
9771
+ if (scores.length > 0) {
9772
+ const scoreMap = new Map(scores.map((s) => [s.provider, s.totalScore]));
9773
+ providersToTry = [...availableProviders].sort((a, b) => {
9774
+ const scoreA = scoreMap.get(a.name) ?? 0;
9775
+ const scoreB = scoreMap.get(b.name) ?? 0;
9776
+ return scoreB - scoreA;
9777
+ });
9778
+ logger.debug("Provider order after multi-factor routing", {
9779
+ original: availableProviders.map((p) => p.name),
9780
+ reordered: providersToTry.map((p) => p.name),
9781
+ scores: Object.fromEntries(scoreMap)
9782
+ });
9783
+ }
9784
+ }
9107
9785
  let lastError;
9108
9786
  let attemptNumber = 0;
9109
- for (const provider of availableProviders) {
9787
+ for (const provider of providersToTry) {
9110
9788
  attemptNumber++;
9111
9789
  try {
9112
9790
  logger.info(`Selecting provider: ${provider.name} (attempt ${attemptNumber}/${availableProviders.length})`);
@@ -9166,6 +9844,24 @@ var Router = class {
9166
9844
  );
9167
9845
  }
9168
9846
  this.penalizedProviders.delete(provider.name);
9847
+ if (this.useMultiFactorRouting) {
9848
+ const metricsTracker = getProviderMetricsTracker();
9849
+ const estimatedCost = this.estimateCost(provider.name, response.tokensUsed);
9850
+ await metricsTracker.recordRequest(
9851
+ provider.name,
9852
+ response.latencyMs,
9853
+ true,
9854
+ // success
9855
+ response.finishReason || "stop",
9856
+ {
9857
+ prompt: response.tokensUsed.prompt,
9858
+ completion: response.tokensUsed.completion,
9859
+ total: response.tokensUsed.total
9860
+ },
9861
+ estimatedCost,
9862
+ response.model
9863
+ );
9864
+ }
9169
9865
  return response;
9170
9866
  } catch (error) {
9171
9867
  lastError = error;
@@ -9203,6 +9899,22 @@ var Router = class {
9203
9899
  this.penalizedProviders.set(provider.name, penaltyExpiry);
9204
9900
  logger.debug(`Provider ${provider.name} penalized until ${new Date(penaltyExpiry).toISOString()}`);
9205
9901
  }
9902
+ if (this.useMultiFactorRouting) {
9903
+ const metricsTracker = getProviderMetricsTracker();
9904
+ await metricsTracker.recordRequest(
9905
+ provider.name,
9906
+ 0,
9907
+ // latency unknown for failed requests
9908
+ false,
9909
+ // failure
9910
+ "error",
9911
+ { prompt: 0, completion: 0, total: 0 },
9912
+ 0,
9913
+ // no cost for failed requests
9914
+ void 0
9915
+ // model unknown
9916
+ );
9917
+ }
9206
9918
  if (!this.fallbackEnabled) {
9207
9919
  throw lastError;
9208
9920
  }
@@ -9440,6 +10152,17 @@ var Router = class {
9440
10152
  })
9441
10153
  };
9442
10154
  }
10155
+ /**
10156
+ * Estimate cost of a request based on tokens used
10157
+ * Phase 3: Helper for metrics tracking
10158
+ */
10159
+ estimateCost(providerName, tokensUsed) {
10160
+ const inputCostPer1M = 2.5;
10161
+ const outputCostPer1M = 10;
10162
+ const inputCost = tokensUsed.prompt / 1e6 * inputCostPer1M;
10163
+ const outputCost = tokensUsed.completion / 1e6 * outputCostPer1M;
10164
+ return inputCost + outputCost;
10165
+ }
9443
10166
  };
9444
10167
 
9445
10168
  // src/core/lazy-memory-manager.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defai.digital/automatosx",
3
- "version": "5.12.2",
3
+ "version": "5.12.3",
4
4
  "description": "AI Agent Orchestration Platform",
5
5
  "type": "module",
6
6
  "publishConfig": {