Files
OmniRoute/src/lib/a2a/routingLogger.ts
diegosouzapw e18cfe1d80 feat: add Phase 3 advanced MCP tools and A2A smart routing skill
Register 8 new advanced MCP tools (simulate_route, set_budget_guard,
set_resilience_profile, test_combo, get_provider_metrics,
best_combo_for_task, explain_route, get_session_snapshot) with their
handler implementations. Add A2A smart routing skill that routes
prompts through the OmniRoute pipeline with routing explanation,
cost envelope, and resilience trace metadata.
2026-03-03 18:53:11 -03:00

117 lines
3.0 KiB
TypeScript

/**
* A2A Routing Decision Logger
*
* Records every routing decision to the `routing_decisions` SQLite table.
* Used by `omniroute_explain_route` (T03) and future learning router.
* Retention: 7 days default.
*/
import { randomUUID } from "crypto";
export interface RoutingFactor {
name: string; // "quota", "health", "cost", "latency", "task_fit"
value: number;
weight: number;
contribution: number;
}
export interface FallbackEntry {
provider: string;
reason: string; // "circuit_breaker_open", "quota_exceeded", "timeout"
}
export interface RoutingDecision {
requestId: string;
taskType: string;
comboId: string;
providerSelected: string;
modelUsed: string;
score: number;
factors: RoutingFactor[];
fallbacksTriggered: FallbackEntry[];
success: boolean;
latencyMs: number;
cost: number;
timestamp: string;
}
// In-memory log (production would use SQLite via routing_decisions table)
const decisions: RoutingDecision[] = [];
const MAX_DECISIONS = 1000;
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
/**
* Log a routing decision.
*/
export function logRoutingDecision(
params: Omit<RoutingDecision, "requestId" | "timestamp">
): RoutingDecision {
const decision: RoutingDecision = {
...params,
requestId: randomUUID(),
timestamp: new Date().toISOString(),
};
decisions.push(decision);
// Cleanup: cap + TTL
if (decisions.length > MAX_DECISIONS) {
const cutoff = new Date(Date.now() - RETENTION_MS);
const validIdx = decisions.findIndex((d) => new Date(d.timestamp) > cutoff);
if (validIdx > 0) decisions.splice(0, validIdx);
else if (decisions.length > MAX_DECISIONS)
decisions.splice(0, decisions.length - MAX_DECISIONS);
}
return decision;
}
/**
* Get a specific routing decision by request ID.
*/
export function getRoutingDecision(requestId: string): RoutingDecision | undefined {
return decisions.find((d) => d.requestId === requestId);
}
/**
* Get recent routing decisions.
*/
export function getRecentDecisions(limit: number = 20): RoutingDecision[] {
return decisions.slice(-limit).reverse();
}
/**
* Get routing decision stats.
*/
export function getDecisionStats(): {
total: number;
successRate: number;
avgLatencyMs: number;
topProviders: Array<{ provider: string; count: number }>;
} {
if (decisions.length === 0) {
return { total: 0, successRate: 1, avgLatencyMs: 0, topProviders: [] };
}
const successful = decisions.filter((d) => d.success);
const providerCounts = new Map<string, number>();
let totalLatency = 0;
for (const d of decisions) {
totalLatency += d.latencyMs;
providerCounts.set(d.providerSelected, (providerCounts.get(d.providerSelected) || 0) + 1);
}
const topProviders = [...providerCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([provider, count]) => ({ provider, count }));
return {
total: decisions.length,
successRate: successful.length / decisions.length,
avgLatencyMs: Math.round(totalLatency / decisions.length),
topProviders,
};
}