arcane-os 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -13,21 +13,68 @@ function plainRecord(value){
13
13
  &&Object.getPrototypeOf(value)===Object.prototype;
14
14
  }
15
15
 
16
+ function coded(error,code){
17
+ error.code=code;
18
+ return error;
19
+ }
20
+
16
21
  function copyToolCalls(value){
17
22
  if(value===undefined) return null;
18
23
  if(!Array.isArray(value)||value.length!==1){
19
- throw new TypeError('assistantMessage.tool_calls must contain exactly one structural tool call.');
24
+ const error=new TypeError(
25
+ 'assistantMessage.tool_calls must contain exactly one structural tool call.'
26
+ );
27
+ error.code=Array.isArray(value)&&value.length>1
28
+ ?'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
29
+ :'AI_CHAT_INVALID_TOOL_CALL';
30
+ throw error;
20
31
  }
21
32
  const ids=new Set();
22
33
  return value.map((call,index)=>{
23
34
  if(!plainRecord(call)||!plainRecord(call.function)||call.type!=='function'){
24
- throw new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
35
+ const error=new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
36
+ error.code='AI_CHAT_INVALID_TOOL_CALL';
37
+ throw error;
25
38
  }
26
- const id=String(call.id??'').trim();
27
- const name=String(call.function.name??'').trim();
39
+ const id=call.id;
40
+ const name=call.function.name;
28
41
  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.`);
42
+ if(
43
+ typeof id!=='string'
44
+ ||!id.trim()
45
+ ||ids.has(id)
46
+ ||typeof name!=='string'
47
+ ||!name.trim()
48
+ ||typeof argumentValue!=='string'
49
+ ){
50
+ const error=new TypeError(`assistantMessage.tool_calls[${index}] is invalid.`);
51
+ error.code='AI_CHAT_INVALID_TOOL_CALL';
52
+ throw error;
53
+ }
54
+ let argumentRecord;
55
+ try{
56
+ argumentRecord=JSON.parse(argumentValue);
57
+ }catch(cause){
58
+ const error=new TypeError(
59
+ `assistantMessage.tool_calls[${index}].function.arguments must encode a JSON object.`,
60
+ {cause}
61
+ );
62
+ error.code='AI_CHAT_INVALID_TOOL_CALL';
63
+ throw error;
64
+ }
65
+ if(!plainRecord(argumentRecord)){
66
+ const error=new TypeError(
67
+ `assistantMessage.tool_calls[${index}].function.arguments must encode a JSON object.`
68
+ );
69
+ error.code='AI_CHAT_INVALID_TOOL_CALL';
70
+ throw error;
71
+ }
72
+ if(typeof argumentRecord.message!=='string'||!argumentRecord.message.trim()){
73
+ const error=new TypeError(
74
+ `assistantMessage.tool_calls[${index}].function.arguments.message must contain user-facing text.`
75
+ );
76
+ error.code='AI_CHAT_TOOL_MESSAGE_REQUIRED';
77
+ throw error;
31
78
  }
32
79
  ids.add(id);
33
80
  return {
@@ -43,8 +90,19 @@ function turnMessage(value,label){
43
90
  throw new TypeError(`${label} must be a user or tool message.`);
44
91
  }
45
92
  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.`);
93
+ const toolCallId=value.tool_call_id;
94
+ if(typeof toolCallId!=='string'||!toolCallId.trim()){
95
+ throw coded(
96
+ new TypeError(`${label}.tool_call_id is invalid.`),
97
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
98
+ );
99
+ }
100
+ if(!value.content.trim()){
101
+ throw coded(
102
+ new TypeError(`${label}.content must contain a user-facing tool result.`),
103
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
104
+ );
105
+ }
48
106
  return {content:value.content,role:'tool',tool_call_id:toolCallId};
49
107
  }
50
108
  return {content:value.content,role:'user'};
@@ -53,20 +111,67 @@ function turnMessage(value,label){
53
111
  function pendingToolCallId(messages){
54
112
  let pending=null;
55
113
  for(const [index,message] of messages.entries()){
114
+ if(!plainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
115
+ throw coded(
116
+ new TypeError(`Chat message ${index+1} is not a supported persisted record.`),
117
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
118
+ );
119
+ }
120
+ if(message.role!=='assistant'&&message.tool_calls!==undefined){
121
+ throw coded(
122
+ new TypeError(`Chat message ${index+1} places structural tool calls on a non-assistant record.`),
123
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
124
+ );
125
+ }
126
+ if(message.role!=='tool'&&message.tool_call_id!==undefined){
127
+ throw coded(
128
+ new TypeError(`Chat message ${index+1} places a tool-call ID on a non-tool record.`),
129
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
130
+ );
131
+ }
132
+ if(message.role==='system'&&pending){
133
+ throw coded(
134
+ new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
135
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
136
+ );
137
+ }
56
138
  if(message.role==='user'&&pending){
57
- throw new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`);
139
+ throw coded(
140
+ new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`),
141
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
142
+ );
58
143
  }
59
- if(message.role==='assistant'&&message.tool_calls){
60
- const calls=copyToolCalls(message.tool_calls);
144
+ if(message.role==='assistant'){
61
145
  if(pending){
62
- throw new TypeError(`Chat message ${index+1} overlaps a pending tool call.`);
146
+ throw coded(
147
+ new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
148
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
149
+ );
150
+ }
151
+ if(message.tool_calls){
152
+ const calls=copyToolCalls(message.tool_calls);
153
+ pending=calls[0].id;
63
154
  }
64
- pending=calls[0].id;
65
155
  }
66
156
  if(message.role==='tool'){
67
- const toolCallId=String(message.tool_call_id??'').trim();
157
+ const toolCallId=message.tool_call_id;
158
+ if(typeof toolCallId!=='string'||!toolCallId.trim()){
159
+ throw coded(
160
+ new TypeError(`Chat message ${index+1} has an invalid tool_call_id.`),
161
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
162
+ );
163
+ }
164
+ if(typeof message.content!=='string'||!message.content.trim()){
165
+ throw coded(
166
+ new TypeError(`Chat message ${index+1} has a blank tool result.`),
167
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
168
+ );
169
+ }
68
170
  if(!pending||pending!==toolCallId){
69
- throw new TypeError(`Chat message ${index+1} does not match the pending tool call.`);
171
+ throw coded(
172
+ new TypeError(`Chat message ${index+1} does not match the pending tool call.`),
173
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
174
+ );
70
175
  }
71
176
  pending=null;
72
177
  }
@@ -91,6 +196,8 @@ function pendingToolCallId(messages){
91
196
  * Internal marker for session-only messages omitted from durable chat and memory.
92
197
  * @property {Array<Object>} [tool_calls]
93
198
  * Assistant tool calls retained in the saved conversation log.
199
+ * @property {string} [reasoning_content]
200
+ * Optional complete provider reasoning retained with an assistant record.
94
201
  * @property {string} [tool_call_id]
95
202
  * Matching tool-call identifier for a tool result.
96
203
  */
@@ -225,21 +332,47 @@ class ChatEntity{
225
332
  * @returns {ChatMessage[]}
226
333
  */
227
334
  get messages(){
228
- return Object.freeze(this.#messages.map(function publicChatMessage(message){
335
+ return this.#messages.map(function publicChatMessage(message){
229
336
  const copy={...message};
230
337
  if(copy.tool_calls){
231
- copy.tool_calls=Object.freeze(copyToolCalls(copy.tool_calls).map(call=>Object.freeze({
232
- function:Object.freeze({...call.function}),
338
+ copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
339
+ function:{...call.function},
233
340
  id:call.id,
234
341
  type:call.type,
235
- })));
342
+ }));
236
343
  }
237
344
  delete copy.memory_excluded;
238
345
  delete copy.persistence_excluded;
239
346
  delete copy.ui_hidden;
240
347
  delete copy.timestamp;
241
- return Object.freeze(copy);
242
- }));
348
+ return copy;
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Returns the UI-visible conversation records with their persisted timestamps.
354
+ * Model-facing callers should continue to use `messages`, which intentionally
355
+ * omits display metadata.
356
+ *
357
+ * @returns {ChatMessage[]}
358
+ */
359
+ get transcript(){
360
+ return this.#messages
361
+ .filter(message=>message.ui_hidden!==true)
362
+ .map(function publicTranscriptMessage(message){
363
+ const copy={...message};
364
+ if(copy.tool_calls){
365
+ copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
366
+ function:{...call.function},
367
+ id:call.id,
368
+ type:call.type,
369
+ }));
370
+ }
371
+ delete copy.memory_excluded;
372
+ delete copy.persistence_excluded;
373
+ delete copy.ui_hidden;
374
+ return copy;
375
+ });
243
376
  }
