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
@@ -13,30 +13,33 @@ import {
13
13
  } from './AIProviderRuntime.js';
14
14
  import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
15
15
 
16
+ const completeValue=(value)=>value;
17
+
16
18
  let credentials='include';
17
19
  const LEGACY_TTS_RESPONSE_FORMAT='opus';
18
20
  credentials='omit';
19
21
 
20
22
  const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
21
23
  export const AI_READY_EVENT='ai-ready';
22
- export const AI_INITIALIZATION_ERROR_CODES=Object.freeze({
24
+ const AI_TTS_FAILURE_EVENT='ai-tts-failure';
25
+ export const AI_INITIALIZATION_ERROR_CODES=completeValue({
23
26
  userReadyRegistrationCollision:
24
27
  'ARCANE_AI_USER_READY_REGISTRATION_COLLISION'
25
28
  });
26
- export const AI_INITIALIZATION_REASONS=Object.freeze({
29
+ export const AI_INITIALIZATION_REASONS=completeValue({
27
30
  initialized:'ai-initialized',
28
31
  userReadyRegistrationCollision:'ai-user-ready-registration-collision'
29
32
  });
30
33
  export const AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL=
31
34
  'arcane-ai-browser-speech-configuration/1';
32
- export const AI_BROWSER_SPEECH_EVENT_TYPES=Object.freeze({
35
+ export const AI_BROWSER_SPEECH_EVENT_TYPES=completeValue({
33
36
  configurationCancelled:'ai-browser-speech-configuration-cancelled',
34
37
  configurationError:'ai-browser-speech-configuration-error',
35
38
  configurationStarted:'ai-browser-speech-configuration-started',
36
39
  configured:'ai-browser-speech-configured',
37
40
  disposed:'ai-browser-speech-disposed'
38
41
  });
39
- export const AI_BROWSER_SPEECH_ERROR_CODES=Object.freeze({
42
+ export const AI_BROWSER_SPEECH_ERROR_CODES=completeValue({
40
43
  artifactStoreConstructionRejected:
41
44
  'ARCANE_AI_BROWSER_SPEECH_ARTIFACT_STORE_CONSTRUCTION_REJECTED',
42
45
  asyncTransitionRequired:
@@ -64,7 +67,7 @@ export const AI_BROWSER_SPEECH_ERROR_CODES=Object.freeze({
64
67
  routeViewUpdateRejected:
65
68
  'ARCANE_AI_BROWSER_SPEECH_ROUTE_VIEW_UPDATE_REJECTED'
66
69
  });
67
- export const AI_BROWSER_SPEECH_REASONS=Object.freeze({
70
+ export const AI_BROWSER_SPEECH_REASONS=completeValue({
68
71
  artifactStoreConstructionRejected:'speech-artifact-store-construction-rejected',
69
72
  asyncTransitionRequired:'speech-configuration-async-transition-required',
70
73
  configurationAdded:'speech-configuration-added',
@@ -123,6 +126,282 @@ function legacyAIProviderError(message,code,cause){
123
126
  return error;
124
127
  }
125
128
 
129
+ function aiStructuralError(code,message,cause){
130
+ const error=cause===undefined
131
+ ?new TypeError(message)
132
+ :new TypeError(message,{cause});
133
+ error.code=code;
134
+ return error;
135
+ }
136
+
137
+ function isPlainAIRecord(value){
138
+ if(!value||typeof value!=='object'||Array.isArray(value)){
139
+ return false;
140
+ }
141
+ const prototype=Object.getPrototypeOf(value);
142
+ return prototype===Object.prototype||prototype===null;
143
+ }
144
+
145
+ function requireAIToolMessageSchemas(value,label='AI tools'){
146
+ if(!Array.isArray(value)){
147
+ throw aiStructuralError(
148
+ 'AI_CHAT_INVALID_TOOL_CALL',
149
+ `${label} must be an array.`
150
+ );
151
+ }
152
+ for(let index=0;index<value.length;index+=1){
153
+ const tool=value[index];
154
+ const parameters=tool?.function?.parameters;
155
+ const messageSchema=parameters?.properties?.message;
156
+ if(
157
+ !isPlainAIRecord(tool)
158
+ ||tool.type!=='function'
159
+ ||!isPlainAIRecord(tool.function)
160
+ ||!isPlainAIRecord(parameters)
161
+ ||parameters.type!=='object'
162
+ ||!isPlainAIRecord(parameters.properties)
163
+ ||!isPlainAIRecord(messageSchema)
164
+ ||messageSchema.type!=='string'
165
+ ||!Number.isInteger(messageSchema.minLength)
166
+ ||messageSchema.minLength<1
167
+ ||!Array.isArray(parameters.required)
168
+ ||!parameters.required.includes('message')
169
+ ){
170
+ throw aiStructuralError(
171
+ 'AI_CHAT_TOOL_MESSAGE_REQUIRED',
172
+ `${label}[${index}] must require a nonempty string parameters.properties.message.`
173
+ );
174
+ }
175
+ }
176
+ }
177
+
178
+ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
179
+ if(
180
+ !isPlainAIRecord(call)
181
+ ||typeof call.id!=='string'
182
+ ||!call.id.trim()
183
+ ||call.type!=='function'
184
+ ||!isPlainAIRecord(call.function)
185
+ ||typeof call.function.name!=='string'
186
+ ||!call.function.name.trim()
187
+ ||typeof call.function.arguments!=='string'
188
+ ){
189
+ throw aiStructuralError(
190
+ 'AI_CHAT_INVALID_TOOL_CALL',
191
+ `${label} is not a complete structural function call.`
192
+ );
193
+ }
194
+ let argumentsRecord;
195
+ try{
196
+ argumentsRecord=JSON.parse(call.function.arguments);
197
+ }catch(cause){
198
+ throw aiStructuralError(
199
+ 'AI_CHAT_INVALID_TOOL_CALL',
200
+ `${label} arguments must encode a JSON object.`,
201
+ cause
202
+ );
203
+ }
204
+ if(!isPlainAIRecord(argumentsRecord)){
205
+ throw aiStructuralError(
206
+ 'AI_CHAT_INVALID_TOOL_CALL',
207
+ `${label} arguments must encode a JSON object.`
208
+ );
209
+ }
210
+ if(typeof argumentsRecord.message!=='string'||!argumentsRecord.message.trim()){
211
+ throw aiStructuralError(
212
+ 'AI_CHAT_TOOL_MESSAGE_REQUIRED',
213
+ `${label} arguments must include a nonempty user-facing message.`
214
+ );
215
+ }
216
+ return {
217
+ id:call.id,
218
+ type:'function',
219
+ function:{
220
+ name:call.function.name,
221
+ arguments:call.function.arguments
222
+ }
223
+ };
224
+ }
225
+
226
+ function validateAIRequestMessages(messages=[]){
227
+ if(!Array.isArray(messages)){
228
+ throw new TypeError('AI messages must be an array.');
229
+ }
230
+ let pendingToolCallId=null;
231
+ for(let messageIndex=0;messageIndex<messages.length;messageIndex+=1){
232
+ const message=messages[messageIndex];
233
+ const calls=message?.tool_calls;
234
+ let openedToolCall=false;
235
+ if(calls!==undefined){
236
+ if(message?.role!=='assistant'||!Array.isArray(calls)){
237
+ throw aiStructuralError(
238
+ 'AI_CHAT_INVALID_TOOL_CALL',
239
+ `AI messages[${messageIndex}].tool_calls is invalid.`
240
+ );
241
+ }
242
+ if(calls.length>1||(pendingToolCallId!==null&&calls.length)){
243
+ throw aiStructuralError(
244
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
245
+ 'The Arcane chat session accepts one structural tool call at a time.'
246
+ );
247
+ }
248
+ if(calls.length){
249
+ pendingToolCallId=normalizeAIStructuralToolCall(
250
+ calls[0],
251
+ `AI messages[${messageIndex}].tool_calls[0]`
252
+ ).id;
253
+ openedToolCall=true;
254
+ }
255
+ }
256
+ if(message?.role==='tool'){
257
+ if(typeof message.content!=='string'||!message.content.trim()){
258
+ throw aiStructuralError(
259
+ 'AI_CHAT_INVALID_TOOL_MESSAGE',
260
+ `AI messages[${messageIndex}] must contain a nonblank user-facing tool result.`
261
+ );
262
+ }
263
+ if(
264
+ pendingToolCallId===null
265
+ ||typeof message.tool_call_id!=='string'
266
+ ||message.tool_call_id!==pendingToolCallId
267
+ ){
268
+ throw aiStructuralError(
269
+ 'AI_CHAT_INVALID_TOOL_MESSAGE',
270
+ `AI messages[${messageIndex}] does not settle the pending structural tool call.`
271
+ );
272
+ }
273
+ pendingToolCallId=null;
274
+ }else if(pendingToolCallId!==null&&!openedToolCall){
275
+ throw aiStructuralError(
276
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
277
+ `AI messages[${messageIndex}] precedes the pending structural tool result.`
278
+ );
279
+ }
280
+ }
281
+ if(pendingToolCallId!==null){
282
+ throw aiStructuralError(
283
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
284
+ 'The pending structural tool call must be settled before requesting another response.'
285
+ );
286
+ }
287
+ }
288
+
289
+ function validateAIStructuralRequest(messages,tools,parallelToolCalls){
290
+ validateAIRequestMessages(messages);
291
+ requireAIToolMessageSchemas(tools);
292
+ if(parallelToolCalls===true){
293
+ throw aiStructuralError(
294
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
295
+ 'The Arcane chat session accepts one structural tool call at a time.'
296
+ );
297
+ }
298
+ }
299
+
300
+ function normalizeAICompletionToolCalls(completion){
301
+ const calls=[];
302
+ const hasMessage=Boolean(
303
+ completion
304
+ &&typeof completion==='object'
305
+ &&Object.hasOwn(completion,'message')
306
+ );
307
+ const hasChoices=Boolean(
308
+ completion
309
+ &&typeof completion==='object'
310
+ &&Object.hasOwn(completion,'choices')
311
+ );
312
+ if(hasMessage&&hasChoices){
313
+ throw aiStructuralError(
314
+ 'AI_CHAT_INVALID_RESPONSE',
315
+ 'The AI completion must not mix message and choices envelopes.'
316
+ );
317
+ }
318
+ if(hasChoices&&!Array.isArray(completion.choices)){
319
+ throw aiStructuralError(
320
+ 'AI_CHAT_INVALID_RESPONSE',
321
+ 'The AI completion choices envelope must be an array.'
322
+ );
323
+ }
324
+ const messages=hasChoices
325
+ ?completion.choices.map(choice=>choice?.message)
326
+ :hasMessage
327
+ ?[completion.message]
328
+ :[];
329
+ for(let messageIndex=0;messageIndex<messages.length;messageIndex+=1){
330
+ const toolCalls=messages[messageIndex]?.tool_calls;
331
+ if(toolCalls!==undefined&&!Array.isArray(toolCalls)){
332
+ throw aiStructuralError(
333
+ 'AI_CHAT_INVALID_TOOL_CALL',
334
+ `AI response message ${messageIndex+1} contains invalid structural tool calls.`
335
+ );
336
+ }
337
+ if(calls.length+(toolCalls?.length??0)>1){
338
+ throw aiStructuralError(
339
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
340
+ 'The Arcane chat session accepts one structural tool call at a time.'
341
+ );
342
+ }
343
+ if(messageIndex>0&&toolCalls?.length){
344
+ throw aiStructuralError(
345
+ 'AI_CHAT_INVALID_RESPONSE',
346
+ 'The AI completion placed a structural tool call outside the selected choice.'
347
+ );
348
+ }
349
+ for(let callIndex=0;callIndex<(toolCalls??[]).length;callIndex+=1){
350
+ calls.push(normalizeAIStructuralToolCall(
351
+ toolCalls[callIndex],
352
+ `AI response structural tool call ${messageIndex+1}.${callIndex+1}`
353
+ ));
354
+ }
355
+ }
356
+ return calls;
357
+ }
358
+
359
+ function sameAIStructuralToolCall(left,right){
360
+ return left?.id===right?.id
361
+ &&left?.type===right?.type
362
+ &&left?.function?.name===right?.function?.name
363
+ &&left?.function?.arguments===right?.function?.arguments;
364
+ }
365
+
366
+ function assertAIStreamToolCallCorrelation(
367
+ streamedCalls,
368
+ terminalCalls,
369
+ label='AI stream'
370
+ ){
371
+ if(!streamedCalls.length){
372
+ return;
373
+ }
374
+ if(
375
+ streamedCalls.length!==terminalCalls.length
376
+ ||streamedCalls.some(
377
+ (call,index)=>!sameAIStructuralToolCall(call,terminalCalls[index])
378
+ )
379
+ ){
380
+ throw aiStructuralError(
381
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
382
+ `${label} changed or omitted its terminal structural tool call.`
383
+ );
384
+ }
385
+ }
386
+
387
+ function normalizeAIStreamToolCallObservation(completion,label){
388
+ try{
389
+ return normalizeAICompletionToolCalls(completion);
390
+ }catch(cause){
391
+ if(
392
+ cause?.code==='AI_CHAT_INVALID_TOOL_CALL'
393
+ ||cause?.code==='AI_CHAT_TOOL_MESSAGE_REQUIRED'
394
+ ){
395
+ throw aiStructuralError(
396
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
397
+ `${label} did not retain a complete structural tool call.`,
398
+ cause
399
+ );
400
+ }
401
+ throw cause;
402
+ }
403
+ }
404
+
126
405
  function createLegacyAIStreamBridge(execute,sourceSignal){
127
406
  const controller=new AbortController();
128
407
  const queue=[];
@@ -254,12 +533,13 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
254
533
  return this;
255
534
  }
256
535
  };
257
- return Object.freeze(handle);
536
+ return completeValue(handle);
258
537
  }
259
538
 
260
539
  function normalizeAIStartupOptions(options){
261
540
  if(options===undefined){
262
- return Object.freeze({
541
+ return completeValue({
542
+ startLanguageModel:true,
263
543
  startMuted:true,
264
544
  startTranscription:false,
265
545
  signal:null
@@ -275,7 +555,8 @@ function normalizeAIStartupOptions(options){
275
555
  const descriptors=Object.getOwnPropertyDescriptors(options);
276
556
  for(const key of Reflect.ownKeys(descriptors)){
277
557
  if(typeof key==='symbol'||(
278
- key!=='startMuted'
558
+ key!=='startLanguageModel'
559
+ &&key!=='startMuted'
279
560
  &&key!=='startTranscription'
280
561
  &&key!=='signal'
281
562
  )){
@@ -285,6 +566,9 @@ function normalizeAIStartupOptions(options){
285
566
  throw new TypeError(`AI startup options.${key} must be a data property.`);
286
567
  }
287
568
  }
569
+ const startLanguageModel=Object.hasOwn(descriptors,'startLanguageModel')
570
+ ?descriptors.startLanguageModel.value
571
+ :true;
288
572
  const startMuted=Object.hasOwn(descriptors,'startMuted')
289
573
  ?descriptors.startMuted.value
290
574
  :true;
@@ -294,6 +578,9 @@ function normalizeAIStartupOptions(options){
294
578
  const signal=Object.hasOwn(descriptors,'signal')
295
579
  ?descriptors.signal.value
296
580
  :null;
581
+ if(typeof startLanguageModel!=='boolean'){
582
+ throw new TypeError('AI startup startLanguageModel must be a boolean.');
583
+ }
297
584
  if(typeof startMuted!=='boolean'){
298
585
  throw new TypeError('AI startup startMuted must be a boolean.');
299
586
  }
@@ -308,7 +595,12 @@ function normalizeAIStartupOptions(options){
308
595
  )){
309
596
  throw new TypeError('AI startup signal must be an AbortSignal.');
310
597
  }
311
- return Object.freeze({startMuted,startTranscription,signal});
598
+ return completeValue({
599
+ startLanguageModel,
600
+ startMuted,
601
+ startTranscription,
602
+ signal
603
+ });
312
604
  }
313
605
 
314
606
  function aiBrowserSpeechError(code,reason,message,cause,{committed=false,name='Error'}={}){
@@ -330,16 +622,15 @@ function isAbortSignal(value){
330
622
  &&typeof value.removeEventListener==='function';
331
623
  }
332
624
 
333
- function frozenClosedRecord(value,keys,required,label){
625
+ function closedRecord(value,keys,required,label){
334
626
  if(!value
335
627
  ||typeof value!=='object'
336
628
  ||Array.isArray(value)
337
- ||![Object.prototype,null].includes(Object.getPrototypeOf(value))
338
- ||!Object.isFrozen(value)){
629
+ ||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
339
630
  throw aiBrowserSpeechError(
340
631
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
341
632
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
342
- `${label} must be a frozen plain data record.`
633
+ `${label} must be a plain data record.`
343
634
  );
344
635
  }
345
636
  const descriptors=Object.getOwnPropertyDescriptors(value);
@@ -369,12 +660,11 @@ function frozenClosedRecord(value,keys,required,label){
369
660
  function browserSpeechIdentifier(value,label){
370
661
  if(typeof value!=='string'
371
662
  ||value.trim()!==value
372
- ||value.length<1
373
- ||value.length>128){
663
+ ||value.length<1){
374
664
  throw aiBrowserSpeechError(
375
665
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
376
666
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
377
- `${label} must be a trimmed 1-128 character string.`
667
+ `${label} must be a nonempty trimmed string.`
378
668
  );
379
669
  }
380
670
  return value;
@@ -382,7 +672,7 @@ function browserSpeechIdentifier(value,label){
382
672
 
383
673
  function normalizeBrowserSpeechRole(value,role){
384
674
  const label=`AI browser speech ${role}`;
385
- const descriptors=frozenClosedRecord(
675
+ const descriptors=closedRecord(
386
676
  value,
387
677
  ['providerId','graph','model','runtime','security','offline'],
388
678
  ['providerId','offline'],
@@ -396,6 +686,7 @@ function normalizeBrowserSpeechRole(value,role){
396
686
  const hasModel=Object.hasOwn(descriptors,'model');
397
687
  const hasRuntime=Object.hasOwn(descriptors,'runtime');
398
688
  const hasSecurity=Object.hasOwn(descriptors,'security');
689
+ let secure=false;
399
690
  if(hasGraph&&(hasModel||hasRuntime)){
400
691
  throw aiBrowserSpeechError(
401
692
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
@@ -412,31 +703,11 @@ function normalizeBrowserSpeechRole(value,role){
412
703
  }
413
704
  if(hasGraph){
414
705
  const graph=descriptors.graph.value;
415
- if(!graph||typeof graph!=='object'||!Object.isFrozen(graph)){
706
+ if(!graph||typeof graph!=='object'){
416
707
  throw aiBrowserSpeechError(
417
708
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
418
709
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
419
- `${label}.graph must be an SDK-created frozen artifact graph.`
420
- );
421
- }
422
- if(!hasSecurity){
423
- throw aiBrowserSpeechError(
424
- AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
425
- AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
426
- `${label}.security with secure:true is required for an artifact graph.`
427
- );
428
- }
429
- const security=frozenClosedRecord(
430
- descriptors.security.value,
431
- ['secure','checks'],
432
- ['secure'],
433
- `${label}.security`
434
- );
435
- if(security.secure.value!==true){
436
- throw aiBrowserSpeechError(
437
- AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
438
- AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
439
- `${label}.security.secure must be true for an artifact graph.`
710
+ `${label}.graph must be an SDK-created artifact graph.`
440
711
  );
441
712
  }
442
713
  }else{
@@ -451,6 +722,12 @@ function normalizeBrowserSpeechRole(value,role){
451
722
  }
452
723
  }
453
724
  }
725
+ if(hasSecurity){
726
+ // Security is an intent-only seam. Stale or future fields do not run
727
+ // checks in ordinary mode, and secure mode requires user review before
728
+ // an implementation may be enabled.
729
+ secure=descriptors.security.value?.secure===true;
730
+ }
454
731
  if(typeof descriptors.offline.value!=='boolean'){
455
732
  throw aiBrowserSpeechError(
456
733
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
@@ -458,24 +735,24 @@ function normalizeBrowserSpeechRole(value,role){
458
735
  `${label}.offline must be a boolean.`
459
736
  );
460
737
  }
461
- return Object.freeze({
738
+ return completeValue({
462
739
  providerId,
463
740
  ...(hasGraph
464
741
  ?{
465
742
  graph:descriptors.graph.value,
466
- security:descriptors.security.value
743
+ ...(secure?{security:{secure:true}}:{})
467
744
  }
468
745
  :{
469
746
  model:descriptors.model.value,
470
747
  runtime:descriptors.runtime.value,
471
- ...(hasSecurity?{security:descriptors.security.value}:{})
748
+ ...(secure?{security:{secure:true}}:{})
472
749
  }),
473
750
  offline:descriptors.offline.value
474
751
  });
475
752
  }
476
753
 
477
754
  function normalizeBrowserSpeechConfiguration(value){
478
- const descriptors=frozenClosedRecord(
755
+ const descriptors=closedRecord(
479
756
  value,
480
757
  ['protocol','id','dbopfs','tableName','stt','tts'],
481
758
  ['protocol','id','dbopfs'],
@@ -514,12 +791,12 @@ function normalizeBrowserSpeechConfiguration(value){
514
791
  'AI browser speech configuration must provide stt, tts, or both.'
515
792
  );
516
793
  }
517
- return Object.freeze({
794
+ return completeValue({
518
795
  configuration:value,
519
796
  id,
520
797
  dbopfs,
521
798
  ...(tableName?{tableName}:{}),
522
- roles:Object.freeze(roles),
799
+ roles:completeValue(roles),
523
800
  ...Object.fromEntries(roles.map(role=>[
524
801
  role,
525
802
  normalizeBrowserSpeechRole(descriptors[role].value,role)
@@ -528,7 +805,7 @@ function normalizeBrowserSpeechConfiguration(value){
528
805
  }
529
806
 
530
807
  function normalizeBrowserSpeechOperationOptions(value,label){
531
- if(value===undefined)return Object.freeze({signal:null});
808
+ if(value===undefined)return completeValue({signal:null});
532
809
  if(!value
533
810
  ||typeof value!=='object'
534
811
  ||Array.isArray(value)
@@ -557,7 +834,7 @@ function normalizeBrowserSpeechOperationOptions(value,label){
557
834
  `${label} signal must be an AbortSignal.`
558
835
  );
559
836
  }
560
- return Object.freeze({signal});
837
+ return completeValue({signal});
561
838
  }
562
839
 
563
840
  class AI {
@@ -676,9 +953,10 @@ class AI {
676
953
  this,
677
954
  {
678
955
  source:'ai',
679
- eventTypes:Object.freeze(
956
+ eventTypes:completeValue(
680
957
  [
681
958
  AI_READY_EVENT,
959
+ AI_TTS_FAILURE_EVENT,
682
960
  ...Object.values(AI_BROWSER_SPEECH_EVENT_TYPES)
683
961
  ]
684
962
  )
@@ -712,7 +990,7 @@ class AI {
712
990
  #events=null;
713
991
  #browserSpeechConfigurationRecord=null;
714
992
  #browserSpeechController=null;
715
- #browserSpeechControllerRoles=Object.freeze([]);
993
+ #browserSpeechControllerRoles=completeValue([]);
716
994
  #browserSpeechGeneration=0;
717
995
  #browserSpeechModulePromise=null;
718
996
  #browserSpeechOperationSequence=0;
@@ -724,8 +1002,9 @@ class AI {
724
1002
  #legacySpeechProviders=new Map();
725
1003
  #legacySpeechReadiness=Promise.resolve(null);
726
1004
  #speechControlGeneration=0;
1005
+ #speechFailureSequence=0;
727
1006
  #stopOllamaReady=null;
728
- #preferenceTuple=Object.freeze([
1007
+ #preferenceTuple=completeValue([
729
1008
  'OPENAI',
730
1009
  'OPENAI',
731
1010
  'OPENAI',
@@ -743,10 +1022,10 @@ class AI {
743
1022
  const reason=AI_INITIALIZATION_REASONS.initialized;
744
1023
  const {occurrence}=this.#events.dispatch(
745
1024
  AI_READY_EVENT,
746
- Object.freeze({db:this,operationId,reason}),
1025
+ completeValue({db:this,operationId,reason}),
747
1026
  {
748
1027
  operationId,
749
- publicDetail:Object.freeze({
1028
+ publicDetail:completeValue({
750
1029
  ready:true,
751
1030
  reason
752
1031
  })
@@ -837,14 +1116,14 @@ class AI {
837
1116
  ||selection.modelId!==this.model
838
1117
  ||selection.localOnly!==localOnly
839
1118
  ||this.llmService!==providerId){
840
- return Object.freeze({
1119
+ return completeValue({
841
1120
  available:false,
842
1121
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
843
1122
  message:'The selected legacy LLM route does not match the active AI configuration.'
844
1123
  });
845
1124
  }
846
1125
  if(!this.#legacyLLMCapability(providerId)){
847
- return Object.freeze({
1126
+ return completeValue({
848
1127
  available:false,
849
1128
  code:providerId==='OLLAMA'
850
1129
  ?'AI_NATIVE_LOCAL_REQUIRED'
@@ -854,13 +1133,12 @@ class AI {
854
1133
  :'AI provider is not configured.'
855
1134
  });
856
1135
  }
857
- return Object.freeze({
1136
+ return completeValue({
858
1137
  available:true,
859
- authority:Object.freeze({
1138
+ authority:completeValue({
860
1139
  protocol:AI_MODEL_AUTHORITY_PROTOCOL,
861
1140
  providerId,
862
1141
  modelId:selection.modelId,
863
- admitted:true
864
1142
  })
865
1143
  });
866
1144
  }
@@ -877,7 +1155,7 @@ class AI {
877
1155
  &&!runtime.#legacyLLMCapability(providerId)){
878
1156
  state='unloaded';
879
1157
  }
880
- return Object.freeze({
1158
+ return completeValue({
881
1159
  state,
882
1160
  loaded:state==='ready',
883
1161
  busy
@@ -902,17 +1180,17 @@ class AI {
902
1180
  busy=false;
903
1181
  }
904
1182
 
905
- return Object.freeze({
1183
+ return completeValue({
906
1184
  protocol:AI_PROVIDER_PROTOCOL,
907
1185
  role:'llm',
908
1186
  id:providerId,
909
1187
  localOnly,
910
1188
  catalog:function catalogLegacyLLMProvider(){
911
1189
  if(runtime.llmService!==providerId||!runtime.model){
912
- return Object.freeze([]);
1190
+ return completeValue([]);
913
1191
  }
914
- return Object.freeze([
915
- Object.freeze({id:runtime.model})
1192
+ return completeValue([
1193
+ completeValue({id:runtime.model})
916
1194
  ]);
917
1195
  },
918
1196
  inspect:function inspectLegacyLLMProvider(selection,{signal}={}){
@@ -964,7 +1242,7 @@ class AI {
964
1242
  unit:'items',
965
1243
  heartbeat:false
966
1244
  });
967
- return Object.freeze({
1245
+ return completeValue({
968
1246
  authority:inspection.authority,
969
1247
  status:statusLegacyLLMProvider()
970
1248
  });
@@ -1087,14 +1365,14 @@ class AI {
1087
1365
  ||selection.modelId!==this.#legacySpeechModel(role)
1088
1366
  ||selection.localOnly!==localOnly
1089
1367
  ||this.#legacySpeechService(role)!==providerId){
1090
- return Object.freeze({
1368
+ return completeValue({
1091
1369
  available:false,
1092
1370
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
1093
1371
  message:`The selected legacy ${role.toUpperCase()} route does not match the active AI configuration.`
1094
1372
  });
1095
1373
  }
1096
1374
  if(!this.#legacySpeechCapability(role,providerId)){
1097
- return Object.freeze({
1375
+ return completeValue({
1098
1376
  available:false,
1099
1377
  code:providerId==='LOCAL_SPEACH'
1100
1378
  ?'AI_NATIVE_LOCAL_REQUIRED'
@@ -1104,13 +1382,12 @@ class AI {
1104
1382
  :'AI provider is not configured.'
1105
1383
  });
1106
1384
  }
1107
- return Object.freeze({
1385
+ return completeValue({
1108
1386
  available:true,
1109
- authority:Object.freeze({
1387
+ authority:completeValue({
1110
1388
  protocol:AI_MODEL_AUTHORITY_PROTOCOL,
1111
1389
  providerId,
1112
1390
  modelId:selection.modelId,
1113
- admitted:true
1114
1391
  })
1115
1392
  });
1116
1393
  }
@@ -1128,7 +1405,7 @@ class AI {
1128
1405
  &&!runtime.#legacySpeechCapability(role,providerId)){
1129
1406
  state='unloaded';
1130
1407
  }
1131
- return Object.freeze({
1408
+ return completeValue({
1132
1409
  state,
1133
1410
  loaded:state==='ready',
1134
1411
  busy
@@ -1154,7 +1431,7 @@ class AI {
1154
1431
  busy=false;
1155
1432
  }
1156
1433
 
1157
- return Object.freeze({
1434
+ return completeValue({
1158
1435
  protocol:AI_PROVIDER_PROTOCOL,
1159
1436
  role,
1160
1437
  id:providerId,
@@ -1162,14 +1439,14 @@ class AI {
1162
1439
  catalog:function catalogLegacySpeechProvider(){
1163
1440
  const model=runtime.#legacySpeechModel(role);
1164
1441
  if(runtime.#legacySpeechService(role)!==providerId||!model){
1165
- return Object.freeze([]);
1442
+ return completeValue([]);
1166
1443
  }
1167
1444
  const defaultVoice=runtime.#legacySpeechDefaultVoice(
1168
1445
  role,
1169
1446
  providerId
1170
1447
  );
1171
- return Object.freeze([
1172
- Object.freeze({
1448
+ return completeValue([
1449
+ completeValue({
1173
1450
  id:model,
1174
1451
  ...(defaultVoice?{defaultVoice}:{})
1175
1452
  })
@@ -1224,7 +1501,7 @@ class AI {
1224
1501
  unit:'items',
1225
1502
  heartbeat:false
1226
1503
  });
1227
- return Object.freeze({
1504
+ return completeValue({
1228
1505
  authority:inspection.authority,
1229
1506
  status:statusLegacySpeechProvider()
1230
1507
  });
@@ -1305,7 +1582,7 @@ class AI {
1305
1582
  const unregister=this.#providerRuntime.register(provider);
1306
1583
  this.#legacyLLMProviders.set(
1307
1584
  providerId,
1308
- Object.freeze({provider,unregister})
1585
+ completeValue({provider,unregister})
1309
1586
  );
1310
1587
  return true;
1311
1588
  }
@@ -1322,7 +1599,7 @@ class AI {
1322
1599
  const unregister=this.#providerRuntime.register(provider);
1323
1600
  this.#legacySpeechProviders.set(
1324
1601
  this.#legacySpeechProviderKey(role,providerId),
1325
- Object.freeze({role,providerId,provider,unregister})
1602
+ completeValue({role,providerId,provider,unregister})
1326
1603
  );
1327
1604
  return true;
1328
1605
  }
@@ -1569,7 +1846,7 @@ class AI {
1569
1846
  }
1570
1847
  return value;
1571
1848
  });
1572
- return Object.freeze(next);
1849
+ return completeValue(next);
1573
1850
  }
1574
1851
 
1575
1852
  #assertValidProviderTuple(tuple){
@@ -1616,7 +1893,7 @@ class AI {
1616
1893
  this.modelTTS=this.#ttsModels[modelTTS]||modelTTS;
1617
1894
  this.modelSTT=this.#sttModels[modelSTT]||modelSTT;
1618
1895
  this.reasoningEffort='';
1619
- this.#preferenceTuple=Object.freeze(tuple.slice());
1896
+ this.#preferenceTuple=completeValue(tuple.slice());
1620
1897
  }
1621
1898
 
1622
1899
  #applySpeechPreferenceTuple(tuple){
@@ -1624,14 +1901,14 @@ class AI {
1624
1901
  this.ttsService=tuple[2];
1625
1902
  this.modelTTS=this.#ttsModels[tuple[4]]||tuple[4];
1626
1903
  this.modelSTT=this.#sttModels[tuple[5]]||tuple[5];
1627
- this.#preferenceTuple=Object.freeze(tuple.slice());
1904
+ this.#preferenceTuple=completeValue(tuple.slice());
1628
1905
  }
1629
1906
 
1630
1907
  #tupleFromProviderRoutes(selections){
1631
1908
  const llm=selections.llm.default;
1632
1909
  const stt=selections.stt.default;
1633
1910
  const tts=selections.tts.default;
1634
- return Object.freeze([
1911
+ return completeValue([
1635
1912
  llm?.providerId||'',
1636
1913
  stt?.providerId||'',
1637
1914
  tts?.providerId||'',
@@ -1645,7 +1922,7 @@ class AI {
1645
1922
  const current=this.#preferenceTuple;
1646
1923
  const stt=selections.stt.default;
1647
1924
  const tts=selections.tts.default;
1648
- return Object.freeze([
1925
+ return completeValue([
1649
1926
  current[0],
1650
1927
  stt?.providerId||'',
1651
1928
  tts?.providerId||'',
@@ -1966,14 +2243,12 @@ class AI {
1966
2243
  }
1967
2244
 
1968
2245
  #browserSpeechOperationId(action){
1969
- if(this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER){
1970
- throw aiBrowserSpeechError(
1971
- AI_BROWSER_SPEECH_ERROR_CODES.operationSequenceExhausted,
1972
- AI_BROWSER_SPEECH_REASONS.operationSequenceExhausted,
1973
- 'The browser speech operation sequence is exhausted.'
1974
- );
1975
- }
1976
- this.#browserSpeechOperationSequence+=1;
2246
+ this.#browserSpeechOperationSequence=
2247
+ typeof this.#browserSpeechOperationSequence==='bigint'
2248
+ ? this.#browserSpeechOperationSequence+1n
2249
+ : this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER
2250
+ ? BigInt(this.#browserSpeechOperationSequence)+1n
2251
+ : this.#browserSpeechOperationSequence+1;
1977
2252
  return `${this.#events.instanceId}:${action}:${this.#browserSpeechOperationSequence.toString(36)}`;
1978
2253
  }
1979
2254
 
@@ -2104,9 +2379,9 @@ class AI {
2104
2379
  expectedProviders[role]=record.provider;
2105
2380
  legacyRecords.push(record);
2106
2381
  }
2107
- return Object.freeze({
2108
- expectedProviders:Object.freeze(expectedProviders),
2109
- legacyRecords:Object.freeze(legacyRecords)
2382
+ return completeValue({
2383
+ expectedProviders:completeValue(expectedProviders),
2384
+ legacyRecords:completeValue(legacyRecords)
2110
2385
  });
2111
2386
  }
2112
2387
 
@@ -2175,7 +2450,7 @@ class AI {
2175
2450
  }
2176
2451
 
2177
2452
  #publishBrowserSpeechEvent(type,normalized,operationId,reason,{descriptor=null,error=null}={}){
2178
- const compatibilityDetail=Object.freeze({
2453
+ const compatibilityDetail=completeValue({
2179
2454
  configuration:normalized.configuration,
2180
2455
  configurationId:normalized.id,
2181
2456
  ...(descriptor?{descriptor}:{}),
@@ -2187,7 +2462,7 @@ class AI {
2187
2462
  compatibilityDetail,
2188
2463
  {
2189
2464
  operationId,
2190
- publicDetail:Object.freeze({
2465
+ publicDetail:completeValue({
2191
2466
  configurationId:normalized.id,
2192
2467
  ...(descriptor?{descriptor}:{}),
2193
2468
  ...(typeof error?.code==='string'?{code:error.code}:{}),
@@ -2199,12 +2474,12 @@ class AI {
2199
2474
 
2200
2475
  #browserSpeechRoutes(normalized,providers,previousRecord){
2201
2476
  function roleRoutes(provider,catalog){
2202
- const selection=Object.freeze({
2477
+ const selection=completeValue({
2203
2478
  providerId:provider.id,
2204
2479
  modelId:catalog.id,
2205
2480
  localOnly:true
2206
2481
  });
2207
- return Object.freeze({default:selection,localOnly:selection});
2482
+ return completeValue({default:selection,localOnly:selection});
2208
2483
  }
2209
2484
  const currentRoutes=this.#currentSpeechRoutes();
2210
2485
  const routes={};
@@ -2228,15 +2503,15 @@ class AI {
2228
2503
  catalogs[role]=catalog[0];
2229
2504
  routes[role]=roleRoutes(providers[role],catalog[0]);
2230
2505
  }
2231
- return Object.freeze({
2232
- routes:Object.freeze(routes),
2233
- catalogs:Object.freeze(catalogs)
2506
+ return completeValue({
2507
+ routes:completeValue(routes),
2508
+ catalogs:completeValue(catalogs)
2234
2509
  });
2235
2510
  }
2236
2511
 
2237
2512
  #browserSpeechDescriptor(normalized,catalogs,previousRecord){
2238
2513
  function roleDescriptor(role,configured,catalog){
2239
- return Object.freeze({
2514
+ return completeValue({
2240
2515
  role,
2241
2516
  providerId:configured.providerId,
2242
2517
  modelId:catalog.id,
@@ -2253,7 +2528,7 @@ class AI {
2253
2528
  ?roleDescriptor(role,normalized[role],catalogs[role])
2254
2529
  :previousRecord?.descriptor[role]??null;
2255
2530
  }
2256
- return Object.freeze({
2531
+ return completeValue({
2257
2532
  protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
2258
2533
  configurationId:normalized.id,
2259
2534
  ...roles
@@ -2275,7 +2550,7 @@ class AI {
2275
2550
  role=>!normalized.roles.includes(role)
2276
2551
  );
2277
2552
  if(carriedRoles.length===0)return normalized.configuration;
2278
- return Object.freeze({
2553
+ return completeValue({
2279
2554
  protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
2280
2555
  id:normalized.id,
2281
2556
  dbopfs:normalized.dbopfs,
@@ -2294,12 +2569,12 @@ class AI {
2294
2569
  }
2295
2570
 
2296
2571
  #currentSpeechRoutes(){
2297
- return Object.freeze({
2298
- stt:Object.freeze({
2572
+ return completeValue({
2573
+ stt:completeValue({
2299
2574
  default:this.#providerRuntime.selection('stt'),
2300
2575
  localOnly:this.#providerRuntime.selection('stt',{localOnly:true})
2301
2576
  }),
2302
- tts:Object.freeze({
2577
+ tts:completeValue({
2303
2578
  default:this.#providerRuntime.selection('tts'),
2304
2579
  localOnly:this.#providerRuntime.selection('tts',{localOnly:true})
2305
2580
  })
@@ -2307,9 +2582,9 @@ class AI {
2307
2582
  }
2308
2583
 
2309
2584
  #emptySpeechRoutes(){
2310
- return Object.freeze({
2311
- stt:Object.freeze({default:null,localOnly:null}),
2312
- tts:Object.freeze({default:null,localOnly:null})
2585
+ return completeValue({
2586
+ stt:completeValue({default:null,localOnly:null}),
2587
+ tts:completeValue({default:null,localOnly:null})
2313
2588
  });
2314
2589
  }
2315
2590
 
@@ -2354,7 +2629,7 @@ class AI {
2354
2629
  previousRecord
2355
2630
  ){
2356
2631
  const configurationByRole={};
2357
- const managedRoles=Object.freeze(['stt','tts'].filter(role=>
2632
+ const managedRoles=completeValue(['stt','tts'].filter(role=>
2358
2633
  normalized.roles.includes(role)
2359
2634
  ||previousRecord?.managedRoles.includes(role)
2360
2635
  ));
@@ -2365,7 +2640,7 @@ class AI {
2365
2640
  }
2366
2641
  return {
2367
2642
  configuration,
2368
- configurationByRole:Object.freeze(configurationByRole),
2643
+ configurationByRole:completeValue(configurationByRole),
2369
2644
  dbopfs:normalized.dbopfs,
2370
2645
  tableName:normalized.tableName,
2371
2646
  descriptor,
@@ -2379,15 +2654,13 @@ class AI {
2379
2654
  };
2380
2655
  }
2381
2656
 
2382
- #freezeBrowserSpeechRecord(record){
2383
- record.unregisters=Object.freeze({...record.unregisters});
2384
- Object.seal(record.registrationState);
2385
- Object.seal(record.retirementState);
2386
- return Object.freeze(record);
2657
+ #finalizeBrowserSpeechRecord(record){
2658
+ record.unregisters=completeValue({...record.unregisters});
2659
+ return completeValue(record);
2387
2660
  }
2388
2661
 
2389
2662
  #browserSpeechRecordFromReplacement(record,replacement){
2390
- return this.#freezeBrowserSpeechRecord({
2663
+ return this.#finalizeBrowserSpeechRecord({
2391
2664
  configuration:record.configuration,
2392
2665
  configurationByRole:record.configurationByRole,
2393
2666
  dbopfs:record.dbopfs,
@@ -2609,22 +2882,16 @@ class AI {
2609
2882
  candidateProviders[role]=factory({
2610
2883
  id:configured.providerId,
2611
2884
  ...(configured.graph
2612
- ?{
2613
- graph:configured.graph,
2614
- security:configured.security
2615
- }
2885
+ ?{graph:configured.graph}
2616
2886
  :{
2617
2887
  model:configured.model,
2618
- runtime:configured.runtime,
2619
- ...(Object.hasOwn(configured,'security')
2620
- ?{security:configured.security}
2621
- :{})
2888
+ runtime:configured.runtime
2622
2889
  }),
2623
2890
  store,
2624
2891
  offline:configured.offline
2625
2892
  });
2626
2893
  }
2627
- providers=Object.freeze({...candidateProviders});
2894
+ providers=completeValue({...candidateProviders});
2628
2895
  prepared=this.#browserSpeechRoutes(
2629
2896
  normalized,
2630
2897
  providers,
@@ -2646,7 +2913,7 @@ class AI {
2646
2913
  const provider=candidateProviders[role];
2647
2914
  const changed=normalized.roles.includes(role);
2648
2915
  const selection=changed&&provider
2649
- ?Object.freeze({
2916
+ ?completeValue({
2650
2917
  providerId:provider.id,
2651
2918
  modelId:normalized[role].graph?.model.id
2652
2919
  ??normalized[role].model.id,
@@ -2654,7 +2921,7 @@ class AI {
2654
2921
  })
2655
2922
  :null;
2656
2923
  partialRoutes[role]=changed
2657
- ?Object.freeze({default:selection,localOnly:selection})
2924
+ ?completeValue({default:selection,localOnly:selection})
2658
2925
  :previousRecord?.managedRoles.includes(role)
2659
2926
  ?previousRecord.routes[role]
2660
2927
  :currentRoutes[role];
@@ -2663,8 +2930,8 @@ class AI {
2663
2930
  normalized,
2664
2931
  configuration,
2665
2932
  null,
2666
- Object.freeze({...candidateProviders}),
2667
- Object.freeze(partialRoutes),
2933
+ completeValue({...candidateProviders}),
2934
+ completeValue(partialRoutes),
2668
2935
  previousRecord
2669
2936
  );
2670
2937
  return this.#throwAfterBrowserSpeechCandidateCleanup(
@@ -2728,9 +2995,9 @@ class AI {
2728
2995
  expectedProvider:replacementBoundary.expectedProviders[role]
2729
2996
  }
2730
2997
  );
2731
- replacement=Object.freeze({
2998
+ replacement=completeValue({
2732
2999
  routes:prepared.routes,
2733
- unregisters:Object.freeze({
3000
+ unregisters:completeValue({
2734
3001
  stt:role==='stt'
2735
3002
  ?roleReplacement.unregister
2736
3003
  :previousRecord?.unregisters.stt??null,
@@ -2804,9 +3071,9 @@ class AI {
2804
3071
  expectedProvider:record.providers[role]
2805
3072
  }
2806
3073
  );
2807
- rollback=Object.freeze({
3074
+ rollback=completeValue({
2808
3075
  routes:previousRecord.routes,
2809
- unregisters:Object.freeze({
3076
+ unregisters:completeValue({
2810
3077
  stt:role==='stt'
2811
3078
  ?roleRollback.unregister
2812
3079
  :previousRecord.unregisters.stt,
@@ -2978,7 +3245,7 @@ class AI {
2978
3245
  operation.signal?.removeEventListener('abort',forwardAbort);
2979
3246
  if(runtime.#browserSpeechController===controller){
2980
3247
  runtime.#browserSpeechController=null;
2981
- runtime.#browserSpeechControllerRoles=Object.freeze([]);
3248
+ runtime.#browserSpeechControllerRoles=completeValue([]);
2982
3249
  }
2983
3250
  }
2984
3251
  }
@@ -3028,7 +3295,7 @@ class AI {
3028
3295
  if(invalidatesSpeech)this.#invalidateSpeechControl();
3029
3296
  const controller=new AbortController();
3030
3297
  this.#browserSpeechController=controller;
3031
- this.#browserSpeechControllerRoles=Object.freeze([]);
3298
+ this.#browserSpeechControllerRoles=completeValue([]);
3032
3299
  const forwardAbort=function cancelBrowserSpeechDisposal(){
3033
3300
  if(!controller.signal.aborted)controller.abort(operation.signal.reason);
3034
3301
  };
@@ -3079,7 +3346,7 @@ class AI {
3079
3346
  }else{
3080
3347
  const role=activeRecord.managedRoles[0];
3081
3348
  const currentRoutes=runtime.#currentSpeechRoutes();
3082
- const emptyRoleRoutes=Object.freeze({
3349
+ const emptyRoleRoutes=completeValue({
3083
3350
  default:null,
3084
3351
  localOnly:null
3085
3352
  });
@@ -3091,8 +3358,8 @@ class AI {
3091
3358
  expectedProvider:activeRecord.providers[role]
3092
3359
  }
3093
3360
  );
3094
- removed=Object.freeze({
3095
- routes:Object.freeze({
3361
+ removed=completeValue({
3362
+ routes:completeValue({
3096
3363
  stt:role==='stt'
3097
3364
  ?emptyRoleRoutes
3098
3365
  :currentRoutes.stt,
@@ -3182,7 +3449,7 @@ class AI {
3182
3449
  operation.signal?.removeEventListener('abort',forwardAbort);
3183
3450
  if(runtime.#browserSpeechController===controller){
3184
3451
  runtime.#browserSpeechController=null;
3185
- runtime.#browserSpeechControllerRoles=Object.freeze([]);
3452
+ runtime.#browserSpeechControllerRoles=completeValue([]);
3186
3453
  }
3187
3454
  }
3188
3455
  }
@@ -3262,7 +3529,7 @@ class AI {
3262
3529
  const errorText=await response.text();
3263
3530
 
3264
3531
  if(errorText&&!errorText.trim().startsWith('<')){
3265
- detail=errorText.trim().slice(0,500);
3532
+ detail=errorText;
3266
3533
  }
3267
3534
  }
3268
3535
  }catch{
@@ -3471,8 +3738,8 @@ class AI {
3471
3738
  #arrayBufferToBase64(arrayBuffer){
3472
3739
  const bytes=new Uint8Array(arrayBuffer);
3473
3740
 
3474
- if(!bytes.length||bytes.length>6*1024*1024){
3475
- throw new RangeError('Microphone audio must be between 1 byte and 6 MiB.');
3741
+ if(!bytes.length){
3742
+ throw new RangeError('Microphone audio must not be empty.');
3476
3743
  }
3477
3744
 
3478
3745
  const chunks=[];
@@ -3485,7 +3752,7 @@ class AI {
3485
3752
  }
3486
3753
 
3487
3754
  #base64ToBytes(value){
3488
- if(typeof value!=='string'||!value||value.length>8*1024*1024){
3755
+ if(typeof value!=='string'||!value){
3489
3756
  throw new TypeError('Arcane returned invalid local speech audio.');
3490
3757
  }
3491
3758
 
@@ -3532,8 +3799,13 @@ class AI {
3532
3799
  if(typeof argumentValue==='string'){
3533
3800
  try{
3534
3801
  argumentValue=JSON.parse(argumentValue);
3535
- }catch{
3536
- argumentValue={};
3802
+ }catch(cause){
3803
+ const error=new TypeError(
3804
+ 'Ollama tool call arguments must contain valid JSON.'
3805
+ );
3806
+ error.code='AI_TOOL_ARGUMENTS_INVALID';
3807
+ error.cause=cause;
3808
+ throw error;
3537
3809
  }
3538
3810
  }
3539
3811
  if(
@@ -3541,7 +3813,11 @@ class AI {
3541
3813
  ||typeof argumentValue!=='object'
3542
3814
  ||Array.isArray(argumentValue)
3543
3815
  ){
3544
- argumentValue={};
3816
+ const error=new TypeError(
3817
+ 'Ollama tool call arguments must contain a JSON object.'
3818
+ );
3819
+ error.code='AI_TOOL_ARGUMENTS_INVALID';
3820
+ throw error;
3545
3821
  }
3546
3822
  if(callId&&name){
3547
3823
  toolNamesByCallId.set(callId,name);
@@ -3577,7 +3853,7 @@ class AI {
3577
3853
  });
3578
3854
  const requiredName=toolChoice?.function?.name;
3579
3855
  const instruction=requiredName
3580
- ?`Call the ${requiredName} function now with concise values for every required field. Do not answer in prose.`
3856
+ ?`Call the ${requiredName} function now with complete values for every required field. Do not answer in prose.`
3581
3857
  :toolChoice==='none'
3582
3858
  ?'Do not call a function in this request. Follow the response instructions and answer in the requested format.'
3583
3859
  :'';
@@ -3645,16 +3921,15 @@ class AI {
3645
3921
  return null;
3646
3922
  }
3647
3923
 
3648
- async #reportRequest(requestHandler,request,id){
3924
+ async #reportRequest(requestHandler,request,id,metadata){
3649
3925
  if(typeof requestHandler!=='function'){
3650
3926
  throw new TypeError('AI onRequest callback must be a function.');
3651
3927
  }
3652
- await requestHandler(request,id);
3928
+ await requestHandler(request,id,metadata);
3653
3929
  }
3654
3930
 
3655
3931
  #providerStreamEmissions(chunk,seeThinking){
3656
3932
  const chunks=[];
3657
- const toolNames=[];
3658
3933
  const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
3659
3934
  for(const choice of choices){
3660
3935
  const delta=choice?.delta||{};
@@ -3664,12 +3939,6 @@ class AI {
3664
3939
  if(typeof delta.content==='string'){
3665
3940
  chunks.push({text:delta.content,thinking:false});
3666
3941
  }
3667
- for(const call of Array.isArray(delta.tool_calls)?delta.tool_calls:[]){
3668
- const name=call?.function?.name;
3669
- if(typeof name==='string'&&name){
3670
- toolNames.push(name);
3671
- }
3672
- }
3673
3942
  }
3674
3943
  if(!choices.length){
3675
3944
  if(seeThinking&&typeof chunk?.thinking==='string'){
@@ -3683,19 +3952,8 @@ class AI {
3683
3952
  if(text){
3684
3953
  chunks.push({text,thinking:false});
3685
3954
  }
3686
- const calls=Array.isArray(chunk?.toolCalls)
3687
- ?chunk.toolCalls
3688
- :Array.isArray(chunk?.tool_calls)
3689
- ?chunk.tool_calls
3690
- :[];
3691
- for(const call of calls){
3692
- const name=call?.function?.name||call?.name;
3693
- if(typeof name==='string'&&name){
3694
- toolNames.push(name);
3695
- }
3696
- }
3697
3955
  }
3698
- return {chunks,toolNames};
3956
+ return {chunks};
3699
3957
  }
3700
3958
 
3701
3959
  #providerCompletionOutput(completion){
@@ -3729,7 +3987,7 @@ class AI {
3729
3987
  payload.structuredOutput??false,
3730
3988
  payload.tools??[],
3731
3989
  payload.toolChoice??'auto',
3732
- payload.parallelToolCalls??true,
3990
+ payload.parallelToolCalls??false,
3733
3991
  payload.id??Date.now(),
3734
3992
  function ignoreLegacyLLMProviderRequest(){},
3735
3993
  signal
@@ -3748,26 +4006,21 @@ class AI {
3748
4006
  );
3749
4007
  }
3750
4008
 
3751
- function emitLegacyLLMStreamTool(name){
3752
- if(typeof name==='string'&&name){
3753
- bridge.emit({toolCalls:[{name}]});
3754
- }
3755
- }
3756
-
3757
4009
  return this.#streamLegacyMessage(
3758
4010
  payload.messages??[],
3759
4011
  emitLegacyLLMStreamText,
3760
4012
  function ignoreLegacyLLMProviderCompletion(){},
3761
4013
  payload.tools??[],
3762
4014
  payload.toolChoice??'auto',
3763
- emitLegacyLLMStreamTool,
3764
- payload.parallelToolCalls??true,
4015
+ function retainLegacyLLMStreamToolUntilCompletion(){},
4016
+ payload.parallelToolCalls??false,
3765
4017
  payload.id??Date.now(),
3766
4018
  payload.seeThinking??false,
3767
4019
  bridge.signal,
3768
4020
  function ignoreLegacyLLMProviderRequest(){},
3769
4021
  payload.structuredOutput??false,
3770
- false
4022
+ false,
4023
+ true
3771
4024
  );
3772
4025
  }
3773
4026
 
@@ -3789,27 +4042,78 @@ class AI {
3789
4042
  }
3790
4043
  }
3791
4044
 
4045
+ #openAICompatibleOllamaToolCalls(value,id=Date.now()){
4046
+ if(value===undefined){
4047
+ return [];
4048
+ }
4049
+ if(!Array.isArray(value)){
4050
+ throw aiStructuralError(
4051
+ 'AI_CHAT_INVALID_TOOL_CALL',
4052
+ 'The native Ollama response contains invalid structural tool calls.'
4053
+ );
4054
+ }
4055
+ return value.map(function adaptNativeOllamaToolCall(call,index){
4056
+ if(!call||typeof call!=='object'||Array.isArray(call)){
4057
+ throw aiStructuralError(
4058
+ 'AI_CHAT_INVALID_TOOL_CALL',
4059
+ `The native Ollama structural tool call ${index+1} is invalid.`
4060
+ );
4061
+ }
4062
+ const nativeFunction=call.function;
4063
+ if(
4064
+ !nativeFunction
4065
+ ||typeof nativeFunction!=='object'
4066
+ ||Array.isArray(nativeFunction)
4067
+ ){
4068
+ throw aiStructuralError(
4069
+ 'AI_CHAT_INVALID_TOOL_CALL',
4070
+ `The native Ollama structural tool call ${index+1} is invalid.`
4071
+ );
4072
+ }
4073
+ let encodedArguments=nativeFunction.arguments;
4074
+ if(
4075
+ encodedArguments
4076
+ &&typeof encodedArguments==='object'
4077
+ &&!Array.isArray(encodedArguments)
4078
+ ){
4079
+ try{
4080
+ encodedArguments=JSON.stringify(encodedArguments);
4081
+ }catch(cause){
4082
+ throw aiStructuralError(
4083
+ 'AI_CHAT_INVALID_TOOL_CALL',
4084
+ `The native Ollama structural tool call ${index+1} arguments cannot be encoded.`,
4085
+ cause
4086
+ );
4087
+ }
4088
+ }
4089
+ return {
4090
+ id:call.id===undefined
4091
+ ?`ollama-${id}-tool-${index+1}`
4092
+ :call.id,
4093
+ type:call.type===undefined?'function':call.type,
4094
+ function:{
4095
+ name:nativeFunction.name,
4096
+ arguments:encodedArguments
4097
+ }
4098
+ };
4099
+ });
4100
+ }
4101
+
3792
4102
  #openAICompatibleOllamaResponse(response={},id=Date.now()){
3793
4103
  const message=response?.message||{};
3794
- const toolCalls=Array.isArray(message.tool_calls)
3795
- ?message.tool_calls.map(
3796
- function normalizeOllamaToolCall(call,index){
3797
- return {
3798
- id:call?.id||`call-${id}-${index}`,
3799
- type:'function',
3800
- function:{
3801
- name:call?.function?.name||'',
3802
- arguments:typeof call?.function?.arguments==='string'
3803
- ?call.function.arguments
3804
- :JSON.stringify(call?.function?.arguments||{})
3805
- }
3806
- };
3807
- }
3808
- )
3809
- :[];
4104
+ const responseId=typeof response?.id==='string'&&response.id
4105
+ ?response.id
4106
+ :`ollama-${id}`;
4107
+ const adaptedToolCalls=this.#openAICompatibleOllamaToolCalls(
4108
+ message.tool_calls,
4109
+ id
4110
+ );
4111
+ const toolCalls=normalizeAICompletionToolCalls({
4112
+ message:{tool_calls:adaptedToolCalls}
4113
+ });
3810
4114
 
3811
4115
  return {
3812
- id:response?.id||`ollama-${id}`,
4116
+ id:responseId,
3813
4117
  object:'chat.completion',
3814
4118
  created:Math.floor(Date.now()/1000),
3815
4119
  model:response?.model||this.model,
@@ -3841,15 +4145,27 @@ class AI {
3841
4145
  localOnly=false,
3842
4146
  onChunk=function ignoreStreamChunk(){},
3843
4147
  onComplete=function finishIgnoredStream(){},
4148
+ onResponse=function ignoreStreamResponse(){},
3844
4149
  tools=[],
3845
4150
  toolChoice='auto',
3846
4151
  onToolCall=function ignoreEarlyFunction(){},
3847
4152
  onRequest=function ignoreStreamRequest(){},
3848
- parallelToolCalls=true,
4153
+ parallelToolCalls=false,
3849
4154
  id=Date.now(),
3850
4155
  seeThinking=false,
3851
- signal=null
4156
+ signal=null,
4157
+ maxOutputTokens,
4158
+ maxTokens,
4159
+ temperature,
4160
+ topK,
4161
+ topP,
4162
+ repeatPenalty,
4163
+ minP,
4164
+ seed,
4165
+ stop,
4166
+ templateOptions
3852
4167
  }={}){
4168
+ validateAIStructuralRequest(messages,tools,parallelToolCalls);
3853
4169
  if(localOnly!==true&&localOnly!==false){
3854
4170
  throw new TypeError('AI localOnly must be a boolean.');
3855
4171
  }
@@ -3868,10 +4184,20 @@ class AI {
3868
4184
  toolChoice,
3869
4185
  parallelToolCalls,
3870
4186
  id,
3871
- seeThinking
4187
+ seeThinking,
4188
+ ...(maxOutputTokens!==undefined
4189
+ ?{maxTokens:maxOutputTokens}
4190
+ :maxTokens!==undefined?{maxTokens}:{}),
4191
+ ...(temperature!==undefined?{temperature}:{}),
4192
+ ...(topK!==undefined?{topK}:{}),
4193
+ ...(topP!==undefined?{topP}:{}),
4194
+ ...(repeatPenalty!==undefined?{repeatPenalty}:{}),
4195
+ ...(minP!==undefined?{minP}:{}),
4196
+ ...(seed!==undefined?{seed}:{}),
4197
+ ...(stop!==undefined?{stop}:{}),
4198
+ ...(templateOptions!==undefined?{templateOptions}:{})
3872
4199
  };
3873
4200
  const displayId=`M-${id}`;
3874
- const announcedTools=new Set();
3875
4201
  let handle=null;
3876
4202
  try{
3877
4203
  if(signal?.aborted){
@@ -3908,40 +4234,22 @@ class AI {
3908
4234
  throw normalizeAIRequestAbort();
3909
4235
  }
3910
4236
  }
3911
- for(const name of emissions.toolNames){
3912
- if(signal?.aborted){
3913
- throw normalizeAIRequestAbort();
3914
- }
3915
- if(!announcedTools.has(name)){
3916
- announcedTools.add(name);
3917
- await onToolCall(name);
3918
- if(signal?.aborted){
3919
- throw normalizeAIRequestAbort();
3920
- }
3921
- }
3922
- }
3923
4237
  }
3924
4238
  const completion=await handle.result;
3925
4239
  if(signal?.aborted){
3926
4240
  throw normalizeAIRequestAbort();
3927
4241
  }
3928
- for(const choice of Array.isArray(completion?.choices)
3929
- ?completion.choices
3930
- :[]){
3931
- for(const call of Array.isArray(choice?.message?.tool_calls)
3932
- ?choice.message.tool_calls
3933
- :[]){
3934
- const name=call?.function?.name;
3935
- if(typeof name==='string'&&name&&!announcedTools.has(name)){
3936
- if(signal?.aborted){
3937
- throw normalizeAIRequestAbort();
3938
- }
3939
- announcedTools.add(name);
3940
- await onToolCall(name);
3941
- if(signal?.aborted){
3942
- throw normalizeAIRequestAbort();
3943
- }
3944
- }
4242
+ const structuralToolCalls=normalizeAICompletionToolCalls(
4243
+ completion
4244
+ );
4245
+ await onResponse(completion,id,false);
4246
+ for(const call of structuralToolCalls){
4247
+ if(signal?.aborted){
4248
+ throw normalizeAIRequestAbort();
4249
+ }
4250
+ await onToolCall(call,displayId);
4251
+ if(signal?.aborted){
4252
+ throw normalizeAIRequestAbort();
3945
4253
  }
3946
4254
  }
3947
4255
  const result=this.#providerCompletionOutput(completion);
@@ -3964,20 +4272,52 @@ class AI {
3964
4272
  }
3965
4273
  }
3966
4274
 
3967
- return this.streamMessage(
3968
- messages,
3969
- onChunk,
3970
- onComplete,
3971
- tools,
3972
- toolChoice,
3973
- onToolCall,
3974
- parallelToolCalls,
3975
- id,
3976
- seeThinking,
3977
- signal,
3978
- onRequest,
3979
- structuredOutput
3980
- );
4275
+ try{
4276
+ const completion=await this.#streamLegacyMessage(
4277
+ messages,
4278
+ onChunk,
4279
+ function retainLegacyCompletionUntilResponse(){},
4280
+ tools,
4281
+ toolChoice,
4282
+ function retainLegacyToolCallUntilResponse(){},
4283
+ parallelToolCalls,
4284
+ id,
4285
+ seeThinking,
4286
+ signal,
4287
+ onRequest,
4288
+ structuredOutput,
4289
+ false,
4290
+ true
4291
+ );
4292
+ const structuralToolCalls=normalizeAICompletionToolCalls(
4293
+ completion
4294
+ );
4295
+ if(signal?.aborted){
4296
+ throw normalizeAIRequestAbort();
4297
+ }
4298
+ await onResponse(completion,id,false);
4299
+ for(const call of structuralToolCalls){
4300
+ if(signal?.aborted){
4301
+ throw normalizeAIRequestAbort();
4302
+ }
4303
+ await onToolCall(call,`M-${id}`);
4304
+ }
4305
+ const result=this.#providerCompletionOutput(completion);
4306
+ if(signal?.aborted){
4307
+ throw normalizeAIRequestAbort();
4308
+ }
4309
+ await onComplete(result,`M-${id}`,false);
4310
+ if(signal?.aborted){
4311
+ throw normalizeAIRequestAbort();
4312
+ }
4313
+ this.finishTTS();
4314
+ return result;
4315
+ }catch(error){
4316
+ this.stopAudio();
4317
+ throw isAIRequestAbort(error,signal)
4318
+ ?normalizeAIRequestAbort(error)
4319
+ :error;
4320
+ }
3981
4321
  }
3982
4322
 
3983
4323
  async streamMessage(
@@ -3987,12 +4327,13 @@ class AI {
3987
4327
  tools=[],
3988
4328
  tool_choice='auto',
3989
4329
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
3990
- parallel_tool_calls=true,
4330
+ parallel_tool_calls=false,
3991
4331
  id=Date.now(),
3992
4332
  seeThinking=false,
3993
4333
  signal=null,
3994
4334
  requestHandler=function ignoreStreamRequest(){},
3995
- structuredOutput=false
4335
+ structuredOutput=false,
4336
+ returnCompletion=false
3996
4337
  ){
3997
4338
  if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
3998
4339
  return this.streamRequest({
@@ -4024,7 +4365,9 @@ class AI {
4024
4365
  seeThinking,
4025
4366
  signal,
4026
4367
  requestHandler,
4027
- structuredOutput
4368
+ structuredOutput,
4369
+ true,
4370
+ returnCompletion
4028
4371
  );
4029
4372
  }
4030
4373
 
@@ -4035,17 +4378,19 @@ class AI {
4035
4378
  tools=[],
4036
4379
  tool_choice='auto',
4037
4380
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
4038
- parallel_tool_calls=true,
4381
+ parallel_tool_calls=false,
4039
4382
  id=Date.now(),
4040
4383
  seeThinking=false,
4041
4384
  signal=null,
4042
4385
  requestHandler=function ignoreStreamRequest(){},
4043
4386
  structuredOutput=false,
4044
- finishSpeech=true
4387
+ finishSpeech=true,
4388
+ returnCompletion=false
4045
4389
  ){
4046
4390
  let speechTurnCompleted=false;
4047
4391
 
4048
4392
  try{
4393
+ validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4049
4394
  this.#assertServiceConfigured(this.llmService);
4050
4395
  if(signal&&(
4051
4396
  typeof signal.aborted!=='boolean'
@@ -4085,7 +4430,7 @@ class AI {
4085
4430
  let isThinking=true;
4086
4431
  let isWaiting=true;
4087
4432
 
4088
- streamHandler('Thinking...',`M-${id}`,isThinking);
4433
+ await streamHandler('Thinking...',`M-${id}`,isThinking);
4089
4434
 
4090
4435
  const nativeOllama=this.#nativeOllama();
4091
4436
 
@@ -4098,8 +4443,7 @@ class AI {
4098
4443
 
4099
4444
  if(nativeOllama){
4100
4445
  let nativeContent='';
4101
- const nativeToolCalls={};
4102
- const triggeredTools=new Set();
4446
+ const nativeStreamedToolCallFrames=[];
4103
4447
  const ollamaTools=this.#ollamaTools(tools,tool_choice);
4104
4448
  const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
4105
4449
  const ollamaRequest={
@@ -4111,46 +4455,19 @@ class AI {
4111
4455
  ...(ollamaTools.length?{tools:ollamaTools}:{})
4112
4456
  };
4113
4457
 
4114
- function reportEarlyFunctionFailure(error){
4115
- console.error('Early tool trigger failed.');
4116
- }
4117
-
4118
4458
  function receiveNativeToolCalls(message={}){
4119
4459
  const calls=Array.isArray(message.tool_calls)?message.tool_calls:[];
4120
4460
 
4121
4461
  if(calls.length){
4122
4462
  isThinking=false;
4123
- }
4124
-
4125
- for(const call of calls){
4126
- const name=call?.function?.name;
4127
-
4128
- if(!name){
4129
- continue;
4130
- }
4131
-
4132
- nativeToolCalls[name]=typeof call.function.arguments==='string'
4133
- ?call.function.arguments
4134
- :JSON.stringify(call.function.arguments||{});
4135
-
4136
- if(!triggeredTools.has(name)){
4137
- triggeredTools.add(name);
4138
- Promise.resolve(earlyFunctionTrigger(name)).catch(
4139
- reportEarlyFunctionFailure
4140
- );
4141
- }
4463
+ nativeStreamedToolCallFrames.push(calls.slice());
4142
4464
  }
4143
4465
  }
4144
4466
 
4145
- await this.#reportRequest(requestHandler,ollamaRequest,id,{
4146
- operation:'stream',
4147
- transport:'native',
4148
- destination:'Arcane.ollama.chat'
4149
- });
4150
- const nativeResponse=await nativeOllama.chat(
4151
- ollamaRequest,
4152
- {
4153
- onChunk:function receiveNativeOllamaChunk(chunk){
4467
+ let nativeChunkPipeline=Promise.resolve();
4468
+ function queueNativeOllamaChunk(chunk){
4469
+ nativeChunkPipeline=nativeChunkPipeline.then(
4470
+ async function processNativeOllamaChunk(){
4154
4471
  if(signal?.aborted){
4155
4472
  return;
4156
4473
  }
@@ -4161,43 +4478,105 @@ class AI {
4161
4478
  const content=String(message.content||'');
4162
4479
 
4163
4480
  if(thinking){
4164
- streamHandler(thinking,`M-${id}`,true);
4481
+ await streamHandler(thinking,`M-${id}`,true);
4165
4482
  }
4166
4483
 
4167
4484
  if(content){
4168
4485
  isThinking=false;
4169
4486
  nativeContent+=content;
4170
- streamHandler(content,`M-${id}`,false);
4487
+ await streamHandler(content,`M-${id}`,false);
4171
4488
  }
4172
4489
 
4173
4490
  receiveNativeToolCalls(message);
4174
- },
4175
- signal
4176
- }
4177
- );
4491
+ }
4492
+ );
4493
+ return nativeChunkPipeline;
4494
+ }
4495
+
4496
+ await this.#reportRequest(requestHandler,ollamaRequest,id,{
4497
+ operation:'stream',
4498
+ transport:'native',
4499
+ destination:'Arcane.ollama.chat'
4500
+ });
4501
+ let nativeResponse;
4502
+ let nativeRequestFailure=null;
4503
+ try{
4504
+ nativeResponse=await nativeOllama.chat(
4505
+ ollamaRequest,
4506
+ {onChunk:queueNativeOllamaChunk,signal}
4507
+ );
4508
+ }catch(error){
4509
+ nativeRequestFailure=error;
4510
+ }
4511
+ try{
4512
+ await nativeChunkPipeline;
4513
+ }catch(error){
4514
+ nativeRequestFailure??=error;
4515
+ }
4516
+ if(nativeRequestFailure){
4517
+ throw nativeRequestFailure;
4518
+ }
4178
4519
  if(signal?.aborted){
4179
4520
  throw normalizeAIRequestAbort();
4180
4521
  }
4181
- receiveNativeToolCalls(nativeResponse?.message);
4522
+ const nativeCompletion=this.#openAICompatibleOllamaResponse(
4523
+ {
4524
+ ...nativeResponse,
4525
+ message:{
4526
+ ...(nativeResponse?.message??{}),
4527
+ content:typeof nativeResponse?.message?.content==='string'
4528
+ &&nativeResponse.message.content
4529
+ ?nativeResponse.message.content
4530
+ :nativeContent
4531
+ }
4532
+ },
4533
+ id
4534
+ );
4535
+ const structuralToolCalls=normalizeAICompletionToolCalls(
4536
+ nativeCompletion
4537
+ );
4538
+ for(let frameIndex=0;
4539
+ frameIndex<nativeStreamedToolCallFrames.length;
4540
+ frameIndex+=1
4541
+ ){
4542
+ const streamedToolCalls=normalizeAIStreamToolCallObservation(
4543
+ {
4544
+ message:{
4545
+ tool_calls:this.#openAICompatibleOllamaToolCalls(
4546
+ nativeStreamedToolCallFrames[frameIndex],
4547
+ id
4548
+ )
4549
+ }
4550
+ },
4551
+ `The native Ollama stream frame ${frameIndex+1}`
4552
+ );
4553
+ assertAIStreamToolCallCorrelation(
4554
+ streamedToolCalls,
4555
+ structuralToolCalls,
4556
+ 'The native Ollama stream'
4557
+ );
4558
+ }
4182
4559
  this.#assertRequiredOllamaToolCall(
4183
- Object.keys(nativeToolCalls).map(function createToolCallName(name){
4184
- return {function:{name}};
4185
- }),
4560
+ structuralToolCalls,
4186
4561
  tool_choice
4187
4562
  );
4563
+ for(const call of structuralToolCalls){
4564
+ if(signal?.aborted){
4565
+ throw normalizeAIRequestAbort();
4566
+ }
4567
+ await earlyFunctionTrigger(call,`M-${id}`);
4568
+ }
4188
4569
 
4189
- const nativeResult=Object.keys(nativeToolCalls).length
4190
- ?nativeToolCalls
4191
- :nativeContent;
4192
- if(Object.keys(nativeToolCalls).length&&!nativeContent){
4193
- streamHandler('',`M-${id}`,false);
4570
+ const nativeResult=this.#providerCompletionOutput(nativeCompletion);
4571
+ if(structuralToolCalls.length&&!nativeContent){
4572
+ await streamHandler('',`M-${id}`,false);
4194
4573
  }
4195
4574
  if(finishSpeech){
4196
4575
  this.finishTTS();
4197
4576
  }
4198
4577
  await streamComplete(nativeResult,`M-${id}`,isThinking);
4199
4578
  speechTurnCompleted=true;
4200
- return nativeResult;
4579
+ return returnCompletion?nativeCompletion:nativeResult;
4201
4580
  }
4202
4581
 
4203
4582
  await this.#reportRequest(requestHandler,request,id,{
@@ -4238,8 +4617,13 @@ class AI {
4238
4617
 
4239
4618
  let chunkString='';
4240
4619
  let chunkCache='';
4241
- const streamedToolCalls=new Map();
4242
- const triggeredTools=new Set();
4620
+ const streamedToolCallsByChoice=new Map();
4621
+ let streamCreated=null;
4622
+ let streamFinishReason=null;
4623
+ let streamId=null;
4624
+ let streamModel=null;
4625
+ let streamObject=null;
4626
+ let streamUsage=null;
4243
4627
  const decoder = new TextDecoder('utf-8');
4244
4628
  //alert(1)
4245
4629
  const reader=response.body?.getReader?.();
@@ -4248,41 +4632,63 @@ class AI {
4248
4632
  throw new TypeError('Streaming response body is not readable');
4249
4633
  }
4250
4634
 
4251
- function receiveStreamedToolCalls(toolCalls=[]){
4635
+ function receiveStreamedToolCalls(toolCalls=[],choicePosition=0){
4636
+ const streamedToolCalls=streamedToolCallsByChoice.get(choicePosition)
4637
+ ||new Map();
4252
4638
  for(let position=0;position<toolCalls.length;position++){
4253
4639
  const toolCall=toolCalls[position]||{};
4254
4640
  const toolFunction=toolCall.function||{};
4255
- const key=Number.isInteger(toolCall.index)
4256
- ?`index:${toolCall.index}`
4257
- :toolCall.id
4258
- ?`id:${toolCall.id}`
4259
- :`position:${position}`;
4641
+ const key=`position:${Number.isInteger(toolCall.index)
4642
+ ?toolCall.index
4643
+ :position}`;
4260
4644
  const record=streamedToolCalls.get(key)||{
4261
4645
  arguments:'',
4646
+ id:'',
4647
+ invalidArguments:false,
4648
+ invalidIdentity:false,
4649
+ invalidName:false,
4262
4650
  name:'',
4263
- order:streamedToolCalls.size
4651
+ order:streamedToolCalls.size,
4652
+ type:''
4264
4653
  };
4265
4654
 
4266
- if(toolFunction.name){
4267
- record.name=toolFunction.name;
4268
- if(!triggeredTools.has(record.name)){
4269
- triggeredTools.add(record.name);
4270
- Promise.resolve(
4271
- earlyFunctionTrigger(record.name)
4272
- ).catch(
4273
- ()=>console.error('Early tool trigger failed.')
4274
- );
4655
+ if(toolCall.id!==undefined){
4656
+ if(typeof toolCall.id!=='string'||!toolCall.id){
4657
+ record.invalidIdentity=true;
4658
+ }else if(record.id&&record.id!==toolCall.id){
4659
+ record.invalidIdentity=true;
4660
+ }else{
4661
+ record.id=toolCall.id;
4662
+ }
4663
+ }
4664
+
4665
+ if(toolCall.type!==undefined){
4666
+ if(typeof toolCall.type!=='string'||!toolCall.type){
4667
+ record.invalidIdentity=true;
4668
+ }else if(record.type&&record.type!==toolCall.type){
4669
+ record.invalidIdentity=true;
4670
+ }else{
4671
+ record.type=toolCall.type;
4672
+ }
4673
+ }
4674
+
4675
+ if(toolFunction.name!==undefined){
4676
+ if(typeof toolFunction.name!=='string'){
4677
+ record.invalidName=true;
4678
+ }else{
4679
+ record.name+=toolFunction.name;
4275
4680
  }
4276
4681
  }
4277
4682
 
4278
4683
  if(typeof toolFunction.arguments==='string'){
4279
4684
  record.arguments+=toolFunction.arguments;
4280
- }else if(toolFunction.arguments&&typeof toolFunction.arguments==='object'){
4281
- record.arguments+=JSON.stringify(toolFunction.arguments);
4685
+ }else if(toolFunction.arguments!==undefined){
4686
+ record.invalidArguments=true;
4282
4687
  }
4283
4688
 
4284
4689
  streamedToolCalls.set(key,record);
4285
4690
  }
4691
+ streamedToolCallsByChoice.set(choicePosition,streamedToolCalls);
4286
4692
  }
4287
4693
 
4288
4694
  try{
@@ -4303,64 +4709,88 @@ class AI {
4303
4709
  //alert(3)
4304
4710
  //console.log(lines);
4305
4711
 
4306
- lines.forEach(
4307
- function parsingAIGeneratedStream(delta,i){
4308
- chunkCache+=delta;
4712
+ for(const eventData of lines){
4713
+ chunkCache+=eventData;
4309
4714
 
4310
- if (chunkCache.trim() === '[DONE]') {
4311
- chunkCache = '';
4312
- return;
4715
+ if(chunkCache.trim()==='[DONE]'){
4716
+ chunkCache='';
4717
+ continue;
4718
+ }
4719
+
4720
+ let streamedResponse;
4721
+ try{
4722
+ streamedResponse=JSON.parse(chunkCache)||{};
4723
+ }catch{
4724
+ continue;
4725
+ }
4726
+ chunkCache='';
4727
+ if(streamedResponse.id!==undefined){
4728
+ streamId=streamedResponse.id;
4729
+ }
4730
+ if(streamedResponse.object!==undefined){
4731
+ streamObject=streamedResponse.object;
4732
+ }
4733
+ if(streamedResponse.created!==undefined){
4734
+ streamCreated=streamedResponse.created;
4735
+ }
4736
+ if(streamedResponse.model!==undefined){
4737
+ streamModel=streamedResponse.model;
4738
+ }
4739
+ if(streamedResponse.usage!==undefined){
4740
+ streamUsage=streamedResponse.usage;
4741
+ }
4742
+
4743
+ const choices=Array.isArray(streamedResponse.choices)
4744
+ ?streamedResponse.choices
4745
+ :[];
4746
+ for(let choicePosition=0;
4747
+ choicePosition<choices.length;
4748
+ choicePosition+=1
4749
+ ){
4750
+ const choice=choices[choicePosition]||{};
4751
+ const choiceDelta=choice.delta||{};
4752
+ const toolCalls=choiceDelta.tool_calls;
4753
+ if(toolCalls!==undefined&&!Array.isArray(toolCalls)){
4754
+ throw aiStructuralError(
4755
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4756
+ `AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
4757
+ );
4758
+ }
4759
+ if(toolCalls?.length){
4760
+ receiveStreamedToolCalls(
4761
+ toolCalls,
4762
+ choicePosition
4763
+ );
4764
+ }
4765
+ if(choicePosition!==0){
4766
+ continue;
4767
+ }
4768
+ if(choice.finish_reason!==undefined){
4769
+ streamFinishReason=choice.finish_reason;
4313
4770
  }
4314
4771
 
4315
- try{
4316
- const resp=JSON.parse(chunkCache)||{};
4317
- //console.log(JSON.stringify(resp));
4318
- //console.log(resp)
4319
- const choice = resp.choices?.[0] || {};
4320
- const delta = choice.delta || {};
4321
- const content = delta.content || '';
4322
- const tool_calls=delta.tool_calls || [];
4323
- let value = content;
4324
-
4325
- let reasoning = '';
4326
-
4327
- if(seeThinking){
4328
- reasoning=delta.reasoning || '';
4329
- }
4330
-
4331
- if (reasoning) {
4332
- isThinking = true;
4333
- value = reasoning;
4334
- }
4335
-
4336
- if (!reasoning && isThinking) {
4337
- //remove thinking chunks
4338
- chunkString='';
4339
- }
4340
-
4341
- if (!reasoning) {
4342
- isThinking = false;
4343
- }
4344
-
4345
- chunkCache='';
4346
-
4347
- if(value==='' && !tool_calls.length){
4348
- return;
4349
- }
4350
-
4351
- if(value){
4352
- streamHandler(value,`M-${id}`, isThinking);
4353
- chunkString+=value;
4354
- }
4355
-
4356
- if(tool_calls.length){
4357
- receiveStreamedToolCalls(tool_calls);
4358
- }
4359
- } catch(err) {
4360
- console.warn('AI stream callback failed.');
4772
+ const content=choiceDelta.content||'';
4773
+ let value=content;
4774
+ let reasoning='';
4775
+ if(seeThinking){
4776
+ reasoning=choiceDelta.reasoning||'';
4777
+ }
4778
+ if(reasoning){
4779
+ isThinking=true;
4780
+ value=reasoning;
4781
+ }
4782
+ if(!reasoning&&isThinking){
4783
+ chunkString='';
4784
+ }
4785
+ if(!reasoning){
4786
+ isThinking=false;
4787
+ }
4788
+ if(value){
4789
+ await streamHandler(value,`M-${id}`,isThinking);
4790
+ chunkString+=value;
4361
4791
  }
4362
4792
  }
4363
- );
4793
+ }
4364
4794
  }
4365
4795
  }catch(error){
4366
4796
  if(isAIRequestAbort(error,signal)){
@@ -4371,30 +4801,108 @@ class AI {
4371
4801
  reader.releaseLock();
4372
4802
  }
4373
4803
 
4374
- const tool_funcs={};
4375
- const orderedToolCalls=[...streamedToolCalls.values()].sort(
4376
- function sortStreamedToolCalls(a,b){
4377
- return a.order-b.order;
4804
+ const structuralToolCallsByChoice=new Map();
4805
+ const orderedStreamChoices=[...streamedToolCallsByChoice.entries()].sort(
4806
+ function sortStreamChoices(a,b){
4807
+ return a[0]-b[0];
4378
4808
  }
4379
4809
  );
4380
-
4381
- for(const toolCall of orderedToolCalls){
4382
- if(!toolCall.name){
4383
- throw new Error('AI stream returned a tool call without a name.');
4810
+ for(const [choicePosition,toolCallRecords] of orderedStreamChoices){
4811
+ const orderedToolCalls=[...toolCallRecords.values()].sort(
4812
+ function sortStreamedToolCalls(a,b){
4813
+ return a.order-b.order;
4814
+ }
4815
+ );
4816
+ const rawToolCalls=orderedToolCalls.map(
4817
+ function completeStreamedToolCall(toolCall,index){
4818
+ if(
4819
+ toolCall.invalidArguments
4820
+ ||toolCall.invalidIdentity
4821
+ ||toolCall.invalidName
4822
+ ){
4823
+ throw aiStructuralError(
4824
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4825
+ `AI stream choice ${choicePosition+1} structural tool call ${index+1} changed an exact field.`
4826
+ );
4827
+ }
4828
+ return {
4829
+ id:toolCall.id,
4830
+ type:toolCall.type,
4831
+ function:{
4832
+ name:toolCall.name,
4833
+ arguments:toolCall.arguments
4834
+ }
4835
+ };
4836
+ }
4837
+ );
4838
+ structuralToolCallsByChoice.set(
4839
+ choicePosition,
4840
+ normalizeAIStreamToolCallObservation(
4841
+ {message:{tool_calls:rawToolCalls}},
4842
+ `AI stream choice ${choicePosition+1}`
4843
+ )
4844
+ );
4845
+ }
4846
+ const structuralToolCalls=structuralToolCallsByChoice.get(0)||[];
4847
+ const completionChoices=[
4848
+ {
4849
+ index:0,
4850
+ message:{
4851
+ role:'assistant',
4852
+ content:chunkString,
4853
+ ...(structuralToolCalls.length
4854
+ ?{tool_calls:structuralToolCalls}
4855
+ :{})
4856
+ },
4857
+ finish_reason:typeof streamFinishReason==='string'
4858
+ &&streamFinishReason
4859
+ ?streamFinishReason
4860
+ :structuralToolCalls.length?'tool_calls':'stop'
4384
4861
  }
4385
-
4386
- if(Object.hasOwn(tool_funcs,toolCall.name)){
4387
- throw new Error(`AI stream returned duplicate tool ${toolCall.name}.`);
4862
+ ];
4863
+ for(const [choicePosition,toolCalls] of structuralToolCallsByChoice){
4864
+ if(choicePosition===0){
4865
+ continue;
4388
4866
  }
4389
-
4390
- tool_funcs[toolCall.name]=toolCall.arguments;
4867
+ completionChoices.push({
4868
+ index:choicePosition,
4869
+ message:{
4870
+ role:'assistant',
4871
+ content:'',
4872
+ ...(toolCalls.length?{tool_calls:toolCalls}:{})
4873
+ },
4874
+ finish_reason:toolCalls.length?'tool_calls':'stop'
4875
+ });
4391
4876
  }
4392
-
4393
- const streamResult=Object.keys(tool_funcs).length
4394
- ?tool_funcs
4395
- :chunkString;
4396
- if(Object.keys(tool_funcs).length&&!chunkString){
4397
- streamHandler('',`M-${id}`,false);
4877
+ const completion={
4878
+ id:typeof streamId==='string'&&streamId?streamId:`legacy-${id}`,
4879
+ object:typeof streamObject==='string'&&streamObject
4880
+ ?streamObject
4881
+ :'chat.completion',
4882
+ created:Number.isSafeInteger(streamCreated)
4883
+ ?streamCreated
4884
+ :Math.floor(Date.now()/1000),
4885
+ model:typeof streamModel==='string'&&streamModel
4886
+ ?streamModel
4887
+ :this.model,
4888
+ choices:completionChoices,
4889
+ ...(isPlainAIRecord(streamUsage)?{usage:streamUsage}:{})
4890
+ };
4891
+ const terminalToolCalls=normalizeAICompletionToolCalls(completion);
4892
+ assertAIStreamToolCallCorrelation(
4893
+ structuralToolCalls,
4894
+ terminalToolCalls,
4895
+ 'The legacy HTTP stream'
4896
+ );
4897
+ for(const call of terminalToolCalls){
4898
+ if(signal?.aborted){
4899
+ throw normalizeAIRequestAbort();
4900
+ }
4901
+ await earlyFunctionTrigger(call,`M-${id}`);
4902
+ }
4903
+ const streamResult=this.#providerCompletionOutput(completion);
4904
+ if(structuralToolCalls.length&&!chunkString){
4905
+ await streamHandler('',`M-${id}`,false);
4398
4906
  }
4399
4907
  if(finishSpeech){
4400
4908
  this.finishTTS();
@@ -4403,7 +4911,7 @@ class AI {
4403
4911
 
4404
4912
  //sync
4405
4913
  speechTurnCompleted=true;
4406
- return streamResult;
4914
+ return returnCompletion?completion:streamResult;
4407
4915
  }catch(error){
4408
4916
  if(isAIRequestAbort(error,signal)){
4409
4917
  throw normalizeAIRequestAbort(error);
@@ -4422,12 +4930,23 @@ class AI {
4422
4930
  localOnly=false,
4423
4931
  tools=[],
4424
4932
  toolChoice='auto',
4425
- parallelToolCalls=true,
4933
+ parallelToolCalls=false,
4426
4934
  id=Date.now(),
4427
4935
  signal=null,
4428
4936
  onRequest=function ignoreFetchRequest(){},
4429
- onResponse=function ignoreFetchResponse(){}
4937
+ onResponse=function ignoreFetchResponse(){},
4938
+ maxOutputTokens,
4939
+ maxTokens,
4940
+ temperature,
4941
+ topK,
4942
+ topP,
4943
+ repeatPenalty,
4944
+ minP,
4945
+ seed,
4946
+ stop,
4947
+ templateOptions
4430
4948
  }={}){
4949
+ validateAIStructuralRequest(messages,tools,parallelToolCalls);
4431
4950
  if(localOnly!==true&&localOnly!==false){
4432
4951
  throw new TypeError('AI localOnly must be a boolean.');
4433
4952
  }
@@ -4448,7 +4967,18 @@ class AI {
4448
4967
  tools,
4449
4968
  toolChoice,
4450
4969
  parallelToolCalls,
4451
- id
4970
+ id,
4971
+ ...(maxOutputTokens!==undefined
4972
+ ?{maxTokens:maxOutputTokens}
4973
+ :maxTokens!==undefined?{maxTokens}:{}),
4974
+ ...(temperature!==undefined?{temperature}:{}),
4975
+ ...(topK!==undefined?{topK}:{}),
4976
+ ...(topP!==undefined?{topP}:{}),
4977
+ ...(repeatPenalty!==undefined?{repeatPenalty}:{}),
4978
+ ...(minP!==undefined?{minP}:{}),
4979
+ ...(seed!==undefined?{seed}:{}),
4980
+ ...(stop!==undefined?{stop}:{}),
4981
+ ...(templateOptions!==undefined?{templateOptions}:{})
4452
4982
  };
4453
4983
  await this.#reportRequest(onRequest,request,id);
4454
4984
  if(signal?.aborted){
@@ -4466,6 +4996,7 @@ class AI {
4466
4996
  if(signal?.aborted){
4467
4997
  throw normalizeAIRequestAbort();
4468
4998
  }
4999
+ normalizeAICompletionToolCalls(response);
4469
5000
  await onResponse(response,id,false);
4470
5001
  if(signal?.aborted){
4471
5002
  throw normalizeAIRequestAbort();
@@ -4492,7 +5023,7 @@ class AI {
4492
5023
  structuredOutput=false,
4493
5024
  tools=[],
4494
5025
  tool_choice='auto',
4495
- parallel_tool_calls=true,
5026
+ parallel_tool_calls=false,
4496
5027
  id=Date.now(),
4497
5028
  requestHandler=function ignoreFetchRequest(){},
4498
5029
  signal=null,
@@ -4531,11 +5062,12 @@ class AI {
4531
5062
  structuredOutput=false,
4532
5063
  tools=[],
4533
5064
  tool_choice='auto',
4534
- parallel_tool_calls=true,
5065
+ parallel_tool_calls=false,
4535
5066
  id=Date.now(),
4536
5067
  requestHandler=function ignoreFetchRequest(){},
4537
5068
  signal=null,
4538
5069
  ){
5070
+ validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4539
5071
  this.#assertServiceConfigured(this.llmService);
4540
5072
  if(signal&&(
4541
5073
  typeof signal.aborted!=='boolean'
@@ -4610,16 +5142,14 @@ class AI {
4610
5142
  if(signal?.aborted){
4611
5143
  throw normalizeAIRequestAbort();
4612
5144
  }
4613
- this.#assertRequiredOllamaToolCall(
4614
- Array.isArray(nativeResponse?.message?.tool_calls)
4615
- ?nativeResponse.message.tool_calls
4616
- :[],
4617
- tool_choice
4618
- );
4619
5145
  const responseJSON=this.#openAICompatibleOllamaResponse(
4620
5146
  nativeResponse,
4621
5147
  id
4622
5148
  );
5149
+ this.#assertRequiredOllamaToolCall(
5150
+ normalizeAICompletionToolCalls(responseJSON),
5151
+ tool_choice
5152
+ );
4623
5153
 
4624
5154
  if(signal?.aborted){
4625
5155
  throw normalizeAIRequestAbort();
@@ -4689,6 +5219,7 @@ class AI {
4689
5219
 
4690
5220
  //console.log(responseJSON);
4691
5221
  //async
5222
+ normalizeAICompletionToolCalls(responseJSON);
4692
5223
  await responseHandler(responseJSON,id,false);
4693
5224
  //sync
4694
5225
  return responseJSON;
@@ -4715,7 +5246,10 @@ class AI {
4715
5246
  try{
4716
5247
  this.#assertServiceConfigured(this.ttsService,'tts');
4717
5248
  }catch(error){
4718
- console.warn('AI speech provider is unavailable.');
5249
+ this.#publishTTSFailure(error,{
5250
+ boundary:'synthesis',
5251
+ generation:this.speechGeneration
5252
+ });
4719
5253
  return Promise.resolve(false);
4720
5254
  }
4721
5255
 
@@ -4739,20 +5273,15 @@ class AI {
4739
5273
 
4740
5274
  #extractSpeechSegments(end=false){
4741
5275
  const segments=[];
4742
- const maximumLength=220;
4743
5276
  let remainder=this.audioMessageChunks;
4744
5277
 
4745
- while(remainder.trim()){
5278
+ while(remainder.length>0){
4746
5279
  const terminator=this.#findSpeechTerminator(remainder,end);
4747
5280
  let boundary=terminator
4748
5281
  ?terminator.index+terminator[0].length
4749
5282
  :-1;
4750
5283
 
4751
- if(boundary<0&&remainder.length>=maximumLength){
4752
- const candidate=remainder.slice(0,maximumLength+1);
4753
- const whitespace=candidate.lastIndexOf(' ');
4754
- boundary=whitespace>=80?whitespace+1:maximumLength;
4755
- }else if(boundary<0&&end){
5284
+ if(boundary<0&&end){
4756
5285
  boundary=remainder.length;
4757
5286
  }
4758
5287
 
@@ -4760,8 +5289,8 @@ class AI {
4760
5289
  break;
4761
5290
  }
4762
5291
 
4763
- const segment=remainder.slice(0,boundary).trim();
4764
- remainder=remainder.slice(boundary).trimStart();
5292
+ const segment=remainder.slice(0,boundary);
5293
+ remainder=remainder.slice(boundary);
4765
5294
 
4766
5295
  if(segment){
4767
5296
  segments.push(segment);
@@ -4827,7 +5356,11 @@ class AI {
4827
5356
  }
4828
5357
  ).catch(
4829
5358
  function discardFailedSpeechJob(error){
4830
- return runtime.#failSpeechJob(job,error);
5359
+ return runtime.#failSpeechJob(
5360
+ job,
5361
+ error,
5362
+ job.state==='decoding'?'decode':'synthesis'
5363
+ );
4831
5364
  }
4832
5365
  );
4833
5366
 
@@ -4852,6 +5385,7 @@ class AI {
4852
5385
  return this.#cancelSpeechJob(job);
4853
5386
  }
4854
5387
 
5388
+ job.state='decoding';
4855
5389
  const audioContext=this.#getSpeechAudioContext();
4856
5390
  return this.playAudio(
4857
5391
  audio.chunks,
@@ -5332,6 +5866,12 @@ class AI {
5332
5866
  this.speechResumePending=false;
5333
5867
  }
5334
5868
  this.#waitForSpeechGesture(error);
5869
+ if(error?.name!=='NotAllowedError'){
5870
+ this.#publishTTSFailure(error,{
5871
+ boundary:'playback-resume',
5872
+ generation:this.speechGeneration
5873
+ });
5874
+ }
5335
5875
  return false;
5336
5876
  }
5337
5877
 
@@ -5341,7 +5881,7 @@ class AI {
5341
5881
 
5342
5882
  async playAudio(
5343
5883
  audioChunks=[],
5344
- audioContext=this.#getSpeechAudioContext(),
5884
+ audioContext=null,
5345
5885
  sourceNode=null,
5346
5886
  audioType=this.audioType,
5347
5887
  speechJob=null
@@ -5364,19 +5904,20 @@ class AI {
5364
5904
 
5365
5905
  try{
5366
5906
  job.state='decoding';
5907
+ const playbackContext=audioContext||this.#getSpeechAudioContext();
5367
5908
  const audioBlob=new Blob(audioChunks,{type:audioType});
5368
5909
  const arrayBuffer=await audioBlob.arrayBuffer();
5369
- const audioBuffer=await audioContext.decodeAudioData(arrayBuffer);
5910
+ const audioBuffer=await playbackContext.decodeAudioData(arrayBuffer);
5370
5911
 
5371
5912
  if(this.muted||job.generation!==this.speechGeneration){
5372
5913
  return this.#cancelSpeechJob(job);
5373
5914
  }
5374
5915
 
5375
- const preparedSource=sourceNode||audioContext.createBufferSource();
5916
+ const preparedSource=sourceNode||playbackContext.createBufferSource();
5376
5917
  const runtime=this;
5377
5918
 
5378
5919
  preparedSource.buffer=audioBuffer;
5379
- preparedSource.connect(audioContext.destination);
5920
+ preparedSource.connect(playbackContext.destination);
5380
5921
  preparedSource.__arcaneStarted=false;
5381
5922
  preparedSource.onended=function finishQueuedSpeechSource(){
5382
5923
  runtime.nextSentance(job);
@@ -5387,14 +5928,20 @@ class AI {
5387
5928
  this.#requestSpeechPlayback();
5388
5929
  return true;
5389
5930
  }catch(error){
5390
- return this.#failSpeechJob(job,error);
5931
+ return this.#failSpeechJob(job,error,'decode');
5391
5932
  }
5392
5933
  }
5393
5934
 
5394
5935
  #requestSpeechPlayback(){
5936
+ const runtime=this;
5395
5937
  this.#pumpSpeechPlayback().catch(
5396
5938
  function reportSpeechPlaybackFailure(error){
5397
- console.warn('AI audio playback failed.');
5939
+ const job=runtime.speechJobs[0];
5940
+ if(job){
5941
+ runtime.#failSpeechJob(job,error,'playback-start');
5942
+ return;
5943
+ }
5944
+ console.error('AI audio playback failed without an active speech job.',error);
5398
5945
  }
5399
5946
  );
5400
5947
  }
@@ -5405,10 +5952,12 @@ class AI {
5405
5952
  }
5406
5953
 
5407
5954
  this.speechPlaybackStarting=true;
5955
+ let activeJob=null;
5408
5956
 
5409
5957
  try{
5410
5958
  while(!this.isSpeaking&&!this.muted){
5411
5959
  const job=this.speechJobs[0];
5960
+ activeJob=job||null;
5412
5961
 
5413
5962
  if(!job){
5414
5963
  return false;
@@ -5449,14 +5998,21 @@ class AI {
5449
5998
  job.sourceNode.__arcaneStarted=true;
5450
5999
  this.currentSpeechJob=job;
5451
6000
  this.isSpeaking=true;
5452
- job.sourceNode.start(0);
6001
+ await job.sourceNode.start(0);
5453
6002
  return true;
5454
6003
  }catch(error){
5455
6004
  this.currentSpeechJob=null;
5456
6005
  this.isSpeaking=false;
5457
- this.#failSpeechJob(job,error);
6006
+ this.#failSpeechJob(job,error,'playback-start');
5458
6007
  }
5459
6008
  }
6009
+ }catch(error){
6010
+ if(activeJob){
6011
+ this.#failSpeechJob(activeJob,error,'playback-start');
6012
+ }else if(!this.muted&&!isAIRequestAbort(error)){
6013
+ console.error('AI audio playback failed without an active speech job.',error);
6014
+ }
6015
+ return false;
5460
6016
  }finally{
5461
6017
  this.speechPlaybackStarting=false;
5462
6018
 
@@ -5502,13 +6058,76 @@ class AI {
5502
6058
  return false;
5503
6059
  }
5504
6060
 
5505
- #failSpeechJob(job,error){
6061
+ #publishTTSFailure(error,{
6062
+ boundary='synthesis',
6063
+ generation=this.speechGeneration
6064
+ }={}){
6065
+ if(
6066
+ generation!==this.speechGeneration
6067
+ ||this.muted
6068
+ ||isAIRequestAbort(error)
6069
+ ){
6070
+ return false;
6071
+ }
6072
+
6073
+ const boundaries=new Set([
6074
+ 'synthesis',
6075
+ 'decode',
6076
+ 'playback-start',
6077
+ 'playback-resume'
6078
+ ]);
6079
+ const normalizedBoundary=boundaries.has(boundary)
6080
+ ?boundary
6081
+ :'synthesis';
6082
+ const reason=`tts-${normalizedBoundary}-rejected`;
6083
+ const operationId=
6084
+ `${this.#events.instanceId}:tts-failure:${(++this.#speechFailureSequence).toString(36)}`;
6085
+
6086
+ console.error(`AI speech ${normalizedBoundary} failed.`,error);
6087
+
6088
+ try{
6089
+ const {occurrence}=this.#events.dispatch(
6090
+ AI_TTS_FAILURE_EVENT,
6091
+ completeValue({
6092
+ ai:this,
6093
+ boundary:normalizedBoundary,
6094
+ error,
6095
+ generation,
6096
+ reason
6097
+ }),
6098
+ {
6099
+ operationId,
6100
+ publicDetail:completeValue({
6101
+ boundary:normalizedBoundary,
6102
+ ...(typeof error?.code==='string'?{code:error.code}:{}),
6103
+ generation,
6104
+ reason
6105
+ })
6106
+ }
6107
+ );
6108
+ projectArcaneDOMEvent(window,occurrence);
6109
+ }catch(reportingError){
6110
+ console.error(
6111
+ 'AI speech failure could not be published to the runtime event boundary.',
6112
+ reportingError
6113
+ );
6114
+ }
6115
+
6116
+ return true;
6117
+ }
6118
+
6119
+ #failSpeechJob(job,error,boundary='synthesis'){
5506
6120
  if(job.state==='failed'||job.state==='cancelled'){
5507
6121
  return false;
5508
6122
  }
5509
6123
 
5510
6124
  job.state='failed';
5511
6125
 
6126
+ this.#publishTTSFailure(error,{
6127
+ boundary,
6128
+ generation:job.generation
6129
+ });
6130
+
5512
6131
  if(job.sourceNode){
5513
6132
  job.sourceNode.onended=null;
5514
6133
  }
@@ -5520,10 +6139,6 @@ class AI {
5520
6139
  this.isSpeaking=false;
5521
6140
  }
5522
6141
 
5523
- if(job.generation===this.speechGeneration&&error?.name!=='AbortError'){
5524
- console.warn('AI speech synthesis failed.');
5525
- }
5526
-
5527
6142
  this.#requestSpeechPlayback();
5528
6143
  return false;
5529
6144
  }
@@ -5635,7 +6250,7 @@ function installAIUserReadyRegistration(){
5635
6250
  delete globalThis[AI_USER_READY_REGISTRATION_KEY];
5636
6251
  }
5637
6252
  }
5638
- registration=Object.freeze({
6253
+ registration=completeValue({
5639
6254
  protocol:AI_USER_READY_REGISTRATION_PROTOCOL,
5640
6255
  dispose:disposeAIUserReadyRegistration
5641
6256
  });