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.
- package/CHANGELOG.md +12 -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 +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 +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/installed-sdk-runtime.mjs +11 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const FORBIDDEN_REQUEST_FIELDS=new Set([
|
|
2
2
|
'messages',
|
|
3
3
|
'onChunk',
|
|
4
|
+
'onDataChunk',
|
|
5
|
+
'onDataResult',
|
|
4
6
|
'onResponse',
|
|
5
7
|
'onToolCall',
|
|
6
8
|
'signal',
|
|
@@ -8,10 +10,9 @@ const FORBIDDEN_REQUEST_FIELDS=new Set([
|
|
|
8
10
|
]);
|
|
9
11
|
|
|
10
12
|
function isPlainRecord(value){
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
13
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return false;
|
|
14
|
+
const prototype=Object.getPrototypeOf(value);
|
|
15
|
+
return prototype===Object.prototype||prototype===null;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function coded(error,code){
|
|
@@ -137,11 +138,10 @@ function normalizeRequestOptions(value,label){
|
|
|
137
138
|
const forbidden=Object.keys(value).find(key=>FORBIDDEN_REQUEST_FIELDS.has(key));
|
|
138
139
|
if(forbidden) throw new TypeError(`${label}.${forbidden} is managed by the chat session.`);
|
|
139
140
|
requireToolMessageSchemas(value.tools,`${label}.tools`);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
new TypeError(`${label}
|
|
143
|
-
|
|
144
|
-
);
|
|
141
|
+
for(const key of ['parallelToolCalls','parallel_tool_calls']){
|
|
142
|
+
if(Object.hasOwn(value,key)&&typeof value[key]!=='boolean'){
|
|
143
|
+
throw new TypeError(`${label}.${key} must be a boolean when provided.`);
|
|
144
|
+
}
|
|
145
145
|
}
|
|
146
146
|
return {...value};
|
|
147
147
|
}
|
|
@@ -149,13 +149,11 @@ function normalizeRequestOptions(value,label){
|
|
|
149
149
|
export function normalizeStructuralToolCall(call,label='Structural tool call'){
|
|
150
150
|
try{
|
|
151
151
|
if(!isPlainRecord(call)) throw new TypeError(`${label} must be a plain object.`);
|
|
152
|
-
assertMessageKeys(call,new Set(['function','id','type']),label);
|
|
153
152
|
if(typeof call.id!=='string'||!call.id.trim()){
|
|
154
153
|
throw new TypeError(`${label}.id must contain text.`);
|
|
155
154
|
}
|
|
156
155
|
if(call.type!=='function') throw new TypeError(`${label}.type must be function.`);
|
|
157
156
|
if(!isPlainRecord(call.function)) throw new TypeError(`${label}.function must be a plain object.`);
|
|
158
|
-
assertMessageKeys(call.function,new Set(['arguments','name']),`${label}.function`);
|
|
159
157
|
if(typeof call.function.name!=='string'||!call.function.name.trim()){
|
|
160
158
|
throw new TypeError(`${label}.function.name must contain text.`);
|
|
161
159
|
}
|
|
@@ -164,7 +162,9 @@ export function normalizeStructuralToolCall(call,label='Structural tool call'){
|
|
|
164
162
|
throw coded(error,'AI_CHAT_INVALID_TOOL_CALL');
|
|
165
163
|
}
|
|
166
164
|
return {
|
|
165
|
+
...call,
|
|
167
166
|
function:{
|
|
167
|
+
...call.function,
|
|
168
168
|
arguments:call.function.arguments,
|
|
169
169
|
name:call.function.name,
|
|
170
170
|
},
|
|
@@ -178,12 +178,6 @@ function normalizeToolCalls(value,label){
|
|
|
178
178
|
if(!Array.isArray(value)){
|
|
179
179
|
throw new TypeError(`${label} must be an array of structural tool calls.`);
|
|
180
180
|
}
|
|
181
|
-
if(value.length>1){
|
|
182
|
-
throw coded(
|
|
183
|
-
new TypeError(`${label} must contain at most one structural tool call.`),
|
|
184
|
-
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
181
|
const ids=new Set();
|
|
188
182
|
return value.map((call,index)=>{
|
|
189
183
|
const normalized=normalizeStructuralToolCall(call,`${label}[${index}]`);
|
|
@@ -202,13 +196,6 @@ function normalizeMessage(value,label,allowedRoles){
|
|
|
202
196
|
if(!isPlainRecord(value)||!allowedRoles.has(value.role)){
|
|
203
197
|
throw new TypeError(`${label} has an unsupported role.`);
|
|
204
198
|
}
|
|
205
|
-
const allowed=new Set(['content','role']);
|
|
206
|
-
if(value.role==='assistant'){
|
|
207
|
-
allowed.add('reasoning_content');
|
|
208
|
-
allowed.add('tool_calls');
|
|
209
|
-
}
|
|
210
|
-
if(value.role==='tool') allowed.add('tool_call_id');
|
|
211
|
-
assertMessageKeys(value,allowed,label);
|
|
212
199
|
const toolCalls=value.role==='assistant'
|
|
213
200
|
?normalizeToolCalls(value.tool_calls,`${label}.tool_calls`)
|
|
214
201
|
:null;
|
|
@@ -225,7 +212,7 @@ function normalizeMessage(value,label,allowedRoles){
|
|
|
225
212
|
&&value.reasoning_content.length
|
|
226
213
|
);
|
|
227
214
|
let content;
|
|
228
|
-
if(value.role==='assistant'&&(toolCalls||hasReasoning)){
|
|
215
|
+
if(value.role==='assistant'&&(toolCalls?.length||hasReasoning)){
|
|
229
216
|
if(value.content===undefined||value.content===null||value.content==='') content='';
|
|
230
217
|
else content=contentText(value.content,`${label}.content`);
|
|
231
218
|
}else{
|
|
@@ -236,6 +223,7 @@ function normalizeMessage(value,label,allowedRoles){
|
|
|
236
223
|
toolCallId=contentText(value.tool_call_id,`${label}.tool_call_id`);
|
|
237
224
|
}
|
|
238
225
|
const normalized={
|
|
226
|
+
...value,
|
|
239
227
|
role:value.role,
|
|
240
228
|
content,
|
|
241
229
|
...(value.role==='assistant'&&Object.hasOwn(value,'reasoning_content')
|
|
@@ -249,6 +237,7 @@ function normalizeMessage(value,label,allowedRoles){
|
|
|
249
237
|
|
|
250
238
|
function cloneMessage(value){
|
|
251
239
|
return {
|
|
240
|
+
...value,
|
|
252
241
|
role:value.role,
|
|
253
242
|
content:value.content,
|
|
254
243
|
...(Object.hasOwn(value,'reasoning_content')
|
|
@@ -256,9 +245,8 @@ function cloneMessage(value){
|
|
|
256
245
|
:{}),
|
|
257
246
|
...(value.tool_calls?{
|
|
258
247
|
tool_calls:value.tool_calls.map(call=>({
|
|
248
|
+
...call,
|
|
259
249
|
function:{...call.function},
|
|
260
|
-
id:call.id,
|
|
261
|
-
type:call.type,
|
|
262
250
|
}))
|
|
263
251
|
}:{}),
|
|
264
252
|
...(value.tool_call_id?{tool_call_id:value.tool_call_id}:{}),
|
|
@@ -267,15 +255,15 @@ function cloneMessage(value){
|
|
|
267
255
|
|
|
268
256
|
function publicMessage(value){
|
|
269
257
|
return {
|
|
258
|
+
...value,
|
|
270
259
|
role:value.role,
|
|
271
260
|
content:value.content,
|
|
272
261
|
...(Object.hasOwn(value,'reasoning_content')
|
|
273
262
|
?{reasoning_content:value.reasoning_content}
|
|
274
263
|
:{}),
|
|
275
264
|
...(value.tool_calls?{tool_calls:value.tool_calls.map(call=>({
|
|
265
|
+
...call,
|
|
276
266
|
function:{...call.function},
|
|
277
|
-
id:call.id,
|
|
278
|
-
type:call.type,
|
|
279
267
|
}))}:{}),
|
|
280
268
|
...(value.tool_call_id?{tool_call_id:value.tool_call_id}:{}),
|
|
281
269
|
};
|
|
@@ -293,6 +281,31 @@ function normalizeInitialMessages(value){
|
|
|
293
281
|
return messages;
|
|
294
282
|
}
|
|
295
283
|
|
|
284
|
+
function normalizeInputMessages(value){
|
|
285
|
+
if(typeof value==='string'){
|
|
286
|
+
return [normalizeMessage(
|
|
287
|
+
{role:'user',content:value},
|
|
288
|
+
'The user message',
|
|
289
|
+
new Set(['user']),
|
|
290
|
+
)];
|
|
291
|
+
}
|
|
292
|
+
if(Array.isArray(value)){
|
|
293
|
+
if(!value.length){
|
|
294
|
+
throw new TypeError('The request tool-result batch must not be empty.');
|
|
295
|
+
}
|
|
296
|
+
return value.map((item,index)=>normalizeMessage(
|
|
297
|
+
item,
|
|
298
|
+
`The request tool-result batch[${index}]`,
|
|
299
|
+
new Set(['tool']),
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
return [normalizeMessage(
|
|
303
|
+
value,
|
|
304
|
+
'The request message',
|
|
305
|
+
new Set(['tool','user']),
|
|
306
|
+
)];
|
|
307
|
+
}
|
|
308
|
+
|
|
296
309
|
function message(role,content){
|
|
297
310
|
return {role,content};
|
|
298
311
|
}
|
|
@@ -334,10 +347,18 @@ function pendingToolCallIds(messages,validate=false){
|
|
|
334
347
|
);
|
|
335
348
|
}
|
|
336
349
|
if(item.role==='assistant'&&item.tool_calls){
|
|
337
|
-
|
|
350
|
+
for(const call of item.tool_calls){
|
|
351
|
+
if(pending.has(call.id)&&validate){
|
|
352
|
+
throw coded(
|
|
353
|
+
new TypeError(`Message ${index+1} repeats a pending assistant tool-call ID.`),
|
|
354
|
+
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
pending.add(call.id);
|
|
358
|
+
}
|
|
338
359
|
}
|
|
339
360
|
if(item.role==='tool'){
|
|
340
|
-
if(
|
|
361
|
+
if(!pending.has(item.tool_call_id)&&validate){
|
|
341
362
|
throw coded(
|
|
342
363
|
new TypeError(`Message ${index+1} does not match the pending assistant tool call.`),
|
|
343
364
|
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
@@ -410,7 +431,6 @@ function normalizeOpenAICompatibleResponse(response){
|
|
|
410
431
|
);
|
|
411
432
|
}
|
|
412
433
|
let responseMessage=null;
|
|
413
|
-
let toolCallCount=0;
|
|
414
434
|
for(let index=0;index<response.choices.length;index++){
|
|
415
435
|
const candidate=response.choices[index];
|
|
416
436
|
if(!isPlainRecord(candidate)){
|
|
@@ -423,21 +443,6 @@ function normalizeOpenAICompatibleResponse(response){
|
|
|
423
443
|
candidate.message,
|
|
424
444
|
{requireRole:true},
|
|
425
445
|
);
|
|
426
|
-
toolCallCount+=candidateMessage.tool_calls?.length??0;
|
|
427
|
-
if(toolCallCount>1){
|
|
428
|
-
throw coded(
|
|
429
|
-
new TypeError('The chat provider completion contains parallel structural tool calls.'),
|
|
430
|
-
'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
if(index>0&&candidateMessage.tool_calls?.length){
|
|
434
|
-
throw coded(
|
|
435
|
-
new TypeError(
|
|
436
|
-
'The chat provider completion placed a structural tool call outside the selected choice.'
|
|
437
|
-
),
|
|
438
|
-
'AI_CHAT_INVALID_RESPONSE',
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
446
|
if(index===0) responseMessage=candidateMessage;
|
|
442
447
|
}
|
|
443
448
|
const choice=response.choices[0];
|
|
@@ -586,17 +591,8 @@ export default class ConfiguredAIChatSession{
|
|
|
586
591
|
if(!signalLike(options.signal)) throw new TypeError('signal must be an AbortSignal.');
|
|
587
592
|
if(options.signal?.aborted) throw abortError();
|
|
588
593
|
const turnRequest=normalizeRequestOptions(options.request,'request');
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
{role:'user',content:input},
|
|
592
|
-
'The user message',
|
|
593
|
-
new Set(['user']),
|
|
594
|
-
)
|
|
595
|
-
:normalizeMessage(
|
|
596
|
-
input,
|
|
597
|
-
'The request message',
|
|
598
|
-
new Set(['tool','user']),
|
|
599
|
-
);
|
|
594
|
+
const inputMessages=normalizeInputMessages(input);
|
|
595
|
+
const inputMessage=inputMessages[0];
|
|
600
596
|
if(this.#pending){
|
|
601
597
|
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
602
598
|
}
|
|
@@ -609,18 +605,27 @@ export default class ConfiguredAIChatSession{
|
|
|
609
605
|
'AI_CHAT_TOOL_RESULT_REQUIRED',
|
|
610
606
|
);
|
|
611
607
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
608
|
+
if(inputMessage.role==='tool'){
|
|
609
|
+
const submittedIds=new Set();
|
|
610
|
+
for(const item of inputMessages){
|
|
611
|
+
if(
|
|
612
|
+
submittedIds.has(item.tool_call_id)
|
|
613
|
+
||!pendingTools.has(item.tool_call_id)
|
|
614
|
+
||matchingToolCallIndex(this.#conversation,item.tool_call_id)<0
|
|
615
|
+
){
|
|
616
|
+
throw coded(
|
|
617
|
+
new TypeError('A tool message does not match a pending assistant tool call in this session.'),
|
|
618
|
+
'AI_CHAT_INVALID_TOOL_MESSAGE',
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
submittedIds.add(item.tool_call_id);
|
|
622
|
+
}
|
|
623
|
+
if(submittedIds.size!==pendingTools.size){
|
|
624
|
+
throw coded(
|
|
625
|
+
new TypeError('Every pending structural tool result must be supplied before provider continuation.'),
|
|
626
|
+
'AI_CHAT_TOOL_RESULT_REQUIRED',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
624
629
|
}
|
|
625
630
|
const context=inputMessage.role==='user'
|
|
626
631
|
?await this.#contextFor(inputMessage.content,options.signal)
|
|
@@ -629,7 +634,7 @@ export default class ConfiguredAIChatSession{
|
|
|
629
634
|
const transientContext=context
|
|
630
635
|
?message('user',context)
|
|
631
636
|
:null;
|
|
632
|
-
const transientMessages=[...(transientContext?[transientContext]:[])
|
|
637
|
+
const transientMessages=[...(transientContext?[transientContext]:[]),...inputMessages];
|
|
633
638
|
const requestMessages=completeHistory(
|
|
634
639
|
this.#systemPrompt,
|
|
635
640
|
[...this.#conversation,...transientMessages],
|
|
@@ -642,11 +647,6 @@ export default class ConfiguredAIChatSession{
|
|
|
642
647
|
...(options.signal?{signal:options.signal}:{}),
|
|
643
648
|
messages:requestMessages.map(publicMessage),
|
|
644
649
|
};
|
|
645
|
-
if(
|
|
646
|
-
providerRequest.tools?.length
|
|
647
|
-
&&providerRequest.parallelToolCalls===undefined
|
|
648
|
-
&&providerRequest.parallel_tool_calls===undefined
|
|
649
|
-
) providerRequest.parallelToolCalls=false;
|
|
650
650
|
providerResponse=await this.#chat(providerRequest);
|
|
651
651
|
}catch(error){
|
|
652
652
|
if(options.signal?.aborted) throw abortError();
|
|
@@ -657,7 +657,7 @@ export default class ConfiguredAIChatSession{
|
|
|
657
657
|
const retainedConversation=this.#conversation.map(cloneMessage);
|
|
658
658
|
const committed=completeHistory(
|
|
659
659
|
this.#systemPrompt,
|
|
660
|
-
[...retainedConversation,
|
|
660
|
+
[...retainedConversation,...inputMessages,response.message],
|
|
661
661
|
);
|
|
662
662
|
const nextConversation=committed.filter(item=>item.role!=='system');
|
|
663
663
|
let settled=false;
|