arcane-os 0.5.16 → 0.5.18

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 (190) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +42 -19
  3. package/bin/arcane-test.mjs +16 -13
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
  5. package/browser-runtime/ai/browser-speech-providers.mjs +53 -46
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
  7. package/browser-runtime/ai/browser-wasm.mjs +6 -3
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
  9. package/browser-runtime/ai/model-controller.mjs +94 -91
  10. package/browser-runtime/ai/speech-worker-client.mjs +12 -9
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
  12. package/browser-runtime/dependencies/strong-type/index.js +207 -82
  13. package/browser-runtime/dependencies/strong-type/package.json +15 -6
  14. package/browser-runtime/dom-event-instrumentation.mjs +24 -20
  15. package/browser-runtime/event-manager.mjs +111 -108
  16. package/browser-runtime/speech-text.mjs +35 -0
  17. package/docs/architecture.md +1 -1
  18. package/docs/reference/README.md +8 -8
  19. package/docs/reference/ai/browser-speech.md +230 -16
  20. package/docs/reference/ai/twin-cloud.md +1 -1
  21. package/docs/reference/availability-and-normalization.md +42 -1
  22. package/docs/reference/cli.md +1 -1
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/inventory/package-api.json +60 -4
  25. package/docs/reference/inventory/runtime-components.json +2 -2
  26. package/docs/reference/inventory/runtime-modules.json +29 -31
  27. package/docs/reference/protocols.md +17 -13
  28. package/docs/reference/runtime-components.md +6 -5
  29. package/docs/reference/runtime-modules.md +212 -70
  30. package/docs/reference/sdk-api.md +197 -11
  31. package/examples/wasm-ai-demo/index.html +1 -0
  32. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  33. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  34. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  35. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  36. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  37. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  38. package/node_modules/strong-type/README.md +42 -24
  39. package/node_modules/strong-type/benchmark/core.js +324 -0
  40. package/node_modules/strong-type/index.js +207 -82
  41. package/node_modules/strong-type/package.json +14 -6
  42. package/package.json +4 -3
  43. package/runtime/arcane/components/app-bar.html +5 -2
  44. package/runtime/arcane/components/assistant-panel.html +5 -2
  45. package/runtime/arcane/components/chart.html +15 -12
  46. package/runtime/arcane/components/chat.html +118 -141
  47. package/runtime/arcane/components/dashboard-config.html +6 -3
  48. package/runtime/arcane/components/data-view.html +4 -1
  49. package/runtime/arcane/components/directory-picker.html +5 -2
  50. package/runtime/arcane/components/document-inspector.html +6 -3
  51. package/runtime/arcane/components/file-drop.html +5 -2
  52. package/runtime/arcane/components/file-inspector.html +9 -6
  53. package/runtime/arcane/components/file-manager.html +16 -13
  54. package/runtime/arcane/components/local-ai-status.html +5 -2
  55. package/runtime/arcane/components/markdown-document.html +14 -11
  56. package/runtime/arcane/components/markdown-editor.html +4 -1
  57. package/runtime/arcane/components/modal.html +4 -1
  58. package/runtime/arcane/components/output-panel.html +6 -3
  59. package/runtime/arcane/components/preferences-form.html +4 -1
  60. package/runtime/arcane/components/record-timeline.html +8 -5
  61. package/runtime/arcane/components/relationship-board.html +9 -6
  62. package/runtime/arcane/components/source-code-viewer.html +12 -9
  63. package/runtime/arcane/components/source-explanation.html +8 -5
  64. package/runtime/arcane/components/speech.html +24 -21
  65. package/runtime/arcane/components/summary-strip.html +4 -1
  66. package/runtime/arcane/components/task-progress.html +6 -3
  67. package/runtime/arcane/components/terminal-workspace.html +4 -1
  68. package/runtime/arcane/components/voice-transcription.html +11 -8
  69. package/runtime/arcane/components/web-navigator.html +5 -2
  70. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  71. package/runtime/arcane/entities/Calculation.js +5 -2
  72. package/runtime/arcane/entities/Chat.js +33 -33
  73. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  74. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  75. package/runtime/arcane/entities/File.js +1 -1
  76. package/runtime/arcane/entities/Image.js +8 -5
  77. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  78. package/runtime/arcane/entities/Preference.js +9 -6
  79. package/runtime/arcane/entities/Theme.js +5 -2
  80. package/runtime/arcane/entities/User.js +11 -11
  81. package/runtime/arcane/entities/Weather.js +5 -2
  82. package/runtime/arcane/modules/AI.js +503 -187
  83. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  84. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  85. package/runtime/arcane/modules/AIProviderRuntime.js +117 -92
  86. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  87. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  88. package/runtime/arcane/modules/AnsiText.js +4 -1
  89. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  90. package/runtime/arcane/modules/AppDataScope.js +8 -5
  91. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  92. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  93. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  94. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  95. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  96. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  97. package/runtime/arcane/modules/ChatRecords.js +18 -15
  98. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  99. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  100. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  101. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  102. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  103. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  104. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  105. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  106. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  107. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  108. package/runtime/arcane/modules/DBLS.js +7 -4
  109. package/runtime/arcane/modules/DBOPFS.js +7 -7
  110. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  111. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  112. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  113. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  114. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  115. package/runtime/arcane/modules/Errors.js +28 -25
  116. package/runtime/arcane/modules/HTMLImport.js +7 -4
  117. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  118. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  119. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  120. package/runtime/arcane/modules/MD.js +1 -1
  121. package/runtime/arcane/modules/Mail.js +46 -43
  122. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  123. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  124. package/runtime/arcane/modules/MarkdownSpeech.js +1 -50
  125. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  126. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  127. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  128. package/runtime/arcane/modules/Ollama.js +6 -3
  129. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  130. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  131. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  132. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  133. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  134. package/runtime/arcane/modules/Questionnaire.js +6 -3
  135. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  136. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  137. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  138. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  139. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  140. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  141. package/runtime/arcane/modules/SpeechPlayback.js +61 -28
  142. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  143. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  144. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  145. package/runtime/arcane/modules/TerminalClient.js +14 -11
  146. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  147. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  148. package/runtime/arcane/modules/ThemeManager.js +5 -2
  149. package/runtime/arcane/modules/TimeGuard.js +1 -1
  150. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  151. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  152. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  153. package/runtime/strong-type/index.js +1276 -352
  154. package/runtime/strong-type/package.json +69 -45
  155. package/src/app-descriptor.mjs +17 -14
  156. package/src/application-tests.mjs +4 -1
  157. package/src/cli/main.mjs +8 -5
  158. package/src/constants.mjs +4 -1
  159. package/src/dev-server.mjs +8 -5
  160. package/src/doctor.mjs +5 -2
  161. package/src/dom-event-instrumentation.mjs +24 -20
  162. package/src/errors.mjs +7 -3
  163. package/src/event-manager.mjs +111 -108
  164. package/src/event-queue.mjs +8 -5
  165. package/src/events.mjs +12 -9
  166. package/src/import-map.mjs +36 -27
  167. package/src/installed-sdk-runtime.mjs +4 -1
  168. package/src/integrated-provider-loader.mjs +9 -6
  169. package/src/mail-credentials.mjs +16 -13
  170. package/src/mail-server.mjs +53 -50
  171. package/src/mail.mjs +18 -15
  172. package/src/native-plan.mjs +12 -9
  173. package/src/native-provider-loader.mjs +7 -4
  174. package/src/packager/core.mjs +18 -15
  175. package/src/process.mjs +6 -3
  176. package/src/release-bundle.mjs +14 -11
  177. package/src/runtime.mjs +4 -1
  178. package/src/scaffold.mjs +9 -6
  179. package/src/sdk-browser-runtime.mjs +4 -1
  180. package/src/source-server.mjs +17 -14
  181. package/src/targets/index.mjs +5 -2
  182. package/src/templates/workspace-template.mjs +13 -6
  183. package/src/testing-loader.mjs +7 -4
  184. package/src/testing.mjs +10 -7
  185. package/src/toolchain.mjs +13 -10
  186. package/src/update-check.mjs +9 -6
  187. package/src/workspace-operation-lock.mjs +14 -11
  188. package/src/workspace-runtime.mjs +4 -1
  189. package/src/workspace.mjs +17 -14
  190. package/runtime/arcane/modules/AIResponseLength.js +0 -32
