arcane-os 0.3.1 → 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 +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  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 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  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 +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  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 +27 -67
  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 +109 -1558
  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 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  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 -719
  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 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  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,
@@ -1248,14 +1609,6 @@ export class AIProviderRuntime {
1248
1609
  );
1249
1610
  }
1250
1611
  }
1251
- if (currentState.revision === Number.MAX_SAFE_INTEGER) {
1252
- throw speechProviderReplacementError(
1253
- 'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
1254
- 'speech-provider-replacement-state-revision-exhausted',
1255
- 'AI speech provider replacement cannot publish another runtime state revision.'
1256
- );
1257
- }
1258
-
1259
1612
  if (expected) {
1260
1613
  this.#providers.delete(providerKey(role, expected.id));
1261
1614
  this.#disposeAllCompletedProviders.delete(expected);
@@ -1272,7 +1625,7 @@ export class AIProviderRuntime {
1272
1625
  this.#configured = true;
1273
1626
  publishAIRuntimeRoleState(role, roleRecord(role, slot.selection));
1274
1627
 
1275
- return Object.freeze(
1628
+ return completeValue(
1276
1629
  {
1277
1630
  role,
1278
1631
  routes: replacement.routes,
@@ -1409,14 +1762,6 @@ export class AIProviderRuntime {
1409
1762
  }
1410
1763
  }
1411
1764
  }
1412
- if (currentState.revision === Number.MAX_SAFE_INTEGER) {
1413
- throw speechProviderReplacementError(
1414
- 'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
1415
- 'speech-provider-replacement-state-revision-exhausted',
1416
- 'AI speech provider replacement cannot publish another runtime state revision.'
1417
- );
1418
- }
1419
-
1420
1765
  for (const role of SPEECH_ROLES) {
1421
1766
  const expected = replacement.expectedProviders[role];
1422
1767
  const provider = replacement.providers[role];
@@ -1447,10 +1792,10 @@ export class AIProviderRuntime {
1447
1792
  }
1448
1793
  );
1449
1794
 
1450
- return Object.freeze(
1795
+ return completeValue(
1451
1796
  {
1452
1797
  routes: replacement.routes,
1453
- unregisters: Object.freeze(
1798
+ unregisters: completeValue(
1454
1799
  {
1455
1800
  stt: replacement.removal
1456
1801
  ? null
@@ -1538,16 +1883,16 @@ export class AIProviderRuntime {
1538
1883
  fail('AI provider.catalog() must synchronously return an array.');
1539
1884
  }
1540
1885
  entries.push(
1541
- Object.freeze(
1886
+ completeValue(
1542
1887
  {
1543
1888
  providerId: provider.id,
1544
1889
  localOnly: provider.localOnly,
1545
- models: Object.freeze(providerCatalog.slice())
1890
+ models: completeValue([...providerCatalog])
1546
1891
  }
1547
1892
  )
1548
1893
  );
1549
1894
  }
1550
- return Object.freeze(entries);
1895
+ return completeValue(entries);
1551
1896
  }
1552
1897
 
1553
1898
  async inspect(role, options = {}) {
@@ -1564,7 +1909,7 @@ export class AIProviderRuntime {
1564
1909
  const generation = slot.generation;
1565
1910
  const selection = this.selection(role, {localOnly});
1566
1911
  if (!selection) {
1567
- return Object.freeze(
1912
+ return completeValue(
1568
1913
  {
1569
1914
  available: false,
1570
1915
  code: 'ARCANE_AI_ROLE_NOT_SELECTED',
@@ -1576,7 +1921,7 @@ export class AIProviderRuntime {
1576
1921
  providerKey(role, selection.providerId)
1577
1922
  ) ?? null;
1578
1923
  if (!provider) {
1579
- return Object.freeze(
1924
+ return completeValue(
1580
1925
  {
1581
1926
  available: false,
1582
1927
  code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
@@ -1608,7 +1953,7 @@ export class AIProviderRuntime {
1608
1953
  error,
1609
1954
  'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
1610
1955
  );
1611
- return Object.freeze(
1956
+ return completeValue(
1612
1957
  {
1613
1958
  available: false,
1614
1959
  code: unavailable.code,
@@ -1627,7 +1972,7 @@ export class AIProviderRuntime {
1627
1972
  error,
1628
1973
  'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED'
1629
1974
  );
1630
- return Object.freeze(
1975
+ return completeValue(
1631
1976
  {
1632
1977
  available: false,
1633
1978
  code: unavailable.code,
@@ -1705,7 +2050,7 @@ export class AIProviderRuntime {
1705
2050
  )
1706
2051
  );
1707
2052
  }
1708
- if (slot.request || slot.requestAdmission) {
2053
+ if (slot.request || slot.requestQueue.length) {
1709
2054
  return Promise.reject(
1710
2055
  operationError(
1711
2056
  `AI role ${role} cannot load during active request ownership.`,
@@ -1903,7 +2248,7 @@ export class AIProviderRuntime {
1903
2248
  state: 'loading',
1904
2249
  operationId,
1905
2250
  progress: {
1906
- phase: 'admission',
2251
+ phase: 'loading',
1907
2252
  completed: 0,
1908
2253
  total: null,
1909
2254
  unit: 'items',
@@ -1948,8 +2293,8 @@ export class AIProviderRuntime {
1948
2293
  || providerStatus?.loaded === true
1949
2294
  || providerStatus?.busy === true;
1950
2295
  if (!slot.loadPromise
1951
- && !slot.requestAdmission
1952
2296
  && !slot.request
2297
+ && slot.requestQueue.length === 0
1953
2298
  && !providerOwned
1954
2299
  && (!provider || providerStatus)) {
1955
2300
  slot.ready = false;
@@ -1967,8 +2312,12 @@ export class AIProviderRuntime {
1967
2312
  this.#speechMuted = true;
1968
2313
  }
1969
2314
  slot.loadController?.abort();
1970
- slot.requestAdmission?.controller.abort();
1971
- 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);
1972
2321
  const capturedLoad = slot.loadPromise;
1973
2322
  const capturedRequestRecord = slot.request;
1974
2323
  const capturedRequest = capturedRequestRecord?.promise ?? null;
@@ -1983,12 +2332,7 @@ export class AIProviderRuntime {
1983
2332
  }
1984
2333
  if (capturedRequestRecord?.cancel) {
1985
2334
  try {
1986
- await capturedRequestRecord.cancel(
1987
- operationError(
1988
- `AI role ${role} is unloading.`,
1989
- 'ARCANE_AI_REQUEST_ABORTED'
1990
- )
1991
- );
2335
+ await capturedRequestRecord.cancel(requestCancellation);
1992
2336
  } catch {
1993
2337
  // Provider unload below remains the authoritative cleanup.
1994
2338
  }
@@ -2213,13 +2557,13 @@ export class AIProviderRuntime {
2213
2557
  for (const role of AI_RUNTIME_ROLES) {
2214
2558
  const slot = runtime.#slots[role];
2215
2559
  slot.generation += 1;
2216
- slot.routes = Object.freeze({default: null, localOnly: null});
2560
+ slot.routes = completeValue({default: null, localOnly: null});
2217
2561
  slot.selection = null;
2218
2562
  slot.loadController = null;
2219
2563
  slot.loadPromise = null;
2220
2564
  slot.unloadPromise = null;
2221
2565
  slot.disposePromise = null;
2222
- slot.requestAdmission = null;
2566
+ slot.requestQueue = [];
2223
2567
  slot.request = null;
2224
2568
  slot.ready = false;
2225
2569
  slot.disposed = true;
@@ -2270,6 +2614,9 @@ export class AIProviderRuntime {
2270
2614
  assertAbortSignal(options.signal);
2271
2615
  try {
2272
2616
  assertCallbackFreeProviderValue(options.payload);
2617
+ if (role === 'llm') {
2618
+ validateLLMRequestPayload(options.payload);
2619
+ }
2273
2620
  } catch (error) {
2274
2621
  return Promise.reject(error);
2275
2622
  }
@@ -2346,8 +2693,8 @@ export class AIProviderRuntime {
2346
2693
  )
2347
2694
  );
2348
2695
  }
2349
- if (slot.request || slot.requestAdmission) {
2350
- return this.#supersedeRoleRequest(slot, provider, options);
2696
+ if (slot.request) {
2697
+ return this.#enqueueRoleRequest(slot, options);
2351
2698
  }
2352
2699
  if (!slot.ready
2353
2700
  || providerStatus.state !== 'ready'
@@ -2392,7 +2739,6 @@ export class AIProviderRuntime {
2392
2739
  function restoreAIProviderRoleAfterRequest(error) {
2393
2740
  if (slot.generation !== generation
2394
2741
  || slot.requestSequence !== requestSequence
2395
- || slot.requestAdmission
2396
2742
  || slot.unloadPromise) {
2397
2743
  return;
2398
2744
  }
@@ -2451,6 +2797,7 @@ export class AIProviderRuntime {
2451
2797
  if (slot.request === requestRecord) {
2452
2798
  slot.request = null;
2453
2799
  }
2800
+ runtime.#drainRoleRequestQueue(slot);
2454
2801
  if (error) {
2455
2802
  restoreAIProviderRoleAfterRequest(error);
2456
2803
  rejectTerminal(error);
@@ -2480,7 +2827,7 @@ export class AIProviderRuntime {
2480
2827
  }
2481
2828
 
2482
2829
  async function cleanupAIProviderStreamHandle(opened, activeIterator, reason) {
2483
- return awaitBoundedStreamCleanup(
2830
+ return awaitStreamCleanup(
2484
2831
  beginAIProviderStreamHandleCleanup(
2485
2832
  opened,
2486
2833
  activeIterator,
@@ -2522,7 +2869,7 @@ export class AIProviderRuntime {
2522
2869
  // A rejected open confirms that no provider handle was returned.
2523
2870
  }
2524
2871
  );
2525
- return awaitBoundedStreamCleanup(
2872
+ return awaitStreamCleanup(
2526
2873
  Promise.allSettled([lateCleanup])
2527
2874
  );
2528
2875
  }
@@ -2539,7 +2886,7 @@ export class AIProviderRuntime {
2539
2886
  rejectCleanup = reject;
2540
2887
  }
2541
2888
  );
2542
- controller.abort();
2889
+ controller.abort(reason);
2543
2890
  (async function closeAIProviderStream() {
2544
2891
  const terminalOutcome = terminalError ?? normalizedAbort(
2545
2892
  reason instanceof Error
@@ -2628,6 +2975,12 @@ export class AIProviderRuntime {
2628
2975
  }
2629
2976
  providerHandle = opened;
2630
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
+ }
2631
2984
  runtime.#assertCurrentRequest(
2632
2985
  slot,
2633
2986
  generation,
@@ -2643,7 +2996,13 @@ export class AIProviderRuntime {
2643
2996
  requestSequence,
2644
2997
  controller.signal
2645
2998
  );
2646
- settleAIProviderStream(null, value);
2999
+ const terminalValue = role === 'llm'
3000
+ ? validateLLMTerminalResult(
3001
+ value,
3002
+ 'AI provider stream result'
3003
+ )
3004
+ : value;
3005
+ settleAIProviderStream(null, terminalValue);
2647
3006
  } catch (error) {
2648
3007
  const normalized = isAbort(error, controller.signal)
2649
3008
  ? normalizedAbort(error)
@@ -2668,14 +3027,30 @@ export class AIProviderRuntime {
2668
3027
  cancel: cancelAIProviderStream,
2669
3028
  async next(value) {
2670
3029
  try {
2671
- const result = await iterator.next(value);
2672
- runtime.#assertCurrentRequest(
2673
- slot,
2674
- generation,
2675
- requestSequence,
2676
- controller.signal
2677
- );
2678
- 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
+ }
2679
3054
  } catch (error) {
2680
3055
  await cancelAIProviderStream(error);
2681
3056
  throw isAbort(error, controller.signal)
@@ -2684,11 +3059,22 @@ export class AIProviderRuntime {
2684
3059
  }
2685
3060
  },
2686
3061
  async return(value) {
2687
- await cancelAIProviderStream(
2688
- operationError(
2689
- 'The AI stream consumer stopped before completion.',
2690
- 'ARCANE_AI_REQUEST_ABORTED'
2691
- )
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
+ }
2692
3078
  );
2693
3079
  return {value, done: true};
2694
3080
  },
@@ -2700,7 +3086,7 @@ export class AIProviderRuntime {
2700
3086
  return this;
2701
3087
  }
2702
3088
  };
2703
- return Object.freeze(handle);
3089
+ return completeValue(handle);
2704
3090
  } catch (error) {
2705
3091
  const normalized = isAbort(error, controller.signal)
2706
3092
  ? normalizedAbort(error)
@@ -2737,8 +3123,8 @@ export class AIProviderRuntime {
2737
3123
  return openPromise;
2738
3124
  }
2739
3125
 
2740
- requestRecord.cancel = async function cancelAIProviderRequest() {
2741
- controller.abort();
3126
+ requestRecord.cancel = async function cancelAIProviderRequest(reason) {
3127
+ controller.abort(reason);
2742
3128
  await requestRecord.promise?.catch(
2743
3129
  function retainCancelledAIProviderRequest() {}
2744
3130
  );
@@ -2764,7 +3150,12 @@ export class AIProviderRuntime {
2764
3150
  requestSequence,
2765
3151
  controller.signal
2766
3152
  );
2767
- return result;
3153
+ return role === 'llm'
3154
+ ? validateLLMTerminalResult(
3155
+ result,
3156
+ 'AI provider chat result'
3157
+ )
3158
+ : result;
2768
3159
  } catch (error) {
2769
3160
  const normalized = isAbort(error, controller.signal)
2770
3161
  ? normalizedAbort(error)
@@ -2777,6 +3168,7 @@ export class AIProviderRuntime {
2777
3168
  if (slot.request === requestRecord) {
2778
3169
  slot.request = null;
2779
3170
  }
3171
+ runtime.#drainRoleRequestQueue(slot);
2780
3172
  if (!requestError
2781
3173
  && slot.generation === generation
2782
3174
  && !slot.unloadPromise) {
@@ -2806,18 +3198,16 @@ export class AIProviderRuntime {
2806
3198
  cancel(role) {
2807
3199
  assertRole(role);
2808
3200
  const slot = this.#slots[role];
2809
- if (!slot.request && !slot.requestAdmission) {
3201
+ if (!slot.request) {
2810
3202
  return false;
2811
3203
  }
2812
- slot.requestAdmission?.controller.abort();
2813
3204
  const requestRecord = slot.request;
2814
- requestRecord?.controller.abort();
2815
- const cancellation = requestRecord?.cancel?.(
2816
- operationError(
2817
- `AI role ${role} request was cancelled.`,
2818
- 'ARCANE_AI_REQUEST_ABORTED'
2819
- )
3205
+ const reason = operationError(
3206
+ `AI role ${role} request was cancelled.`,
3207
+ 'ARCANE_AI_REQUEST_ABORTED'
2820
3208
  );
3209
+ requestRecord?.controller.abort(reason);
3210
+ const cancellation = requestRecord?.cancel?.(reason);
2821
3211
  cancellation?.catch(function retainAIProviderCancelFailureInState() {});
2822
3212
  return true;
2823
3213
  }
@@ -2907,126 +3297,83 @@ export class AIProviderRuntime {
2907
3297
  );
2908
3298
  }
2909
3299
 
2910
- #supersedeRoleRequest(slot, provider, options) {
2911
- const generation = slot.generation;
2912
- const requestSequence = this.#nextRequestSequence(slot);
2913
- const priorAdmission = slot.requestAdmission;
2914
- const activeRequest = priorAdmission?.activeRequest ?? slot.request;
2915
- const controller = new AbortController();
2916
- const detachSignal = this.#forwardAbort(options.signal, controller);
2917
- let settlement = priorAdmission?.settlement ?? null;
2918
- if (!settlement && activeRequest) {
2919
- activeRequest.controller.abort();
2920
- settlement = Promise.allSettled(
2921
- [
2922
- Promise.resolve().then(
2923
- function cancelSupersededAIProviderRequest() {
2924
- return activeRequest.cancel(
2925
- operationError(
2926
- `AI role ${slot.role} request was superseded.`,
2927
- 'ARCANE_AI_OPERATION_SUPERSEDED'
2928
- )
2929
- );
2930
- }
2931
- ),
2932
- activeRequest.promise
2933
- ]
2934
- );
2935
- }
2936
- if (!settlement) {
2937
- settlement = Promise.resolve([]);
2938
- }
2939
- const admission = {
2940
- activeRequest,
2941
- controller,
2942
- generation,
2943
- requestSequence,
2944
- settlement
2945
- };
2946
- slot.requestAdmission = admission;
3300
+ #enqueueRoleRequest(slot, options) {
2947
3301
  const runtime = this;
2948
- return (async function supersedeAIProviderRoleRequest() {
2949
- try {
2950
- const outcomes = await settlement;
2951
- runtime.#assertCurrentRequestAdmission(slot, admission);
2952
- if (outcomes[0]?.status === 'rejected') {
2953
- 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;
2954
3313
  }
2955
- let providerStatus;
2956
- try {
2957
- providerStatus = validateProviderStatus(provider.status());
2958
- } catch (error) {
2959
- throw operationError(
2960
- `The selected ${slot.role} provider did not return a valid status.`,
2961
- 'ARCANE_AI_PROVIDER_STATUS_INVALID',
2962
- error
2963
- );
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;
2964
3322
  }
2965
- if (!slot.ready
2966
- || providerStatus.state !== 'ready'
2967
- || providerStatus.loaded !== true
2968
- || providerStatus.busy !== false) {
2969
- slot.requestAdmission = null;
2970
- slot.ready = false;
2971
- const notReady = operationError(
2972
- `AI role ${slot.role} is not ready after superseding its active request.`,
2973
- '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
2974
3332
  );
2975
- if (providerStatus.state === 'unloaded'
2976
- && !providerStatus.loaded
2977
- && !providerStatus.busy) {
2978
- publishAIRuntimeRoleState(
2979
- slot.role,
2980
- roleRecord(slot.role, slot.selection)
2981
- );
2982
- } else if (providerStatus.state === 'ready'
2983
- && providerStatus.loaded
2984
- && providerStatus.busy) {
2985
- slot.ready = true;
2986
- publishAIRuntimeRoleState(
2987
- slot.role,
2988
- roleRecord(
2989
- slot.role,
2990
- slot.selection,
2991
- {
2992
- state: 'ready',
2993
- loaded: true,
2994
- busy: true
2995
- }
2996
- )
2997
- );
2998
- } else {
2999
- runtime.#publishRoleError(
3000
- slot,
3001
- notReady,
3002
- providerStatus.loaded
3003
- );
3004
- }
3005
- throw notReady;
3006
- }
3007
- slot.requestAdmission = null;
3008
- return await runtime.request(slot.role, options);
3009
- } catch (error) {
3010
- const normalized = isAbort(error, controller.signal)
3011
- ? normalizedAbort(error)
3012
- : error;
3013
- if (slot.requestAdmission === admission) {
3014
- slot.requestAdmission = null;
3015
- if (slot.generation === generation
3016
- && !slot.unloadPromise
3017
- && !slot.disposePromise) {
3018
- runtime.#publishRequestAdmissionFailure(
3019
- slot,
3020
- provider,
3021
- normalized
3022
- );
3023
- }
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);
3024
3366
  }
3025
- throw normalized;
3026
- } finally {
3027
- detachSignal();
3028
3367
  }
3029
- })();
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
+ }
3030
3377
  }
3031
3378
 
3032
3379
  #sameSelection(left, right) {
@@ -3067,8 +3414,8 @@ export class AIProviderRuntime {
3067
3414
  slot.loadPromise
3068
3415
  || slot.unloadPromise
3069
3416
  || slot.disposePromise
3070
- || slot.requestAdmission
3071
3417
  || slot.request
3418
+ || slot.requestQueue.length > 0
3072
3419
  || slot.ready
3073
3420
  );
3074
3421
  }
@@ -3093,24 +3440,12 @@ export class AIProviderRuntime {
3093
3440
  }
3094
3441
 
3095
3442
  #nextRequestSequence(slot) {
3096
- if (slot.requestSequence === Number.MAX_SAFE_INTEGER) {
3097
- throw operationError(
3098
- `AI role ${slot.role} exhausted its request sequence.`,
3099
- 'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
3100
- );
3101
- }
3102
- slot.requestSequence += 1;
3443
+ slot.requestSequence = nextSequence(slot.requestSequence);
3103
3444
  return slot.requestSequence;
3104
3445
  }
3105
3446
 
3106
3447
  #nextOperationId(slot, operation) {
3107
- if (slot.operationSequence === Number.MAX_SAFE_INTEGER) {
3108
- throw operationError(
3109
- `AI role ${slot.role} exhausted its operation sequence.`,
3110
- 'ARCANE_AI_OPERATION_SEQUENCE_EXHAUSTED'
3111
- );
3112
- }
3113
- slot.operationSequence += 1;
3448
+ slot.operationSequence = nextSequence(slot.operationSequence);
3114
3449
  return `${slot.role}-${operation}-${slot.operationSequence}`;
3115
3450
  }
3116
3451
 
@@ -3130,22 +3465,8 @@ export class AIProviderRuntime {
3130
3465
  this.#assertCurrentOperation(slot, generation, signal);
3131
3466
  if (slot.requestSequence !== requestSequence) {
3132
3467
  throw operationError(
3133
- `AI role ${slot.role} request was superseded.`,
3134
- 'ARCANE_AI_OPERATION_SUPERSEDED'
3135
- );
3136
- }
3137
- }
3138
-
3139
- #assertCurrentRequestAdmission(slot, admission) {
3140
- if (admission.controller.signal.aborted) {
3141
- throw normalizedAbort();
3142
- }
3143
- if (slot.generation !== admission.generation
3144
- || slot.requestSequence !== admission.requestSequence
3145
- || slot.requestAdmission !== admission) {
3146
- throw operationError(
3147
- `AI role ${slot.role} request admission was superseded.`,
3148
- 'ARCANE_AI_OPERATION_SUPERSEDED'
3468
+ `AI role ${slot.role} no longer owns the active provider request.`,
3469
+ 'ARCANE_AI_REQUEST_STALE'
3149
3470
  );
3150
3471
  }
3151
3472
  }
@@ -3155,10 +3476,10 @@ export class AIProviderRuntime {
3155
3476
  return function detachAbsentAIProviderAbort() {};
3156
3477
  }
3157
3478
  function forwardAIProviderAbort() {
3158
- controller.abort();
3479
+ controller.abort(signal.reason);
3159
3480
  }
3160
3481
  if (signal.aborted) {
3161
- controller.abort();
3482
+ controller.abort(signal.reason);
3162
3483
  return function detachAlreadyAbortedAIProviderSignal() {};
3163
3484
  }
3164
3485
  signal.addEventListener('abort', forwardAIProviderAbort, {once: true});
@@ -3186,68 +3507,6 @@ export class AIProviderRuntime {
3186
3507
  return true;
3187
3508
  }
3188
3509
 
3189
- #publishRequestAdmissionFailure(slot, provider, error) {
3190
- let providerStatus = null;
3191
- try {
3192
- providerStatus = validateProviderStatus(provider.status());
3193
- } catch {
3194
- // The admission error remains authoritative when status is malformed.
3195
- }
3196
- if (isAbort(error, null)
3197
- && providerStatus?.state === 'ready'
3198
- && providerStatus.loaded
3199
- && !providerStatus.busy) {
3200
- slot.ready = true;
3201
- publishAIRuntimeRoleState(
3202
- slot.role,
3203
- roleRecord(
3204
- slot.role,
3205
- slot.selection,
3206
- {
3207
- state: 'ready',
3208
- loaded: true
3209
- }
3210
- )
3211
- );
3212
- return;
3213
- }
3214
- if (isAbort(error, null)
3215
- && providerStatus?.state === 'unloaded'
3216
- && !providerStatus.loaded
3217
- && !providerStatus.busy) {
3218
- slot.ready = false;
3219
- publishAIRuntimeRoleState(
3220
- slot.role,
3221
- roleRecord(slot.role, slot.selection)
3222
- );
3223
- return;
3224
- }
3225
- if (providerStatus?.state === 'ready'
3226
- && providerStatus.loaded
3227
- && providerStatus.busy) {
3228
- slot.ready = true;
3229
- publishAIRuntimeRoleState(
3230
- slot.role,
3231
- roleRecord(
3232
- slot.role,
3233
- slot.selection,
3234
- {
3235
- state: 'ready',
3236
- loaded: true,
3237
- busy: true
3238
- }
3239
- )
3240
- );
3241
- return;
3242
- }
3243
- slot.ready = false;
3244
- this.#publishRoleError(
3245
- slot,
3246
- error,
3247
- providerStatus?.loaded === true
3248
- );
3249
- }
3250
-
3251
3510
  #publishRoleError(slot, error, loaded) {
3252
3511
  publishAIRuntimeRoleState(
3253
3512
  slot.role,