arcane-os 0.1.2 → 0.2.0

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 (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/NOTICE +5 -3
  3. package/README.md +73 -24
  4. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
  5. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
  6. package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
  7. package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
  8. package/browser-runtime/ai/browser-speech-providers.mjs +475 -0
  9. package/browser-runtime/ai/browser-speech.mjs +9 -0
  10. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
  11. package/browser-runtime/ai/browser-wasm.mjs +46 -1
  12. package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
  13. package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
  14. package/browser-runtime/ai/model-controller.mjs +138 -12
  15. package/browser-runtime/ai/speech-worker-client.mjs +207 -0
  16. package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
  17. package/browser-runtime/ai/wllama/index.mjs +389 -0
  18. package/docs/architecture.md +132 -22
  19. package/docs/reference/README.md +1 -1
  20. package/docs/reference/ai/browser-wasm.md +101 -42
  21. package/docs/reference/availability-and-normalization.md +19 -5
  22. package/docs/reference/behavioral-testing.md +18 -5
  23. package/docs/reference/cli.md +2 -2
  24. package/docs/reference/inventory/package-api.json +14 -14
  25. package/docs/reference/protocols.md +4 -4
  26. package/docs/reference/sdk-api.md +68 -38
  27. package/docs/work-amplification.md +8 -4
  28. package/package.json +7 -3
  29. package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
  30. package/runtime/arcane/components/chat.html +280 -62
  31. package/runtime/arcane/components/speech.html +1113 -265
  32. package/runtime/arcane/entities/Chat.js +246 -43
  33. package/runtime/arcane/modules/AI.js +713 -162
  34. package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
  35. package/runtime/arcane/modules/AIRuntimeState.js +872 -0
  36. package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
  37. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -0
  38. package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
  39. package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
  40. package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
  41. package/schemas/arcane-lock.schema.json +6 -4
  42. package/src/cli/main.mjs +14 -2
  43. package/src/constants.mjs +1 -1
  44. package/src/dev-server.mjs +244 -13
  45. package/src/import-map.mjs +59 -1
  46. package/src/packager/core.mjs +2 -2
  47. package/src/runtime.mjs +14 -4
  48. package/src/sdk-browser-runtime.mjs +28 -75
  49. package/src/templates/workspace-template.mjs +4 -4
  50. package/src/toolchain.mjs +3 -0
  51. package/src/workspace-runtime.mjs +1 -1
  52. package/src/workspace.mjs +1 -1
@@ -6,6 +6,74 @@ import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
6
6
 
7
7
  const is = new Is(false);
8
8
 
9
+ function plainRecord(value){
10
+ return Boolean(value)
11
+ &&typeof value==='object'
12
+ &&!Array.isArray(value)
13
+ &&Object.getPrototypeOf(value)===Object.prototype;
14
+ }
15
+
16
+ function copyToolCalls(value){
17
+ if(value===undefined) return null;
18
+ if(!Array.isArray(value)||value.length!==1){
19
+ throw new TypeError('assistantMessage.tool_calls must contain exactly one structural tool call.');
20
+ }
21
+ const ids=new Set();
22
+ return value.map((call,index)=>{
23
+ if(!plainRecord(call)||!plainRecord(call.function)||call.type!=='function'){
24
+ throw new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
25
+ }
26
+ const id=String(call.id??'').trim();
27
+ const name=String(call.function.name??'').trim();
28
+ const argumentValue=call.function.arguments;
29
+ if(!id||id.length>128||ids.has(id)||!name||name.length>128||typeof argumentValue!=='string'){
30
+ throw new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
31
+ }
32
+ ids.add(id);
33
+ return {
34
+ function:{arguments:argumentValue,name},
35
+ id,
36
+ type:'function'
37
+ };
38
+ });
39
+ }
40
+
41
+ function turnMessage(value,label){
42
+ if(!plainRecord(value)||!['tool','user'].includes(value.role)||typeof value.content!=='string'){
43
+ throw new TypeError(`${label} must be a user or tool message.`);
44
+ }
45
+ if(value.role==='tool'){
46
+ const toolCallId=String(value.tool_call_id??'').trim();
47
+ if(!toolCallId||toolCallId.length>128) throw new TypeError(`${label}.tool_call_id is invalid.`);
48
+ return {content:value.content,role:'tool',tool_call_id:toolCallId};
49
+ }
50
+ return {content:value.content,role:'user'};
51
+ }
52
+
53
+ function pendingToolCallId(messages){
54
+ let pending=null;
55
+ for(const [index,message] of messages.entries()){
56
+ if(message.role==='user'&&pending){
57
+ throw new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`);
58
+ }
59
+ if(message.role==='assistant'&&message.tool_calls){
60
+ const calls=copyToolCalls(message.tool_calls);
61
+ if(pending){
62
+ throw new TypeError(`Chat message ${index+1} overlaps a pending tool call.`);
63
+ }
64
+ pending=calls[0].id;
65
+ }
66
+ if(message.role==='tool'){
67
+ const toolCallId=String(message.tool_call_id??'').trim();
68
+ if(!pending||pending!==toolCallId){
69
+ throw new TypeError(`Chat message ${index+1} does not match the pending tool call.`);
70
+ }
71
+ pending=null;
72
+ }
73
+ }
74
+ return pending;
75
+ }
76
+
9
77
  /**
10
78
  * Represents a single chat message.
11
79
  *
@@ -19,6 +87,8 @@ const is = new Is(false);
19
87
  * Internal persistence marker omitted from model-facing messages.
20
88
  * @property {boolean} [ui_hidden]
21
89
  * Internal persistence marker for messages intentionally omitted from chat UI.
90
+ * @property {boolean} [persistence_excluded]
91
+ * Internal marker for session-only messages omitted from durable chat and memory.
22
92
  * @property {Array<Object>} [tool_calls]
23
93
  * Assistant tool calls retained in the saved conversation log.
24
94
  * @property {string} [tool_call_id]
@@ -111,6 +181,9 @@ class ChatEntity{
111
181
  /** Serializes snapshot and append writes for this chat instance. */
112
182
  #persistenceQueue=Promise.resolve();
113
183
 
184
+ /** Serializes automatic memory updates without delaying the completed turn. */
185
+ #memoryQueue=Promise.resolve(false);
186
+
114
187
 
115
188
  /**
116
189
  * Creates a new chat session.
@@ -154,7 +227,15 @@ class ChatEntity{
154
227
  get messages(){
155
228
  return Object.freeze(this.#messages.map(function publicChatMessage(message){
156
229
  const copy={...message};
230
+ if(copy.tool_calls){
231
+ copy.tool_calls=Object.freeze(copyToolCalls(copy.tool_calls).map(call=>Object.freeze({
232
+ function:Object.freeze({...call.function}),
233
+ id:call.id,
234
+ type:call.type,
235
+ })));
236
+ }
157
237
  delete copy.memory_excluded;
238
+ delete copy.persistence_excluded;
158
239
  delete copy.ui_hidden;
159
240
  delete copy.timestamp;
160
241
  return Object.freeze(copy);
@@ -205,16 +286,22 @@ class ChatEntity{
205
286
  *
206
287
  * @param {string} text
207
288
  * Message content from the user.
208
- * @param {{hidden?:boolean}} options
289
+ * @param {{hidden?:boolean,persist?:boolean}} options
209
290
  * Hidden messages remain in the saved/model context but are not user-authored UI turns.
210
291
  */
211
- addUserMessage(text='',{hidden=false}={}){
292
+ addUserMessage(text='',{hidden=false,persist=true}={}){
212
293
  if(!is.string(text)){
213
294
  throw new Error('user message must be string');
214
295
  }
215
296
  if(!is.boolean(hidden)){
216
297
  throw new Error('hidden must be boolean');
217
298
  }
299
+ if(!is.boolean(persist)){
300
+ throw new Error('persist must be boolean');
301
+ }
302
+ if(pendingToolCallId(this.#messages)){
303
+ throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
304
+ }
218
305
 
219
306
  const message={
220
307
  role:'user',
@@ -227,7 +314,8 @@ class ChatEntity{
227
314
  }
228
315
 
229
316
  return this.#appendMessage(
230
- message
317
+ message,
318
+ {persist}
231
319
  );
232
320
  }
233
321
 
@@ -239,10 +327,10 @@ class ChatEntity{
239
327
  *
240
328
  * @param {string|number} text
241
329
  * Message content generated by the AI.
242
- * @param {{extractMemory?:boolean}} options
330
+ * @param {{extractMemory?:boolean,persist?:boolean}} options
243
331
  * Set extractMemory to false for deterministic application-authored messages.
244
332
  */
245
- addAIMessage(text='',{extractMemory=true}={}){
333
+ addAIMessage(text='',{extractMemory=true,persist=true}={}){
246
334
 
247
335
  if(!is.union(text,'string','number')){
248
336
  throw new Error('assistant message must be string or number');
@@ -250,11 +338,8 @@ class ChatEntity{
250
338
  if(!is.boolean(extractMemory)){
251
339
  throw new Error('extractMemory must be boolean');
252
340
  }
253
-
254
- if(extractMemory){
255
- void this.getMemoriesAboutUser().catch(function reportMemoryFailure(error){
256
- console.warn('Unable to update chat memory.',error);
257
- });
341
+ if(!is.boolean(persist)){
342
+ throw new Error('persist must be boolean');
258
343
  }
259
344
 
260
345
  const message={
@@ -266,21 +351,32 @@ class ChatEntity{
266
351
  message.memory_excluded=true;
267
352
  }
268
353
 
269
- return this.#appendMessage(
270
- message
271
- )
354
+ const appended=this.#appendMessage(message,{persist});
355
+ if(extractMemory&&persist&&this.persist){
356
+ return Promise.resolve(appended).then(result=>{
357
+ this.#queueMemoryUpdate(messages=>ai.fetch(messages));
358
+ return result;
359
+ });
360
+ }
361
+ return appended;
272
362
  }
273
363
 
274
364
  /**
275
365
  * Adds an assistant tool call and its result as one hidden, atomic log exchange.
276
366
  * The host application decides whether anything is rendered in the chat UI.
277
367
  */
278
- addToolExchange({id='',name='',arguments:argumentValue='',result=''}={}){
368
+ addToolExchange({id='',name='',arguments:argumentValue='',result='',persist=true}={}){
279
369
  const toolCallId=String(id).trim();
280
370
  const toolName=String(name).trim();
281
371
  if(!toolCallId||!toolName){
282
372
  throw new TypeError('Tool exchanges require an id and name.');
283
373
  }
374
+ if(!is.boolean(persist)){
375
+ throw new TypeError('persist must be boolean.');
376
+ }
377
+ if(pendingToolCallId(this.#messages)){
378
+ throw new TypeError('The pending structural tool result must be supplied before another tool exchange.');
379
+ }
284
380
 
285
381
  const serializedArguments=typeof argumentValue==='string'
286
382
  ?argumentValue
@@ -317,7 +413,68 @@ class ChatEntity{
317
413
  timestamp,
318
414
  memory_excluded:true
319
415
  }
320
- ]);
416
+ ],{persist});
417
+ }
418
+
419
+ /**
420
+ * Atomically adds one model request and its assistant response. Persistence
421
+ * failures remove both in-memory records so the caller can roll back its
422
+ * recurring provider context without divergence.
423
+ */
424
+ addTurn({
425
+ assistantMessage,
426
+ extractMemory=true,
427
+ memoryRequest=messages=>ai.fetch(messages),
428
+ messagePersist=true,
429
+ requestMessage,
430
+ responsePersist=messagePersist,
431
+ }={}){
432
+ const request=turnMessage(requestMessage,'requestMessage');
433
+ if(!plainRecord(assistantMessage)||assistantMessage.role!=='assistant'){
434
+ throw new TypeError('assistantMessage must be an assistant message.');
435
+ }
436
+ if(!is.boolean(messagePersist)||!is.boolean(responsePersist)||!is.boolean(extractMemory)){
437
+ throw new TypeError('Turn persistence and memory options must be boolean.');
438
+ }
439
+ if(typeof memoryRequest!=='function'){
440
+ throw new TypeError('memoryRequest must be a function.');
441
+ }
442
+ if(messagePersist!==responsePersist){
443
+ throw new TypeError('messagePersist and responsePersist must match for one coherent durable turn.');
444
+ }
445
+ const pendingTool=pendingToolCallId(this.#messages);
446
+ if(request.role==='user'&&pendingTool){
447
+ throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
448
+ }
449
+ if(request.role==='tool'&&pendingTool!==request.tool_call_id){
450
+ throw new TypeError('requestMessage does not match the pending structural tool call.');
451
+ }
452
+ const toolCalls=copyToolCalls(assistantMessage.tool_calls);
453
+ const assistantContent=assistantMessage.content??'';
454
+ if(!is.union(assistantContent,'string','number')||(!String(assistantContent)&&!toolCalls)){
455
+ throw new TypeError('assistantMessage must contain text or structural tool calls.');
456
+ }
457
+ const timestamp=Date.now();
458
+ const requestRecord={...request,timestamp};
459
+ const assistantRecord={
460
+ content:assistantContent,
461
+ role:'assistant',
462
+ timestamp,
463
+ ...(toolCalls?{tool_calls:toolCalls}:{})
464
+ };
465
+ if(request.role==='tool') requestRecord.memory_excluded=true;
466
+ if(!extractMemory||toolCalls) assistantRecord.memory_excluded=true;
467
+ if(!messagePersist) requestRecord.persistence_excluded=true;
468
+ if(!responsePersist) assistantRecord.persistence_excluded=true;
469
+
470
+ const appended=this.#appendMessages([requestRecord,assistantRecord],{prepared:true});
471
+ if(extractMemory&&messagePersist&&responsePersist&&!toolCalls&&this.persist){
472
+ return Promise.resolve(appended).then(result=>{
473
+ this.#queueMemoryUpdate(memoryRequest);
474
+ return result;
475
+ });
476
+ }
477
+ return appended;
321
478
  }
322
479
 
323
480
  /**
@@ -337,8 +494,9 @@ class ChatEntity{
337
494
  this.fileName
338
495
  );
339
496
 
340
- if(!content){
341
- this.#messages=[];
497
+ if(!content||(Array.isArray(content)&&content.length===0)){
498
+ const systemMessage=this.#messages.find(message=>message.role==='system');
499
+ this.#messages=systemMessage?[systemMessage]:[];
342
500
  this.#persistedMessageCount=0;
343
501
  this.#saved=true;
344
502
  return this.messages;
@@ -351,26 +509,44 @@ class ChatEntity{
351
509
  .map(row=>row.trim())
352
510
  .filter(Boolean)
353
511
  .map(row=>JSON.parse(row));
354
- this.#persistedMessageCount=this.#messages.length;
512
+ this.#persistedMessageCount=this.#durableMessages().length;
355
513
  this.#saved=true;
356
514
 
357
515
  return this.messages;
358
516
  }
359
517
 
360
- async getMemoriesAboutUser(){
361
- if(!hasUserEntry(this.#messages)){
518
+ async getMemoriesAboutUser({request=messages=>ai.fetch(messages)}={}){
519
+ if(typeof request!=='function'){
520
+ throw new TypeError('Memory request must be a function.');
521
+ }
522
+ return this.#writeMemory(
523
+ this.#durableMessages().map(message=>({...message})),
524
+ request
525
+ );
526
+ }
527
+
528
+ /** Waits for all automatic memory work owned by this chat instance. */
529
+ async settleMemory(){
530
+ return this.#memoryQueue;
531
+ }
532
+
533
+ async #writeMemory(snapshot,request){
534
+ if(!hasUserEntry(snapshot)){
362
535
  return false;
363
536
  }
364
537
 
365
- const transcript=this.#messages
366
- .slice(2)
367
- .filter(message=>message.memory_excluded!==true)
538
+ const transcript=snapshot
539
+ .filter(message=>
540
+ ['assistant','user'].includes(message.role)
541
+ &&message.memory_excluded!==true
542
+ )
368
543
  .map(function publicMemoryMessage(message){
369
544
  const copy={...message};
370
545
  delete copy.memory_excluded;
546
+ delete copy.persistence_excluded;
371
547
  return copy;
372
548
  });
373
- const summary=await ai.fetch(
549
+ const summary=await request(
374
550
  [
375
551
  {
376
552
  role:'system',
@@ -388,7 +564,7 @@ ${JSON.stringify(transcript)}`
388
564
  );
389
565
 
390
566
  const memory=normalizeMemoryContent(
391
- summary.choices?.[0]?.message?.content
567
+ summary?.choices?.[0]?.message?.content??summary?.message?.content
392
568
  );
393
569
 
394
570
  if(!memory){
@@ -412,12 +588,12 @@ ${JSON.stringify(transcript)}`
412
588
  * @returns {Promise<*>}
413
589
  */
414
590
  async save(){
415
- if(!hasUserEntry(this.#messages)){
591
+ if(!hasUserEntry(this.#durableMessages())){
416
592
  this.#saved=false;
417
593
  return false;
418
594
  }
419
595
 
420
- const snapshot=this.#messages.map(message=>({...message}));
596
+ const snapshot=this.#durableMessages().map(message=>({...message}));
421
597
  this.#saved=false;
422
598
 
423
599
  return this.#queuePersistence(
@@ -425,6 +601,16 @@ ${JSON.stringify(transcript)}`
425
601
  );
426
602
  }
427
603
 
604
+ #queueMemoryUpdate(request){
605
+ const snapshot=this.#durableMessages().map(message=>({...message}));
606
+ const queued=this.#memoryQueue.then(()=>this.#writeMemory(snapshot,request));
607
+ this.#memoryQueue=queued.catch(()=>{
608
+ console.warn('Unable to update chat memory.');
609
+ return false;
610
+ });
611
+ return this.#memoryQueue;
612
+ }
613
+
428
614
  #queuePersistence(operation){
