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.
This commit is contained in:
diegosouzapw
2026-03-03 18:53:11 -03:00
parent 7eb45b2e19
commit e18cfe1d80
13 changed files with 1954 additions and 0 deletions

View File

@@ -27,6 +27,17 @@ import {
import { logToolCall } from "./audit.ts";
import {
handleSimulateRoute,
handleSetBudgetGuard,
handleSetResilienceProfile,
handleTestCombo,
handleGetProviderMetrics,
handleBestComboForTask,
handleExplainRoute,
handleGetSessionSnapshot,
} from "./tools/advancedTools.ts";
// ============ Configuration ============
const OMNIROUTE_BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128";
@@ -407,6 +418,94 @@ export function createMcpServer(): McpServer {
(args) => handleListModelsCatalog(args as any)
);
// ── Advanced Tools (Phase 3) ──────────────────────────────
server.tool(
"omniroute_simulate_route",
"Simulates the routing path a request would take without executing it (dry-run)",
{
model: { type: "string", description: "Target model identifier" },
promptTokenEstimate: { type: "number", description: "Estimated prompt token count" },
combo: {
type: "string",
description: "Specific combo to simulate (optional, default: active)",
},
},
(args) => handleSimulateRoute(args as any)
);
server.tool(
"omniroute_set_budget_guard",
"Sets a session budget limit with configurable action when exceeded (degrade/block/alert)",
{
maxCost: { type: "number", description: "Maximum cost in USD for the session" },
action: { type: "string", description: "Action on exceed: degrade, block, or alert" },
degradeToTier: {
type: "string",
description: "If action=degrade, target tier: cheap or free (optional)",
},
},
(args) => handleSetBudgetGuard(args as any)
);
server.tool(
"omniroute_set_resilience_profile",
"Applies a resilience profile controlling circuit breakers, retries, timeouts, and fallback depth",
{
profile: { type: "string", description: "Profile: aggressive, balanced, or conservative" },
},
(args) => handleSetResilienceProfile(args as any)
);
server.tool(
"omniroute_test_combo",
"Tests each provider in a combo with a real prompt, reporting latency, cost, and success per provider",
{
comboId: { type: "string", description: "ID or name of the combo to test" },
testPrompt: { type: "string", description: "Short test prompt (max 200 chars)" },
},
(args) => handleTestCombo(args as any)
);
server.tool(
"omniroute_get_provider_metrics",
"Returns detailed metrics for a specific provider including latency percentiles and circuit breaker state",
{
provider: { type: "string", description: "Provider name" },
},
(args) => handleGetProviderMetrics(args as any)
);
server.tool(
"omniroute_best_combo_for_task",
"Recommends the best combo for a task type based on provider fitness and constraints",
{
taskType: {
type: "string",
description: "Task type: coding, review, planning, analysis, debugging, documentation",
},
budgetConstraint: { type: "number", description: "Max cost constraint in USD (optional)" },
latencyConstraint: { type: "number", description: "Max latency constraint in ms (optional)" },
},
(args) => handleBestComboForTask(args as any)
);
server.tool(
"omniroute_explain_route",
"Explains why a request was routed to a specific provider, showing scoring factors and fallbacks",
{
requestId: { type: "string", description: "Request ID from X-Request-Id header" },
},
(args) => handleExplainRoute(args as any)
);
server.tool(
"omniroute_get_session_snapshot",
"Returns a full snapshot of the current working session: cost, tokens, top models, errors, budget status",
{},
handleGetSessionSnapshot
);
return server;
}

View File

