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.
Files changed (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +39 -27
  3. package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
  4. package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
  5. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
  6. package/browser-runtime/ai/speech-worker-client.mjs +51 -14
  7. package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
  8. package/browser-runtime/dependencies/strong-type/package.json +22 -23
  9. package/browser-runtime/event-manager.mjs +27 -41
  10. package/package.json +2 -3
  11. package/runtime/arcane/components/chat.html +1 -1
  12. package/runtime/arcane/components/speech.html +8 -38
  13. package/runtime/arcane/components/voice-transcription.html +1 -1
  14. package/runtime/arcane/css/theme.css +1 -1
  15. package/runtime/arcane/entities/Chat.js +12 -7
  16. package/runtime/arcane/modules/AI.js +569 -402
  17. package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
  18. package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
  19. package/runtime/arcane/modules/AIRuntimeState.js +2 -18
  20. package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
  21. package/runtime/arcane/modules/CalculatorEngine.js +0 -4
  22. package/runtime/arcane/modules/ChatRecords.js +169 -1
  23. package/runtime/arcane/modules/CommunicationHub.js +0 -19
  24. package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
  25. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
  26. package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
  27. package/runtime/arcane/modules/Errors.js +6 -19
  28. package/runtime/arcane/modules/Mail.js +1 -2
  29. package/runtime/arcane/modules/MailTransport.mjs +1 -3
  30. package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
  31. package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
  32. package/runtime/arcane/modules/ScreenCapture.js +2 -4
  33. package/runtime/arcane/modules/SpeechPlayback.js +0 -27
  34. package/runtime/arcane/modules/WaitForComponent.js +0 -1
  35. package/src/app-descriptor.mjs +1 -1
  36. package/src/doctor.mjs +1 -3
  37. package/src/event-manager.mjs +27 -41
  38. package/src/import-map.mjs +1 -1
  39. package/src/index.mjs +2 -9
  40. package/src/installed-sdk-runtime.mjs +1 -11
  41. package/src/mail-api.mjs +0 -1
  42. package/src/native-provider-loader.mjs +0 -9
  43. package/src/scaffold.mjs +9 -21
  44. package/src/toolchain.mjs +5 -28
  45. package/src/workspace-runtime.mjs +0 -4
  46. 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 legacy aliases.
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
- request: null,
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.request || slot.requestQueue.length) {
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
- && !slot.request
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.request?.controller.abort(requestCancellation);
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 capturedRequestRecord = slot.request;
2864
- const capturedRequest = capturedRequestRecord?.promise ?? null;
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
- [capturedLoad, capturedRequest].filter(Boolean)
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.request = null;
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
- if (slot.request) {
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.request = requestRecord;
3266
- publishAIRuntimeRoleState(
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.request === requestRecord) {
3402
- slot.request = null;
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.request === requestRecord) {
3794
- slot.request = null;
3822
+ if (slot.activeRequests.get(requestSequence) === requestRecord) {
3823
+ slot.activeRequests.delete(requestSequence);
3795
3824
  }
3796
3825
  runtime.#drainRoleRequestQueue(slot);
3797
- if (!requestError
3798
- && slot.generation === generation
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
- if (!slot.request) {
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.request
3967
- || slot.unloadPromise
3994
+ if (slot.unloadPromise
3968
3995
  || slot.disposePromise
3969
3996
  || slot.disposed) {
3970
3997
  return;
3971
3998
  }
3972
- const entry = slot.requestQueue.shift();
3973
- if (!entry) {
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
- Promise.resolve().then(
3984
- function runQueuedAIProviderRoleRequest() {
3985
- return runtime.request(slot.role, entry.options);
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
- ).then(entry.resolve, entry.reject).finally(
3988
- function continueAIProviderRoleRequestQueue() {
3989
- if (!slot.request) {
3990
- runtime.#drainRoleRequestQueue(slot);
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.request
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(slot, generation, requestSequence, signal) {
4114
+ #assertCurrentRequest(
4115
+ slot,
4116
+ generation,
4117
+ requestSequence,
4118
+ requestRecord,
4119
+ signal
4120
+ ) {
4090
4121
  this.#assertCurrentOperation(slot, generation, signal);
4091
- if (slot.requestSequence !== 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
- aiRuntimeEventCompatibilityView,
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
- callbackRejectedLegacy:'BROWSER_TEST_ERROR',
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.callbackRejectedLegacy){
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
  };