jev-web 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/vega.mjs ADDED
@@ -0,0 +1,636 @@
1
+ /**
2
+ * vega.mjs — Vega 0.8B family (frozen Qwen3.5 backbone + latent decision-physics engine).
3
+ *
4
+ * Vega is the "System One" decision model by the Laya author: the language model
5
+ * is frozen and never trained; everything learned lives in a small physics
6
+ * engine on top of it (github.com/nandakishorm/vega-08b-public-intents ships
7
+ * the reference `vega_common.py` / `vega_api.py`):
8
+ *
9
+ * prompt (template 0, built by buildVegaPrompt — mirrors build_prompt):
10
+ * [BOS] Observation:\n <state> \n\nMeasurement (choice|score|boolean): <instructions>
11
+ * \nPossible outcomes: \n* <opt> … \nOutcome:
12
+ * perception: one causal read up to the deepest captured layer (13 and 19 of
13
+ * 24), each span mean-pooled -> item feats [B, 2, 3, d] (state / question /
14
+ * last token) + option feats [B, K, 1, d] + state chunk means for the
15
+ * memory reader;
16
+ * engine: the state becomes a world latent, the question drops a particle
17
+ * into a 64-d space whose landscape holds one Gaussian well per option, a
18
+ * damped symplectic integrator with a selective-SSM thermostat runs up to
19
+ * n_steps steps (settled particles freeze), and the Boltzmann occupancy of
20
+ * the wells is the answer distribution;
21
+ * calibration: thermal — tau depends on the particle's physical state
22
+ * (residual kinetic energy, distance to the nearest well, steps used,
23
+ * number of options), fitted per question type and shipped in
24
+ * vega_config.json; split-conformal prediction sets and an abstain flag
25
+ * come from the same energies, plus an "unbound particle" OOD signal.
26
+ *
27
+ * Two ONNX graphs (one fused torso -> pooled features, one engine -> logits +
28
+ * physics), mirroring the reference runtime exactly; JS owns the protocol, the
29
+ * packing, the thermal calibration and the answer decoding.
30
+ */
31
+ import { normalizeQuestions } from "./questions.mjs";
32
+ import { softmaxWithTemperature } from "./answers.mjs";
33
+ import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
34
+
35
+ export const VEGA_DEFAULT_MODEL = "warsang/vega-08b-web";
36
+ export const VEGA_DEFAULT_REVISION = "d2bd4604ed770ca614745f07f623b66e89f406e4";
37
+ // The exact fp16 torso ships by default: q8 on this graph drifts on the
38
+ // GatedDeltaNet delta rule (export/vega/04_repo.py measures ~0.46 in the
39
+ // pooled features), so torso_quantized.onnx stays an opt-in experiment.
40
+ export const VEGA_TORSO_FILE = "onnx/torso_fp16.onnx";
41
+ export const VEGA_ENGINE_FILE = "onnx/engine.onnx";
42
+
43
+ /** Reference prompt template 0 (the inference surface form). */
44
+ const VEGA_HEAD = "Observation:\n";
45
+ const VEGA_QUESTION = "\n\nMeasurement ({t}): {ins}\nPossible outcomes:";
46
+ const VEGA_OPTION = "\n* {o}";
47
+ const VEGA_END = "\nOutcome:";
48
+ /** Prefixed to short states so the frozen model reads knowing what is asked. */
49
+ const VEGA_QUERY_FIRST = "Question to keep in mind while reading ({t}): {ins}\n\n";
50
+
51
+ /** jev-web question types -> Vega qtypes (choice=0, score=1, boolean=2). */
52
+ export const VEGA_QTYPE = { choice: 0, score: 1, boolean: 2 };
53
+ export const VEGA_QTYPE_NAMES = ["choice", "score", "boolean"];
54
+
55
+ export const VEGA_DEFAULTS = {
56
+ optMaxTokens: 32,
57
+ insMaxTokens: 256,
58
+ memoryChunks: 16,
59
+ memoryMinTokens: 128,
60
+ memoryChunkTokens: 128,
61
+ prefixMinTokens: 4096,
62
+ queryFirstMaxState: 4096,
63
+ maxLen: 73728,
64
+ nSteps: 12,
65
+ risk: "0.1",
66
+ alpha: "0.1",
67
+ };
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Protocol: question defs, option texts, prompt building
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Map a normalized jev-web question to Vega's internal question def.
75
+ * Vega's `boolean` is the noul primitive; its slots are fixed [no, yes].
76
+ */
77
+ export function vegaQuestionDef(question) {
78
+ const ins = renderVegaText(question.instructions);
79
+ if (question.type === "noul") {
80
+ return { t: "boolean", ins, crit: { ...(question.criteria ?? {}) } };
81
+ }
82
+ if (question.type === "score") {
83
+ return { t: "score", ins, crit: [...question.options] };
84
+ }
85
+ // choice: criteria {label: description} when provided, else bare options.
86
+ const crit = question.criteria && typeof question.criteria === "object"
87
+ && !Array.isArray(question.criteria)
88
+ ? question.criteria
89
+ : Object.fromEntries(question.options.map((o) => [String(o), null]));
90
+ return { t: "choice", ins, crit };
91
+ }
92
+
93
+ function flatText(v) {
94
+ return v === null || v === undefined ? "" : String(v);
95
+ }
96
+
97
+ export function renderVegaText(content) {
98
+ return typeof content === "string" ? content : JSON.stringify(content);
99
+ }
100
+
101
+ /** Option texts in label-index order, exactly as option_texts() renders them. */
102
+ export function vegaOptionTexts(qdef) {
103
+ if (qdef.t === "choice") {
104
+ return Object.entries(qdef.crit).map(([k, v]) => {
105
+ const d = v === null || v === undefined ? "" : flatText(v);
106
+ return d ? `${k} - ${d}` : String(k);
107
+ });
108
+ }
109
+ if (qdef.t === "score") {
110
+ return qdef.crit.map((c, i) => `level ${i} - ${flatText(c)}`);
111
+ }
112
+ const crit = qdef.crit ?? {};
113
+ const no = crit.false === null || crit.false === undefined ? "" : flatText(crit.false);
114
+ const yes = crit.true === null || crit.true === undefined ? "" : flatText(crit.true);
115
+ return [
116
+ `no - ${no || "the statement does not hold"}`,
117
+ `yes - ${yes || "the statement holds"}`,
118
+ ];
119
+ }
120
+
121
+ /**
122
+ * One prompt: BOS + [query-first prefix for short states] + head + state +
123
+ * question + options + end, with the token span of every segment.
124
+ *
125
+ * The state comes first so its span never sees the question (causal
126
+ * attention); the question, options and final token have read the whole
127
+ * state. Options are capped per option, and an over-long option list is
128
+ * shrunk evenly before the instructions are (mirrors build_prompt).
129
+ *
130
+ * @returns {{ids:number[], spans:{state:[number,number], question:[number,number],
131
+ * options:[number,number][], last:number}}}
132
+ */
133
+ export function buildVegaPrompt({
134
+ encode,
135
+ stateText,
136
+ qdef,
137
+ maxLen,
138
+ bosId = null,
139
+ queryFirstMaxState = VEGA_DEFAULTS.queryFirstMaxState,
140
+ optMaxTokens = VEGA_DEFAULTS.optMaxTokens,
141
+ insMaxTokens = VEGA_DEFAULTS.insMaxTokens,
142
+ }) {
143
+ const options = vegaOptionTexts(qdef);
144
+ const enc = (s) => encode(s, false);
145
+ const bos = bosId === null ? [] : [bosId];
146
+ let head = enc(VEGA_HEAD);
147
+ let qids = enc(VEGA_QUESTION.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
148
+ .slice(0, insMaxTokens);
149
+ let oids = options.map((o) => enc(VEGA_OPTION.replace("{o}", o)).slice(0, optMaxTokens));
150
+ const end = enc(VEGA_END);
151
+ let fixed = bos.length + head.length + qids.length
152
+ + oids.reduce((n, o) => n + o.length, 0) + end.length;
153
+ if (fixed > maxLen - 16) {
154
+ const per = Math.max(3, Math.floor(
155
+ (maxLen - 16 - bos.length - head.length - end.length - Math.min(qids.length, 64))
156
+ / Math.max(1, oids.length)));
157
+ oids = oids.map((o) => o.slice(0, per));
158
+ qids = qids.slice(0, Math.max(
159
+ 8, maxLen - 16 - bos.length - head.length - end.length
160
+ - oids.reduce((n, o) => n + o.length, 0)));
161
+ fixed = bos.length + head.length + qids.length
162
+ + oids.reduce((n, o) => n + o.length, 0) + end.length;
163
+ }
164
+ let st = enc(stateText);
165
+ let pre = [];
166
+ if (queryFirstMaxState && st.length < queryFirstMaxState) {
167
+ pre = enc(VEGA_QUERY_FIRST.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
168
+ .slice(0, Math.min(128, insMaxTokens));
169
+ fixed += pre.length;
170
+ }
171
+ const room = Math.max(0, maxLen - fixed);
172
+ st = st.slice(0, room); // truncate_left = false: the head of the state is kept
173
+ const ids = [...bos, ...pre, ...head];
174
+ const s0 = ids.length;
175
+ ids.push(...st);
176
+ const spans = { state: st.length ? [s0, ids.length] : [bos.length, s0] };
177
+ const q0 = ids.length;
178
+ ids.push(...qids);
179
+ spans.question = [q0, ids.length];
180
+ spans.options = [];
181
+ for (const o of oids) {
182
+ const a = ids.length;
183
+ ids.push(...o);
184
+ spans.options.push([a, ids.length]);
185
+ }
186
+ ids.push(...end);
187
+ spans.last = ids.length - 1;
188
+ return { ids, spans };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Batch packing: padding, segment means (one-hot pooling), state chunks
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Pack rendered prompts into tensors + the pooling matrix the torso graph
197
+ * consumes: one row per segment (per item: state, question, options, the
198
+ * final token, then C state-chunk rows), each segment's row is 1/len over its
199
+ * token span, zeros elsewhere. The graph computes pool @ hidden, i.e. exact
200
+ * span means, so no hidden states ever cross into JS.
201
+ *
202
+ * @returns {{inputIds:BigInt64Array, attention:BigInt64Array,
203
+ * pool:Float32Array, poolDims:[number,number,number], n:number, L:number,
204
+ * K:number, qtypes:number[], optMask:Uint8Array, nseg:number}}
205
+ */
206
+ export function packVegaBatch(items, { padId = 0, chunks: C = VEGA_DEFAULTS.memoryChunks } = {}) {
207
+ const n = items.length;
208
+ const L = Math.max(...items.map((it) => it.ids.length));
209
+ const K = Math.max(...items.map((it) => it.options.length));
210
+ const nseg = 3 + K;
211
+ const rows = nseg + C;
212
+ const inputIds = new BigInt64Array(n * L);
213
+ const attention = new BigInt64Array(n * L);
214
+ const pool = new Float32Array(n * rows * L); // [B, nseg + C, L]
215
+ const optMask = new Uint8Array(n * K);
216
+ const qtypes = [];
217
+ items.forEach((it, b) => {
218
+ it.ids.forEach((v, j) => {
219
+ inputIds[b * L + j] = BigInt(v);
220
+ attention[b * L + j] = 1n;
221
+ });
222
+ const setSpan = (row, [a, e]) => {
223
+ const len = Math.max(1, e - a);
224
+ for (let t = a; t < e; t++) pool[(b * rows + row) * L + t] = 1 / len;
225
+ };
226
+ setSpan(0, it.spans.state);
227
+ setSpan(1, it.spans.question);
228
+ it.spans.options.forEach((span, k) => {
229
+ if (k >= K) return;
230
+ setSpan(3 + k, span);
231
+ optMask[b * K + k] = 1;
232
+ });
233
+ pool[(b * rows + 2) * L + it.spans.last] = 1;
234
+ qtypes.push(it.qtype);
235
+ });
236
+ return {
237
+ inputIds, attention, pool, poolDims: [n, rows, L],
238
+ n, L, K, qtypes, optMask, nseg,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * State chunk means for the memory reader: the state span cut into k equal
244
+ * pieces (about one per memoryChunkTokens tokens, at least 2, at most C;
245
+ * states shorter than memoryMinTokens get none) — mirrors _chunk_pool.
246
+ *
247
+ * Fills rows [nseg, nseg + C) of the SAME pooling matrix the segment means
248
+ * use, so the torso graph emits chunk rows in the one forward it already
249
+ * does (a second run would re-read the whole record).
250
+ *
251
+ * @param {Float32Array} pool the [B, nseg + C, L] pooling matrix
252
+ * @param {object[]} items rendered prompts (with spans)
253
+ * @param {number} L padded sequence length
254
+ */
255
+ export function packVegaChunks(pool, items, L, {
256
+ chunks: C = VEGA_DEFAULTS.memoryChunks,
257
+ chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
258
+ chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
259
+ nseg = 3 + Math.max(...items.map((it) => it.options.length)),
260
+ } = {}) {
261
+ const n = items.length;
262
+ const chunkMask = new Uint8Array(n * C);
263
+ items.forEach((it, b) => {
264
+ const [s0, s1] = it.spans.state;
265
+ const len = s1 - s0;
266
+ if (len < Math.max(chunkMinTokens, 2)) return;
267
+ const k = Math.max(2, Math.min(C, Math.floor(len / Math.max(1, chunkTokens))));
268
+ for (let c = 0; c < k; c++) {
269
+ // token t of the state belongs to chunk floor(t * k / len) (the cid map)
270
+ const row = b * (nseg + C) + nseg + c;
271
+ let count = 0;
272
+ for (let t = s0; t < s1; t++) {
273
+ if (Math.floor(((t - s0) * k) / len) === c) { pool[row * L + t] = 1; count++; }
274
+ }
275
+ if (count > 0) {
276
+ for (let t = s0; t < s1; t++) {
277
+ if (pool[row * L + t]) pool[row * L + t] = 1 / count;
278
+ }
279
+ chunkMask[b * C + c] = 1;
280
+ }
281
+ }
282
+ });
283
+ return chunkMask;
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Thermal calibration + answer decoding (mirrors thermal_tau / decide)
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /** Fallback when a checkpoint ships no calibration block (probabilities raw). */
291
+ export const VEGA_DEFAULT_THETA = [[0], [0], [0]];
292
+
293
+ /**
294
+ * Per-type thermal calibration from the checkpoint's vega_config.json:
295
+ * log_tau = b + w . phys, tau in [0.2, 20]; probs = softmax(logits / tau).
296
+ *
297
+ * @param {{theta:number[][]}} calibration
298
+ * @param {{logits:ArrayLike<number>, rowWidth:number, qtypes:number[],
299
+ * phys:ArrayLike<number>, nOpts:number[]}} spec phys is [items, 4]
300
+ * @returns {number[][]} probabilities per item, in prompt order
301
+ */
302
+ export function vegaThermalProbabilities({
303
+ logits, rowWidth, qtypes, phys, nOpts,
304
+ theta = VEGA_DEFAULT_THETA,
305
+ }) {
306
+ const rows = Number(logits.length / rowWidth);
307
+ const out = [];
308
+ for (let b = 0; b < rows; b++) {
309
+ const k = nOpts[b];
310
+ const t = theta[qtypes[b]] ?? theta[0];
311
+ let logTau = t[0];
312
+ for (let f = 0; f < 4; f++) logTau += t[1 + f] * phys[b * 4 + f];
313
+ const tau = Math.exp(Math.min(Math.log(20), Math.max(Math.log(0.2), logTau)));
314
+ out.push(softmaxWithTemperature(
315
+ Array.from(logits.slice(b * rowWidth, b * rowWidth + k)), tau));
316
+ }
317
+ return out;
318
+ }
319
+
320
+ /**
321
+ * Per-item thermal features: log1p(kinetic), log1p(nearest), steps/steps_max,
322
+ * log(k) — exactly thermal_features().
323
+ */
324
+ export function vegaThermalFeatures({ kinetic, nearest, stepsUsed, nOpts, maxSteps }) {
325
+ const out = [];
326
+ for (let b = 0; b < nOpts.length; b++) {
327
+ out.push([
328
+ Math.log1p(kinetic[b]),
329
+ Math.log1p(nearest[b]),
330
+ stepsUsed[b] / Math.max(1, maxSteps),
331
+ Math.log(Math.max(2, nOpts[b])),
332
+ ]);
333
+ }
334
+ return out;
335
+ }
336
+
337
+ /**
338
+ * Vega answers -> jev-web DecideResult answers (additive vega fields:
339
+ * predictionSet / abstain / unbound / settleSteps / energy).
340
+ *
341
+ * @param {{probs:number[][], qtypes:number[], labels:string[][],
342
+ * physics:{nearest:ArrayLike<number>, stepsUsed:ArrayLike<number>},
343
+ * rawLogits:ArrayLike<number>, rowWidth:number,
344
+ * calibration:object, alpha?:string, risk?:string}} spec
345
+ */
346
+ export function vegaAnswers({
347
+ probs, qtypes, physics, rawLogits, rowWidth, calibration, questions, labels,
348
+ alpha = VEGA_DEFAULTS.alpha, risk = VEGA_DEFAULTS.risk,
349
+ }) {
350
+ const answers = [];
351
+ labels.forEach((optionLabels, r) => {
352
+ const p = probs[r];
353
+ const k = optionLabels.length;
354
+ const qtype = VEGA_QTYPE_NAMES[qtypes[r]];
355
+ const conf = Math.max(...p);
356
+ const best = p.indexOf(Math.max(...p));
357
+ // LAC prediction set; the top option is always kept so it is never empty.
358
+ const th = calibration?.conformal?.[qtype]?.[String(alpha)];
359
+ const predictionSet = th === undefined ? null
360
+ : optionLabels.filter((_, i) => i === best || p[i] >= Math.max(1 - th, 1e-12));
361
+ // energy = -max raw logit over the question's real options
362
+ let bestLogit = -Infinity;
363
+ for (let j = 0; j < k; j++) bestLogit = Math.max(bestLogit, rawLogits[r * rowWidth + j]);
364
+ const a = {
365
+ probabilities: Object.fromEntries(optionLabels.map((l, i) => [l, p[i]])),
366
+ confidence: conf,
367
+ predictionSet,
368
+ abstain: conf < (calibration?.abstain_threshold?.[String(risk)] ?? 0),
369
+ unbound: physics.nearest[r] > (calibration?.unbound_threshold ?? Infinity),
370
+ settleSteps: physics.stepsUsed[r],
371
+ energy: -bestLogit,
372
+ };
373
+ if (qtype === "choice") {
374
+ a.type = "choice";
375
+ a.choice = optionLabels[best];
376
+ a.index = best;
377
+ } else if (qtype === "score") {
378
+ a.type = "score";
379
+ a.level = best;
380
+ a.score = p.reduce((acc, pj, j) => acc + pj * j, 0);
381
+ } else {
382
+ a.type = "noul";
383
+ a.noul = p[1];
384
+ }
385
+ answers.push(a);
386
+ });
387
+ return answers;
388
+ }
389
+
390
+ /** Per-question option labels in prompt order (choice keys / score levels / [no, yes]). */
391
+ export function vegaOptionLabels(questions) {
392
+ return questions.map((q) => {
393
+ if (q.type === "noul") return [...q.options];
394
+ if (q.type === "score") return q.options.map((_, i) => i);
395
+ return [...q.options];
396
+ });
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // The decider
401
+ // ---------------------------------------------------------------------------
402
+
403
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
404
+
405
+ const hfUrl = (model, revision, file) =>
406
+ `https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
407
+
408
+ /**
409
+ * Create a Vega decider.
410
+ *
411
+ * @param {object} [opts]
412
+ * model/revision — browser export repo (torso + engine graphs, tokenizer,
413
+ * vega_config.json with the calibration block)
414
+ * torsoFile/engineFile — graph paths within the repo
415
+ * calibrationModel — repo to read vega_config.json from (default: model;
416
+ * point it at the source checkpoint for your own export)
417
+ * maxLen — prompt window; null = checkpoint's max_length
418
+ * alpha / risk — conformal risk level and abstain risk ("0.1" default)
419
+ * device, wasmPaths, sessionFactory, onProgress — as in the other families
420
+ */
421
+ export async function createVegaDecider({
422
+ model = VEGA_DEFAULT_MODEL,
423
+ revision = VEGA_DEFAULT_REVISION,
424
+ torsoFile = VEGA_TORSO_FILE,
425
+ engineFile = VEGA_ENGINE_FILE,
426
+ calibrationModel = null,
427
+ calibrationRevision = null,
428
+ ort = null,
429
+ transformers = null,
430
+ fetchImpl = fetch,
431
+ wasmPaths = null,
432
+ device = "auto",
433
+ sessionFactory = null,
434
+ onProgress = null,
435
+ maxLen = null,
436
+ alpha = VEGA_DEFAULTS.alpha,
437
+ risk = VEGA_DEFAULTS.risk,
438
+ scope = globalThis,
439
+ } = {}) {
440
+ const ortMod = ort ?? (await import("onnxruntime-web"));
441
+ const tf = transformers ?? (await import("@huggingface/transformers"));
442
+ if (wasmPaths) {
443
+ try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
444
+ }
445
+ const resolvedDevice = device !== "auto"
446
+ ? device
447
+ : (scope?.navigator?.gpu ? "webgpu" : "wasm");
448
+ const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
449
+
450
+ // Calibration + architecture constants come from the checkpoint's
451
+ // vega_config.json (thermal theta, conformal thresholds, chunking, window).
452
+ const configUrl = hfUrl(calibrationModel ?? model,
453
+ calibrationRevision ?? (calibrationModel ? "main" : revision),
454
+ "vega_config.json");
455
+ let config = null;
456
+ let configSource = null;
457
+ try {
458
+ const res = await fetchImpl(configUrl);
459
+ if (res?.ok) {
460
+ config = await res.json();
461
+ configSource = configUrl;
462
+ }
463
+ } catch { /* fall through to the shipped defaults */ }
464
+ const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
465
+ const calibration = config?.calibration ?? {};
466
+ const cfgMaxLen = config?.max_len ?? cfg.maxLen;
467
+ const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
468
+
469
+ const tok = await tf.AutoTokenizer.from_pretrained(calibrationModel ?? model, {
470
+ revision: calibrationRevision ?? (calibrationModel ? "main" : revision),
471
+ progress_callback: () => {},
472
+ });
473
+ const encode = (text, addSpecial = false) =>
474
+ Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
475
+
476
+ const createSession = sessionFactory
477
+ ?? (async (u, opts) => {
478
+ const bytes = await fetchCachedBytes(u, {
479
+ fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
480
+ });
481
+ const external = await fetchOnnxExternalData(u, { fetchImpl });
482
+ const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
483
+ return ortMod.InferenceSession.create(bytes, sessionOpts);
484
+ });
485
+ const makeSession = async (file) => {
486
+ try {
487
+ return await createSession(hfUrl(model, revision, file), {
488
+ executionProviders, graphOptimizationLevel: "all",
489
+ });
490
+ } catch (e) {
491
+ if (resolvedDevice !== "webgpu") throw e;
492
+ onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
493
+ return createSession(hfUrl(model, revision, file),
494
+ { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
495
+ }
496
+ };
497
+ const torso = await makeSession(torsoFile);
498
+ const engine = await makeSession(engineFile);
499
+
500
+ const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
501
+ const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
502
+ const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
503
+
504
+ return {
505
+ info: {
506
+ family: "vega",
507
+ model,
508
+ revision,
509
+ device: resolvedDevice,
510
+ maxLen: effMaxLen,
511
+ alpha: String(alpha),
512
+ risk: String(risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk),
513
+ calibrationSource: configSource,
514
+ nSteps: cfg.n_steps ?? cfg.nSteps,
515
+ config,
516
+ },
517
+ async decide(state, questions) {
518
+ const t0 = now();
519
+ const normalized = normalizeQuestions(questions);
520
+ const qdefs = normalized.map(vegaQuestionDef);
521
+ const stateText = renderVegaText(state);
522
+ const bosId = tok.bos_token_id ?? null;
523
+
524
+ const items = qdefs.map((qdef) => {
525
+ const { ids, spans } = buildVegaPrompt({
526
+ encode, stateText, qdef, maxLen: effMaxLen,
527
+ bosId,
528
+ queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
529
+ optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
530
+ insMaxTokens: 256,
531
+ });
532
+ return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
533
+ });
534
+
535
+ const batch = packVegaBatch(items, {
536
+ chunks: cfg.memory_chunks ?? cfg.memoryChunks,
537
+ });
538
+ const chunkMask = packVegaChunks(batch.pool, items, batch.L, {
539
+ chunks: cfg.memory_chunks ?? cfg.memoryChunks,
540
+ chunkMinTokens: cfg.memory_min_tokens ?? cfg.memoryMinTokens,
541
+ chunkTokens: cfg.memory_chunk_tokens ?? cfg.memoryChunkTokens,
542
+ nseg: batch.nseg,
543
+ });
544
+
545
+ const tRun = now();
546
+ // Torso: one causal read to the deepest captured layer, all span means
547
+ // (segments + state chunks) pooled in-graph from the one forward.
548
+ const torsoOut = await torso.run({
549
+ input_ids: int64(batch.inputIds, [batch.n, batch.L]),
550
+ attention_mask: int64(batch.attention, [batch.n, batch.L]),
551
+ pool: f32(batch.pool, batch.poolDims),
552
+ });
553
+ const pooled = torsoOut.pooled ?? torsoOut[Object.keys(torsoOut)[0]];
554
+ // pooled: [B, nLayers, nseg + C, d] — nLayers = feature_layers,
555
+ // rows 0..2 = state/question/last, 3..3+K = options, then C chunk rows.
556
+ const pd = pooled.dims;
557
+ const d = pd[3];
558
+ const Lc = pd[1];
559
+ const rowsTotal = pd[2];
560
+ const C = rowsTotal - batch.nseg;
561
+ const pv = pooled.data;
562
+ const feats = new Float32Array(batch.n * Lc * 3 * d);
563
+ const opts = new Float32Array(batch.n * batch.K * d);
564
+ const chunks = new Float32Array(batch.n * C * d);
565
+ for (let b = 0; b < batch.n; b++) {
566
+ for (let l = 0; l < Lc; l++) {
567
+ for (let s = 0; s < 3; s++) {
568
+ const row = s; // pooling rows 0/1/2 are state/question/last
569
+ const src = ((b * Lc + l) * rowsTotal + row) * d;
570
+ feats.set(pv.subarray(src, src + d), ((b * Lc + l) * 3 + s) * d);
571
+ }
572
+ }
573
+ // options from the deepest captured layer only (option_layers)
574
+ for (let k = 0; k < batch.K; k++) {
575
+ const src = ((b * Lc + Lc - 1) * rowsTotal + 3 + k) * d;
576
+ opts.set(pv.subarray(src, src + d), (b * batch.K + k) * d);
577
+ }
578
+ // chunk means: deepest feature layer, rows nseg..nseg + C
579
+ for (let c = 0; c < C; c++) {
580
+ const src = ((b * Lc + Lc - 1) * rowsTotal + batch.nseg + c) * d;
581
+ chunks.set(pv.subarray(src, src + d), (b * C + c) * d);
582
+ }
583
+ }
584
+
585
+ // Engine: landscape + integrator -> raw logits and the physics state.
586
+ const engineOut = await engine.run({
587
+ feats: f32(feats, [batch.n, Lc, 3, d]),
588
+ opts: f32(opts, [batch.n, batch.K, d]),
589
+ opt_mask: bool(batch.optMask, [batch.n, batch.K]),
590
+ qtype: int64(batch.qtypes, [batch.n]),
591
+ chunks: f32(chunks, [batch.n, C, d]),
592
+ chunk_mask: bool(chunkMask, [batch.n, C]),
593
+ });
594
+ const logits = engineOut.logits.data;
595
+ const kinetic = engineOut.kinetic.data;
596
+ const nearest = engineOut.nearest.data;
597
+ const stepsUsed = Array.from(engineOut.steps_used.data, Number);
598
+
599
+ const nOpts = items.map((it) => it.options.length);
600
+ const phys = vegaThermalFeatures({
601
+ kinetic, nearest, stepsUsed, nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
602
+ });
603
+ const probs = vegaThermalProbabilities({
604
+ logits, rowWidth: batch.K, qtypes: batch.qtypes,
605
+ phys: phys.flat(), nOpts,
606
+ theta: calibration.theta ?? VEGA_DEFAULT_THETA,
607
+ });
608
+ const runMs = now() - tRun;
609
+ const labels = vegaOptionLabels(normalized);
610
+ const answers = vegaAnswers({
611
+ probs, qtypes: batch.qtypes, labels,
612
+ physics: { nearest, stepsUsed },
613
+ rawLogits: logits, rowWidth: batch.K,
614
+ calibration, alpha, risk,
615
+ });
616
+ const decode = (ids) => {
617
+ try {
618
+ return tok.decode(Array.from(ids), { skip_special_tokens: false });
619
+ } catch {
620
+ return Array.from(ids).join(" ");
621
+ }
622
+ };
623
+ return {
624
+ answers,
625
+ truncated: false,
626
+ length: Number(batch.attention.reduce((a, v) => a + v, 0n)),
627
+ prompts: items.map((it) => decode(it.ids)),
628
+ timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
629
+ };
630
+ },
631
+ async dispose() {
632
+ try { await torso.release?.(); } catch { /* optional */ }
633
+ try { await engine.release?.(); } catch { /* optional */ }
634
+ },
635
+ };
636
+ }