arcane-os 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
- package/browser-runtime/ai/model-controller.mjs +439 -95
- package/package.json +1 -1
- package/runtime/arcane/components/assistant-panel.html +2 -1
- package/runtime/arcane/components/chat.html +469 -220
- package/runtime/arcane/components/speech.html +33 -13
- package/runtime/arcane/components/voice-transcription.html +27 -3
- package/runtime/arcane/entities/Chat.js +165 -97
- package/runtime/arcane/entities/IntentEnvelope.js +52 -118
- package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
- package/runtime/arcane/entities/User.js +38 -44
- package/runtime/arcane/modules/AI.js +569 -302
- package/runtime/arcane/modules/AIProviderRuntime.js +776 -151
- package/runtime/arcane/modules/AIRuntimeState.js +22 -24
- package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
- package/runtime/arcane/modules/CommunicationHub.js +90 -94
- package/runtime/arcane/modules/ComponentContracts.js +23 -9
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
- package/runtime/arcane/modules/ConversationTimebox.js +76 -104
- package/runtime/arcane/modules/DBLS.js +14 -12
- package/runtime/arcane/modules/DBOPFS.js +20 -15
- package/runtime/arcane/modules/Errors.js +196 -436
- package/runtime/arcane/modules/HTMLImport.js +49 -32
- package/runtime/arcane/modules/Ollama.js +16 -14
- package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
- package/runtime/arcane/modules/RecordReviewStore.js +40 -35
- package/runtime/arcane/modules/TerminalClient.js +12 -14
- package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
- package/runtime/arcane/modules/ThemeManager.js +5 -5
- package/runtime/arcane/modules/TimeGuard.js +5 -65
- package/runtime/arcane/modules/WaitForComponent.js +43 -40
- package/src/installed-sdk-runtime.mjs +11 -1
|
@@ -149,7 +149,8 @@
|
|
|
149
149
|
subscribeAIRuntimeState
|
|
150
150
|
} = await import('../modules/AIRuntimeState.js');
|
|
151
151
|
const {
|
|
152
|
-
createSTTActivationController
|
|
152
|
+
createSTTActivationController,
|
|
153
|
+
formatAIRuntimeProgress
|
|
153
154
|
} = await import('../modules/ComponentContracts.js');
|
|
154
155
|
|
|
155
156
|
const host = this;
|
|
@@ -333,8 +334,13 @@
|
|
|
333
334
|
muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
|
|
334
335
|
window.addEventListener(
|
|
335
336
|
'pagehide',
|
|
336
|
-
|
|
337
|
-
{
|
|
337
|
+
handlePageHide,
|
|
338
|
+
{ signal: runtimeStateAbortController.signal }
|
|
339
|
+
);
|
|
340
|
+
window.addEventListener(
|
|
341
|
+
'pageshow',
|
|
342
|
+
handlePageShow,
|
|
343
|
+
{ signal: runtimeStateAbortController.signal }
|
|
338
344
|
);
|
|
339
345
|
|
|
340
346
|
subscribeAIRuntimeState(
|
|
@@ -723,10 +729,10 @@
|
|
|
723
729
|
return 'Voice release requested.';
|
|
724
730
|
}
|
|
725
731
|
if (ttsRole.state === 'loading') {
|
|
726
|
-
return `Voice ${
|
|
732
|
+
return `Voice ${formatAIRuntimeProgress(ttsRole.progress, 'loading')}; Cancel is available.`;
|
|
727
733
|
}
|
|
728
734
|
if (ttsRole.state === 'unloading') {
|
|
729
|
-
return `Voice ${
|
|
735
|
+
return `Voice ${formatAIRuntimeProgress(ttsRole.progress, 'releasing')}.`;
|
|
730
736
|
}
|
|
731
737
|
if (ttsRole.state === 'ready') {
|
|
732
738
|
if (host.muted) {
|
|
@@ -746,13 +752,6 @@
|
|
|
746
752
|
return 'Voice unavailable.';
|
|
747
753
|
}
|
|
748
754
|
|
|
749
|
-
function formatRoleProgress(progress, fallback) {
|
|
750
|
-
if (!progress) {
|
|
751
|
-
return fallback;
|
|
752
|
-
}
|
|
753
|
-
return progress.phase;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
755
|
function visibleErrorMessage(error, fallback) {
|
|
757
756
|
try {
|
|
758
757
|
if (error?.userSafe === true) {
|
|
@@ -1693,6 +1692,26 @@
|
|
|
1693
1692
|
keyboardRecording = false;
|
|
1694
1693
|
}
|
|
1695
1694
|
|
|
1695
|
+
function handlePageHide(event) {
|
|
1696
|
+
if (event.persisted === true) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
destroy();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function handlePageShow(event) {
|
|
1703
|
+
if (event.persisted !== true || destroyed) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
restoreFromPageCache();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function restoreFromPageCache() {
|
|
1710
|
+
synchronizeAIMutedState();
|
|
1711
|
+
renderControls();
|
|
1712
|
+
renderStatus();
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1696
1715
|
function destroy() {
|
|
1697
1716
|
if (destroyed) {
|
|
1698
1717
|
return;
|
|
@@ -1701,7 +1720,8 @@
|
|
|
1701
1720
|
destroyed = true;
|
|
1702
1721
|
runtimeStateAbortController.abort();
|
|
1703
1722
|
sttActivationController.destroy();
|
|
1704
|
-
window.removeEventListener('pagehide',
|
|
1723
|
+
window.removeEventListener('pagehide', handlePageHide);
|
|
1724
|
+
window.removeEventListener('pageshow', handlePageShow);
|
|
1705
1725
|
microphonePermissionStatus?.removeEventListener?.(
|
|
1706
1726
|
'change',
|
|
1707
1727
|
microphonePermissionListener
|
|
@@ -212,8 +212,13 @@
|
|
|
212
212
|
completeButton.addEventListener('click',completeStream,lifecycleListenerOptions);
|
|
213
213
|
window.addEventListener(
|
|
214
214
|
'pagehide',
|
|
215
|
-
|
|
216
|
-
{
|
|
215
|
+
handlePageHide,
|
|
216
|
+
{signal:runtimeStateAbortController.signal}
|
|
217
|
+
);
|
|
218
|
+
window.addEventListener(
|
|
219
|
+
'pageshow',
|
|
220
|
+
handlePageShow,
|
|
221
|
+
{signal:runtimeStateAbortController.signal}
|
|
217
222
|
);
|
|
218
223
|
|
|
219
224
|
const sttActivationController=createSTTActivationController(
|
|
@@ -982,6 +987,24 @@
|
|
|
982
987
|
};
|
|
983
988
|
}
|
|
984
989
|
|
|
990
|
+
function handlePageHide(event){
|
|
991
|
+
if(event.persisted===true){
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
destroy();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function handlePageShow(event){
|
|
998
|
+
if(event.persisted!==true||destroyed){
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
restoreFromPageCache();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function restoreFromPageCache(){
|
|
1005
|
+
renderState();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
985
1008
|
function destroy(){
|
|
986
1009
|
if(destroyed){
|
|
987
1010
|
return true;
|
|
@@ -989,7 +1012,8 @@
|
|
|
989
1012
|
destroyed=true;
|
|
990
1013
|
runtimeStateAbortController.abort();
|
|
991
1014
|
sttActivationController.destroy();
|
|
992
|
-
window.removeEventListener('pagehide',
|
|
1015
|
+
window.removeEventListener('pagehide',handlePageHide);
|
|
1016
|
+
window.removeEventListener('pageshow',handlePageShow);
|
|
993
1017
|
if(!cancelSTTOperation('component-destroyed')){
|
|
994
1018
|
sessionGeneration++;
|
|
995
1019
|
}
|
|
@@ -7,10 +7,9 @@ import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
|
|
|
7
7
|
const is = new Is(false);
|
|
8
8
|
|
|
9
9
|
function plainRecord(value){
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return false;
|
|
11
|
+
const prototype=Object.getPrototypeOf(value);
|
|
12
|
+
return prototype===Object.prototype||prototype===null;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
function coded(error,code){
|
|
@@ -18,15 +17,32 @@ function coded(error,code){
|
|
|
18
17
|
return error;
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
function copyCompleteValue(value,seen=new Map()){
|
|
21
|
+
if(value===null||typeof value!=='object')return value;
|
|
22
|
+
if(seen.has(value))return seen.get(value);
|
|
23
|
+
if(Array.isArray(value)){
|
|
24
|
+
const result=[];
|
|
25
|
+
seen.set(value,result);
|
|
26
|
+
for(const item of value)result.push(copyCompleteValue(item,seen));
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
const result={};
|
|
30
|
+
seen.set(value,result);
|
|
31
|
+
for(const [key,descriptor] of Object.entries(
|
|
32
|
+
Object.getOwnPropertyDescriptors(value)
|
|
33
|
+
)){
|
|
34
|
+
if(Object.hasOwn(descriptor,'value')){
|
|
35
|
+
result[key]=copyCompleteValue(descriptor.value,seen);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
function copyToolCalls(value){
|
|
22
42
|
if(value===undefined) return null;
|
|
23
|
-
if(!Array.isArray(value)
|
|
24
|
-
const error=new TypeError(
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
error.code=Array.isArray(value)&&value.length>1
|
|
28
|
-
?'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
|
|
29
|
-
:'AI_CHAT_INVALID_TOOL_CALL';
|
|
43
|
+
if(!Array.isArray(value)){
|
|
44
|
+
const error=new TypeError('assistantMessage.tool_calls must be an array.');
|
|
45
|
+
error.code='AI_CHAT_INVALID_TOOL_CALL';
|
|
30
46
|
throw error;
|
|
31
47
|
}
|
|
32
48
|
const ids=new Set();
|
|
@@ -78,7 +94,8 @@ function copyToolCalls(value){
|
|
|
78
94
|
}
|
|
79
95
|
ids.add(id);
|
|
80
96
|
return {
|
|
81
|
-
|
|
97
|
+
...call,
|
|
98
|
+
function:{...call.function},
|
|
82
99
|
id,
|
|
83
100
|
type:'function'
|
|
84
101
|
};
|
|
@@ -103,13 +120,18 @@ function turnMessage(value,label){
|
|
|
103
120
|
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
104
121
|
);
|
|
105
122
|
}
|
|
106
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
...value,
|
|
125
|
+
content:value.content,
|
|
126
|
+
role:'tool',
|
|
127
|
+
tool_call_id:toolCallId
|
|
128
|
+
};
|
|
107
129
|
}
|
|
108
|
-
return {content:value.content,role:'user'};
|
|
130
|
+
return {...value,content:value.content,role:'user'};
|
|
109
131
|
}
|
|
110
132
|
|
|
111
|
-
function
|
|
112
|
-
|
|
133
|
+
function pendingToolCalls(messages){
|
|
134
|
+
const pending=new Map();
|
|
113
135
|
for(const [index,message] of messages.entries()){
|
|
114
136
|
if(!plainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
|
|
115
137
|
throw coded(
|
|
@@ -129,28 +151,30 @@ function pendingToolCallId(messages){
|
|
|
129
151
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
130
152
|
);
|
|
131
153
|
}
|
|
132
|
-
if(message.role==='system'&&pending){
|
|
154
|
+
if(message.role==='system'&&pending.size){
|
|
133
155
|
throw coded(
|
|
134
|
-
new TypeError(`Chat message ${index+1} precedes the pending tool
|
|
156
|
+
new TypeError(`Chat message ${index+1} precedes the pending tool results.`),
|
|
135
157
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
136
158
|
);
|
|
137
159
|
}
|
|
138
|
-
if(message.role==='user'&&pending){
|
|
160
|
+
if(message.role==='user'&&pending.size){
|
|
139
161
|
throw coded(
|
|
140
|
-
new TypeError(`Chat message ${index+1} starts a user turn before the pending tool
|
|
162
|
+
new TypeError(`Chat message ${index+1} starts a user turn before the pending tool results.`),
|
|
141
163
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
142
164
|
);
|
|
143
165
|
}
|
|
144
166
|
if(message.role==='assistant'){
|
|
145
|
-
if(pending){
|
|
167
|
+
if(pending.size){
|
|
146
168
|
throw coded(
|
|
147
|
-
new TypeError(`Chat message ${index+1} precedes the pending tool
|
|
169
|
+
new TypeError(`Chat message ${index+1} precedes the pending tool results.`),
|
|
148
170
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
149
171
|
);
|
|
150
172
|
}
|
|
151
173
|
if(message.tool_calls){
|
|
152
174
|
const calls=copyToolCalls(message.tool_calls);
|
|
153
|
-
|
|
175
|
+
for(const call of calls){
|
|
176
|
+
pending.set(call.id,call);
|
|
177
|
+
}
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
180
|
if(message.role==='tool'){
|
|
@@ -167,18 +191,37 @@ function pendingToolCallId(messages){
|
|
|
167
191
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
168
192
|
);
|
|
169
193
|
}
|
|
170
|
-
if(!pending
|
|
194
|
+
if(!pending.has(toolCallId)){
|
|
171
195
|
throw coded(
|
|
172
|
-
new TypeError(`Chat message ${index+1} does not match
|
|
196
|
+
new TypeError(`Chat message ${index+1} does not match a pending tool call.`),
|
|
173
197
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
174
198
|
);
|
|
175
199
|
}
|
|
176
|
-
pending
|
|
200
|
+
pending.delete(toolCallId);
|
|
177
201
|
}
|
|
178
202
|
}
|
|
179
203
|
return pending;
|
|
180
204
|
}
|
|
181
205
|
|
|
206
|
+
function turnMessages(requestMessage,requestMessages){
|
|
207
|
+
const hasMessage=requestMessage!==undefined;
|
|
208
|
+
const hasMessages=requestMessages!==undefined;
|
|
209
|
+
if(hasMessage===hasMessages){
|
|
210
|
+
throw new TypeError('Provide exactly one of requestMessage or requestMessages.');
|
|
211
|
+
}
|
|
212
|
+
const values=hasMessages?requestMessages:[requestMessage];
|
|
213
|
+
if(!Array.isArray(values)||values.length===0){
|
|
214
|
+
throw new TypeError('requestMessages must be a nonempty array.');
|
|
215
|
+
}
|
|
216
|
+
const records=values.map((value,index)=>
|
|
217
|
+
turnMessage(value,hasMessages?`requestMessages[${index}]`:'requestMessage')
|
|
218
|
+
);
|
|
219
|
+
if(records.length>1&&records.some(message=>message.role!=='tool')){
|
|
220
|
+
throw new TypeError('A multi-message request may contain only tool results.');
|
|
221
|
+
}
|
|
222
|
+
return records;
|
|
223
|
+
}
|
|
224
|
+
|
|
182
225
|
/**
|
|
183
226
|
* Represents a single chat message.
|
|
184
227
|
*
|
|
@@ -191,7 +234,7 @@ function pendingToolCallId(messages){
|
|
|
191
234
|
* @property {boolean} [memory_excluded]
|
|
192
235
|
* Internal persistence marker omitted from model-facing messages.
|
|
193
236
|
* @property {boolean} [ui_hidden]
|
|
194
|
-
*
|
|
237
|
+
* Legacy persistence metadata retained without suppressing the public transcript.
|
|
195
238
|
* @property {boolean} [persistence_excluded]
|
|
196
239
|
* Internal marker for session-only messages omitted from durable chat and memory.
|
|
197
240
|
* @property {Array<Object>} [tool_calls]
|
|
@@ -329,16 +372,16 @@ class ChatEntity{
|
|
|
329
372
|
* This is intended to be passed directly into
|
|
330
373
|
* the AI request pipeline.
|
|
331
374
|
*
|
|
332
|
-
* @returns {
|
|
375
|
+
* @returns {Array<*>}
|
|
333
376
|
*/
|
|
334
377
|
get messages(){
|
|
378
|
+
pendingToolCalls(this.#messages);
|
|
335
379
|
return this.#messages.map(function publicChatMessage(message){
|
|
336
380
|
const copy={...message};
|
|
337
381
|
if(copy.tool_calls){
|
|
338
382
|
copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
type:call.type,
|
|
383
|
+
...call,
|
|
384
|
+
function:{...call.function}
|
|
342
385
|
}));
|
|
343
386
|
}
|
|
344
387
|
delete copy.memory_excluded;
|
|
@@ -354,41 +397,28 @@ class ChatEntity{
|
|
|
354
397
|
* Model-facing callers should continue to use `messages`, which intentionally
|
|
355
398
|
* omits display metadata.
|
|
356
399
|
*
|
|
357
|
-
* @returns {
|
|
400
|
+
* @returns {Array<*>}
|
|
358
401
|
*/
|
|
359
402
|
get transcript(){
|
|
360
|
-
return this.#messages
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
});
|
|
403
|
+
return this.#messages.map(function publicTranscriptMessage(message){
|
|
404
|
+
return copyCompleteValue(message);
|
|
405
|
+
});
|
|
376
406
|
}
|
|
377
407
|
|
|
378
408
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* Messages should be modified using the helper
|
|
382
|
-
* methods such as `addUserMessage` and `addAIMessage`.
|
|
383
|
-
*
|
|
384
|
-
* @param {ChatMessage[]} v
|
|
385
|
-
* Ignored value.
|
|
409
|
+
* Replaces the current in-memory transcript with complete copied records.
|
|
386
410
|
*
|
|
387
|
-
* @
|
|
411
|
+
* @param {Array<*>} v
|
|
412
|
+
* @returns {Array<*>}
|
|
388
413
|
*/
|
|
389
414
|
set messages(v){
|
|
390
|
-
|
|
391
|
-
|
|
415
|
+
if(!Array.isArray(v)){
|
|
416
|
+
throw new TypeError('messages must be an array.');
|
|
417
|
+
}
|
|
418
|
+
this.#messages=v.map(message=>copyCompleteValue(message));
|
|
419
|
+
this.#persistedMessageCount=0;
|
|
420
|
+
this.#saved=false;
|
|
421
|
+
return this.transcript;
|
|
392
422
|
}
|
|
393
423
|
|
|
394
424
|
/**
|
|
@@ -401,13 +431,15 @@ class ChatEntity{
|
|
|
401
431
|
}
|
|
402
432
|
|
|
403
433
|
/**
|
|
404
|
-
*
|
|
405
|
-
* Saved status is managed internally.
|
|
434
|
+
* Replaces the current saved-status flag.
|
|
406
435
|
*
|
|
407
436
|
* @returns {boolean}
|
|
408
437
|
*/
|
|
409
438
|
set saved(v){
|
|
410
|
-
|
|
439
|
+
if(typeof v!=='boolean'){
|
|
440
|
+
throw new TypeError('saved must be a boolean.');
|
|
441
|
+
}
|
|
442
|
+
this.#saved=v;
|
|
411
443
|
return this.#saved;
|
|
412
444
|
}
|
|
413
445
|
|
|
@@ -420,7 +452,8 @@ class ChatEntity{
|
|
|
420
452
|
* @param {string} text
|
|
421
453
|
* Message content from the user.
|
|
422
454
|
* @param {{hidden?:boolean,persist?:boolean}} options
|
|
423
|
-
*
|
|
455
|
+
* The legacy hidden option excludes the message from memory extraction without
|
|
456
|
+
* removing it from the complete saved or UI transcript.
|
|
424
457
|
*/
|
|
425
458
|
addUserMessage(text='',{hidden=false,persist=true}={}){
|
|
426
459
|
if(!is.string(text)){
|
|
@@ -432,9 +465,9 @@ class ChatEntity{
|
|
|
432
465
|
if(!is.boolean(persist)){
|
|
433
466
|
throw new Error('persist must be boolean');
|
|
434
467
|
}
|
|
435
|
-
if(
|
|
468
|
+
if(pendingToolCalls(this.#messages).size){
|
|
436
469
|
throw coded(
|
|
437
|
-
new TypeError('The pending structural tool
|
|
470
|
+
new TypeError('The pending structural tool results must be supplied before a new user turn.'),
|
|
438
471
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
439
472
|
);
|
|
440
473
|
}
|
|
@@ -445,7 +478,6 @@ class ChatEntity{
|
|
|
445
478
|
timestamp:Date.now()
|
|
446
479
|
};
|
|
447
480
|
if(hidden){
|
|
448
|
-
message.ui_hidden=true;
|
|
449
481
|
message.memory_excluded=true;
|
|
450
482
|
}
|
|
451
483
|
|
|
@@ -477,9 +509,9 @@ class ChatEntity{
|
|
|
477
509
|
if(!is.boolean(persist)){
|
|
478
510
|
throw new Error('persist must be boolean');
|
|
479
511
|
}
|
|
480
|
-
if(
|
|
512
|
+
if(pendingToolCalls(this.#messages).size){
|
|
481
513
|
throw coded(
|
|
482
|
-
new TypeError('The pending structural tool
|
|
514
|
+
new TypeError('The pending structural tool results must be supplied before another assistant turn.'),
|
|
483
515
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
484
516
|
);
|
|
485
517
|
}
|
|
@@ -521,9 +553,9 @@ class ChatEntity{
|
|
|
521
553
|
if(!is.boolean(persist)){
|
|
522
554
|
throw new TypeError('persist must be boolean.');
|
|
523
555
|
}
|
|
524
|
-
if(
|
|
556
|
+
if(pendingToolCalls(this.#messages).size){
|
|
525
557
|
throw coded(
|
|
526
|
-
new TypeError('The pending structural tool
|
|
558
|
+
new TypeError('The pending structural tool results must be supplied before another tool exchange.'),
|
|
527
559
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
528
560
|
);
|
|
529
561
|
}
|
|
@@ -578,9 +610,10 @@ class ChatEntity{
|
|
|
578
610
|
memoryRequest=messages=>ai.fetch(messages),
|
|
579
611
|
messagePersist=true,
|
|
580
612
|
requestMessage,
|
|
613
|
+
requestMessages,
|
|
581
614
|
responsePersist=messagePersist,
|
|
582
615
|
}={}){
|
|
583
|
-
const
|
|
616
|
+
const requests=turnMessages(requestMessage,requestMessages);
|
|
584
617
|
if(!plainRecord(assistantMessage)||assistantMessage.role!=='assistant'){
|
|
585
618
|
throw new TypeError('assistantMessage must be an assistant message.');
|
|
586
619
|
}
|
|
@@ -596,18 +629,32 @@ class ChatEntity{
|
|
|
596
629
|
'AI_CHAT_INCOHERENT_PERSISTENCE'
|
|
597
630
|
);
|
|
598
631
|
}
|
|
599
|
-
const
|
|
600
|
-
|
|
632
|
+
const pendingTools=pendingToolCalls(this.#messages);
|
|
633
|
+
const userRequest=requests.find(message=>message.role==='user');
|
|
634
|
+
const toolRequests=requests.filter(message=>message.role==='tool');
|
|
635
|
+
if(userRequest&&pendingTools.size){
|
|
601
636
|
throw coded(
|
|
602
|
-
new TypeError('The pending structural tool
|
|
637
|
+
new TypeError('The pending structural tool results must be supplied before a new user turn.'),
|
|
603
638
|
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
604
639
|
);
|
|
605
640
|
}
|
|
606
|
-
if(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
641
|
+
if(toolRequests.length){
|
|
642
|
+
const suppliedIds=new Set();
|
|
643
|
+
for(const request of toolRequests){
|
|
644
|
+
if(suppliedIds.has(request.tool_call_id)||!pendingTools.has(request.tool_call_id)){
|
|
645
|
+
throw coded(
|
|
646
|
+
new TypeError('The request tool results do not match the pending structural tool calls.'),
|
|
647
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE'
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
suppliedIds.add(request.tool_call_id);
|
|
651
|
+
}
|
|
652
|
+
if(suppliedIds.size!==pendingTools.size){
|
|
653
|
+
throw coded(
|
|
654
|
+
new TypeError('Every pending structural tool call must receive one matching result.'),
|
|
655
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED'
|
|
656
|
+
);
|
|
657
|
+
}
|
|
611
658
|
}
|
|
612
659
|
const toolCalls=copyToolCalls(assistantMessage.tool_calls);
|
|
613
660
|
const assistantContent=assistantMessage.content??'';
|
|
@@ -619,7 +666,7 @@ class ChatEntity{
|
|
|
619
666
|
!is.union(assistantContent,'string','number')
|
|
620
667
|
||(
|
|
621
668
|
!String(assistantContent)
|
|
622
|
-
&&!toolCalls
|
|
669
|
+
&&!toolCalls?.length
|
|
623
670
|
&&!(typeof assistantReasoning==='string'&&assistantReasoning.length)
|
|
624
671
|
)
|
|
625
672
|
){
|
|
@@ -628,21 +675,31 @@ class ChatEntity{
|
|
|
628
675
|
);
|
|
629
676
|
}
|
|
630
677
|
const timestamp=Date.now();
|
|
631
|
-
const
|
|
678
|
+
const requestRecords=requests.map(request=>({
|
|
679
|
+
...request,
|
|
680
|
+
timestamp,
|
|
681
|
+
...(request.role==='tool'?{memory_excluded:true}:{})
|
|
682
|
+
}));
|
|
632
683
|
const assistantRecord={
|
|
684
|
+
...copyCompleteValue(assistantMessage),
|
|
633
685
|
content:assistantContent,
|
|
634
686
|
role:'assistant',
|
|
635
687
|
timestamp,
|
|
636
688
|
...(assistantReasoning!==undefined?{reasoning_content:assistantReasoning}:{}),
|
|
637
689
|
...(toolCalls?{tool_calls:toolCalls}:{})
|
|
638
690
|
};
|
|
639
|
-
if(
|
|
640
|
-
|
|
641
|
-
if(!messagePersist)
|
|
691
|
+
if(!extractMemory||toolCalls?.length) assistantRecord.memory_excluded=true;
|
|
692
|
+
else delete assistantRecord.memory_excluded;
|
|
693
|
+
if(!messagePersist){
|
|
694
|
+
for(const requestRecord of requestRecords){
|
|
695
|
+
requestRecord.persistence_excluded=true;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
642
698
|
if(!responsePersist) assistantRecord.persistence_excluded=true;
|
|
699
|
+
else delete assistantRecord.persistence_excluded;
|
|
643
700
|
|
|
644
|
-
const appended=this.#appendMessages([
|
|
645
|
-
if(extractMemory&&messagePersist&&responsePersist&&!toolCalls&&this.persist){
|
|
701
|
+
const appended=this.#appendMessages([...requestRecords,assistantRecord],{prepared:true});
|
|
702
|
+
if(extractMemory&&messagePersist&&responsePersist&&!toolCalls?.length&&this.persist){
|
|
646
703
|
return Promise.resolve(appended).then(result=>{
|
|
647
704
|
this.#queueMemoryUpdate(memoryRequest);
|
|
648
705
|
return result;
|
|
@@ -657,7 +714,7 @@ class ChatEntity{
|
|
|
657
714
|
* If the file exists it replaces the current
|
|
658
715
|
* in-memory message list.
|
|
659
716
|
*
|
|
660
|
-
* @returns {Promise<
|
|
717
|
+
* @returns {Promise<Array<*>>}
|
|
661
718
|
*/
|
|
662
719
|
async load(){
|
|
663
720
|
|
|
@@ -673,22 +730,31 @@ class ChatEntity{
|
|
|
673
730
|
this.#messages=systemMessage?[systemMessage]:[];
|
|
674
731
|
this.#persistedMessageCount=0;
|
|
675
732
|
this.#saved=true;
|
|
676
|
-
return this.
|
|
733
|
+
return this.transcript;
|
|
677
734
|
}
|
|
678
735
|
|
|
679
736
|
const loadedMessages=Array.isArray(content)
|
|
680
|
-
?content
|
|
737
|
+
?content.map(message=>copyCompleteValue(message))
|
|
681
738
|
:String(content)
|
|
682
739
|
.split('\n')
|
|
683
|
-
.
|
|
684
|
-
.
|
|
685
|
-
|
|
686
|
-
|
|
740
|
+
.filter(row=>row.trim())
|
|
741
|
+
.map((row,index)=>{
|
|
742
|
+
try{
|
|
743
|
+
return JSON.parse(row);
|
|
744
|
+
}catch(error){
|
|
745
|
+
console.error(
|
|
746
|
+
`Arcane saved chat row ${index+1} could not be parsed.`,
|
|
747
|
+
error,
|
|
748
|
+
row
|
|
749
|
+
);
|
|
750
|
+
return row;
|
|
751
|
+
}
|
|
752
|
+
});
|
|
687
753
|
this.#messages=loadedMessages;
|
|
688
754
|
this.#persistedMessageCount=this.#durableMessages().length;
|
|
689
755
|
this.#saved=true;
|
|
690
756
|
|
|
691
|
-
return this.
|
|
757
|
+
return this.transcript;
|
|
692
758
|
}
|
|
693
759
|
|
|
694
760
|
async getMemoriesAboutUser({request=messages=>ai.fetch(messages)}={}){
|
|
@@ -769,7 +835,7 @@ ${JSON.stringify(transcript)}`
|
|
|
769
835
|
return false;
|
|
770
836
|
}
|
|
771
837
|
|
|
772
|
-
const snapshot=this.#durableMessages().map(message=>(
|
|
838
|
+
const snapshot=this.#durableMessages().map(message=>copyCompleteValue(message));
|
|
773
839
|
this.#saved=false;
|
|
774
840
|
|
|
775
841
|
return this.#queuePersistence(
|
|
@@ -778,7 +844,7 @@ ${JSON.stringify(transcript)}`
|
|
|
778
844
|
}
|
|
779
845
|
|
|
780
846
|
#queueMemoryUpdate(request){
|
|
781
|
-
const snapshot=this.#durableMessages().map(message=>(
|
|
847
|
+
const snapshot=this.#durableMessages().map(message=>copyCompleteValue(message));
|
|
782
848
|
const queued=this.#memoryQueue.then(()=>this.#writeMemory(snapshot,request));
|
|
783
849
|
this.#memoryQueue=queued.catch(()=>{
|
|
784
850
|
console.warn('Unable to update chat memory.');
|
|
@@ -882,7 +948,9 @@ ${JSON.stringify(transcript)}`
|
|
|
882
948
|
}
|
|
883
949
|
|
|
884
950
|
#durableMessages(){
|
|
885
|
-
return this.#messages.filter(
|
|
951
|
+
return this.#messages.filter(
|
|
952
|
+
message=>!message||typeof message!=='object'||message.persistence_excluded!==true
|
|
953
|
+
);
|
|
886
954
|
}
|
|
887
955
|
|
|
888
956
|
async #appendMessages(messages,{persist=true,prepared=false}={}){
|
|
@@ -948,7 +1016,7 @@ ${JSON.stringify(transcript)}`
|
|
|
948
1016
|
}else{
|
|
949
1017
|
const snapshot=durableSnapshot
|
|
950
1018
|
.slice(0,lastDurableIndex+1)
|
|
951
|
-
.map(entry=>(
|
|
1019
|
+
.map(entry=>copyCompleteValue(entry));
|
|
952
1020
|
await this.#writeSnapshot(snapshot);
|
|
953
1021
|
}
|
|
954
1022
|
this.#saved=this.#persistedMessageCount===this.#durableMessages().length;
|