arcane-os 0.3.4 → 0.3.6

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 (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -7
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
  4. package/browser-runtime/ai/model-controller.mjs +439 -95
  5. package/package.json +1 -1
  6. package/runtime/arcane/components/assistant-panel.html +2 -1
  7. package/runtime/arcane/components/chat.html +469 -220
  8. package/runtime/arcane/components/speech.html +33 -13
  9. package/runtime/arcane/components/voice-transcription.html +27 -3
  10. package/runtime/arcane/entities/Chat.js +165 -97
  11. package/runtime/arcane/entities/IntentEnvelope.js +52 -118
  12. package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
  13. package/runtime/arcane/entities/User.js +38 -44
  14. package/runtime/arcane/modules/AI.js +569 -302
  15. package/runtime/arcane/modules/AIProviderRuntime.js +776 -151
  16. package/runtime/arcane/modules/AIRuntimeState.js +22 -24
  17. package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
  18. package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
  19. package/runtime/arcane/modules/CommunicationHub.js +90 -94
  20. package/runtime/arcane/modules/ComponentContracts.js +23 -9
  21. package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
  22. package/runtime/arcane/modules/ConversationTimebox.js +76 -104
  23. package/runtime/arcane/modules/DBLS.js +14 -12
  24. package/runtime/arcane/modules/DBOPFS.js +20 -15
  25. package/runtime/arcane/modules/Errors.js +196 -436
  26. package/runtime/arcane/modules/HTMLImport.js +49 -32
  27. package/runtime/arcane/modules/Ollama.js +16 -14
  28. package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
  29. package/runtime/arcane/modules/RecordReviewStore.js +40 -35
  30. package/runtime/arcane/modules/TerminalClient.js +12 -14
  31. package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
  32. package/runtime/arcane/modules/ThemeManager.js +5 -5
  33. package/runtime/arcane/modules/TimeGuard.js +5 -65
  34. package/runtime/arcane/modules/WaitForComponent.js +43 -40
  35. package/src/installed-sdk-runtime.mjs +11 -1
@@ -214,9 +214,11 @@ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
214
214
  );
215
215
  }
216
216
  return {
217
+ ...call,
217
218
  id:call.id,
218
219
  type:'function',
219
220
  function:{
221
+ ...call.function,
220
222
  name:call.function.name,
221
223
  arguments:call.function.arguments
222
224
  }
@@ -227,7 +229,7 @@ function validateAIRequestMessages(messages=[]){
227
229
  if(!Array.isArray(messages)){
228
230
  throw new TypeError('AI messages must be an array.');
229
231
  }
230
- let pendingToolCallId=null;
232
+ const pendingToolCallIds=new Set();
231
233
  for(let messageIndex=0;messageIndex<messages.length;messageIndex+=1){
232
234
  const message=messages[messageIndex];
233
235
  const calls=message?.tool_calls;
@@ -239,17 +241,26 @@ function validateAIRequestMessages(messages=[]){
239
241
  `AI messages[${messageIndex}].tool_calls is invalid.`
240
242
  );
241
243
  }
242
- if(calls.length>1||(pendingToolCallId!==null&&calls.length)){
244
+ if(pendingToolCallIds.size&&calls.length){
243
245
  throw aiStructuralError(
244
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
245
- 'The Arcane chat session accepts one structural tool call at a time.'
246
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
247
+ 'Every pending structural tool result must be supplied before another assistant tool-call sequence.'
246
248
  );
247
249
  }
248
250
  if(calls.length){
249
- pendingToolCallId=normalizeAIStructuralToolCall(
250
- calls[0],
251
- `AI messages[${messageIndex}].tool_calls[0]`
252
- ).id;
251
+ for(let callIndex=0;callIndex<calls.length;callIndex+=1){
252
+ const normalized=normalizeAIStructuralToolCall(
253
+ calls[callIndex],
254
+ `AI messages[${messageIndex}].tool_calls[${callIndex}]`
255
+ );
256
+ if(pendingToolCallIds.has(normalized.id)){
257
+ throw aiStructuralError(
258
+ 'AI_CHAT_INVALID_TOOL_CALL',
259
+ `AI messages[${messageIndex}].tool_calls contains a duplicate ID.`
260
+ );
261
+ }
262
+ pendingToolCallIds.add(normalized.id);
263
+ }
253
264
  openedToolCall=true;
254
265
  }
255
266
  }
@@ -261,24 +272,24 @@ function validateAIRequestMessages(messages=[]){
261
272
  );
262
273
  }
263
274
  if(
264
- pendingToolCallId===null
275
+ !pendingToolCallIds.size
265
276
  ||typeof message.tool_call_id!=='string'
266
- ||message.tool_call_id!==pendingToolCallId
277
+ ||!pendingToolCallIds.has(message.tool_call_id)
267
278
  ){
268
279
  throw aiStructuralError(
269
280
  'AI_CHAT_INVALID_TOOL_MESSAGE',
270
281
  `AI messages[${messageIndex}] does not settle the pending structural tool call.`
271
282
  );
272
283
  }
273
- pendingToolCallId=null;
274
- }else if(pendingToolCallId!==null&&!openedToolCall){
284
+ pendingToolCallIds.delete(message.tool_call_id);
285
+ }else if(pendingToolCallIds.size&&!openedToolCall){
275
286
  throw aiStructuralError(
276
287
  'AI_CHAT_TOOL_RESULT_REQUIRED',
277
288
  `AI messages[${messageIndex}] precedes the pending structural tool result.`
278
289
  );
279
290
  }
280
291
  }
281
- if(pendingToolCallId!==null){
292
+ if(pendingToolCallIds.size){
282
293
  throw aiStructuralError(
283
294
  'AI_CHAT_TOOL_RESULT_REQUIRED',
284
295
  'The pending structural tool call must be settled before requesting another response.'
@@ -289,11 +300,8 @@ function validateAIRequestMessages(messages=[]){
289
300
  function validateAIStructuralRequest(messages,tools,parallelToolCalls){
290
301
  validateAIRequestMessages(messages);
291
302
  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
- );
303
+ if(parallelToolCalls!==undefined&&typeof parallelToolCalls!=='boolean'){
304
+ throw new TypeError('AI parallelToolCalls must be a boolean when provided.');
297
305
  }
298
306
  }
299
307
 
@@ -334,29 +342,49 @@ function normalizeAICompletionToolCalls(completion){
334
342
  `AI response message ${messageIndex+1} contains invalid structural tool calls.`
335
343
  );
336
344
  }
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
- }
345
+ const messageIds=new Set();
349
346
  for(let callIndex=0;callIndex<(toolCalls??[]).length;callIndex+=1){
350
- calls.push(normalizeAIStructuralToolCall(
347
+ const normalized=normalizeAIStructuralToolCall(
351
348
  toolCalls[callIndex],
352
349
  `AI response structural tool call ${messageIndex+1}.${callIndex+1}`
353
- ));
350
+ );
351
+ if(messageIds.has(normalized.id)){
352
+ throw aiStructuralError(
353
+ 'AI_CHAT_INVALID_TOOL_CALL',
354
+ `AI response message ${messageIndex+1} contains a duplicate structural tool-call ID.`
355
+ );
356
+ }
357
+ messageIds.add(normalized.id);
358
+ if(messageIndex===0)calls.push(normalized);
354
359
  }
355
360
  }
