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
@@ -159,7 +159,7 @@
159
159
  margin: 0;
160
160
  padding: clamp(1rem, 2vw, 1.5rem);
161
161
  list-style: none;
162
- overflow-x: hidden;
162
+ overflow-x: auto;
163
163
  overflow-y: auto;
164
164
  overscroll-behavior: contain;
165
165
  scrollbar-gutter: stable;
@@ -181,7 +181,7 @@
181
181
  clear: both;
182
182
  border: 1px solid var(--arcane-border, var(--border-color));
183
183
  border-radius: calc(var(--arcane-chat-radius) - .1rem);
184
- overflow-x: hidden;
184
+ overflow-x: auto;
185
185
  overflow-wrap: anywhere;
186
186
  }
187
187
 
@@ -238,7 +238,7 @@
238
238
  padding:.65rem;
239
239
  border:1px solid currentColor;
240
240
  border-radius:var(--arcane-radius-small,.3rem);
241
- overflow-x:hidden;
241
+ overflow-x:auto;
242
242
  overflow-wrap:anywhere;
243
243
  }
244
244
 
@@ -256,7 +256,7 @@
256
256
  max-inline-size:100%;
257
257
  max-width:100%;
258
258
  margin-top:.55rem;
259
- overflow-x:hidden;
259
+ overflow-x:auto;
260
260
  }
261
261
 
262
262
  .message_tool_details summary{
@@ -930,8 +930,9 @@
930
930
  let activeSessionMessageToken=null;
931
931
  let sessionBindingGeneration=0;
932
932
  let sessionMessageSequence=0;
933
- let pendingStructuralToolCall=null;
933
+ let pendingStructuralToolCalls=[];
934
934
  let pendingStructuralToolMessage='';
935
+ let sessionHistoryRecoveryMessage='';
935
936
  const activeSubmissionOwnerships=new Set();
936
937
  const conversationTimeboxRetryDelays=[0,500,1_500];
937
938
  const chatReasons={
@@ -1032,10 +1033,9 @@
1032
1033
  }
1033
1034
 
1034
1035
  function isPlainRecord(value){
1035
- return Boolean(value)
1036
- &&typeof value==='object'
1037
- &&!Array.isArray(value)
1038
- &&Object.getPrototypeOf(value)===Object.prototype;
1036
+ if(!value||typeof value!=='object'||Array.isArray(value))return false;
1037
+ const prototype=Object.getPrototypeOf(value);
1038
+ return prototype===Object.prototype||prototype===null;
1039
1039
  }
1040
1040
 
1041
1041
  function visibleErrorMessage(error,fallback){
@@ -1122,6 +1122,7 @@
1122
1122
  failure.code='AI_CHAT_INVALID_TOOL_CALL';
1123
1123
  throw failure;
1124
1124
  }
1125
+ const ids=new Set();
1125
1126
  return value.map((call,index)=>{
1126
1127
  if(
1127
1128
  !call
@@ -1133,6 +1134,7 @@
1133
1134
  ||Array.isArray(call.function)
1134
1135
  ||typeof call.id!=='string'
1135
1136
  ||!call.id.trim()
1137
+ ||ids.has(call.id)
1136
1138
  ||typeof call.function.name!=='string'
1137
1139
  ||!call.function.name.trim()
1138
1140
  ||typeof call.function.arguments!=='string'
@@ -1142,10 +1144,57 @@
1142
1144
  throw failure;
1143
1145
  }
1144
1146
  structuralToolMessage(call);
1145
- return call;
1147
+ ids.add(call.id);
1148
+ return {
1149
+ ...call,
1150
+ function:{...call.function},
1151
+ id:call.id,
1152
+ type:'function'
1153
+ };
1146
1154
  });
1147
1155
  }
1148
1156
 
