arcane-os 0.65.0 → 0.67.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 +47 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +3 -2
- package/browser-runtime/ai/core-image.mjs +84 -34
- package/browser-runtime/ai/core-local.mjs +195 -43
- package/browser-runtime/ai/core-onnx.mjs +71 -12
- package/browser-runtime/ai/decision-runtime.mjs +14 -24
- package/docs/publishing.md +39 -0
- package/docs/reference/README.md +8 -1
- package/docs/reference/ai/browser-decisions.md +7 -4
- package/docs/reference/core-native-packaging.md +11 -4
- package/docs/reference/core-repositories.md +214 -11
- package/docs/reference/core-runtime.md +32 -5
- package/docs/reference/git-text-snapshot.md +15 -1
- package/docs/reference/inventory/package-api.json +41 -13
- package/docs/reference/inventory/runtime-modules.json +5 -4
- package/docs/reference/local-ai.md +119 -2
- package/docs/reference/local-image-generation.md +15 -4
- package/docs/reference/local-whisper.md +7 -1
- package/docs/reference/mcp-stdio.md +1 -1
- package/docs/reference/model-assets.md +50 -3
- package/docs/reference/native-decisions.md +181 -0
- package/docs/reference/native-speech.md +6 -0
- package/docs/reference/sdk-api.md +151 -16
- package/package.json +5 -2
- package/src/core/repositories.mjs +37 -14
- package/src/core/runtime.mjs +11 -0
- package/src/core/services/decisions.mjs +144 -0
- package/src/core/services/local-ai.mjs +31 -9
- package/src/core/services/model-assets.mjs +98 -13
- package/src/git-identity.mjs +96 -0
- package/src/git-text-snapshot.mjs +5 -3
- package/src/local-ai/decision-tokenizer-worker.mjs +31 -0
- package/src/local-ai/decision-tokenizer.mjs +109 -0
- package/src/local-ai/decisions.mjs +273 -0
- package/src/local-ai/dev.mjs +8 -2
- package/src/local-ai/onnx-worker.mjs +67 -7
- package/src/local-ai/onnx.mjs +23 -12
- package/src/native/portable-layout.mjs +8 -3
- package/src/repository.mjs +144 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import {subscribeCoreClient} from '../core/client.mjs';
|
|
3
3
|
|
|
4
4
|
const is = new Is(false);
|
|
5
5
|
const PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
|
|
@@ -22,19 +22,26 @@ function cancelled(reason) {
|
|
|
22
22
|
* selects its model; Core owns the running engine and its loaded sessions.
|
|
23
23
|
* Creating this provider neither connects a transport nor installs a runtime.
|
|
24
24
|
*/
|
|
25
|
-
export function createCoreLocalAIProvider({client
|
|
25
|
+
export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.cpp'} = {}) {
|
|
26
26
|
if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
|
|
27
|
-
if (
|
|
27
|
+
if (suppliedClient !== undefined && suppliedClient !== null
|
|
28
|
+
&& (!is.function(suppliedClient?.invoke) || !is.function(suppliedClient?.events?.on))) {
|
|
28
29
|
throw new TypeError('The Core AI client must expose invoke and events.on.');
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
let runtime = null;
|
|
32
33
|
let models = [];
|
|
33
34
|
let selection = null;
|
|
35
|
+
let selectionConnection = null;
|
|
34
36
|
let disposed = false;
|
|
35
37
|
let loading = false;
|
|
38
|
+
let unloading = false;
|
|
36
39
|
let sequence = 0;
|
|
37
40
|
let lifecycleRevision = 0;
|
|
41
|
+
let client = null;
|
|
42
|
+
let connection = null;
|
|
43
|
+
let stopInstallation;
|
|
44
|
+
let disposing;
|
|
38
45
|
const requests = new Set();
|
|
39
46
|
|
|
40
47
|
function requireClient() {
|
|
@@ -58,7 +65,13 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
function modelReady() {
|
|
61
|
-
return
|
|
68
|
+
return !unloading && selectionConnection === connection && runtime?.available === true
|
|
69
|
+
&& runtime.state === 'ready' && Boolean(loadedModel());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertCurrentConnection(value) {
|
|
73
|
+
if (disposed || connection !== value) throw cancelled();
|
|
74
|
+
value.controller.signal.throwIfAborted();
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
function status() {
|
|
@@ -67,7 +80,9 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
67
80
|
state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
|
|
68
81
|
: runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
|
|
69
82
|
loaded,
|
|
70
|
-
busy: loading || requests.
|
|
83
|
+
busy: Boolean(loading || unloading) || [...requests].some(function currentRequest(request) {
|
|
84
|
+
return request.connection === connection && !request.controller.signal.aborted;
|
|
85
|
+
}),
|
|
71
86
|
modelId: selection?.modelId ?? null,
|
|
72
87
|
available: !disposed && runtime?.available === true,
|
|
73
88
|
installed: runtime?.installed === true,
|
|
@@ -79,6 +94,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
79
94
|
runtime = value ?? null;
|
|
80
95
|
models = is.array(runtime?.models) ? runtime.models : [];
|
|
81
96
|
for (const request of requests) {
|
|
97
|
+
if (request.connection !== connection || request.modelId === null) continue;
|
|
82
98
|
const model = models.find(function requestModel(record) { return record.id === request.modelId; });
|
|
83
99
|
if (model?.loaded === true) request.loaded = true;
|
|
84
100
|
if (runtime?.available !== true || (request.loaded && (runtime.state !== 'ready' || model?.loaded !== true))) {
|
|
@@ -88,13 +104,62 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
88
104
|
return status();
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
if (disposed ||
|
|
107
|
+
function bindClient({client: nextClient, error = null}) {
|
|
108
|
+
if (disposed || (connection && nextClient === client)) return;
|
|
109
|
+
const previous = connection;
|
|
110
|
+
client = nextClient;
|
|
111
|
+
const currentConnection = {client, controller: new AbortController(), unsubscribe: null};
|
|
112
|
+
connection = currentConnection;
|
|
93
113
|
lifecycleRevision += 1;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
114
|
+
loading = false;
|
|
115
|
+
unloading = false;
|
|
116
|
+
runtime = {installed: false, available: false, state: client ? 'unknown' : 'unavailable', error};
|
|
117
|
+
models = [];
|
|
118
|
+
previous?.unsubscribe?.();
|
|
119
|
+
previous?.controller.abort();
|
|
120
|
+
if (client) {
|
|
121
|
+
currentConnection.unsubscribe = client.events.on(
|
|
122
|
+
'localai.state',
|
|
123
|
+
function localRuntimeChanged(snapshot) {
|
|
124
|
+
if (disposed || connection !== currentConnection || !is.array(snapshot?.runtimes)) return;
|
|
125
|
+
lifecycleRevision += 1;
|
|
126
|
+
acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
|
|
127
|
+
return value.id === 'llama.cpp';
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (client && suppliedClient === undefined) {
|
|
133
|
+
const revision = lifecycleRevision;
|
|
134
|
+
invokeOwned(client, 'llama.status', {}).then(
|
|
135
|
+
function installedRuntimeObserved(value) {
|
|
136
|
+
if (!disposed && connection === currentConnection && lifecycleRevision === revision) acceptRuntime(value);
|
|
137
|
+
},
|
|
138
|
+
function installedRuntimeFailed(error) {
|
|
139
|
+
if (!disposed && connection === currentConnection && lifecycleRevision === revision) {
|
|
140
|
+
acceptRuntime({...runtime, available: false, state: 'error', error});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function invokeOwned(core, method, parameters, signal) {
|
|
148
|
+
const request = ownRequest(signal, null, core);
|
|
149
|
+
request.result = Promise.resolve().then(
|
|
150
|
+
function invokeLocalOperation() {
|
|
151
|
+
request.controller.signal.throwIfAborted();
|
|
152
|
+
return core.invoke(method, parameters, {signal: request.controller.signal});
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
try {
|
|
156
|
+
const result = await request.result;
|
|
157
|
+
request.controller.signal.throwIfAborted();
|
|
158
|
+
return result;
|
|
159
|
+
} finally {
|
|
160
|
+
request.release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
98
163
|
|
|
99
164
|
function authority(value) {
|
|
100
165
|
return {protocol: AUTHORITY_PROTOCOL, providerId: id, modelId: value.modelId};
|
|
@@ -103,12 +168,20 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
103
168
|
async function inspect(value, {signal} = {}) {
|
|
104
169
|
requireSelection(value);
|
|
105
170
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
171
|
+
const currentConnection = connection;
|
|
172
|
+
const statusRevision = lifecycleRevision;
|
|
106
173
|
try {
|
|
107
174
|
const core = requireClient();
|
|
108
|
-
const
|
|
109
|
-
|
|
175
|
+
const current = await invokeOwned(core, 'llama.status', {}, signal);
|
|
176
|
+
assertCurrentConnection(currentConnection);
|
|
110
177
|
if (lifecycleRevision === statusRevision) acceptRuntime(current);
|
|
111
|
-
|
|
178
|
+
const knownModel = models.some(function matchesModel(model) { return model.id === value.modelId; });
|
|
179
|
+
// Inspection describes an explicit load capability, never readiness.
|
|
180
|
+
// Core still verifies the exact model at the actual load boundary.
|
|
181
|
+
const starting = !runtime?.error && ['starting', 'loading'].includes(runtime?.state);
|
|
182
|
+
const released = !runtime?.error && runtime?.released === true && runtime.owned === true
|
|
183
|
+
&& runtime.state === 'stopped' && knownModel;
|
|
184
|
+
if (runtime?.error || (runtime?.available !== true && !starting && !released)) {
|
|
112
185
|
return {
|
|
113
186
|
available: false,
|
|
114
187
|
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
@@ -116,7 +189,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
116
189
|
error: runtime?.error ?? null
|
|
117
190
|
};
|
|
118
191
|
}
|
|
119
|
-
if (!
|
|
192
|
+
if (!starting && !knownModel) {
|
|
120
193
|
return {
|
|
121
194
|
available: false,
|
|
122
195
|
code: 'ARCANE_AI_MODEL_UNAVAILABLE',
|
|
@@ -126,48 +199,63 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
126
199
|
return {available: true, authority: authority(value)};
|
|
127
200
|
} catch (error) {
|
|
128
201
|
if (signal?.aborted || error?.name === 'AbortError') throw error;
|
|
129
|
-
|
|
202
|
+
if (!disposed && connection === currentConnection && lifecycleRevision === statusRevision) {
|
|
203
|
+
acceptRuntime({...runtime, available: false, state: 'error', error});
|
|
204
|
+
}
|
|
130
205
|
return {available: false, code: error.code ?? 'ARCANE_AI_CORE_UNAVAILABLE', message: error.message, error};
|
|
131
206
|
}
|
|
132
207
|
}
|
|
133
208
|
|
|
134
209
|
async function load({selection: value, signal, progress} = {}) {
|
|
135
210
|
const core = requireClient();
|
|
211
|
+
const currentConnection = connection;
|
|
136
212
|
requireSelection(value);
|
|
137
213
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
138
214
|
selection = value;
|
|
139
|
-
|
|
215
|
+
selectionConnection = currentConnection;
|
|
216
|
+
const operation = {};
|
|
217
|
+
loading = operation;
|
|
218
|
+
unloading = false;
|
|
219
|
+
lifecycleRevision += 1;
|
|
140
220
|
try {
|
|
141
221
|
progress?.({phase: 'loading', modelId: value.modelId});
|
|
142
222
|
const revision = lifecycleRevision;
|
|
143
|
-
const current = await core
|
|
223
|
+
const current = await invokeOwned(core, 'llama.load', {model: value.modelId}, signal);
|
|
224
|
+
assertCurrentConnection(currentConnection);
|
|
225
|
+
if (loading !== operation || selection !== value) throw cancelled();
|
|
144
226
|
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
145
227
|
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
146
228
|
progress?.({phase: 'ready', modelId: value.modelId});
|
|
229
|
+
assertCurrentConnection(currentConnection);
|
|
230
|
+
if (loading !== operation || selection !== value) throw cancelled();
|
|
147
231
|
return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
|
|
148
232
|
} finally {
|
|
149
|
-
loading = false;
|
|
233
|
+
if (loading === operation) loading = false;
|
|
150
234
|
}
|
|
151
235
|
}
|
|
152
236
|
|
|
153
|
-
function ownRequest(signal, modelId) {
|
|
237
|
+
function ownRequest(signal, modelId, core = requireClient()) {
|
|
238
|
+
const currentConnection = connection;
|
|
239
|
+
if (core !== currentConnection?.client) throw cancelled();
|
|
154
240
|
const controller = new AbortController();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
241
|
+
const operationSignal = signal
|
|
242
|
+
? AbortSignal.any([signal, currentConnection.controller.signal]) : currentConnection.controller.signal;
|
|
243
|
+
function forwardAbort() { controller.abort(operationSignal.reason); }
|
|
244
|
+
operationSignal.addEventListener('abort', forwardAbort, {once: true});
|
|
245
|
+
if (operationSignal.aborted) forwardAbort();
|
|
246
|
+
const request = {controller, connection: currentConnection, result: null, modelId, loaded: models.some(function loadedTarget(model) {
|
|
159
247
|
return model.id === modelId && model.loaded === true;
|
|
160
248
|
})};
|
|
161
249
|
requests.add(request);
|
|
162
250
|
request.release = function releaseRequest() {
|
|
163
|
-
|
|
251
|
+
operationSignal.removeEventListener('abort', forwardAbort);
|
|
164
252
|
requests.delete(request);
|
|
165
253
|
};
|
|
166
254
|
return request;
|
|
167
255
|
}
|
|
168
256
|
|
|
169
257
|
function streamRequest(core, parameters, signal) {
|
|
170
|
-
const request = ownRequest(signal, parameters.model);
|
|
258
|
+
const request = ownRequest(signal, parameters.model, core);
|
|
171
259
|
const chunks = [];
|
|
172
260
|
const readers = [];
|
|
173
261
|
let complete = false;
|
|
@@ -195,6 +283,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
195
283
|
});
|
|
196
284
|
|
|
197
285
|
request.result = Promise.resolve().then(function invokeStream() {
|
|
286
|
+
request.controller.signal.throwIfAborted();
|
|
198
287
|
return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
|
|
199
288
|
}).then(function streamCompleted(result) {
|
|
200
289
|
if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
|
|
@@ -237,15 +326,17 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
237
326
|
|
|
238
327
|
async function request({selection: value, operation, payload, signal} = {}) {
|
|
239
328
|
const core = requireClient();
|
|
329
|
+
const currentConnection = connection;
|
|
240
330
|
requireSelection(value);
|
|
241
331
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
242
|
-
if (loading || selection?.modelId !== value.modelId) {
|
|
332
|
+
if (loading || unloading || selectionConnection !== connection || selection?.modelId !== value.modelId) {
|
|
243
333
|
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
|
|
244
334
|
}
|
|
245
335
|
const model = payload?.model ?? value.modelId;
|
|
246
336
|
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
247
337
|
const revision = lifecycleRevision;
|
|
248
|
-
const current = await core
|
|
338
|
+
const current = await invokeOwned(core, 'llama.status', {}, signal);
|
|
339
|
+
assertCurrentConnection(currentConnection);
|
|
249
340
|
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
250
341
|
}
|
|
251
342
|
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
@@ -258,8 +349,9 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
258
349
|
const parameters = {model, payload};
|
|
259
350
|
if (operation === 'stream') return streamRequest(core, parameters, signal);
|
|
260
351
|
if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
|
|
261
|
-
const pending = ownRequest(signal, model);
|
|
352
|
+
const pending = ownRequest(signal, model, core);
|
|
262
353
|
pending.result = Promise.resolve().then(function invokeChat() {
|
|
354
|
+
pending.controller.signal.throwIfAborted();
|
|
263
355
|
return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
|
|
264
356
|
}).then(function chatCompleted(result) {
|
|
265
357
|
if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
|
|
@@ -268,26 +360,86 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
|
|
|
268
360
|
return pending.result;
|
|
269
361
|
}
|
|
270
362
|
|
|
271
|
-
async function unload({
|
|
363
|
+
async function unload({signal} = {}) {
|
|
364
|
+
const value = selection;
|
|
272
365
|
const core = requireClient();
|
|
366
|
+
const currentConnection = connection;
|
|
273
367
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
368
|
+
if (value && value === selection && selectionConnection !== currentConnection) {
|
|
369
|
+
throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'The selected model belongs to a retired Core connection.');
|
|
370
|
+
}
|
|
371
|
+
if (value) requireSelection(value);
|
|
372
|
+
const operation = {};
|
|
373
|
+
unloading = operation;
|
|
374
|
+
loading = false;
|
|
375
|
+
lifecycleRevision += 1;
|
|
274
376
|
for (const request of requests) request.controller.abort();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
377
|
+
try {
|
|
378
|
+
await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
|
|
379
|
+
assertCurrentConnection(currentConnection);
|
|
380
|
+
if (unloading !== operation) throw cancelled();
|
|
381
|
+
if (value) {
|
|
382
|
+
const revision = lifecycleRevision;
|
|
383
|
+
const current = await invokeOwned(core, 'llama.unload', {model: value.modelId}, signal);
|
|
384
|
+
assertCurrentConnection(currentConnection);
|
|
385
|
+
if (unloading !== operation) throw cancelled();
|
|
386
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
387
|
+
}
|
|
388
|
+
if (unloading !== operation) throw cancelled();
|
|
389
|
+
selection = null;
|
|
390
|
+
selectionConnection = null;
|
|
391
|
+
unloading = false;
|
|
392
|
+
return status();
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (!disposed && connection === currentConnection && unloading === operation) {
|
|
395
|
+
runtime = {...runtime, state: 'error', error};
|
|
396
|
+
}
|
|
397
|
+
throw error;
|
|
398
|
+
} finally {
|
|
399
|
+
if (unloading === operation) unloading = false;
|
|
400
|
+
}
|
|
283
401
|
}
|
|
284
402
|
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
403
|
+
function dispose({signal} = {}) {
|
|
404
|
+
if (disposing) return disposing;
|
|
405
|
+
// The runtime also supplies its configured selection after unload.
|
|
406
|
+
// Only this provider's retained selection owns a native release.
|
|
407
|
+
const value = selection;
|
|
408
|
+
const core = selectionConnection?.client;
|
|
288
409
|
disposed = true;
|
|
289
|
-
|
|
290
|
-
|
|
410
|
+
loading = false;
|
|
411
|
+
unloading = false;
|
|
412
|
+
lifecycleRevision += 1;
|
|
413
|
+
stopInstallation?.();
|
|
414
|
+
connection?.unsubscribe?.();
|
|
415
|
+
connection?.controller.abort();
|
|
416
|
+
disposing = Promise.resolve().then(
|
|
417
|
+
async function disposeLocalProvider() {
|
|
418
|
+
await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
|
|
419
|
+
if (value) {
|
|
420
|
+
requireSelection(value);
|
|
421
|
+
if (!core) throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'The retired Core connection cannot unload the selected model.');
|
|
422
|
+
// Disposal owns this final release on the captured client only.
|
|
423
|
+
// Subscription cleanup above remains complete if it rejects.
|
|
424
|
+
await core.invoke('llama.unload', {model: value.modelId}, {signal});
|
|
425
|
+
}
|
|
426
|
+
selection = null;
|
|
427
|
+
selectionConnection = null;
|
|
428
|
+
return status();
|
|
429
|
+
}
|
|
430
|
+
).catch(
|
|
431
|
+
function nativeReleaseFailed(error) {
|
|
432
|
+
runtime = {...runtime, error};
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
return disposing;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (suppliedClient === undefined) {
|
|
440
|
+
stopInstallation = subscribeCoreClient(bindClient);
|
|
441
|
+
} else {
|
|
442
|
+
bindClient({client: suppliedClient});
|
|
291
443
|
}
|
|
292
444
|
|
|
293
445
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {subscribeCoreClient} from '../core/client.mjs';
|
|
2
2
|
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
3
3
|
import {encodeTensorMap, decodeTensorMap, encodeTensorFetches} from './onnx-tensors.mjs';
|
|
4
4
|
|
|
@@ -9,16 +9,26 @@ function unavailable(message) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** Optional system ONNX access. Construction neither connects nor installs. */
|
|
12
|
-
export function createCoreONNXRuntime({client
|
|
12
|
+
export function createCoreONNXRuntime({client: suppliedClient} = {}) {
|
|
13
13
|
const owner = {};
|
|
14
14
|
const events = createArcaneEventSource(owner, {source: 'core-onnx', eventTypes: ['onnx.state']});
|
|
15
15
|
const lifetime = new AbortController();
|
|
16
16
|
const pending = new Set();
|
|
17
|
+
let client = null;
|
|
18
|
+
let connection = null;
|
|
19
|
+
let stopInstallation;
|
|
17
20
|
let state = {id: 'onnx', installed: false, available: false, state: 'unknown', models: []};
|
|
18
21
|
let revision = 0;
|
|
19
22
|
let closed = false;
|
|
20
23
|
|
|
21
|
-
function current() {
|
|
24
|
+
function current() {
|
|
25
|
+
const busy = [...pending].some(
|
|
26
|
+
function currentRequest(request) {
|
|
27
|
+
return request.connection === connection && !request.signal.aborted;
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
return {...state, busy, closed};
|
|
31
|
+
}
|
|
22
32
|
function publish() { events.dispatch('onnx.state', current()); }
|
|
23
33
|
function accept(value) {
|
|
24
34
|
state = value ?? {id: 'onnx', installed: false, available: false, state: 'unavailable', models: []};
|
|
@@ -26,30 +36,66 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
|
|
|
26
36
|
publish();
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
39
|
+
function bindClient({client: nextClient, error = null}) {
|
|
40
|
+
if (closed || (connection && nextClient === client)) return;
|
|
41
|
+
const previous = connection;
|
|
42
|
+
client = nextClient;
|
|
43
|
+
const currentConnection = {client, controller: new AbortController(), unsubscribe: null};
|
|
44
|
+
connection = currentConnection;
|
|
45
|
+
revision += 1;
|
|
46
|
+
state = {id: 'onnx', installed: false, available: false, state: client ? 'unknown' : 'unavailable', models: [], error};
|
|
47
|
+
previous?.unsubscribe?.();
|
|
48
|
+
previous?.controller.abort();
|
|
49
|
+
if (client) {
|
|
50
|
+
currentConnection.unsubscribe = client.events.on(
|
|
51
|
+
'localai.state',
|
|
52
|
+
function localStateChanged(snapshot) {
|
|
53
|
+
if (!closed && connection === currentConnection) {
|
|
54
|
+
accept(snapshot?.runtimes?.find(function onnxRuntime(runtime) { return runtime.id === 'onnx'; }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
publish();
|
|
60
|
+
if (!closed && connection === currentConnection && client && suppliedClient === undefined) {
|
|
61
|
+
inspect().catch(
|
|
62
|
+
function initialONNXStateUnavailable(error) {
|
|
63
|
+
if (!closed && connection === currentConnection && !currentConnection.controller.signal.aborted) {
|
|
64
|
+
globalThis.console?.error('Core ONNX initial state unavailable.', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
32
70
|
|
|
33
71
|
async function invoke(method, parameters, {signal, timeoutMs = 0} = {}) {
|
|
34
72
|
if (closed) throw unavailable('The Core ONNX accessor is closed.');
|
|
35
73
|
if (!client) throw unavailable('Local ONNX requires an available Core connection.');
|
|
36
|
-
const
|
|
37
|
-
const operationSignal =
|
|
74
|
+
const currentConnection = connection;
|
|
75
|
+
const operationSignal = AbortSignal.any(
|
|
76
|
+
[lifetime.signal, currentConnection.controller.signal, ...(signal ? [signal] : [])]
|
|
77
|
+
);
|
|
78
|
+
const request = {connection: currentConnection, signal: operationSignal};
|
|
38
79
|
operationSignal.throwIfAborted();
|
|
39
80
|
pending.add(request);
|
|
40
81
|
publish();
|
|
41
82
|
try {
|
|
42
|
-
|
|
83
|
+
operationSignal.throwIfAborted();
|
|
84
|
+
const result = await currentConnection.client.invoke(method, parameters, {signal: operationSignal, timeoutMs});
|
|
85
|
+
operationSignal.throwIfAborted();
|
|
86
|
+
return result;
|
|
43
87
|
} finally {
|
|
44
88
|
pending.delete(request);
|
|
45
|
-
if (!closed) publish();
|
|
89
|
+
if (!closed && connection === currentConnection) publish();
|
|
46
90
|
}
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
async function inspect(options) {
|
|
50
94
|
const before = revision;
|
|
95
|
+
const currentConnection = connection;
|
|
51
96
|
try {
|
|
52
97
|
const result = await invoke('onnx.status', {}, options);
|
|
98
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
53
99
|
if (!closed && revision === before) accept(result);
|
|
54
100
|
return current();
|
|
55
101
|
} catch (error) {
|
|
@@ -61,14 +107,19 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
|
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
async function load({id, model, sessionOptions, signal, timeoutMs} = {}) {
|
|
64
|
-
|
|
110
|
+
const currentConnection = connection;
|
|
111
|
+
const result = await invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
|
|
112
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
113
|
+
return result;
|
|
65
114
|
}
|
|
66
115
|
|
|
67
116
|
async function run({id, feeds, fetches, runOptions, signal, timeoutMs} = {}) {
|
|
117
|
+
const currentConnection = connection;
|
|
68
118
|
const result = await invoke('onnx.run', {
|
|
69
119
|
id, feeds: encodeTensorMap(feeds),
|
|
70
120
|
fetches: encodeTensorFetches(fetches), runOptions
|
|
71
121
|
}, {signal, timeoutMs});
|
|
122
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
72
123
|
return decodeTensorMap(result);
|
|
73
124
|
}
|
|
74
125
|
|
|
@@ -86,11 +137,19 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
|
|
|
86
137
|
if (closed) return;
|
|
87
138
|
closed = true;
|
|
88
139
|
lifetime.abort();
|
|
89
|
-
|
|
140
|
+
stopInstallation?.();
|
|
141
|
+
connection?.unsubscribe?.();
|
|
142
|
+
connection?.controller.abort();
|
|
90
143
|
state = {...state, available: false, state: 'closed'};
|
|
91
144
|
publish();
|
|
92
145
|
events.dispose();
|
|
93
146
|
}
|
|
94
147
|
|
|
148
|
+
if (suppliedClient === undefined) {
|
|
149
|
+
stopInstallation = subscribeCoreClient(bindClient, {signal: lifetime.signal});
|
|
150
|
+
} else {
|
|
151
|
+
bindClient({client: suppliedClient});
|
|
152
|
+
}
|
|
153
|
+
|
|
95
154
|
return {load, run, unload, inspect, current, subscribe, close};
|
|
96
155
|
}
|
|
@@ -82,7 +82,8 @@ export function encodeDecisionRows(tokenizer, rows, family) {
|
|
|
82
82
|
return {encoded, pad};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
/** Plain tensor records shared by browser and native ONNX execution owners. */
|
|
86
|
+
export function createDecisionInputs(encoded, pad) {
|
|
86
87
|
let sequenceLength = 0;
|
|
87
88
|
let optionCount = 0;
|
|
88
89
|
for (const row of encoded) {
|
|
@@ -108,26 +109,11 @@ function decisionInputs(Tensor, encoded, pad) {
|
|
|
108
109
|
types[rowIndex] = BigInt(row.qtype);
|
|
109
110
|
}
|
|
110
111
|
return {
|
|
111
|
-
input_ids:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'int64', attention,
|
|
117
|
-
[batch, sequenceLength]
|
|
118
|
-
),
|
|
119
|
-
marker_pos: new Tensor(
|
|
120
|
-
'int64', positions,
|
|
121
|
-
[batch, optionCount]
|
|
122
|
-
),
|
|
123
|
-
marker_mask: new Tensor(
|
|
124
|
-
'bool', mask,
|
|
125
|
-
[batch, optionCount]
|
|
126
|
-
),
|
|
127
|
-
qtype: new Tensor(
|
|
128
|
-
'int64', types,
|
|
129
|
-
[batch]
|
|
130
|
-
)
|
|
112
|
+
input_ids: {type: 'int64', data: ids, dims: [batch, sequenceLength]},
|
|
113
|
+
attention_mask: {type: 'int64', data: attention, dims: [batch, sequenceLength]},
|
|
114
|
+
marker_pos: {type: 'int64', data: positions, dims: [batch, optionCount]},
|
|
115
|
+
marker_mask: {type: 'bool', data: mask, dims: [batch, optionCount]},
|
|
116
|
+
qtype: {type: 'int64', data: types, dims: [batch]}
|
|
131
117
|
};
|
|
132
118
|
}
|
|
133
119
|
|
|
@@ -210,7 +196,8 @@ export function decodeDecisionOutputs(rows, outputs) {
|
|
|
210
196
|
}
|
|
211
197
|
|
|
212
198
|
export async function loadDecisionRuntime(configuration, report) {
|
|
213
|
-
const {family, model: repository, revision = 'main', device = 'webgpu',
|
|
199
|
+
const {family, model: repository, revision = 'main', device = 'webgpu',
|
|
200
|
+
dtype = family === 'laya' ? 'fp16' : 'fp32', runtime} = configuration;
|
|
214
201
|
report(
|
|
215
202
|
{phase: 'loading-runtime'}
|
|
216
203
|
);
|
|
@@ -219,7 +206,7 @@ export async function loadDecisionRuntime(configuration, report) {
|
|
|
219
206
|
namespace.env.backends.onnx.wasm.wasmPaths = runtime.wasmPaths;
|
|
220
207
|
}
|
|
221
208
|
const {AutoTokenizer, AutoModel, PreTrainedModel, Tensor} = namespace;
|
|
222
|
-
const options = {revision, device, dtype
|
|
209
|
+
const options = {revision, device, dtype};
|
|
223
210
|
// No progress_callback: upstream 4.3 otherwise performs file-size
|
|
224
211
|
// aggregation. This owner reports actual semantic lifecycle transitions.
|
|
225
212
|
report(
|
|
@@ -261,7 +248,10 @@ export async function loadDecisionRuntime(configuration, report) {
|
|
|
261
248
|
async function evaluate(rows) {
|
|
262
249
|
const {encoded, pad} = encodeDecisionRows(tokenizer, rows, family);
|
|
263
250
|
if (encoded.length === 0) return {decisions: [], outputs: {}};
|
|
264
|
-
const inputs =
|
|
251
|
+
const inputs = {};
|
|
252
|
+
for (const [name, record] of Object.entries(createDecisionInputs(encoded, pad))) {
|
|
253
|
+
inputs[name] = new Tensor(record.type, record.data, record.dims);
|
|
254
|
+
}
|
|
265
255
|
let output;
|
|
266
256
|
try {
|
|
267
257
|
output = await model(inputs);
|
package/docs/publishing.md
CHANGED
|
@@ -116,6 +116,45 @@ tag and GitHub release title must both be the same bare numeric
|
|
|
116
116
|
`MAJOR.MINOR.PATCH`. Prerelease versions do not get a misleading numeric
|
|
117
117
|
GitHub release, and no release creates a Git branch for an npm dist-tag.
|
|
118
118
|
|
|
119
|
+
## Native runtime assets on numeric releases
|
|
120
|
+
|
|
121
|
+
Default SDK-hosted native asset URLs use the installed SDK's version and its
|
|
122
|
+
matching numeric GitHub release. Publishing npm alone does not make those
|
|
123
|
+
downloads available. Every applicable numeric release therefore includes its
|
|
124
|
+
compatible retained SDK-hosted assets as release follow-through:
|
|
125
|
+
|
|
126
|
+
| Selected capability | Windows x64 release asset |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| Native application host | `arcane-native-windows-x64.tar.gz` |
|
|
129
|
+
| Ordinary native Whisper helper | `arcane-whisper-windows-x64.tar.gz` |
|
|
130
|
+
| Optional Intel NPU Whisper encoder | `arcane-whisper-openvino-windows-x64.tar.gz` |
|
|
131
|
+
|
|
132
|
+
Attach each asset used by the selected package's default SDK-hosted download
|
|
133
|
+
paths, including later releases whose native implementation is unchanged.
|
|
134
|
+
Include the optional NPU asset when that release supplies `encoder:
|
|
135
|
+
"openvino-npu"`; applications still opt in, and the ordinary Whisper asset
|
|
136
|
+
remains available. Upstream runtime and model versions retain their separate
|
|
137
|
+
upstream distribution URLs; they are not renamed to the SDK version.
|
|
138
|
+
|
|
139
|
+
Reuse the producer's retained compatible archive and its recorded selected
|
|
140
|
+
evidence. Do not rebuild or redownload an unchanged archive for each release.
|
|
141
|
+
An actual native change belongs to its producer under the existing build and
|
|
142
|
+
dependency authority. Preserve the complete runtime closure and its packaging
|
|
143
|
+
notices together. Models continue through their upstream acquisition path
|
|
144
|
+
unless their redistribution is separately requested.
|
|
145
|
+
|
|
146
|
+
The release owner assigns each upload, observes its outcome and records the
|
|
147
|
+
exact asset name and public release URL in the durable release handoff. If an
|
|
148
|
+
upload result is uncertain, inspect that release's existing asset before
|
|
149
|
+
retrying. Retain the exact pending operation, executing owner and last result
|
|
150
|
+
until delivery finishes; reassign idle delegated work rather than treating
|
|
151
|
+
delegation as completion.
|
|
152
|
+
|
|
153
|
+
Source delivery, selected package verification, npm publication, native asset
|
|
154
|
+
delivery, requester notices and documentation/site delivery are distinct.
|
|
155
|
+
Native archive follow-through does not gate npm publication or require a new
|
|
156
|
+
package build. Consumer adoption remains with each application owner.
|
|
157
|
+
|
|
119
158
|
## Documentation publication
|
|
120
159
|
|
|
121
160
|
After every successful npm release and its applicable requester notices, the
|