openskynet / src /skynet /runtime-observer /trajectory-learner.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import type { SkynetCausalValenceLabel } from "../causal-valence/episode-ledger.js";
import {
encodeSkynetRuntimeTrajectoryFeatures,
type SkynetRuntimeTrajectorySample,
} from "./trajectory-builder.js";
export type SkynetRuntimeObserverModel = {
labels: SkynetCausalValenceLabel[];
centroids: Record<SkynetCausalValenceLabel, number[]>;
counts: Record<SkynetCausalValenceLabel, number>;
};
export type SkynetRuntimeObserverPrediction = {
label: SkynetCausalValenceLabel;
scores: Record<SkynetCausalValenceLabel, number>;
};
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<SkynetCausalValenceLabel, number[]>,
);
const counts = LABELS.reduce(
(acc, label) => {
acc[label] = 0;
return acc;
},
{} as Record<SkynetCausalValenceLabel, number>,
);
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<SkynetCausalValenceLabel, number[]>,
);
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<SkynetCausalValenceLabel, number>,
);
const label =
model.labels
.slice()
.sort(
(left, right) =>
(scores[right] ?? Number.NEGATIVE_INFINITY) - (scores[left] ?? Number.NEGATIVE_INFINITY),
)
.at(0) ?? "stall";
return { label, scores };
}