arcane-os 0.5.10 → 0.5.12

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +117 -26
  3. package/browser-runtime/ai/browser-speech-providers.mjs +1 -1
  4. package/docs/architecture.md +303 -0
  5. package/docs/compatibility.md +38 -0
  6. package/docs/event-manager.md +263 -0
  7. package/docs/platform-targets.md +104 -0
  8. package/docs/publishing.md +126 -0
  9. package/docs/reference/README.md +206 -0
  10. package/docs/reference/ai/browser-speech.md +879 -0
  11. package/docs/reference/ai/browser-wasm.md +637 -0
  12. package/docs/reference/ai/twin-cloud.md +156 -0
  13. package/docs/reference/arcane-ollama.md +288 -0
  14. package/docs/reference/availability-and-normalization.md +224 -0
  15. package/docs/reference/behavioral-testing.md +129 -0
  16. package/docs/reference/cli.md +820 -0
  17. package/docs/reference/core/README.md +61 -0
  18. package/docs/reference/core/arcane-ai-contracts.md +906 -0
  19. package/docs/reference/core/arcane-api.md +601 -0
  20. package/docs/reference/core/arcane-entities.md +59 -0
  21. package/docs/reference/core/arcane-events.md +134 -0
  22. package/docs/reference/core/ollama-module.md +181 -0
  23. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  24. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  25. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  26. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  27. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  28. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  29. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  30. package/docs/reference/event-manager.md +1409 -0
  31. package/docs/reference/inventory/package-api.json +3194 -0
  32. package/docs/reference/inventory/runtime-components.json +1015 -0
  33. package/docs/reference/inventory/runtime-entities.json +25 -0
  34. package/docs/reference/inventory/runtime-modules.json +1367 -0
  35. package/docs/reference/mail.md +309 -0
  36. package/docs/reference/protocols.md +749 -0
  37. package/docs/reference/runtime-components.md +1532 -0
  38. package/docs/reference/runtime-entities.md +305 -0
  39. package/docs/reference/runtime-modules.md +3310 -0
  40. package/docs/reference/sdk-api.md +6733 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +66 -0
  43. package/examples/wasm-ai-demo/README.md +80 -0
  44. package/examples/wasm-ai-demo/app.js +787 -0
  45. package/examples/wasm-ai-demo/index.html +343 -0
  46. package/examples/wasm-ai-demo/profile-tools.js +217 -0
  47. package/examples/wasm-ai-demo/profiles/BOSS.Modelfile +106 -0
  48. package/examples/wasm-ai-demo/profiles/PreCrisis.Modelfile +693 -0
  49. package/examples/wasm-ai-demo/rag/boss-library.json +3006 -0
  50. package/examples/wasm-ai-demo/rag.js +295 -0
  51. package/examples/wasm-ai-demo/server.mjs +71 -0
  52. package/package.json +10 -1
  53. package/runtime/arcane/modules/AI.js +60 -11
  54. package/runtime/arcane/modules/AIProviderRuntime.js +26 -5
