arcane-os 0.3.4 → 0.3.5

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 +12 -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 +466 -206
  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 +765 -135
  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 +34 -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
@@ -1,6 +1,8 @@
1
1
  const FORBIDDEN_REQUEST_FIELDS=new Set([
2
2
  'messages',
3
3
  'onChunk',
4
+ 'onDataChunk',
5
+ 'onDataResult',
4
6
  'onResponse',
5
7
  'onToolCall',
6
8
  'signal',
@@ -8,10 +10,9 @@ const FORBIDDEN_REQUEST_FIELDS=new Set([
8
10
  ]);
9
11
 
10
12
  function isPlainRecord(value){
11
- return Boolean(value)
12
- &&typeof value==='object'
13
- &&!Array.isArray(value)
14
- &&Object.getPrototypeOf(value)===Object.prototype;
13
+ if(!value||typeof value!=='object'||Array.isArray(value))return false;
14
+ const prototype=Object.getPrototypeOf(value);
15
+ return prototype===Object.prototype||prototype===null;
15
16
  }
16
17
 
17
18
  function coded(error,code){
@@ -137,11 +138,10 @@ function normalizeRequestOptions(value,label){
137
138
  const forbidden=Object.keys(value).find(key=>FORBIDDEN_REQUEST_FIELDS.has(key));
138
139
  if(forbidden) throw new TypeError(`${label}.${forbidden} is managed by the chat session.`);
139
140
  requireToolMessageSchemas(value.tools,`${label}.tools`);
140
- if(value.parallelToolCalls===true||value.parallel_tool_calls===true){
141
- throw coded(
142
- new TypeError(`${label} cannot enable parallel structural tool calls.`),
143
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
144
- );
141
+ for(const key of ['parallelToolCalls','parallel_tool_calls']){
142
+ if(Object.hasOwn(value,key)&&typeof value[key]!=='boolean'){
143
+ throw new TypeError(`${label}.${key} must be a boolean when provided.`);
144
+ }
145
145
  }
146
146
  return {...value};
147
147
  }
@@ -149,13 +149,11 @@ function normalizeRequestOptions(value,label){
149
149
  export function normalizeStructuralToolCall(call,label='Structural tool call'){
150
150
  try{
151
151
  if(!isPlainRecord(call)) throw new TypeError(`${label} must be a plain object.`);
152
- assertMessageKeys(call,new Set(['function','id','type']),label);
153
152
  if(typeof call.id!=='string'||!call.id.trim()){
154
153
  throw new TypeError(`${label}.id must contain text.`);
155
154
  }
156
155
  if(call.type!=='function') throw new TypeError(`${label}.type must be function.`);
157
156
  if(!isPlainRecord(call.function)) throw new TypeError(`${label}.function must be a plain object.`);
158
- assertMessageKeys(call.function,new Set(['arguments','name']),`${label}.function`);
159
157
  if(typeof call.function.name!=='string'||!call.function.name.trim()){
160
158
  throw new TypeError(`${label}.function.name must contain text.`);
161
159
  }
@@ -164,7 +162,9 @@ export function normalizeStructuralToolCall(call,label='Structural tool call'){
164
162
  throw coded(error,'AI_CHAT_INVALID_TOOL_CALL');
165
163
  }
166
164
  return {
165
+ ...call,
167
166
  function:{
167
+ ...call.function,
168
168
  arguments:call.function.arguments,
169
169
  name:call.function.name,
170
170
  },
@@ -178,12 +178,6 @@ function normalizeToolCalls(value,label){
178
178
  if(!Array.isArray(value)){
179
179
  throw new TypeError(`${label} must be an array of structural tool calls.`);
180
180
  }
181
- if(value.length>1){
182
- throw coded(
183
- new TypeError(`${label} must contain at most one structural tool call.`),
184
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
185
- );
186
- }
187
181
  const ids=new Set();
188
182
  return value.map((call,index)=>{
189
183
  const normalized=normalizeStructuralToolCall(call,`${label}[${index}]`);
@@ -202,13 +196,6 @@ function normalizeMessage(value,label,allowedRoles){
202
196
  if(!isPlainRecord(value)||!allowedRoles.has(value.role)){
203
197
  throw new TypeError(`${label} has an unsupported role.`);
204
198
  }
205
- const allowed=new Set(['content','role']);
206
- if(value.role==='assistant'){
207
- allowed.add('reasoning_content');
208
- allowed.add('tool_calls');
209
- }
210
- if(value.role==='tool') allowed.add('tool_call_id');
211
- assertMessageKeys(value,allowed,label);
212
199
  const toolCalls=value.role==='assistant'
213
200
  ?normalizeToolCalls(value.tool_calls,`${label}.tool_calls`)
214
201
  :null;
@@ -225,7 +212,7 @@ function normalizeMessage(value,label,allowedRoles){
225
212
  &&value.reasoning_content.length
226
213
  );
227
214
  let content;
228
- if(value.role==='assistant'&&(toolCalls||hasReasoning)){
215
+ if(value.role==='assistant'&&(toolCalls?.length||hasReasoning)){
229
216
  if(value.content===undefined||value.content===null||value.content==='') content='';
230
217
  else content=contentText(value.content,`${label}.content`);
231
218
  }else{
@@ -236,6 +223,7 @@ function normalizeMessage(value,label,allowedRoles){
236
223
  toolCallId=contentText(value.tool_call_id,`${label}.tool_call_id`);
237
224
  }
238
225
  const normalized={
226
+ ...value,
239
227
  role:value.role,
240
228
  content,
241
229
  ...(value.role==='assistant'&&Object.hasOwn(value,'reasoning_content')
@@ -249,6 +237,7 @@ function normalizeMessage(value,label,allowedRoles){
249
237
 
250
238
  function cloneMessage(value){
251
239
  return {
240
+ ...value,
252
241
  role:value.role,
253
242
  content:value.content,
254
243
  ...(Object.hasOwn(value,'reasoning_content')
@@ -256,9 +245,8 @@ function cloneMessage(value){
256
245
  :{}),
257
246
  ...(value.tool_calls?{
258
247
  tool_calls:value.tool_calls.map(call=>({
248
+ ...call,
259
249
  function:{...call.function},
260
- id:call.id,
261
- type:call.type,
262
250
  }))
263
251
  }:{}),
264
252
  ...(value.tool_call_id?{tool_call_id:value.tool_call_id}:{}),
@@ -267,15 +255,15 @@ function cloneMessage(value){
267
255
 
268
256
  function publicMessage(value){
269
257
  return {
258
+ ...value,
270
259
  role:value.role,
271
260
  content:value.content,
272
261
  ...(Object.hasOwn(value,'reasoning_content')
273
262
  ?{reasoning_content:value.reasoning_content}
274
263
  :{}),
275
264
  ...(value.tool_calls?{tool_calls:value.tool_calls.map(call=>({
265
+ ...call,
276
266
  function:{...call.function},
277
- id:call.id,
278
- type:call.type,
279
267
  }))}:{}),
280
268
  ...(value.tool_call_id?{tool_call_id:value.tool_call_id}:{}),
281
269
  };
@@ -293,6 +281,31 @@ function normalizeInitialMessages(value){
293
281
  return messages;
294
282
  }
295
283
 
284
+ function normalizeInputMessages(value){
285
+ if(typeof value==='string'){
286
+ return [normalizeMessage(
287
+ {role:'user',content:value},
288
+ 'The user message',
289
+ new Set(['user']),
290
+ )];
291
+ }
292
+ if(Array.isArray(value)){
293
+ if(!value.length){
294
+ throw new TypeError('The request tool-result batch must not be empty.');
295
+ }
296
+ return value.map((item,index)=>normalizeMessage(
297
+ item,
298
+ `The request tool-result batch[${index}]`,
299
+ new Set(['tool']),
300
+ ));
301
+ }
302
+ return [normalizeMessage(
303
+ value,
304
+ 'The request message',
305
+ new Set(['tool','user']),
306
+ )];
307
+ }
308
+
296
309
  function message(role,content){
297
310
  return {role,content};
298
311
  }
@@ -334,10 +347,18 @@ function pendingToolCallIds(messages,validate=false){
334
347
  );
335
348
  }
336
349
  if(item.role==='assistant'&&item.tool_calls){
337
- pending.add(item.tool_calls[0].id);
350
+ for(const call of item.tool_calls){
351
+ if(pending.has(call.id)&&validate){
352
+ throw coded(
353
+ new TypeError(`Message ${index+1} repeats a pending assistant tool-call ID.`),
354
+ 'AI_CHAT_INCOHERENT_PERSISTENCE',
355
+ );
356
+ }
357
+ pending.add(call.id);
358
+ }
338
359
  }
339
360
  if(item.role==='tool'){
340
- if((pending.size!==1||!pending.has(item.tool_call_id))&&validate){
361
+ if(!pending.has(item.tool_call_id)&&validate){
341
362
  throw coded(
342
363
  new TypeError(`Message ${index+1} does not match the pending assistant tool call.`),
343
364
  'AI_CHAT_INCOHERENT_PERSISTENCE',
@@ -410,7 +431,6 @@ function normalizeOpenAICompatibleResponse(response){
410
431
  );
411
432
  }
412
433
  let responseMessage=null;
413
- let toolCallCount=0;
414
434
  for(let index=0;index<response.choices.length;index++){
415
435
  const candidate=response.choices[index];
416
436
  if(!isPlainRecord(candidate)){
@@ -423,21 +443,6 @@ function normalizeOpenAICompatibleResponse(response){
423
443
  candidate.message,
424
444
  {requireRole:true},
425
445
  );
426
- toolCallCount+=candidateMessage.tool_calls?.length??0;
427
- if(toolCallCount>1){
428
- throw coded(
429
- new TypeError('The chat provider completion contains parallel structural tool calls.'),
430
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
431
- );
432
- }
433
- if(index>0&&candidateMessage.tool_calls?.length){
434
- throw coded(
435
- new TypeError(
436
- 'The chat provider completion placed a structural tool call outside the selected choice.'
437
- ),
438
- 'AI_CHAT_INVALID_RESPONSE',
439
- );
440
- }
441
446
  if(index===0) responseMessage=candidateMessage;
442
447
  }
443
448
  const choice=response.choices[0];
@@ -586,17 +591,8 @@ export default class ConfiguredAIChatSession{
586
591
  if(!signalLike(options.signal)) throw new TypeError('signal must be an AbortSignal.');
587
592
  if(options.signal?.aborted) throw abortError();
588
593
  const turnRequest=normalizeRequestOptions(options.request,'request');
589
- const inputMessage=typeof input==='string'
590
- ?normalizeMessage(
591
- {role:'user',content:input},
592
- 'The user message',
593
- new Set(['user']),
594
- )
595
- :normalizeMessage(
596
- input,
597
- 'The request message',
598
- new Set(['tool','user']),
599
- );
594
+ const inputMessages=normalizeInputMessages(input);
595
+ const inputMessage=inputMessages[0];
600
596
  if(this.#pending){
601
597
  throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
602
598
  }
@@ -609,18 +605,27 @@ export default class ConfiguredAIChatSession{
609
605
  'AI_CHAT_TOOL_RESULT_REQUIRED',
610
606
  );
611
607
  }
612
- const toolCallIndex=inputMessage.role==='tool'
613
- ?matchingToolCallIndex(this.#conversation,inputMessage.tool_call_id)
614
- :-1;
615
- if(inputMessage.role==='tool'&&(
616
- pendingTools.size!==1
617
- ||!pendingTools.has(inputMessage.tool_call_id)
618
- ||toolCallIndex<0
619
- )){
620
- throw coded(
621
- new TypeError('The tool message does not match an assistant tool call in this session.'),
622
- 'AI_CHAT_INVALID_TOOL_MESSAGE',
623
- );
608
+ if(inputMessage.role==='tool'){
609
+ const submittedIds=new Set();
610
+ for(const item of inputMessages){
611
+ if(
612
+ submittedIds.has(item.tool_call_id)
613
+ ||!pendingTools.has(item.tool_call_id)
614
+ ||matchingToolCallIndex(this.#conversation,item.tool_call_id)<0
615
+ ){
616
+ throw coded(
617
+ new TypeError('A tool message does not match a pending assistant tool call in this session.'),
618
+ 'AI_CHAT_INVALID_TOOL_MESSAGE',
619
+ );
620
+ }
621
+ submittedIds.add(item.tool_call_id);
622
+ }
623
+ if(submittedIds.size!==pendingTools.size){
624
+ throw coded(
625
+ new TypeError('Every pending structural tool result must be supplied before provider continuation.'),
626
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
627
+ );
628
+ }
624
629
  }
625
630
  const context=inputMessage.role==='user'
626
631
  ?await this.#contextFor(inputMessage.content,options.signal)
@@ -629,7 +634,7 @@ export default class ConfiguredAIChatSession{
629
634
  const transientContext=context
630
635
  ?message('user',context)
631
636
  :null;
632
- const transientMessages=[...(transientContext?[transientContext]:[]),inputMessage];
637
+ const transientMessages=[...(transientContext?[transientContext]:[]),...inputMessages];
633
638
  const requestMessages=completeHistory(
634
639
  this.#systemPrompt,
635
640
  [...this.#conversation,...transientMessages],
@@ -642,11 +647,6 @@ export default class ConfiguredAIChatSession{
642
647
  ...(options.signal?{signal:options.signal}:{}),
643
648
  messages:requestMessages.map(publicMessage),
644
649
  };
645
- if(
646
- providerRequest.tools?.length
647
- &&providerRequest.parallelToolCalls===undefined
648
- &&providerRequest.parallel_tool_calls===undefined
649
- ) providerRequest.parallelToolCalls=false;
650
650
  providerResponse=await this.#chat(providerRequest);
651
651
  }catch(error){
652
652
  if(options.signal?.aborted) throw abortError();
@@ -657,7 +657,7 @@ export default class ConfiguredAIChatSession{
657
657
  const retainedConversation=this.#conversation.map(cloneMessage);
658
658
  const committed=completeHistory(
659
659
  this.#systemPrompt,
660
- [...retainedConversation,inputMessage,response.message],
660
+ [...retainedConversation,...inputMessages,response.message],
661
661
  );
662
662
  const nextConversation=committed.filter(item=>item.role!=='system');
663
663
  let settled=false;