@@ -0,0 +1,571 @@
/**
* OmniRoute MCP Advanced Tools — 8 intelligence tools that differentiate
* OmniRoute from any other AI gateway.
*
* Tools:
* 1. omniroute_simulate_route — Dry-run routing simulation
* 2. omniroute_set_budget_guard — Session budget with degrade/block/alert
* 3. omniroute_set_resilience_profile — Circuit breaker/retry profiles
* 4. omniroute_test_combo — Live test each provider in a combo
* 5. omniroute_get_provider_metrics — Detailed per-provider metrics
* 6. omniroute_best_combo_for_task — AI-powered combo recommendation
* 7. omniroute_explain_route — Post-hoc routing decision explainer
* 8. omniroute_get_session_snapshot — Full session state snapshot
*/
import { logToolCall } from "../audit.ts";
const OMNIROUTE_BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128";
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
async function apiFetch(path: string, options: RequestInit = {}): Promise<unknown> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}),
...((options.headers as Record<string, string>) || {}),
};
const response = await fetch(url, { ...options, headers, signal: AbortSignal.timeout(30000) });
if (!response.ok) {
const text = await response.text().catch(() => "Unknown error");
throw new Error(`API [${response.status}]: ${text}`);
}
return response.json();
}
// ============ In-Memory State ============
interface BudgetGuardState {
sessionId: string;
maxCost: number;
action: "degrade" | "block" | "alert";
degradeToTier?: "cheap" | "free";
spent: number;
createdAt: string;
}
let activeBudgetGuard: BudgetGuardState | null = null;
const RESILIENCE_PROFILES = {
aggressive: { circuitBreakerThreshold: 3, retryCount: 1, timeoutMs: 10000, fallbackDepth: 5 },
balanced: { circuitBreakerThreshold: 5, retryCount: 2, timeoutMs: 15000, fallbackDepth: 3 },
conservative: { circuitBreakerThreshold: 10, retryCount: 3, timeoutMs: 30000, fallbackDepth: 2 },
} as const;
const TASK_FITNESS: Record<string, { preferred: string[]; traits: string[] }> = {
coding: { preferred: ["claude", "deepseek", "codex"], traits: ["fast", "code-optimized"] },
review: { preferred: ["claude", "gemini", "openai"], traits: ["analytical", "thorough"] },
planning: { preferred: ["gemini", "claude", "openai"], traits: ["reasoning", "structured"] },
analysis: { preferred: ["gemini", "claude"], traits: ["deep-reasoning", "large-context"] },
debugging: { preferred: ["claude", "deepseek", "codex"], traits: ["code-aware", "fast"] },
documentation: { preferred: ["gemini", "claude", "openai"], traits: ["clear", "structured"] },
};
// ============ Tool Handlers ============
export async function handleSimulateRoute(args: {
model: string;
promptTokenEstimate: number;
combo?: string;
}) {
const start = Date.now();
try {
// Fetch combos and health data for simulation
const [combosRaw, healthRaw, quotaRaw] = await Promise.allSettled([
apiFetch("/api/combos"),
apiFetch("/api/monitoring/health"),
apiFetch("/api/usage/quota"),
]);
const combos = combosRaw.status === "fulfilled" ? (combosRaw.value as any[]) : [];
const health = healthRaw.status === "fulfilled" ? (healthRaw.value as any) : {};
const quota = quotaRaw.status === "fulfilled" ? (quotaRaw.value as any) : {};
// Find target combo
const targetCombo = args.combo
? combos.find((c: any) => c.id === args.combo || c.name === args.combo)
: combos.find((c: any) => c.enabled !== false);
if (!targetCombo) {
return {
content: [
{ type: "text" as const, text: JSON.stringify({ error: "No matching combo found" }) },
],
isError: true,
};
}
const models = targetCombo.models || targetCombo.data?.models || [];
const breakers = health?.circuitBreakers || [];
const providers = quota?.providers || (Array.isArray(quota) ? quota : []);
// Simulate path
const simulatedPath = models.map((m: any, idx: number) => {
const cb = breakers.find((b: any) => b.provider === m.provider);
const q = providers.find((p: any) => p.provider === m.provider);
const estimatedCost = (args.promptTokenEstimate / 1_000_000) * (m.inputCostPer1M || 3.0);
return {
provider: m.provider,
model: m.model || args.model,
probability: idx === 0 ? 0.85 : 0.15 / Math.max(models.length - 1, 1),
estimatedCost: Math.round(estimatedCost * 10000) / 10000,
healthStatus: cb?.state || "CLOSED",
quotaAvailable: q?.percentRemaining ?? 100,
};
});
const costs = simulatedPath.map((p: any) => p.estimatedCost);
const result = {
simulatedPath,
fallbackTree: {
primary: simulatedPath[0]?.provider || "unknown",
fallbacks: simulatedPath.slice(1).map((p: any) => p.provider),
worstCaseCost: Math.max(...costs, 0),
bestCaseCost: Math.min(...costs, 0),
},
};
await logToolCall("omniroute_simulate_route", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_simulate_route", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleSetBudgetGuard(args: {
maxCost: number;
action: "degrade" | "block" | "alert";
degradeToTier?: "cheap" | "free";
}) {
const start = Date.now();
try {
// Get current session cost
let spent = 0;
try {
const analytics = (await apiFetch("/api/usage/analytics?period=session")) as any;
spent = analytics?.totalCost || 0;
} catch {
/* ignore if analytics not available */
}
activeBudgetGuard = {
sessionId: `budget_${Date.now()}`,
maxCost: args.maxCost,
action: args.action,
degradeToTier: args.degradeToTier,
spent,
createdAt: new Date().toISOString(),
};
const remaining = Math.max(0, args.maxCost - spent);
const result = {
sessionId: activeBudgetGuard.sessionId,
budgetTotal: args.maxCost,
budgetSpent: Math.round(spent * 10000) / 10000,
budgetRemaining: Math.round(remaining * 10000) / 10000,
action: args.action,
status: remaining <= 0 ? "exceeded" : remaining < args.maxCost * 0.2 ? "warning" : "active",
};
await logToolCall(
"omniroute_set_budget_guard",
{ maxCost: args.maxCost, action: args.action },
result,
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_set_budget_guard", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleSetResilienceProfile(args: {
profile: "aggressive" | "balanced" | "conservative";
}) {
const start = Date.now();
try {
const settings = RESILIENCE_PROFILES[args.profile];
if (!settings) {
return {
content: [{ type: "text" as const, text: `Error: Invalid profile "${args.profile}"` }],
isError: true,
};
}
// Apply to OmniRoute via API
try {
await apiFetch("/api/resilience", {
method: "PUT",
body: JSON.stringify({
circuitBreakerThreshold: settings.circuitBreakerThreshold,
retryCount: settings.retryCount,
timeoutMs: settings.timeoutMs,
}),
});
} catch {
// Resilience endpoint may not exist yet — return settings anyway
}
const result = { applied: true, profile: args.profile, settings };
await logToolCall("omniroute_set_resilience_profile", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall(
"omniroute_set_resilience_profile",
args,
null,
Date.now() - start,
false,
msg
);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleTestCombo(args: { comboId: string; testPrompt: string }) {
const start = Date.now();
try {
// Get combo details
const combos = (await apiFetch("/api/combos")) as any[];
const combo = combos.find((c: any) => c.id === args.comboId || c.name === args.comboId);
if (!combo) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: `Combo "${args.comboId}" not found` }),
},
],
isError: true,
};
}
const models = combo.models || combo.data?.models || [];
const prompt = (args.testPrompt || "Say hello").slice(0, 200);
// Test each provider in parallel
const results = await Promise.allSettled(
models.map(async (m: any) => {
const providerStart = Date.now();
try {
const resp = (await apiFetch("/v1/chat/completions", {
method: "POST",
body: JSON.stringify({
model: m.model || "auto",
messages: [{ role: "user", content: prompt }],
max_tokens: 50,
stream: false,
"x-provider": m.provider,
}),
})) as any;
return {
provider: m.provider,
model: m.model || resp?.model || "unknown",
success: true,
latencyMs: Date.now() - providerStart,
cost: resp?.cost || 0,
tokenCount: (resp?.usage?.prompt_tokens || 0) + (resp?.usage?.completion_tokens || 0),
};
} catch (err) {
return {
provider: m.provider,
model: m.model || "unknown",
success: false,
latencyMs: Date.now() - providerStart,
cost: 0,
tokenCount: 0,
error: err instanceof Error ? err.message : String(err),
};
}
})
);
const providerResults = results.map((r) =>
r.status === "fulfilled"
? r.value
: {
provider: "unknown",
model: "unknown",
success: false,
latencyMs: 0,
cost: 0,
tokenCount: 0,
error: "Promise rejected",
}
);
const successful = providerResults.filter((r) => r.success);
const fastest = successful.sort((a, b) => a.latencyMs - b.latencyMs)[0];
const cheapest = successful.sort((a, b) => a.cost - b.cost)[0];
const result = {
results: providerResults,
summary: {
totalProviders: providerResults.length,
successful: successful.length,
fastestProvider: fastest?.provider || "none",
cheapestProvider: cheapest?.provider || "none",
},
};
await logToolCall(
"omniroute_test_combo",
{ comboId: args.comboId },
result.summary,
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_test_combo", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleGetProviderMetrics(args: { provider: string }) {
const start = Date.now();
try {
const [healthRaw, quotaRaw, analyticsRaw] = await Promise.allSettled([
apiFetch("/api/monitoring/health"),
apiFetch(`/api/usage/quota?provider=${encodeURIComponent(args.provider)}`),
apiFetch(`/api/usage/analytics?period=session&provider=${encodeURIComponent(args.provider)}`),
]);
const health = healthRaw.status === "fulfilled" ? (healthRaw.value as any) : {};
const quota = quotaRaw.status === "fulfilled" ? (quotaRaw.value as any) : {};
const analytics = analyticsRaw.status === "fulfilled" ? (analyticsRaw.value as any) : {};
const cb = (health.circuitBreakers || []).find((b: any) => b.provider === args.provider);
const providerQuota = Array.isArray(quota?.providers)
? quota.providers.find((p: any) => p.provider === args.provider)
: null;
const result = {
provider: args.provider,
successRate: analytics?.successRate ?? 1.0,
requestCount: analytics?.requestCount ?? 0,
avgLatencyMs: analytics?.avgLatencyMs ?? 0,
p50LatencyMs: analytics?.p50LatencyMs ?? 0,
p95LatencyMs: analytics?.p95LatencyMs ?? 0,
p99LatencyMs: analytics?.p99LatencyMs ?? 0,
errorRate: analytics?.errorRate ?? 0,
lastError: analytics?.lastError || null,
circuitBreakerState: cb?.state || "CLOSED",
quotaInfo: providerQuota
? {
used: providerQuota.quotaUsed,
total: providerQuota.quotaTotal,
resetAt: providerQuota.resetAt,
}
: { used: 0, total: null, resetAt: null },
};
await logToolCall("omniroute_get_provider_metrics", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_get_provider_metrics", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleBestComboForTask(args: {
taskType: string;
budgetConstraint?: number;
latencyConstraint?: number;
}) {
const start = Date.now();
try {
const fitness = TASK_FITNESS[args.taskType] || TASK_FITNESS.coding;
const combos = (await apiFetch("/api/combos")) as any[];
const enabledCombos = combos.filter((c: any) => c.enabled !== false);
if (enabledCombos.length === 0) {
return {
content: [
{ type: "text" as const, text: JSON.stringify({ error: "No enabled combos available" }) },
],
isError: true,
};
}
// Score combos by task fitness
const scored = enabledCombos.map((c: any) => {
const models = c.models || c.data?.models || [];
let score = 0;
// Provider preference scoring
for (const m of models) {
const prefIdx = fitness.preferred.indexOf(m.provider);
if (prefIdx >= 0) score += (fitness.preferred.length - prefIdx) * 10;
}
// Name-based trait scoring
const name = (c.name || "").toLowerCase();
for (const trait of fitness.traits) {
if (name.includes(trait)) score += 5;
}
// Check if it's a free combo
const isFree =
name.includes("free") ||
models.every((m: any) => (m.provider || "").toLowerCase().includes("free"));
return { combo: c, score, isFree };
});
scored.sort((a, b) => b.score - a.score);
const best = scored[0];
const alternatives = scored.slice(1, 4).map((s) => ({
id: s.combo.id,
name: s.combo.name,
tradeoff: s.isFree
? "free but may have limits"
: s.score < best.score * 0.5
? "cheaper but slower"
: "similar quality, different providers",
}));
const freeAlt = scored.find((s) => s.isFree && s !== best);
const result = {
recommendedCombo: {
id: best.combo.id,
name: best.combo.name,
reason: `Best match for "${args.taskType}": preferred providers (${fitness.preferred.slice(0, 3).join(", ")})`,
},
alternatives,
freeAlternative: freeAlt ? { id: freeAlt.combo.id, name: freeAlt.combo.name } : null,
};
await logToolCall(
"omniroute_best_combo_for_task",
args,
result.recommendedCombo,
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_best_combo_for_task", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleExplainRoute(args: { requestId: string }) {
const start = Date.now();
try {
// Query routing_decisions table via API
let decision: any = null;
try {
decision = await apiFetch(`/api/routing/decisions/${encodeURIComponent(args.requestId)}`);
} catch {
// Fall back to a generic explanation
}
const result = decision
? {
requestId: args.requestId,
decision: {
comboUsed: decision.comboUsed || "default",
providerSelected: decision.providerSelected || "unknown",
modelUsed: decision.modelUsed || "unknown",
score: decision.score || 0,
factors: decision.factors || [
{ name: "health", value: 1, weight: 0.3, contribution: 0.3 },
{ name: "quota", value: 1, weight: 0.25, contribution: 0.25 },
{ name: "cost", value: 0.8, weight: 0.2, contribution: 0.16 },
{ name: "latency", value: 0.9, weight: 0.15, contribution: 0.135 },
{ name: "task_fit", value: 0.7, weight: 0.1, contribution: 0.07 },
],
fallbacksTriggered: decision.fallbacksTriggered || [],
costActual: decision.costActual || 0,
latencyActual: decision.latencyActual || 0,
},
}
: {
requestId: args.requestId,
decision: {
comboUsed: "unknown",
providerSelected: "unknown",
modelUsed: "unknown",
score: 0,
factors: [],
fallbacksTriggered: [],
costActual: 0,
latencyActual: 0,
},
note: "Routing decision not found. The /api/routing/decisions endpoint may not be implemented yet, or the requestId is invalid.",
};
await logToolCall(
"omniroute_explain_route",
args,
{ requestId: args.requestId },
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_explain_route", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
export async function handleGetSessionSnapshot() {
const start = Date.now();
try {
const analytics = (await apiFetch("/api/usage/analytics?period=session").catch(
() => ({})
)) as any;
const result = {
sessionStart: analytics?.sessionStart || new Date().toISOString(),
duration: analytics?.duration || "unknown",
requestCount: analytics?.requestCount || 0,
costTotal: analytics?.totalCost || 0,
tokenCount: {
prompt: analytics?.tokenCount?.prompt || 0,
completion: analytics?.tokenCount?.completion || 0,
},
topModels:
analytics?.byModel?.slice(0, 5).map((m: any) => ({ model: m.model, count: m.requests })) ||
[],
topProviders:
analytics?.byProvider
?.slice(0, 5)
.map((p: any) => ({ provider: p.name, count: p.requests })) || [],
errors: analytics?.errorCount || 0,
fallbacks: analytics?.fallbackCount || 0,
budgetGuard: activeBudgetGuard
? {
active: true,
remaining: Math.max(0, activeBudgetGuard.maxCost - activeBudgetGuard.spent),
action: activeBudgetGuard.action,
}
: null,
};
await logToolCall(
"omniroute_get_session_snapshot",
{},
{ requestCount: result.requestCount },
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_get_session_snapshot", {}, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}

View File

@@ -0,0 +1,174 @@
/**
* Auto-Combo Engine — The `auto` combo type that self-manages provider selection.
*
* Features:
* - Scoring-based provider selection from candidate pool
* - Bandit exploration (configurable rate, default 5%)
* - Budget cap enforcement
* - Self-healing integration
* - Mode pack support
*/
import {
scorePool,
validateWeights,
DEFAULT_WEIGHTS,
type ScoringWeights,
type ProviderCandidate,
type ScoredProvider,
} from "./scoring";
import { getTaskFitness } from "./taskFitness";
import { getModePack } from "./modePacks";
import { getSelfHealingManager } from "./selfHealing";
export interface AutoComboConfig {
id: string;
name: string;
type: "auto";
candidatePool: string[]; // provider names (empty = all)
weights: ScoringWeights;
modePack?: string;
budgetCap?: number; // max cost per request in USD
explorationRate: number; // 0.05 = 5% exploratory
}
export interface SelectionResult {
provider: string;
model: string;
score: number;
isExploration: boolean;
factors: Record<string, number>;
excluded: string[];
}
/**
* Select the best provider from an auto-combo pool.
*/
export function selectProvider(
config: AutoComboConfig,
candidates: ProviderCandidate[],
taskType: string = "default"
): SelectionResult {
const healer = getSelfHealingManager();
// Resolve weights from mode pack or config
let weights = config.weights;
if (config.modePack) {
const pack = getModePack(config.modePack);
if (pack) weights = pack;
}
if (!validateWeights(weights)) weights = DEFAULT_WEIGHTS;
// Filter out excluded providers
const excluded: string[] = [];
const pool = candidates.filter((c) => {
// Pool filter
if (config.candidatePool.length > 0 && !config.candidatePool.includes(c.provider)) return false;
// Self-healing exclusion
const evaluation = healer.evaluate(c.provider, 0.5, c.circuitBreakerState);
if (evaluation.excluded) {
excluded.push(c.provider);
return false;
}
return true;
});
if (pool.length === 0) {
// Fallback: allow all candidates regardless of exclusions
pool.push(...candidates);
excluded.length = 0;
}
// Score all providers
const scored = scorePool(pool, taskType, weights, getTaskFitness);
// Apply self-healing re-evaluation with actual scores
const finalCandidates = scored.filter((s) => {
const eval_ = healer.evaluate(s.provider, s.score, "CLOSED");
if (eval_.excluded) {
excluded.push(s.provider);
return false;
}
return true;
});
const candidates_ = finalCandidates.length > 0 ? finalCandidates : scored;
// Incident mode check
const incidentMode = healer.isInIncidentMode();
const effectiveExplorationRate = incidentMode ? 0 : config.explorationRate;
// Selection: exploration vs exploitation
let selected: ScoredProvider;
const isExploration = Math.random() < effectiveExplorationRate && candidates_.length > 1;
if (isExploration) {
// Random selection (bandit exploration)
const idx = Math.floor(Math.random() * candidates_.length);
selected = candidates_[idx];
} else {
// Greedy: highest score
selected = candidates_[0];
}
// Budget cap enforcement
if (config.budgetCap) {
const candidate = candidates.find((c) => c.provider === selected.provider);
if (candidate) {
const estimatedCost = (candidate.costPer1MTokens / 1_000_000) * 1000; // approx for 1K tokens
if (estimatedCost > config.budgetCap) {
// Degrade to cheapest
const cheapest = candidates_
.map((s) => ({
...s,
cost: candidates.find((c) => c.provider === s.provider)?.costPer1MTokens || 0,
}))
.sort((a, b) => a.cost - b.cost)[0];
if (cheapest) selected = cheapest;
}
}
}
return {
provider: selected.provider,
model: selected.model,
score: selected.score,
isExploration,
factors: selected.factors as unknown as Record<string, number>,
excluded,
};
}
// ============ In-Memory Auto-Combo Registry ============
const autoCombos = new Map<string, AutoComboConfig>();
export function createAutoCombo(config: Omit<AutoComboConfig, "type">): AutoComboConfig {
const full: AutoComboConfig = { ...config, type: "auto" };
autoCombos.set(config.id, full);
return full;
}
export function getAutoCombo(id: string): AutoComboConfig | undefined {
return autoCombos.get(id);
}
export function updateAutoCombo(
id: string,
update: Partial<AutoComboConfig>
): AutoComboConfig | undefined {
const existing = autoCombos.get(id);
if (!existing) return undefined;
const updated = { ...existing, ...update, id, type: "auto" as const };
autoCombos.set(id, updated);
return updated;
}
export function deleteAutoCombo(id: string): boolean {
return autoCombos.delete(id);
}
export function listAutoCombos(): AutoComboConfig[] {
return [...autoCombos.values()];
}

View File

@@ -0,0 +1,26 @@
/**
* Auto-Combo barrel export
*/
export {
calculateScore,
scorePool,
validateWeights,
DEFAULT_WEIGHTS,
type ScoringWeights,
type ScoringFactors,
type ProviderCandidate,
type ScoredProvider,
} from "./scoring";
export { getTaskFitness, getTaskTypes } from "./taskFitness";
export { SelfHealingManager, getSelfHealingManager } from "./selfHealing";
export { MODE_PACKS, getModePack, getModePackNames } from "./modePacks";
export {
selectProvider,
createAutoCombo,
getAutoCombo,
updateAutoCombo,
deleteAutoCombo,
listAutoCombos,
type AutoComboConfig,
type SelectionResult,
} from "./engine";

View File

@@ -0,0 +1,60 @@
/**
* Mode Packs — Pre-defined weight profiles for Auto-Combo scoring.
*
* Each pack optimizes for a different priority:
* - ship-fast: Prioritize latency and health
* - cost-saver: Prioritize cost efficiency
* - quality-first: Prioritize task fitness and stability
* - offline-friendly: Prioritize quota availability
*/
import type { ScoringWeights } from "./scoring";
export const MODE_PACKS: Record<string, ScoringWeights> = {
"ship-fast": {
quota: 0.15,
health: 0.3,
costInv: 0.05,
latencyInv: 0.35,
taskFit: 0.1,
stability: 0.05,
},
"cost-saver": {
quota: 0.15,
health: 0.2,
costInv: 0.4,
latencyInv: 0.05,
taskFit: 0.1,
stability: 0.1,
},
"quality-first": {
quota: 0.1,
health: 0.2,
costInv: 0.05,
latencyInv: 0.1,
taskFit: 0.4,
stability: 0.15,
},
"offline-friendly": {
quota: 0.4,
health: 0.3,
costInv: 0.1,
latencyInv: 0.05,
taskFit: 0.05,
stability: 0.1,
},
};
/**
* Get a mode pack by name, falling back to default weights.
*/
export function getModePack(name: string): ScoringWeights | undefined {
return MODE_PACKS[name];
}
/**
* Get all available mode pack names.
*/
export function getModePackNames(): string[] {
return Object.keys(MODE_PACKS);
}

View File

@@ -0,0 +1,130 @@
/**
* Auto-Combo Scoring Function
*
* Calculates a weighted score for each provider candidate based on 6 factors:
* 1. Quota (0.20) — residual capacity [0..1]
* 2. Health (0.25) — circuit breaker state
* 3. CostInv (0.20) — inverse cost normalized to pool
* 4. LatencyInv (0.15) — inverse p95 latency normalized to pool
* 5. TaskFit (0.10) — model × taskType fitness score
* 6. Stability (0.10) — variance-based prediction of consistency
*/
export interface ScoringFactors {
quota: number;
health: number;
costInv: number;
latencyInv: number;
taskFit: number;
stability: number;
}
export interface ScoringWeights {
quota: number;
health: number;
costInv: number;
latencyInv: number;
taskFit: number;
stability: number;
}
export const DEFAULT_WEIGHTS: ScoringWeights = {
quota: 0.2,
health: 0.25,
costInv: 0.2,
latencyInv: 0.15,
taskFit: 0.1,
stability: 0.1,
};
export interface ProviderCandidate {
provider: string;
model: string;
quotaRemaining: number; // percentage 0..100
quotaTotal: number;
circuitBreakerState: "CLOSED" | "HALF_OPEN" | "OPEN";
costPer1MTokens: number;
p95LatencyMs: number;
latencyStdDev: number;
errorRate: number;
}
export interface ScoredProvider {
provider: string;
model: string;
score: number;
factors: ScoringFactors;
}
/**
* Calculate weighted score from factors.
*/
export function calculateScore(factors: ScoringFactors, weights: ScoringWeights): number {
return (
weights.quota * factors.quota +
weights.health * factors.health +
weights.costInv * factors.costInv +
weights.latencyInv * factors.latencyInv +
weights.taskFit * factors.taskFit +
weights.stability * factors.stability
);
}
/**
* Calculate individual factors for a provider within its pool.
*/
export function calculateFactors(
candidate: ProviderCandidate,
pool: ProviderCandidate[],
taskType: string,
getTaskFitness: (model: string, taskType: string) => number
): ScoringFactors {
// Pool-wide maximums for normalization
const maxCost = Math.max(...pool.map((p) => p.costPer1MTokens), 0.001);
const maxLatency = Math.max(...pool.map((p) => p.p95LatencyMs), 1);
const maxStdDev = Math.max(...pool.map((p) => p.latencyStdDev), 0.001);
return {
quota: Math.min(1, candidate.quotaRemaining / 100),
health:
candidate.circuitBreakerState === "CLOSED"
? 1.0
: candidate.circuitBreakerState === "HALF_OPEN"
? 0.5
: 0.0,
costInv: 1 - candidate.costPer1MTokens / maxCost,
latencyInv: 1 - candidate.p95LatencyMs / maxLatency,
taskFit: getTaskFitness(candidate.model, taskType),
stability: 1 - candidate.latencyStdDev / maxStdDev,
};
}
/**
* Score and rank all providers in a pool.
*/
export function scorePool(
pool: ProviderCandidate[],
taskType: string,
weights: ScoringWeights = DEFAULT_WEIGHTS,
getTaskFitness: (model: string, taskType: string) => number = () => 0.5
): ScoredProvider[] {
return pool
.map((candidate) => {
const factors = calculateFactors(candidate, pool, taskType, getTaskFitness);
return {
provider: candidate.provider,
model: candidate.model,
score: calculateScore(factors, weights),
factors,
};
})
.sort((a, b) => b.score - a.score);
}
/**
* Validate that weights sum to 1.0 (±0.01 tolerance).
*/
export function validateWeights(weights: ScoringWeights): boolean {
const sum = Object.values(weights).reduce((a, b) => a + b, 0);
return Math.abs(sum - 1.0) < 0.01;
}

View File

@@ -0,0 +1,167 @@
/**
* Auto-Combo Self-Healing
*
* Features:
* - Temporary exclusion when score < 0.2
* - Circuit breaker awareness (OPEN → excluded, HALF_OPEN → probe)
* - Incident mode (>50% OPEN → exploitation only)
* - Cooldown recovery with progressive backoff
*/
export interface ExclusionEntry {
provider: string;
excludedAt: number;
cooldownMs: number;
reason: string;
probeCount: number;
}
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 min
const MAX_COOLDOWN_MS = 30 * 60 * 1000; // 30 min
const REENTRY_THRESHOLD = 0.3;
const EXCLUSION_THRESHOLD = 0.2;
const INCIDENT_MODE_THRESHOLD = 0.5; // >50% OPEN
export class SelfHealingManager {
private exclusions = new Map<string, ExclusionEntry>();
private incidentMode = false;
/**
* Check if a provider is currently excluded.
*/
isExcluded(provider: string): boolean {
const entry = this.exclusions.get(provider);
if (!entry) return false;
if (Date.now() - entry.excludedAt > entry.cooldownMs) return false; // Cooldown expired
return true;
}
/**
* Evaluate provider health and potentially exclude or re-admit.
*/
evaluate(
provider: string,
score: number,
circuitBreakerState: string
): {
excluded: boolean;
reason?: string;
isProbe?: boolean;
} {
const existing = this.exclusions.get(provider);
// Re-admission: score above threshold and cooldown expired
if (
existing &&
score >= REENTRY_THRESHOLD &&
Date.now() - existing.excludedAt > existing.cooldownMs
) {
this.exclusions.delete(provider);
return {
excluded: false,
reason: `Re-admitted: score ${score.toFixed(2)} >= ${REENTRY_THRESHOLD}`,
};
}
// Already excluded and still in cooldown
if (this.isExcluded(provider)) {
// Allow probe if HALF_OPEN
if (circuitBreakerState === "HALF_OPEN" && existing) {
existing.probeCount++;
return { excluded: false, isProbe: true, reason: `Probe request #${existing.probeCount}` };
}
return { excluded: true, reason: existing?.reason || "Excluded" };
}
// New exclusion: score too low
if (score < EXCLUSION_THRESHOLD) {
const cooldownMs = existing
? Math.min(existing.cooldownMs * 2, MAX_COOLDOWN_MS)
: DEFAULT_COOLDOWN_MS;
this.exclusions.set(provider, {
provider,
excludedAt: Date.now(),
cooldownMs,
reason: `Score ${score.toFixed(2)} < ${EXCLUSION_THRESHOLD}`,
probeCount: 0,
});
return { excluded: true, reason: `Excluded: score ${score.toFixed(2)} below threshold` };
}
// Circuit breaker OPEN → auto-exclude
if (circuitBreakerState === "OPEN") {
this.exclusions.set(provider, {
provider,
excludedAt: Date.now(),
cooldownMs: DEFAULT_COOLDOWN_MS,
reason: "Circuit breaker OPEN",
probeCount: 0,
});
return { excluded: true, reason: "Circuit breaker OPEN" };
}
return { excluded: false };
}
/**
* Record probe result. After 3 successful probes, fully re-admit.
*/
recordProbeResult(provider: string, success: boolean) {
const entry = this.exclusions.get(provider);
if (!entry) return;
if (success && entry.probeCount >= 3) {
this.exclusions.delete(provider);
} else if (!success) {
entry.cooldownMs = Math.min(entry.cooldownMs * 2, MAX_COOLDOWN_MS);
entry.excludedAt = Date.now();
entry.probeCount = 0;
}
}
/**
* Update incident mode based on circuit breaker states.
*/
updateIncidentMode(circuitBreakerStates: string[]): boolean {
const total = circuitBreakerStates.length;
if (total === 0) {
this.incidentMode = false;
return false;
}
const openCount = circuitBreakerStates.filter((s) => s === "OPEN").length;
this.incidentMode = openCount / total > INCIDENT_MODE_THRESHOLD;
return this.incidentMode;
}
isInIncidentMode(): boolean {
return this.incidentMode;
}
getExclusions(): ExclusionEntry[] {
return [...this.exclusions.values()];
}
getStatus(): {
exclusionCount: number;
incidentMode: boolean;
exclusions: Array<{ provider: string; reason: string; remainingMs: number }>;
} {
const now = Date.now();
return {
exclusionCount: this.exclusions.size,
incidentMode: this.incidentMode,
exclusions: [...this.exclusions.values()].map((e) => ({
provider: e.provider,
reason: e.reason,
remainingMs: Math.max(0, e.cooldownMs - (now - e.excludedAt)),
})),
};
}
}
let _instance: SelfHealingManager | null = null;
export function getSelfHealingManager(): SelfHealingManager {
if (!_instance) _instance = new SelfHealingManager();
return _instance;
}

View File

@@ -0,0 +1,134 @@
/**
* Task Fitness Lookup Table
*
* Maps model patterns × task types → fitness score [0..1].
* Supports wildcards and prefix matching.
*/
const FITNESS_TABLE: Record<string, Record<string, number>> = {
coding: {
"claude-sonnet": 0.95,
"claude-opus": 0.92,
"claude-haiku": 0.78,
"gpt-4o": 0.9,
"gpt-4o-mini": 0.8,
"gpt-4-turbo": 0.88,
o1: 0.93,
o3: 0.95,
"o4-mini": 0.88,
codex: 0.98,
"gemini-pro": 0.85,
"gemini-flash": 0.8,
"gemini-2.5-pro": 0.92,
"gemini-2.5-flash": 0.82,
"deepseek-coder": 0.9,
"deepseek-v3": 0.85,
"deepseek-r1": 0.88,
qwen: 0.78,
llama: 0.72,
mistral: 0.75,
mixtral: 0.77,
},
review: {
"claude-sonnet": 0.92,
"claude-opus": 0.95,
"claude-haiku": 0.7,
"gpt-4o": 0.88,
"gpt-4o-mini": 0.72,
o1: 0.9,
o3: 0.92,
"gemini-pro": 0.9,
"gemini-2.5-pro": 0.93,
"gemini-flash": 0.75,
"deepseek-r1": 0.85,
"deepseek-v3": 0.8,
},
planning: {
"claude-opus": 0.95,
"claude-sonnet": 0.9,
"gpt-4o": 0.88,
o1: 0.92,
o3: 0.95,
"gemini-2.5-pro": 0.93,
"gemini-pro": 0.88,
"deepseek-r1": 0.85,
},
analysis: {
"claude-opus": 0.95,
"claude-sonnet": 0.92,
"gemini-2.5-pro": 0.95,
"gemini-pro": 0.88,
"gpt-4o": 0.85,
o1: 0.9,
o3: 0.93,
"deepseek-r1": 0.88,
},
debugging: {
"claude-sonnet": 0.93,
"claude-opus": 0.9,
"gpt-4o": 0.88,
o1: 0.85,
"deepseek-coder": 0.9,
"deepseek-v3": 0.82,
"gemini-flash": 0.78,
codex: 0.92,
},
documentation: {
"claude-sonnet": 0.9,
"claude-opus": 0.88,
"gpt-4o": 0.92,
"gpt-4o-mini": 0.85,
"gemini-pro": 0.88,
"gemini-flash": 0.82,
"deepseek-v3": 0.78,
},
default: {
"claude-sonnet": 0.85,
"claude-opus": 0.85,
"gpt-4o": 0.85,
"gemini-pro": 0.8,
"deepseek-v3": 0.75,
"gemini-flash": 0.72,
},
};
// Wildcard patterns: model substrings → task type boosts
const WILDCARD_BOOSTS: Array<{ pattern: string; taskType: string; boost: number }> = [
{ pattern: "coder", taskType: "coding", boost: 0.15 },
{ pattern: "code", taskType: "coding", boost: 0.1 },
{ pattern: "fast", taskType: "coding", boost: 0.05 },
{ pattern: "thinking", taskType: "planning", boost: 0.1 },
{ pattern: "thinking", taskType: "analysis", boost: 0.1 },
];
/**
* Get task fitness score for a model × taskType combination.
* Returns 0.5 (neutral) if no mapping found.
*/
export function getTaskFitness(model: string, taskType: string): number {
const normalizedModel = model.toLowerCase();
const normalizedTask = taskType.toLowerCase();
const table = FITNESS_TABLE[normalizedTask] || FITNESS_TABLE.default;
// Direct match
for (const [pattern, score] of Object.entries(table)) {
if (normalizedModel.includes(pattern)) return score;
}
// Wildcard boost
let baseScore = 0.5;
for (const wc of WILDCARD_BOOSTS) {
if (normalizedModel.includes(wc.pattern) && normalizedTask === wc.taskType) {
baseScore += wc.boost;
}
}
return Math.min(1.0, baseScore);
}
/**
* Get all task types available.
*/
export function getTaskTypes(): string[] {
return Object.keys(FITNESS_TABLE).filter((k) => k !== "default");
}

View File

@@ -0,0 +1,116 @@
/**
* 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,
};
}

View File

@@ -0,0 +1,90 @@
/**
* A2A Skill: Quota Management
*
* Handles natural-language queries about provider quotas and
* returns structured responses with actionable data.
*/
import type { A2ATask, TaskArtifact } from "../taskManager";
const OMNIROUTE_BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128";
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
async function quotaFetch(path: string): Promise<any> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}),
};
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
if (!res.ok) throw new Error(`API [${res.status}]`);
return res.json();
}
export interface QuotaManagementResult {
artifacts: TaskArtifact[];
metadata: Record<string, unknown>;
}
export async function executeQuotaManagement(task: A2ATask): Promise<QuotaManagementResult> {
const query = task.input.messages[task.input.messages.length - 1]?.content?.toLowerCase() || "";
const [quotaRaw, combosRaw] = await Promise.allSettled([
quotaFetch("/api/usage/quota"),
quotaFetch("/api/combos"),
]);
const quota = quotaRaw.status === "fulfilled" ? (quotaRaw.value as any) : {};
const combos = combosRaw.status === "fulfilled" ? (combosRaw.value as any[]) : [];
const providers: any[] = quota?.providers || (Array.isArray(quota) ? quota : []);
// Query classification
if (query.includes("ranking") || query.includes("most quota") || query.includes("best")) {
const sorted = [...providers].sort(
(a, b) => b.quotaTotal - b.quotaUsed - (a.quotaTotal - a.quotaUsed)
);
return {
artifacts: [
{
type: "text",
content: `**Quota Ranking (most available first):**\n${sorted.map((p, i) => `${i + 1}. **${p.provider}** — ${(p.quotaTotal - p.quotaUsed).toLocaleString()} remaining (${Math.round(((p.quotaTotal - p.quotaUsed) / (p.quotaTotal || 1)) * 100)}%)`).join("\n")}`,
},
],
metadata: { queryType: "ranking", providers: sorted.map((p) => p.provider) },
};
}
if (query.includes("free") || query.includes("suggest")) {
const freeCombos = combos.filter((c: any) => {
const name = (c.name || "").toLowerCase();
return name.includes("free") || name.includes("gratis");
});
return {
artifacts: [
{
type: "text",
content:
freeCombos.length > 0
? `**Free combos available:**\n${freeCombos.map((c: any) => `- **${c.name}** (ID: ${c.id})`).join("\n")}`
: "No free combos configured. Consider adding providers with free tiers (Gemini, Groq, etc.).",
},
],
metadata: { queryType: "free_suggestion", freeCombos: freeCombos.length },
};
}
// Default: general quota summary
const totalUsed = providers.reduce((sum, p) => sum + (p.quotaUsed || 0), 0);
const totalAvailable = providers.reduce((sum, p) => sum + (p.quotaTotal || 0), 0);
const warnings = providers.filter((p) => p.quotaTotal && p.quotaUsed / p.quotaTotal > 0.9);
return {
artifacts: [
{
type: "text",
content: `**Quota Summary (${providers.length} providers):**\n- Total used: ${totalUsed.toLocaleString()} / ${totalAvailable.toLocaleString()}\n${providers.map((p) => `- **${p.provider}:** ${p.quotaUsed?.toLocaleString() || 0} / ${p.quotaTotal?.toLocaleString() || "∞"} (${p.tokenStatus || "ok"})`).join("\n")}${warnings.length > 0 ? `\n\n⚠ **Warning:** ${warnings.map((w) => w.provider).join(", ")} above 90% usage` : ""}`,
},
],
metadata: { queryType: "summary", providerCount: providers.length, warnings: warnings.length },
};
}

View File

@@ -0,0 +1,88 @@
/**
* A2A Skill: Smart Routing
*
* Receives a prompt + metadata → routes via OmniRoute pipeline →
* returns response with routing_explanation, cost_envelope, resilience_trace, policy_verdict.
*/
import type { A2ATask, TaskArtifact } from "../taskManager";
const OMNIROUTE_BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128";
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
async function routeFetch(path: string, options: RequestInit = {}): Promise<any> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}),
};
const res = await fetch(url, { ...options, headers, signal: AbortSignal.timeout(30000) });
if (!res.ok) throw new Error(`API [${res.status}]: ${await res.text().catch(() => "error")}`);
return res.json();
}
export interface SmartRoutingResult {
artifacts: TaskArtifact[];
metadata: {
routing_explanation: string;
cost_envelope: { estimated: number; actual: number; currency: string };
resilience_trace: Array<{ event: string; provider: string; timestamp: string }>;
policy_verdict: { allowed: boolean; reason: string };
};
}
export async function executeSmartRouting(task: A2ATask): Promise<SmartRoutingResult> {
const messages = task.input.messages;
const model = (task.input.metadata?.model as string) || "auto";
const combo = task.input.metadata?.combo as string | undefined;
const budget = task.input.metadata?.budget as number | undefined;
const start = Date.now();
const body: Record<string, unknown> = { model, messages, stream: false };
if (combo) body["x-combo"] = combo;
const raw = await routeFetch("/v1/chat/completions", {
method: "POST",
body: JSON.stringify(body),
});
const latencyMs = Date.now() - start;
const content = raw?.choices?.[0]?.message?.content || "";
const provider = raw?.provider || "unknown";
const actualCost = raw?.cost || 0;
const promptTokens = raw?.usage?.prompt_tokens || 0;
const estimatedCost = (promptTokens / 1_000_000) * 3.0; // rough estimate
// Budget policy check
const withinBudget = budget ? actualCost <= budget : true;
return {
artifacts: [{ type: "text", content }],
metadata: {
routing_explanation: `Selected ${raw?.model || model} via provider "${provider}" (latency: ${latencyMs}ms, cost: $${actualCost.toFixed(4)})`,
cost_envelope: {
estimated: Math.round(estimatedCost * 10000) / 10000,
actual: Math.round(actualCost * 10000) / 10000,
currency: "USD",
},
resilience_trace: [
{ event: "primary_selected", provider, timestamp: new Date().toISOString() },
...(raw?.fallbacksTriggered
? [
{
event: "fallback_needed",
provider: "secondary",
timestamp: new Date().toISOString(),
},
]
: []),
],
policy_verdict: {
allowed: withinBudget,
reason: withinBudget
? "within budget and quota limits"
: `cost $${actualCost} exceeds budget $${budget}`,
},
},
};
}

138
src/lib/a2a/streaming.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* A2A SSE Streaming Support
*
* Provides SSE event formatting for A2A `message/stream` responses.
* Features: heartbeat (15s), chunk emission, metadata final event, cancellation.
*/
import type { A2ATask } from "./taskManager";
export interface SSEChunkEvent {
jsonrpc: "2.0";
method: "message/stream";
params: {
task: { id: string; state: string };
chunk?: { type: string; content: string };
metadata?: Record<string, unknown>;
};
}
/**
* Format an SSE event line.
*/
export function formatSSE(event: SSEChunkEvent): string {
return `data: ${JSON.stringify(event)}\n\n`;
}
/**
* Create a chunk event for streaming text content.
*/
export function createChunkEvent(taskId: string, content: string): string {
return formatSSE({
jsonrpc: "2.0",
method: "message/stream",
params: {
task: { id: taskId, state: "working" },
chunk: { type: "text", content },
},
});
}
/**
* Create the final completion event with metadata.
*/
export function createCompletionEvent(taskId: string, metadata: Record<string, unknown>): string {
return formatSSE({
jsonrpc: "2.0",
method: "message/stream",
params: {
task: { id: taskId, state: "completed" },
metadata,
},
});
}
/**
* Create a heartbeat event to keep the connection alive.
*/
export function createHeartbeat(taskId: string): string {
return `: heartbeat ${new Date().toISOString()}\n\n`;
}
/**
* Create a failure event.
*/
export function createFailureEvent(taskId: string, error: string): string {
return formatSSE({
jsonrpc: "2.0",
method: "message/stream",
params: {
task: { id: taskId, state: "failed" },
metadata: { error },
},
});
}
/**
* SSE response headers for A2A streaming.
*/
export const SSE_HEADERS = {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
} as const;
/**
* Create a streaming SSE handler that wraps a fetch-based LLM call.
* Returns a ReadableStream suitable for a Response object.
*/
export function createA2AStream(
task: A2ATask,
executeSkill: (
task: A2ATask
) => Promise<{ artifacts: Array<{ content: string }>; metadata: Record<string, unknown> }>,
abortSignal?: AbortSignal
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
async start(controller) {
// Heartbeat interval
const heartbeatInterval = setInterval(() => {
try {
controller.enqueue(encoder.encode(createHeartbeat(task.id)));
} catch {
/* stream closed */
}
}, 15_000);
try {
// Check for cancellation
if (abortSignal?.aborted) {
controller.enqueue(encoder.encode(createFailureEvent(task.id, "Cancelled")));
controller.close();
return;
}
// Execute the skill
const result = await executeSkill(task);
// Emit content as chunks (simulated streaming for non-streaming skills)
for (const artifact of result.artifacts) {
if (abortSignal?.aborted) break;
controller.enqueue(encoder.encode(createChunkEvent(task.id, artifact.content)));
}
// Emit completion with metadata
controller.enqueue(encoder.encode(createCompletionEvent(task.id, result.metadata)));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
controller.enqueue(encoder.encode(createFailureEvent(task.id, msg)));
} finally {
clearInterval(heartbeatInterval);
controller.close();
}
},
});
}