244
377
 
245
378
  /**
@@ -300,7 +433,10 @@ class ChatEntity{
300
433
  throw new Error('persist must be boolean');
301
434
  }
302
435
  if(pendingToolCallId(this.#messages)){
303
- throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
436
+ throw coded(
437
+ new TypeError('The pending structural tool result must be supplied before a new user turn.'),
438
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
439
+ );
304
440
  }
305
441
 
306
442
  const message={
@@ -341,6 +477,12 @@ class ChatEntity{
341
477
  if(!is.boolean(persist)){
342
478
  throw new Error('persist must be boolean');
343
479
  }
480
+ if(pendingToolCallId(this.#messages)){
481
+ throw coded(
482
+ new TypeError('The pending structural tool result must be supplied before another assistant turn.'),
483
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
484
+ );
485
+ }
344
486
 
345
487
  const message={
346
488
  role:'assistant',
@@ -366,16 +508,24 @@ class ChatEntity{
366
508
  * The host application decides whether anything is rendered in the chat UI.
367
509
  */
368
510
  addToolExchange({id='',name='',arguments:argumentValue='',result='',persist=true}={}){
369
- const toolCallId=String(id).trim();
370
- const toolName=String(name).trim();
371
- if(!toolCallId||!toolName){
511
+ const toolCallId=id;
512
+ const toolName=name;
513
+ if(
514
+ typeof toolCallId!=='string'
515
+ ||!toolCallId.trim()
516
+ ||typeof toolName!=='string'
517
+ ||!toolName.trim()
518
+ ){
372
519
  throw new TypeError('Tool exchanges require an id and name.');
373
520
  }
374
521
  if(!is.boolean(persist)){
375
522
  throw new TypeError('persist must be boolean.');
376
523
  }
377
524
  if(pendingToolCallId(this.#messages)){
378
- throw new TypeError('The pending structural tool result must be supplied before another tool exchange.');
525
+ throw coded(
526
+ new TypeError('The pending structural tool result must be supplied before another tool exchange.'),
527
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
528
+ );
379
529
  }
380
530
 
381
531
  const serializedArguments=typeof argumentValue==='string'
@@ -387,6 +537,14 @@ class ChatEntity{
387
537
  if(typeof serializedArguments!=='string'||typeof serializedResult!=='string'){
388
538
  throw new TypeError('Tool exchange arguments and results must be JSON-compatible.');
389
539
  }
540
+ const toolCall=copyToolCalls([{
541
+ id:toolCallId,
542
+ type:'function',
543
+ function:{
544
+ name:toolName,
545
+ arguments:serializedArguments
546
+ }
547
+ }])[0];
390
548
 
391
549
  const timestamp=Date.now();
392
550
  return this.#appendMessages([
@@ -394,14 +552,7 @@ class ChatEntity{
394
552
  role:'assistant',
395
553
  content:'',
396
554
  tool_calls:[
397
- {
398
- id:toolCallId,
399
- type:'function',
400
- function:{
401
- name:toolName,
402
- arguments:serializedArguments
403
- }
404
- }
555
+ toolCall
405
556
  ],
406
557
  timestamp,
407
558
  memory_excluded:true
@@ -440,19 +591,41 @@ class ChatEntity{
440
591
  throw new TypeError('memoryRequest must be a function.');
441
592
  }
442
593
  if(messagePersist!==responsePersist){
443
- throw new TypeError('messagePersist and responsePersist must match for one coherent durable turn.');
594
+ throw coded(
595
+ new TypeError('messagePersist and responsePersist must match for one coherent durable turn.'),
596
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
597
+ );
444
598
  }
445
599
  const pendingTool=pendingToolCallId(this.#messages);
446
600
  if(request.role==='user'&&pendingTool){
447
- throw new TypeError('The pending structural tool result must be supplied before a new user turn.');
601
+ throw coded(
602
+ new TypeError('The pending structural tool result must be supplied before a new user turn.'),
603
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
604
+ );
448
605
  }
449
606
  if(request.role==='tool'&&pendingTool!==request.tool_call_id){
450
- throw new TypeError('requestMessage does not match the pending structural tool call.');
607
+ throw coded(
608
+ new TypeError('requestMessage does not match the pending structural tool call.'),
609
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
610
+ );
451
611
  }
452
612
  const toolCalls=copyToolCalls(assistantMessage.tool_calls);
453
613
  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.');
614
+ const assistantReasoning=assistantMessage.reasoning_content;
615
+ if(assistantReasoning!==undefined&&typeof assistantReasoning!=='string'){
616
+ throw new TypeError('assistantMessage.reasoning_content must be a string when provided.');
617
+ }
618
+ if(
619
+ !is.union(assistantContent,'string','number')
620
+ ||(
621
+ !String(assistantContent)
622
+ &&!toolCalls
623
+ &&!(typeof assistantReasoning==='string'&&assistantReasoning.length)
624
+ )
625
+ ){
626
+ throw new TypeError(
627
+ 'assistantMessage must contain text, reasoning, or structural tool calls.'
628
+ );
456
629
  }
457
630
  const timestamp=Date.now();
458
631
  const requestRecord={...request,timestamp};
@@ -460,6 +633,7 @@ class ChatEntity{
460
633
  content:assistantContent,
461
634
  role:'assistant',
462
635
  timestamp,
636
+ ...(assistantReasoning!==undefined?{reasoning_content:assistantReasoning}:{}),
463
637
  ...(toolCalls?{tool_calls:toolCalls}:{})
464
638
  };
465
639
  if(request.role==='tool') requestRecord.memory_excluded=true;
@@ -502,13 +676,15 @@ class ChatEntity{
502
676
  return this.messages;
503
677
  }
504
678
 
505
- this.#messages=Array.isArray(content)
679
+ const loadedMessages=Array.isArray(content)
506
680
  ?content
507
681
  :String(content)
508
682
  .split('\n')
509
683
  .map(row=>row.trim())
510
684
  .filter(Boolean)
511
685
  .map(row=>JSON.parse(row));
686
+ pendingToolCallId(loadedMessages);
687
+ this.#messages=loadedMessages;
512
688
  this.#persistedMessageCount=this.#durableMessages().length;
513
689
  this.#saved=true;
514
690
 
@@ -735,13 +911,19 @@ ${JSON.stringify(transcript)}`
735
911
  try{
736
912
  const messageIndex=this.#messages.indexOf(records[0]);
737
913
  if(messageIndex<0){
738
- return false;
914
+ throw coded(
915
+ new Error('The chat persistence transaction is already settled.'),
916
+ 'AI_CHAT_TRANSACTION_SETTLED'
917
+ );
739
918
  }
740
919
  const recordsAreContiguous=records.every(
741
920
  (record,index)=>this.#messages[messageIndex+index]===record
742
921
  );
743
922
  if(!recordsAreContiguous){
744
- throw new Error('Chat records changed before persistence completed.');
923
+ throw coded(
924
+ new Error('Chat records changed before persistence completed.'),
925
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
926
+ );
745
927
  }
746
928
  const durableSnapshot=this.#durableMessages();
747
929
  const durableIndex=durableSnapshot.indexOf(durableRecords[0]);
@@ -749,7 +931,10 @@ ${JSON.stringify(transcript)}`
749
931
  (record,index)=>durableSnapshot[durableIndex+index]===record
750
932
  );
751
933
  if(durableIndex<0||!durableRecordsAreContiguous){
752
- throw new Error('Durable chat records changed before persistence completed.');
934
+ throw coded(
935
+ new Error('Durable chat records changed before persistence completed.'),
936
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
937
+ );
753
938
  }
754
939
  const lastDurableIndex=durableIndex+durableRecords.length-1;
755
940
  if(this.#persistedMessageCount===durableIndex){
@@ -1,4 +1,4 @@
1
- const KEY_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
1
+ const KEY_PATTERN=/^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
2
2
  const TYPES=new Set(['boolean','number','select','text']);
3
3
 
4
4
  function copy(value){
@@ -25,8 +25,6 @@ export default class Preference{
25
25
  if(type==='select'&&!this.options.some(option=>Object.is(option.value,this.defaultValue))){
26
26
  throw new TypeError(`Preference ${key} has a default value outside its options.`);
27
27
  }
28
- Object.freeze(this.options);
29
- Object.freeze(this);
30
28
  }
31
29
 
32
30
  normalizeOptions(options=[]){
@@ -35,7 +33,7 @@ export default class Preference{
35
33
  const normalized=typeof option==='object'&&option!==null
36
34
  ?{label:String(option.label??option.value),value:this.normalize(option.value)}
37
35
  :{label:String(option),value:this.normalize(option)};
38
- return Object.freeze(normalized);
36
+ return normalized;
39
37
  });
40
38
  }
41
39
 
@@ -79,5 +77,5 @@ export default class Preference{
79
77
  export function preferenceSchema(definitions=[]){
80
78
  const schema=definitions.map(definition=>definition instanceof Preference?definition:new Preference(definition));
81
79
  if(new Set(schema.map(item=>item.key)).size!==schema.length) throw new TypeError('Preference schema keys must be unique.');
82
- return Object.freeze(schema);
80
+ return schema;
83
81
  }
@@ -1,23 +1,23 @@
1
1
  function number(value,label,{minimum=-Infinity,maximum=Infinity}={}){const result=Number(value);if(!Number.isFinite(result)||result<minimum||result>maximum)throw new TypeError(`${label} is invalid.`);return result;}
2
- function text(value,label){const result=String(value??'').trim();if(!result)throw new TypeError(`${label} is required.`);return result;}
2
+ function text(value,label){const result=String(value??'');if(!result.trim())throw new TypeError(`${label} is required.`);return result;}
3
3
  function instant(value,label){const date=new Date(value);if(Number.isNaN(date.valueOf()))throw new TypeError(`${label} is invalid.`);return date.toISOString();}
4
4
 
5
5
  export class WeatherLocation{
6
- constructor({id='',name,region='',country='',latitude,longitude,timezone='auto'}={}){this.id=String(id||`${latitude},${longitude}`);this.name=text(name,'Weather location name');this.region=String(region||'');this.country=String(country||'');this.latitude=number(latitude,'Latitude',{minimum:-90,maximum:90});this.longitude=number(longitude,'Longitude',{minimum:-180,maximum:180});this.timezone=String(timezone||'auto');Object.freeze(this);}
6
+ constructor({id='',name,region='',country='',latitude,longitude,timezone='auto'}={}){this.id=String(id||`${latitude},${longitude}`);this.name=text(name,'Weather location name');this.region=String(region||'');this.country=String(country||'');this.latitude=number(latitude,'Latitude',{minimum:-90,maximum:90});this.longitude=number(longitude,'Longitude',{minimum:-180,maximum:180});this.timezone=String(timezone||'auto');}
7
7
  toJSON(){return {...this};}
8
8
  }
9
9
 
10
10
  export class WeatherObservation{
11
- constructor({time,temperature,apparentTemperature=temperature,humidity=0,precipitation=0,weatherCode=0,windSpeed=0,isDay=true,temperatureUnit='°',windUnit=''}={}){this.time=instant(time,'Observation time');this.temperature=number(temperature,'Temperature');this.apparentTemperature=number(apparentTemperature,'Apparent temperature');this.humidity=number(humidity,'Humidity',{minimum:0,maximum:100});this.precipitation=number(precipitation,'Precipitation',{minimum:0});this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.windSpeed=number(windSpeed,'Wind speed',{minimum:0});this.isDay=Boolean(isDay);this.temperatureUnit=String(temperatureUnit||'°');this.windUnit=String(windUnit||'');Object.freeze(this);}
11
+ constructor({time,temperature,apparentTemperature=temperature,humidity=0,precipitation=0,weatherCode=0,windSpeed=0,isDay=true,temperatureUnit='°',windUnit=''}={}){this.time=instant(time,'Observation time');this.temperature=number(temperature,'Temperature');this.apparentTemperature=number(apparentTemperature,'Apparent temperature');this.humidity=number(humidity,'Humidity',{minimum:0,maximum:100});this.precipitation=number(precipitation,'Precipitation',{minimum:0});this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.windSpeed=number(windSpeed,'Wind speed',{minimum:0});this.isDay=Boolean(isDay);this.temperatureUnit=String(temperatureUnit||'°');this.windUnit=String(windUnit||'');}
12
12
  toJSON(){return {...this};}
13
13
  }
14
14
 
15
15
  export class WeatherDay{
16
- constructor({date,weatherCode=0,temperatureMax,temperatureMin,precipitationProbability=0,sunrise='',sunset='',temperatureUnit='°'}={}){this.date=instant(date,'Forecast date');this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.temperatureMax=number(temperatureMax,'Maximum temperature');this.temperatureMin=number(temperatureMin,'Minimum temperature');this.precipitationProbability=number(precipitationProbability,'Precipitation probability',{minimum:0,maximum:100});this.sunrise=sunrise?instant(sunrise,'Sunrise'):'';this.sunset=sunset?instant(sunset,'Sunset'):'';this.temperatureUnit=String(temperatureUnit||'°');Object.freeze(this);}
16
+ constructor({date,weatherCode=0,temperatureMax,temperatureMin,precipitationProbability=0,sunrise='',sunset='',temperatureUnit='°'}={}){this.date=instant(date,'Forecast date');this.weatherCode=Math.max(0,Math.trunc(Number(weatherCode)||0));this.temperatureMax=number(temperatureMax,'Maximum temperature');this.temperatureMin=number(temperatureMin,'Minimum temperature');this.precipitationProbability=number(precipitationProbability,'Precipitation probability',{minimum:0,maximum:100});this.sunrise=sunrise?instant(sunrise,'Sunrise'):'';this.sunset=sunset?instant(sunset,'Sunset'):'';this.temperatureUnit=String(temperatureUnit||'°');}
17
17
  toJSON(){return {...this};}
18
18
  }
19
19
 
20
20
  export class WeatherSnapshot{
21
- constructor({location,current,daily=[],source='',fetchedAt=new Date()}={}){this.location=location instanceof WeatherLocation?location:new WeatherLocation(location);this.current=current instanceof WeatherObservation?current:new WeatherObservation(current);this.daily=Object.freeze(Array.from(daily,value=>value instanceof WeatherDay?value:new WeatherDay(value)));this.source=String(source||'');this.fetchedAt=instant(fetchedAt,'Weather fetch time');Object.freeze(this);}
21
+ constructor({location,current,daily=[],source='',fetchedAt=new Date()}={}){this.location=location instanceof WeatherLocation?location:new WeatherLocation(location);this.current=current instanceof WeatherObservation?current:new WeatherObservation(current);this.daily=Array.from(daily,value=>value instanceof WeatherDay?value:new WeatherDay(value));this.source=String(source||'');this.fetchedAt=instant(fetchedAt,'Weather fetch time');}
22
22
  toJSON(){return {location:this.location.toJSON(),current:this.current.toJSON(),daily:this.daily.map(day=>day.toJSON()),source:this.source,fetchedAt:this.fetchedAt};}
23
23
  }