429
615
  const queued=this.#persistenceQueue.then(operation);
430
616
 
@@ -444,7 +630,7 @@ ${JSON.stringify(transcript)}`
444
630
  content
445
631
  );
446
632
  this.#persistedMessageCount=snapshot.length;
447
- this.#saved=this.#persistedMessageCount===this.#messages.length;
633
+ this.#saved=this.#persistedMessageCount===this.#durableMessages().length;
448
634
  return true;
449
635
  }catch(error){
450
636
  this.#saved=false;
@@ -515,24 +701,33 @@ ${JSON.stringify(transcript)}`
515
701
  * );
516
702
  * ```
517
703
  */
518
- async #appendMessage(message){
519
- return this.#appendMessages([message]);
704
+ async #appendMessage(message,options){
705
+ return this.#appendMessages([message],options);
520
706
  }
521
707
 
522
- async #appendMessages(messages){
523
- const records=Array.from(messages||[]);
708
+ #durableMessages(){
709
+ return this.#messages.filter(message=>message.persistence_excluded!==true);
710
+ }
711
+
712
+ async #appendMessages(messages,{persist=true,prepared=false}={}){
713
+ const records=Array.from(messages||[]).map(message=>
714
+ prepared?message:(persist?message:{...message,persistence_excluded:true})
715
+ );
524
716
  if(!records.length){
525
717
  return false;
526
718
  }
527
719
 
528
720
  this.#messages.push(...records);
529
- this.#saved=false;
721
+ const durableRecords=records.filter(message=>message.persistence_excluded!==true);
722
+ if(durableRecords.length){
723
+ this.#saved=false;
724
+ }
530
725
 
531
- if(!this.persist){
726
+ if(!this.persist||!durableRecords.length){
532
727
  return;
533
728
  }
534
729
 
535
- if(!hasUserEntry(this.#messages)){
730
+ if(!hasUserEntry(this.#durableMessages())){
536
731
  return false;
537
732
  }
538
733
 
@@ -548,22 +743,30 @@ ${JSON.stringify(transcript)}`
548
743
  if(!recordsAreContiguous){
549
744
  throw new Error('Chat records changed before persistence completed.');
550
745
  }
