// core/engine.js — the inference ENGINE adapter (the only module that touches the wasm
// tokenizer + the WebGPU `gpu` object). DOM-free. Wraps a model that core/loader.js has
// already loaded onto the GPU and exposes a clean, UI-agnostic API:
//
// const engine = await createEngine(modelEntry, { gpu, info, imageKappa });
// const { text, outIds } = await engine.generate(ids, { onToken, signal });
// const rec = await engine.buildReceipt({ ... }); // PROV-O, re-derivable (Law L5)
//
// The token loop, framing, memo and receipt logic are lifted byte-for-byte from the
// original index.html think()/run()/sealReceipt — only the DOM writes are replaced by an
// onToken callback and the running/handedOff flags by an AbortSignal, so output (and the
// receipt κ) is identical to the original app.
import { qvac_tokenize, qvac_continue, kappa } from "../pkg/holospaces_web.js";
import { clean, didHolo, kappaTokens, sealReceipt, verifyIntegrity, idBytes, kappaBytes } from "./kappa.js";
const _perf = () => (typeof performance !== "undefined" ? performance.now() : 0);
const _sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// The engine is itself a content-addressed object — hash the wasm once (lazy).
let _engineK = null;
export async function engineKappa() {
if (_engineK) return _engineK;
try { const b = new Uint8Array(await (await fetch(new URL("../pkg/holospaces_web_bg.wasm", import.meta.url))).arrayBuffer()); _engineK = await kappaBytes(b); }
catch { _engineK = "did:holo:sha256:(engine unavailable)"; }
return _engineK;
}
export async function createEngine(modelEntry, loaded) {
const { gpu, info, imageKappa } = loaded;
const m = modelEntry;
// W2 (Q-WAFER): KV-commons blobs SEALED IN THE WAFER (minted once on a real GPU at forge time) —
// a fresh device restores the persona prefill instead of computing it. Matched by (layout, nIds,
// idsSha) so a blob only restores on the exact kv layout + token ids it was minted for.
const sealedKvc = (loaded.sealed && loaded.sealed.kvc) || null;
const idsSha = async (ids) => { try { const b = new TextEncoder().encode(ids.join(",")); return [...new Uint8Array(await crypto.subtle.digest("SHA-256", b))].map((x) => x.toString(16).padStart(2, "0")).join(""); } catch { return null; } };
const engineReady = engineKappa();
// model κ: the κ-disk's VERIFIED image_kappa when present (a real content address of the
// weights, every sector re-derived); else the model's declared identity.
const modelKappa = imageKappa
? "did:holo:sha256:" + String(imageKappa).replace(/^(did:holo:)?sha256:/, "")
: await didHolo({ "@type": "schema:SoftwareSourceCode", name: m.name, size: m.size, fmt: m.fmt || "", family: m.fam || "" });
const memo = new Map();
let _drafter = null; // learned speculative drafter (fn(seq,max)=>ids); null → standard decode. Set via setDrafter().
// D1 (Q-DERIVED-MIND) SPECULATIVE SELF: the spec path also arms by CATALOG opt-in (m.spec) with no
// learned drafter — gpu.setDrafter(null) keeps the gpu's internal n-gram drafter (prompt-lookup:
// drafts cost ~0 JS, greedy batched verify keeps output BYTE-IDENTICAL, SP.cold self-throttles on
// miss streaks). ?spec=0 kills, ?spec=1 forces — both for honest A/B on the ?stats ledger.
const _specQ = (() => { try { return typeof location !== "undefined" ? new URLSearchParams(location.search).get("spec") : null; } catch { return null; } })();
let _pinLen = 0; // KV-COMMONS prefix pin: length of the pinned shared prefix (0 = none). See pinPrefix/usePin below.
const tokenize = (text) => { try { return JSON.parse(qvac_tokenize(text)).ids || []; } catch { return []; } };
const detokenize = (ids) => { try { return clean(JSON.parse(qvac_continue(JSON.stringify(ids), 0, 0, 0, ids.length)).text || ""); } catch { return ""; } };
const fingerprint = (ids) => kappa(idBytes(ids)); // live mind κ (blake3, from wasm)
// Frame one user turn. Qwen2/3 use ChatML (its <|im_*|> markers are atomic BPE tokens);
// other instruction models use a plain Q/A frame. (Verbatim from the original run().)
function frameTurn(prompt, hasHistory) {
if (m.qwen) {
const noThink = m.qwen3 ? "\n\n\n\n" : ""; // Qwen3: skip the thinking block for fast direct answers
return (hasHistory ? "<|im_end|>\n" : "") + `<|im_start|>user\n${prompt}<|im_end|>\n<|im_start|>assistant\n` + noThink;
}
if (m.llama3) // LLaMA-3 header template (BitNet b1.58 etc.)
return (hasHistory ? "<|eot_id|>" : "") + `<|start_header_id|>user<|end_header_id|>\n\n${prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n`;
if (m.olmo) // OLMo/OLMoE: <|user|>/<|assistant|> role tags (each turn self-delimited; leading bos via m.bos)
return `<|user|>\n${prompt}\n<|assistant|>\n`;
if (m.userWord) // word-frame (Falcon-E: ChatML stalls EMPIRICALLY; "User:/