arcane-os 0.2.2 → 0.3.0
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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -12,6 +12,7 @@ export const AI_PROVIDER_RUNTIME_PROTOCOL = 'arcane-ai-runtime/2';
|
|
|
12
12
|
export const AI_MODEL_AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
|
|
13
13
|
|
|
14
14
|
const ROLE_SET = new Set(AI_RUNTIME_ROLES);
|
|
15
|
+
const SPEECH_ROLES = Object.freeze(['stt', 'tts']);
|
|
15
16
|
const PROVIDER_METHODS = Object.freeze([
|
|
16
17
|
'catalog',
|
|
17
18
|
'inspect',
|
|
@@ -265,6 +266,24 @@ function immutableSelections(value) {
|
|
|
265
266
|
);
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
function immutableSpeechSelections(value) {
|
|
270
|
+
try {
|
|
271
|
+
assertClosedRecord(value, SPEECH_ROLES, 'AI speech provider selections');
|
|
272
|
+
return Object.freeze(
|
|
273
|
+
{
|
|
274
|
+
stt: immutableRoleRoutes('stt', value.stt),
|
|
275
|
+
tts: immutableRoleRoutes('tts', value.tts)
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error?.code === 'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
280
|
+
&& !Object.hasOwn(error, 'reason')) {
|
|
281
|
+
error.reason = 'speech-configuration-contract-mismatch';
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
function immutableStartupOptions(options) {
|
|
269
288
|
if (options === undefined) {
|
|
270
289
|
return Object.freeze({
|
|
@@ -428,6 +447,252 @@ function validateProvider(provider) {
|
|
|
428
447
|
return admitted;
|
|
429
448
|
}
|
|
430
449
|
|
|
450
|
+
function speechProviderReplacementError(code, reason, message, cause) {
|
|
451
|
+
const error = cause === undefined
|
|
452
|
+
? new Error(message)
|
|
453
|
+
: new Error(message, {cause});
|
|
454
|
+
error.code = code;
|
|
455
|
+
error.reason = reason;
|
|
456
|
+
return error;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateSpeechProviderReplacementProvider(provider, role, label) {
|
|
460
|
+
let admitted;
|
|
461
|
+
try {
|
|
462
|
+
admitted = validateProvider(provider);
|
|
463
|
+
} catch (cause) {
|
|
464
|
+
throw speechProviderReplacementError(
|
|
465
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_CONTRACT_MISMATCH',
|
|
466
|
+
'speech-provider-replacement-provider-contract-mismatch',
|
|
467
|
+
`${label} must implement the ${AI_PROVIDER_PROTOCOL} ${role} provider contract.`,
|
|
468
|
+
cause
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (admitted.role !== role) {
|
|
472
|
+
throw speechProviderReplacementError(
|
|
473
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_ROLE_MISMATCH',
|
|
474
|
+
'speech-provider-replacement-provider-role-mismatch',
|
|
475
|
+
`${label}.role must equal ${role}.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return admitted;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function immutableSpeechProviderReplacement(value) {
|
|
482
|
+
try {
|
|
483
|
+
assertClosedRecord(
|
|
484
|
+
value,
|
|
485
|
+
['providers', 'routes', 'expectedProviders'],
|
|
486
|
+
'AI speech provider replacement'
|
|
487
|
+
);
|
|
488
|
+
assertClosedRecord(
|
|
489
|
+
value.providers,
|
|
490
|
+
SPEECH_ROLES,
|
|
491
|
+
'AI speech provider replacement.providers'
|
|
492
|
+
);
|
|
493
|
+
assertClosedRecord(
|
|
494
|
+
value.expectedProviders,
|
|
495
|
+
SPEECH_ROLES,
|
|
496
|
+
'AI speech provider replacement.expectedProviders'
|
|
497
|
+
);
|
|
498
|
+
} catch (cause) {
|
|
499
|
+
throw speechProviderReplacementError(
|
|
500
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_CONTRACT_MISMATCH',
|
|
501
|
+
'speech-provider-replacement-contract-mismatch',
|
|
502
|
+
'AI speech provider replacement must be a closed providers, routes, and expectedProviders record.',
|
|
503
|
+
cause
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let routes;
|
|
508
|
+
try {
|
|
509
|
+
routes = immutableSpeechSelections(value.routes);
|
|
510
|
+
} catch (cause) {
|
|
511
|
+
throw speechProviderReplacementError(
|
|
512
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROUTE_CONTRACT_MISMATCH',
|
|
513
|
+
'speech-provider-replacement-route-contract-mismatch',
|
|
514
|
+
'AI speech provider replacement.routes must be a closed STT/TTS route record.',
|
|
515
|
+
cause
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const providers = {};
|
|
520
|
+
const expectedProviders = {};
|
|
521
|
+
const replacementProviderCount = SPEECH_ROLES.filter(
|
|
522
|
+
function hasSpeechReplacementProvider(role) {
|
|
523
|
+
return value.providers[role] !== null;
|
|
524
|
+
}
|
|
525
|
+
).length;
|
|
526
|
+
const expectedProviderCount = SPEECH_ROLES.filter(
|
|
527
|
+
function hasExpectedSpeechProvider(role) {
|
|
528
|
+
return value.expectedProviders[role] !== null;
|
|
529
|
+
}
|
|
530
|
+
).length;
|
|
531
|
+
if (replacementProviderCount !== 0
|
|
532
|
+
&& replacementProviderCount !== SPEECH_ROLES.length) {
|
|
533
|
+
throw speechProviderReplacementError(
|
|
534
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_PAIR_MISMATCH',
|
|
535
|
+
'speech-provider-replacement-provider-pair-mismatch',
|
|
536
|
+
'AI speech provider replacement must supply both replacement providers or null for both.'
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const removal = replacementProviderCount === 0;
|
|
540
|
+
if (removal && expectedProviderCount !== SPEECH_ROLES.length) {
|
|
541
|
+
throw speechProviderReplacementError(
|
|
542
|
+
'ARCANE_AI_SPEECH_PROVIDER_REMOVAL_EXPECTED_PROVIDER_REQUIRED',
|
|
543
|
+
'speech-provider-removal-expected-provider-required',
|
|
544
|
+
'AI speech provider removal requires exact expected STT and TTS provider identities.'
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
for (const role of SPEECH_ROLES) {
|
|
548
|
+
const provider = removal
|
|
549
|
+
? null
|
|
550
|
+
: validateSpeechProviderReplacementProvider(
|
|
551
|
+
value.providers[role],
|
|
552
|
+
role,
|
|
553
|
+
`AI speech provider replacement.providers.${role}`
|
|
554
|
+
);
|
|
555
|
+
const expectedValue = value.expectedProviders[role];
|
|
556
|
+
if (expectedValue !== null) {
|
|
557
|
+
expectedProviders[role] = validateSpeechProviderReplacementProvider(
|
|
558
|
+
expectedValue,
|
|
559
|
+
role,
|
|
560
|
+
`AI speech provider replacement.expectedProviders.${role}`
|
|
561
|
+
);
|
|
562
|
+
} else {
|
|
563
|
+
expectedProviders[role] = null;
|
|
564
|
+
}
|
|
565
|
+
if (removal) {
|
|
566
|
+
if (routes[role].default || routes[role].localOnly) {
|
|
567
|
+
throw speechProviderReplacementError(
|
|
568
|
+
'ARCANE_AI_SPEECH_PROVIDER_REMOVAL_ROUTE_NOT_EMPTY',
|
|
569
|
+
'speech-provider-removal-route-not-empty',
|
|
570
|
+
`AI speech provider removal routes for ${role} must both be null.`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
} else if (!routes[role].default) {
|
|
574
|
+
throw speechProviderReplacementError(
|
|
575
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_DEFAULT_ROUTE_REQUIRED',
|
|
576
|
+
'speech-provider-replacement-default-route-required',
|
|
577
|
+
`AI speech provider replacement.routes.${role}.default must select the replacement provider.`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
for (const routeName of ROUTE_KEYS) {
|
|
581
|
+
const selection = routes[role][routeName];
|
|
582
|
+
if (!selection) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (!provider
|
|
586
|
+
|| selection.providerId !== provider.id
|
|
587
|
+
|| selection.localOnly !== provider.localOnly) {
|
|
588
|
+
throw speechProviderReplacementError(
|
|
589
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROUTE_PROVIDER_MISMATCH',
|
|
590
|
+
'speech-provider-replacement-route-provider-mismatch',
|
|
591
|
+
`AI speech provider replacement ${role} ${routeName} route must match its replacement provider identity and locality.`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
providers[role] = provider;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return Object.freeze(
|
|
599
|
+
{
|
|
600
|
+
providers: Object.freeze(providers),
|
|
601
|
+
routes,
|
|
602
|
+
expectedProviders: Object.freeze(expectedProviders),
|
|
603
|
+
removal
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function immutableSpeechProviderRoleReplacement(role, value) {
|
|
609
|
+
if (!SPEECH_ROLES.includes(role)) {
|
|
610
|
+
throw speechProviderReplacementError(
|
|
611
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_CONTRACT_MISMATCH',
|
|
612
|
+
'speech-provider-replacement-contract-mismatch',
|
|
613
|
+
'AI speech provider role replacement requires role stt or tts.'
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
assertClosedRecord(
|
|
618
|
+
value,
|
|
619
|
+
['provider', 'routes', 'expectedProvider'],
|
|
620
|
+
'AI speech provider role replacement'
|
|
621
|
+
);
|
|
622
|
+
} catch (cause) {
|
|
623
|
+
throw speechProviderReplacementError(
|
|
624
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_CONTRACT_MISMATCH',
|
|
625
|
+
'speech-provider-replacement-contract-mismatch',
|
|
626
|
+
'AI speech provider role replacement must be a closed provider, routes, and expectedProvider record.',
|
|
627
|
+
cause
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
let routes;
|
|
632
|
+
try {
|
|
633
|
+
routes = immutableRoleRoutes(role, value.routes);
|
|
634
|
+
} catch (cause) {
|
|
635
|
+
throw speechProviderReplacementError(
|
|
636
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROUTE_CONTRACT_MISMATCH',
|
|
637
|
+
'speech-provider-replacement-route-contract-mismatch',
|
|
638
|
+
`AI speech provider role replacement.routes must be a closed ${role.toUpperCase()} route record.`,
|
|
639
|
+
cause
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const removal = value.provider === null;
|
|
643
|
+
const provider = removal
|
|
644
|
+
? null
|
|
645
|
+
: validateSpeechProviderReplacementProvider(
|
|
646
|
+
value.provider,
|
|
647
|
+
role,
|
|
648
|
+
'AI speech provider role replacement.provider'
|
|
649
|
+
);
|
|
650
|
+
const expectedProvider = value.expectedProvider === null
|
|
651
|
+
? null
|
|
652
|
+
: validateSpeechProviderReplacementProvider(
|
|
653
|
+
value.expectedProvider,
|
|
654
|
+
role,
|
|
655
|
+
'AI speech provider role replacement.expectedProvider'
|
|
656
|
+
);
|
|
657
|
+
if (removal && expectedProvider === null) {
|
|
658
|
+
throw speechProviderReplacementError(
|
|
659
|
+
'ARCANE_AI_SPEECH_PROVIDER_REMOVAL_EXPECTED_PROVIDER_REQUIRED',
|
|
660
|
+
'speech-provider-removal-expected-provider-required',
|
|
661
|
+
'AI speech provider role removal requires its exact expected provider identity.'
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (removal && (routes.default || routes.localOnly)) {
|
|
665
|
+
throw speechProviderReplacementError(
|
|
666
|
+
'ARCANE_AI_SPEECH_PROVIDER_REMOVAL_ROUTE_NOT_EMPTY',
|
|
667
|
+
'speech-provider-removal-route-not-empty',
|
|
668
|
+
`AI speech provider removal routes for ${role} must both be null.`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (!removal && !routes.default) {
|
|
672
|
+
throw speechProviderReplacementError(
|
|
673
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_DEFAULT_ROUTE_REQUIRED',
|
|
674
|
+
'speech-provider-replacement-default-route-required',
|
|
675
|
+
'AI speech provider role replacement.routes.default must select the replacement provider.'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
for (const routeName of ROUTE_KEYS) {
|
|
679
|
+
const selection = routes[routeName];
|
|
680
|
+
if (!selection) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!provider
|
|
684
|
+
|| selection.providerId !== provider.id
|
|
685
|
+
|| selection.localOnly !== provider.localOnly) {
|
|
686
|
+
throw speechProviderReplacementError(
|
|
687
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROUTE_PROVIDER_MISMATCH',
|
|
688
|
+
'speech-provider-replacement-route-provider-mismatch',
|
|
689
|
+
`AI speech provider role replacement ${role} ${routeName} route must match its replacement provider identity and locality.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return Object.freeze({role, provider, routes, expectedProvider, removal});
|
|
694
|
+
}
|
|
695
|
+
|
|
431
696
|
function validateProviderStatus(status) {
|
|
432
697
|
assertPlainObject(status, 'AI provider status');
|
|
433
698
|
if (typeof status.state !== 'string'
|
|
@@ -496,6 +761,7 @@ export class AIProviderRuntime {
|
|
|
496
761
|
#speechMuted = true;
|
|
497
762
|
#speechDesiredMuted = true;
|
|
498
763
|
#speechTransition = Promise.resolve();
|
|
764
|
+
#speechTransitionOwners = 0;
|
|
499
765
|
#unsubscribeIntents = null;
|
|
500
766
|
#closed = false;
|
|
501
767
|
#closing = false;
|
|
@@ -601,18 +867,7 @@ export class AIProviderRuntime {
|
|
|
601
867
|
}
|
|
602
868
|
}
|
|
603
869
|
|
|
604
|
-
|
|
605
|
-
let active = true;
|
|
606
|
-
return function unregisterAIProvider() {
|
|
607
|
-
if (!active) {
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
const removed = runtime.unregister(admitted.role, admitted.id, admitted);
|
|
611
|
-
if (removed) {
|
|
612
|
-
active = false;
|
|
613
|
-
}
|
|
614
|
-
return removed;
|
|
615
|
-
};
|
|
870
|
+
return this.#createProviderUnregisterHandle(admitted);
|
|
616
871
|
}
|
|
617
872
|
|
|
618
873
|
unregister(role, providerId, expectedProvider = null) {
|
|
@@ -645,6 +900,37 @@ export class AIProviderRuntime {
|
|
|
645
900
|
return this.#providers.has(providerKey(role, providerId));
|
|
646
901
|
}
|
|
647
902
|
|
|
903
|
+
ownsProvider(role, expectedProvider) {
|
|
904
|
+
if (!ROLE_SET.has(role)) {
|
|
905
|
+
throw speechProviderReplacementError(
|
|
906
|
+
'ARCANE_AI_PROVIDER_IDENTITY_ROLE_INVALID',
|
|
907
|
+
'provider-identity-role-invalid',
|
|
908
|
+
`AI provider identity role must be one of ${AI_RUNTIME_ROLES.join(', ')}.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let admitted;
|
|
913
|
+
try {
|
|
914
|
+
admitted = validateProvider(expectedProvider);
|
|
915
|
+
} catch (cause) {
|
|
916
|
+
throw speechProviderReplacementError(
|
|
917
|
+
'ARCANE_AI_PROVIDER_IDENTITY_CONTRACT_MISMATCH',
|
|
918
|
+
'provider-identity-contract-mismatch',
|
|
919
|
+
`Expected ${role} provider identity must implement the ${AI_PROVIDER_PROTOCOL} provider contract.`,
|
|
920
|
+
cause
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (admitted.role !== role) {
|
|
924
|
+
throw speechProviderReplacementError(
|
|
925
|
+
'ARCANE_AI_PROVIDER_IDENTITY_ROLE_MISMATCH',
|
|
926
|
+
'provider-identity-role-mismatch',
|
|
927
|
+
`Expected provider identity role ${admitted.role} does not match ${role}.`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return this.#providers.get(providerKey(role, admitted.id)) === admitted;
|
|
932
|
+
}
|
|
933
|
+
|
|
648
934
|
providerIdentity(role, providerId) {
|
|
649
935
|
assertRole(role);
|
|
650
936
|
assertIdentifier(providerId, 'AI provider id');
|
|
@@ -709,6 +995,29 @@ export class AIProviderRuntime {
|
|
|
709
995
|
return selections;
|
|
710
996
|
}
|
|
711
997
|
|
|
998
|
+
validateSpeechConfiguration(value) {
|
|
999
|
+
this.#assertOpen();
|
|
1000
|
+
const selections = immutableSpeechSelections(value);
|
|
1001
|
+
for (const role of SPEECH_ROLES) {
|
|
1002
|
+
for (const routeName of ROUTE_KEYS) {
|
|
1003
|
+
const selection = selections[role][routeName];
|
|
1004
|
+
if (!selection) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const provider = this.#providers.get(
|
|
1008
|
+
providerKey(role, selection.providerId)
|
|
1009
|
+
) ?? null;
|
|
1010
|
+
if (provider && selection.localOnly !== provider.localOnly) {
|
|
1011
|
+
throw operationError(
|
|
1012
|
+
`AI ${role} route ${routeName} does not match provider locality.`,
|
|
1013
|
+
'ARCANE_AI_PROVIDER_LOCALITY_MISMATCH'
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return selections;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
712
1021
|
configure(value) {
|
|
713
1022
|
this.#assertOpen();
|
|
714
1023
|
if (this.#configuring) {
|
|
@@ -753,6 +1062,376 @@ export class AIProviderRuntime {
|
|
|
753
1062
|
return selections;
|
|
754
1063
|
}
|
|
755
1064
|
|
|
1065
|
+
configureSpeech(value) {
|
|
1066
|
+
this.#assertOpen();
|
|
1067
|
+
if (this.#configuring) {
|
|
1068
|
+
throw operationError(
|
|
1069
|
+
'AI speech provider configuration cannot be changed reentrantly.',
|
|
1070
|
+
'ARCANE_AI_SPEECH_CONFIGURATION_REENTRANT'
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const selections = this.validateSpeechConfiguration(value);
|
|
1074
|
+
for (const role of SPEECH_ROLES) {
|
|
1075
|
+
const slot = this.#slots[role];
|
|
1076
|
+
if (this.#roleHasOwnedWork(slot)) {
|
|
1077
|
+
throw operationError(
|
|
1078
|
+
`AI role ${role} must be unloaded before it is reconfigured.`,
|
|
1079
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
this.#configuring = true;
|
|
1085
|
+
try {
|
|
1086
|
+
for (const role of SPEECH_ROLES) {
|
|
1087
|
+
const slot = this.#slots[role];
|
|
1088
|
+
slot.generation += 1;
|
|
1089
|
+
slot.disposed = false;
|
|
1090
|
+
slot.ready = false;
|
|
1091
|
+
slot.routes = selections[role];
|
|
1092
|
+
slot.selection = selections[role].default;
|
|
1093
|
+
}
|
|
1094
|
+
this.#speechDesiredMuted = true;
|
|
1095
|
+
this.#speechMuted = true;
|
|
1096
|
+
this.#configured = true;
|
|
1097
|
+
publishAIRuntimeRolesState(
|
|
1098
|
+
{
|
|
1099
|
+
llm: this.status('llm'),
|
|
1100
|
+
stt: roleRecord('stt', this.#slots.stt.selection),
|
|
1101
|
+
tts: roleRecord('tts', this.#slots.tts.selection)
|
|
1102
|
+
}
|
|
1103
|
+
);
|
|
1104
|
+
} finally {
|
|
1105
|
+
this.#configuring = false;
|
|
1106
|
+
}
|
|
1107
|
+
return selections;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
replaceSpeechProvider(role, value) {
|
|
1111
|
+
this.#assertOpen();
|
|
1112
|
+
if (this.#configuring) {
|
|
1113
|
+
throw speechProviderReplacementError(
|
|
1114
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_REENTRANT',
|
|
1115
|
+
'speech-provider-replacement-reentrant',
|
|
1116
|
+
'AI speech provider replacement cannot be committed reentrantly.'
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
this.#configuring = true;
|
|
1120
|
+
try {
|
|
1121
|
+
return this.#commitSpeechProviderRoleReplacement(role, value);
|
|
1122
|
+
} finally {
|
|
1123
|
+
this.#configuring = false;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
#commitSpeechProviderRoleReplacement(role, value) {
|
|
1128
|
+
const replacement = immutableSpeechProviderRoleReplacement(role, value);
|
|
1129
|
+
const currentState = getAIRuntimeState();
|
|
1130
|
+
const slot = this.#slots[role];
|
|
1131
|
+
const roleState = currentState.roles[role];
|
|
1132
|
+
if (this.#roleHasOwnedWork(slot)
|
|
1133
|
+
|| roleState.loaded
|
|
1134
|
+
|| roleState.busy
|
|
1135
|
+
|| (role === 'tts' && this.#speechTransitionOwners > 0)) {
|
|
1136
|
+
throw speechProviderReplacementError(
|
|
1137
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROLE_NOT_UNLOADED',
|
|
1138
|
+
'speech-provider-replacement-role-not-unloaded',
|
|
1139
|
+
`AI role ${role} must be fully unloaded with no owned work before speech provider replacement.`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const expected = replacement.expectedProvider;
|
|
1144
|
+
if (expected) {
|
|
1145
|
+
const registered = this.#providers.get(
|
|
1146
|
+
providerKey(role, expected.id)
|
|
1147
|
+
) ?? null;
|
|
1148
|
+
if (registered !== expected) {
|
|
1149
|
+
throw speechProviderReplacementError(
|
|
1150
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_MISMATCH',
|
|
1151
|
+
'speech-provider-replacement-expected-provider-mismatch',
|
|
1152
|
+
`AI role ${role} expected provider identity is no longer registered.`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
const routeProviderMismatch = ROUTE_KEYS.some(
|
|
1156
|
+
function hasUnexpectedSelectedSpeechProvider(routeName) {
|
|
1157
|
+
const selection = slot.routes[routeName];
|
|
1158
|
+
return selection !== null
|
|
1159
|
+
&& selection.providerId !== expected.id;
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
if (slot.routes.default?.providerId !== expected.id
|
|
1163
|
+
|| slot.selection?.providerId !== expected.id
|
|
1164
|
+
|| routeProviderMismatch) {
|
|
1165
|
+
throw speechProviderReplacementError(
|
|
1166
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_SELECTED_PROVIDER_MISMATCH',
|
|
1167
|
+
'speech-provider-replacement-selected-provider-mismatch',
|
|
1168
|
+
`AI role ${role} selected routes no longer belong to the expected provider identity.`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
} else if (slot.routes.default
|
|
1172
|
+
|| slot.routes.localOnly
|
|
1173
|
+
|| slot.selection) {
|
|
1174
|
+
throw speechProviderReplacementError(
|
|
1175
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
1176
|
+
'speech-provider-replacement-expected-provider-required',
|
|
1177
|
+
`AI role ${role} has a selected provider, so replacement requires its exact expected provider identity.`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const provider = replacement.provider;
|
|
1182
|
+
if (provider) {
|
|
1183
|
+
const registeredAtReplacementKey = this.#providers.get(
|
|
1184
|
+
providerKey(role, provider.id)
|
|
1185
|
+
) ?? null;
|
|
1186
|
+
if (registeredAtReplacementKey
|
|
1187
|
+
&& registeredAtReplacementKey !== expected) {
|
|
1188
|
+
throw speechProviderReplacementError(
|
|
1189
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_ID_COLLISION',
|
|
1190
|
+
'speech-provider-replacement-provider-id-collision',
|
|
1191
|
+
`AI role ${role} replacement provider id is registered by a different provider identity.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const currentProvider = this.#providerFor(slot);
|
|
1197
|
+
const inspectedProviders = new Set([currentProvider, expected, provider]);
|
|
1198
|
+
inspectedProviders.delete(null);
|
|
1199
|
+
for (const inspected of inspectedProviders) {
|
|
1200
|
+
let providerStatus;
|
|
1201
|
+
try {
|
|
1202
|
+
providerStatus = validateProviderStatus(inspected.status());
|
|
1203
|
+
} catch (cause) {
|
|
1204
|
+
throw speechProviderReplacementError(
|
|
1205
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_STATUS_INVALID',
|
|
1206
|
+
'speech-provider-replacement-provider-status-invalid',
|
|
1207
|
+
`AI role ${role} replacement boundary received an invalid provider status.`,
|
|
1208
|
+
cause
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (providerStatus.state !== 'unloaded'
|
|
1212
|
+
|| providerStatus.loaded
|
|
1213
|
+
|| providerStatus.busy) {
|
|
1214
|
+
throw speechProviderReplacementError(
|
|
1215
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_NOT_UNLOADED',
|
|
1216
|
+
'speech-provider-replacement-provider-not-unloaded',
|
|
1217
|
+
`AI role ${role} replacement boundary requires selected, old, and new providers to report unloaded and idle.`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1222
|
+
throw speechProviderReplacementError(
|
|
1223
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1224
|
+
'speech-provider-replacement-state-revision-exhausted',
|
|
1225
|
+
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (expected) {
|
|
1230
|
+
this.#providers.delete(providerKey(role, expected.id));
|
|
1231
|
+
this.#disposeAllCompletedProviders.delete(expected);
|
|
1232
|
+
}
|
|
1233
|
+
if (provider) {
|
|
1234
|
+
this.#providers.set(providerKey(role, provider.id), provider);
|
|
1235
|
+
this.#disposeAllCompletedProviders.delete(provider);
|
|
1236
|
+
}
|
|
1237
|
+
slot.generation += 1;
|
|
1238
|
+
slot.disposed = false;
|
|
1239
|
+
slot.ready = false;
|
|
1240
|
+
slot.routes = replacement.routes;
|
|
1241
|
+
slot.selection = replacement.routes.default;
|
|
1242
|
+
this.#configured = true;
|
|
1243
|
+
publishAIRuntimeRoleState(role, roleRecord(role, slot.selection));
|
|
1244
|
+
|
|
1245
|
+
return Object.freeze(
|
|
1246
|
+
{
|
|
1247
|
+
role,
|
|
1248
|
+
routes: replacement.routes,
|
|
1249
|
+
unregister: replacement.removal
|
|
1250
|
+
? null
|
|
1251
|
+
: this.#createProviderUnregisterHandle(provider)
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
replaceSpeechProviders(value) {
|
|
1257
|
+
this.#assertOpen();
|
|
1258
|
+
if (this.#configuring) {
|
|
1259
|
+
throw speechProviderReplacementError(
|
|
1260
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_REENTRANT',
|
|
1261
|
+
'speech-provider-replacement-reentrant',
|
|
1262
|
+
'AI speech provider replacement cannot be committed reentrantly.'
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
this.#configuring = true;
|
|
1266
|
+
try {
|
|
1267
|
+
return this.#commitSpeechProviderReplacement(value);
|
|
1268
|
+
} finally {
|
|
1269
|
+
this.#configuring = false;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
#commitSpeechProviderReplacement(value) {
|
|
1274
|
+
const replacement = immutableSpeechProviderReplacement(value);
|
|
1275
|
+
const currentState = getAIRuntimeState();
|
|
1276
|
+
for (const role of SPEECH_ROLES) {
|
|
1277
|
+
const slot = this.#slots[role];
|
|
1278
|
+
const roleState = currentState.roles[role];
|
|
1279
|
+
if (this.#roleHasOwnedWork(slot)
|
|
1280
|
+
|| roleState.loaded
|
|
1281
|
+
|| roleState.busy
|
|
1282
|
+
|| this.#speechTransitionOwners > 0) {
|
|
1283
|
+
throw speechProviderReplacementError(
|
|
1284
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_ROLE_NOT_UNLOADED',
|
|
1285
|
+
'speech-provider-replacement-role-not-unloaded',
|
|
1286
|
+
`AI role ${role} must be fully unloaded with no owned work before speech provider replacement.`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const expected = replacement.expectedProviders[role];
|
|
1291
|
+
if (expected) {
|
|
1292
|
+
const registered = this.#providers.get(
|
|
1293
|
+
providerKey(role, expected.id)
|
|
1294
|
+
) ?? null;
|
|
1295
|
+
if (registered !== expected) {
|
|
1296
|
+
throw speechProviderReplacementError(
|
|
1297
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_MISMATCH',
|
|
1298
|
+
'speech-provider-replacement-expected-provider-mismatch',
|
|
1299
|
+
`AI role ${role} expected provider identity is no longer registered.`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
const routeProviderMismatch = ROUTE_KEYS.some(
|
|
1303
|
+
function hasUnexpectedSelectedSpeechProvider(routeName) {
|
|
1304
|
+
const selection = slot.routes[routeName];
|
|
1305
|
+
return selection !== null
|
|
1306
|
+
&& selection.providerId !== expected.id;
|
|
1307
|
+
}
|
|
1308
|
+
);
|
|
1309
|
+
if (slot.routes.default?.providerId !== expected.id
|
|
1310
|
+
|| slot.selection?.providerId !== expected.id
|
|
1311
|
+
|| routeProviderMismatch) {
|
|
1312
|
+
throw speechProviderReplacementError(
|
|
1313
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_SELECTED_PROVIDER_MISMATCH',
|
|
1314
|
+
'speech-provider-replacement-selected-provider-mismatch',
|
|
1315
|
+
`AI role ${role} selected routes no longer belong to the expected provider identity.`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
} else if (slot.routes.default
|
|
1319
|
+
|| slot.routes.localOnly
|
|
1320
|
+
|| slot.selection) {
|
|
1321
|
+
throw speechProviderReplacementError(
|
|
1322
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_EXPECTED_PROVIDER_REQUIRED',
|
|
1323
|
+
'speech-provider-replacement-expected-provider-required',
|
|
1324
|
+
`AI role ${role} has a selected provider, so replacement requires its exact expected provider identity.`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const provider = replacement.providers[role];
|
|
1329
|
+
if (provider) {
|
|
1330
|
+
const registeredAtReplacementKey = this.#providers.get(
|
|
1331
|
+
providerKey(role, provider.id)
|
|
1332
|
+
) ?? null;
|
|
1333
|
+
if (registeredAtReplacementKey
|
|
1334
|
+
&& registeredAtReplacementKey !== expected) {
|
|
1335
|
+
throw speechProviderReplacementError(
|
|
1336
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_ID_COLLISION',
|
|
1337
|
+
'speech-provider-replacement-provider-id-collision',
|
|
1338
|
+
`AI role ${role} replacement provider id is registered by a different provider identity.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const currentProvider = this.#providerFor(slot);
|
|
1344
|
+
const inspectedProviders = new Set([
|
|
1345
|
+
currentProvider,
|
|
1346
|
+
expected,
|
|
1347
|
+
provider
|
|
1348
|
+
]);
|
|
1349
|
+
inspectedProviders.delete(null);
|
|
1350
|
+
for (const inspected of inspectedProviders) {
|
|
1351
|
+
if (!inspected) {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
let providerStatus;
|
|
1355
|
+
try {
|
|
1356
|
+
providerStatus = validateProviderStatus(inspected.status());
|
|
1357
|
+
} catch (cause) {
|
|
1358
|
+
throw speechProviderReplacementError(
|
|
1359
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_STATUS_INVALID',
|
|
1360
|
+
'speech-provider-replacement-provider-status-invalid',
|
|
1361
|
+
`AI role ${role} replacement boundary received an invalid provider status.`,
|
|
1362
|
+
cause
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
if (providerStatus.state !== 'unloaded'
|
|
1366
|
+
|| providerStatus.loaded
|
|
1367
|
+
|| providerStatus.busy) {
|
|
1368
|
+
throw speechProviderReplacementError(
|
|
1369
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_PROVIDER_NOT_UNLOADED',
|
|
1370
|
+
'speech-provider-replacement-provider-not-unloaded',
|
|
1371
|
+
`AI role ${role} replacement boundary requires selected, old, and new providers to report unloaded and idle.`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (currentState.revision === Number.MAX_SAFE_INTEGER) {
|
|
1377
|
+
throw speechProviderReplacementError(
|
|
1378
|
+
'ARCANE_AI_SPEECH_PROVIDER_REPLACEMENT_STATE_REVISION_EXHAUSTED',
|
|
1379
|
+
'speech-provider-replacement-state-revision-exhausted',
|
|
1380
|
+
'AI speech provider replacement cannot publish another runtime state revision.'
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
for (const role of SPEECH_ROLES) {
|
|
1385
|
+
const expected = replacement.expectedProviders[role];
|
|
1386
|
+
const provider = replacement.providers[role];
|
|
1387
|
+
if (expected) {
|
|
1388
|
+
this.#providers.delete(providerKey(role, expected.id));
|
|
1389
|
+
this.#disposeAllCompletedProviders.delete(expected);
|
|
1390
|
+
}
|
|
1391
|
+
if (provider) {
|
|
1392
|
+
this.#providers.set(providerKey(role, provider.id), provider);
|
|
1393
|
+
this.#disposeAllCompletedProviders.delete(provider);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const slot = this.#slots[role];
|
|
1397
|
+
slot.generation += 1;
|
|
1398
|
+
slot.disposed = false;
|
|
1399
|
+
slot.ready = false;
|
|
1400
|
+
slot.routes = replacement.routes[role];
|
|
1401
|
+
slot.selection = replacement.routes[role].default;
|
|
1402
|
+
}
|
|
1403
|
+
this.#speechDesiredMuted = true;
|
|
1404
|
+
this.#speechMuted = true;
|
|
1405
|
+
this.#configured = true;
|
|
1406
|
+
publishAIRuntimeRolesState(
|
|
1407
|
+
{
|
|
1408
|
+
llm: currentState.roles.llm,
|
|
1409
|
+
stt: roleRecord('stt', this.#slots.stt.selection),
|
|
1410
|
+
tts: roleRecord('tts', this.#slots.tts.selection)
|
|
1411
|
+
}
|
|
1412
|
+
);
|
|
1413
|
+
|
|
1414
|
+
return Object.freeze(
|
|
1415
|
+
{
|
|
1416
|
+
routes: replacement.routes,
|
|
1417
|
+
unregisters: Object.freeze(
|
|
1418
|
+
{
|
|
1419
|
+
stt: replacement.removal
|
|
1420
|
+
? null
|
|
1421
|
+
: this.#createProviderUnregisterHandle(
|
|
1422
|
+
replacement.providers.stt
|
|
1423
|
+
),
|
|
1424
|
+
tts: replacement.removal
|
|
1425
|
+
? null
|
|
1426
|
+
: this.#createProviderUnregisterHandle(
|
|
1427
|
+
replacement.providers.tts
|
|
1428
|
+
)
|
|
1429
|
+
}
|
|
1430
|
+
)
|
|
1431
|
+
}
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
756
1435
|
configureFromTuple(tuple) {
|
|
757
1436
|
if (!Array.isArray(tuple) || tuple.length !== 6) {
|
|
758
1437
|
fail('AI preference tuple must contain exactly six entries.');
|
|
@@ -1238,7 +1917,11 @@ export class AIProviderRuntime {
|
|
|
1238
1917
|
&& !providerOwned
|
|
1239
1918
|
&& (!provider || providerStatus)) {
|
|
1240
1919
|
slot.ready = false;
|
|
1241
|
-
|
|
1920
|
+
publishAIRuntimeRoleState(
|
|
1921
|
+
role,
|
|
1922
|
+
roleRecord(role, slot.selection)
|
|
1923
|
+
);
|
|
1924
|
+
return Promise.resolve(this.status(role));
|
|
1242
1925
|
}
|
|
1243
1926
|
|
|
1244
1927
|
slot.generation += 1;
|
|
@@ -2116,6 +2799,7 @@ export class AIProviderRuntime {
|
|
|
2116
2799
|
this.cancel('tts');
|
|
2117
2800
|
}
|
|
2118
2801
|
const runtime = this;
|
|
2802
|
+
this.#speechTransitionOwners += 1;
|
|
2119
2803
|
const transition = this.#speechTransition.catch(
|
|
2120
2804
|
function retainPriorAISpeechTransitionFailure() {}
|
|
2121
2805
|
).then(
|
|
@@ -2145,6 +2829,10 @@ export class AIProviderRuntime {
|
|
|
2145
2829
|
}
|
|
2146
2830
|
}
|
|
2147
2831
|
}
|
|
2832
|
+
).finally(
|
|
2833
|
+
function releaseAIProviderSpeechTransitionOwnership() {
|
|
2834
|
+
runtime.#speechTransitionOwners -= 1;
|
|
2835
|
+
}
|
|
2148
2836
|
);
|
|
2149
2837
|
this.#speechTransition = transition;
|
|
2150
2838
|
return transition;
|
|
@@ -2349,6 +3037,25 @@ export class AIProviderRuntime {
|
|
|
2349
3037
|
);
|
|
2350
3038
|
}
|
|
2351
3039
|
|
|
3040
|
+
#createProviderUnregisterHandle(provider) {
|
|
3041
|
+
const runtime = this;
|
|
3042
|
+
let active = true;
|
|
3043
|
+
return function unregisterAIProvider() {
|
|
3044
|
+
if (!active) {
|
|
3045
|
+
return false;
|
|
3046
|
+
}
|
|
3047
|
+
const removed = runtime.unregister(
|
|
3048
|
+
provider.role,
|
|
3049
|
+
provider.id,
|
|
3050
|
+
provider
|
|
3051
|
+
);
|
|
3052
|
+
if (removed) {
|
|
3053
|
+
active = false;
|
|
3054
|
+
}
|
|
3055
|
+
return removed;
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2352
3059
|
#nextRequestSequence(slot) {
|
|
2353
3060
|
if (slot.requestSequence === Number.MAX_SAFE_INTEGER) {
|
|
2354
3061
|
throw operationError(
|