| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { and, eq, gte } from "drizzle-orm"; |
| import { |
| db, |
| networkEvolutionEvents, |
| networkVersions, |
| toolNetworks, |
| type NetworkVersionRow, |
| } from "@workspace/db"; |
| import { newId } from "../ids"; |
| import { logger } from "../logger"; |
| import { recordEvent } from "./events"; |
| import { |
| DEFAULT_EVOLUTION_STRATEGY, |
| pickStrategy, |
| type CandidateProposal, |
| } from "./strategies"; |
| import { stableStringify } from "./strategies/hyperparameter-grid"; |
|
|
| const NO_CANDIDATE_DEBOUNCE_MS = 60 * 60 * 1000; |
|
|
| export type BuilderStatus = |
| | "proposed" |
| | "no_candidate" |
| | "no_candidate_debounced" |
| | "skipped_no_strategy" |
| | "skipped_no_active" |
| | "skipped_no_tunable_params" |
| | "error"; |
|
|
| export interface BuilderResult { |
| networkId: string; |
| status: BuilderStatus; |
| strategyName?: string; |
| candidateVariantId?: string; |
| versionLabel?: string; |
| config?: Record<string, unknown>; |
| rationale?: string; |
| diff?: string[]; |
| error?: string; |
| } |
|
|
| export interface RunEvolutionBuilderInput { |
| networkId: string; |
| |
| reason: string; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function runEvolutionBuilder( |
| input: RunEvolutionBuilderInput, |
| ): Promise<BuilderResult> { |
| try { |
| return await runEvolutionBuilderInner(input); |
| } catch (err) { |
| |
| |
| |
| |
| |
| |
| const msg = err instanceof Error ? err.message : String(err); |
| logger.error( |
| { err, networkId: input.networkId, reason: input.reason }, |
| "evolution builder: unexpected throw escaped inner handlers", |
| ); |
| await safeWriteEvent({ |
| networkId: input.networkId, |
| kind: "builder_error", |
| payload: { |
| phase: "uncaught", |
| error: msg, |
| reason: input.reason, |
| }, |
| }); |
| return { |
| networkId: input.networkId, |
| status: "error", |
| error: `uncaught: ${msg}`, |
| }; |
| } |
| } |
|
|
| async function runEvolutionBuilderInner( |
| input: RunEvolutionBuilderInput, |
| ): Promise<BuilderResult> { |
| const { networkId, reason } = input; |
|
|
| let network: Awaited<ReturnType<typeof loadNetwork>> | null = null; |
| try { |
| network = await loadNetwork(networkId); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : String(err); |
| logger.error({ err, networkId }, "evolution builder: load network failed"); |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_error", |
| payload: { phase: "load_network", error: msg, reason }, |
| }); |
| return { networkId, status: "error", error: `load_network: ${msg}` }; |
| } |
| if (!network) { |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_error", |
| payload: { phase: "load_network", error: "network_not_found", reason }, |
| }); |
| return { networkId, status: "error", error: "network_not_found" }; |
| } |
|
|
| const cfg = (network.config ?? {}) as Record<string, unknown>; |
| const requestedStrategy = |
| typeof cfg["evolutionStrategy"] === "string" |
| ? (cfg["evolutionStrategy"] as string) |
| : DEFAULT_EVOLUTION_STRATEGY; |
| |
| |
| |
| |
| |
| |
| |
| |
| const strategy = pickStrategy(requestedStrategy); |
| const strategyName = strategy.name; |
|
|
| if (!network.activeVariantId) { |
| return { |
| networkId, |
| status: "skipped_no_active", |
| strategyName, |
| }; |
| } |
|
|
| |
| |
| |
| |
| if (strategyName === DEFAULT_EVOLUTION_STRATEGY) { |
| const tp = cfg["tunableParams"]; |
| if (!tp || typeof tp !== "object" || Array.isArray(tp)) { |
| return { |
| networkId, |
| status: "skipped_no_tunable_params", |
| strategyName, |
| }; |
| } |
| } |
|
|
| let activeVariant: NetworkVersionRow | null = null; |
| let existingVersions: NetworkVersionRow[] = []; |
| try { |
| const activeRows = await db |
| .select() |
| .from(networkVersions) |
| .where(eq(networkVersions.id, network.activeVariantId)) |
| .limit(1); |
| activeVariant = activeRows[0] ?? null; |
| if (!activeVariant) { |
| return { |
| networkId, |
| status: "skipped_no_active", |
| strategyName, |
| }; |
| } |
| existingVersions = await db |
| .select() |
| .from(networkVersions) |
| .where(eq(networkVersions.networkId, networkId)); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : String(err); |
| logger.error( |
| { err, networkId }, |
| "evolution builder: load variants failed", |
| ); |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_error", |
| payload: { |
| strategy: strategyName, |
| phase: "load_variants", |
| error: msg, |
| reason, |
| }, |
| }); |
| return { networkId, status: "error", error: `load_variants: ${msg}`, strategyName }; |
| } |
|
|
| let proposal: CandidateProposal | null = null; |
| try { |
| proposal = await strategy.propose({ |
| network, |
| activeVariant, |
| existingVersions, |
| }); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : String(err); |
| logger.warn( |
| { err, networkId, strategyName }, |
| "evolution builder: strategy.propose threw", |
| ); |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_error", |
| payload: { |
| strategy: strategyName, |
| phase: "propose", |
| error: msg, |
| reason, |
| }, |
| }); |
| return { networkId, status: "error", error: msg, strategyName }; |
| } |
|
|
| if (!proposal) { |
| |
| |
| |
| const debounceSince = new Date(Date.now() - NO_CANDIDATE_DEBOUNCE_MS); |
| let debounced = false; |
| try { |
| const recent = await db |
| .select({ id: networkEvolutionEvents.id }) |
| .from(networkEvolutionEvents) |
| .where( |
| and( |
| eq(networkEvolutionEvents.networkId, networkId), |
| eq(networkEvolutionEvents.kind, "builder_no_candidate"), |
| gte(networkEvolutionEvents.createdAt, debounceSince), |
| ), |
| ) |
| .limit(1); |
| debounced = recent.length > 0; |
| } catch (err) { |
| logger.warn( |
| { err, networkId }, |
| "evolution builder: debounce lookup failed; emitting event", |
| ); |
| } |
| if (!debounced) { |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_no_candidate", |
| payload: { |
| strategy: strategyName, |
| reason, |
| activeVersionId: activeVariant.id, |
| existingVersionCount: existingVersions.length, |
| exhausted: true, |
| }, |
| }); |
| return { |
| networkId, |
| status: "no_candidate", |
| strategyName, |
| }; |
| } |
| return { |
| networkId, |
| status: "no_candidate_debounced", |
| strategyName, |
| }; |
| } |
|
|
| const versionLabel = |
| proposal.versionLabel ?? nextVersionLabel(existingVersions); |
| const candidateId = newId("nver"); |
| const builderTier = proposal.builderModelTier ?? "strong"; |
|
|
| try { |
| await db.insert(networkVersions).values({ |
| id: candidateId, |
| networkId, |
| versionLabel, |
| |
| |
| |
| |
| |
| |
| internalGraph: activeVariant.internalGraph, |
| config: proposal.config, |
| status: "shadow", |
| builtBy: `auto:${strategyName}`, |
| builderModelTier: builderTier, |
| }); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : String(err); |
| logger.warn( |
| { err, networkId, candidateId, versionLabel }, |
| "evolution builder: insert variant failed", |
| ); |
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_error", |
| payload: { |
| strategy: strategyName, |
| phase: "insert_variant", |
| error: msg, |
| reason, |
| attemptedVersionLabel: versionLabel, |
| }, |
| }); |
| return { networkId, status: "error", error: msg, strategyName }; |
| } |
|
|
| const diff = computeConfigDiff( |
| (activeVariant.config ?? {}) as Record<string, unknown>, |
| proposal.config, |
| ); |
|
|
| await safeWriteEvent({ |
| networkId, |
| kind: "builder_proposed", |
| variantId: candidateId, |
| payload: { |
| strategy: strategyName, |
| reason, |
| parentActiveId: activeVariant.id, |
| parentVersionLabel: activeVariant.versionLabel, |
| parentConfig: activeVariant.config, |
| versionLabel, |
| config: proposal.config, |
| rationale: proposal.rationale, |
| builderModelTier: builderTier, |
| diff, |
| }, |
| }); |
|
|
| return { |
| networkId, |
| status: "proposed", |
| strategyName, |
| candidateVariantId: candidateId, |
| versionLabel, |
| config: proposal.config, |
| rationale: proposal.rationale, |
| diff, |
| }; |
| } |
|
|
| async function loadNetwork(networkId: string) { |
| const rows = await db |
| .select() |
| .from(toolNetworks) |
| .where(eq(toolNetworks.id, networkId)) |
| .limit(1); |
| return rows[0] ?? null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function nextVersionLabel(versions: NetworkVersionRow[]): string { |
| let max = 0; |
| let any = false; |
| for (const v of versions) { |
| const m = /^v(\d+)$/i.exec(v.versionLabel ?? ""); |
| if (m && m[1]) { |
| any = true; |
| const n = Number(m[1]); |
| if (Number.isFinite(n) && n > max) max = n; |
| } |
| } |
| if (!any) { |
| |
| |
| const taken = new Set(versions.map((v) => v.versionLabel)); |
| return taken.has("v1") ? `v_${Date.now().toString(36)}` : "v1"; |
| } |
| return `v${max + 1}`; |
| } |
|
|
| |
| |
| |
| |
| |
| function computeConfigDiff( |
| before: Record<string, unknown>, |
| after: Record<string, unknown>, |
| ): string[] { |
| const out: string[] = []; |
| const keys = new Set<string>([ |
| ...Object.keys(before), |
| ...Object.keys(after), |
| ]); |
| for (const k of [...keys].sort()) { |
| const a = stableStringify(before[k]); |
| const b = stableStringify(after[k]); |
| if (a !== b) { |
| out.push(`${k}: ${a ?? "∅"} → ${b ?? "∅"}`); |
| } |
| } |
| return out; |
| } |
|
|
| interface SafeWriteEventInput { |
| networkId: string; |
| kind: "builder_proposed" | "builder_no_candidate" | "builder_error"; |
| variantId?: string; |
| payload: Record<string, unknown>; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function safeWriteEvent(input: SafeWriteEventInput): Promise<void> { |
| try { |
| await recordEvent({ |
| networkId: input.networkId, |
| kind: input.kind, |
| variantId: input.variantId ?? null, |
| payload: input.payload, |
| }); |
| } catch (err) { |
| logger.error( |
| { err, networkId: input.networkId, kind: input.kind }, |
| "evolution builder: failed to record event (suppressed)", |
| ); |
| } |
| } |
|
|