@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.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/README.md +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ToolkitCoordinator
|
|
2
|
+
* ToolkitCoordinator: one per assessment, the owner of every toolkit service
|
|
3
|
+
* (TTSService, ToolCoordinator, HighlightCoordinator, ElementToolStateStore,
|
|
4
|
+
* AccessibilityCatalogResolver, ToolProviderRegistry), the ToolPolicyEngine,
|
|
5
|
+
* the framework-error bus and the section controllers. It holds toolkit
|
|
6
|
+
* concerns only; navigation, timing and progress belong to the player.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Layout: the module's exported config, hook and event types come first, then
|
|
9
|
+
* the class, whose members run in this order:
|
|
10
|
+
* - config resolution and the constructor;
|
|
11
|
+
* - error, telemetry and tool-failure reporting, readiness listeners and the
|
|
12
|
+
* framework-error bus;
|
|
13
|
+
* - element tool-state persistence;
|
|
14
|
+
* - tool registry adoption and provider registration;
|
|
15
|
+
* - section-event subscriptions, which follow the active cohort;
|
|
16
|
+
* - section controller creation, publication and disposal;
|
|
17
|
+
* - coordinator disposal;
|
|
18
|
+
* - TTS bring-up and readiness;
|
|
19
|
+
* - tool config updates and the policy engine surface, with the tool-context
|
|
20
|
+
* resolvers split around it (installation before, queries after);
|
|
21
|
+
* - tool-open requests;
|
|
22
|
+
* - config-change application and TTS reconfiguration, last.
|
|
15
23
|
*/
|
|
16
24
|
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
17
25
|
import { collectToolConfigDiagnostics, normalizeToolConfigStrictness, reportToolConfigDiagnostics, } from "./tool-config-validation.js";
|
|
@@ -22,13 +30,13 @@ import { FrameworkErrorBus, } from "./framework-error-bus.js";
|
|
|
22
30
|
import { HighlightCoordinator } from "./HighlightCoordinator.js";
|
|
23
31
|
import { ToolCoordinator } from "./ToolCoordinator.js";
|
|
24
32
|
import { TTSService } from "./TTSService.js";
|
|
25
|
-
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
33
|
+
import { BrowserTTSProvider, browserFallbackConfig, } from "./tts/browser-provider.js";
|
|
26
34
|
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
|
|
27
35
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
28
|
-
import {
|
|
36
|
+
import { ToolRegistry } from "./ToolRegistry.js";
|
|
29
37
|
import { ToolRequestRegistry } from "./tool-request.js";
|
|
30
38
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
31
|
-
import {
|
|
39
|
+
import { resolveSectionSessionAssignment } from "./section-session-assignment.js";
|
|
32
40
|
class ToolkitCoordinatorDisposedError extends Error {
|
|
33
41
|
constructor() {
|
|
34
42
|
super("ToolkitCoordinator has been disposed.");
|
|
@@ -45,19 +53,22 @@ const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.
|
|
|
45
53
|
const mergeToolConfigUpdate = (toolId, current, updates) => {
|
|
46
54
|
const next = { ...current, ...updates };
|
|
47
55
|
if (toolId === "textToSpeech" &&
|
|
48
|
-
isPlainRecord(current.
|
|
49
|
-
isPlainRecord(updates.
|
|
50
|
-
next.
|
|
51
|
-
if (isPlainRecord(current.settings.mathSpeech) &&
|
|
52
|
-
isPlainRecord(updates.settings.mathSpeech)) {
|
|
53
|
-
next.settings.mathSpeech = {
|
|
54
|
-
...current.settings.mathSpeech,
|
|
55
|
-
...updates.settings.mathSpeech,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
56
|
+
isPlainRecord(current.mathSpeech) &&
|
|
57
|
+
isPlainRecord(updates.mathSpeech)) {
|
|
58
|
+
next.mathSpeech = { ...current.mathSpeech, ...updates.mathSpeech };
|
|
58
59
|
}
|
|
59
60
|
return next;
|
|
60
61
|
};
|
|
62
|
+
const TOOL_FAILURE_SUMMARY = {
|
|
63
|
+
"tool-module-load": "failed to load",
|
|
64
|
+
"tool-request-open": "failed to open on request",
|
|
65
|
+
"tool-request-host-check": "failed its toolbar's host check",
|
|
66
|
+
"tool-visibility": "failed its relevance check",
|
|
67
|
+
"tool-applicability": "failed its applicability check",
|
|
68
|
+
"tool-state-load": "failed to restore its state",
|
|
69
|
+
"tool-state-save": "failed to save its state",
|
|
70
|
+
"tool-playback": "failed during playback",
|
|
71
|
+
};
|
|
61
72
|
const SECTION_ITEM_EVENT_TYPES = [
|
|
62
73
|
"item-selected",
|
|
63
74
|
"item-session-data-changed",
|
|
@@ -81,7 +92,7 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
81
92
|
*
|
|
82
93
|
* @example
|
|
83
94
|
* ```typescript
|
|
84
|
-
* //
|
|
95
|
+
* // Exported by @pie-players/pie-default-tool-loaders
|
|
85
96
|
* const toolRegistry = createPackagedToolRegistry();
|
|
86
97
|
*
|
|
87
98
|
* // Create coordinator with configuration
|
|
@@ -91,7 +102,7 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
91
102
|
* tools: {
|
|
92
103
|
* providers: {
|
|
93
104
|
* textToSpeech: { enabled: true, backend: 'browser' },
|
|
94
|
-
* answerEliminator: { enabled: true
|
|
105
|
+
* answerEliminator: { enabled: true }
|
|
95
106
|
* },
|
|
96
107
|
* placement: {
|
|
97
108
|
* item: ['textToSpeech', 'answerEliminator']
|
|
@@ -123,25 +134,45 @@ export class ToolkitCoordinator {
|
|
|
123
134
|
lazyInit;
|
|
124
135
|
/** Track TTS initialization state */
|
|
125
136
|
ttsInitialized = false;
|
|
137
|
+
/** Text-to-speech failed to start and policy does not grant it. */
|
|
138
|
+
ttsDegraded = false;
|
|
126
139
|
ttsInitPromise;
|
|
127
140
|
ttsReconfigurePromise;
|
|
128
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The loader ran, whether or not it returned state. A failed load is
|
|
143
|
+
* reported once and the coordinator runs without saved tool state.
|
|
144
|
+
*/
|
|
145
|
+
stateLoadSettled = false;
|
|
129
146
|
stateLoadPromise;
|
|
130
147
|
coordinatorReadyPromise;
|
|
131
148
|
coordinatorReadyNotified = false;
|
|
132
149
|
providerInitPromises = new Map();
|
|
150
|
+
/**
|
|
151
|
+
* Recoverable failures, by tool. A policy change that grants one of these
|
|
152
|
+
* tools reports its failure again as fatal.
|
|
153
|
+
*/
|
|
154
|
+
degradedTools = new Map();
|
|
155
|
+
/** Tool failures already reported, by phase and tool, each reported once. */
|
|
156
|
+
reportedToolFailures = new Set();
|
|
157
|
+
readyChangeListeners = new Set();
|
|
158
|
+
eagerInit;
|
|
133
159
|
toolRegistry;
|
|
134
160
|
/** Whether the host passed `toolRegistry`, which then always stands. */
|
|
135
161
|
toolRegistrySupplied;
|
|
136
162
|
/**
|
|
137
163
|
* Whether the registry is final: supplied at construction, or adopted from
|
|
138
|
-
* the toolkit this coordinator is bound to.
|
|
139
|
-
* reports no missing registry, since the toolkit will supply one.
|
|
164
|
+
* the toolkit this coordinator is bound to.
|
|
140
165
|
*/
|
|
141
166
|
toolRegistrySettled;
|
|
167
|
+
/**
|
|
168
|
+
* Whether a bound toolkit reported having no registry. Until then, or until a
|
|
169
|
+
* registry settles, the empty placeholder reports no missing registry, since
|
|
170
|
+
* the toolkit may supply one.
|
|
171
|
+
*/
|
|
172
|
+
toolRegistryAbsent = false;
|
|
142
173
|
toolContextResolvers = new Map();
|
|
143
174
|
toolContextResolverChangeListeners = new Set();
|
|
144
|
-
toolRequests = new ToolRequestRegistry();
|
|
175
|
+
toolRequests = new ToolRequestRegistry((toolId, phase, error) => this.reportToolFailure(toolId, phase, error));
|
|
145
176
|
sectionControllers = new Map();
|
|
146
177
|
sectionControllerKeys = new Map();
|
|
147
178
|
sectionControllerInitEntries = new Map();
|
|
@@ -149,8 +180,8 @@ export class ToolkitCoordinator {
|
|
|
149
180
|
sectionPersistenceStrategies = new Map();
|
|
150
181
|
sectionControllerLifecycleListeners = new Set();
|
|
151
182
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
183
|
+
* Every active section-event subscription, indexed by listener
|
|
184
|
+
* identity. The listener follows the toolkit's active
|
|
154
185
|
* section cohort across transitions; this map is the registry the
|
|
155
186
|
* coordinator iterates on every cohort change to detach the listener
|
|
156
187
|
* from the outgoing controller, attach it to the new one, and replay
|
|
@@ -162,22 +193,26 @@ export class ToolkitCoordinator {
|
|
|
162
193
|
*/
|
|
163
194
|
activeSubscriptions = new Map();
|
|
164
195
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
196
|
+
* Map key of the section controller currently treated as the
|
|
197
|
+
* *active cohort*. Set by `getOrCreateSectionController` (both
|
|
167
198
|
* the create-new and resolve-existing paths) and cleared when the
|
|
168
|
-
* matching controller is disposed
|
|
169
|
-
*
|
|
199
|
+
* matching controller is disposed, and `null` while the next requested
|
|
200
|
+
* cohort is still starting.
|
|
170
201
|
*/
|
|
171
202
|
activeCohortMapKey = null;
|
|
172
203
|
/**
|
|
173
204
|
* Latest requested active cohort. Async controller initialization can
|
|
174
205
|
* resolve out of order; only the most recent request may migrate active
|
|
175
206
|
* subscriptions. Older completions still populate the controller cache.
|
|
207
|
+
* `null` until the first request, which is the only state where
|
|
208
|
+
* `subscribeSectionEvents` throws.
|
|
176
209
|
*/
|
|
177
210
|
latestRequestedActiveCohortMapKey = null;
|
|
178
211
|
telemetryListeners = new Set();
|
|
179
212
|
frameworkErrorBus;
|
|
180
213
|
ownsFrameworkErrorBus;
|
|
214
|
+
/** The instance behind {@link toolCoordinator}, kept for disposal. */
|
|
215
|
+
ownedToolCoordinator;
|
|
181
216
|
frameworkErrorHookUnsubscribe = null;
|
|
182
217
|
disposePromise = null;
|
|
183
218
|
/**
|
|
@@ -188,32 +223,12 @@ export class ToolkitCoordinator {
|
|
|
188
223
|
*/
|
|
189
224
|
policyEngine;
|
|
190
225
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* actually carry (see {@link resolveEffectivePnpEnforcement}).
|
|
195
|
-
* `"on"` / `"off"` are explicit host opt-in / opt-out and stick
|
|
196
|
-
* across subsequent assessment / item swaps until the host clears
|
|
197
|
-
* the override by calling `setPnpEnforcement(null)`.
|
|
198
|
-
*/
|
|
199
|
-
pnpEnforcementOverride = null;
|
|
200
|
-
/**
|
|
201
|
-
* Last assessment passed to {@link updateAssessment}. Read by
|
|
202
|
-
* {@link resolveEffectivePnpEnforcement} to compute auto-mode.
|
|
203
|
-
* The engine's own copy is the canonical record for decisions;
|
|
204
|
-
* this mirror exists only so the auto-mode helper does not need
|
|
205
|
-
* to round-trip through {@link policyEngine}'s frozen snapshot.
|
|
206
|
-
*/
|
|
207
|
-
boundAssessment = null;
|
|
208
|
-
/**
|
|
209
|
-
* Last item reference passed to {@link updateCurrentItemRef}.
|
|
210
|
-
* Mirrored alongside {@link boundAssessment} so
|
|
211
|
-
* {@link resolveEffectivePnpEnforcement} can detect item-level
|
|
212
|
-
* profile policy inputs (`requiredTools` / `restrictedTools` /
|
|
213
|
-
* `toolParameters`) without round-tripping through the engine's
|
|
214
|
-
* frozen snapshot.
|
|
226
|
+
* Policy diagnostics reported since the last input change, keyed by code,
|
|
227
|
+
* tool id and, for `tool-policy.itemSettingNotApplied`, item id; see
|
|
228
|
+
* {@link warnPolicyDiagnostics}.
|
|
215
229
|
*/
|
|
216
|
-
|
|
230
|
+
reportedPolicyDiagnostics = new Map();
|
|
231
|
+
policyDiagnosticListeners = new Set();
|
|
217
232
|
/**
|
|
218
233
|
* Whether {@link decideFeaturePolicy} has already reported serving a decision
|
|
219
234
|
* with no assessment bound.
|
|
@@ -227,7 +242,7 @@ export class ToolkitCoordinator {
|
|
|
227
242
|
reportedUnboundFeaturePolicy = false;
|
|
228
243
|
/**
|
|
229
244
|
* Whether {@link _initializeTTS} has already reported a non-browser backend
|
|
230
|
-
* falling back to browser speech because no
|
|
245
|
+
* falling back to browser speech because no text-to-speech provider is registered.
|
|
231
246
|
* Once per coordinator: a text-to-speech config change re-runs
|
|
232
247
|
* initialization, and the missing provider is the same gap each time.
|
|
233
248
|
*/
|
|
@@ -292,7 +307,7 @@ export class ToolkitCoordinator {
|
|
|
292
307
|
*/
|
|
293
308
|
validateToolsConfig(tools, options) {
|
|
294
309
|
const { config, diagnostics } = collectToolConfigDiagnostics(tools, options.toolRegistry);
|
|
295
|
-
const unreported = this.reportedRegistryUnavailable ||
|
|
310
|
+
const unreported = this.reportedRegistryUnavailable || this.toolRegistryPending()
|
|
296
311
|
? diagnostics.filter((entry) => entry.code !== "tools.registryUnavailable")
|
|
297
312
|
: diagnostics;
|
|
298
313
|
reportToolConfigDiagnostics(unreported, options);
|
|
@@ -313,9 +328,11 @@ export class ToolkitCoordinator {
|
|
|
313
328
|
this.config = resolvedConfig;
|
|
314
329
|
this.toolRegistry = resolvedConfig.toolRegistry ?? new ToolRegistry();
|
|
315
330
|
this.installToolContextResolvers(resolvedConfig.toolContextResolvers);
|
|
316
|
-
|
|
331
|
+
// The coordinator's own copy, so `setHooks` leaves the host's object as it
|
|
332
|
+
// passed it.
|
|
333
|
+
this.hooks = { ...resolvedConfig.hooks };
|
|
317
334
|
this.lazyInit = config.lazyInit === true;
|
|
318
|
-
this.
|
|
335
|
+
this.eagerInit = config.eagerInit ?? !this.lazyInit;
|
|
319
336
|
// Use the host-provided framework-error bus if one was passed
|
|
320
337
|
// (typical when embedded inside <pie-assessment-toolkit>, so
|
|
321
338
|
// pre-coordinator failures from the CE flow through the same
|
|
@@ -324,44 +341,54 @@ export class ToolkitCoordinator {
|
|
|
324
341
|
config.frameworkErrorBus ?? new FrameworkErrorBus();
|
|
325
342
|
this.ownsFrameworkErrorBus = !config.frameworkErrorBus;
|
|
326
343
|
this.subscribeFrameworkErrorHookAdapters();
|
|
327
|
-
|
|
328
|
-
this.toolCoordinator =
|
|
344
|
+
this.ownedToolCoordinator = new ToolCoordinator();
|
|
345
|
+
this.toolCoordinator = this.ownedToolCoordinator;
|
|
329
346
|
this.highlightCoordinator = new HighlightCoordinator();
|
|
330
347
|
this.elementToolStateStore = new ElementToolStateStore();
|
|
331
348
|
this.catalogResolver = new AccessibilityCatalogResolver(resolvedConfig.accessibility?.catalogs || [], resolvedConfig.accessibility?.language || "en-US");
|
|
332
|
-
// Initialize tool provider registry
|
|
333
349
|
this.toolProviderRegistry = new ToolProviderRegistry();
|
|
334
350
|
this._registerToolProviders();
|
|
335
|
-
// Initialize TTS service based on config
|
|
336
351
|
this.ttsService = new TTSService();
|
|
337
352
|
// Selection read-aloud speaks through this service without the inline TTS
|
|
338
353
|
// tool ever having run, so it cannot rely on that tool to attach highlights.
|
|
339
354
|
this.ttsService.setHighlightCoordinator(this.highlightCoordinator);
|
|
355
|
+
// A caller that speaks without `ensureTTSReady()` first, such as selection
|
|
356
|
+
// read-aloud, starts the service instead of finding it uninitialized.
|
|
357
|
+
this.ttsService.setReadinessGate(() => this.ensureTTSReady());
|
|
358
|
+
this.ttsService.setMathSpeechSource(() => buildRuntimeTTSConfig(resolveTTSRuntimeSettings(this.resolveTTSToolConfig())).providerOptions?.mathSpeech);
|
|
340
359
|
this.setupStatePersistenceHooks();
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// actual profile material. Hosts that only consume the
|
|
347
|
-
// engine for placement/policy gating get the pre-PR-2 behavior
|
|
348
|
-
// bit-for-bit.
|
|
360
|
+
// The unified ToolPolicyEngine, seeded with the validated tools config.
|
|
361
|
+
// The assessment starts `null` and items register their settings as they
|
|
362
|
+
// mount; auto-mode enforcement turns on only once the policy material a
|
|
363
|
+
// decision reads is present, so a host that consumes the engine for
|
|
364
|
+
// placement/policy gating alone sees no PNP/profile gate.
|
|
349
365
|
this.policyEngine = new ToolPolicyEngine({
|
|
350
366
|
toolRegistry: this.toolRegistry,
|
|
351
367
|
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
368
|
+
assessmentOptional: this.config.assessmentOptional === true,
|
|
352
369
|
inputs: {
|
|
353
370
|
tools: this.config.tools,
|
|
354
371
|
assessment: null,
|
|
355
|
-
|
|
356
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
372
|
+
pnpEnforcement: this.resolveConfiguredPnpEnforcement(),
|
|
357
373
|
},
|
|
358
374
|
});
|
|
359
|
-
|
|
375
|
+
// Input diagnostics are computed once per change, so they are reported
|
|
376
|
+
// here rather than with each decision.
|
|
377
|
+
this.policyEngine.onPolicyChange((event) => {
|
|
378
|
+
if (event.reason === "disposed")
|
|
379
|
+
return;
|
|
380
|
+
if (event.reason === "inputs" || event.reason === "pnp-enforcement") {
|
|
381
|
+
this.reportedPolicyDiagnostics.clear();
|
|
382
|
+
}
|
|
383
|
+
this.warnPolicyDiagnostics(event.inputs.diagnostics);
|
|
384
|
+
this.reportNewlyGrantedFailures();
|
|
385
|
+
});
|
|
386
|
+
if (this.eagerInit) {
|
|
360
387
|
void this.waitUntilReady().catch((err) => {
|
|
361
388
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
362
389
|
return;
|
|
363
390
|
console.error("[ToolkitCoordinator] Failed eager initialization:", err);
|
|
364
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
391
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
365
392
|
});
|
|
366
393
|
}
|
|
367
394
|
}
|
|
@@ -445,13 +472,108 @@ export class ToolkitCoordinator {
|
|
|
445
472
|
}
|
|
446
473
|
}
|
|
447
474
|
}
|
|
448
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Every report site states whether the coordinator carries on after the
|
|
477
|
+
* failure; one that takes down only its section passes that section.
|
|
478
|
+
*/
|
|
479
|
+
handleError(error, context, cohort) {
|
|
449
480
|
const model = frameworkErrorFromCoordinatorContext({
|
|
450
481
|
error,
|
|
451
482
|
context,
|
|
483
|
+
recoverable: context.recoverable,
|
|
484
|
+
scope: cohort ? "cohort" : "runtime",
|
|
485
|
+
cohort,
|
|
452
486
|
});
|
|
453
487
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
454
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Whether policy grants `toolId` as an accommodation on some surface: a
|
|
491
|
+
* test-administration override, a mounted item's or a district requirement,
|
|
492
|
+
* or profile support. A toolbar tool counts only surfaces where PNP
|
|
493
|
+
* enforcement is on; a region feature ignores enforcement, as its decisions
|
|
494
|
+
* do. Read without the unbound-assessment warning.
|
|
495
|
+
*/
|
|
496
|
+
isToolGranted(toolId) {
|
|
497
|
+
try {
|
|
498
|
+
return this.policyEngine.grantsFeatureAnywhere(toolId, {
|
|
499
|
+
enforced: this.toolRegistry.getToolActivation(toolId) !== "region",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Reports a tool's start failure: recoverable, so the tool reports itself
|
|
508
|
+
* unavailable and the assessment goes on, unless policy grants the tool.
|
|
509
|
+
* `fallbackFollows` marks a failure another path absorbs, which is recoverable
|
|
510
|
+
* whatever the policy.
|
|
511
|
+
*/
|
|
512
|
+
reportStartFailure(error, context, toolIds, fallbackFollows = false) {
|
|
513
|
+
const ids = Array.from(toolIds);
|
|
514
|
+
const recoverable = fallbackFollows || !ids.some((toolId) => this.isToolGranted(toolId));
|
|
515
|
+
if (recoverable && !fallbackFollows) {
|
|
516
|
+
for (const toolId of ids)
|
|
517
|
+
this.degradedTools.set(toolId, { error, context });
|
|
518
|
+
}
|
|
519
|
+
this.handleError(error, { ...context, recoverable });
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Report that a tool failed in `phase`. A module that failed to load follows
|
|
523
|
+
* the start-failure policy: the tool degrades, so toolbars withhold it, unless
|
|
524
|
+
* policy grants it. Every other phase is recoverable, since its report site
|
|
525
|
+
* keeps a recovery, and is reported on a microtask. Reported once per tool and
|
|
526
|
+
* phase however many toolbars or instances fail. A module failure re-announces the request targets, since
|
|
527
|
+
* the reporting toolbar no longer hosts the tool.
|
|
528
|
+
*/
|
|
529
|
+
reportToolFailure(toolId, phase, error) {
|
|
530
|
+
if (phase === "tool-module-load")
|
|
531
|
+
this.toolRequests.notifyTargetsChange();
|
|
532
|
+
const key = `${phase}\u0000${toolId}`;
|
|
533
|
+
if (this.reportedToolFailures.has(key))
|
|
534
|
+
return;
|
|
535
|
+
this.reportedToolFailures.add(key);
|
|
536
|
+
const detail = error instanceof Error && error.message.trim().length > 0
|
|
537
|
+
? error.message
|
|
538
|
+
: String(error);
|
|
539
|
+
const report = () => this.reportStartFailure(new Error(`Tool "${toolId}" ${TOOL_FAILURE_SUMMARY[phase]}: ${detail}`, {
|
|
540
|
+
cause: error,
|
|
541
|
+
}), { phase, details: { toolId } }, [toolId], phase !== "tool-module-load");
|
|
542
|
+
// The recoverable phases fail inside a toolbar's derived state, where a
|
|
543
|
+
// listener writing state would throw.
|
|
544
|
+
if (phase === "tool-module-load")
|
|
545
|
+
report();
|
|
546
|
+
else
|
|
547
|
+
queueMicrotask(report);
|
|
548
|
+
}
|
|
549
|
+
reportNewlyGrantedFailures() {
|
|
550
|
+
for (const [toolId, failure] of this.degradedTools) {
|
|
551
|
+
if (!this.isToolGranted(toolId))
|
|
552
|
+
continue;
|
|
553
|
+
this.degradedTools.delete(toolId);
|
|
554
|
+
this.handleError(failure.error, { ...failure.context, recoverable: false });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Subscribe to changes of {@link isReady}: state loading, text-to-speech
|
|
559
|
+
* starting, failing or reconfiguring. The listener reads the status itself.
|
|
560
|
+
*/
|
|
561
|
+
onReadyChange(listener) {
|
|
562
|
+
this.readyChangeListeners.add(listener);
|
|
563
|
+
return () => {
|
|
564
|
+
this.readyChangeListeners.delete(listener);
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
notifyReadyChange() {
|
|
568
|
+
for (const listener of this.readyChangeListeners) {
|
|
569
|
+
try {
|
|
570
|
+
listener();
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.warn("[ToolkitCoordinator] ready-change listener failed:", error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
455
577
|
/**
|
|
456
578
|
* Subscribe to framework-error events emitted by this coordinator.
|
|
457
579
|
*
|
|
@@ -481,6 +603,13 @@ export class ToolkitCoordinator {
|
|
|
481
603
|
reportFrameworkError(model) {
|
|
482
604
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
483
605
|
}
|
|
606
|
+
sectionControllerKey(args) {
|
|
607
|
+
return {
|
|
608
|
+
assessmentId: this.assessmentId,
|
|
609
|
+
sectionId: args.sectionId,
|
|
610
|
+
attemptId: args.attemptId,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
484
613
|
getSectionControllerMapKey(key) {
|
|
485
614
|
return `${key.assessmentId}::${key.sectionId}::${key.attemptId || ""}`;
|
|
486
615
|
}
|
|
@@ -495,15 +624,20 @@ export class ToolkitCoordinator {
|
|
|
495
624
|
return null;
|
|
496
625
|
}
|
|
497
626
|
})();
|
|
627
|
+
// Stored per attempt. Without an attempt id nothing tells two learners on
|
|
628
|
+
// one device apart, so the section is neither read nor written.
|
|
498
629
|
const getStorageKey = (context) => {
|
|
499
630
|
const { assessmentId, sectionId, attemptId } = context.key;
|
|
500
|
-
|
|
631
|
+
if (!attemptId)
|
|
632
|
+
return null;
|
|
633
|
+
return `pie:section-controller:v1:${assessmentId}:${sectionId}:${attemptId}`;
|
|
501
634
|
};
|
|
502
635
|
return {
|
|
503
636
|
async loadSession(context) {
|
|
504
|
-
|
|
637
|
+
const key = getStorageKey(context);
|
|
638
|
+
if (!storage || !key)
|
|
505
639
|
return null;
|
|
506
|
-
const value = storage.getItem(
|
|
640
|
+
const value = storage.getItem(key);
|
|
507
641
|
if (!value)
|
|
508
642
|
return null;
|
|
509
643
|
try {
|
|
@@ -514,14 +648,16 @@ export class ToolkitCoordinator {
|
|
|
514
648
|
}
|
|
515
649
|
},
|
|
516
650
|
async saveSession(context, session) {
|
|
517
|
-
|
|
651
|
+
const key = getStorageKey(context);
|
|
652
|
+
if (!storage || !key)
|
|
518
653
|
return;
|
|
519
|
-
storage.setItem(
|
|
654
|
+
storage.setItem(key, JSON.stringify(session));
|
|
520
655
|
},
|
|
521
656
|
async clearSession(context) {
|
|
522
|
-
|
|
657
|
+
const key = getStorageKey(context);
|
|
658
|
+
if (!storage || !key)
|
|
523
659
|
return;
|
|
524
|
-
storage.removeItem(
|
|
660
|
+
storage.removeItem(key);
|
|
525
661
|
},
|
|
526
662
|
};
|
|
527
663
|
}
|
|
@@ -543,12 +679,12 @@ export class ToolkitCoordinator {
|
|
|
543
679
|
if (!this.hooks.saveToolState)
|
|
544
680
|
return;
|
|
545
681
|
void Promise.resolve(this.hooks.saveToolState(state)).catch((err) => {
|
|
546
|
-
this.handleError(err, { phase: "state-save" });
|
|
682
|
+
this.handleError(err, { phase: "state-save", recoverable: true });
|
|
547
683
|
});
|
|
548
684
|
});
|
|
549
685
|
}
|
|
550
686
|
async ensureStateLoaded() {
|
|
551
|
-
if (this.
|
|
687
|
+
if (this.stateLoadSettled)
|
|
552
688
|
return;
|
|
553
689
|
if (this.stateLoadPromise)
|
|
554
690
|
return this.stateLoadPromise;
|
|
@@ -563,7 +699,7 @@ export class ToolkitCoordinator {
|
|
|
563
699
|
if (state && typeof state === "object") {
|
|
564
700
|
this.elementToolStateStore.loadState(state);
|
|
565
701
|
}
|
|
566
|
-
this.
|
|
702
|
+
this.stateLoadSettled = true;
|
|
567
703
|
await this.emitTelemetry("pie-toolkit-tool-state-loaded", {
|
|
568
704
|
hasState: Boolean(state),
|
|
569
705
|
});
|
|
@@ -571,43 +707,52 @@ export class ToolkitCoordinator {
|
|
|
571
707
|
catch (err) {
|
|
572
708
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
573
709
|
throw err;
|
|
574
|
-
this.
|
|
710
|
+
this.stateLoadSettled = true;
|
|
711
|
+
this.handleError(err, { phase: "state-load", recoverable: true });
|
|
575
712
|
}
|
|
576
713
|
})().finally(() => {
|
|
577
714
|
this.stateLoadPromise = undefined;
|
|
715
|
+
this.notifyReadyChange();
|
|
578
716
|
});
|
|
579
717
|
return this.stateLoadPromise;
|
|
580
718
|
}
|
|
719
|
+
/** Whether the toolkit this coordinator binds to may still supply a registry. */
|
|
720
|
+
toolRegistryPending() {
|
|
721
|
+
return !this.toolRegistrySettled && !this.toolRegistryAbsent;
|
|
722
|
+
}
|
|
581
723
|
reportMissingTTSProvider(backend) {
|
|
582
724
|
// The toolkit this coordinator binds to may still supply the provider.
|
|
583
|
-
if (
|
|
725
|
+
if (this.toolRegistryPending()) {
|
|
584
726
|
this.heldMissingTTSProviderBackend = backend;
|
|
585
727
|
return;
|
|
586
728
|
}
|
|
587
729
|
if (this.reportedMissingTTSProvider)
|
|
588
730
|
return;
|
|
589
731
|
this.reportedMissingTTSProvider = true;
|
|
590
|
-
console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "
|
|
732
|
+
console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "textToSpeech" tool provider is registered. A coordinator registers tool providers only from its \`toolRegistry\`, or from its toolkit's when constructed without one, so supply one that carries the text-to-speech registration — for the packaged capability set, \`createPackagedToolRegistry()\` from "@pie-players/pie-default-tool-loaders". Reported once per coordinator.`);
|
|
591
733
|
}
|
|
592
734
|
/**
|
|
593
735
|
* Take the registry of the toolkit this coordinator is bound to, when the host
|
|
594
736
|
* constructed it without one: the config is validated against it, its
|
|
595
737
|
* providers register, and text-to-speech re-initializes through it if it
|
|
596
|
-
* already started. The toolkit elements call this on binding
|
|
597
|
-
* passed at construction stands, and the
|
|
598
|
-
* toolkit without one, which
|
|
738
|
+
* already started. The toolkit elements call this on binding, and again when
|
|
739
|
+
* their registry changes. A registry passed at construction stands, and the
|
|
740
|
+
* first registry adopted wins; `null` records a toolkit without one, which
|
|
741
|
+
* reports the registry as missing and still adopts one that arrives later.
|
|
599
742
|
*
|
|
600
743
|
* @returns Whether `registry` was adopted.
|
|
601
744
|
*/
|
|
602
745
|
adoptToolRegistry(registry) {
|
|
603
746
|
if (this.disposePromise !== null || this.toolRegistrySettled)
|
|
604
747
|
return false;
|
|
605
|
-
this.
|
|
748
|
+
if (!registry && this.toolRegistryAbsent)
|
|
749
|
+
return false;
|
|
606
750
|
const heldBackend = this.heldMissingTTSProviderBackend;
|
|
607
751
|
this.heldMissingTTSProviderBackend = null;
|
|
608
752
|
const strictness = this.config.toolConfigStrictness ?? "error";
|
|
609
753
|
const source = "ToolkitCoordinator.adoptToolRegistry";
|
|
610
754
|
if (!registry) {
|
|
755
|
+
this.toolRegistryAbsent = true;
|
|
611
756
|
this.validateToolsConfig(this.config.tools, {
|
|
612
757
|
strictness,
|
|
613
758
|
source,
|
|
@@ -617,6 +762,7 @@ export class ToolkitCoordinator {
|
|
|
617
762
|
this.reportMissingTTSProvider(heldBackend);
|
|
618
763
|
return false;
|
|
619
764
|
}
|
|
765
|
+
this.toolRegistrySettled = true;
|
|
620
766
|
this.toolRegistry = registry;
|
|
621
767
|
this.config.toolRegistry = registry;
|
|
622
768
|
try {
|
|
@@ -644,9 +790,6 @@ export class ToolkitCoordinator {
|
|
|
644
790
|
this.scheduleTTSReconfigure();
|
|
645
791
|
return true;
|
|
646
792
|
}
|
|
647
|
-
/**
|
|
648
|
-
* Register tool providers in the registry
|
|
649
|
-
*/
|
|
650
793
|
_registerToolProviders() {
|
|
651
794
|
const descriptorTools = this.getProviderDescriptorTools();
|
|
652
795
|
for (const tool of descriptorTools) {
|
|
@@ -658,35 +801,32 @@ export class ToolkitCoordinator {
|
|
|
658
801
|
}
|
|
659
802
|
/**
|
|
660
803
|
* Register the provider `tool`'s descriptor creates for the tool's current
|
|
661
|
-
* config, unless the tool is disabled or
|
|
662
|
-
*
|
|
804
|
+
* config under the tool's id, unless the tool is disabled or, without
|
|
805
|
+
* `replace`, a provider is registered under that id. A descriptor that throws
|
|
806
|
+
* is reported the way a failed registration is.
|
|
663
807
|
*/
|
|
664
|
-
async registerProviderFromTool(tool) {
|
|
808
|
+
async registerProviderFromTool(tool, replace = false) {
|
|
665
809
|
const descriptor = tool.provider;
|
|
666
810
|
if (!descriptor)
|
|
667
811
|
return;
|
|
668
|
-
|
|
812
|
+
const { toolId } = tool;
|
|
669
813
|
let registration;
|
|
670
814
|
try {
|
|
671
|
-
const toolConfig = this.getToolConfig(
|
|
672
|
-
if (toolConfig?.enabled === false)
|
|
815
|
+
const toolConfig = this.getToolConfig(toolId) || undefined;
|
|
816
|
+
if (toolConfig?.enabled === false) {
|
|
817
|
+
// A disabled tool keeps no provider, including the one it replaces.
|
|
818
|
+
if (replace)
|
|
819
|
+
void this.toolProviderRegistry.unregister(toolId);
|
|
673
820
|
return;
|
|
674
|
-
|
|
675
|
-
if (!
|
|
821
|
+
}
|
|
822
|
+
if (!replace && this.toolProviderRegistry.has(toolId))
|
|
676
823
|
return;
|
|
677
824
|
const provider = descriptor.createProvider(toolConfig);
|
|
678
825
|
const initConfig = descriptor.getInitConfig?.(toolConfig) ??
|
|
679
826
|
toolConfig?.provider?.init ??
|
|
680
827
|
{};
|
|
681
|
-
const initConfigWithTelemetry = this.addToolTelemetryReporter(
|
|
682
|
-
|
|
683
|
-
providerId,
|
|
684
|
-
initConfig,
|
|
685
|
-
});
|
|
686
|
-
const registryTelemetry = this.createToolTelemetryForwarder({
|
|
687
|
-
toolId: tool.toolId,
|
|
688
|
-
providerId,
|
|
689
|
-
});
|
|
828
|
+
const initConfigWithTelemetry = this.addToolTelemetryReporter(toolId, initConfig);
|
|
829
|
+
const registryTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
690
830
|
const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
|
|
691
831
|
toolConfig?.provider?.runtime?.authFetcher;
|
|
692
832
|
registration = {
|
|
@@ -698,31 +838,24 @@ export class ToolkitCoordinator {
|
|
|
698
838
|
};
|
|
699
839
|
}
|
|
700
840
|
catch (err) {
|
|
701
|
-
this.reportProviderRegisterFailure(err,
|
|
841
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
702
842
|
return;
|
|
703
843
|
}
|
|
704
|
-
await this.registerProvider(
|
|
844
|
+
await this.registerProvider(toolId, registration);
|
|
705
845
|
}
|
|
706
|
-
createToolTelemetryForwarder(
|
|
846
|
+
createToolTelemetryForwarder(toolId) {
|
|
707
847
|
return async (eventName, payload) => {
|
|
708
|
-
await this.emitTelemetry(eventName, {
|
|
709
|
-
...(payload || {}),
|
|
710
|
-
toolId: args.toolId,
|
|
711
|
-
providerId: args.providerId,
|
|
712
|
-
});
|
|
848
|
+
await this.emitTelemetry(eventName, { ...(payload || {}), toolId });
|
|
713
849
|
};
|
|
714
850
|
}
|
|
715
|
-
addToolTelemetryReporter(
|
|
716
|
-
const configObject =
|
|
717
|
-
? { ...
|
|
851
|
+
addToolTelemetryReporter(toolId, initConfig) {
|
|
852
|
+
const configObject = initConfig && typeof initConfig === "object"
|
|
853
|
+
? { ...initConfig }
|
|
718
854
|
: {};
|
|
719
855
|
const existingReporter = typeof configObject.onTelemetry === "function"
|
|
720
856
|
? configObject.onTelemetry
|
|
721
857
|
: null;
|
|
722
|
-
const forwardTelemetry = this.createToolTelemetryForwarder(
|
|
723
|
-
toolId: args.toolId,
|
|
724
|
-
providerId: args.providerId,
|
|
725
|
-
});
|
|
858
|
+
const forwardTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
726
859
|
configObject.onTelemetry = async (eventName, payload) => {
|
|
727
860
|
if (existingReporter) {
|
|
728
861
|
await existingReporter(eventName, payload);
|
|
@@ -731,56 +864,58 @@ export class ToolkitCoordinator {
|
|
|
731
864
|
};
|
|
732
865
|
return configObject;
|
|
733
866
|
}
|
|
734
|
-
async registerProvider(
|
|
867
|
+
async registerProvider(toolId, config) {
|
|
735
868
|
try {
|
|
736
|
-
this.toolProviderRegistry.register(
|
|
869
|
+
this.toolProviderRegistry.register(toolId, config);
|
|
737
870
|
const meta = {
|
|
738
|
-
providerId,
|
|
739
871
|
providerName: config.provider.providerName,
|
|
740
872
|
};
|
|
741
|
-
await this.hooks.onProviderRegistered?.(
|
|
873
|
+
await this.hooks.onProviderRegistered?.(toolId, meta);
|
|
742
874
|
await this.emitTelemetry("pie-toolkit-provider-registered", {
|
|
743
|
-
|
|
875
|
+
toolId,
|
|
744
876
|
providerName: config.provider.providerName,
|
|
745
877
|
});
|
|
746
878
|
}
|
|
747
879
|
catch (err) {
|
|
748
|
-
this.reportProviderRegisterFailure(err,
|
|
880
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
749
881
|
}
|
|
750
882
|
}
|
|
751
883
|
/**
|
|
752
|
-
* A console warning and a `provider-register` framework error
|
|
753
|
-
*
|
|
884
|
+
* A console warning and a `provider-register` framework error, under the
|
|
885
|
+
* start-failure policy every other tool failure follows.
|
|
754
886
|
*/
|
|
755
|
-
reportProviderRegisterFailure(err,
|
|
756
|
-
console.warn(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
providerId: providerId ?? undefined,
|
|
762
|
-
});
|
|
887
|
+
reportProviderRegisterFailure(err, toolId) {
|
|
888
|
+
console.warn(`[ToolkitCoordinator] Failed to register the provider of tool "${toolId}":`, err);
|
|
889
|
+
this.reportStartFailure(err, { phase: "provider-register", toolId }, [toolId]);
|
|
890
|
+
}
|
|
891
|
+
async ensureProviderReady(toolId) {
|
|
892
|
+
return this.initializeProvider(toolId, false);
|
|
763
893
|
}
|
|
764
|
-
|
|
894
|
+
/** `fallbackFollows`: the caller recovers from a failure on its own. */
|
|
895
|
+
async initializeProvider(toolId, fallbackFollows) {
|
|
765
896
|
this.assertNotDisposed();
|
|
766
|
-
const existing = this.providerInitPromises.get(
|
|
897
|
+
const existing = this.providerInitPromises.get(toolId);
|
|
767
898
|
if (existing)
|
|
768
899
|
return existing;
|
|
769
900
|
const promise = (async () => {
|
|
770
|
-
const provider = await this.toolProviderRegistry.getProvider(providerId, false);
|
|
771
|
-
this.assertNotDisposed();
|
|
772
|
-
const meta = {
|
|
773
|
-
providerId,
|
|
774
|
-
providerName: provider.providerName,
|
|
775
|
-
};
|
|
776
901
|
try {
|
|
777
|
-
await this.
|
|
902
|
+
let provider = await this.toolProviderRegistry.getProvider(toolId, false);
|
|
903
|
+
this.assertNotDisposed();
|
|
904
|
+
// A tool asks each time it opens; the lifecycle hooks report the start once.
|
|
905
|
+
if (this.toolProviderRegistry.isInitialized(toolId))
|
|
906
|
+
return provider;
|
|
907
|
+
const meta = {
|
|
908
|
+
providerName: provider.providerName,
|
|
909
|
+
};
|
|
910
|
+
await this.hooks.onProviderInitStart?.(toolId, meta);
|
|
778
911
|
this.assertNotDisposed();
|
|
779
|
-
await this.toolProviderRegistry.initialize(
|
|
912
|
+
await this.toolProviderRegistry.initialize(toolId);
|
|
780
913
|
this.assertNotDisposed();
|
|
781
|
-
|
|
914
|
+
// A config update may have replaced the provider during its start.
|
|
915
|
+
provider = await this.toolProviderRegistry.getProvider(toolId, false);
|
|
916
|
+
await this.hooks.onProviderReady?.(toolId, meta);
|
|
782
917
|
this.assertNotDisposed();
|
|
783
|
-
await this.emitTelemetry("pie-toolkit-provider-ready", {
|
|
918
|
+
await this.emitTelemetry("pie-toolkit-provider-ready", { toolId });
|
|
784
919
|
this.assertNotDisposed();
|
|
785
920
|
return provider;
|
|
786
921
|
}
|
|
@@ -788,13 +923,16 @@ export class ToolkitCoordinator {
|
|
|
788
923
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
789
924
|
throw err;
|
|
790
925
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
791
|
-
this.
|
|
926
|
+
this.reportStartFailure(error, { phase: "provider-init", toolId },
|
|
927
|
+
// Attributed when the id names a tool with a provider; any other id
|
|
928
|
+
// was never a tool's.
|
|
929
|
+
this.toolRegistry.get(toolId)?.provider ? [toolId] : [], fallbackFollows);
|
|
792
930
|
throw error;
|
|
793
931
|
}
|
|
794
932
|
})().finally(() => {
|
|
795
|
-
this.providerInitPromises.delete(
|
|
933
|
+
this.providerInitPromises.delete(toolId);
|
|
796
934
|
});
|
|
797
|
-
this.providerInitPromises.set(
|
|
935
|
+
this.providerInitPromises.set(toolId, promise);
|
|
798
936
|
return promise;
|
|
799
937
|
}
|
|
800
938
|
setHooks(hooks) {
|
|
@@ -804,7 +942,7 @@ export class ToolkitCoordinator {
|
|
|
804
942
|
hooks.onCoordinatorReady &&
|
|
805
943
|
this.isReady()) {
|
|
806
944
|
void Promise.resolve(hooks.onCoordinatorReady(this)).catch((err) => {
|
|
807
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
945
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
808
946
|
});
|
|
809
947
|
}
|
|
810
948
|
}
|
|
@@ -815,20 +953,17 @@ export class ToolkitCoordinator {
|
|
|
815
953
|
};
|
|
816
954
|
}
|
|
817
955
|
getSectionController(args) {
|
|
818
|
-
const key =
|
|
819
|
-
assessmentId: this.assessmentId,
|
|
820
|
-
sectionId: args.sectionId,
|
|
821
|
-
attemptId: args.attemptId,
|
|
822
|
-
};
|
|
956
|
+
const key = this.sectionControllerKey(args);
|
|
823
957
|
return this.sectionControllers.get(this.getSectionControllerMapKey(key));
|
|
824
958
|
}
|
|
825
959
|
subscribeSectionEvents(args) {
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
if (this.activeCohortMapKey === null
|
|
960
|
+
// The listener binds to whichever controller is currently active and
|
|
961
|
+
// re-binds on every `getOrCreateSectionController` /
|
|
962
|
+
// `disposeSectionController` transition, with snapshot replay on each
|
|
963
|
+
// migration. A listener added while the next section is still
|
|
964
|
+
// starting binds when it becomes active.
|
|
965
|
+
if (this.activeCohortMapKey === null &&
|
|
966
|
+
this.latestRequestedActiveCohortMapKey === null) {
|
|
832
967
|
throw new Error("[ToolkitCoordinator] subscribeSectionEvents requires an active section cohort; call getOrCreateSectionController first.");
|
|
833
968
|
}
|
|
834
969
|
const previous = this.activeSubscriptions.get(args.listener);
|
|
@@ -843,7 +978,9 @@ export class ToolkitCoordinator {
|
|
|
843
978
|
unsubscribeCurrent: null,
|
|
844
979
|
};
|
|
845
980
|
this.activeSubscriptions.set(args.listener, sub);
|
|
846
|
-
const controller = this.
|
|
981
|
+
const controller = this.activeCohortMapKey === null
|
|
982
|
+
? undefined
|
|
983
|
+
: this.sectionControllers.get(this.activeCohortMapKey);
|
|
847
984
|
if (controller) {
|
|
848
985
|
this.bindSubscriptionToController(sub, controller);
|
|
849
986
|
}
|
|
@@ -863,7 +1000,7 @@ export class ToolkitCoordinator {
|
|
|
863
1000
|
};
|
|
864
1001
|
}
|
|
865
1002
|
/**
|
|
866
|
-
* Bind
|
|
1003
|
+
* Bind an active-cohort subscription to a section controller. Detaches any
|
|
867
1004
|
* prior controller binding the subscription was holding, attaches a
|
|
868
1005
|
* fresh `controller.subscribe(...)` callback, and replays the
|
|
869
1006
|
* canonical late-subscribe sequence (content-loaded × N then
|
|
@@ -922,15 +1059,9 @@ export class ToolkitCoordinator {
|
|
|
922
1059
|
const controller = this.sectionControllers.get(mapKey);
|
|
923
1060
|
if (!controller)
|
|
924
1061
|
return;
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
// `activeSubscriptions`. Map iteration yields keys inserted
|
|
929
|
-
// during the loop; without snapshotting, the inner subscribe path
|
|
930
|
-
// would bind the new listener once (correctly, with replay) and
|
|
931
|
-
// the outer loop would then visit it a second time, double-
|
|
932
|
-
// replaying the snapshot. Snapshot keeps "replay once per cohort
|
|
933
|
-
// transition" intact.
|
|
1062
|
+
// Iterate a snapshot: a listener may call `subscribeSectionEvents` from
|
|
1063
|
+
// inside its replay delivery, and live Map iteration would then visit the
|
|
1064
|
+
// new entry and replay it a second time.
|
|
934
1065
|
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
935
1066
|
this.bindSubscriptionToController(sub, controller);
|
|
936
1067
|
}
|
|
@@ -946,10 +1077,7 @@ export class ToolkitCoordinator {
|
|
|
946
1077
|
if (this.activeCohortMapKey === mapKey)
|
|
947
1078
|
return;
|
|
948
1079
|
this.activeCohortMapKey = null;
|
|
949
|
-
|
|
950
|
-
sub.unsubscribeCurrent?.();
|
|
951
|
-
sub.unsubscribeCurrent = null;
|
|
952
|
-
}
|
|
1080
|
+
this.detachActiveSubscriptions();
|
|
953
1081
|
}
|
|
954
1082
|
/**
|
|
955
1083
|
* Clear the active cohort if `mapKey` matches it, detaching every
|
|
@@ -961,10 +1089,14 @@ export class ToolkitCoordinator {
|
|
|
961
1089
|
if (this.activeCohortMapKey !== mapKey)
|
|
962
1090
|
return;
|
|
963
1091
|
this.activeCohortMapKey = null;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1092
|
+
this.detachActiveSubscriptions();
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Detach every active subscription from its controller. Iterates a snapshot,
|
|
1096
|
+
* as `setActiveCohort` does, because a disposer may mutate
|
|
1097
|
+
* `activeSubscriptions`.
|
|
1098
|
+
*/
|
|
1099
|
+
detachActiveSubscriptions() {
|
|
968
1100
|
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
969
1101
|
sub.unsubscribeCurrent?.();
|
|
970
1102
|
sub.unsubscribeCurrent = null;
|
|
@@ -974,20 +1106,14 @@ export class ToolkitCoordinator {
|
|
|
974
1106
|
const eventTypeFilter = args.eventTypes ? new Set(args.eventTypes) : null;
|
|
975
1107
|
const itemIdFilter = args.itemIds ? new Set(args.itemIds) : null;
|
|
976
1108
|
return (event) => {
|
|
977
|
-
if (eventTypeFilter
|
|
978
|
-
const eventType = event?.type
|
|
979
|
-
if (eventTypeFilter
|
|
980
|
-
(!eventType || !eventTypeFilter.has(eventType))) {
|
|
1109
|
+
if (eventTypeFilter) {
|
|
1110
|
+
const eventType = event?.type;
|
|
1111
|
+
if (!eventType || !eventTypeFilter.has(eventType))
|
|
981
1112
|
return false;
|
|
982
|
-
}
|
|
983
|
-
if (itemIdFilter) {
|
|
984
|
-
const hasMatchingItem = Array.from(this.collectEventItemIds(event)).some((itemId) => itemIdFilter.has(itemId));
|
|
985
|
-
if (!hasMatchingItem) {
|
|
986
|
-
return false;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
1113
|
}
|
|
990
|
-
|
|
1114
|
+
if (!itemIdFilter)
|
|
1115
|
+
return true;
|
|
1116
|
+
return Array.from(this.collectEventItemIds(event)).some((itemId) => itemIdFilter.has(itemId));
|
|
991
1117
|
};
|
|
992
1118
|
}
|
|
993
1119
|
collectEventItemIds(event) {
|
|
@@ -1113,26 +1239,33 @@ export class ToolkitCoordinator {
|
|
|
1113
1239
|
if (this.disposePromise !== null) {
|
|
1114
1240
|
throw new ToolkitCoordinatorDisposedError();
|
|
1115
1241
|
}
|
|
1116
|
-
const key =
|
|
1117
|
-
assessmentId: this.assessmentId,
|
|
1118
|
-
sectionId: args.sectionId,
|
|
1119
|
-
attemptId: args.attemptId,
|
|
1120
|
-
};
|
|
1242
|
+
const key = this.sectionControllerKey(args);
|
|
1121
1243
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
1122
1244
|
this.latestRequestedActiveCohortMapKey = mapKey;
|
|
1123
1245
|
this.suspendActiveCohortIfSuperseded(mapKey);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1246
|
+
let existingController;
|
|
1247
|
+
try {
|
|
1248
|
+
existingController = await this.resolveExistingSectionController({
|
|
1249
|
+
mapKey,
|
|
1250
|
+
key,
|
|
1251
|
+
input: args.input,
|
|
1252
|
+
updateExisting: args.updateExisting,
|
|
1253
|
+
});
|
|
1254
|
+
if (existingController) {
|
|
1255
|
+
if (this.disposePromise !== null) {
|
|
1256
|
+
throw new ToolkitCoordinatorDisposedError();
|
|
1257
|
+
}
|
|
1258
|
+
if (args.initialSession) {
|
|
1259
|
+
await this.assignSessionToPublishedController(existingController, args.initialSession);
|
|
1260
|
+
}
|
|
1133
1261
|
}
|
|
1134
|
-
return existingController;
|
|
1135
1262
|
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1265
|
+
throw err;
|
|
1266
|
+
}
|
|
1267
|
+
if (existingController)
|
|
1268
|
+
return existingController;
|
|
1136
1269
|
const pendingDisposal = this.sectionControllerDisposePromises.get(mapKey);
|
|
1137
1270
|
if (pendingDisposal) {
|
|
1138
1271
|
// Persistence and hydration share the cohort's durable state. Keep the
|
|
@@ -1145,8 +1278,13 @@ export class ToolkitCoordinator {
|
|
|
1145
1278
|
throw new ToolkitCoordinatorDisposedError();
|
|
1146
1279
|
}
|
|
1147
1280
|
const existingEntry = this.sectionControllerInitEntries.get(mapKey);
|
|
1148
|
-
if (existingEntry)
|
|
1149
|
-
|
|
1281
|
+
if (existingEntry) {
|
|
1282
|
+
if (!args.initialSession)
|
|
1283
|
+
return existingEntry.promise;
|
|
1284
|
+
const pendingController = await existingEntry.promise;
|
|
1285
|
+
await this.assignSessionToPublishedController(pendingController, args.initialSession);
|
|
1286
|
+
return pendingController;
|
|
1287
|
+
}
|
|
1150
1288
|
const token = {
|
|
1151
1289
|
retired: false,
|
|
1152
1290
|
candidateClaimed: false,
|
|
@@ -1159,6 +1297,7 @@ export class ToolkitCoordinator {
|
|
|
1159
1297
|
})
|
|
1160
1298
|
.catch((err) => {
|
|
1161
1299
|
this.handleSectionControllerInitError(err, args);
|
|
1300
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1162
1301
|
throw err;
|
|
1163
1302
|
})
|
|
1164
1303
|
.finally(() => {
|
|
@@ -1189,8 +1328,7 @@ export class ToolkitCoordinator {
|
|
|
1189
1328
|
if (this.sectionControllers.get(args.mapKey) !== existingController) {
|
|
1190
1329
|
return undefined;
|
|
1191
1330
|
}
|
|
1192
|
-
//
|
|
1193
|
-
// resolves to a previously-created controller still represents a
|
|
1331
|
+
// A `getOrCreateSectionController` call that resolves to a previously-created controller still represents a
|
|
1194
1332
|
// cohort transition from the toolkit's perspective (same-cohort
|
|
1195
1333
|
// re-entry is a no-op inside `setActiveCohort`). Active
|
|
1196
1334
|
// subscriptions migrate here so a host that subscribed once on
|
|
@@ -1239,7 +1377,20 @@ export class ToolkitCoordinator {
|
|
|
1239
1377
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1240
1378
|
await controller.initialize?.(args.args.input);
|
|
1241
1379
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1242
|
-
|
|
1380
|
+
// A session the host supplied takes the place of the strategy's stored
|
|
1381
|
+
// snapshot, and is applied before publication so the ready lifecycle
|
|
1382
|
+
// event and every reader after it see it. The strategy still receives
|
|
1383
|
+
// every `persist()`.
|
|
1384
|
+
const initialSession = args.args.initialSession;
|
|
1385
|
+
if (initialSession) {
|
|
1386
|
+
if (!controller.applySession) {
|
|
1387
|
+
throw new Error("Section controller cannot apply the session it was created with: it has no applySession.");
|
|
1388
|
+
}
|
|
1389
|
+
await controller.applySession(initialSession, { mode: "replace" });
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
await controller.hydrate?.();
|
|
1393
|
+
}
|
|
1243
1394
|
await this.finalizeSectionControllerReady({
|
|
1244
1395
|
...candidate,
|
|
1245
1396
|
context,
|
|
@@ -1251,6 +1402,22 @@ export class ToolkitCoordinator {
|
|
|
1251
1402
|
throw error;
|
|
1252
1403
|
}
|
|
1253
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Apply a host-supplied session to a controller that is already published,
|
|
1407
|
+
* under the rules `resolveSectionSessionAssignment` sets: an equal session is a
|
|
1408
|
+
* no-op and recorded responses are not replaced by response-free ones.
|
|
1409
|
+
*/
|
|
1410
|
+
async assignSessionToPublishedController(controller, session) {
|
|
1411
|
+
if (!session)
|
|
1412
|
+
return;
|
|
1413
|
+
if (!controller.applySession) {
|
|
1414
|
+
throw new Error("Section controller cannot apply the assigned session: it has no applySession.");
|
|
1415
|
+
}
|
|
1416
|
+
const resolved = resolveSectionSessionAssignment(controller.getSession?.() ?? null, session);
|
|
1417
|
+
if (!resolved)
|
|
1418
|
+
return;
|
|
1419
|
+
await controller.applySession(resolved, { mode: "replace" });
|
|
1420
|
+
}
|
|
1254
1421
|
createSectionControllerRetirementError() {
|
|
1255
1422
|
return this.disposePromise !== null
|
|
1256
1423
|
? new ToolkitCoordinatorDisposedError()
|
|
@@ -1276,6 +1443,7 @@ export class ToolkitCoordinator {
|
|
|
1276
1443
|
sectionId: args.key.sectionId,
|
|
1277
1444
|
attemptId: args.key.attemptId,
|
|
1278
1445
|
},
|
|
1446
|
+
recoverable: true,
|
|
1279
1447
|
});
|
|
1280
1448
|
}
|
|
1281
1449
|
finally {
|
|
@@ -1300,8 +1468,7 @@ export class ToolkitCoordinator {
|
|
|
1300
1468
|
this.sectionControllers.set(args.mapKey, args.controller);
|
|
1301
1469
|
this.sectionControllerKeys.set(args.mapKey, args.key);
|
|
1302
1470
|
args.token.candidateClaimed = true;
|
|
1303
|
-
//
|
|
1304
|
-
// active cohort. Active subscriptions migrate to it before the
|
|
1471
|
+
// A freshly-resolved controller becomes the active cohort. Active subscriptions migrate to it before the
|
|
1305
1472
|
// `ready` lifecycle event and `onSectionControllerReady` hook
|
|
1306
1473
|
// fire so any synchronous post-ready work observes a coherent
|
|
1307
1474
|
// active-cohort view.
|
|
@@ -1331,14 +1498,36 @@ export class ToolkitCoordinator {
|
|
|
1331
1498
|
sectionId: args.sectionId,
|
|
1332
1499
|
attemptId: args.attemptId,
|
|
1333
1500
|
},
|
|
1334
|
-
|
|
1501
|
+
recoverable: false,
|
|
1502
|
+
}, { sectionId: args.sectionId, attemptId: args.attemptId });
|
|
1335
1503
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* A section that fails to start has no controller of its own to emit its
|
|
1506
|
+
* `section-error`, and the host's subscriptions are already off the outgoing
|
|
1507
|
+
* section, so the coordinator delivers the error to them while that section
|
|
1508
|
+
* is still the one requested.
|
|
1509
|
+
*/
|
|
1510
|
+
deliverSectionStartError(mapKey, error) {
|
|
1511
|
+
if (error instanceof ToolkitCoordinatorDisposedError ||
|
|
1512
|
+
error instanceof SectionControllerRetiredError ||
|
|
1513
|
+
this.latestRequestedActiveCohortMapKey !== mapKey) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const event = {
|
|
1517
|
+
type: "section-error",
|
|
1518
|
+
source: "section-runtime",
|
|
1519
|
+
error,
|
|
1520
|
+
currentItemIndex: 0,
|
|
1521
|
+
timestamp: Date.now(),
|
|
1341
1522
|
};
|
|
1523
|
+
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
1524
|
+
if (this.buildSectionEventPredicate(sub)(event)) {
|
|
1525
|
+
this.deliverSectionEventSafely(sub.listener, event);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
disposeSectionController(args) {
|
|
1530
|
+
const key = this.sectionControllerKey(args);
|
|
1342
1531
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
1343
1532
|
const initEntry = this.sectionControllerInitEntries.get(mapKey);
|
|
1344
1533
|
if (initEntry) {
|
|
@@ -1355,8 +1544,7 @@ export class ToolkitCoordinator {
|
|
|
1355
1544
|
return this.trackSectionControllerDisposal(mapKey, retirementBarrier);
|
|
1356
1545
|
}
|
|
1357
1546
|
const persistenceStrategy = this.sectionPersistenceStrategies.get(mapKey);
|
|
1358
|
-
//
|
|
1359
|
-
// detach all listener-controller bindings before the controller
|
|
1547
|
+
// If the disposing cohort is the active one, detach all listener-controller bindings before the controller
|
|
1360
1548
|
// itself disposes. The subscription registry stays intact so a
|
|
1361
1549
|
// later `getOrCreateSectionController(...)` re-binds the same
|
|
1362
1550
|
// listeners to the new controller.
|
|
@@ -1418,6 +1606,7 @@ export class ToolkitCoordinator {
|
|
|
1418
1606
|
sectionId: args.args.sectionId,
|
|
1419
1607
|
attemptId: args.args.attemptId,
|
|
1420
1608
|
},
|
|
1609
|
+
recoverable: true,
|
|
1421
1610
|
});
|
|
1422
1611
|
}
|
|
1423
1612
|
finally {
|
|
@@ -1517,17 +1706,16 @@ export class ToolkitCoordinator {
|
|
|
1517
1706
|
this.activeSubscriptions.clear();
|
|
1518
1707
|
this.activeCohortMapKey = null;
|
|
1519
1708
|
this.latestRequestedActiveCohortMapKey = null;
|
|
1520
|
-
await cleanup(() => this.ttsService.
|
|
1709
|
+
await cleanup(() => this.ttsService.dispose());
|
|
1521
1710
|
await cleanup(() => this.toolProviderRegistry.destroy());
|
|
1522
1711
|
await cleanup(() => this.highlightCoordinator.destroy());
|
|
1523
|
-
|
|
1524
|
-
await cleanup(() => this.toolCoordinator.releaseTool(toolId));
|
|
1525
|
-
}
|
|
1712
|
+
await cleanup(() => this.ownedToolCoordinator.destroy());
|
|
1526
1713
|
await cleanup(() => this.catalogResolver.destroy());
|
|
1527
1714
|
await cleanup(() => this.toolRequests.dispose());
|
|
1528
1715
|
await cleanup(() => this.policyEngine.dispose());
|
|
1529
1716
|
this.toolContextResolvers.clear();
|
|
1530
1717
|
this.toolContextResolverChangeListeners.clear();
|
|
1718
|
+
this.policyDiagnosticListeners.clear();
|
|
1531
1719
|
this.sectionControllerLifecycleListeners.clear();
|
|
1532
1720
|
this.telemetryListeners.clear();
|
|
1533
1721
|
this.frameworkErrorHookUnsubscribe?.();
|
|
@@ -1557,9 +1745,9 @@ export class ToolkitCoordinator {
|
|
|
1557
1745
|
await Promise.allSettled(pending);
|
|
1558
1746
|
}
|
|
1559
1747
|
/**
|
|
1560
|
-
* Initialize TTS service
|
|
1748
|
+
* Initialize the TTS service from the `textToSpeech` tool config.
|
|
1561
1749
|
*/
|
|
1562
|
-
async ensureTTSReady(
|
|
1750
|
+
async ensureTTSReady() {
|
|
1563
1751
|
this.assertNotDisposed();
|
|
1564
1752
|
await this.waitForPendingTTSReconfigure();
|
|
1565
1753
|
this.assertNotDisposed();
|
|
@@ -1567,10 +1755,11 @@ export class ToolkitCoordinator {
|
|
|
1567
1755
|
return;
|
|
1568
1756
|
if (this.ttsInitPromise)
|
|
1569
1757
|
return this.ttsInitPromise;
|
|
1570
|
-
this.ttsInitPromise = this._initializeTTS(
|
|
1758
|
+
this.ttsInitPromise = this._initializeTTS()
|
|
1571
1759
|
.then(() => this.assertNotDisposed())
|
|
1572
1760
|
.finally(() => {
|
|
1573
1761
|
this.ttsInitPromise = undefined;
|
|
1762
|
+
this.notifyReadyChange();
|
|
1574
1763
|
});
|
|
1575
1764
|
return this.ttsInitPromise;
|
|
1576
1765
|
}
|
|
@@ -1581,10 +1770,10 @@ export class ToolkitCoordinator {
|
|
|
1581
1770
|
pending = this.ttsReconfigurePromise;
|
|
1582
1771
|
}
|
|
1583
1772
|
}
|
|
1584
|
-
async _initializeTTS(
|
|
1773
|
+
async _initializeTTS() {
|
|
1585
1774
|
if (this.ttsInitialized)
|
|
1586
1775
|
return;
|
|
1587
|
-
const resolvedToolConfig = this.resolveTTSToolConfig(
|
|
1776
|
+
const resolvedToolConfig = this.resolveTTSToolConfig();
|
|
1588
1777
|
const runtimeSettings = resolveTTSRuntimeSettings(resolvedToolConfig);
|
|
1589
1778
|
const resolvedBackend = resolveTTSBackend(runtimeSettings);
|
|
1590
1779
|
const runtimeTTSConfig = buildRuntimeTTSConfig(runtimeSettings);
|
|
@@ -1603,10 +1792,10 @@ export class ToolkitCoordinator {
|
|
|
1603
1792
|
operation: "tts-init",
|
|
1604
1793
|
backend: resolvedBackend,
|
|
1605
1794
|
});
|
|
1606
|
-
|
|
1607
|
-
if (this.toolProviderRegistry.has("tts")) {
|
|
1795
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
1608
1796
|
try {
|
|
1609
|
-
|
|
1797
|
+
// Browser speech follows a server provider that fails to start.
|
|
1798
|
+
const ttsProvider = await this.initializeProvider("textToSpeech", true);
|
|
1610
1799
|
const providerInstance = await ttsProvider.createInstance();
|
|
1611
1800
|
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
1612
1801
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
@@ -1618,25 +1807,14 @@ export class ToolkitCoordinator {
|
|
|
1618
1807
|
backend: resolvedBackend,
|
|
1619
1808
|
provider: "registry",
|
|
1620
1809
|
});
|
|
1621
|
-
console.log("[ToolkitCoordinator] TTS initialized via ToolProviderRegistry");
|
|
1622
1810
|
return;
|
|
1623
1811
|
}
|
|
1624
1812
|
catch (error) {
|
|
1625
1813
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1626
1814
|
throw error;
|
|
1627
1815
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1628
|
-
await this.emitTelemetry("pie-tool-init-error", {
|
|
1629
|
-
toolId: "textToSpeech",
|
|
1630
|
-
providerId: "tts",
|
|
1631
|
-
operation: "tts-init",
|
|
1632
|
-
backend: resolvedBackend,
|
|
1633
|
-
errorType: "TTSRegistryInitError",
|
|
1634
|
-
message: normalized.message,
|
|
1635
|
-
recovered: true,
|
|
1636
|
-
});
|
|
1637
1816
|
await this.emitTelemetry("pie-tool-init-fallback", {
|
|
1638
1817
|
toolId: "textToSpeech",
|
|
1639
|
-
providerId: "tts",
|
|
1640
1818
|
operation: "tts-init",
|
|
1641
1819
|
backend: resolvedBackend,
|
|
1642
1820
|
fromProvider: "registry",
|
|
@@ -1649,10 +1827,13 @@ export class ToolkitCoordinator {
|
|
|
1649
1827
|
else if (resolvedBackend !== "browser") {
|
|
1650
1828
|
this.reportMissingTTSProvider(resolvedBackend);
|
|
1651
1829
|
}
|
|
1652
|
-
//
|
|
1830
|
+
// Browser speech, configured or standing in for a backend that has no
|
|
1831
|
+
// provider or failed to start.
|
|
1653
1832
|
const provider = new BrowserTTSProvider();
|
|
1654
1833
|
try {
|
|
1655
|
-
await this.initializeTTSService(provider,
|
|
1834
|
+
await this.initializeTTSService(provider, resolvedBackend === "browser"
|
|
1835
|
+
? runtimeTTSConfig
|
|
1836
|
+
: browserFallbackConfig(runtimeTTSConfig));
|
|
1656
1837
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
1657
1838
|
provider: "browser-fallback",
|
|
1658
1839
|
});
|
|
@@ -1667,7 +1848,7 @@ export class ToolkitCoordinator {
|
|
|
1667
1848
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1668
1849
|
throw error;
|
|
1669
1850
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1670
|
-
this.
|
|
1851
|
+
this.reportStartFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
|
|
1671
1852
|
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
1672
1853
|
message: normalized.message,
|
|
1673
1854
|
});
|
|
@@ -1681,12 +1862,12 @@ export class ToolkitCoordinator {
|
|
|
1681
1862
|
throw normalized;
|
|
1682
1863
|
}
|
|
1683
1864
|
}
|
|
1684
|
-
resolveTTSToolConfig(
|
|
1685
|
-
return
|
|
1865
|
+
resolveTTSToolConfig() {
|
|
1866
|
+
return this.getTTSConfigFromProviders() || {};
|
|
1686
1867
|
}
|
|
1687
1868
|
async initializeTTSService(provider, config) {
|
|
1688
1869
|
const nextProviderOptions = {
|
|
1689
|
-
...
|
|
1870
|
+
...config.providerOptions,
|
|
1690
1871
|
__pieTelemetry: async (eventName, payload) => {
|
|
1691
1872
|
await this.emitTelemetry(eventName, {
|
|
1692
1873
|
toolId: "textToSpeech",
|
|
@@ -1699,93 +1880,14 @@ export class ToolkitCoordinator {
|
|
|
1699
1880
|
providerOptions: nextProviderOptions,
|
|
1700
1881
|
};
|
|
1701
1882
|
await this.ttsService.initialize(provider, nextConfig);
|
|
1702
|
-
await this.ensureBrowserVoicesReady(provider);
|
|
1703
1883
|
this.assertNotDisposed();
|
|
1704
1884
|
this.ttsService.setCatalogResolver(this.catalogResolver);
|
|
1705
1885
|
this.ttsInitialized = true;
|
|
1886
|
+
this.ttsDegraded = false;
|
|
1887
|
+
this.degradedTools.delete("textToSpeech");
|
|
1706
1888
|
await this.hooks.onTTSReady?.();
|
|
1707
1889
|
this.assertNotDisposed();
|
|
1708
1890
|
}
|
|
1709
|
-
async ensureBrowserVoicesReady(provider, timeoutMs = 1200) {
|
|
1710
|
-
if (provider.providerId !== "browser")
|
|
1711
|
-
return;
|
|
1712
|
-
if (typeof window === "undefined")
|
|
1713
|
-
return;
|
|
1714
|
-
if (!("speechSynthesis" in window))
|
|
1715
|
-
return;
|
|
1716
|
-
const synth = window.speechSynthesis;
|
|
1717
|
-
const voices = synth.getVoices();
|
|
1718
|
-
if (voices.length > 0)
|
|
1719
|
-
return;
|
|
1720
|
-
await new Promise((resolve) => {
|
|
1721
|
-
let settled = false;
|
|
1722
|
-
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
1723
|
-
typeof synth.removeEventListener === "function";
|
|
1724
|
-
const canUseHandler = !canUseEventTarget && "onvoiceschanged" in synth;
|
|
1725
|
-
const previousHandler = canUseHandler ? synth.onvoiceschanged : null;
|
|
1726
|
-
let assignedHandler = false;
|
|
1727
|
-
const finish = () => {
|
|
1728
|
-
if (settled)
|
|
1729
|
-
return;
|
|
1730
|
-
settled = true;
|
|
1731
|
-
window.clearTimeout(timeoutId);
|
|
1732
|
-
if (canUseEventTarget) {
|
|
1733
|
-
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
1734
|
-
}
|
|
1735
|
-
else if (assignedHandler &&
|
|
1736
|
-
synth.onvoiceschanged === onVoicesChanged) {
|
|
1737
|
-
synth.onvoiceschanged = previousHandler;
|
|
1738
|
-
}
|
|
1739
|
-
resolve();
|
|
1740
|
-
};
|
|
1741
|
-
const onVoicesChanged = (event) => {
|
|
1742
|
-
if (!canUseEventTarget &&
|
|
1743
|
-
typeof previousHandler === "function" &&
|
|
1744
|
-
event) {
|
|
1745
|
-
previousHandler.call(synth, event);
|
|
1746
|
-
}
|
|
1747
|
-
finish();
|
|
1748
|
-
};
|
|
1749
|
-
const timeoutId = window.setTimeout(finish, timeoutMs);
|
|
1750
|
-
if (canUseEventTarget) {
|
|
1751
|
-
synth.addEventListener("voiceschanged", onVoicesChanged, {
|
|
1752
|
-
once: true,
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
else if (canUseHandler) {
|
|
1756
|
-
synth.onvoiceschanged = onVoicesChanged;
|
|
1757
|
-
assignedHandler = true;
|
|
1758
|
-
}
|
|
1759
|
-
});
|
|
1760
|
-
const voicesAfterWait = synth.getVoices();
|
|
1761
|
-
await this.emitTelemetry("pie-toolkit-tts-browser-voices-ready", {
|
|
1762
|
-
voiceCount: voicesAfterWait.length,
|
|
1763
|
-
timedOut: voicesAfterWait.length === 0,
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Get all services as a bundle for section player convenience.
|
|
1768
|
-
* Section player can extract and pass services to child components.
|
|
1769
|
-
*/
|
|
1770
|
-
getServiceBundle() {
|
|
1771
|
-
return {
|
|
1772
|
-
ttsService: this.ttsService,
|
|
1773
|
-
toolCoordinator: this.toolCoordinator,
|
|
1774
|
-
highlightCoordinator: this.highlightCoordinator,
|
|
1775
|
-
elementToolStateStore: this.elementToolStateStore,
|
|
1776
|
-
catalogResolver: this.catalogResolver,
|
|
1777
|
-
toolProviderRegistry: this.toolProviderRegistry,
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Get a tool provider from the registry
|
|
1782
|
-
*
|
|
1783
|
-
* @param providerId Provider identifier
|
|
1784
|
-
* @returns Tool provider instance
|
|
1785
|
-
*/
|
|
1786
|
-
async getToolProvider(providerId) {
|
|
1787
|
-
return this.ensureProviderReady(providerId);
|
|
1788
|
-
}
|
|
1789
1891
|
async waitUntilReady() {
|
|
1790
1892
|
this.assertNotDisposed();
|
|
1791
1893
|
if (this.isReady())
|
|
@@ -1795,9 +1897,8 @@ export class ToolkitCoordinator {
|
|
|
1795
1897
|
this.coordinatorReadyPromise = (async () => {
|
|
1796
1898
|
await this.ensureStateLoaded();
|
|
1797
1899
|
this.assertNotDisposed();
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
await this.ensureTTSReady(ttsConfig);
|
|
1900
|
+
if (this.ttsRequiredForReadiness()) {
|
|
1901
|
+
await this.startTTSForReadiness();
|
|
1801
1902
|
}
|
|
1802
1903
|
this.assertNotDisposed();
|
|
1803
1904
|
if (!this.coordinatorReadyNotified) {
|
|
@@ -1814,22 +1915,37 @@ export class ToolkitCoordinator {
|
|
|
1814
1915
|
return this.coordinatorReadyPromise;
|
|
1815
1916
|
}
|
|
1816
1917
|
isReady() {
|
|
1817
|
-
return this.
|
|
1918
|
+
return ((this.stateLoadSettled || !this.hooks.loadToolState) &&
|
|
1919
|
+
(this.ttsInitialized ||
|
|
1920
|
+
this.ttsDegraded ||
|
|
1921
|
+
!this.ttsRequiredForReadiness()));
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Readiness waits for text-to-speech when it is enabled and either starts
|
|
1925
|
+
* eagerly or is granted: a granted read-aloud fails before the learner starts.
|
|
1926
|
+
*/
|
|
1927
|
+
ttsRequiredForReadiness() {
|
|
1928
|
+
if (this.getTTSConfigFromProviders()?.enabled === false)
|
|
1929
|
+
return false;
|
|
1930
|
+
return !this.lazyInit || this.isToolGranted("textToSpeech");
|
|
1818
1931
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1932
|
+
/**
|
|
1933
|
+
* Starts text-to-speech for readiness. A failure policy does not grant leaves
|
|
1934
|
+
* the tool degraded, reporting itself unavailable, and readiness settles; a
|
|
1935
|
+
* granted tool's failure rejects.
|
|
1936
|
+
*/
|
|
1937
|
+
async startTTSForReadiness() {
|
|
1938
|
+
try {
|
|
1939
|
+
await this.ensureTTSReady();
|
|
1940
|
+
}
|
|
1941
|
+
catch (error) {
|
|
1942
|
+
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1943
|
+
throw error;
|
|
1944
|
+
if (this.isToolGranted("textToSpeech"))
|
|
1945
|
+
throw error;
|
|
1946
|
+
this.ttsDegraded = true;
|
|
1947
|
+
this.notifyReadyChange();
|
|
1824
1948
|
}
|
|
1825
|
-
const ttsConfig = this.getTTSConfigFromProviders();
|
|
1826
|
-
return {
|
|
1827
|
-
tts: this.ttsInitialized,
|
|
1828
|
-
stateLoaded: this.stateLoaded || !this.hooks.loadToolState,
|
|
1829
|
-
coordinator: (this.stateLoaded || !this.hooks.loadToolState) &&
|
|
1830
|
-
(this.ttsInitialized || ttsConfig?.enabled === false),
|
|
1831
|
-
providers,
|
|
1832
|
-
};
|
|
1833
1949
|
}
|
|
1834
1950
|
getTTSConfigFromProviders() {
|
|
1835
1951
|
return this.config.tools?.providers?.textToSpeech;
|
|
@@ -1838,16 +1954,11 @@ export class ToolkitCoordinator {
|
|
|
1838
1954
|
if (typeof toolId !== "string" || toolId.trim().length === 0) {
|
|
1839
1955
|
throw new Error("Tool id must be a non-empty string.");
|
|
1840
1956
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
// host's every tool-config call into an exception — including the calls
|
|
1847
|
-
// this coordinator's own default-provider block provokes, which is how a
|
|
1848
|
-
// host that passes no registry ended up unable to read its own config.
|
|
1849
|
-
// Validation reports the missing registry once per coordinator, as
|
|
1850
|
-
// `tools.registryUnavailable`; a report per call would be noise.
|
|
1957
|
+
// An empty registry means the host supplied none, so there is nothing to
|
|
1958
|
+
// check an id against. Throwing here would fail every tool-config call,
|
|
1959
|
+
// the default providers' own included, and once left a host without a
|
|
1960
|
+
// registry unable to read its config. Validation reports the missing
|
|
1961
|
+
// registry once per coordinator, as `tools.registryUnavailable`.
|
|
1851
1962
|
if (this.toolRegistry.getAllToolIds().length === 0)
|
|
1852
1963
|
return;
|
|
1853
1964
|
if (!this.toolRegistry.get(toolId)) {
|
|
@@ -1867,6 +1978,11 @@ export class ToolkitCoordinator {
|
|
|
1867
1978
|
this.toolContextResolvers.set(toolId, resolver);
|
|
1868
1979
|
}
|
|
1869
1980
|
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Replace all host-owned render-context resolvers. The toolkit element
|
|
1983
|
+
* calls this from `runtime.toolContextResolvers`; it is not on
|
|
1984
|
+
* `ToolkitCoordinatorApi`.
|
|
1985
|
+
*/
|
|
1870
1986
|
setToolContextResolvers(resolvers) {
|
|
1871
1987
|
this.toolContextResolvers.clear();
|
|
1872
1988
|
this.installToolContextResolvers(resolvers ?? undefined);
|
|
@@ -1882,25 +1998,11 @@ export class ToolkitCoordinator {
|
|
|
1882
1998
|
}
|
|
1883
1999
|
}
|
|
1884
2000
|
}
|
|
1885
|
-
/**
|
|
1886
|
-
* Check if a tool is enabled.
|
|
1887
|
-
* Tools are enabled by default unless explicitly disabled.
|
|
1888
|
-
*
|
|
1889
|
-
* @param toolId Tool identifier (e.g., 'textToSpeech', 'answerEliminator')
|
|
1890
|
-
* @returns True if tool is enabled
|
|
1891
|
-
*/
|
|
1892
|
-
isToolEnabled(toolId) {
|
|
1893
|
-
this.assertCanonicalToolId(toolId);
|
|
1894
|
-
const toolConfig = this.config.tools?.providers?.[toolId];
|
|
1895
|
-
// Enabled by default unless explicitly set to false
|
|
1896
|
-
return toolConfig?.enabled !== false;
|
|
1897
|
-
}
|
|
1898
2001
|
getToolConfig(toolId) {
|
|
1899
2002
|
this.assertCanonicalToolId(toolId);
|
|
1900
2003
|
return this.config.tools?.providers?.[toolId] || null;
|
|
1901
2004
|
}
|
|
1902
2005
|
updateToolConfig(toolId, updates) {
|
|
1903
|
-
// Update config
|
|
1904
2006
|
this.assertCanonicalToolId(toolId);
|
|
1905
2007
|
const current = this.getToolConfig(toolId) || {};
|
|
1906
2008
|
if (!this.config.tools) {
|
|
@@ -1914,10 +2016,10 @@ export class ToolkitCoordinator {
|
|
|
1914
2016
|
this.config.tools.providers = {};
|
|
1915
2017
|
}
|
|
1916
2018
|
const nextProviders = {
|
|
1917
|
-
...
|
|
2019
|
+
...this.config.tools.providers,
|
|
1918
2020
|
[toolId]: mergeToolConfigUpdate(toolId, current, updates),
|
|
1919
2021
|
};
|
|
1920
|
-
|
|
2022
|
+
const validated = this.validateToolsConfig({
|
|
1921
2023
|
...this.config.tools,
|
|
1922
2024
|
providers: nextProviders,
|
|
1923
2025
|
}, {
|
|
@@ -1925,31 +2027,21 @@ export class ToolkitCoordinator {
|
|
|
1925
2027
|
source: "ToolkitCoordinator.updateToolConfig",
|
|
1926
2028
|
toolRegistry: this.toolRegistry,
|
|
1927
2029
|
});
|
|
1928
|
-
|
|
1929
|
-
//
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1932
|
-
this.policyEngine.updateInputs({
|
|
1933
|
-
tools: this.config.tools,
|
|
1934
|
-
});
|
|
2030
|
+
this.config.tools = validated;
|
|
2031
|
+
// The engine's tools input follows the validated config; its `inputs`
|
|
2032
|
+
// change event is what makes toolbars re-decide.
|
|
2033
|
+
this.policyEngine.updateInputs({ tools: validated });
|
|
1935
2034
|
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
1936
|
-
|
|
1937
|
-
this._applyToolConfigChange(toolId, current);
|
|
2035
|
+
this._applyToolConfigChange(toolId);
|
|
1938
2036
|
}
|
|
1939
2037
|
/**
|
|
1940
|
-
*
|
|
2038
|
+
* Patch one or more placement levels in the canonical tools config.
|
|
1941
2039
|
*
|
|
1942
2040
|
* This is the generic placement companion to {@link updateToolConfig}:
|
|
1943
2041
|
* it validates the next canonical tools config, keeps the policy
|
|
1944
2042
|
* engine in lockstep, and emits the same policy-change event used by
|
|
1945
2043
|
* live toolbars/debug panels.
|
|
1946
2044
|
*/
|
|
1947
|
-
updateToolPlacement(level, toolIds) {
|
|
1948
|
-
this.updateToolsPlacement({ [level]: [...toolIds] });
|
|
1949
|
-
}
|
|
1950
|
-
/**
|
|
1951
|
-
* Patch one or more placement levels in the canonical tools config.
|
|
1952
|
-
*/
|
|
1953
2045
|
updateToolsPlacement(partial) {
|
|
1954
2046
|
if (!this.config.tools) {
|
|
1955
2047
|
this.config.tools = this.validateToolsConfig(undefined, {
|
|
@@ -1978,17 +2070,58 @@ export class ToolkitCoordinator {
|
|
|
1978
2070
|
});
|
|
1979
2071
|
validated.placement = nextPlacement;
|
|
1980
2072
|
this.config.tools = validated;
|
|
1981
|
-
this.policyEngine.updateInputs({
|
|
1982
|
-
tools: this.config.tools,
|
|
1983
|
-
});
|
|
2073
|
+
this.policyEngine.updateInputs({ tools: validated });
|
|
1984
2074
|
}
|
|
1985
2075
|
/**
|
|
1986
2076
|
* Resolve the visible tool set for a given placement level + scope.
|
|
1987
2077
|
*
|
|
1988
|
-
*
|
|
2078
|
+
* Delegates to the owned tool-policy engine and logs the decision's
|
|
2079
|
+
* diagnostics through {@link warnPolicyDiagnostics}.
|
|
1989
2080
|
*/
|
|
1990
2081
|
decideToolPolicy(request) {
|
|
1991
|
-
|
|
2082
|
+
const decision = this.policyEngine.decide(request);
|
|
2083
|
+
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2084
|
+
return decision;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Report each policy diagnostic as a console warning and to the
|
|
2088
|
+
* {@link onPolicyDiagnostic} listeners, once per code and tool id, and per
|
|
2089
|
+
* item for `tool-policy.itemSettingNotApplied`. Every toolbar and feature
|
|
2090
|
+
* asks for decisions, so a conflict would otherwise repeat on each; the
|
|
2091
|
+
* engine's input diagnostics arrive with each policy change.
|
|
2092
|
+
*/
|
|
2093
|
+
warnPolicyDiagnostics(diagnostics) {
|
|
2094
|
+
for (const diagnostic of diagnostics) {
|
|
2095
|
+
const itemId = diagnostic.code === "tool-policy.itemSettingNotApplied"
|
|
2096
|
+
? diagnostic.details.itemId
|
|
2097
|
+
: "";
|
|
2098
|
+
const key = `${diagnostic.code}\0${diagnostic.toolId}\0${itemId}`;
|
|
2099
|
+
if (this.reportedPolicyDiagnostics.has(key))
|
|
2100
|
+
continue;
|
|
2101
|
+
this.reportedPolicyDiagnostics.set(key, diagnostic);
|
|
2102
|
+
console.warn(`[ToolkitCoordinator] ${diagnostic.message}`);
|
|
2103
|
+
for (const listener of Array.from(this.policyDiagnosticListeners)) {
|
|
2104
|
+
this.notifyPolicyDiagnosticListener(listener, diagnostic);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
notifyPolicyDiagnosticListener(listener, diagnostic) {
|
|
2109
|
+
try {
|
|
2110
|
+
listener(diagnostic);
|
|
2111
|
+
}
|
|
2112
|
+
catch (error) {
|
|
2113
|
+
console.error("[ToolkitCoordinator] Policy diagnostic listener failed:", error);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* File a mounted item's policy settings (`requiredTools`, `restrictedTools`,
|
|
2118
|
+
* `toolParameters`) under its canonical id. They govern the decisions scoped
|
|
2119
|
+
* to that item: its own item-level toolbar, and the feature decisions its
|
|
2120
|
+
* content asks with the item's scope. `pie-item-scope` registers them from
|
|
2121
|
+
* its `settings` property; the returned function withdraws them.
|
|
2122
|
+
*/
|
|
2123
|
+
registerItemSettings(itemId, settings) {
|
|
2124
|
+
return this.policyEngine.registerItemSettings(itemId, settings);
|
|
1992
2125
|
}
|
|
1993
2126
|
/**
|
|
1994
2127
|
* Resolve eligibility for one PNP/AfA feature id, independent of toolbar
|
|
@@ -1999,8 +2132,8 @@ export class ToolkitCoordinator {
|
|
|
1999
2132
|
* `pnpEnforcement` is not consulted.
|
|
2000
2133
|
*
|
|
2001
2134
|
* Reports once per coordinator when it is asked about a feature with no
|
|
2002
|
-
* assessment bound. A host in that state gets a
|
|
2003
|
-
*
|
|
2135
|
+
* assessment bound. A host in that state gets a denial for every capability
|
|
2136
|
+
* no item requires, which is indistinguishable from a student who was
|
|
2004
2137
|
* properly declined — so without this, forgetting {@link updateAssessment}
|
|
2005
2138
|
* presents as an accommodation that silently never appears.
|
|
2006
2139
|
*
|
|
@@ -2008,25 +2141,28 @@ export class ToolkitCoordinator {
|
|
|
2008
2141
|
* enforcement is explicitly `"on"`. Its toolkit binds whatever assessment the
|
|
2009
2142
|
* host gave, usually none, and a host that passes no profile and leaves
|
|
2010
2143
|
* enforcement unset or `"off"` has asked for no accommodation.
|
|
2144
|
+
*
|
|
2145
|
+
* `scope` is the surface asking; an item's scope brings in that item's
|
|
2146
|
+
* registered settings ({@link registerItemSettings}).
|
|
2011
2147
|
*/
|
|
2012
|
-
decideFeaturePolicy(featureId) {
|
|
2013
|
-
const decision = this.policyEngine.decideFeature(featureId);
|
|
2014
|
-
|
|
2015
|
-
this.pnpEnforcementOverride === "on";
|
|
2148
|
+
decideFeaturePolicy(featureId, scope) {
|
|
2149
|
+
const decision = this.policyEngine.decideFeature(featureId, scope);
|
|
2150
|
+
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2016
2151
|
if (!decision.assessmentBound &&
|
|
2017
|
-
|
|
2152
|
+
this.policyEngine.getInputs().assessmentExpected &&
|
|
2018
2153
|
!this.reportedUnboundFeaturePolicy) {
|
|
2019
2154
|
this.reportedUnboundFeaturePolicy = true;
|
|
2020
|
-
console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so
|
|
2155
|
+
console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so profile, district and test-administration policy have nothing to read and only an item's requiredTools can grant. Pass the assessment to the toolkit's assessment property, or call updateAssessment(...), before relying on any accommodation. Reported once per coordinator.`);
|
|
2021
2156
|
}
|
|
2022
2157
|
return decision;
|
|
2023
2158
|
}
|
|
2024
2159
|
/**
|
|
2025
2160
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
2026
2161
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
2027
|
-
* `
|
|
2028
|
-
*
|
|
2029
|
-
* removed via
|
|
2162
|
+
* `updateToolsPlacement`, `updateAssessment`, `setPnpEnforcement`), an
|
|
2163
|
+
* item's settings are registered or withdrawn ({@link registerItemSettings}),
|
|
2164
|
+
* or a custom `PolicySource` is registered / removed via
|
|
2165
|
+
* {@link registerPolicySource}.
|
|
2030
2166
|
*
|
|
2031
2167
|
* The listener receives a `ToolPolicyChangeEvent` with the event
|
|
2032
2168
|
* `reason` and a frozen snapshot of the engine inputs. Listeners
|
|
@@ -2040,6 +2176,15 @@ export class ToolkitCoordinator {
|
|
|
2040
2176
|
onPolicyChange(listener) {
|
|
2041
2177
|
return this.policyEngine.onPolicyChange(listener);
|
|
2042
2178
|
}
|
|
2179
|
+
onPolicyDiagnostic(listener) {
|
|
2180
|
+
this.policyDiagnosticListeners.add(listener);
|
|
2181
|
+
for (const diagnostic of Array.from(this.reportedPolicyDiagnostics.values())) {
|
|
2182
|
+
this.notifyPolicyDiagnosticListener(listener, diagnostic);
|
|
2183
|
+
}
|
|
2184
|
+
return () => {
|
|
2185
|
+
this.policyDiagnosticListeners.delete(listener);
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2043
2188
|
/**
|
|
2044
2189
|
* Subscribe to accessibility-catalog registrations and removals.
|
|
2045
2190
|
*
|
|
@@ -2056,49 +2201,26 @@ export class ToolkitCoordinator {
|
|
|
2056
2201
|
/**
|
|
2057
2202
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
2058
2203
|
*
|
|
2059
|
-
* Under auto-mode (no host override via {@link setPnpEnforcement}),
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
* carries item-level profile policy inputs. A bare assessment record (just
|
|
2204
|
+
* Under auto-mode (no host override via {@link setPnpEnforcement}), a
|
|
2205
|
+
* decision enforces PNP/profile policy iff the assessment carries profile
|
|
2206
|
+
* precedence material (`personalNeedsProfile`, `settings.districtPolicy`,
|
|
2207
|
+
* `settings.testAdministration`), or the decision is scoped to an item whose
|
|
2208
|
+
* settings carry item-level policy inputs. A bare assessment record (just
|
|
2065
2209
|
* `id` / `name`, no PNP, no settings) keeps `"off"`.
|
|
2066
2210
|
*
|
|
2067
2211
|
* The host override set via {@link setPnpEnforcement} is sticky
|
|
2068
|
-
* across assessment swaps
|
|
2069
|
-
* and re-runs the auto-mode helper.
|
|
2212
|
+
* across assessment swaps.
|
|
2070
2213
|
*/
|
|
2071
2214
|
updateAssessment(assessment) {
|
|
2072
|
-
this.
|
|
2073
|
-
this.policyEngine.updateInputs({
|
|
2074
|
-
assessment,
|
|
2075
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2076
|
-
});
|
|
2215
|
+
this.policyEngine.updateInputs({ assessment });
|
|
2077
2216
|
}
|
|
2078
2217
|
/**
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2082
|
-
* `restrictedTools` / `toolParameters`). Item-level profile material
|
|
2083
|
-
* also feeds {@link resolveEffectivePnpEnforcement} — navigating
|
|
2084
|
-
* to an item with profile settings can flip auto-mode to `"on"` even
|
|
2085
|
-
* when the parent assessment carries no profile block of its own.
|
|
2086
|
-
*/
|
|
2087
|
-
updateCurrentItemRef(itemRef) {
|
|
2088
|
-
this.boundCurrentItemRef = itemRef;
|
|
2089
|
-
this.policyEngine.updateInputs({
|
|
2090
|
-
currentItemRef: itemRef,
|
|
2091
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
/**
|
|
2095
|
-
* Override the auto-mode PNP/profile enforcement decision.
|
|
2218
|
+
* Override the auto-mode PNP/profile enforcement decision; `null` returns
|
|
2219
|
+
* to auto-mode. `"on"` and `"off"` stick across assessment swaps until
|
|
2220
|
+
* cleared.
|
|
2096
2221
|
*/
|
|
2097
2222
|
setPnpEnforcement(mode) {
|
|
2098
|
-
this.
|
|
2099
|
-
this.policyEngine.updateInputs({
|
|
2100
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2101
|
-
});
|
|
2223
|
+
this.policyEngine.updateInputs({ pnpEnforcement: mode });
|
|
2102
2224
|
}
|
|
2103
2225
|
/**
|
|
2104
2226
|
* Get the policy engine inputs currently driving decisions. Useful
|
|
@@ -2122,29 +2244,6 @@ export class ToolkitCoordinator {
|
|
|
2122
2244
|
registerPolicySource(source) {
|
|
2123
2245
|
return this.policyEngine.registerPolicySource(source);
|
|
2124
2246
|
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Register a host-owned resolver for one tool's render context.
|
|
2127
|
-
*
|
|
2128
|
-
* Resolvers are evaluated only for tools that already survived the
|
|
2129
|
-
* framework policy pipeline. They can hide the tool for the current
|
|
2130
|
-
* scope or attach render params consumed by the packaged registration;
|
|
2131
|
-
* they cannot re-enable tools blocked by placement, provider config,
|
|
2132
|
-
* host policy, or PNP/profile rules.
|
|
2133
|
-
*/
|
|
2134
|
-
registerToolContextResolver(toolId, resolver) {
|
|
2135
|
-
this.assertCanonicalToolId(toolId);
|
|
2136
|
-
if (typeof resolver !== "function") {
|
|
2137
|
-
throw new Error(`Invalid tool context resolver for "${toolId}": expected a function.`);
|
|
2138
|
-
}
|
|
2139
|
-
this.toolContextResolvers.set(toolId, resolver);
|
|
2140
|
-
this.notifyToolContextResolverChange();
|
|
2141
|
-
return () => {
|
|
2142
|
-
if (this.toolContextResolvers.get(toolId) !== resolver)
|
|
2143
|
-
return;
|
|
2144
|
-
this.toolContextResolvers.delete(toolId);
|
|
2145
|
-
this.notifyToolContextResolverChange();
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
2247
|
hasToolContextResolver(toolId) {
|
|
2149
2248
|
this.assertCanonicalToolId(toolId);
|
|
2150
2249
|
return this.toolContextResolvers.has(toolId);
|
|
@@ -2192,6 +2291,9 @@ export class ToolkitCoordinator {
|
|
|
2192
2291
|
this.toolContextResolverChangeListeners.delete(listener);
|
|
2193
2292
|
};
|
|
2194
2293
|
}
|
|
2294
|
+
getToolRegistry() {
|
|
2295
|
+
return this.toolRegistry;
|
|
2296
|
+
}
|
|
2195
2297
|
/**
|
|
2196
2298
|
* Claim requests for one placement level. Called by a toolbar on mount.
|
|
2197
2299
|
*/
|
|
@@ -2215,95 +2317,46 @@ export class ToolkitCoordinator {
|
|
|
2215
2317
|
* surface, and a host that swapped the registry for one without the tool would
|
|
2216
2318
|
* otherwise lose the whole surface to an exception over an absent action.
|
|
2217
2319
|
*/
|
|
2218
|
-
canRequestTool(toolId, level) {
|
|
2320
|
+
canRequestTool(toolId, level, scopeId) {
|
|
2219
2321
|
try {
|
|
2220
2322
|
this.assertCanonicalToolId(toolId);
|
|
2221
2323
|
}
|
|
2222
2324
|
catch {
|
|
2223
2325
|
return false;
|
|
2224
2326
|
}
|
|
2225
|
-
return this.toolRequests.canRequest(toolId, level);
|
|
2327
|
+
return this.toolRequests.canRequest(toolId, level, scopeId);
|
|
2226
2328
|
}
|
|
2227
2329
|
onToolRequestTargetsChange(listener) {
|
|
2228
2330
|
return this.toolRequests.onTargetsChange(listener);
|
|
2229
2331
|
}
|
|
2230
|
-
/**
|
|
2231
|
-
* Compute the effective PNP/profile enforcement mode given the explicit
|
|
2232
|
-
* host override and the auto-mode helper.
|
|
2233
|
-
*
|
|
2234
|
-
* Auto-mode (no override) defers to
|
|
2235
|
-
* {@link resolveDefaultPnpEnforcement}, which returns `"on"`
|
|
2236
|
-
* exactly when the bound assessment or current item ref carries
|
|
2237
|
-
* profile precedence material (PNP, district policy, test
|
|
2238
|
-
* administration, item-level required/restricted/parameters), and
|
|
2239
|
-
* `"off"` otherwise. Profile gates engage the moment profile material
|
|
2240
|
-
* is present.
|
|
2241
|
-
*/
|
|
2242
|
-
resolveEffectivePnpEnforcement() {
|
|
2243
|
-
if (this.pnpEnforcementOverride !== null) {
|
|
2244
|
-
return this.pnpEnforcementOverride;
|
|
2245
|
-
}
|
|
2246
|
-
return resolveDefaultPnpEnforcement({
|
|
2247
|
-
assessment: this.boundAssessment,
|
|
2248
|
-
currentItemRef: this.boundCurrentItemRef,
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
2332
|
resolveConfiguredPnpEnforcement() {
|
|
2252
|
-
|
|
2253
|
-
return tools?.pnpEnforcement ?? null;
|
|
2333
|
+
return this.config.tools?.pnpEnforcement ?? null;
|
|
2254
2334
|
}
|
|
2255
|
-
/**
|
|
2256
|
-
|
|
2257
|
-
* Called after updateToolConfig() with the tool's config before the update.
|
|
2258
|
-
*/
|
|
2259
|
-
_applyToolConfigChange(toolId, previousConfig) {
|
|
2335
|
+
/** Apply an `updateToolConfig` change to the underlying services. */
|
|
2336
|
+
_applyToolConfigChange(toolId) {
|
|
2260
2337
|
if (this.disposePromise !== null)
|
|
2261
2338
|
return;
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
this.scheduleTTSReconfigure();
|
|
2266
|
-
break;
|
|
2267
|
-
case "answerEliminator":
|
|
2268
|
-
// Future: Could notify answer eliminator tools of strategy change
|
|
2269
|
-
break;
|
|
2270
|
-
default:
|
|
2271
|
-
this._registerChangedToolProvider(toolId, previousConfig);
|
|
2339
|
+
if (toolId === "textToSpeech") {
|
|
2340
|
+
this.scheduleTTSReconfigure();
|
|
2341
|
+
return;
|
|
2272
2342
|
}
|
|
2343
|
+
this._registerChangedToolProvider(toolId);
|
|
2273
2344
|
}
|
|
2274
2345
|
/**
|
|
2275
|
-
* Register the provider a tool's updated config
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
* before this returns, so a check on the policy change the update dispatched
|
|
2279
|
-
* finds the new provider.
|
|
2346
|
+
* Register the provider a tool's updated config builds, replacing the one
|
|
2347
|
+
* registered under the tool's id. Registration completes before this returns,
|
|
2348
|
+
* so a check on the policy change the update dispatched finds the new provider.
|
|
2280
2349
|
*/
|
|
2281
|
-
_registerChangedToolProvider(toolId
|
|
2350
|
+
_registerChangedToolProvider(toolId) {
|
|
2282
2351
|
const registration = this.getProviderDescriptorTools().find((tool) => tool.toolId === toolId);
|
|
2283
2352
|
if (!registration)
|
|
2284
2353
|
return;
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
return resolveToolProviderId(registration, config);
|
|
2289
|
-
}
|
|
2290
|
-
catch {
|
|
2291
|
-
return null;
|
|
2292
|
-
}
|
|
2293
|
-
};
|
|
2294
|
-
const previousId = resolveQuietly(previousConfig);
|
|
2295
|
-
const nextId = resolveQuietly(this.getToolConfig(toolId) || undefined);
|
|
2296
|
-
if (!previousId ||
|
|
2297
|
-
previousId === nextId ||
|
|
2298
|
-
!this.toolProviderRegistry.has(previousId)) {
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
void this.toolProviderRegistry.unregister(previousId).catch((err) => {
|
|
2302
|
-
console.warn(`[ToolkitCoordinator] Failed to unregister provider "${previousId}":`, err);
|
|
2303
|
-
});
|
|
2354
|
+
// A failure of the replaced provider no longer describes the tool.
|
|
2355
|
+
this.degradedTools.delete(toolId);
|
|
2356
|
+
void this.registerProviderFromTool(registration, true);
|
|
2304
2357
|
}
|
|
2305
2358
|
/**
|
|
2306
|
-
* Re-register the
|
|
2359
|
+
* Re-register the text-to-speech provider for the current config and registry, then
|
|
2307
2360
|
* re-initialize unless initialization is lazy. An initialization already
|
|
2308
2361
|
* running finishes first: it would otherwise mark its provider ready after
|
|
2309
2362
|
* the reset.
|
|
@@ -2319,26 +2372,31 @@ export class ToolkitCoordinator {
|
|
|
2319
2372
|
this.ttsReconfigurePromise = undefined;
|
|
2320
2373
|
}
|
|
2321
2374
|
});
|
|
2322
|
-
void reconfigurePromise
|
|
2375
|
+
void reconfigurePromise
|
|
2376
|
+
.then(async () => {
|
|
2323
2377
|
if (this.disposePromise !== null)
|
|
2324
2378
|
return;
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2379
|
+
if (this.ttsRequiredForReadiness())
|
|
2380
|
+
await this.startTTSForReadiness();
|
|
2381
|
+
})
|
|
2382
|
+
// Reported where it failed; a granted failure has nowhere else to go.
|
|
2383
|
+
.catch(() => { });
|
|
2330
2384
|
}
|
|
2331
2385
|
async _reconfigureTTSProvider() {
|
|
2332
2386
|
this.ttsInitialized = false;
|
|
2387
|
+
this.ttsDegraded = false;
|
|
2388
|
+
this.degradedTools.delete("textToSpeech");
|
|
2333
2389
|
this.ttsInitPromise = undefined;
|
|
2390
|
+
this.notifyReadyChange();
|
|
2334
2391
|
try {
|
|
2335
|
-
|
|
2392
|
+
// The next speak waits on readiness, which starts the new provider.
|
|
2393
|
+
this.ttsService.releaseProvider();
|
|
2336
2394
|
}
|
|
2337
2395
|
catch {
|
|
2338
|
-
// noop:
|
|
2396
|
+
// noop: release best effort
|
|
2339
2397
|
}
|
|
2340
|
-
if (this.toolProviderRegistry.has("
|
|
2341
|
-
await this.toolProviderRegistry.unregister("
|
|
2398
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
2399
|
+
await this.toolProviderRegistry.unregister("textToSpeech");
|
|
2342
2400
|
}
|
|
2343
2401
|
if (this.disposePromise !== null)
|
|
2344
2402
|
return;
|