arcane-os 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
  4. package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
  5. package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
  6. package/browser-runtime/core/app-control.mjs +439 -0
  7. package/docs/reference/ai/browser-speech.md +11 -0
  8. package/docs/reference/ai/browser-wasm.md +21 -10
  9. package/docs/reference/cli.md +9 -1
  10. package/docs/reference/core-native-packaging.md +14 -0
  11. package/docs/reference/inventory/package-api.json +23 -9
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/native-app-control.md +147 -0
  14. package/docs/reference/native-decisions.md +74 -16
  15. package/docs/reference/runtime-components.md +20 -0
  16. package/docs/reference/sdk-api.md +49 -6
  17. package/package.json +2 -1
  18. package/runtime/arcane/components/chat.html +140 -18
  19. package/runtime/arcane/modules/AI.js +10 -5
  20. package/src/cli/app-control.mjs +106 -0
  21. package/src/cli/main.mjs +5 -0
  22. package/src/core/app-control.mjs +23 -0
  23. package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
  24. package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
  25. package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
  26. package/src/core/hosts/windows/ArcaneHost.cs +45 -7
  27. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  28. package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
  29. package/src/core/services/decisions.mjs +16 -2
  30. package/src/core/shared-host.mjs +7 -91
  31. package/src/core/socket-connection.mjs +109 -0
  32. package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
  33. package/src/local-ai/decision-tokenizer.mjs +2 -2
  34. package/src/local-ai/decisions.mjs +33 -10
@@ -1,11 +1,11 @@
1
1
  import {spawn} from 'node:child_process';
2
2
  import {open} from 'node:fs/promises';
3
- import {createConnection, createServer} from 'node:net';
3
+ import {createServer} from 'node:net';
4
4
  import {randomUUID} from 'node:crypto';
5
- import {createCoreClient} from '../../browser-runtime/core/client.mjs';
6
5
  import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
7
6
  import {createCoreRuntime} from './runtime.mjs';
8
7
  import {createCoreFrameDecoder, encodeCoreFrame} from './stdio.mjs';
8
+ import {connectCoreSocket, createCoreSocketConnection, writeCoreSocket as write} from './socket-connection.mjs';
9
9
 
10
10
  function failure(code, message) { return new CoreError({code, message}); }
11
11
  function reportError(error) { console.error('Shared Arcane Core failed:', error); }
@@ -23,29 +23,6 @@ function response(id, result, error) {
23
23
  time: new Date().toISOString()};
24
24
  }
25
25
 
26
- // A stream write owns terminal events as well as its callback. Some destroyed
27
- // streams never deliver that callback; retirement must still settle the write.
28
- function write(output, content) {
29
- return new Promise(function writeFrame(resolve, reject) {
30
- let settled = false;
31
- function finish(error) {
32
- if (settled) return;
33
- settled = true;
34
- output.off('error', finish);
35
- output.off('close', closed);
36
- output.off('finish', closed);
37
- if (error) reject(error);
38
- else resolve();
39
- }
40
- function closed() { finish(failure('CORE_CONNECTION_CLOSED', 'The Core output closed during a write.')); }
41
- output.once('error', finish);
42
- output.once('close', closed);
43
- output.once('finish', closed);
44
- if (output.destroyed || output.writableEnded) { closed(); return; }
45
- try { output.write(content, finish); } catch (error) { finish(error); }
46
- });
47
- }
48
-
49
26
  /** Claim one app-selected pipe/socket before importing or creating services. */