356
361
  return calls;
357
362
  }
358
363
 
364
+ function sameAIDataValue(left,right,seen=new Map()){
365
+ if(Object.is(left,right))return true;
366
+ if(!left||!right||typeof left!=='object'||typeof right!=='object')return false;
367
+ if(Array.isArray(left)!==Array.isArray(right))return false;
368
+ const matched=seen.get(left);
369
+ if(matched!==undefined)return matched===right;
370
+ seen.set(left,right);
371
+ if(Array.isArray(left)){
372
+ return left.length===right.length
373
+ &&left.every((value,index)=>sameAIDataValue(value,right[index],seen));
374
+ }
375
+ if(!isPlainAIRecord(left)||!isPlainAIRecord(right))return false;
376
+ const leftKeys=Reflect.ownKeys(left).filter(key=>Object.prototype.propertyIsEnumerable.call(left,key));
377
+ const rightKeys=Reflect.ownKeys(right).filter(key=>Object.prototype.propertyIsEnumerable.call(right,key));
378
+ return leftKeys.length===rightKeys.length
379
+ &&leftKeys.every(key=>Object.prototype.propertyIsEnumerable.call(right,key)
380
+ &&sameAIDataValue(left[key],right[key],seen));
381
+ }
382
+
359
383
  function sameAIStructuralToolCall(left,right){
384
+ return sameAIDataValue(left,right);
385
+ }
386
+
387
+ function sameAICanonicalToolCall(left,right){
360
388
  return left?.id===right?.id
361
389
  &&left?.type===right?.type
362
390
  &&left?.function?.name===right?.function?.name
@@ -402,6 +430,54 @@ function normalizeAIStreamToolCallObservation(completion,label){
402
430
  }
403
431
  }
404
432
 
