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.
@@ -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(active||failed||entry.terminalError){
163
- entry.terminationRequested=true;
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
- try{
205
- entry.worker.postMessage({requestId:operation.requestId,operation:operation.kind,...operation.payload});
206
- }catch(error){
207
- if(operation.kind==='load')observeStop(entry,error,true);
208
- else{
209
- settle(entry,operation,error);
210
- setState(entry,'error',entry.state.loaded,error);
211
- dispatchNext(entry);
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.terminalError=error;
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
- definitions.push(` register(createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context));`);
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(',