@@ -0,0 +1,787 @@
1
+ import arcaneThemeReady from "arcane/ThemeBootstrap";
2
+ import "arcane/HTMLImport";
3
+ import AI, { AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL } from "arcane/AI";
4
+ import DBOPFS from "arcane/DBOPFS";
5
+ import { loadModelDefinitionSystemPrompt } from "arcane/ModelDefinition";
6
+ import PreferenceStore from "arcane/PreferenceStore";
7
+ import waitForComponent from "arcane/WaitForComponent";
8
+ import { subscribeAIRuntimeState } from "arcane/AIRuntimeState";
9
+ import {
10
+ adaptV1LlmProvider,
11
+ createBrowserModelSource,
12
+ createBrowserWasmLlmProvider,
13
+ createDbopfsModelStore,
14
+ } from "arcane-os/ai/browser-wasm";
15
+ import {
16
+ getRagStats,
17
+ importRagFiles,
18
+ initializeRag,
19
+ retrieveRagContext,
20
+ } from "./rag.js";
21
+ import { toolsForProfile } from "./profile-tools.js";
22
+
23
+ function ggufShards(basename, byteLengths) {
24
+ const total = String(byteLengths.length).padStart(5, "0");
25
+ return byteLengths.map(function ggufShard(bytes, index) {
26
+ return {
27
+ name: `${basename}-${String(index + 1).padStart(5, "0")}-of-${total}.gguf`,
28
+ bytes,
29
+ };
30
+ });
31
+ }
32
+
33
+ const MODELS = {
34
+ "granite-3b": {
35
+ id: "granite-3b",
36
+ label: "Granite 4.1 3B",
37
+ shortLabel: "Granite 3B",
38
+ parameterWords: "3 billion",
39
+ quantization: "Q4_K_M",
40
+ desktopContextTokens: 4_096,
41
+ mobileContextTokens: 2_048,
42
+ files: ggufShards("granite-4.1-3b-Q4_K_M", [
43
+ 513_000_480,
44
+ 509_814_656,
45
+ 508_831_744,
46
+ 506_056_224,
47
+ 61_799_104,
48
+ ]),
49
+ },
50
+ "granite-8b": {
51
+ id: "granite-8b",
52
+ label: "Granite 4.1 8B",
53
+ shortLabel: "Granite 8B",
54
+ parameterWords: "8 billion",
55
+ quantization: "Q4_K_M",
56
+ desktopContextTokens: 32_768,
57
+ mobileContextTokens: 2_048,
58
+ files: ggufShards("granite-4.1-8b-Q4_K_M", [
59
+ 340_745_984,
60
+ 509_298_176,
61
+ 491_112_640,
62
+ 507_087_104,
63
+ 492_505_440,
64
+ 504_302_048,
65
+ 489_392_480,
66
+ 492_488_992,
67
+ 507_103_584,
68
+ 509_462_944,
69
+ 504_416_352,
70
+ ]),
71
+ },
72
+ "gpt-oss-20b": {
73
+ id: "gpt-oss-20b",
74
+ label: "GPT-OSS 20B",
75
+ shortLabel: "GPT-OSS 20B",
76
+ parameterWords: "20 billion",
77
+ quantization: "MXFP4",
78
+ desktopOnly: true,
79
+ desktopContextTokens: 32_768,
80
+ files: ggufShards("gpt-oss-20b-MXFP4", [
81
+ 628_321_344,
82
+ 629_471_744,
83
+ 608_591_904,
84
+ 594_489_088,
85
+ 622_377_056,
86
+ 594_120_192,
87
+ 594_489_088,
88
+ 622_377_056,
89
+ 594_120_192,
90
+ 594_489_088,
91
+ 622_377_088,
92
+ 594_120_224,
93
+ 594_489_088,
94
+ 622_377_088,
95
+ 594_120_224,
96
+ 594_489_088,
97
+ 622_377_088,
98
+ 594_120_224,
99
+ 594_489_088,
100
+ 593_763_072,
101
+ ]),
102
+ },
103
+ };
104
+
105
+ const PROFILES = {
106
+ general: {
107
+ id: "general",
108
+ label: "General",
109
+ sourceUrl: null,
110
+ minimumContextTokens: 0,
111
+ },
112
+ precrisis: {
113
+ id: "precrisis",
114
+ label: "PreCrisis",
115
+ sourceUrl: "./profiles/PreCrisis.Modelfile",
116
+ minimumContextTokens: 8_192,
117
+ },
118
+ boss: {
119
+ id: "boss",
120
+ label: "BOSS",
121
+ sourceUrl: "./profiles/BOSS.Modelfile",
122
+ minimumContextTokens: 8_192,
123
+ },
124
+ };
125
+
126
+ const MOBILE_BROWSER = navigator.userAgentData?.mobile === true
127
+ || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
128
+ || (/Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 1);
129
+ const DEFAULT_MODEL_ID = MOBILE_BROWSER ? "granite-3b" : "granite-8b";
130
+
131
+ const statusElement = document.getElementById("status");
132
+ const statusText = document.getElementById("statusText");
133
+ const modelSelect = document.getElementById("modelSelect");
134
+ const profileSelect = document.getElementById("profile-select");
135
+ const ragFileInput = document.getElementById("rag-file-input");
136
+ const ragImportButton = document.getElementById("rag-import-button");
137
+ const ragStatus = document.getElementById("rag-status");
138
+ const speechExecutionButton = document.getElementById("speech-execution-button");
139
+ const speechExecutionStatus = document.getElementById("speech-execution-status");
140
+ const chat = document.getElementById("chat");
141
+
142
+ const preferenceStore = new PreferenceStore({
143
+ namespace: "wasm-ai-demo",
144
+ schema: [
145
+ {
146
+ key: "chat-model",
147
+ type: "select",
148
+ label: "Chat model",
149
+ defaultValue: DEFAULT_MODEL_ID,
150
+ options: Object.keys(MODELS),
151
+ },
152
+ {
153
+ key: "chat-profile",
154
+ type: "select",
155
+ label: "Chat profile",
156
+ defaultValue: "general",
157
+ options: Object.keys(PROFILES),
158
+ },
159
+ ],
160
+ });
161
+
162
+ let preferenceWarning = "";
163
+ let preferenceValues;
164
+ try {
165
+ preferenceValues = await preferenceStore.load();
166
+ } catch (error) {
167
+ preferenceWarning = error instanceof Error ? error.message : String(error);
168
+ preferenceValues = preferenceStore.defaults();
169
+ }
170
+
171
+ const requestedModel = MODELS[preferenceValues["chat-model"]];
172
+ const selectedModel = requestedModel && !(MOBILE_BROWSER && requestedModel.desktopOnly)
173
+ ? requestedModel
174
+ : MODELS[DEFAULT_MODEL_ID];
175
+ const selectedProfile = PROFILES[preferenceValues["chat-profile"]] || PROFILES.general;
176
+ const baseContextTokens = MOBILE_BROWSER
177
+ ? selectedModel.mobileContextTokens
178
+ : selectedModel.desktopContextTokens;
179
+ const contextTokens = Math.max(
180
+ baseContextTokens || selectedModel.desktopContextTokens,
181
+ selectedProfile.minimumContextTokens,
182
+ );
183
+
184
+ let ai = null;
185
+ let browserModelProvider = null;
186
+ let releaseLlmProvider = null;
187
+ let ragReady = false;
188
+ let ragError = "";
189
+ let ragImporting = false;
190
+ let ragStats = null;
191
+ let systemPrompt = "";
192
+ let stopRuntimeSubscription = null;
193
+ let teardownStarted = false;
194
+ let toolSettlementAttempts = 0;
195
+ let toolSettlementCallId = "";
196
+ let toolSettlementPending = false;
197
+ let toolSettlementWaiting = false;
198
+
199
+ function setPageStatus(state, text) {
200
+ statusElement.dataset.state = state;
201
+ statusText.textContent = text;
202
+ }
203
+
204
+ function formatModelLoadElapsed(value) {
205
+ const elapsedMilliseconds = Number(value);
206
+ if (!Number.isFinite(elapsedMilliseconds) || elapsedMilliseconds < 0) return "";
207
+ const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
208
+ const hours = Math.floor(elapsedSeconds / 3600);
209
+ const minutes = Math.floor((elapsedSeconds % 3600) / 60);
210
+ const seconds = elapsedSeconds % 60;
211
+ if (hours > 0) return `${hours}h ${minutes}m elapsed`;
212
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s elapsed`;
213
+ return `${seconds}s elapsed`;
214
+ }
215
+
216
+ function telemetryNumber(value) {
217
+ return value === null || value === undefined ? Number.NaN : Number(value);
218
+ }
219
+
220
+ function formatBytes(value) {
221
+ const bytes = telemetryNumber(value);
222
+ if (!Number.isFinite(bytes) || bytes < 0) return "";
223
+ const units = ["B", "KB", "MB", "GB", "TB"];
224
+ let amount = bytes;
225
+ let unitIndex = 0;
226
+ while (amount >= 1_000 && unitIndex < units.length - 1) {
227
+ amount /= 1_000;
228
+ unitIndex += 1;
229
+ }
230
+ const fractionDigits = unitIndex === 0 || amount >= 100 ? 0 : (amount >= 10 ? 1 : 2);
231
+ return `${amount.toFixed(fractionDigits)} ${units[unitIndex]}`;
232
+ }
233
+
234
+ function formatEta(value) {
235
+ const seconds = Math.ceil(telemetryNumber(value));
236
+ if (!Number.isFinite(seconds) || seconds < 0) return "";
237
+ const hours = Math.floor(seconds / 3_600);
238
+ const minutes = Math.floor((seconds % 3_600) / 60);
239
+ const remainder = seconds % 60;
240
+ if (hours > 0) return `about ${hours}h ${minutes}m left`;
241
+ if (minutes > 0) return `about ${minutes}m ${remainder}s left`;
242
+ return `about ${remainder}s left`;
243
+ }
244
+
245
+ function transferStatus(progress) {
246
+ const active = telemetryNumber(progress.activeTransfers);
247
+ const limit = telemetryNumber(progress.transferLimit);
248
+ if (!Number.isSafeInteger(active) || active < 0 || !Number.isSafeInteger(limit) || limit < 1) {
249
+ return "";
250
+ }
251
+ const mode = typeof progress.transferMode === "string" && progress.transferMode.trim()
252
+ ? progress.transferMode.trim().toLowerCase()
253
+ : "";
254
+ if (mode === "single") return active > 0 ? "single transfer active" : "single transfer idle";
255
+ if (mode === "probing") return "checking parallel Range support";
256
+ return `${active}/${limit} transfer workers active${mode ? ` (${mode.replaceAll("-", " ")})` : ""}`;
257
+ }
258
+
259
+ function modelLoadStatus(progress = {}) {
260
+ const phase = typeof progress.phase === "string" ? progress.phase : "loading";
261
+ let activity = `Starting ${selectedModel.shortLabel}`;
262
+ if (phase === "cache-check") {
263
+ activity = `Checking the local ${selectedModel.shortLabel} cache`;
264
+ } else if (phase === "download") {
265
+ activity = `Downloading ${selectedModel.shortLabel}`;
266
+ } else if (phase === "initialize") {
267
+ activity = `Loading ${selectedModel.shortLabel} into memory`;
268
+ }
269
+ const parts = [activity];
270
+ const loadedBytes = telemetryNumber(progress.loadedBytes);
271
+ const totalBytes = telemetryNumber(progress.totalBytes);
272
+ const remainingBytes = telemetryNumber(progress.remainingBytes);
273
+ const bytesPerSecond = telemetryNumber(progress.bytesPerSecond);
274
+ const hasByteProgress = phase === "download"
275
+ && Number.isSafeInteger(loadedBytes)
276
+ && loadedBytes >= 0
277
+ && Number.isSafeInteger(totalBytes)
278
+ && totalBytes > 0;
279
+ if (hasByteProgress) {
280
+ parts.push(`${formatBytes(loadedBytes)} / ${formatBytes(totalBytes)}`);
281
+ if (Number.isSafeInteger(remainingBytes) && remainingBytes >= 0) {
282
+ parts.push(`${formatBytes(remainingBytes)} remaining`);
283
+ }
284
+ if (Number.isFinite(bytesPerSecond) && bytesPerSecond >= 0) {
285
+ parts.push(`${formatBytes(bytesPerSecond)}/s`);
286
+ }
287
+ const eta = formatEta(progress.etaSeconds);
288
+ if (eta) parts.push(eta);
289
+ } else {
290
+ const completed = Number(progress.completed);
291
+ const total = Number(progress.total);
292
+ if (Number.isSafeInteger(total) && total > 0 && progress.unit === "files") {
293
+ if (phase === "download" && Number.isSafeInteger(completed) && completed < total) {
294
+ parts.push(`${completed} of ${total} files complete`);
295
+ } else if (phase === "initialize") {
296
+ parts.push(`${total} model parts ready`);
297
+ } else if (phase === "cache-check") {
298
+ parts.push(`${total} model parts`);
299
+ }
300
+ }
301
+ }
302
+ const transfers = transferStatus(progress);
303
+ if (transfers) parts.push(transfers);
304
+ const elapsed = formatModelLoadElapsed(progress.elapsedMs);
305
+ if (elapsed) {
306
+ parts.push(elapsed);
307
+ if (progress.heartbeat === true) parts.push("Still working");
308
+ }
309
+ return parts.join(" · ");
310
+ }
311
+
312
+ function generalSystemPrompt() {
313
+ return `You are ${selectedModel.label}, a ${selectedModel.parameterWords} parameter model running locally in this browser. Be concise, helpful, and direct. Accurately identify yourself as ${selectedModel.label}; do not claim to be a hosted service.`;
314
+ }
315
+
316
+ function browserProfileNote(profile) {
317
+ if (profile.id === "precrisis") {
318
+ return [
319
+ "## Browser example capabilities",
320
+ `This PreCrisis profile is running on ${selectedModel.label} through the live Arcane SDK source.`,
321
+ "Return structural assessment calls through the declared tools when the profile requires them. Every tool call must include a nonempty message argument for the user. SDK Chat displays and persists each call, then records it as not executed because this example has no application action handler. Never claim that a displayed call ran or succeeded. This example is not a diagnosis or an emergency service.",
322
+ ].join("\n\n");
323
+ }
324
+ if (profile.id === "boss") {
325
+ return [
326
+ "## Browser example capabilities",
327
+ `This BOSS profile is running on ${selectedModel.label} through the live Arcane SDK source.`,
328
+ "BOSS library retrieval is completed automatically before generation. Results arrive as local document context; use their complete content, titles, and source links when present. Every structural tool call must include a nonempty message argument for the user. SDK Chat displays and persists the call, then records it as not executed because this example has no application action handler. Never claim that a displayed call ran or succeeded.",
329
+ ].join("\n\n");
330
+ }
331
+ return "";
332
+ }
333
+
334
+ async function loadProfile() {
335
+ if (!selectedProfile.sourceUrl) {
336
+ systemPrompt = generalSystemPrompt();
337
+ return systemPrompt;
338
+ }
339
+ const profilePrompt = await loadModelDefinitionSystemPrompt(selectedProfile.sourceUrl);
340
+ systemPrompt = `${profilePrompt}\n\n${browserProfileNote(selectedProfile)}`;
341
+ return systemPrompt;
342
+ }
343
+
344
+ function integerStat(stats, names) {
345
+ for (const name of names) {
346
+ const value = Number(stats?.[name]);
347
+ if (Number.isInteger(value) && value >= 0) return value;
348
+ }
349
+ return 0;
350
+ }
351
+
352
+ function updateRagStatus(stats = ragStats) {
353
+ if (ragError) {
354
+ ragStatus.textContent = `Local knowledge failed: ${ragError}`;
355
+ return;
356
+ }
357
+ if (!ragReady) {
358
+ ragStatus.textContent = "Opening the Arcane document library…";
359
+ return;
360
+ }
361
+ const bossCount = integerStat(stats, ["bossCount", "bossDocuments", "boss"]);
362
+ const userCount = integerStat(stats, ["userCount", "userDocuments", "imported"]);
363
+ if (selectedProfile.id === "boss") {
364
+ ragStatus.textContent = `${bossCount} BOSS library ${bossCount === 1 ? "record" : "records"} · ${userCount} locally added ${userCount === 1 ? "document" : "documents"}`;
365
+ } else {
366
+ ragStatus.textContent = `${userCount} locally added ${userCount === 1 ? "document" : "documents"}`;
367
+ }
368
+ }
369
+
370
+ async function loadRag() {
371
+ ragReady = false;
372
+ ragError = "";
373
+ try {
374
+ ragStats = await initializeRag({
375
+ dbopfs: globalThis.dbopfs,
376
+ bundleUrl: "./rag/boss-library.json",
377
+ profileId: selectedProfile.id,
378
+ onStatus(text) {
379
+ if (text) ragStatus.textContent = text;
380
+ },
381
+ });
382
+ ragReady = true;
383
+ } catch (error) {
384
+ ragError = error instanceof Error ? error.message : String(error);
385
+ throw error;
386
+ } finally {
387
+ updateRagStatus();
388
+ }
389
+ }
390
+
391
+ function localModelDescriptor(model) {
392
+ return {
393
+ id: model.id,
394
+ files: model.files.map(function localModelFile(file) {
395
+ return {
396
+ name: file.name,
397
+ url: new URL(`./models/${file.name}`, window.location.href).href,
398
+ bytes: file.bytes,
399
+ };
400
+ }),
401
+ };
402
+ }
403
+
404
+ function speechConfiguration(dbopfs) {
405
+ const transformersDistribution = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0/dist/";
406
+ const transformersWasmDistribution = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/";
407
+ const kokoroWasmDistribution = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/dist/";
408
+ return {
409
+ protocol: AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
410
+ id: "wasm-ai-demo-browser-speech",
411
+ dbopfs,
412
+ stt: {
413
+ providerId: "wasm-ai-demo-browser-whisper",
414
+ model: {
415
+ id: "onnx-community/whisper-tiny.en",
416
+ repository: "onnx-community/whisper-tiny.en",
417
+ revision: "2575352d61be1bf7225cf8f8b268a4678025fc58",
418
+ dtype: "fp32",
419
+ },
420
+ runtime: {
421
+ adapter: "transformers-whisper",
422
+ version: "4.2.0",
423
+ revision: "4.2.0",
424
+ entry: "transformers.min.js",
425
+ // Transformers 4.2.0 selects this exact ONNX Runtime Web build. Its
426
+ // WASM factory and binary live in that package, not the JS bundle path.
427
+ wasmPaths: transformersWasmDistribution,
428
+ files: [{
429
+ path: "transformers.min.js",
430
+ url: `${transformersDistribution}transformers.min.js`,
431
+ mediaType: "text/javascript",
432
+ }],
433
+ },
434
+ offline: false,
435
+ },
436
+ tts: {
437
+ // Omit execution to use the SDK's auto device and four synthesis slots.
438
+ providerId: "wasm-ai-demo-browser-kokoro",
439
+ model: {
440
+ id: "onnx-community/Kokoro-82M-v1.0-ONNX",
441
+ repository: "onnx-community/Kokoro-82M-v1.0-ONNX",
442
+ revision: "1939ad2a8e416c0acfeecc08a694d14ef25f2231",
443
+ dtype: "q8",
444
+ defaultVoice: "af_heart",
445
+ },
446
+ runtime: {
447
+ adapter: "kokoro-js",
448
+ version: "1.2.1",
449
+ revision: "664c76a704021239ba59c84dcbaa4d3dece01fe9",
450
+ entry: "kokoro.web.js",
451
+ wasmPaths: kokoroWasmDistribution,
452
+ files: [{
453
+ path: "kokoro.web.js",
454
+ url: "https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.web.js",
455
+ mediaType: "text/javascript",
456
+ }],
457
+ },
458
+ offline: false,
459
+ },
460
+ };
461
+ }
462
+
463
+ async function buildRequestContext({ input, signal } = {}) {
464
+ if (!ragReady) return "";
465
+ try {
466
+ ragStatus.textContent = "Searching the Arcane document library…";
467
+ const result = await retrieveRagContext(input, {
468
+ dbopfs: globalThis.dbopfs,
469
+ profileId: selectedProfile.id,
470
+ signal,
471
+ });
472
+ const count = result.matches.length;
473
+ ragStatus.textContent = count
474
+ ? `Using ${count} local ${count === 1 ? "document" : "documents"} for this response`
475
+ : "No local document matched this request";
476
+ return result.text;
477
+ } catch (error) {
478
+ ragStatus.textContent = `Local retrieval unavailable: ${error instanceof Error ? error.message : String(error)}`;
479
+ return "";
480
+ }
481
+ }
482
+
483
+ function synchronizeRuntime(snapshot) {
484
+ const llm = snapshot.roles.llm;
485
+ if (llm.state === "ready") {
486
+ setPageStatus("ready", `${selectedProfile.label} ready`);
487
+ queueMicrotask(function settleRestoredToolAfterRuntimeUpdate() {
488
+ retryDisplayedToolCallAfterStateChange();
489
+ });
490
+ } else if (llm.state === "loading") {
491
+ setPageStatus("loading", modelLoadStatus(llm.progress));
492
+ } else if (llm.state === "error") {
493
+ setPageStatus("error", llm.error?.message || "Language model failed");
494
+ } else if (llm.state === "unloading") {
495
+ setPageStatus("loading", `Stopping ${selectedModel.shortLabel}…`);
496
+ } else {
497
+ setPageStatus("ready", "SDK ready · start the model in chat");
498
+ }
499
+ }
500
+
501
+ function inspectSpeechExecution() {
502
+ if (!ai) {
503
+ speechExecutionStatus.textContent = "Speech configuration is not ready yet.";
504
+ return;
505
+ }
506
+ try {
507
+ const status = ai.providerRuntime.status("tts", { execution: true });
508
+ const execution = status.execution;
509
+ if (!execution) {
510
+ speechExecutionStatus.textContent = `TTS ${status.state}; no execution report is available.`;
511
+ return;
512
+ }
513
+ const fallback = execution.requestedDevice === "auto" && execution.selectedDevice === "wasm";
514
+ speechExecutionStatus.textContent = `TTS ${status.state}; requested ${execution.requestedDevice}; selected ${execution.selectedDevice ?? "not loaded"}; synthesis capacity ${execution.maxConcurrentRequests}; active ${execution.activeRequestCount}${fallback ? "; automatic WASM fallback" : ""}.`;
515
+ console.log("Arcane SDK TTS execution snapshot:", execution);
516
+ } catch (error) {
517
+ speechExecutionStatus.textContent = `${error.code ?? "ERROR"}: ${error.message}`;
518
+ console.error(error.code, error.message);
519
+ }
520
+ }
521
+
522
+ async function importKnowledgeFiles(files) {
523
+ if (!files.length || !ragReady || ragImporting) return;
524
+ ragImporting = true;
525
+ ragImportButton.disabled = true;
526
+ try {
527
+ const result = await importRagFiles(files, {
528
+ dbopfs: globalThis.dbopfs,
529
+ profileId: selectedProfile.id,
530
+ onStatus(text) {
531
+ if (text) ragStatus.textContent = text;
532
+ },
533
+ });
534
+ ragStats = result.stats || await getRagStats(selectedProfile.id, {
535
+ dbopfs: globalThis.dbopfs,
536
+ });
537
+ updateRagStatus();
538
+ } catch (error) {
539
+ ragStatus.textContent = `Import failed: ${error instanceof Error ? error.message : String(error)}`;
540
+ } finally {
541
+ ragImporting = false;
542
+ ragImportButton.disabled = !ragReady;
543
+ }
544
+ }
545
+
546
+ async function changeModel() {
547
+ const nextModel = MODELS[modelSelect.value];
548
+ if (!nextModel || (MOBILE_BROWSER && nextModel.desktopOnly)) {
549
+ modelSelect.value = selectedModel.id;
550
+ return;
551
+ }
552
+ if (nextModel.id === selectedModel.id) return;
553
+ modelSelect.disabled = true;
554
+ setPageStatus("loading", `Switching to ${nextModel.shortLabel}…`);
555
+ try {
556
+ await preferenceStore.set("chat-model", nextModel.id);
557
+ window.location.reload();
558
+ } catch (error) {
559
+ modelSelect.disabled = false;
560
+ modelSelect.value = selectedModel.id;
561
+ setPageStatus("error", error instanceof Error ? error.message : String(error));
562
+ }
563
+ }
564
+
565
+ async function changeProfile() {
566
+ const nextProfile = PROFILES[profileSelect.value];
567
+ if (!nextProfile) {
568
+ profileSelect.value = selectedProfile.id;
569
+ return;
570
+ }
571
+ if (nextProfile.id === selectedProfile.id) return;
572
+ profileSelect.disabled = true;
573
+ setPageStatus("loading", `Switching to ${nextProfile.label}…`);
574
+ try {
575
+ await preferenceStore.set("chat-profile", nextProfile.id);
576
+ window.location.reload();
577
+ } catch (error) {
578
+ profileSelect.disabled = false;
579
+ profileSelect.value = selectedProfile.id;
580
+ setPageStatus("error", error instanceof Error ? error.message : String(error));
581
+ }
582
+ }
583
+
584
+ async function disposeApplication() {
585
+ if (teardownStarted) return false;
586
+ teardownStarted = true;
587
+ stopRuntimeSubscription?.();
588
+ stopRuntimeSubscription = null;
589
+ const work = [];
590
+ if (ai?.browserSpeechConfiguration) work.push(ai.disposeBrowserSpeech());
591
+ if (browserModelProvider) work.push(browserModelProvider.dispose());
592
+ await Promise.allSettled(work);
593
+ if (typeof releaseLlmProvider === "function") releaseLlmProvider();
594
+ preferenceStore.dispose();
595
+ return true;
596
+ }
597
+
598
+ async function initializeApplication() {
599
+ setPageStatus("loading", "Connecting live Arcane SDK source…");
600
+ modelSelect.value = selectedModel.id;
601
+ profileSelect.value = selectedProfile.id;
602
+ for (const option of modelSelect.options) {
603
+ option.disabled = MOBILE_BROWSER && Boolean(MODELS[option.value]?.desktopOnly);
604
+ }
605
+ if (preferenceWarning) {
606
+ ragStatus.textContent = `Using default selectors: ${preferenceWarning}`;
607
+ }
608
+
609
+ await Promise.all([
610
+ arcaneThemeReady,
611
+ waitForComponent(chat, {
612
+ errorEvent: "html-import-error",
613
+ event: "chat-ready",
614
+ methods: ["bindSession", "submitToolResult"],
615
+ property: "ready",
616
+ }),
617
+ ]);
618
+ chat.name = "You";
619
+ chat.aiName = selectedProfile.label;
620
+ chat.modelName = selectedModel.label;
621
+
622
+ const dbopfs = globalThis.dbopfs || new DBOPFS();
623
+ globalThis.dbopfs = dbopfs;
624
+ await dbopfs.readyPromise;
625
+
626
+ setPageStatus("loading", "Loading profile and local documents…");
627
+ await Promise.all([loadProfile(), loadRag()]);
628
+
629
+ // The ordinary example intentionally omits security configuration. Any future
630
+ // checks require explicit secure:true selection and user review before activation.
631
+ const source = createBrowserModelSource(localModelDescriptor(selectedModel));
632
+ browserModelProvider = createBrowserWasmLlmProvider({
633
+ sources: [source],
634
+ store: createDbopfsModelStore({ dbopfs }),
635
+ loadDefaults: {
636
+ contextTokens,
637
+ threads: 1,
638
+ batchTokens: MOBILE_BROWSER ? 128 : 512,
639
+ microBatchTokens: MOBILE_BROWSER ? 64 : 256,
640
+ },
641
+ });
642
+ const llmProvider = adaptV1LlmProvider(browserModelProvider);
643
+ const llmSelection = {
644
+ providerId: llmProvider.id,
645
+ modelId: selectedModel.id,
646
+ localOnly: true,
647
+ };
648
+
649
+ ai = new AI();
650
+ ai.voiceSpeed = 1.05;
651
+ ai.ready = true;
652
+ globalThis.ai = ai;
653
+ releaseLlmProvider = ai.providerRuntime.register(llmProvider);
654
+ ai.configureProviders({
655
+ llm: { default: llmSelection, localOnly: llmSelection },
656
+ stt: {
657
+ default: ai.providerRuntime.selection("stt"),
658
+ localOnly: ai.providerRuntime.selection("stt", { localOnly: true }),
659
+ },
660
+ tts: {
661
+ default: ai.providerRuntime.selection("tts"),
662
+ localOnly: ai.providerRuntime.selection("tts", { localOnly: true }),
663
+ },
664
+ });
665
+
666
+ try {
667
+ await ai.configureBrowserSpeech(speechConfiguration(dbopfs));
668
+ } catch (error) {
669
+ console.error("Arcane SDK speech configuration failed.", error);
670
+ }
671
+
672
+ stopRuntimeSubscription = subscribeAIRuntimeState(synchronizeRuntime);
673
+ await chat.bindSession({
674
+ ai,
675
+ sessionOptions: {
676
+ chatFileName: `wasm-ai-demo-${selectedProfile.id}-${selectedModel.id}.jsonl`,
677
+ contextBuilder: buildRequestContext,
678
+ loadExisting: true,
679
+ memory: false,
680
+ request: {
681
+ localOnly: true,
682
+ tools: toolsForProfile(selectedProfile.id),
683
+ toolChoice: "auto",
684
+ },
685
+ systemPrompt,
686
+ },
687
+ });
688
+ ragImportButton.disabled = !ragReady;
689
+ setPageStatus("ready", "SDK ready · start the model in chat");
690
+ }
691
+
692
+ modelSelect.addEventListener("change", changeModel);
693
+ profileSelect.addEventListener("change", changeProfile);
694
+ speechExecutionButton.addEventListener("click", inspectSpeechExecution);
695
+ ragImportButton.addEventListener("click", function chooseKnowledgeFiles() {
696
+ if (!ragImporting) ragFileInput.click();
697
+ });
698
+ ragFileInput.addEventListener("change", function importSelectedKnowledgeFiles() {
699
+ const files = Array.from(ragFileInput.files || []);
700
+ ragFileInput.value = "";
701
+ void importKnowledgeFiles(files);
702
+ });
703
+ chat.addEventListener("chat-file-uploaded", function importChatKnowledgeFile(event) {
704
+ const file = event.detail?.file;
705
+ if (!file || (!file.type.startsWith("text/") && !/\.(?:csv|json|md|txt)$/i.test(file.name))) return;
706
+ void importKnowledgeFiles([file]);
707
+ });
708
+ async function settleDisplayedToolCall() {
709
+ const pendingTool = chat.pendingTool;
710
+ if (!pendingTool) {
711
+ toolSettlementAttempts = 0;
712
+ toolSettlementCallId = "";
713
+ toolSettlementWaiting = false;
714
+ return false;
715
+ }
716
+ if (toolSettlementCallId !== pendingTool.id) {
717
+ toolSettlementAttempts = 0;
718
+ toolSettlementCallId = pendingTool.id;
719
+ toolSettlementWaiting = true;
720
+ }
721
+ if (
722
+ chat.aiAvailability?.llm !== true
723
+ || toolSettlementPending
724
+ || !toolSettlementWaiting
725
+ || toolSettlementAttempts >= 2
726
+ ) return false;
727
+ toolSettlementPending = true;
728
+ toolSettlementWaiting = false;
729
+ toolSettlementAttempts += 1;
730
+ let retryAfterSettlement = false;
731
+ try {
732
+ const accepted = await chat.submitToolResult({
733
+ disposition: "not-executed",
734
+ message: "This SDK example displays the requested tool call but does not execute application actions.",
735
+ request: { toolChoice: "none" },
736
+ toolCallId: pendingTool.id,
737
+ });
738
+ if (!accepted) {
739
+ if (toolSettlementAttempts < 2) {
740
+ console.error("Arcane SDK Chat did not accept the not-executed tool disposition; it will retry once after Chat becomes available.");
741
+ toolSettlementWaiting = true;
742
+ } else {
743
+ console.error("Arcane SDK Chat did not accept the not-executed tool disposition after one retry.");
744
+ }
745
+ if (chat.aiAvailability?.llm === true && toolSettlementWaiting) {
746
+ retryAfterSettlement = true;
747
+ }
748
+ return false;
749
+ }
750
+ toolSettlementAttempts = 0;
751
+ toolSettlementCallId = "";
752
+ toolSettlementWaiting = false;
753
+ return true;
754
+ } catch (error) {
755
+ console.error("The displayed SDK tool call could not be settled as not executed.", error);
756
+ return false;
757
+ } finally {
758
+ toolSettlementPending = false;
759
+ if (retryAfterSettlement) {
760
+ queueMicrotask(function retryToolSettlementAfterRejectedSubmission() {
761
+ void settleDisplayedToolCall();
762
+ });
763
+ }
764
+ }
765
+ }
766
+
767
+ function retryDisplayedToolCallAfterStateChange() {
768
+ void settleDisplayedToolCall();
769
+ }
770
+
771
+ chat.addEventListener("chat-session-bound", function settleRestoredToolCall() {
772
+ retryDisplayedToolCallAfterStateChange();
773
+ });
774
+ chat.addEventListener("chat-session-message", function settleNewToolCall() {
775
+ retryDisplayedToolCallAfterStateChange();
776
+ });
777
+ window.addEventListener("pagehide", function disposeDemoOnPageHide() {
778
+ void disposeApplication();
779
+ }, { once: true });
780
+
781
+ try {
782
+ await initializeApplication();
783
+ } catch (error) {
784
+ setPageStatus("error", "SDK initialization failed");
785
+ console.error("Arcane SDK initialization failed.", error);
786
+ ragImportButton.disabled = true;
787
+ }