50
27
  export async function startSharedCoreHost({
51
28
  endpoint, application, version, configure, getReplayEvents, signal, onError = reportError
@@ -268,29 +245,7 @@ export async function startSharedCoreHost({
268
245
  }
269
246
 
270
247
  function connectSocket(endpoint, signal) {
271
- signal?.throwIfAborted();
272
- if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
273
- return new Promise(function connect(resolve, reject) {
274
- const socket = createConnection({path: endpoint});
275
- socket.pause();
276
- function cleanup() {
277
- signal?.removeEventListener('abort', abort);
278
- socket.off('connect', connected);
279
- socket.off('error', failed);
280
- }
281
- function failed(error) {
282
- cleanup();
283
- socket.on('error', reportError);
284
- socket.destroy();
285
- reject(error);
286
- }
287
- function abort() { failed(signal.reason); }
288
- function connected() { cleanup(); resolve(socket); }
289
- socket.once('connect', connected);
290
- socket.once('error', failed);
291
- signal?.addEventListener('abort', abort, {once: true});
292
- if (signal?.aborted) abort();
293
- });
248
+ return connectCoreSocket(endpoint, signal, reportError);
294
249
  }
295
250
 
296
251
  // The child retains its own listener and complete diagnostic file after the
@@ -354,49 +309,10 @@ function launchSelection(start) {
354
309
  /** Connect/reuse, optionally starting an explicit independent headless entry. */
355
310
  export async function connectSharedCoreHost({endpoint, start, signal, onError = reportError} = {}) {
356
311
  const socket = await discover({endpoint, start: launchSelection(start), signal});
357
- let client;
358
- let receive;
359
- let closing = false;
360
- let writes = Promise.resolve();
361
- let resolveClosed;
362
- let rejectClosed;
363
- const closed = new Promise(function ownConnection(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
364
- closed.catch(function observeConnectionFailure() {});
365
- function diagnostic(error) { report(onError, error); }
366
- function failed(error) { client?.failTransport(error); socket.destroy(); rejectClosed(error); }
367
- const decoder = createCoreFrameDecoder(function frame(value) { receive(value); });
368
- socket.on('data', function data(chunk) { try { decoder.push(chunk); } catch (error) { failed(error); } });
369
- socket.on('error', failed);
370
- socket.on('end', function ended() {
371
- try { decoder.finish(); } catch (error) { failed(error); }
372
- });
373
- socket.on('close', function disconnected() {
374
- signal?.removeEventListener('abort', abort);
375
- if (!closing) client?.failTransport(failure('CORE_CONNECTION_CLOSED', 'The shared Core connection closed.'));
376
- resolveClosed();
377
- });
378
- const transport = {
379
- name: 'shared-core',
380
- subscribe(listener) {
381
- receive = listener;
382
- return function disconnect() {
383
- closing = true;
384
- writes.finally(function retireSocket() { socket.end(); }).catch(diagnostic);
385
- };
386
- },
387
- send(frame) {
388
- const content = encodeCoreFrame(frame);
389
- writes = writes.then(function sendFrame() { return write(socket, content); });
390
- writes.catch(failed);
391
- return writes;
392
- }
393
- };
394
- client = createCoreClient({transport, replayRuntimeState: true, onError: diagnostic});
395
- function abort() { client.close(); }
396
- signal?.addEventListener('abort', abort, {once: true});
397
- if (signal?.aborted) abort();
398
- socket.resume();
399
- return {client, endpoint, closed, close: function close() { client.close(); return closed; },
312
+ const connection = createCoreSocketConnection({socket, endpoint, signal, onError,
313
+ replayRuntimeState: true, name: 'shared-core', closedMessage: 'The shared Core connection closed.'});
314
+ const {client, closed} = connection;
315
+ return {...connection,
400
316
  async shutdown(options) {
401
317
  const result = await client.invoke('core.host.shutdown', {}, options);
402
318
  client.close();
@@ -0,0 +1,109 @@
1
+ import {createConnection} from 'node:net';
2
+ import {createCoreClient} from '../../browser-runtime/core/client.mjs';
3
+ import {CoreError} from '../../browser-runtime/core/contracts.mjs';
4
+ import {createCoreFrameDecoder, encodeCoreFrame} from './stdio.mjs';
5
+
6
+ function failure(code, message) { return new CoreError({code, message}); }
7
+ function reportSocketError(error) { console.error('Arcane Core connection failed:', error); }
8
+ function report(observer, error) {
9
+ try {
10
+ Promise.resolve(observer(error)).catch(function observerFailed(cause) { console.error(cause, error); });
11
+ } catch (cause) { console.error(cause, error); }
12
+ }
13
+
14
+ // Terminal events settle writes even when a destroyed stream omits its callback.
15
+ export function writeCoreSocket(output, content) {
16
+ return new Promise(function writeFrame(resolve, reject) {
17
+ let settled = false;
18
+ function finish(error) {
19
+ if (settled) return;
20
+ settled = true;
21
+ output.off('error', finish);
22
+ output.off('close', closed);
23
+ output.off('finish', closed);
24
+ if (error) reject(error);
25
+ else resolve();
26
+ }
27
+ function closed() { finish(failure('CORE_CONNECTION_CLOSED', 'The Core output closed during a write.')); }
28
+ output.once('error', finish);
29
+ output.once('close', closed);
30
+ output.once('finish', closed);
31
+ if (output.destroyed || output.writableEnded) { closed(); return; }
32
+ try { output.write(content, finish); } catch (error) { finish(error); }
33
+ });
34
+ }
35
+
36
+ /** Connect once and pause incoming frames until their owner is attached. */
37
+ export function connectCoreSocket(endpoint, signal, onError = reportSocketError) {
38
+ signal?.throwIfAborted();
39
+ if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
40
+ return new Promise(function connect(resolve, reject) {
41
+ const socket = createConnection({path: endpoint});
42
+ socket.pause();
43
+ function cleanup() {
44
+ signal?.removeEventListener('abort', abort);
45
+ socket.off('connect', connected);
46
+ socket.off('error', failed);
47
+ }
48
+ function failed(error) {
49
+ cleanup();
50
+ socket.on('error', function connectionFailed(error) { report(onError, error); });
51
+ socket.destroy();
52
+ reject(error);
53
+ }
54
+ function abort() { failed(signal.reason); }
55
+ function connected() { cleanup(); resolve(socket); }
56
+ socket.once('connect', connected);
57
+ socket.once('error', failed);
58
+ signal?.addEventListener('abort', abort, {once: true});
59
+ if (signal?.aborted) abort();
60
+ });
61
+ }
62
+
63
+ /** Own one connected socket; the caller decides which RPC methods to expose. */
64
+ export function createCoreSocketConnection({socket, endpoint, signal, onError = reportSocketError,
65
+ replayRuntimeState = false, name = 'core', closedMessage = 'The Core connection closed.'}) {
66
+ let client;
67
+ let receive;
68
+ let closing = false;
69
+ let writes = Promise.resolve();
70
+ let resolveClosed;
71
+ let rejectClosed;
72
+ const closed = new Promise(function ownConnection(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
73
+ closed.catch(function observeConnectionFailure() {});
74
+ function diagnostic(error) { report(onError, error); }
75
+ function failed(error) { client?.failTransport(error); socket.destroy(); rejectClosed(error); }
76
+ const decoder = createCoreFrameDecoder(function frame(value) { receive(value); });
77
+ socket.on('data', function data(chunk) { try { decoder.push(chunk); } catch (error) { failed(error); } });
78
+ socket.on('error', failed);
79
+ socket.on('end', function ended() {
80
+ try { decoder.finish(); } catch (error) { failed(error); }
81
+ });
82
+ socket.on('close', function disconnected() {
83
+ signal?.removeEventListener('abort', abort);
84
+ if (!closing) client?.failTransport(failure('CORE_CONNECTION_CLOSED', closedMessage));
85
+ resolveClosed();
86
+ });
87
+ const transport = {
88
+ name,
89
+ subscribe(listener) {
90
+ receive = listener;
91
+ return function disconnect() {
92
+ closing = true;
93
+ writes.finally(function retireSocket() { socket.end(); }).catch(diagnostic);
94
+ };
95
+ },
96
+ send(frame) {
97
+ const content = encodeCoreFrame(frame);
98
+ writes = writes.then(function sendFrame() { return writeCoreSocket(socket, content); });
99
+ writes.catch(failed);
100
+ return writes;
101
+ }
102
+ };
103
+ client = createCoreClient({transport, replayRuntimeState, onError: diagnostic});
104
+ function abort() { client.close(); }
105
+ signal?.addEventListener('abort', abort, {once: true});
106
+ if (signal?.aborted) abort();
107
+ socket.resume();
108
+ return {client, endpoint, closed, close: function close() { client.close(); return closed; }};
109
+ }
@@ -17,7 +17,7 @@ try {
17
17
  };
18
18
  parentPort.on('message', function encodeRequest({id, rows}) {
19
19
  try {
20
- const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, 'laya');
20
+ const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, workerData.family);
21
21
  const inputs = createDecisionInputs(encoded, pad);
22
22
  parentPort.postMessage({id, result: inputs});
23
23
  } catch (error) {
@@ -6,7 +6,7 @@ function cancelled() {
6
6
  }
7
7
 
8
8
  /** One tokenizer worker owned by one native model activation. */
9
- export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, signal, onError}) {
9
+ export function createDecisionTokenizer({family = 'laya', tokenizerPath, tokenizerConfigPath, signal, onError}) {
10
10
  signal?.throwIfAborted();
11
11
  const pending = new Map();
12
12
  let nextId = 0;
@@ -14,7 +14,7 @@ export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, sig
14
14
  let terminalError;
15
15
  let exited = false;
16
16
  const worker = new Worker(new URL('./decision-tokenizer-worker.mjs', import.meta.url), {
17
- workerData: {tokenizerPath, tokenizerConfigPath}
17
+ workerData: {family, tokenizerPath, tokenizerConfigPath}
18
18
  });
19
19
  let resolveExit;
20
20
  const exit = new Promise(function tokenizerExit(resolve) { resolveExit = resolve; });
@@ -5,7 +5,13 @@ import {decodeDecisionOutputs} from '../../browser-runtime/ai/decision-runtime.m
5
5
  import {createDecisionTokenizer} from './decision-tokenizer.mjs';
6
6
 
7
7
  const STATE_EVENT = 'ai.decisions.state';
8
- const LAYA_FILES = ['onnx/model.onnx', 'onnx/model.onnx_data', 'tokenizer.json', 'tokenizer_config.json'];
8
+
9
+ function selectedGraph(family, dtype) {
10
+ if (family === 'laya' && dtype === 'fp32') return 'onnx/model.onnx';
11
+ if (family === 'laya' && dtype === 'fp16') return 'onnx/model_fp16.onnx';
12
+ if (family === 'julia' && dtype === 'fp32') return 'model.onnx';
13
+ throw new TypeError('Native decisions support Laya fp32/fp16 and Julia fp32 graph selections.');
14
+ }
9
15
 
10
16
  function cancellation(message) {
11
17
  return new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message});
@@ -18,6 +24,7 @@ function sameExecutionTarget(left, right) {
18
24
  }
19
25
 
20
26
  function sameActivation(left, right) {
27
+ if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
21
28
  if (!sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
22
29
  if (left.resourcePaths === right.resourcePaths) return true;
23
30
  if (!left.resourcePaths || !right.resourcePaths) return false;
@@ -27,15 +34,15 @@ function sameActivation(left, right) {
27
34
  }
28
35
 
29
36
  /**
30
- * One explicitly activated Laya FP32 model using an existing native ONNX owner.
37
+ * One explicitly activated typed-decision model using an existing native ONNX owner.
31
38
  * It does not create another inference runtime or load on construction.
32
39
  */
33
40
  export function createNativeDecisionModel({
34
41
  onnx, modelAssets, workingDirectory, paths, assetProjectionId, resourcePaths,
35
- model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
42
+ family = 'laya', model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
36
43
  dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
37
44
  } = {}) {
38
- if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
45
+ selectedGraph(family, dtype);
39
46
  const owner = {};
40
47
  const events = createArcaneEventSource(owner, {source: 'native-decisions', eventTypes: [STATE_EVENT]});
41
48
  const loads = new Set();
@@ -49,15 +56,17 @@ export function createNativeDecisionModel({
49
56
  let lastError = null;
50
57
  let execution = null;
51
58
  let selectedActivation = {
52
- executionTarget,
59
+ family, model, revision, dtype, executionTarget,
53
60
  ...(assetProjectionId == null ? {} : {assetProjectionId, resourcePaths: resourcePaths && {...resourcePaths}})
54
61
  };
62
+ let activeSelection = selectedActivation;
55
63
  let pendingActivation;
56
64
 
57
65
  function current() {
58
66
  const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
59
67
  return {
60
- family: 'laya', model, revision, dtype, state,
68
+ family: activeSelection.family, model: activeSelection.model,
69
+ revision: activeSelection.revision, dtype: activeSelection.dtype, state,
61
70
  loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
62
71
  activeRequests: activation?.jobs.size ?? 0, progress,
63
72
  error: lastError ? serializeCoreError(lastError) : null,
@@ -163,10 +172,14 @@ export function createNativeDecisionModel({
163
172
  }
164
173
  if (paths) return paths;
165
174
  if (!modelAssets) throw new CoreError({code: 'ARCANE_DECISION_ASSETS_UNAVAILABLE', message: 'Native decisions need prepared paths or the Core model-assets owner.'});
166
- const base = `https://huggingface.co/${model}/resolve/${encodeURIComponent(revision)}/`;
175
+ const {family: selectedFamily, model: selectedModel, revision: selectedRevision, dtype: selectedDtype} = entry.selection;
176
+ const graph = selectedGraph(selectedFamily, selectedDtype);
177
+ const companion = selectedFamily === 'laya' ? `${graph}_data` : `${graph}.data`;
178
+ const members = [graph, companion, 'tokenizer.json', 'tokenizer_config.json'];
179
+ const base = `https://huggingface.co/${selectedModel}/resolve/${encodeURIComponent(selectedRevision)}/`;
167
180
  const projection = await modelAssets.prepare({
168
181
  id: entry.id, workingDirectory,
169
- members: LAYA_FILES.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
182
+ members: members.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
170
183
  signal: entry.controller.signal,
171
184
  onProgress(value) { updateProgress(entry, value); }
172
185
  });
@@ -176,7 +189,7 @@ export function createNativeDecisionModel({
176
189
  await modelAssets.release(entry.id);
177
190
  entry.prepared = false;
178
191
  const files = Object.fromEntries(projection.members.map(function memberFile(member) { return [member.path, member.nativePath]; }));
179
- return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
192
+ return {model: files[graph], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
180
193
  }
181
194
 
182
195
  function beginActivation(request) {
@@ -187,6 +200,7 @@ export function createNativeDecisionModel({
187
200
  request.retained = undefined;
188
201
  request.entry = entry;
189
202
  activation = entry;
203
+ activeSelection = entry.selection;
190
204
  state = 'loading';
191
205
  progress = {phase: 'preparing-model'};
192
206
  lastError = null;
@@ -196,6 +210,7 @@ export function createNativeDecisionModel({
196
210
  entry.controller.signal.throwIfAborted();
197
211
  updateProgress(entry, {phase: 'loading-model'});
198
212
  entry.tokenizer = createDecisionTokenizer({
213
+ family: entry.selection.family,
199
214
  tokenizerPath: files.tokenizer, tokenizerConfigPath: files.tokenizerConfig,
200
215
  signal: entry.controller.signal,
201
216
  onError(error) {
@@ -258,14 +273,22 @@ export function createNativeDecisionModel({
258
273
  }).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
259
274
  }
260
275
 
261
- async function load({signal: operationSignal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths} = {}) {
276
+ async function load({
277
+ family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
278
+ signal: operationSignal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
279
+ } = {}) {
262
280
  assertOpen(operationSignal);
263
281
  const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
264
282
  const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
265
283
  const selection = {
284
+ family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
285
+ model: requestedModel === undefined ? selectedActivation.model : requestedModel,
286
+ revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
287
+ dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
266
288
  executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
267
289
  ...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
268
290
  };
291
+ selectedGraph(selection.family, selection.dtype);
269
292
  if (pendingActivation && !pendingActivation.controller.signal.aborted && sameActivation(pendingActivation.selection, selection)) {
270
293
  return observeLoad(pendingActivation, operationSignal);
271
294
  }