arcane-os 0.3.0 → 0.3.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.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -7,13 +7,15 @@ import {
7
7
  subscribeAIRuntimeIntents
8
8
  } from './AIRuntimeState.js';
9
9
 
10
+ const completeValue=(value)=>value;
11
+
10
12
  export const AI_PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
11
13
  export const AI_PROVIDER_RUNTIME_PROTOCOL = 'arcane-ai-runtime/2';
12
14
  export const AI_MODEL_AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
13
15
 
14
16
  const ROLE_SET = new Set(AI_RUNTIME_ROLES);
15
- const SPEECH_ROLES = Object.freeze(['stt', 'tts']);
16
- const PROVIDER_METHODS = Object.freeze([
17
+ const SPEECH_ROLES = completeValue(['stt', 'tts']);
18
+ const PROVIDER_METHODS = completeValue([
17
19
  'catalog',
18
20
  'inspect',
19
21
  'status',
@@ -22,25 +24,22 @@ const PROVIDER_METHODS = Object.freeze([
22
24
  'unload',
23
25
  'dispose'
24
26
  ]);
25
- const ROLE_OPERATIONS = Object.freeze(
27
+ const ROLE_OPERATIONS = completeValue(
26
28
  {
27
- llm: Object.freeze(['chat', 'stream']),
28
- stt: Object.freeze(['transcribe']),
29
- tts: Object.freeze(['synthesize'])
29
+ llm: completeValue(['chat', 'stream']),
30
+ stt: completeValue(['transcribe']),
31
+ tts: completeValue(['synthesize'])
30
32
  }
31
33
  );
32
- const ROLE_OPERATION_SETS = Object.freeze(
34
+ const ROLE_OPERATION_SETS = completeValue(
33
35
  {
34
36
  llm: new Set(ROLE_OPERATIONS.llm),
35
37
  stt: new Set(ROLE_OPERATIONS.stt),
36
38
  tts: new Set(ROLE_OPERATIONS.tts)
37
39
  }
38
40
  );
39
- const IDENTIFIER_LIMIT = 128;
40
- const ERROR_MESSAGE_LIMIT = 512;
41
- const STREAM_CLEANUP_TIMEOUT_MS = 2_000;
42
- const ROUTE_KEYS = Object.freeze(['default', 'localOnly']);
43
- const RUNTIME_CONSTRUCTION_AUTHORITY = Object.freeze({});
41
+ const ROUTE_KEYS = completeValue(['default', 'localOnly']);
42
+ const RUNTIME_CONSTRUCTION_AUTHORITY = completeValue({});
44
43
  const PROVIDER_RECORDS = new WeakMap();
45
44
 
46
45
  function fail(message, code = 'ARCANE_AI_PROVIDER_RUNTIME_INVALID') {
@@ -80,30 +79,9 @@ function isAbort(error, signal) {
80
79
  || error?.code === 'ARCANE_REQUEST_ABORTED';
81
80
  }
82
81
 
83
- async function awaitBoundedStreamCleanup(operation) {
84
- let timer = null;
85
- const timeout = new Promise(function resolveBoundedAIStreamCleanup(resolve) {
86
- timer = setTimeout(
87
- function settleAIStreamCleanupTimeout() {
88
- resolve({completed: false, results: null});
89
- },
90
- STREAM_CLEANUP_TIMEOUT_MS
91
- );
92
- });
93
- try {
94
- return await Promise.race([
95
- Promise.resolve(operation).then(
96
- function settleAIStreamCleanup(results) {
97
- return {completed: true, results};
98
- }
99
- ),
100
- timeout
101
- ]);
102
- } finally {
103
- if (timer !== null) {
104
- clearTimeout(timer);
105
- }
106
- }
82
+ async function awaitStreamCleanup(operation) {
83
+ const results = await Promise.resolve(operation);
84
+ return {completed: true, results};
107
85
  }
108
86
 
109
87
  function assertStreamCleanupComplete(outcome) {
@@ -114,7 +92,7 @@ function assertStreamCleanupComplete(outcome) {
114
92
  });
115
93
  if (!outcome.completed || rejected) {
116
94
  throw operationError(
117
- 'The AI provider stream did not confirm bounded cleanup.',
95
+ 'The AI provider stream did not confirm cleanup.',
118
96
  'ARCANE_AI_STREAM_CLEANUP_INCOMPLETE'
119
97
  );
120
98
  }
@@ -129,10 +107,19 @@ function assertRole(role) {
129
107
  function assertIdentifier(value, label) {
130
108
  if (typeof value !== 'string'
131
109
  || value.length < 1
132
- || value.length > IDENTIFIER_LIMIT
133
110
  || value.trim() !== value) {
134
- fail(`${label} must be a trimmed 1-${IDENTIFIER_LIMIT} character string.`);
111
+ fail(`${label} must be a nonempty trimmed string.`);
112
+ }
113
+ }
114
+
115
+ function nextSequence(value) {
116
+ if (typeof value === 'bigint') {
117
+ return value + 1n;
118
+ }
119
+ if (value === Number.MAX_SAFE_INTEGER) {
120
+ return BigInt(value) + 1n;
135
121
  }
122
+ return value + 1;
136
123
  }
137
124
 
138
125
  function assertAbortSignal(signal) {
@@ -159,7 +146,366 @@ function assertPlainObject(value, label) {
159
146
  }
160
147
  }
161
148
 
162
- function assertCallbackFreeProviderValue(value, seen = new WeakSet(), depth = 0) {
149
+ function isPlainRecord(value) {
150
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
151
+ return false;
152
+ }
153
+ const prototype = Object.getPrototypeOf(value);
154
+ return prototype === Object.prototype || prototype === null;
155
+ }
156
+
157
+ function validateLLMToolDeclarations(value) {
158
+ if (value === undefined) {
159
+ return;
160
+ }
161
+ if (!Array.isArray(value)) {
162
+ fail(
163
+ 'AI LLM tools must be an array.',
164
+ 'ARCANE_AI_TOOL_CALL_INVALID'
165
+ );
166
+ }
167
+ for (let index = 0; index < value.length; index += 1) {
168
+ const tool = value[index];
169
+ const parameters = tool?.function?.parameters;
170
+ const messageSchema = parameters?.properties?.message;
171
+ if (!isPlainRecord(tool)
172
+ || tool.type !== 'function'
173
+ || !isPlainRecord(tool.function)
174
+ || !isPlainRecord(parameters)
175
+ || parameters.type !== 'object'
176
+ || !isPlainRecord(parameters.properties)
177
+ || !isPlainRecord(messageSchema)
178
+ || messageSchema.type !== 'string'
179
+ || !Number.isInteger(messageSchema.minLength)
180
+ || messageSchema.minLength < 1
181
+ || !Array.isArray(parameters.required)
182
+ || !parameters.required.includes('message')) {
183
+ fail(
184
+ `AI LLM tools[${index}] must require a nonempty string parameters.properties.message.`,
185
+ 'ARCANE_AI_TOOL_MESSAGE_REQUIRED'
186
+ );
187
+ }
188
+ }
189
+ }
190
+
191
+ function validateLLMToolCall(call, label) {
192
+ if (!isPlainRecord(call)
193
+ || typeof call.id !== 'string'
194
+ || !call.id.trim()
195
+ || call.type !== 'function'
196
+ || !isPlainRecord(call.function)
197
+ || typeof call.function.name !== 'string'
198
+ || !call.function.name.trim()
199
+ || typeof call.function.arguments !== 'string') {
200
+ fail(
201
+ `${label} must be one complete structural function call.`,
202
+ 'ARCANE_AI_TOOL_CALL_INVALID'
203
+ );
204
+ }
205
+ let argumentsRecord;
206
+ try {
207
+ argumentsRecord = JSON.parse(call.function.arguments);
208
+ } catch (cause) {
209
+ throw operationError(
210
+ `${label} arguments must encode a JSON object.`,
211
+ 'ARCANE_AI_TOOL_CALL_INVALID',
212
+ cause
213
+ );
214
+ }
215
+ if (!isPlainRecord(argumentsRecord)) {
216
+ fail(
217
+ `${label} arguments must encode a JSON object.`,
218
+ 'ARCANE_AI_TOOL_CALL_INVALID'
219
+ );
220
+ }
221
+ if (typeof argumentsRecord.message !== 'string'
222
+ || !argumentsRecord.message.trim()) {
223
+ fail(
224
+ `${label} arguments must include a nonempty user-facing message.`,
225
+ 'ARCANE_AI_TOOL_MESSAGE_REQUIRED'
226
+ );
227
+ }
228
+ return call.id;
229
+ }
230
+
231
+ function validateLLMMessageToolCalls(message, label) {
232
+ if (!isPlainRecord(message)) {
233
+ fail(
234
+ `${label} must be a plain message object.`,
235
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
236
+ );
237
+ }
238
+ if (Object.hasOwn(message, 'toolCalls')
239
+ || Object.hasOwn(message, 'function_call')
240
+ || Object.hasOwn(message, 'functionCall')) {
241
+ fail(
242
+ `${label} contains a noncanonical structural tool-call field.`,
243
+ 'ARCANE_AI_TOOL_CALL_INVALID'
244
+ );
245
+ }
246
+ if (!Object.hasOwn(message, 'tool_calls')) {
247
+ return [];
248
+ }
249
+ const descriptor = Object.getOwnPropertyDescriptor(message, 'tool_calls');
250
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')
251
+ || !Array.isArray(descriptor.value)) {
252
+ fail(
253
+ `${label}.tool_calls must be an array data property.`,
254
+ 'ARCANE_AI_TOOL_CALL_INVALID'
255
+ );
256
+ }
257
+ if (descriptor.value.length > 1) {
258
+ fail(
259
+ 'The Arcane AI runtime accepts one structural tool call at a time.',
260
+ 'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
261
+ );
262
+ }
263
+ for (let index = 0; index < descriptor.value.length; index += 1) {
264
+ validateLLMToolCall(
265
+ descriptor.value[index],
266
+ `${label}.tool_calls[${index}]`
267
+ );
268
+ }
269
+ return descriptor.value;
270
+ }
271
+
272
+ function validateLLMRequestPayload(payload) {
273
+ assertPlainObject(payload, 'AI LLM request payload');
274
+ if (!Array.isArray(payload.messages)) {
275
+ fail('AI LLM request payload.messages must be an array.');
276
+ }
277
+ let pendingToolCallId = null;
278
+ for (let index = 0; index < payload.messages.length; index += 1) {
279
+ const message = payload.messages[index];
280
+ if (!isPlainRecord(message)) {
281
+ fail(`AI LLM request payload.messages[${index}] must be a plain object.`);
282
+ }
283
+ if (Object.hasOwn(message, 'tool_calls')
284
+ && message.role !== 'assistant') {
285
+ fail(
286
+ `AI LLM request payload.messages[${index}].tool_calls is valid only for an assistant message.`,
287
+ 'ARCANE_AI_TOOL_CALL_INVALID'
288
+ );
289
+ }
290
+ const calls = validateLLMMessageToolCalls(
291
+ message,
292
+ `AI LLM request payload.messages[${index}]`
293
+ );
294
+ let openedToolCall = false;
295
+ if (calls.length) {
296
+ if (pendingToolCallId !== null) {
297
+ fail(
298
+ 'The Arcane AI runtime accepts one structural tool call at a time.',
299
+ 'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
300
+ );
301
+ }
302
+ pendingToolCallId = calls[0].id;
303
+ openedToolCall = true;
304
+ }
305
+ if (message.role === 'tool') {
306
+ if (typeof message.content !== 'string'
307
+ || !message.content.trim()) {
308
+ fail(
309
+ `AI LLM request payload.messages[${index}] must contain a nonblank user-facing tool result.`,
310
+ 'ARCANE_AI_INVALID_TOOL_MESSAGE'
311
+ );
312
+ }
313
+ if (pendingToolCallId === null
314
+ || typeof message.tool_call_id !== 'string'
315
+ || message.tool_call_id !== pendingToolCallId) {
316
+ fail(
317
+ `AI LLM request payload.messages[${index}] does not settle the pending structural tool call.`,
318
+ 'ARCANE_AI_INVALID_TOOL_MESSAGE'
319
+ );
320
+ }
321
+ pendingToolCallId = null;
322
+ } else {
323
+ if (Object.hasOwn(message, 'tool_call_id')) {
324
+ fail(
325
+ `AI LLM request payload.messages[${index}].tool_call_id is valid only for a tool result.`,
326
+ 'ARCANE_AI_INVALID_TOOL_MESSAGE'
327
+ );
328
+ }
329
+ if (pendingToolCallId !== null && !openedToolCall) {
330
+ fail(
331
+ `AI LLM request payload.messages[${index}] precedes the pending structural tool result.`,
332
+ 'ARCANE_AI_TOOL_RESULT_REQUIRED'
333
+ );
334
+ }
335
+ }
336
+ }
337
+ if (pendingToolCallId !== null) {
338
+ fail(
339
+ 'The pending structural tool call must be settled before requesting another response.',
340
+ 'ARCANE_AI_TOOL_RESULT_REQUIRED'
341
+ );
342
+ }
343
+ validateLLMToolDeclarations(payload.tools);
344
+ const parallelValues = [
345
+ payload.parallelToolCalls,
346
+ payload.parallel_tool_calls
347
+ ];
348
+ if (parallelValues.some(function enablesParallelLLMTools(value) {
349
+ return value !== undefined && value !== false;
350
+ })) {
351
+ fail(
352
+ 'The Arcane AI runtime accepts one structural tool call at a time.',
353
+ 'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
354
+ );
355
+ }
356
+ }
357
+
358
+ function validateLLMTerminalMessage(message, label) {
359
+ if (!isPlainRecord(message) || message.role !== 'assistant') {
360
+ fail(
361
+ `${label} must contain an assistant message.`,
362
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
363
+ );
364
+ }
365
+ return validateLLMMessageToolCalls(message, label);
366
+ }
367
+
368
+ function validateLLMTerminalResult(value, label) {
369
+ if (typeof value === 'string') {
370
+ return value;
371
+ }
372
+ if (!isPlainRecord(value)) {
373
+ fail(
374
+ `${label} must be text or a chat completion object.`,
375
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
376
+ );
377
+ }
378
+ const hasMessage = Object.hasOwn(value, 'message');
379
+ const hasChoices = Object.hasOwn(value, 'choices');
380
+ if (hasMessage === hasChoices) {
381
+ fail(
382
+ `${label} must contain exactly one message or choices envelope.`,
383
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
384
+ );
385
+ }
386
+ if (hasMessage) {
387
+ const descriptor = Object.getOwnPropertyDescriptor(value, 'message');
388
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
389
+ fail(
390
+ `${label}.message must be a data property.`,
391
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
392
+ );
393
+ }
394
+ validateLLMTerminalMessage(descriptor.value, `${label}.message`);
395
+ return value;
396
+ }
397
+ const choicesDescriptor = Object.getOwnPropertyDescriptor(value, 'choices');
398
+ if (!choicesDescriptor || !Object.hasOwn(choicesDescriptor, 'value')
399
+ || !Array.isArray(choicesDescriptor.value)
400
+ || choicesDescriptor.value.length === 0) {
401
+ fail(
402
+ `${label}.choices must be a nonempty array data property.`,
403
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
404
+ );
405
+ }
406
+ const indexes = new Set();
407
+ let totalToolCalls = 0;
408
+ for (let position = 0; position < choicesDescriptor.value.length; position += 1) {
409
+ const choice = choicesDescriptor.value[position];
410
+ if (!isPlainRecord(choice)
411
+ || !Number.isSafeInteger(choice.index)
412
+ || choice.index < 0
413
+ || indexes.has(choice.index)) {
414
+ fail(
415
+ `${label}.choices[${position}] must have a unique nonnegative safe-integer index.`,
416
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
417
+ );
418
+ }
419
+ indexes.add(choice.index);
420
+ const messageDescriptor = Object.getOwnPropertyDescriptor(choice, 'message');
421
+ if (!messageDescriptor || !Object.hasOwn(messageDescriptor, 'value')) {
422
+ fail(
423
+ `${label}.choices[${position}].message must be a data property.`,
424
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
425
+ );
426
+ }
427
+ const calls = validateLLMTerminalMessage(
428
+ messageDescriptor.value,
429
+ `${label}.choices[${position}].message`
430
+ );
431
+ if (position > 0 && calls.length) {
432
+ fail(
433
+ `${label} placed a structural tool call outside the selected first choice.`,
434
+ 'ARCANE_AI_INVALID_PROVIDER_RESULT'
435
+ );
436
+ }
437
+ totalToolCalls += calls.length;
438
+ if (totalToolCalls > 1) {
439
+ fail(
440
+ 'The Arcane AI runtime accepts one structural tool call at a time.',
441
+ 'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
442
+ );
443
+ }
444
+ }
445
+ return value;
446
+ }
447
+
448
+ function isLLMStreamContentKey(key) {
449
+ return key === 'content'
450
+ || key === 'text'
451
+ || key === 'thinking'
452
+ || key === 'reasoning'
453
+ || key === 'reasoning_content';
454
+ }
455
+
456
+ function projectLLMStreamContent(value, seen = new WeakSet()) {
457
+ if (!value || typeof value !== 'object' || seen.has(value)) {
458
+ return null;
459
+ }
460
+ seen.add(value);
461
+ if (Array.isArray(value)) {
462
+ const result = [];
463
+ for (const item of value) {
464
+ const projected = projectLLMStreamContent(item, seen);
465
+ if (projected !== null) {
466
+ result.push(projected);
467
+ }
468
+ }
469
+ seen.delete(value);
470
+ return result.length ? result : null;
471
+ }
472
+ if (!isPlainRecord(value)) {
473
+ seen.delete(value);
474
+ return null;
475
+ }
476
+ const result = {};
477
+ const descriptors = Object.getOwnPropertyDescriptors(value);
478
+ for (const key of Reflect.ownKeys(descriptors)) {
479
+ if (typeof key === 'symbol') {
480
+ continue;
481
+ }
482
+ const descriptor = descriptors[key];
483
+ if (!Object.hasOwn(descriptor, 'value')) {
484
+ continue;
485
+ }
486
+ if (isLLMStreamContentKey(key)
487
+ && descriptor.value !== null
488
+ && descriptor.value !== undefined) {
489
+ result[key] = descriptor.value;
490
+ continue;
491
+ }
492
+ const projected = projectLLMStreamContent(descriptor.value, seen);
493
+ if (projected !== null) {
494
+ result[key] = projected;
495
+ }
496
+ }
497
+ seen.delete(value);
498
+ return Object.keys(result).length ? result : null;
499
+ }
500
+
501
+ function projectLLMStreamChunk(value) {
502
+ if (typeof value === 'string') {
503
+ return value;
504
+ }
505
+ return projectLLMStreamContent(value);
506
+ }
507
+
508
+ function assertCallbackFreeProviderValue(value, seen = new WeakSet()) {
163
509
  if (typeof value === 'function') {
164
510
  fail(
165
511
  'AI providers receive data-only request payloads.',
@@ -169,9 +515,6 @@ function assertCallbackFreeProviderValue(value, seen = new WeakSet(), depth = 0)
169
515
  if (!value || typeof value !== 'object') {
170
516
  return;
171
517
  }
172
- if (depth > 32) {
173
- fail('AI provider request payload nesting exceeds the supported limit.');
174
- }
175
518
  if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
176
519
  return;
177
520
  }
@@ -188,7 +531,7 @@ function assertCallbackFreeProviderValue(value, seen = new WeakSet(), depth = 0)
188
531
  if (typeof key === 'symbol' || !Object.hasOwn(descriptor, 'value')) {
189
532
  fail('AI provider request payloads must contain data properties only.');
190
533
  }
191
- assertCallbackFreeProviderValue(descriptor.value, seen, depth + 1);
534
+ assertCallbackFreeProviderValue(descriptor.value, seen);
192
535
  }
193
536
  seen.delete(value);
194
537
  }
@@ -231,7 +574,7 @@ function immutableSelection(role, value) {
231
574
  fail(`${role} provider selection.localOnly must be null or a boolean.`);
232
575
  }
233
576
 
234
- return Object.freeze(
577
+ return completeValue(
235
578
  {
236
579
  providerId: value.providerId,
237
580
  modelId: value.modelId,
@@ -247,7 +590,7 @@ function immutableRoleRoutes(role, value) {
247
590
  if (localSelection && localSelection.localOnly !== true) {
248
591
  fail(`${role} localOnly route must identify a local-only selection.`);
249
592
  }
250
- return Object.freeze(
593
+ return completeValue(
251
594
  {
252
595
  default: defaultSelection,
253
596
  localOnly: localSelection
@@ -257,7 +600,7 @@ function immutableRoleRoutes(role, value) {
257
600
 
258
601
  function immutableSelections(value) {
259
602
  assertClosedRecord(value, AI_RUNTIME_ROLES, 'AI provider selections');
260
- return Object.freeze(
603
+ return completeValue(
261
604
  {
262
605
  llm: immutableRoleRoutes('llm', value.llm),
263
606
  stt: immutableRoleRoutes('stt', value.stt),
@@ -269,7 +612,7 @@ function immutableSelections(value) {
269
612
  function immutableSpeechSelections(value) {
270
613
  try {
271
614
  assertClosedRecord(value, SPEECH_ROLES, 'AI speech provider selections');
272
- return Object.freeze(
615
+ return completeValue(
273
616
  {
274
617
  stt: immutableRoleRoutes('stt', value.stt),
275
618
  tts: immutableRoleRoutes('tts', value.tts)
@@ -286,7 +629,8 @@ function immutableSpeechSelections(value) {
286
629
 
287
630
  function immutableStartupOptions(options) {
288
631
  if (options === undefined) {
289
- return Object.freeze({
632
+ return completeValue({
633
+ startLanguageModel: true,
290
634
  startMuted: true,
291
635
  startTranscription: false,
292
636
  signal: null
@@ -297,7 +641,8 @@ function immutableStartupOptions(options) {
297
641
  const descriptors = Object.getOwnPropertyDescriptors(options);
298
642
  for (const key of Reflect.ownKeys(descriptors)) {
299
643
  if (typeof key === 'symbol'
300
- || (key !== 'startMuted'
644
+ || (key !== 'startLanguageModel'
645
+ && key !== 'startMuted'
301
646
  && key !== 'startTranscription'
302
647
  && key !== 'signal')) {
303
648
  fail('AI provider startup options contain an unknown option.');
@@ -306,6 +651,12 @@ function immutableStartupOptions(options) {
306
651
  fail(`AI provider startup options.${key} must be a data property.`);
307
652
  }
308
653
  }
654
+ const startLanguageModel = Object.hasOwn(
655
+ descriptors,
656
+ 'startLanguageModel'
657
+ )
658
+ ? descriptors.startLanguageModel.value
659
+ : true;
309
660
  const startMuted = Object.hasOwn(descriptors, 'startMuted')
310
661
  ? descriptors.startMuted.value
311
662
  : true;
@@ -315,6 +666,9 @@ function immutableStartupOptions(options) {
315
666
  const signal = Object.hasOwn(descriptors, 'signal')
316
667
  ? descriptors.signal.value
317
668
  : null;
669
+ if (typeof startLanguageModel !== 'boolean') {
670
+ fail('AI startup startLanguageModel must be a boolean.');
671
+ }
318
672
  if (typeof startMuted !== 'boolean') {
319
673
  fail('AI startup startMuted must be a boolean.');
320
674
  }
@@ -322,7 +676,12 @@ function immutableStartupOptions(options) {
322
676
  fail('AI startup startTranscription must be a boolean.');
323
677
  }
324
678
  assertAbortSignal(signal);
325
- return Object.freeze({startMuted, startTranscription, signal});
679
+ return completeValue({
680
+ startLanguageModel,
681
+ startMuted,
682
+ startTranscription,
683
+ signal
684
+ });
326
685
  }
327
686
 
328
687
  function immutableInspectionOptions(options) {
@@ -347,12 +706,12 @@ function immutableInspectionOptions(options) {
347
706
  fail('AI provider inspection localOnly must be a boolean.');
348
707
  }
349
708
  assertAbortSignal(signal);
350
- return Object.freeze({localOnly, signal});
709
+ return completeValue({localOnly, signal});
351
710
  }
352
711
 
353
712
  function nullableTupleIdentifier(value) {
354
713
  return typeof value === 'string' && value.trim()
355
- ? value.trim()
714
+ ? value
356
715
  : null;
357
716
  }
358
717
 
@@ -362,7 +721,7 @@ function immutableProgress(value) {
362
721
  ['phase', 'completed', 'total', 'unit', 'heartbeat'],
363
722
  'AI provider progress'
364
723
  );
365
- return Object.freeze(
724
+ return completeValue(
366
725
  {
367
726
  phase: value.phase,
368
727
  completed: value.completed,
@@ -375,21 +734,24 @@ function immutableProgress(value) {
375
734
 
376
735
  function stateError(error, fallbackCode) {
377
736
  const code = typeof error?.code === 'string'
378
- && /^[A-Z][A-Z0-9_]{0,127}$/.test(error.code)
379
- ? error.code.trim()
737
+ && /^[A-Z][A-Z0-9_]*$/.test(error.code)
738
+ ? error.code
380
739
  : fallbackCode;
381
- const message = code === 'ARCANE_AI_REQUEST_ABORTED'
740
+ const fallbackMessage = code === 'ARCANE_AI_REQUEST_ABORTED'
382
741
  || code === 'AI_REQUEST_ABORTED'
383
742
  ? 'The AI operation was cancelled.'
384
743
  : code.includes('AUTHORITY') || code.includes('PROVENANCE')
385
- ? 'The selected AI model is not admitted for use.'
744
+ ? 'The selected AI model could not be used.'
386
745
  : code.includes('UNAVAILABLE') || code.includes('NOT_REGISTERED')
387
746
  ? 'The selected AI provider is unavailable.'
388
747
  : 'The selected AI provider operation failed.';
389
- return Object.freeze(
748
+ const message = typeof error?.message === 'string' && error.message
749
+ ? error.message
750
+ : fallbackMessage;
751
+ return completeValue(
390
752
  {
391
753
  code,
392
- message: message.slice(0, ERROR_MESSAGE_LIMIT)
754
+ message
393
755
  }
394
756
  );
395
757
  }
@@ -442,7 +804,7 @@ function validateProvider(provider) {
442
804
  for (const method of PROVIDER_METHODS) {
443
805
  record[method] = provider[method].bind(provider);
444
806
  }
445
- const admitted = Object.freeze(record);
807
+ const admitted = completeValue(record);
446
808
  PROVIDER_RECORDS.set(provider, admitted);
447
809
  return admitted;
448
810
  }
@@ -595,11 +957,11 @@ function immutableSpeechProviderReplacement(value) {
595
957
  providers[role] = provider;
596
958
  }
597
959
 
598
- return Object.freeze(
960
+ return completeValue(
599
961
  {
600
- providers: Object.freeze(providers),
962
+ providers: completeValue(providers),
601
963
  routes,
602
- expectedProviders: Object.freeze(expectedProviders),
964
+ expectedProviders: completeValue(expectedProviders),
603
965
  removal
604
966
  }
605
967
  );
@@ -690,7 +1052,7 @@ function immutableSpeechProviderRoleReplacement(role, value) {
690
1052
  );
691
1053
  }
692
1054
  }
693
- return Object.freeze({role, provider, routes, expectedProvider, removal});
1055
+ return completeValue({role, provider, routes, expectedProvider, removal});
694
1056
  }
695
1057
 
696
1058
  function validateProviderStatus(status) {
@@ -709,21 +1071,20 @@ function validateInspection(inspection, selection) {
709
1071
  fail('AI provider inspection.available must be a boolean.');
710
1072
  }
711
1073
  if (!inspection.available) {
712
- const code = typeof inspection.code === 'string' && inspection.code.trim()
713
- ? inspection.code.trim()
1074
+ const code = typeof inspection.code === 'string' && inspection.code.length > 0
1075
+ ? inspection.code
714
1076
  : 'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED';
715
- const message = typeof inspection.message === 'string' && inspection.message.trim()
716
- ? inspection.message.trim()
717
- : 'The selected AI provider is not admitted for use.';
1077
+ const message = typeof inspection.message === 'string' && inspection.message.length > 0
1078
+ ? inspection.message
1079
+ : 'The selected AI provider is unavailable.';
718
1080
  throw operationError(message, code);
719
1081
  }
720
1082
  assertPlainObject(inspection.authority, 'AI provider inspection.authority');
721
1083
  if (inspection.authority.protocol !== AI_MODEL_AUTHORITY_PROTOCOL
722
1084
  || inspection.authority.providerId !== selection.providerId
723
- || inspection.authority.modelId !== selection.modelId
724
- || inspection.authority.admitted !== true) {
1085
+ || inspection.authority.modelId !== selection.modelId) {
725
1086
  throw operationError(
726
- 'The selected AI provider did not return an admitted model authority.',
1087
+ 'The selected AI provider returned a different model authority.',
727
1088
  'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
728
1089
  );
729
1090
  }
@@ -733,7 +1094,7 @@ function validateInspection(inspection, selection) {
733
1094
  function createRoleSlot(role) {
734
1095
  return {
735
1096
  role,
736
- routes: Object.freeze({default: null, localOnly: null}),
1097
+ routes: completeValue({default: null, localOnly: null}),
737
1098
  selection: null,
738
1099
  generation: 0,
739
1100
  operationSequence: 0,
@@ -742,7 +1103,7 @@ function createRoleSlot(role) {
742
1103
  loadPromise: null,
743
1104
  unloadPromise: null,
744
1105
  disposePromise: null,
745
- requestAdmission: null,
1106
+ requestQueue: [],
746
1107
  request: null,
747
1108
  disposed: false,
748
1109
  ready: false
@@ -751,7 +1112,7 @@ function createRoleSlot(role) {
751
1112
 
752
1113
  export class AIProviderRuntime {
753
1114
  #providers = new Map();
754
- #slots = Object.freeze(
1115
+ #slots = completeValue(
755
1116
  {
756
1117
  llm: createRoleSlot('llm'),
757
1118
  stt: createRoleSlot('stt'),
@@ -828,7 +1189,7 @@ export class AIProviderRuntime {
828
1189
  );
829
1190
  }
830
1191
  if (selection.localOnly === null) {
831
- nextRoutes[routeName] = Object.freeze(
1192
+ nextRoutes[routeName] = completeValue(
832
1193
  {
833
1194
  providerId: selection.providerId,
834
1195
  modelId: selection.modelId,
@@ -847,7 +1208,7 @@ export class AIProviderRuntime {
847
1208
  this.#providers.set(key, admitted);
848
1209
  if (reconciled) {
849
1210
  const previousSelection = slot.selection;
850
- slot.routes = Object.freeze(nextRoutes);
1211
+ slot.routes = completeValue(nextRoutes);
851
1212
  if (previousSelection?.providerId === admitted.id
852
1213
  && previousSelection.localOnly === null) {
853
1214
  slot.generation += 1;
@@ -938,7 +1299,7 @@ export class AIProviderRuntime {
938
1299
  if (!provider) {
939
1300
  return null;
940
1301
  }
941
- return Object.freeze(
1302
+ return completeValue(
942
1303
  {
943
1304
  protocol: provider.protocol,
944
1305
  role: provider.role,
@@ -1124,6 +1485,30 @@ export class AIProviderRuntime {
1124
1485
  }
1125
1486
  }
1126
1487
 
1488
+ #pendingSpeechProviderHydrationMatches(role, slot, provider, routes) {
1489
+ if (!provider || !slot.selection || !routes.default) {
1490
+ return false;
1491
+ }
1492
+ const pending = slot.selection;
1493
+ if (this.#providers.has(providerKey(role, pending.providerId))
1494
+ || provider.id !== pending.providerId
1495
+ || routes.default.providerId !== pending.providerId
1496
+ || routes.default.modelId !== pending.modelId) {
1497
+ return false;
1498
+ }
1499
+ const currentSelections = [
1500
+ slot.selection,
1501
+ slot.routes.default,
1502
+ slot.routes.localOnly
1503
+ ].filter(Boolean);
1504
+ return currentSelections.length > 0
1505
+ && currentSelections.every(selection =>
1506
+ selection.providerId === pending.providerId
1507
+ && selection.modelId === pending.modelId
1508
+ && selection.localOnly === null
1509
+ );
1510
+ }
1511
+
1127
1512
  #commitSpeechProviderRoleReplacement(role, value) {
1128
1513
  const replacement = immutableSpeechProviderRoleReplacement(role, value);
1129
1514
  const currentState = getAIRuntimeState();
@@ -1168,9 +1553,15 @@ export class AIProviderRuntime {
1168
1553
  `AI role ${role} selected routes no longer belong to the expected provider identity.`
1169
1554
  );
1170
1555
  }
1171
- } else if (slot.routes.default
1556
+ } else if ((slot.routes.default
1172
1557
  || slot.routes.localOnly
1173
- || slot.selection) {
1558
+ || slot.selection)
1559
+ && !this.#pendingSpeechProviderHydrationMatches(
1560
+ role,
1561
+ slot,
1562
+ replacement.provider,
1563
+ replacement.routes
1564
+ )) {
1174
1565
  throw speechProviderReplacementError(
1175
1566
  'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
1176
1567
  'speech-provider-replacement-expected-provider-required',
@@ -1218,14 +1609,6 @@ export class AIProviderRuntime {
1218
1609
  );
1219
1610
  }
1220
1611
  }
1221
- if (currentState.revision === Number.MAX_SAFE_INTEGER) {
1222
- throw speechProviderReplacementError(
1223
- 'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
1224
- 'speech-provider-replacement-state-revision-exhausted',
1225
- 'AI speech provider replacement cannot publish another runtime state revision.'
1226
- );
1227
- }
1228
-
1229
1612
  if (expected) {
1230
1613
  this.#providers.delete(providerKey(role, expected.id));
1231
1614
  this.#disposeAllCompletedProviders.delete(expected);
@@ -1242,7 +1625,7 @@ export class AIProviderRuntime {
1242
1625
  this.#configured = true;
1243
1626
  publishAIRuntimeRoleState(role, roleRecord(role, slot.selection));
1244
1627
 
1245
- return Object.freeze(
1628
+ return completeValue(
1246
1629
  {
1247
1630
  role,
1248
1631
  routes: replacement.routes,
@@ -1315,9 +1698,15 @@ export class AIProviderRuntime {
1315
1698
  `AI role ${role} selected routes no longer belong to the expected provider identity.`
1316
1699
  );
1317
1700
  }
1318
- } else if (slot.routes.default
1701
+ } else if ((slot.routes.default
1319
1702
  || slot.routes.localOnly
1320
- || slot.selection) {
1703
+ || slot.selection)
1704
+ && !this.#pendingSpeechProviderHydrationMatches(
1705
+ role,
1706
+ slot,
1707
+ replacement.providers[role],
1708
+ replacement.routes[role]
1709
+ )) {
1321
1710
  throw speechProviderReplacementError(
1322
1711
  'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
1323
1712
  'speech-provider-replacement-expected-provider-required',
@@ -1373,14 +1762,6 @@ export class AIProviderRuntime {
1373
1762
  }
1374
1763
  }
1375
1764
  }
1376
- if (currentState.revision === Number.MAX_SAFE_INTEGER) {
1377
- throw speechProviderReplacementError(
1378
- 'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
1379
- 'speech-provider-replacement-state-revision-exhausted',
1380
- 'AI speech provider replacement cannot publish another runtime state revision.'
1381
- );
1382
- }
1383
-
1384
1765
  for (const role of SPEECH_ROLES) {
1385
1766
  const expected = replacement.expectedProviders[role];
1386
1767
  const provider = replacement.providers[role];
@@ -1411,10 +1792,10 @@ export class AIProviderRuntime {
1411
1792
  }
1412
1793
  );
1413
1794
 
1414
- return Object.freeze(
1795
+ return completeValue(
1415
1796
  {
1416
1797
  routes: replacement.routes,
1417
- unregisters: Object.freeze(
1798
+ unregisters: completeValue(
1418
1799
  {
1419
1800
  stt: replacement.removal
1420
1801
  ? null
@@ -1502,16 +1883,16 @@ export class AIProviderRuntime {
1502
1883
  fail('AI provider.catalog() must synchronously return an array.');
1503
1884
  }
1504
1885
  entries.push(
1505
- Object.freeze(
1886
+ completeValue(
1506
1887
  {
1507
1888
  providerId: provider.id,
1508
1889
  localOnly: provider.localOnly,
1509
- models: Object.freeze(providerCatalog.slice())
1890
+ models: completeValue([...providerCatalog])
1510
1891
  }
1511
1892
  )
1512
1893
  );
1513
1894
  }
1514
- return Object.freeze(entries);
1895
+ return completeValue(entries);
1515
1896
  }
1516
1897
 
1517
1898
  async inspect(role, options = {}) {
@@ -1528,7 +1909,7 @@ export class AIProviderRuntime {
1528
1909
  const generation = slot.generation;
1529
1910
  const selection = this.selection(role, {localOnly});
1530
1911
  if (!selection) {
1531
- return Object.freeze(
1912
+ return completeValue(
1532
1913
  {
1533
1914
  available: false,
1534
1915
  code: 'ARCANE_AI_ROLE_NOT_SELECTED',
@@ -1540,7 +1921,7 @@ export class AIProviderRuntime {
1540
1921
  providerKey(role, selection.providerId)
1541
1922
  ) ?? null;
1542
1923
  if (!provider) {
1543
- return Object.freeze(
1924
+ return completeValue(
1544
1925
  {
1545
1926
  available: false,
1546
1927
  code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
@@ -1572,7 +1953,7 @@ export class AIProviderRuntime {
1572
1953
  error,
1573
1954
  'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
1574
1955
  );
1575
- return Object.freeze(
1956
+ return completeValue(
1576
1957
  {
1577
1958
  available: false,
1578
1959
  code: unavailable.code,
@@ -1591,7 +1972,7 @@ export class AIProviderRuntime {
1591
1972
  error,
1592
1973
  'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
1593
1974
  );
1594
- return Object.freeze(
1975
+ return completeValue(
1595
1976
  {
1596
1977
  available: false,
1597
1978
  code: unavailable.code,
@@ -1669,7 +2050,7 @@ export class AIProviderRuntime {
1669
2050
  )
1670
2051
  );
1671
2052
  }
1672
- if (slot.request || slot.requestAdmission) {
2053
+ if (slot.request || slot.requestQueue.length) {
1673
2054
  return Promise.reject(
1674
2055
  operationError(
1675
2056
  `AI role ${role} cannot load during active request ownership.`,
@@ -1867,7 +2248,7 @@ export class AIProviderRuntime {
1867
2248
  state: 'loading',
1868
2249
  operationId,
1869
2250
  progress: {
1870
- phase: 'admission',
2251
+ phase: 'loading',
1871
2252
  completed: 0,
1872
2253
  total: null,
1873
2254
  unit: 'items',
@@ -1912,8 +2293,8 @@ export class AIProviderRuntime {
1912
2293
  || providerStatus?.loaded === true
1913
2294
  || providerStatus?.busy === true;
1914
2295
  if (!slot.loadPromise
1915
- && !slot.requestAdmission
1916
2296
  && !slot.request
2297
+ && slot.requestQueue.length === 0
1917
2298
  && !providerOwned
1918
2299
  && (!provider || providerStatus)) {
1919
2300
  slot.ready = false;
@@ -1931,8 +2312,12 @@ export class AIProviderRuntime {
1931
2312
  this.#speechMuted = true;
1932
2313
  }
1933
2314
  slot.loadController?.abort();
1934
- slot.requestAdmission?.controller.abort();
1935
- slot.request?.controller.abort();
2315
+ const requestCancellation = operationError(
2316
+ `AI role ${role} is unloading.`,
2317
+ 'ARCANE_AI_REQUEST_ABORTED'
2318
+ );
2319
+ this.#rejectQueuedRoleRequests(slot, requestCancellation);
2320
+ slot.request?.controller.abort(requestCancellation);
1936
2321
  const capturedLoad = slot.loadPromise;
1937
2322
  const capturedRequestRecord = slot.request;
1938
2323
  const capturedRequest = capturedRequestRecord?.promise ?? null;
@@ -1947,12 +2332,7 @@ export class AIProviderRuntime {
1947
2332
  }
1948
2333
  if (capturedRequestRecord?.cancel) {
1949
2334
  try {
1950
- await capturedRequestRecord.cancel(
1951
- operationError(
1952
- `AI role ${role} is unloading.`,
1953
- 'ARCANE_AI_REQUEST_ABORTED'
1954
- )
1955
- );
2335
+ await capturedRequestRecord.cancel(requestCancellation);
1956
2336
  } catch {
1957
2337
  // Provider unload below remains the authoritative cleanup.
1958
2338
  }
@@ -2177,13 +2557,13 @@ export class AIProviderRuntime {
2177
2557
  for (const role of AI_RUNTIME_ROLES) {
2178
2558
  const slot = runtime.#slots[role];
2179
2559
  slot.generation += 1;
2180
- slot.routes = Object.freeze({default: null, localOnly: null});
2560
+ slot.routes = completeValue({default: null, localOnly: null});
2181
2561
  slot.selection = null;
2182
2562
  slot.loadController = null;
2183
2563
  slot.loadPromise = null;
2184
2564
  slot.unloadPromise = null;
2185
2565
  slot.disposePromise = null;
2186
- slot.requestAdmission = null;
2566
+ slot.requestQueue = [];
2187
2567
  slot.request = null;
2188
2568
  slot.ready = false;
2189
2569
  slot.disposed = true;
@@ -2234,6 +2614,9 @@ export class AIProviderRuntime {
2234
2614
  assertAbortSignal(options.signal);
2235
2615
  try {
2236
2616
  assertCallbackFreeProviderValue(options.payload);
2617
+ if (role === 'llm') {
2618
+ validateLLMRequestPayload(options.payload);
2619
+ }
2237
2620
  } catch (error) {
2238
2621
  return Promise.reject(error);
2239
2622
  }
@@ -2310,8 +2693,8 @@ export class AIProviderRuntime {
2310
2693
  )
2311
2694
  );
2312
2695
  }
2313
- if (slot.request || slot.requestAdmission) {
2314
- return this.#supersedeRoleRequest(slot, provider, options);
2696
+ if (slot.request) {
2697
+ return this.#enqueueRoleRequest(slot, options);
2315
2698
  }
2316
2699
  if (!slot.ready
2317
2700
  || providerStatus.state !== 'ready'
@@ -2356,7 +2739,6 @@ export class AIProviderRuntime {
2356
2739
  function restoreAIProviderRoleAfterRequest(error) {
2357
2740
  if (slot.generation !== generation
2358
2741
  || slot.requestSequence !== requestSequence
2359
- || slot.requestAdmission
2360
2742
  || slot.unloadPromise) {
2361
2743
  return;
2362
2744
  }
@@ -2415,6 +2797,7 @@ export class AIProviderRuntime {
2415
2797
  if (slot.request === requestRecord) {
2416
2798
  slot.request = null;
2417
2799
  }
2800
+ runtime.#drainRoleRequestQueue(slot);
2418
2801
  if (error) {
2419
2802
  restoreAIProviderRoleAfterRequest(error);
2420
2803
  rejectTerminal(error);
@@ -2444,7 +2827,7 @@ export class AIProviderRuntime {
2444
2827
  }
2445
2828
 
2446
2829
  async function cleanupAIProviderStreamHandle(opened, activeIterator, reason) {
2447
- return awaitBoundedStreamCleanup(
2830
+ return awaitStreamCleanup(
2448
2831
  beginAIProviderStreamHandleCleanup(
2449
2832
  opened,
2450
2833
  activeIterator,
@@ -2486,7 +2869,7 @@ export class AIProviderRuntime {
2486
2869
  // A rejected open confirms that no provider handle was returned.
2487
2870
  }
2488
2871
  );
2489
- return awaitBoundedStreamCleanup(
2872
+ return awaitStreamCleanup(
2490
2873
  Promise.allSettled([lateCleanup])
2491
2874
  );
2492
2875
  }
@@ -2503,7 +2886,7 @@ export class AIProviderRuntime {
2503
2886
  rejectCleanup = reject;
2504
2887
  }
2505
2888
  );
2506
- controller.abort();
2889
+ controller.abort(reason);
2507
2890
  (async function closeAIProviderStream() {
2508
2891
  const terminalOutcome = terminalError ?? normalizedAbort(
2509
2892
  reason instanceof Error
@@ -2592,6 +2975,12 @@ export class AIProviderRuntime {
2592
2975
  }
2593
2976
  providerHandle = opened;
2594
2977
  iterator = opened[Symbol.asyncIterator]();
2978
+ if (!iterator || typeof iterator.next !== 'function') {
2979
+ throw operationError(
2980
+ 'The AI provider stream iterator has no next() method.',
2981
+ 'ARCANE_AI_PROVIDER_STREAM_INVALID'
2982
+ );
2983
+ }
2595
2984
  runtime.#assertCurrentRequest(
2596
2985
  slot,
2597
2986
  generation,
@@ -2607,7 +2996,13 @@ export class AIProviderRuntime {
2607
2996
  requestSequence,
2608
2997
  controller.signal
2609
2998
  );
2610
- settleAIProviderStream(null, value);
2999
+ const terminalValue = role === 'llm'
3000
+ ? validateLLMTerminalResult(
3001
+ value,
3002
+ 'AI provider stream result'
3003
+ )
3004
+ : value;
3005
+ settleAIProviderStream(null, terminalValue);
2611
3006
  } catch (error) {
2612
3007
  const normalized = isAbort(error, controller.signal)
2613
3008
  ? normalizedAbort(error)
@@ -2632,14 +3027,30 @@ export class AIProviderRuntime {
2632
3027
  cancel: cancelAIProviderStream,
2633
3028
  async next(value) {
2634
3029
  try {
2635
- const result = await iterator.next(value);
2636
- runtime.#assertCurrentRequest(
2637
- slot,
2638
- generation,
2639
- requestSequence,
2640
- controller.signal
2641
- );
2642
- return result;
3030
+ let nextValue = value;
3031
+ while (true) {
3032
+ const result = await iterator.next(nextValue);
3033
+ nextValue = undefined;
3034
+ runtime.#assertCurrentRequest(
3035
+ slot,
3036
+ generation,
3037
+ requestSequence,
3038
+ controller.signal
3039
+ );
3040
+ if (role !== 'llm') {
3041
+ return result;
3042
+ }
3043
+ if (result.done) {
3044
+ return {value: undefined, done: true};
3045
+ }
3046
+ const projected = projectLLMStreamChunk(result.value);
3047
+ if (projected !== null) {
3048
+ return {
3049
+ value: projected,
3050
+ done: false
3051
+ };
3052
+ }
3053
+ }
2643
3054
  } catch (error) {
2644
3055
  await cancelAIProviderStream(error);
2645
3056
  throw isAbort(error, controller.signal)
@@ -2648,11 +3059,22 @@ export class AIProviderRuntime {
2648
3059
  }
2649
3060
  },
2650
3061
  async return(value) {
2651
- await cancelAIProviderStream(
2652
- operationError(
2653
- 'The AI stream consumer stopped before completion.',
2654
- 'ARCANE_AI_REQUEST_ABORTED'
2655
- )
3062
+ Promise.resolve().then(
3063
+ function beginReturnedAIProviderStreamCancellation() {
3064
+ return cancelAIProviderStream(
3065
+ operationError(
3066
+ 'The AI stream consumer stopped before completion.',
3067
+ 'ARCANE_AI_REQUEST_ABORTED'
3068
+ )
3069
+ );
3070
+ }
3071
+ ).catch(
3072
+ function reportReturnedAIProviderStreamCancellationFailure(error) {
3073
+ console.error(
3074
+ 'Arcane AI provider stream early-return cancellation failed.',
3075
+ error
3076
+ );
3077
+ }
2656
3078
  );
2657
3079
  return {value, done: true};
2658
3080
  },
@@ -2664,7 +3086,7 @@ export class AIProviderRuntime {
2664
3086
  return this;
2665
3087
  }
2666
3088
  };
2667
- return Object.freeze(handle);
3089
+ return completeValue(handle);
2668
3090
  } catch (error) {
2669
3091
  const normalized = isAbort(error, controller.signal)
2670
3092
  ? normalizedAbort(error)
@@ -2701,8 +3123,8 @@ export class AIProviderRuntime {
2701
3123
  return openPromise;
2702
3124
  }
2703
3125
 
2704
- requestRecord.cancel = async function cancelAIProviderRequest() {
2705
- controller.abort();
3126
+ requestRecord.cancel = async function cancelAIProviderRequest(reason) {
3127
+ controller.abort(reason);
2706
3128
  await requestRecord.promise?.catch(
2707
3129
  function retainCancelledAIProviderRequest() {}
2708
3130
  );
@@ -2728,7 +3150,12 @@ export class AIProviderRuntime {
2728
3150
  requestSequence,
2729
3151
  controller.signal
2730
3152
  );
2731
- return result;
3153
+ return role === 'llm'
3154
+ ? validateLLMTerminalResult(
3155
+ result,
3156
+ 'AI provider chat result'
3157
+ )
3158
+ : result;
2732
3159
  } catch (error) {
2733
3160
  const normalized = isAbort(error, controller.signal)
2734
3161
  ? normalizedAbort(error)
@@ -2741,6 +3168,7 @@ export class AIProviderRuntime {
2741
3168
  if (slot.request === requestRecord) {
2742
3169
  slot.request = null;
2743
3170
  }
3171
+ runtime.#drainRoleRequestQueue(slot);
2744
3172
  if (!requestError
2745
3173
  && slot.generation === generation
2746
3174
  && !slot.unloadPromise) {
@@ -2770,18 +3198,16 @@ export class AIProviderRuntime {
2770
3198
  cancel(role) {
2771
3199
  assertRole(role);
2772
3200
  const slot = this.#slots[role];
2773
- if (!slot.request && !slot.requestAdmission) {
3201
+ if (!slot.request) {
2774
3202
  return false;
2775
3203
  }
2776
- slot.requestAdmission?.controller.abort();
2777
3204
  const requestRecord = slot.request;
2778
- requestRecord?.controller.abort();
2779
- const cancellation = requestRecord?.cancel?.(
2780
- operationError(
2781
- `AI role ${role} request was cancelled.`,
2782
- 'ARCANE_AI_REQUEST_ABORTED'
2783
- )
3205
+ const reason = operationError(
3206
+ `AI role ${role} request was cancelled.`,
3207
+ 'ARCANE_AI_REQUEST_ABORTED'
2784
3208
  );
3209
+ requestRecord?.controller.abort(reason);
3210
+ const cancellation = requestRecord?.cancel?.(reason);
2785
3211
  cancellation?.catch(function retainAIProviderCancelFailureInState() {});
2786
3212
  return true;
2787
3213
  }
@@ -2871,126 +3297,83 @@ export class AIProviderRuntime {
2871
3297
  );
2872
3298
  }
2873
3299
 
2874
- #supersedeRoleRequest(slot, provider, options) {
2875
- const generation = slot.generation;
2876
- const requestSequence = this.#nextRequestSequence(slot);
2877
- const priorAdmission = slot.requestAdmission;
2878
- const activeRequest = priorAdmission?.activeRequest ?? slot.request;
2879
- const controller = new AbortController();
2880
- const detachSignal = this.#forwardAbort(options.signal, controller);
2881
- let settlement = priorAdmission?.settlement ?? null;
2882
- if (!settlement && activeRequest) {
2883
- activeRequest.controller.abort();
2884
- settlement = Promise.allSettled(
2885
- [
2886
- Promise.resolve().then(
2887
- function cancelSupersededAIProviderRequest() {
2888
- return activeRequest.cancel(
2889
- operationError(
2890
- `AI role ${slot.role} request was superseded.`,
2891
- 'ARCANE_AI_OPERATION_SUPERSEDED'
2892
- )
2893
- );
2894
- }
2895
- ),
2896
- activeRequest.promise
2897
- ]
2898
- );
2899
- }
2900
- if (!settlement) {
2901
- settlement = Promise.resolve([]);
2902
- }
2903
- const admission = {
2904
- activeRequest,
2905
- controller,
2906
- generation,
2907
- requestSequence,
2908
- settlement
2909
- };
2910
- slot.requestAdmission = admission;
3300
+ #enqueueRoleRequest(slot, options) {
2911
3301
  const runtime = this;
2912
- return (async function supersedeAIProviderRoleRequest() {
2913
- try {
2914
- const outcomes = await settlement;
2915
- runtime.#assertCurrentRequestAdmission(slot, admission);
2916
- if (outcomes[0]?.status === 'rejected') {
2917
- throw outcomes[0].reason;
3302
+ return new Promise(function queueAIProviderRoleRequest(resolve, reject) {
3303
+ const entry = {
3304
+ options,
3305
+ resolve,
3306
+ reject,
3307
+ detachSignal: null
3308
+ };
3309
+ function cancelQueuedAIProviderRoleRequest() {
3310
+ const index = slot.requestQueue.indexOf(entry);
3311
+ if (index === -1) {
3312
+ return;
2918
3313
  }
2919
- let providerStatus;
2920
- try {
2921
- providerStatus = validateProviderStatus(provider.status());
2922
- } catch (error) {
2923
- throw operationError(
2924
- `The selected ${slot.role} provider did not return a valid status.`,
2925
- 'ARCANE_AI_PROVIDER_STATUS_INVALID',
2926
- error
2927
- );
3314
+ slot.requestQueue.splice(index, 1);
3315
+ entry.detachSignal?.();
3316
+ reject(normalizedAbort(options.signal?.reason));
3317
+ }
3318
+ if (options.signal) {
3319
+ if (options.signal.aborted) {
3320
+ reject(normalizedAbort(options.signal.reason));
3321
+ return;
2928
3322
  }
2929
- if (!slot.ready
2930
- || providerStatus.state !== 'ready'
2931
- || providerStatus.loaded !== true
2932
- || providerStatus.busy !== false) {
2933
- slot.requestAdmission = null;
2934
- slot.ready = false;
2935
- const notReady = operationError(
2936
- `AI role ${slot.role} is not ready after superseding its active request.`,
2937
- 'ARCANE_AI_ROLE_NOT_READY'
3323
+ options.signal.addEventListener(
3324
+ 'abort',
3325
+ cancelQueuedAIProviderRoleRequest,
3326
+ {once: true}
3327
+ );
3328
+ entry.detachSignal = function detachQueuedAIProviderRoleRequestSignal() {
3329
+ options.signal.removeEventListener(
3330
+ 'abort',
3331
+ cancelQueuedAIProviderRoleRequest
2938
3332
  );
2939
- if (providerStatus.state === 'unloaded'
2940
- && !providerStatus.loaded
2941
- && !providerStatus.busy) {
2942
- publishAIRuntimeRoleState(
2943
- slot.role,
2944
- roleRecord(slot.role, slot.selection)
2945
- );
2946
- } else if (providerStatus.state === 'ready'
2947
- && providerStatus.loaded
2948
- && providerStatus.busy) {
2949
- slot.ready = true;
2950
- publishAIRuntimeRoleState(
2951
- slot.role,
2952
- roleRecord(
2953
- slot.role,
2954
- slot.selection,
2955
- {
2956
- state: 'ready',
2957
- loaded: true,
2958
- busy: true
2959
- }
2960
- )
2961
- );
2962
- } else {
2963
- runtime.#publishRoleError(
2964
- slot,
2965
- notReady,
2966
- providerStatus.loaded
2967
- );
2968
- }
2969
- throw notReady;
2970
- }
2971
- slot.requestAdmission = null;
2972
- return await runtime.request(slot.role, options);
2973
- } catch (error) {
2974
- const normalized = isAbort(error, controller.signal)
2975
- ? normalizedAbort(error)
2976
- : error;
2977
- if (slot.requestAdmission === admission) {
2978
- slot.requestAdmission = null;
2979
- if (slot.generation === generation
2980
- && !slot.unloadPromise
2981
- && !slot.disposePromise) {
2982
- runtime.#publishRequestAdmissionFailure(
2983
- slot,
2984
- provider,
2985
- normalized
2986
- );
2987
- }
3333
+ };
3334
+ }
3335
+ slot.requestQueue.push(entry);
3336
+ runtime.#drainRoleRequestQueue(slot);
3337
+ });
3338
+ }
3339
+
3340
+ #drainRoleRequestQueue(slot) {
3341
+ if (slot.request
3342
+ || slot.unloadPromise
3343
+ || slot.disposePromise
3344
+ || slot.disposed) {
3345
+ return;
3346
+ }
3347
+ const entry = slot.requestQueue.shift();
3348
+ if (!entry) {
3349
+ return;
3350
+ }
3351
+ entry.detachSignal?.();
3352
+ if (entry.options.signal?.aborted) {
3353
+ entry.reject(normalizedAbort(entry.options.signal.reason));
3354
+ this.#drainRoleRequestQueue(slot);
3355
+ return;
3356
+ }
3357
+ const runtime = this;
3358
+ Promise.resolve().then(
3359
+ function runQueuedAIProviderRoleRequest() {
3360
+ return runtime.request(slot.role, entry.options);
3361
+ }
3362
+ ).then(entry.resolve, entry.reject).finally(
3363
+ function continueAIProviderRoleRequestQueue() {
3364
+ if (!slot.request) {
3365
+ runtime.#drainRoleRequestQueue(slot);
2988
3366
  }
2989
- throw normalized;
2990
- } finally {
2991
- detachSignal();
2992
3367
  }
2993
- })();
3368
+ );
3369
+ }
3370
+
3371
+ #rejectQueuedRoleRequests(slot, error) {
3372
+ const queued = slot.requestQueue.splice(0);
3373
+ for (const entry of queued) {
3374
+ entry.detachSignal?.();
3375
+ entry.reject(error);
3376
+ }
2994
3377
  }
2995
3378
 
2996
3379
  #sameSelection(left, right) {
@@ -3031,8 +3414,8 @@ export class AIProviderRuntime {
3031
3414
  slot.loadPromise
3032
3415
  || slot.unloadPromise
3033
3416
  || slot.disposePromise
3034
- || slot.requestAdmission
3035
3417
  || slot.request
3418
+ || slot.requestQueue.length > 0
3036
3419
  || slot.ready
3037
3420
  );
3038
3421
  }
@@ -3057,24 +3440,12 @@ export class AIProviderRuntime {
3057
3440
  }
3058
3441
 
3059
3442
  #nextRequestSequence(slot) {
3060
- if (slot.requestSequence === Number.MAX_SAFE_INTEGER) {
3061
- throw operationError(
3062
- `AI role ${slot.role} exhausted its request sequence.`,
3063
- 'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
3064
- );
3065
- }
3066
- slot.requestSequence += 1;
3443
+ slot.requestSequence = nextSequence(slot.requestSequence);
3067
3444
  return slot.requestSequence;
3068
3445
  }
3069
3446
 
3070
3447
  #nextOperationId(slot, operation) {
3071
- if (slot.operationSequence === Number.MAX_SAFE_INTEGER) {
3072
- throw operationError(
3073
- `AI role ${slot.role} exhausted its operation sequence.`,
3074
- 'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
3075
- );
3076
- }
3077
- slot.operationSequence += 1;
3448
+ slot.operationSequence = nextSequence(slot.operationSequence);
3078
3449
  return `${slot.role}-${operation}-${slot.operationSequence}`;
3079
3450
  }
3080
3451
 
@@ -3094,22 +3465,8 @@ export class AIProviderRuntime {
3094
3465
  this.#assertCurrentOperation(slot, generation, signal);
3095
3466
  if (slot.requestSequence !== requestSequence) {
3096
3467
  throw operationError(
3097
- `AI role ${slot.role} request was superseded.`,
3098
- 'ARCANE_AI_OPERATION_SUPERSEDED'
3099
- );
3100
- }
3101
- }
3102
-
3103
- #assertCurrentRequestAdmission(slot, admission) {
3104
- if (admission.controller.signal.aborted) {
3105
- throw normalizedAbort();
3106
- }
3107
- if (slot.generation !== admission.generation
3108
- || slot.requestSequence !== admission.requestSequence
3109
- || slot.requestAdmission !== admission) {
3110
- throw operationError(
3111
- `AI role ${slot.role} request admission was superseded.`,
3112
- 'ARCANE_AI_OPERATION_SUPERSEDED'
3468
+ `AI role ${slot.role} no longer owns the active provider request.`,
3469
+ 'ARCANE_AI_REQUEST_STALE'
3113
3470
  );
3114
3471
  }
3115
3472
  }
@@ -3119,10 +3476,10 @@ export class AIProviderRuntime {
3119
3476
  return function detachAbsentAIProviderAbort() {};
3120
3477
  }
3121
3478
  function forwardAIProviderAbort() {
3122
- controller.abort();
3479
+ controller.abort(signal.reason);
3123
3480
  }
3124
3481
  if (signal.aborted) {
3125
- controller.abort();
3482
+ controller.abort(signal.reason);
3126
3483
  return function detachAlreadyAbortedAIProviderSignal() {};
3127
3484
  }
3128
3485
  signal.addEventListener('abort', forwardAIProviderAbort, {once: true});
@@ -3150,68 +3507,6 @@ export class AIProviderRuntime {
3150
3507
  return true;
3151
3508
  }
3152
3509
 
3153
- #publishRequestAdmissionFailure(slot, provider, error) {
3154
- let providerStatus = null;
3155
- try {
3156
- providerStatus = validateProviderStatus(provider.status());
3157
- } catch {
3158
- // The admission error remains authoritative when status is malformed.
3159
- }
3160
- if (isAbort(error, null)
3161
- && providerStatus?.state === 'ready'
3162
- && providerStatus.loaded
3163
- && !providerStatus.busy) {
3164
- slot.ready = true;
3165
- publishAIRuntimeRoleState(
3166
- slot.role,
3167
- roleRecord(
3168
- slot.role,
3169
- slot.selection,
3170
- {
3171
- state: 'ready',
3172
- loaded: true
3173
- }
3174
- )
3175
- );
3176
- return;
3177
- }
3178
- if (isAbort(error, null)
3179
- && providerStatus?.state === 'unloaded'
3180
- && !providerStatus.loaded
3181
- && !providerStatus.busy) {
3182
- slot.ready = false;
3183
- publishAIRuntimeRoleState(
3184
- slot.role,
3185
- roleRecord(slot.role, slot.selection)
3186
- );
3187
- return;
3188
- }
3189
- if (providerStatus?.state === 'ready'
3190
- && providerStatus.loaded
3191
- && providerStatus.busy) {
3192
- slot.ready = true;
3193
- publishAIRuntimeRoleState(
3194
- slot.role,
3195
- roleRecord(
3196
- slot.role,
3197
- slot.selection,
3198
- {
3199
- state: 'ready',
3200
- loaded: true,
3201
- busy: true
3202
- }
3203
- )
3204
- );
3205
- return;
3206
- }
3207
- slot.ready = false;
3208
- this.#publishRoleError(
3209
- slot,
3210
- error,
3211
- providerStatus?.loaded === true
3212
- );
3213
- }
3214
-
3215
3510
  #publishRoleError(slot, error, loaded) {
3216
3511
  publishAIRuntimeRoleState(
3217
3512
  slot.role,