arcane-os 0.66.0 → 0.68.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 +42 -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 +6 -0
- package/docs/reference/availability-and-normalization.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 +46 -5
- package/docs/reference/inventory/package-api.json +62 -20
- package/docs/reference/inventory/runtime-components.json +2 -1
- package/docs/reference/inventory/runtime-modules.json +3 -3
- 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/runtime-components.md +62 -5
- package/docs/reference/runtime-modules.md +44 -7
- package/docs/reference/sdk-api.md +229 -19
- package/package.json +5 -2
- package/runtime/arcane/components/theme-switcher.html +112 -13
- package/runtime/arcane/modules/PreferenceStore.js +18 -13
- package/runtime/arcane/modules/SystemAppearance.js +24 -6
- package/src/core/repositories.mjs +27 -8
- package/src/core/runtime.mjs +26 -1
- 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/src/local-ai/onnx.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
95
95
|
|
|
96
96
|
function current(){
|
|
97
97
|
return {sessions:[...sessions.values()].map(function sessionState(entry){
|
|
98
|
-
return structuredClone(entry.state);
|
|
98
|
+
return {...structuredClone(entry.state), stopping:entry.stopping, exited:entry.exited};
|
|
99
99
|
}),closed};
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -157,20 +157,31 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
157
157
|
entry.stopping=true;
|
|
158
158
|
if(failed)entry.terminalError??=reason;
|
|
159
159
|
entry.stopTask=Promise.resolve().then(async function releaseSession(){
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}else{
|
|
164
|
-
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
165
|
-
catch(error){
|
|
166
|
-
entry.releaseError=error;
|
|
160
|
+
let terminationError;
|
|
161
|
+
try{
|
|
162
|
+
if(active||failed||entry.terminalError){
|
|
167
163
|
entry.terminationRequested=true;
|
|
168
164
|
await worker.terminate();
|
|
165
|
+
}else{
|
|
166
|
+
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
167
|
+
catch(error){
|
|
168
|
+
entry.releaseError=error;
|
|
169
|
+
entry.terminationRequested=true;
|
|
170
|
+
await worker.terminate();
|
|
171
|
+
}
|
|
169
172
|
}
|
|
173
|
+
}catch(error){
|
|
174
|
+
terminationError=error;
|
|
175
|
+
reportBackgroundError(error);
|
|
170
176
|
}
|
|
177
|
+
// Failed termination is not actual worker exit. Keep native file
|
|
178
|
+
// ownership until exit and complete output delivery have settled.
|
|
171
179
|
await entry.exit;
|
|
172
|
-
await Promise.
|
|
173
|
-
|
|
180
|
+
const outputResults=await Promise.allSettled(entry.outputTasks);
|
|
181
|
+
const failures=[entry.releaseError,terminationError].filter(function present(error){return error!==null&&error!==undefined;});
|
|
182
|
+
for(const result of outputResults)if(result.status==='rejected')failures.push(result.reason);
|
|
183
|
+
if(failures.length>1)throw new AggregateError(failures,'Releasing the ONNX worker failed.');
|
|
184
|
+
if(failures.length)throw failures[0];
|
|
174
185
|
return structuredClone(entry.state);
|
|
175
186
|
});
|
|
176
187
|
// A native call has no AbortSignal. Stop delivery immediately, retain
|
|
@@ -262,7 +273,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
262
273
|
finally{entry.resolveExit(code);}
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
async function load({id,model,sessionOptions,signal:operationSignal}={}){
|
|
276
|
+
async function load({id,model,sessionOptions,executionPreference,signal:operationSignal}={}){
|
|
266
277
|
assertOpen(operationSignal);
|
|
267
278
|
if(!is.string(id)||!is.string(model))throw new TypeError('ONNX load requires a session id and model path.');
|
|
268
279
|
const previous=sessions.get(id);
|
|
@@ -312,7 +323,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
312
323
|
}
|
|
313
324
|
// Queue load before publishing readiness so a synchronous subscriber
|
|
314
325
|
// can enqueue inference or request release without racing ownership.
|
|
315
|
-
const task=enqueue(entry,'load',{model,sessionOptions},operationSignal,true);
|
|
326
|
+
const task=enqueue(entry,'load',{model,sessionOptions,executionPreference},operationSignal,true);
|
|
316
327
|
publish(entry);
|
|
317
328
|
dispatchNext(entry);
|
|
318
329
|
return task;
|
|
@@ -153,6 +153,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
153
153
|
const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
|
|
154
154
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
|
|
155
155
|
}) ?? false;
|
|
156
|
+
const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedONNX(requirement) {
|
|
157
|
+
return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
|
|
158
|
+
}) ?? false);
|
|
156
159
|
const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
|
|
157
160
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
|
|
158
161
|
}) ?? false;
|
|
@@ -175,10 +178,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
175
178
|
imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
|
|
176
179
|
definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
|
|
177
180
|
}
|
|
178
|
-
if (
|
|
179
|
-
imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
|
|
181
|
+
if (modelAssetsSelected) {
|
|
180
182
|
imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
|
|
181
183
|
definitions.push(' modelAssets');
|
|
184
|
+
}
|
|
185
|
+
if (imageSelected) {
|
|
186
|
+
imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
|
|
182
187
|
definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
|
|
183
188
|
}
|
|
184
189
|
if (whisperSelected) {
|
|
@@ -239,7 +244,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
239
244
|
' ...await readCoreLaunchContext()',
|
|
240
245
|
'};',
|
|
241
246
|
'',
|
|
242
|
-
...(
|
|
247
|
+
...(modelAssetsSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
|
|
243
248
|
...(whisperSelected ? [
|
|
244
249
|
'const stt = createWhisperRuntime(',
|
|
245
250
|
' {',
|
package/src/repository.mjs
CHANGED
|
@@ -1,10 +1,142 @@
|
|
|
1
1
|
import {runProcess} from './process.mjs';
|
|
2
2
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import Is from 'strong-type';
|
|
5
|
+
|
|
6
|
+
const is = new Is(false);
|
|
3
7
|
|
|
4
8
|
async function git(args,{run=runProcess,...options}){
|
|
5
9
|
return run('git',args,options);
|
|
6
10
|
}
|
|
7
11
|
|
|
12
|
+
/** Private shared reader for Git text protocols, retaining native diagnostics. */
|
|
13
|
+
export async function readGitText(args, {
|
|
14
|
+
cwd, signal, onEvent, run = runProcess, allowNonzero = false,
|
|
15
|
+
notTextCode = 'ARCANE_GIT_CONFIGURATION_NOT_TEXT', label = 'Git configuration'
|
|
16
|
+
} = {}) {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
try {
|
|
19
|
+
const result = await run('git', args, {
|
|
20
|
+
cwd, signal, onEvent, allowNonzero,
|
|
21
|
+
outputEncoding: {stdout: null}, captureOutput: {stdout: false}, emitOutputEvents: {stdout: false},
|
|
22
|
+
onOutput: function observeOutput({stream, chunk}) {
|
|
23
|
+
if (stream === 'stdout') chunks.push(chunk);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
try { throwIfAborted(signal); }
|
|
27
|
+
catch (error) { error.details = result; throw error; }
|
|
28
|
+
const rawStdout = Buffer.concat(chunks);
|
|
29
|
+
let output = null;
|
|
30
|
+
if (result.code === 0) {
|
|
31
|
+
try { output = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}).decode(rawStdout); }
|
|
32
|
+
catch (cause) {
|
|
33
|
+
throw new ArcaneError(notTextCode, `${label} cannot be represented as UTF-8 text.`, {cause, details: result});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {result, output, rawStdout};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const rawStdout = Buffer.concat(chunks);
|
|
39
|
+
let attached = false;
|
|
40
|
+
let attachmentError;
|
|
41
|
+
try {
|
|
42
|
+
if (error !== null && (is.object(error) || is.function(error)) && !('rawStdout' in error)) {
|
|
43
|
+
Object.defineProperty(error, 'rawStdout', {
|
|
44
|
+
value: rawStdout, writable: true, enumerable: true, configurable: true
|
|
45
|
+
});
|
|
46
|
+
attached = true;
|
|
47
|
+
}
|
|
48
|
+
} catch (cause) { attachmentError = cause; }
|
|
49
|
+
if (attached) throw error;
|
|
50
|
+
const failure = new ArcaneError(error?.code ?? ERROR_CODES.operationFailed,
|
|
51
|
+
error instanceof Error ? error.message : String(error),
|
|
52
|
+
{cause: error, details: error?.details, exitCode: error?.exitCode});
|
|
53
|
+
failure.rawStdout = rawStdout;
|
|
54
|
+
if (attachmentError) failure.attachmentError = attachmentError;
|
|
55
|
+
throw failure;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Capture a caller-selected operation target without changing its strings. */
|
|
60
|
+
export function captureRepositoryTarget(target) {
|
|
61
|
+
if (target === undefined) return undefined;
|
|
62
|
+
const {remote, ref} = target;
|
|
63
|
+
for (const [name, value] of Object.entries({remote, ref})) {
|
|
64
|
+
if (!is.string(value) || value === '' || !value.isWellFormed() || value.includes('\0')) {
|
|
65
|
+
throw new TypeError(`target.${name} must be nonempty text representable as a native process argument.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {remote, ref};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function repositoryPushArguments(target) {
|
|
72
|
+
return target === undefined ? ['push'] : ['push', '--no-follow-tags', '--', target.remote, `HEAD:${target.ref}`];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Observe an existing workspace without initializing or refreshing it. */
|
|
76
|
+
export async function repositoryConfiguration({workspaceRoot = process.cwd(), signal, onEvent, run = runProcess} = {}) {
|
|
77
|
+
const repositoryRoot = path.resolve(workspaceRoot);
|
|
78
|
+
const options = {cwd: repositoryRoot, signal, onEvent, run, allowNonzero: true};
|
|
79
|
+
throwIfAborted(signal);
|
|
80
|
+
|
|
81
|
+
function processFailure(observation, message) {
|
|
82
|
+
const failure = new ArcaneError(ERROR_CODES.operationFailed, message, {details: observation.result});
|
|
83
|
+
failure.rawStdout = observation.rawStdout;
|
|
84
|
+
return failure;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function configuration(pattern) {
|
|
88
|
+
const observation = await readGitText(['config', '--includes', '--null', '--get-regexp', pattern], options);
|
|
89
|
+
const values = new Map();
|
|
90
|
+
if (observation.result.code === 1) return values;
|
|
91
|
+
if (observation.result.code !== 0) throw processFailure(observation, 'Git configuration observation failed.');
|
|
92
|
+
for (const record of observation.output.split('\0')) {
|
|
93
|
+
if (record === '') continue;
|
|
94
|
+
const separator = record.indexOf('\n');
|
|
95
|
+
const key = separator === -1 ? record : record.slice(0, separator);
|
|
96
|
+
const value = separator === -1 ? '' : record.slice(separator + 1);
|
|
97
|
+
const entries = values.get(key) ?? [];
|
|
98
|
+
entries.push(value);
|
|
99
|
+
values.set(key, entries);
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function literalPattern(value) {
|
|
105
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const root = await readGitText(['rev-parse', '--is-inside-work-tree', '--show-prefix'], options);
|
|
109
|
+
if (root.result.code !== 0) throw processFailure(root, 'Git working repository observation failed.');
|
|
110
|
+
if (root.output !== 'true\n\n' && root.output !== 'true\r\n\r\n') {
|
|
111
|
+
const failure = new ArcaneError('ARCANE_REPOSITORY_DIRECTORY_INVALID',
|
|
112
|
+
'The selected directory is not a Git working repository root.', {details: root.result});
|
|
113
|
+
failure.rawStdout = root.rawStdout;
|
|
114
|
+
throw failure;
|
|
115
|
+
}
|
|
116
|
+
const head = await readGitText(['symbolic-ref', '--quiet', 'HEAD'], options);
|
|
117
|
+
if (head.result.code !== 0 && head.result.code !== 1) throw processFailure(head, 'Git symbolic HEAD observation failed.');
|
|
118
|
+
const headRef = head.result.code === 1 ? null : head.output.replace(/\r?\n$/u, '');
|
|
119
|
+
let upstream = null;
|
|
120
|
+
let remoteName;
|
|
121
|
+
if (headRef?.startsWith('refs/heads/')) {
|
|
122
|
+
const key = `branch.${headRef.slice('refs/heads/'.length)}`;
|
|
123
|
+
const branch = await configuration(`^${literalPattern(key)}[.](remote|merge)$`);
|
|
124
|
+
const remoteNames = branch.get(`${key}.remote`) ?? [];
|
|
125
|
+
remoteName = remoteNames.at(-1);
|
|
126
|
+
upstream = {remoteNames, mergeRefs: branch.get(`${key}.merge`) ?? [], urls: [], pushUrls: []};
|
|
127
|
+
}
|
|
128
|
+
const selectedNames = ['origin'];
|
|
129
|
+
if (remoteName !== undefined && remoteName !== '.' && remoteName !== 'origin') selectedNames.push(remoteName);
|
|
130
|
+
const names = selectedNames.map(literalPattern).join('|');
|
|
131
|
+
const remotes = await configuration(`^remote[.](${names})[.](url|pushurl)$`);
|
|
132
|
+
const origin = {urls: remotes.get('remote.origin.url') ?? [], pushUrls: remotes.get('remote.origin.pushurl') ?? []};
|
|
133
|
+
if (upstream && remoteName !== undefined && remoteName !== '.') {
|
|
134
|
+
upstream.urls = remotes.get(`remote.${remoteName}.url`) ?? [];
|
|
135
|
+
upstream.pushUrls = remotes.get(`remote.${remoteName}.pushurl`) ?? [];
|
|
136
|
+
}
|
|
137
|
+
return {repositoryRoot, headRef, origin, upstream};
|
|
138
|
+
}
|
|
139
|
+
|
|
8
140
|
export async function repositoryStatus({
|
|
9
141
|
workspaceRoot=process.cwd(),
|
|
10
142
|
signal,
|
|
@@ -28,11 +160,13 @@ export async function repositoryStatus({
|
|
|
28
160
|
|
|
29
161
|
export async function repositoryPull({
|
|
30
162
|
workspaceRoot=process.cwd(),
|
|
163
|
+
target,
|
|
31
164
|
signal,
|
|
32
165
|
onEvent,
|
|
33
166
|
run=runProcess
|
|
34
167
|
}={}){
|
|
35
168
|
throwIfAborted(signal);
|
|
169
|
+
const selectedTarget=captureRepositoryTarget(target);
|
|
36
170
|
const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
|
|
37
171
|
if(!before.clean){
|
|
38
172
|
throw new ArcaneError(
|
|
@@ -41,22 +175,27 @@ export async function repositoryPull({
|
|
|
41
175
|
{details:before}
|
|
42
176
|
);
|
|
43
177
|
}
|
|
44
|
-
const
|
|
178
|
+
const args=selectedTarget===undefined?['pull','--ff-only']
|
|
179
|
+
:['pull','--ff-only','--',selectedTarget.remote,`${selectedTarget.ref}:`];
|
|
180
|
+
const result=await git(args,{cwd:workspaceRoot,signal,onEvent,run});
|
|
45
181
|
return {
|
|
46
182
|
action:'pull',
|
|
47
183
|
repositoryRoot:before.repositoryRoot,
|
|
48
184
|
branch:before.branch,
|
|
49
|
-
output:result.stdout.trim()
|
|
185
|
+
output:result.stdout.trim(),
|
|
186
|
+
...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
|
|
50
187
|
};
|
|
51
188
|
}
|
|
52
189
|
|
|
53
190
|
export async function repositoryPush({
|
|
54
191
|
workspaceRoot=process.cwd(),
|
|
192
|
+
target,
|
|
55
193
|
signal,
|
|
56
194
|
onEvent,
|
|
57
195
|
run=runProcess
|
|
58
196
|
}={}){
|
|
59
197
|
throwIfAborted(signal);
|
|
198
|
+
const selectedTarget=captureRepositoryTarget(target);
|
|
60
199
|
const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
|
|
61
200
|
if(!before.branch){
|
|
62
201
|
throw new ArcaneError(
|
|
@@ -64,12 +203,13 @@ export async function repositoryPush({
|
|
|
64
203
|
'Refusing to push from a detached HEAD.'
|
|
65
204
|
);
|
|
66
205
|
}
|
|
67
|
-
const result=await git(
|
|
206
|
+
const result=await git(repositoryPushArguments(selectedTarget),{cwd:workspaceRoot,signal,onEvent,run});
|
|
68
207
|
return {
|
|
69
208
|
action:'push',
|
|
70
209
|
repositoryRoot:before.repositoryRoot,
|
|
71
210
|
branch:before.branch,
|
|
72
|
-
output:(result.stdout||result.stderr).trim()
|
|
211
|
+
output:(result.stdout||result.stderr).trim(),
|
|
212
|
+
...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
|
|
73
213
|
};
|
|
74
214
|
}
|
|
75
215
|
|