/** * Provider Adapter SDK - Registry * * Registry for managing provider adapters with lifecycle hooks. */ import { EventEmitter } from "node:events"; import { SDK_VERSION } from "./types.js"; import type { AdapterInfo, AdapterLifecycle, AdapterResult, AdapterState, AdapterVersion, CompatibilityResult, ErrorAnalysis, HealthCheckResult, ProviderCapability, ProviderMessage, ProviderRequest, } from "./types.js"; import type { BuiltAdapter } from "./builder.js"; import { AdapterAlreadyRegisteredError, AdapterNotFoundError, AdapterStateError, CompatibilityError, LifecycleError, } from "./errors.js"; /** * Registry configuration */ export interface RegistryConfig { /** Enable automatic health checks */ autoHealthCheck?: boolean; /** Health check interval in ms (default: 60000) */ healthCheckInterval?: number; /** Timeout for health checks in ms (default: 5000) */ healthCheckTimeout?: number; /** Enable lifecycle hooks */ enableLifecycle?: boolean; } /** * Registry entry */ interface RegistryEntry { adapter: BuiltAdapter; lifecycle?: AdapterLifecycle; state: AdapterState; version?: AdapterVersion; registeredAt: string; lastHealthCheck?: HealthCheckResult; healthCheckTimer?: ReturnType; } /** * Adapter registry with lifecycle management */ export class AdapterRegistry extends EventEmitter { private readonly adapters: Map = new Map(); private readonly config: Required; constructor(config: RegistryConfig = {}) { super(); this.config = { autoHealthCheck: config.autoHealthCheck ?? false, healthCheckInterval: config.healthCheckInterval ?? 60000, healthCheckTimeout: config.healthCheckTimeout ?? 5000, enableLifecycle: config.enableLifecycle ?? true, }; } /** * Register an adapter */ async register( adapter: | BuiltAdapter | { name: string; provider?: string; complete: ( prompt: string, options?: Record, ) => Promise<{ content: string }>; }, lifecycle?: AdapterLifecycle, ): Promise { const id = "id" in adapter ? adapter.id : adapter.name; if (this.adapters.has(id)) { throw new AdapterAlreadyRegisteredError(id); } // Wrap plain adapters into BuiltAdapter-compatible object let wrappedAdapter: BuiltAdapter; if ("invoke" in adapter) { wrappedAdapter = adapter as BuiltAdapter; } else { // Wrap simple adapter const simpleAdapter = adapter as { name: string; provider?: string; complete: Function; }; wrappedAdapter = { id, name: simpleAdapter.name, provider: simpleAdapter.provider, invoke: async (request: ProviderRequest) => { const prompt = request.messages .filter((m: ProviderMessage) => m.role === "user") .map((m: ProviderMessage) => m.content) .join("\n"); return simpleAdapter.complete(prompt); }, parseError: () => ({ quotaExceeded: false, rateLimited: false, timeout: false, serverError: false, clientError: false, }), getCapabilities: () => [], getModels: () => [], } as unknown as BuiltAdapter; } // Check compatibility const compatibility = this.checkCompatibility(wrappedAdapter); if (!compatibility.compatible) { throw new CompatibilityError(id, compatibility.issues, SDK_VERSION); } // Initialize lifecycle if enabled if (lifecycle?.onInit && this.config.enableLifecycle) { try { await lifecycle.onInit(); } catch (error) { throw new LifecycleError("onInit", id, error); } } // Create version info const version: AdapterVersion = { sdk: SDK_VERSION, capabilities: wrappedAdapter.getCapabilities(), }; // Create registry entry const entry: RegistryEntry = { adapter: wrappedAdapter, lifecycle, state: "ready", version, registeredAt: new Date().toISOString(), }; this.adapters.set(id, entry); // Emit events const info = this.createAdapterInfo(entry); this.emit("adapterRegistered", info); // Start health check timer if enabled if (this.config.autoHealthCheck) { this.startHealthCheck(id); } return info; } /** * Unregister an adapter */ async unregister(id: string): Promise { const entry = this.adapters.get(id); if (!entry) { throw new AdapterNotFoundError(id); } // Stop health check timer if (entry.healthCheckTimer) { clearInterval(entry.healthCheckTimer); } // Run teardown lifecycle if (entry.lifecycle?.onTeardown && this.config.enableLifecycle) { try { await entry.lifecycle.onTeardown(); } catch (error) { console.error(`[Registry] Teardown failed for ${id}:`, error); } } this.adapters.delete(id); this.emit("adapterUnregistered", id); } /** * Get an adapter by ID */ getAdapter(id: string): BuiltAdapter | undefined { return this.adapters.get(id)?.adapter; } /** * Get adapter by ID (alias) */ get(id: string): BuiltAdapter | undefined { return this.getAdapter(id); } /** * Check if adapter exists */ has(id: string): boolean { return this.adapters.has(id); } /** * List all adapters (alias) */ listAll(): AdapterInfo[] { return this.listAdapters(); } /** * Get adapter info */ getAdapterInfo(id: string): AdapterInfo | undefined { const entry = this.adapters.get(id); return entry ? this.createAdapterInfo(entry) : undefined; } /** * List all adapters */ listAdapters(): AdapterInfo[] { return Array.from(this.adapters.values()).map((entry) => this.createAdapterInfo(entry), ); } /** * List adapters by capability */ listByCapability(capability: ProviderCapability): AdapterInfo[] { return this.listAdapters().filter((info) => info.capabilities.includes(capability), ); } /** * Check if provider is supported */ supportsCapability(capability: ProviderCapability): boolean { return this.listByCapability(capability).length > 0; } /** * Get adapters in a specific state */ listByState(state: AdapterState): AdapterInfo[] { return this.listAdapters().filter((info) => info.state === state); } /** * Invoke an adapter */ async invoke(id: string, request: ProviderRequest): Promise { const entry = this.adapters.get(id); if (!entry) { throw new AdapterNotFoundError(id); } if (entry.state !== "ready") { throw new AdapterStateError(id, "ready", entry.state); } return entry.adapter.invoke(request); } /** * Parse error for an adapter */ parseError(id: string, error: unknown): ErrorAnalysis { const entry = this.adapters.get(id); if (!entry) { throw new AdapterNotFoundError(id); } return entry.adapter.parseError(error); } /** * Health check for a single adapter */ async healthCheck(id: string): Promise { const entry = this.adapters.get(id); if (!entry) { throw new AdapterNotFoundError(id); } const timeout = this.config.healthCheckTimeout; const healthCheckPromise = entry.adapter.healthCheck(); const timeoutPromise = new Promise((resolve) => setTimeout( () => resolve({ healthy: false, message: "Health check timed out", timestamp: new Date().toISOString(), }), timeout, ), ); const result = await Promise.race([healthCheckPromise, timeoutPromise]); // Update entry entry.lastHealthCheck = result; // Update state based on health const wasHealthy = entry.state === "ready"; if (!result.healthy && wasHealthy) { entry.state = "error"; this.emit("adapterStateChanged", id, "error"); this.emit("healthCheckFailed", id, result); } else if (result.healthy && entry.state === "error") { entry.state = "ready"; this.emit("adapterStateChanged", id, "ready"); } return result; } /** * Health check all adapters */ async healthCheckAll(): Promise> { const results = new Map(); await Promise.allSettled( Array.from(this.adapters.keys()).map(async (id) => { const result = await this.healthCheck(id); results.set(id, result); }), ); return results; } /** * Start automatic health checks for an adapter */ private startHealthCheck(id: string): void { const entry = this.adapters.get(id); if (!entry) return; // Clear existing timer if (entry.healthCheckTimer) { clearInterval(entry.healthCheckTimer); } // Start new timer entry.healthCheckTimer = setInterval(async () => { await this.healthCheck(id); }, this.config.healthCheckInterval); } /** * Stop automatic health checks for an adapter */ stopHealthCheck(id: string): void { const entry = this.adapters.get(id); if (entry?.healthCheckTimer) { clearInterval(entry.healthCheckTimer); entry.healthCheckTimer = undefined; } } /** * Stop all health checks */ stopAllHealthChecks(): void { for (const [id] of this.adapters) { this.stopHealthCheck(id); } } /** * Check adapter compatibility with SDK */ private checkCompatibility(adapter: BuiltAdapter): CompatibilityResult { const issues: string[] = []; const warnings: string[] = []; // Check if adapter has required methods if (typeof adapter.invoke !== "function") { issues.push("Adapter missing invoke method"); } if (typeof adapter.parseError !== "function") { issues.push("Adapter missing parseError method"); } // Check models if (adapter.getModels().length === 0) { warnings.push("Adapter has no models specified"); } return { compatible: issues.length === 0, issues, warnings, }; } /** * Create adapter info from entry */ private createAdapterInfo(entry: RegistryEntry): AdapterInfo { return { id: entry.adapter.id, name: entry.adapter.name, version: entry.version, capabilities: entry.adapter.getCapabilities(), models: entry.adapter.getModels(), registeredAt: entry.registeredAt, state: entry.state, lastHealthCheck: entry.lastHealthCheck, }; } /** * Get registry statistics */ getStats(): { total: number; byState: Record; byCapability: Record; } { const byState: Record = { registered: 0, initializing: 0, ready: 0, error: 0, unavailable: 0, }; const byCapability: Record = { code: 0, review: 0, plan: 0, test: 0, e2e: 0, refactor: 0, analysis: 0, debug: 0, }; for (const entry of this.adapters.values()) { byState[entry.state]++; for (const cap of entry.adapter.getCapabilities()) { byCapability[cap as ProviderCapability]++; } } return { total: this.adapters.size, byState, byCapability, }; } } // ─── Factory Function ────────────────────────────────────────────────────── /** * Create a new adapter registry */ export function createAdapterRegistry( config?: RegistryConfig, ): AdapterRegistry { return new AdapterRegistry(config); }