@@ -1,4 +1,8 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  import {arcaneLogging} from 'arcane-os/logging';
5
+ import {stripSpeechFormatting} from 'arcane-os/speech-text';
2
6
  import {
3
7
  AI_RUNTIME_ROLES,
4
8
  getAIRuntimeState,
@@ -87,7 +91,7 @@ async function awaitStreamCleanup(operation) {
87
91
 
88
92
  function assertStreamCleanupComplete(outcome) {
89
93
  const rejected = outcome.completed
90
- && Array.isArray(outcome.results)
94
+ && is.array(outcome.results)
91
95
  && outcome.results.some(function hasRejectedAIStreamCleanup(result) {
92
96
  return result.status === 'rejected';
93
97
  });
@@ -106,7 +110,7 @@ function assertRole(role) {
106
110
  }
107
111
 
108
112
  function assertIdentifier(value, label) {
109
- if (typeof value !== 'string'
113
+ if (!is.string(value)
110
114
  || value.length < 1
111
115
  || value.trim() !== value) {
112
116
  fail(`${label} must be a nonempty trimmed string.`);
@@ -114,7 +118,7 @@ function assertIdentifier(value, label) {
114
118
  }
115
119
 
116
120
  function nextSequence(value) {
117
- if (typeof value === 'bigint') {
121
+ if (is.bigint(value)) {
118
122
  return value + 1n;
119
123
  }
120
124
  if (value === Number.MAX_SAFE_INTEGER) {
@@ -128,16 +132,16 @@ function assertAbortSignal(signal) {
128
132
  return;
129
133
  }
130
134
 
131
- if (typeof signal !== 'object'
132
- || typeof signal.aborted !== 'boolean'
133
- || typeof signal.addEventListener !== 'function'
134
- || typeof signal.removeEventListener !== 'function') {
135
+ if (!is.object(signal)
136
+ || !is.boolean(signal.aborted)
137
+ || !is.function(signal.addEventListener)
138
+ || !is.function(signal.removeEventListener)) {
135
139
  fail('AI provider operation signal must be an AbortSignal.');
136
140
  }
137
141
  }
138
142
 
139
143
  function assertPlainObject(value, label) {
140
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
144
+ if (!value || !is.object(value) || is.array(value)) {
141
145
  fail(`${label} must be a plain object.`);
142
146
  }
143
147
 
@@ -148,7 +152,7 @@ function assertPlainObject(value, label) {
148
152
  }
149
153
 
150
154
  function isPlainRecord(value) {
151
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
155
+ if (!value || !is.object(value) || is.array(value)) {
152
156
  return false;
153
157
  }
154
158
  const prototype = Object.getPrototypeOf(value);
@@ -159,7 +163,7 @@ function validateLLMToolDeclarations(value) {
159
163
  if (value === undefined) {
160
164
  return;
161
165
  }
162
- if (!Array.isArray(value)) {
166
+ if (!is.array(value)) {
163
167
  fail(
164
168
  'AI LLM tools must be an array.',
165
169
  'ARCANE_AI_TOOL_CALL_INVALID'
@@ -177,9 +181,9 @@ function validateLLMToolDeclarations(value) {
177
181
  || !isPlainRecord(parameters.properties)
178
182
  || !isPlainRecord(messageSchema)
179
183
  || messageSchema.type !== 'string'
180
- || !Number.isInteger(messageSchema.minLength)
184
+ || !is.integer(messageSchema.minLength)
181
185
  || messageSchema.minLength < 1
182
- || !Array.isArray(parameters.required)
186
+ || !is.array(parameters.required)
183
187
  || !parameters.required.includes('message')) {
184
188
  fail(
185
189
  `AI LLM tools[${index}] must require a nonempty string parameters.properties.message.`,
@@ -191,13 +195,13 @@ function validateLLMToolDeclarations(value) {
191
195
 
192
196
  function validateLLMToolCall(call, label) {
193
197
  if (!isPlainRecord(call)
194
- || typeof call.id !== 'string'
198
+ || !is.string(call.id)
195
199
  || !call.id.trim()
196
200
  || call.type !== 'function'
197
201
  || !isPlainRecord(call.function)
198
- || typeof call.function.name !== 'string'
202
+ || !is.string(call.function.name)
199
203
  || !call.function.name.trim()
200
- || typeof call.function.arguments !== 'string') {
204
+ || !is.string(call.function.arguments)) {
201
205
  fail(
202
206
  `${label} must be one complete structural function call.`,
203
207
  'ARCANE_AI_TOOL_CALL_INVALID'
@@ -219,7 +223,7 @@ function validateLLMToolCall(call, label) {
219
223
  'ARCANE_AI_TOOL_CALL_INVALID'
220
224
  );
221
225
  }
222
- if (typeof argumentsRecord.message !== 'string'
226
+ if (!is.string(argumentsRecord.message)
223
227
  || !argumentsRecord.message.trim()) {
224
228
  fail(
225
229
  `${label} arguments must include a nonempty user-facing message.`,
@@ -251,7 +255,7 @@ function validateLLMMessageToolCalls(message, label) {
251
255
  }
252
256
  const descriptor = Object.getOwnPropertyDescriptor(message, 'tool_calls');
253
257
  if (!descriptor || !Object.hasOwn(descriptor, 'value')
254
- || !Array.isArray(descriptor.value)) {
258
+ || !is.array(descriptor.value)) {
255
259
  fail(
256
260
  `${label}.tool_calls must be an array data property.`,
257
261
  'ARCANE_AI_TOOL_CALL_INVALID'
@@ -276,7 +280,7 @@ function validateLLMMessageToolCalls(message, label) {
276
280
 
277
281
  function validateLLMRequestPayload(payload) {
278
282
  assertPlainObject(payload, 'AI LLM request payload');
279
- if (!Array.isArray(payload.messages)) {
283
+ if (!is.array(payload.messages)) {
280
284
  fail('AI LLM request payload.messages must be an array.');
281
285
  }
282
286
  const pendingToolCallIds = new Set();
@@ -316,7 +320,7 @@ function validateLLMRequestPayload(payload) {
316
320
  openedToolCall = true;
317
321
  }
318
322
  if (message.role === 'tool') {
319
- if (typeof message.content !== 'string'
323
+ if (!is.string(message.content)
320
324
  || !message.content.trim()) {
321
325
  fail(
322
326
  `AI LLM request payload.messages[${index}] must contain a nonblank user-facing tool result.`,
@@ -324,7 +328,7 @@ function validateLLMRequestPayload(payload) {
324
328
  );
325
329
  }
326
330
  if (!pendingToolCallIds.size
327
- || typeof message.tool_call_id !== 'string'
331
+ || !is.string(message.tool_call_id)
328
332
  || !pendingToolCallIds.has(message.tool_call_id)) {
329
333
  fail(
330
334
  `AI LLM request payload.messages[${index}] does not settle the pending structural tool call.`,
@@ -359,7 +363,7 @@ function validateLLMRequestPayload(payload) {
359
363
  payload.parallel_tool_calls
360
364
  ];
361
365
  if (parallelValues.some(function invalidParallelLLMToolPreference(value) {
362
- return value !== undefined && typeof value !== 'boolean';
366
+ return value !== undefined && !is.boolean(value);
363
367
  })) {
364
368
  fail('AI LLM parallel tool-call preferences must be boolean when provided.');
365
369
  }
@@ -376,7 +380,7 @@ function validateLLMTerminalMessage(message, label) {
376
380
  }
377
381
 
378
382
  function validateLLMTerminalResult(value, label) {
379
- if (typeof value === 'string') {
383
+ if (is.string(value)) {
380
384
  return value;
381
385
  }
382
386
  if (!isPlainRecord(value)) {
@@ -406,7 +410,7 @@ function validateLLMTerminalResult(value, label) {
406
410
  }
407
411
  const choicesDescriptor = Object.getOwnPropertyDescriptor(value, 'choices');
408
412
  if (!choicesDescriptor || !Object.hasOwn(choicesDescriptor, 'value')
409
- || !Array.isArray(choicesDescriptor.value)
413
+ || !is.array(choicesDescriptor.value)
410
414
  || choicesDescriptor.value.length === 0) {
411
415
  fail(
412
416
  `${label}.choices must be a nonempty array data property.`,
@@ -417,7 +421,7 @@ function validateLLMTerminalResult(value, label) {
417
421
  for (let position = 0; position < choicesDescriptor.value.length; position += 1) {
418
422
  const choice = choicesDescriptor.value[position];
419
423
  if (!isPlainRecord(choice)
420
- || !Number.isSafeInteger(choice.index)
424
+ || !is.safeInteger(choice.index)
421
425
  || choice.index < 0
422
426
  || indexes.has(choice.index)) {
423
427
  fail(
@@ -461,22 +465,22 @@ function canonicalLLMToolCall(call) {
461
465
  }
462
466
 
463
467
  function copyLLMDataValue(value, seen = new Map()) {
464
- if (!value || typeof value !== 'object') {
468
+ if (!value || !is.object(value)) {
465
469
  return value;
466
470
  }
467
471
  if (seen.has(value)) {
468
472
  return seen.get(value);
469
473
  }
470
- if (!Array.isArray(value) && !isPlainRecord(value)) {
474
+ if (!is.array(value) && !isPlainRecord(value)) {
471
475
  return value;
472
476
  }
473
- const result = Array.isArray(value)
477
+ const result = is.array(value)
474
478
  ? new Array(value.length)
475
479
  : Object.create(Object.getPrototypeOf(value));
476
480
  seen.set(value, result);
477
481
  const descriptors = Object.getOwnPropertyDescriptors(value);
478
482
  for (const key of Reflect.ownKeys(descriptors)) {
479
- if (Array.isArray(value) && key === 'length') {
483
+ if (is.array(value) && key === 'length') {
480
484
  continue;
481
485
  }
482
486
  const descriptor = descriptors[key];
@@ -493,12 +497,12 @@ function sameLLMDataValue(left, right, seen = new Map()) {
493
497
  return true;
494
498
  }
495
499
  if (!left || !right
496
- || typeof left !== 'object'
497
- || typeof right !== 'object'
498
- || Array.isArray(left) !== Array.isArray(right)) {
500
+ || !is.object(left)
501
+ || !is.object(right)
502
+ || is.array(left) !== is.array(right)) {
499
503
  return false;
500
504
  }
501
- if (!Array.isArray(left)
505
+ if (!is.array(left)
502
506
  && (!isPlainRecord(left) || !isPlainRecord(right))) {
503
507
  return false;
504
508
  }
@@ -561,11 +565,11 @@ function terminalLLMToolCallRecord(choiceIndex, completeCalls) {
561
565
  }
562
566
 
563
567
  function terminalLLMToolCalls(value) {
564
- if (typeof value === 'string') {
568
+ if (is.string(value)) {
565
569
  return {direct: terminalLLMToolCallRecord(null, []), choices: []};
566
570
  }
567
571
  if (Object.hasOwn(value, 'message')) {
568
- const completeCalls = Array.isArray(value.message?.tool_calls)
572
+ const completeCalls = is.array(value.message?.tool_calls)
569
573
  ? value.message.tool_calls
570
574
  : [];
571
575
  return {
@@ -576,7 +580,7 @@ function terminalLLMToolCalls(value) {
576
580
  return {
577
581
  direct: null,
578
582
  choices: value.choices.map(function retainTerminalLLMChoice(choice) {
579
- const completeCalls = Array.isArray(choice.message?.tool_calls)
583
+ const completeCalls = is.array(choice.message?.tool_calls)
580
584
  ? choice.message.tool_calls
581
585
  : [];
582
586
  return terminalLLMToolCallRecord(choice.index, completeCalls);
@@ -636,7 +640,7 @@ function createLLMStreamToolCallCorrelation() {
636
640
  }
637
641
  const descriptor = Object.getOwnPropertyDescriptor(source, 'tool_calls');
638
642
  if (!descriptor || !Object.hasOwn(descriptor, 'value')
639
- || !Array.isArray(descriptor.value)) {
643
+ || !is.array(descriptor.value)) {
640
644
  throw llmStreamToolCallMismatch(
641
645
  `${label}.tool_calls must be an array data property.`
642
646
  );
@@ -685,7 +689,7 @@ function createLLMStreamToolCallCorrelation() {
685
689
  );
686
690
  }
687
691
  if (fragment.index !== undefined
688
- && (!Number.isSafeInteger(fragment.index) || fragment.index < 0)) {
692
+ && (!is.safeInteger(fragment.index) || fragment.index < 0)) {
689
693
  throw llmStreamToolCallMismatch(
690
694
  `${label}.tool_calls[${position}] has an invalid structural call index.`
691
695
  );
@@ -700,7 +704,7 @@ function createLLMStreamToolCallCorrelation() {
700
704
  invalid: false
701
705
  };
702
706
  if (fragment.id !== undefined) {
703
- if (typeof fragment.id !== 'string'
707
+ if (!is.string(fragment.id)
704
708
  || !fragment.id
705
709
  || (retained.id && retained.id !== fragment.id)) {
706
710
  retained.invalid = true;
@@ -709,7 +713,7 @@ function createLLMStreamToolCallCorrelation() {
709
713
  }
710
714
  }
711
715
  if (fragment.type !== undefined) {
712
- if (typeof fragment.type !== 'string'
716
+ if (!is.string(fragment.type)
713
717
  || !fragment.type
714
718
  || (retained.type && retained.type !== fragment.type)) {
715
719
  retained.invalid = true;
@@ -725,14 +729,14 @@ function createLLMStreamToolCallCorrelation() {
725
729
  ? fragment.function
726
730
  : {};
727
731
  if (functionFragment.name !== undefined) {
728
- if (typeof functionFragment.name !== 'string') {
732
+ if (!is.string(functionFragment.name)) {
729
733
  retained.invalid = true;
730
734
  } else {
731
735
  retained.name += functionFragment.name;
732
736
  }
733
737
  }
734
738
  if (functionFragment.arguments !== undefined) {
735
- if (typeof functionFragment.arguments !== 'string') {
739
+ if (!is.string(functionFragment.arguments)) {
736
740
  retained.invalid = true;
737
741
  } else {
738
742
  retained.arguments += functionFragment.arguments;
@@ -761,7 +765,7 @@ function createLLMStreamToolCallCorrelation() {
761
765
  }
762
766
  );
763
767
  if (choice.index !== undefined
764
- && (!Number.isSafeInteger(choice.index) || choice.index < 0)) {
768
+ && (!is.safeInteger(choice.index) || choice.index < 0)) {
765
769
  throw llmStreamToolCallMismatch(
766
770
  `AI provider stream choice ${position} has an invalid index.`
767
771
  );
@@ -810,7 +814,7 @@ function createLLMStreamToolCallCorrelation() {
810
814
  'AI provider stream chunk.message'
811
815
  );
812
816
  }
813
- if (Array.isArray(chunk.choices)) {
817
+ if (is.array(chunk.choices)) {
814
818
  for (let position = 0; position < chunk.choices.length; position += 1) {
815
819
  observeChoice(chunk.choices[position], position);
816
820
  }
@@ -1008,13 +1012,13 @@ function projectLLMStreamData(value, seen = new Map()) {
1008
1012
  if (value === null || value === undefined) {
1009
1013
  return value;
1010
1014
  }
1011
- if (typeof value !== 'object') {
1015
+ if (!is.object(value)) {
1012
1016
  return value;
1013
1017
  }
1014
1018
  if (seen.has(value)) {
1015
1019
  return seen.get(value);
1016
1020
  }
1017
- if (Array.isArray(value)) {
1021
+ if (is.array(value)) {
1018
1022
  const result = [];
1019
1023
  seen.set(value, result);
1020
1024
  for (const item of value) {
@@ -1030,7 +1034,7 @@ function projectLLMStreamData(value, seen = new Map()) {
1030
1034
  let sourceDataFields = 0;
1031
1035
  const descriptors = Object.getOwnPropertyDescriptors(value);
1032
1036
  for (const key of Reflect.ownKeys(descriptors)) {
1033
- if (typeof key === 'symbol') {
1037
+ if (is.symbol(key)) {
1034
1038
  continue;
1035
1039
  }
1036
1040
  const descriptor = descriptors[key];
@@ -1054,19 +1058,19 @@ function projectLLMStreamChunk(value) {
1054
1058
  }
1055
1059
 
1056
1060
  function assertCallbackFreeProviderValue(value, seen = new WeakSet()) {
1057
- if (typeof value === 'function') {
1061
+ if (is.function(value)) {
1058
1062
  fail(
1059
1063
  'AI providers receive data-only request payloads.',
1060
1064
  'ARCANE_AI_PROVIDER_CALLBACK_BOUNDARY'
1061
1065
  );
1062
1066
  }
1063
- if (!value || typeof value !== 'object') {
1067
+ if (!value || !is.object(value)) {
1064
1068
  return;
1065
1069
  }
1066
- if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
1070
+ if (value instanceof ArrayBuffer || is.arrayBufferView(value)) {
1067
1071
  return;
1068
1072
  }
1069
- if (typeof Blob === 'function' && value instanceof Blob) {
1073
+ if (is.function(globalThis.Blob) && value instanceof Blob) {
1070
1074
  return;
1071
1075
  }
1072
1076
  if (seen.has(value)) {
@@ -1076,7 +1080,7 @@ function assertCallbackFreeProviderValue(value, seen = new WeakSet()) {
1076
1080
  const descriptors = Object.getOwnPropertyDescriptors(value);
1077
1081
  for (const key of Reflect.ownKeys(descriptors)) {
1078
1082
  const descriptor = descriptors[key];
1079
- if (typeof key === 'symbol' || !Object.hasOwn(descriptor, 'value')) {
1083
+ if (is.symbol(key) || !Object.hasOwn(descriptor, 'value')) {
1080
1084
  fail('AI provider request payloads must contain data properties only.');
1081
1085
  }
1082
1086
  assertCallbackFreeProviderValue(descriptor.value, seen);
@@ -1089,7 +1093,7 @@ function assertClosedRecord(value, keys, label) {
1089
1093
  const descriptors = Object.getOwnPropertyDescriptors(value);
1090
1094
  const actual = Reflect.ownKeys(value);
1091
1095
  if (actual.some(function hasSymbolKey(key) {
1092
- return typeof key === 'symbol';
1096
+ return is.symbol(key);
1093
1097
  })) {
1094
1098
  fail(`${label} must not contain symbol keys.`);
1095
1099
  }
@@ -1118,7 +1122,7 @@ function immutableSelection(role, value) {
1118
1122
  );
1119
1123
  assertIdentifier(value.providerId, `${role} provider selection.providerId`);
1120
1124
  assertIdentifier(value.modelId, `${role} provider selection.modelId`);
1121
- if (value.localOnly !== null && typeof value.localOnly !== 'boolean') {
1125
+ if (value.localOnly !== null && !is.boolean(value.localOnly)) {
1122
1126
  fail(`${role} provider selection.localOnly must be null or a boolean.`);
1123
1127
  }
1124
1128
 
@@ -1188,7 +1192,7 @@ function immutableStartupOptions(options) {
1188
1192
  assertPlainObject(options, 'AI provider startup options');
1189
1193
  const descriptors = Object.getOwnPropertyDescriptors(options);
1190
1194
  for (const key of Reflect.ownKeys(descriptors)) {
1191
- if (typeof key === 'symbol'
1195
+ if (is.symbol(key)
1192
1196
  || (key !== 'startLanguageModel'
1193
1197
  && key !== 'startMuted'
1194
1198
  && key !== 'startTranscription'
@@ -1214,13 +1218,13 @@ function immutableStartupOptions(options) {
1214
1218
  const signal = Object.hasOwn(descriptors, 'signal')
1215
1219
  ? descriptors.signal.value
1216
1220
  : null;
1217
- if (typeof startLanguageModel !== 'boolean') {
1221
+ if (!is.boolean(startLanguageModel)) {
1218
1222
  fail('AI startup startLanguageModel must be a boolean.');
1219
1223
  }
1220
- if (typeof startMuted !== 'boolean') {
1224
+ if (!is.boolean(startMuted)) {
1221
1225
  fail('AI startup startMuted must be a boolean.');
1222
1226
  }
1223
- if (typeof startTranscription !== 'boolean') {
1227
+ if (!is.boolean(startTranscription)) {
1224
1228
  fail('AI startup startTranscription must be a boolean.');
1225
1229
  }
1226
1230
  assertAbortSignal(signal);
@@ -1236,7 +1240,7 @@ function immutableInspectionOptions(options) {
1236
1240
  assertPlainObject(options, 'AI provider inspection options');
1237
1241
  const descriptors = Object.getOwnPropertyDescriptors(options);
1238
1242
  for (const key of Reflect.ownKeys(descriptors)) {
1239
- if (typeof key === 'symbol'
1243
+ if (is.symbol(key)
1240
1244
  || (key !== 'localOnly' && key !== 'signal')) {
1241
1245
  fail('AI provider inspection options contain an unknown option.');
1242
1246
  }
@@ -1250,7 +1254,7 @@ function immutableInspectionOptions(options) {
1250
1254
  const signal = Object.hasOwn(descriptors, 'signal')
1251
1255
  ? descriptors.signal.value
1252
1256
  : null;
1253
- if (typeof localOnly !== 'boolean') {
1257
+ if (!is.boolean(localOnly)) {
1254
1258
  fail('AI provider inspection localOnly must be a boolean.');
1255
1259
  }
1256
1260
  assertAbortSignal(signal);
@@ -1258,7 +1262,7 @@ function immutableInspectionOptions(options) {
1258
1262
  }
1259
1263
 
1260
1264
  function nullableTupleIdentifier(value) {
1261
- return typeof value === 'string' && value.trim()
1265
+ return is.string(value) && value.trim()
1262
1266
  ? value
1263
1267
  : null;
1264
1268
  }
@@ -1271,7 +1275,7 @@ function completeProgress(value) {
1271
1275
  const descriptors = Object.getOwnPropertyDescriptors(value);
1272
1276
  for (const key of Reflect.ownKeys(descriptors)) {
1273
1277
  const descriptor = descriptors[key];
1274
- if (typeof key === 'symbol' || !Object.hasOwn(descriptor, 'value')) {
1278
+ if (is.symbol(key) || !Object.hasOwn(descriptor, 'value')) {
1275
1279
  fail('AI provider progress must contain string-keyed data properties only.');
1276
1280
  }
1277
1281
  result[key] = descriptor.value;
@@ -1280,7 +1284,7 @@ function completeProgress(value) {
1280
1284
  }
1281
1285
 
1282
1286
  function stateError(error, fallbackCode) {
1283
- const code = typeof error?.code === 'string'
1287
+ const code = is.string(error?.code)
1284
1288
  && /^[A-Z][A-Z0-9_]*$/.test(error.code)
1285
1289
  ? error.code
1286
1290
  : fallbackCode;
@@ -1292,7 +1296,7 @@ function stateError(error, fallbackCode) {
1292
1296
  : code.includes('UNAVAILABLE') || code.includes('NOT_REGISTERED')
1293
1297
  ? 'The selected AI provider is unavailable.'
1294
1298
  : 'The selected AI provider operation failed.';
1295
- const message = typeof error?.message === 'string' && error.message
1299
+ const message = is.string(error?.message) && error.message
1296
1300
  ? error.message
1297
1301
  : fallbackMessage;
1298
1302
  return completeValue(
@@ -1334,13 +1338,13 @@ function validateProvider(provider) {
1334
1338
  }
1335
1339
  assertRole(provider.role);
1336
1340
  assertIdentifier(provider.id, 'AI provider.id');
1337
- if (typeof provider.localOnly !== 'boolean') {
1341
+ if (!is.boolean(provider.localOnly)) {
1338
1342
  fail('AI provider.localOnly must be a boolean.');
1339
1343
  }
1340
1344
  const maxConcurrentRequests = provider.maxConcurrentRequests === undefined
1341
1345
  ? 1
1342
1346
  : provider.maxConcurrentRequests;
1343
- if (!Number.isSafeInteger(maxConcurrentRequests)
1347
+ if (!is.safeInteger(maxConcurrentRequests)
1344
1348
  || maxConcurrentRequests < 1) {
1345
1349
  fail('AI provider.maxConcurrentRequests must be a positive safe integer.');
1346
1350
  }
@@ -1348,7 +1352,7 @@ function validateProvider(provider) {
1348
1352
  fail('Only TTS providers may process more than one concurrent request.');
1349
1353
  }
1350
1354
  for (const method of PROVIDER_METHODS) {
1351
- if (typeof provider[method] !== 'function') {
1355
+ if (!is.function(provider[method])) {
1352
1356
  fail(`AI provider.${method} must be a function.`);
1353
1357
  }
1354
1358
  }
@@ -1615,9 +1619,9 @@ function immutableSpeechProviderRoleReplacement(role, value) {
1615
1619
 
1616
1620
  function validateProviderStatus(status) {
1617
1621
  assertPlainObject(status, 'AI provider status');
1618
- if (typeof status.state !== 'string'
1619
- || typeof status.loaded !== 'boolean'
1620
- || typeof status.busy !== 'boolean') {
1622
+ if (!is.string(status.state)
1623
+ || !is.boolean(status.loaded)
1624
+ || !is.boolean(status.busy)) {
1621
1625
  fail('AI provider status must include state, loaded, and busy values.');
1622
1626
  }
1623
1627
  return status;
@@ -1625,14 +1629,14 @@ function validateProviderStatus(status) {
1625
1629
 
1626
1630
  function validateInspection(inspection, selection) {
1627
1631
  assertPlainObject(inspection, 'AI provider inspection');
1628
- if (typeof inspection.available !== 'boolean') {
1632
+ if (!is.boolean(inspection.available)) {
1629
1633
  fail('AI provider inspection.available must be a boolean.');
1630
1634
  }
1631
1635
  if (!inspection.available) {
1632
- const code = typeof inspection.code === 'string' && inspection.code.length > 0
1636
+ const code = is.string(inspection.code) && inspection.code.length > 0
1633
1637
  ? inspection.code
1634
1638
  : 'ARCANE_AI_PROVIDER_AUTHORITY_BLOCKED';
1635
- const message = typeof inspection.message === 'string' && inspection.message.length > 0
1639
+ const message = is.string(inspection.message) && inspection.message.length > 0
1636
1640
  ? inspection.message
1637
1641
  : 'The selected AI provider is unavailable.';
1638
1642
  throw operationError(message, code);
@@ -1922,7 +1926,7 @@ export class AIProviderRuntime {
1922
1926
  const localOnly = Object.hasOwn(options, 'localOnly')
1923
1927
  ? options.localOnly
1924
1928
  : false;
1925
- if (typeof localOnly !== 'boolean') {
1929
+ if (!is.boolean(localOnly)) {
1926
1930
  fail('AI provider route localOnly must be a boolean.');
1927
1931
  }
1928
1932
  const slot = this.#slots[role];
@@ -2437,7 +2441,7 @@ export class AIProviderRuntime {
2437
2441
 
2438
2442
  configureFromTuple(tuple) {
2439
2443
  arcaneLogging.debug('[Arcane speech runtime] configureFromTuple', tuple);
2440
- if (!Array.isArray(tuple) || tuple.length !== 6) {
2444
+ if (!is.array(tuple) || tuple.length !== 6) {
2441
2445
  fail('AI preference tuple must contain exactly six entries.');
2442
2446
  }
2443
2447
 
@@ -2543,7 +2547,7 @@ export class AIProviderRuntime {
2543
2547
  if (SPEECH_ROLES.includes(role)) {
2544
2548
  arcaneLogging.debug('[Arcane speech runtime] provider.catalog.result', role, provider.id, providerCatalog);
2545
2549
  }
2546
- if (!Array.isArray(providerCatalog)) {
2550
+ if (!is.array(providerCatalog)) {
2547
2551
  fail('AI provider.catalog() must synchronously return an array.');
2548
2552
  }
2549
2553
  entries.push(
@@ -2727,7 +2731,7 @@ export class AIProviderRuntime {
2727
2731
  ? options.localOnly
2728
2732
  : false;
2729
2733
  assertAbortSignal(signal);
2730
- if (typeof localOnly !== 'boolean') {
2734
+ if (!is.boolean(localOnly)) {
2731
2735
  fail('AI provider load localOnly must be a boolean.');
2732
2736
  }
2733
2737
  const slot = this.#slots[role];
@@ -3343,14 +3347,14 @@ export class AIProviderRuntime {
3343
3347
  return this.#disposeAllPromise;
3344
3348
  }
3345
3349
 
3346
- request(role, options = {}) {
3350
+ request(role, options = {}, {speechInputPrepared = false} = {}) {
3347
3351
  if (SPEECH_ROLES.includes(role)) {
3348
3352
  arcaneLogging.debug('[Arcane speech runtime] request', role, options);
3349
3353
  }
3350
- return this.#requestRole(role, options, false);
3354
+ return this.#requestRole(role, options, false, speechInputPrepared);
3351
3355
  }
3352
3356
 
3353
- #requestRole(role, options, queued) {
3357
+ #requestRole(role, options, queued, speechInputPrepared = false) {
3354
3358
  this.#assertOpen();
3355
3359
  this.#assertNotConfiguring();
3356
3360
  assertRole(role);
@@ -3379,6 +3383,24 @@ export class AIProviderRuntime {
3379
3383
  if (options.signal?.aborted) {
3380
3384
  return Promise.reject(normalizedAbort());
3381
3385
  }
3386
+ if (role === 'tts' && !queued && speechInputPrepared !== true) {
3387
+ const payload = options.payload;
3388
+ const field = Object.hasOwn(payload ?? {}, 'input') ? 'input' : 'text';
3389
+ const text = Object.getOwnPropertyDescriptor(payload ?? {}, field)?.value;
3390
+ if (is.string(text)) {
3391
+ const input = stripSpeechFormatting(text);
3392
+ if (input !== text) {
3393
+ const payloadProperties = Object.getOwnPropertyDescriptors(payload);
3394
+ payloadProperties[field] = {...payloadProperties[field], value: input};
3395
+ const requestProperties = Object.getOwnPropertyDescriptors(options);
3396
+ requestProperties.payload = {
3397
+ ...requestProperties.payload,
3398
+ value: Object.create(Object.getPrototypeOf(payload), payloadProperties)
3399
+ };
3400
+ options = Object.create(Object.getPrototypeOf(options), requestProperties);
3401
+ }
3402
+ }
3403
+ }
3382
3404
  const slot = this.#slots[role];
3383
3405
  if (slot.disposed) {
3384
3406
  return Promise.reject(
@@ -3640,14 +3662,14 @@ export class AIProviderRuntime {
3640
3662
 
3641
3663
  function beginAIProviderStreamHandleCleanup(opened, activeIterator, reason) {
3642
3664
  const cleanup = [];
3643
- if (typeof opened?.cancel === 'function') {
3665
+ if (is.function(opened?.cancel)) {
3644
3666
  cleanup.push(
3645
3667
  Promise.resolve().then(function cancelOpenedAIStream() {
3646
3668
  return opened.cancel(reason);
3647
3669
  })
3648
3670
  );
3649
3671
  }
3650
- if (activeIterator && typeof activeIterator.return === 'function') {
3672
+ if (activeIterator && is.function(activeIterator.return)) {
3651
3673
  cleanup.push(
3652
3674
  Promise.resolve().then(function returnOpenedAIStream() {
3653
3675
  return activeIterator.return();
@@ -3680,7 +3702,7 @@ export class AIProviderRuntime {
3680
3702
  }
3681
3703
  const lateCleanup = providerOpenPromise.then(
3682
3704
  async function cleanupLateAIProviderStream(lateHandle) {
3683
- if (!lateHandle || typeof lateHandle.cancel !== 'function') {
3705
+ if (!lateHandle || !is.function(lateHandle.cancel)) {
3684
3706
  throw operationError(
3685
3707
  'The late AI provider stream did not expose cancellable ownership.',
3686
3708
  'ARCANE_AI_STREAM_CLEANUP_INCOMPLETE'
@@ -3796,10 +3818,10 @@ export class AIProviderRuntime {
3796
3818
  });
3797
3819
  opened = await Promise.race([providerOpenPromise, abortedOpen]);
3798
3820
  if (!opened
3799
- || typeof opened[Symbol.asyncIterator] !== 'function'
3800
- || typeof opened.cancel !== 'function'
3821
+ || !is.function(opened[Symbol.asyncIterator])
3822
+ || !is.function(opened.cancel)
3801
3823
  || !opened.result
3802
- || typeof opened.result.then !== 'function') {
3824
+ || !is.function(opened.result.then)) {
3803
3825
  throw operationError(
3804
3826
  'AI stream providers must return an async iterable with result and cancel().',
3805
3827
  'ARCANE_AI_PROVIDER_STREAM_INVALID'
@@ -3807,7 +3829,7 @@ export class AIProviderRuntime {
3807
3829
  }
3808
3830
  providerHandle = opened;
3809
3831
  iterator = opened[Symbol.asyncIterator]();
3810
- if (!iterator || typeof iterator.next !== 'function') {
3832
+ if (!iterator || !is.function(iterator.next)) {
3811
3833
  throw operationError(
3812
3834
  'The AI provider stream iterator has no next() method.',
3813
3835
  'ARCANE_AI_PROVIDER_STREAM_INVALID'
@@ -4001,7 +4023,9 @@ export class AIProviderRuntime {
4001
4023
  if (SPEECH_ROLES.includes(role)) {
4002
4024
  arcaneLogging.debug('[Arcane speech runtime] provider.request', {generation, requestSequence, operationId}, providerRequest);
4003
4025
  }
4004
- const result = await provider.request(providerRequest);
4026
+ const result = role === 'tts'
4027
+ ? await provider.request(providerRequest, {speechInputPrepared: true})
4028
+ : await provider.request(providerRequest);
4005
4029
  if (SPEECH_ROLES.includes(role)) {
4006
4030
  arcaneLogging.debug('[Arcane speech runtime] provider.request.result', {role, generation, requestSequence, operationId}, result);
4007
4031
  }
@@ -4065,9 +4089,9 @@ export class AIProviderRuntime {
4065
4089
  return this.#roleRequestAlias('stt', 'transcribe', payload, options);
4066
4090
  }
4067
4091
 
4068
- synthesize(payload, options = {}) {
4092
+ synthesize(payload, options = {}, preparation = {}) {
4069
4093
  arcaneLogging.debug('[Arcane speech runtime] synthesize', payload, options);
4070
- return this.#roleRequestAlias('tts', 'synthesize', payload, options);
4094
+ return this.#roleRequestAlias('tts', 'synthesize', payload, options, preparation);
4071
4095
  }
4072
4096
 
4073
4097
  cancel(role) {
@@ -4100,7 +4124,7 @@ export class AIProviderRuntime {
4100
4124
  arcaneLogging.debug('[Arcane speech runtime] setSpeechMuted', muted);
4101
4125
  this.#assertOpen();
4102
4126
  this.#assertNotConfiguring();
4103
- if (typeof muted !== 'boolean') {
4127
+ if (!is.boolean(muted)) {
4104
4128
  fail('AI speech muted state must be a boolean.');
4105
4129
  }
4106
4130
  this.#speechDesiredMuted = muted;
@@ -4159,7 +4183,7 @@ export class AIProviderRuntime {
4159
4183
  ) ?? null;
4160
4184
  }
4161
4185
 
4162
- #roleRequestAlias(role, operation, payload, options) {
4186
+ #roleRequestAlias(role, operation, payload, options, preparation = {}) {
4163
4187
  assertPlainObject(options, `AI ${operation} options`);
4164
4188
  for (const key of Reflect.ownKeys(options)) {
4165
4189
  if (key !== 'localOnly' && key !== 'signal') {
@@ -4179,7 +4203,8 @@ export class AIProviderRuntime {
4179
4203
  payload,
4180
4204
  localOnly,
4181
4205
  signal
4182
- }
4206
+ },
4207
+ preparation
4183
4208
  );
4184
4209
  }
4185
4210