arcane-os 0.83.0 → 0.84.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.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/docs/reference/core-native-packaging.md +21 -0
- package/docs/reference/core-runtime.md +28 -1
- package/docs/reference/core-shared-host.md +71 -8
- package/docs/reference/inventory/package-api.json +20 -6
- package/docs/reference/local-ai.md +24 -3
- package/docs/reference/native-decisions.md +13 -0
- package/docs/reference/sdk-api.md +87 -3
- package/package.json +1 -1
- package/src/core/host.mjs +1 -1
- package/src/core/runtime-connection.mjs +165 -0
- package/src/core/runtime.mjs +10 -6
- package/src/core/shared-host.mjs +270 -70
- package/src/core/stdio.mjs +18 -3
- package/src/local-ai/onnx-worker.mjs +39 -5
- package/src/local-ai/onnx.mjs +3 -3
- package/src/native/portable-layout.mjs +30 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {parentPort,workerData} from 'node:worker_threads';
|
|
2
2
|
import {pathToFileURL} from 'node:url';
|
|
3
3
|
import Is from 'strong-type';
|
|
4
|
+
import {encodeTensorMap,encodeTensorFetches} from '../../browser-runtime/ai/onnx-tensors.mjs';
|
|
4
5
|
|
|
5
6
|
const is=new Is(false);
|
|
6
7
|
const imported=await import(pathToFileURL(workerData.modulePath).href);
|
|
7
8
|
const ort=imported.default??imported;
|
|
8
9
|
let session;
|
|
10
|
+
let sessionExecution;
|
|
9
11
|
|
|
10
12
|
function describeError(error){
|
|
11
13
|
if(!(error instanceof Error))return {name:'Error',message:String(error),cause:error};
|
|
@@ -27,6 +29,7 @@ async function createSession(request) {
|
|
|
27
29
|
resolution: 'automatic',
|
|
28
30
|
reason: selectProviders ? 'engine-default-required' : 'caller-session-options',
|
|
29
31
|
...request.targetResolution,
|
|
32
|
+
deviceInventory: request.deviceInventory ?? null,
|
|
30
33
|
configuredTarget: null,
|
|
31
34
|
observedTarget: null,
|
|
32
35
|
supportedBackends: [],
|
|
@@ -225,28 +228,35 @@ function tensorMap(records,inputs=false){
|
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
async function handleRequest(request){
|
|
231
|
+
let runStage;
|
|
232
|
+
let feeds;
|
|
233
|
+
let fetches;
|
|
228
234
|
try{
|
|
229
235
|
let result;
|
|
230
236
|
if(request.operation==='load'){
|
|
231
|
-
|
|
237
|
+
sessionExecution = await createSession(request);
|
|
232
238
|
result={
|
|
233
239
|
inputNames:session.inputNames,
|
|
234
240
|
outputNames:session.outputNames,
|
|
235
241
|
inputMetadata:session.inputMetadata,
|
|
236
242
|
outputMetadata:session.outputMetadata
|
|
237
243
|
};
|
|
238
|
-
if (
|
|
244
|
+
if (sessionExecution) result.execution = sessionExecution;
|
|
239
245
|
}else if(request.operation==='run'){
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
runStage='feed-conversion';
|
|
247
|
+
feeds=tensorMap(request.feeds,true);
|
|
248
|
+
runStage='fetch-conversion';
|
|
249
|
+
fetches=request.fetches===undefined||request.fetches===null||is.array(request.fetches)
|
|
242
250
|
?request.fetches:tensorMap(request.fetches);
|
|
243
251
|
// ORT treats an empty fetch map as its options overload and would
|
|
244
252
|
// ignore the third argument. Both forms select every output.
|
|
245
253
|
const allOutputs=fetches===undefined
|
|
246
254
|
||(fetches!==null&&!is.array(fetches)&&Object.keys(fetches).length===0);
|
|
255
|
+
runStage='session.run';
|
|
247
256
|
const outputs=allOutputs
|
|
248
257
|
?await session.run(feeds,request.runOptions)
|
|
249
258
|
:await session.run(feeds,fetches,request.runOptions);
|
|
259
|
+
runStage='output-conversion';
|
|
250
260
|
result=Object.fromEntries(Object.entries(outputs).map(function tensorRecord([name,tensor]){
|
|
251
261
|
return [name,{type:tensor.type,data:tensor.data,dims:tensor.dims}];
|
|
252
262
|
}));
|
|
@@ -258,9 +268,33 @@ async function handleRequest(request){
|
|
|
258
268
|
}
|
|
259
269
|
// Structured cloning retains typed tensors and BigInt without taking
|
|
260
270
|
// ownership of the caller's buffers or converting their contents.
|
|
271
|
+
if(request.operation==='run')runStage='result-delivery';
|
|
261
272
|
parentPort.postMessage({requestId:request.requestId,operation:request.operation,result});
|
|
262
273
|
}catch(error){
|
|
263
|
-
|
|
274
|
+
const record=describeError(error);
|
|
275
|
+
if(request.operation==='run'){
|
|
276
|
+
const diagnostic={
|
|
277
|
+
sessionId:workerData.sessionId,requestId:request.requestId,stage:runStage,
|
|
278
|
+
model:workerData.model,runtimeModulePath:workerData.modulePath,
|
|
279
|
+
execution:sessionExecution??null,runOptions:request.runOptions,
|
|
280
|
+
feedsSource:feeds===undefined?'request':'converted',
|
|
281
|
+
fetchesSource:runStage==='feed-conversion'||runStage==='fetch-conversion'?'request':'converted'
|
|
282
|
+
};
|
|
283
|
+
record.onnxRun=diagnostic;
|
|
284
|
+
try{
|
|
285
|
+
diagnostic.inputNames=session?.inputNames??null;
|
|
286
|
+
diagnostic.inputMetadata=session?.inputMetadata??null;
|
|
287
|
+
diagnostic.outputNames=session?.outputNames??null;
|
|
288
|
+
diagnostic.outputMetadata=session?.outputMetadata??null;
|
|
289
|
+
// Copy complete tensors only after failure. The same codec used
|
|
290
|
+
// by Core tensor transport preserves BigInt and typed values.
|
|
291
|
+
diagnostic.feeds=encodeTensorMap(feeds===undefined?request.feeds:feeds);
|
|
292
|
+
diagnostic.fetches=encodeTensorFetches(diagnostic.fetchesSource==='request'?request.fetches:fetches);
|
|
293
|
+
}catch(diagnosticError){
|
|
294
|
+
diagnostic.diagnosticError=describeError(diagnosticError);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
parentPort.postMessage({requestId:request.requestId,operation:request.operation,error:record});
|
|
264
298
|
}finally{
|
|
265
299
|
if(request.operation==='unload')parentPort.close();
|
|
266
300
|
}
|
package/src/local-ai/onnx.mjs
CHANGED
|
@@ -216,7 +216,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
|
|
|
216
216
|
operationSignal.throwIfAborted();
|
|
217
217
|
if (entry.stopping || entry.exited || entry.active !== operation) return;
|
|
218
218
|
operation.payload = {...operation.payload, targetResolution, deviceInventory};
|
|
219
|
-
entry.state.execution = {...entry.state.execution, ...targetResolution};
|
|
219
|
+
entry.state.execution = {...entry.state.execution, ...targetResolution, deviceInventory: deviceInventory ?? null};
|
|
220
220
|
publish(entry);
|
|
221
221
|
postOperation(entry, operation);
|
|
222
222
|
}
|
|
@@ -348,7 +348,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
|
|
|
348
348
|
state:{id,model,state:'loading',loaded:false,error:null,inputNames:[],outputNames:[],inputMetadata:[],outputMetadata:[],
|
|
349
349
|
execution: {requestedTarget: executionTarget ?? null, resolvedDevice: null,
|
|
350
350
|
resolution: executionTarget === undefined || executionTarget === null ? 'automatic' : null,
|
|
351
|
-
reason: 'session-loading', configuredTarget: null, observedTarget: null}},
|
|
351
|
+
reason: 'session-loading', configuredTarget: null, observedTarget: null, deviceInventory: null}},
|
|
352
352
|
controller: new AbortController(), preparation: null,
|
|
353
353
|
worker:null,active:null,queue:[],stopping:false,exited:false,stopTask:null,terminalError:null,releaseError:null,terminationRequested:false,outputTasks:[]
|
|
354
354
|
};
|
|
@@ -356,7 +356,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
|
|
|
356
356
|
sessions.set(id,entry);
|
|
357
357
|
try{
|
|
358
358
|
entry.worker=new Worker(new URL('./onnx-worker.mjs',import.meta.url),{
|
|
359
|
-
workerData:{modulePath},stdout:true,stderr:true
|
|
359
|
+
workerData:{modulePath,sessionId:id,model},stdout:true,stderr:true
|
|
360
360
|
});
|
|
361
361
|
entry.outputTasks.push(
|
|
362
362
|
captureOutput(entry,entry.worker.stdout,'local-ai.onnx.stdout'),
|
|
@@ -203,7 +203,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
203
203
|
"import path from 'node:path';",
|
|
204
204
|
"import {isSea} from 'node:sea';",
|
|
205
205
|
...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
|
|
206
|
-
"import {readCoreLaunchContext, startCoreHost, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
|
|
206
|
+
"import {readCoreLaunchContext, startCoreHost, startCoreListener, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
|
|
207
207
|
'',
|
|
208
208
|
...(localAI === undefined ? [] : [
|
|
209
209
|
'// Bundled runtime paths are artifact-root-relative POSIX paths.',
|
|
@@ -249,6 +249,10 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
249
249
|
: ` ...await readCoreLaunchContext({appId: ${JSON.stringify(application.id)}, defaults: JSON.parse(${JSON.stringify(JSON.stringify(launchDefaults))})})`,
|
|
250
250
|
'};',
|
|
251
251
|
'',
|
|
252
|
+
'if (context.coreListener !== undefined && context.sharedHost !== undefined) {',
|
|
253
|
+
" throw new TypeError('coreListener attaches to the window-owned Core; sharedHost selects its separate owning host.');",
|
|
254
|
+
'}',
|
|
255
|
+
'',
|
|
252
256
|
...(launchDefaults === undefined ? [] : [
|
|
253
257
|
'// The shared endpoint and diagnostic log require their selected state directory.',
|
|
254
258
|
'if (context.sharedHost !== undefined) await mkdir(context.stateRoot, {recursive: true});',
|
|
@@ -284,10 +288,29 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
284
288
|
`const application = JSON.parse(${JSON.stringify(JSON.stringify(application))});`,
|
|
285
289
|
`const version = ${JSON.stringify(version)};`,
|
|
286
290
|
'let host;',
|
|
291
|
+
'let listener;',
|
|
287
292
|
'if (context.sharedHost === undefined) {',
|
|
288
293
|
' const services = [];',
|
|
289
294
|
' await createServices(function register(service) { services.push(service); });',
|
|
290
295
|
' host = startCoreHost({application, version, services});',
|
|
296
|
+
' if (context.coreListener !== undefined) {',
|
|
297
|
+
" process.on('SIGINT', closeCore);",
|
|
298
|
+
" process.on('SIGTERM', closeCore);",
|
|
299
|
+
' try {',
|
|
300
|
+
' listener = await startCoreListener({runtime: host.runtime, endpoint: context.coreListener?.endpoint});',
|
|
301
|
+
' listener.closed.catch(function listenerFailed() { process.exitCode = 1; });',
|
|
302
|
+
' } catch (error) {',
|
|
303
|
+
' try {',
|
|
304
|
+
' await host.close();',
|
|
305
|
+
' } catch (closeError) {',
|
|
306
|
+
" throw new AggregateError([error, closeError], 'Core listener startup and host drain failed.');",
|
|
307
|
+
' }',
|
|
308
|
+
' throw error;',
|
|
309
|
+
' } finally {',
|
|
310
|
+
" process.off('SIGINT', closeCore);",
|
|
311
|
+
" process.off('SIGTERM', closeCore);",
|
|
312
|
+
' }',
|
|
313
|
+
' }',
|
|
291
314
|
"} else if (process.argv.includes('--arcane-core-headless')) {",
|
|
292
315
|
' host = await runSharedCoreHost({',
|
|
293
316
|
' endpoint: context.sharedHost.endpoint, application, version,',
|
|
@@ -319,6 +342,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
319
342
|
" console.error('Arcane Core host failed:', error);",
|
|
320
343
|
' process.exitCode = 1;',
|
|
321
344
|
'} finally {',
|
|
345
|
+
' try {',
|
|
346
|
+
' await listener?.close();',
|
|
347
|
+
' } catch {',
|
|
348
|
+
' // The listener reports its complete failure through onError.',
|
|
349
|
+
' process.exitCode = 1;',
|
|
350
|
+
' }',
|
|
322
351
|
" process.off('SIGINT', closeCore);",
|
|
323
352
|
" process.off('SIGTERM', closeCore);",
|
|
324
353
|
'}',
|