arcane-os 0.84.0 → 0.85.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 +22 -0
- package/README.md +1 -1
- package/browser-runtime/ai/model-observation.mjs +315 -0
- package/docs/reference/core-native-packaging.md +14 -0
- package/docs/reference/core-runtime.md +20 -11
- package/docs/reference/core-shared-host.md +9 -3
- package/docs/reference/execution-devices.md +6 -0
- package/docs/reference/inventory/package-api.json +49 -7
- package/docs/reference/model-observation.md +220 -0
- package/docs/reference/sdk-api.md +106 -8
- package/package.json +3 -1
- package/src/core/runtime-connection.mjs +4 -1
- package/src/core/runtime.mjs +15 -7
- package/src/core/services/model-observation.mjs +231 -0
- package/src/local-ai/execution-devices-windows.ps1 +3 -1
- package/src/native/portable-layout.mjs +16 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
|
|
6
|
+
function observationError(code, message) {
|
|
7
|
+
return new CoreError({code, message});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Observe supplied owners without acquiring, starting or replacing a model. */
|
|
11
|
+
export function createModelObservationService({runtime, localAI = null, image = null, decisions = null} = {}) {
|
|
12
|
+
if (!is.function(runtime?.current) || !is.function(runtime?.onFrame)) {
|
|
13
|
+
throw new TypeError('Model observation requires the existing Core runtime.');
|
|
14
|
+
}
|
|
15
|
+
const owners = {localAI, image, decisions};
|
|
16
|
+
const observedMethods = new Set();
|
|
17
|
+
for (const owner of Object.values(owners)) {
|
|
18
|
+
if (owner === null) continue;
|
|
19
|
+
if (!is.function(owner.current)) throw new TypeError('An observed Core model owner must expose current().');
|
|
20
|
+
for (const method of Object.keys(owner.methods ?? {})) observedMethods.add(method);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const renderers = new Map();
|
|
24
|
+
const watchers = new Set();
|
|
25
|
+
const requests = new Map();
|
|
26
|
+
let stopFrames = null;
|
|
27
|
+
let disposed = false;
|
|
28
|
+
|
|
29
|
+
function current() {
|
|
30
|
+
return {
|
|
31
|
+
core: runtime.current(),
|
|
32
|
+
native: {
|
|
33
|
+
localAI: localAI?.current() ?? null,
|
|
34
|
+
image: image?.current() ?? null,
|
|
35
|
+
decisions: decisions?.current() ?? null
|
|
36
|
+
},
|
|
37
|
+
renderers: [...renderers.values()].map(function rendererState(renderer) {
|
|
38
|
+
return {
|
|
39
|
+
attachmentRequestId: renderer.request.clientRequestId,
|
|
40
|
+
coreRequestId: renderer.request.coreRequestId,
|
|
41
|
+
owners: renderer.snapshot
|
|
42
|
+
};
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireActive(request) {
|
|
48
|
+
request.signal.throwIfAborted();
|
|
49
|
+
if (disposed) throw observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireContext(request) {
|
|
53
|
+
requireActive(request);
|
|
54
|
+
if (!is.string(request.clientRequestId) || !is.string(request.coreRequestId)) {
|
|
55
|
+
throw observationError('MODEL_OBSERVATION_CONTEXT_UNAVAILABLE',
|
|
56
|
+
'Model observation requires the actual client and Core request identities.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function observeActiveRequests(snapshot) {
|
|
61
|
+
for (const request of snapshot.activeRequests) {
|
|
62
|
+
if (observedMethods.has(request.method)) requests.set(request.id, request.method);
|
|
63
|
+
}
|
|
64
|
+
// Core publishes request removal before its response. Keep correlation
|
|
65
|
+
// until that response arrives, rather than treating removal as settlement.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stopFrameObservation() {
|
|
69
|
+
if (watchers.size) return;
|
|
70
|
+
stopFrames?.();
|
|
71
|
+
stopFrames = null;
|
|
72
|
+
requests.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function send(watcher, event, value) {
|
|
76
|
+
if (!watchers.has(watcher) || watcher.request.signal.aborted) return;
|
|
77
|
+
try {
|
|
78
|
+
watcher.request.emit(event, {watchRequestId: watcher.request.clientRequestId, ...value});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
watcher.stop(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function publishState() {
|
|
85
|
+
if (!watchers.size) return;
|
|
86
|
+
let snapshot;
|
|
87
|
+
try {
|
|
88
|
+
snapshot = current();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
for (const watcher of [...watchers]) watcher.stop(error);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const watcher of [...watchers]) send(watcher, 'model.observation.state', {snapshot});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function observeFrame(frame) {
|
|
97
|
+
try {
|
|
98
|
+
if (frame.type === 'response') {
|
|
99
|
+
const method = requests.get(frame.id);
|
|
100
|
+
requests.delete(frame.id);
|
|
101
|
+
if (method !== undefined && frame.ok === false) {
|
|
102
|
+
for (const watcher of [...watchers]) {
|
|
103
|
+
if (watcher.diagnostics) {
|
|
104
|
+
send(watcher, 'model.observation.error', {source: 'core', method, frame});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (frame.type !== 'event') return;
|
|
111
|
+
if (frame.event === 'core.state') {
|
|
112
|
+
observeActiveRequests(frame.data);
|
|
113
|
+
publishState();
|
|
114
|
+
} else if (frame.event === 'localai.state' || frame.event === 'image.state'
|
|
115
|
+
|| frame.event === 'decisions.state') {
|
|
116
|
+
publishState();
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// An observation failure must settle observers, never the operation
|
|
120
|
+
// whose existing owner emitted this frame.
|
|
121
|
+
for (const watcher of [...watchers]) watcher.stop(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hold(request, release) {
|
|
126
|
+
let reject;
|
|
127
|
+
let stopped = false;
|
|
128
|
+
const task = new Promise(function ownObservationRequest(_resolve, rejectRequest) {
|
|
129
|
+
reject = rejectRequest;
|
|
130
|
+
});
|
|
131
|
+
function stop(error) {
|
|
132
|
+
if (stopped) return;
|
|
133
|
+
stopped = true;
|
|
134
|
+
request.signal.removeEventListener('abort', aborted);
|
|
135
|
+
try {
|
|
136
|
+
release();
|
|
137
|
+
} catch (releaseError) {
|
|
138
|
+
error = new AggregateError([error, releaseError], 'Observation retirement failed.');
|
|
139
|
+
}
|
|
140
|
+
reject(error);
|
|
141
|
+
}
|
|
142
|
+
function aborted() {
|
|
143
|
+
stop(request.signal.reason ?? observationError('ARCANE_REQUEST_ABORTED', 'Model observation was cancelled.'));
|
|
144
|
+
}
|
|
145
|
+
request.signal.addEventListener('abort', aborted, {once: true});
|
|
146
|
+
return {task, stop, checkAbort: function checkObservationAbort() { if (request.signal.aborted) aborted(); }};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function watch({diagnostics = false} = {}, request) {
|
|
150
|
+
requireContext(request);
|
|
151
|
+
if (!is.boolean(diagnostics)) throw new TypeError('diagnostics must be a boolean.');
|
|
152
|
+
const watcher = {request, diagnostics, stop: null};
|
|
153
|
+
const owned = hold(request, function releaseWatcher() {
|
|
154
|
+
watchers.delete(watcher);
|
|
155
|
+
stopFrameObservation();
|
|
156
|
+
});
|
|
157
|
+
watcher.stop = owned.stop;
|
|
158
|
+
watchers.add(watcher);
|
|
159
|
+
try {
|
|
160
|
+
if (!stopFrames) stopFrames = runtime.onFrame(observeFrame);
|
|
161
|
+
observeActiveRequests(runtime.current());
|
|
162
|
+
send(watcher, 'model.observation.state', {snapshot: current()});
|
|
163
|
+
owned.checkAbort();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
owned.stop(error);
|
|
166
|
+
}
|
|
167
|
+
return owned.task;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function attach({snapshot} = {}, request) {
|
|
171
|
+
requireContext(request);
|
|
172
|
+
if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
|
|
173
|
+
if (renderers.has(request.coreRequestId)) {
|
|
174
|
+
throw observationError('MODEL_OBSERVATION_ATTACHMENT_ACTIVE', 'This Core attachment request is already active.');
|
|
175
|
+
}
|
|
176
|
+
const renderer = {request, snapshot, stop: null};
|
|
177
|
+
const owned = hold(request, function releaseRenderer() {
|
|
178
|
+
if (renderers.get(request.coreRequestId) === renderer) renderers.delete(request.coreRequestId);
|
|
179
|
+
publishState();
|
|
180
|
+
});
|
|
181
|
+
renderer.stop = owned.stop;
|
|
182
|
+
renderers.set(request.coreRequestId, renderer);
|
|
183
|
+
try {
|
|
184
|
+
request.emit('model.observation.renderer.attached', {
|
|
185
|
+
attachmentRequestId: request.clientRequestId,
|
|
186
|
+
coreRequestId: request.coreRequestId
|
|
187
|
+
});
|
|
188
|
+
publishState();
|
|
189
|
+
owned.checkAbort();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
owned.stop(error);
|
|
192
|
+
}
|
|
193
|
+
return owned.task;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function publishRenderer({attachmentRequestId, coreRequestId, snapshot} = {}, request) {
|
|
197
|
+
requireActive(request);
|
|
198
|
+
const renderer = renderers.get(coreRequestId);
|
|
199
|
+
if (!renderer || renderer.request.clientRequestId !== attachmentRequestId || renderer.request.signal.aborted) {
|
|
200
|
+
throw observationError('MODEL_OBSERVATION_ATTACHMENT_RETIRED', 'The selected renderer observation attachment is no longer active.');
|
|
201
|
+
}
|
|
202
|
+
if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
|
|
203
|
+
renderer.snapshot = snapshot;
|
|
204
|
+
publishState();
|
|
205
|
+
return {attachmentRequestId, coreRequestId};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dispose() {
|
|
209
|
+
if (disposed) return;
|
|
210
|
+
disposed = true;
|
|
211
|
+
const error = observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
|
|
212
|
+
for (const watcher of [...watchers]) watcher.stop(error);
|
|
213
|
+
for (const renderer of [...renderers.values()]) renderer.stop(error);
|
|
214
|
+
stopFrameObservation();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: 'model-observation',
|
|
219
|
+
current,
|
|
220
|
+
dispose,
|
|
221
|
+
methods: {
|
|
222
|
+
'model.observation.current': function currentObservation(_parameters, request) {
|
|
223
|
+
requireActive(request);
|
|
224
|
+
return current();
|
|
225
|
+
},
|
|
226
|
+
'model.observation.watch': watch,
|
|
227
|
+
'model.observation.renderer.attach': attach,
|
|
228
|
+
'model.observation.renderer.publish': publishRenderer
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -107,8 +107,10 @@ function Get-PresentPciDevices {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
if ($devicesById.Count -gt 0) {
|
|
110
|
+
# Concurrent CIM property operations can duplicate some device results
|
|
111
|
+
# and omit others. Serialize only this query's internal operations.
|
|
110
112
|
$properties = @(
|
|
111
|
-
Get-PnpDeviceProperty -InstanceId ([string[]]@($devicesById.Keys)) -KeyName 'DEVPKEY_Device_BusNumber', 'DEVPKEY_Device_Address' -ErrorAction Continue 2>&1 3>&1
|
|
113
|
+
Get-PnpDeviceProperty -InstanceId ([string[]]@($devicesById.Keys)) -KeyName 'DEVPKEY_Device_BusNumber', 'DEVPKEY_Device_Address' -ThrottleLimit 1 -ErrorAction Continue 2>&1 3>&1
|
|
112
114
|
)
|
|
113
115
|
foreach ($property in $properties) {
|
|
114
116
|
if ($property -is [System.Management.Automation.ErrorRecord]) {
|
|
@@ -204,6 +204,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
204
204
|
"import {isSea} from 'node:sea';",
|
|
205
205
|
...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
|
|
206
206
|
"import {readCoreLaunchContext, startCoreHost, startCoreListener, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
|
|
207
|
+
"import {createModelObservationService} from 'arcane-os/core/model-observation';",
|
|
207
208
|
'',
|
|
208
209
|
...(localAI === undefined ? [] : [
|
|
209
210
|
'// Bundled runtime paths are artifact-root-relative POSIX paths.',
|
|
@@ -297,6 +298,21 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
297
298
|
" process.on('SIGINT', closeCore);",
|
|
298
299
|
" process.on('SIGTERM', closeCore);",
|
|
299
300
|
' try {',
|
|
301
|
+
' const observation = createModelObservationService(',
|
|
302
|
+
' {',
|
|
303
|
+
' runtime: host.runtime,',
|
|
304
|
+
' localAI: services.find(',
|
|
305
|
+
" function localAIOwner(service) { return Object.hasOwn(service.methods ?? {}, 'localai.status'); }",
|
|
306
|
+
' ) ?? null,',
|
|
307
|
+
' image: services.find(',
|
|
308
|
+
" function imageOwner(service) { return Object.hasOwn(service.methods ?? {}, 'image.status'); }",
|
|
309
|
+
' ) ?? null,',
|
|
310
|
+
' decisions: services.find(',
|
|
311
|
+
" function decisionOwner(service) { return Object.hasOwn(service.methods ?? {}, 'decisions.status'); }",
|
|
312
|
+
' ) ?? null',
|
|
313
|
+
' }',
|
|
314
|
+
' );',
|
|
315
|
+
' host.runtime.registerService(observation);',
|
|
300
316
|
' listener = await startCoreListener({runtime: host.runtime, endpoint: context.coreListener?.endpoint});',
|
|
301
317
|
' listener.closed.catch(function listenerFailed() { process.exitCode = 1; });',
|
|
302
318
|
' } catch (error) {',
|