1157
+ function copyCompleteChatData(value,seen=new Map()){
1158
+ if(value===null||typeof value!=='object') return value;
1159
+ if(seen.has(value)) return seen.get(value);
1160
+ if(Array.isArray(value)){
1161
+ const result=[];
1162
+ seen.set(value,result);
1163
+ for(const item of value) result.push(copyCompleteChatData(item,seen));
1164
+ return result;
1165
+ }
1166
+ const result={};
1167
+ seen.set(value,result);
1168
+ for(const [key,descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))){
1169
+ if(Object.hasOwn(descriptor,'value')){
1170
+ result[key]=copyCompleteChatData(descriptor.value,seen);
1171
+ }
1172
+ }
1173
+ return result;
1174
+ }
1175
+
1176
+ function sameCompleteChatData(left,right,seen=new Map()){
1177
+ if(Object.is(left,right)) return true;
1178
+ if(!left||!right||typeof left!=='object'||typeof right!=='object') return false;
1179
+ if(Array.isArray(left)!==Array.isArray(right)) return false;
1180
+ const matched=seen.get(left);
1181
+ if(matched!==undefined) return matched===right;
1182
+ seen.set(left,right);
1183
+ if(Array.isArray(left)){
1184
+ return left.length===right.length
1185
+ &&left.every((value,index)=>sameCompleteChatData(value,right[index],seen));
1186
+ }
1187
+ const leftKeys=Reflect.ownKeys(left).filter(
1188
+ key=>Object.prototype.propertyIsEnumerable.call(left,key)
1189
+ );
1190
+ const rightKeys=Reflect.ownKeys(right).filter(
1191
+ key=>Object.prototype.propertyIsEnumerable.call(right,key)
1192
+ );
1193
+ return leftKeys.length===rightKeys.length
1194
+ &&leftKeys.every(key=>Object.prototype.propertyIsEnumerable.call(right,key)
1195
+ &&sameCompleteChatData(left[key],right[key],seen));
1196
+ }
1197
+
1149
1198
  function structuralToolMessage(call){
1150
1199
  let argumentsRecord;
1151
1200
  try{
@@ -1175,40 +1224,50 @@
1175
1224
  return argumentsRecord.message;
1176
1225
  }
1177
1226
 
1178
- function setPendingStructuralToolCall(call=null){
1179
- if(call===null){
1180
- pendingStructuralToolCall=null;
1227
+ function setPendingStructuralToolCalls(calls=[]){
1228
+ if(!Array.isArray(calls)||calls.length===0){
1229
+ pendingStructuralToolCalls=[];
1181
1230
  pendingStructuralToolMessage='';
1182
- return null;
1231
+ return [];
1183
1232
  }
1184
- pendingStructuralToolMessage=structuralToolMessage(call);
1185
- pendingStructuralToolCall={
1186
- id:call.id,
1187
- type:'function',
1188
- function:{
1189
- name:call.function.name,
1190
- arguments:call.function.arguments
1191
- }
1192
- };
1193
- return pendingStructuralToolCall;
1233
+ pendingStructuralToolCalls=normalizeVisibleToolCalls(calls);
1234
+ pendingStructuralToolMessage=pendingStructuralToolCalls
1235
+ .map(structuralToolMessage)
1236
+ .join(' · ');
1237
+ return pendingStructuralToolCalls;
1194
1238
  }
1195
1239
 
1196
1240
  function sameStructuralToolCall(left,right){
1197
- return Boolean(left&&right)
1198
- &&left.id===right.id
1199
- &&left.type===right.type
1200
- &&left.function?.name===right.function?.name
1201
- &&left.function?.arguments===right.function?.arguments;
1241
+ return sameCompleteChatData(left,right);
1242
+ }
1243
+
1244
+ function sameStructuralToolCalls(left,right){
1245
+ return Array.isArray(left)
1246
+ &&Array.isArray(right)
1247
+ &&left.length===right.length
1248
+ &&left.every((call,index)=>sameStructuralToolCall(call,right[index]));
1249
+ }
1250
+
1251
+ function pendingStructuralToolSummaries(){
1252
+ return pendingStructuralToolCalls.map(call=>({
1253
+ id:call.id,
1254
+ name:call.function.name,
1255
+ message:structuralToolMessage(call)
1256
+ }));
1202
1257
  }
1203
1258
 
1204
1259
  function pendingStructuralToolSummary(){
1205
- return pendingStructuralToolCall
1206
- ?{
1207
- id:pendingStructuralToolCall.id,
1208
- name:pendingStructuralToolCall.function.name,
1209
- message:pendingStructuralToolMessage
1210
- }
1211
- :null;
1260
+ const summaries=pendingStructuralToolSummaries();
1261
+ return summaries.length===1?summaries[0]:null;
1262
+ }
1263
+
1264
+ function pendingStructuralToolCallsComplete(){
1265
+ return pendingStructuralToolCalls.map(call=>copyCompleteChatData(call));
1266
+ }
1267
+
1268
+ function pendingStructuralToolCallComplete(){
1269
+ const calls=pendingStructuralToolCallsComplete();
1270
+ return calls.length===1?calls[0]:null;
1212
1271
  }
1213
1272
 
1214
1273
  function appendVisibleToolCall(item,call){
@@ -1237,7 +1296,7 @@
1237
1296
  summary.textContent='Tool call details';
1238
1297
  const argumentsBlock=item.ownerDocument.createElement('pre');
1239
1298
  const code=item.ownerDocument.createElement('code');
1240
- code.textContent=call.function.arguments;
1299
+ code.textContent=completeVisibleValue(call);
1241
1300
  argumentsBlock.append(code);
1242
1301
  details.append(summary,argumentsBlock);
1243
1302
  entry.append(heading,userMessage,details);
@@ -1273,23 +1332,27 @@
1273
1332
  return true;
1274
1333
  }
1275
1334
 
1276
- function latestCommittedTranscriptTurn(transcript,requestMessage){
1335
+ function sameTranscriptRequest(actual,expected){
1336
+ return actual?.role===expected?.role
1337
+ &&actual.content===expected.content
1338
+ &&(
1339
+ expected.role!=='tool'
1340
+ ||actual.tool_call_id===expected.tool_call_id
1341
+ );
1342
+ }
1343
+
1344
+ function latestCommittedTranscriptTurn(transcript,requestMessages){
1277
1345
  if(!Array.isArray(transcript)) return null;
1278
- const requestRole=requestMessage?.role??'user';
1279
- const requestContent=requestMessage?.content;
1280
- for(let index=transcript.length-1;index>0;index--){
1346
+ const expected=Array.isArray(requestMessages)
1347
+ ?requestMessages
1348
+ :[requestMessages];
1349
+ if(!expected.length) return null;
1350
+ for(let index=transcript.length-1;index>=expected.length;index--){
1281
1351
  const response=transcript[index];
1282
- const request=transcript[index-1];
1283
- if(
1284
- response?.role==='assistant'
1285
- &&request?.role===requestRole
1286
- &&request.content===requestContent
1287
- &&(
1288
- requestRole!=='tool'
1289
- ||request.tool_call_id===requestMessage.tool_call_id
1290
- )
1291
- ){
1292
- return {request,response};
1352
+ if(response?.role!=='assistant') continue;
1353
+ const requests=transcript.slice(index-expected.length,index);
1354
+ if(requests.every((request,offset)=>sameTranscriptRequest(request,expected[offset]))){
1355
+ return {request:requests.at(-1),requests,response};
1293
1356
  }
1294
1357
  }
1295
1358
  return null;
@@ -1325,87 +1388,131 @@
1325
1388
  return item;
1326
1389
  }
1327
1390
 
1391
+ function completeVisibleValue(value){
1392
+ try{
1393
+ const serialized=JSON.stringify(value,null,2);
1394
+ if(serialized!==undefined) return serialized;
1395
+ }catch(error){
1396
+ console.error('Arcane saved chat record could not be serialized as JSON.',error);
1397
+ }
1398
+ return String(value);
1399
+ }
1400
+
1401
+ function createSavedRecordFallback(message,index){
1402
+ const content=isPlainRecord(message)&&typeof message.content==='string'
1403
+ ?message.content
1404
+ :'Saved record content is available in details.';
1405
+ const item=createTranscriptMessage(
1406
+ 'assistant',
1407
+ content,
1408
+ `Saved record ${index+1}`,
1409
+ {timestamp:isPlainRecord(message)?message.timestamp:undefined}
1410
+ );
1411
+ const details=item.ownerDocument.createElement('details');
1412
+ details.className='message_tool_details';
1413
+ const summary=item.ownerDocument.createElement('summary');
1414
+ summary.textContent='Saved record details';
1415
+ const block=item.ownerDocument.createElement('pre');
1416
+ const code=item.ownerDocument.createElement('code');
1417
+ code.textContent=completeVisibleValue(message);
1418
+ block.append(code);
1419
+ details.append(summary,block);
1420
+ item.insertBefore(details,item.querySelector('.message_timestamp'));
1421
+ return item;
1422
+ }
1423
+
1328
1424
  function renderSessionHistory(history){
1329
1425
  if(!Array.isArray(history)){
1330
1426
  throw new TypeError('Chat session history must be an array.');
1331
1427
  }
1332
1428
  const fragment=chatOutput.ownerDocument.createDocumentFragment();
1333
1429
  const rendered=[];
1334
- let restoredPendingToolCall=null;
1335
- for(const message of history){
1336
- if(!message||typeof message!=='object'||Array.isArray(message)){
1337
- throw new TypeError('Chat session history contains an invalid message.');
1338
- }
1339
- if(message.role==='system'){
1340
- if(restoredPendingToolCall){
1430
+ const restoredPendingToolCalls=new Map();
1431
+ let actionable=true;
1432
+ let recoveryNeeded=false;
1433
+ for(const [index,message] of history.entries()){
1434
+ let item;
1435
+ try{
1436
+ if(!isPlainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
1341
1437
  throw chatError(
1342
- 'Saved chat contains a system record before its pending structural tool result.',
1438
+ 'Saved chat contains an unsupported record.',
1343
1439
  'AI_CHAT_INCOHERENT_PERSISTENCE'
1344
1440
  );
1345
1441
  }
1346
- continue;
1347
- }
1348
- if(!['assistant','tool','user'].includes(message.role)){
1349
- throw new TypeError(`Chat session history contains an unsupported role: ${message.role}.`);
1350
- }
1351
- if(typeof message.content!=='string'){
1352
- throw new TypeError('Chat session history message content must be a string.');
1353
- }
1354
- if(message.role==='tool'&&!message.content.trim()){
1355
- throw chatError(
1356
- 'Saved chat contains a blank structural tool result.',
1357
- 'AI_CHAT_INVALID_TOOL_MESSAGE'
1358
- );
1359
- }
1360
- const calls=normalizeVisibleToolCalls(message.tool_calls);
1361
- if(calls.length>1){
1362
- throw chatError(
1363
- 'Saved chat contains parallel structural tool calls.',
1364
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
1365
- );
1366
- }
1367
- if(message.role!=='assistant'&&calls.length){
1368
- throw chatError(
1369
- 'Only assistant records may contain structural tool calls.',
1370
- 'AI_CHAT_INVALID_TOOL_CALL'
1371
- );
1372
- }
1373
- if(message.role==='tool'){
1374
- if(
1375
- !restoredPendingToolCall
1376
- ||typeof message.tool_call_id!=='string'
1377
- ||message.tool_call_id!==restoredPendingToolCall.id
1378
- ){
1442
+ if(typeof message.content!=='string'){
1379
1443
  throw chatError(
1380
- 'Saved chat contains a tool result that does not match its pending structural tool call.',
1381
- 'AI_CHAT_INVALID_TOOL_MESSAGE'
1444
+ 'Saved chat contains a record without text content.',
1445
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
1382
1446
  );
1383
1447
  }
1384
- restoredPendingToolCall=null;
1385
- }else{
1386
- if(restoredPendingToolCall){
1448
+ const calls=normalizeVisibleToolCalls(message.tool_calls);
1449
+ if(message.role!=='assistant'&&calls.length){
1450
+ throw chatError(
1451
+ 'Only assistant records may contain structural tool calls.',
1452
+ 'AI_CHAT_INVALID_TOOL_CALL'
1453
+ );
1454
+ }
1455
+ if(message.role!=='tool'&&message.tool_call_id!==undefined){
1387
1456
  throw chatError(
1388
- 'Saved chat continues before its pending structural tool result.',
1389
- 'AI_CHAT_TOOL_RESULT_REQUIRED'
1457
+ 'Only tool records may contain a structural tool-call ID.',
1458
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1390
1459
  );
1391
1460
  }
1392
- if(calls.length){
1393
- restoredPendingToolCall=calls[0];
1461
+ if(actionable){
1462
+ if(message.role==='tool'){
1463
+ if(
1464
+ typeof message.tool_call_id!=='string'
1465
+ ||!message.tool_call_id.trim()
1466
+ ||!message.content.trim()
1467
+ ||!restoredPendingToolCalls.has(message.tool_call_id)
1468
+ ){
1469
+ throw chatError(
1470
+ 'Saved chat contains a tool result that does not match a pending structural tool call.',
1471
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1472
+ );
1473
+ }
1474
+ restoredPendingToolCalls.delete(message.tool_call_id);
1475
+ }else{
1476
+ if(restoredPendingToolCalls.size){
1477
+ throw chatError(
1478
+ 'Saved chat continues before all pending structural tool results.',
1479
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
1480
+ );
1481
+ }
1482
+ if(calls.length){
1483
+ for(const call of calls){
1484
+ restoredPendingToolCalls.set(call.id,call);
1485
+ }
1486
+ }
1487
+ }
1394
1488
  }
1489
+ const name=message.role==='user'
1490
+ ?host.name
1491
+ :message.role==='tool'
1492
+ ?'Tool'
1493
+ :message.role==='system'
1494
+ ?'System'
1495
+ :host.aiName;
1496
+ item=createTranscriptMessage(message.role,message.content,name,{
1497
+ timestamp:message.timestamp,
1498
+ toolCalls:calls,
1499
+ });
1500
+ }catch(error){
1501
+ recoveryNeeded=true;
1502
+ actionable=false;
1503
+ restoredPendingToolCalls.clear();
1504
+ console.error(`Arcane saved chat record ${index+1} is not actionable.`,error);
1505
+ item=createSavedRecordFallback(message,index);
1395
1506
  }
1396
- const name=message.role==='user'
1397
- ?host.name
1398
- :message.role==='tool'
1399
- ?'Tool'
1400
- :host.aiName;
1401
- const item=createTranscriptMessage(message.role,message.content,name,{
1402
- timestamp:message.timestamp,
1403
- toolCalls:calls,
1404
- });
1405
1507
  fragment.append(item);
1406
1508
  rendered.push(item);
1407
1509
  }
1408
- setPendingStructuralToolCall(restoredPendingToolCall);
1510
+ sessionHistoryRecoveryMessage=recoveryNeeded
1511
+ ?'This saved chat is readable, but some saved actions cannot be resumed.'
1512
+ :'';
1513
+ setPendingStructuralToolCalls(
1514
+ actionable?[...restoredPendingToolCalls.values()]:[]
1515
+ );
1409
1516
  chatOutput.replaceChildren(fragment);
1410
1517
  scrollTranscriptToBottom();
1411
1518
  return rendered;
@@ -1474,7 +1581,7 @@
1474
1581
  host.session=session;
1475
1582
  setSessionStatus(
1476
1583
  pendingStructuralToolMessage?'tool':'ready',
1477
- pendingStructuralToolMessage||'Chat ready.'
1584
+ pendingStructuralToolMessage||sessionHistoryRecoveryMessage||'Chat ready.'
1478
1585
  );
1479
1586
  applyAIAvailability(host.aiAvailability);
1480
1587
  sessionBindingPending=false;
@@ -1483,7 +1590,10 @@
1483
1590
  session,
1484
1591
  ai:boundChatAI,
1485
1592
  history,
1486
- pendingTool:pendingStructuralToolSummary()
1593
+ pendingTool:pendingStructuralToolSummary(),
1594
+ pendingTools:pendingStructuralToolSummaries(),
1595
+ pendingToolCall:pendingStructuralToolCallComplete(),
1596
+ pendingToolCalls:pendingStructuralToolCallsComplete()
1487
1597
  };
1488
1598
  dispatchChatEvent(
1489
1599
  'chat-session-bound',
@@ -1652,11 +1762,27 @@
1652
1762
  host.bindSession=bindSession;
1653
1763
  host.submitMessage=submitMessage;
1654
1764
  host.submitToolResult=submitToolResult;
1765
+ host.submitToolResults=submitToolResults;
1655
1766
  Object.defineProperty(host,'pendingTool',{
1656
1767
  configurable:true,
1657
1768
  enumerable:true,
1658
1769
  get:pendingStructuralToolSummary
1659
1770
  });
1771
+ Object.defineProperty(host,'pendingTools',{
1772
+ configurable:true,
1773
+ enumerable:true,
1774
+ get:pendingStructuralToolSummaries
1775
+ });
1776
+ Object.defineProperty(host,'pendingToolCall',{
1777
+ configurable:true,
1778
+ enumerable:true,
1779
+ get:pendingStructuralToolCallComplete
1780
+ });
1781
+ Object.defineProperty(host,'pendingToolCalls',{
1782
+ configurable:true,
1783
+ enumerable:true,
1784
+ get:pendingStructuralToolCallsComplete
1785
+ });
1660
1786
  host.destroy=destroy;
1661
1787
  host.session=null;
1662
1788
  host.sessionStatus={state:'idle',message:'Chat session is not connected.'};
@@ -1672,8 +1798,13 @@
1672
1798
  );
1673
1799
  window.addEventListener(
1674
1800
  'pagehide',
1675
- destroy,
1676
- {once:true,signal:aiRuntimeStateAbortController.signal}
1801
+ handlePageHide,
1802
+ {signal:aiRuntimeStateAbortController.signal}
1803
+ );
1804
+ window.addEventListener(
1805
+ 'pageshow',
1806
+ handlePageShow,
1807
+ {signal:aiRuntimeStateAbortController.signal}
1677
1808
  );
1678
1809
  window.addEventListener(
1679
1810
  AI_TTS_FAILURE_EVENT,
@@ -1863,9 +1994,12 @@
1863
1994
  send.disabled=host.conversationComplete
1864
1995
  ||!availability.llm
1865
1996
  ||sessionMessagePending
1866
- ||Boolean(pendingStructuralToolMessage);
1997
+ ||Boolean(pendingStructuralToolMessage)
1998
+ ||Boolean(sessionHistoryRecoveryMessage);
1867
1999
  send.title=pendingStructuralToolMessage
1868
2000
  ?pendingStructuralToolMessage
2001
+ :sessionHistoryRecoveryMessage
2002
+ ?'This saved chat is readable but cannot continue.'
1869
2003
  :availability.llm
1870
2004
  ?'Send message'
1871
2005
  :'The selected language model service is unavailable.';
@@ -2727,6 +2861,7 @@
2727
2861
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
2728
2862
  'AI_CHAT_TOOL_MESSAGE_REQUIRED',
2729
2863
  'AI_CHAT_TOOL_RESULT_NOT_PENDING',
2864
+ 'AI_CHAT_TOOL_RESULT_BATCH_REQUIRED',
2730
2865
  'AI_CHAT_TOOL_RESULT_REQUIRED',
2731
2866
  'AI_CHAT_TRANSACTION_SETTLED'
2732
2867
  ].includes(error?.code);
@@ -2756,7 +2891,7 @@
2756
2891
  async function sendMessageThroughBoundSession(
2757
2892
  text,
2758
2893
  context,
2759
- sessionRequestMessage={content:text,role:'user'},
2894
+ sessionRequest={content:text,role:'user'},
2760
2895
  perTurnRequest=null
2761
2896
  ){
2762
2897
  const session=boundChatSession;
@@ -2774,13 +2909,21 @@
2774
2909
  );
2775
2910
  }
2776
2911
 
2912
+ const sessionRequestMessages=Array.isArray(sessionRequest)
2913
+ ?sessionRequest
2914
+ :[sessionRequest];
2915
+ if(!sessionRequestMessages.length){
2916
+ throw new TypeError('A chat session request must contain at least one message.');
2917
+ }
2777
2918
  const request={
2778
- message:sessionRequestMessage,
2919
+ ...(sessionRequestMessages.length===1
2920
+ ?{message:sessionRequestMessages[0]}
2921
+ :{messages:sessionRequestMessages}),
2779
2922
  signal:context.signal,
2780
2923
  ...(perTurnRequest===null?{}:{request:perTurnRequest})
2781
2924
  };
2782
- const previousPendingToolCall=pendingStructuralToolCall;
2783
- let streamedStructuralToolCall=null;
2925
+ const previousPendingToolCalls=[...pendingStructuralToolCalls];
2926
+ const streamedStructuralToolCalls=[];
2784
2927
  const messageId=`session-${++sessionMessageSequence}`;
2785
2928
  const sessionMessageToken=Symbol(messageId);
2786
2929
  activeSessionMessageToken=sessionMessageToken;
@@ -2821,20 +2964,20 @@
2821
2964
  )||null;
2822
2965
  if(!message) return false;
2823
2966
  const normalized=normalizeVisibleToolCalls([call])[0];
2824
- if(
2825
- streamedStructuralToolCall
2826
- &&!sameStructuralToolCall(streamedStructuralToolCall,normalized)
2827
- ){
2967
+ const existing=streamedStructuralToolCalls.find(
2968
+ candidate=>candidate.id===normalized.id
2969
+ );
2970
+ if(existing&&!sameStructuralToolCall(existing,normalized)){
2828
2971
  throw chatError(
2829
2972
  'The streamed structural tool call changed before completion.',
2830
2973
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2831
2974
  );
2832
2975
  }
2833
- if(streamedStructuralToolCall){
2976
+ if(existing){
2834
2977
  return true;
2835
2978
  }
2836
- streamedStructuralToolCall=normalized;
2837
- setPendingStructuralToolCall(normalized);
2979
+ streamedStructuralToolCalls.push(normalized);
2980
+ setPendingStructuralToolCalls(streamedStructuralToolCalls);
2838
2981
  appendVisibleToolCall(message,normalized);
2839
2982
  setSessionStatus('tool',pendingStructuralToolMessage);
2840
2983
  return true;
@@ -2858,7 +3001,7 @@
2858
3001
  const committedTurn=typeof session.transcript==='function'
2859
3002
  ?latestCommittedTranscriptTurn(
2860
3003
  await session.transcript(),
2861
- sessionRequestMessage
3004
+ sessionRequestMessages
2862
3005
  )
2863
3006
  :null;
2864
3007
  const message=[...chatOutput.children].find(
@@ -2872,41 +3015,32 @@
2872
3015
  result.message.content,
2873
3016
  committedTurn?.response?.timestamp??result.message.timestamp
2874
3017
  );
2875
- const requestMessage=[...chatOutput.children].find(
3018
+ const requestMessages=[...chatOutput.children].filter(
2876
3019
  candidate=>candidate.dataset.operationId===context.operationId
2877
- )||null;
2878
- setTranscriptMessageTimestamp(
2879
- requestMessage,
2880
- committedTurn?.request?.timestamp
2881
3020
  );
2882
- const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
2883
- if(terminalToolCalls.length>1){
2884
- throw chatError(
2885
- 'The chat session returned parallel structural tool calls.',
2886
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
3021
+ for(const [index,requestMessage] of requestMessages.entries()){
3022
+ setTranscriptMessageTimestamp(
3023
+ requestMessage,
3024
+ committedTurn?.requests?.[index]?.timestamp
3025
+ ??committedTurn?.request?.timestamp
2887
3026
  );
2888
3027
  }
3028
+ const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
2889
3029
  if(
2890
- streamedStructuralToolCall
2891
- &&(
2892
- terminalToolCalls.length!==1
2893
- ||!sameStructuralToolCall(
2894
- streamedStructuralToolCall,
2895
- terminalToolCalls[0]
2896
- )
2897
- )
3030
+ streamedStructuralToolCalls.length
3031
+ &&!sameStructuralToolCalls(streamedStructuralToolCalls,terminalToolCalls)
2898
3032
  ){
2899
3033
  throw chatError(
2900
- 'The terminal structural tool call does not match the streamed call.',
3034
+ 'The terminal structural tool calls do not match the streamed calls.',
2901
3035
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2902
3036
  );
2903
3037
  }
2904
- if(!streamedStructuralToolCall&&terminalToolCalls.length){
2905
- appendVisibleToolCall(message,terminalToolCalls[0]);
3038
+ if(!streamedStructuralToolCalls.length){
3039
+ for(const call of terminalToolCalls){
3040
+ appendVisibleToolCall(message,call);
3041
+ }
2906
3042
  }
2907
- setPendingStructuralToolCall(
2908
- terminalToolCalls.length?terminalToolCalls.at(-1):null
2909
- );
3043
+ setPendingStructuralToolCalls(terminalToolCalls);
2910
3044
  setSessionStatus(
2911
3045
  pendingStructuralToolMessage?'tool':'ready',
2912
3046
  pendingStructuralToolMessage||'Chat ready.'
@@ -2916,7 +3050,11 @@
2916
3050
  request,
2917
3051
  result,
2918
3052
  message:result.message,
2919
- context
3053
+ context,
3054
+ pendingTool:pendingStructuralToolSummary(),
3055
+ pendingTools:pendingStructuralToolSummaries(),
3056
+ pendingToolCall:pendingStructuralToolCallComplete(),
3057
+ pendingToolCalls:pendingStructuralToolCallsComplete()
2920
3058
  };
2921
3059
  if(activeSessionMessageToken===sessionMessageToken){
2922
3060
  activeSessionMessageToken=null;
@@ -2939,38 +3077,42 @@
2939
3077
  return result;
2940
3078
  }catch(error){
2941
3079
  if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2942
- setPendingStructuralToolCall(previousPendingToolCall);
3080
+ setPendingStructuralToolCalls(previousPendingToolCalls);
2943
3081
  }
2944
3082
  if(
2945
3083
  !destroyed
2946
3084
  &&bindingGeneration===sessionBindingGeneration
2947
3085
  &&internalStructuralToolFailure(error)
2948
3086
  ){
2949
- if(sessionRequestMessage.role==='tool'){
3087
+ if(sessionRequestMessages.every(message=>message.role==='tool')){
2950
3088
  const response=[...chatOutput.children].find(
2951
3089
  candidate=>candidate.id===`message-${messageId}`
2952
3090
  )||null;
2953
- const requestMessage=[...chatOutput.children].find(
3091
+ const requestMessages=[...chatOutput.children].filter(
2954
3092
  candidate=>candidate.dataset.operationId===context.operationId
2955
- )||null;
3093
+ );
2956
3094
  response?.remove();
2957
- requestMessage?.remove();
3095
+ for(const requestMessage of requestMessages){
3096
+ requestMessage.remove();
3097
+ }
2958
3098
  scrollTranscriptToBottom();
2959
3099
  }else{
2960
3100
  restoreRejectedStructuralDraft(messageId,context.operationId,text);
2961
3101
  }
2962
3102
  console.error('Arcane structural tool protocol failure.',error);
2963
3103
  setSessionStatus(
2964
- pendingStructuralToolCall?'tool':'ready',
3104
+ pendingStructuralToolCalls.length?'tool':'ready',
2965
3105
  pendingStructuralToolMessage||'Chat ready.'
2966
3106
  );
2967
3107
  }else if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2968
3108
  console.error('Arcane chat request failed.',error);
2969
- if(sessionRequestMessage.role==='tool'){
2970
- const requestMessage=[...chatOutput.children].find(
3109
+ if(sessionRequestMessages.every(message=>message.role==='tool')){
3110
+ const requestMessages=[...chatOutput.children].filter(
2971
3111
  candidate=>candidate.dataset.operationId===context.operationId
2972
- )||null;
2973
- requestMessage?.remove();
3112
+ );
3113
+ for(const requestMessage of requestMessages){
3114
+ requestMessage.remove();
3115
+ }
2974
3116
  }
2975
3117
  renderSessionMessageFailure(messageId,error);
2976
3118
  setSessionStatus(
@@ -3010,18 +3152,18 @@
3010
3152
  }
3011
3153
  }
3012
3154
 
3013
- async function submitToolResult(options={},context={}){
3155
+ async function submitToolResults(options={},context={}){
3014
3156
  if(destroyed||host.conversationComplete){
3015
3157
  return false;
3016
3158
  }
3017
3159
  if(!isPlainRecord(options)||!isPlainRecord(context)){
3018
- throw new TypeError('Tool-result options and context must be plain objects.');
3160
+ throw new TypeError('Tool-results options and context must be plain objects.');
3019
3161
  }
3020
3162
  const unsupportedOption=Object.keys(options).find(
3021
- key=>!['disposition','message','persist','request','toolCallId'].includes(key)
3163
+ key=>!['request','results'].includes(key)
3022
3164
  );
3023
3165
  if(unsupportedOption){
3024
- throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
3166
+ throw new TypeError(`Unsupported tool-results option: ${unsupportedOption}.`);
3025
3167
  }
3026
3168
  const unsupportedContext=Object.keys(context).find(
3027
3169
  key=>!['operationId','signal'].includes(key)
@@ -3035,53 +3177,102 @@
3035
3177
  if(!host.aiAvailability.llm){
3036
3178
  return false;
3037
3179
  }
3038
- if(!pendingStructuralToolCall){
3039
- const error=new TypeError('There is no pending structural tool call to settle.');
3180
+ if(!pendingStructuralToolCalls.length){
3181
+ const error=new TypeError('There are no pending structural tool calls to settle.');
3040
3182
  error.code='AI_CHAT_TOOL_RESULT_NOT_PENDING';
3041
3183
  throw error;
3042
3184
  }
3043
- const disposition=options.disposition;
3185
+ if(!Array.isArray(options.results)||options.results.length===0){
3186
+ throw new TypeError('Tool results must be a nonempty array.');
3187
+ }
3044
3188
  const dispositions=new Map([
3045
3189
  ['executed','Executed'],
3046
3190
  ['declined','Declined'],
3047
3191
  ['cancelled','Cancelled'],
3048
3192
  ['not-executed','Not executed']
3049
3193
  ]);
3050
- if(!dispositions.has(disposition)){
3051
- throw new TypeError(
3052
- 'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
3053
- );
3054
- }
3055
- if(typeof options.message!=='string'||!options.message.trim()){
3056
- throw new TypeError('Tool-result message must contain user-facing text.');
3057
- }
3058
3194
  const perTurnRequest=options.request??null;
3059
3195
  if(perTurnRequest!==null&&!isPlainRecord(perTurnRequest)){
3060
3196
  throw new TypeError('Tool-result request options must be a plain object.');
3061
3197
  }
3062
3198
  const managedRequestField=perTurnRequest&&Object.keys(perTurnRequest).find(
3063
- key=>['messages','onChunk','onResponse','onToolCall','signal','stream'].includes(key)
3199
+ key=>[
3200
+ 'message','messages','onChunk','onComplete','onDataChunk','onDataResult',
3201
+ 'onResponse','onToolCall','signal','stream'
3202
+ ].includes(key)
3064
3203
  );
3065
3204
  if(managedRequestField){
3066
3205
  throw new TypeError(
3067
3206
  `Tool-result request.${managedRequestField} is managed by the chat session.`
3068
3207
  );
3069
3208
  }
3070
- const toolCallId=options.toolCallId??pendingStructuralToolCall.id;
3071
- if(typeof toolCallId!=='string'||toolCallId!==pendingStructuralToolCall.id){
3072
- const error=new TypeError('Tool-result ID does not match the pending structural tool call.');
3073
- error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
3209
+ const pendingById=new Map(
3210
+ pendingStructuralToolCalls.map(call=>[call.id,call])
3211
+ );
3212
+ const resultsById=new Map();
3213
+ for(const [index,result] of options.results.entries()){
3214
+ if(!isPlainRecord(result)){
3215
+ throw new TypeError(`Tool result ${index+1} must be a plain object.`);
3216
+ }
3217
+ const unsupportedResultField=Object.keys(result).find(
3218
+ key=>!['disposition','message','persist','toolCallId'].includes(key)
3219
+ );
3220
+ if(unsupportedResultField){
3221
+ throw new TypeError(
3222
+ `Unsupported tool result ${index+1} field: ${unsupportedResultField}.`
3223
+ );
3224
+ }
3225
+ const toolCallId=result.toolCallId;
3226
+ if(
3227
+ typeof toolCallId!=='string'
3228
+ ||!pendingById.has(toolCallId)
3229
+ ||resultsById.has(toolCallId)
3230
+ ){
3231
+ const error=new TypeError(
3232
+ `Tool result ${index+1} does not match a pending structural tool call.`
3233
+ );
3234
+ error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
3235
+ throw error;
3236
+ }
3237
+ if(!dispositions.has(result.disposition)){
3238
+ throw new TypeError(
3239
+ 'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
3240
+ );
3241
+ }
3242
+ if(typeof result.message!=='string'||!result.message.trim()){
3243
+ throw new TypeError('Tool-result message must contain user-facing text.');
3244
+ }
3245
+ const persist=result.persist??true;
3246
+ if(typeof persist!=='boolean'){
3247
+ throw new TypeError('Tool-result persist must be boolean.');
3248
+ }
3249
+ resultsById.set(toolCallId,{
3250
+ content:`${dispositions.get(result.disposition)} — ${result.message}`,
3251
+ persist,
3252
+ role:'tool',
3253
+ tool_call_id:toolCallId
3254
+ });
3255
+ }
3256
+ if(resultsById.size!==pendingById.size){
3257
+ const error=new TypeError(
3258
+ 'Every pending structural tool call must receive one matching result.'
3259
+ );
3260
+ error.code='AI_CHAT_TOOL_RESULT_REQUIRED';
3074
3261
  throw error;
3075
3262
  }
3076
- const persist=options.persist??true;
3077
- if(typeof persist!=='boolean'){
3078
- throw new TypeError('Tool-result persist must be boolean.');
3263
+ const orderedResults=pendingStructuralToolCalls.map(
3264
+ call=>resultsById.get(call.id)
3265
+ );
3266
+ if(new Set(orderedResults.map(result=>result.persist)).size>1){
3267
+ const error=new TypeError(
3268
+ 'All tool results in one atomic settlement must use the same persistence choice.'
3269
+ );
3270
+ error.code='AI_CHAT_INCOHERENT_PERSISTENCE';
3271
+ throw error;
3079
3272
  }
3080
-
3081
- const text=`${dispositions.get(disposition)} — ${options.message}`;
3082
3273
  const operationId=typeof context.operationId==='string'&&context.operationId
3083
3274
  ?context.operationId
3084
- :nextChatOperationId('tool-result');
3275
+ :nextChatOperationId('tool-results');
3085
3276
  const ownership=createChatSubmissionOwnership(context.signal??null);
3086
3277
  const eventContext={
3087
3278
  source:'tool',
@@ -3094,17 +3285,15 @@
3094
3285
  return false;
3095
3286
  }
3096
3287
  try{
3097
- const visibleMessage=appendTranscriptMessage('tool',text,'Tool');
3098
- visibleMessage.dataset.operationId=operationId;
3288
+ for(const result of orderedResults){
3289
+ const visibleMessage=appendTranscriptMessage('tool',result.content,'Tool');
3290
+ visibleMessage.dataset.operationId=operationId;
3291
+ visibleMessage.dataset.toolCallId=result.tool_call_id;
3292
+ }
3099
3293
  const result=sendMessageThroughBoundSession(
3100
- text,
3294
+ orderedResults.map(message=>message.content).join('\n'),
3101
3295
  eventContext,
3102
- {
3103
- content:text,
3104
- persist,
3105
- role:'tool',
3106
- tool_call_id:toolCallId
3107
- },
3296
+ orderedResults,
3108
3297
  perTurnRequest
3109
3298
  );
3110
3299
  return observeHostSubmission(result,eventContext,ownership);
@@ -3114,6 +3303,50 @@
3114
3303
  }
3115
3304
  }
3116
3305
 
3306
+ async function submitToolResult(options={},context={}){
3307
+ if(
3308
+ destroyed
3309
+ ||host.conversationComplete
3310
+ ||!boundChatSession
3311
+ ||sessionBindingPending
3312
+ ||sessionMessagePending
3313
+ ||!host.aiAvailability.llm
3314
+ ){
3315
+ return false;
3316
+ }
3317
+ if(!isPlainRecord(options)){
3318
+ throw new TypeError('Tool-result options must be a plain object.');
3319
+ }
3320
+ const unsupportedOption=Object.keys(options).find(
3321
+ key=>!['disposition','message','persist','request','toolCallId'].includes(key)
3322
+ );
3323
+ if(unsupportedOption){
3324
+ throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
3325
+ }
3326
+ if(pendingStructuralToolCalls.length!==1){
3327
+ const error=new TypeError(
3328
+ pendingStructuralToolCalls.length
3329
+ ?'Parallel structural tool calls must be settled together with submitToolResults().'
3330
+ :'There is no pending structural tool call to settle.'
3331
+ );
3332
+ error.code=pendingStructuralToolCalls.length
3333
+ ?'AI_CHAT_TOOL_RESULT_BATCH_REQUIRED'
3334
+ :'AI_CHAT_TOOL_RESULT_NOT_PENDING';
3335
+ throw error;
3336
+ }
3337
+ const {request,...result}=options;
3338
+ return submitToolResults(
3339
+ {
3340
+ results:[{
3341
+ ...result,
3342
+ toolCallId:result.toolCallId??pendingStructuralToolCalls[0].id
3343
+ }],
3344
+ ...(request===undefined?{}:{request})
3345
+ },
3346
+ context
3347
+ );
3348
+ }
3349
+
3117
3350
  function observeHostSubmission(result,context,ownership){
3118
3351
  const observed=hostSubmissionBarrier.track(result).then(
3119
3352
  function settleHostSubmission(value){
@@ -3126,9 +3359,12 @@
3126
3359
  if(destroyed||context.signal.aborted){
3127
3360
  return false;
3128
3361
  }
3129
- if(internalStructuralToolFailure(error)){
3130
- return false;
3131
- }
3362
+ console.error(
3363
+ internalStructuralToolFailure(error)
3364
+ ?'Arcane structural tool protocol failure.'
3365
+ :'The chat message could not be submitted.',
3366
+ error
3367
+ );
3132
3368
  dispatchChatEvent(
3133
3369
  'chat-send-error',
3134
3370
  {error,context:{...context}},
@@ -3146,7 +3382,6 @@
3146
3382
  }
3147
3383
  }
3148
3384
  );
3149
- console.error('The chat message could not be submitted.',error);
3150
3385
  return false;
3151
3386
  }
3152
3387
  ).finally(
@@ -3173,7 +3408,12 @@
3173
3408
  if(!host.aiAvailability.llm){
3174
3409
  return false;
3175
3410
  }
3176
- if(sessionBindingPending||sessionMessagePending||pendingStructuralToolMessage){
3411
+ if(
3412
+ sessionBindingPending
3413
+ ||sessionMessagePending
3414
+ ||pendingStructuralToolMessage
3415
+ ||sessionHistoryRecoveryMessage
3416
+ ){
3177
3417
  return false;
3178
3418
  }
3179
3419
  const submissionContext={
@@ -3333,6 +3573,25 @@
3333
3573
  return false;
3334
3574
  }
3335
3575
 
3576
+ function handlePageHide(event){
3577
+ if(event.persisted===true){
3578
+ return;
3579
+ }
3580
+ destroy();
3581
+ }
3582
+
3583
+ function handlePageShow(event){
3584
+ if(event.persisted!==true||destroyed){
3585
+ return;
3586
+ }
3587
+ restoreFromPageCache();
3588
+ }
3589
+
3590
+ function restoreFromPageCache(){
3591
+ setAIAvailability();
3592
+ scrollTranscriptToBottom();
3593
+ }
3594
+
3336
3595
  function destroy(){
3337
3596
  if(destroyed){
3338
3597
  return false;
@@ -3349,7 +3608,8 @@
3349
3608
  }
3350
3609
  aiRuntimeStateAbortController.abort(destroyReason);
3351
3610
  aiActivationController.destroy();
3352
- window.removeEventListener('pagehide',destroy);
3611
+ window.removeEventListener('pagehide',handlePageHide);
3612
+ window.removeEventListener('pageshow',handlePageShow);
3353
3613
  conversationTimeboxUnsubscribe?.();
3354
3614
  conversationTimeboxUnsubscribe=null;
3355
3615
  conversationTimeboxController=null;
@@ -3357,7 +3617,7 @@
3357
3617
  sessionBindingPending=false;
3358
3618
  sessionMessagePending=false;
3359
3619
  activeSessionMessageToken=null;
3360
- setPendingStructuralToolCall();
3620
+ setPendingStructuralToolCalls();
3361
3621
  boundChatSession=null;
3362
3622
  boundChatAI=null;
3363
3623
  host.session=null;