433
+ function isAIStreamStructuralKey(key){
434
+ return key==='tool_calls'
435
+ ||key==='toolCalls'
436
+ ||key==='tool_call'
437
+ ||key==='toolCall'
438
+ ||key==='function_call'
439
+ ||key==='functionCall';
440
+ }
441
+
442
+ const OMITTED_AI_STREAM_DATA=Symbol('omitted-ai-stream-data');
443
+
444
+ function projectAIStreamData(value,seen=new Map()){
445
+ if(value===null||value===undefined||typeof value!=='object'){
446
+ return value;
447
+ }
448
+ if(seen.has(value)) return seen.get(value);
449
+ if(Array.isArray(value)){
450
+ const result=[];
451
+ seen.set(value,result);
452
+ for(const item of value){
453
+ const projected=projectAIStreamData(item,seen);
454
+ if(projected!==OMITTED_AI_STREAM_DATA)result.push(projected);
455
+ }
456
+ return result.length||value.length===0?result:OMITTED_AI_STREAM_DATA;
457
+ }
458
+ const result={};
459
+ seen.set(value,result);
460
+ let sourceDataFields=0;
461
+ for(const [key,descriptor] of Object.entries(
462
+ Object.getOwnPropertyDescriptors(value)
463
+ )){
464
+ if(!Object.hasOwn(descriptor,'value')){
465
+ continue;
466
+ }
467
+ sourceDataFields+=1;
468
+ if(isAIStreamStructuralKey(key))continue;
469
+ const projected=projectAIStreamData(descriptor.value,seen);
470
+ if(projected!==OMITTED_AI_STREAM_DATA)result[key]=projected;
471
+ }
472
+ return Object.keys(result).length||sourceDataFields===0
473
+ ?result
474
+ :OMITTED_AI_STREAM_DATA;
475
+ }
476
+
477
+ function projectAIStreamChunk(value){
478
+ return projectAIStreamData(value);
479
+ }
480
+
405
481
  function createLegacyAIStreamBridge(execute,sourceSignal){
406
482
  const controller=new AbortController();
407
483
  const queue=[];
@@ -3930,13 +4006,21 @@ class AI {
3930
4006
 
3931
4007
  #providerStreamEmissions(chunk,seeThinking){
3932
4008
  const chunks=[];
4009
+ if(typeof chunk==='string'){
4010
+ if(chunk) chunks.push({text:chunk,thinking:false});
4011
+ return {chunks};
4012
+ }
3933
4013
  const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
3934
4014
  for(const choice of choices){
3935
4015
  const delta=choice?.delta||{};
3936
- if(seeThinking&&typeof delta.reasoning_content==='string'){
4016
+ if(
4017
+ seeThinking
4018
+ &&typeof delta.reasoning_content==='string'
4019
+ &&delta.reasoning_content
4020
+ ){
3937
4021
  chunks.push({text:delta.reasoning_content,thinking:true});
3938
4022
  }
3939
- if(typeof delta.content==='string'){
4023
+ if(typeof delta.content==='string'&&delta.content){
3940
4024
  chunks.push({text:delta.content,thinking:false});
3941
4025
  }
3942
4026
  }
@@ -3960,34 +4044,26 @@ class AI {
3960
4044
  if(typeof completion==='string'){
3961
4045
  return completion;
3962
4046
  }
3963
- const toolRecord={};
3964
- let toolCount=0;
3965
- for(const choice of Array.isArray(completion?.choices)?completion.choices:[]){
3966
- for(const call of Array.isArray(choice?.message?.tool_calls)
3967
- ?choice.message.tool_calls
3968
- :[]){
3969
- const name=call?.function?.name;
3970
- if(typeof name==='string'&&name){
3971
- toolRecord[name]=call.function.arguments;
3972
- toolCount+=1;
3973
- }
3974
- }
3975
- }
3976
- if(toolCount){
3977
- return toolRecord;
4047
+ if(Array.isArray(completion?.choices)&&completion.choices.length>1){
4048
+ return completion;
3978
4049
  }
4050
+ const structuralToolCalls=normalizeAICompletionToolCalls(completion);
4051
+ if(structuralToolCalls.length)return structuralToolCalls;
3979
4052
  const content=completion?.choices?.[0]?.message?.content;
3980
4053
  return typeof content==='string'?content:completion;
3981
4054
  }
3982
4055
 
3983
4056
  #requestLegacyLLMChat(payload={},signal=null){
4057
+ const parallelToolCalls=payload.parallelToolCalls!==undefined
4058
+ ?payload.parallelToolCalls
4059
+ :payload.parallel_tool_calls;
3984
4060
  return this.#fetchLegacy(
3985
4061
  payload.messages??[],
3986
4062
  function ignoreLegacyLLMProviderResponse(){},
3987
4063
  payload.structuredOutput??false,
3988
4064
  payload.tools??[],
3989
4065
  payload.toolChoice??'auto',
3990
- payload.parallelToolCalls??false,
4066
+ parallelToolCalls,
3991
4067
  payload.id??Date.now(),
3992
4068
  function ignoreLegacyLLMProviderRequest(){},
3993
4069
  signal
@@ -3995,32 +4071,29 @@ class AI {
3995
4071
  }
3996
4072
 
3997
4073
  #requestLegacyLLMStream(payload={},bridge){
3998
- function emitLegacyLLMStreamText(text,id,thinking){
3999
- if(typeof text!=='string'||!text){
4000
- return;
4001
- }
4002
- bridge.emit(
4003
- thinking
4004
- ?{thinking:text}
4005
- :{content:text}
4006
- );
4074
+ const parallelToolCalls=payload.parallelToolCalls!==undefined
4075
+ ?payload.parallelToolCalls
4076
+ :payload.parallel_tool_calls;
4077
+ function emitLegacyLLMStreamData(chunk){
4078
+ bridge.emit(chunk);
4007
4079
  }
4008
4080
 
4009
4081
  return this.#streamLegacyMessage(
4010
4082
  payload.messages??[],
4011
- emitLegacyLLMStreamText,
4083
+ function ignoreLegacyLLMScalarStream(){},
4012
4084
  function ignoreLegacyLLMProviderCompletion(){},
4013
4085
  payload.tools??[],
4014
4086
  payload.toolChoice??'auto',
4015
4087
  function retainLegacyLLMStreamToolUntilCompletion(){},
4016
- payload.parallelToolCalls??false,
4088
+ parallelToolCalls,
4017
4089
  payload.id??Date.now(),
4018
4090
  payload.seeThinking??false,
4019
4091
  bridge.signal,
4020
4092
  function ignoreLegacyLLMProviderRequest(){},
4021
4093
  payload.structuredOutput??false,
4022
4094
  false,
4023
- true
4095
+ true,
4096
+ emitLegacyLLMStreamData
4024
4097
  );
4025
4098
  }
4026
4099
 
@@ -4087,11 +4160,13 @@ class AI {
4087
4160
  }
4088
4161
  }
4089
4162
  return {
4163
+ ...call,
4090
4164
  id:call.id===undefined
4091
4165
  ?`ollama-${id}-tool-${index+1}`
4092
4166
  :call.id,
4093
4167
  type:call.type===undefined?'function':call.type,
4094
4168
  function:{
4169
+ ...nativeFunction,
4095
4170
  name:nativeFunction.name,
4096
4171
  arguments:encodedArguments
4097
4172
  }
@@ -4100,7 +4175,12 @@ class AI {
4100
4175
  }
4101
4176
 
4102
4177
  #openAICompatibleOllamaResponse(response={},id=Date.now()){
4103
- const message=response?.message||{};
4178
+ const responseRecord=isPlainAIRecord(response)?response:{};
4179
+ const message=isPlainAIRecord(responseRecord.message)
4180
+ ?responseRecord.message
4181
+ :{};
4182
+ const responseFields={...responseRecord};
4183
+ delete responseFields.message;
4104
4184
  const responseId=typeof response?.id==='string'&&response.id
4105
4185
  ?response.id
4106
4186
  :`ollama-${id}`;
@@ -4111,25 +4191,43 @@ class AI {
4111
4191
  const toolCalls=normalizeAICompletionToolCalls({
4112
4192
  message:{tool_calls:adaptedToolCalls}
4113
4193
  });
4194
+ const messageRecord={
4195
+ ...message,
4196
+ role:typeof message.role==='string'&&message.role
4197
+ ?message.role
4198
+ :'assistant',
4199
+ content:Object.hasOwn(message,'content')?message.content:'',
4200
+ ...(Object.hasOwn(message,'tool_calls')||toolCalls.length
4201
+ ?{tool_calls:toolCalls}
4202
+ :{})
4203
+ };
4204
+ const usage=isPlainAIRecord(responseRecord.usage)
4205
+ ?responseRecord.usage
4206
+ :{};
4114
4207
 
4115
4208
  return {
4209
+ ...responseFields,
4116
4210
  id:responseId,
4117
- object:'chat.completion',
4118
- created:Math.floor(Date.now()/1000),
4119
- model:response?.model||this.model,
4211
+ object:Object.hasOwn(responseRecord,'object')
4212
+ ?responseRecord.object
4213
+ :'chat.completion',
4214
+ created:Object.hasOwn(responseRecord,'created')
4215
+ ?responseRecord.created
4216
+ :Math.floor(Date.now()/1000),
4217
+ model:Object.hasOwn(responseRecord,'model')
4218
+ ?responseRecord.model
4219
+ :this.model,
4120
4220
  choices:[
4121
4221
  {
4122
4222
  index:0,
4123
- message:{
4124
- role:message.role||'assistant',
4125
- content:message.content||'',
4126
- ...(toolCalls.length?{tool_calls:toolCalls}:{})
4127
- },
4128
- finish_reason:response?.done_reason
4129
- ||(toolCalls.length?'tool_calls':'stop')
4223
+ message:messageRecord,
4224
+ finish_reason:Object.hasOwn(responseRecord,'done_reason')
4225
+ ?responseRecord.done_reason
4226
+ :toolCalls.length?'tool_calls':'stop'
4130
4227
  }
4131
4228
  ],
4132
4229
  usage:{
4230
+ ...usage,
4133
4231
  prompt_tokens:Number(response?.prompt_eval_count)||0,
4134
4232
  completion_tokens:Number(response?.eval_count)||0,
4135
4233
  total_tokens:(Number(response?.prompt_eval_count)||0)
@@ -4145,12 +4243,14 @@ class AI {
4145
4243
  localOnly=false,
4146
4244
  onChunk=function ignoreStreamChunk(){},
4147
4245
  onComplete=function finishIgnoredStream(){},
4246
+ onDataChunk=function ignoreStreamDataChunk(){},
4247
+ onDataResult=function ignoreStreamDataResult(){},
4148
4248
  onResponse=function ignoreStreamResponse(){},
4149
4249
  tools=[],
4150
4250
  toolChoice='auto',
4151
4251
  onToolCall=function ignoreEarlyFunction(){},
4152
4252
  onRequest=function ignoreStreamRequest(){},
4153
- parallelToolCalls=false,
4253
+ parallelToolCalls,
4154
4254
  id=Date.now(),
4155
4255
  seeThinking=false,
4156
4256
  signal=null,
@@ -4182,7 +4282,7 @@ class AI {
4182
4282
  structuredOutput,
4183
4283
  tools,
4184
4284
  toolChoice,
4185
- parallelToolCalls,
4285
+ ...(parallelToolCalls!==undefined?{parallelToolCalls}:{}),
4186
4286
  id,
4187
4287
  seeThinking,
4188
4288
  ...(maxOutputTokens!==undefined
@@ -4217,6 +4317,10 @@ class AI {
4217
4317
  }
4218
4318
  );
4219
4319
  for await(const chunk of handle){
4320
+ if(signal?.aborted){
4321
+ throw normalizeAIRequestAbort();
4322
+ }
4323
+ await onDataChunk(chunk,id);
4220
4324
  if(signal?.aborted){
4221
4325
  throw normalizeAIRequestAbort();
4222
4326
  }
@@ -4242,6 +4346,10 @@ class AI {
4242
4346
  const structuralToolCalls=normalizeAICompletionToolCalls(
4243
4347
  completion
4244
4348
  );
4349
+ await onDataResult(completion,id);
4350
+ if(signal?.aborted){
4351
+ throw normalizeAIRequestAbort();
4352
+ }
4245
4353
  await onResponse(completion,id,false);
4246
4354
  for(const call of structuralToolCalls){
4247
4355
  if(signal?.aborted){
@@ -4287,7 +4395,9 @@ class AI {
4287
4395
  onRequest,
4288
4396
  structuredOutput,
4289
4397
  false,
4290
- true
4398
+ true,
4399
+ onDataChunk,
4400
+ onDataResult
4291
4401
  );
4292
4402
  const structuralToolCalls=normalizeAICompletionToolCalls(
4293
4403
  completion
@@ -4327,13 +4437,15 @@ class AI {
4327
4437
  tools=[],
4328
4438
  tool_choice='auto',
4329
4439
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
4330
- parallel_tool_calls=false,
4440
+ parallel_tool_calls,
4331
4441
  id=Date.now(),
4332
4442
  seeThinking=false,
4333
4443
  signal=null,
4334
4444
  requestHandler=function ignoreStreamRequest(){},
4335
4445
  structuredOutput=false,
4336
- returnCompletion=false
4446
+ returnCompletion=false,
4447
+ onDataChunk=function ignoreStreamDataChunk(){},
4448
+ onDataResult=function ignoreStreamDataResult(){}
4337
4449
  ){
4338
4450
  if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
4339
4451
  return this.streamRequest({
@@ -4342,6 +4454,8 @@ class AI {
4342
4454
  localOnly:false,
4343
4455
  onChunk:streamHandler,
4344
4456
  onComplete:streamComplete,
4457
+ onDataChunk,
4458
+ onDataResult,
4345
4459
  tools,
4346
4460
  toolChoice:tool_choice,
4347
4461
  onToolCall:earlyFunctionTrigger,
@@ -4367,7 +4481,9 @@ class AI {
4367
4481
  requestHandler,
4368
4482
  structuredOutput,
4369
4483
  true,
4370
- returnCompletion
4484
+ returnCompletion,
4485
+ onDataChunk,
4486
+ onDataResult
4371
4487
  );
4372
4488
  }
4373
4489
 
@@ -4378,14 +4494,16 @@ class AI {
4378
4494
  tools=[],
4379
4495
  tool_choice='auto',
4380
4496
  earlyFunctionTrigger=function ignoreEarlyFunction(){},
4381
- parallel_tool_calls=false,
4497
+ parallel_tool_calls,
4382
4498
  id=Date.now(),
4383
4499
  seeThinking=false,
4384
4500
  signal=null,
4385
4501
  requestHandler=function ignoreStreamRequest(){},
4386
4502
  structuredOutput=false,
4387
4503
  finishSpeech=true,
4388
- returnCompletion=false
4504
+ returnCompletion=false,
4505
+ dataChunkHandler=function ignoreLegacyStreamDataChunk(){},
4506
+ dataResultHandler=function ignoreLegacyStreamDataResult(){}
4389
4507
  ){
4390
4508
  let speechTurnCompleted=false;
4391
4509
 
@@ -4420,7 +4538,9 @@ class AI {
4420
4538
  if(tools.length){
4421
4539
  request.tools=tools;
4422
4540
  request.tool_choice=tool_choice;
4423
- request.parallel_tool_calls=parallel_tool_calls;
4541
+ if(parallel_tool_calls!==undefined){
4542
+ request.parallel_tool_calls=parallel_tool_calls;
4543
+ }
4424
4544
  }
4425
4545
 
4426
4546
  if(this.llmService==='OLLAMA'&&this.reasoningEffort){
@@ -4443,9 +4563,10 @@ class AI {
4443
4563
 
4444
4564
  if(nativeOllama){
4445
4565
  let nativeContent='';
4446
- const nativeStreamedToolCallFrames=[];
4566
+ let streamedNativeToolCalls=null;
4447
4567
  const ollamaTools=this.#ollamaTools(tools,tool_choice);
4448
4568
  const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
4569
+ const adaptNativeToolCalls=value=>this.#openAICompatibleOllamaToolCalls(value,id);
4449
4570
  const ollamaRequest={
4450
4571
  model:this.model,
4451
4572
  messages:ollamaMessages,
@@ -4455,23 +4576,42 @@ class AI {
4455
4576
  ...(ollamaTools.length?{tools:ollamaTools}:{})
4456
4577
  };
4457
4578
 
4458
- function receiveNativeToolCalls(message={}){
4459
- const calls=Array.isArray(message.tool_calls)?message.tool_calls:[];
4460
-
4461
- if(calls.length){
4462
- isThinking=false;
4463
- nativeStreamedToolCallFrames.push(calls.slice());
4464
- }
4465
- }
4466
-
4467
4579
  let nativeChunkPipeline=Promise.resolve();
4468
4580
  function queueNativeOllamaChunk(chunk){
4469
4581
  nativeChunkPipeline=nativeChunkPipeline.then(
4470
4582
  async function processNativeOllamaChunk(){
4583
+ if(signal?.aborted){
4584
+ return;
4585
+ }
4586
+ const dataChunk=projectAIStreamChunk(chunk);
4587
+ if(dataChunk!==OMITTED_AI_STREAM_DATA){
4588
+ await dataChunkHandler(dataChunk,id);
4589
+ }
4471
4590
  if(signal?.aborted){
4472
4591
  return;
4473
4592
  }
4474
4593
  const message=chunk?.message||{};
4594
+ if(isPlainAIRecord(message)&&Object.hasOwn(message,'tool_calls')){
4595
+ const observed=normalizeAIStreamToolCallObservation(
4596
+ {message:{tool_calls:adaptNativeToolCalls(message.tool_calls)}},
4597
+ 'The native Ollama stream'
4598
+ );
4599
+ if(
4600
+ streamedNativeToolCalls
4601
+ &&(
4602
+ streamedNativeToolCalls.length!==observed.length
4603
+ ||streamedNativeToolCalls.some(
4604
+ (call,index)=>!sameAIStructuralToolCall(call,observed[index])
4605
+ )
4606
+ )
4607
+ ){
4608
+ throw aiStructuralError(
4609
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4610
+ 'The native Ollama stream changed its complete structural tool calls.'
4611
+ );
4612
+ }
4613
+ streamedNativeToolCalls=observed;
4614
+ }
4475
4615
  const thinking=seeThinking
4476
4616
  ?String(message.thinking||'')
4477
4617
  :'';
@@ -4486,8 +4626,6 @@ class AI {
4486
4626
  nativeContent+=content;
4487
4627
  await streamHandler(content,`M-${id}`,false);
4488
4628
  }
4489
-
4490
- receiveNativeToolCalls(message);
4491
4629
  }
4492
4630
  );
4493
4631
  return nativeChunkPipeline;
@@ -4519,14 +4657,16 @@ class AI {
4519
4657
  if(signal?.aborted){
4520
4658
  throw normalizeAIRequestAbort();
4521
4659
  }
4660
+ const nativeMessage=isPlainAIRecord(nativeResponse?.message)
4661
+ ?nativeResponse.message
4662
+ :{};
4522
4663
  const nativeCompletion=this.#openAICompatibleOllamaResponse(
4523
4664
  {
4524
4665
  ...nativeResponse,
4525
4666
  message:{
4526
- ...(nativeResponse?.message??{}),
4527
- content:typeof nativeResponse?.message?.content==='string'
4528
- &&nativeResponse.message.content
4529
- ?nativeResponse.message.content
4667
+ ...nativeMessage,
4668
+ content:Object.hasOwn(nativeMessage,'content')
4669
+ ?nativeMessage.content
4530
4670
  :nativeContent
4531
4671
  }
4532
4672
  },
@@ -4535,31 +4675,16 @@ class AI {
4535
4675
  const structuralToolCalls=normalizeAICompletionToolCalls(
4536
4676
  nativeCompletion
4537
4677
  );
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
- }
4678
+ assertAIStreamToolCallCorrelation(
4679
+ streamedNativeToolCalls??[],
4680
+ structuralToolCalls,
4681
+ 'The native Ollama stream'
4682
+ );
4559
4683
  this.#assertRequiredOllamaToolCall(
4560
4684
  structuralToolCalls,
4561
4685
  tool_choice
4562
4686
  );
4687
+ await dataResultHandler(nativeCompletion,id);
4563
4688
  for(const call of structuralToolCalls){
4564
4689
  if(signal?.aborted){
4565
4690
  throw normalizeAIRequestAbort();
@@ -4568,9 +4693,6 @@ class AI {
4568
4693
  }
4569
4694
 
4570
4695
  const nativeResult=this.#providerCompletionOutput(nativeCompletion);
4571
- if(structuralToolCalls.length&&!nativeContent){
4572
- await streamHandler('',`M-${id}`,false);
4573
- }
4574
4696
  if(finishSpeech){
4575
4697
  this.finishTTS();
4576
4698
  }
@@ -4615,15 +4737,14 @@ class AI {
4615
4737
 
4616
4738
  await this.#assertResponseOK(response);
4617
4739
 
4618
- let chunkString='';
4619
- let chunkCache='';
4740
+ let sseBuffer='';
4741
+ const completeToolCallsByChoice=new Map();
4620
4742
  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;
4743
+ const streamedChoicesByIndex=new Map();
4744
+ const streamedChoiceOrder=[];
4745
+ let selectedChoiceIndex=null;
4746
+ const streamMetadata={};
4747
+ let receivedSseDone=false;
4627
4748
  const decoder = new TextDecoder('utf-8');
4628
4749
  //alert(1)
4629
4750
  const reader=response.body?.getReader?.();
@@ -4632,23 +4753,30 @@ class AI {
4632
4753
  throw new TypeError('Streaming response body is not readable');
4633
4754
  }
4634
4755
 
4635
- function receiveStreamedToolCalls(toolCalls=[],choicePosition=0){
4636
- const streamedToolCalls=streamedToolCallsByChoice.get(choicePosition)
4756
+ function receiveStreamedToolCalls(toolCalls=[],choiceIndex=0){
4757
+ const streamedToolCalls=streamedToolCallsByChoice.get(choiceIndex)
4637
4758
  ||new Map();
4638
4759
  for(let position=0;position<toolCalls.length;position++){
4639
4760
  const toolCall=toolCalls[position]||{};
4640
4761
  const toolFunction=toolCall.function||{};
4641
- const key=`position:${Number.isInteger(toolCall.index)
4642
- ?toolCall.index
4643
- :position}`;
4644
- const record=streamedToolCalls.get(key)||{
4762
+ if(
4763
+ toolCall.index!==undefined
4764
+ &&(!Number.isSafeInteger(toolCall.index)||toolCall.index<0)
4765
+ ){
4766
+ throw aiStructuralError(
4767
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4768
+ `AI stream choice ${choiceIndex} contains an invalid structural tool-call index.`
4769
+ );
4770
+ }
4771
+ const toolIndex=toolCall.index??position;
4772
+ const record=streamedToolCalls.get(toolIndex)||{
4645
4773
  arguments:'',
4646
4774
  id:'',
4775
+ index:toolIndex,
4647
4776
  invalidArguments:false,
4648
4777
  invalidIdentity:false,
4649
4778
  invalidName:false,
4650
4779
  name:'',
4651
- order:streamedToolCalls.size,
4652
4780
  type:''
4653
4781
  };
4654
4782
 
@@ -4686,111 +4814,224 @@ class AI {
4686
4814
  record.invalidArguments=true;
4687
4815
  }
4688
4816
 
4689
- streamedToolCalls.set(key,record);
4817
+ streamedToolCalls.set(toolIndex,record);
4690
4818
  }
4691
- streamedToolCallsByChoice.set(choicePosition,streamedToolCalls);
4819
+ streamedToolCallsByChoice.set(choiceIndex,streamedToolCalls);
4692
4820
  }
4693
4821
 
4694
- try{
4695
- while(true){
4696
- const {done,value:chunk}=await reader.read();
4697
-
4698
- if(signal?.aborted){
4699
- throw normalizeAIRequestAbort();
4822
+ function receiveStreamedChoice(choice={},choicePosition=0){
4823
+ if(
4824
+ choice.index!==undefined
4825
+ &&(!Number.isSafeInteger(choice.index)||choice.index<0)
4826
+ ){
4827
+ throw aiStructuralError(
4828
+ 'AI_CHAT_INVALID_RESPONSE',
4829
+ `AI stream choice ${choicePosition+1} has an invalid index.`
4830
+ );
4831
+ }
4832
+ const choiceIndex=choice.index??choicePosition;
4833
+ if(!streamedChoicesByIndex.has(choiceIndex)){
4834
+ streamedChoiceOrder.push(choiceIndex);
4835
+ if(selectedChoiceIndex===null) selectedChoiceIndex=choiceIndex;
4836
+ }
4837
+ const record=streamedChoicesByIndex.get(choiceIndex)||{
4838
+ choice:{index:choiceIndex},
4839
+ message:{role:'assistant'}
4840
+ };
4841
+ for(const [key,value] of Object.entries(choice)){
4842
+ if(key!=='delta'&&key!=='message') record.choice[key]=value;
4843
+ }
4844
+ record.choice.index=choiceIndex;
4845
+ const delta=isPlainAIRecord(choice.delta)?choice.delta:null;
4846
+ const completeMessage=isPlainAIRecord(choice.message)?choice.message:null;
4847
+ for(const [source,replaceText] of [
4848
+ [delta,false],
4849
+ [completeMessage,true]
4850
+ ]){
4851
+ if(!source) continue;
4852
+ for(const [key,value] of Object.entries(source)){
4853
+ if(isAIStreamStructuralKey(key)) continue;
4854
+ if(key==='content'||key==='reasoning'||key==='reasoning_content'){
4855
+ if(
4856
+ !replaceText
4857
+ &&typeof value==='string'
4858
+ &&typeof record.message[key]==='string'
4859
+ ){
4860
+ record.message[key]+=value;
4861
+ }else{
4862
+ record.message[key]=value;
4863
+ }
4864
+ continue;
4865
+ }
4866
+ record.message[key]=value;
4700
4867
  }
4868
+ }
4869
+ if(typeof record.message.role!=='string'||!record.message.role){
4870
+ record.message.role='assistant';
4871
+ }
4872
+ if(!Object.hasOwn(record.message,'content')) record.message.content='';
4873
+ streamedChoicesByIndex.set(choiceIndex,record);
4874
+ return choiceIndex;
4875
+ }
4701
4876
 
4702
- if(done){
4703
- break;
4877
+ async function receiveStreamedResponse(streamedResponse){
4878
+ if(!isPlainAIRecord(streamedResponse)){
4879
+ throw aiStructuralError(
4880
+ 'AI_CHAT_INVALID_RESPONSE',
4881
+ 'The AI stream returned a non-object event payload.'
4882
+ );
4883
+ }
4884
+ const dataChunk=projectAIStreamChunk(streamedResponse);
4885
+ if(dataChunk!==OMITTED_AI_STREAM_DATA){
4886
+ await dataChunkHandler(dataChunk,id);
4887
+ }
4888
+ for(const [key,value] of Object.entries(streamedResponse)){
4889
+ if(key!=='choices') streamMetadata[key]=value;
4890
+ }
4891
+ const choices=Array.isArray(streamedResponse.choices)
4892
+ ?streamedResponse.choices
4893
+ :[];
4894
+ for(let choicePosition=0;choicePosition<choices.length;choicePosition+=1){
4895
+ const choice=choices[choicePosition];
4896
+ if(!isPlainAIRecord(choice)){
4897
+ throw aiStructuralError(
4898
+ 'AI_CHAT_INVALID_RESPONSE',
4899
+ `AI stream choice ${choicePosition+1} is not an object.`
4900
+ );
4704
4901
  }
4902
+ const choiceIndex=receiveStreamedChoice(choice,choicePosition);
4903
+ if(choice.delta!==undefined&&!isPlainAIRecord(choice.delta)){
4904
+ throw aiStructuralError(
4905
+ 'AI_CHAT_INVALID_RESPONSE',
4906
+ `AI stream choice ${choicePosition+1} contains an invalid delta.`
4907
+ );
4908
+ }
4909
+ if(choice.message!==undefined&&!isPlainAIRecord(choice.message)){
4910
+ throw aiStructuralError(
4911
+ 'AI_CHAT_INVALID_RESPONSE',
4912
+ `AI stream choice ${choicePosition+1} contains an invalid terminal message.`
4913
+ );
4914
+ }
4915
+ const choiceDelta=choice.delta??{};
4916
+ const choiceMessage=choice.message??null;
4917
+ if(Object.hasOwn(choiceDelta,'tool_calls')){
4918
+ if(!Array.isArray(choiceDelta.tool_calls)){
4919
+ throw aiStructuralError(
4920
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4921
+ `AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
4922
+ );
4923
+ }
4924
+ receiveStreamedToolCalls(choiceDelta.tool_calls,choiceIndex);
4925
+ }
4926
+ if(choiceMessage&&Object.hasOwn(choiceMessage,'tool_calls')){
4927
+ if(!Array.isArray(choiceMessage.tool_calls)){
4928
+ throw aiStructuralError(
4929
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4930
+ `AI stream choice ${choicePosition+1} contains invalid terminal structural tool-call data.`
4931
+ );
4932
+ }
4933
+ completeToolCallsByChoice.set(
4934
+ choiceIndex,
4935
+ normalizeAIStreamToolCallObservation(
4936
+ {message:{tool_calls:choiceMessage.tool_calls}},
4937
+ `AI stream choice ${choicePosition+1} terminal message`
4938
+ )
4939
+ );
4940
+ }
4941
+ const content=typeof choiceDelta.content==='string'
4942
+ ?choiceDelta.content
4943
+ :'';
4944
+ let value=content;
4945
+ let reasoning='';
4946
+ if(seeThinking){
4947
+ reasoning=typeof choiceDelta.reasoning_content==='string'
4948
+ ?choiceDelta.reasoning_content
4949
+ :typeof choiceDelta.reasoning==='string'
4950
+ ?choiceDelta.reasoning
4951
+ :'';
4952
+ }
4953
+ isThinking=Boolean(reasoning);
4954
+ if(reasoning) value=reasoning;
4955
+ if(value) await streamHandler(value,`M-${id}`,isThinking);
4956
+ }
4957
+ }
4705
4958
 
4706
- //alert(2) //const data=String.fromCharCode.apply(null, chunk).trim().replaceAll('data: ','');
4707
- const data = decoder.decode(chunk, { stream: true})?.trim()?.replaceAll('data: ','');
4708
- const lines=data.split('\n\n');
4709
- //alert(3)
4710
- //console.log(lines);
4959
+ function sseDataText(eventText){
4960
+ const values=[];
4961
+ for(const line of eventText.split(/\r\n|\r|\n/)){
4962
+ if(!line||line.startsWith(':')) continue;
4963
+ const separator=line.indexOf(':');
4964
+ const field=separator<0?line:line.slice(0,separator);
4965
+ if(field!=='data') continue;
4966
+ let value=separator<0?'':line.slice(separator+1);
4967
+ if(value.startsWith(' ')) value=value.slice(1);
4968
+ values.push(value);
4969
+ }
4970
+ return values.length?values.join('\n'):null;
4971
+ }
4711
4972
 
4712
- for(const eventData of lines){
4713
- chunkCache+=eventData;
4973
+ async function receiveSseEvent(eventText){
4974
+ const eventData=sseDataText(eventText);
4975
+ if(eventData===null) return false;
4976
+ if(eventData==='[DONE]'){
4977
+ receivedSseDone=true;
4978
+ return true;
4979
+ }
4980
+ if(receivedSseDone) return true;
4981
+ let streamedResponse;
4982
+ try{
4983
+ streamedResponse=JSON.parse(eventData);
4984
+ }catch(cause){
4985
+ throw aiStructuralError(
4986
+ 'AI_CHAT_INVALID_RESPONSE',
4987
+ 'The AI stream returned malformed JSON event data.',
4988
+ cause
4989
+ );
4990
+ }
4991
+ await receiveStreamedResponse(streamedResponse);
4992
+ return false;
4993
+ }
4714
4994
 
4715
- if(chunkCache.trim()==='[DONE]'){
4716
- chunkCache='';
4717
- continue;
4718
- }
4995
+ async function drainSseBuffer(final=false){
4996
+ while(true){
4997
+ const separator=sseBuffer.match(/(?:\r\n|\r|\n){2}/);
4998
+ if(!separator) break;
4999
+ const eventText=sseBuffer.slice(0,separator.index);
5000
+ sseBuffer=sseBuffer.slice(separator.index+separator[0].length);
5001
+ if(await receiveSseEvent(eventText)){
5002
+ sseBuffer='';
5003
+ return true;
5004
+ }
5005
+ }
5006
+ if(final&&sseBuffer.length){
5007
+ const eventText=sseBuffer;
5008
+ sseBuffer='';
5009
+ return receiveSseEvent(eventText);
5010
+ }
5011
+ return receivedSseDone;
5012
+ }
4719
5013
 
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
- }
5014
+ try{
5015
+ while(!receivedSseDone){
5016
+ const {done,value:chunk}=await reader.read();
4742
5017
 
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;
4770
- }
5018
+ if(signal?.aborted){
5019
+ throw normalizeAIRequestAbort();
5020
+ }
4771
5021
 
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;
4791
- }
4792
- }
5022
+ if(done){
5023
+ break;
4793
5024
  }
5025
+ sseBuffer+=decoder.decode(chunk,{stream:true});
5026
+ if(await drainSseBuffer()) break;
5027
+ }
5028
+ if(receivedSseDone){
5029
+ await reader.cancel('[DONE]').catch(
5030
+ error=>console.error('Arcane SSE reader cleanup failed.',error)
5031
+ );
5032
+ }else{
5033
+ sseBuffer+=decoder.decode();
5034
+ await drainSseBuffer(true);
4794
5035
  }
4795
5036
  }catch(error){
4796
5037
  if(isAIRequestAbort(error,signal)){
@@ -4802,17 +5043,25 @@ class AI {
4802
5043
  }
4803
5044
 
4804
5045
  const structuralToolCallsByChoice=new Map();
4805
- const orderedStreamChoices=[...streamedToolCallsByChoice.entries()].sort(
4806
- function sortStreamChoices(a,b){
4807
- return a[0]-b[0];
4808
- }
4809
- );
4810
- for(const [choicePosition,toolCallRecords] of orderedStreamChoices){
5046
+ const toolChoiceIndexes=new Set([
5047
+ ...streamedToolCallsByChoice.keys(),
5048
+ ...completeToolCallsByChoice.keys()
5049
+ ]);
5050
+ for(const choiceIndex of [...toolChoiceIndexes].sort((left,right)=>left-right)){
5051
+ const toolCallRecords=streamedToolCallsByChoice.get(choiceIndex)??new Map();
4811
5052
  const orderedToolCalls=[...toolCallRecords.values()].sort(
4812
5053
  function sortStreamedToolCalls(a,b){
4813
- return a.order-b.order;
5054
+ return a.index-b.index;
4814
5055
  }
4815
5056
  );
5057
+ for(let index=0;index<orderedToolCalls.length;index+=1){
5058
+ if(orderedToolCalls[index].index!==index){
5059
+ throw aiStructuralError(
5060
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5061
+ `AI stream choice ${choiceIndex} omitted an ordered structural tool-call index.`
5062
+ );
5063
+ }
5064
+ }
4816
5065
  const rawToolCalls=orderedToolCalls.map(
4817
5066
  function completeStreamedToolCall(toolCall,index){
4818
5067
  if(
@@ -4822,7 +5071,7 @@ class AI {
4822
5071
  ){
4823
5072
  throw aiStructuralError(
4824
5073
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
4825
- `AI stream choice ${choicePosition+1} structural tool call ${index+1} changed an exact field.`
5074
+ `AI stream choice ${choiceIndex} structural tool call ${index+1} changed an exact field.`
4826
5075
  );
4827
5076
  }
4828
5077
  return {
@@ -4835,58 +5084,79 @@ class AI {
4835
5084
  };
4836
5085
  }
4837
5086
  );
4838
- structuralToolCallsByChoice.set(
4839
- choicePosition,
4840
- normalizeAIStreamToolCallObservation(
5087
+ const streamedCalls=normalizeAIStreamToolCallObservation(
4841
5088
  {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'
5089
+ `AI stream choice ${choiceIndex}`
5090
+ );
5091
+ const completeCalls=completeToolCallsByChoice.get(choiceIndex);
5092
+ if(completeCalls!==undefined){
5093
+ if(
5094
+ streamedCalls.length
5095
+ &&(
5096
+ streamedCalls.length!==completeCalls.length
5097
+ ||streamedCalls.some(
5098
+ (call,index)=>!sameAICanonicalToolCall(call,completeCalls[index])
5099
+ )
5100
+ )
5101
+ ){
5102
+ throw aiStructuralError(
5103
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5104
+ `AI stream choice ${choiceIndex} changed or omitted its terminal structural tool calls.`
5105
+ );
5106
+ }
5107
+ structuralToolCallsByChoice.set(choiceIndex,completeCalls);
5108
+ }else{
5109
+ structuralToolCallsByChoice.set(choiceIndex,streamedCalls);
4861
5110
  }
4862
- ];
4863
- for(const [choicePosition,toolCalls] of structuralToolCallsByChoice){
4864
- if(choicePosition===0){
4865
- continue;
5111
+ }
5112
+ const completionChoiceIndexes=[...streamedChoiceOrder];
5113
+ for(const choiceIndex of structuralToolCallsByChoice.keys()){
5114
+ if(!completionChoiceIndexes.includes(choiceIndex)){
5115
+ completionChoiceIndexes.push(choiceIndex);
4866
5116
  }
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
- });
4876
5117
  }
5118
+ if(!completionChoiceIndexes.length) completionChoiceIndexes.push(0);
5119
+ const selectedTerminalChoiceIndex=selectedChoiceIndex??completionChoiceIndexes[0];
5120
+ const structuralToolCalls=structuralToolCallsByChoice.get(selectedTerminalChoiceIndex)||[];
5121
+ const completionChoices=completionChoiceIndexes
5122
+ .map(function completeLegacyStreamChoice(choicePosition){
5123
+ const retained=streamedChoicesByIndex.get(choicePosition)||{
5124
+ choice:{index:choicePosition},
5125
+ message:{role:'assistant'}
5126
+ };
5127
+ const toolCalls=structuralToolCallsByChoice.get(choicePosition)||[];
5128
+ const finishReason=retained.choice.finish_reason;
5129
+ const hasToolCalls=structuralToolCallsByChoice.has(choicePosition);
5130
+ return {
5131
+ ...retained.choice,
5132
+ index:Number.isSafeInteger(retained.choice.index)
5133
+ &&retained.choice.index>=0
5134
+ ?retained.choice.index
5135
+ :choicePosition,
5136
+ message:{
5137
+ ...retained.message,
5138
+ role:'assistant',
5139
+ content:Object.hasOwn(retained.message,'content')
5140
+ ?retained.message.content
5141
+ :'',
5142
+ ...(hasToolCalls?{tool_calls:toolCalls}:{})
5143
+ },
5144
+ finish_reason:Object.hasOwn(retained.choice,'finish_reason')
5145
+ ?finishReason
5146
+ :toolCalls.length?'tool_calls':'stop'
5147
+ };
5148
+ });
4877
5149
  const completion={
4878
- id:typeof streamId==='string'&&streamId?streamId:`legacy-${id}`,
4879
- object:typeof streamObject==='string'&&streamObject
4880
- ?streamObject
5150
+ ...streamMetadata,
5151
+ id:Object.hasOwn(streamMetadata,'id')?streamMetadata.id:`legacy-${id}`,
5152
+ object:Object.hasOwn(streamMetadata,'object')
5153
+ ?streamMetadata.object
4881
5154
  :'chat.completion',
4882
- created:Number.isSafeInteger(streamCreated)
4883
- ?streamCreated
5155
+ created:Object.hasOwn(streamMetadata,'created')
5156
+ ?streamMetadata.created
4884
5157
  :Math.floor(Date.now()/1000),
4885
- model:typeof streamModel==='string'&&streamModel
4886
- ?streamModel
4887
- :this.model,
4888
- choices:completionChoices,
4889
- ...(isPlainAIRecord(streamUsage)?{usage:streamUsage}:{})
5158
+ model:Object.hasOwn(streamMetadata,'model')?streamMetadata.model:this.model,
5159
+ choices:completionChoices
4890
5160
  };
4891
5161
  const terminalToolCalls=normalizeAICompletionToolCalls(completion);
4892
5162
  assertAIStreamToolCallCorrelation(
@@ -4894,6 +5164,7 @@ class AI {
4894
5164
  terminalToolCalls,
4895
5165
  'The legacy HTTP stream'
4896
5166
  );
5167
+ await dataResultHandler(completion,id);
4897
5168
  for(const call of terminalToolCalls){
4898
5169
  if(signal?.aborted){
4899
5170
  throw normalizeAIRequestAbort();
@@ -4901,9 +5172,6 @@ class AI {
4901
5172
  await earlyFunctionTrigger(call,`M-${id}`);
4902
5173
  }
4903
5174
  const streamResult=this.#providerCompletionOutput(completion);
4904
- if(structuralToolCalls.length&&!chunkString){
4905
- await streamHandler('',`M-${id}`,false);
4906
- }
4907
5175
  if(finishSpeech){
4908
5176
  this.finishTTS();
4909
5177
  }
@@ -4930,7 +5198,7 @@ class AI {
4930
5198
  localOnly=false,
4931
5199
  tools=[],
4932
5200
  toolChoice='auto',
4933
- parallelToolCalls=false,
5201
+ parallelToolCalls,
4934
5202
  id=Date.now(),
4935
5203
  signal=null,
4936
5204
  onRequest=function ignoreFetchRequest(){},
@@ -4966,7 +5234,7 @@ class AI {
4966
5234
  structuredOutput,
4967
5235
  tools,
4968
5236
  toolChoice,
4969
- parallelToolCalls,
5237
+ ...(parallelToolCalls!==undefined?{parallelToolCalls}:{}),
4970
5238
  id,
4971
5239
  ...(maxOutputTokens!==undefined
4972
5240
  ?{maxTokens:maxOutputTokens}
@@ -5023,7 +5291,7 @@ class AI {
5023
5291
  structuredOutput=false,
5024
5292
  tools=[],
5025
5293
  tool_choice='auto',
5026
- parallel_tool_calls=false,
5294
+ parallel_tool_calls,
5027
5295
  id=Date.now(),
5028
5296
  requestHandler=function ignoreFetchRequest(){},
5029
5297
  signal=null,
@@ -5062,7 +5330,7 @@ class AI {
5062
5330
  structuredOutput=false,
5063
5331
  tools=[],
5064
5332
  tool_choice='auto',
5065
- parallel_tool_calls=false,
5333
+ parallel_tool_calls,
5066
5334
  id=Date.now(),
5067
5335
  requestHandler=function ignoreFetchRequest(){},
5068
5336
  signal=null,
@@ -5095,7 +5363,9 @@ class AI {
5095
5363
  if(tools.length){
5096
5364
  request.tools=tools;
5097
5365
  request.tool_choice=tool_choice;
5098
- request.parallel_tool_calls=parallel_tool_calls;
5366
+ if(parallel_tool_calls!==undefined){
5367
+ request.parallel_tool_calls=parallel_tool_calls;
5368
+ }
5099
5369
  }
5100
5370
 
5101
5371
  if(this.llmService==='OLLAMA'&&this.reasoningEffort){
@@ -6256,11 +6526,10 @@ function installAIUserReadyRegistration(){
6256
6526
  });
6257
6527
  unsubscribe=arcaneEvents.subscribe(
6258
6528
  'user-entity-loaded',
6259
- function initializeAIFromCanonicalUser(event){
6260
- if(event?.detail?.user&&event.detail.user!==window.user)return;
6529
+ function initializeAIFromCanonicalUser(){
6261
6530
  if(!window.user?.ready)return;
6262
6531
  registration.dispose();
6263
- instantiateAI(event);
6532
+ instantiateAI();
6264
6533
  }
6265
6534
  );
6266
6535
  Object.defineProperty(
@@ -6270,20 +6539,18 @@ function installAIUserReadyRegistration(){
6270
6539
  value:registration,
6271
6540
  configurable:true,
6272
6541
  enumerable:false,
6273
- writable:false
6542
+ writable:true
6274
6543
  }
6275
6544
  );
6545
+ if(window.user?.ready){
6546
+ registration.dispose();
6547
+ instantiateAI();
6548
+ return null;
6549
+ }
6276
6550
  return registration;
6277
6551
  }
6278
6552
 
6279
- function instantiateAI(event) {
6280
- if(
6281
- event?.detail?.user
6282
- &&event.detail.user!==window.user
6283
- ){
6284
- return;
6285
- }
6286
-
6553
+ function instantiateAI() {
6287
6554
  if(!window.user?.ready){
6288
6555
  return;
6289
6556
  }