@pie-players/pie-assessment-toolkit 0.3.74 → 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 +349 -313
- 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.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
- 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 +27 -6
- package/dist/index.d.ts +22 -47
- package/dist/index.js +12 -36
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
- package/dist/policy/core/ToolPolicyEngine.js +136 -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 +11 -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 +8 -10
- package/dist/runtime/tool-host-contract.js +11 -11
- 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 +21 -21
- package/dist/services/HighlightCoordinator.js +431 -325
- package/dist/services/RangeSerializer.d.ts +10 -1
- package/dist/services/RangeSerializer.js +87 -14
- package/dist/services/TTSService.d.ts +117 -70
- package/dist/services/TTSService.js +668 -578
- package/dist/services/ToolCoordinator.d.ts +43 -60
- package/dist/services/ToolCoordinator.js +164 -206
- package/dist/services/ToolRegistry.d.ts +74 -57
- package/dist/services/ToolRegistry.js +104 -114
- package/dist/services/ToolkitCoordinator.d.ts +183 -189
- package/dist/services/ToolkitCoordinator.js +477 -431
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +22 -7
- package/dist/services/framework-error.js +9 -7
- package/dist/services/interfaces.d.ts +158 -129
- 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 +0 -2
- 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 +4 -5
- package/dist/services/tool-config-validation.js +36 -61
- package/dist/services/tool-context.d.ts +0 -8
- package/dist/services/tool-context.js +0 -12
- 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 +18 -19
- package/dist/services/tool-providers/TTSToolProvider.js +37 -34
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
- package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
- 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 +17 -22
- 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/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/highlight-pipeline/create-highlight-plan.js +12 -4
- 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-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +32 -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 +11 -0
- package/dist/services/tts/math-speech.js +51 -18
- package/dist/services/tts/provider-options.d.ts +41 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +11 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +7 -0
- package/dist/services/tts/text-segmentation.js +47 -0
- package/dist/services/tts-runtime-config.d.ts +25 -17
- package/dist/services/tts-runtime-config.js +16 -95
- 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 +13 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
|
@@ -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
|
-
import {
|
|
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,8 +74,10 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
81
74
|
*
|
|
82
75
|
* @example
|
|
83
76
|
* ```typescript
|
|
84
|
-
* //
|
|
85
|
-
* const toolRegistry = createPackagedToolRegistry(
|
|
77
|
+
* // Both are exported by @pie-players/pie-default-tool-loaders
|
|
78
|
+
* const toolRegistry = createPackagedToolRegistry({
|
|
79
|
+
* toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
|
|
80
|
+
* });
|
|
86
81
|
*
|
|
87
82
|
* // Create coordinator with configuration
|
|
88
83
|
* const coordinator = new ToolkitCoordinator({
|
|
@@ -91,7 +86,7 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
91
86
|
* tools: {
|
|
92
87
|
* providers: {
|
|
93
88
|
* textToSpeech: { enabled: true, backend: 'browser' },
|
|
94
|
-
* answerEliminator: { enabled: true
|
|
89
|
+
* answerEliminator: { enabled: true }
|
|
95
90
|
* },
|
|
96
91
|
* placement: {
|
|
97
92
|
* item: ['textToSpeech', 'answerEliminator']
|
|
@@ -123,22 +118,43 @@ export class ToolkitCoordinator {
|
|
|
123
118
|
lazyInit;
|
|
124
119
|
/** Track TTS initialization state */
|
|
125
120
|
ttsInitialized = false;
|
|
121
|
+
/** Text-to-speech failed to start and policy does not grant it. */
|
|
122
|
+
ttsDegraded = false;
|
|
126
123
|
ttsInitPromise;
|
|
127
124
|
ttsReconfigurePromise;
|
|
128
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;
|
|
129
131
|
stateLoadPromise;
|
|
130
132
|
coordinatorReadyPromise;
|
|
131
133
|
coordinatorReadyNotified = false;
|
|
132
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;
|
|
133
144
|
toolRegistry;
|
|
134
145
|
/** Whether the host passed `toolRegistry`, which then always stands. */
|
|
135
146
|
toolRegistrySupplied;
|
|
136
147
|
/**
|
|
137
148
|
* Whether the registry is final: supplied at construction, or adopted from
|
|
138
|
-
* the toolkit this coordinator is bound to.
|
|
139
|
-
* reports no missing registry, since the toolkit will supply one.
|
|
149
|
+
* the toolkit this coordinator is bound to.
|
|
140
150
|
*/
|
|
141
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;
|
|
142
158
|
toolContextResolvers = new Map();
|
|
143
159
|
toolContextResolverChangeListeners = new Set();
|
|
144
160
|
toolRequests = new ToolRequestRegistry();
|
|
@@ -165,14 +181,16 @@ export class ToolkitCoordinator {
|
|
|
165
181
|
* Phase D: map key of the section controller currently treated as
|
|
166
182
|
* the *active cohort*. Set by `getOrCreateSectionController` (both
|
|
167
183
|
* the create-new and resolve-existing paths) and cleared when the
|
|
168
|
-
* matching controller is disposed
|
|
169
|
-
*
|
|
184
|
+
* matching controller is disposed, and `null` while the next requested
|
|
185
|
+
* cohort is still starting.
|
|
170
186
|
*/
|
|
171
187
|
activeCohortMapKey = null;
|
|
172
188
|
/**
|
|
173
189
|
* Latest requested active cohort. Async controller initialization can
|
|
174
190
|
* resolve out of order; only the most recent request may migrate active
|
|
175
191
|
* subscriptions. Older completions still populate the controller cache.
|
|
192
|
+
* `null` until the first request, which is the only state where
|
|
193
|
+
* `subscribeSectionEvents` throws.
|
|
176
194
|
*/
|
|
177
195
|
latestRequestedActiveCohortMapKey = null;
|
|
178
196
|
telemetryListeners = new Set();
|
|
@@ -188,32 +206,20 @@ export class ToolkitCoordinator {
|
|
|
188
206
|
*/
|
|
189
207
|
policyEngine;
|
|
190
208
|
/**
|
|
191
|
-
* Host-set override for PNP/profile enforcement
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* 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.
|
|
195
212
|
* `"on"` / `"off"` are explicit host opt-in / opt-out and stick
|
|
196
|
-
* across subsequent assessment
|
|
213
|
+
* across subsequent assessment swaps until the host clears
|
|
197
214
|
* the override by calling `setPnpEnforcement(null)`.
|
|
198
215
|
*/
|
|
199
216
|
pnpEnforcementOverride = null;
|
|
200
217
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* this mirror exists only so the auto-mode helper does not need
|
|
205
|
-
* to round-trip through {@link policyEngine}'s frozen snapshot.
|
|
206
|
-
*/
|
|
207
|
-
boundAssessment = null;
|
|
208
|
-
/**
|
|
209
|
-
* Last item reference passed to {@link updateCurrentItemRef}.
|
|
210
|
-
* Mirrored alongside {@link boundAssessment} so
|
|
211
|
-
* {@link resolveEffectivePnpEnforcement} can detect item-level
|
|
212
|
-
* profile policy inputs (`requiredTools` / `restrictedTools` /
|
|
213
|
-
* `toolParameters`) without round-tripping through the engine's
|
|
214
|
-
* frozen snapshot.
|
|
218
|
+
* Policy diagnostics already logged, keyed by code, tool id and, for
|
|
219
|
+
* `tool-policy.itemSettingNotApplied`, item id; see
|
|
220
|
+
* {@link warnPolicyDiagnostics}.
|
|
215
221
|
*/
|
|
216
|
-
|
|
222
|
+
warnedPolicyDiagnostics = new Set();
|
|
217
223
|
/**
|
|
218
224
|
* Whether {@link decideFeaturePolicy} has already reported serving a decision
|
|
219
225
|
* with no assessment bound.
|
|
@@ -227,7 +233,7 @@ export class ToolkitCoordinator {
|
|
|
227
233
|
reportedUnboundFeaturePolicy = false;
|
|
228
234
|
/**
|
|
229
235
|
* Whether {@link _initializeTTS} has already reported a non-browser backend
|
|
230
|
-
* falling back to browser speech because no
|
|
236
|
+
* falling back to browser speech because no text-to-speech provider is registered.
|
|
231
237
|
* Once per coordinator: a text-to-speech config change re-runs
|
|
232
238
|
* initialization, and the missing provider is the same gap each time.
|
|
233
239
|
*/
|
|
@@ -292,7 +298,7 @@ export class ToolkitCoordinator {
|
|
|
292
298
|
*/
|
|
293
299
|
validateToolsConfig(tools, options) {
|
|
294
300
|
const { config, diagnostics } = collectToolConfigDiagnostics(tools, options.toolRegistry);
|
|
295
|
-
const unreported = this.reportedRegistryUnavailable ||
|
|
301
|
+
const unreported = this.reportedRegistryUnavailable || this.toolRegistryPending()
|
|
296
302
|
? diagnostics.filter((entry) => entry.code !== "tools.registryUnavailable")
|
|
297
303
|
: diagnostics;
|
|
298
304
|
reportToolConfigDiagnostics(unreported, options);
|
|
@@ -313,8 +319,11 @@ export class ToolkitCoordinator {
|
|
|
313
319
|
this.config = resolvedConfig;
|
|
314
320
|
this.toolRegistry = resolvedConfig.toolRegistry ?? new ToolRegistry();
|
|
315
321
|
this.installToolContextResolvers(resolvedConfig.toolContextResolvers);
|
|
316
|
-
|
|
322
|
+
// The coordinator's own copy, so `setHooks` leaves the host's object as it
|
|
323
|
+
// passed it.
|
|
324
|
+
this.hooks = { ...resolvedConfig.hooks };
|
|
317
325
|
this.lazyInit = config.lazyInit === true;
|
|
326
|
+
this.eagerInit = config.eagerInit ?? !this.lazyInit;
|
|
318
327
|
this.pnpEnforcementOverride = this.resolveConfiguredPnpEnforcement();
|
|
319
328
|
// Use the host-provided framework-error bus if one was passed
|
|
320
329
|
// (typical when embedded inside <pie-assessment-toolkit>, so
|
|
@@ -337,31 +346,35 @@ export class ToolkitCoordinator {
|
|
|
337
346
|
// Selection read-aloud speaks through this service without the inline TTS
|
|
338
347
|
// tool ever having run, so it cannot rely on that tool to attach highlights.
|
|
339
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);
|
|
340
353
|
this.setupStatePersistenceHooks();
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// actual profile material. Hosts that only consume the
|
|
347
|
-
// engine for placement/policy gating get the pre-PR-2 behavior
|
|
348
|
-
// bit-for-bit.
|
|
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.
|
|
349
359
|
this.policyEngine = new ToolPolicyEngine({
|
|
350
360
|
toolRegistry: this.toolRegistry,
|
|
351
361
|
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
352
362
|
inputs: {
|
|
353
363
|
tools: this.config.tools,
|
|
354
364
|
assessment: null,
|
|
355
|
-
|
|
356
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
365
|
+
pnpEnforcement: this.pnpEnforcementOverride,
|
|
357
366
|
},
|
|
358
367
|
});
|
|
359
|
-
|
|
368
|
+
this.policyEngine.onPolicyChange((event) => {
|
|
369
|
+
if (event.reason !== "disposed")
|
|
370
|
+
this.reportNewlyGrantedFailures();
|
|
371
|
+
});
|
|
372
|
+
if (this.eagerInit) {
|
|
360
373
|
void this.waitUntilReady().catch((err) => {
|
|
361
374
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
362
375
|
return;
|
|
363
376
|
console.error("[ToolkitCoordinator] Failed eager initialization:", err);
|
|
364
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
377
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
365
378
|
});
|
|
366
379
|
}
|
|
367
380
|
}
|
|
@@ -445,13 +458,92 @@ export class ToolkitCoordinator {
|
|
|
445
458
|
}
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
|
-
|
|
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") {
|
|
449
466
|
const model = frameworkErrorFromCoordinatorContext({
|
|
450
467
|
error,
|
|
451
468
|
context,
|
|
469
|
+
recoverable: context.recoverable,
|
|
470
|
+
scope,
|
|
452
471
|
});
|
|
453
472
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
454
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
|
+
}
|
|
455
547
|
/**
|
|
456
548
|
* Subscribe to framework-error events emitted by this coordinator.
|
|
457
549
|
*
|
|
@@ -495,15 +587,20 @@ export class ToolkitCoordinator {
|
|
|
495
587
|
return null;
|
|
496
588
|
}
|
|
497
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.
|
|
498
592
|
const getStorageKey = (context) => {
|
|
499
593
|
const { assessmentId, sectionId, attemptId } = context.key;
|
|
500
|
-
|
|
594
|
+
if (!attemptId)
|
|
595
|
+
return null;
|
|
596
|
+
return `pie:section-controller:v1:${assessmentId}:${sectionId}:${attemptId}`;
|
|
501
597
|
};
|
|
502
598
|
return {
|
|
503
599
|
async loadSession(context) {
|
|
504
|
-
|
|
600
|
+
const key = getStorageKey(context);
|
|
601
|
+
if (!storage || !key)
|
|
505
602
|
return null;
|
|
506
|
-
const value = storage.getItem(
|
|
603
|
+
const value = storage.getItem(key);
|
|
507
604
|
if (!value)
|
|
508
605
|
return null;
|
|
509
606
|
try {
|
|
@@ -514,14 +611,16 @@ export class ToolkitCoordinator {
|
|
|
514
611
|
}
|
|
515
612
|
},
|
|
516
613
|
async saveSession(context, session) {
|
|
517
|
-
|
|
614
|
+
const key = getStorageKey(context);
|
|
615
|
+
if (!storage || !key)
|
|
518
616
|
return;
|
|
519
|
-
storage.setItem(
|
|
617
|
+
storage.setItem(key, JSON.stringify(session));
|
|
520
618
|
},
|
|
521
619
|
async clearSession(context) {
|
|
522
|
-
|
|
620
|
+
const key = getStorageKey(context);
|
|
621
|
+
if (!storage || !key)
|
|
523
622
|
return;
|
|
524
|
-
storage.removeItem(
|
|
623
|
+
storage.removeItem(key);
|
|
525
624
|
},
|
|
526
625
|
};
|
|
527
626
|
}
|
|
@@ -543,12 +642,12 @@ export class ToolkitCoordinator {
|
|
|
543
642
|
if (!this.hooks.saveToolState)
|
|
544
643
|
return;
|
|
545
644
|
void Promise.resolve(this.hooks.saveToolState(state)).catch((err) => {
|
|
546
|
-
this.handleError(err, { phase: "state-save" });
|
|
645
|
+
this.handleError(err, { phase: "state-save", recoverable: true });
|
|
547
646
|
});
|
|
548
647
|
});
|
|
549
648
|
}
|
|
550
649
|
async ensureStateLoaded() {
|
|
551
|
-
if (this.
|
|
650
|
+
if (this.stateLoadSettled)
|
|
552
651
|
return;
|
|
553
652
|
if (this.stateLoadPromise)
|
|
554
653
|
return this.stateLoadPromise;
|
|
@@ -564,6 +663,7 @@ export class ToolkitCoordinator {
|
|
|
564
663
|
this.elementToolStateStore.loadState(state);
|
|
565
664
|
}
|
|
566
665
|
this.stateLoaded = true;
|
|
666
|
+
this.stateLoadSettled = true;
|
|
567
667
|
await this.emitTelemetry("pie-toolkit-tool-state-loaded", {
|
|
568
668
|
hasState: Boolean(state),
|
|
569
669
|
});
|
|
@@ -571,43 +671,52 @@ export class ToolkitCoordinator {
|
|
|
571
671
|
catch (err) {
|
|
572
672
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
573
673
|
throw err;
|
|
574
|
-
this.
|
|
674
|
+
this.stateLoadSettled = true;
|
|
675
|
+
this.handleError(err, { phase: "state-load", recoverable: true });
|
|
575
676
|
}
|
|
576
677
|
})().finally(() => {
|
|
577
678
|
this.stateLoadPromise = undefined;
|
|
679
|
+
this.notifyReadyChange();
|
|
578
680
|
});
|
|
579
681
|
return this.stateLoadPromise;
|
|
580
682
|
}
|
|
683
|
+
/** Whether the toolkit this coordinator binds to may still supply a registry. */
|
|
684
|
+
toolRegistryPending() {
|
|
685
|
+
return !this.toolRegistrySettled && !this.toolRegistryAbsent;
|
|
686
|
+
}
|
|
581
687
|
reportMissingTTSProvider(backend) {
|
|
582
688
|
// The toolkit this coordinator binds to may still supply the provider.
|
|
583
|
-
if (
|
|
689
|
+
if (this.toolRegistryPending()) {
|
|
584
690
|
this.heldMissingTTSProviderBackend = backend;
|
|
585
691
|
return;
|
|
586
692
|
}
|
|
587
693
|
if (this.reportedMissingTTSProvider)
|
|
588
694
|
return;
|
|
589
695
|
this.reportedMissingTTSProvider = true;
|
|
590
|
-
console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "
|
|
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.`);
|
|
591
697
|
}
|
|
592
698
|
/**
|
|
593
699
|
* Take the registry of the toolkit this coordinator is bound to, when the host
|
|
594
700
|
* constructed it without one: the config is validated against it, its
|
|
595
701
|
* providers register, and text-to-speech re-initializes through it if it
|
|
596
|
-
* already started. The toolkit elements call this on binding
|
|
597
|
-
* passed at construction stands, and the
|
|
598
|
-
* toolkit without one, which
|
|
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.
|
|
599
706
|
*
|
|
600
707
|
* @returns Whether `registry` was adopted.
|
|
601
708
|
*/
|
|
602
709
|
adoptToolRegistry(registry) {
|
|
603
710
|
if (this.disposePromise !== null || this.toolRegistrySettled)
|
|
604
711
|
return false;
|
|
605
|
-
this.
|
|
712
|
+
if (!registry && this.toolRegistryAbsent)
|
|
713
|
+
return false;
|
|
606
714
|
const heldBackend = this.heldMissingTTSProviderBackend;
|
|
607
715
|
this.heldMissingTTSProviderBackend = null;
|
|
608
716
|
const strictness = this.config.toolConfigStrictness ?? "error";
|
|
609
717
|
const source = "ToolkitCoordinator.adoptToolRegistry";
|
|
610
718
|
if (!registry) {
|
|
719
|
+
this.toolRegistryAbsent = true;
|
|
611
720
|
this.validateToolsConfig(this.config.tools, {
|
|
612
721
|
strictness,
|
|
613
722
|
source,
|
|
@@ -617,6 +726,7 @@ export class ToolkitCoordinator {
|
|
|
617
726
|
this.reportMissingTTSProvider(heldBackend);
|
|
618
727
|
return false;
|
|
619
728
|
}
|
|
729
|
+
this.toolRegistrySettled = true;
|
|
620
730
|
this.toolRegistry = registry;
|
|
621
731
|
this.config.toolRegistry = registry;
|
|
622
732
|
try {
|
|
@@ -658,35 +768,32 @@ export class ToolkitCoordinator {
|
|
|
658
768
|
}
|
|
659
769
|
/**
|
|
660
770
|
* Register the provider `tool`'s descriptor creates for the tool's current
|
|
661
|
-
* config, unless the tool is disabled or
|
|
662
|
-
*
|
|
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.
|
|
663
774
|
*/
|
|
664
|
-
async registerProviderFromTool(tool) {
|
|
775
|
+
async registerProviderFromTool(tool, replace = false) {
|
|
665
776
|
const descriptor = tool.provider;
|
|
666
777
|
if (!descriptor)
|
|
667
778
|
return;
|
|
668
|
-
|
|
779
|
+
const { toolId } = tool;
|
|
669
780
|
let registration;
|
|
670
781
|
try {
|
|
671
|
-
const toolConfig = this.getToolConfig(
|
|
672
|
-
if (toolConfig?.enabled === false)
|
|
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);
|
|
673
787
|
return;
|
|
674
|
-
|
|
675
|
-
if (!
|
|
788
|
+
}
|
|
789
|
+
if (!replace && this.toolProviderRegistry.has(toolId))
|
|
676
790
|
return;
|
|
677
791
|
const provider = descriptor.createProvider(toolConfig);
|
|
678
792
|
const initConfig = descriptor.getInitConfig?.(toolConfig) ??
|
|
679
793
|
toolConfig?.provider?.init ??
|
|
680
794
|
{};
|
|
681
|
-
const initConfigWithTelemetry = this.addToolTelemetryReporter(
|
|
682
|
-
|
|
683
|
-
providerId,
|
|
684
|
-
initConfig,
|
|
685
|
-
});
|
|
686
|
-
const registryTelemetry = this.createToolTelemetryForwarder({
|
|
687
|
-
toolId: tool.toolId,
|
|
688
|
-
providerId,
|
|
689
|
-
});
|
|
795
|
+
const initConfigWithTelemetry = this.addToolTelemetryReporter(toolId, initConfig);
|
|
796
|
+
const registryTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
690
797
|
const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
|
|
691
798
|
toolConfig?.provider?.runtime?.authFetcher;
|
|
692
799
|
registration = {
|
|
@@ -698,31 +805,24 @@ export class ToolkitCoordinator {
|
|
|
698
805
|
};
|
|
699
806
|
}
|
|
700
807
|
catch (err) {
|
|
701
|
-
this.reportProviderRegisterFailure(err,
|
|
808
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
702
809
|
return;
|
|
703
810
|
}
|
|
704
|
-
await this.registerProvider(
|
|
811
|
+
await this.registerProvider(toolId, registration);
|
|
705
812
|
}
|
|
706
|
-
createToolTelemetryForwarder(
|
|
813
|
+
createToolTelemetryForwarder(toolId) {
|
|
707
814
|
return async (eventName, payload) => {
|
|
708
|
-
await this.emitTelemetry(eventName, {
|
|
709
|
-
...(payload || {}),
|
|
710
|
-
toolId: args.toolId,
|
|
711
|
-
providerId: args.providerId,
|
|
712
|
-
});
|
|
815
|
+
await this.emitTelemetry(eventName, { ...(payload || {}), toolId });
|
|
713
816
|
};
|
|
714
817
|
}
|
|
715
|
-
addToolTelemetryReporter(
|
|
716
|
-
const configObject =
|
|
717
|
-
? { ...
|
|
818
|
+
addToolTelemetryReporter(toolId, initConfig) {
|
|
819
|
+
const configObject = initConfig && typeof initConfig === "object"
|
|
820
|
+
? { ...initConfig }
|
|
718
821
|
: {};
|
|
719
822
|
const existingReporter = typeof configObject.onTelemetry === "function"
|
|
720
823
|
? configObject.onTelemetry
|
|
721
824
|
: null;
|
|
722
|
-
const forwardTelemetry = this.createToolTelemetryForwarder(
|
|
723
|
-
toolId: args.toolId,
|
|
724
|
-
providerId: args.providerId,
|
|
725
|
-
});
|
|
825
|
+
const forwardTelemetry = this.createToolTelemetryForwarder(toolId);
|
|
726
826
|
configObject.onTelemetry = async (eventName, payload) => {
|
|
727
827
|
if (existingReporter) {
|
|
728
828
|
await existingReporter(eventName, payload);
|
|
@@ -731,56 +831,58 @@ export class ToolkitCoordinator {
|
|
|
731
831
|
};
|
|
732
832
|
return configObject;
|
|
733
833
|
}
|
|
734
|
-
async registerProvider(
|
|
834
|
+
async registerProvider(toolId, config) {
|
|
735
835
|
try {
|
|
736
|
-
this.toolProviderRegistry.register(
|
|
836
|
+
this.toolProviderRegistry.register(toolId, config);
|
|
737
837
|
const meta = {
|
|
738
|
-
providerId,
|
|
739
838
|
providerName: config.provider.providerName,
|
|
740
839
|
};
|
|
741
|
-
await this.hooks.onProviderRegistered?.(
|
|
840
|
+
await this.hooks.onProviderRegistered?.(toolId, meta);
|
|
742
841
|
await this.emitTelemetry("pie-toolkit-provider-registered", {
|
|
743
|
-
|
|
842
|
+
toolId,
|
|
744
843
|
providerName: config.provider.providerName,
|
|
745
844
|
});
|
|
746
845
|
}
|
|
747
846
|
catch (err) {
|
|
748
|
-
this.reportProviderRegisterFailure(err,
|
|
847
|
+
this.reportProviderRegisterFailure(err, toolId);
|
|
749
848
|
}
|
|
750
849
|
}
|
|
751
850
|
/**
|
|
752
|
-
* A console warning and a `provider-register` framework error
|
|
753
|
-
*
|
|
851
|
+
* A console warning and a `provider-register` framework error, under the
|
|
852
|
+
* start-failure policy every other tool failure follows.
|
|
754
853
|
*/
|
|
755
|
-
reportProviderRegisterFailure(err,
|
|
756
|
-
console.warn(
|
|
757
|
-
|
|
758
|
-
: `[ToolkitCoordinator] Failed to register the provider of tool "${toolId}":`, err);
|
|
759
|
-
this.handleError(err, {
|
|
760
|
-
phase: "provider-register",
|
|
761
|
-
providerId: providerId ?? undefined,
|
|
762
|
-
});
|
|
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]);
|
|
763
857
|
}
|
|
764
|
-
async ensureProviderReady(
|
|
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) {
|
|
765
863
|
this.assertNotDisposed();
|
|
766
|
-
const existing = this.providerInitPromises.get(
|
|
864
|
+
const existing = this.providerInitPromises.get(toolId);
|
|
767
865
|
if (existing)
|
|
768
866
|
return existing;
|
|
769
867
|
const promise = (async () => {
|
|
770
|
-
const provider = await this.toolProviderRegistry.getProvider(providerId, false);
|
|
771
|
-
this.assertNotDisposed();
|
|
772
|
-
const meta = {
|
|
773
|
-
providerId,
|
|
774
|
-
providerName: provider.providerName,
|
|
775
|
-
};
|
|
776
868
|
try {
|
|
777
|
-
await this.
|
|
869
|
+
let provider = await this.toolProviderRegistry.getProvider(toolId, false);
|
|
870
|
+
this.assertNotDisposed();
|
|
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);
|
|
778
878
|
this.assertNotDisposed();
|
|
779
|
-
await this.toolProviderRegistry.initialize(
|
|
879
|
+
await this.toolProviderRegistry.initialize(toolId);
|
|
780
880
|
this.assertNotDisposed();
|
|
781
|
-
|
|
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);
|
|
782
884
|
this.assertNotDisposed();
|
|
783
|
-
await this.emitTelemetry("pie-toolkit-provider-ready", {
|
|
885
|
+
await this.emitTelemetry("pie-toolkit-provider-ready", { toolId });
|
|
784
886
|
this.assertNotDisposed();
|
|
785
887
|
return provider;
|
|
786
888
|
}
|
|
@@ -788,13 +890,16 @@ export class ToolkitCoordinator {
|
|
|
788
890
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
789
891
|
throw err;
|
|
790
892
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
791
|
-
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);
|
|
792
897
|
throw error;
|
|
793
898
|
}
|
|
794
899
|
})().finally(() => {
|
|
795
|
-
this.providerInitPromises.delete(
|
|
900
|
+
this.providerInitPromises.delete(toolId);
|
|
796
901
|
});
|
|
797
|
-
this.providerInitPromises.set(
|
|
902
|
+
this.providerInitPromises.set(toolId, promise);
|
|
798
903
|
return promise;
|
|
799
904
|
}
|
|
800
905
|
setHooks(hooks) {
|
|
@@ -804,7 +909,7 @@ export class ToolkitCoordinator {
|
|
|
804
909
|
hooks.onCoordinatorReady &&
|
|
805
910
|
this.isReady()) {
|
|
806
911
|
void Promise.resolve(hooks.onCoordinatorReady(this)).catch((err) => {
|
|
807
|
-
this.handleError(err, { phase: "coordinator-ready" });
|
|
912
|
+
this.handleError(err, { phase: "coordinator-ready", recoverable: false });
|
|
808
913
|
});
|
|
809
914
|
}
|
|
810
915
|
}
|
|
@@ -827,8 +932,11 @@ export class ToolkitCoordinator {
|
|
|
827
932
|
// cohort* automatically. We bind the listener to whichever
|
|
828
933
|
// controller is currently active and re-bind it on every
|
|
829
934
|
// `getOrCreateSectionController` / `disposeSectionController`
|
|
830
|
-
// transition, with snapshot replay on each migration.
|
|
831
|
-
|
|
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) {
|
|
832
940
|
throw new Error("[ToolkitCoordinator] subscribeSectionEvents requires an active section cohort; call getOrCreateSectionController first.");
|
|
833
941
|
}
|
|
834
942
|
const previous = this.activeSubscriptions.get(args.listener);
|
|
@@ -843,7 +951,9 @@ export class ToolkitCoordinator {
|
|
|
843
951
|
unsubscribeCurrent: null,
|
|
844
952
|
};
|
|
845
953
|
this.activeSubscriptions.set(args.listener, sub);
|
|
846
|
-
const controller = this.
|
|
954
|
+
const controller = this.activeCohortMapKey === null
|
|
955
|
+
? undefined
|
|
956
|
+
: this.sectionControllers.get(this.activeCohortMapKey);
|
|
847
957
|
if (controller) {
|
|
848
958
|
this.bindSubscriptionToController(sub, controller);
|
|
849
959
|
}
|
|
@@ -1121,18 +1231,29 @@ export class ToolkitCoordinator {
|
|
|
1121
1231
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
1122
1232
|
this.latestRequestedActiveCohortMapKey = mapKey;
|
|
1123
1233
|
this.suspendActiveCohortIfSuperseded(mapKey);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
+
}
|
|
1133
1249
|
}
|
|
1134
|
-
return existingController;
|
|
1135
1250
|
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1253
|
+
throw err;
|
|
1254
|
+
}
|
|
1255
|
+
if (existingController)
|
|
1256
|
+
return existingController;
|
|
1136
1257
|
const pendingDisposal = this.sectionControllerDisposePromises.get(mapKey);
|
|
1137
1258
|
if (pendingDisposal) {
|
|
1138
1259
|
// Persistence and hydration share the cohort's durable state. Keep the
|
|
@@ -1145,8 +1266,13 @@ export class ToolkitCoordinator {
|
|
|
1145
1266
|
throw new ToolkitCoordinatorDisposedError();
|
|
1146
1267
|
}
|
|
1147
1268
|
const existingEntry = this.sectionControllerInitEntries.get(mapKey);
|
|
1148
|
-
if (existingEntry)
|
|
1149
|
-
|
|
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
|
+
}
|
|
1150
1276
|
const token = {
|
|
1151
1277
|
retired: false,
|
|
1152
1278
|
candidateClaimed: false,
|
|
@@ -1159,6 +1285,7 @@ export class ToolkitCoordinator {
|
|
|
1159
1285
|
})
|
|
1160
1286
|
.catch((err) => {
|
|
1161
1287
|
this.handleSectionControllerInitError(err, args);
|
|
1288
|
+
this.deliverSectionStartError(mapKey, err);
|
|
1162
1289
|
throw err;
|
|
1163
1290
|
})
|
|
1164
1291
|
.finally(() => {
|
|
@@ -1239,7 +1366,20 @@ export class ToolkitCoordinator {
|
|
|
1239
1366
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1240
1367
|
await controller.initialize?.(args.args.input);
|
|
1241
1368
|
await this.retireUnpublishedSectionControllerIfNeeded(candidate);
|
|
1242
|
-
|
|
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
|
+
}
|
|
1243
1383
|
await this.finalizeSectionControllerReady({
|
|
1244
1384
|
...candidate,
|
|
1245
1385
|
context,
|
|
@@ -1251,6 +1391,22 @@ export class ToolkitCoordinator {
|
|
|
1251
1391
|
throw error;
|
|
1252
1392
|
}
|
|
1253
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
|
+
}
|
|
1254
1410
|
createSectionControllerRetirementError() {
|
|
1255
1411
|
return this.disposePromise !== null
|
|
1256
1412
|
? new ToolkitCoordinatorDisposedError()
|
|
@@ -1276,6 +1432,7 @@ export class ToolkitCoordinator {
|
|
|
1276
1432
|
sectionId: args.key.sectionId,
|
|
1277
1433
|
attemptId: args.key.attemptId,
|
|
1278
1434
|
},
|
|
1435
|
+
recoverable: true,
|
|
1279
1436
|
});
|
|
1280
1437
|
}
|
|
1281
1438
|
finally {
|
|
@@ -1331,7 +1488,33 @@ export class ToolkitCoordinator {
|
|
|
1331
1488
|
sectionId: args.sectionId,
|
|
1332
1489
|
attemptId: args.attemptId,
|
|
1333
1490
|
},
|
|
1334
|
-
|
|
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
|
+
}
|
|
1335
1518
|
}
|
|
1336
1519
|
disposeSectionController(args) {
|
|
1337
1520
|
const key = {
|
|
@@ -1418,6 +1601,7 @@ export class ToolkitCoordinator {
|
|
|
1418
1601
|
sectionId: args.args.sectionId,
|
|
1419
1602
|
attemptId: args.args.attemptId,
|
|
1420
1603
|
},
|
|
1604
|
+
recoverable: true,
|
|
1421
1605
|
});
|
|
1422
1606
|
}
|
|
1423
1607
|
finally {
|
|
@@ -1517,7 +1701,7 @@ export class ToolkitCoordinator {
|
|
|
1517
1701
|
this.activeSubscriptions.clear();
|
|
1518
1702
|
this.activeCohortMapKey = null;
|
|
1519
1703
|
this.latestRequestedActiveCohortMapKey = null;
|
|
1520
|
-
await cleanup(() => this.ttsService.
|
|
1704
|
+
await cleanup(() => this.ttsService.dispose());
|
|
1521
1705
|
await cleanup(() => this.toolProviderRegistry.destroy());
|
|
1522
1706
|
await cleanup(() => this.highlightCoordinator.destroy());
|
|
1523
1707
|
for (const toolId of this.toolCoordinator.getRegisteredTools()) {
|
|
@@ -1571,6 +1755,7 @@ export class ToolkitCoordinator {
|
|
|
1571
1755
|
.then(() => this.assertNotDisposed())
|
|
1572
1756
|
.finally(() => {
|
|
1573
1757
|
this.ttsInitPromise = undefined;
|
|
1758
|
+
this.notifyReadyChange();
|
|
1574
1759
|
});
|
|
1575
1760
|
return this.ttsInitPromise;
|
|
1576
1761
|
}
|
|
@@ -1604,9 +1789,10 @@ export class ToolkitCoordinator {
|
|
|
1604
1789
|
backend: resolvedBackend,
|
|
1605
1790
|
});
|
|
1606
1791
|
// Try to use TTS provider from registry if available
|
|
1607
|
-
if (this.toolProviderRegistry.has("
|
|
1792
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
1608
1793
|
try {
|
|
1609
|
-
|
|
1794
|
+
// Browser speech follows a server provider that fails to start.
|
|
1795
|
+
const ttsProvider = await this.initializeProvider("textToSpeech", true);
|
|
1610
1796
|
const providerInstance = await ttsProvider.createInstance();
|
|
1611
1797
|
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
1612
1798
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
@@ -1618,25 +1804,14 @@ export class ToolkitCoordinator {
|
|
|
1618
1804
|
backend: resolvedBackend,
|
|
1619
1805
|
provider: "registry",
|
|
1620
1806
|
});
|
|
1621
|
-
console.log("[ToolkitCoordinator] TTS initialized via ToolProviderRegistry");
|
|
1622
1807
|
return;
|
|
1623
1808
|
}
|
|
1624
1809
|
catch (error) {
|
|
1625
1810
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1626
1811
|
throw error;
|
|
1627
1812
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1628
|
-
await this.emitTelemetry("pie-tool-init-error", {
|
|
1629
|
-
toolId: "textToSpeech",
|
|
1630
|
-
providerId: "tts",
|
|
1631
|
-
operation: "tts-init",
|
|
1632
|
-
backend: resolvedBackend,
|
|
1633
|
-
errorType: "TTSRegistryInitError",
|
|
1634
|
-
message: normalized.message,
|
|
1635
|
-
recovered: true,
|
|
1636
|
-
});
|
|
1637
1813
|
await this.emitTelemetry("pie-tool-init-fallback", {
|
|
1638
1814
|
toolId: "textToSpeech",
|
|
1639
|
-
providerId: "tts",
|
|
1640
1815
|
operation: "tts-init",
|
|
1641
1816
|
backend: resolvedBackend,
|
|
1642
1817
|
fromProvider: "registry",
|
|
@@ -1649,10 +1824,13 @@ export class ToolkitCoordinator {
|
|
|
1649
1824
|
else if (resolvedBackend !== "browser") {
|
|
1650
1825
|
this.reportMissingTTSProvider(resolvedBackend);
|
|
1651
1826
|
}
|
|
1652
|
-
//
|
|
1827
|
+
// Browser speech, configured or standing in for a backend that has no
|
|
1828
|
+
// provider or failed to start.
|
|
1653
1829
|
const provider = new BrowserTTSProvider();
|
|
1654
1830
|
try {
|
|
1655
|
-
await this.initializeTTSService(provider,
|
|
1831
|
+
await this.initializeTTSService(provider, resolvedBackend === "browser"
|
|
1832
|
+
? runtimeTTSConfig
|
|
1833
|
+
: browserFallbackConfig(runtimeTTSConfig));
|
|
1656
1834
|
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
1657
1835
|
provider: "browser-fallback",
|
|
1658
1836
|
});
|
|
@@ -1667,7 +1845,7 @@ export class ToolkitCoordinator {
|
|
|
1667
1845
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1668
1846
|
throw error;
|
|
1669
1847
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1670
|
-
this.
|
|
1848
|
+
this.reportToolFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
|
|
1671
1849
|
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
1672
1850
|
message: normalized.message,
|
|
1673
1851
|
});
|
|
@@ -1686,7 +1864,7 @@ export class ToolkitCoordinator {
|
|
|
1686
1864
|
}
|
|
1687
1865
|
async initializeTTSService(provider, config) {
|
|
1688
1866
|
const nextProviderOptions = {
|
|
1689
|
-
...
|
|
1867
|
+
...config.providerOptions,
|
|
1690
1868
|
__pieTelemetry: async (eventName, payload) => {
|
|
1691
1869
|
await this.emitTelemetry(eventName, {
|
|
1692
1870
|
toolId: "textToSpeech",
|
|
@@ -1699,93 +1877,14 @@ export class ToolkitCoordinator {
|
|
|
1699
1877
|
providerOptions: nextProviderOptions,
|
|
1700
1878
|
};
|
|
1701
1879
|
await this.ttsService.initialize(provider, nextConfig);
|
|
1702
|
-
await this.ensureBrowserVoicesReady(provider);
|
|
1703
1880
|
this.assertNotDisposed();
|
|
1704
1881
|
this.ttsService.setCatalogResolver(this.catalogResolver);
|
|
1705
1882
|
this.ttsInitialized = true;
|
|
1883
|
+
this.ttsDegraded = false;
|
|
1884
|
+
this.degradedTools.delete("textToSpeech");
|
|
1706
1885
|
await this.hooks.onTTSReady?.();
|
|
1707
1886
|
this.assertNotDisposed();
|
|
1708
1887
|
}
|
|
1709
|
-
async ensureBrowserVoicesReady(provider, timeoutMs = 1200) {
|
|
1710
|
-
if (provider.providerId !== "browser")
|
|
1711
|
-
return;
|
|
1712
|
-
if (typeof window === "undefined")
|
|
1713
|
-
return;
|
|
1714
|
-
if (!("speechSynthesis" in window))
|
|
1715
|
-
return;
|
|
1716
|
-
const synth = window.speechSynthesis;
|
|
1717
|
-
const voices = synth.getVoices();
|
|
1718
|
-
if (voices.length > 0)
|
|
1719
|
-
return;
|
|
1720
|
-
await new Promise((resolve) => {
|
|
1721
|
-
let settled = false;
|
|
1722
|
-
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
1723
|
-
typeof synth.removeEventListener === "function";
|
|
1724
|
-
const canUseHandler = !canUseEventTarget && "onvoiceschanged" in synth;
|
|
1725
|
-
const previousHandler = canUseHandler ? synth.onvoiceschanged : null;
|
|
1726
|
-
let assignedHandler = false;
|
|
1727
|
-
const finish = () => {
|
|
1728
|
-
if (settled)
|
|
1729
|
-
return;
|
|
1730
|
-
settled = true;
|
|
1731
|
-
window.clearTimeout(timeoutId);
|
|
1732
|
-
if (canUseEventTarget) {
|
|
1733
|
-
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
1734
|
-
}
|
|
1735
|
-
else if (assignedHandler &&
|
|
1736
|
-
synth.onvoiceschanged === onVoicesChanged) {
|
|
1737
|
-
synth.onvoiceschanged = previousHandler;
|
|
1738
|
-
}
|
|
1739
|
-
resolve();
|
|
1740
|
-
};
|
|
1741
|
-
const onVoicesChanged = (event) => {
|
|
1742
|
-
if (!canUseEventTarget &&
|
|
1743
|
-
typeof previousHandler === "function" &&
|
|
1744
|
-
event) {
|
|
1745
|
-
previousHandler.call(synth, event);
|
|
1746
|
-
}
|
|
1747
|
-
finish();
|
|
1748
|
-
};
|
|
1749
|
-
const timeoutId = window.setTimeout(finish, timeoutMs);
|
|
1750
|
-
if (canUseEventTarget) {
|
|
1751
|
-
synth.addEventListener("voiceschanged", onVoicesChanged, {
|
|
1752
|
-
once: true,
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
else if (canUseHandler) {
|
|
1756
|
-
synth.onvoiceschanged = onVoicesChanged;
|
|
1757
|
-
assignedHandler = true;
|
|
1758
|
-
}
|
|
1759
|
-
});
|
|
1760
|
-
const voicesAfterWait = synth.getVoices();
|
|
1761
|
-
await this.emitTelemetry("pie-toolkit-tts-browser-voices-ready", {
|
|
1762
|
-
voiceCount: voicesAfterWait.length,
|
|
1763
|
-
timedOut: voicesAfterWait.length === 0,
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Get all services as a bundle for section player convenience.
|
|
1768
|
-
* Section player can extract and pass services to child components.
|
|
1769
|
-
*/
|
|
1770
|
-
getServiceBundle() {
|
|
1771
|
-
return {
|
|
1772
|
-
ttsService: this.ttsService,
|
|
1773
|
-
toolCoordinator: this.toolCoordinator,
|
|
1774
|
-
highlightCoordinator: this.highlightCoordinator,
|
|
1775
|
-
elementToolStateStore: this.elementToolStateStore,
|
|
1776
|
-
catalogResolver: this.catalogResolver,
|
|
1777
|
-
toolProviderRegistry: this.toolProviderRegistry,
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Get a tool provider from the registry
|
|
1782
|
-
*
|
|
1783
|
-
* @param providerId Provider identifier
|
|
1784
|
-
* @returns Tool provider instance
|
|
1785
|
-
*/
|
|
1786
|
-
async getToolProvider(providerId) {
|
|
1787
|
-
return this.ensureProviderReady(providerId);
|
|
1788
|
-
}
|
|
1789
1888
|
async waitUntilReady() {
|
|
1790
1889
|
this.assertNotDisposed();
|
|
1791
1890
|
if (this.isReady())
|
|
@@ -1795,9 +1894,8 @@ export class ToolkitCoordinator {
|
|
|
1795
1894
|
this.coordinatorReadyPromise = (async () => {
|
|
1796
1895
|
await this.ensureStateLoaded();
|
|
1797
1896
|
this.assertNotDisposed();
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
await this.ensureTTSReady(ttsConfig);
|
|
1897
|
+
if (this.ttsRequiredForReadiness()) {
|
|
1898
|
+
await this.startTTSForReadiness();
|
|
1801
1899
|
}
|
|
1802
1900
|
this.assertNotDisposed();
|
|
1803
1901
|
if (!this.coordinatorReadyNotified) {
|
|
@@ -1814,22 +1912,37 @@ export class ToolkitCoordinator {
|
|
|
1814
1912
|
return this.coordinatorReadyPromise;
|
|
1815
1913
|
}
|
|
1816
1914
|
isReady() {
|
|
1817
|
-
return this.
|
|
1915
|
+
return ((this.stateLoadSettled || !this.hooks.loadToolState) &&
|
|
1916
|
+
(this.ttsInitialized ||
|
|
1917
|
+
this.ttsDegraded ||
|
|
1918
|
+
!this.ttsRequiredForReadiness()));
|
|
1818
1919
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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");
|
|
1928
|
+
}
|
|
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();
|
|
1824
1945
|
}
|
|
1825
|
-
const ttsConfig = this.getTTSConfigFromProviders();
|
|
1826
|
-
return {
|
|
1827
|
-
tts: this.ttsInitialized,
|
|
1828
|
-
stateLoaded: this.stateLoaded || !this.hooks.loadToolState,
|
|
1829
|
-
coordinator: (this.stateLoaded || !this.hooks.loadToolState) &&
|
|
1830
|
-
(this.ttsInitialized || ttsConfig?.enabled === false),
|
|
1831
|
-
providers,
|
|
1832
|
-
};
|
|
1833
1946
|
}
|
|
1834
1947
|
getTTSConfigFromProviders() {
|
|
1835
1948
|
return this.config.tools?.providers?.textToSpeech;
|
|
@@ -1838,9 +1951,6 @@ export class ToolkitCoordinator {
|
|
|
1838
1951
|
if (typeof toolId !== "string" || toolId.trim().length === 0) {
|
|
1839
1952
|
throw new Error("Tool id must be a non-empty string.");
|
|
1840
1953
|
}
|
|
1841
|
-
if (toolId === "tts") {
|
|
1842
|
-
throw new Error(`Tool id "tts" is no longer supported. Use "textToSpeech".`);
|
|
1843
|
-
}
|
|
1844
1954
|
// An empty registry means the host supplied none, not that every id is
|
|
1845
1955
|
// wrong. There is nothing to check an id against, and throwing turns a
|
|
1846
1956
|
// host's every tool-config call into an exception — including the calls
|
|
@@ -1867,6 +1977,11 @@ export class ToolkitCoordinator {
|
|
|
1867
1977
|
this.toolContextResolvers.set(toolId, resolver);
|
|
1868
1978
|
}
|
|
1869
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
|
+
*/
|
|
1870
1985
|
setToolContextResolvers(resolvers) {
|
|
1871
1986
|
this.toolContextResolvers.clear();
|
|
1872
1987
|
this.installToolContextResolvers(resolvers ?? undefined);
|
|
@@ -1882,19 +1997,6 @@ export class ToolkitCoordinator {
|
|
|
1882
1997
|
}
|
|
1883
1998
|
}
|
|
1884
1999
|
}
|
|
1885
|
-
/**
|
|
1886
|
-
* Check if a tool is enabled.
|
|
1887
|
-
* Tools are enabled by default unless explicitly disabled.
|
|
1888
|
-
*
|
|
1889
|
-
* @param toolId Tool identifier (e.g., 'textToSpeech', 'answerEliminator')
|
|
1890
|
-
* @returns True if tool is enabled
|
|
1891
|
-
*/
|
|
1892
|
-
isToolEnabled(toolId) {
|
|
1893
|
-
this.assertCanonicalToolId(toolId);
|
|
1894
|
-
const toolConfig = this.config.tools?.providers?.[toolId];
|
|
1895
|
-
// Enabled by default unless explicitly set to false
|
|
1896
|
-
return toolConfig?.enabled !== false;
|
|
1897
|
-
}
|
|
1898
2000
|
getToolConfig(toolId) {
|
|
1899
2001
|
this.assertCanonicalToolId(toolId);
|
|
1900
2002
|
return this.config.tools?.providers?.[toolId] || null;
|
|
@@ -1925,31 +2027,23 @@ export class ToolkitCoordinator {
|
|
|
1925
2027
|
source: "ToolkitCoordinator.updateToolConfig",
|
|
1926
2028
|
toolRegistry: this.toolRegistry,
|
|
1927
2029
|
});
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
// `inputs` change event so subscribers (e.g. PR 3 toolbars) can
|
|
1931
|
-
// 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.
|
|
1932
2032
|
this.policyEngine.updateInputs({
|
|
1933
2033
|
tools: this.config.tools,
|
|
1934
2034
|
});
|
|
1935
2035
|
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
1936
2036
|
// Apply configuration changes to services
|
|
1937
|
-
this._applyToolConfigChange(toolId
|
|
2037
|
+
this._applyToolConfigChange(toolId);
|
|
1938
2038
|
}
|
|
1939
2039
|
/**
|
|
1940
|
-
*
|
|
2040
|
+
* Patch one or more placement levels in the canonical tools config.
|
|
1941
2041
|
*
|
|
1942
2042
|
* This is the generic placement companion to {@link updateToolConfig}:
|
|
1943
2043
|
* it validates the next canonical tools config, keeps the policy
|
|
1944
2044
|
* engine in lockstep, and emits the same policy-change event used by
|
|
1945
2045
|
* live toolbars/debug panels.
|
|
1946
2046
|
*/
|
|
1947
|
-
updateToolPlacement(level, toolIds) {
|
|
1948
|
-
this.updateToolsPlacement({ [level]: [...toolIds] });
|
|
1949
|
-
}
|
|
1950
|
-
/**
|
|
1951
|
-
* Patch one or more placement levels in the canonical tools config.
|
|
1952
|
-
*/
|
|
1953
2047
|
updateToolsPlacement(partial) {
|
|
1954
2048
|
if (!this.config.tools) {
|
|
1955
2049
|
this.config.tools = this.validateToolsConfig(undefined, {
|
|
@@ -1985,10 +2079,40 @@ export class ToolkitCoordinator {
|
|
|
1985
2079
|
/**
|
|
1986
2080
|
* Resolve the visible tool set for a given placement level + scope.
|
|
1987
2081
|
*
|
|
1988
|
-
*
|
|
2082
|
+
* Delegates to the owned tool-policy engine and logs the decision's
|
|
2083
|
+
* diagnostics through {@link warnPolicyDiagnostics}.
|
|
1989
2084
|
*/
|
|
1990
2085
|
decideToolPolicy(request) {
|
|
1991
|
-
|
|
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);
|
|
1992
2116
|
}
|
|
1993
2117
|
/**
|
|
1994
2118
|
* Resolve eligibility for one PNP/AfA feature id, independent of toolbar
|
|
@@ -2008,9 +2132,13 @@ export class ToolkitCoordinator {
|
|
|
2008
2132
|
* enforcement is explicitly `"on"`. Its toolkit binds whatever assessment the
|
|
2009
2133
|
* host gave, usually none, and a host that passes no profile and leaves
|
|
2010
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}).
|
|
2011
2138
|
*/
|
|
2012
|
-
decideFeaturePolicy(featureId) {
|
|
2013
|
-
const decision = this.policyEngine.decideFeature(featureId);
|
|
2139
|
+
decideFeaturePolicy(featureId, scope) {
|
|
2140
|
+
const decision = this.policyEngine.decideFeature(featureId, scope);
|
|
2141
|
+
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2014
2142
|
const unboundIsMisconfigured = this.config.assessmentOptional !== true ||
|
|
2015
2143
|
this.pnpEnforcementOverride === "on";
|
|
2016
2144
|
if (!decision.assessmentBound &&
|
|
@@ -2024,9 +2152,10 @@ export class ToolkitCoordinator {
|
|
|
2024
2152
|
/**
|
|
2025
2153
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
2026
2154
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
2027
|
-
* `
|
|
2028
|
-
*
|
|
2029
|
-
* 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}.
|
|
2030
2159
|
*
|
|
2031
2160
|
* The listener receives a `ToolPolicyChangeEvent` with the event
|
|
2032
2161
|
* `reason` and a frozen snapshot of the engine inputs. Listeners
|
|
@@ -2056,49 +2185,26 @@ export class ToolkitCoordinator {
|
|
|
2056
2185
|
/**
|
|
2057
2186
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
2058
2187
|
*
|
|
2059
|
-
* Under auto-mode (no host override via {@link setPnpEnforcement}),
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
* carries item-level profile policy inputs. A bare assessment record (just
|
|
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
|
|
2065
2193
|
* `id` / `name`, no PNP, no settings) keeps `"off"`.
|
|
2066
2194
|
*
|
|
2067
2195
|
* The host override set via {@link setPnpEnforcement} is sticky
|
|
2068
|
-
* across assessment swaps
|
|
2069
|
-
* and re-runs the auto-mode helper.
|
|
2196
|
+
* across assessment swaps.
|
|
2070
2197
|
*/
|
|
2071
2198
|
updateAssessment(assessment) {
|
|
2072
|
-
this.
|
|
2073
|
-
this.policyEngine.updateInputs({
|
|
2074
|
-
assessment,
|
|
2075
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2076
|
-
});
|
|
2077
|
-
}
|
|
2078
|
-
/**
|
|
2079
|
-
* Bind (or clear) the current item reference for policy decisions.
|
|
2080
|
-
*
|
|
2081
|
-
* Used by item-level profile gates (item `requiredTools` /
|
|
2082
|
-
* `restrictedTools` / `toolParameters`). Item-level profile material
|
|
2083
|
-
* also feeds {@link resolveEffectivePnpEnforcement} — navigating
|
|
2084
|
-
* to an item with profile settings can flip auto-mode to `"on"` even
|
|
2085
|
-
* when the parent assessment carries no profile block of its own.
|
|
2086
|
-
*/
|
|
2087
|
-
updateCurrentItemRef(itemRef) {
|
|
2088
|
-
this.boundCurrentItemRef = itemRef;
|
|
2089
|
-
this.policyEngine.updateInputs({
|
|
2090
|
-
currentItemRef: itemRef,
|
|
2091
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2092
|
-
});
|
|
2199
|
+
this.policyEngine.updateInputs({ assessment });
|
|
2093
2200
|
}
|
|
2094
2201
|
/**
|
|
2095
|
-
* Override the auto-mode PNP/profile enforcement decision
|
|
2202
|
+
* Override the auto-mode PNP/profile enforcement decision; `null` returns
|
|
2203
|
+
* to auto-mode.
|
|
2096
2204
|
*/
|
|
2097
2205
|
setPnpEnforcement(mode) {
|
|
2098
2206
|
this.pnpEnforcementOverride = mode;
|
|
2099
|
-
this.policyEngine.updateInputs({
|
|
2100
|
-
pnpEnforcement: this.resolveEffectivePnpEnforcement(),
|
|
2101
|
-
});
|
|
2207
|
+
this.policyEngine.updateInputs({ pnpEnforcement: mode });
|
|
2102
2208
|
}
|
|
2103
2209
|
/**
|
|
2104
2210
|
* Get the policy engine inputs currently driving decisions. Useful
|
|
@@ -2122,29 +2228,6 @@ export class ToolkitCoordinator {
|
|
|
2122
2228
|
registerPolicySource(source) {
|
|
2123
2229
|
return this.policyEngine.registerPolicySource(source);
|
|
2124
2230
|
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Register a host-owned resolver for one tool's render context.
|
|
2127
|
-
*
|
|
2128
|
-
* Resolvers are evaluated only for tools that already survived the
|
|
2129
|
-
* framework policy pipeline. They can hide the tool for the current
|
|
2130
|
-
* scope or attach render params consumed by the packaged registration;
|
|
2131
|
-
* they cannot re-enable tools blocked by placement, provider config,
|
|
2132
|
-
* host policy, or PNP/profile rules.
|
|
2133
|
-
*/
|
|
2134
|
-
registerToolContextResolver(toolId, resolver) {
|
|
2135
|
-
this.assertCanonicalToolId(toolId);
|
|
2136
|
-
if (typeof resolver !== "function") {
|
|
2137
|
-
throw new Error(`Invalid tool context resolver for "${toolId}": expected a function.`);
|
|
2138
|
-
}
|
|
2139
|
-
this.toolContextResolvers.set(toolId, resolver);
|
|
2140
|
-
this.notifyToolContextResolverChange();
|
|
2141
|
-
return () => {
|
|
2142
|
-
if (this.toolContextResolvers.get(toolId) !== resolver)
|
|
2143
|
-
return;
|
|
2144
|
-
this.toolContextResolvers.delete(toolId);
|
|
2145
|
-
this.notifyToolContextResolverChange();
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
2231
|
hasToolContextResolver(toolId) {
|
|
2149
2232
|
this.assertCanonicalToolId(toolId);
|
|
2150
2233
|
return this.toolContextResolvers.has(toolId);
|
|
@@ -2192,6 +2275,9 @@ export class ToolkitCoordinator {
|
|
|
2192
2275
|
this.toolContextResolverChangeListeners.delete(listener);
|
|
2193
2276
|
};
|
|
2194
2277
|
}
|
|
2278
|
+
getToolRegistry() {
|
|
2279
|
+
return this.toolRegistry;
|
|
2280
|
+
}
|
|
2195
2281
|
/**
|
|
2196
2282
|
* Claim requests for one placement level. Called by a toolbar on mount.
|
|
2197
2283
|
*/
|
|
@@ -2215,95 +2301,50 @@ export class ToolkitCoordinator {
|
|
|
2215
2301
|
* surface, and a host that swapped the registry for one without the tool would
|
|
2216
2302
|
* otherwise lose the whole surface to an exception over an absent action.
|
|
2217
2303
|
*/
|
|
2218
|
-
canRequestTool(toolId, level) {
|
|
2304
|
+
canRequestTool(toolId, level, scopeId) {
|
|
2219
2305
|
try {
|
|
2220
2306
|
this.assertCanonicalToolId(toolId);
|
|
2221
2307
|
}
|
|
2222
2308
|
catch {
|
|
2223
2309
|
return false;
|
|
2224
2310
|
}
|
|
2225
|
-
return this.toolRequests.canRequest(toolId, level);
|
|
2311
|
+
return this.toolRequests.canRequest(toolId, level, scopeId);
|
|
2226
2312
|
}
|
|
2227
2313
|
onToolRequestTargetsChange(listener) {
|
|
2228
2314
|
return this.toolRequests.onTargetsChange(listener);
|
|
2229
2315
|
}
|
|
2230
|
-
/**
|
|
2231
|
-
* Compute the effective PNP/profile enforcement mode given the explicit
|
|
2232
|
-
* host override and the auto-mode helper.
|
|
2233
|
-
*
|
|
2234
|
-
* Auto-mode (no override) defers to
|
|
2235
|
-
* {@link resolveDefaultPnpEnforcement}, which returns `"on"`
|
|
2236
|
-
* exactly when the bound assessment or current item ref carries
|
|
2237
|
-
* profile precedence material (PNP, district policy, test
|
|
2238
|
-
* administration, item-level required/restricted/parameters), and
|
|
2239
|
-
* `"off"` otherwise. Profile gates engage the moment profile material
|
|
2240
|
-
* is present.
|
|
2241
|
-
*/
|
|
2242
|
-
resolveEffectivePnpEnforcement() {
|
|
2243
|
-
if (this.pnpEnforcementOverride !== null) {
|
|
2244
|
-
return this.pnpEnforcementOverride;
|
|
2245
|
-
}
|
|
2246
|
-
return resolveDefaultPnpEnforcement({
|
|
2247
|
-
assessment: this.boundAssessment,
|
|
2248
|
-
currentItemRef: this.boundCurrentItemRef,
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
2316
|
resolveConfiguredPnpEnforcement() {
|
|
2252
2317
|
const tools = this.config.tools;
|
|
2253
2318
|
return tools?.pnpEnforcement ?? null;
|
|
2254
2319
|
}
|
|
2255
2320
|
/**
|
|
2256
2321
|
* Apply tool configuration changes to underlying services.
|
|
2257
|
-
* Called after updateToolConfig()
|
|
2322
|
+
* Called after updateToolConfig().
|
|
2258
2323
|
*/
|
|
2259
|
-
_applyToolConfigChange(toolId
|
|
2324
|
+
_applyToolConfigChange(toolId) {
|
|
2260
2325
|
if (this.disposePromise !== null)
|
|
2261
2326
|
return;
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
this.scheduleTTSReconfigure();
|
|
2266
|
-
break;
|
|
2267
|
-
case "answerEliminator":
|
|
2268
|
-
// Future: Could notify answer eliminator tools of strategy change
|
|
2269
|
-
break;
|
|
2270
|
-
default:
|
|
2271
|
-
this._registerChangedToolProvider(toolId, previousConfig);
|
|
2327
|
+
if (toolId === "textToSpeech") {
|
|
2328
|
+
this.scheduleTTSReconfigure();
|
|
2329
|
+
return;
|
|
2272
2330
|
}
|
|
2331
|
+
this._registerChangedToolProvider(toolId);
|
|
2273
2332
|
}
|
|
2274
2333
|
/**
|
|
2275
|
-
* Register the provider a tool's updated config
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
* before this returns, so a check on the policy change the update dispatched
|
|
2279
|
-
* finds the new provider.
|
|
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.
|
|
2280
2337
|
*/
|
|
2281
|
-
_registerChangedToolProvider(toolId
|
|
2338
|
+
_registerChangedToolProvider(toolId) {
|
|
2282
2339
|
const registration = this.getProviderDescriptorTools().find((tool) => tool.toolId === toolId);
|
|
2283
2340
|
if (!registration)
|
|
2284
2341
|
return;
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
return resolveToolProviderId(registration, config);
|
|
2289
|
-
}
|
|
2290
|
-
catch {
|
|
2291
|
-
return null;
|
|
2292
|
-
}
|
|
2293
|
-
};
|
|
2294
|
-
const previousId = resolveQuietly(previousConfig);
|
|
2295
|
-
const nextId = resolveQuietly(this.getToolConfig(toolId) || undefined);
|
|
2296
|
-
if (!previousId ||
|
|
2297
|
-
previousId === nextId ||
|
|
2298
|
-
!this.toolProviderRegistry.has(previousId)) {
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
void this.toolProviderRegistry.unregister(previousId).catch((err) => {
|
|
2302
|
-
console.warn(`[ToolkitCoordinator] Failed to unregister provider "${previousId}":`, err);
|
|
2303
|
-
});
|
|
2342
|
+
// A failure of the replaced provider no longer describes the tool.
|
|
2343
|
+
this.degradedTools.delete(toolId);
|
|
2344
|
+
void this.registerProviderFromTool(registration, true);
|
|
2304
2345
|
}
|
|
2305
2346
|
/**
|
|
2306
|
-
* Re-register the
|
|
2347
|
+
* Re-register the text-to-speech provider for the current config and registry, then
|
|
2307
2348
|
* re-initialize unless initialization is lazy. An initialization already
|
|
2308
2349
|
* running finishes first: it would otherwise mark its provider ready after
|
|
2309
2350
|
* the reset.
|
|
@@ -2319,26 +2360,31 @@ export class ToolkitCoordinator {
|
|
|
2319
2360
|
this.ttsReconfigurePromise = undefined;
|
|
2320
2361
|
}
|
|
2321
2362
|
});
|
|
2322
|
-
void reconfigurePromise
|
|
2363
|
+
void reconfigurePromise
|
|
2364
|
+
.then(async () => {
|
|
2323
2365
|
if (this.disposePromise !== null)
|
|
2324
2366
|
return;
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2367
|
+
if (this.ttsRequiredForReadiness())
|
|
2368
|
+
await this.startTTSForReadiness();
|
|
2369
|
+
})
|
|
2370
|
+
// Reported where it failed; a granted failure has nowhere else to go.
|
|
2371
|
+
.catch(() => { });
|
|
2330
2372
|
}
|
|
2331
2373
|
async _reconfigureTTSProvider() {
|
|
2332
2374
|
this.ttsInitialized = false;
|
|
2375
|
+
this.ttsDegraded = false;
|
|
2376
|
+
this.degradedTools.delete("textToSpeech");
|
|
2333
2377
|
this.ttsInitPromise = undefined;
|
|
2378
|
+
this.notifyReadyChange();
|
|
2334
2379
|
try {
|
|
2335
|
-
|
|
2380
|
+
// The next speak waits on readiness, which starts the new provider.
|
|
2381
|
+
this.ttsService.releaseProvider();
|
|
2336
2382
|
}
|
|
2337
2383
|
catch {
|
|
2338
|
-
// noop:
|
|
2384
|
+
// noop: release best effort
|
|
2339
2385
|
}
|
|
2340
|
-
if (this.toolProviderRegistry.has("
|
|
2341
|
-
await this.toolProviderRegistry.unregister("
|
|
2386
|
+
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
2387
|
+
await this.toolProviderRegistry.unregister("textToSpeech");
|
|
2342
2388
|
}
|
|
2343
2389
|
if (this.disposePromise !== null)
|
|
2344
2390
|
return;
|