arcane-os 0.78.0 → 0.79.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/CHANGELOG.md +30 -0
- package/README.md +6 -1
- package/browser-runtime/ai/core-onnx.mjs +6 -2
- package/browser-runtime/ai/speech-worker-runtime.mjs +1 -1
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/publishing.md +20 -3
- package/docs/reference/ai/browser-speech.md +7 -1
- package/docs/reference/execution-devices.md +157 -0
- package/docs/reference/inventory/package-api.json +39 -11
- package/docs/reference/local-ai.md +104 -10
- package/docs/reference/native-decisions.md +65 -10
- package/package.json +4 -2
- package/src/core/services/decisions.mjs +51 -15
- package/src/core/services/execution-devices.mjs +32 -0
- package/src/core/services/local-ai.mjs +12 -5
- package/src/local-ai/decisions.mjs +93 -15
- package/src/local-ai/dev.mjs +7 -3
- package/src/local-ai/execution-devices-windows.ps1 +833 -0
- package/src/local-ai/execution-devices.mjs +288 -0
- package/src/local-ai/onnx-worker.mjs +149 -19
- package/src/local-ai/onnx.mjs +87 -27
- package/src/native/portable-layout.mjs +8 -1
package/src/local-ai/onnx.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {Worker} from 'node:worker_threads';
|
|
|
2
2
|
import Is from 'strong-type';
|
|
3
3
|
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
4
4
|
import {createEventQueue} from '../event-queue.mjs';
|
|
5
|
+
import {createExecutionDeviceCatalog} from './execution-devices.mjs';
|
|
5
6
|
|
|
6
7
|
const is=new Is(false);
|
|
7
8
|
|
|
@@ -46,12 +47,16 @@ function waitForRelease(task,signal){
|
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
50
|
+
export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:suppliedExecutionDevices}={}){
|
|
50
51
|
const owner={};
|
|
51
52
|
const events=createArcaneEventSource(owner,{source:'local-ai.onnx',eventTypes:['onnx.state']});
|
|
52
53
|
const diagnosticFailures=[];
|
|
53
54
|
const diagnostics=createEventQueue(deliverDiagnostic,{onFailure:reportBackgroundError});
|
|
54
55
|
const sessions=new Map();
|
|
56
|
+
const ownsExecutionDevices = suppliedExecutionDevices === undefined;
|
|
57
|
+
const executionDevices = ownsExecutionDevices ? createExecutionDeviceCatalog(
|
|
58
|
+
{signal}
|
|
59
|
+
) : suppliedExecutionDevices;
|
|
55
60
|
let nextRequestId=0;
|
|
56
61
|
let closing=false;
|
|
57
62
|
let closed=false;
|
|
@@ -150,24 +155,25 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
function stopSession(entry,reason,failed=false){
|
|
158
|
+
if(failed)entry.terminalError??=reason;
|
|
153
159
|
if(entry.stopTask)return entry.stopTask;
|
|
154
|
-
if(entry.exited)return Promise.resolve(structuredClone(entry.state));
|
|
155
160
|
const active=Boolean(entry.active);
|
|
156
161
|
const worker=entry.worker;
|
|
157
162
|
entry.stopping=true;
|
|
158
|
-
if(failed)entry.terminalError??=reason;
|
|
159
163
|
entry.stopTask=Promise.resolve().then(async function releaseSession(){
|
|
160
164
|
let terminationError;
|
|
161
165
|
try{
|
|
162
|
-
if(
|
|
163
|
-
entry.
|
|
164
|
-
await worker.terminate();
|
|
165
|
-
}else{
|
|
166
|
-
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
167
|
-
catch(error){
|
|
168
|
-
entry.releaseError=error;
|
|
166
|
+
if(!entry.exited){
|
|
167
|
+
if(active||failed||entry.terminalError){
|
|
169
168
|
entry.terminationRequested=true;
|
|
170
169
|
await worker.terminate();
|
|
170
|
+
}else{
|
|
171
|
+
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
172
|
+
catch(error){
|
|
173
|
+
entry.releaseError=error;
|
|
174
|
+
entry.terminationRequested=true;
|
|
175
|
+
await worker.terminate();
|
|
176
|
+
}
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
179
|
}catch(error){
|
|
@@ -177,6 +183,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
177
183
|
// Failed termination is not actual worker exit. Keep native file
|
|
178
184
|
// ownership until exit and complete output delivery have settled.
|
|
179
185
|
await entry.exit;
|
|
186
|
+
if (entry.preparation) await Promise.allSettled([entry.preparation]);
|
|
180
187
|
const outputResults=await Promise.allSettled(entry.outputTasks);
|
|
181
188
|
const failures=[entry.releaseError,terminationError].filter(function present(error){return error!==null&&error!==undefined;});
|
|
182
189
|
for(const result of outputResults)if(result.status==='rejected')failures.push(result.reason);
|
|
@@ -184,13 +191,56 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
184
191
|
if(failures.length)throw failures[0];
|
|
185
192
|
return structuredClone(entry.state);
|
|
186
193
|
});
|
|
194
|
+
entry.controller.abort(reason);
|
|
187
195
|
// A native call has no AbortSignal. Stop delivery immediately, retain
|
|
188
196
|
// unloading state, and join actual worker exit before reporting release.
|
|
189
197
|
rejectOperations(entry,reason);
|
|
190
|
-
setState(entry,entry.terminalError?'error':'unloading',entry.state.loaded,entry.terminalError);
|
|
198
|
+
if(!entry.exited)setState(entry,entry.terminalError?'error':'unloading',entry.state.loaded,entry.terminalError);
|
|
191
199
|
return entry.stopTask;
|
|
192
200
|
}
|
|
193
201
|
|
|
202
|
+
async function prepareTarget(entry, operation) {
|
|
203
|
+
const operationSignal = operation.signal
|
|
204
|
+
? AbortSignal.any([entry.controller.signal, operation.signal]) : entry.controller.signal;
|
|
205
|
+
const {executionTarget, executionPreference, sessionOptions} = operation.payload;
|
|
206
|
+
const targetResolution = await executionDevices.resolveTarget(
|
|
207
|
+
{executionTarget, signal: operationSignal}
|
|
208
|
+
);
|
|
209
|
+
operationSignal.throwIfAborted();
|
|
210
|
+
const selectProviders = sessionOptions === undefined
|
|
211
|
+
|| (sessionOptions !== null && is.object(sessionOptions) && sessionOptions.executionProviders === undefined);
|
|
212
|
+
const deviceInventory = executionTarget === null && executionPreference === 'gpu' && selectProviders
|
|
213
|
+
? await executionDevices.devices(
|
|
214
|
+
{signal: operationSignal}
|
|
215
|
+
) : undefined;
|
|
216
|
+
operationSignal.throwIfAborted();
|
|
217
|
+
if (entry.stopping || entry.exited || entry.active !== operation) return;
|
|
218
|
+
operation.payload = {...operation.payload, targetResolution, deviceInventory};
|
|
219
|
+
entry.state.execution = {...entry.state.execution, ...targetResolution};
|
|
220
|
+
publish(entry);
|
|
221
|
+
postOperation(entry, operation);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function postOperation(entry, operation) {
|
|
225
|
+
if (closing || entry.stopping || entry.exited || entry.active !== operation) return;
|
|
226
|
+
if (operation.signal?.aborted) {
|
|
227
|
+
operation.cancel();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
entry.worker.postMessage(
|
|
232
|
+
{requestId: operation.requestId, operation: operation.kind, ...operation.payload}
|
|
233
|
+
);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (operation.kind === 'load') observeStop(entry, error, true);
|
|
236
|
+
else {
|
|
237
|
+
settle(entry, operation, error);
|
|
238
|
+
setState(entry, 'error', entry.state.loaded, error);
|
|
239
|
+
dispatchNext(entry);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
194
244
|
function dispatchNext(entry){
|
|
195
245
|
if(closing||entry.stopping||entry.exited||entry.active)return;
|
|
196
246
|
const operation=entry.queue.shift();
|
|
@@ -201,16 +251,16 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
201
251
|
return;
|
|
202
252
|
}
|
|
203
253
|
entry.active=operation;
|
|
204
|
-
|
|
205
|
-
entry.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
254
|
+
if (operation.kind === 'load' && operation.payload.executionTarget !== undefined) {
|
|
255
|
+
entry.preparation = prepareTarget(entry, operation);
|
|
256
|
+
void entry.preparation.catch(
|
|
257
|
+
function targetPreparationFailed(error) {
|
|
258
|
+
if (!entry.stopping && !entry.exited) observeStop(entry, error, true);
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
return;
|
|
213
262
|
}
|
|
263
|
+
postOperation(entry, operation);
|
|
214
264
|
}
|
|
215
265
|
|
|
216
266
|
function enqueue(entry,kind,payload,operationSignal,deferred=false){
|
|
@@ -244,6 +294,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
244
294
|
if(operation.signal?.aborted){operation.cancel();return;}
|
|
245
295
|
if(message.error){
|
|
246
296
|
const error=restoreError(message.error);
|
|
297
|
+
if (operation.kind === 'load' && error.execution) entry.state.execution = error.execution;
|
|
247
298
|
if(operation.kind==='load')observeStop(entry,error,true);
|
|
248
299
|
else{
|
|
249
300
|
settle(entry,operation,error);
|
|
@@ -269,11 +320,12 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
269
320
|
rejectOperations(entry,entry.terminalError);
|
|
270
321
|
}
|
|
271
322
|
const error=entry.releaseError??entry.terminalError;
|
|
323
|
+
if(!entry.stopTask)observeStop(entry,error,Boolean(error));
|
|
272
324
|
try{setState(entry,error?'error':'unloaded',false,error);}
|
|
273
325
|
finally{entry.resolveExit(code);}
|
|
274
326
|
}
|
|
275
327
|
|
|
276
|
-
async function load({id,model,sessionOptions,executionPreference,signal:operationSignal}={}){
|
|
328
|
+
async function load({id,model,sessionOptions,executionPreference,executionTarget,signal:operationSignal}={}){
|
|
277
329
|
assertOpen(operationSignal);
|
|
278
330
|
if(!is.string(id)||!is.string(model))throw new TypeError('ONNX load requires a session id and model path.');
|
|
279
331
|
const previous=sessions.get(id);
|
|
@@ -293,7 +345,11 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
293
345
|
throw failure('LOCAL_AI_SESSION_EXISTS',`ONNX session ${String(id)} already exists. Unload it before loading a replacement.`);
|
|
294
346
|
}
|
|
295
347
|
const entry={
|
|
296
|
-
state:{id,model,state:'loading',loaded:false,error:null,inputNames:[],outputNames:[],inputMetadata:[],outputMetadata:[]
|
|
348
|
+
state:{id,model,state:'loading',loaded:false,error:null,inputNames:[],outputNames:[],inputMetadata:[],outputMetadata:[],
|
|
349
|
+
execution: {requestedTarget: executionTarget ?? null, resolvedDevice: null,
|
|
350
|
+
resolution: executionTarget === undefined || executionTarget === null ? 'automatic' : null,
|
|
351
|
+
reason: 'session-loading', configuredTarget: null, observedTarget: null}},
|
|
352
|
+
controller: new AbortController(), preparation: null,
|
|
297
353
|
worker:null,active:null,queue:[],stopping:false,exited:false,stopTask:null,terminalError:null,releaseError:null,terminationRequested:false,outputTasks:[]
|
|
298
354
|
};
|
|
299
355
|
entry.exit=new Promise(function workerExit(resolve){entry.resolveExit=resolve;});
|
|
@@ -309,10 +365,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
309
365
|
entry.worker.on('message',function workerMessage(message){receiveResult(entry,message);});
|
|
310
366
|
entry.worker.on('messageerror',function workerMessageError(error){observeStop(entry,error,true);});
|
|
311
367
|
entry.worker.on('error',function workerError(error){
|
|
312
|
-
entry
|
|
313
|
-
entry.stopping=true;
|
|
314
|
-
rejectOperations(entry,error);
|
|
315
|
-
setState(entry,'error',entry.state.loaded,error);
|
|
368
|
+
observeStop(entry,error,true);
|
|
316
369
|
});
|
|
317
370
|
entry.worker.once('exit',function workerExit(code){workerExited(entry,code);});
|
|
318
371
|
}catch(error){
|
|
@@ -323,7 +376,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
323
376
|
}
|
|
324
377
|
// Queue load before publishing readiness so a synchronous subscriber
|
|
325
378
|
// can enqueue inference or request release without racing ownership.
|
|
326
|
-
const task=enqueue(entry,'load',{model,sessionOptions,executionPreference},operationSignal,true);
|
|
379
|
+
const task=enqueue(entry,'load',{model,sessionOptions,executionPreference,executionTarget},operationSignal,true);
|
|
327
380
|
publish(entry);
|
|
328
381
|
dispatchNext(entry);
|
|
329
382
|
return task;
|
|
@@ -364,6 +417,13 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
364
417
|
try{await diagnostics.drain();}
|
|
365
418
|
catch(error){failures.push(error);}
|
|
366
419
|
failures.push(...diagnosticFailures);
|
|
420
|
+
if (closed && ownsExecutionDevices) {
|
|
421
|
+
try {
|
|
422
|
+
await executionDevices.dispose();
|
|
423
|
+
} catch (error) {
|
|
424
|
+
failures.push(error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
367
427
|
if(closed)events.dispose();
|
|
368
428
|
if(failures.length)throw new AggregateError(failures,'The ONNX runtime closed with errors.');
|
|
369
429
|
return current();
|
|
@@ -178,7 +178,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
178
178
|
})
|
|
179
179
|
};
|
|
180
180
|
imports.push({binding: '{createLocalAIService}', specifier: 'arcane-os/core/local-ai'});
|
|
181
|
-
|
|
181
|
+
imports.push({binding: '{createExecutionDeviceCatalog}', specifier: 'arcane-os/local-ai/execution-devices'});
|
|
182
|
+
imports.push({binding: '{createExecutionDeviceService}', specifier: 'arcane-os/core/execution-devices'});
|
|
183
|
+
definitions.push(` register(createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), {...context, executionDevices}));`);
|
|
182
184
|
}
|
|
183
185
|
if (modelAssetsSelected) {
|
|
184
186
|
imports.push({binding: '{createModelAssetService}', specifier: 'arcane-os/core/model-assets'});
|
|
@@ -259,6 +261,11 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
259
261
|
function separateImport(line, index) { return line + (index < imports.length - 1 ? ',' : ''); }
|
|
260
262
|
),
|
|
261
263
|
' ]);',
|
|
264
|
+
...(localAI === undefined ? [] : [
|
|
265
|
+
' // One lazy inventory owner per Core host; no model startup dependency.',
|
|
266
|
+
' const executionDevices = createExecutionDeviceCatalog({signal: context.signal});',
|
|
267
|
+
' register(createExecutionDeviceService({catalog: executionDevices}));'
|
|
268
|
+
]),
|
|
262
269
|
...(modelAssetsSelected ? [' const modelAssets = createModelAssetService({appRoot: context.appRoot});', ' register(modelAssets);'] : []),
|
|
263
270
|
...(whisperSelected ? [
|
|
264
271
|
' const stt = createWhisperRuntime(',
|