arcane-os 0.2.0 → 0.2.2
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 +35 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +79 -13
- package/docs/reference/ai/browser-speech.md +336 -0
- package/docs/reference/ai/browser-wasm.md +207 -82
- package/docs/reference/availability-and-normalization.md +30 -4
- package/docs/reference/behavioral-testing.md +4 -1
- package/docs/reference/cli.md +29 -10
- package/docs/reference/core/arcane-ai-contracts.md +43 -9
- package/docs/reference/inventory/package-api.json +110 -14
- package/docs/reference/inventory/runtime-components.json +19 -6
- package/docs/reference/inventory/runtime-modules.json +113 -9
- package/docs/reference/protocols.md +260 -38
- package/docs/reference/runtime-components.md +108 -15
- package/docs/reference/runtime-modules.md +422 -8
- package/docs/reference/sdk-api.md +626 -85
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
- package/runtime/arcane/components/chat.html +288 -52
- package/runtime/arcane/components/speech.html +339 -15
- package/runtime/arcane/modules/AI.js +1245 -136
- package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
- package/runtime/arcane/modules/AIRuntimeState.js +23 -4
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
|
@@ -267,14 +267,20 @@ function immutableSelections(value) {
|
|
|
267
267
|
|
|
268
268
|
function immutableStartupOptions(options) {
|
|
269
269
|
if (options === undefined) {
|
|
270
|
-
return Object.freeze({
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
startMuted: true,
|
|
272
|
+
startTranscription: false,
|
|
273
|
+
signal: null
|
|
274
|
+
});
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
assertPlainObject(options, 'AI provider startup options');
|
|
274
278
|
const descriptors = Object.getOwnPropertyDescriptors(options);
|
|
275
279
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
276
280
|
if (typeof key === 'symbol'
|
|
277
|
-
|| (key !== 'startMuted'
|
|
281
|
+
|| (key !== 'startMuted'
|
|
282
|
+
&& key !== 'startTranscription'
|
|
283
|
+
&& key !== 'signal')) {
|
|
278
284
|
fail('AI provider startup options contain an unknown option.');
|
|
279
285
|
}
|
|
280
286
|
if (!Object.hasOwn(descriptors[key], 'value')) {
|
|
@@ -284,14 +290,20 @@ function immutableStartupOptions(options) {
|
|
|
284
290
|
const startMuted = Object.hasOwn(descriptors, 'startMuted')
|
|
285
291
|
? descriptors.startMuted.value
|
|
286
292
|
: true;
|
|
293
|
+
const startTranscription = Object.hasOwn(descriptors, 'startTranscription')
|
|
294
|
+
? descriptors.startTranscription.value
|
|
295
|
+
: false;
|
|
287
296
|
const signal = Object.hasOwn(descriptors, 'signal')
|
|
288
297
|
? descriptors.signal.value
|
|
289
298
|
: null;
|
|
290
299
|
if (typeof startMuted !== 'boolean') {
|
|
291
300
|
fail('AI startup startMuted must be a boolean.');
|
|
292
301
|
}
|
|
302
|
+
if (typeof startTranscription !== 'boolean') {
|
|
303
|
+
fail('AI startup startTranscription must be a boolean.');
|
|
304
|
+
}
|
|
293
305
|
assertAbortSignal(signal);
|
|
294
|
-
return Object.freeze({startMuted, signal});
|
|
306
|
+
return Object.freeze({startMuted, startTranscription, signal});
|
|
295
307
|
}
|
|
296
308
|
|
|
297
309
|
function immutableInspectionOptions(options) {
|
|
@@ -460,10 +472,12 @@ function createRoleSlot(role) {
|
|
|
460
472
|
selection: null,
|
|
461
473
|
generation: 0,
|
|
462
474
|
operationSequence: 0,
|
|
475
|
+
requestSequence: 0,
|
|
463
476
|
loadController: null,
|
|
464
477
|
loadPromise: null,
|
|
465
478
|
unloadPromise: null,
|
|
466
479
|
disposePromise: null,
|
|
480
|
+
requestAdmission: null,
|
|
467
481
|
request: null,
|
|
468
482
|
disposed: false,
|
|
469
483
|
ready: false
|
|
@@ -976,10 +990,10 @@ export class AIProviderRuntime {
|
|
|
976
990
|
)
|
|
977
991
|
);
|
|
978
992
|
}
|
|
979
|
-
if (slot.request) {
|
|
993
|
+
if (slot.request || slot.requestAdmission) {
|
|
980
994
|
return Promise.reject(
|
|
981
995
|
operationError(
|
|
982
|
-
`AI role ${role} cannot load during
|
|
996
|
+
`AI role ${role} cannot load during active request ownership.`,
|
|
983
997
|
'ARCANE_AI_ROLE_BUSY'
|
|
984
998
|
)
|
|
985
999
|
);
|
|
@@ -1036,6 +1050,14 @@ export class AIProviderRuntime {
|
|
|
1036
1050
|
);
|
|
1037
1051
|
return Promise.resolve(this.status(role));
|
|
1038
1052
|
}
|
|
1053
|
+
if (readyStatus?.busy === true) {
|
|
1054
|
+
return Promise.reject(
|
|
1055
|
+
operationError(
|
|
1056
|
+
`AI role ${role} cannot load while its provider remains busy.`,
|
|
1057
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
1058
|
+
)
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1039
1061
|
} catch {
|
|
1040
1062
|
// The private ready flag is revoked when provider proof is stale.
|
|
1041
1063
|
}
|
|
@@ -1211,6 +1233,7 @@ export class AIProviderRuntime {
|
|
|
1211
1233
|
|| providerStatus?.loaded === true
|
|
1212
1234
|
|| providerStatus?.busy === true;
|
|
1213
1235
|
if (!slot.loadPromise
|
|
1236
|
+
&& !slot.requestAdmission
|
|
1214
1237
|
&& !slot.request
|
|
1215
1238
|
&& !providerOwned
|
|
1216
1239
|
&& (!provider || providerStatus)) {
|
|
@@ -1225,6 +1248,7 @@ export class AIProviderRuntime {
|
|
|
1225
1248
|
this.#speechMuted = true;
|
|
1226
1249
|
}
|
|
1227
1250
|
slot.loadController?.abort();
|
|
1251
|
+
slot.requestAdmission?.controller.abort();
|
|
1228
1252
|
slot.request?.controller.abort();
|
|
1229
1253
|
const capturedLoad = slot.loadPromise;
|
|
1230
1254
|
const capturedRequestRecord = slot.request;
|
|
@@ -1476,6 +1500,7 @@ export class AIProviderRuntime {
|
|
|
1476
1500
|
slot.loadPromise = null;
|
|
1477
1501
|
slot.unloadPromise = null;
|
|
1478
1502
|
slot.disposePromise = null;
|
|
1503
|
+
slot.requestAdmission = null;
|
|
1479
1504
|
slot.request = null;
|
|
1480
1505
|
slot.ready = false;
|
|
1481
1506
|
slot.disposed = true;
|
|
@@ -1529,6 +1554,9 @@ export class AIProviderRuntime {
|
|
|
1529
1554
|
} catch (error) {
|
|
1530
1555
|
return Promise.reject(error);
|
|
1531
1556
|
}
|
|
1557
|
+
if (options.signal?.aborted) {
|
|
1558
|
+
return Promise.reject(normalizedAbort());
|
|
1559
|
+
}
|
|
1532
1560
|
const slot = this.#slots[role];
|
|
1533
1561
|
if (slot.disposed) {
|
|
1534
1562
|
return Promise.reject(
|
|
@@ -1546,14 +1574,6 @@ export class AIProviderRuntime {
|
|
|
1546
1574
|
)
|
|
1547
1575
|
);
|
|
1548
1576
|
}
|
|
1549
|
-
if (slot.request) {
|
|
1550
|
-
return Promise.reject(
|
|
1551
|
-
operationError(
|
|
1552
|
-
`AI role ${role} already owns an active request.`,
|
|
1553
|
-
'ARCANE_AI_ROLE_BUSY'
|
|
1554
|
-
)
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
1577
|
const targetSelection = options.localOnly
|
|
1558
1578
|
? slot.routes.localOnly
|
|
1559
1579
|
: slot.routes.default;
|
|
@@ -1599,6 +1619,17 @@ export class AIProviderRuntime {
|
|
|
1599
1619
|
this.#publishRoleError(slot, invalidStatus, false);
|
|
1600
1620
|
return Promise.reject(invalidStatus);
|
|
1601
1621
|
}
|
|
1622
|
+
if (options.localOnly && targetSelection.localOnly !== true) {
|
|
1623
|
+
return Promise.reject(
|
|
1624
|
+
operationError(
|
|
1625
|
+
`AI role ${role} does not have a local-only route.`,
|
|
1626
|
+
'AI_LOCAL_MODEL_REQUIRED'
|
|
1627
|
+
)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
if (slot.request || slot.requestAdmission) {
|
|
1631
|
+
return this.#supersedeRoleRequest(slot, provider, options);
|
|
1632
|
+
}
|
|
1602
1633
|
if (!slot.ready
|
|
1603
1634
|
|| providerStatus.state !== 'ready'
|
|
1604
1635
|
|| providerStatus.loaded !== true
|
|
@@ -1611,22 +1642,16 @@ export class AIProviderRuntime {
|
|
|
1611
1642
|
)
|
|
1612
1643
|
);
|
|
1613
1644
|
}
|
|
1614
|
-
if (options.localOnly && targetSelection.localOnly !== true) {
|
|
1615
|
-
return Promise.reject(
|
|
1616
|
-
operationError(
|
|
1617
|
-
`AI role ${role} does not have a local-only route.`,
|
|
1618
|
-
'AI_LOCAL_MODEL_REQUIRED'
|
|
1619
|
-
)
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
1645
|
|
|
1623
1646
|
const generation = slot.generation;
|
|
1647
|
+
const requestSequence = this.#nextRequestSequence(slot);
|
|
1624
1648
|
const operationId = this.#nextOperationId(slot, options.operation);
|
|
1625
1649
|
const controller = new AbortController();
|
|
1626
1650
|
const detachSignal = this.#forwardAbort(options.signal, controller);
|
|
1627
1651
|
const runtime = this;
|
|
1628
1652
|
const requestRecord = {
|
|
1629
1653
|
controller,
|
|
1654
|
+
requestSequence,
|
|
1630
1655
|
promise: null,
|
|
1631
1656
|
cancel: null
|
|
1632
1657
|
};
|
|
@@ -1646,7 +1671,10 @@ export class AIProviderRuntime {
|
|
|
1646
1671
|
);
|
|
1647
1672
|
|
|
1648
1673
|
function restoreAIProviderRoleAfterRequest(error) {
|
|
1649
|
-
if (slot.generation !== generation
|
|
1674
|
+
if (slot.generation !== generation
|
|
1675
|
+
|| slot.requestSequence !== requestSequence
|
|
1676
|
+
|| slot.requestAdmission
|
|
1677
|
+
|| slot.unloadPromise) {
|
|
1650
1678
|
return;
|
|
1651
1679
|
}
|
|
1652
1680
|
let providerState = null;
|
|
@@ -1881,13 +1909,19 @@ export class AIProviderRuntime {
|
|
|
1881
1909
|
}
|
|
1882
1910
|
providerHandle = opened;
|
|
1883
1911
|
iterator = opened[Symbol.asyncIterator]();
|
|
1884
|
-
runtime.#
|
|
1912
|
+
runtime.#assertCurrentRequest(
|
|
1913
|
+
slot,
|
|
1914
|
+
generation,
|
|
1915
|
+
requestSequence,
|
|
1916
|
+
controller.signal
|
|
1917
|
+
);
|
|
1885
1918
|
Promise.resolve(opened.result).then(
|
|
1886
1919
|
function acceptAIProviderStreamResult(value) {
|
|
1887
1920
|
try {
|
|
1888
|
-
runtime.#
|
|
1921
|
+
runtime.#assertCurrentRequest(
|
|
1889
1922
|
slot,
|
|
1890
1923
|
generation,
|
|
1924
|
+
requestSequence,
|
|
1891
1925
|
controller.signal
|
|
1892
1926
|
);
|
|
1893
1927
|
settleAIProviderStream(null, value);
|
|
@@ -1915,7 +1949,14 @@ export class AIProviderRuntime {
|
|
|
1915
1949
|
cancel: cancelAIProviderStream,
|
|
1916
1950
|
async next(value) {
|
|
1917
1951
|
try {
|
|
1918
|
-
|
|
1952
|
+
const result = await iterator.next(value);
|
|
1953
|
+
runtime.#assertCurrentRequest(
|
|
1954
|
+
slot,
|
|
1955
|
+
generation,
|
|
1956
|
+
requestSequence,
|
|
1957
|
+
controller.signal
|
|
1958
|
+
);
|
|
1959
|
+
return result;
|
|
1919
1960
|
} catch (error) {
|
|
1920
1961
|
await cancelAIProviderStream(error);
|
|
1921
1962
|
throw isAbort(error, controller.signal)
|
|
@@ -1998,7 +2039,12 @@ export class AIProviderRuntime {
|
|
|
1998
2039
|
signal: controller.signal
|
|
1999
2040
|
}
|
|
2000
2041
|
);
|
|
2001
|
-
runtime.#
|
|
2042
|
+
runtime.#assertCurrentRequest(
|
|
2043
|
+
slot,
|
|
2044
|
+
generation,
|
|
2045
|
+
requestSequence,
|
|
2046
|
+
controller.signal
|
|
2047
|
+
);
|
|
2002
2048
|
return result;
|
|
2003
2049
|
} catch (error) {
|
|
2004
2050
|
const normalized = isAbort(error, controller.signal)
|
|
@@ -2041,16 +2087,19 @@ export class AIProviderRuntime {
|
|
|
2041
2087
|
cancel(role) {
|
|
2042
2088
|
assertRole(role);
|
|
2043
2089
|
const slot = this.#slots[role];
|
|
2044
|
-
if (!slot.request) {
|
|
2090
|
+
if (!slot.request && !slot.requestAdmission) {
|
|
2045
2091
|
return false;
|
|
2046
2092
|
}
|
|
2047
|
-
slot.
|
|
2048
|
-
slot.request
|
|
2093
|
+
slot.requestAdmission?.controller.abort();
|
|
2094
|
+
const requestRecord = slot.request;
|
|
2095
|
+
requestRecord?.controller.abort();
|
|
2096
|
+
const cancellation = requestRecord?.cancel?.(
|
|
2049
2097
|
operationError(
|
|
2050
2098
|
`AI role ${role} request was cancelled.`,
|
|
2051
2099
|
'ARCANE_AI_REQUEST_ABORTED'
|
|
2052
2100
|
)
|
|
2053
|
-
)
|
|
2101
|
+
);
|
|
2102
|
+
cancellation?.catch(function retainAIProviderCancelFailureInState() {});
|
|
2054
2103
|
return true;
|
|
2055
2104
|
}
|
|
2056
2105
|
|
|
@@ -2134,6 +2183,128 @@ export class AIProviderRuntime {
|
|
|
2134
2183
|
);
|
|
2135
2184
|
}
|
|
2136
2185
|
|
|
2186
|
+
#supersedeRoleRequest(slot, provider, options) {
|
|
2187
|
+
const generation = slot.generation;
|
|
2188
|
+
const requestSequence = this.#nextRequestSequence(slot);
|
|
2189
|
+
const priorAdmission = slot.requestAdmission;
|
|
2190
|
+
const activeRequest = priorAdmission?.activeRequest ?? slot.request;
|
|
2191
|
+
const controller = new AbortController();
|
|
2192
|
+
const detachSignal = this.#forwardAbort(options.signal, controller);
|
|
2193
|
+
let settlement = priorAdmission?.settlement ?? null;
|
|
2194
|
+
if (!settlement && activeRequest) {
|
|
2195
|
+
activeRequest.controller.abort();
|
|
2196
|
+
settlement = Promise.allSettled(
|
|
2197
|
+
[
|
|
2198
|
+
Promise.resolve().then(
|
|
2199
|
+
function cancelSupersededAIProviderRequest() {
|
|
2200
|
+
return activeRequest.cancel(
|
|
2201
|
+
operationError(
|
|
2202
|
+
`AI role ${slot.role} request was superseded.`,
|
|
2203
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
),
|
|
2208
|
+
activeRequest.promise
|
|
2209
|
+
]
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
if (!settlement) {
|
|
2213
|
+
settlement = Promise.resolve([]);
|
|
2214
|
+
}
|
|
2215
|
+
const admission = {
|
|
2216
|
+
activeRequest,
|
|
2217
|
+
controller,
|
|
2218
|
+
generation,
|
|
2219
|
+
requestSequence,
|
|
2220
|
+
settlement
|
|
2221
|
+
};
|
|
2222
|
+
slot.requestAdmission = admission;
|
|
2223
|
+
const runtime = this;
|
|
2224
|
+
return (async function supersedeAIProviderRoleRequest() {
|
|
2225
|
+
try {
|
|
2226
|
+
const outcomes = await settlement;
|
|
2227
|
+
runtime.#assertCurrentRequestAdmission(slot, admission);
|
|
2228
|
+
if (outcomes[0]?.status === 'rejected') {
|
|
2229
|
+
throw outcomes[0].reason;
|
|
2230
|
+
}
|
|
2231
|
+
let providerStatus;
|
|
2232
|
+
try {
|
|
2233
|
+
providerStatus = validateProviderStatus(provider.status());
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
throw operationError(
|
|
2236
|
+
`The selected ${slot.role} provider did not return a valid status.`,
|
|
2237
|
+
'ARCANE_AI_PROVIDER_STATUS_INVALID',
|
|
2238
|
+
error
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
if (!slot.ready
|
|
2242
|
+
|| providerStatus.state !== 'ready'
|
|
2243
|
+
|| providerStatus.loaded !== true
|
|
2244
|
+
|| providerStatus.busy !== false) {
|
|
2245
|
+
slot.requestAdmission = null;
|
|
2246
|
+
slot.ready = false;
|
|
2247
|
+
const notReady = operationError(
|
|
2248
|
+
`AI role ${slot.role} is not ready after superseding its active request.`,
|
|
2249
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
2250
|
+
);
|
|
2251
|
+
if (providerStatus.state === 'unloaded'
|
|
2252
|
+
&& !providerStatus.loaded
|
|
2253
|
+
&& !providerStatus.busy) {
|
|
2254
|
+
publishAIRuntimeRoleState(
|
|
2255
|
+
slot.role,
|
|
2256
|
+
roleRecord(slot.role, slot.selection)
|
|
2257
|
+
);
|
|
2258
|
+
} else if (providerStatus.state === 'ready'
|
|
2259
|
+
&& providerStatus.loaded
|
|
2260
|
+
&& providerStatus.busy) {
|
|
2261
|
+
slot.ready = true;
|
|
2262
|
+
publishAIRuntimeRoleState(
|
|
2263
|
+
slot.role,
|
|
2264
|
+
roleRecord(
|
|
2265
|
+
slot.role,
|
|
2266
|
+
slot.selection,
|
|
2267
|
+
{
|
|
2268
|
+
state: 'ready',
|
|
2269
|
+
loaded: true,
|
|
2270
|
+
busy: true
|
|
2271
|
+
}
|
|
2272
|
+
)
|
|
2273
|
+
);
|
|
2274
|
+
} else {
|
|
2275
|
+
runtime.#publishRoleError(
|
|
2276
|
+
slot,
|
|
2277
|
+
notReady,
|
|
2278
|
+
providerStatus.loaded
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
throw notReady;
|
|
2282
|
+
}
|
|
2283
|
+
slot.requestAdmission = null;
|
|
2284
|
+
return await runtime.request(slot.role, options);
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
const normalized = isAbort(error, controller.signal)
|
|
2287
|
+
? normalizedAbort(error)
|
|
2288
|
+
: error;
|
|
2289
|
+
if (slot.requestAdmission === admission) {
|
|
2290
|
+
slot.requestAdmission = null;
|
|
2291
|
+
if (slot.generation === generation
|
|
2292
|
+
&& !slot.unloadPromise
|
|
2293
|
+
&& !slot.disposePromise) {
|
|
2294
|
+
runtime.#publishRequestAdmissionFailure(
|
|
2295
|
+
slot,
|
|
2296
|
+
provider,
|
|
2297
|
+
normalized
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
throw normalized;
|
|
2302
|
+
} finally {
|
|
2303
|
+
detachSignal();
|
|
2304
|
+
}
|
|
2305
|
+
})();
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2137
2308
|
#sameSelection(left, right) {
|
|
2138
2309
|
return left === right
|
|
2139
2310
|
|| Boolean(
|
|
@@ -2172,11 +2343,23 @@ export class AIProviderRuntime {
|
|
|
2172
2343
|
slot.loadPromise
|
|
2173
2344
|
|| slot.unloadPromise
|
|
2174
2345
|
|| slot.disposePromise
|
|
2346
|
+
|| slot.requestAdmission
|
|
2175
2347
|
|| slot.request
|
|
2176
2348
|
|| slot.ready
|
|
2177
2349
|
);
|
|
2178
2350
|
}
|
|
2179
2351
|
|
|
2352
|
+
#nextRequestSequence(slot) {
|
|
2353
|
+
if (slot.requestSequence === Number.MAX_SAFE_INTEGER) {
|
|
2354
|
+
throw operationError(
|
|
2355
|
+
`AI role ${slot.role} exhausted its request sequence.`,
|
|
2356
|
+
'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
slot.requestSequence += 1;
|
|
2360
|
+
return slot.requestSequence;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2180
2363
|
#nextOperationId(slot, operation) {
|
|
2181
2364
|
if (slot.operationSequence === Number.MAX_SAFE_INTEGER) {
|
|
2182
2365
|
throw operationError(
|
|
@@ -2200,6 +2383,30 @@ export class AIProviderRuntime {
|
|
|
2200
2383
|
}
|
|
2201
2384
|
}
|
|
2202
2385
|
|
|
2386
|
+
#assertCurrentRequest(slot, generation, requestSequence, signal) {
|
|
2387
|
+
this.#assertCurrentOperation(slot, generation, signal);
|
|
2388
|
+
if (slot.requestSequence !== requestSequence) {
|
|
2389
|
+
throw operationError(
|
|
2390
|
+
`AI role ${slot.role} request was superseded.`,
|
|
2391
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
#assertCurrentRequestAdmission(slot, admission) {
|
|
2397
|
+
if (admission.controller.signal.aborted) {
|
|
2398
|
+
throw normalizedAbort();
|
|
2399
|
+
}
|
|
2400
|
+
if (slot.generation !== admission.generation
|
|
2401
|
+
|| slot.requestSequence !== admission.requestSequence
|
|
2402
|
+
|| slot.requestAdmission !== admission) {
|
|
2403
|
+
throw operationError(
|
|
2404
|
+
`AI role ${slot.role} request admission was superseded.`,
|
|
2405
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2203
2410
|
#forwardAbort(signal, controller) {
|
|
2204
2411
|
if (!signal) {
|
|
2205
2412
|
return function detachAbsentAIProviderAbort() {};
|
|
@@ -2236,6 +2443,68 @@ export class AIProviderRuntime {
|
|
|
2236
2443
|
return true;
|
|
2237
2444
|
}
|
|
2238
2445
|
|
|
2446
|
+
#publishRequestAdmissionFailure(slot, provider, error) {
|
|
2447
|
+
let providerStatus = null;
|
|
2448
|
+
try {
|
|
2449
|
+
providerStatus = validateProviderStatus(provider.status());
|
|
2450
|
+
} catch {
|
|
2451
|
+
// The admission error remains authoritative when status is malformed.
|
|
2452
|
+
}
|
|
2453
|
+
if (isAbort(error, null)
|
|
2454
|
+
&& providerStatus?.state === 'ready'
|
|
2455
|
+
&& providerStatus.loaded
|
|
2456
|
+
&& !providerStatus.busy) {
|
|
2457
|
+
slot.ready = true;
|
|
2458
|
+
publishAIRuntimeRoleState(
|
|
2459
|
+
slot.role,
|
|
2460
|
+
roleRecord(
|
|
2461
|
+
slot.role,
|
|
2462
|
+
slot.selection,
|
|
2463
|
+
{
|
|
2464
|
+
state: 'ready',
|
|
2465
|
+
loaded: true
|
|
2466
|
+
}
|
|
2467
|
+
)
|
|
2468
|
+
);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
if (isAbort(error, null)
|
|
2472
|
+
&& providerStatus?.state === 'unloaded'
|
|
2473
|
+
&& !providerStatus.loaded
|
|
2474
|
+
&& !providerStatus.busy) {
|
|
2475
|
+
slot.ready = false;
|
|
2476
|
+
publishAIRuntimeRoleState(
|
|
2477
|
+
slot.role,
|
|
2478
|
+
roleRecord(slot.role, slot.selection)
|
|
2479
|
+
);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
if (providerStatus?.state === 'ready'
|
|
2483
|
+
&& providerStatus.loaded
|
|
2484
|
+
&& providerStatus.busy) {
|
|
2485
|
+
slot.ready = true;
|
|
2486
|
+
publishAIRuntimeRoleState(
|
|
2487
|
+
slot.role,
|
|
2488
|
+
roleRecord(
|
|
2489
|
+
slot.role,
|
|
2490
|
+
slot.selection,
|
|
2491
|
+
{
|
|
2492
|
+
state: 'ready',
|
|
2493
|
+
loaded: true,
|
|
2494
|
+
busy: true
|
|
2495
|
+
}
|
|
2496
|
+
)
|
|
2497
|
+
);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
slot.ready = false;
|
|
2501
|
+
this.#publishRoleError(
|
|
2502
|
+
slot,
|
|
2503
|
+
error,
|
|
2504
|
+
providerStatus?.loaded === true
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2239
2508
|
#publishRoleError(slot, error, loaded) {
|
|
2240
2509
|
publishAIRuntimeRoleState(
|
|
2241
2510
|
slot.role,
|
|
@@ -57,6 +57,7 @@ const INTENT_SUBSCRIPTION_OPTION_KEYS = Object.freeze([
|
|
|
57
57
|
]);
|
|
58
58
|
const STARTUP_OPTION_KEYS = Object.freeze([
|
|
59
59
|
'startMuted',
|
|
60
|
+
'startTranscription',
|
|
60
61
|
'signal'
|
|
61
62
|
]);
|
|
62
63
|
const ROLE_SET = new Set(AI_RUNTIME_ROLES);
|
|
@@ -395,6 +396,7 @@ function startupOptions(options) {
|
|
|
395
396
|
if (options === undefined) {
|
|
396
397
|
return {
|
|
397
398
|
startMuted: true,
|
|
399
|
+
startTranscription: false,
|
|
398
400
|
signal: null
|
|
399
401
|
};
|
|
400
402
|
}
|
|
@@ -403,14 +405,21 @@ function startupOptions(options) {
|
|
|
403
405
|
const startMuted = Object.hasOwn(options, 'startMuted')
|
|
404
406
|
? options.startMuted
|
|
405
407
|
: true;
|
|
408
|
+
const startTranscription = Object.hasOwn(options, 'startTranscription')
|
|
409
|
+
? options.startTranscription
|
|
410
|
+
: false;
|
|
406
411
|
const signal = Object.hasOwn(options, 'signal') ? options.signal : null;
|
|
407
412
|
if (typeof startMuted !== 'boolean') {
|
|
408
413
|
fail('startup options.startMuted must be a boolean.');
|
|
409
414
|
}
|
|
415
|
+
if (typeof startTranscription !== 'boolean') {
|
|
416
|
+
fail('startup options.startTranscription must be a boolean.');
|
|
417
|
+
}
|
|
410
418
|
assertAbortSignal(signal);
|
|
411
419
|
|
|
412
420
|
return {
|
|
413
421
|
startMuted,
|
|
422
|
+
startTranscription,
|
|
414
423
|
signal
|
|
415
424
|
};
|
|
416
425
|
}
|
|
@@ -419,11 +428,11 @@ function hasAIRuntimeSelection(roleState) {
|
|
|
419
428
|
return roleState.providerId !== null && roleState.modelId !== null;
|
|
420
429
|
}
|
|
421
430
|
|
|
422
|
-
function startupRequestedRoles(snapshot, startMuted) {
|
|
431
|
+
function startupRequestedRoles(snapshot, startMuted, startTranscription) {
|
|
423
432
|
return Object.freeze(
|
|
424
433
|
{
|
|
425
434
|
llm: hasAIRuntimeSelection(snapshot.roles.llm),
|
|
426
|
-
stt: hasAIRuntimeSelection(snapshot.roles.stt),
|
|
435
|
+
stt: startTranscription && hasAIRuntimeSelection(snapshot.roles.stt),
|
|
427
436
|
tts: !startMuted && hasAIRuntimeSelection(snapshot.roles.tts)
|
|
428
437
|
}
|
|
429
438
|
);
|
|
@@ -444,13 +453,20 @@ function startupRoleReport(requested, roleState) {
|
|
|
444
453
|
);
|
|
445
454
|
}
|
|
446
455
|
|
|
447
|
-
function startupReport(
|
|
456
|
+
function startupReport(
|
|
457
|
+
snapshot,
|
|
458
|
+
startRevision,
|
|
459
|
+
startMuted,
|
|
460
|
+
startTranscription,
|
|
461
|
+
requestedRoles
|
|
462
|
+
) {
|
|
448
463
|
return Object.freeze(
|
|
449
464
|
{
|
|
450
465
|
protocol: AI_RUNTIME_PROTOCOL,
|
|
451
466
|
startRevision,
|
|
452
467
|
currentRevision: snapshot.revision,
|
|
453
468
|
startMuted,
|
|
469
|
+
startTranscription,
|
|
454
470
|
chatReady: snapshot.roles.llm.state === 'ready',
|
|
455
471
|
roles: Object.freeze(
|
|
456
472
|
{
|
|
@@ -726,6 +742,7 @@ export function startAIRuntime(options) {
|
|
|
726
742
|
snapshot,
|
|
727
743
|
startRevision,
|
|
728
744
|
normalized.startMuted,
|
|
745
|
+
normalized.startTranscription,
|
|
729
746
|
requestedRoles
|
|
730
747
|
);
|
|
731
748
|
barrierResolved = true;
|
|
@@ -747,6 +764,7 @@ export function startAIRuntime(options) {
|
|
|
747
764
|
snapshot,
|
|
748
765
|
startRevision,
|
|
749
766
|
normalized.startMuted,
|
|
767
|
+
normalized.startTranscription,
|
|
750
768
|
requestedRoles
|
|
751
769
|
)
|
|
752
770
|
);
|
|
@@ -847,7 +865,8 @@ export function startAIRuntime(options) {
|
|
|
847
865
|
startRevision = latestSnapshot.revision;
|
|
848
866
|
requestedRoles = startupRequestedRoles(
|
|
849
867
|
latestSnapshot,
|
|
850
|
-
normalized.startMuted
|
|
868
|
+
normalized.startMuted,
|
|
869
|
+
normalized.startTranscription
|
|
851
870
|
);
|
|
852
871
|
normalized.signal?.addEventListener(
|
|
853
872
|
'abort',
|