arcane-os 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -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
 
@@ -2089,6 +2364,14 @@ class AI {
2089
2364
  );
2090
2365
  if(!record
2091
2366
  ||!this.#providerRuntime.ownsProvider(role,record.provider)){
2367
+ const pendingIdentity=this.#providerRuntime.providerIdentity(
2368
+ role,
2369
+ selection.providerId
2370
+ );
2371
+ if(pendingIdentity===null&&selection.localOnly===null){
2372
+ expectedProviders[role]=null;
2373
+ continue;
2374
+ }
2092
2375
  throw this.#browserSpeechProviderRouteOwnershipError(
2093
2376
  `The selected ${role} route is not owned by the replaceable AI legacy speech boundary.`
2094
2377
  );
@@ -2096,9 +2379,9 @@ class AI {
2096
2379
  expectedProviders[role]=record.provider;
2097
2380
  legacyRecords.push(record);
2098
2381
  }
2099
- return Object.freeze({
2100
- expectedProviders:Object.freeze(expectedProviders),
2101
- legacyRecords:Object.freeze(legacyRecords)
2382
+ return completeValue({
2383
+ expectedProviders:completeValue(expectedProviders),
2384
+ legacyRecords:completeValue(legacyRecords)
2102
2385
  });
2103
2386
  }
2104
2387
 
@@ -2167,7 +2450,7 @@ class AI {
2167
2450
  }
2168
2451
 
2169
2452
  #publishBrowserSpeechEvent(type,normalized,operationId,reason,{descriptor=null,error=null}={}){
2170
- const compatibilityDetail=Object.freeze({
2453
+ const compatibilityDetail=completeValue({
2171
2454
  configuration:normalized.configuration,
2172
2455
  configurationId:normalized.id,
2173
2456
  ...(descriptor?{descriptor}:{}),
@@ -2179,7 +2462,7 @@ class AI {
2179
2462
  compatibilityDetail,
2180
2463
  {
2181
2464
  operationId,
2182
- publicDetail:Object.freeze({
2465
+ publicDetail:completeValue({
2183
2466
  configurationId:normalized.id,
2184
2467
  ...(descriptor?{descriptor}:{}),
2185
2468
  ...(typeof error?.code==='string'?{code:error.code}:{}),
@@ -2191,12 +2474,12 @@ class AI {
2191
2474
 
2192
2475
  #browserSpeechRoutes(normalized,providers,previousRecord){
2193
2476
  function roleRoutes(provider,catalog){
2194
- const selection=Object.freeze({
2477
+ const selection=completeValue({
2195
2478
  providerId:provider.id,
2196
2479
  modelId:catalog.id,
2197
2480
  localOnly:true
2198
2481
  });
2199
- return Object.freeze({default:selection,localOnly:selection});
2482
+ return completeValue({default:selection,localOnly:selection});
2200
2483
  }
2201
2484
  const currentRoutes=this.#currentSpeechRoutes();
2202
2485
  const routes={};
@@ -2220,15 +2503,15 @@ class AI {
2220
2503
  catalogs[role]=catalog[0];
2221
2504
  routes[role]=roleRoutes(providers[role],catalog[0]);
2222
2505
  }
2223
- return Object.freeze({
2224
- routes:Object.freeze(routes),
2225
- catalogs:Object.freeze(catalogs)
2506
+ return completeValue({
2507
+ routes:completeValue(routes),
2508
+ catalogs:completeValue(catalogs)
2226
2509
  });
2227
2510
  }
2228
2511
 
2229
2512
  #browserSpeechDescriptor(normalized,catalogs,previousRecord){
2230
2513
  function roleDescriptor(role,configured,catalog){
2231
- return Object.freeze({
2514
+ return completeValue({
2232
2515
  role,
2233
2516
  providerId:configured.providerId,
2234
2517
  modelId:catalog.id,
@@ -2245,7 +2528,7 @@ class AI {
2245
2528
  ?roleDescriptor(role,normalized[role],catalogs[role])
2246
2529
  :previousRecord?.descriptor[role]??null;
2247
2530
  }
2248
- return Object.freeze({
2531
+ return completeValue({
2249
2532
  protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
2250
2533
  configurationId:normalized.id,
2251
2534
  ...roles
@@ -2267,7 +2550,7 @@ class AI {
2267
2550
  role=>!normalized.roles.includes(role)
2268
2551
  );
2269
2552
  if(carriedRoles.length===0)return normalized.configuration;
2270
- return Object.freeze({
2553
+ return completeValue({
2271
2554
  protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
2272
2555
  id:normalized.id,
2273
2556
  dbopfs:normalized.dbopfs,
@@ -2286,12 +2569,12 @@ class AI {
2286
2569
  }
2287
2570
 
2288
2571
  #currentSpeechRoutes(){
2289
- return Object.freeze({
2290
- stt:Object.freeze({
2572
+ return completeValue({
2573
+ stt:completeValue({
2291
2574
  default:this.#providerRuntime.selection('stt'),
2292
2575
  localOnly:this.#providerRuntime.selection('stt',{localOnly:true})
2293
2576
  }),
2294
- tts:Object.freeze({
2577
+ tts:completeValue({
2295
2578
  default:this.#providerRuntime.selection('tts'),
2296
2579
  localOnly:this.#providerRuntime.selection('tts',{localOnly:true})
2297
2580
  })
@@ -2299,9 +2582,9 @@ class AI {
2299
2582
  }
2300
2583
 
2301
2584
  #emptySpeechRoutes(){
2302
- return Object.freeze({
2303
- stt:Object.freeze({default:null,localOnly:null}),
2304
- tts:Object.freeze({default:null,localOnly:null})
2585
+ return completeValue({
2586
+ stt:completeValue({default:null,localOnly:null}),
2587
+ tts:completeValue({default:null,localOnly:null})
2305
2588
  });
2306
2589
  }
2307
2590
 
@@ -2346,7 +2629,7 @@ class AI {
2346
2629
  previousRecord
2347
2630
  ){
2348
2631
  const configurationByRole={};
2349
- const managedRoles=Object.freeze(['stt','tts'].filter(role=>
2632
+ const managedRoles=completeValue(['stt','tts'].filter(role=>
2350
2633
  normalized.roles.includes(role)
2351
2634
  ||previousRecord?.managedRoles.includes(role)
2352
2635
  ));
@@ -2357,7 +2640,7 @@ class AI {
2357
2640
  }
2358
2641
  return {
2359
2642
  configuration,
2360
- configurationByRole:Object.freeze(configurationByRole),
2643
+ configurationByRole:completeValue(configurationByRole),
2361
2644
  dbopfs:normalized.dbopfs,
2362
2645
  tableName:normalized.tableName,
2363
2646
  descriptor,
@@ -2371,15 +2654,13 @@ class AI {
2371
2654
  };
2372
2655
  }
2373
2656
 
2374
- #freezeBrowserSpeechRecord(record){
2375
- record.unregisters=Object.freeze({...record.unregisters});
2376
- Object.seal(record.registrationState);
2377
- Object.seal(record.retirementState);
2378
- return Object.freeze(record);
2657
+ #finalizeBrowserSpeechRecord(record){
2658
+ record.unregisters=completeValue({...record.unregisters});
2659
+ return completeValue(record);
2379
2660
  }
2380
2661
 
2381
2662
  #browserSpeechRecordFromReplacement(record,replacement){
2382
- return this.#freezeBrowserSpeechRecord({
2663
+ return this.#finalizeBrowserSpeechRecord({
2383
2664
  configuration:record.configuration,
2384
2665
  configurationByRole:record.configurationByRole,
2385
2666
  dbopfs:record.dbopfs,
@@ -2601,22 +2882,16 @@ class AI {
2601
2882
  candidateProviders[role]=factory({
2602
2883
  id:configured.providerId,
2603
2884
  ...(configured.graph
2604
- ?{
2605
- graph:configured.graph,
2606
- security:configured.security
2607
- }
2885
+ ?{graph:configured.graph}
2608
2886
  :{
2609
2887
  model:configured.model,
2610
- runtime:configured.runtime,
2611
- ...(Object.hasOwn(configured,'security')
2612
- ?{security:configured.security}
2613
- :{})
2888
+ runtime:configured.runtime
2614
2889
  }),
2615
2890
  store,
2616
2891
  offline:configured.offline
2617
2892
  });
2618
2893
  }
2619
- providers=Object.freeze({...candidateProviders});
2894
+ providers=completeValue({...candidateProviders});
2620
2895
  prepared=this.#browserSpeechRoutes(
2621
2896
  normalized,
2622
2897
  providers,
@@ -2638,7 +2913,7 @@ class AI {
2638
2913
  const provider=candidateProviders[role];
2639
2914
  const changed=normalized.roles.includes(role);
2640
2915
  const selection=changed&&provider
2641
- ?Object.freeze({
2916
+ ?completeValue({
2642
2917
  providerId:provider.id,
2643
2918
  modelId:normalized[role].graph?.model.id
2644
2919
  ??normalized[role].model.id,
@@ -2646,7 +2921,7 @@ class AI {
2646
2921
  })
2647
2922
  :null;
2648
2923
  partialRoutes[role]=changed
2649
- ?Object.freeze({default:selection,localOnly:selection})
2924
+ ?completeValue({default:selection,localOnly:selection})
2650
2925
  :previousRecord?.managedRoles.includes(role)
2651
2926
  ?previousRecord.routes[role]
2652
2927
  :currentRoutes[role];
@@ -2655,8 +2930,8 @@ class AI {
2655
2930
  normalized,
2656
2931
  configuration,
2657
2932
  null,
2658
- Object.freeze({...candidateProviders}),
2659
- Object.freeze(partialRoutes),
2933
+ completeValue({...candidateProviders}),
2934
+ completeValue(partialRoutes),
2660
2935
  previousRecord
2661
2936
  );
2662
2937
  return this.#throwAfterBrowserSpeechCandidateCleanup(
@@ -2720,9 +2995,9 @@ class AI {
2720
2995
  expectedProvider:replacementBoundary.expectedProviders[role]
2721
2996
  }
2722
2997
  );
2723
- replacement=Object.freeze({
2998
+ replacement=completeValue({
2724
2999
  routes:prepared.routes,
2725
- unregisters:Object.freeze({
3000
+ unregisters:completeValue({
2726
3001
  stt:role==='stt'
2727
3002
  ?roleReplacement.unregister
2728
3003
  :previousRecord?.unregisters.stt??null,
@@ -2796,9 +3071,9 @@ class AI {
2796
3071
  expectedProvider:record.providers[role]
2797
3072
  }
2798
3073
  );
2799
- rollback=Object.freeze({
3074
+ rollback=completeValue({
2800
3075
  routes:previousRecord.routes,
2801
- unregisters:Object.freeze({
3076
+ unregisters:completeValue({
2802
3077
  stt:role==='stt'
2803
3078
  ?roleRollback.unregister
2804
3079
  :previousRecord.unregisters.stt,
@@ -2970,7 +3245,7 @@ class AI {
2970
3245
  operation.signal?.removeEventListener('abort',forwardAbort);
2971
3246
  if(runtime.#browserSpeechController===controller){
2972
3247
  runtime.#browserSpeechController=null;
2973
- runtime.#browserSpeechControllerRoles=Object.freeze([]);
3248
+ runtime.#browserSpeechControllerRoles=completeValue([]);
2974
3249
  }
2975
3250
  }
2976
3251
  }
@@ -3020,7 +3295,7 @@ class AI {
3020
3295
  if(invalidatesSpeech)this.#invalidateSpeechControl();
3021
3296
  const controller=new AbortController();
3022
3297
  this.#browserSpeechController=controller;
3023
- this.#browserSpeechControllerRoles=Object.freeze([]);
3298
+ this.#browserSpeechControllerRoles=completeValue([]);
3024
3299
  const forwardAbort=function cancelBrowserSpeechDisposal(){
3025
3300
  if(!controller.signal.aborted)controller.abort(operation.signal.reason);
3026
3301
  };
@@ -3071,7 +3346,7 @@ class AI {
3071
3346
  }else{
3072
3347
  const role=activeRecord.managedRoles[0];
3073
3348
  const currentRoutes=runtime.#currentSpeechRoutes();
3074
- const emptyRoleRoutes=Object.freeze({
3349
+ const emptyRoleRoutes=completeValue({
3075
3350
  default:null,
3076
3351
  localOnly:null
3077
3352
  });
@@ -3083,8 +3358,8 @@ class AI {
3083
3358
  expectedProvider:activeRecord.providers[role]
3084
3359
  }
3085
3360
  );
3086
- removed=Object.freeze({
3087
- routes:Object.freeze({
3361
+ removed=completeValue({
3362
+ routes:completeValue({
3088
3363
  stt:role==='stt'
3089
3364
  ?emptyRoleRoutes
3090
3365
  :currentRoutes.stt,
@@ -3174,7 +3449,7 @@ class AI {
3174
3449
  operation.signal?.removeEventListener('abort',forwardAbort);
3175
3450
  if(runtime.#browserSpeechController===controller){
3176
3451
  runtime.#browserSpeechController=null;
3177
- runtime.#browserSpeechControllerRoles=Object.freeze([]);
3452
+ runtime.#browserSpeechControllerRoles=completeValue([]);
3178
3453
  }
3179
3454
  }
3180
3455
  }
@@ -3254,7 +3529,7 @@ class AI {
3254
3529
  const errorText=await response.text();
3255
3530
 
3256
3531
  if(errorText&&!errorText.trim().startsWith('<')){
3257
- detail=errorText.trim().slice(0,500);
3532
+ detail=errorText;
3258
3533
  }
3259
3534
  }
3260
3535
  }catch{
@@ -3463,8 +3738,8 @@ class AI {
3463
3738
  #arrayBufferToBase64(arrayBuffer){
3464
3739
  const bytes=new Uint8Array(arrayBuffer);
3465
3740
 
3466
- if(!bytes.length||bytes.length>6*1024*1024){
3467
- 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.');
3468
3743
  }
3469
3744
 
3470
3745
  const chunks=[];
@@ -3477,7 +3752,7 @@ class AI {
3477
3752
  }
3478
3753
 
3479
3754
  #base64ToBytes(value){
3480
- if(typeof value!=='string'||!value||value.length>8*1024*1024){
3755
+ if(typeof value!=='string'||!value){
3481
3756
  throw new TypeError('Arcane returned invalid local speech audio.');
3482
3757
  }
3483
3758
 
@@ -3524,8 +3799,13 @@ class AI {
3524
3799
  if(typeof argumentValue==='string'){
3525
3800
  try{
3526
3801
  argumentValue=JSON.parse(argumentValue);
3527
- }catch{
3528
- 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;
3529
3809
  }
3530
3810
  }
3531
3811
  if(
@@ -3533,7 +3813,11 @@ class AI {
3533
3813
  ||typeof argumentValue!=='object'
3534
3814
  ||Array.isArray(argumentValue)
3535
3815
  ){
3536
- 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;
3537
3821
  }
3538
3822
  if(callId&&name){
3539
3823
  toolNamesByCallId.set(callId,name);
@@ -3569,7 +3853,7 @@ class AI {
3569
3853
  });
3570
3854
  const requiredName=toolChoice?.function?.name;
3571
3855
  const instruction=requiredName
3572
- ?`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.`
3573
3857
  :toolChoice==='none'
3574
3858
  ?'Do not call a function in this request. Follow the response instructions and answer in the requested format.'
3575
3859
  :'';
@@ -3637,16 +3921,15 @@ class AI {
3637
3921
  return null;
3638
3922
  }
3639
3923
 
3640
- async #reportRequest(requestHandler,request,id){
3924
+ async #reportRequest(requestHandler,request,id,metadata){
3641
3925
  if(typeof requestHandler!=='function'){
3642
3926
  throw new TypeError('AI onRequest callback must be a function.');
3643
3927
  }
3644
- await requestHandler(request,id);
3928
+ await requestHandler(request,id,metadata);
3645
3929
  }
3646
3930
 
3647
3931
  #providerStreamEmissions(chunk,seeThinking){
3648
3932
  const chunks=[];
3649
- const toolNames=[];
3650
3933
  const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
3651
3934
  for(const choice of choices){
3652
3935
  const delta=choice?.delta||{};
@@ -3656,12 +3939,6 @@ class AI {
3656
3939
  if(typeof delta.content==='string'){
3657
3940
  chunks.push({text:delta.content,thinking:false});
3658
3941
  }
3659
- for(const call of Array.isArray(delta.tool_calls)?delta.tool_calls:[]){
3660
- const name=call?.function?.name;
3661
- if(typeof name==='string'&&name){
3662
- toolNames.push(name);
3663
- }
3664
- }
3665
3942
  }
3666
3943
  if(!choices.length){
3667
3944
  if(seeThinking&&typeof chunk?.thinking==='string'){
@@ -3675,19 +3952,8 @@ class AI {
3675
3952
  if(text){
3676
3953
  chunks.push({text,thinking:false});
3677
3954
  }
3678
- const calls=Array.isArray(chunk?.toolCalls)
3679
- ?chunk.toolCalls
3680
- :Array.isArray(chunk?.tool_calls)
3681
- ?chunk.tool_calls
3682
- :[];
3683
- for(const call of calls){
3684
- const name=call?.function?.name||call?.name;
3685
- if(typeof name==='string'&&name){
3686
- toolNames.push(name);
3687
- }
3688
- }
3689
3955
  }
3690
- return {chunks,toolNames};
3956
+ return {chunks};
3691
3957
  }
3692
3958
 
3693
3959
  #providerCompletionOutput(completion){
@@ -3721,7 +3987,7 @@ class AI {
3721
3987
  payload.structuredOutput??false,
3722
3988
  payload.tools??[],
3723
3989
  payload.toolChoice??'auto',
3724
- payload.parallelToolCalls??true,
3990
+ payload.parallelToolCalls??false,
3725
3991
  payload.id??Date.now(),
3726
3992
  function ignoreLegacyLLMProviderRequest(){},
3727
3993
  signal
@@ -3740,26 +4006,21 @@ class AI {
3740
4006
  );
3741
4007
  }
3742
4008
 
3743
- function emitLegacyLLMStreamTool(name){
3744
- if(typeof name==='string'&&name){
3745
- bridge.emit({toolCalls:[{name}]});
3746
- }
3747
- }
3748
-
3749
4009
  return this.#streamLegacyMessage(
3750
4010
  payload.messages??[],
3751
4011
  emitLegacyLLMStreamText,
3752
4012
  function ignoreLegacyLLMProviderCompletion(){},
3753
4013
  payload.tools??[],
3754
4014
  payload.toolChoice??'auto',
3755
- emitLegacyLLMStreamTool,
3756
- payload.parallelToolCalls??true,
4015
+ function retainLegacyLLMStreamToolUntilCompletion(){},
4016
+ payload.parallelToolCalls??false,
3757
4017
  payload.id??Date.now(),
3758
4018
  payload.seeThinking??false,
3759
4019
  bridge.signal,
3760
4020
  function ignoreLegacyLLMProviderRequest(){},
3761
4021
  payload.structuredOutput??false,
3762
- false
4022
+ false,
4023
+ true
3763
4024
  );
3764
4025
  }
3765
4026
 
@@ -3781,27 +4042,78 @@ class AI {
3781
4042
  }
3782
4043
  }
3783
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
+
3784
4102
  #openAICompatibleOllamaResponse(response={},id=Date.now()){
3785
4103
  const message=response?.message||{};
3786
- const toolCalls=Array.isArray(message.tool_calls)
3787
- ?message.tool_calls.map(
3788
- function normalizeOllamaToolCall(call,index){
3789
- return {
3790
- id:call?.id||`call-${id}-${index}`,
3791
- type:'function',
3792
- function:{
3793
- name:call?.function?.name||'',
3794
- arguments:typeof call?.function?.arguments==='string'
3795
- ?call.function.arguments
3796
- :JSON.stringify(call?.function?.arguments||{})
3797
- }
3798
- };
3799
- }
3800
- )
3801
- :[];
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
+ });
3802
4114
 
3803
4115
  return {
3804
- id:response?.id||`ollama-${id}`,
4116
+ id:responseId,
3805
4117
  object:'chat.completion',
3806
4118
  created:Math.floor(Date.now()/1000),
3807
4119
  model:response?.model||this.model,
@@ -3833,15 +4145,27 @@ class AI {
3833
4145
  localOnly=false,
3834
4146
  onChunk=function ignoreStreamChunk(){},
3835
4147
  onComplete=function finishIgnoredStream(){},
4148
+ onResponse=function ignoreStreamResponse(){},
3836
4149
  tools=[],
3837
4150
  toolChoice='auto',
3838
4151
  onToolCall=function ignoreEarlyFunction(){},
3839
4152
  onRequest=function ignoreStreamRequest(){},
3840
- parallelToolCalls=true,
4153
+ parallelToolCalls=false,
3841
4154
  id=Date.now(),
3842
4155
  seeThinking=false,
3843
- 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
3844
4167
  }={}){
4168
+ validateAIStructuralRequest(messages,tools,parallelToolCalls);
3845
4169
  if(localOnly!==true&&localOnly!==false){
3846
4170
  throw new TypeError('AI localOnly must be a boolean.');
3847
4171
  }
@@ -3860,10 +4184,20 @@ class AI {
3860
4184
  toolChoice,
3861
4185
  parallelToolCalls,
3862
4186
  id,
3863
- 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}:{})
3864
4199
  };
3865
4200
  const displayId=`M-${id}`;
3866
- const announcedTools=new Set();
3867
4201
  let handle=null;
3868
4202
  try{
3869
4203
  if(signal?.aborted){
@@ -3900,40 +4234,22 @@ class AI {
3900
4234
  throw normalizeAIRequestAbort();
3901
4235
  }
3902
4236
  }
3903
- for(const name of emissions.toolNames){
3904
- if(signal?.aborted){
3905
- throw normalizeAIRequestAbort();
3906
- }
3907
- if(!announcedTools.has(name)){
3908
- announcedTools.add(name);
3909
- await onToolCall(name);
3910
- if(signal?.aborted){
3911
- throw normalizeAIRequestAbort();
3912
- }
3913
- }
3914
- }
3915
4237
  }
3916
4238
  const completion=await handle.result;
3917
4239
  if(signal?.aborted){
3918
4240
  throw normalizeAIRequestAbort();
3919
4241
  }
3920
- for(const choice of Array.isArray(completion?.choices)
3921
- ?completion.choices
3922
- :[]){
3923
- for(const call of Array.isArray(choice?.message?.tool_calls)
3924
- ?choice.message.tool_calls
3925
- :[]){
3926
- const name=call?.function?.name;
3927
- if(typeof name==='string'&&name&&!announcedTools.has(name)){
3928
- if(signal?.aborted){
3929
- throw normalizeAIRequestAbort();
3930
- }
3931
- announcedTools.add(name);
3932
- await onToolCall(name);
3933
- if(signal?.aborted){
3934
- throw normalizeAIRequestAbort();
3935
- }
3936
- }
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();
3937
4253
  }
3938
4254
  }
3939
4255
  const result=this.#providerCompletionOutput(completion);
@@ -3956,20 +4272,52 @@ class AI {
3956
4272
  }
3957
4273
  }
3958
4274
 
3959
- return this.streamMessage(
3960
- messages,
3961
- onChunk,
3962
- onComplete,
3963
- tools,
3964
- toolChoice,
3965
- onToolCall,
3966
- parallelToolCalls,
3967
- id,
3968
- seeThinking,
3969
- signal,
3970
- onRequest,
3971
- structuredOutput
3972
- );
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
+ }
3973
4321
  }
3974
4322
 
3975
4323
  async streamMessage(
@@ -3979,12 +4327,13 @@ class AI {
3979
4327
  tools=[],
3980
4328
  tool_choice='auto',
3981
4329
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
3982
- parallel_tool_calls=true,
4330
+ parallel_tool_calls=false,
3983
4331
  id=Date.now(),
3984
4332
  seeThinking=false,
3985
4333
  signal=null,
3986
4334
  requestHandler=function ignoreStreamRequest(){},
3987
- structuredOutput=false
4335
+ structuredOutput=false,
4336
+ returnCompletion=false
3988
4337
  ){
3989
4338
  if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
3990
4339
  return this.streamRequest({
@@ -4016,7 +4365,9 @@ class AI {
4016
4365
  seeThinking,
4017
4366
  signal,
4018
4367
  requestHandler,
4019
- structuredOutput
4368
+ structuredOutput,
4369
+ true,
4370
+ returnCompletion
4020
4371
  );
4021
4372
  }
4022
4373
 
@@ -4027,17 +4378,19 @@ class AI {
4027
4378
  tools=[],
4028
4379
  tool_choice='auto',
4029
4380
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
4030
- parallel_tool_calls=true,
4381
+ parallel_tool_calls=false,
4031
4382
  id=Date.now(),
4032
4383
  seeThinking=false,
4033
4384
  signal=null,
4034
4385
  requestHandler=function ignoreStreamRequest(){},
4035
4386
  structuredOutput=false,
4036
- finishSpeech=true
4387
+ finishSpeech=true,
4388
+ returnCompletion=false
4037
4389
  ){
4038
4390
  let speechTurnCompleted=false;
4039
4391
 
4040
4392
  try{
4393
+ validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4041
4394
  this.#assertServiceConfigured(this.llmService);
4042
4395
  if(signal&&(
4043
4396
  typeof signal.aborted!=='boolean'
@@ -4077,7 +4430,7 @@ class AI {
4077
4430
  let isThinking=true;
4078
4431
  let isWaiting=true;
4079
4432
 
4080
- streamHandler('Thinking...',`M-${id}`,isThinking);
4433
+ await streamHandler('Thinking...',`M-${id}`,isThinking);
4081
4434
 
4082
4435
  const nativeOllama=this.#nativeOllama();
4083
4436
 
@@ -4090,8 +4443,7 @@ class AI {
4090
4443
 
4091
4444
  if(nativeOllama){
4092
4445
  let nativeContent='';
4093
- const nativeToolCalls={};
4094
- const triggeredTools=new Set();
4446
+ const nativeStreamedToolCallFrames=[];
4095
4447
  const ollamaTools=this.#ollamaTools(tools,tool_choice);
4096
4448
  const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
4097
4449
  const ollamaRequest={
@@ -4103,46 +4455,19 @@ class AI {
4103
4455
  ...(ollamaTools.length?{tools:ollamaTools}:{})
4104
4456
  };
4105
4457
 
4106
- function reportEarlyFunctionFailure(error){
4107
- console.error('Early tool trigger failed.');
4108
- }
4109
-
4110
4458
  function receiveNativeToolCalls(message={}){
4111
4459
  const calls=Array.isArray(message.tool_calls)?message.tool_calls:[];
4112
4460
 
4113
4461
  if(calls.length){
4114
4462
  isThinking=false;
4115
- }
4116
-
4117
- for(const call of calls){
4118
- const name=call?.function?.name;
4119
-
4120
- if(!name){
4121
- continue;
4122
- }
4123
-
4124
- nativeToolCalls[name]=typeof call.function.arguments==='string'
4125
- ?call.function.arguments
4126
- :JSON.stringify(call.function.arguments||{});
4127
-
4128
- if(!triggeredTools.has(name)){
4129
- triggeredTools.add(name);
4130
- Promise.resolve(earlyFunctionTrigger(name)).catch(
4131
- reportEarlyFunctionFailure
4132
- );
4133
- }
4463
+ nativeStreamedToolCallFrames.push(calls.slice());
4134
4464
  }
4135
4465
  }
4136
4466
 
4137
- await this.#reportRequest(requestHandler,ollamaRequest,id,{
4138
- operation:'stream',
4139
- transport:'native',
4140
- destination:'Arcane.ollama.chat'
4141
- });
4142
- const nativeResponse=await nativeOllama.chat(
4143
- ollamaRequest,
4144
- {
4145
- onChunk:function receiveNativeOllamaChunk(chunk){
4467
+ let nativeChunkPipeline=Promise.resolve();
4468
+ function queueNativeOllamaChunk(chunk){
4469
+ nativeChunkPipeline=nativeChunkPipeline.then(
4470
+ async function processNativeOllamaChunk(){
4146
4471
  if(signal?.aborted){
4147
4472
  return;
4148
4473
  }
@@ -4153,43 +4478,105 @@ class AI {
4153
4478
  const content=String(message.content||'');
4154
4479
 
4155
4480
  if(thinking){
4156
- streamHandler(thinking,`M-${id}`,true);
4481
+ await streamHandler(thinking,`M-${id}`,true);
4157
4482
  }
4158
4483
 
4159
4484
  if(content){
4160
4485
  isThinking=false;
4161
4486
  nativeContent+=content;
4162
- streamHandler(content,`M-${id}`,false);
4487
+ await streamHandler(content,`M-${id}`,false);
4163
4488
  }
4164
4489
 
4165
4490
  receiveNativeToolCalls(message);
4166
- },
4167
- signal
4168
- }
4169
- );
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
+ }
4170
4519
  if(signal?.aborted){
4171
4520
  throw normalizeAIRequestAbort();
4172
4521
  }
4173
- 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
+ }
4174
4559
  this.#assertRequiredOllamaToolCall(
4175
- Object.keys(nativeToolCalls).map(function createToolCallName(name){
4176
- return {function:{name}};
4177
- }),
4560
+ structuralToolCalls,
4178
4561
  tool_choice
4179
4562
  );
4563
+ for(const call of structuralToolCalls){
4564
+ if(signal?.aborted){
4565
+ throw normalizeAIRequestAbort();
4566
+ }
4567
+ await earlyFunctionTrigger(call,`M-${id}`);
4568
+ }
4180
4569
 
4181
- const nativeResult=Object.keys(nativeToolCalls).length
4182
- ?nativeToolCalls
4183
- :nativeContent;
4184
- if(Object.keys(nativeToolCalls).length&&!nativeContent){
4185
- streamHandler('',`M-${id}`,false);
4570
+ const nativeResult=this.#providerCompletionOutput(nativeCompletion);
4571
+ if(structuralToolCalls.length&&!nativeContent){
4572
+ await streamHandler('',`M-${id}`,false);
4186
4573
  }
4187
4574
  if(finishSpeech){
4188
4575
  this.finishTTS();
4189
4576
  }
4190
4577
  await streamComplete(nativeResult,`M-${id}`,isThinking);
4191
4578
  speechTurnCompleted=true;
4192
- return nativeResult;
4579
+ return returnCompletion?nativeCompletion:nativeResult;
4193
4580
  }
4194
4581
 
4195
4582
  await this.#reportRequest(requestHandler,request,id,{
@@ -4230,8 +4617,13 @@ class AI {
4230
4617
 
4231
4618
  let chunkString='';
4232
4619
  let chunkCache='';
4233
- const streamedToolCalls=new Map();
4234
- 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;
4235
4627
  const decoder = new TextDecoder('utf-8');
4236
4628
  //alert(1)
4237
4629
  const reader=response.body?.getReader?.();
@@ -4240,41 +4632,63 @@ class AI {
4240
4632
  throw new TypeError('Streaming response body is not readable');
4241
4633
  }
4242
4634
 
4243
- function receiveStreamedToolCalls(toolCalls=[]){
4635
+ function receiveStreamedToolCalls(toolCalls=[],choicePosition=0){
4636
+ const streamedToolCalls=streamedToolCallsByChoice.get(choicePosition)
4637
+ ||new Map();
4244
4638
  for(let position=0;position<toolCalls.length;position++){
4245
4639
  const toolCall=toolCalls[position]||{};
4246
4640
  const toolFunction=toolCall.function||{};
4247
- const key=Number.isInteger(toolCall.index)
4248
- ?`index:${toolCall.index}`
4249
- :toolCall.id
4250
- ?`id:${toolCall.id}`
4251
- :`position:${position}`;
4641
+ const key=`position:${Number.isInteger(toolCall.index)
4642
+ ?toolCall.index
4643
+ :position}`;
4252
4644
  const record=streamedToolCalls.get(key)||{
4253
4645
  arguments:'',
4646
+ id:'',
4647
+ invalidArguments:false,
4648
+ invalidIdentity:false,
4649
+ invalidName:false,
4254
4650
  name:'',
4255
- order:streamedToolCalls.size
4651
+ order:streamedToolCalls.size,
4652
+ type:''
4256
4653
  };
4257
4654
 
4258
- if(toolFunction.name){
4259
- record.name=toolFunction.name;
4260
- if(!triggeredTools.has(record.name)){
4261
- triggeredTools.add(record.name);
4262
- Promise.resolve(
4263
- earlyFunctionTrigger(record.name)
4264
- ).catch(
4265
- ()=>console.error('Early tool trigger failed.')
4266
- );
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;
4267
4680
  }
4268
4681
  }
4269
4682
 
4270
4683
  if(typeof toolFunction.arguments==='string'){
4271
4684
  record.arguments+=toolFunction.arguments;
4272
- }else if(toolFunction.arguments&&typeof toolFunction.arguments==='object'){
4273
- record.arguments+=JSON.stringify(toolFunction.arguments);
4685
+ }else if(toolFunction.arguments!==undefined){
4686
+ record.invalidArguments=true;
4274
4687
  }
4275
4688
 
4276
4689
  streamedToolCalls.set(key,record);
4277
4690
  }
4691
+ streamedToolCallsByChoice.set(choicePosition,streamedToolCalls);
4278
4692
  }
4279
4693
 
4280
4694
  try{
@@ -4295,64 +4709,88 @@ class AI {
4295
4709
  //alert(3)
4296
4710
  //console.log(lines);
4297
4711
 
4298
- lines.forEach(
4299
- function parsingAIGeneratedStream(delta,i){
4300
- chunkCache+=delta;
4712
+ for(const eventData of lines){
4713
+ chunkCache+=eventData;
4301
4714
 
4302
- if (chunkCache.trim() === '[DONE]') {
4303
- chunkCache = '';
4304
- 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;
4305
4770
  }
4306
4771
 
4307
- try{
4308
- const resp=JSON.parse(chunkCache)||{};
4309
- //console.log(JSON.stringify(resp));
4310
- //console.log(resp)
4311
- const choice = resp.choices?.[0] || {};
4312
- const delta = choice.delta || {};
4313
- const content = delta.content || '';
4314
- const tool_calls=delta.tool_calls || [];
4315
- let value = content;
4316
-
4317
- let reasoning = '';
4318
-
4319
- if(seeThinking){
4320
- reasoning=delta.reasoning || '';
4321
- }
4322
-
4323
- if (reasoning) {
4324
- isThinking = true;
4325
- value = reasoning;
4326
- }
4327
-
4328
- if (!reasoning && isThinking) {
4329
- //remove thinking chunks
4330
- chunkString='';
4331
- }
4332
-
4333
- if (!reasoning) {
4334
- isThinking = false;
4335
- }
4336
-
4337
- chunkCache='';
4338
-
4339
- if(value==='' && !tool_calls.length){
4340
- return;
4341
- }
4342
-
4343
- if(value){
4344
- streamHandler(value,`M-${id}`, isThinking);
4345
- chunkString+=value;
4346
- }
4347
-
4348
- if(tool_calls.length){
4349
- receiveStreamedToolCalls(tool_calls);
4350
- }
4351
- } catch(err) {
4352
- 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;
4353
4791
  }
4354
4792
  }
4355
- );
4793
+ }
4356
4794
  }
4357
4795
  }catch(error){
4358
4796
  if(isAIRequestAbort(error,signal)){
@@ -4363,30 +4801,108 @@ class AI {
4363
4801
  reader.releaseLock();
4364
4802
  }
4365
4803
 
4366
- const tool_funcs={};
4367
- const orderedToolCalls=[...streamedToolCalls.values()].sort(
4368
- function sortStreamedToolCalls(a,b){
4369
- 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];
4370
4808
  }
4371
4809
  );
4372
-
4373
- for(const toolCall of orderedToolCalls){
4374
- if(!toolCall.name){
4375
- 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'
4376
4861
  }
4377
-
4378
- if(Object.hasOwn(tool_funcs,toolCall.name)){
4379
- throw new Error(`AI stream returned duplicate tool ${toolCall.name}.`);
4862
+ ];
4863
+ for(const [choicePosition,toolCalls] of structuralToolCallsByChoice){
4864
+ if(choicePosition===0){
4865
+ continue;
4380
4866
  }
4381
-
4382
- 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
+ });
4383
4876
  }
4384
-
4385
- const streamResult=Object.keys(tool_funcs).length
4386
- ?tool_funcs
4387
- :chunkString;
4388
- if(Object.keys(tool_funcs).length&&!chunkString){
4389
- 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);
4390
4906
  }
4391
4907
  if(finishSpeech){
4392
4908
  this.finishTTS();
@@ -4395,7 +4911,7 @@ class AI {
4395
4911
 
4396
4912
  //sync
4397
4913
  speechTurnCompleted=true;
4398
- return streamResult;
4914
+ return returnCompletion?completion:streamResult;
4399
4915
  }catch(error){
4400
4916
  if(isAIRequestAbort(error,signal)){
4401
4917
  throw normalizeAIRequestAbort(error);
@@ -4414,12 +4930,23 @@ class AI {
4414
4930
  localOnly=false,
4415
4931
  tools=[],
4416
4932
  toolChoice='auto',
4417
- parallelToolCalls=true,
4933
+ parallelToolCalls=false,
4418
4934
  id=Date.now(),
4419
4935
  signal=null,
4420
4936
  onRequest=function ignoreFetchRequest(){},
4421
- 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
4422
4948
  }={}){
4949
+ validateAIStructuralRequest(messages,tools,parallelToolCalls);
4423
4950
  if(localOnly!==true&&localOnly!==false){
4424
4951
  throw new TypeError('AI localOnly must be a boolean.');
4425
4952
  }
@@ -4440,7 +4967,18 @@ class AI {
4440
4967
  tools,
4441
4968
  toolChoice,
4442
4969
  parallelToolCalls,
4443
- 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}:{})
4444
4982
  };
4445
4983
  await this.#reportRequest(onRequest,request,id);
4446
4984
  if(signal?.aborted){
@@ -4458,6 +4996,7 @@ class AI {
4458
4996
  if(signal?.aborted){
4459
4997
  throw normalizeAIRequestAbort();
4460
4998
  }
4999
+ normalizeAICompletionToolCalls(response);
4461
5000
  await onResponse(response,id,false);
4462
5001
  if(signal?.aborted){
4463
5002
  throw normalizeAIRequestAbort();
@@ -4484,7 +5023,7 @@ class AI {
4484
5023
  structuredOutput=false,
4485
5024
  tools=[],
4486
5025
  tool_choice='auto',
4487
- parallel_tool_calls=true,
5026
+ parallel_tool_calls=false,
4488
5027
  id=Date.now(),
4489
5028
  requestHandler=function ignoreFetchRequest(){},
4490
5029
  signal=null,
@@ -4523,11 +5062,12 @@ class AI {
4523
5062
  structuredOutput=false,
4524
5063
  tools=[],
4525
5064
  tool_choice='auto',
4526
- parallel_tool_calls=true,
5065
+ parallel_tool_calls=false,
4527
5066
  id=Date.now(),
4528
5067
  requestHandler=function ignoreFetchRequest(){},
4529
5068
  signal=null,
4530
5069
  ){
5070
+ validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4531
5071
  this.#assertServiceConfigured(this.llmService);
4532
5072
  if(signal&&(
4533
5073
  typeof signal.aborted!=='boolean'
@@ -4602,16 +5142,14 @@ class AI {
4602
5142
  if(signal?.aborted){
4603
5143
  throw normalizeAIRequestAbort();
4604
5144
  }
4605
- this.#assertRequiredOllamaToolCall(
4606
- Array.isArray(nativeResponse?.message?.tool_calls)
4607
- ?nativeResponse.message.tool_calls
4608
- :[],
4609
- tool_choice
4610
- );
4611
5145
  const responseJSON=this.#openAICompatibleOllamaResponse(
4612
5146
  nativeResponse,
4613
5147
  id
4614
5148
  );
5149
+ this.#assertRequiredOllamaToolCall(
5150
+ normalizeAICompletionToolCalls(responseJSON),
5151
+ tool_choice
5152
+ );
4615
5153
 
4616
5154
  if(signal?.aborted){
4617
5155
  throw normalizeAIRequestAbort();
@@ -4681,6 +5219,7 @@ class AI {
4681
5219
 
4682
5220
  //console.log(responseJSON);
4683
5221
  //async
5222
+ normalizeAICompletionToolCalls(responseJSON);
4684
5223
  await responseHandler(responseJSON,id,false);
4685
5224
  //sync
4686
5225
  return responseJSON;
@@ -4707,7 +5246,10 @@ class AI {
4707
5246
  try{
4708
5247
  this.#assertServiceConfigured(this.ttsService,'tts');
4709
5248
  }catch(error){
4710
- console.warn('AI speech provider is unavailable.');
5249
+ this.#publishTTSFailure(error,{
5250
+ boundary:'synthesis',
5251
+ generation:this.speechGeneration
5252
+ });
4711
5253
  return Promise.resolve(false);
4712
5254
  }
4713
5255
 
@@ -4731,20 +5273,15 @@ class AI {
4731
5273
 
4732
5274
  #extractSpeechSegments(end=false){
4733
5275
  const segments=[];
4734
- const maximumLength=220;
4735
5276
  let remainder=this.audioMessageChunks;
4736
5277
 
4737
- while(remainder.trim()){
5278
+ while(remainder.length>0){
4738
5279
  const terminator=this.#findSpeechTerminator(remainder,end);
4739
5280
  let boundary=terminator
4740
5281
  ?terminator.index+terminator[0].length
4741
5282
  :-1;
4742
5283
 
4743
- if(boundary<0&&remainder.length>=maximumLength){
4744
- const candidate=remainder.slice(0,maximumLength+1);
4745
- const whitespace=candidate.lastIndexOf(' ');
4746
- boundary=whitespace>=80?whitespace+1:maximumLength;
4747
- }else if(boundary<0&&end){
5284
+ if(boundary<0&&end){
4748
5285
  boundary=remainder.length;
4749
5286
  }
4750
5287
 
@@ -4752,8 +5289,8 @@ class AI {
4752
5289
  break;
4753
5290
  }
4754
5291
 
4755
- const segment=remainder.slice(0,boundary).trim();
4756
- remainder=remainder.slice(boundary).trimStart();
5292
+ const segment=remainder.slice(0,boundary);
5293
+ remainder=remainder.slice(boundary);
4757
5294
 
4758
5295
  if(segment){
4759
5296
  segments.push(segment);
@@ -4819,7 +5356,11 @@ class AI {
4819
5356
  }
4820
5357
  ).catch(
4821
5358
  function discardFailedSpeechJob(error){
4822
- return runtime.#failSpeechJob(job,error);
5359
+ return runtime.#failSpeechJob(
5360
+ job,
5361
+ error,
5362
+ job.state==='decoding'?'decode':'synthesis'
5363
+ );
4823
5364
  }
4824
5365
  );
4825
5366
 
@@ -4844,6 +5385,7 @@ class AI {
4844
5385
  return this.#cancelSpeechJob(job);
4845
5386
  }
4846
5387
 
5388
+ job.state='decoding';
4847
5389
  const audioContext=this.#getSpeechAudioContext();
4848
5390
  return this.playAudio(
4849
5391
  audio.chunks,
@@ -5324,6 +5866,12 @@ class AI {
5324
5866
  this.speechResumePending=false;
5325
5867
  }
5326
5868
  this.#waitForSpeechGesture(error);
5869
+ if(error?.name!=='NotAllowedError'){
5870
+ this.#publishTTSFailure(error,{
5871
+ boundary:'playback-resume',
5872
+ generation:this.speechGeneration
5873
+ });
5874
+ }
5327
5875
  return false;
5328
5876
  }
5329
5877
 
@@ -5333,7 +5881,7 @@ class AI {
5333
5881
 
5334
5882
  async playAudio(
5335
5883
  audioChunks=[],
5336
- audioContext=this.#getSpeechAudioContext(),
5884
+ audioContext=null,
5337
5885
  sourceNode=null,
5338
5886
  audioType=this.audioType,
5339
5887
  speechJob=null
@@ -5356,19 +5904,20 @@ class AI {
5356
5904
 
5357
5905
  try{
5358
5906
  job.state='decoding';
5907
+ const playbackContext=audioContext||this.#getSpeechAudioContext();
5359
5908
  const audioBlob=new Blob(audioChunks,{type:audioType});
5360
5909
  const arrayBuffer=await audioBlob.arrayBuffer();
5361
- const audioBuffer=await audioContext.decodeAudioData(arrayBuffer);
5910
+ const audioBuffer=await playbackContext.decodeAudioData(arrayBuffer);
5362
5911
 
5363
5912
  if(this.muted||job.generation!==this.speechGeneration){
5364
5913
  return this.#cancelSpeechJob(job);
5365
5914
  }
5366
5915
 
5367
- const preparedSource=sourceNode||audioContext.createBufferSource();
5916
+ const preparedSource=sourceNode||playbackContext.createBufferSource();
5368
5917
  const runtime=this;
5369
5918
 
5370
5919
  preparedSource.buffer=audioBuffer;
5371
- preparedSource.connect(audioContext.destination);
5920
+ preparedSource.connect(playbackContext.destination);
5372
5921
  preparedSource.__arcaneStarted=false;
5373
5922
  preparedSource.onended=function finishQueuedSpeechSource(){
5374
5923
  runtime.nextSentance(job);
@@ -5379,14 +5928,20 @@ class AI {
5379
5928
  this.#requestSpeechPlayback();
5380
5929
  return true;
5381
5930
  }catch(error){
5382
- return this.#failSpeechJob(job,error);
5931
+ return this.#failSpeechJob(job,error,'decode');
5383
5932
  }
5384
5933
  }
5385
5934
 
5386
5935
  #requestSpeechPlayback(){
5936
+ const runtime=this;
5387
5937
  this.#pumpSpeechPlayback().catch(
5388
5938
  function reportSpeechPlaybackFailure(error){
5389
- 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);
5390
5945
  }
5391
5946
  );
5392
5947
  }
@@ -5397,10 +5952,12 @@ class AI {
5397
5952
  }
5398
5953
 
5399
5954
  this.speechPlaybackStarting=true;
5955
+ let activeJob=null;
5400
5956
 
5401
5957
  try{
5402
5958
  while(!this.isSpeaking&&!this.muted){
5403
5959
  const job=this.speechJobs[0];
5960
+ activeJob=job||null;
5404
5961
 
5405
5962
  if(!job){
5406
5963
  return false;
@@ -5441,14 +5998,21 @@ class AI {
5441
5998
  job.sourceNode.__arcaneStarted=true;
5442
5999
  this.currentSpeechJob=job;
5443
6000
  this.isSpeaking=true;
5444
- job.sourceNode.start(0);
6001
+ await job.sourceNode.start(0);
5445
6002
  return true;
5446
6003
  }catch(error){
5447
6004
  this.currentSpeechJob=null;
5448
6005
  this.isSpeaking=false;
5449
- this.#failSpeechJob(job,error);
6006
+ this.#failSpeechJob(job,error,'playback-start');
5450
6007
  }
5451
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;
5452
6016
  }finally{
5453
6017
  this.speechPlaybackStarting=false;
5454
6018
 
@@ -5494,13 +6058,76 @@ class AI {
5494
6058
  return false;
5495
6059
  }
5496
6060
 
5497
- #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'){
5498
6120
  if(job.state==='failed'||job.state==='cancelled'){
5499
6121
  return false;
5500
6122
  }
5501
6123
 
5502
6124
  job.state='failed';
5503
6125
 
6126
+ this.#publishTTSFailure(error,{
6127
+ boundary,
6128
+ generation:job.generation
6129
+ });
6130
+
5504
6131
  if(job.sourceNode){
5505
6132
  job.sourceNode.onended=null;
5506
6133
  }
@@ -5512,10 +6139,6 @@ class AI {
5512
6139
  this.isSpeaking=false;
5513
6140
  }
5514
6141
 
5515
- if(job.generation===this.speechGeneration&&error?.name!=='AbortError'){
5516
- console.warn('AI speech synthesis failed.');
5517
- }
5518
-
5519
6142
  this.#requestSpeechPlayback();
5520
6143
  return false;
5521
6144
  }
@@ -5627,7 +6250,7 @@ function installAIUserReadyRegistration(){
5627
6250
  delete globalThis[AI_USER_READY_REGISTRATION_KEY];
5628
6251
  }
5629
6252
  }
5630
- registration=Object.freeze({
6253
+ registration=completeValue({
5631
6254
  protocol:AI_USER_READY_REGISTRATION_PROTOCOL,
5632
6255
  dispose:disposeAIUserReadyRegistration
5633
6256
  });