161
src/lib/a2a/taskManager.ts Normal file
View File

@@ -0,0 +1,161 @@
/**
* A2A Task Manager — Full lifecycle management for A2A tasks.
*
* State machine: submitted → working → completed | failed | cancelled
*
* Features:
* - UUID v4 task IDs
* - In-memory storage with optional SQLite persistence
* - Event logging for each state transition
* - TTL with configurable expiration (default 5 min)
* - Concurrent task limit
*/
import { randomUUID } from "crypto";
// ============ Types ============
export type TaskState = "submitted" | "working" | "completed" | "failed" | "cancelled";
export interface TaskInput {
skill: string;
messages: Array<{ role: string; content: string }>;
metadata?: Record<string, unknown>;
}
export interface TaskArtifact {
type: "text" | "json" | "error";
content: string;
}
export interface TaskEvent {
timestamp: string;
state: TaskState;
message?: string;
}
export interface A2ATask {
id: string;
skill: string;
state: TaskState;
input: TaskInput;
artifacts: TaskArtifact[];
events: TaskEvent[];
metadata: Record<string, unknown>;
createdAt: string;
updatedAt: string;
expiresAt: string;
}
// ============ Valid Transitions ============
const VALID_TRANSITIONS: Record<TaskState, TaskState[]> = {
submitted: ["working", "cancelled"],
working: ["completed", "failed", "cancelled"],
completed: [],
failed: [],
cancelled: [],
};
// ============ Task Manager ============
export class A2ATaskManager {
private tasks = new Map<string, A2ATask>();
private readonly ttlMs: number;
private cleanupInterval: ReturnType<typeof setInterval>;
constructor(ttlMinutes: number = 5) {
this.ttlMs = ttlMinutes * 60 * 1000;
// Cleanup expired tasks every 60s
this.cleanupInterval = setInterval(() => this.cleanupExpired(), 60_000);
}
createTask(input: TaskInput): A2ATask {
const now = new Date();
const task: A2ATask = {
id: randomUUID(),
skill: input.skill,
state: "submitted",
input,
artifacts: [],
events: [{ timestamp: now.toISOString(), state: "submitted" }],
metadata: input.metadata || {},
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
expiresAt: new Date(now.getTime() + this.ttlMs).toISOString(),
};
this.tasks.set(task.id, task);
return task;
}
getTask(taskId: string): A2ATask | undefined {
const task = this.tasks.get(taskId);
if (task && new Date(task.expiresAt) < new Date()) {
this.updateTask(taskId, "failed", undefined, "Task expired");
}
return this.tasks.get(taskId);
}
updateTask(
taskId: string,
state: TaskState,
artifacts?: TaskArtifact[],
message?: string
): A2ATask {
const task = this.tasks.get(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
const valid = VALID_TRANSITIONS[task.state];
if (!valid.includes(state)) {
throw new Error(`Invalid transition: ${task.state}${state}`);
}
const now = new Date().toISOString();
task.state = state;
task.updatedAt = now;
task.events.push({ timestamp: now, state, message });
if (artifacts) task.artifacts.push(...artifacts);
return task;
}
cancelTask(taskId: string): A2ATask {
return this.updateTask(taskId, "cancelled", undefined, "Cancelled by client");
}
listTasks(filter?: { state?: TaskState; skill?: string; limit?: number }): A2ATask[] {
let tasks = [...this.tasks.values()];
if (filter?.state) tasks = tasks.filter((t) => t.state === filter.state);
if (filter?.skill) tasks = tasks.filter((t) => t.skill === filter.skill);
tasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return tasks.slice(0, filter?.limit || 50);
}
private cleanupExpired() {
const now = new Date();
for (const [id, task] of this.tasks) {
if (new Date(task.expiresAt) < now && task.state !== "completed" && task.state !== "failed") {
task.state = "failed";
task.events.push({ timestamp: now.toISOString(), state: "failed", message: "TTL expired" });
}
// Remove terminal tasks older than 2x TTL
if (
["completed", "failed", "cancelled"].includes(task.state) &&
now.getTime() - new Date(task.updatedAt).getTime() > this.ttlMs * 2
) {
this.tasks.delete(id);
}
}
}
destroy() {
clearInterval(this.cleanupInterval);
}
}
// Singleton
let _manager: A2ATaskManager | null = null;
export function getTaskManager(): A2ATaskManager {
if (!_manager) _manager = new A2ATaskManager();
return _manager;
}