arcane-os 0.66.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 +23 -0
- package/README.md +1 -1
- 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/docs/reference/README.md +1 -0
- package/docs/reference/core-native-packaging.md +11 -4
- package/docs/reference/core-repositories.md +93 -9
- package/docs/reference/core-runtime.md +32 -5
- package/docs/reference/inventory/package-api.json +9 -9
- package/docs/reference/local-ai.md +112 -2
- package/docs/reference/local-image-generation.md +15 -4
- package/docs/reference/model-assets.md +50 -3
- package/docs/reference/native-decisions.md +181 -0
- package/docs/reference/sdk-api.md +55 -12
- package/package.json +5 -2
- package/src/core/repositories.mjs +27 -8
- 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 +22 -58
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.67.0
|
|
4
|
+
|
|
5
|
+
- Add native Laya FP32 typed decisions through `arcane-os/local-ai/decisions`
|
|
6
|
+
and `arcane-os/core/decisions`, using the existing native ONNX and model-assets
|
|
7
|
+
owners plus the approved `@huggingface/tokenizers@0.2.0` Worker dependency.
|
|
8
|
+
Explicit activation preserves complete rows, graph outputs, ordered results,
|
|
9
|
+
current-state replay, cancellation and actual worker-exit file lifetime.
|
|
10
|
+
- Compose selected native services through one Core owner, prepare complete
|
|
11
|
+
model files through the existing asset owner, and expose actual GPU-provider
|
|
12
|
+
attempts with CPU fallback. Accepted configuration does not prove GPU execution.
|
|
13
|
+
- Follow late Core client installation and retirement in default AI accessors.
|
|
14
|
+
Keep explicit clients fixed, model observation non-loading and disposal tied
|
|
15
|
+
to actual retained ownership. Preserve recoverable native inference errors
|
|
16
|
+
without retiring a healthy session or cancelling sibling work; terminal
|
|
17
|
+
stopping revokes readiness while resources remain retained until actual exit.
|
|
18
|
+
- Add repository `configuration()` observations and captured `target:{remote,ref}`
|
|
19
|
+
options for ordinary fast-forward pull, non-force push and write. Preserve
|
|
20
|
+
complete configuration values, payloads and failures without switching branches,
|
|
21
|
+
rewriting Git configuration or changing omitted-target behavior.
|
|
22
|
+
- Keep theme controls, shared same-app host discovery and optional NPU asset
|
|
23
|
+
transport follow-through separate. No app data migration or automatic model
|
|
24
|
+
activation is introduced; applications own their selections and orchestration.
|
|
25
|
+
|
|
3
26
|
## 0.66.0
|
|
4
27
|
|
|
5
28
|
- Add `readGitIdentity` from `arcane-os/core/repositories` for separate global,
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.67.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -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
|
import {CoreError, serializeCoreError} from '../core/contracts.mjs';
|
|
4
4
|
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
5
5
|
|
|
@@ -50,7 +50,7 @@ async function encodeImage(image, signal) {
|
|
|
50
50
|
|
|
51
51
|
/** Browser access to the selected native image service; no result persistence. */
|
|
52
52
|
export function createCoreImageRuntime(
|
|
53
|
-
{client
|
|
53
|
+
{client: suppliedClient, signal, onEvent} = {}
|
|
54
54
|
) {
|
|
55
55
|
const owner = {};
|
|
56
56
|
const events = createArcaneEventSource(
|
|
@@ -60,26 +60,37 @@ export function createCoreImageRuntime(
|
|
|
60
60
|
const lifetime = new AbortController();
|
|
61
61
|
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
62
62
|
const pending = new Set();
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
selectedModel: null,
|
|
68
|
-
loaded: false,
|
|
69
|
-
state: client ? 'unknown' : 'unavailable',
|
|
70
|
-
backend: null,
|
|
71
|
-
devices: [],
|
|
72
|
-
resources: {},
|
|
73
|
-
progress: null,
|
|
74
|
-
error: null,
|
|
75
|
-
models: []
|
|
76
|
-
};
|
|
63
|
+
let client = null;
|
|
64
|
+
let connection = null;
|
|
65
|
+
let stopInstallation;
|
|
66
|
+
let state = emptyState();
|
|
77
67
|
let revision = 0;
|
|
78
68
|
let closed = false;
|
|
79
69
|
let closing;
|
|
80
70
|
|
|
71
|
+
function emptyState(error = null) {
|
|
72
|
+
return {
|
|
73
|
+
id: 'stable-diffusion.cpp',
|
|
74
|
+
installed: false,
|
|
75
|
+
available: false,
|
|
76
|
+
selectedModel: null,
|
|
77
|
+
loaded: false,
|
|
78
|
+
state: client ? 'unknown' : 'unavailable',
|
|
79
|
+
backend: null,
|
|
80
|
+
devices: [],
|
|
81
|
+
resources: {},
|
|
82
|
+
progress: null,
|
|
83
|
+
error: error ? serializeCoreError(error) : null,
|
|
84
|
+
models: []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
function current() {
|
|
82
|
-
const requests = [...pending].
|
|
89
|
+
const requests = [...pending].filter(
|
|
90
|
+
function currentConnectionRequest(request) {
|
|
91
|
+
return request.connection === connection && !request.signal.aborted;
|
|
92
|
+
}
|
|
93
|
+
).map(
|
|
83
94
|
function requestState(request) {
|
|
84
95
|
return {
|
|
85
96
|
method: request.method,
|
|
@@ -97,7 +108,7 @@ export function createCoreImageRuntime(
|
|
|
97
108
|
return request.status === 'Thinking';
|
|
98
109
|
}
|
|
99
110
|
) ? 'Thinking' : null,
|
|
100
|
-
busy:
|
|
111
|
+
busy: requests.length > 0,
|
|
101
112
|
closed
|
|
102
113
|
};
|
|
103
114
|
}
|
|
@@ -146,8 +157,38 @@ export function createCoreImageRuntime(
|
|
|
146
157
|
publish();
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
function bindClient({client: nextClient, error = null}) {
|
|
161
|
+
if (closed || (connection && nextClient === client)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const previous = connection;
|
|
165
|
+
client = nextClient;
|
|
166
|
+
const currentConnection = {client, controller: new AbortController(), unsubscribe: null};
|
|
167
|
+
connection = currentConnection;
|
|
168
|
+
revision += 1;
|
|
169
|
+
state = emptyState(error);
|
|
170
|
+
previous?.unsubscribe?.();
|
|
171
|
+
previous?.controller.abort();
|
|
172
|
+
if (client) {
|
|
173
|
+
currentConnection.unsubscribe = client.events.on(
|
|
174
|
+
'image.state',
|
|
175
|
+
function currentImageState(value) {
|
|
176
|
+
if (connection === currentConnection) accept(value);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
publish();
|
|
181
|
+
if (!closed && connection === currentConnection && client) {
|
|
182
|
+
// Observe only; installation never loads an engine or model.
|
|
183
|
+
inspect().catch(
|
|
184
|
+
function initialImageStateUnavailable(error) {
|
|
185
|
+
if (!closed && connection === currentConnection && !currentConnection.controller.signal.aborted) {
|
|
186
|
+
reportObserverFailure(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
151
192
|
|
|
152
193
|
function invoke(method, parameters, {signal: requestSignal, onProgress} = {}) {
|
|
153
194
|
if (closed) {
|
|
@@ -156,11 +197,15 @@ export function createCoreImageRuntime(
|
|
|
156
197
|
if (!client) {
|
|
157
198
|
throw unavailable('Local image generation requires an available Core connection.');
|
|
158
199
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
200
|
+
const currentConnection = connection;
|
|
201
|
+
const operationSignal = AbortSignal.any(
|
|
202
|
+
[lifetimeSignal, currentConnection.controller.signal, ...(requestSignal ? [requestSignal] : [])]
|
|
203
|
+
);
|
|
161
204
|
operationSignal.throwIfAborted();
|
|
162
205
|
const request = {
|
|
163
206
|
method,
|
|
207
|
+
connection: currentConnection,
|
|
208
|
+
signal: operationSignal,
|
|
164
209
|
revision,
|
|
165
210
|
streamId: parameters.streamId,
|
|
166
211
|
status: method === 'image.generate' || method === 'image.edit' ? 'Thinking' : null,
|
|
@@ -169,7 +214,7 @@ export function createCoreImageRuntime(
|
|
|
169
214
|
};
|
|
170
215
|
let stopProgress;
|
|
171
216
|
if (request.streamId) {
|
|
172
|
-
stopProgress = client.events.on(
|
|
217
|
+
stopProgress = currentConnection.client.events.on(
|
|
173
218
|
'image.progress',
|
|
174
219
|
function imageProgress(value) {
|
|
175
220
|
if (closed || operationSignal.aborted || value.streamId !== request.streamId) {
|
|
@@ -202,7 +247,7 @@ export function createCoreImageRuntime(
|
|
|
202
247
|
{type: 'image.request', data: {method, parameters: requestParameters}}
|
|
203
248
|
);
|
|
204
249
|
operationSignal.throwIfAborted();
|
|
205
|
-
const result = await client.invoke(
|
|
250
|
+
const result = await currentConnection.client.invoke(
|
|
206
251
|
method,
|
|
207
252
|
requestParameters,
|
|
208
253
|
{signal: operationSignal, timeoutMs: 0}
|
|
@@ -238,7 +283,7 @@ export function createCoreImageRuntime(
|
|
|
238
283
|
function releaseImageRequest() {
|
|
239
284
|
stopProgress?.();
|
|
240
285
|
pending.delete(request);
|
|
241
|
-
if (!closed) {
|
|
286
|
+
if (!closed && connection === currentConnection) {
|
|
242
287
|
publish();
|
|
243
288
|
}
|
|
244
289
|
}
|
|
@@ -251,12 +296,14 @@ export function createCoreImageRuntime(
|
|
|
251
296
|
|
|
252
297
|
async function inspect({signal: requestSignal} = {}) {
|
|
253
298
|
const before = revision;
|
|
299
|
+
const currentConnection = connection;
|
|
254
300
|
try {
|
|
255
301
|
const result = await invoke(
|
|
256
302
|
'image.status',
|
|
257
303
|
{},
|
|
258
304
|
{signal: requestSignal}
|
|
259
305
|
);
|
|
306
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
260
307
|
if (!closed && revision === before) {
|
|
261
308
|
accept(result);
|
|
262
309
|
}
|
|
@@ -273,11 +320,13 @@ export function createCoreImageRuntime(
|
|
|
273
320
|
|
|
274
321
|
async function load({model, context, assetProjectionId, resourcePaths, signal: requestSignal} = {}) {
|
|
275
322
|
const before = revision;
|
|
323
|
+
const currentConnection = connection;
|
|
276
324
|
const result = await invoke(
|
|
277
325
|
'image.load',
|
|
278
326
|
{model, context, assetProjectionId, resourcePaths},
|
|
279
327
|
{signal: requestSignal}
|
|
280
328
|
);
|
|
329
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
281
330
|
if (!closed && revision === before) {
|
|
282
331
|
accept(result);
|
|
283
332
|
}
|
|
@@ -308,11 +357,13 @@ export function createCoreImageRuntime(
|
|
|
308
357
|
|
|
309
358
|
async function unload({signal: requestSignal} = {}) {
|
|
310
359
|
const before = revision;
|
|
360
|
+
const currentConnection = connection;
|
|
311
361
|
const result = await invoke(
|
|
312
362
|
'image.unload',
|
|
313
363
|
{},
|
|
314
364
|
{signal: requestSignal}
|
|
315
365
|
);
|
|
366
|
+
currentConnection.controller.signal.throwIfAborted();
|
|
316
367
|
if (!closed && revision === before) {
|
|
317
368
|
accept(result);
|
|
318
369
|
}
|
|
@@ -365,8 +416,10 @@ export function createCoreImageRuntime(
|
|
|
365
416
|
);
|
|
366
417
|
lifetime.abort();
|
|
367
418
|
lifetimeSignal.removeEventListener('abort', close);
|
|
368
|
-
|
|
369
|
-
|
|
419
|
+
stopInstallation?.();
|
|
420
|
+
connection?.unsubscribe?.();
|
|
421
|
+
connection?.controller.abort();
|
|
422
|
+
state = {...state, available: false, loaded: false, state: 'closed'};
|
|
370
423
|
publish();
|
|
371
424
|
events.dispose();
|
|
372
425
|
return closing;
|
|
@@ -379,13 +432,10 @@ export function createCoreImageRuntime(
|
|
|
379
432
|
);
|
|
380
433
|
if (lifetimeSignal.aborted) {
|
|
381
434
|
close();
|
|
382
|
-
} else if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
globalThis.console?.error('Core image initial state unavailable.', error);
|
|
387
|
-
}
|
|
388
|
-
);
|
|
435
|
+
} else if (suppliedClient === undefined) {
|
|
436
|
+
stopInstallation = subscribeCoreClient(bindClient, {signal: lifetimeSignal});
|
|
437
|
+
} else {
|
|
438
|
+
bindClient({client: suppliedClient});
|
|
389
439
|
}
|
|
390
440
|
|
|
391
441
|
return {load, generate, edit, unload, inspect, current, subscribe, close};
|
|
@@ -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 {
|