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
|
@@ -236,6 +236,8 @@ function validateLLMMessageToolCalls(message, label) {
|
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
238
|
if (Object.hasOwn(message, 'toolCalls')
|
|
239
|
+
|| Object.hasOwn(message, 'tool_call')
|
|
240
|
+
|| Object.hasOwn(message, 'toolCall')
|
|
239
241
|
|| Object.hasOwn(message, 'function_call')
|
|
240
242
|
|| Object.hasOwn(message, 'functionCall')) {
|
|
241
243
|
fail(
|
|
@@ -254,17 +256,19 @@ function validateLLMMessageToolCalls(message, label) {
|
|
|
254
256
|
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
255
257
|
);
|
|
256
258
|
}
|
|
257
|
-
|
|
258
|
-
fail(
|
|
259
|
-
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
260
|
-
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
261
|
-
);
|
|
262
|
-
}
|
|
259
|
+
const ids = new Set();
|
|
263
260
|
for (let index = 0; index < descriptor.value.length; index += 1) {
|
|
264
|
-
validateLLMToolCall(
|
|
261
|
+
const id = validateLLMToolCall(
|
|
265
262
|
descriptor.value[index],
|
|
266
263
|
`${label}.tool_calls[${index}]`
|
|
267
264
|
);
|
|
265
|
+
if (ids.has(id)) {
|
|
266
|
+
fail(
|
|
267
|
+
`${label}.tool_calls contains a duplicate structural tool-call ID.`,
|
|
268
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
ids.add(id);
|
|
268
272
|
}
|
|
269
273
|
return descriptor.value;
|
|
270
274
|
}
|
|
@@ -274,7 +278,7 @@ function validateLLMRequestPayload(payload) {
|
|
|
274
278
|
if (!Array.isArray(payload.messages)) {
|
|
275
279
|
fail('AI LLM request payload.messages must be an array.');
|
|
276
280
|
}
|
|
277
|
-
|
|
281
|
+
const pendingToolCallIds = new Set();
|
|
278
282
|
for (let index = 0; index < payload.messages.length; index += 1) {
|
|
279
283
|
const message = payload.messages[index];
|
|
280
284
|
if (!isPlainRecord(message)) {
|
|
@@ -293,13 +297,21 @@ function validateLLMRequestPayload(payload) {
|
|
|
293
297
|
);
|
|
294
298
|
let openedToolCall = false;
|
|
295
299
|
if (calls.length) {
|
|
296
|
-
if (
|
|
300
|
+
if (pendingToolCallIds.size) {
|
|
297
301
|
fail(
|
|
298
|
-
'
|
|
299
|
-
'
|
|
302
|
+
'Every pending structural tool result must be supplied before another assistant tool-call sequence.',
|
|
303
|
+
'ARCANE_AI_TOOL_RESULT_REQUIRED'
|
|
300
304
|
);
|
|
301
305
|
}
|
|
302
|
-
|
|
306
|
+
for (const call of calls) {
|
|
307
|
+
if (pendingToolCallIds.has(call.id)) {
|
|
308
|
+
fail(
|
|
309
|
+
'An assistant structural tool-call sequence contains a duplicate ID.',
|
|
310
|
+
'ARCANE_AI_TOOL_CALL_INVALID'
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
pendingToolCallIds.add(call.id);
|
|
314
|
+
}
|
|
303
315
|
openedToolCall = true;
|
|
304
316
|
}
|
|
305
317
|
if (message.role === 'tool') {
|
|
@@ -310,15 +322,15 @@ function validateLLMRequestPayload(payload) {
|
|
|
310
322
|
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
311
323
|
);
|
|
312
324
|
}
|
|
313
|
-
if (
|
|
325
|
+
if (!pendingToolCallIds.size
|
|
314
326
|
|| typeof message.tool_call_id !== 'string'
|
|
315
|
-
|| message.tool_call_id
|
|
327
|
+
|| !pendingToolCallIds.has(message.tool_call_id)) {
|
|
316
328
|
fail(
|
|
317
329
|
`AI LLM request payload.messages[${index}] does not settle the pending structural tool call.`,
|
|
318
330
|
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
319
331
|
);
|
|
320
332
|
}
|
|
321
|
-
|
|
333
|
+
pendingToolCallIds.delete(message.tool_call_id);
|
|
322
334
|
} else {
|
|
323
335
|
if (Object.hasOwn(message, 'tool_call_id')) {
|
|
324
336
|
fail(
|
|
@@ -326,7 +338,7 @@ function validateLLMRequestPayload(payload) {
|
|
|
326
338
|
'ARCANE_AI_INVALID_TOOL_MESSAGE'
|
|
327
339
|
);
|
|
328
340
|
}
|
|
329
|
-
if (
|
|
341
|
+
if (pendingToolCallIds.size && !openedToolCall) {
|
|
330
342
|
fail(
|
|
331
343
|
`AI LLM request payload.messages[${index}] precedes the pending structural tool result.`,
|
|
332
344
|
'ARCANE_AI_TOOL_RESULT_REQUIRED'
|
|
@@ -334,7 +346,7 @@ function validateLLMRequestPayload(payload) {
|
|
|
334
346
|
}
|
|
335
347
|
}
|
|
336
348
|
}
|
|
337
|
-
if (
|
|
349
|
+
if (pendingToolCallIds.size) {
|
|
338
350
|
fail(
|
|
339
351
|
'The pending structural tool call must be settled before requesting another response.',
|
|
340
352
|
'ARCANE_AI_TOOL_RESULT_REQUIRED'
|
|
@@ -345,13 +357,10 @@ function validateLLMRequestPayload(payload) {
|
|
|
345
357
|
payload.parallelToolCalls,
|
|
346
358
|
payload.parallel_tool_calls
|
|
347
359
|
];
|
|
348
|
-
if (parallelValues.some(function
|
|
349
|
-
return value !== undefined && value !==
|
|
360
|
+
if (parallelValues.some(function invalidParallelLLMToolPreference(value) {
|
|
361
|
+
return value !== undefined && typeof value !== 'boolean';
|
|
350
362
|
})) {
|
|
351
|
-
fail(
|
|
352
|
-
'The Arcane AI runtime accepts one structural tool call at a time.',
|
|
353
|
-
'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED'
|
|
354
|
-
);
|
|
363
|
+
fail('AI LLM parallel tool-call preferences must be boolean when provided.');
|
|
355
364
|
}
|
|
356
365
|
}
|
|
357
366
|
|
|
@@ -404,7 +413,6 @@ function validateLLMTerminalResult(value, label) {
|
|
|
404
413
|
);
|
|
405
414
|
}
|
|
406
415
|
const indexes = new Set();
|
|
407
|
-
let totalToolCalls = 0;
|
|
408
416
|
for (let position = 0; position < choicesDescriptor.value.length; position += 1) {
|
|
409
417
|
const choice = choicesDescriptor.value[position];
|
|
410
418
|
if (!isPlainRecord(choice)
|
|
@@ -424,56 +432,601 @@ function validateLLMTerminalResult(value, label) {
|
|
|
424
432
|
'ARCANE_AI_INVALID_PROVIDER_RESULT'
|
|
425
433
|
);
|
|
426
434
|
}
|
|
427
|
-
|
|
435
|
+
validateLLMTerminalMessage(
|
|
428
436
|
messageDescriptor.value,
|
|
429
437
|
`${label}.choices[${position}].message`
|
|
430
438
|
);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
439
|
+
}
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function llmStreamToolCallMismatch(message, cause) {
|
|
444
|
+
return operationError(
|
|
445
|
+
message,
|
|
446
|
+
'ARCANE_AI_STREAM_TOOL_CALL_MISMATCH',
|
|
447
|
+
cause
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function canonicalLLMToolCall(call) {
|
|
452
|
+
return {
|
|
453
|
+
id: call.id,
|
|
454
|
+
type: call.type,
|
|
455
|
+
function: {
|
|
456
|
+
name: call.function.name,
|
|
457
|
+
arguments: call.function.arguments
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function copyLLMDataValue(value, seen = new Map()) {
|
|
463
|
+
if (!value || typeof value !== 'object') {
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
if (seen.has(value)) {
|
|
467
|
+
return seen.get(value);
|
|
468
|
+
}
|
|
469
|
+
if (!Array.isArray(value) && !isPlainRecord(value)) {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
const result = Array.isArray(value)
|
|
473
|
+
? new Array(value.length)
|
|
474
|
+
: Object.create(Object.getPrototypeOf(value));
|
|
475
|
+
seen.set(value, result);
|
|
476
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
477
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
478
|
+
if (Array.isArray(value) && key === 'length') {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const descriptor = descriptors[key];
|
|
482
|
+
if (Object.hasOwn(descriptor, 'value')) {
|
|
483
|
+
descriptor.value = copyLLMDataValue(descriptor.value, seen);
|
|
484
|
+
}
|
|
485
|
+
Object.defineProperty(result, key, descriptor);
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function sameLLMDataValue(left, right, seen = new Map()) {
|
|
491
|
+
if (Object.is(left, right)) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
if (!left || !right
|
|
495
|
+
|| typeof left !== 'object'
|
|
496
|
+
|| typeof right !== 'object'
|
|
497
|
+
|| Array.isArray(left) !== Array.isArray(right)) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
if (!Array.isArray(left)
|
|
501
|
+
&& (!isPlainRecord(left) || !isPlainRecord(right))) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const matched = seen.get(left);
|
|
505
|
+
if (matched !== undefined) {
|
|
506
|
+
return matched === right;
|
|
507
|
+
}
|
|
508
|
+
seen.set(left, right);
|
|
509
|
+
const leftDescriptors = Object.getOwnPropertyDescriptors(left);
|
|
510
|
+
const rightDescriptors = Object.getOwnPropertyDescriptors(right);
|
|
511
|
+
const leftKeys = Reflect.ownKeys(leftDescriptors);
|
|
512
|
+
const rightKeys = Reflect.ownKeys(rightDescriptors);
|
|
513
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
for (const key of leftKeys) {
|
|
517
|
+
if (!Object.hasOwn(rightDescriptors, key)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const leftDescriptor = leftDescriptors[key];
|
|
521
|
+
const rightDescriptor = rightDescriptors[key];
|
|
522
|
+
const leftIsData = Object.hasOwn(leftDescriptor, 'value');
|
|
523
|
+
const rightIsData = Object.hasOwn(rightDescriptor, 'value');
|
|
524
|
+
if (leftIsData !== rightIsData) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (leftIsData) {
|
|
528
|
+
if (!sameLLMDataValue(
|
|
529
|
+
leftDescriptor.value,
|
|
530
|
+
rightDescriptor.value,
|
|
531
|
+
seen
|
|
532
|
+
)) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
} else if (leftDescriptor.get !== rightDescriptor.get
|
|
536
|
+
|| leftDescriptor.set !== rightDescriptor.set) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function sameCanonicalLLMToolCalls(left, right) {
|
|
544
|
+
return left.length === right.length
|
|
545
|
+
&& left.every(function sameCanonicalLLMToolCall(call, index) {
|
|
546
|
+
const other = right[index];
|
|
547
|
+
return call.id === other?.id
|
|
548
|
+
&& call.type === other?.type
|
|
549
|
+
&& call.function.name === other?.function?.name
|
|
550
|
+
&& call.function.arguments === other?.function?.arguments;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function terminalLLMToolCallRecord(choiceIndex, completeCalls) {
|
|
555
|
+
return {
|
|
556
|
+
choiceIndex,
|
|
557
|
+
completeCalls,
|
|
558
|
+
canonicalCalls: completeCalls.map(canonicalLLMToolCall)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function terminalLLMToolCalls(value) {
|
|
563
|
+
if (typeof value === 'string') {
|
|
564
|
+
return {direct: terminalLLMToolCallRecord(null, []), choices: []};
|
|
565
|
+
}
|
|
566
|
+
if (Object.hasOwn(value, 'message')) {
|
|
567
|
+
const completeCalls = Array.isArray(value.message?.tool_calls)
|
|
568
|
+
? value.message.tool_calls
|
|
569
|
+
: [];
|
|
570
|
+
return {
|
|
571
|
+
direct: terminalLLMToolCallRecord(null, completeCalls),
|
|
572
|
+
choices: []
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
direct: null,
|
|
577
|
+
choices: value.choices.map(function retainTerminalLLMChoice(choice) {
|
|
578
|
+
const completeCalls = Array.isArray(choice.message?.tool_calls)
|
|
579
|
+
? choice.message.tool_calls
|
|
580
|
+
: [];
|
|
581
|
+
return terminalLLMToolCallRecord(choice.index, completeCalls);
|
|
582
|
+
})
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function createLLMStreamToolCallCorrelation() {
|
|
587
|
+
const choices = new Map();
|
|
588
|
+
const choiceOrder = [];
|
|
589
|
+
let direct = null;
|
|
590
|
+
|
|
591
|
+
function createRecord(choiceIndex = null) {
|
|
592
|
+
return {
|
|
593
|
+
choiceIndex,
|
|
594
|
+
completeCalls: null,
|
|
595
|
+
fragments: new Map(),
|
|
596
|
+
seen: false,
|
|
597
|
+
observed: false
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function directRecord() {
|
|
602
|
+
if (!direct) {
|
|
603
|
+
direct = createRecord();
|
|
604
|
+
}
|
|
605
|
+
return direct;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function choiceRecord(index, hasExplicitIndex) {
|
|
609
|
+
const key = hasExplicitIndex
|
|
610
|
+
? `index:${index}`
|
|
611
|
+
: `position:${index}`;
|
|
612
|
+
if (!choices.has(key)) {
|
|
613
|
+
const record = createRecord(hasExplicitIndex ? index : null);
|
|
614
|
+
choices.set(key, record);
|
|
615
|
+
choiceOrder.push(record);
|
|
616
|
+
}
|
|
617
|
+
return choices.get(key);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function structuralToolCalls(source, label) {
|
|
621
|
+
if (!isPlainRecord(source)) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
if (Object.hasOwn(source, 'toolCalls')
|
|
625
|
+
|| Object.hasOwn(source, 'tool_call')
|
|
626
|
+
|| Object.hasOwn(source, 'toolCall')
|
|
627
|
+
|| Object.hasOwn(source, 'function_call')
|
|
628
|
+
|| Object.hasOwn(source, 'functionCall')) {
|
|
629
|
+
throw llmStreamToolCallMismatch(
|
|
630
|
+
`${label} contains noncanonical structural tool-call data.`
|
|
435
631
|
);
|
|
436
632
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
633
|
+
if (!Object.hasOwn(source, 'tool_calls')) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, 'tool_calls');
|
|
637
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
638
|
+
|| !Array.isArray(descriptor.value)) {
|
|
639
|
+
throw llmStreamToolCallMismatch(
|
|
640
|
+
`${label}.tool_calls must be an array data property.`
|
|
442
641
|
);
|
|
443
642
|
}
|
|
643
|
+
return descriptor.value;
|
|
444
644
|
}
|
|
445
|
-
|
|
645
|
+
|
|
646
|
+
function observeCompleteMessage(message, record, label) {
|
|
647
|
+
const calls = structuralToolCalls(message, label);
|
|
648
|
+
if (!calls?.length) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
let completeCalls;
|
|
652
|
+
try {
|
|
653
|
+
completeCalls = validateLLMMessageToolCalls(message, label)
|
|
654
|
+
.map(function retainCompleteLLMStreamToolCall(call) {
|
|
655
|
+
return copyLLMDataValue(call);
|
|
656
|
+
});
|
|
657
|
+
} catch (cause) {
|
|
658
|
+
throw llmStreamToolCallMismatch(
|
|
659
|
+
`${label} did not contain complete structural tool calls.`,
|
|
660
|
+
cause
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (record.completeCalls
|
|
664
|
+
&& !sameLLMDataValue(record.completeCalls, completeCalls)) {
|
|
665
|
+
throw llmStreamToolCallMismatch(
|
|
666
|
+
`${label} changed its complete structural tool calls during streaming.`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
record.completeCalls = completeCalls;
|
|
670
|
+
record.observed = true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function observeFragments(source, record, label) {
|
|
674
|
+
const calls = structuralToolCalls(source, label);
|
|
675
|
+
if (!calls?.length) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
record.observed = true;
|
|
679
|
+
for (let position = 0; position < calls.length; position += 1) {
|
|
680
|
+
const fragment = calls[position];
|
|
681
|
+
if (!isPlainRecord(fragment)) {
|
|
682
|
+
throw llmStreamToolCallMismatch(
|
|
683
|
+
`${label}.tool_calls[${position}] must be a structural fragment object.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (fragment.index !== undefined
|
|
687
|
+
&& (!Number.isSafeInteger(fragment.index) || fragment.index < 0)) {
|
|
688
|
+
throw llmStreamToolCallMismatch(
|
|
689
|
+
`${label}.tool_calls[${position}] has an invalid structural call index.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const toolIndex = fragment.index ?? position;
|
|
693
|
+
const retained = record.fragments.get(toolIndex) ?? {
|
|
694
|
+
index: toolIndex,
|
|
695
|
+
id: '',
|
|
696
|
+
type: '',
|
|
697
|
+
name: '',
|
|
698
|
+
arguments: '',
|
|
699
|
+
invalid: false
|
|
700
|
+
};
|
|
701
|
+
if (fragment.id !== undefined) {
|
|
702
|
+
if (typeof fragment.id !== 'string'
|
|
703
|
+
|| !fragment.id
|
|
704
|
+
|| (retained.id && retained.id !== fragment.id)) {
|
|
705
|
+
retained.invalid = true;
|
|
706
|
+
} else {
|
|
707
|
+
retained.id = fragment.id;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (fragment.type !== undefined) {
|
|
711
|
+
if (typeof fragment.type !== 'string'
|
|
712
|
+
|| !fragment.type
|
|
713
|
+
|| (retained.type && retained.type !== fragment.type)) {
|
|
714
|
+
retained.invalid = true;
|
|
715
|
+
} else {
|
|
716
|
+
retained.type = fragment.type;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (fragment.function !== undefined
|
|
720
|
+
&& !isPlainRecord(fragment.function)) {
|
|
721
|
+
retained.invalid = true;
|
|
722
|
+
}
|
|
723
|
+
const functionFragment = isPlainRecord(fragment.function)
|
|
724
|
+
? fragment.function
|
|
725
|
+
: {};
|
|
726
|
+
if (functionFragment.name !== undefined) {
|
|
727
|
+
if (typeof functionFragment.name !== 'string') {
|
|
728
|
+
retained.invalid = true;
|
|
729
|
+
} else {
|
|
730
|
+
retained.name += functionFragment.name;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (functionFragment.arguments !== undefined) {
|
|
734
|
+
if (typeof functionFragment.arguments !== 'string') {
|
|
735
|
+
retained.invalid = true;
|
|
736
|
+
} else {
|
|
737
|
+
retained.arguments += functionFragment.arguments;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
record.fragments.set(toolIndex, retained);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function observeChoice(choice, position) {
|
|
745
|
+
if (!isPlainRecord(choice)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const delta = isPlainRecord(choice.delta) ? choice.delta : null;
|
|
749
|
+
const message = isPlainRecord(choice.message) ? choice.message : null;
|
|
750
|
+
const hasStructuralData = [choice, delta, message].some(
|
|
751
|
+
function hasChoiceStructuralData(source) {
|
|
752
|
+
return source && (
|
|
753
|
+
Object.hasOwn(source, 'tool_calls')
|
|
754
|
+
|| Object.hasOwn(source, 'toolCalls')
|
|
755
|
+
|| Object.hasOwn(source, 'tool_call')
|
|
756
|
+
|| Object.hasOwn(source, 'toolCall')
|
|
757
|
+
|| Object.hasOwn(source, 'function_call')
|
|
758
|
+
|| Object.hasOwn(source, 'functionCall')
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
if (choice.index !== undefined
|
|
763
|
+
&& (!Number.isSafeInteger(choice.index) || choice.index < 0)) {
|
|
764
|
+
throw llmStreamToolCallMismatch(
|
|
765
|
+
`AI provider stream choice ${position} has an invalid index.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const hasExplicitIndex = choice.index !== undefined;
|
|
769
|
+
const index = choice.index ?? position;
|
|
770
|
+
const record = choiceRecord(index, hasExplicitIndex);
|
|
771
|
+
record.seen = true;
|
|
772
|
+
if (!hasStructuralData) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
observeFragments(choice, record, `AI provider stream choice ${position}`);
|
|
776
|
+
if (delta) {
|
|
777
|
+
observeFragments(
|
|
778
|
+
delta,
|
|
779
|
+
record,
|
|
780
|
+
`AI provider stream choice ${position}.delta`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (message) {
|
|
784
|
+
observeCompleteMessage(
|
|
785
|
+
message,
|
|
786
|
+
record,
|
|
787
|
+
`AI provider stream choice ${position}.message`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function observe(chunk) {
|
|
793
|
+
if (!isPlainRecord(chunk)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const record = directRecord();
|
|
797
|
+
observeFragments(chunk, record, 'AI provider stream chunk');
|
|
798
|
+
if (isPlainRecord(chunk.delta)) {
|
|
799
|
+
observeFragments(
|
|
800
|
+
chunk.delta,
|
|
801
|
+
record,
|
|
802
|
+
'AI provider stream chunk.delta'
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (isPlainRecord(chunk.message)) {
|
|
806
|
+
observeCompleteMessage(
|
|
807
|
+
chunk.message,
|
|
808
|
+
record,
|
|
809
|
+
'AI provider stream chunk.message'
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (Array.isArray(chunk.choices)) {
|
|
813
|
+
for (let position = 0; position < chunk.choices.length; position += 1) {
|
|
814
|
+
observeChoice(chunk.choices[position], position);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function completedCalls(record, label) {
|
|
820
|
+
if (!record?.observed) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
let fragmentCalls = null;
|
|
824
|
+
if (record.fragments.size) {
|
|
825
|
+
const orderedFragments = [...record.fragments.values()]
|
|
826
|
+
.sort(function orderLLMStreamToolCalls(left, right) {
|
|
827
|
+
return left.index - right.index;
|
|
828
|
+
});
|
|
829
|
+
for (let index = 0; index < orderedFragments.length; index += 1) {
|
|
830
|
+
if (orderedFragments[index].index !== index) {
|
|
831
|
+
throw llmStreamToolCallMismatch(
|
|
832
|
+
`${label} omitted an ordered structural tool-call index.`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
fragmentCalls = orderedFragments.map(
|
|
837
|
+
function completeLLMStreamToolCall(fragment, index) {
|
|
838
|
+
if (fragment.invalid) {
|
|
839
|
+
throw llmStreamToolCallMismatch(
|
|
840
|
+
`${label} structural tool call ${index} changed an exact field.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const call = {
|
|
844
|
+
id: fragment.id,
|
|
845
|
+
type: fragment.type,
|
|
846
|
+
function: {
|
|
847
|
+
name: fragment.name,
|
|
848
|
+
arguments: fragment.arguments
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
try {
|
|
852
|
+
validateLLMToolCall(
|
|
853
|
+
call,
|
|
854
|
+
`${label} structural tool call ${index}`
|
|
855
|
+
);
|
|
856
|
+
} catch (cause) {
|
|
857
|
+
throw llmStreamToolCallMismatch(
|
|
858
|
+
`${label} did not retain a complete structural tool call.`,
|
|
859
|
+
cause
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return call;
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (record.completeCalls
|
|
867
|
+
&& fragmentCalls
|
|
868
|
+
&& !sameCanonicalLLMToolCalls(record.completeCalls, fragmentCalls)) {
|
|
869
|
+
throw llmStreamToolCallMismatch(
|
|
870
|
+
`${label} complete and fragmented structural tool calls do not match.`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
completeCalls: record.completeCalls,
|
|
875
|
+
fragmentCalls
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function assertRecordMatchesTerminal(record, terminal, label) {
|
|
880
|
+
const streamedCalls = completedCalls(record, label);
|
|
881
|
+
if (!streamedCalls) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (streamedCalls.completeCalls
|
|
885
|
+
&& !sameLLMDataValue(
|
|
886
|
+
streamedCalls.completeCalls,
|
|
887
|
+
terminal.completeCalls
|
|
888
|
+
)) {
|
|
889
|
+
throw llmStreamToolCallMismatch(
|
|
890
|
+
`${label} complete tool-call envelopes do not match its terminal result.`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (streamedCalls.fragmentCalls
|
|
894
|
+
&& !sameCanonicalLLMToolCalls(
|
|
895
|
+
streamedCalls.fragmentCalls,
|
|
896
|
+
terminal.canonicalCalls
|
|
897
|
+
)) {
|
|
898
|
+
throw llmStreamToolCallMismatch(
|
|
899
|
+
`${label} structural tool-call fragments do not match its terminal result.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function assertTerminal(value) {
|
|
905
|
+
const terminal = terminalLLMToolCalls(value);
|
|
906
|
+
const streamedChoices = choiceOrder.filter(
|
|
907
|
+
function retainSeenLLMStreamChoice(record) {
|
|
908
|
+
return record.seen;
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
const observedDirect = direct?.observed ? direct : null;
|
|
912
|
+
if (terminal.direct) {
|
|
913
|
+
if (observedDirect) {
|
|
914
|
+
assertRecordMatchesTerminal(
|
|
915
|
+
observedDirect,
|
|
916
|
+
terminal.direct,
|
|
917
|
+
'AI provider direct stream result'
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (streamedChoices.length > 1) {
|
|
921
|
+
throw llmStreamToolCallMismatch(
|
|
922
|
+
'The AI provider stream observed choices that are omitted from its direct terminal result.'
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
if (streamedChoices.length === 1) {
|
|
926
|
+
assertRecordMatchesTerminal(
|
|
927
|
+
streamedChoices[0],
|
|
928
|
+
terminal.direct,
|
|
929
|
+
'AI provider first-seen stream choice'
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const terminalByIndex = new Map(
|
|
936
|
+
terminal.choices.map(function indexTerminalLLMChoice(choice) {
|
|
937
|
+
return [choice.choiceIndex, choice];
|
|
938
|
+
})
|
|
939
|
+
);
|
|
940
|
+
const matchedTerminalIndexes = new Set();
|
|
941
|
+
for (const record of streamedChoices) {
|
|
942
|
+
if (record.choiceIndex === null) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const terminalChoice = terminalByIndex.get(record.choiceIndex);
|
|
946
|
+
if (!terminalChoice) {
|
|
947
|
+
throw llmStreamToolCallMismatch(
|
|
948
|
+
`The AI provider stream observed choice ${record.choiceIndex}, which is omitted from its terminal result.`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
assertRecordMatchesTerminal(
|
|
952
|
+
record,
|
|
953
|
+
terminalChoice,
|
|
954
|
+
`AI provider stream choice ${record.choiceIndex}`
|
|
955
|
+
);
|
|
956
|
+
matchedTerminalIndexes.add(record.choiceIndex);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (observedDirect) {
|
|
960
|
+
const firstTerminalChoice = terminal.choices[0];
|
|
961
|
+
assertRecordMatchesTerminal(
|
|
962
|
+
observedDirect,
|
|
963
|
+
firstTerminalChoice,
|
|
964
|
+
`AI provider first terminal choice ${firstTerminalChoice.choiceIndex}`
|
|
965
|
+
);
|
|
966
|
+
matchedTerminalIndexes.add(firstTerminalChoice.choiceIndex);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
for (const record of streamedChoices) {
|
|
970
|
+
if (record.choiceIndex !== null) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const terminalChoice = terminal.choices.find(
|
|
974
|
+
function findFirstUnmatchedTerminalLLMChoice(choice) {
|
|
975
|
+
return !matchedTerminalIndexes.has(choice.choiceIndex);
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
if (!terminalChoice) {
|
|
979
|
+
throw llmStreamToolCallMismatch(
|
|
980
|
+
'The AI provider stream observed a first-seen choice that is omitted from its terminal result.'
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
assertRecordMatchesTerminal(
|
|
984
|
+
record,
|
|
985
|
+
terminalChoice,
|
|
986
|
+
`AI provider first-seen stream choice for terminal choice ${terminalChoice.choiceIndex}`
|
|
987
|
+
);
|
|
988
|
+
matchedTerminalIndexes.add(terminalChoice.choiceIndex);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return {observe, assertTerminal};
|
|
446
993
|
}
|
|
447
994
|
|
|
448
|
-
function
|
|
449
|
-
return key === '
|
|
450
|
-
|| key === '
|
|
451
|
-
|| key === '
|
|
452
|
-
|| key === '
|
|
453
|
-
|| key === '
|
|
995
|
+
function isLLMStreamStructuralKey(key) {
|
|
996
|
+
return key === 'tool_calls'
|
|
997
|
+
|| key === 'toolCalls'
|
|
998
|
+
|| key === 'tool_call'
|
|
999
|
+
|| key === 'toolCall'
|
|
1000
|
+
|| key === 'function_call'
|
|
1001
|
+
|| key === 'functionCall';
|
|
454
1002
|
}
|
|
455
1003
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
1004
|
+
const OMITTED_LLM_STREAM_DATA = Symbol('omitted-llm-stream-data');
|
|
1005
|
+
|
|
1006
|
+
function projectLLMStreamData(value, seen = new Map()) {
|
|
1007
|
+
if (value === null || value === undefined) {
|
|
1008
|
+
return value;
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof value !== 'object') {
|
|
1011
|
+
return value;
|
|
1012
|
+
}
|
|
1013
|
+
if (seen.has(value)) {
|
|
1014
|
+
return seen.get(value);
|
|
459
1015
|
}
|
|
460
|
-
seen.add(value);
|
|
461
1016
|
if (Array.isArray(value)) {
|
|
462
1017
|
const result = [];
|
|
1018
|
+
seen.set(value, result);
|
|
463
1019
|
for (const item of value) {
|
|
464
|
-
const projected =
|
|
465
|
-
if (projected !==
|
|
466
|
-
result.push(projected);
|
|
467
|
-
}
|
|
1020
|
+
const projected = projectLLMStreamData(item, seen);
|
|
1021
|
+
if (projected !== OMITTED_LLM_STREAM_DATA) result.push(projected);
|
|
468
1022
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (!isPlainRecord(value)) {
|
|
473
|
-
seen.delete(value);
|
|
474
|
-
return null;
|
|
1023
|
+
return result.length || value.length === 0
|
|
1024
|
+
? result
|
|
1025
|
+
: OMITTED_LLM_STREAM_DATA;
|
|
475
1026
|
}
|
|
476
1027
|
const result = {};
|
|
1028
|
+
seen.set(value, result);
|
|
1029
|
+
let sourceDataFields = 0;
|
|
477
1030
|
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
478
1031
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
479
1032
|
if (typeof key === 'symbol') {
|
|
@@ -483,26 +1036,20 @@ function projectLLMStreamContent(value, seen = new WeakSet()) {
|
|
|
483
1036
|
if (!Object.hasOwn(descriptor, 'value')) {
|
|
484
1037
|
continue;
|
|
485
1038
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
&& descriptor.value !== undefined) {
|
|
489
|
-
result[key] = descriptor.value;
|
|
1039
|
+
sourceDataFields += 1;
|
|
1040
|
+
if (isLLMStreamStructuralKey(key)) {
|
|
490
1041
|
continue;
|
|
491
1042
|
}
|
|
492
|
-
const projected =
|
|
493
|
-
if (projected !==
|
|
494
|
-
result[key] = projected;
|
|
495
|
-
}
|
|
1043
|
+
const projected = projectLLMStreamData(descriptor.value, seen);
|
|
1044
|
+
if (projected !== OMITTED_LLM_STREAM_DATA) result[key] = projected;
|
|
496
1045
|
}
|
|
497
|
-
|
|
498
|
-
|
|
1046
|
+
return Object.keys(result).length || sourceDataFields === 0
|
|
1047
|
+
? result
|
|
1048
|
+
: OMITTED_LLM_STREAM_DATA;
|
|
499
1049
|
}
|
|
500
1050
|
|
|
501
1051
|
function projectLLMStreamChunk(value) {
|
|
502
|
-
|
|
503
|
-
return value;
|
|
504
|
-
}
|
|
505
|
-
return projectLLMStreamContent(value);
|
|
1052
|
+
return projectLLMStreamData(value);
|
|
506
1053
|
}
|
|
507
1054
|
|
|
508
1055
|
function assertCallbackFreeProviderValue(value, seen = new WeakSet()) {
|
|
@@ -715,21 +1262,20 @@ function nullableTupleIdentifier(value) {
|
|
|
715
1262
|
: null;
|
|
716
1263
|
}
|
|
717
1264
|
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
total: value.total,
|
|
729
|
-
unit: value.unit,
|
|
730
|
-
heartbeat: value.heartbeat
|
|
1265
|
+
function completeProgress(value) {
|
|
1266
|
+
if (!isPlainRecord(value)) {
|
|
1267
|
+
fail('AI provider progress must be a plain object.');
|
|
1268
|
+
}
|
|
1269
|
+
const result = {};
|
|
1270
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1271
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1272
|
+
const descriptor = descriptors[key];
|
|
1273
|
+
if (typeof key === 'symbol' || !Object.hasOwn(descriptor, 'value')) {
|
|
1274
|
+
fail('AI provider progress must contain string-keyed data properties only.');
|
|
731
1275
|
}
|
|
732
|
-
|
|
1276
|
+
result[key] = descriptor.value;
|
|
1277
|
+
}
|
|
1278
|
+
return completeValue(result);
|
|
733
1279
|
}
|
|
734
1280
|
|
|
735
1281
|
function stateError(error, fallbackCode) {
|
|
@@ -1485,15 +2031,12 @@ export class AIProviderRuntime {
|
|
|
1485
2031
|
}
|
|
1486
2032
|
}
|
|
1487
2033
|
|
|
1488
|
-
#pendingSpeechProviderHydrationMatches(role, slot
|
|
1489
|
-
if (!
|
|
2034
|
+
#pendingSpeechProviderHydrationMatches(role, slot) {
|
|
2035
|
+
if (!slot.selection) {
|
|
1490
2036
|
return false;
|
|
1491
2037
|
}
|
|
1492
2038
|
const pending = slot.selection;
|
|
1493
|
-
if (this.#providers.has(providerKey(role, pending.providerId))
|
|
1494
|
-
|| provider.id !== pending.providerId
|
|
1495
|
-
|| routes.default.providerId !== pending.providerId
|
|
1496
|
-
|| routes.default.modelId !== pending.modelId) {
|
|
2039
|
+
if (this.#providers.has(providerKey(role, pending.providerId))) {
|
|
1497
2040
|
return false;
|
|
1498
2041
|
}
|
|
1499
2042
|
const currentSelections = [
|
|
@@ -1502,10 +2045,12 @@ export class AIProviderRuntime {
|
|
|
1502
2045
|
slot.routes.localOnly
|
|
1503
2046
|
].filter(Boolean);
|
|
1504
2047
|
return currentSelections.length > 0
|
|
1505
|
-
&& currentSelections.every(
|
|
1506
|
-
selection
|
|
1507
|
-
|
|
1508
|
-
|
|
2048
|
+
&& currentSelections.every(
|
|
2049
|
+
function isSamePendingSpeechSelection(selection) {
|
|
2050
|
+
return selection.providerId === pending.providerId
|
|
2051
|
+
&& selection.modelId === pending.modelId
|
|
2052
|
+
&& selection.localOnly === null;
|
|
2053
|
+
}
|
|
1509
2054
|
);
|
|
1510
2055
|
}
|
|
1511
2056
|
|
|
@@ -1558,9 +2103,7 @@ export class AIProviderRuntime {
|
|
|
1558
2103
|
|| slot.selection)
|
|
1559
2104
|
&& !this.#pendingSpeechProviderHydrationMatches(
|
|
1560
2105
|
role,
|
|
1561
|
-
slot
|
|
1562
|
-
replacement.provider,
|
|
1563
|
-
replacement.routes
|
|
2106
|
+
slot
|
|
1564
2107
|
)) {
|
|
1565
2108
|
throw speechProviderReplacementError(
|
|
1566
2109
|
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
@@ -1703,9 +2246,7 @@ export class AIProviderRuntime {
|
|
|
1703
2246
|
|| slot.selection)
|
|
1704
2247
|
&& !this.#pendingSpeechProviderHydrationMatches(
|
|
1705
2248
|
role,
|
|
1706
|
-
slot
|
|
1707
|
-
replacement.providers[role],
|
|
1708
|
-
replacement.routes[role]
|
|
2249
|
+
slot
|
|
1709
2250
|
)) {
|
|
1710
2251
|
throw speechProviderReplacementError(
|
|
1711
2252
|
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
@@ -2774,6 +3315,14 @@ export class AIProviderRuntime {
|
|
|
2774
3315
|
let providerOpenPromise = null;
|
|
2775
3316
|
let iterator = null;
|
|
2776
3317
|
let cleanupPromise = null;
|
|
3318
|
+
const llmToolCallCorrelation = role === 'llm'
|
|
3319
|
+
? createLLMStreamToolCallCorrelation()
|
|
3320
|
+
: null;
|
|
3321
|
+
let privateStreamObservation = null;
|
|
3322
|
+
const projectedStreamChunks = [];
|
|
3323
|
+
const projectedStreamReaders = [];
|
|
3324
|
+
let projectedStreamClosed = false;
|
|
3325
|
+
let projectedStreamError = null;
|
|
2777
3326
|
let terminalSettled = false;
|
|
2778
3327
|
let resolveTerminal;
|
|
2779
3328
|
let rejectTerminal;
|
|
@@ -2784,6 +3333,61 @@ export class AIProviderRuntime {
|
|
|
2784
3333
|
terminal.catch(function retainAIProviderStreamTerminalRejection() {});
|
|
2785
3334
|
requestRecord.promise = terminal;
|
|
2786
3335
|
|
|
3336
|
+
function drainProjectedAIProviderStreamReaders() {
|
|
3337
|
+
while (projectedStreamReaders.length
|
|
3338
|
+
&& projectedStreamChunks.length) {
|
|
3339
|
+
const reader = projectedStreamReaders.shift();
|
|
3340
|
+
reader.resolve({
|
|
3341
|
+
value: projectedStreamChunks.shift(),
|
|
3342
|
+
done: false
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
if (!projectedStreamClosed || projectedStreamChunks.length) {
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
while (projectedStreamReaders.length) {
|
|
3349
|
+
const reader = projectedStreamReaders.shift();
|
|
3350
|
+
if (projectedStreamError) {
|
|
3351
|
+
reader.reject(projectedStreamError);
|
|
3352
|
+
} else {
|
|
3353
|
+
reader.resolve({value: undefined, done: true});
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
function publishProjectedAIProviderStreamChunk(value) {
|
|
3359
|
+
projectedStreamChunks.push(value);
|
|
3360
|
+
drainProjectedAIProviderStreamReaders();
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function closeProjectedAIProviderStream(error = null) {
|
|
3364
|
+
if (projectedStreamClosed) {
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
projectedStreamClosed = true;
|
|
3368
|
+
projectedStreamError = error;
|
|
3369
|
+
drainProjectedAIProviderStreamReaders();
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function readProjectedAIProviderStreamChunk() {
|
|
3373
|
+
if (projectedStreamChunks.length) {
|
|
3374
|
+
return Promise.resolve({
|
|
3375
|
+
value: projectedStreamChunks.shift(),
|
|
3376
|
+
done: false
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
if (projectedStreamClosed) {
|
|
3380
|
+
return projectedStreamError
|
|
3381
|
+
? Promise.reject(projectedStreamError)
|
|
3382
|
+
: Promise.resolve({value: undefined, done: true});
|
|
3383
|
+
}
|
|
3384
|
+
return new Promise(
|
|
3385
|
+
function awaitProjectedAIProviderStreamChunk(resolve, reject) {
|
|
3386
|
+
projectedStreamReaders.push({resolve, reject});
|
|
3387
|
+
}
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
2787
3391
|
function settleAIProviderStream(error, value) {
|
|
2788
3392
|
if (terminalSettled) {
|
|
2789
3393
|
return;
|
|
@@ -2799,9 +3403,11 @@ export class AIProviderRuntime {
|
|
|
2799
3403
|
}
|
|
2800
3404
|
runtime.#drainRoleRequestQueue(slot);
|
|
2801
3405
|
if (error) {
|
|
3406
|
+
closeProjectedAIProviderStream(error);
|
|
2802
3407
|
restoreAIProviderRoleAfterRequest(error);
|
|
2803
3408
|
rejectTerminal(error);
|
|
2804
3409
|
} else {
|
|
3410
|
+
closeProjectedAIProviderStream();
|
|
2805
3411
|
restoreAIProviderRoleAfterRequest(null);
|
|
2806
3412
|
resolveTerminal(value);
|
|
2807
3413
|
}
|
|
@@ -2875,6 +3481,15 @@ export class AIProviderRuntime {
|
|
|
2875
3481
|
}
|
|
2876
3482
|
|
|
2877
3483
|
async function cancelAIProviderStream(reason, terminalError = null) {
|
|
3484
|
+
const terminalOutcome = terminalError ?? normalizedAbort(
|
|
3485
|
+
reason instanceof Error
|
|
3486
|
+
? reason
|
|
3487
|
+
: operationError(
|
|
3488
|
+
'The AI stream was cancelled.',
|
|
3489
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
3490
|
+
)
|
|
3491
|
+
);
|
|
3492
|
+
closeProjectedAIProviderStream(terminalOutcome);
|
|
2878
3493
|
if (cleanupPromise) {
|
|
2879
3494
|
return cleanupPromise;
|
|
2880
3495
|
}
|
|
@@ -2888,14 +3503,6 @@ export class AIProviderRuntime {
|
|
|
2888
3503
|
);
|
|
2889
3504
|
controller.abort(reason);
|
|
2890
3505
|
(async function closeAIProviderStream() {
|
|
2891
|
-
const terminalOutcome = terminalError ?? normalizedAbort(
|
|
2892
|
-
reason instanceof Error
|
|
2893
|
-
? reason
|
|
2894
|
-
: operationError(
|
|
2895
|
-
'The AI stream was cancelled.',
|
|
2896
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
2897
|
-
)
|
|
2898
|
-
);
|
|
2899
3506
|
try {
|
|
2900
3507
|
const cleanupOutcome = await cleanupOwnedAIProviderStream(reason);
|
|
2901
3508
|
assertStreamCleanupComplete(cleanupOutcome);
|
|
@@ -2987,9 +3594,51 @@ export class AIProviderRuntime {
|
|
|
2987
3594
|
requestSequence,
|
|
2988
3595
|
controller.signal
|
|
2989
3596
|
);
|
|
3597
|
+
privateStreamObservation = (
|
|
3598
|
+
async function observePrivateAIProviderStream() {
|
|
3599
|
+
try {
|
|
3600
|
+
while (true) {
|
|
3601
|
+
const result = await iterator.next();
|
|
3602
|
+
runtime.#assertCurrentRequest(
|
|
3603
|
+
slot,
|
|
3604
|
+
generation,
|
|
3605
|
+
requestSequence,
|
|
3606
|
+
controller.signal
|
|
3607
|
+
);
|
|
3608
|
+
if (result.done) {
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
if (role !== 'llm') {
|
|
3612
|
+
publishProjectedAIProviderStreamChunk(result.value);
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
llmToolCallCorrelation.observe(result.value);
|
|
3616
|
+
const projected = projectLLMStreamChunk(result.value);
|
|
3617
|
+
if (projected !== OMITTED_LLM_STREAM_DATA) {
|
|
3618
|
+
publishProjectedAIProviderStreamChunk(projected);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
} catch (error) {
|
|
3622
|
+
const normalized = isAbort(error, controller.signal)
|
|
3623
|
+
? normalizedAbort(error)
|
|
3624
|
+
: error;
|
|
3625
|
+
closeProjectedAIProviderStream(normalized);
|
|
3626
|
+
try {
|
|
3627
|
+
await cancelAIProviderStream(normalized, normalized);
|
|
3628
|
+
} catch {
|
|
3629
|
+
// Cancellation owns any cleanup failure.
|
|
3630
|
+
}
|
|
3631
|
+
throw normalized;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
)();
|
|
3635
|
+
privateStreamObservation.catch(
|
|
3636
|
+
function retainPrivateAIProviderStreamRejection() {}
|
|
3637
|
+
);
|
|
2990
3638
|
Promise.resolve(opened.result).then(
|
|
2991
|
-
function acceptAIProviderStreamResult(value) {
|
|
3639
|
+
async function acceptAIProviderStreamResult(value) {
|
|
2992
3640
|
try {
|
|
3641
|
+
await privateStreamObservation;
|
|
2993
3642
|
runtime.#assertCurrentRequest(
|
|
2994
3643
|
slot,
|
|
2995
3644
|
generation,
|
|
@@ -3002,8 +3651,17 @@ export class AIProviderRuntime {
|
|
|
3002
3651
|
'AI provider stream result'
|
|
3003
3652
|
)
|
|
3004
3653
|
: value;
|
|
3654
|
+
llmToolCallCorrelation?.assertTerminal(terminalValue);
|
|
3005
3655
|
settleAIProviderStream(null, terminalValue);
|
|
3006
3656
|
} catch (error) {
|
|
3657
|
+
if (cleanupPromise) {
|
|
3658
|
+
try {
|
|
3659
|
+
await cleanupPromise;
|
|
3660
|
+
} catch {
|
|
3661
|
+
// Stream cancellation owns terminal cleanup failure.
|
|
3662
|
+
}
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3007
3665
|
const normalized = isAbort(error, controller.signal)
|
|
3008
3666
|
? normalizedAbort(error)
|
|
3009
3667
|
: error;
|
|
@@ -3025,49 +3683,15 @@ export class AIProviderRuntime {
|
|
|
3025
3683
|
const handle = {
|
|
3026
3684
|
result: terminal,
|
|
3027
3685
|
cancel: cancelAIProviderStream,
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
let nextValue = value;
|
|
3031
|
-
while (true) {
|
|
3032
|
-
const result = await iterator.next(nextValue);
|
|
3033
|
-
nextValue = undefined;
|
|
3034
|
-
runtime.#assertCurrentRequest(
|
|
3035
|
-
slot,
|
|
3036
|
-
generation,
|
|
3037
|
-
requestSequence,
|
|
3038
|
-
controller.signal
|
|
3039
|
-
);
|
|
3040
|
-
if (role !== 'llm') {
|
|
3041
|
-
return result;
|
|
3042
|
-
}
|
|
3043
|
-
if (result.done) {
|
|
3044
|
-
return {value: undefined, done: true};
|
|
3045
|
-
}
|
|
3046
|
-
const projected = projectLLMStreamChunk(result.value);
|
|
3047
|
-
if (projected !== null) {
|
|
3048
|
-
return {
|
|
3049
|
-
value: projected,
|
|
3050
|
-
done: false
|
|
3051
|
-
};
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
} catch (error) {
|
|
3055
|
-
await cancelAIProviderStream(error);
|
|
3056
|
-
throw isAbort(error, controller.signal)
|
|
3057
|
-
? normalizedAbort(error)
|
|
3058
|
-
: error;
|
|
3059
|
-
}
|
|
3686
|
+
next() {
|
|
3687
|
+
return readProjectedAIProviderStreamChunk();
|
|
3060
3688
|
},
|
|
3061
3689
|
async return(value) {
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
3068
|
-
)
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3690
|
+
cancelAIProviderStream(
|
|
3691
|
+
operationError(
|
|
3692
|
+
'The AI stream consumer stopped before completion.',
|
|
3693
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
3694
|
+
)
|
|
3071
3695
|
).catch(
|
|
3072
3696
|
function reportReturnedAIProviderStreamCancellationFailure(error) {
|
|
3073
3697
|
console.error(
|
|
@@ -3079,7 +3703,7 @@ export class AIProviderRuntime {
|
|
|
3079
3703
|
return {value, done: true};
|
|
3080
3704
|
},
|
|
3081
3705
|
async throw(error) {
|
|
3082
|
-
await cancelAIProviderStream(error);
|
|
3706
|
+
await cancelAIProviderStream(error, error);
|
|
3083
3707
|
throw error;
|
|
3084
3708
|
},
|
|
3085
3709
|
[Symbol.asyncIterator]() {
|
|
@@ -3091,6 +3715,7 @@ export class AIProviderRuntime {
|
|
|
3091
3715
|
const normalized = isAbort(error, controller.signal)
|
|
3092
3716
|
? normalizedAbort(error)
|
|
3093
3717
|
: error;
|
|
3718
|
+
closeProjectedAIProviderStream(normalized);
|
|
3094
3719
|
if (cleanupPromise) {
|
|
3095
3720
|
try {
|
|
3096
3721
|
await cleanupPromise;
|
|
@@ -3500,7 +4125,7 @@ export class AIProviderRuntime {
|
|
|
3500
4125
|
{
|
|
3501
4126
|
state: 'loading',
|
|
3502
4127
|
operationId,
|
|
3503
|
-
progress:
|
|
4128
|
+
progress: completeProgress(progress)
|
|
3504
4129
|
}
|
|
3505
4130
|
)
|
|
3506
4131
|
);
|