arcane-os 0.1.0-dev.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +10 -0
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +162 -0
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +69 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1151 -0
- package/browser-runtime/ai/browser-wasm.mjs +44 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +390 -0
- package/browser-runtime/ai/internal/sha256.mjs +166 -0
- package/browser-runtime/ai/model-controller.mjs +581 -0
- package/browser-runtime/ai/wllama/LICENCE +21 -0
- package/browser-runtime/ai/wllama/index.mjs +3494 -0
- package/browser-runtime/ai/wllama/llama.cpp-LICENSE +21 -0
- package/browser-runtime/ai/wllama/wllama.wasm +0 -0
- package/browser-runtime/dependencies/event-pubsub/index.js +141 -0
- package/browser-runtime/dependencies/event-pubsub/licence +21 -0
- package/browser-runtime/dependencies/event-pubsub/package.json +59 -0
- package/browser-runtime/dependencies/strong-type/index.js +1151 -0
- package/browser-runtime/dependencies/strong-type/licence +21 -0
- package/browser-runtime/dependencies/strong-type/package.json +61 -0
- package/browser-runtime/dom-event-instrumentation.mjs +594 -0
- package/browser-runtime/event-manager.mjs +1342 -0
- package/docs/publishing.md +65 -67
- package/docs/reference/README.md +2 -1
- package/docs/reference/cli.md +86 -3
- package/docs/reference/event-manager.md +20 -11
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/protocols.md +112 -14
- package/docs/reference/sdk-api.md +40 -11
- package/docs/work-amplification.md +4 -3
- package/package.json +15 -8
- package/runtime/ARCANE_RUNTIME_RELEASE.json +1 -1
- package/schemas/arcane-lock.schema.json +97 -1
- package/src/cli/main.mjs +5 -0
- package/src/dev-server.mjs +78 -34
- package/src/doctor.mjs +77 -3
- package/src/import-map.mjs +2352 -0
- package/src/packager/core.mjs +607 -29
- package/src/scaffold.mjs +122 -5
- package/src/sdk-browser-runtime.mjs +702 -0
- package/src/targets/index.mjs +31 -4
- package/src/templates/workspace-template.mjs +141 -23
- package/src/toolchain.mjs +288 -55
- package/src/workspace-operation-lock.mjs +716 -0
- package/src/workspace-runtime.mjs +841 -0
- package/src/workspace.mjs +292 -37
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createModelController, ModelController } from "./model-controller.mjs";
|
|
2
|
+
import {
|
|
3
|
+
createBrowserModelSource,
|
|
4
|
+
createBrowserWasmLlmProvider,
|
|
5
|
+
createDbopfsModelStore,
|
|
6
|
+
} from "./browser-wasm-llm-provider.mjs";
|
|
7
|
+
import { BROWSER_WASM_RUNTIME_AUTHORITY } from "./browser-wllama-runtime.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates the generic Arcane browser-local AI facade. The SDK owns lifecycle,
|
|
11
|
+
* integrity, cache, streaming, and structural tool visibility; applications
|
|
12
|
+
* continue to own prompts, tools, policies, and any decision to execute a tool.
|
|
13
|
+
*/
|
|
14
|
+
function createArcaneAI({
|
|
15
|
+
llm = null,
|
|
16
|
+
provider = null,
|
|
17
|
+
loadPolicy = "on-demand",
|
|
18
|
+
} = {}) {
|
|
19
|
+
const selected = llm ?? provider;
|
|
20
|
+
if (!selected) throw new TypeError("createArcaneAI requires an llm provider.");
|
|
21
|
+
const controller = selected instanceof ModelController
|
|
22
|
+
? selected
|
|
23
|
+
: createModelController({ provider: selected, loadPolicy });
|
|
24
|
+
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
llm: controller,
|
|
27
|
+
runtime: BROWSER_WASM_RUNTIME_AUTHORITY,
|
|
28
|
+
status: () => Object.freeze({ llm: controller.status() }),
|
|
29
|
+
load: (options) => controller.load(options),
|
|
30
|
+
unload: (options) => controller.unload(options),
|
|
31
|
+
probe: (options) => controller.probe(options),
|
|
32
|
+
fetchRequest: (options) => controller.fetchRequest(options),
|
|
33
|
+
streamRequest: (options) => controller.streamRequest(options),
|
|
34
|
+
dispose: (options) => controller.dispose(options),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
BROWSER_WASM_RUNTIME_AUTHORITY,
|
|
40
|
+
createArcaneAI,
|
|
41
|
+
createBrowserModelSource,
|
|
42
|
+
createBrowserWasmLlmProvider,
|
|
43
|
+
createDbopfsModelStore,
|
|
44
|
+
};
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { Wllama } from "./wllama/index.mjs";
|
|
2
|
+
|
|
3
|
+
const MODULE_URL = new URL("./wllama/index.mjs", import.meta.url).href;
|
|
4
|
+
const WASM_URL = new URL("./wllama/wllama.wasm", import.meta.url).href;
|
|
5
|
+
|
|
6
|
+
function deepFreeze(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
8
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
9
|
+
return Object.freeze(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const BROWSER_WASM_RUNTIME_AUTHORITY = deepFreeze({
|
|
13
|
+
protocol: "arcane-ai-browser-wasm/1",
|
|
14
|
+
provider: "wllama",
|
|
15
|
+
package: {
|
|
16
|
+
name: "@wllama/wllama",
|
|
17
|
+
version: "3.6.0",
|
|
18
|
+
sourceRevision: "f16050d8d51a00602c6a2a6b8ac9c09f490eea7f",
|
|
19
|
+
resolved: "https://registry.npmjs.org/@wllama/wllama/-/wllama-3.6.0.tgz",
|
|
20
|
+
npmIntegrity: "sha512-NN3ZBXqaaUwGXTQubkNvsCaLPjN2XVa0bVS40OYCE8zquYmRc2W3oHYEgwvuSWWDB8aUqTLyMioySCXNkcnD1w==",
|
|
21
|
+
tarballBytes: 5_671_369,
|
|
22
|
+
tarballSha256: "137c35ceccb4911a9b0ce9b427889f75991654ec6a6d1dd8fabd879b14b07a1b",
|
|
23
|
+
licenseSpdx: "MIT",
|
|
24
|
+
license: {
|
|
25
|
+
path: "ai/wllama/LICENCE",
|
|
26
|
+
bytes: 1_071,
|
|
27
|
+
sha256: "5866e3bd7e3cbd3f7c8bea6efd8a1e7fa7cc8de68c30f428aff7c6584a0fb720",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
llamaCpp: {
|
|
31
|
+
sourceRevision: "4df29be4f4c3673f428170fda944a5b19f743bb8",
|
|
32
|
+
licenseSpdx: "MIT",
|
|
33
|
+
license: {
|
|
34
|
+
path: "ai/wllama/llama.cpp-LICENSE",
|
|
35
|
+
bytes: 1_078,
|
|
36
|
+
sha256: "94f29bbed6a22c35b992c5c6ebf0e7c92f13b836b90f36f461c9cf2f0f1d010d",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
runtimeAssets: {
|
|
40
|
+
module: {
|
|
41
|
+
path: "ai/wllama/index.mjs",
|
|
42
|
+
url: MODULE_URL,
|
|
43
|
+
bytes: 373_519,
|
|
44
|
+
sha256: "4637e42d636010493a9b274fbbe70bfd8120365da726b1d9e589d85ca84a00d6",
|
|
45
|
+
mediaType: "text/javascript",
|
|
46
|
+
},
|
|
47
|
+
wasm: {
|
|
48
|
+
path: "ai/wllama/wllama.wasm",
|
|
49
|
+
url: WASM_URL,
|
|
50
|
+
bytes: 8_524_865,
|
|
51
|
+
sha256: "95c6ff9ef2a03ff2c63bc91db132f0126a0bd0456b272cd8ae2e0f592fb059f6",
|
|
52
|
+
mediaType: "application/wasm",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
networkPolicy: {
|
|
56
|
+
compatibilityRuntime: "disabled",
|
|
57
|
+
remoteModelHelpers: false,
|
|
58
|
+
modelInput: "verified-local-file-only",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function runtimeCapabilitySnapshot() {
|
|
63
|
+
const navigatorObject = globalThis.navigator;
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
webAssembly: typeof globalThis.WebAssembly === "object",
|
|
66
|
+
opfs: typeof navigatorObject?.storage?.getDirectory === "function",
|
|
67
|
+
webgpu: Boolean(navigatorObject?.gpu),
|
|
68
|
+
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
|
69
|
+
secureContext: globalThis.isSecureContext === true,
|
|
70
|
+
hardwareConcurrency: Number.isSafeInteger(navigatorObject?.hardwareConcurrency)
|
|
71
|
+
? navigatorObject.hardwareConcurrency
|
|
72
|
+
: null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizePositiveInteger(value, fallback, { maximum = Number.MAX_SAFE_INTEGER } = {}) {
|
|
77
|
+
if (value === undefined || value === null) return fallback;
|
|
78
|
+
const number = Number(value);
|
|
79
|
+
if (!Number.isSafeInteger(number) || number < 1 || number > maximum) {
|
|
80
|
+
throw new RangeError(`Expected an integer from 1 through ${maximum}.`);
|
|
81
|
+
}
|
|
82
|
+
return number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates one packaged Wllama session. This factory has no network or browser
|
|
87
|
+
* side effects until load() is called. Runtime URLs are fixed relative to this
|
|
88
|
+
* module so the same bytes work from npm and a materialized /arcane/sdk tree.
|
|
89
|
+
*/
|
|
90
|
+
export function createPackagedWllamaRuntime({ logger = console } = {}) {
|
|
91
|
+
let engine = null;
|
|
92
|
+
let pending = null;
|
|
93
|
+
const trackedOperations = new Set();
|
|
94
|
+
const sessionExitPromises = new WeakMap();
|
|
95
|
+
|
|
96
|
+
function cancellationError(reason, fallback = "The Wllama operation was cancelled.") {
|
|
97
|
+
return reason instanceof Error ? reason : new Error(reason ? String(reason) : fallback);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function trackOperation(rawOperation) {
|
|
101
|
+
const raw = Promise.resolve(rawOperation);
|
|
102
|
+
raw.catch(() => undefined);
|
|
103
|
+
let rejectCancellation;
|
|
104
|
+
const cancellation = new Promise((_, reject) => {
|
|
105
|
+
rejectCancellation = reject;
|
|
106
|
+
});
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
const record = {
|
|
109
|
+
cancel(reason) {
|
|
110
|
+
if (cancelled) return false;
|
|
111
|
+
cancelled = true;
|
|
112
|
+
rejectCancellation(cancellationError(reason));
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
trackedOperations.add(record);
|
|
117
|
+
const result = Promise.race([raw, cancellation]).finally(() => {
|
|
118
|
+
trackedOperations.delete(record);
|
|
119
|
+
});
|
|
120
|
+
result.catch(() => undefined);
|
|
121
|
+
return Object.freeze({ raw, result, cancel: record.cancel });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cancelTrackedOperations(reason) {
|
|
125
|
+
for (const operation of [...trackedOperations]) operation.cancel(reason);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function requireConfigurableDataProperty(object, key, label) {
|
|
129
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
130
|
+
if (!descriptor?.configurable || !("value" in descriptor) || descriptor.get || descriptor.set) {
|
|
131
|
+
throw new Error(`Pinned Wllama ${label} is not a configurable data property.`);
|
|
132
|
+
}
|
|
133
|
+
return descriptor;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function guardLoadingSession(session) {
|
|
137
|
+
const sessionDescriptor = requireConfigurableDataProperty(session, "proxy", "proxy");
|
|
138
|
+
let proxy = sessionDescriptor.value;
|
|
139
|
+
let cancelled = null;
|
|
140
|
+
const workerGuards = new Map();
|
|
141
|
+
|
|
142
|
+
function guardProxy(nextProxy) {
|
|
143
|
+
if (!nextProxy || workerGuards.has(nextProxy)) return;
|
|
144
|
+
const descriptor = requireConfigurableDataProperty(nextProxy, "worker", "proxy worker");
|
|
145
|
+
let worker = descriptor.value;
|
|
146
|
+
Object.defineProperty(nextProxy, "worker", {
|
|
147
|
+
enumerable: descriptor.enumerable,
|
|
148
|
+
configurable: true,
|
|
149
|
+
get: () => worker,
|
|
150
|
+
set(value) {
|
|
151
|
+
if (value && cancelled) {
|
|
152
|
+
value.terminate?.();
|
|
153
|
+
throw cancelled;
|
|
154
|
+
}
|
|
155
|
+
worker = value;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
workerGuards.set(nextProxy, () => {
|
|
159
|
+
Object.defineProperty(nextProxy, "worker", { ...descriptor, value: worker });
|
|
160
|
+
});
|
|
161
|
+
if (worker && cancelled) {
|
|
162
|
+
worker.terminate?.();
|
|
163
|
+
throw cancelled;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (proxy) guardProxy(proxy);
|
|
168
|
+
Object.defineProperty(session, "proxy", {
|
|
169
|
+
enumerable: sessionDescriptor.enumerable,
|
|
170
|
+
configurable: true,
|
|
171
|
+
get: () => proxy,
|
|
172
|
+
set(value) {
|
|
173
|
+
if (value && cancelled) {
|
|
174
|
+
value.worker?.terminate?.();
|
|
175
|
+
throw cancelled;
|
|
176
|
+
}
|
|
177
|
+
proxy = value;
|
|
178
|
+
if (value) guardProxy(value);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
cancel(reason) {
|
|
184
|
+
cancelled ||= cancellationError(reason, "The Wllama model load was cancelled.");
|
|
185
|
+
const current = proxy;
|
|
186
|
+
current?.worker?.terminate?.();
|
|
187
|
+
try {
|
|
188
|
+
current?.abort?.(cancelled.message, "");
|
|
189
|
+
} catch {
|
|
190
|
+
// The tracked operation gate remains the stable cancellation result.
|
|
191
|
+
}
|
|
192
|
+
return cancelled;
|
|
193
|
+
},
|
|
194
|
+
restore() {
|
|
195
|
+
for (const restoreWorker of workerGuards.values()) restoreWorker();
|
|
196
|
+
workerGuards.clear();
|
|
197
|
+
Object.defineProperty(session, "proxy", { ...sessionDescriptor, value: proxy });
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function exitSession(session, reason) {
|
|
203
|
+
if (!session) return Promise.resolve(false);
|
|
204
|
+
let exitPromise = sessionExitPromises.get(session);
|
|
205
|
+
if (!exitPromise) {
|
|
206
|
+
const attempt = Promise.resolve().then(() => {
|
|
207
|
+
try {
|
|
208
|
+
session.proxy?.abort?.(cancellationError(reason).message, "");
|
|
209
|
+
} catch {
|
|
210
|
+
// Public session.exit() still owns Worker termination.
|
|
211
|
+
}
|
|
212
|
+
return session.exit();
|
|
213
|
+
}).then(() => true);
|
|
214
|
+
exitPromise = attempt.catch((error) => {
|
|
215
|
+
// A failed cleanup attempt is not proof that this session is closed.
|
|
216
|
+
// Evict only this attempt so a later exit() can retry the same handle.
|
|
217
|
+
if (sessionExitPromises.get(session) === exitPromise) {
|
|
218
|
+
sessionExitPromises.delete(session);
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
222
|
+
sessionExitPromises.set(session, exitPromise);
|
|
223
|
+
}
|
|
224
|
+
return exitPromise;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function newEngine() {
|
|
228
|
+
const next = new Wllama({ default: WASM_URL }, {
|
|
229
|
+
logger,
|
|
230
|
+
allowOffline: true,
|
|
231
|
+
});
|
|
232
|
+
next.setCompat(null);
|
|
233
|
+
const resources = next.getWorkerResources();
|
|
234
|
+
if (resources.compat !== false || resources.wasmPath !== WASM_URL || resources.jsPath) {
|
|
235
|
+
throw new Error("The packaged Wllama resource projection was not exact.");
|
|
236
|
+
}
|
|
237
|
+
return next;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function capabilities() {
|
|
241
|
+
return runtimeCapabilitySnapshot();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function load(files, options = {}) {
|
|
245
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
246
|
+
throw new TypeError("Wllama load() requires at least one verified File or Blob.");
|
|
247
|
+
}
|
|
248
|
+
if (typeof globalThis.WebAssembly !== "object") {
|
|
249
|
+
throw new Error("WebAssembly is unavailable in this browser.");
|
|
250
|
+
}
|
|
251
|
+
if (engine || pending) {
|
|
252
|
+
throw new Error("The packaged Wllama runtime is already loaded or loading.");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Wllama defaults to a CDN compatibility runtime. Arcane never admits it.
|
|
256
|
+
const next = newEngine();
|
|
257
|
+
|
|
258
|
+
const threads = normalizePositiveInteger(options.threads, 1, { maximum: 64 });
|
|
259
|
+
const contextTokens = normalizePositiveInteger(options.contextTokens, 4_096, {
|
|
260
|
+
maximum: 1_048_576,
|
|
261
|
+
});
|
|
262
|
+
const loadOptions = {
|
|
263
|
+
n_threads: threads,
|
|
264
|
+
n_ctx: contextTokens,
|
|
265
|
+
n_gpu_layers: Number.isSafeInteger(options.gpuLayers) ? options.gpuLayers : 0,
|
|
266
|
+
};
|
|
267
|
+
if (options.batchTokens !== undefined) {
|
|
268
|
+
loadOptions.n_batch = normalizePositiveInteger(options.batchTokens, 512, {
|
|
269
|
+
maximum: contextTokens,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (options.microBatchTokens !== undefined) {
|
|
273
|
+
loadOptions.n_ubatch = normalizePositiveInteger(options.microBatchTokens, 128, {
|
|
274
|
+
maximum: contextTokens,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const loadGuard = guardLoadingSession(next);
|
|
279
|
+
const operation = trackOperation(Promise.resolve().then(() => next.loadModel(files, loadOptions)));
|
|
280
|
+
operation.raw.finally(loadGuard.restore).catch(() => undefined);
|
|
281
|
+
const cancel = (reason) => {
|
|
282
|
+
const error = loadGuard.cancel(reason);
|
|
283
|
+
operation.cancel(error);
|
|
284
|
+
};
|
|
285
|
+
pending = Object.freeze({
|
|
286
|
+
engine: next,
|
|
287
|
+
cancel,
|
|
288
|
+
});
|
|
289
|
+
const signal = options.signal ?? null;
|
|
290
|
+
const onAbort = () => cancel(signal.reason);
|
|
291
|
+
if (signal?.aborted) onAbort();
|
|
292
|
+
else signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
293
|
+
try {
|
|
294
|
+
await operation.result;
|
|
295
|
+
if (pending?.engine !== next) throw new Error("Wllama load was cancelled.");
|
|
296
|
+
pending = null;
|
|
297
|
+
engine = next;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
try {
|
|
300
|
+
await exitSession(next, error);
|
|
301
|
+
if (pending?.engine === next) pending = null;
|
|
302
|
+
} catch {
|
|
303
|
+
// Preserve the exact pending session for a later runtime.exit() retry.
|
|
304
|
+
// The original load failure remains the stable public rejection.
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
} finally {
|
|
308
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
loaded: true,
|
|
313
|
+
contextTokens,
|
|
314
|
+
threads,
|
|
315
|
+
metadata: engine.getModelMetadata?.() ?? null,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertLoaded() {
|
|
320
|
+
if (!engine?.isModelLoaded?.()) throw new Error("The packaged Wllama model is not loaded.");
|
|
321
|
+
return engine;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function chat(options) {
|
|
325
|
+
const session = assertLoaded();
|
|
326
|
+
return trackOperation(
|
|
327
|
+
Promise.resolve().then(() => session.createChatCompletion({ ...options, stream: false })),
|
|
328
|
+
).result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function stream(options, onData) {
|
|
332
|
+
if (typeof onData !== "function") {
|
|
333
|
+
throw new TypeError("Wllama stream() requires an onData callback.");
|
|
334
|
+
}
|
|
335
|
+
// Wllama owns the request slot until this promise settles. Its documented
|
|
336
|
+
// abortSignal path cancels the llama.cpp request in a finally block.
|
|
337
|
+
const session = assertLoaded();
|
|
338
|
+
return trackOperation(
|
|
339
|
+
Promise.resolve().then(() => session.createChatCompletion({ ...options, stream: true, onData })),
|
|
340
|
+
).result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function exit() {
|
|
344
|
+
const current = engine;
|
|
345
|
+
const loading = pending;
|
|
346
|
+
const reason = new Error("Wllama was cancelled by unload.");
|
|
347
|
+
loading?.cancel(reason);
|
|
348
|
+
cancelTrackedOperations(reason);
|
|
349
|
+
const sessions = new Set([current, loading?.engine].filter(Boolean));
|
|
350
|
+
if (!sessions.size) return false;
|
|
351
|
+
await Promise.all([...sessions].map((session) => exitSession(session, reason)));
|
|
352
|
+
// Retain ownership through cleanup failure. Clear only the exact handles
|
|
353
|
+
// whose exit attempts completed successfully; replacement sessions, if
|
|
354
|
+
// any, remain owned by the runtime.
|
|
355
|
+
if (engine === current) engine = null;
|
|
356
|
+
if (pending === loading) pending = null;
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function probe({ args = ["-o", "ADD"] } = {}) {
|
|
361
|
+
if (engine) throw new Error("The no-model Wllama probe cannot run while a model is loaded.");
|
|
362
|
+
if (!Array.isArray(args) || args.some((value) => typeof value !== "string")) {
|
|
363
|
+
throw new TypeError("Wllama probe args must be an array of strings.");
|
|
364
|
+
}
|
|
365
|
+
const temporary = newEngine();
|
|
366
|
+
// testBackendOps owns and closes its temporary worker. It requires browser
|
|
367
|
+
// multithreading, but that is a probe capability—not model admission.
|
|
368
|
+
const result = await temporary.testBackendOps(args);
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
...result,
|
|
371
|
+
args: Object.freeze([...args]),
|
|
372
|
+
origin: globalThis.location?.origin ?? null,
|
|
373
|
+
capabilities: capabilities(),
|
|
374
|
+
runtime: BROWSER_WASM_RUNTIME_AUTHORITY,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return Object.freeze({
|
|
379
|
+
authority: BROWSER_WASM_RUNTIME_AUTHORITY,
|
|
380
|
+
runtimeAssets: BROWSER_WASM_RUNTIME_AUTHORITY.runtimeAssets,
|
|
381
|
+
capabilities,
|
|
382
|
+
load,
|
|
383
|
+
chat,
|
|
384
|
+
stream,
|
|
385
|
+
probe,
|
|
386
|
+
exit,
|
|
387
|
+
isLoaded: () => Boolean(engine?.isModelLoaded?.()),
|
|
388
|
+
isLoading: () => Boolean(pending),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const ROUND_CONSTANTS = new Uint32Array([
|
|
2
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
3
|
+
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
4
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
5
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
6
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
7
|
+
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
8
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
9
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
10
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
11
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
12
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
13
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
14
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
15
|
+
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
16
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
17
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function rotateRight(value, bits) {
|
|
21
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toBytes(value) {
|
|
25
|
+
if (value instanceof Uint8Array) return value;
|
|
26
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
27
|
+
if (ArrayBuffer.isView(value)) {
|
|
28
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
29
|
+
}
|
|
30
|
+
throw new TypeError("SHA-256 input must be bytes.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dependency-free incremental SHA-256. It retains one 64-byte tail and never
|
|
35
|
+
* buffers an entire model file in JavaScript memory.
|
|
36
|
+
*/
|
|
37
|
+
export function createStreamingSha256() {
|
|
38
|
+
const state = new Uint32Array([
|
|
39
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
40
|
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
41
|
+
]);
|
|
42
|
+
const schedule = new Uint32Array(64);
|
|
43
|
+
const tail = new Uint8Array(64);
|
|
44
|
+
let tailLength = 0;
|
|
45
|
+
let byteLength = 0;
|
|
46
|
+
let finished = false;
|
|
47
|
+
|
|
48
|
+
function compress(block, offset = 0) {
|
|
49
|
+
for (let index = 0; index < 16; index += 1) {
|
|
50
|
+
const cursor = offset + (index * 4);
|
|
51
|
+
schedule[index] = (
|
|
52
|
+
(block[cursor] << 24)
|
|
53
|
+
| (block[cursor + 1] << 16)
|
|
54
|
+
| (block[cursor + 2] << 8)
|
|
55
|
+
| block[cursor + 3]
|
|
56
|
+
) >>> 0;
|
|
57
|
+
}
|
|
58
|
+
for (let index = 16; index < 64; index += 1) {
|
|
59
|
+
const previous = schedule[index - 15];
|
|
60
|
+
const prior = schedule[index - 2];
|
|
61
|
+
const sigma0 = (
|
|
62
|
+
rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ (previous >>> 3)
|
|
63
|
+
) >>> 0;
|
|
64
|
+
const sigma1 = (
|
|
65
|
+
rotateRight(prior, 17) ^ rotateRight(prior, 19) ^ (prior >>> 10)
|
|
66
|
+
) >>> 0;
|
|
67
|
+
schedule[index] = (
|
|
68
|
+
schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1
|
|
69
|
+
) >>> 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let a = state[0];
|
|
73
|
+
let b = state[1];
|
|
74
|
+
let c = state[2];
|
|
75
|
+
let d = state[3];
|
|
76
|
+
let e = state[4];
|
|
77
|
+
let f = state[5];
|
|
78
|
+
let g = state[6];
|
|
79
|
+
let h = state[7];
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < 64; index += 1) {
|
|
82
|
+
const sum1 = (rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25)) >>> 0;
|
|
83
|
+
const choice = ((e & f) ^ ((~e) & g)) >>> 0;
|
|
84
|
+
const temporary1 = (
|
|
85
|
+
h + sum1 + choice + ROUND_CONSTANTS[index] + schedule[index]
|
|
86
|
+
) >>> 0;
|
|
87
|
+
const sum0 = (rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)) >>> 0;
|
|
88
|
+
const majority = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
|
|
89
|
+
const temporary2 = (sum0 + majority) >>> 0;
|
|
90
|
+
|
|
91
|
+
h = g;
|
|
92
|
+
g = f;
|
|
93
|
+
f = e;
|
|
94
|
+
e = (d + temporary1) >>> 0;
|
|
95
|
+
d = c;
|
|
96
|
+
c = b;
|
|
97
|
+
b = a;
|
|
98
|
+
a = (temporary1 + temporary2) >>> 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
state[0] = (state[0] + a) >>> 0;
|
|
102
|
+
state[1] = (state[1] + b) >>> 0;
|
|
103
|
+
state[2] = (state[2] + c) >>> 0;
|
|
104
|
+
state[3] = (state[3] + d) >>> 0;
|
|
105
|
+
state[4] = (state[4] + e) >>> 0;
|
|
106
|
+
state[5] = (state[5] + f) >>> 0;
|
|
107
|
+
state[6] = (state[6] + g) >>> 0;
|
|
108
|
+
state[7] = (state[7] + h) >>> 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function update(value) {
|
|
112
|
+
if (finished) throw new Error("SHA-256 digest is already finalized.");
|
|
113
|
+
const input = toBytes(value);
|
|
114
|
+
byteLength += input.byteLength;
|
|
115
|
+
if (!Number.isSafeInteger(byteLength)) {
|
|
116
|
+
throw new RangeError("SHA-256 input exceeds the supported byte length.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let offset = 0;
|
|
120
|
+
if (tailLength) {
|
|
121
|
+
const take = Math.min(64 - tailLength, input.byteLength);
|
|
122
|
+
tail.set(input.subarray(0, take), tailLength);
|
|
123
|
+
tailLength += take;
|
|
124
|
+
offset = take;
|
|
125
|
+
if (tailLength === 64) {
|
|
126
|
+
compress(tail);
|
|
127
|
+
tailLength = 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (offset + 64 <= input.byteLength) {
|
|
132
|
+
compress(input, offset);
|
|
133
|
+
offset += 64;
|
|
134
|
+
}
|
|
135
|
+
if (offset < input.byteLength) {
|
|
136
|
+
tail.set(input.subarray(offset), 0);
|
|
137
|
+
tailLength = input.byteLength - offset;
|
|
138
|
+
}
|
|
139
|
+
return api;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function digestHex() {
|
|
143
|
+
if (finished) throw new Error("SHA-256 digest is already finalized.");
|
|
144
|
+
finished = true;
|
|
145
|
+
const finalLength = tailLength < 56 ? 64 : 128;
|
|
146
|
+
const finalBlocks = new Uint8Array(finalLength);
|
|
147
|
+
finalBlocks.set(tail.subarray(0, tailLength));
|
|
148
|
+
finalBlocks[tailLength] = 0x80;
|
|
149
|
+
let bitLength = BigInt(byteLength) * 8n;
|
|
150
|
+
for (let index = 0; index < 8; index += 1) {
|
|
151
|
+
finalBlocks[finalLength - 1 - index] = Number(bitLength & 0xffn);
|
|
152
|
+
bitLength >>= 8n;
|
|
153
|
+
}
|
|
154
|
+
for (let offset = 0; offset < finalLength; offset += 64) compress(finalBlocks, offset);
|
|
155
|
+
return [...state].map((word) => word.toString(16).padStart(8, "0")).join("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const api = Object.freeze({
|
|
159
|
+
update,
|
|
160
|
+
digestHex,
|
|
161
|
+
get bytes() {
|
|
162
|
+
return byteLength;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return api;
|
|
166
|
+
}
|