@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75
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 +469 -318
- package/dist/attempt/AssessmentSession.d.ts +1 -5
- package/dist/attempt/AssessmentSession.js +0 -29
- package/dist/attempt/TestSession.d.ts +1 -20
- package/dist/attempt/TestSession.js +8 -99
- 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.d.ts +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
- package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
- package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-jd8cc08x.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 +32 -6
- package/dist/index.d.ts +23 -48
- package/dist/index.js +12 -37
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
- package/dist/policy/core/ToolPolicyEngine.js +148 -46
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +75 -54
- package/dist/policy/core/decision-types.d.ts +65 -34
- package/dist/policy/core/feature-decision.d.ts +16 -9
- package/dist/policy/core/feature-decision.js +14 -8
- package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
- package/dist/policy/core/pnp-policy-inputs.js +28 -56
- package/dist/policy/core/provenance.d.ts +9 -4
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
- package/dist/policy/sources/PnpPolicySource.js +117 -118
- package/dist/runtime/SectionControllerBinding.d.ts +279 -0
- package/dist/runtime/SectionControllerBinding.js +450 -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 -2
- package/dist/runtime/adapter/subscriber-fanout.js +1 -2
- 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 +9 -15
- package/dist/runtime/composition-emit-scheduler.js +9 -15
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +17 -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 -2
- package/dist/runtime/core/SectionEngineCore.js +2 -2
- package/dist/runtime/core/cohort.d.ts +1 -1
- package/dist/runtime/core/cohort.js +1 -1
- 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 +16 -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 +71 -51
- 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 +14 -0
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- 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 +188 -0
- package/dist/runtime/tool-host-contract.d.ts +28 -10
- package/dist/runtime/tool-host-contract.js +31 -10
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/ElementToolStateStore.d.ts +14 -10
- package/dist/services/ElementToolStateStore.js +37 -13
- package/dist/services/HighlightCoordinator.d.ts +22 -28
- package/dist/services/HighlightCoordinator.js +431 -327
- package/dist/services/RangeSerializer.d.ts +10 -1
- package/dist/services/RangeSerializer.js +87 -14
- package/dist/services/TTSService.d.ts +130 -69
- package/dist/services/TTSService.js +713 -570
- package/dist/services/ToolCoordinator.d.ts +43 -66
- package/dist/services/ToolCoordinator.js +164 -208
- package/dist/services/ToolRegistry.d.ts +78 -54
- package/dist/services/ToolRegistry.js +107 -105
- package/dist/services/ToolkitCoordinator.d.ts +261 -175
- package/dist/services/ToolkitCoordinator.js +667 -433
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +11 -1
- package/dist/services/catalog-owner.js +25 -0
- package/dist/services/create-tools-config.d.ts +2 -2
- 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 +30 -8
- package/dist/services/framework-error.js +9 -7
- package/dist/services/interfaces.d.ts +158 -129
- package/dist/services/section-controller-types.d.ts +15 -0
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +82 -0
- package/dist/services/spoken-audio-cards.d.ts +0 -2
- package/dist/services/spoken-audio-cards.js +1 -3
- 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 +14 -4
- package/dist/services/tool-config-validation.js +63 -71
- package/dist/services/tool-context.d.ts +0 -8
- package/dist/services/tool-context.js +29 -25
- package/dist/services/tool-icons.d.ts +4 -3
- package/dist/services/tool-icons.js +4 -3
- package/dist/services/tool-instance-id.d.ts +0 -1
- package/dist/services/tool-instance-id.js +0 -6
- package/dist/services/tool-providers/TTSToolProvider.d.ts +41 -80
- package/dist/services/tool-providers/TTSToolProvider.js +81 -40
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
- package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
- package/dist/services/tool-providers/index.d.ts +0 -9
- package/dist/services/tool-providers/index.js +2 -4
- package/dist/services/tool-request.d.ts +27 -12
- package/dist/services/tool-request.js +24 -14
- package/dist/services/toolbar-decided-tools.d.ts +15 -0
- package/dist/services/toolbar-decided-tools.js +28 -0
- package/dist/services/tools-config-normalizer.d.ts +37 -23
- package/dist/services/tools-config-normalizer.js +2 -10
- package/dist/services/tts/browser-provider.d.ts +38 -2
- package/dist/services/tts/browser-provider.js +276 -140
- package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
- package/dist/services/tts/catalog-span-alignment.js +17 -142
- package/dist/services/tts/flat-tree.d.ts +71 -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/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 +0 -12
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
- 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/math-alignment/index.d.ts +0 -5
- package/dist/services/tts/math-alignment/index.js +5 -25
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
- package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
- package/dist/services/tts/math-alignment/types.d.ts +4 -2
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +74 -18
- 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 +16 -1
- package/dist/services/tts/math-speech.js +55 -19
- package/dist/services/tts/provider-options.d.ts +41 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/sre-engine.d.ts +7 -0
- package/dist/services/tts/sre-engine.js +11 -0
- package/dist/services/tts/sre-locales.d.ts +14 -0
- package/dist/services/tts/sre-locales.js +57 -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 +11 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -1
- package/dist/services/tts/text-processing.js +46 -34
- package/dist/services/tts/text-segmentation.d.ts +7 -0
- package/dist/services/tts/text-segmentation.js +47 -0
- package/dist/services/tts-runtime-config.d.ts +44 -18
- package/dist/services/tts-runtime-config.js +22 -93
- package/dist/tools/content-capability-resolution.d.ts +7 -9
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +58 -0
- package/dist/tools/registration.js +54 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +44 -42
- 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 +24 -59
- 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-byq80z42.js +0 -40
- package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -51
- 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 -80
- package/dist/runtime/section-runtime-engine-host-context.js +0 -95
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- 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/ToolConfigResolver.d.ts +0 -161
- package/dist/services/ToolConfigResolver.js +0 -229
- package/dist/services/catalog-media.d.ts +0 -56
- package/dist/services/catalog-media.js +0 -190
- package/dist/services/createDefaultToolRegistry.d.ts +0 -53
- package/dist/services/createDefaultToolRegistry.js +0 -45
- 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/tool-providers/CortexToolProvider.d.ts +0 -13
- package/dist/services/tool-providers/CortexToolProvider.js +0 -32
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
- package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
- package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
- package/dist/services/tts/visual-line-ranges.d.ts +0 -18
- package/dist/services/tts/visual-line-ranges.js +0 -226
- package/dist/tools/client.d.ts +0 -12
- package/dist/tools/client.js +0 -14
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js +0 -10
- package/dist/tools/internal.d.ts +0 -43
- package/dist/tools/internal.js +0 -41
- package/dist/tools/response-discovery.d.ts +0 -61
- package/dist/tools/response-discovery.js +0 -113
- package/dist/tools/types.d.ts +0 -337
- package/dist/tools/types.js +0 -12
- package/dist/tools/variant-resolver.d.ts +0 -41
- package/dist/tools/variant-resolver.js +0 -207
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Part of PIE Assessment Toolkit.
|
|
15
15
|
*/
|
|
16
16
|
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
17
|
-
import {
|
|
17
|
+
import { collectToolConfigDiagnostics, normalizeToolConfigStrictness, reportToolConfigDiagnostics, } from "./tool-config-validation.js";
|
|
18
18
|
import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
|
|
19
19
|
import { ElementToolStateStore } from "./ElementToolStateStore.js";
|
|
20
20
|
import { frameworkErrorFromCoordinatorContext, } from "./framework-error.js";
|
|
@@ -22,13 +22,13 @@ import { FrameworkErrorBus, } from "./framework-error-bus.js";
|
|
|
22
22
|
import { HighlightCoordinator } from "./HighlightCoordinator.js";
|
|
23
23
|
import { ToolCoordinator } from "./ToolCoordinator.js";
|
|
24
24
|
import { TTSService } from "./TTSService.js";
|
|
25
|
-
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
25
|
+
import { BrowserTTSProvider, browserFallbackConfig, } from "./tts/browser-provider.js";
|
|
26
26
|
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
|
|
27
27
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
28
28
|
import { ToolRegistry } from "./ToolRegistry.js";
|
|
29
29
|
import { ToolRequestRegistry } from "./tool-request.js";
|
|
30
30
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
31
|
-
import {
|
|
31
|
+
import { resolveSectionSessionAssignment } from "./section-session-assignment.js";
|
|
32
32
|
class ToolkitCoordinatorDisposedError extends Error {
|
|
33
33
|
constructor() {
|
|
34
34
|
super("ToolkitCoordinator has been disposed.");
|
|
@@ -45,16 +45,9 @@ const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.
|
|
|
45
45
|
const mergeToolConfigUpdate = (toolId, current, updates) => {
|
|
46
46
|
const next = { ...current, ...updates };
|
|
47
47
|
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
|
-
}
|
|
48
|
+
isPlainRecord(current.mathSpeech) &&
|
|
49
|
+
isPlainRecord(updates.mathSpeech)) {
|
|
50
|
+
next.mathSpeech = { ...current.mathSpeech, ...updates.mathSpeech };
|
|
58
51
|
}
|
|
59
52
|
return next;
|
|
60
53
|
};
|
|
@@ -81,13 +74,19 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
81
74
|
*
|
|
82
75
|
* @example
|
|
83
76
|
* ```typescript
|
|
77
|
+
* // Both are exported by @pie-players/pie-default-tool-loaders
|
|
78
|
+
* const toolRegistry = createPackagedToolRegistry({
|
|
79
|
+
* toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
84
82
|
* // Create coordinator with configuration
|
|
85
83
|
* const coordinator = new ToolkitCoordinator({
|
|
86
84
|
* assessmentId: 'demo-three-questions',
|
|
85
|
+
* toolRegistry,
|
|
87
86
|
* tools: {
|
|
88
87
|
* providers: {
|
|
89
88
|
* textToSpeech: { enabled: true, backend: 'browser' },
|
|
90
|
-
* answerEliminator: { enabled: true
|
|
89
|
+
* answerEliminator: { enabled: true }
|
|
91
90
|
* },
|
|
92
91
|
* placement: {
|
|
93
92
|
* item: ['textToSpeech', 'answerEliminator']
|
|
@@ -96,7 +95,7 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
96
95
|
* });
|
|
97
96
|
*
|
|
98
97
|
* // Pass to section player
|
|
99
|
-
* player.
|
|
98
|
+
* player.runtime = { ...(player.runtime ?? {}), coordinator };
|
|
100
99
|
*
|
|
101
100
|
* // Access services directly
|
|
102
101
|
* const ttsService = coordinator.ttsService;
|
|
@@ -119,14 +118,43 @@ export class ToolkitCoordinator {
|
|
|
119
118
|
lazyInit;
|
|
120
119
|
/** Track TTS initialization state */
|
|
121
120
|
ttsInitialized = false;
|
|
121
|
+
/** Text-to-speech failed to start and policy does not grant it. */
|
|
122
|
+
ttsDegraded = false;
|
|
122
123
|
ttsInitPromise;
|
|
123
124
|
ttsReconfigurePromise;
|
|
124
125
|
stateLoaded = false;
|
|
126
|
+
/**
|
|
127
|
+
* The loader ran, whether or not it returned state. A failed load is
|
|
128
|
+
* reported once and the coordinator runs without saved tool state.
|
|
129
|
+
*/
|
|
130
|
+
stateLoadSettled = false;
|
|
125
131
|
stateLoadPromise;
|
|
126
132
|
coordinatorReadyPromise;
|
|
127
133
|
coordinatorReadyNotified = false;
|
|
128
134
|
providerInitPromises = new Map();
|
|
135
|
+
/**
|
|
136
|
+
* Recoverable failures, by tool. A policy change that grants one of these
|
|
137
|
+
* tools reports its failure again as fatal.
|
|
138
|
+
*/
|
|
139
|
+
degradedTools = new Map();
|
|
140
|
+
/** Tools a toolbar reported a module failure for, each reported once. */
|
|
141
|
+
toolModuleFailures = new Set();
|
|
142
|
+
readyChangeListeners = new Set();
|
|
143
|
+
eagerInit;
|
|
129
144
|
toolRegistry;
|
|
145
|
+
/** Whether the host passed `toolRegistry`, which then always stands. */
|
|
146
|
+
toolRegistrySupplied;
|
|
147
|
+
/**
|
|
148
|
+
* Whether the registry is final: supplied at construction, or adopted from
|
|
149
|
+
* the toolkit this coordinator is bound to.
|
|
150
|
+
*/
|
|
151
|
+
toolRegistrySettled;
|
|
152
|
+
/**
|
|
153
|
+
* Whether a bound toolkit reported having no registry. Until then, or until a
|
|
154
|
+
* registry settles, the empty placeholder reports no missing registry, since
|
|
155
|
+
* the toolkit may supply one.
|
|
156
|
+
*/
|
|
157
|
+
toolRegistryAbsent = false;
|
|
130
158
|
toolContextResolvers = new Map();
|
|
131
159
|
toolContextResolverChangeListeners = new Set();
|
|
132
160
|
toolRequests = new ToolRequestRegistry();
|
|
@@ -153,14 +181,16 @@ export class ToolkitCoordinator {
|
|
|
153
181
|
* Phase D: map key of the section controller currently treated as
|
|
154
182
|
* the *active cohort*. Set by `getOrCreateSectionController` (both
|
|
155
183
|
* the create-new and resolve-existing paths) and cleared when the
|
|
156
|
-
* matching controller is disposed
|
|
157
|
-
*
|
|
184
|
+
* matching controller is disposed, and `null` while the next requested
|
|
185
|
+
* cohort is still starting.
|
|
158
186
|
*/
|
|
159
187
|
activeCohortMapKey = null;
|
|
160
188
|
/**
|
|
161
189
|
* Latest requested active cohort. Async controller initialization can
|
|
162
190
|
* resolve out of order; only the most recent request may migrate active
|
|
163
191
|
* subscriptions. Older completions still populate the controller cache.
|
|
192
|
+
* `null` until the first request, which is the only state where
|
|
193
|
+
* `subscribeSectionEvents` throws.
|
|
164
194
|
*/
|
|
165
195
|
latestRequestedActiveCohortMapKey = null;
|
|
166
196
|
telemetryListeners = new Set();
|
|
@@ -176,32 +206,20 @@ export class ToolkitCoordinator {
|
|
|
176
206
|
*/
|
|
177
207
|
policyEngine;
|
|
178
208
|
/**
|
|
179
|
-
* Host-set override for PNP/profile enforcement
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* actually carry (see {@link resolveEffectivePnpEnforcement}).
|
|
209
|
+
* Host-set override for PNP/profile enforcement, passed to the engine.
|
|
210
|
+
* `null` (the default) is auto-mode, which the engine resolves per decision
|
|
211
|
+
* from the policy material the bound assessment and the decision's item carry.
|
|
183
212
|
* `"on"` / `"off"` are explicit host opt-in / opt-out and stick
|
|
184
|
-
* across subsequent assessment
|
|
213
|
+
* across subsequent assessment swaps until the host clears
|
|
185
214
|
* the override by calling `setPnpEnforcement(null)`.
|
|
186
215
|
*/
|
|
187
216
|
pnpEnforcementOverride = null;
|
|
188
217
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* this mirror exists only so the auto-mode helper does not need
|
|
193
|
-
* to round-trip through {@link policyEngine}'s frozen snapshot.
|
|
194
|
-
*/
|
|
195
|
-
boundAssessment = null;
|
|
196
|
-
/**
|
|
197
|
-
* Last item reference passed to {@link updateCurrentItemRef}.
|
|
198
|
-
* Mirrored alongside {@link boundAssessment} so
|
|
199
|
-
* {@link resolveEffectivePnpEnforcement} can detect item-level
|
|
200
|
-
* profile policy inputs (`requiredTools` / `restrictedTools` /
|
|
201
|
-
* `toolParameters`) without round-tripping through the engine's
|
|
202
|
-
* frozen snapshot.
|
|
218
|
+
* Policy diagnostics already logged, keyed by code, tool id and, for
|
|
219
|
+
* `tool-policy.itemSettingNotApplied`, item id; see
|
|
220
|
+
* {@link warnPolicyDiagnostics}.
|
|
203
221
|
*/
|
|
204
|
-
|
|
222
|
+
warnedPolicyDiagnostics = new Set();
|
|
205
223
|
/**
|
|
206
224
|
* Whether {@link decideFeaturePolicy} has already reported serving a decision
|
|
207
225
|
* with no assessment bound.
|
|
@@ -213,22 +231,40 @@ export class ToolkitCoordinator {
|
|
|
213
231
|
* lifetime for no new information.
|
|
214
232
|
*/
|
|
215
233
|
reportedUnboundFeaturePolicy = false;
|
|
216
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Whether {@link _initializeTTS} has already reported a non-browser backend
|
|
236
|
+
* falling back to browser speech because no text-to-speech provider is registered.
|
|
237
|
+
* Once per coordinator: a text-to-speech config change re-runs
|
|
238
|
+
* initialization, and the missing provider is the same gap each time.
|
|
239
|
+
*/
|
|
240
|
+
reportedMissingTTSProvider = false;
|
|
241
|
+
/**
|
|
242
|
+
* The backend of a fallback {@link reportMissingTTSProvider} held back while
|
|
243
|
+
* the registry was unsettled, reported if the bound toolkit has no registry.
|
|
244
|
+
*/
|
|
245
|
+
heldMissingTTSProviderBackend = null;
|
|
246
|
+
/**
|
|
247
|
+
* Whether {@link validateToolsConfig} has already reported validating with
|
|
248
|
+
* no registry (`tools.registryUnavailable`). Once per coordinator: every
|
|
249
|
+
* {@link updateToolConfig} and {@link updateToolsPlacement} re-validates, and
|
|
250
|
+
* the missing registry is the same gap each time.
|
|
251
|
+
*/
|
|
252
|
+
reportedRegistryUnavailable = false;
|
|
253
|
+
resolveConfig(config) {
|
|
217
254
|
const strictness = normalizeToolConfigStrictness(config.toolConfigStrictness);
|
|
218
|
-
// An empty
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// against an empty registry (with a diagnostic saying so) rather than
|
|
255
|
+
// An empty placeholder when the host supplies none, replaced by the bound
|
|
256
|
+
// toolkit's registry in `adoptToolRegistry`. This package does not hold the
|
|
257
|
+
// packaged capability set, and importing it would be a dependency cycle.
|
|
258
|
+
// Tool-id validation is skipped against an empty registry rather than
|
|
223
259
|
// rejecting every configured id.
|
|
224
260
|
const toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
225
261
|
const normalized = config.deferToolConfigValidation === true
|
|
226
262
|
? normalizeToolsConfig(config.tools)
|
|
227
|
-
:
|
|
263
|
+
: this.validateToolsConfig(config.tools, {
|
|
228
264
|
strictness,
|
|
229
265
|
source: "ToolkitCoordinator.init",
|
|
230
266
|
toolRegistry,
|
|
231
|
-
})
|
|
267
|
+
});
|
|
232
268
|
const defaultProviders = {
|
|
233
269
|
textToSpeech: {
|
|
234
270
|
enabled: true,
|
|
@@ -256,17 +292,38 @@ export class ToolkitCoordinator {
|
|
|
256
292
|
},
|
|
257
293
|
};
|
|
258
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* `normalizeAndValidateToolsConfig`, except that `tools.registryUnavailable`
|
|
297
|
+
* is reported once per coordinator, whichever call validates.
|
|
298
|
+
*/
|
|
299
|
+
validateToolsConfig(tools, options) {
|
|
300
|
+
const { config, diagnostics } = collectToolConfigDiagnostics(tools, options.toolRegistry);
|
|
301
|
+
const unreported = this.reportedRegistryUnavailable || this.toolRegistryPending()
|
|
302
|
+
? diagnostics.filter((entry) => entry.code !== "tools.registryUnavailable")
|
|
303
|
+
: diagnostics;
|
|
304
|
+
reportToolConfigDiagnostics(unreported, options);
|
|
305
|
+
if (options.strictness !== "off" &&
|
|
306
|
+
unreported.some((entry) => entry.code === "tools.registryUnavailable")) {
|
|
307
|
+
this.reportedRegistryUnavailable = true;
|
|
308
|
+
}
|
|
309
|
+
return config;
|
|
310
|
+
}
|
|
259
311
|
constructor(config) {
|
|
260
312
|
if (!config.assessmentId) {
|
|
261
313
|
throw new Error("ToolkitCoordinator requires assessmentId in config");
|
|
262
314
|
}
|
|
263
|
-
|
|
315
|
+
this.toolRegistrySupplied = config.toolRegistry != null;
|
|
316
|
+
this.toolRegistrySettled = this.toolRegistrySupplied;
|
|
317
|
+
const resolvedConfig = this.resolveConfig(config);
|
|
264
318
|
this.assessmentId = resolvedConfig.assessmentId;
|
|
265
319
|
this.config = resolvedConfig;
|
|
266
320
|
this.toolRegistry = resolvedConfig.toolRegistry ?? new ToolRegistry();
|
|
267
321
|
this.installToolContextResolvers(resolvedConfig.toolContextResolvers);
|
|
268
|
-
|
|
322
|
+
// The coordinator's own copy, so `setHooks` leaves the host's object as it
|
|
323
|
+
// passed it.
|
|
324
|
+
this.hooks = { ...resolvedConfig.hooks };
|
|
269
325
|
this.lazyInit = config.lazyInit === true;
|
|
326
|
+
this.eagerInit = config.eagerInit ?? !this.lazyInit;
|
|
270
327
|
this.pnpEnforcementOverride = this.resolveConfiguredPnpEnforcement();
|
|
271
328
|
// Use the host-provided framework-error bus if one was passed
|
|
272
329
|
// (typical when embedded inside <pie-assessment-toolkit>, so
|
|
@@ -286,31 +343,38 @@ export class ToolkitCoordinator {
|
|
|
286
343
|
this._registerToolProviders();
|
|
287
344
|
// Initialize TTS service based on config
|
|
288
345
|
this.ttsService = new TTSService();
|
|
346
|
+
// Selection read-aloud speaks through this service without the inline TTS
|
|
347
|
+
// tool ever having run, so it cannot rely on that tool to attach highlights.
|
|
348
|
+
this.ttsService.setHighlightCoordinator(this.highlightCoordinator);
|
|
349
|
+
// A caller that speaks without `ensureTTSReady()` first, such as selection
|
|
350
|
+
// read-aloud, starts the service instead of finding it uninitialized.
|
|
351
|
+
this.ttsService.setReadinessGate(() => this.ensureTTSReady());
|
|
352
|
+
this.ttsService.setMathSpeechSource(() => buildRuntimeTTSConfig(resolveTTSRuntimeSettings(this.resolveTTSToolConfig())).providerOptions?.mathSpeech);
|
|
289
353
|
this.setupStatePersistenceHooks();
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// actual profile material. Hosts that only consume the
|
|
296
|
-
// engine for placement/policy gating get the pre-PR-2 behavior
|
|
297
|
-
// bit-for-bit.
|
|
354
|
+
// The unified ToolPolicyEngine, seeded with the validated tools config.
|
|
355
|
+
// The assessment starts `null` and items register their settings as they
|
|
356
|
+
// mount; auto-mode enforcement turns on only once the policy material a
|
|
357
|
+
// decision reads is present, so a host that consumes the engine for
|
|
358
|
+
// placement/policy gating alone sees no PNP/profile gate.
|
|
298
359
|
this.policyEngine = new ToolPolicyEngine({
|
|
299
360
|
toolRegistry: this.toolRegistry,
|
|
300
361
|
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
301
362
|
inputs: {
|
|
302
363
|
tools: this.config.tools,
|
|
303
364
|
assessment: null,
|
|
304
|
-
|
|
305
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
365
|
+
pnpEnforcement: this.pnpEnforcementOverride,
|
|
306
366
|
},
|
|
307
367
|
});
|
|
308
|
-
|
|
368
|
+
this.policyEngine.onPolicyChange((event) => {
|
|
369
|
+
if (event.reason !== "disposed")
|
|
370
|
+
this.reportNewlyGrantedFailures();
|
|
371
|
+
});
|
|
372
|
+
if (this.eagerInit) {
|
|
309
373
|
void this.waitUntilReady().catch((err) => {
|
|
310
374
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
311
375
|
return;
|
|
312
376
|
console.error("[ToolkitCoordinator] Failed eager initialization:", err);
|
|
313
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
377
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
314
378
|
});
|
|
315
379
|
}
|
|
316
380
|
}
|
|
@@ -394,13 +458,92 @@ export class ToolkitCoordinator {
|
|
|
394
458
|
}
|
|
395
459
|
}
|
|
396
460
|
}
|
|
397
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Every report site states whether the coordinator carries on after the
|
|
463
|
+
* failure; one that takes down only its section passes `scope: "cohort"`.
|
|
464
|
+
*/
|
|
465
|
+
handleError(error, context, scope = "runtime") {
|
|
398
466
|
const model = frameworkErrorFromCoordinatorContext({
|
|
399
467
|
error,
|
|
400
468
|
context,
|
|
469
|
+
recoverable: context.recoverable,
|
|
470
|
+
scope,
|
|
401
471
|
});
|
|
402
472
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
403
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Whether policy grants `toolId` as an accommodation on some surface: a
|
|
476
|
+
* test-administration override, a mounted item's or a district requirement,
|
|
477
|
+
* or profile support. Read without the unbound-assessment warning.
|
|
478
|
+
*/
|
|
479
|
+
isToolGranted(toolId) {
|
|
480
|
+
try {
|
|
481
|
+
return this.policyEngine.grantsFeatureAnywhere(toolId);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Reports a tool's start failure: recoverable, so the tool reports itself
|
|
489
|
+
* unavailable and the assessment goes on, unless policy grants the tool.
|
|
490
|
+
* `fallbackFollows` marks a failure another path absorbs, which is recoverable
|
|
491
|
+
* whatever the policy.
|
|
492
|
+
*/
|
|
493
|
+
reportToolFailure(error, context, toolIds, fallbackFollows = false) {
|
|
494
|
+
const ids = Array.from(toolIds);
|
|
495
|
+
const recoverable = fallbackFollows || !ids.some((toolId) => this.isToolGranted(toolId));
|
|
496
|
+
if (recoverable && !fallbackFollows) {
|
|
497
|
+
for (const toolId of ids)
|
|
498
|
+
this.degradedTools.set(toolId, { error, context });
|
|
499
|
+
}
|
|
500
|
+
this.handleError(error, { ...context, recoverable });
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Report that a toolbar could not load a tool's module. The failure follows
|
|
504
|
+
* the start-failure policy: the tool degrades, so the toolbar withholds it,
|
|
505
|
+
* unless policy grants it. Reported once per tool however many toolbars fail
|
|
506
|
+
* to load it. Every report re-announces the request targets, since the
|
|
507
|
+
* reporting toolbar no longer hosts the tool.
|
|
508
|
+
*/
|
|
509
|
+
reportToolModuleFailure(toolId, error) {
|
|
510
|
+
this.toolRequests.notifyTargetsChange();
|
|
511
|
+
if (this.toolModuleFailures.has(toolId))
|
|
512
|
+
return;
|
|
513
|
+
this.toolModuleFailures.add(toolId);
|
|
514
|
+
const detail = error instanceof Error && error.message.trim().length > 0
|
|
515
|
+
? error.message
|
|
516
|
+
: String(error);
|
|
517
|
+
this.reportToolFailure(new Error(`Tool "${toolId}" failed to load: ${detail}`, { cause: error }), { phase: "tool-module-load", details: { toolId } }, [toolId]);
|
|
518
|
+
}
|
|
519
|
+
reportNewlyGrantedFailures() {
|
|
520
|
+
for (const [toolId, failure] of this.degradedTools) {
|
|
521
|
+
if (!this.isToolGranted(toolId))
|
|
522
|
+
continue;
|
|
523
|
+
this.degradedTools.delete(toolId);
|
|
524
|
+
this.handleError(failure.error, { ...failure.context, recoverable: false });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Subscribe to changes of {@link isReady}: state loading, text-to-speech
|
|
529
|
+
* starting, failing or reconfiguring. The listener reads the status itself.
|
|
530
|
+
*/
|
|
531
|
+
onReadyChange(listener) {
|
|
532
|
+
this.readyChangeListeners.add(listener);
|
|
533
|
+
return () => {
|
|
534
|
+
this.readyChangeListeners.delete(listener);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
notifyReadyChange() {
|
|
538
|
+
for (const listener of this.readyChangeListeners) {
|
|
539
|
+
try {
|
|
540
|
+
listener();
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.warn("[ToolkitCoordinator] ready-change listener failed:", error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
404
547
|
/**
|
|
405
548
|
* Subscribe to framework-error events emitted by this coordinator.
|
|
406
549
|
*
|
|
@@ -444,15 +587,20 @@ export class ToolkitCoordinator {
|
|
|
444
587
|
return null;
|
|
445
588
|
}
|
|
446
589
|
})();
|
|
590
|
+
// Stored per attempt. Without an attempt id nothing tells two learners on
|
|
591
|
+
// one device apart, so the section is neither read nor written.
|
|
447
592
|
const getStorageKey = (context) => {
|
|
448
593
|
const { assessmentId, sectionId, attemptId } = context.key;
|
|
449
|
-
|
|
594
|
+
if (!attemptId)
|
|
595
|
+
return null;
|
|
596
|
+
return `pie:section-controller:v1:${assessmentId}:${sectionId}:${attemptId}`;
|
|
450
597
|
};
|
|
451
598
|
return {
|
|
452
599
|
async loadSession(context) {
|
|
453
|
-
|
|
600
|
+
const key = getStorageKey(context);
|
|
601
|
+
if (!storage || !key)
|
|
454
602
|
return null;
|
|
455
|
-
const value = storage.getItem(
|
|
603
|
+
const value = storage.getItem(key);
|
|
456
604
|
if (!value)
|
|
457
605
|
return null;
|
|
458
606
|
try {
|
|
@@ -463,14 +611,16 @@ export class ToolkitCoordinator {
|
|
|
463
611
|
}
|
|
464
612
|
},
|
|
465
613
|
async saveSession(context, session) {
|
|
466
|
-
|
|
614
|
+
const key = getStorageKey(context);
|
|
615
|
+
if (!storage || !key)
|
|
467
616
|
return;
|
|
468
|
-
storage.setItem(
|
|
617
|
+
storage.setItem(key, JSON.stringify(session));
|
|
469
618
|
},
|
|
470
619
|
async clearSession(context) {
|
|
471
|
-
|
|
620
|
+
const key = getStorageKey(context);
|
|
621
|
+
if (!storage || !key)
|
|
472
622
|
return;
|
|
473
|
-
storage.removeItem(
|
|
623
|
+
storage.removeItem(key);
|
|
474
624
|
},
|
|
475
625
|
};
|
|
476
626
|
}
|
|
@@ -492,12 +642,12 @@ export class ToolkitCoordinator {
|
|
|
492
642
|
if (!this.hooks.saveToolState)
|
|
493
643
|
return;
|
|
494
644
|
void Promise.resolve(this.hooks.saveToolState(state)).catch((err) => {
|
|
495
|
-
this.handleError(err, { phase: "state-save" });
|
|
645
|
+
this.handleError(err, { phase: "state-save", recoverable: true });
|
|
496
646
|
});
|
|
497
647
|
});
|
|
498
648
|
}
|
|
499
649
|
async ensureStateLoaded() {
|
|
500
|
-
if (this.
|
|
650
|
+
if (this.stateLoadSettled)
|
|
501
651
|
return;
|
|
502
652
|
if (this.stateLoadPromise)
|
|
503
653
|
return this.stateLoadPromise;
|
|
@@ -513,6 +663,7 @@ export class ToolkitCoordinator {
|
|
|
513
663
|
this.elementToolStateStore.loadState(state);
|
|
514
664
|
}
|
|
515
665
|
this.stateLoaded = true;
|
|
666
|
+
this.stateLoadSettled = true;
|
|
516
667
|
await this.emitTelemetry("pie-toolkit-tool-state-loaded", {
|
|
517
668
|
hasState: Boolean(state),
|
|
518
669
|
});
|
|
@@ -520,13 +671,89 @@ export class ToolkitCoordinator {
|
|
|
520
671
|
catch (err) {
|
|
521
672
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
522
673
|
throw err;
|
|
523
|
-
this.
|
|
674
|
+
this.stateLoadSettled = true;
|
|
675
|
+
this.handleError(err, { phase: "state-load", recoverable: true });
|
|
524
676
|
}
|
|
525
677
|
})().finally(() => {
|
|
526
678
|
this.stateLoadPromise = undefined;
|
|
679
|
+
this.notifyReadyChange();
|
|
527
680
|
});
|
|
528
681
|
return this.stateLoadPromise;
|
|
529
682
|
}
|
|
683
|
+
/** Whether the toolkit this coordinator binds to may still supply a registry. */
|
|
684
|
+
toolRegistryPending() {
|
|
685
|
+
return !this.toolRegistrySettled && !this.toolRegistryAbsent;
|
|
686
|
+
}
|
|
687
|
+
reportMissingTTSProvider(backend) {
|
|
688
|
+
// The toolkit this coordinator binds to may still supply the provider.
|
|
689
|
+
if (this.toolRegistryPending()) {
|
|
690
|
+
this.heldMissingTTSProviderBackend = backend;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (this.reportedMissingTTSProvider)
|
|
694
|
+
return;
|
|
695
|
+
this.reportedMissingTTSProvider = true;
|
|
696
|
+
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.`);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Take the registry of the toolkit this coordinator is bound to, when the host
|
|
700
|
+
* constructed it without one: the config is validated against it, its
|
|
701
|
+
* providers register, and text-to-speech re-initializes through it if it
|
|
702
|
+
* already started. The toolkit elements call this on binding, and again when
|
|
703
|
+
* their registry changes. A registry passed at construction stands, and the
|
|
704
|
+
* first registry adopted wins; `null` records a toolkit without one, which
|
|
705
|
+
* reports the registry as missing and still adopts one that arrives later.
|
|
706
|
+
*
|
|
707
|
+
* @returns Whether `registry` was adopted.
|
|
708
|
+
*/
|
|
709
|
+
adoptToolRegistry(registry) {
|
|
710
|
+
if (this.disposePromise !== null || this.toolRegistrySettled)
|
|
711
|
+
return false;
|
|
712
|
+
if (!registry && this.toolRegistryAbsent)
|
|
713
|
+
return false;
|
|
714
|
+
const heldBackend = this.heldMissingTTSProviderBackend;
|
|
715
|
+
this.heldMissingTTSProviderBackend = null;
|
|
716
|
+
const strictness = this.config.toolConfigStrictness ?? "error";
|
|
717
|
+
const source = "ToolkitCoordinator.adoptToolRegistry";
|
|
718
|
+
if (!registry) {
|
|
719
|
+
this.toolRegistryAbsent = true;
|
|
720
|
+
this.validateToolsConfig(this.config.tools, {
|
|
721
|
+
strictness,
|
|
722
|
+
source,
|
|
723
|
+
toolRegistry: this.toolRegistry,
|
|
724
|
+
});
|
|
725
|
+
if (heldBackend)
|
|
726
|
+
this.reportMissingTTSProvider(heldBackend);
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
this.toolRegistrySettled = true;
|
|
730
|
+
this.toolRegistry = registry;
|
|
731
|
+
this.config.toolRegistry = registry;
|
|
732
|
+
try {
|
|
733
|
+
// Warn at most: the config was accepted at construction, and a binding
|
|
734
|
+
// toolkit has no caller to throw to.
|
|
735
|
+
this.config.tools = this.validateToolsConfig(this.config.tools, {
|
|
736
|
+
strictness: strictness === "off" ? "off" : "warn",
|
|
737
|
+
source,
|
|
738
|
+
toolRegistry: registry,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
console.warn("[ToolkitCoordinator] Tool config does not validate against the adopted tool registry:", err);
|
|
743
|
+
}
|
|
744
|
+
this.policyEngine.replaceToolRegistry(registry, this.config.tools);
|
|
745
|
+
const ttsStarted = this.ttsInitialized ||
|
|
746
|
+
this.ttsInitPromise !== undefined ||
|
|
747
|
+
this.ttsReconfigurePromise !== undefined;
|
|
748
|
+
for (const tool of this.getProviderDescriptorTools()) {
|
|
749
|
+
if (ttsStarted && tool.toolId === "textToSpeech")
|
|
750
|
+
continue;
|
|
751
|
+
void this.registerProviderFromTool(tool);
|
|
752
|
+
}
|
|
753
|
+
if (ttsStarted)
|
|
754
|
+
this.scheduleTTSReconfigure();
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
530
757
|
/**
|
|
531
758
|
* Register tool providers in the registry
|
|
532
759
|
*/
|
|
@@ -539,61 +766,63 @@ export class ToolkitCoordinator {
|
|
|
539
766
|
getProviderDescriptorTools() {
|
|
540
767
|
return this.toolRegistry.getAllTools().filter((tool) => !!tool.provider);
|
|
541
768
|
}
|
|
542
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Register the provider `tool`'s descriptor creates for the tool's current
|
|
771
|
+
* config under the tool's id, unless the tool is disabled or, without
|
|
772
|
+
* `replace`, a provider is registered under that id. A descriptor that throws
|
|
773
|
+
* is reported the way a failed registration is.
|
|
774
|
+
*/
|
|
775
|
+
async registerProviderFromTool(tool, replace = false) {
|
|
543
776
|
const descriptor = tool.provider;
|
|
544
777
|
if (!descriptor)
|
|
545
778
|
return;
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
toolConfig?.
|
|
551
|
-
|
|
552
|
-
|
|
779
|
+
const { toolId } = tool;
|
|
780
|
+
let registration;
|
|
781
|
+
try {
|
|
782
|
+
const toolConfig = this.getToolConfig(toolId) || undefined;
|
|
783
|
+
if (toolConfig?.enabled === false) {
|
|
784
|
+
// A disabled tool keeps no provider, including the one it replaces.
|
|
785
|
+
if (replace)
|
|
786
|
+
void this.toolProviderRegistry.unregister(toolId);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (!replace && this.toolProviderRegistry.has(toolId))
|
|
790
|
+
return;
|
|
791
|
+
const provider = descriptor.createProvider(toolConfig);
|
|
792
|
+
const initConfig = descriptor.getInitConfig?.(toolConfig) ??
|
|
793
|
+
toolConfig?.provider?.init ??
|
|
794
|
+
{};
|
|
795
|
+
const initConfigWithTelemetry = this.addToolTelemetryReporter(toolId, initConfig);
|
|
796
|
+
const registryTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
797
|
+
const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
|
|
798
|
+
toolConfig?.provider?.runtime?.authFetcher;
|
|
799
|
+
registration = {
|
|
800
|
+
provider,
|
|
801
|
+
config: initConfigWithTelemetry,
|
|
802
|
+
lazy: descriptor.lazy ?? true,
|
|
803
|
+
authFetcher,
|
|
804
|
+
onTelemetry: registryTelemetry,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
553
809
|
return;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
toolConfig?.provider?.init ??
|
|
557
|
-
{};
|
|
558
|
-
const initConfigWithTelemetry = this.addToolTelemetryReporter({
|
|
559
|
-
toolId: tool.toolId,
|
|
560
|
-
providerId,
|
|
561
|
-
initConfig,
|
|
562
|
-
});
|
|
563
|
-
const registryTelemetry = this.createToolTelemetryForwarder({
|
|
564
|
-
toolId: tool.toolId,
|
|
565
|
-
providerId,
|
|
566
|
-
});
|
|
567
|
-
const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
|
|
568
|
-
toolConfig?.provider?.runtime?.authFetcher;
|
|
569
|
-
await this.registerProvider(providerId, {
|
|
570
|
-
provider,
|
|
571
|
-
config: initConfigWithTelemetry,
|
|
572
|
-
lazy: descriptor.lazy ?? true,
|
|
573
|
-
authFetcher,
|
|
574
|
-
onTelemetry: registryTelemetry,
|
|
575
|
-
});
|
|
810
|
+
}
|
|
811
|
+
await this.registerProvider(toolId, registration);
|
|
576
812
|
}
|
|
577
|
-
createToolTelemetryForwarder(
|
|
813
|
+
createToolTelemetryForwarder(toolId) {
|
|
578
814
|
return async (eventName, payload) => {
|
|
579
|
-
await this.emitTelemetry(eventName, {
|
|
580
|
-
...(payload || {}),
|
|
581
|
-
toolId: args.toolId,
|
|
582
|
-
providerId: args.providerId,
|
|
583
|
-
});
|
|
815
|
+
await this.emitTelemetry(eventName, { ...(payload || {}), toolId });
|
|
584
816
|
};
|
|
585
817
|
}
|
|
586
|
-
addToolTelemetryReporter(
|
|
587
|
-
const configObject =
|
|
588
|
-
? { ...
|
|
818
|
+
addToolTelemetryReporter(toolId, initConfig) {
|
|
819
|
+
const configObject = initConfig && typeof initConfig === "object"
|
|
820
|
+
? { ...initConfig }
|
|
589
821
|
: {};
|
|
590
822
|
const existingReporter = typeof configObject.onTelemetry === "function"
|
|
591
823
|
? configObject.onTelemetry
|
|
592
824
|
: null;
|
|
593
|
-
const forwardTelemetry = this.createToolTelemetryForwarder(
|
|
594
|
-
toolId: args.toolId,
|
|
595
|
-
providerId: args.providerId,
|
|
596
|
-
});
|
|
825
|
+
const forwardTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
597
826
|
configObject.onTelemetry = async (eventName, payload) => {
|
|
598
827
|
if (existingReporter) {
|
|
599
828
|
await existingReporter(eventName, payload);
|
|
@@ -602,44 +831,58 @@ export class ToolkitCoordinator {
|
|
|
602
831
|
};
|
|
603
832
|
return configObject;
|
|
604
833
|
}
|
|
605
|
-
async registerProvider(
|
|
834
|
+
async registerProvider(toolId, config) {
|
|
606
835
|
try {
|
|
607
|
-
this.toolProviderRegistry.register(
|
|
836
|
+
this.toolProviderRegistry.register(toolId, config);
|
|
608
837
|
const meta = {
|
|
609
|
-
providerId,
|
|
610
838
|
providerName: config.provider.providerName,
|
|
611
839
|
};
|
|
612
|
-
await this.hooks.onProviderRegistered?.(
|
|
840
|
+
await this.hooks.onProviderRegistered?.(toolId, meta);
|
|
613
841
|
await this.emitTelemetry("pie-toolkit-provider-registered", {
|
|
614
|
-
|
|
842
|
+
toolId,
|
|
615
843
|
providerName: config.provider.providerName,
|
|
616
844
|
});
|
|
617
845
|
}
|
|
618
846
|
catch (err) {
|
|
619
|
-
|
|
620
|
-
this.handleError(err, { phase: "provider-register", providerId });
|
|
847
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
621
848
|
}
|
|
622
849
|
}
|
|
623
|
-
|
|
850
|
+
/**
|
|
851
|
+
* A console warning and a `provider-register` framework error, under the
|
|
852
|
+
* start-failure policy every other tool failure follows.
|
|
853
|
+
*/
|
|
854
|
+
reportProviderRegisterFailure(err, toolId) {
|
|
855
|
+
console.warn(`[ToolkitCoordinator] Failed to register the provider of tool "${toolId}":`, err);
|
|
856
|
+
this.reportToolFailure(err, { phase: "provider-register", toolId }, [toolId]);
|
|
857
|
+
}
|
|
858
|
+
async ensureProviderReady(toolId) {
|
|
859
|
+
return this.initializeProvider(toolId, false);
|
|
860
|
+
}
|
|
861
|
+
/** `fallbackFollows`: the caller recovers from a failure on its own. */
|
|
862
|
+
async initializeProvider(toolId, fallbackFollows) {
|
|
624
863
|
this.assertNotDisposed();
|
|
625
|
-
const existing = this.providerInitPromises.get(
|
|
864
|
+
const existing = this.providerInitPromises.get(toolId);
|
|
626
865
|
if (existing)
|
|
627
866
|
return existing;
|
|
628
867
|
const promise = (async () => {
|
|
629
|
-
const provider = await this.toolProviderRegistry.getProvider(providerId, false);
|
|
630
|
-
this.assertNotDisposed();
|
|
631
|
-
const meta = {
|
|
632
|
-
providerId,
|
|
633
|
-
providerName: provider.providerName,
|
|
634
|
-
};
|
|
635
868
|
try {
|
|
636
|
-
await this.
|
|
869
|
+
let provider = await this.toolProviderRegistry.getProvider(toolId, false);
|
|
637
870
|
this.assertNotDisposed();
|
|
638
|
-
|
|
871
|
+
// A tool asks each time it opens; the lifecycle hooks report the start once.
|
|
872
|
+
if (this.toolProviderRegistry.isInitialized(toolId))
|
|
873
|
+
return provider;
|
|
874
|
+
const meta = {
|
|
875
|
+
providerName: provider.providerName,
|
|
876
|
+
};
|
|
877
|
+
await this.hooks.onProviderInitStart?.(toolId, meta);
|
|
639
878
|
this.assertNotDisposed();
|
|
640
|
-
await this.
|
|
879
|
+
await this.toolProviderRegistry.initialize(toolId);
|
|
641
880
|
this.assertNotDisposed();
|
|
642
|
-
|
|
881
|
+
// A config update may have replaced the provider during its start.
|
|
882
|
+
provider = await this.toolProviderRegistry.getProvider(toolId, false);
|
|
883
|
+
await this.hooks.onProviderReady?.(toolId, meta);
|
|
884
|
+
this.assertNotDisposed();
|
|
885
|
+
await this.emitTelemetry("pie-toolkit-provider-ready", { toolId });
|
|
643
886
|
this.assertNotDisposed();
|
|
644
887
|
return provider;
|
|
645
888
|
}
|
|
@@ -647,13 +890,16 @@ export class ToolkitCoordinator {
|
|
|
647
890
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
648
891
|
throw err;
|
|
649
892
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
650
|
-
this.
|
|
893
|
+
this.reportToolFailure(error, { phase: "provider-init", toolId },
|
|
894
|
+
// Attributed when the id names a tool with a provider; any other id
|
|
895
|
+
// was never a tool's.
|
|
896
|
+
this.toolRegistry.get(toolId)?.provider ? [toolId] : [], fallbackFollows);
|
|
651
897
|
throw error;
|
|
652
898
|
}
|
|
653
899
|
})().finally(() => {
|
|
654
|
-
this.providerInitPromises.delete(
|
|
900
|
+
this.providerInitPromises.delete(toolId);
|
|
655
901
|
});
|
|
656
|
-
this.providerInitPromises.set(
|
|
902
|
+
this.providerInitPromises.set(toolId, promise);
|
|
657
903
|
return promise;
|
|
658
904
|
}
|
|
659
905
|
setHooks(hooks) {
|
|
@@ -663,7 +909,7 @@ export class ToolkitCoordinator {
|
|
|
663
909
|
hooks.onCoordinatorReady &&
|
|
664
910
|
this.isReady()) {
|
|
665
911
|
void Promise.resolve(hooks.onCoordinatorReady(this)).catch((err) => {
|
|
666
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
912
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
667
913
|
});
|
|
668
914
|
}
|
|
669
915
|
}
|
|
@@ -686,8 +932,11 @@ export class ToolkitCoordinator {
|
|
|
686
932
|
// cohort* automatically. We bind the listener to whichever
|
|
687
933
|
// controller is currently active and re-bind it on every
|
|
688
934
|
// `getOrCreateSectionController` / `disposeSectionController`
|
|
689
|
-
// transition, with snapshot replay on each migration.
|
|
690
|
-
|
|
935
|
+
// transition, with snapshot replay on each migration. A listener
|
|
936
|
+
// added while the next section is still starting binds when it
|
|
937
|
+
// becomes active.
|
|
938
|
+
if (this.activeCohortMapKey === null &&
|
|
939
|
+
this.latestRequestedActiveCohortMapKey === null) {
|
|
691
940
|
throw new Error("[ToolkitCoordinator] subscribeSectionEvents requires an active section cohort; call getOrCreateSectionController first.");
|
|
692
941
|
}
|
|
693
942
|
const previous = this.activeSubscriptions.get(args.listener);
|
|
@@ -702,7 +951,9 @@ export class ToolkitCoordinator {
|
|
|
702
951
|
unsubscribeCurrent: null,
|
|
703
952
|
};
|
|
704
953
|
this.activeSubscriptions.set(args.listener, sub);
|
|
705
|
-
const controller = this.
|
|
954
|
+
const controller = this.activeCohortMapKey === null
|
|
955
|
+
? undefined
|
|
956
|
+
: this.sectionControllers.get(this.activeCohortMapKey);
|
|
706
957
|
if (controller) {
|
|
707
958
|
this.bindSubscriptionToController(sub, controller);
|
|
708
959
|
}
|
|
@@ -980,18 +1231,29 @@ export class ToolkitCoordinator {
|
|
|
980
1231
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
981
1232
|
this.latestRequestedActiveCohortMapKey = mapKey;
|
|
982
1233
|
this.suspendActiveCohortIfSuperseded(mapKey);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1234
|
+
let existingController;
|
|
1235
|
+
try {
|
|
1236
|
+
existingController = await this.resolveExistingSectionController({
|
|
1237
|
+
mapKey,
|
|
1238
|
+
key,
|
|
1239
|
+
input: args.input,
|
|
1240
|
+
updateExisting: args.updateExisting,
|
|
1241
|
+
});
|
|
1242
|
+
if (existingController) {
|
|
1243
|
+
if (this.disposePromise !== null) {
|
|
1244
|
+
throw new ToolkitCoordinatorDisposedError();
|
|
1245
|
+
}
|
|
1246
|
+
if (args.initialSession) {
|
|
1247
|
+
await this.assignSessionToPublishedController(existingController, args.initialSession);
|
|
1248
|
+
}
|
|
992
1249
|
}
|
|
993
|
-
return existingController;
|
|
994
1250
|
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1253
|
+
throw err;
|
|
1254
|
+
}
|
|
1255
|
+
if (existingController)
|
|
1256
|
+
return existingController;
|
|
995
1257
|
const pendingDisposal = this.sectionControllerDisposePromises.get(mapKey);
|
|
996
1258
|
if (pendingDisposal) {
|
|
997
1259
|
// Persistence and hydration share the cohort's durable state. Keep the
|
|
@@ -1004,8 +1266,13 @@ export class ToolkitCoordinator {
|
|
|
1004
1266
|
throw new ToolkitCoordinatorDisposedError();
|
|
1005
1267
|
}
|
|
1006
1268
|
const existingEntry = this.sectionControllerInitEntries.get(mapKey);
|
|
1007
|
-
if (existingEntry)
|
|
1008
|
-
|
|
1269
|
+
if (existingEntry) {
|
|
1270
|
+
if (!args.initialSession)
|
|
1271
|
+
return existingEntry.promise;
|
|
1272
|
+
const pendingController = await existingEntry.promise;
|
|
1273
|
+
await this.assignSessionToPublishedController(pendingController, args.initialSession);
|
|
1274
|
+
return pendingController;
|
|
1275
|
+
}
|
|
1009
1276
|
const token = {
|
|
1010
1277
|
retired: false,
|
|
1011
1278
|
candidateClaimed: false,
|
|
@@ -1018,6 +1285,7 @@ export class ToolkitCoordinator {
|
|
|
1018
1285
|
})
|
|
1019
1286
|
.catch((err) => {
|
|
1020
1287
|
this.handleSectionControllerInitError(err, args);
|
|
1288
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1021
1289
|
throw err;
|
|
1022
1290
|
})
|
|
1023
1291
|
.finally(() => {
|
|
@@ -1098,7 +1366,20 @@ export class ToolkitCoordinator {
|
|
|
1098
1366
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1099
1367
|
await controller.initialize?.(args.args.input);
|
|
1100
1368
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1101
|
-
|
|
1369
|
+
// A session the host supplied takes the place of the strategy's stored
|
|
1370
|
+
// snapshot, and is applied before publication so the ready lifecycle
|
|
1371
|
+
// event and every reader after it see it. The strategy still receives
|
|
1372
|
+
// every `persist()`.
|
|
1373
|
+
const initialSession = args.args.initialSession;
|
|
1374
|
+
if (initialSession) {
|
|
1375
|
+
if (!controller.applySession) {
|
|
1376
|
+
throw new Error("Section controller cannot apply the session it was created with: it has no applySession.");
|
|
1377
|
+
}
|
|
1378
|
+
await controller.applySession(initialSession, { mode: "replace" });
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
await controller.hydrate?.();
|
|
1382
|
+
}
|
|
1102
1383
|
await this.finalizeSectionControllerReady({
|
|
1103
1384
|
...candidate,
|
|
1104
1385
|
context,
|
|
@@ -1110,6 +1391,22 @@ export class ToolkitCoordinator {
|
|
|
1110
1391
|
throw error;
|
|
1111
1392
|
}
|
|
1112
1393
|
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Apply a host-supplied session to a controller that is already published,
|
|
1396
|
+
* under the rules `resolveSectionSessionAssignment` sets: an equal session is a
|
|
1397
|
+
* no-op and recorded responses are not replaced by response-free ones.
|
|
1398
|
+
*/
|
|
1399
|
+
async assignSessionToPublishedController(controller, session) {
|
|
1400
|
+
if (!session)
|
|
1401
|
+
return;
|
|
1402
|
+
if (!controller.applySession) {
|
|
1403
|
+
throw new Error("Section controller cannot apply the assigned session: it has no applySession.");
|
|
1404
|
+
}
|
|
1405
|
+
const resolved = resolveSectionSessionAssignment(controller.getSession?.() ?? null, session);
|
|
1406
|
+
if (!resolved)
|
|
1407
|
+
return;
|
|
1408
|
+
await controller.applySession(resolved, { mode: "replace" });
|
|
1409
|
+
}
|
|
1113
1410
|
createSectionControllerRetirementError() {
|
|
1114
1411
|
return this.disposePromise !== null
|
|
1115
1412
|
? new ToolkitCoordinatorDisposedError()
|
|
@@ -1135,6 +1432,7 @@ export class ToolkitCoordinator {
|
|
|
1135
1432
|
sectionId: args.key.sectionId,
|
|
1136
1433
|
attemptId: args.key.attemptId,
|
|
1137
1434
|
},
|
|
1435
|
+
recoverable: true,
|
|
1138
1436
|
});
|
|
1139
1437
|
}
|
|
1140
1438
|
finally {
|
|
@@ -1190,7 +1488,33 @@ export class ToolkitCoordinator {
|
|
|
1190
1488
|
sectionId: args.sectionId,
|
|
1191
1489
|
attemptId: args.attemptId,
|
|
1192
1490
|
},
|
|
1193
|
-
|
|
1491
|
+
recoverable: false,
|
|
1492
|
+
}, "cohort");
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* A section that fails to start has no controller of its own to emit its
|
|
1496
|
+
* `section-error`, and the host's subscriptions are already off the outgoing
|
|
1497
|
+
* section, so the coordinator delivers the error to them while that section
|
|
1498
|
+
* is still the one requested.
|
|
1499
|
+
*/
|
|
1500
|
+
deliverSectionStartError(mapKey, error) {
|
|
1501
|
+
if (error instanceof ToolkitCoordinatorDisposedError ||
|
|
1502
|
+
error instanceof SectionControllerRetiredError ||
|
|
1503
|
+
this.latestRequestedActiveCohortMapKey !== mapKey) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const event = {
|
|
1507
|
+
type: "section-error",
|
|
1508
|
+
source: "section-runtime",
|
|
1509
|
+
error,
|
|
1510
|
+
currentItemIndex: 0,
|
|
1511
|
+
timestamp: Date.now(),
|
|
1512
|
+
};
|
|
1513
|
+
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
1514
|
+
if (this.buildSectionEventPredicate(sub)(event)) {
|
|
1515
|
+
this.deliverSectionEventSafely(sub.listener, event);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1194
1518
|
}
|
|
1195
1519
|
disposeSectionController(args) {
|
|
1196
1520
|
const key = {
|
|
@@ -1277,6 +1601,7 @@ export class ToolkitCoordinator {
|
|
|
1277
1601
|
sectionId: args.args.sectionId,
|
|
1278
1602
|
attemptId: args.args.attemptId,
|
|
1279
1603
|
},
|
|
1604
|
+
recoverable: true,
|
|
1280
1605
|
});
|
|
1281
1606
|
}
|
|
1282
1607
|
finally {
|
|
@@ -1376,7 +1701,7 @@ export class ToolkitCoordinator {
|
|
|
1376
1701
|
this.activeSubscriptions.clear();
|
|
1377
1702
|
this.activeCohortMapKey = null;
|
|
1378
1703
|
this.latestRequestedActiveCohortMapKey = null;
|
|
1379
|
-
await cleanup(() => this.ttsService.
|
|
1704
|
+
await cleanup(() => this.ttsService.dispose());
|
|
1380
1705
|
await cleanup(() => this.toolProviderRegistry.destroy());
|
|
1381
1706
|
await cleanup(() => this.highlightCoordinator.destroy());
|
|
1382
1707
|
for (const toolId of this.toolCoordinator.getRegisteredTools()) {
|
|
@@ -1430,6 +1755,7 @@ export class ToolkitCoordinator {
|
|
|
1430
1755
|
.then(() => this.assertNotDisposed())
|
|
1431
1756
|
.finally(() => {
|
|
1432
1757
|
this.ttsInitPromise = undefined;
|
|
1758
|
+
this.notifyReadyChange();
|
|
1433
1759
|
});
|
|
1434
1760
|
return this.ttsInitPromise;
|
|
1435
1761
|
}
|
|
@@ -1463,9 +1789,10 @@ export class ToolkitCoordinator {
|
|
|
1463
1789
|
backend: resolvedBackend,
|
|
1464
1790
|
});
|
|
1465
1791
|
// Try to use TTS provider from registry if available
|
|
1466
|
-
if (this.toolProviderRegistry.has("
|
|
1792
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
1467
1793
|
try {
|
|
1468
|
-
|
|
1794
|
+
// Browser speech follows a server provider that fails to start.
|
|
1795
|
+
const ttsProvider = await this.initializeProvider("textToSpeech", true);
|
|
1469
1796
|
const providerInstance = await ttsProvider.createInstance();
|
|
1470
1797
|
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
1471
1798
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
@@ -1477,25 +1804,14 @@ export class ToolkitCoordinator {
|
|
|
1477
1804
|
backend: resolvedBackend,
|
|
1478
1805
|
provider: "registry",
|
|
1479
1806
|
});
|
|
1480
|
-
console.log("[ToolkitCoordinator] TTS initialized via ToolProviderRegistry");
|
|
1481
1807
|
return;
|
|
1482
1808
|
}
|
|
1483
1809
|
catch (error) {
|
|
1484
1810
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1485
1811
|
throw error;
|
|
1486
1812
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1487
|
-
await this.emitTelemetry("pie-tool-init-error", {
|
|
1488
|
-
toolId: "textToSpeech",
|
|
1489
|
-
providerId: "tts",
|
|
1490
|
-
operation: "tts-init",
|
|
1491
|
-
backend: resolvedBackend,
|
|
1492
|
-
errorType: "TTSRegistryInitError",
|
|
1493
|
-
message: normalized.message,
|
|
1494
|
-
recovered: true,
|
|
1495
|
-
});
|
|
1496
1813
|
await this.emitTelemetry("pie-tool-init-fallback", {
|
|
1497
1814
|
toolId: "textToSpeech",
|
|
1498
|
-
providerId: "tts",
|
|
1499
1815
|
operation: "tts-init",
|
|
1500
1816
|
backend: resolvedBackend,
|
|
1501
1817
|
fromProvider: "registry",
|
|
@@ -1505,10 +1821,16 @@ export class ToolkitCoordinator {
|
|
|
1505
1821
|
console.warn("[ToolkitCoordinator] Failed to initialize TTS via registry, falling back to browser provider:", normalized);
|
|
1506
1822
|
}
|
|
1507
1823
|
}
|
|
1508
|
-
|
|
1824
|
+
else if (resolvedBackend !== "browser") {
|
|
1825
|
+
this.reportMissingTTSProvider(resolvedBackend);
|
|
1826
|
+
}
|
|
1827
|
+
// Browser speech, configured or standing in for a backend that has no
|
|
1828
|
+
// provider or failed to start.
|
|
1509
1829
|
const provider = new BrowserTTSProvider();
|
|
1510
1830
|
try {
|
|
1511
|
-
await this.initializeTTSService(provider,
|
|
1831
|
+
await this.initializeTTSService(provider, resolvedBackend === "browser"
|
|
1832
|
+
? runtimeTTSConfig
|
|
1833
|
+
: browserFallbackConfig(runtimeTTSConfig));
|
|
1512
1834
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
1513
1835
|
provider: "browser-fallback",
|
|
1514
1836
|
});
|
|
@@ -1523,7 +1845,7 @@ export class ToolkitCoordinator {
|
|
|
1523
1845
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1524
1846
|
throw error;
|
|
1525
1847
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1526
|
-
this.
|
|
1848
|
+
this.reportToolFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
|
|
1527
1849
|
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
1528
1850
|
message: normalized.message,
|
|
1529
1851
|
});
|
|
@@ -1542,7 +1864,7 @@ export class ToolkitCoordinator {
|
|
|
1542
1864
|
}
|
|
1543
1865
|
async initializeTTSService(provider, config) {
|
|
1544
1866
|
const nextProviderOptions = {
|
|
1545
|
-
...
|
|
1867
|
+
...config.providerOptions,
|
|
1546
1868
|
__pieTelemetry: async (eventName, payload) => {
|
|
1547
1869
|
await this.emitTelemetry(eventName, {
|
|
1548
1870
|
toolId: "textToSpeech",
|
|
@@ -1555,93 +1877,14 @@ export class ToolkitCoordinator {
|
|
|
1555
1877
|
providerOptions: nextProviderOptions,
|
|
1556
1878
|
};
|
|
1557
1879
|
await this.ttsService.initialize(provider, nextConfig);
|
|
1558
|
-
await this.ensureBrowserVoicesReady(provider);
|
|
1559
1880
|
this.assertNotDisposed();
|
|
1560
1881
|
this.ttsService.setCatalogResolver(this.catalogResolver);
|
|
1561
1882
|
this.ttsInitialized = true;
|
|
1883
|
+
this.ttsDegraded = false;
|
|
1884
|
+
this.degradedTools.delete("textToSpeech");
|
|
1562
1885
|
await this.hooks.onTTSReady?.();
|
|
1563
1886
|
this.assertNotDisposed();
|
|
1564
1887
|
}
|
|
1565
|
-
async ensureBrowserVoicesReady(provider, timeoutMs = 1200) {
|
|
1566
|
-
if (provider.providerId !== "browser")
|
|
1567
|
-
return;
|
|
1568
|
-
if (typeof window === "undefined")
|
|
1569
|
-
return;
|
|
1570
|
-
if (!("speechSynthesis" in window))
|
|
1571
|
-
return;
|
|
1572
|
-
const synth = window.speechSynthesis;
|
|
1573
|
-
const voices = synth.getVoices();
|
|
1574
|
-
if (voices.length > 0)
|
|
1575
|
-
return;
|
|
1576
|
-
await new Promise((resolve) => {
|
|
1577
|
-
let settled = false;
|
|
1578
|
-
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
1579
|
-
typeof synth.removeEventListener === "function";
|
|
1580
|
-
const canUseHandler = !canUseEventTarget && "onvoiceschanged" in synth;
|
|
1581
|
-
const previousHandler = canUseHandler ? synth.onvoiceschanged : null;
|
|
1582
|
-
let assignedHandler = false;
|
|
1583
|
-
const finish = () => {
|
|
1584
|
-
if (settled)
|
|
1585
|
-
return;
|
|
1586
|
-
settled = true;
|
|
1587
|
-
window.clearTimeout(timeoutId);
|
|
1588
|
-
if (canUseEventTarget) {
|
|
1589
|
-
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
1590
|
-
}
|
|
1591
|
-
else if (assignedHandler &&
|
|
1592
|
-
synth.onvoiceschanged === onVoicesChanged) {
|
|
1593
|
-
synth.onvoiceschanged = previousHandler;
|
|
1594
|
-
}
|
|
1595
|
-
resolve();
|
|
1596
|
-
};
|
|
1597
|
-
const onVoicesChanged = (event) => {
|
|
1598
|
-
if (!canUseEventTarget &&
|
|
1599
|
-
typeof previousHandler === "function" &&
|
|
1600
|
-
event) {
|
|
1601
|
-
previousHandler.call(synth, event);
|
|
1602
|
-
}
|
|
1603
|
-
finish();
|
|
1604
|
-
};
|
|
1605
|
-
const timeoutId = window.setTimeout(finish, timeoutMs);
|
|
1606
|
-
if (canUseEventTarget) {
|
|
1607
|
-
synth.addEventListener("voiceschanged", onVoicesChanged, {
|
|
1608
|
-
once: true,
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
else if (canUseHandler) {
|
|
1612
|
-
synth.onvoiceschanged = onVoicesChanged;
|
|
1613
|
-
assignedHandler = true;
|
|
1614
|
-
}
|
|
1615
|
-
});
|
|
1616
|
-
const voicesAfterWait = synth.getVoices();
|
|
1617
|
-
await this.emitTelemetry("pie-toolkit-tts-browser-voices-ready", {
|
|
1618
|
-
voiceCount: voicesAfterWait.length,
|
|
1619
|
-
timedOut: voicesAfterWait.length === 0,
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Get all services as a bundle for section player convenience.
|
|
1624
|
-
* Section player can extract and pass services to child components.
|
|
1625
|
-
*/
|
|
1626
|
-
getServiceBundle() {
|
|
1627
|
-
return {
|
|
1628
|
-
ttsService: this.ttsService,
|
|
1629
|
-
toolCoordinator: this.toolCoordinator,
|
|
1630
|
-
highlightCoordinator: this.highlightCoordinator,
|
|
1631
|
-
elementToolStateStore: this.elementToolStateStore,
|
|
1632
|
-
catalogResolver: this.catalogResolver,
|
|
1633
|
-
toolProviderRegistry: this.toolProviderRegistry,
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* Get a tool provider from the registry
|
|
1638
|
-
*
|
|
1639
|
-
* @param providerId Provider identifier
|
|
1640
|
-
* @returns Tool provider instance
|
|
1641
|
-
*/
|
|
1642
|
-
async getToolProvider(providerId) {
|
|
1643
|
-
return this.ensureProviderReady(providerId);
|
|
1644
|
-
}
|
|
1645
1888
|
async waitUntilReady() {
|
|
1646
1889
|
this.assertNotDisposed();
|
|
1647
1890
|
if (this.isReady())
|
|
@@ -1651,9 +1894,8 @@ export class ToolkitCoordinator {
|
|
|
1651
1894
|
this.coordinatorReadyPromise = (async () => {
|
|
1652
1895
|
await this.ensureStateLoaded();
|
|
1653
1896
|
this.assertNotDisposed();
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
await this.ensureTTSReady(ttsConfig);
|
|
1897
|
+
if (this.ttsRequiredForReadiness()) {
|
|
1898
|
+
await this.startTTSForReadiness();
|
|
1657
1899
|
}
|
|
1658
1900
|
this.assertNotDisposed();
|
|
1659
1901
|
if (!this.coordinatorReadyNotified) {
|
|
@@ -1670,41 +1912,52 @@ export class ToolkitCoordinator {
|
|
|
1670
1912
|
return this.coordinatorReadyPromise;
|
|
1671
1913
|
}
|
|
1672
1914
|
isReady() {
|
|
1673
|
-
return this.
|
|
1915
|
+
return ((this.stateLoadSettled || !this.hooks.loadToolState) &&
|
|
1916
|
+
(this.ttsInitialized ||
|
|
1917
|
+
this.ttsDegraded ||
|
|
1918
|
+
!this.ttsRequiredForReadiness()));
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Readiness waits for text-to-speech when it is enabled and either starts
|
|
1922
|
+
* eagerly or is granted: a granted read-aloud fails before the learner starts.
|
|
1923
|
+
*/
|
|
1924
|
+
ttsRequiredForReadiness() {
|
|
1925
|
+
if (this.getTTSConfigFromProviders()?.enabled === false)
|
|
1926
|
+
return false;
|
|
1927
|
+
return !this.lazyInit || this.isToolGranted("textToSpeech");
|
|
1674
1928
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1929
|
+
/**
|
|
1930
|
+
* Starts text-to-speech for readiness. A failure policy does not grant leaves
|
|
1931
|
+
* the tool degraded, reporting itself unavailable, and readiness settles; a
|
|
1932
|
+
* granted tool's failure rejects.
|
|
1933
|
+
*/
|
|
1934
|
+
async startTTSForReadiness() {
|
|
1935
|
+
try {
|
|
1936
|
+
await this.ensureTTSReady(this.getTTSConfigFromProviders());
|
|
1937
|
+
}
|
|
1938
|
+
catch (error) {
|
|
1939
|
+
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1940
|
+
throw error;
|
|
1941
|
+
if (this.isToolGranted("textToSpeech"))
|
|
1942
|
+
throw error;
|
|
1943
|
+
this.ttsDegraded = true;
|
|
1944
|
+
this.notifyReadyChange();
|
|
1680
1945
|
}
|
|
1681
|
-
const ttsConfig = this.getTTSConfigFromProviders();
|
|
1682
|
-
return {
|
|
1683
|
-
tts: this.ttsInitialized,
|
|
1684
|
-
stateLoaded: this.stateLoaded || !this.hooks.loadToolState,
|
|
1685
|
-
coordinator: (this.stateLoaded || !this.hooks.loadToolState) &&
|
|
1686
|
-
(this.ttsInitialized || ttsConfig?.enabled === false),
|
|
1687
|
-
providers,
|
|
1688
|
-
};
|
|
1689
1946
|
}
|
|
1690
1947
|
getTTSConfigFromProviders() {
|
|
1691
|
-
|
|
1692
|
-
return providers.textToSpeech;
|
|
1948
|
+
return this.config.tools?.providers?.textToSpeech;
|
|
1693
1949
|
}
|
|
1694
1950
|
assertCanonicalToolId(toolId) {
|
|
1695
1951
|
if (typeof toolId !== "string" || toolId.trim().length === 0) {
|
|
1696
1952
|
throw new Error("Tool id must be a non-empty string.");
|
|
1697
1953
|
}
|
|
1698
|
-
if (toolId === "tts") {
|
|
1699
|
-
throw new Error(`Tool id "tts" is no longer supported. Use "textToSpeech".`);
|
|
1700
|
-
}
|
|
1701
1954
|
// An empty registry means the host supplied none, not that every id is
|
|
1702
1955
|
// wrong. There is nothing to check an id against, and throwing turns a
|
|
1703
1956
|
// host's every tool-config call into an exception — including the calls
|
|
1704
1957
|
// this coordinator's own default-provider block provokes, which is how a
|
|
1705
1958
|
// host that passes no registry ended up unable to read its own config.
|
|
1706
|
-
//
|
|
1707
|
-
//
|
|
1959
|
+
// Validation reports the missing registry once per coordinator, as
|
|
1960
|
+
// `tools.registryUnavailable`; a report per call would be noise.
|
|
1708
1961
|
if (this.toolRegistry.getAllToolIds().length === 0)
|
|
1709
1962
|
return;
|
|
1710
1963
|
if (!this.toolRegistry.get(toolId)) {
|
|
@@ -1724,6 +1977,11 @@ export class ToolkitCoordinator {
|
|
|
1724
1977
|
this.toolContextResolvers.set(toolId, resolver);
|
|
1725
1978
|
}
|
|
1726
1979
|
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Replace all host-owned render-context resolvers. The toolkit element
|
|
1982
|
+
* calls this from `runtime.toolContextResolvers`; it is not on
|
|
1983
|
+
* `ToolkitCoordinatorApi`.
|
|
1984
|
+
*/
|
|
1727
1985
|
setToolContextResolvers(resolvers) {
|
|
1728
1986
|
this.toolContextResolvers.clear();
|
|
1729
1987
|
this.installToolContextResolvers(resolvers ?? undefined);
|
|
@@ -1739,39 +1997,20 @@ export class ToolkitCoordinator {
|
|
|
1739
1997
|
}
|
|
1740
1998
|
}
|
|
1741
1999
|
}
|
|
1742
|
-
/**
|
|
1743
|
-
* Check if a tool is enabled.
|
|
1744
|
-
* Tools are enabled by default unless explicitly disabled.
|
|
1745
|
-
*
|
|
1746
|
-
* @param toolId Tool identifier (e.g., 'textToSpeech', 'answerEliminator')
|
|
1747
|
-
* @returns True if tool is enabled
|
|
1748
|
-
*/
|
|
1749
|
-
isToolEnabled(toolId) {
|
|
1750
|
-
this.assertCanonicalToolId(toolId);
|
|
1751
|
-
const toolConfig = this.config.tools?.providers?.[toolId];
|
|
1752
|
-
// Enabled by default unless explicitly set to false
|
|
1753
|
-
return toolConfig?.enabled !== false;
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* Get tool configuration.
|
|
1757
|
-
*
|
|
1758
|
-
* @param toolId Tool identifier
|
|
1759
|
-
* @returns Tool configuration or null if not configured
|
|
1760
|
-
*/
|
|
1761
2000
|
getToolConfig(toolId) {
|
|
1762
2001
|
this.assertCanonicalToolId(toolId);
|
|
1763
|
-
return
|
|
2002
|
+
return this.config.tools?.providers?.[toolId] || null;
|
|
1764
2003
|
}
|
|
1765
2004
|
updateToolConfig(toolId, updates) {
|
|
1766
2005
|
// Update config
|
|
1767
2006
|
this.assertCanonicalToolId(toolId);
|
|
1768
2007
|
const current = this.getToolConfig(toolId) || {};
|
|
1769
2008
|
if (!this.config.tools) {
|
|
1770
|
-
this.config.tools =
|
|
2009
|
+
this.config.tools = this.validateToolsConfig(undefined, {
|
|
1771
2010
|
strictness: this.config.toolConfigStrictness ?? "error",
|
|
1772
2011
|
source: "ToolkitCoordinator.updateToolConfig",
|
|
1773
2012
|
toolRegistry: this.toolRegistry,
|
|
1774
|
-
})
|
|
2013
|
+
});
|
|
1775
2014
|
}
|
|
1776
2015
|
if (!this.config.tools.providers) {
|
|
1777
2016
|
this.config.tools.providers = {};
|
|
@@ -1780,7 +2019,7 @@ export class ToolkitCoordinator {
|
|
|
1780
2019
|
...(this.config.tools.providers || {}),
|
|
1781
2020
|
[toolId]: mergeToolConfigUpdate(toolId, current, updates),
|
|
1782
2021
|
};
|
|
1783
|
-
|
|
2022
|
+
this.config.tools = this.validateToolsConfig({
|
|
1784
2023
|
...this.config.tools,
|
|
1785
2024
|
providers: nextProviders,
|
|
1786
2025
|
}, {
|
|
@@ -1788,39 +2027,30 @@ export class ToolkitCoordinator {
|
|
|
1788
2027
|
source: "ToolkitCoordinator.updateToolConfig",
|
|
1789
2028
|
toolRegistry: this.toolRegistry,
|
|
1790
2029
|
});
|
|
1791
|
-
|
|
1792
|
-
//
|
|
1793
|
-
// with the validated coordinator config. The engine emits an
|
|
1794
|
-
// `inputs` change event so subscribers (e.g. PR 3 toolbars) can
|
|
1795
|
-
// re-decide without us managing a parallel pub/sub.
|
|
2030
|
+
// The engine's tools input follows the validated config; its `inputs`
|
|
2031
|
+
// change event is what makes toolbars re-decide.
|
|
1796
2032
|
this.policyEngine.updateInputs({
|
|
1797
2033
|
tools: this.config.tools,
|
|
1798
2034
|
});
|
|
1799
2035
|
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
1800
2036
|
// Apply configuration changes to services
|
|
1801
|
-
this._applyToolConfigChange(toolId
|
|
2037
|
+
this._applyToolConfigChange(toolId);
|
|
1802
2038
|
}
|
|
1803
2039
|
/**
|
|
1804
|
-
*
|
|
2040
|
+
* Patch one or more placement levels in the canonical tools config.
|
|
1805
2041
|
*
|
|
1806
2042
|
* This is the generic placement companion to {@link updateToolConfig}:
|
|
1807
2043
|
* it validates the next canonical tools config, keeps the policy
|
|
1808
2044
|
* engine in lockstep, and emits the same policy-change event used by
|
|
1809
2045
|
* live toolbars/debug panels.
|
|
1810
2046
|
*/
|
|
1811
|
-
updateToolPlacement(level, toolIds) {
|
|
1812
|
-
this.updateToolsPlacement({ [level]: [...toolIds] });
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Patch one or more placement levels in the canonical tools config.
|
|
1816
|
-
*/
|
|
1817
2047
|
updateToolsPlacement(partial) {
|
|
1818
2048
|
if (!this.config.tools) {
|
|
1819
|
-
this.config.tools =
|
|
2049
|
+
this.config.tools = this.validateToolsConfig(undefined, {
|
|
1820
2050
|
strictness: this.config.toolConfigStrictness ?? "error",
|
|
1821
2051
|
source: "ToolkitCoordinator.updateToolsPlacement",
|
|
1822
2052
|
toolRegistry: this.toolRegistry,
|
|
1823
|
-
})
|
|
2053
|
+
});
|
|
1824
2054
|
}
|
|
1825
2055
|
const currentPlacement = this.config.tools?.placement ?? {
|
|
1826
2056
|
section: [],
|
|
@@ -1832,7 +2062,7 @@ export class ToolkitCoordinator {
|
|
|
1832
2062
|
item: [...(partial.item ?? currentPlacement.item ?? [])],
|
|
1833
2063
|
passage: [...(partial.passage ?? currentPlacement.passage ?? [])],
|
|
1834
2064
|
};
|
|
1835
|
-
const validated =
|
|
2065
|
+
const validated = this.validateToolsConfig({
|
|
1836
2066
|
...this.config.tools,
|
|
1837
2067
|
placement: nextPlacement,
|
|
1838
2068
|
}, {
|
|
@@ -1840,8 +2070,8 @@ export class ToolkitCoordinator {
|
|
|
1840
2070
|
source: "ToolkitCoordinator.updateToolsPlacement",
|
|
1841
2071
|
toolRegistry: this.toolRegistry,
|
|
1842
2072
|
});
|
|
1843
|
-
validated.
|
|
1844
|
-
this.config.tools = validated
|
|
2073
|
+
validated.placement = nextPlacement;
|
|
2074
|
+
this.config.tools = validated;
|
|
1845
2075
|
this.policyEngine.updateInputs({
|
|
1846
2076
|
tools: this.config.tools,
|
|
1847
2077
|
});
|
|
@@ -1849,10 +2079,40 @@ export class ToolkitCoordinator {
|
|
|
1849
2079
|
/**
|
|
1850
2080
|
* Resolve the visible tool set for a given placement level + scope.
|
|
1851
2081
|
*
|
|
1852
|
-
*
|
|
2082
|
+
* Delegates to the owned tool-policy engine and logs the decision's
|
|
2083
|
+
* diagnostics through {@link warnPolicyDiagnostics}.
|
|
1853
2084
|
*/
|
|
1854
2085
|
decideToolPolicy(request) {
|
|
1855
|
-
|
|
2086
|
+
const decision = this.policyEngine.decide(request);
|
|
2087
|
+
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2088
|
+
return decision;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Log each policy diagnostic as a console warning, once per code and tool
|
|
2092
|
+
* id, and per item for `tool-policy.itemSettingNotApplied`. Decisions re-run
|
|
2093
|
+
* on every input change, so a conflict would otherwise repeat on each.
|
|
2094
|
+
*/
|
|
2095
|
+
warnPolicyDiagnostics(diagnostics) {
|
|
2096
|
+
for (const diagnostic of diagnostics) {
|
|
2097
|
+
const itemId = diagnostic.code === "tool-policy.itemSettingNotApplied"
|
|
2098
|
+
? diagnostic.details.itemId
|
|
2099
|
+
: "";
|
|
2100
|
+
const key = `${diagnostic.code}\0${diagnostic.toolId}\0${itemId}`;
|
|
2101
|
+
if (this.warnedPolicyDiagnostics.has(key))
|
|
2102
|
+
continue;
|
|
2103
|
+
this.warnedPolicyDiagnostics.add(key);
|
|
2104
|
+
console.warn(`[ToolkitCoordinator] ${diagnostic.message}`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* File a mounted item's policy settings (`requiredTools`, `restrictedTools`,
|
|
2109
|
+
* `toolParameters`) under its canonical id. They govern the decisions scoped
|
|
2110
|
+
* to that item: its own item-level toolbar, and the feature decisions its
|
|
2111
|
+
* content asks with the item's scope. `pie-item-scope` registers them from
|
|
2112
|
+
* its `settings` property; the returned function withdraws them.
|
|
2113
|
+
*/
|
|
2114
|
+
registerItemSettings(itemId, settings) {
|
|
2115
|
+
return this.policyEngine.registerItemSettings(itemId, settings);
|
|
1856
2116
|
}
|
|
1857
2117
|
/**
|
|
1858
2118
|
* Resolve eligibility for one PNP/AfA feature id, independent of toolbar
|
|
@@ -1867,10 +2127,23 @@ export class ToolkitCoordinator {
|
|
|
1867
2127
|
* capability it asks about, which is indistinguishable from a student who was
|
|
1868
2128
|
* properly declined — so without this, forgetting {@link updateAssessment}
|
|
1869
2129
|
* presents as an accommodation that silently never appears.
|
|
2130
|
+
*
|
|
2131
|
+
* A coordinator built with `assessmentOptional` reports only while PNP
|
|
2132
|
+
* enforcement is explicitly `"on"`. Its toolkit binds whatever assessment the
|
|
2133
|
+
* host gave, usually none, and a host that passes no profile and leaves
|
|
2134
|
+
* enforcement unset or `"off"` has asked for no accommodation.
|
|
2135
|
+
*
|
|
2136
|
+
* `scope` is the surface asking; an item's scope brings in that item's
|
|
2137
|
+
* registered settings ({@link registerItemSettings}).
|
|
1870
2138
|
*/
|
|
1871
|
-
decideFeaturePolicy(featureId) {
|
|
1872
|
-
const decision = this.policyEngine.decideFeature(featureId);
|
|
1873
|
-
|
|
2139
|
+
decideFeaturePolicy(featureId, scope) {
|
|
2140
|
+
const decision = this.policyEngine.decideFeature(featureId, scope);
|
|
2141
|
+
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2142
|
+
const unboundIsMisconfigured = this.config.assessmentOptional !== true ||
|
|
2143
|
+
this.pnpEnforcementOverride === "on";
|
|
2144
|
+
if (!decision.assessmentBound &&
|
|
2145
|
+
unboundIsMisconfigured &&
|
|
2146
|
+
!this.reportedUnboundFeaturePolicy) {
|
|
1874
2147
|
this.reportedUnboundFeaturePolicy = true;
|
|
1875
2148
|
console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so every capability will be declined for want of a profile to read. Call updateAssessment(...) with the assessment (its personalNeedsProfile, settings.districtPolicy and settings.testAdministration are what policy reads) before relying on any accommodation. Reported once per coordinator.`);
|
|
1876
2149
|
}
|
|
@@ -1879,9 +2152,10 @@ export class ToolkitCoordinator {
|
|
|
1879
2152
|
/**
|
|
1880
2153
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
1881
2154
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
1882
|
-
* `
|
|
1883
|
-
*
|
|
1884
|
-
* removed via
|
|
2155
|
+
* `updateToolsPlacement`, `updateAssessment`, `setPnpEnforcement`), an
|
|
2156
|
+
* item's settings are registered or withdrawn ({@link registerItemSettings}),
|
|
2157
|
+
* or a custom `PolicySource` is registered / removed via
|
|
2158
|
+
* {@link registerPolicySource}.
|
|
1885
2159
|
*
|
|
1886
2160
|
* The listener receives a `ToolPolicyChangeEvent` with the event
|
|
1887
2161
|
* `reason` and a frozen snapshot of the engine inputs. Listeners
|
|
@@ -1911,49 +2185,26 @@ export class ToolkitCoordinator {
|
|
|
1911
2185
|
/**
|
|
1912
2186
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
1913
2187
|
*
|
|
1914
|
-
* Under auto-mode (no host override via {@link setPnpEnforcement}),
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
* carries item-level profile policy inputs. A bare assessment record (just
|
|
2188
|
+
* Under auto-mode (no host override via {@link setPnpEnforcement}), a
|
|
2189
|
+
* decision enforces PNP/profile policy iff the assessment carries profile
|
|
2190
|
+
* precedence material (`personalNeedsProfile`, `settings.districtPolicy`,
|
|
2191
|
+
* `settings.testAdministration`), or the decision is scoped to an item whose
|
|
2192
|
+
* settings carry item-level policy inputs. A bare assessment record (just
|
|
1920
2193
|
* `id` / `name`, no PNP, no settings) keeps `"off"`.
|
|
1921
2194
|
*
|
|
1922
2195
|
* The host override set via {@link setPnpEnforcement} is sticky
|
|
1923
|
-
* across assessment swaps
|
|
1924
|
-
* and re-runs the auto-mode helper.
|
|
2196
|
+
* across assessment swaps.
|
|
1925
2197
|
*/
|
|
1926
2198
|
updateAssessment(assessment) {
|
|
1927
|
-
this.
|
|
1928
|
-
this.policyEngine.updateInputs({
|
|
1929
|
-
assessment,
|
|
1930
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
/**
|
|
1934
|
-
* Bind (or clear) the current item reference for policy decisions.
|
|
1935
|
-
*
|
|
1936
|
-
* Used by item-level profile gates (item `requiredTools` /
|
|
1937
|
-
* `restrictedTools` / `toolParameters`). Item-level profile material
|
|
1938
|
-
* also feeds {@link resolveEffectivePnpEnforcement} — navigating
|
|
1939
|
-
* to an item with profile settings can flip auto-mode to `"on"` even
|
|
1940
|
-
* when the parent assessment carries no profile block of its own.
|
|
1941
|
-
*/
|
|
1942
|
-
updateCurrentItemRef(itemRef) {
|
|
1943
|
-
this.boundCurrentItemRef = itemRef;
|
|
1944
|
-
this.policyEngine.updateInputs({
|
|
1945
|
-
currentItemRef: itemRef,
|
|
1946
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
1947
|
-
});
|
|
2199
|
+
this.policyEngine.updateInputs({ assessment });
|
|
1948
2200
|
}
|
|
1949
2201
|
/**
|
|
1950
|
-
* Override the auto-mode PNP/profile enforcement decision
|
|
2202
|
+
* Override the auto-mode PNP/profile enforcement decision; `null` returns
|
|
2203
|
+
* to auto-mode.
|
|
1951
2204
|
*/
|
|
1952
2205
|
setPnpEnforcement(mode) {
|
|
1953
2206
|
this.pnpEnforcementOverride = mode;
|
|
1954
|
-
this.policyEngine.updateInputs({
|
|
1955
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
1956
|
-
});
|
|
2207
|
+
this.policyEngine.updateInputs({ pnpEnforcement: mode });
|
|
1957
2208
|
}
|
|
1958
2209
|
/**
|
|
1959
2210
|
* Get the policy engine inputs currently driving decisions. Useful
|
|
@@ -1977,29 +2228,6 @@ export class ToolkitCoordinator {
|
|
|
1977
2228
|
registerPolicySource(source) {
|
|
1978
2229
|
return this.policyEngine.registerPolicySource(source);
|
|
1979
2230
|
}
|
|
1980
|
-
/**
|
|
1981
|
-
* Register a host-owned resolver for one tool's render context.
|
|
1982
|
-
*
|
|
1983
|
-
* Resolvers are evaluated only for tools that already survived the
|
|
1984
|
-
* framework policy pipeline. They can hide the tool for the current
|
|
1985
|
-
* scope or attach render params consumed by the packaged registration;
|
|
1986
|
-
* they cannot re-enable tools blocked by placement, provider config,
|
|
1987
|
-
* host policy, or PNP/profile rules.
|
|
1988
|
-
*/
|
|
1989
|
-
registerToolContextResolver(toolId, resolver) {
|
|
1990
|
-
this.assertCanonicalToolId(toolId);
|
|
1991
|
-
if (typeof resolver !== "function") {
|
|
1992
|
-
throw new Error(`Invalid tool context resolver for "${toolId}": expected a function.`);
|
|
1993
|
-
}
|
|
1994
|
-
this.toolContextResolvers.set(toolId, resolver);
|
|
1995
|
-
this.notifyToolContextResolverChange();
|
|
1996
|
-
return () => {
|
|
1997
|
-
if (this.toolContextResolvers.get(toolId) !== resolver)
|
|
1998
|
-
return;
|
|
1999
|
-
this.toolContextResolvers.delete(toolId);
|
|
2000
|
-
this.notifyToolContextResolverChange();
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
2231
|
hasToolContextResolver(toolId) {
|
|
2004
2232
|
this.assertCanonicalToolId(toolId);
|
|
2005
2233
|
return this.toolContextResolvers.has(toolId);
|
|
@@ -2047,6 +2275,9 @@ export class ToolkitCoordinator {
|
|
|
2047
2275
|
this.toolContextResolverChangeListeners.delete(listener);
|
|
2048
2276
|
};
|
|
2049
2277
|
}
|
|
2278
|
+
getToolRegistry() {
|
|
2279
|
+
return this.toolRegistry;
|
|
2280
|
+
}
|
|
2050
2281
|
/**
|
|
2051
2282
|
* Claim requests for one placement level. Called by a toolbar on mount.
|
|
2052
2283
|
*/
|
|
@@ -2070,39 +2301,18 @@ export class ToolkitCoordinator {
|
|
|
2070
2301
|
* surface, and a host that swapped the registry for one without the tool would
|
|
2071
2302
|
* otherwise lose the whole surface to an exception over an absent action.
|
|
2072
2303
|
*/
|
|
2073
|
-
canRequestTool(toolId, level) {
|
|
2304
|
+
canRequestTool(toolId, level, scopeId) {
|
|
2074
2305
|
try {
|
|
2075
2306
|
this.assertCanonicalToolId(toolId);
|
|
2076
2307
|
}
|
|
2077
2308
|
catch {
|
|
2078
2309
|
return false;
|
|
2079
2310
|
}
|
|
2080
|
-
return this.toolRequests.canRequest(toolId, level);
|
|
2311
|
+
return this.toolRequests.canRequest(toolId, level, scopeId);
|
|
2081
2312
|
}
|
|
2082
2313
|
onToolRequestTargetsChange(listener) {
|
|
2083
2314
|
return this.toolRequests.onTargetsChange(listener);
|
|
2084
2315
|
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Compute the effective PNP/profile enforcement mode given the explicit
|
|
2087
|
-
* host override and the auto-mode helper.
|
|
2088
|
-
*
|
|
2089
|
-
* Auto-mode (no override) defers to
|
|
2090
|
-
* {@link resolveDefaultPnpEnforcement}, which returns `"on"`
|
|
2091
|
-
* exactly when the bound assessment or current item ref carries
|
|
2092
|
-
* profile precedence material (PNP, district policy, test
|
|
2093
|
-
* administration, item-level required/restricted/parameters), and
|
|
2094
|
-
* `"off"` otherwise. Profile gates engage the moment profile material
|
|
2095
|
-
* is present.
|
|
2096
|
-
*/
|
|
2097
|
-
resolveEffectivePnpEnforcement() {
|
|
2098
|
-
if (this.pnpEnforcementOverride !== null) {
|
|
2099
|
-
return this.pnpEnforcementOverride;
|
|
2100
|
-
}
|
|
2101
|
-
return resolveDefaultPnpEnforcement({
|
|
2102
|
-
assessment: this.boundAssessment,
|
|
2103
|
-
currentItemRef: this.boundCurrentItemRef,
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
2316
|
resolveConfiguredPnpEnforcement() {
|
|
2107
2317
|
const tools = this.config.tools;
|
|
2108
2318
|
return tools?.pnpEnforcement ?? null;
|
|
@@ -2111,46 +2321,70 @@ export class ToolkitCoordinator {
|
|
|
2111
2321
|
* Apply tool configuration changes to underlying services.
|
|
2112
2322
|
* Called after updateToolConfig().
|
|
2113
2323
|
*/
|
|
2114
|
-
_applyToolConfigChange(toolId
|
|
2324
|
+
_applyToolConfigChange(toolId) {
|
|
2115
2325
|
if (this.disposePromise !== null)
|
|
2116
2326
|
return;
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
const reconfigurePromise = this._reconfigureTTSProvider();
|
|
2121
|
-
this.ttsReconfigurePromise = reconfigurePromise;
|
|
2122
|
-
void reconfigurePromise.finally(() => {
|
|
2123
|
-
if (this.ttsReconfigurePromise === reconfigurePromise) {
|
|
2124
|
-
this.ttsReconfigurePromise = undefined;
|
|
2125
|
-
}
|
|
2126
|
-
});
|
|
2127
|
-
void reconfigurePromise.then(async () => {
|
|
2128
|
-
if (this.disposePromise !== null)
|
|
2129
|
-
return;
|
|
2130
|
-
const ttsConfig = this.getTTSConfigFromProviders();
|
|
2131
|
-
if (!this.lazyInit && ttsConfig?.enabled !== false) {
|
|
2132
|
-
await this.ensureTTSReady(ttsConfig);
|
|
2133
|
-
}
|
|
2134
|
-
});
|
|
2135
|
-
break;
|
|
2136
|
-
}
|
|
2137
|
-
case "answerEliminator":
|
|
2138
|
-
// Future: Could notify answer eliminator tools of strategy change
|
|
2139
|
-
break;
|
|
2140
|
-
// Add cases for other tools as needed
|
|
2327
|
+
if (toolId === "textToSpeech") {
|
|
2328
|
+
this.scheduleTTSReconfigure();
|
|
2329
|
+
return;
|
|
2141
2330
|
}
|
|
2331
|
+
this._registerChangedToolProvider(toolId);
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Register the provider a tool's updated config builds, replacing the one
|
|
2335
|
+
* registered under the tool's id. Registration completes before this returns,
|
|
2336
|
+
* so a check on the policy change the update dispatched finds the new provider.
|
|
2337
|
+
*/
|
|
2338
|
+
_registerChangedToolProvider(toolId) {
|
|
2339
|
+
const registration = this.getProviderDescriptorTools().find((tool) => tool.toolId === toolId);
|
|
2340
|
+
if (!registration)
|
|
2341
|
+
return;
|
|
2342
|
+
// A failure of the replaced provider no longer describes the tool.
|
|
2343
|
+
this.degradedTools.delete(toolId);
|
|
2344
|
+
void this.registerProviderFromTool(registration, true);
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Re-register the text-to-speech provider for the current config and registry, then
|
|
2348
|
+
* re-initialize unless initialization is lazy. An initialization already
|
|
2349
|
+
* running finishes first: it would otherwise mark its provider ready after
|
|
2350
|
+
* the reset.
|
|
2351
|
+
*/
|
|
2352
|
+
scheduleTTSReconfigure() {
|
|
2353
|
+
const inFlight = this.ttsInitPromise;
|
|
2354
|
+
const reconfigurePromise = inFlight
|
|
2355
|
+
? inFlight.catch(() => { }).then(() => this._reconfigureTTSProvider())
|
|
2356
|
+
: this._reconfigureTTSProvider();
|
|
2357
|
+
this.ttsReconfigurePromise = reconfigurePromise;
|
|
2358
|
+
void reconfigurePromise.finally(() => {
|
|
2359
|
+
if (this.ttsReconfigurePromise === reconfigurePromise) {
|
|
2360
|
+
this.ttsReconfigurePromise = undefined;
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
void reconfigurePromise
|
|
2364
|
+
.then(async () => {
|
|
2365
|
+
if (this.disposePromise !== null)
|
|
2366
|
+
return;
|
|
2367
|
+
if (this.ttsRequiredForReadiness())
|
|
2368
|
+
await this.startTTSForReadiness();
|
|
2369
|
+
})
|
|
2370
|
+
// Reported where it failed; a granted failure has nowhere else to go.
|
|
2371
|
+
.catch(() => { });
|
|
2142
2372
|
}
|
|
2143
2373
|
async _reconfigureTTSProvider() {
|
|
2144
2374
|
this.ttsInitialized = false;
|
|
2375
|
+
this.ttsDegraded = false;
|
|
2376
|
+
this.degradedTools.delete("textToSpeech");
|
|
2145
2377
|
this.ttsInitPromise = undefined;
|
|
2378
|
+
this.notifyReadyChange();
|
|
2146
2379
|
try {
|
|
2147
|
-
|
|
2380
|
+
// The next speak waits on readiness, which starts the new provider.
|
|
2381
|
+
this.ttsService.releaseProvider();
|
|
2148
2382
|
}
|
|
2149
2383
|
catch {
|
|
2150
|
-
// noop:
|
|
2384
|
+
// noop: release best effort
|
|
2151
2385
|
}
|
|
2152
|
-
if (this.toolProviderRegistry.has("
|
|
2153
|
-
await this.toolProviderRegistry.unregister("
|
|
2386
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
2387
|
+
await this.toolProviderRegistry.unregister("textToSpeech");
|
|
2154
2388
|
}
|
|
2155
2389
|
if (this.disposePromise !== null)
|
|
2156
2390
|
return;
|