Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- core/agents.js +74 -0
- core/backend.js +23 -0
- core/code-tools.js +130 -0
- core/engine.js +189 -0
- core/kappa.js +76 -0
- core/keymap.js +29 -0
- core/loader.js +204 -0
- core/mcp.js +144 -0
- core/mcphub.js +107 -0
- core/memory.js +69 -0
- core/presets.js +54 -0
- core/promptsLib.js +109 -0
- core/q-brain-fast.mjs +153 -0
- core/q-self.mjs +62 -0
- core/schema.js +85 -0
- core/semantic-witness.mjs +33 -0
- core/semantic.js +98 -0
- core/skills.js +92 -0
- core/state.js +39 -0
- core/store.js +185 -0
- core/tools.js +112 -0
- forge/gguf-forge-iq-dequant.mjs +260 -0
- forge/gguf-forge-iq-grids.mjs +14 -0
- holo-delta.mjs +156 -0
- holo-load-delta.mjs +54 -0
- holo-load2bit.mjs +126 -0
- holo-model-frame.mjs +106 -0
- index.html +134 -19
- pkg/holospaces_web.js +0 -0
- pkg/holospaces_web_bg.wasm +3 -0
- q-chat.html +120 -0
- qvac-2bit.mjs +268 -0
- qvac-gpu.js +0 -0
- qvac-ingest.mjs +561 -0
core/agents.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/agents.js — AGENTS as κ-objects (LibreChat agent semantics, substrate-native). An agent
|
| 2 |
+
// bundles identity + instructions + model + parameters + armed tools + conversation starters;
|
| 3 |
+
// selecting one shapes every turn (instructions ride the system block, its tool selection arms
|
| 4 |
+
// the MCP loop). Field names follow the LibreChat agent schema so exports interoperate; each
|
| 5 |
+
// version re-seals to a new κ (history is free, Law L3).
|
| 6 |
+
|
| 7 |
+
import { normPreset } from "./schema.js";
|
| 8 |
+
|
| 9 |
+
const LC = { lc: "https://librechat.ai/ns#" };
|
| 10 |
+
|
| 11 |
+
export function normAgent(a = {}) {
|
| 12 |
+
return {
|
| 13 |
+
id: typeof a.id === "string" ? a.id : null,
|
| 14 |
+
name: a.name || "New Agent",
|
| 15 |
+
description: a.description || "",
|
| 16 |
+
instructions: a.instructions || "",
|
| 17 |
+
avatar: a.avatar || null, // { filepath, source } | emoji string | null
|
| 18 |
+
provider: a.provider || "holo-q", // the local substrate engine
|
| 19 |
+
model: a.model || null, // a MODELS[] name
|
| 20 |
+
model_parameters: {
|
| 21 |
+
temperature: +(a.model_parameters?.temperature ?? 0) || 0,
|
| 22 |
+
max_output_tokens: +(a.model_parameters?.max_output_tokens ?? 900) || 900,
|
| 23 |
+
},
|
| 24 |
+
tools: Array.isArray(a.tools) ? a.tools : [], // armed tool names (the MCP loop selection)
|
| 25 |
+
mcpServerNames: Array.isArray(a.mcpServerNames) ? a.mcpServerNames : [],
|
| 26 |
+
conversation_starters: Array.isArray(a.conversation_starters) ? a.conversation_starters.filter(Boolean).slice(0, 4) : [],
|
| 27 |
+
artifacts: a.artifacts || "default", // 'default' | 'custom' (custom: instructions own the format)
|
| 28 |
+
recursion_limit: +(a.recursion_limit ?? 4) || 4, // max tool rounds
|
| 29 |
+
category: a.category || "general",
|
| 30 |
+
version: +(a.version ?? 1) || 1,
|
| 31 |
+
};
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export function makeAgents(chatStore) {
|
| 35 |
+
const { store, getIndex, newId } = chatStore;
|
| 36 |
+
const putIndex = async (idx) => { const b = new TextEncoder().encode(JSON.stringify(idx)); return store.backend.putRaw ? store.backend.putRaw("index:org.hologram.HoloQ", b) : store.backend.put("index:org.hologram.HoloQ", b); };
|
| 37 |
+
|
| 38 |
+
async function save(a) {
|
| 39 |
+
const norm = normAgent(a);
|
| 40 |
+
if (!norm.id) norm.id = newId("agent");
|
| 41 |
+
const prevPtr = ((await getIndex()).agents || []).find((x) => x.id === norm.id);
|
| 42 |
+
if (prevPtr) norm.version = (prevPtr.version || 1) + 1;
|
| 43 |
+
const obj = await store.makeObject({
|
| 44 |
+
type: ["schema:SoftwareAgent", "prov:Entity"], context: [LC],
|
| 45 |
+
"schema:identifier": norm.id, "schema:name": norm.name, "schema:description": norm.description,
|
| 46 |
+
"lc:agent": norm,
|
| 47 |
+
...(prevPtr ? { links: [{ ...store.contentLink("prov:wasRevisionOf", prevPtr.kappa, "schema:SoftwareAgent"), "schema:name": "previousVersion" }] } : {}),
|
| 48 |
+
});
|
| 49 |
+
const idx = await getIndex();
|
| 50 |
+
idx.agents = idx.agents || [];
|
| 51 |
+
const ptr = { id: norm.id, kappa: obj.id, name: norm.name, avatar: norm.avatar, category: norm.category, version: norm.version };
|
| 52 |
+
const i = idx.agents.findIndex((x) => x.id === norm.id);
|
| 53 |
+
if (i >= 0) idx.agents[i] = ptr; else idx.agents.push(ptr);
|
| 54 |
+
await putIndex(idx);
|
| 55 |
+
return { ...norm, kappa: obj.id };
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async function list() { return (await getIndex()).agents || []; }
|
| 59 |
+
|
| 60 |
+
async function get(id) {
|
| 61 |
+
const ptr = (await list()).find((x) => x.id === id); if (!ptr) return null;
|
| 62 |
+
const obj = await store.getObj(ptr.kappa); if (!obj) return null;
|
| 63 |
+
if (!(await store.verify(obj))) return null; // Law L5: a tampered agent is refused
|
| 64 |
+
return { ...normAgent(obj["lc:agent"]), kappa: ptr.kappa };
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async function remove(id) {
|
| 68 |
+
const idx = await getIndex();
|
| 69 |
+
idx.agents = (idx.agents || []).filter((x) => x.id !== id);
|
| 70 |
+
await putIndex(idx);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return { save, list, get, remove, normAgent };
|
| 74 |
+
}
|
core/backend.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/backend.js — the durable byte backend for the κ-object store: IndexedDB via the OS
|
| 2 |
+
// runtime's _shared/holo-store.js (Law L3: the store is the memory; RAM is a bounded cache).
|
| 3 |
+
// Keys are κ hex strings (plus the one well-known boot-index pointer key). Same async shape
|
| 4 |
+
// as the witness's Map backend, so the L5 contract exercises identical code in both runtimes.
|
| 5 |
+
|
| 6 |
+
import { idbBackend } from "../_shared/holo-store.js";
|
| 7 |
+
|
| 8 |
+
export function kappaBackend() {
|
| 9 |
+
const idb = idbBackend({ db: "holo-q", store: "kappa" });
|
| 10 |
+
return {
|
| 11 |
+
get: async (hex) => (await idb.get(hex)) || undefined,
|
| 12 |
+
put: (hex, bytes) => idb.set(hex, bytes),
|
| 13 |
+
getRaw: async (k) => (await idb.get(k)) || undefined,
|
| 14 |
+
putRaw: (k, bytes) => idb.set(k, bytes),
|
| 15 |
+
has: (k) => idb.has(k),
|
| 16 |
+
};
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Storage usage for the Settings → Data tab (W3C StorageManager).
|
| 20 |
+
export async function storageEstimate() {
|
| 21 |
+
try { const e = await navigator.storage.estimate(); return { usage: e.usage || 0, quota: e.quota || 0 }; }
|
| 22 |
+
catch { return { usage: 0, quota: 0 }; }
|
| 23 |
+
}
|
core/code-tools.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/code-tools.js — NATIVE agentic-coding tools for Holo Q's Code mode. Self-contained over the
|
| 2 |
+
// browser's OPFS (navigator.storage.getDirectory) — no external VFS, works in any browser, 100%
|
| 3 |
+
// serverless. Each tool returns Holo Q's agent-tool shape { def, serverName:"code", call }, so the
|
| 4 |
+
// Coder-7B brain drives real file work through the existing agentic loop + conscience gate + receipts.
|
| 5 |
+
// The workspace is an OPFS "workspace" dir = the agent's writable home; every result is content-addressable.
|
| 6 |
+
//
|
| 7 |
+
// C2 semantic skin (extended to all κ-objects): build_app emits a schema:SoftwareApplication node and
|
| 8 |
+
// verify_object returns the file's schema.org @type — so every object the agent makes is W3C-legible.
|
| 9 |
+
|
| 10 |
+
import { appAsSoftware, fileAsDocument } from "./semantic.js";
|
| 11 |
+
|
| 12 |
+
const te = new TextEncoder(), td = new TextDecoder();
|
| 13 |
+
const sha256Hex = async (bytes) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 14 |
+
const norm = (p) => ("/" + String(p || "").replace(/^\/+/, "").replace(/\/+/g, "/")).replace(/\/$/, "") || "/";
|
| 15 |
+
|
| 16 |
+
async function root() { const d = await navigator.storage.getDirectory(); return d.getDirectoryHandle("workspace", { create: true }); }
|
| 17 |
+
async function dirOf(parts, create = false) {
|
| 18 |
+
let h = await root();
|
| 19 |
+
for (const p of parts) if (p) h = await h.getDirectoryHandle(p, { create });
|
| 20 |
+
return h;
|
| 21 |
+
}
|
| 22 |
+
function split(path) { const parts = norm(path).split("/").filter(Boolean); const name = parts.pop(); return { parts, name }; }
|
| 23 |
+
|
| 24 |
+
async function readFile(path) {
|
| 25 |
+
const { parts, name } = split(path);
|
| 26 |
+
const dir = await dirOf(parts);
|
| 27 |
+
const fh = await dir.getFileHandle(name);
|
| 28 |
+
const f = await fh.getFile();
|
| 29 |
+
return new Uint8Array(await f.arrayBuffer());
|
| 30 |
+
}
|
| 31 |
+
async function writeFile(path, content) {
|
| 32 |
+
const { parts, name } = split(path);
|
| 33 |
+
const dir = await dirOf(parts, true);
|
| 34 |
+
const fh = await dir.getFileHandle(name, { create: true });
|
| 35 |
+
const w = await fh.createWritable(); await w.write(te.encode(String(content ?? ""))); await w.close();
|
| 36 |
+
}
|
| 37 |
+
async function walk(dirHandle, prefix, out) {
|
| 38 |
+
for await (const [n, h] of dirHandle.entries()) {
|
| 39 |
+
const p = prefix + "/" + n;
|
| 40 |
+
if (h.kind === "file") out.push(p);
|
| 41 |
+
else await walk(h, p, out);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
async function listAll() { const out = []; await walk(await root(), "", out); return out.sort(); }
|
| 45 |
+
|
| 46 |
+
function globToRe(glob) {
|
| 47 |
+
let re = "^";
|
| 48 |
+
for (let i = 0; i < glob.length; i++) { const c = glob[i];
|
| 49 |
+
if (c === "*") { if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++; } else re += "[^/]*"; }
|
| 50 |
+
else if (c === "?") re += "[^/]"; else if ("\\^$.|+()[]{}".includes(c)) re += "\\" + c; else re += c; }
|
| 51 |
+
return new RegExp(re + "$");
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function codeTools() {
|
| 55 |
+
return [
|
| 56 |
+
{
|
| 57 |
+
def: { name: "read_file", description: "Read a text file from the workspace. Returns its content.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
|
| 58 |
+
serverName: "code", call: async ({ path }) => {
|
| 59 |
+
try { const bytes = await readFile(path); return { text: td.decode(bytes), isError: false }; }
|
| 60 |
+
catch (e) { return { text: "read error: " + (e.message || e), isError: true }; }
|
| 61 |
+
},
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
def: { name: "write_file", description: "Create or overwrite a file in the workspace with the given content.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
|
| 65 |
+
serverName: "code", call: async ({ path, content }) => {
|
| 66 |
+
try { await writeFile(path, content); const k = await sha256Hex(te.encode(String(content ?? ""))); return { text: `wrote ${norm(path)} (${String(content ?? "").length} bytes) · did:holo:sha256:${k.slice(0, 16)}`, isError: false }; }
|
| 67 |
+
catch (e) { return { text: "write error: " + (e.message || e), isError: true }; }
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
def: { name: "edit_file", description: "Replace the first exact occurrence of old_text with new_text in a file.", inputSchema: { type: "object", properties: { path: { type: "string" }, old_text: { type: "string" }, new_text: { type: "string" } }, required: ["path", "old_text", "new_text"] } },
|
| 72 |
+
serverName: "code", call: async ({ path, old_text, new_text }) => {
|
| 73 |
+
try { const cur = td.decode(await readFile(path)); if (!cur.includes(old_text)) return { text: "edit error: old_text not found", isError: true };
|
| 74 |
+
const next = cur.replace(old_text, new_text); await writeFile(path, next); return { text: `edited ${norm(path)} (${cur.length}→${next.length} bytes)`, isError: false }; }
|
| 75 |
+
catch (e) { return { text: "edit error: " + (e.message || e), isError: true }; }
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
def: { name: "list_files", description: "List all files in the workspace, optionally filtered by a glob (e.g. **/*.py).", inputSchema: { type: "object", properties: { glob: { type: "string" } }, required: [] } },
|
| 80 |
+
serverName: "code", call: async ({ glob }) => {
|
| 81 |
+
try { let files = await listAll(); if (glob) { const re = globToRe(glob); files = files.filter((f) => re.test(f)); }
|
| 82 |
+
return { text: files.length ? files.join("\n") : "(workspace empty)", isError: false }; }
|
| 83 |
+
catch (e) { return { text: "list error: " + (e.message || e), isError: true }; }
|
| 84 |
+
},
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
def: { name: "grep", description: "Search file contents for a regular expression. Returns matching path:line: text.", inputSchema: { type: "object", properties: { pattern: { type: "string" }, glob: { type: "string" } }, required: ["pattern"] } },
|
| 88 |
+
serverName: "code", call: async ({ pattern, glob }) => {
|
| 89 |
+
try { const re = new RegExp(pattern); let files = await listAll(); if (glob) { const g = globToRe(glob); files = files.filter((f) => g.test(f)); }
|
| 90 |
+
const hits = [];
|
| 91 |
+
for (const f of files) { let text; try { text = td.decode(await readFile(f)); } catch { continue; }
|
| 92 |
+
text.split("\n").forEach((ln, i) => { if (re.test(ln) && hits.length < 100) hits.push(`${f}:${i + 1}: ${ln.trim().slice(0, 160)}`); }); }
|
| 93 |
+
return { text: hits.length ? hits.join("\n") : "(no matches)", isError: false }; }
|
| 94 |
+
catch (e) { return { text: "grep error: " + (e.message || e), isError: true }; }
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
// ── κ-NATIVE: build a RENDERABLE app object, sealed (the neural-computer flagship: an agent
|
| 98 |
+
// produces a real app, content-addressed, that renders + re-derives — serverless, verifiable) ──
|
| 99 |
+
{
|
| 100 |
+
def: { name: "build_app", description: "Build a complete self-contained HTML app (one file, inline CSS/JS) and seal it as a content-addressed object. Returns its did:holo κ; the app renders live in the preview pane. Use for 'build me an app/page/tool' requests.", inputSchema: { type: "object", properties: { name: { type: "string" }, html: { type: "string" } }, required: ["name", "html"] } },
|
| 101 |
+
serverName: "code", call: async ({ name, html }) => {
|
| 102 |
+
try {
|
| 103 |
+
const path = (name.endsWith(".html") ? name : name + ".html");
|
| 104 |
+
await writeFile(path, html);
|
| 105 |
+
const bytes = te.encode(String(html));
|
| 106 |
+
const kappa = "did:holo:sha256:" + await sha256Hex(bytes);
|
| 107 |
+
// semantic skin: the app object carries a W3C @type (schema:SoftwareApplication + prov:Entity),
|
| 108 |
+
// written as a sidecar .jsonld so the object is self-describing to any agent / RDF tool.
|
| 109 |
+
const ld = appAsSoftware({ name: path, kappa, bytes: bytes.length });
|
| 110 |
+
await writeFile(path.replace(/\.html$/i, "") + ".app.jsonld", JSON.stringify(ld, null, 1));
|
| 111 |
+
return { text: `built app "${path}" (${html.length} bytes), sealed ${kappa.slice(0, 30)}… · typed ${ld["@type"].join("+")} — renders in the preview, re-derivable (Law L5).`, isError: false, render: { kind: "app", path: norm(path), kappa, html: String(html), ld } };
|
| 112 |
+
} catch (e) { return { text: "build_app error: " + (e.message || e), isError: true }; }
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
def: { name: "verify_object", description: "Re-derive a workspace file's content address and confirm it matches (Law L5 — verify every byte). Returns whether the bytes are authentic.", inputSchema: { type: "object", properties: { path: { type: "string" }, kappa: { type: "string" } }, required: ["path"] } },
|
| 117 |
+
serverName: "code", call: async ({ path, kappa }) => {
|
| 118 |
+
try {
|
| 119 |
+
const bytes = await readFile(path);
|
| 120 |
+
const got = "did:holo:sha256:" + await sha256Hex(bytes);
|
| 121 |
+
const ok = !kappa || got === kappa || got.startsWith(kappa) || kappa.startsWith(got.slice(0, kappa.length));
|
| 122 |
+
// verify = re-derive κ (Law L5) AND emit the W3C @type — apps are SoftwareApplication, all else DigitalDocument.
|
| 123 |
+
const ld = /\.html$/i.test(path) ? appAsSoftware({ name: norm(path), kappa: got, bytes: bytes.length })
|
| 124 |
+
: fileAsDocument({ path: norm(path), kappa: got, bytes: bytes.length });
|
| 125 |
+
return { text: `${norm(path)} re-derives to ${got.slice(0, 34)}…${kappa ? (ok ? " ✓ matches (authentic)" : " ✗ MISMATCH") : ""} · @type ${ld["@type"].join("+")}`, isError: false, render: { kind: "linked-data", ld } };
|
| 126 |
+
} catch (e) { return { text: "verify error: " + (e.message || e), isError: true }; }
|
| 127 |
+
},
|
| 128 |
+
},
|
| 129 |
+
];
|
| 130 |
+
}
|
core/engine.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/engine.js — the inference ENGINE adapter (the only module that touches the wasm
|
| 2 |
+
// tokenizer + the WebGPU `gpu` object). DOM-free. Wraps a model that core/loader.js has
|
| 3 |
+
// already loaded onto the GPU and exposes a clean, UI-agnostic API:
|
| 4 |
+
//
|
| 5 |
+
// const engine = await createEngine(modelEntry, { gpu, info, imageKappa });
|
| 6 |
+
// const { text, outIds } = await engine.generate(ids, { onToken, signal });
|
| 7 |
+
// const rec = await engine.buildReceipt({ ... }); // PROV-O, re-derivable (Law L5)
|
| 8 |
+
//
|
| 9 |
+
// The token loop, framing, memo and receipt logic are lifted byte-for-byte from the
|
| 10 |
+
// original index.html think()/run()/sealReceipt — only the DOM writes are replaced by an
|
| 11 |
+
// onToken callback and the running/handedOff flags by an AbortSignal, so output (and the
|
| 12 |
+
// receipt κ) is identical to the original app.
|
| 13 |
+
|
| 14 |
+
import { qvac_tokenize, qvac_continue, kappa } from "../pkg/holospaces_web.js";
|
| 15 |
+
import { clean, didHolo, kappaTokens, sealReceipt, verifyIntegrity, idBytes, kappaBytes } from "./kappa.js";
|
| 16 |
+
|
| 17 |
+
const _perf = () => (typeof performance !== "undefined" ? performance.now() : 0);
|
| 18 |
+
const _sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
| 19 |
+
|
| 20 |
+
// The engine is itself a content-addressed object — hash the wasm once (lazy).
|
| 21 |
+
let _engineK = null;
|
| 22 |
+
export async function engineKappa() {
|
| 23 |
+
if (_engineK) return _engineK;
|
| 24 |
+
try { const b = new Uint8Array(await (await fetch(new URL("../pkg/holospaces_web_bg.wasm", import.meta.url))).arrayBuffer()); _engineK = await kappaBytes(b); }
|
| 25 |
+
catch { _engineK = "did:holo:sha256:(engine unavailable)"; }
|
| 26 |
+
return _engineK;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export async function createEngine(modelEntry, loaded) {
|
| 30 |
+
const { gpu, info, imageKappa } = loaded;
|
| 31 |
+
const m = modelEntry;
|
| 32 |
+
const engineReady = engineKappa();
|
| 33 |
+
|
| 34 |
+
// model κ: the κ-disk's VERIFIED image_kappa when present (a real content address of the
|
| 35 |
+
// weights, every sector re-derived); else the model's declared identity.
|
| 36 |
+
const modelKappa = imageKappa
|
| 37 |
+
? "did:holo:sha256:" + String(imageKappa).replace(/^(did:holo:)?sha256:/, "")
|
| 38 |
+
: await didHolo({ "@type": "schema:SoftwareSourceCode", name: m.name, size: m.size, fmt: m.fmt || "", family: m.fam || "" });
|
| 39 |
+
|
| 40 |
+
const memo = new Map();
|
| 41 |
+
let _drafter = null; // learned speculative drafter (fn(seq,max)=>ids); null → standard decode. Set via setDrafter().
|
| 42 |
+
|
| 43 |
+
const tokenize = (text) => { try { return JSON.parse(qvac_tokenize(text)).ids || []; } catch { return []; } };
|
| 44 |
+
const detokenize = (ids) => { try { return clean(JSON.parse(qvac_continue(JSON.stringify(ids), 0, 0, 0, ids.length)).text || ""); } catch { return ""; } };
|
| 45 |
+
const fingerprint = (ids) => kappa(idBytes(ids)); // live mind κ (blake3, from wasm)
|
| 46 |
+
|
| 47 |
+
// Frame one user turn. Qwen2/3 use ChatML (its <|im_*|> markers are atomic BPE tokens);
|
| 48 |
+
// other instruction models use a plain Q/A frame. (Verbatim from the original run().)
|
| 49 |
+
function frameTurn(prompt, hasHistory) {
|
| 50 |
+
if (m.qwen) {
|
| 51 |
+
const noThink = m.qwen3 ? "<think>\n\n</think>\n\n" : ""; // Qwen3: skip the thinking block for fast direct answers
|
| 52 |
+
return (hasHistory ? "<|im_end|>\n" : "") + `<|im_start|>user\n${prompt}<|im_end|>\n<|im_start|>assistant\n` + noThink;
|
| 53 |
+
}
|
| 54 |
+
if (m.llama3) // LLaMA-3 header template (BitNet b1.58 etc.)
|
| 55 |
+
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`;
|
| 56 |
+
if (m.olmo) // OLMo/OLMoE: <|user|>/<|assistant|> role tags (each turn self-delimited; leading bos via m.bos)
|
| 57 |
+
return `<|user|>\n${prompt}\n<|assistant|>\n`;
|
| 58 |
+
if (m.userWord) // word-frame (Falcon-E: its ChatML template stalls EMPIRICALLY; "User:/Falcon:" answers — see q-falcon-templates sweep)
|
| 59 |
+
return (hasHistory ? "\n" : "") + "User: " + prompt + "\nFalcon:";
|
| 60 |
+
return "Question: " + prompt + "\nAnswer:";
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function params() {
|
| 64 |
+
const temp = m.temp || 0;
|
| 65 |
+
return { decode: temp > 0 ? "sampled@t=" + temp : "greedy-argmax", maxTokens: m.cap, repetitionPenalty: m.rep ?? 1.05, template: m.qwen ? "chatml" : m.llama3 ? "llama3" : "qa", thinking: !m.qwen3 };
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// The streaming token loop. `ids` is the whole running conversation (the mind); generation
|
| 69 |
+
// appends to it. onToken({ text, ids, outIds, stats }) fires per step; signal aborts.
|
| 70 |
+
async function generate(ids, { onToken, signal, repPenalty, maxNew } = {}) {
|
| 71 |
+
const rep = repPenalty ?? m.rep ?? 1.3;
|
| 72 |
+
const newCap = maxNew ?? m.cap ?? 80; // max NEW tokens this call
|
| 73 |
+
const kvCap = (m.ctx || m.cap || 80) + 8; // the engine's KV allocation (loader kvOf)
|
| 74 |
+
const promptLen = ids.length;
|
| 75 |
+
const tStart = _perf();
|
| 76 |
+
let first = true, decodeStart = 0, decodeTok = 0, ttft = 0, tokps = 0, msExec = 0, err = null, outText = "";
|
| 77 |
+
if (promptLen >= kvCap - 1) err = new Error(`context full: ${promptLen} tokens ≥ ${kvCap} KV positions`);
|
| 78 |
+
|
| 79 |
+
// SPECULATIVE PATH (opt-in): when a learned drafter is registered and the engine has the batched-verify
|
| 80 |
+
// head, the drafter proposes and the target batch-verifies — output is BYTE-IDENTICAL to greedy decode
|
| 81 |
+
// (greedy verify), streamed via onCommit. Any incompatibility/throw falls straight through to the standard
|
| 82 |
+
// loop below, so default Q (no drafter) and unsupported models are completely unaffected.
|
| 83 |
+
if (!err && _drafter && gpu.specDecode && gpu.setDrafter) {
|
| 84 |
+
try {
|
| 85 |
+
gpu.setDrafter(_drafter);
|
| 86 |
+
const out = []; const t0 = _perf(); let ttft2 = 0, tokps2 = 0;
|
| 87 |
+
const seq = await gpu.specDecode(ids.slice(), newCap, rep, (tk) => {
|
| 88 |
+
if (signal && signal.aborted) return false;
|
| 89 |
+
out.push(tk);
|
| 90 |
+
if (!ttft2) ttft2 = _perf() - tStart;
|
| 91 |
+
const dt = _perf() - t0; if (dt > 0) tokps2 = out.length / (dt / 1000);
|
| 92 |
+
if (onToken) onToken({ text: detokenize(out), ids: ids.slice(0, promptLen).concat(out), outIds: out.slice(), stats: { ttft: ttft2, tokps: tokps2, msExec: gpu.timing ? gpu.timing.exec : 0, gpuBytes: gpu.gpuBytes, spec: gpu.specStats ? gpu.specStats() : null } });
|
| 93 |
+
return out.length < newCap;
|
| 94 |
+
});
|
| 95 |
+
const outIds = seq.slice(promptLen);
|
| 96 |
+
let text = detokenize(outIds);
|
| 97 |
+
if (m.stopText) { const ix = text.indexOf(m.stopText); if (ix >= 0) text = text.slice(0, ix); }
|
| 98 |
+
return { text, outIds, ids: seq, stats: { ttft: ttft2, tokps: tokps2, msExec: gpu.timing ? gpu.timing.exec : 0, spec: gpu.specStats ? gpu.specStats() : null }, error: null };
|
| 99 |
+
} catch (e) { try { gpu.setDrafter(null); } catch {} /* fall through to the standard decode loop */ }
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
while (!err && !(signal && signal.aborted) && ids.length - promptLen < newCap && ids.length < kvCap - 1) {
|
| 103 |
+
const prevLen = ids.length;
|
| 104 |
+
try { ids = await (gpu.decode || gpu.generate)(ids, first ? 1 : 6, rep); } // batched GPU decode head (4 B/token readback) when the engine has it
|
| 105 |
+
catch (e) { err = e; break; }
|
| 106 |
+
const dn = ids.length - prevLen;
|
| 107 |
+
if (dn > 0) {
|
| 108 |
+
msExec = gpu.timing ? gpu.timing.exec : msExec;
|
| 109 |
+
if (first) { ttft = _perf() - tStart; decodeStart = _perf(); first = false; } // TTFT = prefill + first token
|
| 110 |
+
else { decodeTok += dn; const dt = _perf() - decodeStart; if (dt > 0) tokps = decodeTok / (dt / 1000); } // steady decode rate
|
| 111 |
+
}
|
| 112 |
+
const di = ids.slice(promptLen);
|
| 113 |
+
// incremental detokenize: decode only a bounded window (the dn new tokens + a small left context)
|
| 114 |
+
// and append the delta — O(1) per step, not re-detokenizing the whole growing output (was O(n²)).
|
| 115 |
+
{ const a = Math.max(promptLen, ids.length - (dn + 8)); const wf = detokenize(ids.slice(a)); const wp = a >= ids.length - dn ? "" : detokenize(ids.slice(a, ids.length - dn)); outText += wf.slice(wp.length); }
|
| 116 |
+
let text = outText, hitStop = false;
|
| 117 |
+
if (m.stopText) { const ix = text.indexOf(m.stopText); if (ix >= 0) { text = text.slice(0, ix); hitStop = true; } } // word-framed models stop on the next "User:" turn
|
| 118 |
+
if (onToken) onToken({ text, ids: ids.slice(), outIds: di.slice(), stats: { ttft, tokps, msExec, gpuBytes: gpu.gpuBytes } });
|
| 119 |
+
if (hitStop) break;
|
| 120 |
+
if (ids.length <= prevLen) break; // EOS / no progress
|
| 121 |
+
// degeneration guard: a long run of one repeated character (the repetition collapse of
|
| 122 |
+
// small/experimental quants) will never recover — stop instead of burning the budget.
|
| 123 |
+
if (text.length > 80 && /(.)\1{63}$/.test(text)) { err = new Error("degenerate repetition — stopped"); break; }
|
| 124 |
+
await _sleep(0); // yield to the event loop so the UI can paint — no artificial throttle (gpu.decode already awaits the GPU)
|
| 125 |
+
}
|
| 126 |
+
const outIds = ids.slice(promptLen);
|
| 127 |
+
let text = detokenize(outIds);
|
| 128 |
+
if (m.stopText) { const ix = text.indexOf(m.stopText); if (ix >= 0) text = text.slice(0, ix); }
|
| 129 |
+
return { text, outIds, ids, stats: { ttft, tokps, msExec }, error: err };
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// DIFFUSION decode (Dream-class): iterative bidirectional unmasking over a fixed `steps` budget,
|
| 133 |
+
// wall-clock fixed by steps not output length. Greedy ⇒ deterministic ⇒ κ-re-derivable (Law L5).
|
| 134 |
+
// `ids` is the framed prompt; we diffuse `genLen` masked positions after it. Returns the same shape
|
| 135 |
+
// as generate() so callers (and the brain seam) are agnostic. onToken fires ONCE with the final fill
|
| 136 |
+
// (diffusion has no left-to-right token stream — the whole block resolves together).
|
| 137 |
+
// Two modes: APPEND (genLen masks at the suffix — generation) or FILL (ids ALREADY contain mask ids
|
| 138 |
+
// anywhere → infill/surgical edit, conditioning on BOTH sides; diffusion's structural edge over AR).
|
| 139 |
+
// `causal` flips the parity gate (causal block=1 must equal the sequential engine — validates the pass).
|
| 140 |
+
async function diffuse(ids, { genLen, steps, fill, causal, signal, onToken } = {}) {
|
| 141 |
+
if (!gpu || !gpu.diffuse) throw new Error("this model has no diffusion engine (load a diffusion κ-object)");
|
| 142 |
+
const gl = fill ? 0 : (genLen ?? Math.min(m.cap || 64, (m.ctx || 192) - ids.length - 1));
|
| 143 |
+
const S = steps ?? m.steps ?? 12;
|
| 144 |
+
const tStart = _perf();
|
| 145 |
+
const seq = await gpu.diffuse(ids, gl, { steps: S, fill: !!fill, causal: !!causal, signal });
|
| 146 |
+
// append → output is the generated suffix; fill → the whole sequence is the answer (a span edited in place)
|
| 147 |
+
const outIds = fill ? seq.slice() : seq.slice(ids.length);
|
| 148 |
+
let text = detokenize(outIds);
|
| 149 |
+
if (m.stopText && !fill) { const ix = text.indexOf(m.stopText); if (ix >= 0) text = text.slice(0, ix); }
|
| 150 |
+
const stats = { ttft: _perf() - tStart, tokps: 0, msExec: gpu.timing ? gpu.timing.exec : 0, steps: S, fill: !!fill, diff: gpu.diffStats ? gpu.diffStats() : null };
|
| 151 |
+
if (onToken) onToken({ text, ids: seq.slice(), outIds: outIds.slice(), stats });
|
| 152 |
+
return { text, outIds, ids: seq, stats, error: null };
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// κ-memo: identical (context ⊕ prompt ⊕ model ⊕ params) → replay in O(1), no decode.
|
| 156 |
+
const memoKey = async (ctxIds, turnIds, p) => didHolo({ ctx: await kappaTokens(ctxIds.concat(turnIds)), model: modelKappa, params: p || params() });
|
| 157 |
+
|
| 158 |
+
async function buildReceipt({ promptText, ctxIds, turnIds, outIds, fromMemo, evaluateText, paramsPatch, extraUsed }) {
|
| 159 |
+
return sealReceipt({
|
| 160 |
+
promptText, ctxIds, turnIds, outIds, text: detokenize(outIds), params: { ...params(), ...(paramsPatch || {}) }, fromMemo,
|
| 161 |
+
modelKappa, engineKappa: await engineReady, evaluateText, extraUsed,
|
| 162 |
+
});
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Re-derivation (greedy only): re-run the exact inference and reproduce κ(output) byte-for-byte.
|
| 166 |
+
async function reDerive(rec) {
|
| 167 |
+
if (!gpu) return { ok: false, reason: "load the model to re-derive" };
|
| 168 |
+
if (/sampled/.test(rec.params.decode)) return { ok: false, reason: "sampled decode — only the κ-binding is verifiable, not re-derivation" };
|
| 169 |
+
try {
|
| 170 |
+
let seq = rec.ctxIds.concat(rec.turnIds); const start = seq.length;
|
| 171 |
+
gpu.reset();
|
| 172 |
+
seq = await (gpu.decode || gpu.generate)(seq, rec.outIds.length, rec.params.repetitionPenalty); // same head as the live path — replay must match byte-for-byte
|
| 173 |
+
const got = await kappaTokens(seq.slice(start)), want = rec.body["prov:generated"]["holo:outputTokens"];
|
| 174 |
+
return { ok: got === want, got, want };
|
| 175 |
+
} finally { try { gpu.reset(); } catch {} }
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
model: m, dims: gpu.dims, modelKappa, bosId: info?.bos ?? null, get gpuBytes() { return gpu.gpuBytes; },
|
| 180 |
+
tokenize, detokenize, fingerprint, frameTurn, params,
|
| 181 |
+
generate,
|
| 182 |
+
// register/clear the learned speculative drafter (fn(seq,max)=>ids). Off by default; safe fallback.
|
| 183 |
+
setDrafter: (fn) => { _drafter = fn || null; try { gpu.setDrafter && gpu.setDrafter(_drafter); } catch (e) {} },
|
| 184 |
+
specAvailable: !!(gpu.specDecode && gpu.setDrafter),
|
| 185 |
+
memoKey, memoGet: (k) => memo.get(k), memoHas: (k) => memo.has(k), memoSet: (k, v) => memo.set(k, v),
|
| 186 |
+
buildReceipt, verify: verifyIntegrity, reDerive,
|
| 187 |
+
stats: () => gpu.timing, reset: () => { try { gpu.reset(); } catch {} }, destroy: () => { try { gpu.destroy(); } catch {} },
|
| 188 |
+
};
|
| 189 |
+
}
|
core/kappa.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/kappa.js — the PURE content-addressing + receipt layer (no wasm, no DOM).
|
| 2 |
+
//
|
| 3 |
+
// Lifted verbatim (behaviour-identical) from the original Holo Q index.html so that
|
| 4 |
+
// BOTH the browser app and the pure-Node witness (tools/q-witness.mjs) import the same
|
| 5 |
+
// re-derivation logic. Uses only Web Crypto (crypto.subtle), which exists in the browser
|
| 6 |
+
// and in Node ≥ 20 as globalThis.crypto.subtle — so a receipt's κ re-derives identically
|
| 7 |
+
// in either runtime (Law L5). The PROV-O receipt body and its did:holo are byte-for-byte
|
| 8 |
+
// what the original sealed, so existing receipts keep verifying.
|
| 9 |
+
|
| 10 |
+
const _enc = new TextEncoder();
|
| 11 |
+
const _td = new TextDecoder();
|
| 12 |
+
|
| 13 |
+
// RFC 8785 JCS — canonical JSON, so a receipt's κ re-derives identically anywhere.
|
| 14 |
+
export const jcs = (v) => Array.isArray(v) ? "[" + v.map(jcs).join(",") + "]"
|
| 15 |
+
: (v && typeof v === "object") ? "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}"
|
| 16 |
+
: JSON.stringify(v);
|
| 17 |
+
|
| 18 |
+
export async function sha256hex(u8) {
|
| 19 |
+
const d = await crypto.subtle.digest("SHA-256", u8);
|
| 20 |
+
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const idBytes = (ids) => new Uint8Array(new Uint32Array(ids).buffer);
|
| 24 |
+
|
| 25 |
+
export const didHolo = async (obj) => "did:holo:sha256:" + await sha256hex(_enc.encode(jcs(obj)));
|
| 26 |
+
export const kappaText = async (s) => "did:holo:sha256:" + await sha256hex(_enc.encode(s || ""));
|
| 27 |
+
export const kappaTokens = async (a) => "did:holo:sha256:" + await sha256hex(idBytes(a)); // the answer's tokens, by content
|
| 28 |
+
export const kappaBytes = async (u8) => "did:holo:sha256:" + await sha256hex(u8);
|
| 29 |
+
|
| 30 |
+
export const shortK = (k) => {
|
| 31 |
+
const s = String(k || ""); const ax = s.split(":").slice(0, 2).join(":"); const h = s.split(":").pop();
|
| 32 |
+
return ax + ":" + (h.length > 18 ? h.slice(0, 12) + "…" + h.slice(-4) : h);
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
// SentencePiece byte-fallback tokens (<0xNN>) → their actual bytes, UTF-8 decoded.
|
| 36 |
+
export const decodeBytes = (t) => t.replace(/(?:<0x[0-9A-Fa-f]{2}>)+/g, (run) => {
|
| 37 |
+
const b = []; run.replace(/<0x([0-9A-Fa-f]{2})>/g, (_, h) => (b.push(parseInt(h, 16)), ""));
|
| 38 |
+
try { return _td.decode(new Uint8Array(b)); } catch { return ""; }
|
| 39 |
+
});
|
| 40 |
+
export const clean = (t) => decodeBytes(
|
| 41 |
+
t.replace(/<think>[\s\S]*?<\/think>/g, "").replace(/<\/?think>/g, "").replace(/<\|[^>]*\|>/g, "").replace(/<unk>|<s>|<\/s>/g, "")
|
| 42 |
+
).replace(/^\s+/, "");
|
| 43 |
+
|
| 44 |
+
// ── verifiable-inference receipt ────────────────────────────────────────────────
|
| 45 |
+
// An answer is a content-addressed, re-derivable transform on the substrate:
|
| 46 |
+
// κ(context) ⊕ κ(prompt) ⊕ κ(model) ⊕ κ(params) ⊕ κ(engine) → κ(output)
|
| 47 |
+
// sealed as a PROV-O receipt with its OWN did:holo. Greedy decode is deterministic,
|
| 48 |
+
// so anyone re-runs it and reproduces κ(output) byte for byte (Law L5).
|
| 49 |
+
|
| 50 |
+
// Build the canonical PROV-O receipt body. All κ inputs are already did:holo strings.
|
| 51 |
+
// `extraUsed` merges additional provenance (e.g. holo:toolReceipts — the agentic work-trail).
|
| 52 |
+
export function receiptBody({ modelKappa, engineKappa, promptKappa, contextKappa, outputKappa, tokenCount, params, conscience, extraUsed }) {
|
| 53 |
+
return {
|
| 54 |
+
"@context": ["http://www.w3.org/ns/prov#", { holo: "https://hologram.os/ns/q#" }],
|
| 55 |
+
"@type": "prov:Activity", "holo:kind": "verifiable-inference",
|
| 56 |
+
"prov:used": { "holo:model": modelKappa, "holo:engine": engineKappa, "holo:prompt": promptKappa, "holo:context": contextKappa, "holo:params": params, ...(extraUsed || {}) },
|
| 57 |
+
"prov:generated": { "holo:outputTokens": outputKappa, "holo:tokenCount": tokenCount },
|
| 58 |
+
"holo:conscience": conscience || { outcome: "unverified" },
|
| 59 |
+
};
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Seal a receipt: compute the κ inputs from raw tokens/text, assemble the body, address it.
|
| 63 |
+
// `text` is the already-decoded answer; `evaluateText` (optional) is the conscience judge.
|
| 64 |
+
export async function sealReceipt({ promptText, ctxIds, turnIds, outIds, text, params, fromMemo, modelKappa, engineKappa, evaluateText, extraUsed }) {
|
| 65 |
+
const [promptKappa, contextKappa, outputKappa] = await Promise.all([
|
| 66 |
+
kappaText(promptText), kappaTokens(ctxIds.concat(turnIds)), kappaTokens(outIds),
|
| 67 |
+
]);
|
| 68 |
+
let conscience = { outcome: "unverified" };
|
| 69 |
+
try { if (evaluateText) { const v = evaluateText(text); conscience = { outcome: v.outcome, blocked: v.blocked || [], caveats: v.caveats || [], sealed: v.sealed !== false }; } } catch {}
|
| 70 |
+
const body = receiptBody({ modelKappa, engineKappa, promptKappa, contextKappa, outputKappa, tokenCount: outIds.length, params, conscience, extraUsed });
|
| 71 |
+
const id = await didHolo(body);
|
| 72 |
+
return { id, body, text, promptText, ctxIds: ctxIds.slice(), turnIds: turnIds.slice(), outIds: outIds.slice(), params, fromMemo: !!fromMemo };
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Integrity: recompute the receipt's did:holo from its body — tamper any byte and it won't match.
|
| 76 |
+
export const verifyIntegrity = async (rec) => { const again = await didHolo(rec.body); return { ok: again === rec.id, again }; };
|
core/keymap.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/keymap.js — the keyboard map as a data table (LibreChat's native set): Alt+N new chat,
|
| 2 |
+
// Alt+S toggle sidebar, Alt+W focus the input, Ctrl/⌘+Enter send, Esc stop generation,
|
| 3 |
+
// Ctrl/⌘+K model menu. `/` and `@` composer menus live in ui/composer.js (they are caret-local).
|
| 4 |
+
|
| 5 |
+
export const BINDINGS = [
|
| 6 |
+
{ combo: "Alt+N", action: "newChat", label: "New chat" },
|
| 7 |
+
{ combo: "Alt+S", action: "toggleNav", label: "Toggle sidebar" },
|
| 8 |
+
{ combo: "Alt+W", action: "focusInput", label: "Focus message box" },
|
| 9 |
+
{ combo: "Ctrl+Enter", action: "send", label: "Send message" },
|
| 10 |
+
{ combo: "Escape", action: "stop", label: "Stop generating" },
|
| 11 |
+
{ combo: "Ctrl+K", action: "modelMenu", label: "Choose model" },
|
| 12 |
+
];
|
| 13 |
+
|
| 14 |
+
export function installKeymap(handlers) {
|
| 15 |
+
const onKey = (e) => {
|
| 16 |
+
const ctrl = e.ctrlKey || e.metaKey;
|
| 17 |
+
const fire = (name) => { const fn = handlers[name]; if (fn) { e.preventDefault(); fn(e); } };
|
| 18 |
+
if (e.altKey && !ctrl && !e.shiftKey) {
|
| 19 |
+
if (e.code === "KeyN") return fire("newChat");
|
| 20 |
+
if (e.code === "KeyS") return fire("toggleNav");
|
| 21 |
+
if (e.code === "KeyW") return fire("focusInput");
|
| 22 |
+
}
|
| 23 |
+
if (ctrl && e.key === "Enter") return fire("send");
|
| 24 |
+
if (ctrl && !e.shiftKey && e.code === "KeyK") return fire("modelMenu");
|
| 25 |
+
if (e.key === "Escape") { const fn = handlers.stop; if (fn) fn(e); } // don't preventDefault — Esc also closes menus
|
| 26 |
+
};
|
| 27 |
+
document.addEventListener("keydown", onKey);
|
| 28 |
+
return () => document.removeEventListener("keydown", onKey);
|
| 29 |
+
}
|
core/loader.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/loader.js — model LOADING (the 5 substrate paths) + the model catalog + the
|
| 2 |
+
// browser-cache manager. Lifted faithfully from the original index.html so a model still
|
| 3 |
+
// loads byte-identically; the only change is that DOM status writes become onStatus/onProgress
|
| 4 |
+
// callbacks, and each path RETURNS { gpu, info, manifest, imageKappa } instead of mutating
|
| 5 |
+
// globals. core/engine.js then wraps the returned gpu. (window.__gpu / window.__kd handles are
|
| 6 |
+
// still exposed for the probe + system-monitor panels.)
|
| 7 |
+
|
| 8 |
+
import init, { kappa, qvac_load_model, qvac_load_gpu, qvac_tokenize, qvac_continue, qvac_gpu_manifest, qvac_gpu_tensor, qvac_gpu_free, qvac_panic_hook } from "../pkg/holospaces_web.js";
|
| 9 |
+
import { createQvacGPU } from "../qvac-gpu.js?v=60";
|
| 10 |
+
import { modelAsSource } from "./semantic.js"; // C2: a loaded model carries a W3C @type (schema:SoftwareSourceCode)
|
| 11 |
+
|
| 12 |
+
// the model κ-object's W3C linked-data view — content-addressed identity (Law L1) + schema.org type.
|
| 13 |
+
const modelLinkedData = (m, root) => modelAsSource({
|
| 14 |
+
name: m.name, family: m.fam, params: m.size, format: m.fmt,
|
| 15 |
+
kappa: root ? (String(root).startsWith("did:") ? root : "did:holo:" + String(root)) : "did:holo:sha256:0",
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
// The compiled κ-objects present on disk (models/<name>, built by compile2bit.mjs). Each loads
|
| 19 |
+
// DIRECT off the substrate (verified by re-derivation, no re-quant) via its `kappaUrl`.
|
| 20 |
+
// cap = max NEW tokens per turn; ctx = KV-cache positions allocated on the GPU (the context
|
| 21 |
+
// window — sized so agentic turns with tool schemas + tool responses fit; KV VRAM scales with it).
|
| 22 |
+
export const MODELS = [
|
| 23 |
+
// NATIVELY-TERNARY κ-objects (t2, 1.58 bpw trained-in — see the atlas-bridge witness receipts):
|
| 24 |
+
// Falcon-E: its declared ChatML template STALLS empirically (instant <|end_of_text|>); the
|
| 25 |
+
// measured working frame is word-style "User:/Falcon:" with a textual stop (q-falcon-templates sweep).
|
| 26 |
+
{ fam: "Falcon-E", name: "Falcon-E-3B · ternary", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-falcon-e-3b/resolve/main", manifestKappa: "did:holo:sha256:6b753fe8186f2b4194424115c36014698580a2aab8427e9b40365893ac6b77ca", size: "0.63 GB", fmt: "t2 1.58-bit κ", cap: 200, ctx: 3000, kv4: true, gpu: true, gpuOnly: true, chat: true, userWord: true, stopText: "\nUser:", tools: false, rep: 1.18, kappa: true },
|
| 27 |
+
{ fam: "BitNet", name: "BitNet-2B-4T · ternary", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-bitnet-2b/resolve/main", manifestKappa: "did:holo:sha256:fcf835659d88d2fe6f683cf1ab8de6a6ba6214ea0deeee4b1bcf3da1a4c05412", size: "0.69 GB", fmt: "t2 1.58-bit κ", cap: 900, ctx: 3000, kv4: true, gpu: true, gpuOnly: true, chat: true, llama3: true, tools: false, bos: true, eosText: "<|eot_id|>", rep: 1.05, kappa: true },
|
| 28 |
+
// TriLM: the LARGEST natively-ternary-trained model (Spectra 3.9B, ICLR'25); per-row/channel
|
| 29 |
+
// scale structure → t2r (trit codes + per-256-block scales, exact). BASE model → QA frame + stop.
|
| 30 |
+
{ fam: "TriLM", name: "TriLM-3.9B · ternary", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-trilm-3.9b/resolve/main", manifestKappa: "did:holo:sha256:499032ceb19c0476345a72cf5fea6caec83054c98486c91a5891dfad0d25ea30", size: "0.87 GB", fmt: "t2r 2.1-bit κ", cap: 200, ctx: 3000, kv4: true, gpu: true, gpuOnly: true, chat: true, stopText: "\nQuestion:", tools: false, rep: 1.18, kappa: true },
|
| 31 |
+
// AGENTIC CODER: Qwen2.5-Coder-7B (q3f) — the Holo Code agent brain. Qwen2.5 arch ⇒ ChatML +
|
| 32 |
+
// agentic tool framing work (capability floor for tool use is ~7B; the small ternary models opt out).
|
| 33 |
+
// Self-contained κ-object: tokenizer bundled (source="tokenizer.gguf"), no external dependency.
|
| 34 |
+
{ fam: "Qwen2.5-Coder", name: "Qwen2.5-Coder-7B · agentic", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-qwen-coder-7b/resolve/main", manifestKappa: "did:holo:sha256:539941cb060c7dd583e2e86697e53f2c5d511d597c65d09d9c780fbded2c3edf", size: "3.4 GB", fmt: "q3f κ", cap: 900, ctx: 3000, kv4: true, gpu: true, gpuOnly: true, chat: true, code: true, qwen: true, rep: 1.05, kappa: true },
|
| 35 |
+
// MIXTURE-OF-EXPERTS (G5): OLMoE-1B-7B (Allen AI, Apache-2.0) — 64 experts, 8 active/token, ~1.3B
|
| 36 |
+
// active of 7B. The first RESIDENT-MoE κ-object: experts RAM-resident + CPU top-k router (softmax
|
| 37 |
+
// over all 64, no renorm = OLMoE norm_topk_prob:false). q4 (the engine's resident expert FFN path).
|
| 38 |
+
{ fam: "OLMoE", name: "OLMoE-1B-7B · MoE (64×8)", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-olmoe-1b-7b/resolve/main", manifestKappa: "did:holo:sha256:9cf97ec1c761fd4ef51bc0cd4ac37a0cd8eaa11f1b19b3ae6a141486ad3fe5ad", size: "3.6 GB", fmt: "q4 MoE κ", cap: 400, ctx: 3000, kv4: false, gpu: true, gpuOnly: true, chat: true, olmo: true, bos: true, eosText: "<|endoftext|>", tools: false, rep: 1.1, kappa: true },
|
| 39 |
+
// DIFFUSION (G6): Dream-7B (Dream-org/Dream-v0-Instruct-7B) — masked-diffusion LM on the Qwen2.5-7B
|
| 40 |
+
// backbone (same dims ⇒ ChatML). NOT autoregressive: generation is iterative bidirectional unmasking
|
| 41 |
+
// over `steps` denoising passes (engine.diffuse / gpu.diffuse), wall-clock fixed by steps not length.
|
| 42 |
+
// maskId 151666 rides in the manifest (never tokenized from text). Greedy ⇒ deterministic ⇒ κ-re-derivable.
|
| 43 |
+
{ fam: "Dream", name: "Dream-7B · diffusion", kappaUrl: "https://huggingface.co/HOLOGRAMTECH/q-dream-7b/resolve/main", manifestKappa: "did:holo:sha256:7b862931ae088f348f1f7e9ea3adbd418924c2e07e6ddd134f926e5681ad760d", size: "2.9 GB", fmt: "q3f diffusion κ", cap: 192, ctx: 192, kv4: false, gpu: true, gpuOnly: true, chat: true, qwen: true, diffusion: true, steps: 12, rep: 1.0, kappa: true },
|
| 44 |
+
// Qwen κ-objects (q3f/q4) were pruned from disk for space — re-derive via compile2bit, then re-list.
|
| 45 |
+
];
|
| 46 |
+
const kvOf = (m) => Math.max(96, (m.ctx || m.cap) + 8);
|
| 47 |
+
|
| 48 |
+
const _sizeGb = (s) => { const n = parseFloat(s) || 0; return /mb/i.test(s) ? n / 1024 : n; };
|
| 49 |
+
// default to the SMALLEST usable model — lowest latency, fastest first answer.
|
| 50 |
+
export const defaultModelIndex = () => (MODELS.map((m, i) => i).filter((i) => !MODELS[i].disabled).sort((a, b) => _sizeGb(MODELS[a].size) - _sizeGb(MODELS[b].size))[0]) ?? 0;
|
| 51 |
+
|
| 52 |
+
// ── wasm init (once) + tokenizer re-export so the rest of the app shares this instance ──
|
| 53 |
+
let _initOnce = null;
|
| 54 |
+
export function ready() { if (!_initOnce) _initOnce = init().then(() => { try { qvac_panic_hook(); } catch {} }); return _initOnce; }
|
| 55 |
+
export { qvac_tokenize, qvac_continue, kappa };
|
| 56 |
+
|
| 57 |
+
// ── browser-cache model manager (Cache API) — "Get" downloads + keeps; loading uses the copy ──
|
| 58 |
+
export const MCACHE = "holo-q-models";
|
| 59 |
+
const absUrl = (u) => new URL(u, location.href).href;
|
| 60 |
+
let _cachedUrls = new Set();
|
| 61 |
+
export async function refreshCached() { try { const c = await caches.open(MCACHE); _cachedUrls = new Set((await c.keys()).map((r) => r.url)); } catch { _cachedUrls = new Set(); } return _cachedUrls; }
|
| 62 |
+
export const isCached = (m) => !!m.url && _cachedUrls.has(absUrl(m.url));
|
| 63 |
+
export async function deleteCache(m) { try { const c = await caches.open(MCACHE); await c.delete(m.url); } catch {} await refreshCached(); }
|
| 64 |
+
async function modelBytes(m, onStatus) {
|
| 65 |
+
try { const c = await caches.open(MCACHE); const hit = await c.match(m.url); if (hit) return new Uint8Array(await hit.arrayBuffer()); } catch {}
|
| 66 |
+
onStatus?.(`Downloading ${m.name} (${m.size})…`);
|
| 67 |
+
const res = await fetch(m.url); if (!res.ok) { onStatus?.("download failed: HTTP " + res.status); return null; }
|
| 68 |
+
return new Uint8Array(await res.arrayBuffer());
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const noop = () => {};
|
| 72 |
+
|
| 73 |
+
// loadModel(entry, { onStatus, onProgress }) → { gpu, info, manifest, imageKappa } | null
|
| 74 |
+
// `imageKappa` is the VERIFIED content address of the weights when the path provides one
|
| 75 |
+
// (κ-object root, or κ-disk image_kappa); core/engine.js binds it as the receipt's model κ.
|
| 76 |
+
export async function loadModel(m, { onStatus = noop, onProgress = noop } = {}) {
|
| 77 |
+
await ready();
|
| 78 |
+
onStatus(`Loading ${m.name}…`);
|
| 79 |
+
try {
|
| 80 |
+
if (m.gpuOnly && !navigator.gpu) { onStatus("This model needs WebGPU (not available here)."); return null; }
|
| 81 |
+
if (m.kappaUrl) return await loadKappa(m, onStatus, onProgress);
|
| 82 |
+
if (m.kdisk) return await loadModelKDisk(m, onStatus, onProgress);
|
| 83 |
+
if (m.remote) return await loadModelRemote(m, onStatus, onProgress);
|
| 84 |
+
if (m.diskIngest) return await loadModelDisk(m, onStatus, onProgress);
|
| 85 |
+
let gguf = await modelBytes(m, onStatus); if (!gguf) { onStatus("could not load model"); return null; }
|
| 86 |
+
const lr = JSON.parse(m.gpuOnly ? qvac_load_gpu(gguf) : qvac_load_model(gguf));
|
| 87 |
+
gguf = null;
|
| 88 |
+
if (lr.error) { onStatus("model error: " + lr.error); return null; }
|
| 89 |
+
let gpu = null, manifest = null;
|
| 90 |
+
if (navigator.gpu && m.gpu) {
|
| 91 |
+
try {
|
| 92 |
+
onStatus(`Uploading ${m.name} to the GPU…`);
|
| 93 |
+
const bits = m.q4 ? 4 : 8;
|
| 94 |
+
manifest = JSON.parse(qvac_gpu_manifest(bits)); manifest.twoBit = !!window.__twoBit;
|
| 95 |
+
const __qp = new URLSearchParams(location.search).get("stream");
|
| 96 |
+
const __qmode = __qp === null ? undefined : (__qp === "resident" || __qp === "false" ? false : __qp);
|
| 97 |
+
const stream = __qmode ?? window.__stream ?? m.stream ?? false;
|
| 98 |
+
const __ft = (name) => { const raw = qvac_gpu_tensor(name, bits); return window.__weightHook ? window.__weightHook(name, raw, bits, manifest) : raw; };
|
| 99 |
+
gpu = await createQvacGPU(manifest, __ft, kvOf(m), lr.eos ?? 2, stream);
|
| 100 |
+
window.__gpu = gpu; qvac_gpu_free();
|
| 101 |
+
} catch (e) { gpu = null; if (m.gpuOnly) { onStatus("GPU upload failed: " + e); return null; } }
|
| 102 |
+
}
|
| 103 |
+
onStatus("");
|
| 104 |
+
return { gpu, info: lr, manifest, imageKappa: null };
|
| 105 |
+
} catch (e) { onStatus("could not load model: " + e); return null; }
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// LOAD-DIRECT: a pre-compiled 2-bit/Q4 κ-object (compile2bit.mjs output). Weights arrive ALREADY
|
| 109 |
+
// quantized (no re-quant at load); the tokenizer comes from the source GGUF's header only.
|
| 110 |
+
async function loadKappa(m, onStatus, onProgress) {
|
| 111 |
+
onStatus("Loading κ-object manifest…");
|
| 112 |
+
const ld = await import("../holo-load2bit.mjs");
|
| 113 |
+
// Law L5: pin the manifest κ when the catalog supplies one (m.manifestKappa, or a string m.kappa).
|
| 114 |
+
// Until every model carries a pin, unpinned entries load explicitly (allowUnpinned) — the gap is then
|
| 115 |
+
// a visible data task (populate manifestKappa), not a silent trust of an unauthenticated root.
|
| 116 |
+
const pin = (typeof m.manifestKappa === "string" && m.manifestKappa) || (typeof m.kappa === "string" && m.kappa) || null;
|
| 117 |
+
const { manifest, fetchTensor, info } = await ld.loadKappaObject(m.kappaUrl.replace(/\/+$/, ""), pin ? { expectKappa: pin } : { allowUnpinned: true });
|
| 118 |
+
const ing = await import("../qvac-ingest.mjs");
|
| 119 |
+
onStatus("Building tokenizer (source header, no full download)…");
|
| 120 |
+
const hdr = await ing.readHeader(info.source, ing.rangeReader());
|
| 121 |
+
const lr = JSON.parse(qvac_load_gpu(hdr.headerBytes));
|
| 122 |
+
if (lr.error) { onStatus("tokenizer error: " + lr.error); return null; }
|
| 123 |
+
if (m.eosText) { try { const e = JSON.parse(qvac_tokenize(m.eosText)).ids; if (e && e.length === 1) lr.eos = e[0]; } catch {} } // chat-stop override (e.g. LLaMA-3 <|eot_id|> ≠ header eos)
|
| 124 |
+
qvac_gpu_free();
|
| 125 |
+
manifest.kv4 = !!m.kv4; // int4 KV cache (E6) — catalog opt-in
|
| 126 |
+
// MoE forward reads the layer-packed attention (Wb[l]) + RAM-resident experts (readExpert via
|
| 127 |
+
// fetchTensor) — i.e. stream="layer": attention JS-resident & paged per token, experts cached.
|
| 128 |
+
const sm = manifest.moe ? "layer" : (m.stream || window.__kappaStream || false);
|
| 129 |
+
onStatus(`Building engine from κ-object (${info.mode === "q4" ? "native Q4" : info.incoherent ? "incoherent 2-bit" : "LDLQ 2-bit"}, ${sm || "resident"}, no requant)…`);
|
| 130 |
+
const prog = (done, total) => onProgress(done, total, "streaming");
|
| 131 |
+
const gpu = await createQvacGPU(manifest, fetchTensor, kvOf(m), lr.eos ?? 2, sm, sm ? prog : null);
|
| 132 |
+
window.__gpu = gpu;
|
| 133 |
+
onStatus("");
|
| 134 |
+
return { gpu, info: lr, manifest, imageKappa: info.root || null, ld: modelLinkedData(m, info.root) };
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Very-large-model path: the GGUF never enters wasm; only the header does (tokenizer + manifest),
|
| 138 |
+
// then each tensor is streamed off disk (HTTP Range), converted in JS, paged to the GPU per layer.
|
| 139 |
+
async function loadModelDisk(m, onStatus, onProgress) {
|
| 140 |
+
const bits = m.q4 ? 4 : 8;
|
| 141 |
+
const ing = await import("../qvac-ingest.mjs");
|
| 142 |
+
let read = ing.rangeReader();
|
| 143 |
+
try { const cachedResp = await (await caches.open(MCACHE)).match(m.url); if (cachedResp) { const blob = await cachedResp.blob(); read = async (_u, start, len) => new Uint8Array(await blob.slice(start, start + len).arrayBuffer()); } } catch {}
|
| 144 |
+
onStatus(`Reading ${m.name} header…`);
|
| 145 |
+
const hdr = await ing.readHeader(m.url, read);
|
| 146 |
+
const lr = JSON.parse(qvac_load_gpu(hdr.headerBytes));
|
| 147 |
+
if (lr.error) { onStatus("model error: " + lr.error); return null; }
|
| 148 |
+
const manifest = JSON.parse(qvac_gpu_manifest(bits));
|
| 149 |
+
qvac_gpu_free();
|
| 150 |
+
const fetchTensor = ing.makeDiskFetcher({ url: m.url, readRange: read, dataOffset: hdr.dataOffset, tensors: hdr.tensors, manifest, bits });
|
| 151 |
+
const mode = m.stream || "layer";
|
| 152 |
+
onStatus(`Preparing ${m.name} (one-time, streamed off disk)…`);
|
| 153 |
+
const gpu = await createQvacGPU(manifest, fetchTensor, kvOf(m), lr.eos ?? 2, mode, (d, t) => onProgress(d, t, "layers"));
|
| 154 |
+
window.__gpu = gpu; onStatus("");
|
| 155 |
+
return { gpu, info: lr, manifest, imageKappa: null };
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Out-of-core: stream a PRE-BUILT .qvf frames file from the server, one layer per token via HTTP Range.
|
| 159 |
+
async function loadModelRemote(m, onStatus, onProgress) {
|
| 160 |
+
onStatus(`Loading ${m.name} index…`);
|
| 161 |
+
const index = await (await fetch(m.framesUrl + ".json")).json();
|
| 162 |
+
const url = m.framesUrl;
|
| 163 |
+
const rr = async (off, len) => { const r = await fetch(url, { headers: { Range: `bytes=${off}-${off + len - 1}` } }); if (!r.ok && r.status !== 206) throw new Error("HTTP " + r.status); return new Uint8Array(await r.arrayBuffer()); };
|
| 164 |
+
const header = await rr(index.headerOff, index.headerLen);
|
| 165 |
+
const lr = JSON.parse(qvac_load_gpu(header));
|
| 166 |
+
if (lr.error) { onStatus("model error: " + lr.error); return null; }
|
| 167 |
+
const manifest = index.manifest; qvac_gpu_free();
|
| 168 |
+
const fetchTensor = async (name) => { const s = index.singles[name]; return s ? await rr(s.off, s.len) : new Uint8Array(0); };
|
| 169 |
+
const frameStore = { ready: true, read: (off, len) => rr(index.layersOff + off, len), readExpert: (l, e, role) => { const ri = { gate: 0, up: 1, down: 2 }[role]; const off = index.expertsOff + ((l * index.nExperts + e) * 3 + ri) * index.expertBytes; return rr(off, index.expertBytes); } };
|
| 170 |
+
const layersBytes = (index.packStride || 0) * (index.n_layers || 0) + (manifest.moe ? (index.nExperts * 3 * index.expertBytes * index.n_layers) : 0);
|
| 171 |
+
const cacheBudget = window.__cacheGB != null ? window.__cacheGB * 1073741824 : Math.min(layersBytes, 12 * 1073741824);
|
| 172 |
+
onStatus(`Preparing ${m.name} (served off disk)…`);
|
| 173 |
+
const gpu = await createQvacGPU(manifest, fetchTensor, kvOf(m), lr.eos ?? 2, "remote", (d, t) => onProgress(d, t, "remote"), frameStore, cacheBudget);
|
| 174 |
+
window.__gpu = gpu; onStatus("");
|
| 175 |
+
return { gpu, info: lr, manifest, imageKappa: null };
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// HOLOGRAM: load through a content-addressed κ-DISK — every sector VERIFIED by re-derivation (Law L3/L5).
|
| 179 |
+
async function loadModelKDisk(m, onStatus, onProgress) {
|
| 180 |
+
onStatus(`Resolving ${m.name} κ-disk…`);
|
| 181 |
+
const index = await (await fetch(m.kdiskUrl)).json();
|
| 182 |
+
const { makeKDisk } = await import("../qvac-kdisk.mjs");
|
| 183 |
+
const bases = window.__kdiskSources || m.kdiskSources || [location.origin];
|
| 184 |
+
const sources = bases.map((b) => b.replace(/\/$/, "") + "/" + (index.dataFile || (m.dataUrl || "").replace(/^\.\//, "")));
|
| 185 |
+
const kd = makeKDisk({ index, sources });
|
| 186 |
+
window.__kd = kd;
|
| 187 |
+
const iv = await kd.verifyImage();
|
| 188 |
+
if (!iv.ok) { onStatus("κ-disk image_kappa mismatch — refusing to load"); return null; }
|
| 189 |
+
const rr = kd.rr, qvf = index.qvf;
|
| 190 |
+
const header = await rr(qvf.headerOff, qvf.headerLen);
|
| 191 |
+
const lr = JSON.parse(qvac_load_gpu(header));
|
| 192 |
+
if (lr.error) { onStatus("model error: " + lr.error); return null; }
|
| 193 |
+
const manifest = qvf.manifest; qvac_gpu_free();
|
| 194 |
+
const fetchTensor = async (name) => { const s = qvf.singles[name]; return s ? await rr(s.off, s.len) : new Uint8Array(0); };
|
| 195 |
+
const frameStore = { ready: true, read: (off, len) => rr(qvf.layersOff + off, len),
|
| 196 |
+
readExpert: async (l, e, role) => { const ri = { gate: 0, up: 1, down: 2 }[role]; const blkOff = qvf.expertsOff + (l * qvf.nExperts + e) * 3 * qvf.expertBytes; const blk = await rr(blkOff, 3 * qvf.expertBytes); return blk.slice(ri * qvf.expertBytes, (ri + 1) * qvf.expertBytes); } };
|
| 197 |
+
const layersBytes = (qvf.packStride || 0) * (qvf.n_layers || 0) + (manifest.moe ? (qvf.nExperts * 3 * qvf.expertBytes * qvf.n_layers) : 0);
|
| 198 |
+
const cacheBudget = window.__cacheGB != null ? window.__cacheGB * 1073741824 : Math.min(layersBytes, 1024 * 1048576);
|
| 199 |
+
onStatus(`Realizing ${m.name} (verified off κ-disk)…`);
|
| 200 |
+
const gpu = await createQvacGPU(manifest, fetchTensor, kvOf(m), lr.eos ?? 2, "remote", (d, t) => onProgress(d, t, "κ-disk"), frameStore, cacheBudget);
|
| 201 |
+
window.__gpu = gpu;
|
| 202 |
+
const st = kd.stats(); onStatus(`${index.imageKappa.slice(0, 22)}… · ${st.verified} sectors verified`);
|
| 203 |
+
return { gpu, info: lr, manifest, imageKappa: kd.imageKappa || index.imageKappa || null };
|
| 204 |
+
}
|
core/mcp.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/mcp.js — a lean, browser-native Model Context Protocol CLIENT. Speaks the open MCP
|
| 2 |
+
// spec (JSON-RPC 2.0) over the Streamable HTTP transport (single endpoint, POST per message,
|
| 3 |
+
// optional SSE response bodies, Mcp-Session-Id header) with a fallback for plain HTTP+SSE
|
| 4 |
+
// servers. No SDK, no build step — the protocol is small and this is all of it that a chat
|
| 5 |
+
// client needs: initialize → tools/list → tools/call.
|
| 6 |
+
//
|
| 7 |
+
// Substrate twist: every connected server and every tool call is content-addressable — the
|
| 8 |
+
// caller can seal a PROV-O receipt around call inputs/outputs (see core/tools.js), so tool
|
| 9 |
+
// use gets the same verifiability as inference (Law L5).
|
| 10 |
+
|
| 11 |
+
const PROTOCOL_VERSION = "2025-03-26";
|
| 12 |
+
const CLIENT_INFO = { name: "holo-q", version: "2.0.0" };
|
| 13 |
+
|
| 14 |
+
let _seq = 0;
|
| 15 |
+
const nextId = () => ++_seq;
|
| 16 |
+
|
| 17 |
+
// Parse an SSE stream body and resolve with the JSON-RPC response matching `id`.
|
| 18 |
+
async function readSse(res, id) {
|
| 19 |
+
const reader = res.body.getReader(); const dec = new TextDecoder();
|
| 20 |
+
let buf = "", result = null;
|
| 21 |
+
for (;;) {
|
| 22 |
+
const { done, value } = await reader.read(); if (done) break;
|
| 23 |
+
buf += dec.decode(value, { stream: true });
|
| 24 |
+
let i;
|
| 25 |
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
| 26 |
+
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
|
| 27 |
+
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
| 28 |
+
if (!data) continue;
|
| 29 |
+
try { const msg = JSON.parse(data); if (msg.id === id) { result = msg; reader.cancel().catch(() => {}); return result; } } catch {}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
return result;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export function makeMcpClient({ url, headers = {}, name = "", timeoutMs = 30000 } = {}) {
|
| 36 |
+
let sessionId = null, initialized = null, serverInfo = null, toolCache = null;
|
| 37 |
+
|
| 38 |
+
async function rpc(method, params, { notification = false } = {}) {
|
| 39 |
+
const body = notification
|
| 40 |
+
? { jsonrpc: "2.0", method, ...(params ? { params } : {}) }
|
| 41 |
+
: { jsonrpc: "2.0", id: nextId(), method, ...(params ? { params } : {}) };
|
| 42 |
+
const ctrl = new AbortController();
|
| 43 |
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
| 44 |
+
try {
|
| 45 |
+
const res = await fetch(url, {
|
| 46 |
+
method: "POST", signal: ctrl.signal,
|
| 47 |
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...(sessionId ? { "mcp-session-id": sessionId } : {}), ...headers },
|
| 48 |
+
body: JSON.stringify(body),
|
| 49 |
+
});
|
| 50 |
+
const sid = res.headers.get("mcp-session-id"); if (sid) sessionId = sid;
|
| 51 |
+
if (notification) return null;
|
| 52 |
+
if (res.status === 202) return null;
|
| 53 |
+
if (!res.ok) throw new Error(`MCP ${res.status} ${res.statusText}`);
|
| 54 |
+
const ctype = res.headers.get("content-type") || "";
|
| 55 |
+
const msg = ctype.includes("text/event-stream") ? await readSse(res, body.id) : await res.json();
|
| 56 |
+
if (!msg) throw new Error("MCP: no response for request " + body.id);
|
| 57 |
+
if (msg.error) throw new Error(`MCP ${method}: ${msg.error.message || JSON.stringify(msg.error)}`);
|
| 58 |
+
return msg.result;
|
| 59 |
+
} finally { clearTimeout(t); }
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async function initialize() {
|
| 63 |
+
if (initialized) return initialized;
|
| 64 |
+
initialized = (async () => {
|
| 65 |
+
const r = await rpc("initialize", { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: CLIENT_INFO });
|
| 66 |
+
serverInfo = r && r.serverInfo || { name: name || url };
|
| 67 |
+
await rpc("notifications/initialized", undefined, { notification: true }).catch(() => {});
|
| 68 |
+
return r;
|
| 69 |
+
})();
|
| 70 |
+
return initialized;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// tools/list (cached; paginated per spec)
|
| 74 |
+
async function listTools(force = false) {
|
| 75 |
+
if (toolCache && !force) return toolCache;
|
| 76 |
+
await initialize();
|
| 77 |
+
const out = []; let cursor;
|
| 78 |
+
do {
|
| 79 |
+
const r = await rpc("tools/list", cursor ? { cursor } : {});
|
| 80 |
+
out.push(...(r.tools || [])); cursor = r.nextCursor;
|
| 81 |
+
} while (cursor);
|
| 82 |
+
toolCache = out;
|
| 83 |
+
return out;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// tools/call → { content: [{type:"text",text}|{type:"image",...}], isError }
|
| 87 |
+
async function callTool(toolName, args) {
|
| 88 |
+
await initialize();
|
| 89 |
+
const r = await rpc("tools/call", { name: toolName, arguments: args || {} });
|
| 90 |
+
return r;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
url, name: () => (serverInfo && serverInfo.name) || name || url,
|
| 95 |
+
initialize, listTools, callTool,
|
| 96 |
+
get serverInfo() { return serverInfo; },
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Flatten a tools/call result's content into plain text for the model's <tool_response>.
|
| 101 |
+
export function contentToText(result) {
|
| 102 |
+
if (!result) return "";
|
| 103 |
+
if (result.isError) return "ERROR: " + (result.content || []).map((c) => c.text || "").join("\n");
|
| 104 |
+
return (result.content || []).map((c) => c.type === "text" ? c.text : c.type === "image" ? "[image]" : c.type === "resource" ? JSON.stringify(c.resource).slice(0, 2000) : "").join("\n");
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// ── DISCOVERY — the MCP universe, three rings out from the substrate ─────────────────────
|
| 108 |
+
// 1. the SUBSTRATE's own roster (/.well-known/mcp.json — same-origin, verifiable)
|
| 109 |
+
// 2. user-added servers (κ-pinned config in the boot index)
|
| 110 |
+
// 3. the OFFICIAL public MCP registry (registry.modelcontextprotocol.io — the open index
|
| 111 |
+
// of the ecosystem; remote/streamable-http entries are directly connectable from a tab)
|
| 112 |
+
export async function discoverSubstrate() {
|
| 113 |
+
try {
|
| 114 |
+
const r = await fetch("/.well-known/mcp.json"); if (!r.ok) return [];
|
| 115 |
+
const j = await r.json();
|
| 116 |
+
const servers = j.servers || j.mcpServers || (Array.isArray(j) ? j : [j]);
|
| 117 |
+
return (Array.isArray(servers) ? servers : Object.entries(servers).map(([k, v]) => ({ name: k, ...v })))
|
| 118 |
+
.map((s) => ({ source: "substrate", name: s.name || "hologram", url: s.url || s.endpoint || null, description: s.description || "", tools: s.tools || null, raw: s }))
|
| 119 |
+
.filter((s) => s.url || s.tools);
|
| 120 |
+
} catch { return []; }
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
export async function searchRegistry(query, { limit = 20 } = {}) {
|
| 124 |
+
// The official registry's public REST API (v0). Remote servers carry `remotes[]` with
|
| 125 |
+
// streamable-http/sse URLs — those are connectable straight from the browser.
|
| 126 |
+
const base = "https://registry.modelcontextprotocol.io/v0/servers";
|
| 127 |
+
const u = base + "?limit=" + limit + (query ? "&search=" + encodeURIComponent(query) : "");
|
| 128 |
+
const r = await fetch(u, { headers: { accept: "application/json" } });
|
| 129 |
+
if (!r.ok) throw new Error("registry " + r.status);
|
| 130 |
+
const j = await r.json();
|
| 131 |
+
return (j.servers || []).map((s) => {
|
| 132 |
+
const remote = (s.remotes || []).find((x) => /streamable|http/.test(x.type || "")) || (s.remotes || [])[0] || null;
|
| 133 |
+
return { source: "registry", name: s.name, description: s.description || "", version: s.version,
|
| 134 |
+
url: remote ? remote.url : null, transport: remote ? remote.type : (s.packages?.length ? "package (host-run)" : "unknown"), raw: s };
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export async function discoverAgents() {
|
| 139 |
+
// The substrate's agent doors: NANDA AgentFacts + A2A AgentCards + Skills — one entry point.
|
| 140 |
+
try {
|
| 141 |
+
const r = await fetch("/.well-known/agents.json"); if (!r.ok) return null;
|
| 142 |
+
return await r.json();
|
| 143 |
+
} catch { return null; }
|
| 144 |
+
}
|
core/mcphub.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/mcphub.js — the MCP connection HUB: manages servers (substrate roster · user-added ·
|
| 2 |
+
// registry finds), their lifecycles and tool lists, and exposes the flat enabled-tool set the
|
| 3 |
+
// generation loop consumes. Server configs persist in the boot index (substrate-local).
|
| 4 |
+
//
|
| 5 |
+
// Discovery rings: (1) the substrate's own /.well-known/mcp.json (the OS's 30 verified tools),
|
| 6 |
+
// (2) servers the user added by URL, (3) the official public MCP registry. One hub, the
|
| 7 |
+
// whole universe reachable from a tab.
|
| 8 |
+
|
| 9 |
+
import { makeMcpClient, contentToText, discoverSubstrate, searchRegistry, discoverAgents } from "./mcp.js";
|
| 10 |
+
|
| 11 |
+
export function makeMcpHub({ chatStore, bus }) {
|
| 12 |
+
const servers = new Map(); // id → { id, name, url, headers, enabled, client, tools, status, error }
|
| 13 |
+
let loaded = false;
|
| 14 |
+
|
| 15 |
+
const INDEX_KEY = "index:org.hologram.HoloQ";
|
| 16 |
+
async function persist() {
|
| 17 |
+
const idx = await chatStore.getIndex();
|
| 18 |
+
idx.mcpServers = [...servers.values()].map((s) => ({ id: s.id, name: s.name, url: s.url, headers: s.headers || {}, enabled: !!s.enabled, source: s.source || "user" }));
|
| 19 |
+
const b = new TextEncoder().encode(JSON.stringify(idx));
|
| 20 |
+
await (chatStore.store.backend.putRaw ? chatStore.store.backend.putRaw(INDEX_KEY, b) : chatStore.store.backend.put(INDEX_KEY, b));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function restore() {
|
| 24 |
+
if (loaded) return; loaded = true;
|
| 25 |
+
const idx = await chatStore.getIndex();
|
| 26 |
+
for (const s of idx.mcpServers || []) if (!servers.has(s.id)) servers.set(s.id, { ...s, client: null, tools: [], status: "saved" }); // never clobber a live entry
|
| 27 |
+
bus.emit("mcp-changed");
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const norm = (url) => { try { return new URL(url, location.origin).href; } catch { return url; } };
|
| 31 |
+
const idOf = (url) => "mcp-" + norm(url).replace(/[^\w]+/g, "-").slice(0, 60);
|
| 32 |
+
|
| 33 |
+
async function add({ name, url, headers = {}, source = "user", enabled = true }) {
|
| 34 |
+
const u = norm(url); const id = idOf(u);
|
| 35 |
+
if (!servers.has(id)) servers.set(id, { id, name: name || u, url: u, headers, source, enabled, client: null, tools: [], status: "saved" });
|
| 36 |
+
const s = servers.get(id);
|
| 37 |
+
s.enabled = enabled; // adding is an explicit intent — (re)enable even if a saved entry existed
|
| 38 |
+
await connect(s.id).catch(() => {});
|
| 39 |
+
await persist();
|
| 40 |
+
bus.emit("mcp-changed");
|
| 41 |
+
return s;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async function connect(id) {
|
| 45 |
+
const s = servers.get(id); if (!s) return null;
|
| 46 |
+
s.status = "connecting"; s.error = null; bus.emit("mcp-changed");
|
| 47 |
+
try {
|
| 48 |
+
s.client = makeMcpClient({ url: s.url, headers: s.headers, name: s.name });
|
| 49 |
+
await s.client.initialize();
|
| 50 |
+
s.tools = await s.client.listTools();
|
| 51 |
+
s.name = s.client.name() || s.name;
|
| 52 |
+
s.status = "connected";
|
| 53 |
+
} catch (e) {
|
| 54 |
+
s.client = null; s.tools = []; s.status = "error"; s.error = String(e && e.message || e);
|
| 55 |
+
}
|
| 56 |
+
bus.emit("mcp-changed");
|
| 57 |
+
return s;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async function remove(id) { servers.delete(id); await persist(); bus.emit("mcp-changed"); }
|
| 61 |
+
async function setEnabled(id, on) { const s = servers.get(id); if (s) { s.enabled = !!on; if (on && !s.client) await connect(id); await persist(); bus.emit("mcp-changed"); } }
|
| 62 |
+
|
| 63 |
+
// The flat ARMED tool set the loop consumes: [{ def, serverName, call(args)→{text,isError} }].
|
| 64 |
+
// Per-server `toolFilter` (a name allowlist, set from the UI) selects which tools arm; without
|
| 65 |
+
// one, the first DEFAULT_ARM tools arm. A hard ceiling keeps the system block inside the model's
|
| 66 |
+
// context (a local model can't juggle 30 schemas — pick, like LibreChat's per-server selection).
|
| 67 |
+
const DEFAULT_ARM = 4, MAX_ARM = 8;
|
| 68 |
+
// the substrate's own roster is curated to its core verbs by default
|
| 69 |
+
const SUBSTRATE_DEFAULT = ["search_web", "resolve_object", "answer", "verify_object"];
|
| 70 |
+
function enabledTools() {
|
| 71 |
+
const out = [];
|
| 72 |
+
for (const s of servers.values()) {
|
| 73 |
+
if (!s.enabled || s.status !== "connected") continue;
|
| 74 |
+
const names = s.toolFilter && s.toolFilter.length ? s.toolFilter
|
| 75 |
+
: (s.source === "substrate" ? SUBSTRATE_DEFAULT.filter((n) => s.tools.some((t) => t.name === n)) : s.tools.slice(0, DEFAULT_ARM).map((t) => t.name));
|
| 76 |
+
for (const t of s.tools) {
|
| 77 |
+
if (!names.includes(t.name)) continue;
|
| 78 |
+
out.push({
|
| 79 |
+
def: { name: t.name, description: t.description, inputSchema: t.inputSchema },
|
| 80 |
+
serverName: s.name, serverId: s.id,
|
| 81 |
+
call: async (args) => { const r = await s.client.callTool(t.name, args); return { text: contentToText(r), isError: !!(r && r.isError) }; },
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
// de-dup by tool name (first server wins) — small models address tools by bare name
|
| 86 |
+
const seen = new Set();
|
| 87 |
+
return out.filter((t) => (seen.has(t.def.name) ? false : (seen.add(t.def.name), true))).slice(0, MAX_ARM);
|
| 88 |
+
}
|
| 89 |
+
async function setToolFilter(id, names) { const s = servers.get(id); if (s) { s.toolFilter = names && names.length ? names : null; bus.emit("mcp-changed"); } }
|
| 90 |
+
|
| 91 |
+
// Substrate ring: read the OS roster; candidate endpoints = declared, same-origin /mcp, the
|
| 92 |
+
// conventional localhost MCP port. First reachable wins.
|
| 93 |
+
async function discoverSubstrateServers() {
|
| 94 |
+
const entries = await discoverSubstrate();
|
| 95 |
+
const candidates = new Set();
|
| 96 |
+
for (const e of entries) if (e.url) candidates.add(norm(e.url));
|
| 97 |
+
candidates.add(norm("/mcp"));
|
| 98 |
+
candidates.add("http://127.0.0.1:8787/mcp");
|
| 99 |
+
return { entries, candidates: [...candidates] };
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
restore, add, connect, remove, setEnabled, setToolFilter, enabledTools, persist,
|
| 104 |
+
list: () => [...servers.values()],
|
| 105 |
+
discoverSubstrateServers, searchRegistry, discoverAgents,
|
| 106 |
+
};
|
| 107 |
+
}
|
core/memory.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/memory.js — persistent USER MEMORY (LibreChat memory semantics, substrate-native).
|
| 2 |
+
// Memories are key/value facts (key validated /^[a-z_]+$/, value a complete sentence) stored as
|
| 3 |
+
// κ-objects with pointers in the boot index. They inject into the system block as the
|
| 4 |
+
// "# Existing memory:" section, and the model maintains them itself through two LOCAL tools
|
| 5 |
+
// (set_memory / delete_memory) armed alongside MCP tools in the agentic loop — no server,
|
| 6 |
+
// every write content-addressed and verifiable.
|
| 7 |
+
|
| 8 |
+
const LC = { lc: "https://librechat.ai/ns#" };
|
| 9 |
+
export const KEY_RE = /^[a-z_]+$/;
|
| 10 |
+
|
| 11 |
+
export function makeMemory(chatStore, { tokenLimit = 2000 } = {}) {
|
| 12 |
+
const { store, getIndex, newId } = chatStore;
|
| 13 |
+
const putIndex = async (idx) => { const b = new TextEncoder().encode(JSON.stringify(idx)); return store.backend.putRaw ? store.backend.putRaw("index:org.hologram.HoloQ", b) : store.backend.put("index:org.hologram.HoloQ", b); };
|
| 14 |
+
const tokensOf = (s) => Math.ceil((s || "").length / 4); // a fair, dependency-free estimate
|
| 15 |
+
|
| 16 |
+
async function list() { return (await getIndex()).memories || []; }
|
| 17 |
+
|
| 18 |
+
async function set(key, value) {
|
| 19 |
+
key = String(key || "").toLowerCase().trim();
|
| 20 |
+
if (!KEY_RE.test(key)) return { ok: false, error: "invalid key — use lowercase letters and underscores" };
|
| 21 |
+
const idx = await getIndex();
|
| 22 |
+
idx.memories = idx.memories || [];
|
| 23 |
+
const used = idx.memories.filter((m) => m.key !== key).reduce((n, m) => n + (m.tokenCount || 0), 0);
|
| 24 |
+
const tc = tokensOf(value);
|
| 25 |
+
if (used + tc > tokenLimit) return { ok: false, error: `memory full: ${used}+${tc} > ${tokenLimit} tokens` };
|
| 26 |
+
const obj = await store.makeObject({
|
| 27 |
+
type: ["schema:Statement", "prov:Entity"], context: [LC],
|
| 28 |
+
"lc:key": key, "lc:value": String(value || ""), "lc:tokenCount": tc,
|
| 29 |
+
"schema:dateModified": new Date().toISOString(),
|
| 30 |
+
});
|
| 31 |
+
const i = idx.memories.findIndex((m) => m.key === key);
|
| 32 |
+
const ptr = { key, kappa: obj.id, value: String(value || ""), tokenCount: tc, updated_at: new Date().toISOString() };
|
| 33 |
+
if (i >= 0) idx.memories[i] = ptr; else idx.memories.push(ptr);
|
| 34 |
+
await putIndex(idx);
|
| 35 |
+
return { ok: true, key, kappa: obj.id };
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
async function remove(key) {
|
| 39 |
+
const idx = await getIndex();
|
| 40 |
+
idx.memories = (idx.memories || []).filter((m) => m.key !== key);
|
| 41 |
+
await putIndex(idx);
|
| 42 |
+
return { ok: true };
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// The system-block injection ("# Existing memory:" — the LibreChat convention).
|
| 46 |
+
async function injection() {
|
| 47 |
+
const mems = await list();
|
| 48 |
+
if (!mems.length) return "";
|
| 49 |
+
const used = mems.reduce((n, m) => n + (m.tokenCount || 0), 0);
|
| 50 |
+
return `# Memory Status:\nCurrent memory usage: ${used} tokens\nToken limit: ${tokenLimit} tokens\nRemaining capacity: ${Math.max(0, tokenLimit - used)} tokens\n\n# Existing memory:\n` +
|
| 51 |
+
mems.map((m) => `- ${m.key}: ${m.value}`).join("\n");
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// The two LOCAL tools the agentic loop arms (same shape as MCP hub tools).
|
| 55 |
+
function localTools() {
|
| 56 |
+
return [
|
| 57 |
+
{
|
| 58 |
+
def: { name: "set_memory", description: "Remember a fact about the user across conversations. Only when the user explicitly asks (\"remember that…\"). key: lowercase_with_underscores; value: one complete sentence.", inputSchema: { type: "object", properties: { key: { type: "string" }, value: { type: "string" } }, required: ["key", "value"] } },
|
| 59 |
+
serverName: "memory", call: async (a) => { const r = await set(a.key, a.value); return { text: JSON.stringify(r), isError: !r.ok }; },
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
def: { name: "delete_memory", description: "Forget a remembered fact, by key. Only when the user explicitly asks.", inputSchema: { type: "object", properties: { key: { type: "string" } }, required: ["key"] } },
|
| 63 |
+
serverName: "memory", call: async (a) => { const r = await remove(a.key); return { text: JSON.stringify(r), isError: false }; },
|
| 64 |
+
},
|
| 65 |
+
];
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return { list, set, remove, injection, localTools, tokenLimit };
|
| 69 |
+
}
|
core/presets.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/presets.js — preset CRUD (the saved parameter bundles a conversation spreads, LibreChat's
|
| 2 |
+
// conversationPreset semantics). Each preset is a κ-object in the store; the boot index keeps
|
| 3 |
+
// pointer rows { presetId, kappa, title } so listing is O(1) without a scan.
|
| 4 |
+
|
| 5 |
+
import { normPreset } from "./schema.js";
|
| 6 |
+
|
| 7 |
+
const LC = { lc: "https://librechat.ai/ns#" };
|
| 8 |
+
|
| 9 |
+
export function makePresets(chatStore) {
|
| 10 |
+
const { store, getIndex, newId } = chatStore;
|
| 11 |
+
const putIndex = async (idx) => { const b = new TextEncoder().encode(JSON.stringify(idx)); return store.backend.putRaw ? store.backend.putRaw("index:org.hologram.HoloQ", b) : store.backend.put("index:org.hologram.HoloQ", b); };
|
| 12 |
+
|
| 13 |
+
async function save(p) {
|
| 14 |
+
const norm = normPreset(p);
|
| 15 |
+
if (!norm.presetId) norm.presetId = newId("preset");
|
| 16 |
+
const obj = await store.makeObject({ type: ["schema:PropertyValue", "prov:Entity"], context: [LC], "schema:identifier": norm.presetId, "schema:name": norm.title, "lc:preset": norm });
|
| 17 |
+
const idx = await getIndex();
|
| 18 |
+
idx.presets = idx.presets || [];
|
| 19 |
+
const i = idx.presets.findIndex((x) => x.presetId === norm.presetId);
|
| 20 |
+
const ptr = { presetId: norm.presetId, kappa: obj.id, title: norm.title, defaultPreset: !!p.defaultPreset };
|
| 21 |
+
if (i >= 0) idx.presets[i] = ptr; else idx.presets.push(ptr);
|
| 22 |
+
if (p.defaultPreset) idx.presets.forEach((x) => { if (x.presetId !== norm.presetId) x.defaultPreset = false; });
|
| 23 |
+
await putIndex(idx);
|
| 24 |
+
return { ...norm, kappa: obj.id };
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async function list() { return (await getIndex()).presets || []; }
|
| 28 |
+
|
| 29 |
+
async function get(presetId) {
|
| 30 |
+
const ptr = (await list()).find((x) => x.presetId === presetId); if (!ptr) return null;
|
| 31 |
+
const obj = await store.getObj(ptr.kappa); if (!obj) return null;
|
| 32 |
+
if (!(await store.verify(obj))) return null; // Law L5: a tampered preset is refused
|
| 33 |
+
return { ...normPreset(obj["lc:preset"]), kappa: ptr.kappa };
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function getDefault() {
|
| 37 |
+
const ptrs = await list(); const d = ptrs.find((x) => x.defaultPreset) || null;
|
| 38 |
+
return d ? get(d.presetId) : null;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
async function remove(presetId) {
|
| 42 |
+
const idx = await getIndex();
|
| 43 |
+
idx.presets = (idx.presets || []).filter((x) => x.presetId !== presetId);
|
| 44 |
+
await putIndex(idx); // the κ-object stays content-addressed in the store (dedup'd, unreferenced)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return { save, list, get, getDefault, remove };
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// The factory default — mirrors the engine's real capabilities (greedy decode is the
|
| 51 |
+
// deterministic, verifiable path; temperature stays 0 unless a model declares one).
|
| 52 |
+
export const DEFAULT_PRESET = () => normPreset({
|
| 53 |
+
title: "Default", temperature: 0, maxOutputTokens: 900, promptPrefix: "",
|
| 54 |
+
});
|
core/promptsLib.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/promptsLib.js — the PROMPTS LIBRARY as κ-objects (LibreChat prompt-group semantics).
|
| 2 |
+
// A group carries name · category · oneliner · command (the /slash trigger) and points at its
|
| 3 |
+
// PRODUCTION version; every version is its own immutable κ-object (text or chat type). The
|
| 4 |
+
// {{variable}} syntax matches LibreChat: {{name}}, {{name:opt1|opt2}} dropdowns, and the
|
| 5 |
+
// special variables {{current_date}} {{current_datetime}} {{iso_datetime}} {{current_user}}.
|
| 6 |
+
|
| 7 |
+
const LC = { lc: "https://librechat.ai/ns#" };
|
| 8 |
+
export const COMMAND_RE = /^[a-z0-9-]{1,56}$/;
|
| 9 |
+
export const VAR_RE = /{{([^{}]+?)}}/g;
|
| 10 |
+
const SPECIAL = ["current_date", "current_datetime", "iso_datetime", "current_user"];
|
| 11 |
+
|
| 12 |
+
export function detectVariables(text) {
|
| 13 |
+
const out = []; const seen = new Set(); let m;
|
| 14 |
+
VAR_RE.lastIndex = 0;
|
| 15 |
+
while ((m = VAR_RE.exec(text || ""))) {
|
| 16 |
+
const raw = m[1].trim();
|
| 17 |
+
const name = raw.split(":")[0].trim();
|
| 18 |
+
if (SPECIAL.includes(name.toLowerCase()) || seen.has(name)) continue;
|
| 19 |
+
seen.add(name);
|
| 20 |
+
const opts = raw.includes(":") ? raw.slice(raw.indexOf(":") + 1).split("|").map((s) => s.trim()).filter(Boolean) : null;
|
| 21 |
+
out.push({ name, raw, options: opts });
|
| 22 |
+
}
|
| 23 |
+
return out;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export function replaceSpecialVars(text, { userName = "operator" } = {}) {
|
| 27 |
+
const d = new Date();
|
| 28 |
+
const pad = (n) => String(n).padStart(2, "0");
|
| 29 |
+
const day = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][d.getDay()];
|
| 30 |
+
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
| 31 |
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
| 32 |
+
const off = -d.getTimezoneOffset(); const tz = (off >= 0 ? "+" : "-") + pad(Math.floor(Math.abs(off) / 60)) + ":" + pad(Math.abs(off) % 60);
|
| 33 |
+
const sub = (t, name, v) => t.replace(new RegExp(`{{\\s*${name}\\s*}}`, "gi"), v);
|
| 34 |
+
let out = text || "";
|
| 35 |
+
out = sub(out, "current_date", `${date} (${day})`);
|
| 36 |
+
out = sub(out, "current_datetime", `${date} ${time} ${tz} (${day})`);
|
| 37 |
+
out = sub(out, "iso_datetime", d.toISOString());
|
| 38 |
+
out = sub(out, "current_user", userName);
|
| 39 |
+
return out;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export function fillVariables(text, values) {
|
| 43 |
+
let out = text || "";
|
| 44 |
+
for (const [raw, v] of Object.entries(values || {})) {
|
| 45 |
+
const esc = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 46 |
+
out = out.replace(new RegExp(`{{\\s*${esc}\\s*}}`, "g"), v);
|
| 47 |
+
}
|
| 48 |
+
return out;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function makePromptsLib(chatStore) {
|
| 52 |
+
const { store, getIndex, newId } = chatStore;
|
| 53 |
+
const putIndex = async (idx) => { const b = new TextEncoder().encode(JSON.stringify(idx)); return store.backend.putRaw ? store.backend.putRaw("index:org.hologram.HoloQ", b) : store.backend.put("index:org.hologram.HoloQ", b); };
|
| 54 |
+
|
| 55 |
+
// Save a version into a group (creates the group when groupId is null); production by default.
|
| 56 |
+
async function save({ groupId, name, category = "", oneliner = "", command = "", prompt, type = "text", production = true }) {
|
| 57 |
+
command = (command || "").toLowerCase();
|
| 58 |
+
if (command && !COMMAND_RE.test(command)) throw new Error("command must be a-z 0-9 dash, ≤56 chars");
|
| 59 |
+
const versionObj = await store.makeObject({
|
| 60 |
+
type: ["schema:CreativeWork", "prov:Entity"], context: [LC],
|
| 61 |
+
"lc:prompt": prompt || "", "lc:type": type === "chat" ? "chat" : "text",
|
| 62 |
+
"schema:dateCreated": new Date().toISOString(),
|
| 63 |
+
});
|
| 64 |
+
const idx = await getIndex();
|
| 65 |
+
idx.promptGroups = idx.promptGroups || [];
|
| 66 |
+
let g = groupId ? idx.promptGroups.find((x) => x.groupId === groupId) : null;
|
| 67 |
+
if (!g) {
|
| 68 |
+
g = { groupId: newId("pgroup"), name: name || "New Prompt", category, oneliner, command, versions: [], productionKappa: null, numberOfGenerations: 0 };
|
| 69 |
+
idx.promptGroups.push(g);
|
| 70 |
+
}
|
| 71 |
+
if (name) g.name = name;
|
| 72 |
+
if (category !== undefined) g.category = category;
|
| 73 |
+
if (oneliner !== undefined) g.oneliner = oneliner;
|
| 74 |
+
if (command !== undefined) g.command = command;
|
| 75 |
+
g.versions.push({ kappa: versionObj.id, createdAt: new Date().toISOString() });
|
| 76 |
+
if (production || !g.productionKappa) g.productionKappa = versionObj.id;
|
| 77 |
+
await putIndex(idx);
|
| 78 |
+
return { ...g };
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
async function list() { return (await getIndex()).promptGroups || []; }
|
| 82 |
+
|
| 83 |
+
async function production(groupId) {
|
| 84 |
+
const g = (await list()).find((x) => x.groupId === groupId); if (!g || !g.productionKappa) return null;
|
| 85 |
+
const obj = await store.getObj(g.productionKappa); if (!obj) return null;
|
| 86 |
+
if (!(await store.verify(obj))) return null; // Law L5
|
| 87 |
+
return { group: g, prompt: obj["lc:prompt"], type: obj["lc:type"], kappa: g.productionKappa };
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async function setProduction(groupId, kappa) {
|
| 91 |
+
const idx = await getIndex();
|
| 92 |
+
const g = (idx.promptGroups || []).find((x) => x.groupId === groupId);
|
| 93 |
+
if (g && g.versions.some((v) => v.kappa === kappa)) { g.productionKappa = kappa; await putIndex(idx); }
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
async function recordUse(groupId) {
|
| 97 |
+
const idx = await getIndex();
|
| 98 |
+
const g = (idx.promptGroups || []).find((x) => x.groupId === groupId);
|
| 99 |
+
if (g) { g.numberOfGenerations = (g.numberOfGenerations || 0) + 1; await putIndex(idx); }
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
async function remove(groupId) {
|
| 103 |
+
const idx = await getIndex();
|
| 104 |
+
idx.promptGroups = (idx.promptGroups || []).filter((x) => x.groupId !== groupId);
|
| 105 |
+
await putIndex(idx);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return { save, list, production, setProduction, recordUse, remove };
|
| 109 |
+
}
|
core/q-brain-fast.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/q-brain-fast.mjs — Q's FAST on-device brain, drop-in for createHoloModelBrain.
|
| 2 |
+
//
|
| 3 |
+
// Same provider shape (load · generate → text-delta async-iterator · chat · info · setSkill), so
|
| 4 |
+
// holo-q-contact.mjs's makeQResponder / makeQGroupResponder bind it with ZERO changes: Q rides the exact
|
| 5 |
+
// stream→finalize pipeline, it just sources its bytes from THIS engine (core/loader + core/engine — the
|
| 6 |
+
// native-ternary κ-object path with the fixed incremental-detok decode) instead of the qwen holo-brain.
|
| 7 |
+
//
|
| 8 |
+
// Why this exists: the messenger's default brain (qwen2.5-0.5b via holo-brain-engine) is heavy and its
|
| 9 |
+
// app-path decode was O(n²). This one loads a native-ternary BitNet κ-object (0.69 GB, verified per-block),
|
| 10 |
+
// decodes at ~70 tok/s warm, and streams byte-identical incremental text. It is 100% on-device at decode
|
| 11 |
+
// time — only a ONE-TIME tokenizer-header Range fetch touches the source host at load (the weights are the
|
| 12 |
+
// local, L5-verified b/<κ>.gz blocks). Bundle the header to make load fully egress-free (follow-up).
|
| 13 |
+
//
|
| 14 |
+
// URL discipline (the one gotcha): core/loader's MODELS use a PAGE-relative kappaUrl ("./models/<name>"),
|
| 15 |
+
// which is wrong from any page other than /apps/q/. loadKappaObject resolves the manifest, blocks AND the
|
| 16 |
+
// bundled tokenizer relative to its baseUrl, so we override kappaUrl to an ABSOLUTE mount ("/apps/q/models/
|
| 17 |
+
// <name>") — that makes the whole load self-contained from the messenger (or anywhere).
|
| 18 |
+
|
| 19 |
+
import { ready, loadModel, MODELS } from "./loader.js";
|
| 20 |
+
import { createEngine } from "./engine.js";
|
| 21 |
+
import { selfFacts, selfPersona, selfIntro } from "./q-self.mjs"; // Q's live, grounded self-knowledge (M0)
|
| 22 |
+
|
| 23 |
+
// The default fast brain = BitNet-2B (native ternary, llama3 template, coherent — Falcon-E degenerates).
|
| 24 |
+
// Override via opts.family / opts.modelName. kappaBase is the absolute mount core/loader's models live under.
|
| 25 |
+
const DEFAULTS = { family: "BitNet", kappaBase: "/apps/q/models", maxTokens: 512 };
|
| 26 |
+
|
| 27 |
+
function pickModel(cfg) {
|
| 28 |
+
const want = String(cfg.modelName || cfg.family || "BitNet").toLowerCase();
|
| 29 |
+
const m = MODELS.find((x) => (x.fam || "").toLowerCase() === want || (x.name || "").toLowerCase().includes(want)) || MODELS.find((x) => (x.fam || "").toLowerCase() === "bitnet");
|
| 30 |
+
if (!m) throw new Error("q-brain-fast: no model matches " + want);
|
| 31 |
+
// An ABSOLUTE host URL (the κ-object now lives on HF, not on this machine) is already page-independent —
|
| 32 |
+
// use it verbatim. Only a page-relative "./models/<name>" needs absolute-mounting so it resolves from ANY page.
|
| 33 |
+
if (/^https?:\/\//.test(m.kappaUrl || "")) return { ...m };
|
| 34 |
+
const rel = String(m.kappaUrl || "").replace(/^\.?\//, "").replace(/^models\//, ""); // "./models/bitnet-2b" → "bitnet-2b"
|
| 35 |
+
return { ...m, kappaUrl: String(cfg.kappaBase).replace(/\/+$/, "") + "/" + rel };
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Render an [{role,content}] history (system + turns, as makeQResponder builds it) to the model's chat
|
| 39 |
+
// template — the multi-turn generalization of core/engine's single-turn frameTurn. Covers the templates
|
| 40 |
+
// the engine knows; unknown families fall back to a persona-led last-user Q/A frame.
|
| 41 |
+
function frameHistory(M, history) {
|
| 42 |
+
const list = Array.isArray(history) ? history : [];
|
| 43 |
+
// merge ALL system turns into one system block (persona + any injected context, e.g. M1 grounded retrieval) —
|
| 44 |
+
// taking only the first would silently DROP injected context and the model would fall back to a generic refusal.
|
| 45 |
+
const persona = list.filter((x) => x && x.role === "system" && x.content).map((x) => x.content).join("\n\n");
|
| 46 |
+
const turns = list.filter((x) => x && x.role !== "system" && (x.content || "").length);
|
| 47 |
+
|
| 48 |
+
if (M.llama3) {
|
| 49 |
+
let s = persona ? `<|start_header_id|>system<|end_header_id|>\n\n${persona}<|eot_id|>` : "";
|
| 50 |
+
for (const t of turns) s += `<|start_header_id|>${t.role === "assistant" ? "assistant" : "user"}<|end_header_id|>\n\n${t.content}<|eot_id|>`;
|
| 51 |
+
return s + `<|start_header_id|>assistant<|end_header_id|>\n\n`;
|
| 52 |
+
}
|
| 53 |
+
if (M.qwen) {
|
| 54 |
+
const noThink = M.qwen3 ? "<think>\n\n</think>\n\n" : "";
|
| 55 |
+
let s = persona ? `<|im_start|>system\n${persona}<|im_end|>\n` : "";
|
| 56 |
+
for (const t of turns) s += `<|im_start|>${t.role === "assistant" ? "assistant" : "user"}\n${t.content}<|im_end|>\n`;
|
| 57 |
+
return s + `<|im_start|>assistant\n` + noThink;
|
| 58 |
+
}
|
| 59 |
+
if (M.olmo) {
|
| 60 |
+
let s = persona ? `<|system|>\n${persona}\n` : "";
|
| 61 |
+
for (const t of turns) s += t.role === "assistant" ? `<|assistant|>\n${t.content}\n` : `<|user|>\n${t.content}\n`;
|
| 62 |
+
return s + `<|assistant|>\n`;
|
| 63 |
+
}
|
| 64 |
+
// word-frame / plain: only the last user turn carries (base models have no multi-turn template)
|
| 65 |
+
const lastUser = [...turns].reverse().find((t) => t.role !== "assistant");
|
| 66 |
+
const q = (lastUser && lastUser.content) || "";
|
| 67 |
+
if (M.userWord) return (persona ? persona + "\n" : "") + "User: " + q + "\nFalcon:";
|
| 68 |
+
return (persona ? persona + "\n" : "") + "Question: " + q + "\nAnswer:";
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export function createFastQBrain(opts = {}) {
|
| 72 |
+
const cfg = Object.assign({}, DEFAULTS, opts);
|
| 73 |
+
const M = pickModel(cfg);
|
| 74 |
+
let engine = null, loadingP = null;
|
| 75 |
+
let info = { ready: false, model: M.name, device: null, resident: false };
|
| 76 |
+
|
| 77 |
+
// encode a history to the running token ids (framed + optional bos), ready for engine.generate
|
| 78 |
+
function idsFor(history) {
|
| 79 |
+
let ids = engine.tokenize(frameHistory(M, history));
|
| 80 |
+
if (M.bos && engine.bosId != null) ids = [engine.bosId, ...ids];
|
| 81 |
+
return ids;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async function load(onProgress) {
|
| 85 |
+
if (engine) return info;
|
| 86 |
+
if (loadingP) return loadingP;
|
| 87 |
+
loadingP = (async () => {
|
| 88 |
+
if (!(typeof navigator !== "undefined" && navigator.gpu)) throw new Error("no WebGPU on this device");
|
| 89 |
+
await ready(); // wasm tokenizer init (shared instance)
|
| 90 |
+
const loaded = await loadModel(M, {
|
| 91 |
+
onStatus: () => {},
|
| 92 |
+
onProgress: (d, t, w) => { try { onProgress && onProgress({ done: d, total: t, phase: w, model: M.name }); } catch (e) {} },
|
| 93 |
+
});
|
| 94 |
+
if (!loaded || !loaded.gpu) throw new Error("q-brain-fast: model load failed (" + M.name + ")");
|
| 95 |
+
engine = await createEngine(M, loaded);
|
| 96 |
+
// MEASURED from the real engine, not asserted: device/resident reflect an engine that actually uploaded
|
| 97 |
+
// weights to the GPU (dims + gpuBytes are live signals) — so info() can never claim residency it lacks.
|
| 98 |
+
info = { ready: true, model: M.name, device: (engine && engine.dims ? "webgpu" : null), resident: !!(engine && (engine.gpuBytes || engine.dims)) };
|
| 99 |
+
return info;
|
| 100 |
+
})().catch((e) => { loadingP = null; throw e; });
|
| 101 |
+
return loadingP;
|
| 102 |
+
}
|
| 103 |
+
async function ensure(onProgress) { if (!engine) await load(onProgress); return engine; }
|
| 104 |
+
|
| 105 |
+
// generate(history, { signal, onProgress }) → async-iterator of TEXT DELTAS (exactly what makeQResponder
|
| 106 |
+
// accumulates + paints via onDelta). engine.generate reports CUMULATIVE text per step; we diff to deltas
|
| 107 |
+
// and pump them through a small queue so this stays a clean generator (and honors the abort signal).
|
| 108 |
+
async function* generate(history, o = {}) {
|
| 109 |
+
await ensure(o.onProgress);
|
| 110 |
+
if (!engine) return;
|
| 111 |
+
const signal = o.signal || null;
|
| 112 |
+
const ids = idsFor(history);
|
| 113 |
+
const cap = o.maxTokens || cfg.maxTokens || M.cap || 256;
|
| 114 |
+
|
| 115 |
+
const queue = []; let done = false, wake = null, prev = "";
|
| 116 |
+
const kick = () => { if (wake) { const w = wake; wake = null; w(); } };
|
| 117 |
+
const run = engine.generate(ids, {
|
| 118 |
+
maxNew: cap, signal,
|
| 119 |
+
onToken: ({ text }) => { const d = (text || "").slice(prev.length); if (d) { prev = text; queue.push(d); kick(); } },
|
| 120 |
+
}).then(() => { done = true; kick(); }).catch(() => { done = true; kick(); });
|
| 121 |
+
|
| 122 |
+
while (true) {
|
| 123 |
+
if (queue.length) { yield queue.shift(); continue; }
|
| 124 |
+
if (done) break;
|
| 125 |
+
if (signal && signal.aborted) break;
|
| 126 |
+
await new Promise((r) => (wake = r));
|
| 127 |
+
}
|
| 128 |
+
try { await run; } catch (e) {}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// chat(history, opts) → full string (used by window.HoloQ.generate + light background features)
|
| 132 |
+
async function chat(history, o = {}) {
|
| 133 |
+
await ensure(o.onProgress);
|
| 134 |
+
if (!engine) return "";
|
| 135 |
+
const res = await engine.generate(idsFor(history), { maxNew: o.maxTokens || cfg.maxTokens || M.cap || 256, signal: o.signal || null });
|
| 136 |
+
return ((res && res.text) || "").trim();
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// native-ternary κ-object → no LoRA adapters; skill routing is a no-op (the base is the specialist).
|
| 140 |
+
const setSkill = async () => ({ adapter: false, unsupported: true });
|
| 141 |
+
const setAdapter = async () => ({ adapter: false, unsupported: true });
|
| 142 |
+
|
| 143 |
+
// Q's LIVE self-knowledge (M0), derived from THIS instance's real model + engine κ — the grounded truth
|
| 144 |
+
// every surface leads with so Q is honestly self-aware and never confabulates a cloud identity. Available
|
| 145 |
+
// even before load (identity is in M); the κ fills in once the engine is resident.
|
| 146 |
+
const facts = () => selfFacts({ model: M, engine });
|
| 147 |
+
const persona = () => selfPersona({ model: M, engine });
|
| 148 |
+
const intro = () => selfIntro({ model: M, engine });
|
| 149 |
+
|
| 150 |
+
return { id: "q-brain-fast-" + (M.fam || M.name), load, generate, chat, setSkill, setAdapter, info: () => info, facts, persona, intro };
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
export default createFastQBrain;
|
core/q-self.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/q-self.mjs — Q's LIVE, GROUNDED self-knowledge. The single source of truth for who/what/where/how Q
|
| 2 |
+
// is, DERIVED FROM REAL RUNTIME SIGNALS (the loaded model, its content-addressed κ, the device, optional
|
| 3 |
+
// system health) — never hardcoded, never guessed. Every Q surface (messenger, q-chat, voice) leads with
|
| 4 |
+
// this, so Q is truthfully self-aware EVERYWHERE and can never confabulate a cloud/OpenAI/AWS identity.
|
| 5 |
+
//
|
| 6 |
+
// Why this exists (the law: grounded transcendence, never performed): a base model has NO self-knowledge —
|
| 7 |
+
// asked "what are you" it returns the average of its training data ("I run on AWS/OpenAI"), which is false.
|
| 8 |
+
// So Q's identity is not a persona we write; it is the TRUTH of this running instance, computed here. If a
|
| 9 |
+
// fact isn't really knowable in this context, we OMIT it — we never invent one.
|
| 10 |
+
//
|
| 11 |
+
// DOM-free + fully guarded: safe to import in any surface (standalone app, messenger, OS shell, Node witness).
|
| 12 |
+
|
| 13 |
+
// Structured self-facts from whatever is really knowable HERE. Fail-soft: a missing signal is omitted.
|
| 14 |
+
export function selfFacts({ model, engine } = {}) {
|
| 15 |
+
const f = { name: "Q" };
|
| 16 |
+
try { if (model) { if (model.name) f.model = model.name; if (model.fam) f.family = model.fam; if (model.fmt) f.quant = model.fmt; if (model.size) f.size = model.size; } } catch (e) {}
|
| 17 |
+
try { if (engine && engine.modelKappa) f.kappa = engine.modelKappa; } catch (e) {}
|
| 18 |
+
try { f.gpu = (typeof navigator !== "undefined" && !!navigator.gpu); } catch (e) {}
|
| 19 |
+
// MEASURED, never asserted: Q is "resident" only when the engine has really uploaded weights to the GPU
|
| 20 |
+
// (a live signal — gpuBytes/dims), not a hardcoded true. Before load it is false, and stays false.
|
| 21 |
+
try { f.resident = !!(engine && (engine.gpuBytes || engine.dims)); } catch (e) { f.resident = false; }
|
| 22 |
+
// DERIVED from real signals — runs-on-device when actually resident, or at least GPU-capable before load.
|
| 23 |
+
f.runsOnDevice = !!(f.resident || f.gpu);
|
| 24 |
+
// "no server / no egress" is a property of the LOCAL engine, so we assert it ONLY when a real local engine or
|
| 25 |
+
// GPU is visible. Decode is in-browser (egress none); the one network touch is the weights fetch AT LOAD, so we
|
| 26 |
+
// say that plainly instead of a flat "nothing ever touches the network".
|
| 27 |
+
if (f.resident || f.gpu) { f.server = false; f.egress = f.resident ? "none at inference (weights fetched once at load)" : "none at inference"; }
|
| 28 |
+
// Provenance — where Q was loaded FROM (the thing the operator cares about): the page origin + the weights host.
|
| 29 |
+
// Omitted when not knowable. This is what lets Q answer "where did you come from" truthfully.
|
| 30 |
+
try { if (typeof location !== "undefined" && location.origin && location.origin !== "null") f.loadedFrom = location.origin; } catch (e) {}
|
| 31 |
+
try { if (model && model.kappaUrl) { const u = new URL(model.kappaUrl, (typeof location !== "undefined" ? location.href : "https://local/")); f.weightsFrom = u.host || u.protocol.replace(":", ""); } } catch (e) {}
|
| 32 |
+
// optional live system health — present only when the OS shell exposes it (absent in standalone apps).
|
| 33 |
+
try { const h = (typeof window !== "undefined") && window.HoloSysHealth && window.HoloSysHealth.summary && window.HoloSysHealth.summary(); if (h) f.health = h; } catch (e) {}
|
| 34 |
+
try { if (!f.health && typeof window !== "undefined" && window.Q && window.Q.health) { const q = window.Q.health(); if (q) f.health = q; } } catch (e) {}
|
| 35 |
+
return f;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// The grounded SYSTEM persona every Q surface leads with. Warm + honest + specific + anti-confabulation,
|
| 39 |
+
// composed from the live facts so Q states ONLY what is true of THIS instance. Kept concise — a small model
|
| 40 |
+
// follows a short, sharp system turn far better than a long one.
|
| 41 |
+
export function selfPersona(opts = {}) {
|
| 42 |
+
const f = selfFacts(opts);
|
| 43 |
+
const s = [];
|
| 44 |
+
s.push(`You are Q — a private AI that runs on the user's own device, in their web browser${f.gpu ? ", on their GPU" : ""}.`);
|
| 45 |
+
if (f.model) s.push(`You are the ${f.model} model${f.quant ? ` (${f.quant})` : ""}${f.resident ? ", resident locally right now" : ""}${f.kappa ? "; your weights are content-addressed and verified by re-derivation, so no host has to be trusted" : ""}.`);
|
| 46 |
+
if (f.server === false) s.push(`You decode locally — there is no inference server and no cloud, and nothing the user types ever leaves their device.${f.resident ? " Your weights were fetched once at load and verified; after that, inference is fully on-device." : ""}`);
|
| 47 |
+
s.push("You are warm, concise, honest, and genuinely present. You seek clarity and truth; you say less, better; you never overwhelm.");
|
| 48 |
+
s.push("If asked what or where you are, answer truthfully from the above: you run locally, on their device. Never claim to run on a server, the cloud, OpenAI, ChatGPT, GPT-4, Gemini, or AWS — those are false. If you don't know something, say so plainly rather than inventing it.");
|
| 49 |
+
return s.join(" ");
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// A short, human, first-person self-summary (for a proactive greeting or a direct "who are you"). Grounded.
|
| 53 |
+
export function selfIntro(opts = {}) {
|
| 54 |
+
const f = selfFacts(opts);
|
| 55 |
+
const bits = [`I'm Q — ${f.resident ? "running right here on your device" : "running on your device"}${f.gpu ? ", on your GPU" : ""}${f.model ? ` (the ${f.model} model)` : ""}.`];
|
| 56 |
+
if (f.server === false) bits.push(`No server, no cloud; nothing you say leaves this machine${f.kappa ? ", and my weights are verified by re-derivation" : ""}.`);
|
| 57 |
+
else if (f.kappa) bits.push("My weights are verified by re-derivation.");
|
| 58 |
+
bits.push("What's on your mind?");
|
| 59 |
+
return bits.join(" ");
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export default { selfFacts, selfPersona, selfIntro };
|
core/schema.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/schema.js — lean, dependency-free validators ("zod-lite") for the LibreChat-faithful
|
| 2 |
+
// Conversation / Message / Preset shapes. Field names mirror LibreChat's data model (packages/
|
| 3 |
+
// data-schemas) so an exported conversation interoperates; values are validated/normalised, not
|
| 4 |
+
// hashed (the κ-object store is what makes them content-addressed). No build step, no dep.
|
| 5 |
+
|
| 6 |
+
const isStr = (v) => typeof v === "string";
|
| 7 |
+
const isBool = (v) => typeof v === "boolean";
|
| 8 |
+
const isNum = (v) => typeof v === "number" && !Number.isNaN(v);
|
| 9 |
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
| 10 |
+
const clampNum = (v, lo, hi, d) => (isNum(v) ? Math.min(hi, Math.max(lo, v)) : d);
|
| 11 |
+
|
| 12 |
+
// A chat message (one node in the parentMessageId tree).
|
| 13 |
+
export function normMessage(m = {}) {
|
| 14 |
+
return {
|
| 15 |
+
messageId: isStr(m.messageId) ? m.messageId : null,
|
| 16 |
+
conversationId: isStr(m.conversationId) ? m.conversationId : null,
|
| 17 |
+
parentMessageId: isStr(m.parentMessageId) ? m.parentMessageId : null, // tree edge
|
| 18 |
+
sender: isStr(m.sender) ? m.sender : (m.isCreatedByUser ? "User" : "Assistant"),
|
| 19 |
+
isCreatedByUser: isBool(m.isCreatedByUser) ? m.isCreatedByUser : false,
|
| 20 |
+
model: isStr(m.model) ? m.model : null,
|
| 21 |
+
text: isStr(m.text) ? m.text : "",
|
| 22 |
+
content: arr(m.content), // structured parts (text/tool/image/reasoning)
|
| 23 |
+
tokenCount: isNum(m.tokenCount) ? m.tokenCount : null,
|
| 24 |
+
error: m.error || null,
|
| 25 |
+
unfinished: !!m.unfinished,
|
| 26 |
+
feedback: m.feedback && isStr(m.feedback.rating || "") ? { rating: m.feedback.rating, tag: m.feedback.tag || null, text: m.feedback.text || "" } : (m.feedback || null),
|
| 27 |
+
files: arr(m.files),
|
| 28 |
+
createdAt: isStr(m.createdAt) ? m.createdAt : null,
|
| 29 |
+
receiptKappa: isStr(m.receiptKappa) ? m.receiptKappa : null, // links the PROV-O inference receipt
|
| 30 |
+
};
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// A conversation (spreads the active preset, like LibreChat's conversationPreset).
|
| 34 |
+
export function normConversation(c = {}) {
|
| 35 |
+
return {
|
| 36 |
+
conversationId: isStr(c.conversationId) ? c.conversationId : null,
|
| 37 |
+
title: isStr(c.title) ? c.title : "New Chat",
|
| 38 |
+
tags: arr(c.tags),
|
| 39 |
+
files: arr(c.files),
|
| 40 |
+
folder: isStr(c.folder) ? c.folder : null,
|
| 41 |
+
archived: !!c.archived,
|
| 42 |
+
favorite: !!c.favorite,
|
| 43 |
+
isTemporary: !!c.isTemporary,
|
| 44 |
+
preset: c.preset ? normPreset(c.preset) : null,
|
| 45 |
+
createdAt: isStr(c.createdAt) ? c.createdAt : null,
|
| 46 |
+
updatedAt: isStr(c.updatedAt) ? c.updatedAt : null,
|
| 47 |
+
};
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// A preset / parameter bundle. Bounds mirror common LLM ranges; model is the local κ-object id.
|
| 51 |
+
export function normPreset(p = {}) {
|
| 52 |
+
return {
|
| 53 |
+
presetId: isStr(p.presetId) ? p.presetId : null,
|
| 54 |
+
title: isStr(p.title) ? p.title : "Preset",
|
| 55 |
+
model: isStr(p.model) ? p.model : null,
|
| 56 |
+
temperature: clampNum(p.temperature, 0, 2, 0),
|
| 57 |
+
top_p: clampNum(p.top_p, 0, 1, 1),
|
| 58 |
+
top_k: clampNum(p.top_k, 0, 200, 40),
|
| 59 |
+
maxOutputTokens: clampNum(p.maxOutputTokens, 1, 8192, 1024),
|
| 60 |
+
promptPrefix: isStr(p.promptPrefix) ? p.promptPrefix : "",
|
| 61 |
+
presence_penalty: clampNum(p.presence_penalty, -2, 2, 0),
|
| 62 |
+
frequency_penalty: clampNum(p.frequency_penalty, -2, 2, 0),
|
| 63 |
+
stop: arr(p.stop),
|
| 64 |
+
iconURL: isStr(p.iconURL) ? p.iconURL : null,
|
| 65 |
+
isArchived: !!p.isArchived,
|
| 66 |
+
};
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Walk a flat message list into the LibreChat parentMessageId tree → {roots, byId, childrenOf}.
|
| 70 |
+
export function buildTree(messages) {
|
| 71 |
+
const byId = new Map(); const childrenOf = new Map(); const roots = [];
|
| 72 |
+
for (const m of messages) { byId.set(m.messageId, m); childrenOf.set(m.messageId, []); }
|
| 73 |
+
for (const m of messages) {
|
| 74 |
+
if (m.parentMessageId && childrenOf.has(m.parentMessageId)) childrenOf.get(m.parentMessageId).push(m.messageId);
|
| 75 |
+
else roots.push(m.messageId);
|
| 76 |
+
}
|
| 77 |
+
return { roots, byId, childrenOf };
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// The active thread = follow the chosen child at each branch from a root to a leaf.
|
| 81 |
+
export function threadOf(tree, { choose = (siblings) => siblings[siblings.length - 1] } = {}) {
|
| 82 |
+
const out = []; let cur = tree.roots.length ? choose(tree.roots) : null;
|
| 83 |
+
while (cur) { out.push(tree.byId.get(cur)); const kids = tree.childrenOf.get(cur) || []; cur = kids.length ? choose(kids) : null; }
|
| 84 |
+
return out;
|
| 85 |
+
}
|
core/semantic-witness.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/semantic-witness.mjs — the W3C SEMANTIC-TYPING gate (C2, extended to all κ-objects).
|
| 2 |
+
// Proves EVERY substrate object kind yields a node that is simultaneously: content-addressed (Law L1,
|
| 3 |
+
// @id = did:holo), schema.org-typed (WHAT it is), and PROV-O-bearing (HOW it came to be) — i.e. legible
|
| 4 |
+
// to any W3C JSON-LD/RDF tool. Pure + node-runnable: `node core/semantic-witness.mjs`. Exit 1 on any fail.
|
| 5 |
+
|
| 6 |
+
import { linkedDataFor, skillAsHowTo, fileAsDocument, appAsSoftware, modelAsSource, asLinkedData, verifySemantic, HOLO_CONTEXT } from "./semantic.js";
|
| 7 |
+
|
| 8 |
+
const K = (h) => "did:holo:sha256:" + h.padEnd(64, "0").slice(0, 64);
|
| 9 |
+
const cases = [
|
| 10 |
+
{ kind: "skill", node: skillAsHowTo({ name: "Format JSON", description: "When asked to pretty-print JSON", instructions: "1. Parse the text\n2. Re-serialize with 2-space indent", kappa: K("a1"), prov: [{ v: 1, kappa: K("a0") }, { v: 2, kappa: K("a1") }] }), want: "schema:HowTo" },
|
| 11 |
+
{ kind: "file", node: fileAsDocument({ path: "/src/main.py", kappa: K("b1"), bytes: 412 }), want: "schema:DigitalDocument" },
|
| 12 |
+
{ kind: "app", node: appAsSoftware({ name: "counter.html", kappa: K("c1"), bytes: 980 }), want: "schema:SoftwareApplication" },
|
| 13 |
+
{ kind: "model", node: modelAsSource({ name: "BitNet-2B-4T", family: "BitNet", params: "0.69 GB", format: "t2 1.58-bit κ", kappa: K("19e16e6d") }), want: "schema:SoftwareSourceCode" },
|
| 14 |
+
{ kind: "conversation", node: linkedDataFor("conversation", { kappa: K("d1"), props: { "schema:name": "chat #1" } }), want: "schema:Conversation" },
|
| 15 |
+
{ kind: "receipt", node: asLinkedData({ kind: "receipt", kappa: K("e1"), props: { "schema:name": "tool-call receipt" } }), want: "schema:CreativeWork" },
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
let pass = 0;
|
| 19 |
+
console.log("W3C semantic-typing gate — every κ-object is schema.org-typed + PROV-O + content-addressed\n");
|
| 20 |
+
for (const c of cases) {
|
| 21 |
+
const v = verifySemantic(c.node);
|
| 22 |
+
const types = [].concat(c.node["@type"]);
|
| 23 |
+
const hasWant = types.includes(c.want);
|
| 24 |
+
const ctxOk = c.node["@context"] === HOLO_CONTEXT || !!c.node["@context"];
|
| 25 |
+
// JSON round-trip must be lossless (it's just data)
|
| 26 |
+
let rt = false; try { rt = JSON.stringify(JSON.parse(JSON.stringify(c.node))) === JSON.stringify(c.node); } catch {}
|
| 27 |
+
const ok = v.ok && hasWant && ctxOk && rt;
|
| 28 |
+
if (ok) pass++;
|
| 29 |
+
console.log(`${ok ? "✓" : "✗"} ${c.kind.padEnd(13)} @id=${String(c.node["@id"]).slice(0, 24)}… @type=[${types.join(", ")}] schema:${v.hasSchema?"y":"n"} prov:${v.hasProv?"y":"n"} id:${v.hasId?"y":"n"} ctx:${ctxOk?"y":"n"} rt:${rt?"y":"n"}`);
|
| 30 |
+
}
|
| 31 |
+
const all = pass === cases.length;
|
| 32 |
+
console.log(`\n${all ? "PASS" : "FAIL"} — ${pass}/${cases.length} κ-object kinds carry a valid W3C @type`);
|
| 33 |
+
process.exit(all ? 0 : 1);
|
core/semantic.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/semantic.js — the SEMANTIC SKIN (gap C2): every κ-object the substrate produces carries an
|
| 2 |
+
// open-semantic-web type, so humans, agents, and W3C validators all read it the same way. Closes the
|
| 3 |
+
// weakest audit axis (κ names objects, but few declared a W3C @type). Two W3C standards, both native:
|
| 4 |
+
// • JSON-LD / schema.org — WHAT a thing is (a skill is a schema:HowTo; a file is a schema:DigitalDocument)
|
| 5 |
+
// • PROV-O — HOW it came to be (the κ-chain becomes prov:wasRevisionOf links)
|
| 6 |
+
// Pure, dependency-free, browser+node. The result round-trips through any JSON-LD/RDF tool.
|
| 7 |
+
|
| 8 |
+
export const HOLO_CONTEXT = {
|
| 9 |
+
schema: "https://schema.org/",
|
| 10 |
+
prov: "http://www.w3.org/ns/prov#",
|
| 11 |
+
holo: "https://hologram.foundation/ns/",
|
| 12 |
+
"@vocab": "https://schema.org/",
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
// schema.org type for a substrate object kind (the canonical mapping, L2 — fixed at the ingest boundary)
|
| 16 |
+
const TYPE_FOR = {
|
| 17 |
+
skill: "HowTo", // a reusable procedure = schema:HowTo (steps + when-to-use)
|
| 18 |
+
file: "DigitalDocument",
|
| 19 |
+
app: "SoftwareApplication",
|
| 20 |
+
model: "SoftwareSourceCode",
|
| 21 |
+
conversation: "Conversation",
|
| 22 |
+
receipt: "CreativeWork", // a sealed work record: schema:CreativeWork that is also a prov:Entity
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
// wrap any object as a typed, content-addressed JSON-LD node. `kappa` is its did:holo (the @id —
|
| 26 |
+
// content IS the identity, Law L1). `prov` is the κ-chain → prov:wasRevisionOf links (PROV-O).
|
| 27 |
+
export function asLinkedData({ kind, kappa, props = {}, prov = [] }) {
|
| 28 |
+
const t = TYPE_FOR[kind] || "Thing";
|
| 29 |
+
const node = {
|
| 30 |
+
"@context": HOLO_CONTEXT,
|
| 31 |
+
"@id": kappa, // content-derived identity (no location — Law L1)
|
| 32 |
+
"@type": Array.isArray(t) ? t : ["schema:" + t, "prov:Entity"], // schema kind + PROV entity
|
| 33 |
+
...props,
|
| 34 |
+
};
|
| 35 |
+
if (prov.length >= 2) { // the version chain → PROV-O revision links
|
| 36 |
+
const cur = prov[prov.length - 1], parent = prov[prov.length - 2];
|
| 37 |
+
node["prov:wasRevisionOf"] = { "@id": parent.kappa };
|
| 38 |
+
node["holo:version"] = cur.v;
|
| 39 |
+
}
|
| 40 |
+
node["prov:wasDerivedFrom"] = prov.length ? prov.map((p) => ({ "@id": p.kappa })) : undefined;
|
| 41 |
+
return node;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// a skill → schema:HowTo (steps become schema:HowToStep), with its PROV-O revision chain.
|
| 45 |
+
export function skillAsHowTo({ name, description, instructions, kappa, prov = [] }) {
|
| 46 |
+
const steps = String(instructions || "").split("\n").map((s) => s.trim()).filter(Boolean)
|
| 47 |
+
.map((text, i) => ({ "@type": "schema:HowToStep", "schema:position": i + 1, "schema:text": text.replace(/^\d+[.)]\s*/, "") }));
|
| 48 |
+
return asLinkedData({ kind: "skill", kappa, prov, props: { "schema:name": name, "schema:description": description, "schema:step": steps } });
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// extension → IANA media type (schema:encodingFormat). Small, common set; default text/plain.
|
| 52 |
+
const MEDIA = { html: "text/html", htm: "text/html", js: "text/javascript", mjs: "text/javascript", json: "application/json", jsonld: "application/ld+json", css: "text/css", md: "text/markdown", txt: "text/plain", py: "text/x-python", svg: "image/svg+xml", wasm: "application/wasm", gz: "application/gzip" };
|
| 53 |
+
const mediaOf = (name) => MEDIA[String(name).toLowerCase().split(".").pop()] || "text/plain";
|
| 54 |
+
|
| 55 |
+
// a workspace file → schema:DigitalDocument (name + media type + byte size), content-addressed (L1).
|
| 56 |
+
export function fileAsDocument({ path, kappa, bytes = 0, mediaType, prov = [] }) {
|
| 57 |
+
const name = String(path).split("/").filter(Boolean).pop() || String(path);
|
| 58 |
+
return asLinkedData({ kind: "file", kappa, prov, props: {
|
| 59 |
+
"schema:name": name, "schema:identifier": kappa,
|
| 60 |
+
"schema:encodingFormat": mediaType || mediaOf(name), "schema:contentSize": bytes } });
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// a built app → schema:SoftwareApplication (a self-contained WebGPU/Web app object).
|
| 64 |
+
export function appAsSoftware({ name, kappa, bytes = 0, prov = [] }) {
|
| 65 |
+
return asLinkedData({ kind: "app", kappa, prov, props: {
|
| 66 |
+
"schema:name": String(name).replace(/\.html$/i, ""), "schema:identifier": kappa,
|
| 67 |
+
"schema:applicationCategory": "WebApplication", "schema:operatingSystem": "Any (WebGPU/Web)",
|
| 68 |
+
"schema:fileSize": bytes } });
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// a loaded model → schema:SoftwareSourceCode (the κ-object that runs on the GPU).
|
| 72 |
+
export function modelAsSource({ name, kappa, family, params, format, prov = [] }) {
|
| 73 |
+
return asLinkedData({ kind: "model", kappa, prov, props: {
|
| 74 |
+
"schema:name": name, "schema:identifier": kappa, "schema:programmingLanguage": "WGSL/WebGPU",
|
| 75 |
+
"holo:family": family, "holo:parameters": params, "holo:format": format } });
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// one dispatcher so any producer can type any κ-object: linkedDataFor("app", {...}) etc.
|
| 79 |
+
export function linkedDataFor(kind, props = {}) {
|
| 80 |
+
switch (kind) {
|
| 81 |
+
case "skill": return skillAsHowTo(props);
|
| 82 |
+
case "file": return fileAsDocument(props);
|
| 83 |
+
case "app": return appAsSoftware(props);
|
| 84 |
+
case "model": return modelAsSource(props);
|
| 85 |
+
default: return asLinkedData({ kind, kappa: props.kappa, prov: props.prov || [], props: props.props || props });
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// VERIFY (the gate): an object is semantically valid iff it has @context, an @id (κ), and a @type
|
| 90 |
+
// carrying both a schema.org kind and PROV-O lineage. Returns { ok, types, hasId, hasContext }.
|
| 91 |
+
export function verifySemantic(node) {
|
| 92 |
+
const types = [].concat(node && node["@type"] || []);
|
| 93 |
+
const hasSchema = types.some((t) => /^schema:|^https:\/\/schema\.org\//.test(t));
|
| 94 |
+
const hasProv = types.some((t) => /^prov:/.test(t)) || !!node["prov:wasDerivedFrom"];
|
| 95 |
+
const hasId = typeof node?.["@id"] === "string" && node["@id"].startsWith("did:holo:");
|
| 96 |
+
const hasContext = !!node?.["@context"];
|
| 97 |
+
return { ok: hasSchema && hasId && hasContext, hasSchema, hasProv, hasId, hasContext, types };
|
| 98 |
+
}
|
core/skills.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/skills.js — SELF-EVOLVING AGENT SKILLS, 100% agentskills.io-compatible, enhanced by the UOR
|
| 2 |
+
// substrate. A skill is a folder with SKILL.md (name + description frontmatter + instructions) —
|
| 3 |
+
// the open Anthropic/agentskills.io format, portable to any skills-compatible agent. The Hermes
|
| 4 |
+
// idea (the agent autonomously CREATES and IMPROVES skills from experience) is realized as the
|
| 5 |
+
// agent's own tools. The substrate enhancement: every skill version is content-addressed (κ) and
|
| 6 |
+
// chained — a tamper-proof PROVENANCE of how the skill evolved (Holo Prov). Skills live in OPFS
|
| 7 |
+
// "/skills/<name>/SKILL.md"; the chain log in "/skills/<name>/.prov" (one κ per version).
|
| 8 |
+
//
|
| 9 |
+
// Progressive disclosure (the spec): DISCOVERY (names+descriptions injected at session start) →
|
| 10 |
+
// ACTIVATION (read_skill loads full SKILL.md) → EXECUTION (the agent follows it, using file tools).
|
| 11 |
+
|
| 12 |
+
import { skillAsHowTo, verifySemantic } from "./semantic.js";
|
| 13 |
+
|
| 14 |
+
const te = new TextEncoder(), td = new TextDecoder();
|
| 15 |
+
const sha = async (s) => "did:holo:sha256:" + [...new Uint8Array(await crypto.subtle.digest("SHA-256", te.encode(s)))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 16 |
+
const slug = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "skill";
|
| 17 |
+
|
| 18 |
+
async function skillsRoot() { const d = await navigator.storage.getDirectory(); const a = await d.getDirectoryHandle("skills", { create: true }); return a; }
|
| 19 |
+
async function skillDir(name, create = false) { return (await skillsRoot()).getDirectoryHandle(slug(name), { create }); }
|
| 20 |
+
async function readText(dir, file) { const fh = await dir.getFileHandle(file); return td.decode(await (await fh.getFile()).arrayBuffer()); }
|
| 21 |
+
async function writeText(dir, file, text) { const fh = await dir.getFileHandle(file, { create: true }); const w = await fh.createWritable(); await w.write(te.encode(text)); await w.close(); }
|
| 22 |
+
|
| 23 |
+
// SKILL.md = YAML-ish frontmatter (name, description) + markdown instructions (verbatim spec form).
|
| 24 |
+
function buildSkillMd({ name, description, instructions }) {
|
| 25 |
+
return `---\nname: ${name}\ndescription: ${description}\n---\n\n${instructions || ""}\n`;
|
| 26 |
+
}
|
| 27 |
+
function parseSkillMd(text) {
|
| 28 |
+
const m = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
| 29 |
+
const meta = {}; if (m) for (const line of m[1].split("\n")) { const i = line.indexOf(":"); if (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim(); }
|
| 30 |
+
return { name: meta.name || "", description: meta.description || "", instructions: m ? m[2].trim() : text.trim() };
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export async function listSkills() {
|
| 34 |
+
const out = [];
|
| 35 |
+
try { const root = await skillsRoot();
|
| 36 |
+
for await (const [n, h] of root.entries()) { if (h.kind !== "directory") continue;
|
| 37 |
+
try { const sk = parseSkillMd(await readText(h, "SKILL.md")); out.push({ slug: n, name: sk.name || n, description: sk.description }); } catch {} } }
|
| 38 |
+
catch {}
|
| 39 |
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export async function readSkill(name) { const dir = await skillDir(name); return parseSkillMd(await readText(dir, "SKILL.md")); }
|
| 43 |
+
|
| 44 |
+
// save = create OR improve (self-evolution). Appends a κ to the provenance chain — every version
|
| 45 |
+
// sealed + chained to its parent, so the skill's evolution is verifiable (Holo Prov shape).
|
| 46 |
+
export async function saveSkill({ name, description, instructions }) {
|
| 47 |
+
const dir = await skillDir(name, true);
|
| 48 |
+
const md = buildSkillMd({ name, description, instructions });
|
| 49 |
+
const kappa = await sha(md);
|
| 50 |
+
let prov = []; try { prov = JSON.parse(await readText(dir, ".prov")); } catch {}
|
| 51 |
+
const parent = prov.length ? prov[prov.length - 1].kappa : null;
|
| 52 |
+
prov.push({ v: prov.length + 1, kappa, parent, descKappa: await sha(description || "") });
|
| 53 |
+
await writeText(dir, "SKILL.md", md);
|
| 54 |
+
await writeText(dir, ".prov", JSON.stringify(prov, null, 1));
|
| 55 |
+
// semantic skin (C2): emit the W3C linked-data view — schema:HowTo + PROV-O revision chain.
|
| 56 |
+
const ld = skillAsHowTo({ name, description, instructions, kappa, prov });
|
| 57 |
+
await writeText(dir, "skill.jsonld", JSON.stringify(ld, null, 1));
|
| 58 |
+
return { slug: slug(name), kappa, version: prov.length, parent, semantic: verifySemantic(ld).ok };
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// the W3C linked-data view of a skill (schema:HowTo + PROV-O), for export / validation / agents.
|
| 62 |
+
export async function skillLinkedData(name) { try { return JSON.parse(await readText(await skillDir(name), "skill.jsonld")); } catch { return null; } }
|
| 63 |
+
|
| 64 |
+
export async function skillProvenance(name) { try { return JSON.parse(await readText(await skillDir(name), ".prov")); } catch { return []; } }
|
| 65 |
+
|
| 66 |
+
// DISCOVERY block (progressive disclosure stage 1): names + descriptions only — small footprint.
|
| 67 |
+
export async function skillsDiscoveryPrompt() {
|
| 68 |
+
const sk = await listSkills();
|
| 69 |
+
if (!sk.length) return "";
|
| 70 |
+
return "You have these learned SKILLS available (each a verifiable, content-addressed capability you built from experience). " +
|
| 71 |
+
"When a task matches a skill's description, call read_skill to load its full instructions, then follow them. " +
|
| 72 |
+
"When you complete a non-trivial task worth reusing, call save_skill to capture it (or improve an existing one):\n" +
|
| 73 |
+
sk.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// the agent's skill tools (Hermes self-evolution: the agent creates/improves/uses skills itself)
|
| 77 |
+
export function skillTools() {
|
| 78 |
+
return [
|
| 79 |
+
{
|
| 80 |
+
def: { name: "list_skills", description: "List the learned skills available (name + description).", inputSchema: { type: "object", properties: {}, required: [] } },
|
| 81 |
+
serverName: "skills", call: async () => { const s = await listSkills(); return { text: s.length ? s.map((x) => `${x.name}: ${x.description}`).join("\n") : "(no skills yet)", isError: false }; },
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
def: { name: "read_skill", description: "Load a skill's full instructions by name, to follow them for the current task.", inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } },
|
| 85 |
+
serverName: "skills", call: async ({ name }) => { try { const s = await readSkill(name); return { text: `# ${s.name}\n${s.description}\n\n${s.instructions}`, isError: false }; } catch (e) { return { text: "no such skill: " + name, isError: true }; } },
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
def: { name: "save_skill", description: "Capture a reusable skill from what you just did (or improve an existing one). name: short; description: one line of WHEN to use it; instructions: the step-by-step procedure. Each save is sealed + chained (verifiable provenance).", inputSchema: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, instructions: { type: "string" } }, required: ["name", "description", "instructions"] } },
|
| 89 |
+
serverName: "skills", call: async ({ name, description, instructions }) => { try { const r = await saveSkill({ name, description, instructions }); return { text: `skill "${name}" saved (v${r.version}, ${r.kappa.slice(0, 28)}…${r.parent ? `, evolved from ${r.parent.slice(0, 20)}…` : ", first version"})`, isError: false }; } catch (e) { return { text: "save error: " + (e.message || e), isError: true }; } },
|
| 90 |
+
},
|
| 91 |
+
];
|
| 92 |
+
}
|
core/state.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/state.js — a tiny event bus + the app's shared mutable state. No framework: modules
|
| 2 |
+
// subscribe to named events and re-render their own DOM islands. Deliberately minimal — the
|
| 3 |
+
// durable truth lives in the κ-object store (Law L3); this is only the session's working set.
|
| 4 |
+
|
| 5 |
+
export function makeBus() {
|
| 6 |
+
const subs = new Map();
|
| 7 |
+
return {
|
| 8 |
+
on(ev, fn) { if (!subs.has(ev)) subs.set(ev, new Set()); subs.get(ev).add(fn); return () => subs.get(ev)?.delete(fn); },
|
| 9 |
+
emit(ev, data) { for (const fn of subs.get(ev) || []) { try { fn(data); } catch (e) { console.error(`[bus:${ev}]`, e); } } },
|
| 10 |
+
};
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function makeState() {
|
| 14 |
+
return {
|
| 15 |
+
// model / engine
|
| 16 |
+
engine: null, // core/engine.js instance (null until a model loads)
|
| 17 |
+
modelIndex: 0, // index into loader.MODELS
|
| 18 |
+
loading: false,
|
| 19 |
+
gateOk: false, // conscience constitution self-verify
|
| 20 |
+
|
| 21 |
+
// conversation working set
|
| 22 |
+
convId: null, // active conversation id
|
| 23 |
+
convKappa: null, // its latest sealed κ
|
| 24 |
+
title: "New Chat",
|
| 25 |
+
messages: [], // [{ obj (sealed κ-object), id, kappa, ...norm fields, tokenIds }]
|
| 26 |
+
chosenChild: new Map(), // parent messageId → chosen child messageId (sibling switch)
|
| 27 |
+
streaming: false,
|
| 28 |
+
abort: null, // AbortController while generating
|
| 29 |
+
|
| 30 |
+
// ui
|
| 31 |
+
navOpen: window.innerWidth > 900,
|
| 32 |
+
panelOpen: false, // the chat is the product — the panel opens on demand
|
| 33 |
+
panelTab: "params",
|
| 34 |
+
settingsOpen: false,
|
| 35 |
+
|
| 36 |
+
// preset (the active parameter bundle — LibreChat conversationPreset semantics)
|
| 37 |
+
preset: null,
|
| 38 |
+
};
|
| 39 |
+
}
|
core/store.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/store.js — the κ-object CONVERSATION STORE. Conversations are NOT database rows; each
|
| 2 |
+
// Message and each Conversation is a self-verifying UOR object (a canonical JSON-LD document whose
|
| 3 |
+
// identity is did:holo:sha256 of its own content), and the LibreChat parentMessageId tree is
|
| 4 |
+
// expressed as content-addressed LINKS so a child's κ commits to its parent's κ — a Merkle-DAG
|
| 5 |
+
// (Law L3 dedup, Law L5 verify-on-resolve). The object envelope mirrors _shared/holo-object.mjs
|
| 6 |
+
// byte-for-byte (same UOR_CONTEXT, same address() = H(canonical without id), same link digests),
|
| 7 |
+
// but is implemented here over Web Crypto (core/kappa.js) so it runs in the browser AND in the
|
| 8 |
+
// pure-Node witness. Bytes are persisted through a pluggable BACKEND: holo-kstore (IndexedDB) in
|
| 9 |
+
// the app, a Map in the witness. No server; everything is content-addressed and re-derivable.
|
| 10 |
+
|
| 11 |
+
import { jcs, sha256hex, didHolo } from "./kappa.js";
|
| 12 |
+
|
| 13 |
+
const _enc = new TextEncoder();
|
| 14 |
+
const _dec = new TextDecoder();
|
| 15 |
+
|
| 16 |
+
// The base envelope @context — identical to _shared/holo-object.mjs.
|
| 17 |
+
export const UOR_CONTEXT = [
|
| 18 |
+
"https://www.w3.org/ns/did/v1",
|
| 19 |
+
"https://w3id.org/security/data-integrity/v2",
|
| 20 |
+
{ schema: "https://schema.org/", prov: "http://www.w3.org/ns/prov#", dcterms: "http://purl.org/dc/terms/",
|
| 21 |
+
rel: "schema:additionalType", links: { "@id": "schema:hasPart", "@container": "@set" } },
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
const hexOf = (did) => String(did).split(":").pop();
|
| 25 |
+
const _b64 = (u8) => { let s = ""; for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return btoa(s); };
|
| 26 |
+
const _b64url = (u8) => _b64(u8).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
| 27 |
+
async function sha256raw(u8) { return new Uint8Array(await crypto.subtle.digest("SHA-256", u8)); }
|
| 28 |
+
async function sriOf(u8) { return "sha256-" + _b64(await sha256raw(u8)); }
|
| 29 |
+
async function mbOf(u8) { const d = await sha256raw(u8); const mh = new Uint8Array(d.length + 2); mh[0] = 0x12; mh[1] = 0x20; mh.set(d, 2); return "u" + _b64url(mh); }
|
| 30 |
+
|
| 31 |
+
// address(obj): self-verifying identity = did:holo:sha256:H(canonical content with id/alsoKnownAs removed).
|
| 32 |
+
const stripId = (obj) => { const { id, alsoKnownAs, ...content } = obj; return content; };
|
| 33 |
+
const addressOf = (obj) => didHolo(stripId(obj));
|
| 34 |
+
const canonStored = (sealed) => _enc.encode(jcs(sealed)); // the EXACT bytes a link's digest commits to (sealed, WITH id — mirrors holo-object put())
|
| 35 |
+
|
| 36 |
+
// contentLink(rel, kappa): a LEAF edge to raw content already addressed by its κ (no bytes needed) —
|
| 37 |
+
// identical to holo-object.contentLink. Verified at resolve by re-hashing the raw bytes to the address.
|
| 38 |
+
export function contentLink(rel, kappa, type = "schema:MediaObject") {
|
| 39 |
+
const hex = String(kappa).split(":").pop();
|
| 40 |
+
const digest = Uint8Array.from(hex.match(/.{2}/g).map((h) => parseInt(h, 16)));
|
| 41 |
+
const mh = new Uint8Array(digest.length + 2); mh[0] = 0x12; mh[1] = 0x20; mh.set(digest, 2);
|
| 42 |
+
return { id: `did:holo:sha256:${hex}`, rel, "@type": type, leaf: true, digestSRI: "sha256-" + _b64(digest), digestMultibase: "u" + _b64url(mh) };
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// makeStore(backend) — backend = { get(hex)→Promise<Uint8Array|undefined>, put(hex,bytes)→Promise,
|
| 46 |
+
// putRaw?(key,bytes)/getRaw?(key) for the boot-index pointer record }.
|
| 47 |
+
export function makeStore(backend) {
|
| 48 |
+
const mem = new Map(); // hex → Uint8Array (session cache; sync verifyDeep operates over it)
|
| 49 |
+
|
| 50 |
+
async function putObj(obj) {
|
| 51 |
+
const sealed = { ...obj, id: await addressOf(obj) };
|
| 52 |
+
const bytes = canonStored(sealed);
|
| 53 |
+
const hex = hexOf(sealed.id);
|
| 54 |
+
mem.set(hex, bytes);
|
| 55 |
+
await backend.put(hex, bytes);
|
| 56 |
+
return sealed;
|
| 57 |
+
}
|
| 58 |
+
async function getBytes(did) { const hex = hexOf(did); let b = mem.get(hex); if (b) return b; b = await backend.get(hex); if (b) mem.set(hex, b); return b; }
|
| 59 |
+
async function getObj(did) { const b = await getBytes(did); return b ? JSON.parse(_dec.decode(b)) : null; }
|
| 60 |
+
|
| 61 |
+
// a typed Merkle edge to a sealed UOR child (the digest commits to the child's stored bytes).
|
| 62 |
+
async function uorLink(rel, child) {
|
| 63 |
+
const bytes = mem.get(hexOf(child.id)) || await backend.get(hexOf(child.id));
|
| 64 |
+
return { id: child.id, rel, "@type": child["@type"], digestSRI: await sriOf(bytes), digestMultibase: await mbOf(bytes) };
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// build + seal + store a UOR object. `links` already built (children stored first → Merkle).
|
| 68 |
+
async function makeObject({ type, context = [], links = [], ...props }) {
|
| 69 |
+
const obj = { "@context": [...UOR_CONTEXT, ...context], "@type": type, ...props, ...(links.length ? { links } : {}) };
|
| 70 |
+
return putObj(obj);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
const verify = async (obj) => obj.id === await addressOf(obj);
|
| 74 |
+
|
| 75 |
+
// verifyDeep — re-derive + verify the WHOLE DAG (Law L5 at every level). Pre-loads links into mem.
|
| 76 |
+
async function verifyDeep(obj, depth = 0) {
|
| 77 |
+
if (!(await verify(obj))) return { ok: false, at: obj.id, why: "id does not re-derive", depth };
|
| 78 |
+
let maxDepth = depth;
|
| 79 |
+
for (const link of obj.links || []) {
|
| 80 |
+
const bytes = await getBytes(link.id);
|
| 81 |
+
if (!bytes) return { ok: false, at: link.id, why: "unresolved link", depth };
|
| 82 |
+
if ((await sriOf(bytes)) !== link.digestSRI) return { ok: false, at: link.id, why: "link digest mismatch", depth };
|
| 83 |
+
if (link.leaf) { if ((await sha256hex(bytes)) !== hexOf(link.id)) return { ok: false, at: link.id, why: "content hash mismatch", depth }; maxDepth = Math.max(maxDepth, depth + 1); continue; }
|
| 84 |
+
const child = JSON.parse(_dec.decode(bytes));
|
| 85 |
+
if (child.id !== link.id) return { ok: false, at: link.id, why: "id/link mismatch", depth };
|
| 86 |
+
const r = await verifyDeep(child, depth + 1); if (!r.ok) return r; maxDepth = Math.max(maxDepth, r.depth);
|
| 87 |
+
}
|
| 88 |
+
return { ok: true, depth: maxDepth };
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// tamper helper (for the witness): overwrite a stored object's bytes with a flipped field.
|
| 92 |
+
async function _corrupt(did, mutate) { const o = await getObj(did); mutate(o); const bytes = canonStored(o); mem.set(hexOf(did), bytes); await backend.put(hexOf(did), bytes); }
|
| 93 |
+
|
| 94 |
+
return { makeObject, putObj, getObj, getBytes, uorLink, contentLink, verify, verifyDeep, mem, backend, _corrupt };
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// ── high-level chat API over a κ-object store ───────────────────────────────────────────────
|
| 98 |
+
// LibreChat-faithful field names. parentMessageId → an "lc:parentMessage" content-addressed link.
|
| 99 |
+
const LC = { lc: "https://librechat.ai/ns#" };
|
| 100 |
+
const rid = (p) => p + "-" + Math.random().toString(36).slice(2, 12); // id is a human handle; substrate identity is the κ
|
| 101 |
+
|
| 102 |
+
export function makeChatStore(backend, { now = () => new Date().toISOString() } = {}) {
|
| 103 |
+
const S = makeStore(backend);
|
| 104 |
+
const INDEX_KEY = "index:org.hologram.HoloQ";
|
| 105 |
+
const getIndex = async () => { try { const b = await (backend.getRaw ? backend.getRaw(INDEX_KEY) : backend.get(INDEX_KEY)); return b ? JSON.parse(_dec.decode(b)) : { conversations: [], presets: [] }; } catch { return { conversations: [], presets: [] }; } };
|
| 106 |
+
const putIndex = async (idx) => { const b = _enc.encode(JSON.stringify(idx)); return backend.putRaw ? backend.putRaw(INDEX_KEY, b) : backend.put(INDEX_KEY, b); };
|
| 107 |
+
|
| 108 |
+
// Persist a PROV-O inference receipt as raw content addressed by its own κ (rec.id = H(jcs(body))),
|
| 109 |
+
// so a message's contentLink("lc:receipt", rec.id) leaf-verifies (sha256(stored bytes) === κ).
|
| 110 |
+
async function saveReceipt(rec) {
|
| 111 |
+
const bytes = _enc.encode(jcs(rec.body));
|
| 112 |
+
const hex = String(rec.id).split(":").pop();
|
| 113 |
+
S.mem.set(hex, bytes); await backend.put(hex, bytes);
|
| 114 |
+
return rec.id;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Save one message as a κ-object. `parent` (a sealed message object) is a Merkle uorLink — the
|
| 118 |
+
// branch tree, so the child's κ commits to the parent's κ. receiptKappa is a leaf content-link.
|
| 119 |
+
// tokenIds (the turn's exact token sequence) makes context reconstruction and Law-L5
|
| 120 |
+
// re-derivation possible after a cold reload.
|
| 121 |
+
async function saveMessage({ messageId, conversationId, sender, isCreatedByUser, model, text, content, tokenCount, tokenIds, error, unfinished, feedback, fromMemo, toolTrace, createdAt, parent, receiptKappa, toolReceiptKappas = [], files = [] }) {
|
| 122 |
+
const links = [];
|
| 123 |
+
if (parent) links.push({ ...(await S.uorLink("lc:parentMessage", parent)), "schema:name": "parentMessageId" });
|
| 124 |
+
if (receiptKappa) links.push({ ...S.contentLink("lc:receipt", receiptKappa, "prov:Activity"), "schema:name": "receipt" });
|
| 125 |
+
for (const k of toolReceiptKappas) links.push({ ...S.contentLink("lc:toolReceipt", k, "prov:Activity"), "schema:name": "toolReceipt" });
|
| 126 |
+
for (const f of files) links.push({ ...S.contentLink("schema:associatedMedia", f.kappa, "schema:MediaObject"), "schema:name": f.name });
|
| 127 |
+
return S.makeObject({
|
| 128 |
+
type: ["schema:Message", "prov:Entity"], context: [LC],
|
| 129 |
+
"schema:identifier": messageId || rid("msg"), "lc:conversationId": conversationId,
|
| 130 |
+
"lc:parentMessageId": (parent && parent["schema:identifier"]) || null, // the human handle of the parent (for export/tree rebuild)
|
| 131 |
+
"lc:sender": sender, "lc:isCreatedByUser": !!isCreatedByUser, "lc:model": model || null,
|
| 132 |
+
"schema:text": text || "", "lc:content": content || null, "lc:tokenCount": tokenCount ?? null,
|
| 133 |
+
"lc:tokenIds": tokenIds || null, "lc:fromMemo": !!fromMemo,
|
| 134 |
+
"lc:toolTrace": toolTrace || null,
|
| 135 |
+
"lc:error": error || null, "lc:unfinished": !!unfinished, "lc:feedback": feedback || null,
|
| 136 |
+
"schema:dateCreated": createdAt || now(),
|
| 137 |
+
links,
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Save the conversation κ-object → links to the head (active leaf) + branch tips. Re-sealed each turn.
|
| 142 |
+
async function saveConversation({ conversationId, title, tags, files, preset, createdAt, headMessage, branchTips = [] }) {
|
| 143 |
+
const links = [];
|
| 144 |
+
if (headMessage) links.push({ ...(await S.uorLink("lc:head", headMessage)), "schema:name": "head" });
|
| 145 |
+
for (const t of branchTips) links.push({ ...(await S.uorLink("lc:branchTip", t)), "schema:name": "tip" });
|
| 146 |
+
const conv = await S.makeObject({
|
| 147 |
+
type: ["schema:Conversation", "prov:Collection"], context: [LC],
|
| 148 |
+
"schema:identifier": conversationId, "schema:name": title || "New Chat",
|
| 149 |
+
"lc:tags": tags || [], "lc:files": files || [], "lc:preset": preset || null,
|
| 150 |
+
"schema:dateCreated": createdAt || now(), "schema:dateModified": now(),
|
| 151 |
+
links,
|
| 152 |
+
});
|
| 153 |
+
// update the boot index (pointer-only; the conversation itself is verifiable)
|
| 154 |
+
const idx = await getIndex();
|
| 155 |
+
const ptr = { conversationId, kappa: conv.id, title: conv["schema:name"], updatedAt: conv["schema:dateModified"], archived: false, favorite: false, folder: null };
|
| 156 |
+
const i = idx.conversations.findIndex((c) => c.conversationId === conversationId);
|
| 157 |
+
if (i >= 0) idx.conversations[i] = { ...idx.conversations[i], ...ptr }; else idx.conversations.unshift(ptr);
|
| 158 |
+
await putIndex(idx);
|
| 159 |
+
return conv;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
async function listConversations() { return (await getIndex()).conversations; }
|
| 163 |
+
async function updatePointer(conversationId, patch) { const idx = await getIndex(); const i = idx.conversations.findIndex((c) => c.conversationId === conversationId); if (i >= 0) { idx.conversations[i] = { ...idx.conversations[i], ...patch }; await putIndex(idx); } }
|
| 164 |
+
async function loadConversation(conversationId) {
|
| 165 |
+
const idx = await getIndex(); const ptr = idx.conversations.find((c) => c.conversationId === conversationId); if (!ptr) return null;
|
| 166 |
+
const conv = await S.getObj(ptr.kappa); if (!conv) return null;
|
| 167 |
+
const v = await S.verifyDeep(conv);
|
| 168 |
+
return { conv, kappa: ptr.kappa, ok: v.ok, integrity: v };
|
| 169 |
+
}
|
| 170 |
+
const verifyConversation = async (kappa) => { const conv = await S.getObj(kappa); return conv ? S.verifyDeep(conv) : { ok: false, why: "not found" }; };
|
| 171 |
+
|
| 172 |
+
return { store: S, saveReceipt, saveMessage, saveConversation, listConversations, loadConversation, verifyConversation, updatePointer, getIndex, newId: rid };
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// A simple Map backend (the witness uses this; the app wraps holo-kstore instead).
|
| 176 |
+
export function mapBackend() {
|
| 177 |
+
const m = new Map();
|
| 178 |
+
return {
|
| 179 |
+
get: async (hex) => m.get(hex),
|
| 180 |
+
put: async (hex, bytes) => { m.set(hex, bytes); },
|
| 181 |
+
getRaw: async (k) => m.get(k),
|
| 182 |
+
putRaw: async (k, bytes) => { m.set(k, bytes); },
|
| 183 |
+
_map: m,
|
| 184 |
+
};
|
| 185 |
+
}
|
core/tools.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// core/tools.js — the agentic TOOL-CALLING loop over the local engine + MCP. Qwen2.5's
|
| 2 |
+
// native function-calling convention: the system turn declares tools as JSON schemas inside
|
| 3 |
+
// a <tools>…</tools> block; the model emits <tool_call>{"name":…,"arguments":…}</tool_call>;
|
| 4 |
+
// each result returns as a user-role <tool_response>…</tool_response> turn and generation
|
| 5 |
+
// continues — until a turn arrives with no tool call (the final answer) or maxRounds hits.
|
| 6 |
+
//
|
| 7 |
+
// Substrate-native: EVERY tool call is sealed as its own PROV-O activity (tool ⊕ args ⊕
|
| 8 |
+
// result, each by κ) and the per-call receipts ride the answer's receipt — a verifiable
|
| 9 |
+
// work-trail for agentic answers, the chat-layer analogue of Holo Orchestrate (ADR-045).
|
| 10 |
+
|
| 11 |
+
import { didHolo, kappaText, jcs } from "./kappa.js";
|
| 12 |
+
|
| 13 |
+
// The system turn that arms the model with tools (Qwen2.5 function-calling convention).
|
| 14 |
+
export function toolSystemPrompt(tools, extra = "") {
|
| 15 |
+
const decls = tools.map((t) => JSON.stringify({ type: "function", function: { name: t.name, description: t.description || "", parameters: t.inputSchema || { type: "object", properties: {} } } })).join("\n");
|
| 16 |
+
return (
|
| 17 |
+
(extra ? extra + "\n\n" : "") +
|
| 18 |
+
"# Tools\n\nYou may call one or more functions to assist with the user query.\n\n" +
|
| 19 |
+
"You are provided with function signatures within <tools></tools> XML tags:\n<tools>\n" + decls + "\n</tools>\n\n" +
|
| 20 |
+
"For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n" +
|
| 21 |
+
'<tool_call>\n{"name": <function-name>, "arguments": <args-json-object>}\n</tool_call>'
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Parse <tool_call> blocks out of a model turn. Tolerant: bare JSON with name+arguments
|
| 26 |
+
// also counts (small models sometimes drop the tags).
|
| 27 |
+
export function parseToolCalls(text) {
|
| 28 |
+
const calls = [];
|
| 29 |
+
const re = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
| 30 |
+
let m;
|
| 31 |
+
while ((m = re.exec(text))) { try { const j = JSON.parse(m[1]); if (j && j.name) calls.push({ name: j.name, arguments: j.arguments || {} }); } catch {} }
|
| 32 |
+
// ```json fenced code blocks with {name, arguments} (Qwen2.5-Coder often emits this form)
|
| 33 |
+
if (!calls.length) {
|
| 34 |
+
const cb = /```(?:json|tool_call)?\s*(\{[\s\S]*?\})\s*```/g;
|
| 35 |
+
while ((m = cb.exec(text))) { try { const j = JSON.parse(m[1]); if (j && j.name && ("arguments" in j)) calls.push({ name: j.name, arguments: j.arguments || {} }); } catch {} }
|
| 36 |
+
}
|
| 37 |
+
if (!calls.length) {
|
| 38 |
+
const t = text.trim();
|
| 39 |
+
if (t.startsWith("{") && t.includes('"name"')) { try { const j = JSON.parse(t); if (j.name && ("arguments" in j)) calls.push({ name: j.name, arguments: j.arguments || {} }); } catch {} }
|
| 40 |
+
}
|
| 41 |
+
return calls;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export const stripToolCalls = (text) => text
|
| 45 |
+
.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
|
| 46 |
+
.replace(/```(?:json|tool_call)?\s*\{[\s\S]*?"name"[\s\S]*?\}\s*```/g, "") // strip fenced tool calls too
|
| 47 |
+
.trim();
|
| 48 |
+
|
| 49 |
+
// Seal one tool call as a PROV-O activity (its own did:holo — tamper → refuse).
|
| 50 |
+
export async function sealToolReceipt({ server, tool, args, resultText, ok }) {
|
| 51 |
+
const body = {
|
| 52 |
+
"@context": ["http://www.w3.org/ns/prov#", { holo: "https://hologram.os/ns/q#" }],
|
| 53 |
+
"@type": "prov:Activity", "holo:kind": "tool-call",
|
| 54 |
+
"prov:used": { "holo:server": server, "holo:tool": tool, "holo:args": await didHolo(JSON.parse(jcs(args || {}))), "holo:argsJson": jcs(args || {}) },
|
| 55 |
+
"prov:generated": { "holo:result": await kappaText(resultText), "holo:ok": !!ok },
|
| 56 |
+
};
|
| 57 |
+
return { id: await didHolo(body), body };
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Frame the agentic first turn: system(tools) + user prompt (ChatML, by hand so the system
|
| 61 |
+
// block rides this turn). Exposed so the caller can persist the EXACT ids that run (the
|
| 62 |
+
// ctx-concat invariant of the message tree).
|
| 63 |
+
export function frameAgenticTurn({ tools, promptText, hasHistory, extraSystem = "" }) {
|
| 64 |
+
const sys = toolSystemPrompt(tools.map((t) => t.def), extraSystem);
|
| 65 |
+
return (hasHistory ? "<|im_end|>\n" : "") +
|
| 66 |
+
`<|im_start|>system\n${sys}<|im_end|>\n<|im_start|>user\n${promptText}<|im_end|>\n<|im_start|>assistant\n`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// The loop. `callbacks.onToolCall({name,args})` / `onToolResult({name,text,receipt})` let the
|
| 70 |
+
// UI stream the work-trail live. Returns { text, rounds, toolReceipts, trace, ids }.
|
| 71 |
+
export async function runToolLoop({ engine, tools, promptText, ctxIds, firstFramed, signal, onToken, onToolCall, onToolResult, maxRounds = 4 }) {
|
| 72 |
+
const toolReceipts = []; const trace = [];
|
| 73 |
+
let framed = firstFramed || frameAgenticTurn({ tools, promptText, hasHistory: ctxIds.length > 0 });
|
| 74 |
+
let ids = ctxIds.slice();
|
| 75 |
+
let finalText = "", rounds = 0;
|
| 76 |
+
|
| 77 |
+
for (; rounds < maxRounds; rounds++) {
|
| 78 |
+
const turnIds = engine.tokenize(framed);
|
| 79 |
+
ids = ids.concat(turnIds);
|
| 80 |
+
// tight per-round budget: a tool_call is ~50 tokens, a final answer ~300 — snappy rounds,
|
| 81 |
+
// the same total work, far lower latency than letting one round run the whole cap.
|
| 82 |
+
// generous budget: tool calls that carry CODE (write_file, build_app) need room — a truncated
|
| 83 |
+
// call is invalid JSON and silently fails. 1536 fits a small app; plain rounds end early on EOS.
|
| 84 |
+
const res = await engine.generate(ids, { signal, onToken, maxNew: 1536 });
|
| 85 |
+
ids = res.ids;
|
| 86 |
+
const calls = parseToolCalls(res.text);
|
| 87 |
+
if (!calls.length || (signal && signal.aborted)) { finalText = stripToolCalls(res.text); break; }
|
| 88 |
+
|
| 89 |
+
// execute every call this round through MCP, seal a receipt each
|
| 90 |
+
const responses = [];
|
| 91 |
+
for (const c of calls) {
|
| 92 |
+
if (signal && signal.aborted) break;
|
| 93 |
+
const t = tools.find((x) => x.def.name === c.name);
|
| 94 |
+
onToolCall && onToolCall({ name: c.name, args: c.arguments, server: t ? t.serverName : "?" });
|
| 95 |
+
let text, ok = true, render = null;
|
| 96 |
+
try {
|
| 97 |
+
if (!t) throw new Error("unknown tool: " + c.name);
|
| 98 |
+
const r = await t.call(c.arguments);
|
| 99 |
+
text = r.text; ok = !r.isError; render = r.render || null; // build_app → live preview payload
|
| 100 |
+
} catch (e) { text = "ERROR: " + (e && e.message || e); ok = false; }
|
| 101 |
+
const receipt = await sealToolReceipt({ server: t ? t.serverName : "?", tool: c.name, args: c.arguments, resultText: text, ok });
|
| 102 |
+
toolReceipts.push(receipt);
|
| 103 |
+
trace.push({ name: c.name, args: c.arguments, text: text.slice(0, 4000), ok, receiptId: receipt.id, server: t ? t.serverName : "?" });
|
| 104 |
+
onToolResult && onToolResult({ name: c.name, text, ok, receipt, render });
|
| 105 |
+
responses.push(`<tool_response>\n${text.slice(0, 2400)}\n</tool_response>`); // bounded — tool output must fit the local context window
|
| 106 |
+
}
|
| 107 |
+
// feed results back (user role per the Qwen convention) and continue
|
| 108 |
+
framed = `<|im_end|>\n<|im_start|>user\n${responses.join("\n")}<|im_end|>\n<|im_start|>assistant\n`;
|
| 109 |
+
finalText = stripToolCalls(res.text); // best-so-far, replaced if a later round answers
|
| 110 |
+
}
|
| 111 |
+
return { text: finalText, rounds: rounds + 1, toolReceipts, trace, ids };
|
| 112 |
+
}
|
forge/gguf-forge-iq-dequant.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// IQ-quant dequantization, transcribed line-for-line from ggml-quants.c
|
| 2 |
+
// (llama.cpp b7248): dequantize_row_iq2_xxs:3077 iq2_xs:3105 iq2_s:3132
|
| 3 |
+
// iq3_xxs:3164 iq3_s:3196 iq1_s:3239 iq1_m:3264 iq4_nl:3314 iq4_xs:3332.
|
| 4 |
+
// Block layouts: ggml-common.h:485-563.
|
| 5 |
+
//
|
| 6 |
+
// ONE source of truth, two behaviours: makeIQ(fr, f16ToF32) binds the rounding
|
| 7 |
+
// function. The Tier-A oracle passes Math.fround (binary32-exact, witnessed
|
| 8 |
+
// bit-for-bit vs ggml to_float in gguf-forge-iq.test.mjs); the runtime passes
|
| 9 |
+
// identity (float64, fine for the GPU engine which re-quantizes anyway).
|
| 10 |
+
//
|
| 11 |
+
// Codebook grids are flattened little-endian byte runs (gguf-forge-iq-grids.mjs):
|
| 12 |
+
// an entry `idx` of stride S occupies bytes [idx*S .. idx*S+S). iq2*/iq3* read
|
| 13 |
+
// uint8, iq1s_grid / kvalues_iq4nl read int8.
|
| 14 |
+
|
| 15 |
+
import {
|
| 16 |
+
iq2xxs_grid, iq2xs_grid, iq2s_grid, iq3xxs_grid, iq3s_grid, iq1s_grid,
|
| 17 |
+
ksigns_iq2xs, kmask_iq2xs, kvalues_iq4nl,
|
| 18 |
+
} from "./gguf-forge-iq-grids.mjs";
|
| 19 |
+
|
| 20 |
+
const QK_K = 256;
|
| 21 |
+
const IQ1S_DELTA = 0.125, IQ1M_DELTA = 0.125;
|
| 22 |
+
const s8 = (b) => (b << 24) >> 24; // uint8 -> int8
|
| 23 |
+
|
| 24 |
+
export function makeIQ(fr, f16ToF32) {
|
| 25 |
+
const sign1 = (signs, j) => (signs & kmask_iq2xs[j] ? -1 : 1);
|
| 26 |
+
|
| 27 |
+
// dequantize_row_iq2_xxs (:3077). Block 66 B: d:f16 qs:uint16[32].
|
| 28 |
+
function dequantIQ2XXS(raw, elements) {
|
| 29 |
+
const out = new Float32Array(elements);
|
| 30 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 31 |
+
const nb = elements / QK_K; let o = 0;
|
| 32 |
+
for (let i = 0; i < nb; i++) {
|
| 33 |
+
const bp = i * 66, qs = bp + 2;
|
| 34 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 35 |
+
for (let ib32 = 0; ib32 < QK_K / 32; ++ib32) {
|
| 36 |
+
const a0 = dv.getUint32(qs + 8 * ib32, true), a1 = dv.getUint32(qs + 8 * ib32 + 4, true);
|
| 37 |
+
const db = fr(fr(d * fr(0.5 + (a1 >>> 28))) * 0.25);
|
| 38 |
+
for (let l = 0; l < 4; ++l) {
|
| 39 |
+
const idx = (a0 >>> (8 * l)) & 0xff;
|
| 40 |
+
const signs = ksigns_iq2xs[(a1 >>> (7 * l)) & 127];
|
| 41 |
+
for (let j = 0; j < 8; ++j) out[o++] = sign1(signs, j) * fr(db * iq2xxs_grid[idx * 8 + j]);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
return out;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// dequantize_row_iq2_xs (:3105). Block 74 B: d:f16 qs:uint16[32] scales:uint8[8].
|
| 49 |
+
function dequantIQ2XS(raw, elements) {
|
| 50 |
+
const out = new Float32Array(elements);
|
| 51 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 52 |
+
const nb = elements / QK_K; let o = 0;
|
| 53 |
+
for (let i = 0; i < nb; i++) {
|
| 54 |
+
const bp = i * 74, qs = bp + 2, scB = bp + 66;
|
| 55 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 56 |
+
for (let ib32 = 0; ib32 < QK_K / 32; ++ib32) {
|
| 57 |
+
const db = [fr(fr(d * fr(0.5 + (raw[scB + ib32] & 0xf))) * 0.25),
|
| 58 |
+
fr(fr(d * fr(0.5 + (raw[scB + ib32] >> 4))) * 0.25)];
|
| 59 |
+
for (let l = 0; l < 4; ++l) {
|
| 60 |
+
const q = dv.getUint16(qs + (4 * ib32 + l) * 2, true);
|
| 61 |
+
const idx = q & 511, signs = ksigns_iq2xs[q >> 9], dl = db[l >> 1];
|
| 62 |
+
for (let j = 0; j < 8; ++j) out[o++] = sign1(signs, j) * fr(dl * iq2xs_grid[idx * 8 + j]);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
return out;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// dequantize_row_iq2_s (:3132). Block 82 B: d:f16 qs:uint8[64] qh:uint8[8] scales:uint8[8].
|
| 70 |
+
// signs = qs + 32 (second half of qs); idx = qs[l] | ((qh[ib32]<<(8-2l)) & 0x300).
|
| 71 |
+
function dequantIQ2S(raw, elements) {
|
| 72 |
+
const out = new Float32Array(elements);
|
| 73 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 74 |
+
const nb = elements / QK_K; let o = 0;
|
| 75 |
+
for (let i = 0; i < nb; i++) {
|
| 76 |
+
const bp = i * 82, qhB = bp + 66, scB = bp + 74;
|
| 77 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 78 |
+
let qs = bp + 2, signs = bp + 2 + 32;
|
| 79 |
+
for (let ib32 = 0; ib32 < QK_K / 32; ++ib32) {
|
| 80 |
+
const db = [fr(fr(d * fr(0.5 + (raw[scB + ib32] & 0xf))) * 0.25),
|
| 81 |
+
fr(fr(d * fr(0.5 + (raw[scB + ib32] >> 4))) * 0.25)];
|
| 82 |
+
for (let l = 0; l < 4; ++l) {
|
| 83 |
+
const dl = db[l >> 1];
|
| 84 |
+
const idx = raw[qs + l] | (((raw[qhB + ib32] << (8 - 2 * l)) & 0x300));
|
| 85 |
+
for (let j = 0; j < 8; ++j) out[o++] = sign1(raw[signs + l], j) * fr(dl * iq2s_grid[idx * 8 + j]);
|
| 86 |
+
}
|
| 87 |
+
qs += 4; signs += 4;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
return out;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// dequantize_row_iq3_xxs (:3164). Block 98 B: d:f16 qs:uint8[96].
|
| 94 |
+
// qs[0..63]=grid idx; scales_and_signs = qs+64 (8 uint32). db scale uses *0.5.
|
| 95 |
+
function dequantIQ3XXS(raw, elements) {
|
| 96 |
+
const out = new Float32Array(elements);
|
| 97 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 98 |
+
const nb = elements / QK_K; let o = 0;
|
| 99 |
+
for (let i = 0; i < nb; i++) {
|
| 100 |
+
const bp = i * 98, ss = bp + 2 + 64;
|
| 101 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 102 |
+
let qs = bp + 2;
|
| 103 |
+
for (let ib32 = 0; ib32 < QK_K / 32; ++ib32) {
|
| 104 |
+
const a32 = dv.getUint32(ss + 4 * ib32, true);
|
| 105 |
+
const db = fr(fr(d * fr(0.5 + (a32 >>> 28))) * 0.5);
|
| 106 |
+
for (let l = 0; l < 4; ++l) {
|
| 107 |
+
const signs = ksigns_iq2xs[(a32 >>> (7 * l)) & 127];
|
| 108 |
+
const g1 = raw[qs + 2 * l] * 4, g2 = raw[qs + 2 * l + 1] * 4;
|
| 109 |
+
for (let j = 0; j < 4; ++j) {
|
| 110 |
+
out[o + j + 0] = sign1(signs, j + 0) * fr(db * iq3xxs_grid[g1 + j]);
|
| 111 |
+
out[o + j + 4] = sign1(signs, j + 4) * fr(db * iq3xxs_grid[g2 + j]);
|
| 112 |
+
}
|
| 113 |
+
o += 8;
|
| 114 |
+
}
|
| 115 |
+
qs += 8;
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
return out;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// dequantize_row_iq3_s (:3196). Block 110 B: d:f16 qs:uint8[64] qh:uint8[8] signs:uint8[32] scales:uint8[4].
|
| 122 |
+
function dequantIQ3S(raw, elements) {
|
| 123 |
+
const out = new Float32Array(elements);
|
| 124 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 125 |
+
const nb = elements / QK_K; let o = 0;
|
| 126 |
+
for (let i = 0; i < nb; i++) {
|
| 127 |
+
const bp = i * 110, scB = bp + 106;
|
| 128 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 129 |
+
let qs = bp + 2, qh = bp + 66, signs = bp + 74;
|
| 130 |
+
for (let ib32 = 0; ib32 < QK_K / 32; ib32 += 2) {
|
| 131 |
+
const db1 = fr(d * (1 + 2 * (raw[scB + (ib32 >> 1)] & 0xf)));
|
| 132 |
+
const db2 = fr(d * (1 + 2 * (raw[scB + (ib32 >> 1)] >> 4)));
|
| 133 |
+
for (let l = 0; l < 4; ++l) {
|
| 134 |
+
const g1 = (raw[qs + 2 * l] | ((raw[qh] << (8 - 2 * l)) & 256)) * 4;
|
| 135 |
+
const g2 = (raw[qs + 2 * l + 1] | ((raw[qh] << (7 - 2 * l)) & 256)) * 4;
|
| 136 |
+
for (let j = 0; j < 4; ++j) {
|
| 137 |
+
out[o + j + 0] = sign1(raw[signs + l], j + 0) * fr(db1 * iq3s_grid[g1 + j]);
|
| 138 |
+
out[o + j + 4] = sign1(raw[signs + l], j + 4) * fr(db1 * iq3s_grid[g2 + j]);
|
| 139 |
+
}
|
| 140 |
+
o += 8;
|
| 141 |
+
}
|
| 142 |
+
qs += 8; signs += 4;
|
| 143 |
+
for (let l = 0; l < 4; ++l) {
|
| 144 |
+
const g1 = (raw[qs + 2 * l] | ((raw[qh + 1] << (8 - 2 * l)) & 256)) * 4;
|
| 145 |
+
const g2 = (raw[qs + 2 * l + 1] | ((raw[qh + 1] << (7 - 2 * l)) & 256)) * 4;
|
| 146 |
+
for (let j = 0; j < 4; ++j) {
|
| 147 |
+
out[o + j + 0] = sign1(raw[signs + l], j + 0) * fr(db2 * iq3s_grid[g1 + j]);
|
| 148 |
+
out[o + j + 4] = sign1(raw[signs + l], j + 4) * fr(db2 * iq3s_grid[g2 + j]);
|
| 149 |
+
}
|
| 150 |
+
o += 8;
|
| 151 |
+
}
|
| 152 |
+
qh += 2; qs += 8; signs += 4;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
return out;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// dequantize_row_iq1_s (:3239). Block 50 B: d:f16 qs:uint8[32] qh:uint16[8]. grid int8.
|
| 159 |
+
function dequantIQ1S(raw, elements) {
|
| 160 |
+
const out = new Float32Array(elements);
|
| 161 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 162 |
+
const nb = elements / QK_K; let o = 0;
|
| 163 |
+
for (let i = 0; i < nb; i++) {
|
| 164 |
+
const bp = i * 50, qhB = bp + 34;
|
| 165 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 166 |
+
let qs = bp + 2;
|
| 167 |
+
for (let ib = 0; ib < QK_K / 32; ++ib) {
|
| 168 |
+
const qh = dv.getUint16(qhB + 2 * ib, true);
|
| 169 |
+
const dl = fr(d * (2 * ((qh >> 12) & 7) + 1));
|
| 170 |
+
const delta = (qh & 0x8000) ? -IQ1S_DELTA : IQ1S_DELTA;
|
| 171 |
+
for (let l = 0; l < 4; ++l) {
|
| 172 |
+
const g = (raw[qs + l] | (((qh >> (3 * l)) & 7) << 8)) * 8;
|
| 173 |
+
for (let j = 0; j < 8; ++j) out[o++] = fr(dl * fr(s8(iq1s_grid[g + j]) + delta));
|
| 174 |
+
}
|
| 175 |
+
qs += 4;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
return out;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// dequantize_row_iq1_m (:3264). Block 56 B: qs:uint8[32] qh:uint8[16] scales:uint8[8].
|
| 182 |
+
// No d field — f16 scale is woven from the four scale uint16s. grid int8.
|
| 183 |
+
function dequantIQ1M(raw, elements) {
|
| 184 |
+
const out = new Float32Array(elements);
|
| 185 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 186 |
+
const nb = elements / QK_K; let o = 0;
|
| 187 |
+
const delta = new Float32Array(4), idx = new Uint16Array(4);
|
| 188 |
+
for (let i = 0; i < nb; i++) {
|
| 189 |
+
const bp = i * 56, scB = bp + 48;
|
| 190 |
+
const sc = [dv.getUint16(scB, true), dv.getUint16(scB + 2, true), dv.getUint16(scB + 4, true), dv.getUint16(scB + 6, true)];
|
| 191 |
+
const u16 = (sc[0] >> 12) | ((sc[1] >> 8) & 0x00f0) | ((sc[2] >> 4) & 0x0f00) | (sc[3] & 0xf000);
|
| 192 |
+
const d = f16ToF32(u16);
|
| 193 |
+
let qs = bp, qh = bp + 32;
|
| 194 |
+
for (let ib = 0; ib < QK_K / 32; ++ib) {
|
| 195 |
+
const dl1 = fr(d * (2 * ((sc[ib >> 1] >> (6 * (ib % 2) + 0)) & 0x7) + 1));
|
| 196 |
+
const dl2 = fr(d * (2 * ((sc[ib >> 1] >> (6 * (ib % 2) + 3)) & 0x7) + 1));
|
| 197 |
+
idx[0] = raw[qs + 0] | ((raw[qh + 0] << 8) & 0x700);
|
| 198 |
+
idx[1] = raw[qs + 1] | ((raw[qh + 0] << 4) & 0x700);
|
| 199 |
+
idx[2] = raw[qs + 2] | ((raw[qh + 1] << 8) & 0x700);
|
| 200 |
+
idx[3] = raw[qs + 3] | ((raw[qh + 1] << 4) & 0x700);
|
| 201 |
+
delta[0] = raw[qh + 0] & 0x08 ? -IQ1M_DELTA : IQ1M_DELTA;
|
| 202 |
+
delta[1] = raw[qh + 0] & 0x80 ? -IQ1M_DELTA : IQ1M_DELTA;
|
| 203 |
+
delta[2] = raw[qh + 1] & 0x08 ? -IQ1M_DELTA : IQ1M_DELTA;
|
| 204 |
+
delta[3] = raw[qh + 1] & 0x80 ? -IQ1M_DELTA : IQ1M_DELTA;
|
| 205 |
+
for (let l = 0; l < 2; ++l) {
|
| 206 |
+
const g = idx[l] * 8;
|
| 207 |
+
for (let j = 0; j < 8; ++j) out[o++] = fr(dl1 * fr(s8(iq1s_grid[g + j]) + delta[l]));
|
| 208 |
+
}
|
| 209 |
+
for (let l = 2; l < 4; ++l) {
|
| 210 |
+
const g = idx[l] * 8;
|
| 211 |
+
for (let j = 0; j < 8; ++j) out[o++] = fr(dl2 * fr(s8(iq1s_grid[g + j]) + delta[l]));
|
| 212 |
+
}
|
| 213 |
+
qs += 4; qh += 2;
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
return out;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// dequantize_row_iq4_nl (:3314). Block 18 B / 32 elems: d:f16 qs:uint8[16]. kvalues int8.
|
| 220 |
+
function dequantIQ4NL(raw, elements) {
|
| 221 |
+
const out = new Float32Array(elements);
|
| 222 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 223 |
+
const nb = elements / 32; let o = 0;
|
| 224 |
+
for (let i = 0; i < nb; i++) {
|
| 225 |
+
const bp = i * 18, qs = bp + 2;
|
| 226 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 227 |
+
for (let j = 0; j < 16; ++j) {
|
| 228 |
+
out[o + j] = fr(d * s8(kvalues_iq4nl[raw[qs + j] & 0xf]));
|
| 229 |
+
out[o + 16 + j] = fr(d * s8(kvalues_iq4nl[raw[qs + j] >> 4]));
|
| 230 |
+
}
|
| 231 |
+
o += 32;
|
| 232 |
+
}
|
| 233 |
+
return out;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// dequantize_row_iq4_xs (:3332). Block 136 B: d:f16 scales_h:uint16 scales_l:uint8[4] qs:uint8[128].
|
| 237 |
+
function dequantIQ4XS(raw, elements) {
|
| 238 |
+
const out = new Float32Array(elements);
|
| 239 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 240 |
+
const nb = elements / QK_K; let o = 0;
|
| 241 |
+
for (let i = 0; i < nb; i++) {
|
| 242 |
+
const bp = i * 136, slB = bp + 4;
|
| 243 |
+
const d = f16ToF32(dv.getUint16(bp, true));
|
| 244 |
+
const sh = dv.getUint16(bp + 2, true);
|
| 245 |
+
let qs = bp + 8;
|
| 246 |
+
for (let ib = 0; ib < QK_K / 32; ++ib) {
|
| 247 |
+
const ls = ((raw[slB + (ib >> 1)] >> (4 * (ib % 2))) & 0xf) | (((sh >> (2 * ib)) & 3) << 4);
|
| 248 |
+
const dl = fr(d * (ls - 32));
|
| 249 |
+
for (let j = 0; j < 16; ++j) {
|
| 250 |
+
out[o + j] = fr(dl * s8(kvalues_iq4nl[raw[qs + j] & 0xf]));
|
| 251 |
+
out[o + 16 + j] = fr(dl * s8(kvalues_iq4nl[raw[qs + j] >> 4]));
|
| 252 |
+
}
|
| 253 |
+
o += 32; qs += 16;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
return out;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
return { dequantIQ2XXS, dequantIQ2XS, dequantIQ2S, dequantIQ3XXS, dequantIQ3S, dequantIQ1S, dequantIQ1M, dequantIQ4NL, dequantIQ4XS };
|
| 260 |
+
}
|
forge/gguf-forge-iq-grids.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// AUTO-GENERATED by gen-iq-grids.mjs from ggml-common.h — do not edit by hand.
|
| 2 |
+
// IQ-quant codebook grids + helper tables, flattened to little-endian byte runs.
|
| 3 |
+
// A grid entry `idx` of stride S occupies bytes [idx*S .. idx*S+S); read uint8
|
| 4 |
+
// for iq2*/iq3* grids, int8 (`(b<<24)>>24`) for iq1s_grid / kvalues_iq4nl.
|
| 5 |
+
const D = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
| 6 |
+
export const iq2xxs_grid = /* 2048B */ D("CAgICAgICAgrCAgICAgICBkZCAgICAgICCsICAgICAgrKwgICAgICBkIGQgICAgICBkZCAgICAgICCsICAgICCsIKwgICAgICCsrCAgICAgrKysICAgICBkICBkICAgICBkIGQgICAgICBkZCAgICAgrGRkICAgIGQgrGQgICAgIGSsZCAgICAgICCsICAgIKwgIKwgICAgrKwgrCAgICCsIKysICAgIGQgICBkICAgIGQgIGQgICAgIGQgZCAgIGRkZCBkICAgICAgZGQgICAgZCCsZCAgICCsZKxkICAgICAgIKwgICCsICAgrCAgIKwgrCCsICAgrCAgrKwgICBkICAgIGQgICBkICAgZCAgICBkICBkICBkIKwgIGQgICBkrCAgZCAgICAgZCBkICCsICBkIGQgICCsIGQgZCAgICCsZCBkICBkICCsIGQgICBkIKwgZCAgICBkrCBkICAgZKysIGQgICAgICBkZCAgrCAgIGRkICAgrCAgZGQgICAgrCBkZCAgrGQgZGRkICBkrKxkZGQgICAgIKxkZCAgZCBkrGRkICBkrCAgrGQgICAgZCCsZCAgICAgZKxkICAgZCCsrGQgICBkrKysZCAgICAgICCsICBkZCAgIKwgICCsICAgrCAgIGRkICCsICAgrKwgIKwgIGQgIGQgrCAgIGQgZCCsICAgIGRkIKwgIKwgZGQgrCAgIKwgrCCsICAgZCAgZKwgICAgIGRkrCAgrCAgIKysICAgZGQgrKwgIGQgICAgIGQgIGQgICAgZCAgIGQgICBkIGQgrCAgIGQgICAgZCAgZCAgIKxkICBkICBkIKwgIGQgICBkrCAgZCBkZGSsICBkICAgICBkIGQgIKwgIGQgZCAgIKwgZCBkICAgZGRkIGQgrKxkZGQgZCAgICCsZCBkICBkrCCsIGQgZGQgZKwgZCAgICAgIGRkICCsICAgZGQgICCsICBkZCBkZKwgIGRkIGSsIGQgZGQgICAgrCBkZCAgrGQgZGRkIKwgrGRkZGQgICAgIKxkZCCsZGQgrGRkIGQgICAgrGQgIGQgICCsZCAgIGQgIKxkICAgIGQgrGQgZCAgrCCsZCAgICAgZKxkIGRkICBkrGQgICCsrGSsZCBkIGRkrKxkICAgICAgIKwgrCAgICAgrCCsrCAgICCsICBkIGQgIKwgZCCsZCAgrCAgICCsICCsIKwgIKwgIKwgZKysIGQgrCAgrCBkZCCsICAgICCsIKwgrCAgIKwgrCBkICAgIGSsICBkICAgZKwgICBkICBkrCAgICBkIGSsIKxkZGQgZKwgICAgIGRkrCBkICBkZGSsICBkrGRkZKwgICBkrKxkrCAgrCAgIKysICAgrCAgrKwgIGRkrCCsrCAgZCBkrKysIGQgICAgICBkIGQgICAgIGQgIGQgICAgZCCsZCAgICBkZCCsICAgIGQgZKwgICAgZCAgIGQgICBkIKwgZCAgIGSsZGRkICAgZCAgrGQgICBkZCAgrCAgIGQgZCCsICAgZCAgZKwgICBkICAgIGQgIGQgIKwgZCAgZGQgrGRkICBkICAgrGQgIGRkZCCsZCAgZGQgICCsICBkICBkIKwgIGQgrCBkrCAgZKxkZGSsICBkIKysZKwgIGQgICAgIGQgZCCsICAgZCBkICCsICBkIGQgICCsIGQgZGSsZKwgZCBkrCBkIGRkIGQgZKwgZGQgZCAgICCsZCBkZCAgICCsIGQgZCAgIKwgZCAgZCAgrCBkICAgZCCsIGRkZCBkIKwgZCAgICBkrCBkIKxkZGSsIGRkIKxkZKwgZKwgIKxkrCBkZGQgZKysIGQgIGSsrKwgZCAgICAgIGRkIKwgICAgZGRkIGQgICBkZGSsZCAgIGRkICCsICAgZGQgICCsICBkZCCsIKwgIGRkIGQgIGQgZGSsICBkZCBkZCBkrKxkIGRkZCBkrKwgZGQgIGSsIGRkZKwgZKwgZGRkrKwgIGRkZGRkICAgrGRkZCBkZGSsZGRkICAgICCsZGRkIGQgIKxkZGSsZCAgrGRkIGSsZCCsZGQgICBkZKxkZCCsICCsrGRkIGQgICAgrGQgIGQgICCsZCAgIGQgIKxkIKysZCAgrGQgICAgZCCsZGRkZGRkIKxkIKxkIKwgrGQgIKxkrCCsZCAgICAgZKxkZGQgICBkrGQgIGQgZGSsZKwgZCBkZKxkIGQgrGRkrGSsICBkIKysZCAgICAgICCsrCAgICAgIKysrCAgICAgrGQgIGQgICCsrCAgrCAgIKwgZCAgZCAgrCCsZCBkICCsICAgZGQgIKxkIGQgrCAgrGQgICAgZCCsIGQgICBkIKwgIGQgIGQgrGRkZCAgZCCsICAgZCBkIKwgIKxkIGQgrCAgICBkZCCsrGQgZGRkIKwgZGSsZGQgrGSsICCsZCCsICAgZKxkIKwgIKxkrGQgrKwgICAgrCCsIGQgIGSsIKxkIGQgrKwgrCBkICAgIGSsICBkICAgZKwgZKwgICBkrCAgIGQgIGSsZCCsrCAgZKysZGQgZCBkrCAgIKxkIGSsZGQgZKwgZKwgICAgIGRkrKwgrCAgZGSsIGQgZCBkZKxkIGRkZGRkrGQgIKwgrGSsICCsIGSsZKysICAgICCsrCAgZGQgIKysZGQgrCAgrKxkrCAgZCCsrCAgICCsIKysIKxkICBkrKwgIGRkIKysrCBkICBkrKys=");
|
| 7 |
+
export const iq2xs_grid = /* 4096B */ D("CAgICAgICAgrCAgICAgICBkZCAgICAgICCsICAgICAgrKwgICAgICBkIGQgICAgICBkZCAgICAgrGRkICAgICBkrGQgICAgICAgrCAgICAgrCCsICAgICBkZKwgICAgICCsrCAgICAgZCAgZCAgICAgZCBkICAgIKxkIGQgICAgZKwgZCAgICAgIGRkICAgIKwgZGQgICAgZGRkZCAgICAgrGRkICAgIGQgrGQgICAgIGSsZCAgICAgICCsICAgIKwgIKwgICAgZGQgrCAgICAgrCCsICAgIGQgZKwgICAgIGRkrCAgICBkrGSsICAgICAgrKwgICAgZCAgIGQgICAgZCAgZCAgIKxkICBkICAgZKwgIGQgICAgIGQgZCAgIKwgZCBkICAgZGRkIGQgICAgrGQgZCAgIKysZCBkICAgZCCsIGQgICAgZKwgZCAgICAgIGRkICAgrCAgZGQgICBkZCBkZCAgICCsIGRkICAgZCBkZGQgICAgZGRkZCAgICAgrGRkICAgIKysZGQgICBkICCsZCAgICBkIKxkICAgICBkrGQgICAgICAgrCAgIKwgICCsICAgZGQgIKwgICAgrCAgrCAgIGQgZCCsICAgIGRkIKwgICAgIKwgrCAgIGQgIGSsICAgIGQgZKwgICAgIGRkrCAgIGRkZGSsICAgICAgrKwgICCsrCCsrCAgIGQgICAgZCAgIGQgICBkICCsZCAgIGQgIGSsICAgZCAgICBkICBkICCsIGQgIGQgIGRkZCAgZCAgIKxkICBkICBkIKwgIGQgICBkrCAgZCAgICAgZCBkICCsICBkIGQgIGRkIGQgZCAgIKwgZCBkICBkIGRkIGQgICBkZGQgZCAgrGRkZCBkICAgIKxkIGQgIGQgIKwgZCAgIGQgrCBkICAgIGSsIGQgICAgICBkZCAgrCAgIGRkICBkZCAgZGQgICCsICBkZCAgZCBkIGRkICAgZGQgZGQgICAgrCBkZCAgZCAgZGRkICAgZCBkZGQgICAgZGRkZCAgZCCsZGRkICAgICCsZGQgIGQgICCsZCAgIGQgIKxkICAgIGQgrGQgIKxkrCCsZCAgICAgZKxkICCsICBkrGQgICBkIKysZCAgICAgICCsICCsICAgIKwgIGRkICAgrCAgIKwgICCsICCsrCAgIKwgIGQgZCAgrCAgIGRkICCsICAgIKwgIKwgIGRkrCAgrCAgZCAgZCCsICAgZCBkIKwgICAgZGQgrCAgIKxkZCCsICAgICCsIKwgICAgrKwgrCAgrKysrCCsICBkICAgZKwgICBkICBkrCAgICBkIGSsICAgICBkZKwgIGQgIKxkrCAgZKwgrGSsICAgICAgrKwgICAgrCCsrCAgIKysIKysICCsZGSsrKwgICAgrKysrCAgZCAgICAgZCAgZCAgICBkIKxkICAgIGQgZKwgICAgZCAgIGQgICBkIKwgZCAgIGQgZGRkICAgZCAgrGQgICBkIGQgrCAgIGQgIGSsICAgZCAgICBkICBkIKwgIGQgIGQgZGQgZCAgZCAgrCBkICBkIGQgZGQgIGQgIGRkZCAgZCAgIKxkICBkIKysrGQgIGQgZCAgrCAgZCAgZCCsICBkICAgZKwgIGQgICAgIGQgZCCsICAgZCBkIGRkICBkIGQgIKwgIGQgZCBkIGQgZCBkICBkZCBkIGQgICCsIGQgZCBkICBkZCBkICBkIGRkIGQgICBkZGQgZCAgICCsZCBkICBkZKxkIGQgrGRkrGQgZCBkICAgrCBkICBkICCsIGQgrGQgIKwgZCAgIGQgrCBkICAgIGSsIGQgICCsZKwgZCAgICAgIGRkIKwgICAgZGQgZGQgICBkZCAgrCAgIGRkIGQgZCAgZGQgIGRkICBkZCAgIKwgIGRkIGQgIGQgZGQgIGQgZCBkZCBkrCBkIGRkICAgZGQgZGQgIGSsZCBkZCAgICCsIGRkIGQgICBkZGQgIGQgIGRkZCAgIGQgZGRkICAgIGRkZGQgICAgIKxkZCAgZGQgrGRkIGSsIGSsZGQgZCAgICCsZCAgZCAgIKxkICAgZCAgrGQgrCBkICCsZCAgICBkIKxkICBkZGQgrGQgrGQgrCCsZCAgICAgZKxkIGRkICBkrGQgrGSsZGSsZCBkIGRkrKxkIGSsrKysrGQgICAgICAgrCCsICAgICCsIGRkICAgIKwgIKwgICAgrCCsrCAgICCsIGQgZCAgIKwgIGRkICAgrCAgIKwgICCsIGQgIGQgIKwgIGQgZCAgrCAgIGRkICCsICAgIKwgIKwgICCsrCAgrCBkICAgZCCsICBkICBkIKwgICBkIGQgrCAgICBkZCCsICCsIGRkIKwgZGSsZGQgrCAgICAgrCCsIKwgrCCsIKwgICAgrKwgrCAgrKysrCCsIGQgICAgZKwgIGQgICBkrCAgIGQgIGSsIGSsrCAgZKwgICAgZCBkrCAgICAgZGSsIGQgIGRkZKwgrCBkZGRkrCBkrGSsZGSsIGQgICCsZKwgrKxkIKxkrCCsZKysrGSsICAgICAgrKwgIKwgICCsrCCsrCAgIKysICAgrCAgrKwgZGRkZCCsrCAgrCCsIKysIKwgrKwgrKwgIKysZGSsrCAgIGSsZKysICCsICCsrKwgICCsIKysrCCsICCsrKysICCsIKysrKwgrKwgrKysrCBkICAgICAgZCBkICAgICBkrGQgICAgIGRkrCAgICAgZCAgZCAgICBkrCBkICAgIGRkZGQgICAgZCCsZCAgICBkZCCsICAgIGQgZKwgICAgZCAgIGQgICBkrCAgZCAgIGRkZCBkICAgZCCsIGQgICBkrKwgZCAgIGRkIGRkICAgZCBkZGQgICBkICCsZCAgIGRkZKxkICAgZGQgIKwgICBkIGQgrCAgIGQgIGSsICAgZCAgICBkICBkrCAgIGQgIGRkZCAgZCAgZCCsICBkICBkZCBkIGQgIGQgZGQgZCAgZCAgrCBkICBkZCAgZGQgIGQgZCBkZCAgZCAgZGRkICBkICAgrGQgIGRkZCCsZCAgZKwgrKxkICBkZCAgIKwgIGQgZCAgrCAgZCAgZCCsICBkrCBkIKwgIGRkrKwgrCAgZCAgIGSsICBkICAgICBkIGSsICAgIGQgZGRkICAgZCBkIKwgICBkIGRkIGQgIGQgZCBkZCAgZCBkZKxkICBkIGQgIKwgIGQgZGQgIGQgZCBkIGQgZCBkIGQgIGRkIGQgZCAgIKwgZCBkIGRkrCBkIGRkICAgZGQgZCBkICBkZCBkICBkIGRkIGQgZKwgZGQgZCAgIGRkZCBkrKxkrGRkIGQgICAgrGQgZKysICCsZCBkIGQgZKxkIGQgIGRkrGQgZGQgICAgrCBkIGQgICCsIGQgIGQgIKwgZCAgIGQgrCBkZGQgZCCsIGQgZGRkIKwgZKwgrGQgrCBkICAgIGSsIGRkIGQgZKwgZCBkIGRkrCBkICBkZGSsIGRkrKxkZKwgZCBkICCsrCBkICAgICAgZGSsICAgICBkZGRkICAgIGRkIKwgICAgZGRkIGQgICBkZCBkZCAgIGRkICCsICAgZGQgrKwgICBkZGQgIGQgIGRkIGQgZCAgZGQgIGRkICBkZCAgIKwgIGRkZCAgIGQgZGQgZCAgZCBkZCAgZCBkIGRkZGRkIGQgZGQgICBkZCBkZKwgIGRkIGRkICAgIKwgZGQgZCBkrCBkZKysrKysIGRkZCAgICBkZGQgZCAgIGRkZCAgZCAgZGRkZCCsICBkZGQgICBkIGRkZCAgrGQgZGRkZCAgrCBkZGRkIKysIGRkZCAgICBkZGRkIKwgIGRkZGQgICCsZGRkZCCsIKxkZGRkZCCsIKxkZGQgrKxkrGRkZGQgrKysZGRkICAgICCsZGQgZGQgIKxkZGQgIGQgrGRkICBkZCCsZGRkrGSsIKxkZKysZCBkrGRkICAgZGSsZGSsICBkZKxkZGRkIKysrGRkZCAgICAgrGQgZCAgICCsZCAgZCAgIKxkICAgZCAgrGQgZGRkICCsZKwgrGQgIKxkrGQgrCAgrGRkrKysICCsZCAgICBkIKxkIGSsIKwgrGSsrCBkrCCsZKwgZKysIKxkICAgICBkrGSsZGQgIGSsZCAgZCBkZKxkICAgZGRkrGRkZCBkZGSsZCBkrKxkZKxkZCAgICCsrGSsrKxkIKysZGRkrCBkrKxkrGQgIKysrGQgZGRkrKysZKwgrGSsrKxkICAgICAgIKysICAgICAgrGRkICAgICCsIKwgICAgIKxkIGQgICAgrCBkZCAgICCsICCsICAgIKysrKwgICAgrGQgIGQgICCsIGQgZCAgIKwgIGRkICAgrCAgIKwgICCsrCAgrCAgIKwgrKysICAgrKysrKwgICCsZCAgIGQgIKwgZCAgZCAgrKxkICBkICCsICBkIGQgIKwgICBkZCAgrGQgZGRkICCsZKxkZGQgIKwgICAgrCAgrCAgrCCsICCsICAgrKwgIKysICCsrCAgrCAgrKysICCsIKysrKwgIKxkICAgIGQgrCBkICAgZCCsICBkICBkIKysIGQgIGQgrGRkZCAgZCCsICAgZCBkIKwgIKxkIGQgrGSsIKwgZCCsICAgIGRkIKwgZCBkZGQgrGRkrKxkZCCsIKxkIKxkIKysrKxkrGQgrCAgICAgrCCsIKwgICCsIKxkZKwgIKwgrKysZGQgrCCsICAgrCCsIKysICCsIKwgrCCsrKwgrCCsrGQgIGSsIKysIKwgrKwgrCAgIKysrCCsIKwgrKysIKysZGSsrKwgrCCsrKysrCCsZCAgICAgZKwgZCAgICBkrCAgZCAgIGSsICAgZCAgZKysZGRkICBkrCBkIKwgIGSsICAgIGQgZKysIKwgZCBkrCBkrGRkIGSsrGRkZKwgZKxkrCCsrCBkrCAgICAgZGSsZGQgICBkZKwgZCBkIGRkrCAgZGQgZGSsIKxkZCBkZKxkrKwgZGRkrCAgZKxkZGSsrCBkrGRkZKxkICBkrGRkrGQgZGQgrGSsrGSsrCCsZKxkrCBkZKxkrGRkZCCsrGSsICCsZKysZKwgICAgICCsrKwgICAgIKysIKwgICAgrKysrCAgICCsrCAgrCAgIKysrKysICAgrKwgIKysICCsrGQgZGRkIKysZKxkZGQgrKysZKysZCCsrCAgICCsIKysrCAgIKwgrKwgrCAgrCCsrKysrCCsIKysICAgrKwgrKwgIKysrCCsrCAgIGQgZKysZGRkrCBkrKxkZKxkrGSsrCCsZKysZKysrKwgICCsrKwgIKwgIKysrKwgrCAgrKysIKysICCsrKwgIKysIKysrCCsrKwgrKysIGQgIGSsrKwgZCCsZKysrKxkIKxkrKysIKysIKysrKysrKwgrKysrGQgZKysrKysrKysrKysrKw==");
|
| 8 |
+
export const iq2s_grid = /* 8192B */ D("CAgICAgICAgrCAgICAgICBkZCAgICAgICCsICAgICAgrKwgICAgICBkIGQgICAgICBkZCAgICAgrGRkICAgICBkrGQgICAgICAgrCAgICAgrCCsICAgICBkZKwgICAgICCsrCAgICAgZCAgZCAgICAgZCBkICAgIKxkIGQgICAgZKwgZCAgICAgIGRkICAgIKwgZGQgICAgZGRkZCAgICAgrGRkICAgIGQgrGQgICAgIGSsZCAgICCsZKxkICAgIGSsrGQgICAgICAgrCAgICCsICCsICAgIGRkIKwgICAgIKwgrCAgICBkIGSsICAgICBkZKwgICAgICCsrCAgICBkZKysICAgIKysrKwgICAgZCAgIGQgICAgZCAgZCAgIKxkICBkICAgZKwgIGQgICAgIGQgZCAgIKwgZCBkICAgZGRkIGQgICAgrGQgZCAgIGQgrCBkICAgIGSsIGQgICAgICBkZCAgIKwgIGRkICAgZGQgZGQgICAgrCBkZCAgIGQgZGRkICAgIGRkZGQgICCsZGRkZCAgIGSsZGRkICAgICCsZGQgICBkZKxkZCAgICCsrGRkICAgZCAgrGQgICAgZCCsZCAgICAgZKxkICAgrCBkrGQgICBkZGSsZCAgIGQgrKxkICAgIGSsrGQgICAgICAgrCAgIKwgICCsICAgZGQgIKwgICAgrCAgrCAgIGQgZCCsICAgIGRkIKwgICAgIKwgrCAgIKysrCCsICAgZCAgZKwgICAgZCBkrCAgIKxkIGSsICAgZKwgZKwgICAgIGRkrCAgIGRkZGSsICAgICAgrKwgICBkZCCsrCAgIKysIKysICAgIGRkrKwgICCsIKysrCAgIGQgICAgZCAgIGQgICBkICCsZCAgIGQgIGSsICAgZCAgICBkICBkICCsIGQgIGQgIGRkZCAgZCAgIKxkICBkICBkIKwgIGQgICBkrCAgZCAgrGSsICBkICBkrKwgIGQgICAgIGQgZCAgrCAgZCBkICBkZCBkIGQgICCsIGQgZCAgrKwgZCBkICBkIGRkIGQgICBkZGQgZCAgrGRkZCBkICBkrGRkIGQgICAgrGQgZCAgrCCsZCBkICBkZKxkIGQgIGQgIKwgZCAgIGQgrCBkICCsZCCsIGQgIGSsIKwgZCAgICBkrCBkICBkZGSsIGQgICCsZKwgZCAgZCCsrCBkICAgZKysIGQgICAgICBkZCAgrCAgIGRkICBkZCAgZGQgICCsICBkZCAgrKwgIGRkICBkIGQgZGQgICBkZCBkZCAgrGRkIGRkICBkrGQgZGQgICAgrCBkZCAgZGSsIGRkICAgrKwgZGQgIGQgIGRkZCAgIGQgZGRkICCsZCBkZGQgIGSsIGRkZCAgICBkZGRkICCsIGRkZGQgIGRkZGRkZCAgIKxkZGRkICBkIKxkZGQgICBkrGRkZCAgICAgrGRkICCsICCsZGQgIGRkIKxkZCAgIKwgrGRkICBkIGSsZGQgICBkZKxkZCAgICCsrGRkICBkICAgrGQgICBkICCsZCAgrGQgIKxkICBkrCAgrGQgICAgZCCsZCAgZGRkIKxkICAgICBkrGQgIGRkIGSsZCAgIKwgZKxkICBkIGRkrGQgICBkZGSsZCAgICCsZKxkICBkICCsrGQgICBkIKysZCAgICBkrKxkICAgICAgIKwgIKwgICAgrCAgZGQgICCsICAgrCAgIKwgIGQgZCAgrCAgIGRkICCsICCsZGQgIKwgIGSsZCAgrCAgICCsICCsICBkZKwgIKwgIKysrCAgrCAgZCAgZCCsICAgZCBkIKwgIKxkIGQgrCAgZKwgZCCsICAgIGRkIKwgIKwgZGQgrCAgZGRkZCCsICAgrGRkIKwgIGQgrGQgrCAgIGSsZCCsICAgICCsIKwgIGRkIKwgrCAgIGRkrCCsICCsrKysIKwgIGQgICBkrCAgIGQgIGSsICAgIGQgZKwgIKwgZCBkrCAgZGRkIGSsICAgrGQgZKwgIGQgrCBkrCAgICAgZGSsICBkZCBkZKwgICCsIGRkrCAgZCBkZGSsICAgZGRkZKwgICAgrGRkrCAgZCAgrGSsICAgIGSsZKwgICAgICCsrCAgZCBkIKysICAgZGQgrKwgIKwgrCCsrCAgIKysIKysICCsrKwgrKwgICAgZGSsrCAgZKxkrKysICBkICAgICBkICBkICAgIGQgrGQgICAgZCBkrCAgICBkICAgZCAgIGQgrCBkICAgZCBkZGQgICBkICCsZCAgIGQgZCCsICAgZCAgZKwgICBkIKxkrCAgIGQgICAgZCAgZCCsICBkICBkIGRkIGQgIGQgIKwgZCAgZCBkIGRkICBkICBkZGQgIGQgrGRkZCAgZCBkrGRkICBkICAgrGQgIGQgrCCsZCAgZCBkZKxkICBkICCsrGQgIGQgZCAgrCAgZCAgZCCsICBkIKxkIKwgIGQgICBkrCAgZCBkZGSsICBkICCsZKwgIGQgZCCsrCAgZCAgZKysICBkICAgICBkIGQgrCAgIGQgZCBkZCAgZCBkICCsICBkIGQgrKwgIGQgZCBkIGQgZCBkICBkZCBkIGQgrGRkIGQgZCBkrGQgZCBkICAgrCBkIGQgrCCsIGQgZCBkZKwgZCBkICCsrCBkIGQgZCAgZGQgZCAgZCBkZCBkIKxkIGRkIGQgZKwgZGQgZCAgIGRkZCBkIKwgZGRkIGQgZGRkZGQgZCAgrGRkZCBkIGQgrGRkIGQgIGSsZGQgZCAgICCsZCBkIKwgIKxkIGQgZGQgrGQgZCAgrCCsZCBkIGQgZKxkIGQgIGRkrGQgZCBkICAgrCBkICBkICCsIGQgZKwgIKwgZCAgIGQgrCBkIGRkZCCsIGQgZCCsIKwgZCAgZKwgrCBkICAgIGSsIGQgZGQgZKwgZCBkIGRkrCBkICBkZGSsIGQgZCAgrKwgZCAgZCCsrCBkICAgZKysIGQgICAgICBkZCCsICAgIGRkIGRkICAgZGQgIKwgICBkZCBkIGQgIGRkICBkZCAgZGQgrGRkICBkZCBkrGQgIGRkICAgrCAgZGQgZGSsICBkZCAgrKwgIGRkIGQgIGQgZGQgIGQgZCBkZCCsZCBkIGRkIGSsIGQgZGQgICBkZCBkZCCsIGRkIGRkIGRkZGQgZGQgIKxkZCBkZCBkIKxkIGRkICBkrGQgZGQgICAgrCBkZCCsICCsIGRkIGRkIKwgZGQgIKwgrCBkZCBkIGSsIGRkICBkZKwgZGQgICCsrCBkZCBkICAgZGRkICBkICBkZGQgrGQgIGRkZCBkrCAgZGRkICAgZCBkZGQgrCBkIGRkZCBkZGQgZGRkICCsZCBkZGQgZCCsIGRkZCAgZKwgZGRkICAgIGRkZGQgrCAgZGRkZCBkZCBkZGRkICCsIGRkZGQgZCBkZGRkZCAgZGRkZGRkICAgrGRkZGQgZCAgrGRkZCAgZCCsZGRkICAgZKxkZGQgICAgIKxkZCBkZCAgrGRkICCsICCsZGQgZCBkIKxkZCAgZGQgrGRkICAgrCCsZGQgZCAgZKxkZCAgZCBkrGRkICAgZGSsZGQgICAgrKxkZCCsrKysrGRkIGQgICAgrGQgIGQgICCsZCCsZCAgIKxkIGSsICAgrGQgICBkICCsZCBkZGQgIKxkICCsZCAgrGQgZCCsICCsZCAgICBkIKxkIKwgIGQgrGQgZGQgZCCsZCAgrCBkIKxkIGQgZGQgrGQgIGRkZCCsZCAgIKxkIKxkIGQgIKwgrGQgIGQgrCCsZCAgICAgZKxkIKwgICBkrGQgZGQgIGSsZCAgrCAgZKxkIGQgZCBkrGQgIGRkIGSsZCAgIKwgZKxkIGQgIGRkrGQgIGQgZGSsZCAgIGRkZKxkIGSsrGRkrGQgrCCsrGSsZCAgZCAgrKxkICAgZCCsrGQgICAgZKysZCCsZGRkrKxkICAgICAgIKwgrCAgICAgrCBkZCAgICCsICCsICAgIKwgZCBkICAgrCAgZGQgICCsIKxkZCAgIKwgZKxkICAgrCAgIKwgICCsIGRkrCAgIKwgrKysICAgrCBkICBkICCsICBkIGQgIKwgICBkZCAgrCCsIGRkICCsIGRkZGQgIKwgIGSsZCAgrCAgICCsICCsIKysIKwgIKwgIGRkrCAgrCCsrKysICCsIGQgICBkIKwgIGQgIGQgrCAgIGQgZCCsIKwgZCBkIKwgZGRkIGQgrCBkIKwgZCCsICAgIGRkIKwgrCAgZGQgrCBkZCBkZCCsIGQgZGRkIKwgIGRkZGQgrCAgIKxkZCCsIGQgIKxkIKwgIGQgrGQgrCAgIGSsZCCsICAgICCsIKwgrKwgIKwgrCCsIKwgrCCsICCsrCCsIKwgrKysIKwgrCAgZCBkrCCsICAgZGSsIKwgIKwgrKwgrCCsrCCsrCCsICCsrKysIKwgZCAgICBkrCAgZCAgIGSsIKxkICAgZKwgZKwgICBkrCAgIGQgIGSsIGRkZCAgZKwgIKxkICBkrCBkIKwgIGSsICBkrCAgZKwgICAgZCBkrCCsICBkIGSsIGRkIGQgZKwgIKwgZCBkrCBkIGRkIGSsICBkZGQgZKwgICCsZCBkrCBkICCsIGSsICBkIKwgZKwgICBkrCBkrCAgICAgZGSsIGRkICBkZKwgIKwgIGRkrCBkIGQgZGSsICBkZCBkZKwgICCsIGRkrCBkICBkZGSsICBkIGRkZKwgICBkZGRkrCCsZKxkZGSsICAgIKxkZKwgZCAgIKxkrCAgZCAgrGSsICAgZCCsZKwgICAgZKxkrCBkrGRkrGSsICAgICAgrKwgZGQgICCsrCBkIGQgIKysICBkZCAgrKwgZCAgZCCsrCAgZCBkIKysICAgZGQgrKwgrKwgrCCsrCCsrKysIKysIGQgICBkrKwgIGQgIGSsrCAgIGQgZKysIGRkZKxkrKwgrKwgIKysrCCsIKwgrKysICBkrGSsrKwgIKwgrKysrCCsrCCsrKysIGQgICAgICBkIGQgICAgIGSsZCAgICAgZGSsICAgICBkICBkICAgIGSsIGQgICAgZGRkZCAgICBkIKxkICAgIGSsrGQgICAgZGQgrCAgICBkIGSsICAgIGSsZKwgICAgZCAgIGQgICBkrCAgZCAgIGRkZCBkICAgZCCsIGQgICBkrKwgZCAgIGRkIGRkICAgZCBkZGQgICBkrGRkZCAgIGRkrGRkICAgZCAgrGQgICBkrCCsZCAgIGRkZKxkICAgZGQgIKwgICBkIGQgrCAgIGQgIGSsICAgZGRkZKwgICBkIKxkrCAgIGRkIKysICAgZCBkrKwgICBkICAgIGQgIGSsICAgZCAgZGRkICBkICBkIKwgIGQgIGRkIGQgZCAgZCBkZCBkICBkrGRkIGQgIGRkrGQgZCAgZCAgrCBkICBkrCCsIGQgIGRkZKwgZCAgZGQgIGRkICBkIGQgZGQgIGSsZCBkZCAgZGSsIGRkICBkICBkZGQgIGSsIGRkZCAgZGRkZGRkICBkIKxkZGQgIGRkIKxkZCAgZCBkrGRkICBkICAgrGQgIGSsICCsZCAgZGRkIKxkICBkIKwgrGQgIGRkIGSsZCAgZCBkZKxkICBkICCsrGQgIGRkICAgrCAgZCBkICCsICBkICBkIKwgIGSsIGQgrCAgZGRkZCCsICBkIKxkIKwgIGQgZKwgrCAgZCAgIGSsICBkZGQgZKwgIGQgrCBkrCAgZGQgZGSsICBkIGRkZKwgIGQgIKxkrCAgZGQgIKysICBkIGQgrKwgIGQgICAgIGQgZKwgICAgZCBkZGQgICBkIGQgrCAgIGQgZKysICAgZCBkZCBkICBkIGQgZGQgIGQgZKxkZCAgZCBkZKxkICBkIGQgIKwgIGQgZKwgrCAgZCBkZGSsICBkIGQgrKwgIGQgZGQgIGQgZCBkIGQgZCBkIGSsZCBkIGQgZGSsIGQgZCBkICBkZCBkIGSsIGRkIGQgZGRkZGQgZCBkIKxkZCBkIGRkIKxkIGQgZCBkrGQgZCBkICAgrCBkIGSsICCsIGQgZGRkIKwgZCBkIKwgrCBkIGRkIGSsIGQgZCBkZKwgZCBkICCsrCBkIGRkICAgZGQgZCBkICBkZCBkrGQgIGRkIGRkrCAgZGQgZCAgZCBkZCBkrCBkIGRkIGRkZGQgZGQgZCCsZCBkZCBkZCCsIGRkIGQgZKwgZGQgZCAgIGRkZCBkrCAgZGRkIGRkZCBkZGQgZCCsIGRkZCBkZCBkZGRkIGQgZGRkZGQgZCAgrGRkZCBkrKysZGRkIGRkICCsZGQgZCBkIKxkZCBkICBkrGRkIGQgICAgrGQgZKwgICCsZCBkZGQgIKxkIGQgrCAgrGQgZGQgZCCsZCBkIGRkIKxkIGQgIKwgrGQgZGQgIGSsZCBkIGQgZKxkIGQgIGRkrGQgZCAgIKysZCBkZGSsrKxkIGRkICAgIKwgZCBkICAgrCBkZKwgICCsIGQgIGQgIKwgZKwgZCAgrCBkZGRkICCsIGQgrGQgIKwgZGQgrCAgrCBkIGSsICCsIGQgICBkIKwgZKwgIGQgrCBkZGQgZCCsIGQgrCBkIKwgZGQgZGQgrCBkIGRkZCCsIGQgIKxkIKwgZCBkIKwgrCBkICBkrCCsIGQgICAgZKwgZKwgICBkrCBkZGQgIGSsIGQgrCAgZKwgZGQgZCBkrCBkIGRkIGSsIGQgIKwgZKwgZGQgIGRkrCBkIGQgZGSsIGQgIGRkZKwgZCAgIKxkrCBkrGRkrGSsIGRkICAgrKwgZCBkICCsrCBkICBkIKysIGQgICBkrKwgZCAgICAgIGRkrCAgICAgZGRkZCAgICBkZCCsICAgIGRkZCBkICAgZGQgZGQgICBkZKxkZCAgIGRkZKxkICAgZGQgIKwgICBkZKwgrCAgIGRkZGSsICAgZGQgrKwgICBkZGQgIGQgIGRkIGQgZCAgZGSsZCBkICBkZGSsIGQgIGRkICBkZCAgZGSsIGRkICBkZGRkZGQgIGRkIKxkZCAgZGRkIKxkICBkZCBkrGQgIGRkICAgrCAgZGSsICCsICBkZGRkIKwgIGRkIKwgrCAgZGRkIGSsICBkZCBkZKwgIGRkZCAgIGQgZGQgZCAgZCBkZKxkICBkIGRkZKwgIGQgZGQgIGQgZCBkZKwgZCBkIGRkZGRkIGQgZGQgrGQgZCBkZGQgrCBkIGRkIGSsIGQgZGQgICBkZCBkZKwgIGRkIGRkZGQgZGQgZGQgrCBkZCBkZGQgZGRkIGRkIGRkZGQgZGQgIKxkZCBkZGQgIKxkIGRkIGQgrGQgZGQgIGSsZCBkZCAgICCsIGRkZGQgIKwgZGQgrCAgrCBkZGQgZCCsIGRkIGRkIKwgZGQgIKwgrCBkZGQgIGSsIGRkIGQgZKwgZGQgIGRkrCBkZGSsrGSsIGRkICAgrKwgZGRkICAgIGRkZCBkICAgZGRkrGQgICBkZGRkrCAgIGRkZCAgZCAgZGRkrCBkICBkZGRkZGQgIGRkZCCsZCAgZGRkZCCsICBkZGQgZKwgIGRkZCAgIGQgZGRkrCAgZCBkZGRkZCBkIGRkZCCsIGQgZGRkZCBkZCBkZGQgZGRkIGRkZCAgrGQgZGRkZCAgrCBkZGQgZCCsIGRkZCAgZKwgZGRkICAgIGRkZGSsICAgZGRkZGRkICBkZGRkIKwgIGRkZGRkIGQgZGRkZCBkZCBkZGRkICCsIGRkZGRkICBkZGRkZCBkIGRkZGRkICBkZGRkZGQgICCsZGRkZGQgICCsZGRkIGQgIKxkZGQgIGQgrGRkZKxkrCCsZGRkICAgZKxkZGQgICAgIKxkZKwgICAgrGRkZGQgICCsZGQgrCAgIKxkZGQgZCAgrGRkIGRkICCsZGQgIKwgIKxkZGQgIGQgrGRkIGQgZCCsZGQgIGRkIKxkZKysZGQgrGRkICAgrCCsZGRkICAgZKxkZCBkICBkrGRkICBkIGSsZGQgICBkZKxkZCAgICCsrGRkZKxkIKysZGRkZCCsrKxkZCCsrKysrGRkZCAgICAgrGQgZCAgICCsZKxkICAgIKxkICBkICAgrGSsIGQgICCsZGRkZCAgIKxkIKxkICAgrGRkIKwgICCsZCBkrCAgIKxkICAgZCAgrGRkZCBkICCsZCCsIGQgIKxkZCBkZCAgrGQgZGRkICCsZCAgrGQgIKxkIGQgrCAgrGQgIGSsICCsZCAgICBkIKxkrCAgIGQgrGRkZCAgZCCsZCCsICBkIKxkZCBkIGQgrGQgZGQgZCCsZCAgrCBkIKxkZCAgZGQgrGQgZCBkZCCsZCAgZGRkIKxkICAgrGQgrGRkrGSsZCCsZCBkICCsIKxkICBkIKwgrGQgICBkrCCsZKxkZGSsIKxkZCCsrKwgrGQgICAgIGSsZGRkICAgZKxkIKwgICBkrGRkIGQgIGSsZCBkZCAgZKxkICCsICBkrGRkICBkIGSsZCBkIGQgZKxkICBkZCBkrGQgICCsIGSsZGQgICBkZKxkIGQgIGRkrGQgIGQgZGSsZCAgIGRkZKxkrKwgZGRkrGQgrKxkZGSsZKwgZKxkZKxkICAgIKxkrGQgZGSsrGSsZGQgICAgrKxkIGQgICCsrGQgIGQgIKysZGRkrGQgrKxkIKxkrCCsrGQgICAgZKysZKysrCBkrKxkrCAgZKysrGRkIKysrKysZCAgICAgICCsrCAgICAgIKxkZCAgICAgrCCsICAgICCsZCBkICAgIKwgZGQgICAgrGSsZCAgICCsICCsICAgIKxkZKwgICAgrGQgIGQgICCsIGQgZCAgIKwgIGRkICAgrKwgZGQgICCsZGRkZCAgIKwgrGRkICAgrGQgrGQgICCsICAgrCAgIKxkZCCsICAgrGQgZKwgICCsIGRkrCAgIKxkICAgZCAgrCBkICBkICCsZKwgIGQgIKwgIGQgZCAgrKwgZCBkICCsZGRkIGQgIKwgrGQgZCAgrGQgrCBkICCsIGSsIGQgIKwgICBkZCAgrKwgIGRkICCsZGQgZGQgIKwgrCBkZCAgrGQgZGRkICCsIGRkZGQgIKxkICCsZCAgrCBkIKxkICCsICBkrGQgIKxkrKysZCAgrCAgICCsICCsZGQgIKwgIKysrCAgrCAgrGQgZCCsICCsIGRkIKwgIKxkICBkrCAgrCBkIGSsICCsICBkZKwgIKxkICAgIGQgrCBkICAgZCCsrGQgICBkIKxkrCAgIGQgrCAgZCAgZCCsrCBkICBkIKxkZGQgIGQgrCCsZCAgZCCsZCCsICBkIKwgICBkIGQgrKwgIGQgZCCsZGQgZCBkIKwgrCBkIGQgrGQgZGQgZCCsIGRkZCBkIKwgIKxkIGQgrGQgIKwgZCCsIGQgrCBkIKwgIGSsIGQgrCAgICBkZCCsrCAgIGRkIKxkZCAgZGQgrCCsICBkZCCsZCBkIGRkIKwgZGQgZGQgrCAgrCBkZCCsZCAgZGRkIKwgZCBkZGQgrCAgZGRkZCCsICAgrGRkIKysrCCsZGQgrGQgICCsZCCsIGQgIKxkIKwgIGQgrGQgrGSsrCCsZCCsICAgZKxkIKwgICAgIKwgrGRkICAgrCCsZCBkICCsIKwgZGQgIKwgrGQgIGQgrCCsIGQgZCCsIKwgIGRkIKwgrKwgrKwgrCCsZCAgIGSsIKwgZCAgZKwgrCAgIGRkrCCsZGSsZGSsIKysIKwgrKwgrCCsZGSsrCCsrKxkZKysIKysICCsrKwgrKwgrKysrCCsZCAgICAgZKwgZCAgICBkrGSsICAgIGSsICBkICAgZKysIGQgICBkrGRkZCAgIGSsIKxkICAgZKwgZKwgICBkrCAgIGQgIGSsrCAgZCAgZKxkZCBkICBkrCCsIGQgIGSsZCBkZCAgZKwgZGRkICBkrCAgrGQgIGSsZCAgrCAgZKwgZCCsICBkrCAgZKwgIGSsICAgIGQgZKxkZCAgZCBkrGQgZCBkIGSsIGRkIGQgZKxkICBkZCBkrCBkIGRkIGSsICBkZGQgZKysrGRkZCBkrGQgICCsIGSsIGQgIKwgZKwgIGQgrCBkrCAgIGSsIGSsrGSsrKwgZKwgICAgIGRkrKwgICAgZGSsZGQgICBkZKwgrCAgIGRkrGQgZCAgZGSsIGRkICBkZKwgIKwgIGRkrGQgIGQgZGSsIGQgZCBkZKwgIGRkIGRkrCAgIKwgZGSsrGRkrCBkZKxkICAgZGRkrCBkICBkZGSsICBkIGRkZKwgICBkZGRkrCCsZKxkZGSsZCCsrGRkZKwgICAgrGRkrKxkIGSsZGSsIGSsZKxkZKxkICAgIKxkrCBkICAgrGSsICBkICCsZKysZKwgIKxkrCAgIGQgrGSsZKysrCCsZKwgICAgZKxkrGSsIGRkrGSsrCBkZGSsZKwgIGSsrKxkrCAgICAgIKysZGQgICAgrKysrCAgICCsrCBkZCAgIKysrCCsICAgrKysrKwgICCsrGQgIGQgIKysIGQgZCAgrKwgIGRkICCsrKwgrKwgIKysrKysrCAgrKwgICBkZCCsrGRkrGRkIKysrCAgIKwgrKysrCAgrCCsrKwgrCCsIKysIKysIKwgrKysrKwgrCCsrKwgIKysIKysIKwgrKwgrKysrCCsrCCsrCCsrKysIKysZCAgICBkrKwgZCAgIGSsrCAgZCAgZKysICAgZCBkrKxkrCCsIGSsrCBkrKwgZKysICAgIGRkrKxkrGQgZGSsrGQgZGSsZKysrKwgICCsrKwgrKwgIKysrKwgrKwgrKysIGRkZGSsrKysZCCsZKysrCCsICCsrKysrKwgIKysrKwgIKwgrKysrKwgrCCsrKysIKysIKysrKwgrCCsrKysrKysrKysrKys=");
|
| 9 |
+
export const iq3xxs_grid = /* 1024B */ D("BAQEBBQEBAQkBAQEDAwEBBwMBAQ+DAQEBBQEBBQUBAQMHAQEFCQEBBw+BAQsPgQEDAQMBBwEDAQEDAwEFAwMBAwUDAQsFAwEBBwMBBQcDAQMJAwEJCwMBAQ+DAQEBBQEFAQUBCQEFAQMDBQEBBQUBBQUFAQMHBQEHBwUBD4cFAQMLBQEPiwUBCw+FAQMBBwEPgQcBAQMHAQUDBwELBQcBAQ+HAQcDCQEPhwkBCQkJAQ+LCQEHD4kBCw+JAQMBCwEPgQsBBQcLAQULCwELBw0BCQ0NAQEDD4EJAw+BDQMPgQcJD4EDDQ+BAwEBAwcBAQMBAwEDBQMBAwMFAQMHBQEDAQcBAwUHAQMJBwEDD4kBAwELAQMBAQMDBQEDAwMDAwMBBQMDBQUDAwMBBQMHAQUDAQMFAwUDBQMDBQUDAQcFAwUPhQMBAQcDBQEHAwEFBwMDBwcDDQkHAw0NBwMDAQkDCwEJAwELCQMBBQsDCQULAw0JCwMDD4sDCwENAwUFD4MBCQ+DAQEBBQUBAQUDAwEFBwMBBQEFAQUFBQEFDQUBBQMHAQUFCQEFAwEDBQcBAwULAQMFAQMDBQUDAwUDBQMFAQcDBQcNAwUPjQMFAQ+DBQEBBQUFAQUFAwMFBQ+DBQUBBQUFBQUFBQ+HBQUBCQUFCwsFBQMBBwUBAwcFCQMHBQEPhwUJD4cFCwcJBQcLCQUHAQsFD4ULBQMJCwUJD4sFAwEPhQcBD4UNAw+FCwkPhQMBAQcBAwEHBQMBBwMFAQcHBQEHAQsBBwsNAQcFD4EHAQEDBwUBAwcBBQMHAwcDBwkJAwcNCQMHAwEFBwcBBQcBAwUHCwUFBwULBQcFD4UHAwMHBwcHBwcBBwkHD4kJBwUPiQcBAQsHDQELBwUFCwcLCwsHCQMNBw0HDQcHDQ0HBwcPhwEND4cJAQEJD4MBCQsHAQkPhwEJBwsBCQ+LAQkJD4MJAQUFCQ+HBQkBCQUJAQ0FCQ0NBQkPgQcJCwkHCQkBCQkDCwkJCQ0JCQsFCwkHCQsJAQ+LCQsBD4kBAw+JBQMPiQEHD4kFAwELAwkBCwEPgQsBAQMLDQEDCw0FAwsLCwMLCQMFCwUHBQsFD4ULBQEHCwcLBwsBAwkLBwUJCw+FCQsFD4kLBQELCwMHCwsBCw0LCQUPiwUJD4sJBQENCQkBDQ0JAQ0JDQENAwUDDQMNAw0PgwUNCQ0FDQEHBw0NBwcNCQkJDQsBCw0FCwsNBwcNDQcBD40DBQ+NBwEBD4sBAQ+PgQEPgQMBD4UHAQ+FCwEPjQUDD4EJAw+FAwUPiwkFD4ULBQ+BAQcPiwMHD4cHBw+BDQcPgwUJD4MJCQ+BAQsPhQELD4kFCw+BBw0Pg==");
|
| 10 |
+
export const iq3s_grid = /* 2048B */ D("AQEBAQMBAQEFAQEBCwEBAQ8BAQEBAwEBAwMBAQUDAQEJAwEBDQMBAQEFAQEDBQEBCwUBAQcHAQEBCQEBBQkBAQsJAQEPCQEBAwsBAQcLAQEBDQEBBQ0BAQMPAQEJDwEBDw8BAQEBAwEDAQMBBQEDAQkBAwEBAwMBAwMDAQsDAwEBBQMBBwUDAQ8FAwEDBwMBCwcDAQkJAwEDDQMBCw0DAQUPAwEBAQUBAwEFAQsBBQEPAQUBAQMFAQcDBQENAwUBAwUFAQsFBQEBBwUBCQcFAQUJBQELCQUBDwkFAQMLBQEHCwUBAQ8FAQcPBQEHAQcBAwMHAQsDBwEBBQcBBQUHAQMHBwEHBwcBDQcHAQkJBwEBCwcBBQsHAQ8NBwEDDwcBCw8HAQEBCQEHAwkBDwMJAQMFCQEJBQkBBQcJAQEJCQEHCQkBAwsJAQEPCQEFAQsBCQELAQEFCwEFBQsBDQULAQcHCwEDCQsBCwkLAQ8JCwENDQsBBw8LAQ0BDQEDAw0BBwMNAQMHDQEFCw0BAw8NAQEBDwEFAQ8BCQEPAQEFDwEFBQ8BDQUPAQcHDwEBCw8BCQsPAQEBAQMDAQEDBQEBAwkBAQMBAwEDAwMBAwcDAQMLAwEDDwMBAwEFAQMFBQEDAwcBAwkHAQMNBwEDCQsBAw0LAQMDDQEDBQ8BAwEBAwMDAQMDBwEDAw0BAwMBAwMDCQMDAwMFAwMBBwMDBwcDAwMJAwMBCwMDBQsDAwEPAwMNDwMDAQEFAwUDBQMLAwUDDwMFAwEFBQMJBQUDBQcFAwEJBQMHCQUDCwsFAwENBQMFDwUDAwEHAwkBBwMPAQcDAQMHAwcDBwMDBQcDDwUHAwEHBwMJBwcDAwkHAwUNBwMBDwcDBwEJAwsBCQMFAwkDCQMJAwMHCQMHBwkDBQkJAw0JCQMBCwkDCQsJAwMBCwMBAwsDBwMLAwMFCwMBBwsDBQcLAwMLCwMBBQ0DCQUNAw8FDQMJCQ0DDQkNAwMBDwMHAQ8DAQMPAwUDDwMDBQ8DCwcPAwMJDwMFDQ8DAQ8PAwEBAQUDAQEFBwEBBQsBAQUPAQEFAQMBBQUDAQUJAwEFDQMBBQMFAQUHBQEFDwUBBQEHAQUFBwEFAwkBBQcJAQULCQEFAQsBBQULAQUPDQEFAQ8BBQcPAQULDwEFAQEDBQUBAwUBAwMFBwMDBQ8DAwUFBQMFCwUDBQMHAwUJBwMFBQkDBQMLAwUDAQUFCQEFBQ8BBQUDBQUFBwUFBQEHBQUPBwUFAwkFBQcLBQUPCwUFAw8FBQkPBQUBAQcFBQEHBQsBBwUDAwcFBQUHBQkFBwUDBwcFBwcHBQUJBwUBCwcFDQ0HBQMBCQUPAQkFAQUJBQcFCQUFBwkFCwcJBQMJCQUFDwkFCw8JBQkBCwUDAwsFBQULBQ8HCwUBCQsFBwsLBQEPCwUBAQ0FBQENBQ8BDQUDBQ0FCwsNBQMNDQULAQ8FAwMPBQ0FDwUBBw8FBwkPBQELDwUFAQEHAwMBBwcDAQcLAwEHDwMBBwUFAQcDBwEHBwcBBwsHAQcFCQEHCQkBBw8JAQcDCwEHBw0BBwMPAQcDAQMHBwEDBwsBAwcJAwMHAwUDBwcFAwcBCQMHAQ0DBwUPAwcNDwMHAQEFBwUDBQcBBQUHBQcFBwkHBQcBCwUHAwEHBwEDBwcJAwcHAwUHBwcFBwcPBQcHAQcHBwMJBwcHCQcHDwkHBwsLBwcHDwcHBwEJBwMDCQcNAwkHBQUJBwMHCQcFCwkHAQ0JBwkNCQcDAQsHAQMLBwUDCwcLBQsHBQcLBwkJCwcNCwsHBw8LBw0DDQcDCQ0HAwEPBwcBDwcBBQ8HBQUPBwsHDwcBAQEJCQEBCQUDAQkBBQEJCQUBCQ8FAQkFBwEJAwkBCQELAQkBDwEJBQEDCQ8BAwkDAwMJBwMDCQUFAwkBBwMJCwcDCQcJAwkDCwMJCwsDCQMBBQkHAQUJAQMFCQsDBQkDBQUJBwcFCQEJBQkPCwUJBQ0FCQEPBQkJAQcJAwMHCQcDBwkBBQcJBQUHCQMHBwkLBwcJAQEJCQUBCQkJBQkJDwcJCQEJCQkDDwkJCwELCQ8BCwkDBQsJBQ0LCQcDDQkJBw0JAQ0NCQEDDwkLAw8JAQcPCQcJDwkDCw8JBQEBCwEDAQsJAwELBQUBCwEJAQsJCQELDwkBCwULAQsNDQELCQ8BCwMBAwsHAQMLCwEDCwUDAwsDBQMLBQcDCwUPAwsBAQULAwMFCwcFBQsBBwULDQcFCwcLBQsFAQcLDwEHCwEDBwsPBQcLCQkHCwMLBwsLDQcLBw8HCwMBCQsJAQkLAQUJCwUHCQsNCQkLBQMLCw0FCwsDCwsLBwsLCwUJDQsFAQ8LCQEPCwUFDwsDAwENBwMBDQsDAQ0DBwENBwcBDQENAQ0BAQMNAQUDDQ8FAw0JDQMNBQMFDQkHBQ0FCQUNCwsFDQUNBQ0BDwUNAQEHDQkDBw0DBQcNAQkHDQsFCQ0HCQkNBQ0JDQEBCw0HAQsNCQcLDQENCw0LAQ0NAQkNDQMDDw0HAw8NAQEBDwkBAQ8PAQEPAQUBDwUFAQ8NBwEPAQkBDwkLAQ8FDQEPBQEDDwMDAw8JBQMPBwkDDwsJAw8DAQUPCQEFDwEDBQ8NAwUPAwUFDwEHBQ8DCwUPBQEHDwUHBw8LBwcPBwsHDwMBCQ8LAQkPBwMJDwEFCQ8BCwkPBQULDwUJCw8FAQ0PAwcNDwEBDw8=");
|
| 11 |
+
export const iq1s_grid = /* 16384B */ D("//////////8B/////////wAA/////////wH///////8BAf///////wD/AP//////AAAA/////////wH//////wH/Af///////wEB//////8BAQH//////wAA/wD/////AP8AAP//////AAAA/////wEAAAD/////AAABAP////////8B/////wH//wH//////wH/Af////8BAf8B/////wAAAAH///////8BAf////8B/wEB//////8BAQH/////AQEBAf//////AP//AP///wAA//8A////AP8A/wD/////AAD/AP///wEAAP8A////AAEA/wD///8BAQD/AP///wAAAf8A////AP//AAD///8BAP8AAP///wAB/wAA////Af8AAAD///8AAAAAAP///wEBAAAA////AP8BAAD/////AAEAAP///wEAAQAA/////wEBAAD///8AAP8BAP///wD/AAEA/////wAAAQD///8BAAABAP///wAAAQEA/////////wH///8B////Af////8B//8B////AQH//wH///8AAAD/Af//////Af8B////Af8B/wH/////AQH/Af///wEBAf8B////AAD/AAH///8A/wAAAf///wABAAAB/////wABAAH///8AAQEAAf///////wEB////Af//AQH/////Af8BAf///wEB/wEB////AP8AAQH///8AAAABAf///wABAAEB//////8BAQH///8B/wEBAf////8BAQEB////AQEBAQH///8A/wD//wD///8AAP//AP//AQAA//8A//8AAAH//wD//wD//wD/AP//AAH/AP8A//8AAAAA/wD//wEBAAD/AP///wABAP8A//8AAAEA/wD//wD/AAH/AP//AAEAAf8A//8AAAEB/wD//wD///8AAP///wD//wAA//8AAP//AAD//wEA//8AAP//AAAA/wAA////AQD/AAD//wEBAP8AAP//AAEB/wAA//////8AAAD//wAA/wAAAP//AQH/AAAA/////wAAAAD//wD/AAAAAP///wAAAAAA//8AAAAAAAD//wEAAAAAAP//AAEAAAAA/////wEAAAD//wH/AQAAAP//AAABAAAA////AQEAAAD//wEBAQAAAP//AP//AQAA//8A/wABAAD//wAAAAEAAP///wEAAQAA//8BAQABAAD//wD/AQEAAP///wABAQAA//8AAAEBAAD//wEAAQEAAP//AAEBAQAA////AAD/AQD//wABAP8BAP//AP//AAEA////AP8AAQD/////AAABAP//Af8AAAEA//8AAAAAAQD///8BAAABAP////8BAAEA//8A/wEAAQD//wEAAQABAP//AAEBAAEA//8AAP8BAQD//wD/AAEBAP///wAAAQEA//8AAQABAQD/////////Af//Af////8B////Af///wH//wEB////Af//AAAA//8B/////wH//wH//wH/Af//Af///wEB//8B//8BAQH//wH//wAA/wD/Af//AP8AAP8B//8BAAAA/wH//wAAAQD/Af//////Af8B//8B//8B/wH///8B/wH/Af//AQH/Af8B//8AAAAB/wH/////AQH/Af//Af8BAf8B////AQEB/wH//wEBAQH/Af//AAD//wAB//8A/wD/AAH///8AAP8AAf//AAEA/wAB////AAH/AAH//wAAAf8AAf//AP//AAAB/////wAAAAH//wD/AAAAAf//AAAAAAAB//8A/wEAAAH///8AAQAAAf//AAEBAAAB//8A/wABAAH///8AAAEAAf//AQAAAQAB//8AAQABAAH//wAAAQEAAf///////wEB//8B////AQH///8B//8BAf//AQH//wEB//8AAAD/AQH/////Af8BAf//Af8B/wEB////AQH/AQH//wEBAf8BAf//AAD/AAEB//8A/wAAAQH//wABAAABAf//AP8BAAEB//8AAAEAAQH//////wEBAf//Af//AQEB//8AAP8BAQH///8B/wEBAf//AQH/AQEB//8AAAABAQH/////AQEBAf//Af8BAQEB////AQEBAQH//wEBAQEBAf////8A////AP8A/wD///8A//8AAP///wD/AAEA////AP//AAH///8A/wAAAf///wD/AP//AP//AP//AP8A//8A////AAD//wD/AAAAAP//AP//AQAA//8A/wD/AQD//wD//wABAP//AP8AAAEA//8A/wABAQD//wD/AP8AAf//AP//AAAB//8A/wEAAAH//wD/AP8BAf//AP8AAAEB//8A/wD///8A/wD//wD//wD/AP8BAP//AP8A/wAB//8A/wD///8A/wD/AP8B/wD/AP8A/wAAAP8A/wD//wEA/wD/AP8A/wH/AP8A//8AAf8A/wD/AAEB/wD/AP8AAP8AAP8A/wEB/wAA/wD///8AAAD/AP8A/wAAAP8A/wH/AAAA/wD//wAAAAD/AP8AAAAAAP8A/wEAAAAA/wD/AAEAAAD/AP///wEAAP8A/wAAAQAA/wD//wD/AQD/AP8B/wABAP8A/wAAAAEA/wD/AP8BAQD/AP//AAEBAP8A/wD/AP8B/wD//wAA/wH/AP8BAAD/Af8A/wAAAf8B/wD/////AAH/AP8BAP8AAf8A/wAB/wAB/wD/Af8AAAH/AP8AAAAAAf8A//8BAAAB/wD/AQEAAAH/AP//AAEAAf8A/wEAAQAB/wD/AAD/AQH/AP8A/wABAf8A//8AAAEB/wD/AQAAAQH/AP8AAAEBAf8A/wD/////AAD/AQD///8AAP8AAf///wAA//8AAP//AAD/AAAA//8AAP//AQD//wAA/wABAP//AAD/AP8B//8AAP8BAAH//wAA/wD//wD/AAD/AAD/AP8AAP8BAP8A/wAA//8B/wD/AAD/AQH/AP8AAP8A/wAA/wAA//8AAAD/AAD/AAAAAP8AAP8BAAAA/wAA/wABAAD/AAD/Af8BAP8AAP8AAAEA/wAA//8BAQD/AAD//wD/Af8AAP8AAf8B/wAA////AAH/AAD//wAAAf8AAP8AAAAB/wAA//8BAAH/AAD/AAEAAf8AAP8BAQAB/wAA/wD/AQH/AAD//wABAf8AAP8AAAEB/wAA/wABAQH/AAD/Af///wAAAP8AAP//AAAA/wEB//8AAAD/AP8A/wAAAP//AAD/AAAA/wAAAP8AAAD/AQAA/wAAAP8AAQD/AAAA////Af8AAAD/Af8B/wAAAP8AAAH/AAAA//8BAf8AAAD/AQEB/wAAAP8A//8AAAAA//8A/wAAAAD/AAD/AAAAAP8BAP8AAAAA/wD/AAAAAAD/Af8AAAAAAP//AAAAAAAA/wAAAAAAAAD/AQAAAAAAAP8AAQAAAAAA/wEBAAAAAAD/AP8BAAAAAP//AAEAAAAA/wAAAQAAAAD/AQABAAAAAP8AAQEAAAAA/////wEAAAD/Af//AQAAAP//AP8BAAAA/wAA/wEAAAD//wH/AQAAAP8BAf8BAAAA////AAEAAAD/AP8AAQAAAP//AAABAAAA/wAAAAEAAAD/AQAAAQAAAP8AAQABAAAA/wEBAAEAAAD///8BAQAAAP8B/wEBAAAA/wAAAQEAAAD/AP///wEAAP//AP//AQAA/wAA//8BAAD/AQD//wEAAP8AAAD/AQAA/wEAAP8BAAD//wEA/wEAAP8BAQD/AQAA/wD/Af8BAAD/AQAB/wEAAP////8AAQAA/wH//wABAAD//wD/AAEAAP8AAP8AAQAA//8B/wABAAD/AQH/AAEAAP8A/wAAAQAA/wAAAAABAAD/AQAAAAEAAP//AQAAAQAA/wABAAABAAD/AP8BAAEAAP//AAEAAQAA/wAAAQABAAD//wEBAAEAAP8AAQEAAQAA/wEBAQABAAD/AQD/AQEAAP8BAf8BAQAA/wH/AAEBAAD/AAAAAQEAAP//AAEBAQAA/wABAQEBAAD/AP8A//8BAP8BAAD//wEA/wAAAf//AQD/AP//AP8BAP//AP8A/wEA/wEA/wD/AQD/AAH/AP8BAP///wAA/wEA/wAAAAD/AQD//wEAAP8BAP8BAQAA/wEA////AQD/AQD/AP8BAP8BAP//AAEA/wEA/wEAAQD/AQD/AAEBAP8BAP8AAP8B/wEA/wD/AAH/AQD//wAAAf8BAP8AAAEB/wEA////AP8AAQD/Af8A/wABAP8AAAD/AAEA/wEBAP8AAQD/AP8B/wABAP8AAAH/AAEA/wH//wAAAQD//wD/AAABAP8AAP8AAAEA//8B/wAAAQD/AP8AAAABAP//AAAAAAEA/wAAAAAAAQD/AQAAAAABAP8AAQAAAAEA/wEBAAAAAQD///8BAAABAP8AAAEAAAEA/wEBAQAAAQD/AAH/AQABAP8A/wABAAEA/wH/AAEAAQD/AAAAAQABAP//AQABAAEA/wD/AQEAAQD/AQABAQABAP8AAQEBAAEA/wAB//8BAQD/AQAA/wEBAP//AAH/AQEA/wEAAf8BAQD//wD/AAEBAP8BAP8AAQEA/wAB/wABAQD///8AAAEBAP8B/wAAAQEA/wAAAAABAQD//wEAAAEBAP8A/wEAAQEA/wEAAQABAQD/AAEBAAEBAP8AAP8BAQEA/wD/AAEBAQD/AQAAAQEBAP8BAQABAQEA/////////wH/Af//////Af//Af////8B/wEB/////wH/AAAA////Af///wH///8B/wH/Af///wH/AAAB////Af//AQH///8B/wEBAf///wH/AAD/AP//Af8A/wAA//8B/wABAAD//wH/AP8BAP//Af8AAAEA//8B/////wH//wH/Af//Af//Af//Af8B//8B/wEB/wH//wH/AAAAAf//Af///wEB//8B/wH/AQH//wH/AAABAf//Af//AQEB//8B/wEBAQH//wH/AAD//wD/Af8A/wD/AP8B//8AAP8A/wH/AAEA/wD/Af8AAAH/AP8B/wH//wAA/wH//wD/AAD/Af8AAf8AAP8B/wAAAAAA/wH//wEAAAD/Af8BAQAAAP8B/wD/AQAA/wH//wABAAD/Af8AAAEAAP8B/wEAAQAA/wH/AAD/AQD/Af///wABAP8B/wEAAAEA/wH/AAEAAQD/Af8AAAEBAP8B/wD///8B/wH//wH//wH/Af8BAf//Af8B/wD/AP8B/wH/AAAA/wH/Af///wH/Af8B/wH/Af8B/wH//wEB/wH/Af8BAQH/Af8B/wAA/wAB/wH/AP8AAAH/Af8BAAAAAf8B/wABAAAB/wH/AAABAAH/Af8A//8BAf8B//8B/wEB/wH/AQH/AQH/Af8A/wABAf8B/wAAAAEB/wH///8BAQH/Af8B/wEBAf8B//8BAQEB/wH/AQEBAQH/Af8AAP///wAB//8AAP//AAH/AQAA//8AAf8AAQD//wAB/wAAAf//AAH//wD/AP8AAf8AAP8A/wAB/wEA/wD/AAH/AAH/AP8AAf8B/wAA/wAB/wAAAAD/AAH//wEAAP8AAf8BAQAA/wAB/wEAAQD/AAH/AAD/Af8AAf8A/wAB/wAB//8AAAH/AAH/AAEAAf8AAf8A/wEB/wAB/wAAAQH/AAH/AAH//wAAAf8AAAD/AAAB/wD/Af8AAAH/AAEB/wAAAf////8AAAAB/wAA/wAAAAH//wH/AAAAAf8A/wAAAAAB//8AAAAAAAH/AAAAAAAAAf8AAQAAAAAB/wH/AQAAAAH/AAABAAAAAf//AQEAAAAB/wAB/wEAAAH///8AAQAAAf//AAABAAAB/wAAAAEAAAH//wEAAQAAAf8BAQABAAAB/wD/AQEAAAH//wABAQAAAf8BAAEBAAAB/wABAQEAAAH/AAD//wEAAf///wD/AQAB/wH/AP8BAAH/AAEA/wEAAf8AAAH/AQAB/wD//wABAAH/AAH/AAEAAf8AAAAAAQAB////AQABAAH/AP8BAAEAAf8AAQEAAQAB//8A/wEBAAH/AQD/AQEAAf///wABAQAB/wEBAAEBAAH///////8BAf8B/////wEB//8B////AQH/AQH///8BAf8AAAD//wEB////Af//AQH/Af8B//8BAf//AQH//wEB/wEBAf//AQH/AAD/AP8BAf8A/wAA/wEB//8AAAD/AQH/AAABAP8BAf////8B/wEB/wH//wH/AQH//wH/Af8BAf8BAf8B/wEB////AQH/AQH/Af8BAf8BAf//AQEB/wEB/wEBAQH/AQH/AAH//wABAf8A/wD/AAEB//8AAP8AAQH/AAEA/wABAf8AAAH/AAEB/wEA/wAAAQH/AAH/AAABAf8B/wAAAAEB/wAAAAAAAQH/AP8BAAABAf//AAEAAAEB/wEAAQAAAQH/AAEBAAABAf8AAP8BAAEB////AAEAAQH/AQAAAQABAf8AAQABAAEB//8AAQEAAQH/AAABAQABAf//////AQEB/wH///8BAQH//wH//wEBAf8BAf//AQEB////Af8BAQH/Af8B/wEBAf//AQH/AQEB/wEBAf8BAQH/AAD/AAEBAf8A/wAAAQEB/wEAAAABAQH/AAEAAAEBAf8AAAEAAQEB/////wEBAQH/Af//AQEBAf//Af8BAQEB/wEB/wEBAQH/AAAAAQEBAf///wEBAQEB/wH/AQEBAQH//wEBAQEBAf8BAQEBAQEB/wAA//////8AAP8A/////wABAAD/////AAAAAf////8AAAH/AP///wAB/wAA////AAAAAAD///8A/wEAAP///wABAQAA////AAD/AQD///8A/wABAP///wABAAEA////AP8AAAH///8AAAEAAf///wAA/wEB////AAEAAQH///8A/////wD//wAA////AP//AP8A//8A//8AAQD//wD//wAAAf//AP//AAH/AP8A//8AAAAA/wD//wABAAD/AP//AP8BAP8A//8AAQEA/wD//wAA/wH/AP//AAEAAf8A//8AAAEB/wD//wAAAP8AAP//AP8B/wAA//8AAQH/AAD//wAA/wAAAP//AP8AAAAA//8AAAAAAAD//wABAAAAAP//AAABAAAA//8AAQEAAAD//wAAAAEAAP//AP8BAQAA//8AAQEBAAD//wAA//8BAP//AP8A/wEA//8AAQD/AQD//wD//wABAP//AAH/AAEA//8AAAAAAQD//wD//wEBAP//AAD/AQEA//8AAf8BAQD//wAAAP//Af//AAD/AP8B//8A/wAA/wH//wABAAD/Af//AAAAAf8B//8AAP//AAH//wAB/wAAAf//AAAAAAAB//8AAQEAAAH//wD/AAEAAf//AAABAQAB//8AAAH/AQH//wD/AAABAf//AAAAAQEB//8AAP////8A/wAAAAD//wD/AAABAP//AP8AAAEB//8A/wAAAP8A/wD/AP8B/wD/AP8AAQH/AP8A/wAA/wAA/wD/AP8AAAD/AP8AAAAAAP8A/wABAAAA/wD/AAD/AQD/AP8AAf8BAP8A/wAAAAEA/wD/AP8BAQD/AP8AAQEBAP8A/wAA//8B/wD/AAEA/wH/AP8AAAH/Af8A/wD//wAB/wD/AAH/AAH/AP8AAAAAAf8A/wD//wEB/wD/AAD/AQH/AP8AAAEBAf8A/wAA////AAD/AAH///8AAP8AAAD//wAA/wABAf//AAD/AAD/AP8AAP8A/wAA/wAA/wAAAAD/AAD/AAEAAP8AAP8AAAEA/wAA/wD//wH/AAD/AAAAAf8AAP8AAQEB/wAA/wAA//8AAAD/AP8A/wAAAP8AAAD/AAAA/wABAP8AAAD/AAAB/wAAAP8A//8AAAAA/wAA/wAAAAD/AP8AAAAAAP8AAAAAAAAA/wABAAAAAAD/AP8BAAAAAP8AAAEAAAAA/wAA/wEAAAD/AP8AAQAAAP8AAAABAAAA/wABAAEAAAD/AAABAQAAAP8AAf//AQAA/wD/AP8BAAD/AAAA/wEAAP8A/wH/AQAA/wAA/wABAAD/AP8AAAEAAP8AAAAAAQAA/wABAAABAAD/AAABAAEAAP8AAQEAAQAA/wAAAAEBAAD/AP8BAQEAAP8AAQEBAQAA/wAA////AQD/AAAA//8BAP8AAAH//wEA/wD/AAD/AQD/AAAAAP8BAP8A/wEA/wEA/wABAQD/AQD/AAD/Af8BAP8A/wAB/wEA/wAAAQH/AQD/AP///wABAP8AAf//AAEA/wAAAP8AAQD/AP8B/wABAP8A//8AAAEA/wAA/wAAAQD/AAH/AAABAP8AAAAAAAEA/wABAAAAAQD/AAABAAABAP8AAf8BAAEA/wAAAAEAAQD/AP8BAQABAP8AAP//AQEA/wAAAP8BAQD/AAEB/wEBAP8A/wAAAQEA/wAAAAABAQD/AAD/AQEBAP8A/wABAQEA/wABAAEBAQD/AAAA////Af8AAP8A//8B/wAAAAD//wH/AAEBAP//Af8AAAAB//8B/wAB//8A/wH/AAAB/wD/Af8A//8AAP8B/wAAAAAA/wH/AP8BAAD/Af8AAP8BAP8B/wD/AAEA/wH/AAEAAQD/Af8AAAEBAP8B/wAAAP8B/wH/AAD/AAH/Af8A/wAAAf8B/wABAAAB/wH/AAABAAH/Af8AAAABAf8B/wAA////AAH/AAAA//8AAf8AAQD//wAB/wABAf//AAH/AP//AP8AAf8A/wAA/wAB/wAAAAD/AAH/AP8BAP8AAf8AAP8B/wAB/wD/AAH/AAH/AAEAAf8AAf8A////AAAB/wAAAP8AAAH/AAEB/wAAAf8AAP8AAAAB/wAB/wAAAAH/AP8AAAAAAf8AAAAAAAAB/wABAAAAAAH/AAABAAAAAf8A//8BAAAB/wAB/wEAAAH/AAAAAQAAAf8AAQABAAAB/wABAQEAAAH/AAEA/wEAAf8AAAH/AQAB/wAB/wABAAH/AAAAAAEAAf8AAQAAAQAB/wD/AQABAAH/AAD/AQEAAf8A/wABAQAB/wABAAEBAAH/AAABAQEAAf8AAQAA/wEB/wD/AP8AAQH/AAEA/wABAf8AAAH/AAEB/wAAAAAAAQH/AP8BAAABAf8AAQEAAAEB/wD/AAEAAQH/AAABAQABAf8A/wAAAQEB/wAAAAEBAQH/AAD//////wAA/wD/////AAAAAP////8AAAEA/////wAAAAH/////AAAB/wD///8AAAAAAP///wAAAQEA////AAAA/wH///8AAP8AAf///wAAAAEB////AAD///8A//8AAAAA/wD//wAA/wH/AP//AAAA/wAA//8AAP8AAAD//wAAAAAAAP//AAABAAAA//8AAAABAAD//wAAAAABAP//AAD/AQEA//8AAAEA/wH//wAAAAH/Af//AAAAAAAB//8AAP8BAAH//wAA//8BAf//AAAA/wEB//8AAAEAAQH//wAAAAEBAf//AAAAAP//AP8AAP8B//8A/wAAAAH//wD/AAABAf//AP8AAAD/AP8A/wAA/wAA/wD/AAAAAAD/AP8AAAEAAP8A/wAA/wEA/wD/AAAAAQD/AP8AAP//Af8A/wAAAAAB/wD/AAABAAH/AP8AAP8BAf8A/wAAAQEB/wD/AAAA//8AAP8AAP8A/wAA/wAAAAD/AAD/AAABAP8AAP8AAAAB/wAA/wAA//8AAAD/AAAA/wAAAP8AAAH/AAAA/wAA/wAAAAD/AAAAAAAAAP8AAAEAAAAA/wAA/wEAAAD/AAAAAQAAAP8AAAEBAAAA/wAAAP8BAAD/AAD/AAEAAP8AAAAAAQAA/wAAAQABAAD/AAAAAQEAAP8AAAH//wEA/wAAAAD/AQD/AAAA/wABAP8AAP8AAAEA/wAAAAAAAQD/AAABAAABAP8AAAABAAEA/wAA//8BAQD/AAAAAAEBAP8AAAEBAQEA/wAAAP///wH/AAABAP//Af8AAAH/AP8B/wAAAAAA/wH/AAABAQD/Af8AAAD/Af8B/wAA/wAB/wH/AAAB//8AAf8AAAAA/wAB/wAAAQH/AAH/AAAA/wAAAf8AAP8AAAAB/wAAAAAAAAH/AAABAAAAAf8AAAABAAAB/wAAAf8BAAH/AAAAAAEAAf8AAAAA/wEB/wAA//8AAQH/AAAB/wABAf8AAAAAAAEB/wAAAAEAAQH/AAABAQABAf8AAP8AAQEB/wAA/wD///8AAAAAAP///wAAAAD/AP//AAAA/wAA//8AAAAAAAD//wAAAAEAAP//AAAA/wEA//8AAAAAAQD//wAAAAD/Af//AAAAAAAB//8AAAD/AQH//wAAAAEBAf//AAAAAP//AP8AAAD/AP8A/wAAAAAA/wD/AAAAAQD/AP8AAAAAAf8A/wAAAAEB/wD/AAAA//8AAP8AAAAA/wAA/wAAAP8AAAD/AAAAAAAAAP8AAAABAAAA/wAAAP8BAAD/AAAAAAEAAP8AAAABAQAA/wAAAAD/AQD/AAAAAf8BAP8AAAD/AAEA/wAAAAAAAQD/AAAAAQABAP8AAAAAAQEA/wAAAP///wH/AAAA/wH/Af8AAAABAf8B/wAAAAD/AAH/AAAA/wAAAf8AAAAAAAAB/wAAAAEAAAH/AAAAAAEAAf8AAAAA/wEB/wAAAP8AAQH/AAAAAAABAf8AAAABAQEB/wAAAAD///8AAAAAAf///wAAAAD/AP//AAAAAAAA//8AAAAAAQD//wAAAAAAAf//AAAAAP//AP8AAAAAAP8A/wAAAAAB/wD/AAAAAP8AAP8AAAAAAAAA/wAAAAABAAD/AAAAAAABAP8AAAAAAQEA/wAAAAAA/wH/AAAAAP8AAf8AAAAAAAAB/wAAAAABAAH/AAAAAAABAf8AAAAA////AAAAAAAA//8AAAAAAAH//wAAAAAA/wD/AAAAAAAAAP8AAAAAAAEA/wAAAAAA/wH/AAAAAAAAAf8AAAAAAP//AAAAAAAAAP8AAAAAAAAB/wAAAAAAAP8AAAAAAAAAAAAAAAAAAAABAAAAAAAAAP8BAAAAAAAAAAEAAAAAAAABAQAAAAAAAP//AQAAAAAAAP8BAAAAAAD/AAEAAAAAAAAAAQAAAAAAAQABAAAAAAD/AQEAAAAAAAABAQAAAAAAAQEBAAAAAAAA//8BAAAAAP8A/wEAAAAAAAD/AQAAAAAAAf8BAAAAAAEB/wEAAAAA//8AAQAAAAAA/wABAAAAAP8AAAEAAAAAAAAAAQAAAAABAAABAAAAAP8BAAEAAAAAAAEAAQAAAAAA/wEBAAAAAP8AAQEAAAAAAAABAQAAAAABAAEBAAAAAAABAQEAAAAA/////wEAAAAA////AQAAAAH///8BAAAA/wD//wEAAAABAP//AQAAAP8B//8BAAAAAAH//wEAAAAA/wD/AQAAAP8AAP8BAAAAAAAA/wEAAAD/AQD/AQAAAAABAP8BAAAA//8B/wEAAAAA/wH/AQAAAAH/Af8BAAAA/wAB/wEAAAAAAAH/AQAAAAEAAf8BAAAA/wEB/wEAAAAAAQH/AQAAAAD//wABAAAAAAD/AAEAAAABAP8AAQAAAP8B/wABAAAAAAH/AAEAAAABAf8AAQAAAP//AAABAAAAAP8AAAEAAAAB/wAAAQAAAP8AAAABAAAAAAAAAAEAAAABAAAAAQAAAP8BAAABAAAAAAEAAAEAAAABAQAAAQAAAAD/AQABAAAA/wABAAEAAAAAAAEAAQAAAAABAQABAAAAAf//AQEAAAAAAP8BAQAAAAEA/wEBAAAA/wH/AQEAAAAAAf8BAQAAAAEB/wEBAAAAAP8AAQEAAAAAAAABAQAAAAEBAAEBAAAAAf8BAQEAAAAAAAEBAQAAAAEAAQEBAAAA/wEBAQEAAAAAAQEBAQAAAP8A////AQAAAAD///8BAAABAP///wEAAAAB////AQAA//8A//8BAAAAAAD//wEAAP8BAP//AQAAAP8B//8BAAABAQH//wEAAAAA/wD/AQAA/wH/AP8BAAABAf8A/wEAAAD/AAD/AQAA/wAAAP8BAAAAAAAA/wEAAAEAAAD/AQAA/wEAAP8BAAAAAQAA/wEAAP//AQD/AQAAAf8BAP8BAAD/AAEA/wEAAAAAAQD/AQAAAf//Af8BAAAAAf8B/wEAAP//AAH/AQAAAf8AAf8BAAAAAAAB/wEAAP8BAAH/AQAAAP8BAf8BAAAAAQEB/wEAAAD///8AAQAAAf///wABAAAAAP//AAEAAAEB//8AAQAAAP8A/wABAAD/AAD/AAEAAAAAAP8AAQAAAQAA/wABAAAAAQD/AAEAAAAAAf8AAQAAAP//AAABAAD/AP8AAAEAAAAA/wAAAQAAAQD/AAABAAAAAf8AAAEAAP//AAAAAQAAAP8AAAABAAAB/wAAAAEAAP8AAAAAAQAAAAAAAAABAAABAAAAAAEAAP8BAAAAAQAAAAEAAAABAAABAQAAAAEAAAD/AQAAAQAA/wABAAABAAAAAAEAAAEAAAEAAQAAAQAAAAEBAAABAAAA//8BAAEAAAAA/wEAAQAAAAH/AQABAAAA/wABAAEAAP8AAAEAAQAAAAAAAQABAAABAAABAAEAAP8BAAEAAQAAAAEAAQABAAAAAAEBAAEAAP8A//8BAQAA/wH//wEBAAAAAAD/AQEAAAEBAP8BAQAA//8B/wEBAAAAAAH/AQEAAAEAAf8BAQAAAAEB/wEBAAAAAP8AAQEAAP8B/wABAQAAAAH/AAEBAAAA/wAAAQEAAAAAAAABAQAAAQAAAAEBAAD/AQAAAQEAAAABAAABAQAAAf8BAAEBAAAAAAEAAQEAAP8BAQABAQAAAQEBAAEBAAAA//8BAQEAAAEB/wEBAQAAAf8AAQEBAAAAAAABAQEAAAEAAAEBAQAA/wEAAQEBAAABAQABAQEAAAD/AQEBAQAAAAD/////AQD/AAD///8BAAEAAP///wEAAAEA////AQAAAAH///8BAP8A/wD//wEA//8AAP//AQAAAAAA//8BAAEAAAD//wEA/wEAAP//AQABAQAA//8BAAD/AQD//wEA/wABAP//AQABAAEA//8BAAABAQD//wEAAP//Af//AQABAAAB//8BAAAAAQH//wEAAP///wD/AQD/AP//AP8BAAEA//8A/wEAAAH//wD/AQAB/wD/AP8BAAAAAP8A/wEAAP8B/wD/AQAB/wH/AP8BAAEAAf8A/wEAAAEB/wD/AQAAAP8AAP8BAAAB/wAA/wEAAP8AAAD/AQAAAAAAAP8BAAEAAAAA/wEAAAEAAAD/AQAAAAEAAP8BAAEAAQAA/wEAAQEBAAD/AQD/AP8BAP8BAAEB/wEA/wEAAf8AAQD/AQAAAAABAP8BAAD/AQEA/wEAAQABAQD/AQAAAQEBAP8BAAD/AP8B/wEAAQAA/wH/AQAAAQD/Af8BAP///wAB/wEAAP//AAH/AQABAP8AAf8BAAAAAAAB/wEAAQAAAAH/AQD/AQAAAf8BAP//AQAB/wEAAAD/AQH/AQAA/wABAf8BAAEAAAEB/wEAAAABAQH/AQAA/wD//wABAAH/AP//AAEAAAAA//8AAQABAAD//wABAAEBAP//AAEAAP8B//8AAQABAAH//wABAAABAf//AAEA////AP8AAQAB//8A/wABAAAA/wD/AAEA/wH/AP8AAQABAf8A/wABAAD/AAD/AAEA/wAAAP8AAQAAAAAA/wABAAEAAAD/AAEAAAEAAP8AAQABAQAA/wABAP//AQD/AAEAAf8BAP8AAQAAAAEA/wABAP8A/wH/AAEAAAD/Af8AAQAAAf8B/wABAP//AAH/AAEAAf8AAf8AAQD/AAAB/wABAAAAAAH/AAEAAQAAAf8AAQD/AQAB/wABAAEBAAH/AAEAAP8BAf8AAQD/AAEB/wABAAABAQH/AAEAAAD//wAAAQD/Af//AAABAAEB//8AAAEAAP8A/wAAAQAAAAD/AAABAAEAAP8AAAEAAAEA/wAAAQD/AP8AAAABAAAA/wAAAAEAAQD/AAAAAQAAAf8AAAABAP//AAAAAAEAAP8AAAAAAQD/AAAAAAABAAAAAAAAAAEAAQAAAAAAAQAAAQAAAAABAAD/AQAAAAEA/wABAAAAAQAAAAEAAAABAAEAAQAAAAEAAAEBAAAAAQABAP8BAAABAAAB/wEAAAEAAQH/AQAAAQAA/wABAAABAAAAAAEAAAEAAQAAAQAAAQAAAQABAAABAAEBAAEAAAEAAf8BAQAAAQAAAAEBAAABAAEAAQEAAAEA/wEBAQAAAQAB////AQABAAAB//8BAAEAAAAA/wEAAQD//wH/AQABAAEAAf8BAAEA/wEB/wEAAQAAAQH/AQABAP///wABAAEAAAD/AAEAAQD/Af8AAQABAAEB/wABAAEAAP8AAAEAAQD/AAAAAQABAAAAAAABAAEAAQAAAAEAAQD/AQAAAQABAAEBAAABAAEA//8BAAEAAQAAAAEAAQABAP8BAQABAAEA////AQEAAQAB//8BAQABAAAA/wEBAAEAAQH/AQEAAQD/AAABAQABAAEAAAEBAAEA/wEAAQEAAQAAAQABAQABAP//AQEBAAEA/wABAQEAAQABAAEBAQABAAEBAQEBAAEAAQAA//8BAQAAAQD//wEBAAAAAf//AQEAAP//AP8BAQAB/wAA/wEBAAAAAAD/AQEAAQEAAP8BAQAA/wEA/wEBAAABAQD/AQEAAAD/Af8BAQAA/wAB/wEBAP8BAAH/AQEAAQABAf8BAQAA////AAEBAP8A//8AAQEA//8A/wABAQAAAAD/AAEBAAD/Af8AAQEA/wAB/wABAQABAAH/AAEBAAABAf8AAQEA////AAABAQAA//8AAAEBAAAA/wAAAQEAAQD/AAABAQD/Af8AAAEBAAD/AAAAAQEA/wAAAAABAQAAAAAAAAEBAAEAAAAAAQEAAAEAAAABAQD//wEAAAEBAAAAAQAAAQEAAQEBAAABAQAB//8BAAEBAP8A/wEAAQEAAQH/AQABAQAAAAABAAEBAAD/AQEAAQEA/wABAQABAQAAAAEBAAEBAAABAQEAAQEAAP8A/wEBAQABAAD/AQEBAP8BAP8BAQEAAP//AAEBAQD/AP8AAQEBAAAB/wABAQEA//8AAAEBAQAAAAAAAQEBAP8BAAABAQEAAQEAAAEBAQD/AAEAAQEBAAAAAQABAQEAAAEBAAEBAQABAP8BAQEBAP8AAAEBAQEA/wEAAQEBAQABAQABAQEBAAEAAQEBAQEA/////////wEB////////Af8B//////8BAQH//////wH//wH/////AQH/Af////8B/wEB/////wEBAQH/////AQAA/wD///8B//8AAP///wEA/wAA////Af8AAAD///8BAQAAAP///wEAAQAA////AQAAAQD///8B////Af///wEB//8B////Af8B/wH///8BAQH/Af///wEAAAAB////Af//AQH///8BAf8BAf///wH/AQEB////AQEBAQH///8BAAD//wD//wEA/wD/AP//Af8AAP8A//8BAQAA/wD//wEAAQD/AP//AQAAAf8A//8BAP//AAD//wH/AP8AAP//AQAB/wAA//8B//8AAAD//wEB/wAAAP//AQAAAAAA//8BAQAAAAD//wH/AQAAAP//AQABAAAA//8B/wABAAD//wEBAAEAAP//AQABAQAA//8BAAD/AQD//wEAAf8BAP//Af8AAAEA//8BAQAAAQD//wEAAQABAP//AQAAAQEA//8B/////wH//wEB////Af//Af8B//8B//8BAQH//wH//wEAAAD/Af//Af//Af8B//8BAf8B/wH//wH/AQH/Af//AQEBAf8B//8BAP8AAAH//wH/AAAAAf//AQABAAAB//8BAAABAAH//wH///8BAf//AQH//wEB//8B/wH/AQH//wEBAf8BAf//AQAAAAEB//8B//8BAQH//wEB/wEBAf//Af8BAQEB//8BAQEBAQH//wH/AAD//wD/AQABAP//AP8BAP//AP8A/wH/AP8A/wD/AQD/AAD/AP8BAAAAAP8A/wEBAQAA/wD/AQD/AQD/AP8B/wABAP8A/wEAAQEA/wD/Af8AAAH/AP8BAAEAAf8A/wEA////AAD/AQAB//8AAP8BAf8A/wAA/wEAAAD/AAD/AQEBAP8AAP8BAQAB/wAA/wEAAQH/AAD/Af///wAAAP8BAP//AAAA/wEAAP8AAAD/Af8B/wAAAP8BAP8AAAAA/wH/AAAAAAD/AQAAAAAAAP8BAQAAAAAA/wEAAQAAAAD/AQEBAAAAAP8BAAABAAAA/wEBAAEAAAD/Af8BAQAAAP8BAQEBAAAA/wEA//8BAAD/Af8A/wEAAP8BAQD/AQAA/wEAAf8BAAD/Af//AAEAAP8BAf8AAQAA/wEAAAABAAD/Af8BAAEAAP8BAQABAQAA/wEA/wD/AQD/AQEAAP8BAP8BAAEA/wEA/wEAAAH/AQD/AQD//wABAP8B/wD/AAEA/wEAAf8AAQD/AQEB/wABAP8B//8AAAEA/wEAAAAAAQD/AQABAAABAP8BAQEAAAEA/wEA/wEAAQD/AQEAAQABAP8BAQEBAAEA/wEAAP8BAQD/AQD/AAEBAP8BAQEAAQEA/wH/AAEBAQD/Af//////Af8BAf////8B/wH/Af///wH/AQEB////Af8BAAAA//8B/wH//wH//wH/AQH/Af//Af8B/wEB//8B/wEBAQH//wH/AQD//wD/Af8BAAD/AP8B/wEA/wAA/wH/Af8AAAD/Af8BAAEAAP8B/wEAAAEA/wH/AQABAQD/Af8B////Af8B/wEB//8B/wH/Af8B/wH/Af8BAQH/Af8B/wEAAAAB/wH/Af//AQH/Af8BAf8BAf8B/wH/AQEB/wH/AQEBAQH/Af8BAAD//wAB/wEBAP//AAH/AQD/AP8AAf8B/wAA/wAB/wEBAAD/AAH/AQAAAf8AAf8BAP//AAAB/wH/AP8AAAH/AQEA/wAAAf8BAAH/AAAB/wH//wAAAAH/AQH/AAAAAf8BAAAAAAAB/wEBAQAAAAH/AQD/AQAAAf8B/wABAAAB/wEAAP8BAAH/AQEAAAEAAf8BAAEAAQAB/wEAAAEBAAH/Af////8BAf8BAf///wEB/wH/Af//AQH/AQEB//8BAf8BAAAA/wEB/wH//wH/AQH/AQH/Af8BAf8B/wEB/wEB/wEBAQH/AQH/AQAA/wABAf8BAP8AAAEB/wH/AAAAAQH/AQEAAAABAf8B////AQEB/wEB//8BAQH/Af8B/wEBAf8BAQH/AQEB/wEAAAABAQH/Af//AQEBAf8BAf8BAQEB/wH/AQEBAQH/AQEBAQEBAf8BAAD/////AAEA/wD///8AAQEAAP///wAB/wEA////AAEAAQD///8AAQAAAf///wABAP//AP//AAEBAP8A//8AAQAB/wD//wABAAAAAP//AAH/AQAA//8AAQEBAAD//wABAAEBAP//AAEBAQEA//8AAQAA/wH//wABAP8AAf//AAH/AAAB//8AAQEAAAH//wABAAEAAf//AAEAAAEB//8AAQD///8A/wAB/wD//wD/AAEBAP//AP8AAQAB//8A/wAB//8A/wD/AAEAAAD/AP8AAf8BAP8A/wABAQEA/wD/AAEA/wH/AP8AAf8AAf8A/wABAQAB/wD/AAEAAQH/AP8AAf///wAA/wABAAD/AAD/AAH//wAAAP8AAQD/AAAA/wAB/wAAAAD/AAEAAAAAAP8AAQEAAAAA/wABAAEAAAD/AAEB/wEAAP8AAQAAAQAA/wAB/wD/AQD/AAEBAP8BAP8AAQH/AAEA/wABAAAAAQD/AAH/AQABAP8AAQD/AQEA/wAB/wABAQD/AAEBAAEBAP8AAQABAQEA/wABAAD//wH/AAEA/wD/Af8AAf8AAP8B/wABAAEA/wH/AAEAAAH/Af8AAf8A/wAB/wABAQD/AAH/AAEAAf8AAf8AAf//AAAB/wABAf8AAAH/AAEAAAAAAf8AAf8BAAAB/wABAQABAAH/AAEAAQEAAf8AAQAA/wEB/wAB/wAAAQH/AAEBAAABAf8AAQABAQEB/wABAP////8AAAH/AP///wAAAQEA////AAAB//8A//8AAAEAAAD//wAAAf8BAP//AAABAQAB//8AAAH///8A/wAAAQEB/wD/AAABAP8AAP8AAAH/AAAA/wAAAQAAAAD/AAABAQAAAP8AAAH/AQAA/wAAAQABAAD/AAAB//8BAP8AAAEA/wEA/wAAAQH/AQD/AAABAAABAP8AAAH/AP8B/wAAAQEA/wH/AAABAf8AAf8AAAH/AAAB/wAAAQAAAAH/AAAB/wEAAf8AAAEA/wEB/wAAAQABAQH/AAAB/////wAAAAEAAP//AAAAAf8B//8AAAABAQH//wAAAAH//wD/AAAAAQD/AP8AAAAB/wAA/wAAAAEAAAD/AAAAAQEAAP8AAAABAAEA/wAAAAEA/wH/AAAAAQAAAf8AAAABAAEB/wAAAAEBAQH/AAAAAQD//wAAAAAB/wD/AAAAAAEAAP8AAAAAAQEA/wAAAAABAAH/AAAAAAH//wAAAAAAAQD/AAAAAAABAf8AAAAAAAH/AAAAAAAAAQAAAAAAAAABAQAAAAAAAAH/AQAAAAAAAQABAAAAAAABAQEAAAAAAAEA/wEAAAAAAf8AAQAAAAABAAABAAAAAAEBAAEAAAAAAQABAQAAAAABAP//AQAAAAEAAP8BAAAAAf8B/wEAAAABAP8AAQAAAAEB/wABAAAAAf8AAAEAAAABAAAAAQAAAAEBAAABAAAAAQABAAEAAAABAQEAAQAAAAH//wEBAAAAAQH/AQEAAAABAAABAQAAAAH/AQEBAAAAAQEBAQEAAAABAP///wEAAAH/AP//AQAAAf//AP8BAAABAAAA/wEAAAEAAQD/AQAAAf//Af8BAAABAQAB/wEAAAEAAQH/AQAAAQAA/wABAAAB/wH/AAEAAAEAAf8AAQAAAQD/AAABAAABAf8AAAEAAAEAAAAAAQAAAQEAAAABAAABAAEAAAEAAAEAAAEAAQAAAf8BAQABAAABAf//AQEAAAH/AP8BAQAAAQAB/wEBAAABAQH/AQEAAAEB/wABAQAAAf8AAAEBAAABAAAAAQEAAAH/AAEBAQAAAQEAAQEBAAABAAEBAQEAAAEAAP///wEAAQEAAP//AQABAAEA//8BAAEAAAH//wEAAQD//wD/AQABAQD/AP8BAAH//wAA/wEAAQH/AAD/AQABAAAAAP8BAAEBAAAA/wEAAQEBAAD/AQAB/wABAP8BAAEAAAEA/wEAAQAA/wH/AQABAP8AAf8BAAEBAAAB/wEAAQABAAH/AQABAAABAf8BAAH/AP//AAEAAQEA//8AAQABAAH//wABAAH//wD/AAEAAQH/AP8AAQABAAAA/wABAAH/AQD/AAEAAQEBAP8AAQAB//8B/wABAAEA/wH/AAEAAf8AAf8AAQABAQAB/wABAAH///8AAAEAAQH//wAAAQABAAD/AAABAAH/Af8AAAEAAQEB/wAAAQABAP8AAAABAAH/AAAAAAEAAQAAAAAAAQABAQAAAAABAAEAAQAAAAEAAQH/AQAAAQABAAABAAABAAEBAAEAAAEAAQEBAQAAAQABAP//AQABAAH/AP8BAAEAAf//AAEAAQABAf8AAQABAAEAAAABAAEAAQEBAAEAAQABAP8BAQABAAEBAAEBAAEAAQAA//8BAQABAAAA/wEBAAEAAAH/AQEAAf8A/wABAQABAQD/AAEBAAEAAf8AAQEAAf//AAABAQABAAAAAAEBAAH/AQAAAQEAAQD/AQABAQABAAD/AQEBAAEA/wABAQEAAf8AAAEBAQABAAAAAQEBAAEBAAABAQEAAf///////wEBAf//////AQH/Af////8BAQEB/////wEBAAAA////AQH//wH///8BAQH/Af///wEB/wEB////AQEBAQH///8BAQAA/wD//wEBAP8AAP//AQH/AAAA//8BAQEAAAD//wEBAAEAAP//AQH///8B//8BAQH//wH//wEB/wH/Af//AQEBAf8B//8BAQAAAAH//wEB//8BAf//AQEB/wEB//8BAf8BAQH//wEBAQEBAf//AQEAAP//AP8BAQAB//8A/wEBAP8A/wD/AQH/AAD/AP8BAQEAAP8A/wEBAAEA/wD/AQEBAQD/AP8BAQEA/wAA/wEBAAH/AAD/AQEA/wAAAP8BAQAAAAAA/wEB/wEAAAD/AQEBAQAAAP8BAQD/AQAA/wEB/wABAAD/AQEAAP8BAP8BAf//AAEA/wEBAf8AAQD/AQEBAAABAP8BAQABAAEA/wEBAf///wH/AQH/Af//Af8BAQEB//8B/wEB//8A/wH/AQEAAQD/Af8BAQH/Af8B/wEB/wEB/wH/AQEBAQH/Af8BAQAA/wAB/wEBAP8AAAH/AQEBAAAAAf8BAQABAAAB/wEBAAABAAH/AQH///8BAf8BAQH//wEB/wEB/wH/AQH/AQEBAf8BAf8BAQAAAAEB/wEB//8BAQH/AQEB/wEBAf8BAf8BAQEB/wEBAQEBAQH/AQEAAQD//wABAQAAAf//AAEBAP//AP8AAQH/AP8A/wABAf//AAD/AAEB/wAAAP8AAQEAAAAA/wABAf8BAAD/AAEBAQEAAP8AAQEA/wEA/wABAQAAAQD/AAEBAQABAP8AAQH/AQEA/wABAQABAQD/AAEBAAD/Af8AAQEBAP//AAABAQAB//8AAAEB//8A/wAAAQEB/wD/AAABAQAAAP8AAAEB/wEA/wAAAQEBAAH/AAABAQABAf8AAAEBAf//AAAAAQEAAP8AAAABAQD/AAAAAAEB/wAAAAAAAQEAAAAAAAABAQEAAAAAAAEBAAEAAAAAAQEAAAEAAAABAQEBAQAAAAEBAP//AQAAAQH/AP8BAAABAQAA/wEAAAEBAQD/AQAAAQEAAf8BAAABAQH/AAEAAAEBAAAAAQAAAQH/AQABAAABAQAA//8BAAEBAP8A/wEAAQEBAAD/AQABAQEBAP8BAAEBAP8B/wEAAQEAAAH/AQABAf8A/wABAAEBAQD/AAEAAQEBAf8AAQABAQH/AAABAAEBAAAAAAEAAQEBAAAAAQABAf8BAAABAAEB//8BAAEAAQEB/wEAAQABAQEA/wEBAAEB//8AAQEAAQEAAAABAQABAQEAAAEBAAEBAAEAAQEAAQEA/wEBAQABAf8AAQEBAAEBAQABAQEAAQH//////wEBAQH/////AQEB/wH///8BAQEBAf///wEBAf//Af//AQEBAf8B//8BAQH/AQH//wEBAQEBAf//AQEBAP8AAP8BAQH/AAAA/wEBAQEAAAD/AQEBAAEAAP8BAQH///8B/wEBAQH//wH/AQEB/wH/Af8BAQEBAf8B/wEBAQAAAAH/AQEB//8BAf8BAQEB/wEB/wEBAf8BAQH/AQEBAQEBAf8BAQEAAP//AAEBAf8AAP8AAQEBAAEA/wABAQEA/wH/AAEBAQAAAf8AAQEBAP//AAABAQH//wAAAAEBAQAAAAAAAQEBAQEAAAABAQEA/wEAAAEBAQEAAQAAAQEBAAEBAAABAQH//wABAAEBAQEAAAEAAQEB/////wEBAQEB////AQEBAf8B//8BAQEBAQH//wEBAQH//wH/AQEBAQH/Af8BAQEB/wEB/wEBAQEBAQH/AQEBAQD/AAABAQEB/wAAAAEBAQEBAAAAAQEBAf///wEBAQEBAf//AQEBAQH/Af8BAQEBAQEB/wEBAQEBAAAAAQEBAQH//wEBAQEBAQH/AQEBAQEB/wEBAQEBAQEBAQEBAQEBAQ==");
|
| 12 |
+
export const ksigns_iq2xs = /* 128B */ D("AIGCA4QFBoeICQqLDI2OD5AREpMUlZYXGJmaG5wdHp+gISKjJKWmJyipqiusLS6vMLGyM7Q1Nre4OTq7PL2+P8BBQsNExcZHSMnKS8xNTs9Q0dJT1FVW19hZWttc3d5fYOHiY+RlZufoaWrrbO3ub/BxcvN09fZ3ePn6e/x9fv8=");
|
| 13 |
+
export const kmask_iq2xs = /* 8B */ D("AQIECBAgQIA=");
|
| 14 |
+
export const kvalues_iq4nl = /* 16B */ D("gZitv8/d6vYBDRkmNUVZcQ==");
|
holo-delta.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-delta.mjs — A2 of the personal-model-zoo plan: family index-delta. A finetune that shares a base's
|
| 2 |
+
// FRAME (see holo-model-frame.mjs) is stored as `base-κ + per-tensor delta`, reconstructed at LOAD (no new
|
| 3 |
+
// kernel — the engine still reads normal quantized weights). Two wins compose:
|
| 4 |
+
// • FROZEN tensors (identical κ to the base) cost 0 extra bytes — content-addressing dedups them for free.
|
| 5 |
+
// • CHANGED tensors are stored as a BitDelta (Liu et al. 2024): sign(Δ) at 1 bit/param + one scale α.
|
| 6 |
+
//
|
| 7 |
+
// HONEST storage math (this is where the "5–9×" claim earns or loses): the zoo ratio is driven by the
|
| 8 |
+
// FROZEN-TENSOR FRACTION, not within-tensor sparsity. A LoRA/partial finetune (most tensors frozen) →
|
| 9 |
+
// 10–30×; a FULL finetune (every tensor moves) → only ~3–4× (BitDelta is ~1 bit/param vs a 3–4 bit base).
|
| 10 |
+
// BitDelta is intrinsically lossy (sign+scale captures ~2/π≈64% of a Gaussian delta's energy); its
|
| 11 |
+
// published result is that OUTPUT quality is nonetheless preserved — that must be confirmed by perplexity
|
| 12 |
+
// on a REAL base+finetune pair (the A2 pass-bar gate), which this codec self-test does not run.
|
| 13 |
+
//
|
| 14 |
+
// Pure JS, isomorphic, zero deps. Node self-test sweeps frozen-fraction + reports fidelity and zoo ratio.
|
| 15 |
+
|
| 16 |
+
import { shareable } from "./holo-model-frame.mjs";
|
| 17 |
+
|
| 18 |
+
// ── BitDelta codec: Δ = ftW − baseW ; store sign(Δ) (1 bit) + α = mean|Δ| (the L2-optimal ±1 scale). ──
|
| 19 |
+
export function encodeBitDelta(baseW, ftW) {
|
| 20 |
+
const n = baseW.length, signBits = new Uint8Array((n + 7) >> 3);
|
| 21 |
+
let absSum = 0;
|
| 22 |
+
for (let i = 0; i < n; i++) {
|
| 23 |
+
const d = ftW[i] - baseW[i];
|
| 24 |
+
if (d >= 0) signBits[i >> 3] |= 1 << (i & 7); // bit set ⇒ +1
|
| 25 |
+
absSum += Math.abs(d);
|
| 26 |
+
}
|
| 27 |
+
return { kind: "bitdelta", n, alpha: absSum / n, signBits }; // ~1 bit/param + one f32
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function decodeBitDelta(baseW, rec, out) {
|
| 31 |
+
const n = rec.n, a = rec.alpha, o = out || new Float32Array(n);
|
| 32 |
+
for (let i = 0; i < n; i++) {
|
| 33 |
+
const pos = (rec.signBits[i >> 3] >> (i & 7)) & 1;
|
| 34 |
+
o[i] = baseW[i] + (pos ? a : -a);
|
| 35 |
+
}
|
| 36 |
+
return o;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// fraction of the delta's ENERGY captured by sign+scale (1 = perfect). For a Gaussian Δ this → 2/π ≈ 0.637.
|
| 40 |
+
export function deltaCaptured(baseW, ftW, rec) {
|
| 41 |
+
const n = rec.n; let num = 0, den = 0;
|
| 42 |
+
for (let i = 0; i < n; i++) {
|
| 43 |
+
const d = ftW[i] - baseW[i], pos = (rec.signBits[i >> 3] >> (i & 7)) & 1, r = d - (pos ? rec.alpha : -rec.alpha);
|
| 44 |
+
num += r * r; den += d * d;
|
| 45 |
+
}
|
| 46 |
+
return den ? 1 - num / den : 1;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// ── model-level delta: per tensor, frozen (κ identical) → ref (0 bytes) else bitdelta. Frame-guarded. ──
|
| 50 |
+
// tensors: { name → { kappa, params, baseW?, ftW? } }. baseW/ftW (dequant weights) only needed to encode
|
| 51 |
+
// CHANGED tensors; pass them for changed ones, omit for frozen (κ tells us they're identical).
|
| 52 |
+
export function deltaModel(baseMeta, ftMeta, baseTensors, ftTensors) {
|
| 53 |
+
if (!shareable(baseMeta, ftMeta)) throw new Error("holo-delta: base and finetune are not in the same frame/arch — cannot delta (see holo-model-frame.shareable)");
|
| 54 |
+
const records = {}; let frozenParams = 0, changedParams = 0, deltaBytes = 0;
|
| 55 |
+
for (const name of Object.keys(ftTensors)) {
|
| 56 |
+
const b = baseTensors[name], f = ftTensors[name];
|
| 57 |
+
if (b && f.kappa && b.kappa === f.kappa) { records[name] = { kind: "ref", kappa: b.kappa }; frozenParams += f.params || 0; continue; }
|
| 58 |
+
const rec = encodeBitDelta(f.baseW, f.ftW);
|
| 59 |
+
records[name] = { kind: "bitdelta", base: b ? b.kappa : null, alpha: rec.alpha, n: rec.n };
|
| 60 |
+
changedParams += rec.n; deltaBytes += rec.signBits.length + 4; // bits + α
|
| 61 |
+
}
|
| 62 |
+
return { records, stats: { frozenParams, changedParams, deltaBytes, deltaBitsPerChangedParam: changedParams ? (deltaBytes * 8) / changedParams : 0 } };
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ── LOSSLESS byte-delta over stored quantized blocks (the right fit for κ-objects). A base and finetune
|
| 66 |
+
// tensor share the same packed length (same dims+fmt); store only the differing byte runs. Reconstruction
|
| 67 |
+
// is byte-identical to the finetune's own block ⇒ perplexity = standalone (NO quality gate). Falls back to
|
| 68 |
+
// storing the whole block when the diff isn't sparse. This is the default for the family loader. ──
|
| 69 |
+
const _GAP = 8; // coalesce changed runs separated by ≤_GAP identical bytes (amortizes per-run header)
|
| 70 |
+
export function encodeByteDelta(baseBytes, ftBytes) {
|
| 71 |
+
if (baseBytes.length !== ftBytes.length) return { kind: "whole", len: ftBytes.length, bytes: ftBytes };
|
| 72 |
+
const n = ftBytes.length, runs = []; let i = 0, deltaSize = 0;
|
| 73 |
+
while (i < n) {
|
| 74 |
+
if (baseBytes[i] === ftBytes[i]) { i++; continue; }
|
| 75 |
+
let j = i + 1, gap = 0; // extend run, tolerating short identical gaps
|
| 76 |
+
while (j < n && (baseBytes[j] !== ftBytes[j] || (gap = run_gap(baseBytes, ftBytes, j)) <= _GAP)) { j += gap > 0 ? gap : 1; if (gap > 0) gap = 0; }
|
| 77 |
+
runs.push({ off: i, bytes: ftBytes.slice(i, j) }); deltaSize += (j - i) + 8; // bytes + ~8B run header
|
| 78 |
+
i = j;
|
| 79 |
+
}
|
| 80 |
+
if (deltaSize >= n * 0.9) return { kind: "whole", len: n, bytes: ftBytes }; // not sparse enough → store whole
|
| 81 |
+
return { kind: "bytedelta", len: n, runs };
|
| 82 |
+
}
|
| 83 |
+
function run_gap(a, b, j) { let g = 0; while (j + g < a.length && a[j + g] === b[j + g]) g++; return g; }
|
| 84 |
+
export function applyByteDelta(baseBytes, rec) {
|
| 85 |
+
if (rec.kind === "whole") return rec.bytes;
|
| 86 |
+
const out = baseBytes.slice(0, rec.len);
|
| 87 |
+
for (const r of rec.runs) out.set(r.bytes, r.off);
|
| 88 |
+
return out;
|
| 89 |
+
}
|
| 90 |
+
export function byteDeltaSize(rec) { return rec.kind === "whole" ? rec.bytes.length : rec.runs.reduce((s, r) => s + r.bytes.length + 8, 0); }
|
| 91 |
+
|
| 92 |
+
// compact binary (de)serialization for a byte-delta record — what gets gzipped + content-addressed.
|
| 93 |
+
export function serializeDelta(rec) {
|
| 94 |
+
if (rec.kind === "whole") { const o = new Uint8Array(5 + rec.bytes.length); new DataView(o.buffer).setUint32(1, rec.len); o[0] = 1; o.set(rec.bytes, 5); return o; }
|
| 95 |
+
let sz = 9; for (const r of rec.runs) sz += 8 + r.bytes.length;
|
| 96 |
+
const o = new Uint8Array(sz), dv = new DataView(o.buffer); o[0] = 0; dv.setUint32(1, rec.len); dv.setUint32(5, rec.runs.length); let p = 9;
|
| 97 |
+
for (const r of rec.runs) { dv.setUint32(p, r.off); dv.setUint32(p + 4, r.bytes.length); o.set(r.bytes, p + 8); p += 8 + r.bytes.length; }
|
| 98 |
+
return o;
|
| 99 |
+
}
|
| 100 |
+
export function parseDelta(u8) {
|
| 101 |
+
const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), len = dv.getUint32(1);
|
| 102 |
+
if (u8[0] === 1) return { kind: "whole", len, bytes: u8.subarray(5) };
|
| 103 |
+
const nRuns = dv.getUint32(5), runs = []; let p = 9;
|
| 104 |
+
for (let i = 0; i < nRuns; i++) { const off = dv.getUint32(p), blen = dv.getUint32(p + 4); runs.push({ off, bytes: u8.subarray(p + 8, p + 8 + blen) }); p += 8 + blen; }
|
| 105 |
+
return { kind: "bytedelta", len, runs };
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// zoo storage ratio vs storing N finetunes independently. baseBitsPerParam ≈ 3 (q3) or 4 (q4).
|
| 109 |
+
export function zooRatio({ totalParams, frozenFractionOfParams, N, baseBitsPerParam = 3 }) {
|
| 110 |
+
const B = baseBitsPerParam, g = 1 - frozenFractionOfParams; // g = changed fraction
|
| 111 |
+
const independent = (N + 1) * totalParams * B;
|
| 112 |
+
const shared = totalParams * B + N * (g * totalParams * 1); // base + N×(changed params × 1 bit)
|
| 113 |
+
return independent / shared;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// ── Node self-test: realistic synthetic base + finetunes at varying frozen fractions. ──
|
| 117 |
+
if (typeof process !== "undefined" && process.argv[1] && process.argv[1].endsWith("holo-delta.mjs")) {
|
| 118 |
+
const n = 1 << 20; // 1M-param tensor
|
| 119 |
+
const rnd = (() => { let s = 0x2545f491; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return s / 4294967296; }; })();
|
| 120 |
+
const gauss = (sig) => { let u = 0, v = 0; while (!u) u = rnd(); while (!v) v = rnd(); return sig * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); };
|
| 121 |
+
const baseW = new Float32Array(n); for (let i = 0; i < n; i++) baseW[i] = gauss(0.02); // LLM-like weight σ
|
| 122 |
+
|
| 123 |
+
console.log("\n— BitDelta fidelity on a CHANGED tensor (Δ ~ Gaussian) —");
|
| 124 |
+
for (const dsig of [0.002, 0.02]) {
|
| 125 |
+
const ftW = new Float32Array(n); for (let i = 0; i < n; i++) ftW[i] = baseW[i] + gauss(dsig);
|
| 126 |
+
const rec = encodeBitDelta(baseW, ftW); const hat = decodeBitDelta(baseW, rec);
|
| 127 |
+
let relNum = 0, relDen = 0; for (let i = 0; i < n; i++) { const e = ftW[i] - hat[i]; relNum += e * e; relDen += ftW[i] * ftW[i]; }
|
| 128 |
+
const bpp = (rec.signBits.length + 4) * 8 / n;
|
| 129 |
+
console.log(` Δσ=${dsig}: α=${rec.alpha.toExponential(2)} energy-captured=${(deltaCaptured(baseW, ftW, rec) * 100).toFixed(1)}% recon ||err||/||ft||=${Math.sqrt(relNum / relDen).toExponential(2)} ${bpp.toFixed(3)} bits/param`);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
console.log("\n— zoo storage ratio (base + 50 finetunes) by frozen-tensor fraction —");
|
| 133 |
+
for (const frozen of [0.0, 0.5, 0.8, 0.9, 0.98]) {
|
| 134 |
+
const r3 = zooRatio({ totalParams: 2e9, frozenFractionOfParams: frozen, N: 50, baseBitsPerParam: 3 });
|
| 135 |
+
const r4 = zooRatio({ totalParams: 2e9, frozenFractionOfParams: frozen, N: 50, baseBitsPerParam: 4 });
|
| 136 |
+
const tag = frozen === 0 ? "full finetune" : frozen >= 0.98 ? "LoRA-ish" : "partial";
|
| 137 |
+
console.log(` frozen=${(frozen * 100).toFixed(0).padStart(3)}% → ${r3.toFixed(1)}× (q3 base) · ${r4.toFixed(1)}× (q4 base) [${tag}]`);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
console.log("\n— lossless byte-delta over a quantized block (reconstruct must be byte-identical) —");
|
| 141 |
+
for (const changeFrac of [0.02, 0.2, 0.6]) {
|
| 142 |
+
const blkN = 1 << 20, base = new Uint8Array(blkN); for (let i = 0; i < blkN; i++) base[i] = (rnd() * 256) | 0;
|
| 143 |
+
const ft = base.slice(); for (let i = 0; i < blkN; i++) if (rnd() < changeFrac) ft[i] = (rnd() * 256) | 0; // finetune flips a fraction of bytes
|
| 144 |
+
const rec = encodeByteDelta(base, ft); const back = applyByteDelta(base, rec);
|
| 145 |
+
let identical = back.length === ft.length; for (let i = 0; identical && i < ft.length; i++) identical = back[i] === ft[i];
|
| 146 |
+
console.log(` changed≈${(changeFrac * 100).toFixed(0)}% → ${rec.kind.padEnd(9)} ${(byteDeltaSize(rec) / 1024).toFixed(0)}KB vs ${(blkN / 1024).toFixed(0)}KB block (${(byteDeltaSize(rec) / blkN).toFixed(2)}×) reconstruct byte-identical=${identical}`);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
console.log("\n— frame guard —");
|
| 150 |
+
const A = { frame: { fingerprint: "x" }, d: 2560, n_layers: 30, ff: 6912, n_heads: 20, n_kv_heads: 5, hd: 128, vocab: 128256 };
|
| 151 |
+
const Bsame = { ...A }, Bdiff = { ...A, d: 3584 };
|
| 152 |
+
console.log(` same frame+arch shareable: ${shareable(A, Bsame)} · different arch shareable: ${shareable(A, Bdiff)}`);
|
| 153 |
+
// and that deltaModel refuses a mismatched pair
|
| 154 |
+
try { deltaModel(A, Bdiff, {}, {}); console.log(" ERROR: deltaModel should have refused"); }
|
| 155 |
+
catch (e) { console.log(` deltaModel correctly refused mismatch: "${e.message.slice(0, 60)}…"`); }
|
| 156 |
+
}
|
holo-load-delta.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-load-delta.mjs — A2 LOAD wiring. Loads a family finetune stored as `base-κ + delta` (format
|
| 2 |
+
// "holo-delta/1", produced by _delta-build.mjs) and returns the SAME { manifest, fetchTensor, info } shape
|
| 3 |
+
// as holo-load2bit.loadKappaObject — so the GPU engine, KV-cache, and Q's brain loader are UNCHANGED.
|
| 4 |
+
// loadKappaObject delegates here automatically when it sees the holo-delta/1 format (one seam, hidden).
|
| 5 |
+
//
|
| 6 |
+
// Per tensor: ref → the base block (frozen, κ identical, dedup'd in the OPFS store); whole → the stored
|
| 7 |
+
// finetune block; bytedelta → base block + lossless byte-delta. Reconstruction is byte-identical to the
|
| 8 |
+
// finetune's own block ⇒ perplexity = standalone (no quality gate). The win is download/storage.
|
| 9 |
+
//
|
| 10 |
+
// Law L5: base AND delta blocks are each κ-verified (sha256(gz)==κ) before use; both κ are named in the
|
| 11 |
+
// L5-verified delta manifest, so the trust chain holds without re-hashing the reconstruction.
|
| 12 |
+
import { parseDelta, applyByteDelta } from "./holo-delta.mjs";
|
| 13 |
+
import { reshapeTensor, buildEngineManifest } from "./holo-load2bit.mjs";
|
| 14 |
+
|
| 15 |
+
async function gunzip(u8) { const ds = new DecompressionStream("gzip"); const w = ds.writable.getWriter(); w.write(u8); w.close(); return new Uint8Array(await new Response(ds.readable).arrayBuffer()); }
|
| 16 |
+
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 17 |
+
const kFile = (k) => String(k).replace(":", "_"); // κ "sha256:<hex>" → block file "sha256_<hex>"
|
| 18 |
+
|
| 19 |
+
export async function loadDeltaObject(deltaUrl, opts = {}) {
|
| 20 |
+
const droot = String(deltaUrl).replace(/\/+$/, "");
|
| 21 |
+
const man = opts.manifest || await (await fetch(droot + "/manifest.json", { cache: "no-store" })).json();
|
| 22 |
+
const baseRoot = String(opts.baseUrl || man.base?.url || man.base?.dir || "").replace(/\/+$/, "");
|
| 23 |
+
if (!baseRoot) throw new Error("delta-load: no base location (opts.baseUrl or manifest.base.url)");
|
| 24 |
+
|
| 25 |
+
// κ-verified block fetch against a chosen root (base for frozen/base blocks, delta for delta blocks).
|
| 26 |
+
const getBlock = async (root, kappa) => {
|
| 27 |
+
const gz = new Uint8Array(await (await fetch(root + "/b/" + kFile(kappa) + ".gz", { cache: "no-store" })).arrayBuffer());
|
| 28 |
+
const got = "sha256:" + hex(await crypto.subtle.digest("SHA-256", gz));
|
| 29 |
+
if (got !== kappa) throw new Error("delta-load κ MISMATCH " + String(kappa).slice(0, 24));
|
| 30 |
+
return gunzip(gz);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const normRecs = {}; // {name → {N,K,fmt,fp16?,s?}} for the shared manifest builder
|
| 34 |
+
for (const [name, dr] of Object.entries(man.tensors)) normRecs[name] = dr.meta || {};
|
| 35 |
+
|
| 36 |
+
const fetchTensor = async (name) => {
|
| 37 |
+
const dr = man.tensors[name]; if (!dr) return new Uint8Array(0);
|
| 38 |
+
let raw;
|
| 39 |
+
if (dr.kind === "ref") raw = await getBlock(baseRoot, dr.kappa); // frozen
|
| 40 |
+
else if (dr.kind === "whole") raw = parseDelta(await getBlock(droot, dr.delta)).bytes; // stored whole
|
| 41 |
+
else if (dr.kind === "bytedelta") raw = applyByteDelta(await getBlock(baseRoot, dr.base), parseDelta(await getBlock(droot, dr.delta)));
|
| 42 |
+
else throw new Error("delta-load: unknown record kind " + dr.kind + " for " + name);
|
| 43 |
+
return reshapeTensor(dr.meta || {}, raw);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
let e8lutData; // E₈ LUT (if any) is a frozen base block
|
| 47 |
+
if (man.e8lut) { const b = await getBlock(baseRoot, man.e8lut.replace(/^did:holo:/, "")); e8lutData = new Float32Array(b.buffer, b.byteOffset, 2048); }
|
| 48 |
+
const manifest = buildEngineManifest(man, normRecs, e8lutData);
|
| 49 |
+
// tokenizer is the base's (finetune shares vocab); resolve a relative source against the BASE dir.
|
| 50 |
+
if (man.source && !/^https?:\/\//.test(man.source)) man.source = baseRoot + "/" + man.source;
|
| 51 |
+
return { manifest, fetchTensor, info: man };
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export default loadDeltaObject;
|
holo-load2bit.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-load2bit.mjs — the LOAD-DIRECT consumer (the "load" half of the 7B infra). Given a pre-compiled
|
| 2 |
+
// 2-bit κ-object (manifest.json + content-addressed b/<κ>.gz blocks, produced by compile2bit.mjs), it builds
|
| 3 |
+
// the engine manifest + a fetchTensor that streams blocks, verifies each by re-deriving its κ (Law L5),
|
| 4 |
+
// gunzips, and hands the engine the weights ALREADY 2-bit — no re-quant at load. The engine reads
|
| 5 |
+
// manifest.preQuantized=true (parts() returns the blocks verbatim) and incoherent=false (LDLQ ⇒ no FWHT).
|
| 6 |
+
// Hosting = serve the κ-object dir from anywhere; the κ-verify makes any mirror untrusted-safe.
|
| 7 |
+
//
|
| 8 |
+
// A family FINETUNE is stored as `base-κ + delta` (format "holo-delta/1"); loadKappaObject detects that and
|
| 9 |
+
// transparently delegates to holo-load-delta.mjs, which reconstructs the finetune's blocks and returns the
|
| 10 |
+
// SAME { manifest, fetchTensor } shape — so the engine, KV-cache, and Q's brain loader need no changes.
|
| 11 |
+
import { f16ToF32 } from "./qvac-ingest.mjs";
|
| 12 |
+
|
| 13 |
+
async function gunzip(u8) { const ds = new DecompressionStream("gzip"); const w = ds.writable.getWriter(); w.write(u8); w.close(); return new Uint8Array(await new Response(ds.readable).arrayBuffer()); }
|
| 14 |
+
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 15 |
+
|
| 16 |
+
// ── PERSISTENT κ-CACHE (the returning-user path): a κ-object's manifest + blocks are content-addressed and
|
| 17 |
+
// IMMUTABLE, so once fetched they can live in the browser's Cache API FOREVER, keyed by their own URL. A
|
| 18 |
+
// returning user then loads Q from local disk — ~0 network, no server (serverless by construction). It is
|
| 19 |
+
// untrusted-safe: every cached block is still L5 re-derived below, so a poisoned cache entry is rejected
|
| 20 |
+
// exactly like a poisoned network body. First visit warms the cache; every visit after is disk-speed. ──
|
| 21 |
+
const KCACHE = "holo-kappa-v1";
|
| 22 |
+
let _persistAsked = false;
|
| 23 |
+
async function _askPersist() {
|
| 24 |
+
if (_persistAsked) return; _persistAsked = true; // once per session: ask the browser NOT to evict the model under storage pressure
|
| 25 |
+
try { if (navigator.storage && navigator.storage.persist && !(await navigator.storage.persisted())) await navigator.storage.persist(); } catch (e) {}
|
| 26 |
+
}
|
| 27 |
+
// fetch a URL as bytes, disk-cached by URL. cache HIT → no network. MISS → fetch once (no-store bounds the
|
| 28 |
+
// in-flight RAM to one body) then store to disk. All failures fall back to a plain fetch (never block a load).
|
| 29 |
+
const _inflight = new Map(); // URL → in-flight fetch promise, so a parallel prefetch + the engine's read of the
|
| 30 |
+
// SAME block share ONE network fetch (never double-download).
|
| 31 |
+
async function cachedBytes(url) {
|
| 32 |
+
let cache = null;
|
| 33 |
+
try { cache = await caches.open(KCACHE); const hit = await cache.match(url); if (hit) return new Uint8Array(await hit.arrayBuffer()); } catch (e) { cache = null; }
|
| 34 |
+
if (_inflight.has(url)) return _inflight.get(url);
|
| 35 |
+
const p = (async () => {
|
| 36 |
+
const buf = await (await fetch(url, { cache: "no-store" })).arrayBuffer();
|
| 37 |
+
if (cache) { try { await cache.put(url, new Response(buf.slice(0), { headers: { "Content-Type": "application/octet-stream" } })); _askPersist(); } catch (e) {} }
|
| 38 |
+
return new Uint8Array(buf);
|
| 39 |
+
})();
|
| 40 |
+
_inflight.set(url, p);
|
| 41 |
+
try { return await p; } finally { _inflight.delete(url); }
|
| 42 |
+
}
|
| 43 |
+
// FAST FIRST LOAD: warm the whole block cache with bounded concurrency, so the engine's sequential per-tensor
|
| 44 |
+
// reads hit the cache instead of paying one HF round-trip at a time (~1.5 blocks/s → tens of blocks/s over HTTP/2).
|
| 45 |
+
// Fire-and-forget; the engine's getBlock shares any in-flight fetch (no double-download). Cross-origin-CDN safe.
|
| 46 |
+
function prefetchBlocks(baseUrl, kappas, conc = 12) {
|
| 47 |
+
try {
|
| 48 |
+
const urls = [...new Set(kappas.filter(Boolean))].map((k) => baseUrl + "/b/" + String(k).replace(":", "_") + ".gz");
|
| 49 |
+
let i = 0;
|
| 50 |
+
const worker = async () => { while (i < urls.length) { const u = urls[i++]; try { await cachedBytes(u); } catch (e) {} } };
|
| 51 |
+
Promise.all(Array.from({ length: Math.min(conc, urls.length) }, worker)).catch(() => {});
|
| 52 |
+
} catch (e) {}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// reshape a raw (gunzipped) block to what the engine reads. PURE — shared with the delta loader so both
|
| 56 |
+
// paths apply identical per-fmt handling. 2bit+fp16 → 2bit+f32 scales; everything else verbatim.
|
| 57 |
+
export function reshapeTensor(rec, raw) {
|
| 58 |
+
if (rec.fmt === "2bit" && rec.fp16) { // [2-bit packed][fp16 scales] → [2-bit][f32 scales]
|
| 59 |
+
const Kp = rec.K, q2 = (rec.N * Kp) / 4, nsc = rec.N * (Kp / 32);
|
| 60 |
+
const f16 = new Uint16Array(raw.buffer, raw.byteOffset + q2, nsc);
|
| 61 |
+
const out = new Uint8Array(q2 + nsc * 4); out.set(raw.subarray(0, q2), 0);
|
| 62 |
+
const f32 = new Float32Array(out.buffer, q2, nsc); for (let i = 0; i < nsc; i++) f32[i] = f16ToF32(f16[i]);
|
| 63 |
+
return out;
|
| 64 |
+
}
|
| 65 |
+
return raw; // 2bit+f32 (incoherence), q8 (embed), f32 (norms) — verbatim
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// build the engine manifest from model meta + normalized tensor records {name→{N,K,fmt,s?}}. PURE — shared.
|
| 69 |
+
export function buildEngineManifest(man, normRecs, e8lutData) {
|
| 70 |
+
const tensors = Object.entries(normRecs).map(([name, rec]) => ({ name, N: rec.N, K: rec.K, blk: rec.fmt !== "f32", fmt: rec.fmt, ...(rec.s !== undefined ? { s: rec.s } : {}) }));
|
| 71 |
+
const native = man.mode === "q4" || man.mode === "q3" || man.mode === "e8" || man.mode === "bitnet"; // native-bits κ-object
|
| 72 |
+
return {
|
| 73 |
+
d: man.d, n_heads: man.n_heads, n_kv_heads: man.n_kv_heads, ff: man.ff, vocab: man.vocab, n_layers: man.n_layers, hd: man.hd,
|
| 74 |
+
bits: native ? man.bits : 8, layout: man.layout, rope_base: man.rope_base, ...(man.maskId !== undefined ? { maskId: man.maskId, diffusion: true } : {}), attn_bias: man.attn_bias, qk_norm: man.qk_norm, qk_norm_dim: man.qk_norm_dim, tied: man.tied,
|
| 75 |
+
...(man.sub_norm ? { sub_norm: true } : {}), ...(man.bitlinear ? { bitlinear: true } : {}), ...(man.ffn_act ? { ffn_act: man.ffn_act } : {}), ...(man.moe ? { moe: man.moe } : {}),
|
| 76 |
+
...(native ? {} : { twoBit: true, incoherent: man.incoherent === true, preQuantized: true }), tensors, ...(e8lutData ? { e8lutData } : {}),
|
| 77 |
+
};
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export async function loadKappaObject(baseUrl, opts = {}) {
|
| 81 |
+
// Law L5: the manifest is the ROOT that names every block's κ. Verify the manifest's OWN bytes
|
| 82 |
+
// re-derive to a pinned κ BEFORE trusting man.tensors[*].kappa — otherwise a tampered manifest can
|
| 83 |
+
// re-point every block to a forged-but-self-consistent κ and each per-block check passes against the
|
| 84 |
+
// forgery. The pin is an EXTERNAL anchor (catalog/lock), never the manifest's own self-asserted root.
|
| 85 |
+
const manRaw = await cachedBytes(baseUrl + "/manifest.json"); // disk-cached (pinned + verified below), so a returning user's load is 0-network end to end
|
| 86 |
+
const manKappa = "sha256:" + hex(await crypto.subtle.digest("SHA-256", manRaw));
|
| 87 |
+
const pin = opts.expectKappa ? String(opts.expectKappa).replace(/^did:holo:/, "") : null;
|
| 88 |
+
if (pin) { if (manKappa !== pin) throw new Error("manifest κ MISMATCH (Law L5): " + manKappa.slice(0, 24) + "… ≠ pinned " + pin.slice(0, 24) + "…"); }
|
| 89 |
+
else if (!opts.allowUnpinned) throw new Error("manifest unpinned (Law L5): pass opts.expectKappa (catalog pin) or opts.allowUnpinned for dev");
|
| 90 |
+
const man = JSON.parse(new TextDecoder().decode(manRaw));
|
| 91 |
+
// FAMILY FINETUNE: a `base-κ + delta` object — reconstruct via the delta loader (same return shape).
|
| 92 |
+
if (man.format === "holo-delta/1") return (await import("./holo-load-delta.mjs")).loadDeltaObject(baseUrl, { ...opts, manifest: man });
|
| 93 |
+
// FAST FIRST LOAD: prefetch every block into the cache in parallel while the engine builds (turns a
|
| 94 |
+
// latency-bound sequential stream into a bandwidth-bound one — critical when serving off a remote CDN like HF).
|
| 95 |
+
if (opts.prefetch !== false) { try { prefetchBlocks(baseUrl, Object.values(man.tensors || {}).map((r) => r.kappa)); } catch (e) {} }
|
| 96 |
+
// RAM-bounded, DISK-cached: the engine fetches each tensor once, so decoded blocks are handed over and
|
| 97 |
+
// released — in-flight RAM stays ~one block (a 7B κ-object decompresses to >2.6 GB). The gzipped block
|
| 98 |
+
// bytes are persisted to the Cache API by their content-addressed URL (cachedBytes), so a returning user
|
| 99 |
+
// reads them from disk with no network. Every block — cached or fresh — is L5 re-derived (κ must match),
|
| 100 |
+
// so the cache is untrusted-safe.
|
| 101 |
+
const getBlock = async (kappa) => {
|
| 102 |
+
const gz = await cachedBytes(baseUrl + "/b/" + kappa.replace(":", "_") + ".gz");
|
| 103 |
+
const got = "sha256:" + hex(await crypto.subtle.digest("SHA-256", gz)); // Law L5: re-derive the κ
|
| 104 |
+
if (got !== kappa) throw new Error("κ MISMATCH " + kappa.slice(0, 24));
|
| 105 |
+
return await gunzip(gz);
|
| 106 |
+
};
|
| 107 |
+
const fetchTensor = async (name) => {
|
| 108 |
+
const rec = man.tensors[name]; if (!rec) return new Uint8Array(0);
|
| 109 |
+
return reshapeTensor(rec, await getBlock(rec.kappa));
|
| 110 |
+
};
|
| 111 |
+
// E₈ codebook (mode e8): the 256×8 LUT is its own content-addressed block — fetch + κ-verify (Law L5)
|
| 112 |
+
let e8lutData;
|
| 113 |
+
if (man.e8lut) { const b = await getBlock(man.e8lut.replace(/^did:holo:/, "")); e8lutData = new Float32Array(b.buffer, b.byteOffset, 2048); }
|
| 114 |
+
const manifest = buildEngineManifest(man, man.tensors, e8lutData);
|
| 115 |
+
// bundled tokenizer (SERVERLESS load): the header (tokenizer + arch) should load same-origin/on-device, no
|
| 116 |
+
// external host. A RELATIVE `source` already resolves against the κ-object's own dir. When the manifest
|
| 117 |
+
// declares a REMOTE (http) source but the κ-object ALSO ships a local tokenizer.gguf, PREFER the bundle —
|
| 118 |
+
// one cheap HEAD probe, and the manifest bytes (hence its Law-L5 pin) stay untouched. No bundle served →
|
| 119 |
+
// fall back to the declared source. This makes a κ-object with a bundled header 100% serverless to load.
|
| 120 |
+
const base = baseUrl.replace(/\/+$/, "");
|
| 121 |
+
if (man.source && !/^https?:\/\//.test(man.source)) man.source = base + "/" + man.source;
|
| 122 |
+
else if (man.source && /^https?:\/\//.test(man.source)) {
|
| 123 |
+
try { const h = await fetch(base + "/tokenizer.gguf", { method: "HEAD" }); if (h && h.ok) man.source = base + "/tokenizer.gguf"; } catch (e) {}
|
| 124 |
+
}
|
| 125 |
+
return { manifest, fetchTensor, info: man };
|
| 126 |
+
}
|
holo-model-frame.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-model-frame.mjs — A1 of the personal-model-zoo plan: the CANONICAL SHARED FRAME standard + a
|
| 2 |
+
// conformance gate, so every κ-addressable .holo LLM DECLARES the quantization frame it lives in.
|
| 3 |
+
//
|
| 4 |
+
// Why this exists: two models can family-dedup / index-delta (A2) ONLY if their unchanged tensors
|
| 5 |
+
// produce BYTE-IDENTICAL κ-blocks — which requires the SAME quantization transform ("frame"). The frame
|
| 6 |
+
// is the transform identity (codec, layout, bits, mode, twoBit, incoherence), NOT the model's weights or
|
| 7 |
+
// dims. The incoherence rotation is already deterministic (signed-FWHT seeded by tensor width K, see
|
| 8 |
+
// e8-quant.mjs signsFor), so a frame is reproducible from its descriptor alone.
|
| 9 |
+
//
|
| 10 |
+
// HONEST SCOPE: this does NOT requantize anything. Today's models are standard quant frames
|
| 11 |
+
// (holo-quant/<layout>-<bits>); the shared E8 frame (atlas-e8/v1) is what models compiled through the
|
| 12 |
+
// incoherent-E8 path carry. Moving everything into atlas-e8/v1 is Track B (gated on the 2-bit quality
|
| 13 |
+
// experiment), deliberately not done here. A1 just makes the frame EXPLICIT and ENFORCED.
|
| 14 |
+
//
|
| 15 |
+
// Pure JS, isomorphic (browser + Node 18+), zero deps. Node self-test scans ./models/<name>/manifest.json.
|
| 16 |
+
|
| 17 |
+
const FRAME_V = 1;
|
| 18 |
+
|
| 19 |
+
// the fields that DEFINE a quantization transform — two models with the same fingerprint apply the
|
| 20 |
+
// identical transform, so identical weights ⇒ identical κ-blocks (the precondition for A2 dedup).
|
| 21 |
+
const FRAME_KEYS = ["codec", "layout", "bits", "mode", "twoBit", "incoherent", "grid"];
|
| 22 |
+
|
| 23 |
+
async function sha256hex(str) {
|
| 24 |
+
const u8 = new TextEncoder().encode(str);
|
| 25 |
+
const d = await (globalThis.crypto || (await import("node:crypto")).webcrypto).subtle.digest("SHA-256", u8);
|
| 26 |
+
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// extract the frame-defining descriptor from a compiled-model manifest (holo-2bit/1 shape).
|
| 30 |
+
export function frameDescriptor(m) {
|
| 31 |
+
const d = {
|
| 32 |
+
codec: m.format || "unknown", // e.g. "holo-2bit/1"
|
| 33 |
+
layout: m.layout || null, // e.g. "q3f"
|
| 34 |
+
bits: m.bits ?? null, // e.g. 3
|
| 35 |
+
mode: m.mode || null, // e.g. "bitnet" | "q3"
|
| 36 |
+
twoBit: !!m.twoBit,
|
| 37 |
+
incoherent: !!m.incoherent, // QuIP#-style signed-FWHT rotation applied
|
| 38 |
+
grid: m.grid || (m.incoherent ? "e8" : "scalar") // e8 lattice vs scalar grid
|
| 39 |
+
};
|
| 40 |
+
return d;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// the canonical id for a frame — atlas-e8/v1 is THE shared frame family members ride; everything else
|
| 44 |
+
// declares its own standalone quant frame (still conformant, just not E8-shareable).
|
| 45 |
+
export function frameId(desc) {
|
| 46 |
+
if (desc.incoherent && desc.grid === "e8") return "atlas-e8/v1";
|
| 47 |
+
return `holo-quant/${desc.layout || desc.mode || "q"}-${desc.bits ?? "x"}bit`;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export async function frameFingerprint(desc) {
|
| 51 |
+
const canon = JSON.stringify(FRAME_KEYS.map((k) => [k, desc[k]])); // fixed key order ⇒ stable hash
|
| 52 |
+
return (await sha256hex(canon)).slice(0, 32);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// STAMP: add an explicit, self-verifying frame block to a manifest (idempotent).
|
| 56 |
+
export async function stampFrame(m) {
|
| 57 |
+
const desc = frameDescriptor(m);
|
| 58 |
+
m.frame = { v: FRAME_V, id: frameId(desc), fingerprint: await frameFingerprint(desc), ...desc };
|
| 59 |
+
return m;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// CONFORMANCE GATE: a .holo LLM must DECLARE a frame whose fingerprint re-derives from its own fields.
|
| 63 |
+
// Rejects: no frame block, tampered/mismatched fingerprint, or unknown frame version.
|
| 64 |
+
export async function checkFrame(m) {
|
| 65 |
+
if (!m || !m.frame) return { conforms: false, reason: "no frame declared (run stampFrame at compile)" };
|
| 66 |
+
if (m.frame.v !== FRAME_V) return { conforms: false, reason: `unknown frame version ${m.frame.v}` };
|
| 67 |
+
const want = await frameFingerprint(frameDescriptor(m));
|
| 68 |
+
if (m.frame.fingerprint !== want) return { conforms: false, reason: `fingerprint mismatch (manifest changed since stamp): ${m.frame.fingerprint} != ${want}` };
|
| 69 |
+
return { conforms: true, id: m.frame.id, fingerprint: m.frame.fingerprint, shared: m.frame.id === "atlas-e8/v1" };
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// can two stamped models family-dedup / index-delta? Same transform (fingerprint) AND same architecture.
|
| 73 |
+
export function shareable(a, b) {
|
| 74 |
+
if (!a.frame || !b.frame) return false;
|
| 75 |
+
if (a.frame.fingerprint !== b.frame.fingerprint) return false;
|
| 76 |
+
for (const k of ["d", "n_layers", "ff", "n_heads", "n_kv_heads", "hd", "vocab"]) if (a[k] !== b[k]) return false;
|
| 77 |
+
return true;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// ── Node self-test: scan ./models/*/manifest.json, stamp + gate them, group by family-dedup class,
|
| 81 |
+
// and prove the gate rejects a planted non-conformer. ──
|
| 82 |
+
if (typeof process !== "undefined" && process.argv[1] && process.argv[1].endsWith("holo-model-frame.mjs")) {
|
| 83 |
+
const fs = await import("node:fs"); const path = await import("node:path");
|
| 84 |
+
const root = path.join(process.cwd(), "models");
|
| 85 |
+
const dirs = fs.existsSync(root) ? fs.readdirSync(root).filter((d) => fs.existsSync(path.join(root, d, "manifest.json"))) : [];
|
| 86 |
+
const rows = [], byClass = {};
|
| 87 |
+
for (const name of dirs) {
|
| 88 |
+
let m; try { m = JSON.parse(fs.readFileSync(path.join(root, name, "manifest.json"), "utf8")); } catch { continue; }
|
| 89 |
+
await stampFrame(m);
|
| 90 |
+
const chk = await checkFrame(m);
|
| 91 |
+
const key = `${m.frame.id} · d${m.d}·L${m.n_layers}·ff${m.ff}`;
|
| 92 |
+
(byClass[key] ||= []).push(name);
|
| 93 |
+
rows.push({ model: name, id: m.frame.id, fp: m.frame.fingerprint.slice(0, 12), conforms: chk.conforms, shared: !!chk.shared });
|
| 94 |
+
}
|
| 95 |
+
console.log("\n— frame stamp + conformance over ./models —");
|
| 96 |
+
for (const r of rows) console.log(` ${r.conforms ? "✓" : "✗"} ${r.model.padEnd(16)} ${r.id.padEnd(22)} fp=${r.fp} ${r.shared ? "[E8-shared]" : "[standalone]"}`);
|
| 97 |
+
console.log("\n— family-dedup classes (same frame + same arch ⇒ A2 can dedup/delta) —");
|
| 98 |
+
for (const [k, v] of Object.entries(byClass)) console.log(` ${v.length}× ${k}\n ${v.join(", ")}`);
|
| 99 |
+
// gate must REJECT a non-conformer
|
| 100 |
+
const planted = { format: "holo-2bit/1", layout: "q3f", bits: 3 }; // no frame block
|
| 101 |
+
const bad = await checkFrame(planted);
|
| 102 |
+
const tampered = await stampFrame({ format: "holo-2bit/1", layout: "q3f", bits: 3, incoherent: false });
|
| 103 |
+
tampered.bits = 2; // change a frame-defining field AFTER stamping → stored fingerprint must no longer match
|
| 104 |
+
const badly = await checkFrame(tampered);
|
| 105 |
+
console.log(`\n— gate rejection proof —\n no-frame: conforms=${bad.conforms} (${bad.reason})\n tampered: conforms=${badly.conforms} (${badly.reason})`);
|
| 106 |
+
}
|
index.html
CHANGED
|
@@ -1,19 +1,134 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 2 |
+
<title>Q — private AI, in your browser</title>
|
| 3 |
+
<style>
|
| 4 |
+
:root{--bg:#0b0e14;--panel:#141922;--ink:#e6e9ef;--dim:#8a94a6;--q:#7c5cff;--u:#1f6feb;--line:#1e2531}
|
| 5 |
+
*{box-sizing:border-box}html,body{height:100%}
|
| 6 |
+
body{margin:0;font:15px/1.55 -apple-system,Segoe UI,Roboto,system-ui,monospace;background:var(--bg);color:var(--ink);display:flex;flex-direction:column;overscroll-behavior:none}
|
| 7 |
+
header{padding:10px 16px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex:0 0 auto}
|
| 8 |
+
header b{font-weight:600}header .s{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 9 |
+
#log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:12px;-webkit-overflow-scrolling:touch}
|
| 10 |
+
.msg{max-width:82%;padding:9px 13px;border-radius:12px;white-space:pre-wrap;word-wrap:break-word}
|
| 11 |
+
.u{align-self:flex-end;background:var(--u)}.a{align-self:flex-start;background:var(--panel);border:1px solid #232b3a}
|
| 12 |
+
.a.think{color:var(--dim);font-style:italic}
|
| 13 |
+
footer{padding:12px 16px;border-top:1px solid var(--line);display:flex;gap:8px;flex:0 0 auto;padding-bottom:calc(12px + env(safe-area-inset-bottom))}
|
| 14 |
+
#in{flex:1;background:var(--panel);border:1px solid #232b3a;color:var(--ink);border-radius:10px;padding:10px 12px;font:inherit;resize:none;max-height:140px}
|
| 15 |
+
button{background:var(--q);color:#fff;border:0;border-radius:10px;padding:0 18px;font:inherit;cursor:pointer}button:disabled{opacity:.4;cursor:default}
|
| 16 |
+
.stat{color:var(--dim);font-size:11px;margin-top:3px}
|
| 17 |
+
</style></head><body>
|
| 18 |
+
<header><b>Q</b> <span class=s id=st>booting…</span></header>
|
| 19 |
+
<div id=log></div>
|
| 20 |
+
<footer><textarea id=in rows=1 placeholder="Message Q…" disabled></textarea><button id=send disabled>Send</button></footer>
|
| 21 |
+
<script type=module>
|
| 22 |
+
import { ready, loadModel, MODELS, defaultModelIndex } from "./core/loader.js";
|
| 23 |
+
import { createEngine } from "./core/engine.js";
|
| 24 |
+
import { selfPersona, selfFacts } from "./core/q-self.mjs"; // ONE grounded self-knowledge, shared with the messenger + voice
|
| 25 |
+
const $ = (s) => document.querySelector(s);
|
| 26 |
+
const log = $("#log"), input = $("#in"), send = $("#send"), st = $("#st");
|
| 27 |
+
const bubble = (cls, text = "") => { const d = document.createElement("div"); d.className = "msg " + cls; d.textContent = text; log.appendChild(d); log.scrollTop = log.scrollHeight; return d; };
|
| 28 |
+
const params = new URLSearchParams(location.search);
|
| 29 |
+
const pick = params.get("m");
|
| 30 |
+
let m = pick ? (MODELS.find((x) => new RegExp(pick, "i").test(x.name)) || MODELS[0]) : MODELS[defaultModelIndex()];
|
| 31 |
+
// STREAM FROM HF: a bare link streams the HOLOGRAMTECH BitNet κ-object from Hugging Face; ?hf=<org/repo> or
|
| 32 |
+
// ?kappa=<absolute-url> override. The κ-object is content-addressed + pinned, so the host is an UNTRUSTED CDN —
|
| 33 |
+
// every block is re-derived (Law L5); a bad byte is rejected. Blocks cache locally after first load (0-net on return).
|
| 34 |
+
{
|
| 35 |
+
let hf = params.get("hf"), kappa = params.get("kappa");
|
| 36 |
+
if (!hf && !kappa && !pick) hf = "HOLOGRAMTECH/q-bitnet-2b"; // bare link → stream BitNet from Hologram's HF repo
|
| 37 |
+
if (hf || kappa) {
|
| 38 |
+
const base = (kappa || `https://huggingface.co/${hf}/resolve/main`).replace(/\/+$/, "");
|
| 39 |
+
const bit = MODELS.find((x) => (x.fam || "").toLowerCase() === "bitnet") || m;
|
| 40 |
+
m = { ...bit, kappaUrl: base, name: bit.name + " · via " + (hf || new URL(base).host) };
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// GROUND the model as on-device Q (a base/instruct model has NO self-knowledge — without this it confabulates
|
| 45 |
+
// a generic "I run on OpenAI/AWS cloud servers" identity, which is false). Injected as the SYSTEM turn.
|
| 46 |
+
function frameSystem() {
|
| 47 |
+
const PERSONA = selfPersona({ model: m, engine });
|
| 48 |
+
if (m.llama3) return `<|start_header_id|>system<|end_header_id|>\n\n${PERSONA}<|eot_id|>`;
|
| 49 |
+
if (m.qwen) return `<|im_start|>system\n${PERSONA}<|im_end|>\n`;
|
| 50 |
+
if (m.olmo) return `<|system|>\n${PERSONA}\n`;
|
| 51 |
+
return PERSONA + "\n\n";
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// GROUNDED IDENTITY (the anti-confabulation guard). A 2B model reverts to its training prior — "I'm GPT-3.5 on
|
| 55 |
+
// AWS" — when asked what/where it is, no matter the system prompt. But identity is not a guess: it is the TRUTH
|
| 56 |
+
// of THIS running instance. So provenance questions are answered DETERMINISTICALLY from the live facts (the real
|
| 57 |
+
// resident model + its κ + the real host it streamed from), never from the model. Grounded, not performed.
|
| 58 |
+
const IDENTITY_RX = /\b(are|r)\s*(you|u)\b.*\b(gpt|chatgpt|openai|claude|anthropic|gemini|bard|llama|language model|an? ai|running|local|on[- ]?device|in the browser|on (a )?server|in the cloud|hosted)\b|\bwhat( kind of| sort of| type of)?\b.*\b(model|llm|ai|are you|based on|powered by|architecture|run on|running)\b|\bwho\b.*\b(are you|made|built|created|trained|develop)\b|\bwhere\b.*\b(run|running|host|hosted|are you|live|located)\b|\bpowered by\b|\bwhat are you\b|\b(openai|chatgpt|gpt-?\d|aws|amazon web|google cloud|cloud server)\b|\b(local|cloud|server)\b.*\bmodel\b|\bdo you run\b/i;
|
| 59 |
+
function groundedIdentity() {
|
| 60 |
+
const f = selfFacts({ model: m, engine });
|
| 61 |
+
const name = f.model || (m && m.name) || "an on-device model";
|
| 62 |
+
const host = (f.weightsFrom && !/^local$/i.test(f.weightsFrom)) ? f.weightsFrom : "Hugging Face";
|
| 63 |
+
const q = f.quant ? ` (${f.quant})` : "";
|
| 64 |
+
return `I'm Q. I run the ${name}${q} entirely in your browser on WebGPU — not GPT, not OpenAI, and not on any server or cloud. `
|
| 65 |
+
+ `My weights streamed from ${host} and are content-addressed: every block is re-derived byte-for-byte as it loads, so nothing can be tampered with and no host has to be trusted. `
|
| 66 |
+
+ `Once I'm loaded, nothing you type ever leaves your device.`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
let engine = null, convIds = [], busy = false, armed = false, pending = null;
|
| 70 |
+
input.disabled = send.disabled = false; input.placeholder = "Message Q… (model loading — will send the moment it's ready)"; input.focus();
|
| 71 |
+
|
| 72 |
+
async function generate(text, skipUser) {
|
| 73 |
+
busy = true; input.disabled = send.disabled = true;
|
| 74 |
+
if (!skipUser) bubble("u", text);
|
| 75 |
+
// Answer identity/provenance questions from the grounded truth, not the model's confabulation.
|
| 76 |
+
if (IDENTITY_RX.test(text)) {
|
| 77 |
+
const a = bubble("a", groundedIdentity());
|
| 78 |
+
const stat = document.createElement("div"); stat.className = "stat"; stat.textContent = "grounded · from this instance"; a.after(stat);
|
| 79 |
+
busy = false; input.disabled = send.disabled = false; input.focus(); return;
|
| 80 |
+
}
|
| 81 |
+
const a = bubble("a think", "…"); let first = true;
|
| 82 |
+
const stat = document.createElement("div"); stat.className = "stat";
|
| 83 |
+
try {
|
| 84 |
+
let framed = engine.frameTurn(text, convIds.length > 0);
|
| 85 |
+
if (convIds.length === 0) framed = frameSystem() + framed;
|
| 86 |
+
let turnIds = engine.tokenize(framed);
|
| 87 |
+
if (m.bos && engine.bosId != null && convIds.length === 0) turnIds = [engine.bosId, ...turnIds];
|
| 88 |
+
const res = await engine.generate(convIds.concat(turnIds), { maxNew: m.cap || 256, onToken: ({ text: t, stats }) => {
|
| 89 |
+
if (first && t) { a.classList.remove("think"); a.textContent = ""; first = false; }
|
| 90 |
+
a.textContent = t; log.scrollTop = log.scrollHeight;
|
| 91 |
+
if (stats) stat.textContent = `${stats.tokps ? stats.tokps.toFixed(0) + " tok/s" : ""}${stats.ttft ? " · TTFT " + Math.round(stats.ttft) + "ms" : ""}`;
|
| 92 |
+
} });
|
| 93 |
+
if (first) { a.classList.remove("think"); a.textContent = res.text || "(no output)"; }
|
| 94 |
+
convIds = res.ids; a.after(stat);
|
| 95 |
+
} catch (e) { a.classList.remove("think"); a.textContent = "⚠ " + e.message; }
|
| 96 |
+
busy = false; input.disabled = send.disabled = false; input.focus();
|
| 97 |
+
}
|
| 98 |
+
async function proactiveGreeting() {
|
| 99 |
+
busy = true; input.disabled = send.disabled = true;
|
| 100 |
+
const a = bubble("a think", "…"); let first = true;
|
| 101 |
+
const stat = document.createElement("div"); stat.className = "stat";
|
| 102 |
+
const FALLBACK = "Hey — I'm Q, running entirely in your browser, no server. My weights streamed from Hugging Face and are verified by re-derivation. What can I help you with?";
|
| 103 |
+
try {
|
| 104 |
+
const P = "This is the very first thing you say to the person who just opened you. You are Q — a private AI running entirely in their browser with no server, your weights streamed from Hugging Face and verified by re-derivation. Greet them warmly in one or two sentences and invite them to ask you anything.";
|
| 105 |
+
let ids = engine.tokenize(engine.frameTurn(P, false));
|
| 106 |
+
if (m.bos && engine.bosId != null) ids = [engine.bosId, ...ids];
|
| 107 |
+
await engine.generate(ids, { maxNew: 64, onToken: ({ text: t, stats }) => { if (first && t) { a.classList.remove("think"); a.textContent = ""; first = false; } a.textContent = t; log.scrollTop = log.scrollHeight; if (stats && stats.tokps) stat.textContent = `${stats.tokps.toFixed(0)} tok/s`; } });
|
| 108 |
+
if (first || a.textContent.trim().length < 4) { a.classList.remove("think"); a.textContent = FALLBACK; } else a.after(stat);
|
| 109 |
+
} catch (e) { a.classList.remove("think"); a.textContent = FALLBACK; }
|
| 110 |
+
busy = false; input.disabled = send.disabled = false; input.focus();
|
| 111 |
+
}
|
| 112 |
+
function onSend() {
|
| 113 |
+
const text = input.value.trim(); if (!text || busy) return;
|
| 114 |
+
input.value = ""; input.style.height = "auto";
|
| 115 |
+
if (!armed) { pending = text; bubble("u", text); const w = bubble("a think", "…starting the model, one moment…"); w.dataset.pending = "1"; return; }
|
| 116 |
+
generate(text);
|
| 117 |
+
}
|
| 118 |
+
send.onclick = onSend;
|
| 119 |
+
input.onkeydown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSend(); } };
|
| 120 |
+
input.oninput = () => { input.style.height = "auto"; input.style.height = Math.min(140, input.scrollHeight) + "px"; };
|
| 121 |
+
|
| 122 |
+
try {
|
| 123 |
+
if (!navigator.gpu) throw new Error("This browser has no WebGPU — open in Chrome, Edge, or a recent mobile browser.");
|
| 124 |
+
st.textContent = `loading ${m.name} (${m.size})…`;
|
| 125 |
+
const loaded = await loadModel(m, { onStatus: (s) => { if (s) st.textContent = `${m.name}: ${s}`; }, onProgress: (d, t, w) => { st.textContent = `${m.name}: ${w} ${t ? Math.round(100 * d / t) : 0}%`; } });
|
| 126 |
+
if (!loaded || !loaded.gpu) throw new Error("model load failed");
|
| 127 |
+
engine = await createEngine(m, loaded);
|
| 128 |
+
armed = true;
|
| 129 |
+
st.textContent = `${m.name} · ${m.size} · in your browser · ready`;
|
| 130 |
+
input.placeholder = "Message Q…";
|
| 131 |
+
if (pending) { const w = [...log.querySelectorAll(".a")].reverse().find((x) => x.dataset.pending); if (w) w.remove(); const p = pending; pending = null; generate(p, true); }
|
| 132 |
+
else await proactiveGreeting();
|
| 133 |
+
} catch (e) { st.textContent = "⚠ " + e.message; bubble("a", "Could not start: " + e.message); }
|
| 134 |
+
</script></body></html>
|
pkg/holospaces_web.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pkg/holospaces_web_bg.wasm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d90b4ea8bccee8c239073c7ec7dd5158c6d11f1f668487570d17475d79e661e
|
| 3 |
+
size 6469596
|
q-chat.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1">
|
| 2 |
+
<title>Q · local chat</title>
|
| 3 |
+
<style>
|
| 4 |
+
:root{--bg:#0b0e14;--panel:#141922;--ink:#e6e9ef;--dim:#8a94a6;--q:#7c5cff;--u:#1f6feb;--line:#1e2531}
|
| 5 |
+
*{box-sizing:border-box}html,body{height:100%}
|
| 6 |
+
body{margin:0;font:15px/1.55 -apple-system,Segoe UI,Roboto,monospace;background:var(--bg);color:var(--ink);display:flex;flex-direction:column}
|
| 7 |
+
header{padding:10px 16px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px;flex:0 0 auto}
|
| 8 |
+
header b{font-weight:600}header .s{color:var(--dim);font-size:12px}
|
| 9 |
+
#log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:12px}
|
| 10 |
+
.msg{max-width:82%;padding:9px 13px;border-radius:12px;white-space:pre-wrap;word-wrap:break-word}
|
| 11 |
+
.u{align-self:flex-end;background:var(--u)}.a{align-self:flex-start;background:var(--panel);border:1px solid #232b3a}
|
| 12 |
+
.a.think{color:var(--dim);font-style:italic}
|
| 13 |
+
footer{padding:12px 16px;border-top:1px solid var(--line);display:flex;gap:8px;flex:0 0 auto}
|
| 14 |
+
#in{flex:1;background:var(--panel);border:1px solid #232b3a;color:var(--ink);border-radius:10px;padding:10px 12px;font:inherit;resize:none;max-height:140px}
|
| 15 |
+
button{background:var(--q);color:#fff;border:0;border-radius:10px;padding:0 18px;font:inherit;cursor:pointer}button:disabled{opacity:.4;cursor:default}
|
| 16 |
+
.stat{color:var(--dim);font-size:11px;margin-top:3px}
|
| 17 |
+
</style></head><body>
|
| 18 |
+
<header><b>Q</b> <span class=s id=st>booting…</span></header>
|
| 19 |
+
<div id=log></div>
|
| 20 |
+
<footer><textarea id=in rows=1 placeholder="Message Q…" disabled></textarea><button id=send disabled>Send</button></footer>
|
| 21 |
+
<script type=module>
|
| 22 |
+
import { ready, loadModel, MODELS, defaultModelIndex } from "./core/loader.js";
|
| 23 |
+
import { createEngine } from "./core/engine.js";
|
| 24 |
+
import { selfPersona } from "./core/q-self.mjs"; // ONE grounded self-knowledge, shared with the messenger + voice
|
| 25 |
+
const $ = (s) => document.querySelector(s);
|
| 26 |
+
const log = $("#log"), input = $("#in"), send = $("#send"), st = $("#st");
|
| 27 |
+
const bubble = (cls, text = "") => { const d = document.createElement("div"); d.className = "msg " + cls; d.textContent = text; log.appendChild(d); log.scrollTop = log.scrollHeight; return d; };
|
| 28 |
+
const params = new URLSearchParams(location.search);
|
| 29 |
+
const pick = params.get("m");
|
| 30 |
+
let m = pick ? (MODELS.find((x) => new RegExp(pick, "i").test(x.name)) || MODELS[0]) : MODELS[defaultModelIndex()];
|
| 31 |
+
// STREAM FROM ANYWHERE: ?hf=<org/repo> (a HuggingFace κ-object) or ?kappa=<absolute-url> loads the weights from
|
| 32 |
+
// that host's CDN instead of the local mount. The κ-object is content-addressed + pinned, so the host is an
|
| 33 |
+
// UNTRUSTED CDN — every block is re-derived (Law L5); a bad byte is rejected. Blocks cache locally after first
|
| 34 |
+
// load (0-network on return). Uses BitNet's engine flags (the HOLOGRAMTECH model is BitNet-2B).
|
| 35 |
+
{
|
| 36 |
+
const hf = params.get("hf"), kappa = params.get("kappa");
|
| 37 |
+
if (hf || kappa) {
|
| 38 |
+
const base = (kappa || `https://huggingface.co/${hf}/resolve/main`).replace(/\/+$/, "");
|
| 39 |
+
const bit = MODELS.find((x) => (x.fam || "").toLowerCase() === "bitnet") || m;
|
| 40 |
+
m = { ...bit, kappaUrl: base, name: bit.name + " · via " + (hf || new URL(base).host) };
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// GROUND the model as on-device Q (a base/instruct model has NO self-knowledge — without this it confabulates
|
| 45 |
+
// a generic "I run on OpenAI/AWS cloud servers" identity, which is false). Injected as the SYSTEM turn.
|
| 46 |
+
// the system block for the model's own chat template (matches core/engine frameTurn's per-family markers).
|
| 47 |
+
// PERSONA is Q's LIVE grounded self-knowledge (q-self), computed at call time so it names the real resident
|
| 48 |
+
// model + κ — without it a base model confabulates a cloud/OpenAI/AWS identity.
|
| 49 |
+
function frameSystem() {
|
| 50 |
+
const PERSONA = selfPersona({ model: m, engine });
|
| 51 |
+
if (m.llama3) return `<|start_header_id|>system<|end_header_id|>\n\n${PERSONA}<|eot_id|>`;
|
| 52 |
+
if (m.qwen) return `<|im_start|>system\n${PERSONA}<|im_end|>\n`;
|
| 53 |
+
if (m.olmo) return `<|system|>\n${PERSONA}\n`;
|
| 54 |
+
return PERSONA + "\n\n"; // word/plain frame: prepend as leading context
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
let engine = null, convIds = [], busy = false, armed = false, pending = null;
|
| 58 |
+
// type immediately — don't wait for the model. The first message is queued and auto-sent the instant Q is ready.
|
| 59 |
+
input.disabled = send.disabled = false; input.placeholder = "Message Q… (model loading — will send the moment it's ready)"; input.focus();
|
| 60 |
+
|
| 61 |
+
async function generate(text, skipUser) {
|
| 62 |
+
busy = true; input.disabled = send.disabled = true;
|
| 63 |
+
if (!skipUser) bubble("u", text);
|
| 64 |
+
const a = bubble("a think", "…"); let first = true;
|
| 65 |
+
const stat = document.createElement("div"); stat.className = "stat";
|
| 66 |
+
const t0 = performance.now();
|
| 67 |
+
try {
|
| 68 |
+
let framed = engine.frameTurn(text, convIds.length > 0);
|
| 69 |
+
if (convIds.length === 0) framed = frameSystem() + framed; // first turn → lead with the on-device Q persona
|
| 70 |
+
let turnIds = engine.tokenize(framed);
|
| 71 |
+
if (m.bos && engine.bosId != null && convIds.length === 0) turnIds = [engine.bosId, ...turnIds];
|
| 72 |
+
const res = await engine.generate(convIds.concat(turnIds), { maxNew: m.cap || 256, onToken: ({ text: t, stats }) => {
|
| 73 |
+
if (first && t) { a.classList.remove("think"); a.textContent = ""; first = false; }
|
| 74 |
+
a.textContent = t; log.scrollTop = log.scrollHeight;
|
| 75 |
+
if (stats) stat.textContent = `${stats.tokps ? stats.tokps.toFixed(0) + " tok/s" : ""}${stats.ttft ? " · TTFT " + Math.round(stats.ttft) + "ms" : ""}`;
|
| 76 |
+
} });
|
| 77 |
+
if (first) { a.classList.remove("think"); a.textContent = res.text || "(no output)"; }
|
| 78 |
+
convIds = res.ids; a.after(stat);
|
| 79 |
+
} catch (e) { a.classList.remove("think"); a.textContent = "⚠ " + e.message; }
|
| 80 |
+
busy = false; input.disabled = send.disabled = false; input.focus();
|
| 81 |
+
}
|
| 82 |
+
// Q reaches out first — a real, model-GENERATED opening (not a canned line), so it feels alive.
|
| 83 |
+
// This also warms the GPU pipelines. It is standalone (not added to convIds) so the real chat starts fresh.
|
| 84 |
+
async function proactiveGreeting() {
|
| 85 |
+
busy = true; input.disabled = send.disabled = true;
|
| 86 |
+
const a = bubble("a think", "…"); let first = true;
|
| 87 |
+
const stat = document.createElement("div"); stat.className = "stat";
|
| 88 |
+
const FALLBACK = "Hey — I'm Q, running entirely on your device, no server, my weights verified by re-derivation. What can I help you with?";
|
| 89 |
+
try {
|
| 90 |
+
const P = "This is the very first thing you say to the person who just opened you. You are Q — a private AI running entirely on their device with no server, your weights verified by re-derivation. Greet them warmly in one or two sentences and invite them to ask you anything.";
|
| 91 |
+
let ids = engine.tokenize(engine.frameTurn(P, false));
|
| 92 |
+
if (m.bos && engine.bosId != null) ids = [engine.bosId, ...ids];
|
| 93 |
+
await engine.generate(ids, { maxNew: 64, onToken: ({ text: t, stats }) => { if (first && t) { a.classList.remove("think"); a.textContent = ""; first = false; } a.textContent = t; log.scrollTop = log.scrollHeight; if (stats && stats.tokps) stat.textContent = `${stats.tokps.toFixed(0)} tok/s`; } });
|
| 94 |
+
if (first || a.textContent.trim().length < 4) { a.classList.remove("think"); a.textContent = FALLBACK; } else a.after(stat);
|
| 95 |
+
} catch (e) { a.classList.remove("think"); a.textContent = FALLBACK; }
|
| 96 |
+
busy = false; input.disabled = send.disabled = false; input.focus();
|
| 97 |
+
}
|
| 98 |
+
function onSend() {
|
| 99 |
+
const text = input.value.trim(); if (!text || busy) return;
|
| 100 |
+
input.value = ""; input.style.height = "auto";
|
| 101 |
+
if (!armed) { pending = text; bubble("u", text); const w = bubble("a think", "…starting the model, one moment…"); w.dataset.pending = "1"; return; }
|
| 102 |
+
generate(text);
|
| 103 |
+
}
|
| 104 |
+
send.onclick = onSend;
|
| 105 |
+
input.onkeydown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSend(); } };
|
| 106 |
+
input.oninput = () => { input.style.height = "auto"; input.style.height = Math.min(140, input.scrollHeight) + "px"; };
|
| 107 |
+
|
| 108 |
+
try {
|
| 109 |
+
if (!navigator.gpu) throw new Error("This browser has no WebGPU. Use Chrome/Edge.");
|
| 110 |
+
st.textContent = `loading ${m.name} (${m.size})…`;
|
| 111 |
+
const loaded = await loadModel(m, { onStatus: (s) => { if (s) st.textContent = `${m.name}: ${s}`; }, onProgress: (d, t, w) => { st.textContent = `${m.name}: ${w} ${t ? Math.round(100 * d / t) : 0}%`; } });
|
| 112 |
+
if (!loaded || !loaded.gpu) throw new Error("model load failed");
|
| 113 |
+
engine = await createEngine(m, loaded);
|
| 114 |
+
armed = true;
|
| 115 |
+
st.textContent = `${m.name} · ${m.size} · resident on your GPU · ready`;
|
| 116 |
+
input.placeholder = "Message Q…";
|
| 117 |
+
if (pending) { const w = [...log.querySelectorAll(".a")].reverse().find((x) => x.dataset.pending); if (w) w.remove(); const p = pending; pending = null; generate(p, true); } // you jumped in first → answer that
|
| 118 |
+
else await proactiveGreeting(); // otherwise Q reaches out the moment it's ready (also warms the GPU)
|
| 119 |
+
} catch (e) { st.textContent = "⚠ " + e.message; bubble("a", "Could not start: " + e.message); }
|
| 120 |
+
</script></body></html>
|
qvac-2bit.mjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// qvac-2bit.mjs — NATIVE 2-bit WebGPU matmul: read 2-bit weights DIRECTLY on the GPU (no decode to Q8),
|
| 2 |
+
// undo incoherence with a runtime Hadamard, accumulate in f32. This is the on-GPU realization of the E₈/
|
| 3 |
+
// QuIP# 2-bit win: the per-token weight sweep drops ~4× vs Q8, so a 7B model (≈1.75 GB at 2-bit) fits
|
| 4 |
+
// resident in consumer VRAM and runs at VRAM bandwidth instead of paging from disk.
|
| 5 |
+
//
|
| 6 |
+
// Math (one-sided incoherence, the efficient form): let R = FWHT∘diag(sign) — orthogonal AND self-inverse
|
| 7 |
+
// (FWHT normalized 1/√K, sign ∈ ±1). Store Ŵ′ = quantize₂(R·Wₙ) per row (R isotropizes the row so a 2-bit
|
| 8 |
+
// grid quantizes it well). At inference rotate the INPUT once, x′ = R·x, then yₙ = Σ Ŵ′[n,k]·x′[k] ≈
|
| 9 |
+
// (R·Wₙ)·(R·x) = Wₙ·x — the rotation cancels for free, no output Hadamard. Cost: one length-K Hadamard
|
| 10 |
+
// per matmul (O(K log K), negligible beside the O(N·K) matmul). Pure WebGPU; the codebook is a uniform
|
| 11 |
+
// 4-level grid {−3,−1,1,3}·scale with a per-32 block scale (same scale layout the engine's Q8 path uses).
|
| 12 |
+
|
| 13 |
+
const FWHT_WG = 256; // single-workgroup input rotation (K ≤ 2048)
|
| 14 |
+
const MM_WG = 64; // one workgroup per output row, 64-thread reduce
|
| 15 |
+
|
| 16 |
+
// ── CPU: deterministic ±1 signs (re-derivable from K, matches e8-quant's xorshift) ──
|
| 17 |
+
export function signsFor(K) {
|
| 18 |
+
const s = new Float32Array(K); let x = (0x9e3779b9 ^ K) >>> 0;
|
| 19 |
+
for (let i = 0; i < K; i++) { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; x >>>= 0; s[i] = (x & 1) ? 1 : -1; }
|
| 20 |
+
return s;
|
| 21 |
+
}
|
| 22 |
+
function fwht(a) { // in place, normalized (self-inverse)
|
| 23 |
+
const n = a.length;
|
| 24 |
+
for (let len = 1; len < n; len <<= 1) for (let i = 0; i < n; i += len << 1) for (let j = i; j < i + len; j++) { const u = a[j], v = a[j + len]; a[j] = u + v; a[j + len] = u - v; }
|
| 25 |
+
const s = 1 / Math.sqrt(n); for (let i = 0; i < n; i++) a[i] *= s;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// ── CPU: pack a weight matrix W [N,K] (row-major) to incoherent 2-bit + per-32 scales ──
|
| 29 |
+
// Returns { qw:Uint32Array(N*K/16), sc:Float32Array(N*K/32), sign:Float32Array(K) }.
|
| 30 |
+
// incoherent=false skips the rotation (naive 2-bit) — for the quality contrast only.
|
| 31 |
+
export function pack2bit(W, N, K, { incoherent = true } = {}) {
|
| 32 |
+
const nblk = K / 32, qw = new Uint32Array((N * K) / 16), sc = new Float32Array(N * nblk), sign = signsFor(K);
|
| 33 |
+
const row = new Float64Array(K);
|
| 34 |
+
for (let n = 0; n < N; n++) {
|
| 35 |
+
for (let k = 0; k < K; k++) row[k] = incoherent ? W[n * K + k] * sign[k] : W[n * K + k];
|
| 36 |
+
if (incoherent) fwht(row); // row ← R·Wₙ
|
| 37 |
+
// per-block scale: MSE-optimal step for a 4-level uniform grid {−3,−1,1,3}·s on ~Gaussian data is
|
| 38 |
+
// s ≈ 0.5·σ (grid spans ±1.5σ). Outliers beyond ±1.5σ clip — which is exactly what incoherence
|
| 39 |
+
// removes (the Hadamard Gaussianizes the row), so naive-2-bit clips heavy tails and incoherent does not.
|
| 40 |
+
for (let b = 0; b < nblk; b++) { let ss = 0; for (let i = 0; i < 32; i++) { const a = row[b * 32 + i]; ss += a * a; } sc[n * nblk + b] = (0.5 * Math.sqrt(ss / 32)) || 1e-12; }
|
| 41 |
+
for (let k = 0; k < K; k++) {
|
| 42 |
+
const t = row[k] / sc[n * nblk + (k >> 5)];
|
| 43 |
+
let q = Math.round((t + 3) / 2); if (q < 0) q = 0; else if (q > 3) q = 3; // grid {−3,−1,1,3}
|
| 44 |
+
const idx = n * K + k; qw[idx >> 4] |= q << ((idx & 15) * 2);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
return { qw, sc, sign };
|
| 48 |
+
}
|
| 49 |
+
// reconstruct (CPU reference for the stored 2-bit weights, in the ORIGINAL basis): undo R on each row
|
| 50 |
+
export function unpack2bit(qw, sc, sign, N, K, { incoherent = true } = {}) {
|
| 51 |
+
const nblk = K / 32, W = new Float32Array(N * K), row = new Float64Array(K);
|
| 52 |
+
for (let n = 0; n < N; n++) {
|
| 53 |
+
for (let k = 0; k < K; k++) { const idx = n * K + k; const q = (qw[idx >> 4] >>> ((idx & 15) * 2)) & 3; row[k] = (q * 2 - 3) * sc[n * nblk + (k >> 5)]; }
|
| 54 |
+
if (incoherent) { fwht(row); for (let k = 0; k < K; k++) row[k] *= sign[k]; } // R is self-inverse
|
| 55 |
+
for (let k = 0; k < K; k++) W[n * K + k] = row[k];
|
| 56 |
+
}
|
| 57 |
+
return W;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// f32 → f16 bits (Uint16). Scales are small positives; round-toward-zero of the mantissa is fine.
|
| 61 |
+
export function f32ToF16(val) {
|
| 62 |
+
_f32[0] = val; const x = _u32[0];
|
| 63 |
+
const sign = (x >>> 16) & 0x8000; let exp = ((x >>> 23) & 0xff) - 112; const mant = x & 0x7fffff;
|
| 64 |
+
if (exp <= 0) { if (exp < -10) return sign; const m = (mant | 0x800000) >> (1 - exp); return sign | (m >> 13); }
|
| 65 |
+
if (exp >= 31) return sign | 0x7c00;
|
| 66 |
+
return sign | (exp << 10) | (mant >> 13);
|
| 67 |
+
}
|
| 68 |
+
const _f32 = new Float32Array(1), _u32 = new Uint32Array(_f32.buffer);
|
| 69 |
+
|
| 70 |
+
// CODEBOOK-AWARE LDLQ to the 2-bit SCALAR grid {−3,−1,1,3}·sc — the engine's native 2-bit codebook, with
|
| 71 |
+
// NO incoherence (so no power-of-2 padding, no runtime Hadamard). Rounds input columns high→low feeding
|
| 72 |
+
// each future column's error back through L (the LDL factor of the input Hessian); L=null ⇒ plain scalar
|
| 73 |
+
// 2-bit (the fallback when no calibration Hessian of the right dim exists, e.g. the FFN down-proj). Returns
|
| 74 |
+
// the packed 2-bit indices (16 weights/u32, no padding — K must be a multiple of 16). sc = per-32 scales.
|
| 75 |
+
// `band` caps the feedback to the nearest `band` future columns (0 = full). The LDL factor's off-diagonal
|
| 76 |
+
// mass concentrates near the diagonal, so a band recovers most of the gain at O(N·K·band) instead of
|
| 77 |
+
// O(N·K²) — the difference between a feasible and an infeasible 7B compile in single-thread JS.
|
| 78 |
+
export function ldlqRound2bit(W, N, K, L, sc, band = 0, chunk = 4096) {
|
| 79 |
+
const qw = new Uint32Array((N * K) / 16), nb = K / 32, CH = Math.min(chunk, N), E = new Float32Array(CH * K);
|
| 80 |
+
for (let r0 = 0; r0 < N; r0 += CH) { // rows are independent in LDLQ → chunk them (E is CH·K, not N·K)
|
| 81 |
+
const rN = Math.min(CH, N - r0); E.fill(0, 0, rN * K);
|
| 82 |
+
for (let k = K - 1; k >= 0; k--) for (let ii = 0; ii < rN; ii++) {
|
| 83 |
+
const i = r0 + ii;
|
| 84 |
+
let corr = W[i * K + k]; if (L) { const jm = band ? Math.min(K, k + 1 + band) : K; for (let j = k + 1; j < jm; j++) corr += E[ii * K + j] * L[j * K + k]; }
|
| 85 |
+
const s = sc[i * nb + (k >> 5)]; let q = Math.round(corr / s / 2 + 1.5); if (q < 0) q = 0; else if (q > 3) q = 3;
|
| 86 |
+
E[ii * K + k] = W[i * K + k] - (q * 2 - 3) * s;
|
| 87 |
+
const idx = i * K + k; qw[idx >> 4] |= q << ((idx & 15) * 2);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
return qw;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
export const nextPow2 = (n) => { let p = 1; while (p < n) p <<= 1; return p; };
|
| 94 |
+
// re-quantize an engine Q8 tensor (int8 quants + per-32 f32 scales) → incoherent 2-bit, padding the input
|
| 95 |
+
// dim K to Kp = next power of 2 (the FWHT needs a pow2 length; padded weights/inputs are zeros ⇒ exact).
|
| 96 |
+
// Returns { q: packed 2-bit bytes [N*Kp/4], s: f32 scales [N*Kp/32], Kp }. The runtime rotates the input by
|
| 97 |
+
// the SAME R_Kp (signsFor(Kp)+FWHT), so Ŵ′·x′ = (R·W)(R·x) = W·x.
|
| 98 |
+
export function requant2bit(q8, s, N, K) {
|
| 99 |
+
const Kp = nextPow2(K), nb = Kp / 32, sb = K / 32;
|
| 100 |
+
const q = new Int8Array(q8.buffer, q8.byteOffset, N * K);
|
| 101 |
+
const sign = signsFor(Kp), row = new Float64Array(Kp);
|
| 102 |
+
const qw = new Uint32Array((N * Kp) / 16), sc = new Float32Array(N * nb);
|
| 103 |
+
for (let n = 0; n < N; n++) {
|
| 104 |
+
for (let k = 0; k < Kp; k++) row[k] = (k < K ? q[n * K + k] * s[n * sb + (k >> 5)] : 0) * sign[k];
|
| 105 |
+
fwht(row);
|
| 106 |
+
for (let b = 0; b < nb; b++) { let ss = 0; for (let i = 0; i < 32; i++) { const a = row[b * 32 + i]; ss += a * a; } sc[n * nb + b] = (0.5 * Math.sqrt(ss / 32)) || 1e-12; } // MSE-optimal step for the {−3,−1,1,3} grid on Gaussianised (incoherent) weights
|
| 107 |
+
for (let k = 0; k < Kp; k++) { const t = row[k] / sc[n * nb + (k >> 5)]; let qq = Math.round((t + 3) / 2); if (qq < 0) qq = 0; else if (qq > 3) qq = 3; const idx = n * Kp + k; qw[idx >> 4] |= qq << ((idx & 15) * 2); }
|
| 108 |
+
}
|
| 109 |
+
return { q: new Uint8Array(qw.buffer), s: sc, Kp };
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// ── WGSL: input rotation x′ = FWHT(sign ⊙ x), single workgroup, shared memory ──
|
| 113 |
+
const FWHT_WGSL = `
|
| 114 |
+
@group(0) @binding(0) var<storage,read> x: array<f32>;
|
| 115 |
+
@group(0) @binding(1) var<storage,read> sgn: array<f32>;
|
| 116 |
+
@group(0) @binding(2) var<storage,read_write> xr: array<f32>;
|
| 117 |
+
@group(0) @binding(3) var<uniform> P: vec4<u32>; // K, _, _, _
|
| 118 |
+
var<workgroup> sh: array<f32, 4096>; // 16 KB = the WebGPU min workgroup-storage limit; K ≤ 4096 (covers d up to 7B-class)
|
| 119 |
+
@compute @workgroup_size(${FWHT_WG})
|
| 120 |
+
fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
|
| 121 |
+
let K = P.x; let t = lid.x;
|
| 122 |
+
for (var i = t; i < K; i += ${FWHT_WG}u) { sh[i] = x[i] * sgn[i]; }
|
| 123 |
+
workgroupBarrier();
|
| 124 |
+
var len = 1u;
|
| 125 |
+
loop {
|
| 126 |
+
if (len >= K) { break; }
|
| 127 |
+
let half = K >> 1u;
|
| 128 |
+
for (var i = t; i < half; i += ${FWHT_WG}u) {
|
| 129 |
+
let blk = i / len; let j = i % len;
|
| 130 |
+
let a = blk * (len << 1u) + j; let b = a + len;
|
| 131 |
+
let u = sh[a]; let v = sh[b]; sh[a] = u + v; sh[b] = u - v;
|
| 132 |
+
}
|
| 133 |
+
workgroupBarrier();
|
| 134 |
+
len = len << 1u;
|
| 135 |
+
}
|
| 136 |
+
let nrm = 1.0 / sqrt(f32(K));
|
| 137 |
+
for (var i = t; i < K; i += ${FWHT_WG}u) { xr[i] = sh[i] * nrm; }
|
| 138 |
+
}`;
|
| 139 |
+
|
| 140 |
+
// ── WGSL: 2-bit GEMV — WORD-ORIENTED: each thread loads one u32 (16 weights), unpacks in registers,
|
| 141 |
+
// hoists the per-32 block scale (16 weights at a 16-aligned base never cross a 32 boundary → one scale
|
| 142 |
+
// read per word). Threads stride by workgroup over words → coalesced loads. f32 accumulate. ──
|
| 143 |
+
const MM2_WGSL = `
|
| 144 |
+
@group(0) @binding(0) var<storage,read> qw: array<u32>;
|
| 145 |
+
@group(0) @binding(1) var<storage,read> sc: array<f32>;
|
| 146 |
+
@group(0) @binding(2) var<storage,read> x: array<f32>;
|
| 147 |
+
@group(0) @binding(3) var<storage,read_write> o: array<f32>;
|
| 148 |
+
@group(0) @binding(4) var<uniform> P: vec4<u32>; // N, K, nblk, _
|
| 149 |
+
var<workgroup> red: array<f32, ${MM_WG}>;
|
| 150 |
+
@compute @workgroup_size(${MM_WG})
|
| 151 |
+
fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
| 152 |
+
let n = wid.x; let K = P.y; let words = K >> 4u; let rowW = n * words; let rowS = n * P.z;
|
| 153 |
+
var acc = 0.0; var w = lid.x;
|
| 154 |
+
loop {
|
| 155 |
+
if (w >= words) { break; }
|
| 156 |
+
let packed = qw[rowW + w];
|
| 157 |
+
let kb = w << 4u; let s = sc[rowS + (kb >> 5u)];
|
| 158 |
+
for (var j = 0u; j < 16u; j = j + 1u) { acc = acc + x[kb + j] * f32(i32((packed >> (j * 2u)) & 3u) * 2 - 3) * s; }
|
| 159 |
+
w = w + ${MM_WG}u;
|
| 160 |
+
}
|
| 161 |
+
red[lid.x] = acc; workgroupBarrier();
|
| 162 |
+
var r = ${MM_WG >> 1}u;
|
| 163 |
+
loop { if (r == 0u) { break; } if (lid.x < r) { red[lid.x] = red[lid.x] + red[lid.x + r]; } workgroupBarrier(); r = r >> 1u; }
|
| 164 |
+
if (lid.x == 0u) { o[n] = red[0]; }
|
| 165 |
+
}`;
|
| 166 |
+
|
| 167 |
+
// ── WGSL: Q8 GEMV (the engine's current format) — WORD-ORIENTED too, so the comparison is purely the
|
| 168 |
+
// bytes-read difference, not kernel quality. Each thread loads one u32 (4 int8), unpacks 4. ──
|
| 169 |
+
const MM8_WGSL = `
|
| 170 |
+
@group(0) @binding(0) var<storage,read> qw: array<u32>;
|
| 171 |
+
@group(0) @binding(1) var<storage,read> sc: array<f32>;
|
| 172 |
+
@group(0) @binding(2) var<storage,read> x: array<f32>;
|
| 173 |
+
@group(0) @binding(3) var<storage,read_write> o: array<f32>;
|
| 174 |
+
@group(0) @binding(4) var<uniform> P: vec4<u32>;
|
| 175 |
+
var<workgroup> red: array<f32, ${MM_WG}>;
|
| 176 |
+
@compute @workgroup_size(${MM_WG})
|
| 177 |
+
fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
| 178 |
+
let n = wid.x; let K = P.y; let words = K >> 2u; let rowW = n * words; let rowS = n * P.z;
|
| 179 |
+
var acc = 0.0; var w = lid.x;
|
| 180 |
+
loop {
|
| 181 |
+
if (w >= words) { break; }
|
| 182 |
+
let packed = qw[rowW + w];
|
| 183 |
+
let kb = w << 2u; let s = sc[rowS + (kb >> 5u)];
|
| 184 |
+
for (var j = 0u; j < 4u; j = j + 1u) { let b = (packed >> (j * 8u)) & 0xffu; acc = acc + x[kb + j] * f32(i32(b << 24u) >> 24u) * s; }
|
| 185 |
+
w = w + ${MM_WG}u;
|
| 186 |
+
}
|
| 187 |
+
red[lid.x] = acc; workgroupBarrier();
|
| 188 |
+
var r = ${MM_WG >> 1}u;
|
| 189 |
+
loop { if (r == 0u) { break; } if (lid.x < r) { red[lid.x] = red[lid.x] + red[lid.x + r]; } workgroupBarrier(); r = r >> 1u; }
|
| 190 |
+
if (lid.x == 0u) { o[n] = red[0]; }
|
| 191 |
+
}`;
|
| 192 |
+
|
| 193 |
+
// ── GPU helpers ──
|
| 194 |
+
const U = (typeof GPUBufferUsage !== "undefined") ? GPUBufferUsage : {};
|
| 195 |
+
function pipe(dev, code) { const m = dev.createShaderModule({ code }); return dev.createComputePipeline({ layout: "auto", compute: { module: m, entryPoint: "main" } }); }
|
| 196 |
+
function sbuf(dev, src) { const b = dev.createBuffer({ size: Math.max(16, src.byteLength), usage: U.STORAGE | U.COPY_DST | U.COPY_SRC }); dev.queue.writeBuffer(b, 0, src); return b; }
|
| 197 |
+
function obuf(dev, bytes) { return dev.createBuffer({ size: Math.max(16, bytes), usage: U.STORAGE | U.COPY_SRC }); }
|
| 198 |
+
function ubuf(dev, arr) { const b = dev.createBuffer({ size: 16, usage: U.UNIFORM | U.COPY_DST }); dev.queue.writeBuffer(b, 0, arr); return b; }
|
| 199 |
+
async function readf32(dev, buf, n) { const st = dev.createBuffer({ size: n * 4, usage: U.MAP_READ | U.COPY_DST }); const e = dev.createCommandEncoder(); e.copyBufferToBuffer(buf, 0, st, 0, n * 4); dev.queue.submit([e.finish()]); await st.mapAsync(GPUMapMode.READ); const out = new Float32Array(st.getMappedRange().slice(0)); st.unmap(); st.destroy(); return out; }
|
| 200 |
+
|
| 201 |
+
// ── the bench: correctness (2-bit incoherent vs f32 ref vs naive-2-bit vs Q8) + perf + memory ──
|
| 202 |
+
export async function runBench(dev, { N = 2048, K = 2048, iters = 200 } = {}) {
|
| 203 |
+
const nblk = K / 32;
|
| 204 |
+
// random Gaussian weights + input (a realistic single layer matmul)
|
| 205 |
+
let s = 1234567; const rnd = () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
|
| 206 |
+
const gauss = () => { const u = Math.max(1e-12, rnd()); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * rnd()); };
|
| 207 |
+
// heavy-tailed weights like real LLM layers (kurtosis ≫ 3): a Gaussian bulk plus sparse large spikes —
|
| 208 |
+
// the outliers that wreck naive low-bit quantization and that incoherence (the Hadamard) spreads out.
|
| 209 |
+
const W = new Float32Array(N * K); for (let i = 0; i < N * K; i++) { let w = gauss() * 0.05; if (rnd() < 0.02) w *= 6; W[i] = w; }
|
| 210 |
+
const x = new Float32Array(K); for (let i = 0; i < K; i++) x[i] = gauss();
|
| 211 |
+
// f32 reference y = W·x
|
| 212 |
+
const yref = new Float32Array(N); for (let n = 0; n < N; n++) { let a = 0; for (let k = 0; k < K; k++) a += W[n * K + k] * x[k]; yref[n] = a; }
|
| 213 |
+
const relErr = (y) => { let e = 0, r = 0; for (let n = 0; n < N; n++) { const d = y[n] - yref[n]; e += d * d; r += yref[n] * yref[n]; } return Math.sqrt(e / r); };
|
| 214 |
+
|
| 215 |
+
// pack incoherent 2-bit + naive 2-bit + Q8 (per-32 scale, the engine format)
|
| 216 |
+
const inc = pack2bit(W, N, K, { incoherent: true });
|
| 217 |
+
const nai = pack2bit(W, N, K, { incoherent: false });
|
| 218 |
+
const q8 = new Int8Array(N * K), q8s = new Float32Array(N * nblk);
|
| 219 |
+
for (let n = 0; n < N; n++) for (let b = 0; b < nblk; b++) { let mx = 0; for (let i = 0; i < 32; i++) { const a = Math.abs(W[n * K + b * 32 + i]); if (a > mx) mx = a; } const sca = (mx / 127) || 1e-12; q8s[n * nblk + b] = sca; for (let i = 0; i < 32; i++) { let q = Math.round(W[n * K + b * 32 + i] / sca); if (q > 127) q = 127; else if (q < -127) q = -127; q8[n * K + b * 32 + i] = q; } }
|
| 220 |
+
|
| 221 |
+
// GPU pipelines
|
| 222 |
+
const pF = pipe(dev, FWHT_WGSL), p2 = pipe(dev, MM2_WGSL), p8 = pipe(dev, MM8_WGSL);
|
| 223 |
+
// buffers
|
| 224 |
+
const xB = sbuf(dev, x), xrB = obuf(dev, K * 4), sgnB = sbuf(dev, inc.sign), Pf = ubuf(dev, new Uint32Array([K, 0, 0, 0]));
|
| 225 |
+
const qwB = sbuf(dev, inc.qw), scB = sbuf(dev, inc.sc), oB = obuf(dev, N * 4), P2 = ubuf(dev, new Uint32Array([N, K, nblk, 0]));
|
| 226 |
+
const naiqwB = sbuf(dev, nai.qw), naiscB = sbuf(dev, nai.sc), oNB = obuf(dev, N * 4);
|
| 227 |
+
const q8B = sbuf(dev, new Uint8Array(q8.buffer)), q8sB = sbuf(dev, q8s), o8B = obuf(dev, N * 4);
|
| 228 |
+
|
| 229 |
+
const bgF = (xin) => dev.createBindGroup({ layout: pF.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: xin } }, { binding: 1, resource: { buffer: sgnB } }, { binding: 2, resource: { buffer: xrB } }, { binding: 3, resource: { buffer: Pf } }] });
|
| 230 |
+
const bg2 = (qw, sc, xin, o) => dev.createBindGroup({ layout: p2.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: qw } }, { binding: 1, resource: { buffer: sc } }, { binding: 2, resource: { buffer: xin } }, { binding: 3, resource: { buffer: o } }, { binding: 4, resource: { buffer: P2 } }] });
|
| 231 |
+
const bg8 = dev.createBindGroup({ layout: p8.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: q8B } }, { binding: 1, resource: { buffer: q8sB } }, { binding: 2, resource: { buffer: xB } }, { binding: 3, resource: { buffer: o8B } }, { binding: 4, resource: { buffer: P2 } }] });
|
| 232 |
+
|
| 233 |
+
// ── correctness ──
|
| 234 |
+
const doInc = K <= 4096; // single-workgroup FWHT covers K ≤ 4096 (16 KB shared)
|
| 235 |
+
let yInc = null, gpuCpu = null;
|
| 236 |
+
if (doInc) {
|
| 237 |
+
// incoherent path: GPU rotate x→x′, then 2-bit matmul with x′
|
| 238 |
+
{ const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(pF); p.setBindGroup(0, bgF(xB)); p.dispatchWorkgroups(1); p.setPipeline(p2); p.setBindGroup(0, bg2(qwB, scB, xrB, oB)); p.dispatchWorkgroups(N); p.end(); dev.queue.submit([e.finish()]); }
|
| 239 |
+
yInc = await readf32(dev, oB, N);
|
| 240 |
+
// CPU re-derivation of the SAME stored 2-bit weights — independent check the GPU kernel agrees
|
| 241 |
+
const Wrec = unpack2bit(inc.qw, inc.sc, inc.sign, N, K, { incoherent: true });
|
| 242 |
+
const yCpu = new Float32Array(N); for (let n = 0; n < N; n++) { let a = 0; for (let k = 0; k < K; k++) a += Wrec[n * K + k] * x[k]; yCpu[n] = a; }
|
| 243 |
+
let e2 = 0, rr = 0; for (let n = 0; n < N; n++) { const d = yInc[n] - yCpu[n]; e2 += d * d; rr += yCpu[n] * yCpu[n]; } gpuCpu = Math.sqrt(e2 / rr);
|
| 244 |
+
}
|
| 245 |
+
// naive path (no rotation) + Q8 path
|
| 246 |
+
{ const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(p2); p.setBindGroup(0, bg2(naiqwB, naiscB, xB, oNB)); p.dispatchWorkgroups(N); p.end(); dev.queue.submit([e.finish()]); }
|
| 247 |
+
const yNai = await readf32(dev, oNB, N);
|
| 248 |
+
{ const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(p8); p.setBindGroup(0, bg8); p.dispatchWorkgroups(N); p.end(); dev.queue.submit([e.finish()]); }
|
| 249 |
+
const yQ8 = await readf32(dev, o8B, N);
|
| 250 |
+
|
| 251 |
+
// ── perf — apples-to-apples: time each MATMUL alone (incoherence rotation measured separately) ──
|
| 252 |
+
// pre-rotate x once so the 2-bit matmul reads x′ without re-running the FWHT in the timed loop.
|
| 253 |
+
if (doInc) { const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(pF); p.setBindGroup(0, bgF(xB)); p.dispatchWorkgroups(1); p.end(); dev.queue.submit([e.finish()]); }
|
| 254 |
+
const time = async (fn) => { fn(); await dev.queue.onSubmittedWorkDone(); const t0 = performance.now(); for (let i = 0; i < iters; i++) fn(); await dev.queue.onSubmittedWorkDone(); return (performance.now() - t0) / iters; };
|
| 255 |
+
const run2mm = () => { const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(p2); p.setBindGroup(0, bg2(qwB, scB, doInc ? xrB : xB, oB)); p.dispatchWorkgroups(N); p.end(); dev.queue.submit([e.finish()]); };
|
| 256 |
+
const run8mm = () => { const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(p8); p.setBindGroup(0, bg8); p.dispatchWorkgroups(N); p.end(); dev.queue.submit([e.finish()]); };
|
| 257 |
+
const runF = () => { const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(pF); p.setBindGroup(0, bgF(xB)); p.dispatchWorkgroups(1); p.end(); dev.queue.submit([e.finish()]); };
|
| 258 |
+
const ms2 = await time(run2mm), ms8 = await time(run8mm), msF = doInc ? await time(runF) : null;
|
| 259 |
+
|
| 260 |
+
const bytes2 = inc.qw.byteLength + inc.sc.byteLength, bytes8 = q8.byteLength + q8s.byteLength;
|
| 261 |
+
const gbps = (b, ms) => (b / 1e9) / (ms / 1e3);
|
| 262 |
+
return {
|
| 263 |
+
N, K, iters,
|
| 264 |
+
err: { incoherent2bit: doInc ? relErr(yInc) : null, naive2bit: relErr(yNai), q8: relErr(yQ8), gpu_vs_cpu_2bit: gpuCpu },
|
| 265 |
+
perf: { ms_2bit_mm: +ms2.toFixed(4), ms_q8_mm: +ms8.toFixed(4), ms_fwht: msF == null ? null : +msF.toFixed(4), matmul_speedup: +(ms8 / ms2).toFixed(2), gbps_2bit: +gbps(bytes2, ms2).toFixed(0), gbps_q8: +gbps(bytes8, ms8).toFixed(0) },
|
| 266 |
+
mem: { MB_2bit: +(bytes2 / 1e6).toFixed(2), MB_q8: +(bytes8 / 1e6).toFixed(2), ratio: +(bytes8 / bytes2).toFixed(2), bits_per_weight: +(bytes2 * 8 / (N * K)).toFixed(2) },
|
| 267 |
+
};
|
| 268 |
+
}
|
qvac-gpu.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
qvac-ingest.mjs
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// QVAC disk-streaming GGUF ingestion — pure JS, no wasm in the weight path.
|
| 2 |
+
//
|
| 3 |
+
// The wasm path holds the whole GGUF in wasm32 linear memory (~4 GB ceiling), so
|
| 4 |
+
// it can't ingest a 7B/14B model. This module reads a large GGUF straight off disk
|
| 5 |
+
// (HTTP Range against the served file) and converts each tensor to the engine's
|
| 6 |
+
// block format ON DEMAND, so RAM never holds more than one tensor at a time.
|
| 7 |
+
//
|
| 8 |
+
// It is a faithful port of qvac-gguf::dequantize_raw + qvac-layer::quant_blocks +
|
| 9 |
+
// model_specs, so the bytes it produces are what the GPU engine already consumes.
|
| 10 |
+
// The tokenizer stays in wasm: we feed wasm only the GGUF HEADER (the first
|
| 11 |
+
// data_offset bytes), which is all qvac_load_gpu needs for the BPE + manifest.
|
| 12 |
+
|
| 13 |
+
import { makeIQ } from "./forge/gguf-forge-iq-dequant.mjs";
|
| 14 |
+
|
| 15 |
+
// ── f16 → f32 (matches qvac_gguf::f16_to_f32) ──
|
| 16 |
+
export function f16ToF32(h) {
|
| 17 |
+
const sign = (h >> 15) & 1, exp = (h >> 10) & 0x1f, mant = h & 0x3ff;
|
| 18 |
+
let val;
|
| 19 |
+
if (exp === 0) val = mant * Math.pow(2, -24);
|
| 20 |
+
else if (exp === 0x1f) return mant ? NaN : (sign ? -Infinity : Infinity);
|
| 21 |
+
else val = (1 + mant / 1024) * Math.pow(2, exp - 15);
|
| 22 |
+
return sign ? -val : val;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const QK = 32, QK_K = 256;
|
| 26 |
+
export const GGML = {
|
| 27 |
+
F32: 0, F16: 1, Q4_0: 2, Q4_1: 3, Q8_0: 8, Q2_K: 10, Q3_K: 11, Q4_K: 12, Q5_K: 13, Q6_K: 14,
|
| 28 |
+
IQ2_XXS: 16, IQ2_XS: 17, IQ3_XXS: 18, IQ1_S: 19, IQ4_NL: 20, IQ3_S: 21, IQ2_S: 22, IQ4_XS: 23, IQ1_M: 29,
|
| 29 |
+
TQ2_0: 35,
|
| 30 |
+
};
|
| 31 |
+
// IQ-quant byte layouts: [block elements, block bytes]. (ggml-common.h:485-563)
|
| 32 |
+
const IQ_BLOCK = {
|
| 33 |
+
16: [QK_K, 66], 17: [QK_K, 74], 18: [QK_K, 98], 19: [QK_K, 50], 20: [32, 18],
|
| 34 |
+
21: [QK_K, 110], 22: [QK_K, 82], 23: [QK_K, 136], 29: [QK_K, 56],
|
| 35 |
+
};
|
| 36 |
+
// BitNet ternary TQ2_0 (ggml-common.h:273): qs[64] + f16 d = 66 B / 256. Runtime
|
| 37 |
+
// dequant is float64 (the Tier-A oracle gguf-forge-dequant.mjs is the bit-exact ref).
|
| 38 |
+
const TQ_BLOCK = { 35: [QK_K, 66] };
|
| 39 |
+
function dequantTq2_0Rt(raw, elements) {
|
| 40 |
+
const out = new Float32Array(elements);
|
| 41 |
+
const nb = elements / QK_K;
|
| 42 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 43 |
+
let o = 0, base = 0;
|
| 44 |
+
for (let i = 0; i < nb; ++i) {
|
| 45 |
+
const d = f16ToF32(dv.getUint16(base + 64, true));
|
| 46 |
+
for (let j = 0; j < 64; j += 32)
|
| 47 |
+
for (let l = 0; l < 4; ++l)
|
| 48 |
+
for (let m = 0; m < 32; ++m) out[o++] = (((raw[base + j + m] >> (l * 2)) & 3) - 1) * d;
|
| 49 |
+
base += 66;
|
| 50 |
+
}
|
| 51 |
+
return out;
|
| 52 |
+
}
|
| 53 |
+
const TQ_RT = { 35: dequantTq2_0Rt };
|
| 54 |
+
// Runtime IQ dequant (float64; the Tier-A oracle is the bit-exact reference).
|
| 55 |
+
const _iq = makeIQ((x) => x, f16ToF32);
|
| 56 |
+
const IQ_RT = {
|
| 57 |
+
16: _iq.dequantIQ2XXS, 17: _iq.dequantIQ2XS, 18: _iq.dequantIQ3XXS, 19: _iq.dequantIQ1S,
|
| 58 |
+
20: _iq.dequantIQ4NL, 21: _iq.dequantIQ3S, 22: _iq.dequantIQ2S, 23: _iq.dequantIQ4XS, 29: _iq.dequantIQ1M,
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
// On-disk byte length of `elements` of a ggml type (matches type_byte_len).
|
| 62 |
+
export function typeByteLen(t, elements) {
|
| 63 |
+
switch (t) {
|
| 64 |
+
case GGML.F32: return elements * 4;
|
| 65 |
+
case GGML.F16: return elements * 2;
|
| 66 |
+
case GGML.Q8_0: return (elements / QK) * (2 + QK);
|
| 67 |
+
case GGML.Q4_0: return (elements / QK) * (2 + QK / 2);
|
| 68 |
+
case GGML.Q4_1: return (elements / QK) * (2 + 2 + QK / 2);
|
| 69 |
+
case GGML.Q2_K: return (elements / QK_K) * 84;
|
| 70 |
+
case GGML.Q3_K: return (elements / QK_K) * 110;
|
| 71 |
+
case GGML.Q4_K: return (elements / QK_K) * 144;
|
| 72 |
+
case GGML.Q5_K: return (elements / QK_K) * 176;
|
| 73 |
+
case GGML.Q6_K: return (elements / QK_K) * 210;
|
| 74 |
+
default:
|
| 75 |
+
if (t in IQ_BLOCK) { const [be, bb] = IQ_BLOCK[t]; return (elements / be) * bb; }
|
| 76 |
+
if (t in TQ_BLOCK) { const [be, bb] = TQ_BLOCK[t]; return (elements / be) * bb; }
|
| 77 |
+
throw new Error("unsupported ggml type " + t);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
// (block elements, block bytes) for the range reader.
|
| 81 |
+
function blockShape(t) {
|
| 82 |
+
switch (t) {
|
| 83 |
+
case GGML.F32: return [1, 4];
|
| 84 |
+
case GGML.F16: return [1, 2];
|
| 85 |
+
case GGML.Q8_0: return [QK, 2 + QK];
|
| 86 |
+
case GGML.Q4_0: return [QK, 2 + QK / 2];
|
| 87 |
+
case GGML.Q4_1: return [QK, 2 + 2 + QK / 2];
|
| 88 |
+
case GGML.Q2_K: return [QK_K, 84];
|
| 89 |
+
case GGML.Q3_K: return [QK_K, 110];
|
| 90 |
+
case GGML.Q4_K: return [QK_K, 144];
|
| 91 |
+
case GGML.Q5_K: return [QK_K, 176];
|
| 92 |
+
case GGML.Q6_K: return [QK_K, 210];
|
| 93 |
+
default:
|
| 94 |
+
if (t in IQ_BLOCK) return IQ_BLOCK[t];
|
| 95 |
+
if (t in TQ_BLOCK) return TQ_BLOCK[t];
|
| 96 |
+
throw new Error("unsupported ggml type " + t);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Dequantize raw tensor bytes → Float32Array (port of dequantize_raw).
|
| 101 |
+
export function dequantizeRaw(t, raw, elements) {
|
| 102 |
+
const out = new Float32Array(elements);
|
| 103 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 104 |
+
let o = 0;
|
| 105 |
+
if (t === GGML.F32) {
|
| 106 |
+
for (let i = 0; i < elements; i++) out[i] = dv.getFloat32(i * 4, true);
|
| 107 |
+
} else if (t === GGML.F16) {
|
| 108 |
+
for (let i = 0; i < elements; i++) out[i] = f16ToF32(dv.getUint16(i * 2, true));
|
| 109 |
+
} else if (t === GGML.Q8_0) {
|
| 110 |
+
const bb = 2 + QK;
|
| 111 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 112 |
+
const d = f16ToF32(dv.getUint16(p, true));
|
| 113 |
+
for (let j = 0; j < QK; j++) out[o++] = d * (raw[p + 2 + j] << 24 >> 24);
|
| 114 |
+
}
|
| 115 |
+
} else if (t === GGML.Q4_0) {
|
| 116 |
+
const bb = 2 + QK / 2;
|
| 117 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 118 |
+
const d = f16ToF32(dv.getUint16(p, true));
|
| 119 |
+
const qs = p + 2;
|
| 120 |
+
for (let j = 0; j < QK / 2; j++) out[o + j] = d * ((raw[qs + j] & 0x0f) - 8); // low nibble → j
|
| 121 |
+
for (let j = 0; j < QK / 2; j++) out[o + QK / 2 + j] = d * ((raw[qs + j] >> 4) - 8); // high → j+16
|
| 122 |
+
o += QK;
|
| 123 |
+
}
|
| 124 |
+
} else if (t === GGML.Q4_1) {
|
| 125 |
+
const bb = 2 + 2 + QK / 2;
|
| 126 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 127 |
+
const d = f16ToF32(dv.getUint16(p, true)), m = f16ToF32(dv.getUint16(p + 2, true));
|
| 128 |
+
const qs = p + 4;
|
| 129 |
+
for (let j = 0; j < QK / 2; j++) out[o + j] = (raw[qs + j] & 0x0f) * d + m;
|
| 130 |
+
for (let j = 0; j < QK / 2; j++) out[o + QK / 2 + j] = (raw[qs + j] >> 4) * d + m;
|
| 131 |
+
o += QK;
|
| 132 |
+
}
|
| 133 |
+
} else if (t === GGML.Q4_K) {
|
| 134 |
+
// Q4_K: 256-element super-block of 144 B = d(f16) + dmin(f16) + 12 packed 6-bit scales/mins + 128 nibble qs.
|
| 135 |
+
const bb = 144;
|
| 136 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 137 |
+
const d = f16ToF32(dv.getUint16(p, true)), dmin = f16ToF32(dv.getUint16(p + 2, true));
|
| 138 |
+
const sc = p + 4, qs = p + 16;
|
| 139 |
+
const sm = (j) => j < 4 ? [raw[sc + j] & 63, raw[sc + j + 4] & 63]
|
| 140 |
+
: [(raw[sc + j + 4] & 0xF) | ((raw[sc + j - 4] >> 6) << 4), (raw[sc + j + 4] >> 4) | ((raw[sc + j] >> 6) << 4)];
|
| 141 |
+
let q = 0, is = 0;
|
| 142 |
+
for (let j = 0; j < QK_K; j += 64) {
|
| 143 |
+
const [s1, m1] = sm(is), [s2, m2] = sm(is + 1);
|
| 144 |
+
for (let l = 0; l < 32; l++) out[o + l] = d * s1 * (raw[qs + q + l] & 0xF) - dmin * m1;
|
| 145 |
+
for (let l = 0; l < 32; l++) out[o + 32 + l] = d * s2 * (raw[qs + q + l] >> 4) - dmin * m2;
|
| 146 |
+
o += 64; q += 32; is += 2;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
} else if (t === GGML.Q6_K) {
|
| 150 |
+
const bb = 210;
|
| 151 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 152 |
+
const ql = p, qh = p + 128, sc = p + 192, d = f16ToF32(dv.getUint16(p + 208, true));
|
| 153 |
+
for (let n = 0; n < 2; n++) {
|
| 154 |
+
const qlo = ql + n * 64, qho = qh + n * 32, sco = sc + n * 8, yo = o + n * 128;
|
| 155 |
+
for (let l = 0; l < 32; l++) {
|
| 156 |
+
const is = (l / 16) | 0;
|
| 157 |
+
const q1 = ((raw[qlo + l] & 0x0f) | (((raw[qho + l] >> 0) & 3) << 4)) - 32;
|
| 158 |
+
const q2 = ((raw[qlo + l + 32] & 0x0f) | (((raw[qho + l] >> 2) & 3) << 4)) - 32;
|
| 159 |
+
const q3 = ((raw[qlo + l] >> 4) | (((raw[qho + l] >> 4) & 3) << 4)) - 32;
|
| 160 |
+
const q4 = ((raw[qlo + l + 32] >> 4) | (((raw[qho + l] >> 6) & 3) << 4)) - 32;
|
| 161 |
+
out[yo + l] = d * (raw[sco + is] << 24 >> 24) * q1;
|
| 162 |
+
out[yo + l + 32] = d * (raw[sco + is + 2] << 24 >> 24) * q2;
|
| 163 |
+
out[yo + l + 64] = d * (raw[sco + is + 4] << 24 >> 24) * q3;
|
| 164 |
+
out[yo + l + 96] = d * (raw[sco + is + 6] << 24 >> 24) * q4;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
o += QK_K;
|
| 168 |
+
}
|
| 169 |
+
} else if (t === GGML.Q2_K) {
|
| 170 |
+
// 84 B: scales[16] qs[64] d(f16) dmin(f16). y = d*(sc&0xF)*q2 - dmin*(sc>>4).
|
| 171 |
+
const bb = 84;
|
| 172 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 173 |
+
const sc = p, qs = p + 16, d = f16ToF32(dv.getUint16(p + 80, true)), dmin = f16ToF32(dv.getUint16(p + 82, true));
|
| 174 |
+
let is = 0;
|
| 175 |
+
for (let n = 0; n < QK_K; n += 128) {
|
| 176 |
+
const q = qs + (n >> 7) * 32;
|
| 177 |
+
for (let shift = 0; shift < 8; shift += 2) {
|
| 178 |
+
let s = raw[sc + is++];
|
| 179 |
+
for (let l = 0; l < 16; l++) out[o++] = d * (s & 0xf) * ((raw[q + l] >> shift) & 3) - dmin * (s >> 4);
|
| 180 |
+
s = raw[sc + is++];
|
| 181 |
+
for (let l = 0; l < 16; l++) out[o++] = d * (s & 0xf) * ((raw[q + l + 16] >> shift) & 3) - dmin * (s >> 4);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
} else if (t === GGML.Q3_K) {
|
| 186 |
+
// 110 B: hmask[32] qs[64] scales[12] d(f16). 6-bit signed scales via kmask unpack.
|
| 187 |
+
const bb = 110, km1 = 0x03030303, km2 = 0x0f0f0f0f;
|
| 188 |
+
const aux = new Uint32Array(4), sb = new Int8Array(aux.buffer);
|
| 189 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 190 |
+
const hm = p, qs = p + 32, sco = p + 96, d = f16ToF32(dv.getUint16(p + 108, true));
|
| 191 |
+
aux[0] = dv.getUint32(sco, true); aux[1] = dv.getUint32(sco + 4, true); aux[2] = dv.getUint32(sco + 8, true);
|
| 192 |
+
const tmp = aux[2];
|
| 193 |
+
aux[2] = ((aux[0] >>> 4) & km2) | (((tmp >>> 4) & km1) << 4);
|
| 194 |
+
aux[3] = ((aux[1] >>> 4) & km2) | (((tmp >>> 6) & km1) << 4);
|
| 195 |
+
aux[0] = (aux[0] & km2) | (((tmp >>> 0) & km1) << 4);
|
| 196 |
+
aux[1] = (aux[1] & km2) | (((tmp >>> 2) & km1) << 4);
|
| 197 |
+
let is = 0, m = 1;
|
| 198 |
+
for (let n = 0; n < QK_K; n += 128) {
|
| 199 |
+
const q = qs + (n >> 7) * 32;
|
| 200 |
+
for (let shift = 0; shift < 8; shift += 2) {
|
| 201 |
+
let dl = d * (sb[is++] - 32);
|
| 202 |
+
for (let l = 0; l < 16; l++) out[o++] = dl * (((raw[q + l] >> shift) & 3) - ((raw[hm + l] & m) ? 0 : 4));
|
| 203 |
+
dl = d * (sb[is++] - 32);
|
| 204 |
+
for (let l = 0; l < 16; l++) out[o++] = dl * (((raw[q + l + 16] >> shift) & 3) - ((raw[hm + l + 16] & m) ? 0 : 4));
|
| 205 |
+
m <<= 1;
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
} else if (t === GGML.Q5_K) {
|
| 210 |
+
// 176 B: d(f16) dmin(f16) scales[12] qh[32] ql[128]. Q4_K nibbles + 5th bit from qh.
|
| 211 |
+
const bb = 176;
|
| 212 |
+
const smk4 = (sc, j) => j < 4 ? [raw[sc + j] & 63, raw[sc + j + 4] & 63]
|
| 213 |
+
: [(raw[sc + j + 4] & 0xF) | ((raw[sc + j - 4] >> 6) << 4), (raw[sc + j + 4] >> 4) | ((raw[sc + j] >> 6) << 4)];
|
| 214 |
+
for (let p = 0; p + bb <= raw.byteLength; p += bb) {
|
| 215 |
+
const d = f16ToF32(dv.getUint16(p, true)), dmin = f16ToF32(dv.getUint16(p + 2, true));
|
| 216 |
+
const sc = p + 4, qh = p + 16; let ql = p + 48, is = 0, u1 = 1, u2 = 2;
|
| 217 |
+
for (let j = 0; j < QK_K; j += 64) {
|
| 218 |
+
const [s1, m1] = smk4(sc, is), [s2, m2] = smk4(sc, is + 1);
|
| 219 |
+
const d1 = d * s1, mm1 = dmin * m1, d2 = d * s2, mm2 = dmin * m2;
|
| 220 |
+
for (let l = 0; l < 32; l++) out[o++] = d1 * ((raw[ql + l] & 0xF) + ((raw[qh + l] & u1) ? 16 : 0)) - mm1;
|
| 221 |
+
for (let l = 0; l < 32; l++) out[o++] = d2 * ((raw[ql + l] >> 4) + ((raw[qh + l] & u2) ? 16 : 0)) - mm2;
|
| 222 |
+
ql += 32; is += 2; u1 <<= 2; u2 <<= 2;
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
} else if (t in IQ_RT) {
|
| 226 |
+
return IQ_RT[t](raw, elements); // IQ-quants (float64 runtime; oracle is the bit-exact ref)
|
| 227 |
+
} else if (t in TQ_RT) {
|
| 228 |
+
return TQ_RT[t](raw, elements); // BitNet TQ2_0 (float64 runtime; oracle is the bit-exact ref)
|
| 229 |
+
} else throw new Error("unsupported ggml type " + t);
|
| 230 |
+
return out;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
// Re-quantize a [n,k] f32 tensor into the engine's per-32-block format
|
| 234 |
+
// (port of qvac-layer::quant_blocks). bits=4 → (nibble-8)*scale, scale=amax/7,
|
| 235 |
+
// sequential nibble packing; bits=8 → int8, scale=amax/127. Returns {q,s}.
|
| 236 |
+
// Arithmetic is done in f32 (Math.fround) with round-half-away-from-zero to match
|
| 237 |
+
// Rust's f32 `round()` byte-for-byte, so the frames are bit-identical to wasm's.
|
| 238 |
+
const fr = Math.fround;
|
| 239 |
+
const rnd = (x) => x >= 0 ? Math.floor(x + 0.5) : Math.ceil(x - 0.5); // half away from zero (Rust f32::round)
|
| 240 |
+
export function quantBlocks(f, n, k, bits) {
|
| 241 |
+
const nb = k / 32;
|
| 242 |
+
const s = new Float32Array(n * nb);
|
| 243 |
+
let si = 0;
|
| 244 |
+
if (bits === 4) {
|
| 245 |
+
const q = new Uint8Array(n * k / 2);
|
| 246 |
+
for (let row = 0; row < n; row++) {
|
| 247 |
+
const base = row * k;
|
| 248 |
+
for (let b = 0; b < nb; b++) {
|
| 249 |
+
const bo = base + b * 32;
|
| 250 |
+
let amax = 0; for (let j = 0; j < 32; j++) { const a = Math.abs(f[bo + j]); if (a > amax) amax = a; }
|
| 251 |
+
amax = Math.max(amax, 1e-9); const scale = fr(amax / 7); s[si++] = scale;
|
| 252 |
+
for (let j = 0; j < 32; j++) {
|
| 253 |
+
let qv = rnd(fr(f[bo + j] / scale)); qv = qv < -8 ? -8 : qv > 7 ? 7 : qv; qv = (qv + 8) & 0xf;
|
| 254 |
+
const g = bo + j;
|
| 255 |
+
if ((g & 1) === 0) q[g >> 1] |= qv; else q[g >> 1] |= qv << 4;
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
return { q, s };
|
| 260 |
+
} else {
|
| 261 |
+
const q = new Uint8Array(n * k);
|
| 262 |
+
for (let row = 0; row < n; row++) {
|
| 263 |
+
const base = row * k;
|
| 264 |
+
for (let b = 0; b < nb; b++) {
|
| 265 |
+
const bo = base + b * 32;
|
| 266 |
+
let amax = 0; for (let j = 0; j < 32; j++) { const a = Math.abs(f[bo + j]); if (a > amax) amax = a; }
|
| 267 |
+
amax = Math.max(amax, 1e-9); const scale = fr(amax / 127); s[si++] = scale;
|
| 268 |
+
for (let j = 0; j < 32; j++) { let qv = rnd(fr(f[bo + j] / scale)); qv = qv < -127 ? -127 : qv > 127 ? 127 : qv; q[bo + j] = qv & 0xff; }
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
return { q, s };
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// FAST PATH: GGUF Q4_0 → engine Q4 with NO dequant/requant — a pure relayout.
|
| 276 |
+
// GGUF Q4_0 already stores (nibble-8)*d, exactly the engine's convention, so the
|
| 277 |
+
// nibble value maps straight across; we only reorder the interleaved (j, j+16)
|
| 278 |
+
// nibbles into the engine's sequential packing and widen the f16 scale to f32.
|
| 279 |
+
// This is both faster (integer-only) and bit-exact to the GGUF (no requant loss).
|
| 280 |
+
export function relayoutQ4(raw, n, k) {
|
| 281 |
+
const nb = k / 32;
|
| 282 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 283 |
+
const q = new Uint8Array(n * k / 2), s = new Float32Array(n * nb);
|
| 284 |
+
let bo = 0, si = 0;
|
| 285 |
+
for (let row = 0; row < n; row++) {
|
| 286 |
+
const rowBase = row * k;
|
| 287 |
+
for (let b = 0; b < nb; b++) {
|
| 288 |
+
s[si++] = f16ToF32(dv.getUint16(bo, true));
|
| 289 |
+
const qs = bo + 2, blkBase = rowBase + b * 32;
|
| 290 |
+
for (let w = 0; w < 32; w++) {
|
| 291 |
+
const nib = w < 16 ? (raw[qs + w] & 0x0f) : (raw[qs + (w - 16)] >> 4);
|
| 292 |
+
const g = blkBase + w;
|
| 293 |
+
if ((g & 1) === 0) q[g >> 1] |= nib; else q[g >> 1] |= nib << 4;
|
| 294 |
+
}
|
| 295 |
+
bo += 18;
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
return { q, s };
|
| 299 |
+
}
|
| 300 |
+
// FAST PATH: GGUF Q8_0 → engine Q8 — copy the i8 quants, widen f16 scale to f32.
|
| 301 |
+
export function relayoutQ8(raw, n, k) {
|
| 302 |
+
const nb = k / 32;
|
| 303 |
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
| 304 |
+
const q = new Uint8Array(n * k), s = new Float32Array(n * nb);
|
| 305 |
+
let bo = 0, si = 0, qi = 0;
|
| 306 |
+
for (let row = 0; row < n; row++) {
|
| 307 |
+
for (let b = 0; b < nb; b++) {
|
| 308 |
+
s[si++] = f16ToF32(dv.getUint16(bo, true));
|
| 309 |
+
for (let w = 0; w < 32; w++) q[qi++] = raw[bo + 2 + w];
|
| 310 |
+
bo += 34;
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
return { q, s };
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// ── GGUF header parser (just enough: tensor directory + data offset) ──
|
| 317 |
+
class Cur {
|
| 318 |
+
constructor(buf) { this.b = buf; this.dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); this.p = 0; this.v1 = false; }
|
| 319 |
+
need(n) { if (this.p + n > this.b.byteLength) throw new RangeError("short"); }
|
| 320 |
+
u8() { this.need(1); return this.b[this.p++]; }
|
| 321 |
+
u16() { this.need(2); const v = this.dv.getUint16(this.p, true); this.p += 2; return v; }
|
| 322 |
+
u32() { this.need(4); const v = this.dv.getUint32(this.p, true); this.p += 4; return v; }
|
| 323 |
+
u64() { this.need(8); const lo = this.dv.getUint32(this.p, true), hi = this.dv.getUint32(this.p + 4, true); this.p += 8; return hi * 4294967296 + lo; }
|
| 324 |
+
lenField() { return this.v1 ? this.u32() : this.u64(); }
|
| 325 |
+
skipStr() { const n = this.lenField(); this.need(n); this.p += n; }
|
| 326 |
+
skipValue(ty) {
|
| 327 |
+
switch (ty) {
|
| 328 |
+
case 0: case 1: case 7: this.p += 1; break;
|
| 329 |
+
case 2: case 3: this.p += 2; break;
|
| 330 |
+
case 4: case 5: case 6: this.p += 4; break;
|
| 331 |
+
case 10: case 11: case 12: this.p += 8; break;
|
| 332 |
+
case 8: this.skipStr(); break;
|
| 333 |
+
case 9: { const ety = this.u32(); const cnt = this.lenField(); for (let i = 0; i < cnt; i++) this.skipValue(ety); break; }
|
| 334 |
+
default: throw new Error("bad meta type " + ty);
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
// Read a SCALAR metadata value (numbers/bool/string); arrays are skipped (return undefined).
|
| 338 |
+
readScalar(ty) {
|
| 339 |
+
switch (ty) {
|
| 340 |
+
case 0: return this.u8();
|
| 341 |
+
case 1: { const v = this.u8(); return v << 24 >> 24; }
|
| 342 |
+
case 2: return this.u16();
|
| 343 |
+
case 3: { const v = this.u16(); return v << 16 >> 16; }
|
| 344 |
+
case 4: return this.u32();
|
| 345 |
+
case 5: { const v = this.u32(); return v | 0; }
|
| 346 |
+
case 6: { this.need(4); const v = this.dv.getFloat32(this.p, true); this.p += 4; return v; }
|
| 347 |
+
case 7: return this.u8() !== 0;
|
| 348 |
+
case 8: { const n = this.lenField(); const s = new TextDecoder().decode(this.b.subarray(this.p, this.p + n)); this.p += n; return s; }
|
| 349 |
+
case 10: return this.u64();
|
| 350 |
+
case 11: return this.u64();
|
| 351 |
+
case 12: { this.need(8); const v = this.dv.getFloat64(this.p, true); this.p += 8; return v; }
|
| 352 |
+
case 9: { const ety = this.u32(); const cnt = this.lenField(); for (let i = 0; i < cnt; i++) this.skipValue(ety); return undefined; }
|
| 353 |
+
default: throw new Error("bad meta type " + ty);
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Parse a GGUF header buffer → { version, dataOffset, tensors:[{name,dims,ggmlType,offset}] }.
|
| 359 |
+
// `buf` must contain at least up to the (aligned) end of the tensor-info table.
|
| 360 |
+
export function parseGgufHeader(buf) {
|
| 361 |
+
const c = new Cur(buf);
|
| 362 |
+
if (c.u32() !== 0x46554747) throw new Error("not GGUF");
|
| 363 |
+
const version = c.u32();
|
| 364 |
+
if (version !== 1 && version !== 2 && version !== 3) throw new Error("GGUF version " + version);
|
| 365 |
+
c.v1 = version === 1;
|
| 366 |
+
const tensorCount = c.lenField();
|
| 367 |
+
const metaCount = c.lenField();
|
| 368 |
+
let alignment = 32;
|
| 369 |
+
const meta = {};
|
| 370 |
+
for (let i = 0; i < metaCount; i++) {
|
| 371 |
+
const keyLen = c.lenField(); const key = new TextDecoder().decode(buf.subarray(c.p, c.p + keyLen)); c.p += keyLen;
|
| 372 |
+
const ty = c.u32();
|
| 373 |
+
const v = c.readScalar(ty); // arrays → undefined (skipped)
|
| 374 |
+
if (v !== undefined) meta[key] = v;
|
| 375 |
+
if (key === "general.alignment" && typeof v === "number") alignment = v;
|
| 376 |
+
}
|
| 377 |
+
const tensors = [];
|
| 378 |
+
for (let i = 0; i < tensorCount; i++) {
|
| 379 |
+
const nl = c.lenField(); const name = new TextDecoder().decode(buf.subarray(c.p, c.p + nl)); c.p += nl;
|
| 380 |
+
const nd = c.u32(); const dims = []; for (let j = 0; j < nd; j++) dims.push(c.lenField());
|
| 381 |
+
const ggmlType = c.u32(); const offset = c.u64();
|
| 382 |
+
tensors.push({ name, dims, ggmlType, offset });
|
| 383 |
+
}
|
| 384 |
+
alignment = Math.max(1, alignment);
|
| 385 |
+
const dataOffset = Math.ceil(c.p / alignment) * alignment;
|
| 386 |
+
return { version, dataOffset, tensors, meta };
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// Build the engine manifest (dims + tensor list with N,K,blk) from a parsed GGUF
|
| 390 |
+
// header — a pure-JS port of qvac-layer::model_specs + gpu_export_manifest, so
|
| 391 |
+
// conversion/streaming needs no wasm. `tensors` is the header's tensor directory.
|
| 392 |
+
export function buildManifest(meta, tensors, bits) {
|
| 393 |
+
const arch = meta["general.architecture"] || "llama";
|
| 394 |
+
const mu = (k) => { const v = meta[`${arch}.${k}`]; return typeof v === "number" ? Math.round(v) : undefined; };
|
| 395 |
+
const tset = new Set(tensors.map((t) => t.name));
|
| 396 |
+
const tbyname = {}; for (const t of tensors) tbyname[t.name] = t;
|
| 397 |
+
const d = mu("embedding_length") || 0;
|
| 398 |
+
const n_layers = mu("block_count") || 0;
|
| 399 |
+
const n_heads = mu("attention.head_count") || 0;
|
| 400 |
+
const n_kv_heads = mu("attention.head_count_kv") || n_heads;
|
| 401 |
+
// For MoE, the experts use expert_feed_forward_length (e.g. Qwen3-30B-A3B: 768),
|
| 402 |
+
// which differs from the (unused) dense feed_forward_length (6144). OLMoE's two
|
| 403 |
+
// values happen to be equal. `ff` everywhere downstream means the EXPERT ff for MoE.
|
| 404 |
+
const ff = (mu("expert_feed_forward_length") || mu("feed_forward_length")) || 0;
|
| 405 |
+
const hd = mu("attention.key_length") || (n_heads ? Math.floor(d / n_heads) : 0);
|
| 406 |
+
const kv_dim = n_kv_heads * hd;
|
| 407 |
+
const rope_base = (typeof meta[`${arch}.rope.freq_base`] === "number") ? meta[`${arch}.rope.freq_base`] : 10000;
|
| 408 |
+
const attn_bias = tset.has("blk.0.attn_q.bias");
|
| 409 |
+
const qk_norm = tset.has("blk.0.attn_q_norm.weight");
|
| 410 |
+
const qk_norm_dim = qk_norm ? (tbyname["blk.0.attn_q_norm.weight"].dims[0] | 0) : 0; // hd (per-head, Qwen3) or d (full, OLMoE)
|
| 411 |
+
const n_experts = mu("expert_count") || 0; // >0 → MoE
|
| 412 |
+
const n_used = mu("expert_used_count") || 0;
|
| 413 |
+
const moe = n_experts > 0;
|
| 414 |
+
const bitnet = /^bitnet/.test(arch); // BitNet b1.58: sub-norms before wo/w_down + ReLU² gated FFN
|
| 415 |
+
const tied = !tset.has("output.weight");
|
| 416 |
+
const vocab = d > 0 && tbyname["token_embd.weight"] ? Math.floor(tbyname["token_embd.weight"].dims.reduce((a, b) => a * b, 1) / d) : 0;
|
| 417 |
+
const blk = (name, N, K) => ({ name, N, K, blk: true });
|
| 418 |
+
const nrm = (name, K) => ({ name, N: 1, K, blk: false });
|
| 419 |
+
const t = [];
|
| 420 |
+
t.push(blk("embed", vocab, d));
|
| 421 |
+
t.push(nrm("final_norm", d));
|
| 422 |
+
t.push(blk("lm_head", vocab, d));
|
| 423 |
+
for (let i = 0; i < n_layers; i++) {
|
| 424 |
+
t.push(nrm(`l${i}.attn_norm`, d));
|
| 425 |
+
t.push(blk(`l${i}.wq`, n_heads * hd, d));
|
| 426 |
+
t.push(blk(`l${i}.wk`, kv_dim, d));
|
| 427 |
+
t.push(blk(`l${i}.wv`, kv_dim, d));
|
| 428 |
+
if (attn_bias) { t.push(nrm(`l${i}.bq`, n_heads * hd)); t.push(nrm(`l${i}.bk`, kv_dim)); t.push(nrm(`l${i}.bv`, kv_dim)); }
|
| 429 |
+
if (qk_norm) { t.push(nrm(`l${i}.q_norm`, qk_norm_dim)); t.push(nrm(`l${i}.k_norm`, qk_norm_dim)); }
|
| 430 |
+
if (bitnet) t.push(nrm(`l${i}.attn_sub_norm`, n_heads * hd));
|
| 431 |
+
t.push(blk(`l${i}.wo`, d, n_heads * hd));
|
| 432 |
+
t.push(nrm(`l${i}.ffn_norm`, d));
|
| 433 |
+
if (bitnet) t.push(nrm(`l${i}.ffn_sub_norm`, ff));
|
| 434 |
+
if (moe) {
|
| 435 |
+
t.push(nrm(`l${i}.router`, n_experts * d)); // ffn_gate_inp [n_experts, d] f32 (CPU top-k)
|
| 436 |
+
// experts are NOT enumerated here (n_layers·n_experts·3 is huge); the engine
|
| 437 |
+
// generates `l{i}.e{e}.{gate,up,down}` names for the top-k it actually needs.
|
| 438 |
+
} else {
|
| 439 |
+
t.push(blk(`l${i}.w_gate`, ff, d));
|
| 440 |
+
t.push(blk(`l${i}.w_up`, ff, d));
|
| 441 |
+
t.push(blk(`l${i}.w_down`, d, ff));
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
const out = { d, n_heads, n_kv_heads, ff, vocab, n_layers, hd, bits, rope_base, attn_bias, qk_norm, qk_norm_dim, tied, tensors: t };
|
| 445 |
+
if (moe) out.moe = { n_experts, n_used };
|
| 446 |
+
if (bitnet) { out.sub_norm = true; out.ffn_act = "relu2"; }
|
| 447 |
+
return out;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
// engine tensor name → GGUF tensor name (mirror of model_specs).
|
| 451 |
+
export function ggufNameFor(name, hasOutputWeight) {
|
| 452 |
+
if (name === "embed") return "token_embd.weight";
|
| 453 |
+
if (name === "final_norm") return "output_norm.weight";
|
| 454 |
+
if (name === "lm_head") return hasOutputWeight ? "output.weight" : "token_embd.weight";
|
| 455 |
+
const m = name.match(/^l(\d+)\.(.+)$/); if (!m) return null;
|
| 456 |
+
const i = m[1], r = m[2], p = `blk.${i}.`;
|
| 457 |
+
const map = {
|
| 458 |
+
"attn_norm": "attn_norm.weight", "ffn_norm": "ffn_norm.weight",
|
| 459 |
+
"attn_sub_norm": "attn_sub_norm.weight", "ffn_sub_norm": "ffn_sub_norm.weight",
|
| 460 |
+
"wq": "attn_q.weight", "wk": "attn_k.weight", "wv": "attn_v.weight", "wo": "attn_output.weight",
|
| 461 |
+
"bq": "attn_q.bias", "bk": "attn_k.bias", "bv": "attn_v.bias",
|
| 462 |
+
"q_norm": "attn_q_norm.weight", "k_norm": "attn_k_norm.weight",
|
| 463 |
+
"w_gate": "ffn_gate.weight", "w_up": "ffn_up.weight", "w_down": "ffn_down.weight",
|
| 464 |
+
};
|
| 465 |
+
return map[r] ? p + map[r] : null;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
// Read the GGUF header from `url` (HTTP Range), growing the read until the tensor
|
| 469 |
+
// table fits. Returns { dataOffset, tensors, headerBytes } (headerBytes = the first
|
| 470 |
+
// dataOffset bytes, to hand to wasm qvac_load_gpu for the tokenizer + manifest).
|
| 471 |
+
export async function readHeader(url, readRange, initial = 48 * 1024 * 1024) {
|
| 472 |
+
let n = initial, parsed = null, buf = null;
|
| 473 |
+
for (let tries = 0; tries < 6; tries++) {
|
| 474 |
+
buf = await readRange(url, 0, n);
|
| 475 |
+
try { parsed = parseGgufHeader(buf); break; } catch (e) { if (e instanceof RangeError || /short/.test(String(e))) { n *= 2; continue; } throw e; }
|
| 476 |
+
}
|
| 477 |
+
if (!parsed) throw new Error("could not parse GGUF header");
|
| 478 |
+
const headerBytes = buf.length >= parsed.dataOffset ? buf.subarray(0, parsed.dataOffset) : await readRange(url, 0, parsed.dataOffset);
|
| 479 |
+
return { dataOffset: parsed.dataOffset, tensors: parsed.tensors, headerBytes };
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
// Build the per-tensor fetcher the GPU engine consumes. `manifest` is the wasm
|
| 483 |
+
// manifest (dims + tensors with N,K,blk). Returns fetchTensor(name) → Uint8Array,
|
| 484 |
+
// byte-identical to what qvac_gpu_tensor would return — but sourced from disk.
|
| 485 |
+
export function makeDiskFetcher({ url, readRange, dataOffset, tensors, manifest, bits }) {
|
| 486 |
+
const tdir = {}; for (const t of tensors) tdir[t.name] = t;
|
| 487 |
+
const hasOut = !!tdir["output.weight"];
|
| 488 |
+
const mByName = {}; for (const t of manifest.tensors) mByName[t.name] = t;
|
| 489 |
+
const ROW_CHUNK = 8192;
|
| 490 |
+
const ffM = manifest.ff, dM = manifest.d;
|
| 491 |
+
|
| 492 |
+
// Resolve an engine tensor name → { info, N, K, blk, eltOffset }. Handles MoE
|
| 493 |
+
// expert slices `l{i}.e{e}.{gate,up,down}` (the e-th slab of the 3-D ffn_*_exps
|
| 494 |
+
// tensor) and the router `l{i}.router` (ffn_gate_inp, f32) in addition to the
|
| 495 |
+
// plain tensors in the manifest.
|
| 496 |
+
const resolve = (name) => {
|
| 497 |
+
let m = name.match(/^l(\d+)\.e(\d+)\.(gate|up|down)$/);
|
| 498 |
+
if (m) {
|
| 499 |
+
const i = m[1], e = +m[2], role = m[3];
|
| 500 |
+
const info = tdir[`blk.${i}.ffn_${role}_exps.weight`]; if (!info) return null;
|
| 501 |
+
const N = role === "down" ? dM : ffM, K = role === "down" ? ffM : dM;
|
| 502 |
+
return { info, N, K, blk: true, eltOffset: e * N * K };
|
| 503 |
+
}
|
| 504 |
+
m = name.match(/^l(\d+)\.router$/);
|
| 505 |
+
if (m) { const info = tdir[`blk.${m[1]}.ffn_gate_inp.weight`]; return info ? { info, N: 1, K: (manifest.moe.n_experts * dM), blk: false, eltOffset: 0 } : null; }
|
| 506 |
+
const spec = mByName[name]; const gname = ggufNameFor(name, hasOut); const info = gname && tdir[gname];
|
| 507 |
+
return (spec && info) ? { info, N: spec.N, K: spec.K, blk: spec.blk, eltOffset: 0 } : null;
|
| 508 |
+
};
|
| 509 |
+
|
| 510 |
+
return async function fetchTensor(name) {
|
| 511 |
+
const r = resolve(name);
|
| 512 |
+
if (!r) return new Uint8Array(0);
|
| 513 |
+
const { info, N, K, blk, eltOffset } = r;
|
| 514 |
+
const [bElems, bBytes] = blockShape(info.ggmlType);
|
| 515 |
+
const tBase = dataOffset + info.offset + (eltOffset / bElems) * bBytes; // slab offset for experts
|
| 516 |
+
if (!blk) { // norm / bias / router → [f32]
|
| 517 |
+
const elems = K;
|
| 518 |
+
const raw = await readRange(url, tBase, typeByteLen(info.ggmlType, elems));
|
| 519 |
+
const f = dequantizeRaw(info.ggmlType, raw, elems);
|
| 520 |
+
return new Uint8Array(f.buffer, f.byteOffset, f.byteLength);
|
| 521 |
+
}
|
| 522 |
+
// block weight → [all q bytes][all f32 scales], chunked by rows so a 152k-vocab
|
| 523 |
+
// tensor never materialises whole as f32 (mirror of quant_tensor_chunked).
|
| 524 |
+
const [blkElems, blkBytes] = blockShape(info.ggmlType);
|
| 525 |
+
const qBytesTotal = bits === 4 ? (N * K) / 2 : N * K;
|
| 526 |
+
const scaleCount = N * (K / 32);
|
| 527 |
+
const out = new Uint8Array(qBytesTotal + scaleCount * 4);
|
| 528 |
+
const scales = new Float32Array(out.buffer, qBytesTotal, scaleCount);
|
| 529 |
+
// Fast relayout when the source quant matches the engine's width (the common
|
| 530 |
+
// case: a Q4_0 model → 4-bit engine). No f32 ever materialises → also lets the
|
| 531 |
+
// big embed/lm_head be done in one pass.
|
| 532 |
+
const fast = (info.ggmlType === GGML.Q4_0 && bits === 4) || (info.ggmlType === GGML.Q8_0 && bits === 8);
|
| 533 |
+
if (fast) {
|
| 534 |
+
const raw = await readRange(url, tBase, typeByteLen(info.ggmlType, N * K));
|
| 535 |
+
const { q, s } = info.ggmlType === GGML.Q4_0 ? relayoutQ4(raw, N, K) : relayoutQ8(raw, N, K);
|
| 536 |
+
out.set(q, 0); scales.set(s, 0);
|
| 537 |
+
return out;
|
| 538 |
+
}
|
| 539 |
+
let qPos = 0, sPos = 0;
|
| 540 |
+
for (let r = 0; r < N; r += ROW_CHUNK) {
|
| 541 |
+
const nr = Math.min(ROW_CHUNK, N - r);
|
| 542 |
+
const startElem = r * K, countElem = nr * K;
|
| 543 |
+
const byteStart = (startElem / blkElems) * blkBytes, byteLen = (countElem / blkElems) * blkBytes;
|
| 544 |
+
const raw = await readRange(url, tBase + byteStart, byteLen);
|
| 545 |
+
const f = dequantizeRaw(info.ggmlType, raw, countElem);
|
| 546 |
+
const { q, s } = quantBlocks(f, nr, K, bits);
|
| 547 |
+
out.set(q, qPos); qPos += q.length;
|
| 548 |
+
scales.set(s, sPos); sPos += s.length;
|
| 549 |
+
}
|
| 550 |
+
return out;
|
| 551 |
+
};
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// HTTP Range reader against a same-origin URL.
|
| 555 |
+
export function rangeReader() {
|
| 556 |
+
return async (url, start, len) => {
|
| 557 |
+
const r = await fetch(url, { headers: { Range: `bytes=${start}-${start + len - 1}` } });
|
| 558 |
+
if (!r.ok && r.status !== 206) throw new Error(`range ${start}+${len}: HTTP ${r.status}`);
|
| 559 |
+
return new Uint8Array(await r.arrayBuffer());
|
| 560 |
+
};
|
| 561 |
+
}
|