arcane-os 0.3.3 → 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.
- 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 +466 -206
- package/runtime/arcane/components/speech.html +107 -30
- 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 +765 -135
- 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 +34 -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/cli/main.mjs +12 -11
- package/src/installed-sdk-runtime.mjs +11 -1
- package/src/mail-server.mjs +12 -5
- package/src/mail.mjs +25 -19
- package/src/workspace.mjs +32 -9
|
@@ -6,12 +6,20 @@ import ConfiguredAIChatSession,{
|
|
|
6
6
|
const SESSION_MANAGED_REQUEST_FIELDS=new Set([
|
|
7
7
|
'messages',
|
|
8
8
|
'onChunk',
|
|
9
|
+
'onDataChunk',
|
|
10
|
+
'onDataResult',
|
|
9
11
|
'onResponse',
|
|
10
12
|
'onToolCall',
|
|
11
13
|
'signal',
|
|
12
14
|
'stream',
|
|
13
15
|
]);
|
|
14
|
-
const PROVIDER_LIFECYCLE_FIELDS=new Set([
|
|
16
|
+
const PROVIDER_LIFECYCLE_FIELDS=new Set([
|
|
17
|
+
'onChunk',
|
|
18
|
+
'onDataChunk',
|
|
19
|
+
'onDataResult',
|
|
20
|
+
'onResponse',
|
|
21
|
+
'onToolCall'
|
|
22
|
+
]);
|
|
15
23
|
|
|
16
24
|
function coded(error,code){
|
|
17
25
|
if(!error.code) error.code=code;
|
|
@@ -19,10 +27,9 @@ function coded(error,code){
|
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
function isPlainRecord(value){
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
30
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return false;
|
|
31
|
+
const prototype=Object.getPrototypeOf(value);
|
|
32
|
+
return prototype===Object.prototype||prototype===null;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
function assertKnownKeys(value,allowed,label){
|
|
@@ -71,14 +78,20 @@ function configuredAIChat(ai){
|
|
|
71
78
|
function normalizeStreamHandlers(value){
|
|
72
79
|
if(value===undefined) return {};
|
|
73
80
|
if(!isPlainRecord(value)) throw new TypeError('Persistent chat stream handlers must be a plain object.');
|
|
74
|
-
assertKnownKeys(
|
|
75
|
-
|
|
81
|
+
assertKnownKeys(
|
|
82
|
+
value,
|
|
83
|
+
new Set(['onChunk','onDataChunk','onDataResult','onToolCall']),
|
|
84
|
+
'Persistent chat stream handlers',
|
|
85
|
+
);
|
|
86
|
+
for(const key of ['onChunk','onDataChunk','onDataResult','onToolCall']){
|
|
76
87
|
if(value[key]!==undefined&&typeof value[key]!=='function'){
|
|
77
88
|
throw new TypeError(`${key} must be a function when provided.`);
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
return {
|
|
81
92
|
onChunk:value.onChunk??function ignorePersistentChatChunk(){},
|
|
93
|
+
onDataChunk:value.onDataChunk??function ignorePersistentChatDataChunk(){},
|
|
94
|
+
onDataResult:value.onDataResult??function ignorePersistentChatDataResult(){},
|
|
82
95
|
onToolCall:value.onToolCall??function ignorePersistentChatToolCall(){},
|
|
83
96
|
};
|
|
84
97
|
}
|
|
@@ -95,7 +108,7 @@ function normalizeStreamResponse(terminal,output){
|
|
|
95
108
|
);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
function
|
|
111
|
+
function terminalStructuralToolCalls(response){
|
|
99
112
|
const hasMessage=Object.hasOwn(response,'message');
|
|
100
113
|
const hasChoices=Object.hasOwn(response,'choices');
|
|
101
114
|
if(hasMessage&&hasChoices){
|
|
@@ -120,49 +133,97 @@ function terminalStructuralToolCall(response){
|
|
|
120
133
|
);
|
|
121
134
|
}
|
|
122
135
|
for(let callIndex=0;callIndex<value.length;callIndex++){
|
|
123
|
-
|
|
136
|
+
const normalized=normalizeStructuralToolCall(
|
|
124
137
|
value[callIndex],
|
|
125
138
|
`The terminal structural tool call ${messageIndex+1}.${callIndex+1}`,
|
|
126
|
-
)
|
|
139
|
+
);
|
|
140
|
+
if(messageIndex===0) calls.push(normalized);
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
return calls;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sameDataValue(left,right,seen=new Map()){
|
|
147
|
+
if(Object.is(left,right)) return true;
|
|
148
|
+
if(!left||!right||typeof left!=='object'||typeof right!=='object') return false;
|
|
149
|
+
if(Array.isArray(left)!==Array.isArray(right)) return false;
|
|
150
|
+
const matched=seen.get(left);
|
|
151
|
+
if(matched!==undefined) return matched===right;
|
|
152
|
+
seen.set(left,right);
|
|
153
|
+
if(Array.isArray(left)){
|
|
154
|
+
return left.length===right.length
|
|
155
|
+
&&left.every((value,index)=>sameDataValue(value,right[index],seen));
|
|
134
156
|
}
|
|
135
|
-
return
|
|
157
|
+
if(!isPlainRecord(left)||!isPlainRecord(right)) return false;
|
|
158
|
+
const leftKeys=Reflect.ownKeys(left).filter(key=>Object.prototype.propertyIsEnumerable.call(left,key));
|
|
159
|
+
const rightKeys=Reflect.ownKeys(right).filter(key=>Object.prototype.propertyIsEnumerable.call(right,key));
|
|
160
|
+
return leftKeys.length===rightKeys.length
|
|
161
|
+
&&leftKeys.every(key=>Object.prototype.propertyIsEnumerable.call(right,key)
|
|
162
|
+
&&sameDataValue(left[key],right[key],seen));
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
function sameStructuralToolCall(left,right){
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
166
|
+
return sameDataValue(left,right);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sameStructuralToolCalls(left,right){
|
|
170
|
+
return Array.isArray(left)
|
|
171
|
+
&&Array.isArray(right)
|
|
172
|
+
&&left.length===right.length
|
|
173
|
+
&&left.every((call,index)=>sameStructuralToolCall(call,right[index]));
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
function normalizeSend(input){
|
|
147
177
|
if(!isPlainRecord(input)) throw new TypeError('Persistent chat input must be a plain object.');
|
|
148
|
-
assertKnownKeys(input,new Set(['message','request','response','signal']),'Persistent chat input');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if(
|
|
152
|
-
throw new TypeError('
|
|
178
|
+
assertKnownKeys(input,new Set(['message','messages','request','response','signal']),'Persistent chat input');
|
|
179
|
+
const hasMessage=Object.hasOwn(input,'message');
|
|
180
|
+
const hasMessages=Object.hasOwn(input,'messages');
|
|
181
|
+
if(hasMessage===hasMessages){
|
|
182
|
+
throw new TypeError('Persistent chat input must contain exactly one message or messages field.');
|
|
183
|
+
}
|
|
184
|
+
const sourceMessages=hasMessages?input.messages:[input.message];
|
|
185
|
+
if(!Array.isArray(sourceMessages)||!sourceMessages.length){
|
|
186
|
+
throw new TypeError('messages must be a nonempty array of tool-result messages.');
|
|
153
187
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
const normalizedMessages=sourceMessages.map((value,index)=>{
|
|
189
|
+
const label=hasMessages?`messages[${index}]`:'message';
|
|
190
|
+
if(!isPlainRecord(value)) throw new TypeError(`${label} must be a plain object.`);
|
|
191
|
+
if(typeof value.content!=='string'||!value.content.trim()){
|
|
192
|
+
throw new TypeError(`${label}.content must contain text.`);
|
|
193
|
+
}
|
|
194
|
+
const role=value.role??'user';
|
|
195
|
+
if(!['tool','user'].includes(role)) throw new TypeError(`${label}.role must be user or tool.`);
|
|
196
|
+
if(hasMessages&&role!=='tool'){
|
|
197
|
+
throw new TypeError('messages accepts only tool-result messages.');
|
|
198
|
+
}
|
|
199
|
+
let toolCallId=null;
|
|
200
|
+
if(role==='tool'){
|
|
201
|
+
if(typeof value.tool_call_id!=='string'||!value.tool_call_id.trim()){
|
|
202
|
+
throw new TypeError(`${label}.tool_call_id is required for tool messages.`);
|
|
203
|
+
}
|
|
204
|
+
toolCallId=value.tool_call_id;
|
|
205
|
+
}else if(value.tool_call_id!==undefined){
|
|
206
|
+
throw new TypeError(`${label}.tool_call_id is supported only for tool messages.`);
|
|
160
207
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
const completeMessage={...value};
|
|
209
|
+
delete completeMessage.persist;
|
|
210
|
+
return {
|
|
211
|
+
persist:boolean(value.persist,`${label}.persist`,true),
|
|
212
|
+
message:{
|
|
213
|
+
...completeMessage,
|
|
214
|
+
content:value.content,
|
|
215
|
+
role,
|
|
216
|
+
...(toolCallId?{tool_call_id:toolCallId}:{})
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
const messagePersist=normalizedMessages[0].persist;
|
|
221
|
+
if(normalizedMessages.some(item=>item.persist!==messagePersist)){
|
|
222
|
+
throw coded(
|
|
223
|
+
new TypeError('Every message in one tool-result batch must use the same persistence choice.'),
|
|
224
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
225
|
+
);
|
|
164
226
|
}
|
|
165
|
-
const messagePersist=boolean(input.message.persist,'message.persist',true);
|
|
166
227
|
const response=input.response??{};
|
|
167
228
|
if(!isPlainRecord(response)) throw new TypeError('response must be a plain object.');
|
|
168
229
|
assertKnownKeys(response,new Set(['persist']),'response');
|
|
@@ -184,11 +245,7 @@ function normalizeSend(input){
|
|
|
184
245
|
if(!signalLike(input.signal)) throw new TypeError('signal must be an AbortSignal.');
|
|
185
246
|
return {
|
|
186
247
|
messagePersist,
|
|
187
|
-
|
|
188
|
-
content:input.message.content,
|
|
189
|
-
role,
|
|
190
|
-
...(toolCallId?{tool_call_id:toolCallId}:{}),
|
|
191
|
-
},
|
|
248
|
+
requestMessages:normalizedMessages.map(item=>item.message),
|
|
192
249
|
responsePersist,
|
|
193
250
|
request:{...request},
|
|
194
251
|
signal:input.signal??null,
|
|
@@ -196,14 +253,14 @@ function normalizeSend(input){
|
|
|
196
253
|
}
|
|
197
254
|
|
|
198
255
|
function fileName(value){
|
|
199
|
-
if(typeof value!=='string'
|
|
200
|
-
throw new TypeError('chatFileName must be a
|
|
256
|
+
if(typeof value!=='string'||value.length===0){
|
|
257
|
+
throw new TypeError('chatFileName must be a nonempty string.');
|
|
201
258
|
}
|
|
202
259
|
return value;
|
|
203
260
|
}
|
|
204
261
|
|
|
205
262
|
/**
|
|
206
|
-
* Composes the
|
|
263
|
+
* Composes the configured chat session with one automatically selected
|
|
207
264
|
* ChatEntity. Request-only context is delegated to ConfiguredAIChatSession;
|
|
208
265
|
* per-turn persistence affects DBOPFS and memory, never the live model context.
|
|
209
266
|
*/
|
|
@@ -212,6 +269,7 @@ class PersistentAIChatSession{
|
|
|
212
269
|
#configured=null;
|
|
213
270
|
#entity;
|
|
214
271
|
#fetchChat;
|
|
272
|
+
#historyError=null;
|
|
215
273
|
#memory;
|
|
216
274
|
#options;
|
|
217
275
|
#pending=false;
|
|
@@ -258,20 +316,10 @@ class PersistentAIChatSession{
|
|
|
258
316
|
if(managedRequestField){
|
|
259
317
|
throw new TypeError(`request.${managedRequestField} is managed by the chat session.`);
|
|
260
318
|
}
|
|
261
|
-
|
|
262
|
-
request
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
throw coded(
|
|
266
|
-
new TypeError('Persistent chat supports one structural tool call at a time.'),
|
|
267
|
-
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if(
|
|
271
|
-
request.parallelToolCalls===undefined
|
|
272
|
-
&&request.parallel_tool_calls===undefined
|
|
273
|
-
){
|
|
274
|
-
request.parallelToolCalls=false;
|
|
319
|
+
for(const key of ['parallelToolCalls','parallel_tool_calls']){
|
|
320
|
+
if(Object.hasOwn(request,key)&&typeof request[key]!=='boolean'){
|
|
321
|
+
throw new TypeError(`request.${key} must be a boolean when provided.`);
|
|
322
|
+
}
|
|
275
323
|
}
|
|
276
324
|
const systemPrompt=options.systemPrompt??'';
|
|
277
325
|
if(typeof systemPrompt!=='string') throw new TypeError('systemPrompt must be a string.');
|
|
@@ -307,56 +355,70 @@ class PersistentAIChatSession{
|
|
|
307
355
|
const providerRequest=providerRequestWithoutLifecycleCallbacks(request);
|
|
308
356
|
if(!this.#activeStream) return this.#fetchChat(providerRequest);
|
|
309
357
|
if(typeof this.#options.ai?.streamRequest!=='function'){
|
|
310
|
-
|
|
358
|
+
const response=await this.#fetchChat(providerRequest);
|
|
359
|
+
await this.#activeStream.onDataResult(response,providerRequest.id??null);
|
|
360
|
+
return response;
|
|
311
361
|
}
|
|
312
362
|
const activeStream=this.#activeStream;
|
|
313
363
|
let terminal=null;
|
|
314
|
-
|
|
315
|
-
|
|
364
|
+
const streamedToolCalls=[];
|
|
365
|
+
const streamedToolDetails=[];
|
|
316
366
|
const output=await this.#options.ai.streamRequest({
|
|
317
367
|
...providerRequest,
|
|
318
368
|
onChunk:activeStream.onChunk,
|
|
369
|
+
onDataChunk:activeStream.onDataChunk,
|
|
370
|
+
onDataResult:activeStream.onDataResult,
|
|
319
371
|
onToolCall:function retainStructuralToolCall(call,...details){
|
|
320
372
|
const normalized=normalizeStructuralToolCall(
|
|
321
373
|
call,
|
|
322
374
|
'The streamed structural tool call',
|
|
323
375
|
);
|
|
324
|
-
if(
|
|
376
|
+
if(streamedToolCalls.some(call=>call.id===normalized.id)){
|
|
325
377
|
throw coded(
|
|
326
|
-
new TypeError('The AI stream emitted
|
|
378
|
+
new TypeError('The AI stream emitted a duplicate structural tool call.'),
|
|
327
379
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
328
380
|
);
|
|
329
381
|
}
|
|
330
|
-
|
|
331
|
-
streamedToolDetails
|
|
382
|
+
streamedToolCalls.push(normalized);
|
|
383
|
+
streamedToolDetails.push(details);
|
|
332
384
|
},
|
|
333
385
|
onResponse:async function retainPersistentChatTerminal(response){
|
|
334
386
|
terminal=response;
|
|
335
387
|
},
|
|
336
388
|
});
|
|
337
389
|
const response=normalizeStreamResponse(terminal,output);
|
|
338
|
-
const
|
|
339
|
-
if(
|
|
390
|
+
const terminalToolCalls=terminalStructuralToolCalls(response);
|
|
391
|
+
if(streamedToolCalls.length&&!sameStructuralToolCalls(streamedToolCalls,terminalToolCalls)){
|
|
340
392
|
throw coded(
|
|
341
393
|
new TypeError(
|
|
342
|
-
'The AI stream terminal response omitted or changed its structural tool
|
|
394
|
+
'The AI stream terminal response omitted, reordered, or changed its structural tool calls.'
|
|
343
395
|
),
|
|
344
396
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
345
397
|
);
|
|
346
398
|
}
|
|
347
|
-
activeStream.
|
|
399
|
+
activeStream.streamedToolCalls=streamedToolCalls;
|
|
348
400
|
activeStream.streamedToolDetails=streamedToolDetails;
|
|
349
|
-
activeStream.
|
|
401
|
+
activeStream.terminalToolCalls=terminalToolCalls;
|
|
350
402
|
return response;
|
|
351
403
|
}
|
|
352
404
|
|
|
353
405
|
async #initialize(){
|
|
354
|
-
|
|
355
|
-
|
|
406
|
+
let storedMessages=[];
|
|
407
|
+
try{
|
|
408
|
+
if(this.#options.loadExisting) await this.#entity.load();
|
|
409
|
+
storedMessages=this.#entity.messages;
|
|
410
|
+
}catch(error){
|
|
411
|
+
this.#historyError=coded(
|
|
412
|
+
error instanceof Error?error:new Error('The saved chat history is invalid.'),
|
|
413
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
414
|
+
);
|
|
415
|
+
console.error('Arcane saved chat history is readable but not actionable.',error);
|
|
416
|
+
}
|
|
356
417
|
const storedSystem=storedMessages.find(message=>message.role==='system');
|
|
357
418
|
const initialMessages=storedMessages
|
|
358
419
|
.filter(message=>['user','assistant','tool'].includes(message.role))
|
|
359
420
|
.map(message=>({
|
|
421
|
+
...message,
|
|
360
422
|
role:message.role,
|
|
361
423
|
content:String(message.content??''),
|
|
362
424
|
...(message.role==='assistant'&&Object.hasOwn(message,'reasoning_content')
|
|
@@ -396,6 +458,7 @@ class PersistentAIChatSession{
|
|
|
396
458
|
|
|
397
459
|
async history(){
|
|
398
460
|
await this.ready();
|
|
461
|
+
if(this.#historyError)throw this.#historyError;
|
|
399
462
|
return this.#configured.history();
|
|
400
463
|
}
|
|
401
464
|
|
|
@@ -418,35 +481,49 @@ class PersistentAIChatSession{
|
|
|
418
481
|
let prepared=null;
|
|
419
482
|
try{
|
|
420
483
|
await this.ready();
|
|
484
|
+
if(this.#historyError)throw this.#historyError;
|
|
421
485
|
await this.#entity.settleMemory();
|
|
422
|
-
|
|
486
|
+
const requestMessage=settings.requestMessages[0];
|
|
487
|
+
if(requestMessage.role==='user'&&this.#toolCallPersistence.size){
|
|
423
488
|
throw coded(
|
|
424
489
|
new TypeError('The pending structural tool result must be supplied before a new user turn.'),
|
|
425
490
|
'AI_CHAT_TOOL_RESULT_REQUIRED',
|
|
426
491
|
);
|
|
427
492
|
}
|
|
428
|
-
if(
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
493
|
+
if(requestMessage.role==='tool'){
|
|
494
|
+
const submittedIds=new Set();
|
|
495
|
+
for(const message of settings.requestMessages){
|
|
496
|
+
const persistence=this.#toolCallPersistence.get(message.tool_call_id);
|
|
497
|
+
if(persistence===undefined||submittedIds.has(message.tool_call_id)){
|
|
498
|
+
throw coded(new TypeError('A tool message has no unique pending structural tool call.'),'AI_CHAT_INVALID_TOOL_MESSAGE');
|
|
499
|
+
}
|
|
500
|
+
if(persistence!==settings.messagePersist){
|
|
501
|
+
throw coded(
|
|
502
|
+
new TypeError('A tool result must use the persistence of its assistant tool call.'),
|
|
503
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
submittedIds.add(message.tool_call_id);
|
|
432
507
|
}
|
|
433
|
-
if(
|
|
508
|
+
if(submittedIds.size!==this.#toolCallPersistence.size){
|
|
434
509
|
throw coded(
|
|
435
|
-
new TypeError('
|
|
436
|
-
'
|
|
510
|
+
new TypeError('Every pending structural tool result must be supplied before provider continuation.'),
|
|
511
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED',
|
|
437
512
|
);
|
|
438
513
|
}
|
|
439
514
|
}
|
|
440
515
|
const streamState=streamHandlers?{
|
|
441
516
|
...streamHandlers,
|
|
442
|
-
|
|
517
|
+
streamedToolCalls:[],
|
|
443
518
|
streamedToolDetails:[],
|
|
444
|
-
|
|
519
|
+
terminalToolCalls:[],
|
|
445
520
|
}:null;
|
|
446
521
|
if(streamState) this.#activeStream=streamState;
|
|
447
522
|
try{
|
|
448
523
|
prepared=await this.#configured.prepare(
|
|
449
|
-
settings.
|
|
524
|
+
settings.requestMessages.length===1
|
|
525
|
+
?settings.requestMessages[0]
|
|
526
|
+
:settings.requestMessages,
|
|
450
527
|
{request:settings.request,signal:settings.signal},
|
|
451
528
|
);
|
|
452
529
|
}finally{
|
|
@@ -454,22 +531,22 @@ class PersistentAIChatSession{
|
|
|
454
531
|
}
|
|
455
532
|
const result=prepared.response;
|
|
456
533
|
if(streamState){
|
|
457
|
-
const
|
|
534
|
+
const validatedToolCalls=result.message.tool_calls??[];
|
|
458
535
|
if(
|
|
459
|
-
streamState.
|
|
460
|
-
&&!
|
|
536
|
+
streamState.terminalToolCalls.length
|
|
537
|
+
&&!sameStructuralToolCalls(streamState.terminalToolCalls,validatedToolCalls)
|
|
461
538
|
){
|
|
462
539
|
throw coded(
|
|
463
540
|
new TypeError(
|
|
464
|
-
'The validated AI response changed its terminal structural tool
|
|
541
|
+
'The validated AI response changed its terminal structural tool calls.'
|
|
465
542
|
),
|
|
466
543
|
'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
|
|
467
544
|
);
|
|
468
545
|
}
|
|
469
|
-
|
|
546
|
+
for(let index=0;index<validatedToolCalls.length;index++){
|
|
470
547
|
await streamState.onToolCall(
|
|
471
|
-
|
|
472
|
-
...streamState.streamedToolDetails,
|
|
548
|
+
validatedToolCalls[index],
|
|
549
|
+
...(streamState.streamedToolDetails[index]??[]),
|
|
473
550
|
);
|
|
474
551
|
}
|
|
475
552
|
}
|
|
@@ -483,12 +560,16 @@ class PersistentAIChatSession{
|
|
|
483
560
|
})
|
|
484
561
|
),
|
|
485
562
|
messagePersist:settings.messagePersist,
|
|
486
|
-
|
|
563
|
+
...(settings.requestMessages.length===1
|
|
564
|
+
?{requestMessage:settings.requestMessages[0]}
|
|
565
|
+
:{requestMessages:settings.requestMessages}),
|
|
487
566
|
responsePersist:settings.responsePersist,
|
|
488
567
|
});
|
|
489
568
|
const committed=prepared.commit();
|
|
490
|
-
if(
|
|
491
|
-
|
|
569
|
+
if(requestMessage.role==='tool'){
|
|
570
|
+
for(const message of settings.requestMessages){
|
|
571
|
+
this.#toolCallPersistence.delete(message.tool_call_id);
|
|
572
|
+
}
|
|
492
573
|
}
|
|
493
574
|
for(const call of result.message.tool_calls??[]){
|
|
494
575
|
this.#toolCallPersistence.set(call.id,settings.responsePersist);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
2
|
import {resolveApplicationLocalStorageKey} from './AppDataScope.js';
|
|
3
3
|
|
|
4
|
-
export const RECORD_REVIEW_STORE_ERROR_CODES=
|
|
4
|
+
export const RECORD_REVIEW_STORE_ERROR_CODES={
|
|
5
5
|
adapterInvalid:'ARCANE_RECORD_REVIEW_STORE_ADAPTER_INVALID',
|
|
6
6
|
disposed:'ARCANE_RECORD_REVIEW_STORE_DISPOSED',
|
|
7
7
|
operationAborted:'ARCANE_RECORD_REVIEW_STORE_OPERATION_ABORTED',
|
|
8
8
|
operationOptionsInvalid:'ARCANE_RECORD_REVIEW_STORE_OPERATION_OPTIONS_INVALID',
|
|
9
9
|
recordIdInvalid:'ARCANE_RECORD_REVIEW_ID_INVALID',
|
|
10
10
|
storedRecordsInvalid:'ARCANE_RECORD_REVIEW_STORED_RECORDS_INVALID'
|
|
11
|
-
}
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
export const RECORD_REVIEW_STORE_EVENT_TYPES=
|
|
13
|
+
export const RECORD_REVIEW_STORE_EVENT_TYPES={
|
|
14
14
|
change:'record-review-change'
|
|
15
|
-
}
|
|
15
|
+
};
|
|
16
16
|
|
|
17
17
|
function recordReviewStoreError(code,reason,message,ErrorType=Error,cause){
|
|
18
18
|
const error=new ErrorType(message);
|
|
@@ -85,7 +85,7 @@ function normalizeOperationOptions(value={}){
|
|
|
85
85
|
TypeError
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
|
-
return
|
|
88
|
+
return {signal};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function setDataProperty(target,key,value){
|
|
@@ -97,8 +97,8 @@ function setDataProperty(target,key,value){
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function normalizeRecordId(value=''){
|
|
100
|
-
const id=String(value)
|
|
101
|
-
if(!id
|
|
100
|
+
const id=String(value);
|
|
101
|
+
if(!id.trim()||/[\x00-\x1f]/.test(id)){
|
|
102
102
|
throw recordReviewStoreError(
|
|
103
103
|
RECORD_REVIEW_STORE_ERROR_CODES.recordIdInvalid,
|
|
104
104
|
'record-review-id-invalid',
|
|
@@ -113,39 +113,42 @@ function normalizeReview(value={}){
|
|
|
113
113
|
const source=value&&typeof value==='object'?value:{};
|
|
114
114
|
const attributes={};
|
|
115
115
|
if(source.attributes&&typeof source.attributes==='object'&&!Array.isArray(source.attributes)){
|
|
116
|
-
for(const [key,value] of Object.entries(source.attributes)
|
|
117
|
-
const normalizedKey=String(key)
|
|
118
|
-
if(!normalizedKey) continue;
|
|
116
|
+
for(const [key,value] of Object.entries(source.attributes)){
|
|
117
|
+
const normalizedKey=String(key);
|
|
119
118
|
setDataProperty(
|
|
120
119
|
attributes,
|
|
121
120
|
normalizedKey,
|
|
122
121
|
Array.isArray(value)
|
|
123
|
-
?
|
|
124
|
-
return String(item)
|
|
125
|
-
})
|
|
126
|
-
:String(value??'')
|
|
122
|
+
?value.map(function normalizeReviewAttributeItem(item){
|
|
123
|
+
return String(item);
|
|
124
|
+
})
|
|
125
|
+
:String(value??'')
|
|
127
126
|
);
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
|
-
return
|
|
131
|
-
status:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
return {
|
|
130
|
+
status:source.status===undefined||source.status===null||source.status===''
|
|
131
|
+
?'not-reviewed'
|
|
132
|
+
:String(source.status),
|
|
133
|
+
classification:source.classification===undefined||source.classification===null||source.classification===''
|
|
134
|
+
?'unassigned'
|
|
135
|
+
:String(source.classification),
|
|
136
|
+
attributes,
|
|
137
|
+
notes:String(source.notes??''),
|
|
135
138
|
updatedAt:source.updatedAt?String(source.updatedAt):null
|
|
136
|
-
}
|
|
139
|
+
};
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
function
|
|
142
|
+
function recordMap(records){
|
|
140
143
|
const copy={};
|
|
141
144
|
for(const [id,review] of Object.entries(records)){
|
|
142
145
|
setDataProperty(copy,id,normalizeReview(review));
|
|
143
146
|
}
|
|
144
|
-
return
|
|
147
|
+
return copy;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
function normalizedStoredRecords(value){
|
|
148
|
-
if(!isPlainRecord(value)) return
|
|
151
|
+
if(!isPlainRecord(value)) return {};
|
|
149
152
|
const records={};
|
|
150
153
|
for(const [recordId,review] of Object.entries(value)){
|
|
151
154
|
let id;
|
|
@@ -170,7 +173,7 @@ function normalizedStoredRecords(value){
|
|
|
170
173
|
}
|
|
171
174
|
setDataProperty(records,id,normalizeReview(review));
|
|
172
175
|
}
|
|
173
|
-
return
|
|
176
|
+
return records;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
function validateAdapter(adapter){
|
|
@@ -229,15 +232,17 @@ class RecordReviewStore extends EventTarget{
|
|
|
229
232
|
|
|
230
233
|
constructor({namespace='records',adapter=null}={}){
|
|
231
234
|
super();
|
|
232
|
-
this.namespace=
|
|
235
|
+
this.namespace=namespace===undefined||namespace===null||namespace===''
|
|
236
|
+
?'records'
|
|
237
|
+
:String(namespace);
|
|
233
238
|
this.adapter=validateAdapter(adapter||nativeAdapter(this.namespace)||localAdapter(this.namespace));
|
|
234
|
-
this.records=
|
|
239
|
+
this.records={};
|
|
235
240
|
this.loaded=false;
|
|
236
241
|
this.#events=createArcaneEventSource(
|
|
237
242
|
this,
|
|
238
243
|
{
|
|
239
244
|
source:'record-review-store',
|
|
240
|
-
eventTypes:Object.
|
|
245
|
+
eventTypes:Object.values(RECORD_REVIEW_STORE_EVENT_TYPES)
|
|
241
246
|
}
|
|
242
247
|
);
|
|
243
248
|
}
|
|
@@ -254,7 +259,7 @@ class RecordReviewStore extends EventTarget{
|
|
|
254
259
|
return this.#enqueueOperation(
|
|
255
260
|
async function loadRecordReviews(){
|
|
256
261
|
const stored=await store.adapter.get(
|
|
257
|
-
|
|
262
|
+
{operationId,signal:operation.signal}
|
|
258
263
|
);
|
|
259
264
|
store.#assertOperationActive(operation.signal);
|
|
260
265
|
store.records=normalizedStoredRecords(stored);
|
|
@@ -282,18 +287,18 @@ class RecordReviewStore extends EventTarget{
|
|
|
282
287
|
...value,
|
|
283
288
|
updatedAt:new Date().toISOString()
|
|
284
289
|
});
|
|
285
|
-
const records=
|
|
290
|
+
const records=recordMap({...store.records,[id]:review});
|
|
286
291
|
await store.adapter.set(
|
|
287
|
-
records,
|
|
288
|
-
|
|
292
|
+
recordMap(records),
|
|
293
|
+
{operationId,signal:operation.signal}
|
|
289
294
|
);
|
|
290
295
|
store.#assertOperationActive(operation.signal);
|
|
291
|
-
store.records=records;
|
|
292
|
-
const detail=
|
|
296
|
+
store.records=recordMap(records);
|
|
297
|
+
const detail={
|
|
293
298
|
namespace:store.namespace,
|
|
294
299
|
recordId:id,
|
|
295
300
|
review:normalizeReview(review)
|
|
296
|
-
}
|
|
301
|
+
};
|
|
297
302
|
store.#events.dispatch(
|
|
298
303
|
RECORD_REVIEW_STORE_EVENT_TYPES.change,
|
|
299
304
|
detail,
|
|
@@ -306,7 +311,7 @@ class RecordReviewStore extends EventTarget{
|
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
snapshot(){
|
|
309
|
-
return
|
|
314
|
+
return recordMap(this.records);
|
|
310
315
|
}
|
|
311
316
|
|
|
312
317
|
dispose(){
|