arcane-os 0.5.7 → 0.5.9
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 +25 -0
- package/README.md +39 -27
- package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
- package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
- package/browser-runtime/ai/speech-worker-client.mjs +51 -14
- package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
- package/browser-runtime/dependencies/strong-type/package.json +22 -23
- package/browser-runtime/event-manager.mjs +27 -41
- package/package.json +2 -3
- package/runtime/arcane/components/chat.html +1 -1
- package/runtime/arcane/components/speech.html +8 -38
- package/runtime/arcane/components/voice-transcription.html +1 -1
- package/runtime/arcane/css/theme.css +1 -1
- package/runtime/arcane/entities/Chat.js +12 -7
- package/runtime/arcane/modules/AI.js +569 -402
- package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
- package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
- package/runtime/arcane/modules/AIRuntimeState.js +2 -18
- package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
- package/runtime/arcane/modules/CalculatorEngine.js +0 -4
- package/runtime/arcane/modules/ChatRecords.js +169 -1
- package/runtime/arcane/modules/CommunicationHub.js +0 -19
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
- package/runtime/arcane/modules/Errors.js +6 -19
- package/runtime/arcane/modules/Mail.js +1 -2
- package/runtime/arcane/modules/MailTransport.mjs +1 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
- package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
- package/runtime/arcane/modules/ScreenCapture.js +2 -4
- package/runtime/arcane/modules/SpeechPlayback.js +0 -27
- package/runtime/arcane/modules/WaitForComponent.js +0 -1
- package/src/app-descriptor.mjs +1 -1
- package/src/doctor.mjs +1 -3
- package/src/event-manager.mjs +27 -41
- package/src/import-map.mjs +1 -1
- package/src/index.mjs +2 -9
- package/src/installed-sdk-runtime.mjs +1 -11
- package/src/mail-api.mjs +0 -1
- package/src/native-provider-loader.mjs +0 -9
- package/src/scaffold.mjs +9 -21
- package/src/toolchain.mjs +5 -28
- package/src/workspace-runtime.mjs +0 -4
- package/src/workspace.mjs +2 -23
|
@@ -34,7 +34,7 @@ function allowedTokensForSlot(allowedValues,index){
|
|
|
34
34
|
/**
|
|
35
35
|
* Normalizes the shared six-slot AI preference contract without choosing any
|
|
36
36
|
* provider or model policy. Applications supply their own defaults, allowed
|
|
37
|
-
* values, and
|
|
37
|
+
* values, and provider aliases.
|
|
38
38
|
*
|
|
39
39
|
* Slot order:
|
|
40
40
|
* [LLM provider, STT provider, TTS provider, LLM model, TTS model, STT model]
|
|
@@ -1336,6 +1336,16 @@ function validateProvider(provider) {
|
|
|
1336
1336
|
if (typeof provider.localOnly !== 'boolean') {
|
|
1337
1337
|
fail('AI provider.localOnly must be a boolean.');
|
|
1338
1338
|
}
|
|
1339
|
+
const maxConcurrentRequests = provider.maxConcurrentRequests === undefined
|
|
1340
|
+
? 1
|
|
1341
|
+
: provider.maxConcurrentRequests;
|
|
1342
|
+
if (!Number.isSafeInteger(maxConcurrentRequests)
|
|
1343
|
+
|| maxConcurrentRequests < 1) {
|
|
1344
|
+
fail('AI provider.maxConcurrentRequests must be a positive safe integer.');
|
|
1345
|
+
}
|
|
1346
|
+
if (provider.role !== 'tts' && maxConcurrentRequests !== 1) {
|
|
1347
|
+
fail('Only TTS providers may process more than one concurrent request.');
|
|
1348
|
+
}
|
|
1339
1349
|
for (const method of PROVIDER_METHODS) {
|
|
1340
1350
|
if (typeof provider[method] !== 'function') {
|
|
1341
1351
|
fail(`AI provider.${method} must be a function.`);
|
|
@@ -1345,7 +1355,8 @@ function validateProvider(provider) {
|
|
|
1345
1355
|
protocol: AI_PROVIDER_PROTOCOL,
|
|
1346
1356
|
role: provider.role,
|
|
1347
1357
|
id: provider.id,
|
|
1348
|
-
localOnly: provider.localOnly
|
|
1358
|
+
localOnly: provider.localOnly,
|
|
1359
|
+
maxConcurrentRequests
|
|
1349
1360
|
};
|
|
1350
1361
|
for (const method of PROVIDER_METHODS) {
|
|
1351
1362
|
record[method] = provider[method].bind(provider);
|
|
@@ -1650,7 +1661,7 @@ function createRoleSlot(role) {
|
|
|
1650
1661
|
unloadPromise: null,
|
|
1651
1662
|
disposePromise: null,
|
|
1652
1663
|
requestQueue: [],
|
|
1653
|
-
|
|
1664
|
+
activeRequests: new Map(),
|
|
1654
1665
|
disposed: false,
|
|
1655
1666
|
ready: false
|
|
1656
1667
|
};
|
|
@@ -2591,7 +2602,7 @@ export class AIProviderRuntime {
|
|
|
2591
2602
|
)
|
|
2592
2603
|
);
|
|
2593
2604
|
}
|
|
2594
|
-
if (slot.
|
|
2605
|
+
if (slot.activeRequests.size || slot.requestQueue.length) {
|
|
2595
2606
|
return Promise.reject(
|
|
2596
2607
|
operationError(
|
|
2597
2608
|
`AI role ${role} cannot load during active request ownership.`,
|
|
@@ -2831,10 +2842,11 @@ export class AIProviderRuntime {
|
|
|
2831
2842
|
return Promise.resolve(before);
|
|
2832
2843
|
}
|
|
2833
2844
|
const providerOwned = slot.ready
|
|
2845
|
+
|| slot.activeRequests.size > 0
|
|
2834
2846
|
|| providerStatus?.loaded === true
|
|
2835
2847
|
|| providerStatus?.busy === true;
|
|
2836
2848
|
if (!slot.loadPromise
|
|
2837
|
-
&&
|
|
2849
|
+
&& slot.activeRequests.size === 0
|
|
2838
2850
|
&& slot.requestQueue.length === 0
|
|
2839
2851
|
&& !providerOwned
|
|
2840
2852
|
&& (!provider || providerStatus)) {
|
|
@@ -2858,10 +2870,16 @@ export class AIProviderRuntime {
|
|
|
2858
2870
|
'ARCANE_AI_REQUEST_ABORTED'
|
|
2859
2871
|
);
|
|
2860
2872
|
this.#rejectQueuedRoleRequests(slot, requestCancellation);
|
|
2861
|
-
slot.
|
|
2873
|
+
const capturedRequestRecords = [...slot.activeRequests.values()];
|
|
2874
|
+
for (const requestRecord of capturedRequestRecords) {
|
|
2875
|
+
requestRecord.controller.abort(requestCancellation);
|
|
2876
|
+
}
|
|
2862
2877
|
const capturedLoad = slot.loadPromise;
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2878
|
+
const capturedRequests = capturedRequestRecords.map(
|
|
2879
|
+
function selectAIProviderRequestPromise(requestRecord) {
|
|
2880
|
+
return requestRecord.promise;
|
|
2881
|
+
}
|
|
2882
|
+
).filter(Boolean);
|
|
2865
2883
|
const unloadOperationId = providerOwned
|
|
2866
2884
|
? this.#nextOperationId(slot, 'unload')
|
|
2867
2885
|
: null;
|
|
@@ -2871,15 +2889,22 @@ export class AIProviderRuntime {
|
|
|
2871
2889
|
if (signal?.aborted) {
|
|
2872
2890
|
throw normalizedAbort();
|
|
2873
2891
|
}
|
|
2874
|
-
if (capturedRequestRecord?.cancel) {
|
|
2875
|
-
try {
|
|
2876
|
-
await capturedRequestRecord.cancel(requestCancellation);
|
|
2877
|
-
} catch {
|
|
2878
|
-
// Provider unload below remains the authoritative cleanup.
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
2892
|
await Promise.allSettled(
|
|
2882
|
-
|
|
2893
|
+
capturedRequestRecords.map(
|
|
2894
|
+
function cancelActiveAIProviderRequest(requestRecord) {
|
|
2895
|
+
if (!requestRecord.cancel) {
|
|
2896
|
+
return undefined;
|
|
2897
|
+
}
|
|
2898
|
+
return Promise.resolve().then(
|
|
2899
|
+
function invokeActiveAIProviderRequestCancellation() {
|
|
2900
|
+
return requestRecord.cancel(requestCancellation);
|
|
2901
|
+
}
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
)
|
|
2905
|
+
);
|
|
2906
|
+
await Promise.allSettled(
|
|
2907
|
+
[capturedLoad, ...capturedRequests].filter(Boolean)
|
|
2883
2908
|
);
|
|
2884
2909
|
if (signal?.aborted) {
|
|
2885
2910
|
throw normalizedAbort();
|
|
@@ -2926,6 +2951,7 @@ export class AIProviderRuntime {
|
|
|
2926
2951
|
{
|
|
2927
2952
|
state: 'unloading',
|
|
2928
2953
|
loaded: true,
|
|
2954
|
+
busy: capturedRequestRecords.length > 0,
|
|
2929
2955
|
operationId: unloadOperationId
|
|
2930
2956
|
}
|
|
2931
2957
|
)
|
|
@@ -3105,7 +3131,7 @@ export class AIProviderRuntime {
|
|
|
3105
3131
|
slot.unloadPromise = null;
|
|
3106
3132
|
slot.disposePromise = null;
|
|
3107
3133
|
slot.requestQueue = [];
|
|
3108
|
-
slot.
|
|
3134
|
+
slot.activeRequests.clear();
|
|
3109
3135
|
slot.ready = false;
|
|
3110
3136
|
slot.disposed = true;
|
|
3111
3137
|
}
|
|
@@ -3136,6 +3162,10 @@ export class AIProviderRuntime {
|
|
|
3136
3162
|
}
|
|
3137
3163
|
|
|
3138
3164
|
request(role, options = {}) {
|
|
3165
|
+
return this.#requestRole(role, options, false);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
#requestRole(role, options, queued) {
|
|
3139
3169
|
this.#assertOpen();
|
|
3140
3170
|
this.#assertNotConfiguring();
|
|
3141
3171
|
assertRole(role);
|
|
@@ -3234,13 +3264,16 @@ export class AIProviderRuntime {
|
|
|
3234
3264
|
)
|
|
3235
3265
|
);
|
|
3236
3266
|
}
|
|
3237
|
-
|
|
3267
|
+
const maxConcurrentRequests = this.#providerRequestCapacity(provider);
|
|
3268
|
+
if ((!queued && slot.requestQueue.length)
|
|
3269
|
+
|| slot.activeRequests.size >= maxConcurrentRequests) {
|
|
3238
3270
|
return this.#enqueueRoleRequest(slot, options);
|
|
3239
3271
|
}
|
|
3240
3272
|
if (!slot.ready
|
|
3241
3273
|
|| providerStatus.state !== 'ready'
|
|
3242
3274
|
|| providerStatus.loaded !== true
|
|
3243
|
-
|| providerStatus.busy !== false
|
|
3275
|
+
|| (providerStatus.busy !== false
|
|
3276
|
+
&& slot.activeRequests.size === 0)) {
|
|
3244
3277
|
slot.ready = false;
|
|
3245
3278
|
return Promise.reject(
|
|
3246
3279
|
operationError(
|
|
@@ -3258,31 +3291,24 @@ export class AIProviderRuntime {
|
|
|
3258
3291
|
const runtime = this;
|
|
3259
3292
|
const requestRecord = {
|
|
3260
3293
|
controller,
|
|
3294
|
+
operationId,
|
|
3261
3295
|
requestSequence,
|
|
3262
3296
|
promise: null,
|
|
3263
3297
|
cancel: null
|
|
3264
3298
|
};
|
|
3265
|
-
slot.
|
|
3266
|
-
|
|
3267
|
-
role,
|
|
3268
|
-
roleRecord(
|
|
3269
|
-
role,
|
|
3270
|
-
slot.selection,
|
|
3271
|
-
{
|
|
3272
|
-
state: 'ready',
|
|
3273
|
-
loaded: true,
|
|
3274
|
-
busy: true,
|
|
3275
|
-
operationId
|
|
3276
|
-
}
|
|
3277
|
-
)
|
|
3278
|
-
);
|
|
3299
|
+
slot.activeRequests.set(requestSequence, requestRecord);
|
|
3300
|
+
this.#publishRoleRequestState(slot);
|
|
3279
3301
|
|
|
3280
3302
|
function restoreAIProviderRoleAfterRequest(error) {
|
|
3281
3303
|
if (slot.generation !== generation
|
|
3282
|
-
|| slot.requestSequence !== requestSequence
|
|
3283
3304
|
|| slot.unloadPromise) {
|
|
3284
3305
|
return;
|
|
3285
3306
|
}
|
|
3307
|
+
if (slot.activeRequests.size || slot.requestQueue.length) {
|
|
3308
|
+
slot.ready = true;
|
|
3309
|
+
runtime.#publishRoleRequestState(slot);
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3286
3312
|
let providerState = null;
|
|
3287
3313
|
try {
|
|
3288
3314
|
providerState = validateProviderStatus(provider.status());
|
|
@@ -3398,8 +3424,8 @@ export class AIProviderRuntime {
|
|
|
3398
3424
|
cancelAbortedAIProviderStream
|
|
3399
3425
|
);
|
|
3400
3426
|
detachSignal();
|
|
3401
|
-
if (slot.
|
|
3402
|
-
slot.
|
|
3427
|
+
if (slot.activeRequests.get(requestSequence) === requestRecord) {
|
|
3428
|
+
slot.activeRequests.delete(requestSequence);
|
|
3403
3429
|
}
|
|
3404
3430
|
runtime.#drainRoleRequestQueue(slot);
|
|
3405
3431
|
if (error) {
|
|
@@ -3592,6 +3618,7 @@ export class AIProviderRuntime {
|
|
|
3592
3618
|
slot,
|
|
3593
3619
|
generation,
|
|
3594
3620
|
requestSequence,
|
|
3621
|
+
requestRecord,
|
|
3595
3622
|
controller.signal
|
|
3596
3623
|
);
|
|
3597
3624
|
privateStreamObservation = (
|
|
@@ -3603,6 +3630,7 @@ export class AIProviderRuntime {
|
|
|
3603
3630
|
slot,
|
|
3604
3631
|
generation,
|
|
3605
3632
|
requestSequence,
|
|
3633
|
+
requestRecord,
|
|
3606
3634
|
controller.signal
|
|
3607
3635
|
);
|
|
3608
3636
|
if (result.done) {
|
|
@@ -3643,6 +3671,7 @@ export class AIProviderRuntime {
|
|
|
3643
3671
|
slot,
|
|
3644
3672
|
generation,
|
|
3645
3673
|
requestSequence,
|
|
3674
|
+
requestRecord,
|
|
3646
3675
|
controller.signal
|
|
3647
3676
|
);
|
|
3648
3677
|
const terminalValue = role === 'llm'
|
|
@@ -3773,6 +3802,7 @@ export class AIProviderRuntime {
|
|
|
3773
3802
|
slot,
|
|
3774
3803
|
generation,
|
|
3775
3804
|
requestSequence,
|
|
3805
|
+
requestRecord,
|
|
3776
3806
|
controller.signal
|
|
3777
3807
|
);
|
|
3778
3808
|
return role === 'llm'
|
|
@@ -3786,18 +3816,15 @@ export class AIProviderRuntime {
|
|
|
3786
3816
|
? normalizedAbort(error)
|
|
3787
3817
|
: error;
|
|
3788
3818
|
requestError = normalized;
|
|
3789
|
-
restoreAIProviderRoleAfterRequest(normalized);
|
|
3790
3819
|
throw normalized;
|
|
3791
3820
|
} finally {
|
|
3792
3821
|
detachSignal();
|
|
3793
|
-
if (slot.
|
|
3794
|
-
slot.
|
|
3822
|
+
if (slot.activeRequests.get(requestSequence) === requestRecord) {
|
|
3823
|
+
slot.activeRequests.delete(requestSequence);
|
|
3795
3824
|
}
|
|
3796
3825
|
runtime.#drainRoleRequestQueue(slot);
|
|
3797
|
-
if (!
|
|
3798
|
-
|
|
3799
|
-
&& !slot.unloadPromise) {
|
|
3800
|
-
restoreAIProviderRoleAfterRequest(null);
|
|
3826
|
+
if (slot.generation === generation && !slot.unloadPromise) {
|
|
3827
|
+
restoreAIProviderRoleAfterRequest(requestError);
|
|
3801
3828
|
}
|
|
3802
3829
|
}
|
|
3803
3830
|
})();
|
|
@@ -3823,10 +3850,10 @@ export class AIProviderRuntime {
|
|
|
3823
3850
|
cancel(role) {
|
|
3824
3851
|
assertRole(role);
|
|
3825
3852
|
const slot = this.#slots[role];
|
|
3826
|
-
|
|
3853
|
+
const requestRecord = slot.activeRequests.values().next().value ?? null;
|
|
3854
|
+
if (!requestRecord) {
|
|
3827
3855
|
return false;
|
|
3828
3856
|
}
|
|
3829
|
-
const requestRecord = slot.request;
|
|
3830
3857
|
const reason = operationError(
|
|
3831
3858
|
`AI role ${role} request was cancelled.`,
|
|
3832
3859
|
'ARCANE_AI_REQUEST_ABORTED'
|
|
@@ -3958,39 +3985,37 @@ export class AIProviderRuntime {
|
|
|
3958
3985
|
};
|
|
3959
3986
|
}
|
|
3960
3987
|
slot.requestQueue.push(entry);
|
|
3988
|
+
runtime.#publishRoleRequestState(slot);
|
|
3961
3989
|
runtime.#drainRoleRequestQueue(slot);
|
|
3962
3990
|
});
|
|
3963
3991
|
}
|
|
3964
3992
|
|
|
3965
3993
|
#drainRoleRequestQueue(slot) {
|
|
3966
|
-
if (slot.
|
|
3967
|
-
|| slot.unloadPromise
|
|
3994
|
+
if (slot.unloadPromise
|
|
3968
3995
|
|| slot.disposePromise
|
|
3969
3996
|
|| slot.disposed) {
|
|
3970
3997
|
return;
|
|
3971
3998
|
}
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
return;
|
|
3975
|
-
}
|
|
3976
|
-
entry.detachSignal?.();
|
|
3977
|
-
if (entry.options.signal?.aborted) {
|
|
3978
|
-
entry.reject(normalizedAbort(entry.options.signal.reason));
|
|
3979
|
-
this.#drainRoleRequestQueue(slot);
|
|
3980
|
-
return;
|
|
3981
|
-
}
|
|
3999
|
+
const provider = this.#providerFor(slot);
|
|
4000
|
+
const maxConcurrentRequests = this.#providerRequestCapacity(provider);
|
|
3982
4001
|
const runtime = this;
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4002
|
+
while (slot.requestQueue.length
|
|
4003
|
+
&& slot.activeRequests.size < maxConcurrentRequests) {
|
|
4004
|
+
const entry = slot.requestQueue.shift();
|
|
4005
|
+
entry.detachSignal?.();
|
|
4006
|
+
if (entry.options.signal?.aborted) {
|
|
4007
|
+
entry.reject(normalizedAbort(entry.options.signal.reason));
|
|
4008
|
+
continue;
|
|
3986
4009
|
}
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
4010
|
+
let operation;
|
|
4011
|
+
try {
|
|
4012
|
+
operation = runtime.#requestRole(slot.role, entry.options, true);
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
entry.reject(error);
|
|
4015
|
+
continue;
|
|
3992
4016
|
}
|
|
3993
|
-
|
|
4017
|
+
Promise.resolve(operation).then(entry.resolve, entry.reject);
|
|
4018
|
+
}
|
|
3994
4019
|
}
|
|
3995
4020
|
|
|
3996
4021
|
#rejectQueuedRoleRequests(slot, error) {
|
|
@@ -4039,7 +4064,7 @@ export class AIProviderRuntime {
|
|
|
4039
4064
|
slot.loadPromise
|
|
4040
4065
|
|| slot.unloadPromise
|
|
4041
4066
|
|| slot.disposePromise
|
|
4042
|
-
|| slot.
|
|
4067
|
+
|| slot.activeRequests.size > 0
|
|
4043
4068
|
|| slot.requestQueue.length > 0
|
|
4044
4069
|
|| slot.ready
|
|
4045
4070
|
);
|
|
@@ -4086,9 +4111,15 @@ export class AIProviderRuntime {
|
|
|
4086
4111
|
}
|
|
4087
4112
|
}
|
|
4088
4113
|
|
|
4089
|
-
#assertCurrentRequest(
|
|
4114
|
+
#assertCurrentRequest(
|
|
4115
|
+
slot,
|
|
4116
|
+
generation,
|
|
4117
|
+
requestSequence,
|
|
4118
|
+
requestRecord,
|
|
4119
|
+
signal
|
|
4120
|
+
) {
|
|
4090
4121
|
this.#assertCurrentOperation(slot, generation, signal);
|
|
4091
|
-
if (slot.requestSequence !==
|
|
4122
|
+
if (slot.activeRequests.get(requestSequence) !== requestRecord) {
|
|
4092
4123
|
throw operationError(
|
|
4093
4124
|
`AI role ${slot.role} no longer owns the active provider request.`,
|
|
4094
4125
|
'ARCANE_AI_REQUEST_STALE'
|
|
@@ -4096,6 +4127,33 @@ export class AIProviderRuntime {
|
|
|
4096
4127
|
}
|
|
4097
4128
|
}
|
|
4098
4129
|
|
|
4130
|
+
#providerRequestCapacity(provider) {
|
|
4131
|
+
return provider?.role === 'tts'
|
|
4132
|
+
? provider.maxConcurrentRequests
|
|
4133
|
+
: 1;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
#publishRoleRequestState(slot) {
|
|
4137
|
+
const requestRecord = slot.activeRequests.values().next().value ?? null;
|
|
4138
|
+
if (!requestRecord && slot.requestQueue.length === 0) {
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
publishAIRuntimeRoleState(
|
|
4142
|
+
slot.role,
|
|
4143
|
+
roleRecord(
|
|
4144
|
+
slot.role,
|
|
4145
|
+
slot.selection,
|
|
4146
|
+
{
|
|
4147
|
+
state: 'ready',
|
|
4148
|
+
loaded: true,
|
|
4149
|
+
busy: true,
|
|
4150
|
+
operationId: requestRecord?.operationId ?? null
|
|
4151
|
+
}
|
|
4152
|
+
)
|
|
4153
|
+
);
|
|
4154
|
+
return true;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4099
4157
|
#forwardAbort(signal, controller) {
|
|
4100
4158
|
if (!signal) {
|
|
4101
4159
|
return function detachAbsentAIProviderAbort() {};
|
|
@@ -4133,6 +4191,7 @@ export class AIProviderRuntime {
|
|
|
4133
4191
|
}
|
|
4134
4192
|
|
|
4135
4193
|
#publishRoleError(slot, error, loaded) {
|
|
4194
|
+
const requestRecord = slot.activeRequests.values().next().value ?? null;
|
|
4136
4195
|
publishAIRuntimeRoleState(
|
|
4137
4196
|
slot.role,
|
|
4138
4197
|
roleRecord(
|
|
@@ -4141,6 +4200,8 @@ export class AIProviderRuntime {
|
|
|
4141
4200
|
{
|
|
4142
4201
|
state: 'error',
|
|
4143
4202
|
loaded,
|
|
4203
|
+
busy: Boolean(requestRecord || slot.requestQueue.length),
|
|
4204
|
+
operationId: requestRecord?.operationId ?? null,
|
|
4144
4205
|
error: stateError(
|
|
4145
4206
|
error,
|
|
4146
4207
|
'ARCANE_AI_PROVIDER_OPERATION_FAILED'
|
|
@@ -82,24 +82,9 @@ const STARTUP_TERMINAL_STATES = new Set([
|
|
|
82
82
|
'disposed'
|
|
83
83
|
]);
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
* @deprecated Subscribe through the focused runtime helpers or arcaneEvents.
|
|
87
|
-
* This state-free EventTarget compatibility view delegates to the module's
|
|
88
|
-
* canonical source; it does not own a second listener registry or event bus.
|
|
89
|
-
*/
|
|
90
|
-
const aiRuntimeEventCompatibilityView = {
|
|
91
|
-
addEventListener(type, listener, options) {
|
|
92
|
-
return aiRuntimeEventSource.addEventListener(type, listener, options);
|
|
93
|
-
},
|
|
94
|
-
removeEventListener(type, listener, options) {
|
|
95
|
-
return aiRuntimeEventSource.removeEventListener(type, listener, options);
|
|
96
|
-
},
|
|
97
|
-
dispatchEvent(event) {
|
|
98
|
-
return aiRuntimeEventSource.dispatchEvent(event);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
85
|
+
const aiRuntimeEventOwner = {};
|
|
101
86
|
const aiRuntimeEventSource = createArcaneEventSource(
|
|
102
|
-
|
|
87
|
+
aiRuntimeEventOwner,
|
|
103
88
|
{
|
|
104
89
|
source: 'ai-runtime-state',
|
|
105
90
|
eventTypes: completeValue([
|
|
@@ -109,7 +94,6 @@ const aiRuntimeEventSource = createArcaneEventSource(
|
|
|
109
94
|
])
|
|
110
95
|
}
|
|
111
96
|
);
|
|
112
|
-
export const aiRuntimeEvents = completeValue(aiRuntimeEventCompatibilityView);
|
|
113
97
|
|
|
114
98
|
function fail(message) {
|
|
115
99
|
throw new TypeError(`ARCANE_AI_RUNTIME_STATE_INVALID: ${message}`);
|
|
@@ -12,14 +12,11 @@ export const BROWSER_TEST_SUITE_EVENT_TYPES={
|
|
|
12
12
|
export const BROWSER_TEST_SUITE_ERROR_CODES={
|
|
13
13
|
assertion:'BROWSER_TEST_ASSERTION',
|
|
14
14
|
busy:'BROWSER_TEST_BUSY',
|
|
15
|
-
|
|
15
|
+
callbackRejected:'BROWSER_TEST_ERROR',
|
|
16
16
|
clockInvalid:'BROWSER_TEST_INVALID_CLOCK',
|
|
17
17
|
descriptorCaseCollision:'BROWSER_TEST_CASE_COLLISION',
|
|
18
|
-
descriptorDuplicate:'BROWSER_TEST_CASE_COLLISION',
|
|
19
18
|
descriptorInvalid:'BROWSER_TEST_INVALID_DESCRIPTOR',
|
|
20
19
|
disposed:'BROWSER_TEST_SUITE_DISPOSED',
|
|
21
|
-
limitExceeded:'BROWSER_TEST_LIMIT',
|
|
22
|
-
limitInvalid:'BROWSER_TEST_INVALID_LIMIT',
|
|
23
20
|
optionsInvalid:'BROWSER_TEST_INVALID_OPTIONS',
|
|
24
21
|
resultInvalid:'BROWSER_TEST_INVALID_RESULT',
|
|
25
22
|
runAborted:'BROWSER_TEST_ABORTED',
|
|
@@ -230,7 +227,7 @@ function browserTestResultReason(result){
|
|
|
230
227
|
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.timedOut){
|
|
231
228
|
return BROWSER_TEST_SUITE_REASONS.testTimedOut;
|
|
232
229
|
}
|
|
233
|
-
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.
|
|
230
|
+
if(result.code===BROWSER_TEST_SUITE_ERROR_CODES.callbackRejected){
|
|
234
231
|
return BROWSER_TEST_SUITE_REASONS.testCallbackRejected;
|
|
235
232
|
}
|
|
236
233
|
if(result.status==='pass')return BROWSER_TEST_SUITE_REASONS.testPassed;
|
|
@@ -50,10 +50,6 @@ export default class CalculatorEngine{
|
|
|
50
50
|
#events;
|
|
51
51
|
#operationSequence=0;
|
|
52
52
|
constructor(){this.#events=createArcaneEventSource(this,{source:'calculator-engine',eventTypes:['calculator-result','calculator-error']});}
|
|
53
|
-
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
54
|
-
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
55
|
-
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
56
|
-
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
57
53
|
calculate(expression){
|
|
58
54
|
if(this.#disposed)throw disposedError();
|
|
59
55
|
const operationId=`${this.#events.instanceId}:calculate:${(++this.#operationSequence).toString(36)}`;
|
|
@@ -21,7 +21,175 @@ function hasConversationEntry(chat=[]){
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function plainRecord(value){
|
|
25
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return false;
|
|
26
|
+
const prototype=Object.getPrototypeOf(value);
|
|
27
|
+
return prototype===Object.prototype||prototype===null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coded(error,code){
|
|
31
|
+
if(!error.code)error.code=code;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function copyCompleteValue(value,seen=new Map()){
|
|
36
|
+
if(value===null||typeof value!=='object')return value;
|
|
37
|
+
if(seen.has(value))return seen.get(value);
|
|
38
|
+
if(Array.isArray(value)){
|
|
39
|
+
const result=[];
|
|
40
|
+
seen.set(value,result);
|
|
41
|
+
for(const item of value)result.push(copyCompleteValue(item,seen));
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
const result={};
|
|
45
|
+
seen.set(value,result);
|
|
46
|
+
for(const [key,descriptor] of Object.entries(
|
|
47
|
+
Object.getOwnPropertyDescriptors(value)
|
|
48
|
+
)){
|
|
49
|
+
if(Object.hasOwn(descriptor,'value')){
|
|
50
|
+
result[key]=copyCompleteValue(descriptor.value,seen);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function providerMessageCopy(message){
|
|
57
|
+
const copy=copyCompleteValue(message);
|
|
58
|
+
if(copy.role==='tool'){
|
|
59
|
+
delete copy.message;
|
|
60
|
+
delete copy.name;
|
|
61
|
+
delete copy.status;
|
|
62
|
+
}
|
|
63
|
+
delete copy.memory_excluded;
|
|
64
|
+
delete copy.persistence_excluded;
|
|
65
|
+
delete copy.persistence_message;
|
|
66
|
+
delete copy.persistence_name;
|
|
67
|
+
delete copy.persistence_status;
|
|
68
|
+
delete copy.timestamp;
|
|
69
|
+
delete copy.ui_hidden;
|
|
70
|
+
return copy;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function structuralToolMessage(call,label){
|
|
74
|
+
const argumentValue=call?.function?.arguments;
|
|
75
|
+
if(typeof argumentValue!=='string'){
|
|
76
|
+
throw coded(
|
|
77
|
+
new TypeError(`${label}.function.arguments must be a JSON string.`),
|
|
78
|
+
'AI_CHAT_INVALID_TOOL_CALL'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
let argumentsRecord;
|
|
82
|
+
try{
|
|
83
|
+
argumentsRecord=JSON.parse(argumentValue);
|
|
84
|
+
}catch(cause){
|
|
85
|
+
throw coded(
|
|
86
|
+
new TypeError(`${label}.function.arguments must encode a JSON object.`,{cause}),
|
|
87
|
+
'AI_CHAT_INVALID_TOOL_CALL'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if(!plainRecord(argumentsRecord)){
|
|
91
|
+
throw coded(
|
|
92
|
+
new TypeError(`${label}.function.arguments must encode a JSON object.`),
|
|
93
|
+
'AI_CHAT_INVALID_TOOL_CALL'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if(typeof argumentsRecord.message!=='string'||!argumentsRecord.message.trim()){
|
|
97
|
+
throw coded(
|
|
98
|
+
new TypeError(`${label}.function.arguments.message must contain user-facing text.`),
|
|
99
|
+
'AI_CHAT_TOOL_MESSAGE_REQUIRED'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return argumentsRecord.message;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function activeToolProtocolState(messages){
|
|
106
|
+
const pending=new Set();
|
|
107
|
+
let start=-1;
|
|
108
|
+
for(let index=0;index<messages.length;index++){
|
|
109
|
+
const message=messages[index];
|
|
110
|
+
if(message?.role==='assistant'){
|
|
111
|
+
if(start>=0&&!pending.size)start=-1;
|
|
112
|
+
if(Array.isArray(message.tool_calls)){
|
|
113
|
+
if(!pending.size&&message.tool_calls.length)start=index;
|
|
114
|
+
for(const call of message.tool_calls){
|
|
115
|
+
if(typeof call?.id==='string'&&call.id)pending.add(call.id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if(message?.role==='tool'&&typeof message.tool_call_id==='string'){
|
|
120
|
+
pending.delete(message.tool_call_id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {pending:pending.size,start};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function appendOrdinaryMessages(result,message,index){
|
|
127
|
+
if(!plainRecord(message))return;
|
|
128
|
+
if(['system','user'].includes(message.role)){
|
|
129
|
+
if(Object.hasOwn(message,'content')){
|
|
130
|
+
result.push({role:message.role,content:message.content});
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if(message.role==='assistant'){
|
|
135
|
+
if(message.content!==undefined&&message.content!==null&&String(message.content)){
|
|
136
|
+
result.push({role:'assistant',content:String(message.content)});
|
|
137
|
+
}
|
|
138
|
+
if(message.tool_calls===undefined)return;
|
|
139
|
+
if(!Array.isArray(message.tool_calls)){
|
|
140
|
+
throw coded(
|
|
141
|
+
new TypeError(`Chat message ${index+1}.tool_calls must be an array.`),
|
|
142
|
+
'AI_CHAT_INVALID_TOOL_CALL'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
for(let callIndex=0;callIndex<message.tool_calls.length;callIndex++){
|
|
146
|
+
result.push({
|
|
147
|
+
role:'assistant',
|
|
148
|
+
content:structuralToolMessage(
|
|
149
|
+
message.tool_calls[callIndex],
|
|
150
|
+
`Chat message ${index+1}.tool_calls[${callIndex}]`
|
|
151
|
+
)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if(message.role!=='tool')return;
|
|
157
|
+
const content=typeof message.tool_call_id==='string'
|
|
158
|
+
?message.persistence_message??message.message
|
|
159
|
+
:message.content;
|
|
160
|
+
if(typeof content==='string'&&content.trim()){
|
|
161
|
+
result.push({role:'assistant',content});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Projects retained chat state into recurring provider context. Raw structural
|
|
167
|
+
* protocol is preserved only for the one unresolved continuation at the tail.
|
|
168
|
+
* Settled exchanges become complete ordinary visible conversation messages.
|
|
169
|
+
*/
|
|
170
|
+
function recurringChatMessages(chat=[],{settleCompleteToolTail=false}={}){
|
|
171
|
+
const messages=Array.isArray(chat)
|
|
172
|
+
?chat
|
|
173
|
+
:chat?.messages||[];
|
|
174
|
+
const active=activeToolProtocolState(messages);
|
|
175
|
+
const activeStart=settleCompleteToolTail&&!active.pending
|
|
176
|
+
?-1
|
|
177
|
+
:active.start;
|
|
178
|
+
const settledEnd=activeStart<0?messages.length:activeStart;
|
|
179
|
+
const result=[];
|
|
180
|
+
for(let index=0;index<settledEnd;index++){
|
|
181
|
+
appendOrdinaryMessages(result,messages[index],index);
|
|
182
|
+
}
|
|
183
|
+
if(activeStart>=0){
|
|
184
|
+
for(let index=activeStart;index<messages.length;index++){
|
|
185
|
+
result.push(providerMessageCopy(messages[index]));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
24
191
|
export {
|
|
25
192
|
hasConversationEntry,
|
|
26
|
-
hasUserEntry
|
|
193
|
+
hasUserEntry,
|
|
194
|
+
recurringChatMessages
|
|
27
195
|
};
|