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
|
@@ -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) {
|
|
@@ -2774,6 +3320,14 @@ export class AIProviderRuntime {
|
|
|
2774
3320
|
let providerOpenPromise = null;
|
|
2775
3321
|
let iterator = null;
|
|
2776
3322
|
let cleanupPromise = null;
|
|
3323
|
+
const llmToolCallCorrelation = role === 'llm'
|
|
3324
|
+
? createLLMStreamToolCallCorrelation()
|
|
3325
|
+
: null;
|
|
3326
|
+
let privateStreamObservation = null;
|
|
3327
|
+
const projectedStreamChunks = [];
|
|
3328
|
+
const projectedStreamReaders = [];
|
|
3329
|
+
let projectedStreamClosed = false;
|
|
3330
|
+
let projectedStreamError = null;
|
|
2777
3331
|
let terminalSettled = false;
|
|
2778
3332
|
let resolveTerminal;
|
|
2779
3333
|
let rejectTerminal;
|
|
@@ -2784,6 +3338,61 @@ export class AIProviderRuntime {
|
|
|
2784
3338
|
terminal.catch(function retainAIProviderStreamTerminalRejection() {});
|
|
2785
3339
|
requestRecord.promise = terminal;
|
|
2786
3340
|
|
|
3341
|
+
function drainProjectedAIProviderStreamReaders() {
|
|
3342
|
+
while (projectedStreamReaders.length
|
|
3343
|
+
&& projectedStreamChunks.length) {
|
|
3344
|
+
const reader = projectedStreamReaders.shift();
|
|
3345
|
+
reader.resolve({
|
|
3346
|
+
value: projectedStreamChunks.shift(),
|
|
3347
|
+
done: false
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
if (!projectedStreamClosed || projectedStreamChunks.length) {
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
while (projectedStreamReaders.length) {
|
|
3354
|
+
const reader = projectedStreamReaders.shift();
|
|
3355
|
+
if (projectedStreamError) {
|
|
3356
|
+
reader.reject(projectedStreamError);
|
|
3357
|
+
} else {
|
|
3358
|
+
reader.resolve({value: undefined, done: true});
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function publishProjectedAIProviderStreamChunk(value) {
|
|
3364
|
+
projectedStreamChunks.push(value);
|
|
3365
|
+
drainProjectedAIProviderStreamReaders();
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function closeProjectedAIProviderStream(error = null) {
|
|
3369
|
+
if (projectedStreamClosed) {
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
projectedStreamClosed = true;
|
|
3373
|
+
projectedStreamError = error;
|
|
3374
|
+
drainProjectedAIProviderStreamReaders();
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
function readProjectedAIProviderStreamChunk() {
|
|
3378
|
+
if (projectedStreamChunks.length) {
|
|
3379
|
+
return Promise.resolve({
|
|
3380
|
+
value: projectedStreamChunks.shift(),
|
|
3381
|
+
done: false
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
if (projectedStreamClosed) {
|
|
3385
|
+
return projectedStreamError
|
|
3386
|
+
? Promise.reject(projectedStreamError)
|
|
3387
|
+
: Promise.resolve({value: undefined, done: true});
|
|
3388
|
+
}
|
|
3389
|
+
return new Promise(
|
|
3390
|
+
function awaitProjectedAIProviderStreamChunk(resolve, reject) {
|
|
3391
|
+
projectedStreamReaders.push({resolve, reject});
|
|
3392
|
+
}
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
2787
3396
|
function settleAIProviderStream(error, value) {
|
|
2788
3397
|
if (terminalSettled) {
|
|
2789
3398
|
return;
|
|
@@ -2799,9 +3408,11 @@ export class AIProviderRuntime {
|
|
|
2799
3408
|
}
|
|
2800
3409
|
runtime.#drainRoleRequestQueue(slot);
|
|
2801
3410
|
if (error) {
|
|
3411
|
+
closeProjectedAIProviderStream(error);
|
|
2802
3412
|
restoreAIProviderRoleAfterRequest(error);
|
|
2803
3413
|
rejectTerminal(error);
|
|
2804
3414
|
} else {
|
|
3415
|
+
closeProjectedAIProviderStream();
|
|
2805
3416
|
restoreAIProviderRoleAfterRequest(null);
|
|
2806
3417
|
resolveTerminal(value);
|
|
2807
3418
|
}
|
|
@@ -2875,6 +3486,15 @@ export class AIProviderRuntime {
|
|
|
2875
3486
|
}
|
|
2876
3487
|
|
|
2877
3488
|
async function cancelAIProviderStream(reason, terminalError = null) {
|
|
3489
|
+
const terminalOutcome = terminalError ?? normalizedAbort(
|
|
3490
|
+
reason instanceof Error
|
|
3491
|
+
? reason
|
|
3492
|
+
: operationError(
|
|
3493
|
+
'The AI stream was cancelled.',
|
|
3494
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
3495
|
+
)
|
|
3496
|
+
);
|
|
3497
|
+
closeProjectedAIProviderStream(terminalOutcome);
|
|
2878
3498
|
if (cleanupPromise) {
|
|
2879
3499
|
return cleanupPromise;
|
|
2880
3500
|
}
|
|
@@ -2888,14 +3508,6 @@ export class AIProviderRuntime {
|
|
|
2888
3508
|
);
|
|
2889
3509
|
controller.abort(reason);
|
|
2890
3510
|
(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
3511
|
try {
|
|
2900
3512
|
const cleanupOutcome = await cleanupOwnedAIProviderStream(reason);
|
|
2901
3513
|
assertStreamCleanupComplete(cleanupOutcome);
|
|
@@ -2987,9 +3599,51 @@ export class AIProviderRuntime {
|
|
|
2987
3599
|
requestSequence,
|
|
2988
3600
|
controller.signal
|
|
2989
3601
|
);
|
|
3602
|
+
privateStreamObservation = (
|
|
3603
|
+
async function observePrivateAIProviderStream() {
|
|
3604
|
+
try {
|
|
3605
|
+
while (true) {
|
|
3606
|
+
const result = await iterator.next();
|
|
3607
|
+
runtime.#assertCurrentRequest(
|
|
3608
|
+
slot,
|
|
3609
|
+
generation,
|
|
3610
|
+
requestSequence,
|
|
3611
|
+
controller.signal
|
|
3612
|
+
);
|
|
3613
|
+
if (result.done) {
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (role !== 'llm') {
|
|
3617
|
+
publishProjectedAIProviderStreamChunk(result.value);
|
|
3618
|
+
continue;
|
|
3619
|
+
}
|
|
3620
|
+
llmToolCallCorrelation.observe(result.value);
|
|
3621
|
+
const projected = projectLLMStreamChunk(result.value);
|
|
3622
|
+
if (projected !== OMITTED_LLM_STREAM_DATA) {
|
|
3623
|
+
publishProjectedAIProviderStreamChunk(projected);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
const normalized = isAbort(error, controller.signal)
|
|
3628
|
+
? normalizedAbort(error)
|
|
3629
|
+
: error;
|
|
3630
|
+
closeProjectedAIProviderStream(normalized);
|
|
3631
|
+
try {
|
|
3632
|
+
await cancelAIProviderStream(normalized, normalized);
|
|
3633
|
+
} catch {
|
|
3634
|
+
// Cancellation owns any cleanup failure.
|
|
3635
|
+
}
|
|
3636
|
+
throw normalized;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
)();
|
|
3640
|
+
privateStreamObservation.catch(
|
|
3641
|
+
function retainPrivateAIProviderStreamRejection() {}
|
|
3642
|
+
);
|
|
2990
3643
|
Promise.resolve(opened.result).then(
|
|
2991
|
-
function acceptAIProviderStreamResult(value) {
|
|
3644
|
+
async function acceptAIProviderStreamResult(value) {
|
|
2992
3645
|
try {
|
|
3646
|
+
await privateStreamObservation;
|
|
2993
3647
|
runtime.#assertCurrentRequest(
|
|
2994
3648
|
slot,
|
|
2995
3649
|
generation,
|
|
@@ -3002,8 +3656,17 @@ export class AIProviderRuntime {
|
|
|
3002
3656
|
'AI provider stream result'
|
|
3003
3657
|
)
|
|
3004
3658
|
: value;
|
|
3659
|
+
llmToolCallCorrelation?.assertTerminal(terminalValue);
|
|
3005
3660
|
settleAIProviderStream(null, terminalValue);
|
|
3006
3661
|
} catch (error) {
|
|
3662
|
+
if (cleanupPromise) {
|
|
3663
|
+
try {
|
|
3664
|
+
await cleanupPromise;
|
|
3665
|
+
} catch {
|
|
3666
|
+
// Stream cancellation owns terminal cleanup failure.
|
|
3667
|
+
}
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3007
3670
|
const normalized = isAbort(error, controller.signal)
|
|
3008
3671
|
? normalizedAbort(error)
|
|
3009
3672
|
: error;
|
|
@@ -3025,49 +3688,15 @@ export class AIProviderRuntime {
|
|
|
3025
3688
|
const handle = {
|
|
3026
3689
|
result: terminal,
|
|
3027
3690
|
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
|
-
}
|
|
3691
|
+
next() {
|
|
3692
|
+
return readProjectedAIProviderStreamChunk();
|
|
3060
3693
|
},
|
|
3061
3694
|
async return(value) {
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
'ARCANE_AI_REQUEST_ABORTED'
|
|
3068
|
-
)
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3695
|
+
cancelAIProviderStream(
|
|
3696
|
+
operationError(
|
|
3697
|
+
'The AI stream consumer stopped before completion.',
|
|
3698
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
3699
|
+
)
|
|
3071
3700
|
).catch(
|
|
3072
3701
|
function reportReturnedAIProviderStreamCancellationFailure(error) {
|
|
3073
3702
|
console.error(
|
|
@@ -3079,7 +3708,7 @@ export class AIProviderRuntime {
|
|
|
3079
3708
|
return {value, done: true};
|
|
3080
3709
|
},
|
|
3081
3710
|
async throw(error) {
|
|
3082
|
-
await cancelAIProviderStream(error);
|
|
3711
|
+
await cancelAIProviderStream(error, error);
|
|
3083
3712
|
throw error;
|
|
3084
3713
|
},
|
|
3085
3714
|
[Symbol.asyncIterator]() {
|
|
@@ -3091,6 +3720,7 @@ export class AIProviderRuntime {
|
|
|
3091
3720
|
const normalized = isAbort(error, controller.signal)
|
|
3092
3721
|
? normalizedAbort(error)
|
|
3093
3722
|
: error;
|
|
3723
|
+
closeProjectedAIProviderStream(normalized);
|
|
3094
3724
|
if (cleanupPromise) {
|
|
3095
3725
|
try {
|
|
3096
3726
|
await cleanupPromise;
|
|
@@ -3500,7 +4130,7 @@ export class AIProviderRuntime {
|
|
|
3500
4130
|
{
|
|
3501
4131
|
state: 'loading',
|
|
3502
4132
|
operationId,
|
|
3503
|
-
progress:
|
|
4133
|
+
progress: completeProgress(progress)
|
|
3504
4134
|
}
|
|
3505
4135
|
)
|
|
3506
4136
|
);
|