551
- const lastMessageIndex=messageIndex+records.length-1;
552
- if(this.#persistedMessageCount===messageIndex){
746
+ const durableSnapshot=this.#durableMessages();
747
+ const durableIndex=durableSnapshot.indexOf(durableRecords[0]);
748
+ const durableRecordsAreContiguous=durableRecords.every(
749
+ (record,index)=>durableSnapshot[durableIndex+index]===record
750
+ );
751
+ if(durableIndex<0||!durableRecordsAreContiguous){
752
+ throw new Error('Durable chat records changed before persistence completed.');
753
+ }
754
+ const lastDurableIndex=durableIndex+durableRecords.length-1;
755
+ if(this.#persistedMessageCount===durableIndex){
553
756
  await dbopfs.set(
554
757
  this.#tableName,
555
758
  this.fileName,
556
- records.map(record=>JSON.stringify(record)).join('\n')+'\n',
759
+ durableRecords.map(record=>JSON.stringify(record)).join('\n')+'\n',
557
760
  true
558
761
  );
559
- this.#persistedMessageCount=lastMessageIndex+1;
762
+ this.#persistedMessageCount=lastDurableIndex+1;
560
763
  }else{
561
- const snapshot=this.#messages
562
- .slice(0,lastMessageIndex+1)
764
+ const snapshot=durableSnapshot
765
+ .slice(0,lastDurableIndex+1)
563
766
  .map(entry=>({...entry}));
564
767
  await this.#writeSnapshot(snapshot);
565
768
  }
566
- this.#saved=this.#persistedMessageCount===this.#messages.length;
769
+ this.#saved=this.#persistedMessageCount===this.#durableMessages().length;
567
770
  return true;
568
771
  }catch(error){
569
772
  const failedIndex=this.#messages.indexOf(records[0]);
@@ -571,7 +774,7 @@ ${JSON.stringify(transcript)}`
571
774
  this.#messages.splice(failedIndex,records.length);
572
775
  }
573
776
  this.#saved=
574
- this.#persistedMessageCount===this.#messages.length;
777
+ this.#persistedMessageCount===this.#durableMessages().length;
575
778
  throw error;
576
779
  }
577
780
  });