import type { SkynetCausalValenceLabel } from "../causal-valence/episode-ledger.js"; import { encodeSkynetRuntimeTrajectoryFeatures, type SkynetRuntimeTrajectorySample, } from "./trajectory-builder.js"; export type SkynetRuntimeObserverModel = { labels: SkynetCausalValenceLabel[]; centroids: Record; counts: Record; }; export type SkynetRuntimeObserverPrediction = { label: SkynetCausalValenceLabel; scores: Record; }; const LABELS: SkynetCausalValenceLabel[] = ["progress", "relief", "stall", "frustration", "damage"]; function zeroVector(length: number): number[] { return Array.from({ length }, () => 0); } function cosineSimilarity(a: number[], b: number[]): number { let dot = 0; let normA = 0; let normB = 0; for (let index = 0; index < a.length; index += 1) { const av = a[index] ?? 0; const bv = b[index] ?? 0; dot += av * bv; normA += av * av; normB += bv * bv; } if (normA === 0 || normB === 0) { return 0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } export function trainSkynetRuntimeObserverModel( samples: SkynetRuntimeTrajectorySample[], ): SkynetRuntimeObserverModel | null { if (samples.length === 0) { return null; } const vectorLength = encodeSkynetRuntimeTrajectoryFeatures(samples[0]).length; const sums = LABELS.reduce( (acc, label) => { acc[label] = zeroVector(vectorLength); return acc; }, {} as Record, ); const counts = LABELS.reduce( (acc, label) => { acc[label] = 0; return acc; }, {} as Record, ); for (const sample of samples) { const vector = encodeSkynetRuntimeTrajectoryFeatures(sample); counts[sample.targetLabel] += 1; for (let index = 0; index < vector.length; index += 1) { sums[sample.targetLabel][index] += vector[index] ?? 0; } } const centroids = LABELS.reduce( (acc, label) => { const count = counts[label]; acc[label] = count > 0 ? sums[label].map((value) => value / count) : zeroVector(vectorLength); return acc; }, {} as Record, ); return { labels: LABELS.filter((label) => counts[label] > 0), centroids, counts, }; } export function predictSkynetRuntimeObserverLabel( model: SkynetRuntimeObserverModel, sample: SkynetRuntimeTrajectorySample, ): SkynetRuntimeObserverPrediction { const vector = encodeSkynetRuntimeTrajectoryFeatures(sample); const scores = model.labels.reduce( (acc, label) => { acc[label] = cosineSimilarity(vector, model.centroids[label]); return acc; }, {} as Record, ); const label = model.labels .slice() .sort( (left, right) => (scores[right] ?? Number.NEGATIVE_INFINITY) - (scores[left] ?? Number.NEGATIVE_INFINITY), ) .at(0) ?? "stall"; return { label, scores }; }