@pie-players/pie-assessment-toolkit 1.0.0-beta.7 → 1.0.0-beta.9

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.
Files changed (46) hide show
  1. package/custom-elements.json +7 -0
  2. package/dist/components/ItemScope.custom-element.js +1 -1
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-bsww9e8f.js +4 -0
  7. package/dist/components/chunks/ItemToolBar-eg8jarh0.js +2 -0
  8. package/dist/components/chunks/{ItemToolBar-pxan157g.js → ItemToolBar-gsb96731.js} +2 -2
  9. package/dist/components/chunks/ItemToolBar-na4e5vwt.js +2 -0
  10. package/dist/components/chunks/{ItemToolBar-xwjrpdmz.js → ItemToolBar-wcw4yxjy.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-zvy0hskj.js +40 -0
  12. package/dist/components/chunks/sre-engine-wazxma67.js +1 -0
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/runtime/SectionControllerBinding.d.ts +16 -5
  16. package/dist/runtime/SectionControllerBinding.js +14 -6
  17. package/dist/runtime/registration-events.d.ts +31 -4
  18. package/dist/runtime/registration-events.js +20 -1
  19. package/dist/runtime/shell-event-bridge.d.ts +7 -1
  20. package/dist/runtime/shell-event-bridge.js +44 -1
  21. package/dist/runtime/shell-scope.d.ts +2 -2
  22. package/dist/runtime/shell-scope.js +14 -5
  23. package/dist/services/SSMLExtractor.js +8 -23
  24. package/dist/services/ToolRegistry.d.ts +11 -3
  25. package/dist/services/ToolRegistry.js +8 -4
  26. package/dist/services/ToolkitCoordinator.d.ts +12 -47
  27. package/dist/services/ToolkitCoordinator.js +45 -284
  28. package/dist/services/section-controller-types.d.ts +71 -20
  29. package/dist/services/tool-config-validation.d.ts +1 -1
  30. package/dist/services/tool-context.js +94 -11
  31. package/dist/services/tool-icons.js +2 -0
  32. package/dist/services/tool-provider-defaults.d.ts +13 -0
  33. package/dist/services/tool-provider-defaults.js +20 -0
  34. package/dist/services/tts/browser-provider.js +3 -1
  35. package/dist/services/tts/tts-lifecycle.d.ts +142 -0
  36. package/dist/services/tts/tts-lifecycle.js +341 -0
  37. package/dist/tools/content-surfaces.d.ts +35 -0
  38. package/dist/tools/content-surfaces.js +35 -0
  39. package/dist/tools/registration.d.ts +2 -0
  40. package/dist/tools/registration.js +5 -0
  41. package/package.json +8 -8
  42. package/dist/components/chunks/ItemToolBar-4vq9nbxz.js +0 -2
  43. package/dist/components/chunks/ItemToolBar-f971t0cw.js +0 -4
  44. package/dist/components/chunks/ItemToolBar-n05xjkp8.js +0 -2
  45. package/dist/components/chunks/ItemToolBar-rgmvw9sm.js +0 -40
  46. package/dist/components/chunks/sre-engine-dq0w45fv.js +0 -1
@@ -1,13 +1,22 @@
1
1
  import type { SessionCommitReason } from "@pie-players/pie-players-shared/pie";
2
- import type { ItemSettings } from "@pie-players/pie-players-shared/types";
2
+ import type { ItemSettings, ServedContentIdentity } from "@pie-players/pie-players-shared/types";
3
3
  import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
4
4
  export declare const PIE_REGISTER_EVENT = "pie-register";
5
5
  export declare const PIE_UNREGISTER_EVENT = "pie-unregister";
6
6
  export declare const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed";
7
7
  export declare const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
8
+ export declare const PIE_ITEM_PLAYER_EVENT = "item-player-event";
9
+ /**
10
+ * The item-player events an item scope in a section relays as
11
+ * `item-player-event`. Each keeps its own behaviour: `load-complete` and
12
+ * `player-error` still stop at the scope, the rest still bubble.
13
+ */
14
+ export declare const RELAYED_ITEM_PLAYER_EVENTS: readonly ["load-complete", "player-error", "correct-responses-populated", "bundle-retry-status", "session-snapshot-available", "backend-load-complete", "backend-model-complete", "backend-session-saved", "backend-score-complete", "backend-error"];
15
+ export type RelayedItemPlayerEventType = (typeof RELAYED_ITEM_PLAYER_EVENTS)[number];
8
16
  export declare const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
17
+ export declare const PIE_INTERNAL_CONTENT_IDENTITY_EVENT = "pie-content-identity";
9
18
  export declare const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
10
- export declare const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
19
+ export declare const PIE_INTERNAL_TRY_ACTION_EVENT = "pie-try-action";
11
20
  export declare const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
12
21
  export type RuntimeRegistrationKind = "item" | "passage";
13
22
  export interface RuntimeRegistrationDetail {
@@ -36,6 +45,14 @@ export interface ItemSessionChangedDetail {
36
45
  /** Set when the change is a commit at a teardown, navigation or page-hidden seam. */
37
46
  sessionCommitReason?: SessionCommitReason;
38
47
  }
48
+ /** An item player's event, with the item it came from. */
49
+ export interface ItemPlayerEventDetail {
50
+ itemId: string;
51
+ canonicalItemId: string;
52
+ type: RelayedItemPlayerEventType;
53
+ /** The item player's own event detail. */
54
+ detail: unknown;
55
+ }
39
56
  export interface InternalItemSessionChangedDetail {
40
57
  itemId: string;
41
58
  session: unknown;
@@ -46,6 +63,16 @@ export interface InternalContentLoadedDetail {
46
63
  contentKind?: string;
47
64
  detail?: unknown;
48
65
  }
66
+ /**
67
+ * The content a backend served an item as, from the item player's
68
+ * `backend-load-complete`. `identity` is absent when a load reported none, which
69
+ * clears what an earlier load reported.
70
+ */
71
+ export interface InternalContentIdentityDetail {
72
+ itemId: string;
73
+ canonicalItemId?: string;
74
+ identity?: ServedContentIdentity;
75
+ }
49
76
  export interface InternalItemPlayerErrorDetail {
50
77
  itemId: string;
51
78
  canonicalItemId?: string;
@@ -53,7 +80,7 @@ export interface InternalItemPlayerErrorDetail {
53
80
  error: unknown;
54
81
  }
55
82
  /**
56
- * A learner's formative action, dispatched by the component that owns the
83
+ * A learner's Try action, dispatched by the component that owns the
57
84
  * control and the item player node — the only place that can call
58
85
  * `provideScore()`. It reports outcomes rather than interpreting them; the
59
86
  * section controller derives correctness and owns the state.
@@ -62,7 +89,7 @@ export interface InternalItemPlayerErrorDetail {
62
89
  * including the `undefined` slots it leaves for models with no element or
63
90
  * controller.
64
91
  */
65
- export interface InternalFormativeActionDetail {
92
+ export interface InternalTryActionDetail {
66
93
  itemId: string;
67
94
  canonicalItemId?: string;
68
95
  action: "check" | "retry";
@@ -2,7 +2,26 @@ export const PIE_REGISTER_EVENT = "pie-register";
2
2
  export const PIE_UNREGISTER_EVENT = "pie-unregister";
3
3
  export const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed";
4
4
  export const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
5
+ export const PIE_ITEM_PLAYER_EVENT = "item-player-event";
6
+ /**
7
+ * The item-player events an item scope in a section relays as
8
+ * `item-player-event`. Each keeps its own behaviour: `load-complete` and
9
+ * `player-error` still stop at the scope, the rest still bubble.
10
+ */
11
+ export const RELAYED_ITEM_PLAYER_EVENTS = [
12
+ "load-complete",
13
+ "player-error",
14
+ "correct-responses-populated",
15
+ "bundle-retry-status",
16
+ "session-snapshot-available",
17
+ "backend-load-complete",
18
+ "backend-model-complete",
19
+ "backend-session-saved",
20
+ "backend-score-complete",
21
+ "backend-error",
22
+ ];
5
23
  export const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
24
+ export const PIE_INTERNAL_CONTENT_IDENTITY_EVENT = "pie-content-identity";
6
25
  export const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
7
- export const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
26
+ export const PIE_INTERNAL_TRY_ACTION_EVENT = "pie-try-action";
8
27
  export const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
@@ -3,7 +3,13 @@ import type { ShellContextKind } from "../context/assessment-toolkit-context.js"
3
3
  * `section`: the shell is its content's only channel to the section. The
4
4
  * player's `session-changed`, `load-complete` and `player-error` stop at the
5
5
  * shell and travel on as the runtime's internal events, with an item's
6
- * response changes also announced as `item-session-changed`.
6
+ * response changes also announced as `item-session-changed`. A session the
7
+ * player filled with the item's correct responses goes neither way: it renders
8
+ * in the player and never enters the section session. An item's
9
+ * host-actionable events are also announced as `item-player-event`, with the
10
+ * item's identity. The identity in
11
+ * an item's delivery `backend-load-complete` travels on too, for the section
12
+ * to record on the item's session; the event itself goes on to the host.
7
13
  *
8
14
  * `plain`: the host owns the player and listens to it, so the player's events
9
15
  * pass through untouched. The runtime still hears that the content loaded or
@@ -1,5 +1,5 @@
1
1
  import { commitPendingSessions, createPieLogger, normalizeItemSessionChange, } from "@pie-players/pie-players-shared";
2
- import { PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, } from "./registration-events.js";
2
+ import { PIE_INTERNAL_CONTENT_IDENTITY_EVENT, PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_ITEM_PLAYER_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, RELAYED_ITEM_PLAYER_EVENTS, } from "./registration-events.js";
3
3
  import { dispatchCrossBoundaryEvent } from "./tool-host-contract.js";
4
4
  /**
5
5
  * Elements repeat themselves, a `complete` echo most often, so a session event
@@ -21,6 +21,9 @@ function sessionEventFingerprint(detail) {
21
21
  return String(semanticDetail);
22
22
  }
23
23
  }
24
+ const isCorrectResponseSession = (detail) => Boolean(detail &&
25
+ typeof detail === "object" &&
26
+ detail.sessionSource === "correct-response");
24
27
  const isCommit = (detail) => Boolean(detail &&
25
28
  typeof detail === "object" &&
26
29
  detail.sessionCommitReason);
@@ -70,6 +73,8 @@ export function createShellEventBridge(options) {
70
73
  if (kind !== "item")
71
74
  return;
72
75
  const detail = event.detail;
76
+ if (isCorrectResponseSession(detail))
77
+ return;
73
78
  const fingerprint = sessionEventFingerprint(detail);
74
79
  // A commit is the response's last chance to reach the section.
75
80
  if (!isCommit(detail) && fingerprint === lastForwardedFingerprint)
@@ -91,6 +96,22 @@ export function createShellEventBridge(options) {
91
96
  };
92
97
  send(PIE_INTERNAL_CONTENT_LOADED_EVENT, detail);
93
98
  };
99
+ const onBackendLoadComplete = (event) => {
100
+ if (mode() !== "section" || kind !== "item")
101
+ return;
102
+ const loaded = event.detail;
103
+ if (loaded?.scope !== "delivery")
104
+ return;
105
+ const { itemId, canonicalItemId } = identity();
106
+ if (!itemId)
107
+ return;
108
+ const detail = {
109
+ itemId,
110
+ canonicalItemId: canonicalItemId || itemId,
111
+ ...(loaded.identity ? { identity: loaded.identity } : {}),
112
+ };
113
+ send(PIE_INTERNAL_CONTENT_IDENTITY_EVENT, detail);
114
+ };
94
115
  const onPlayerError = (event) => {
95
116
  if (mode() === "section")
96
117
  event.stopPropagation();
@@ -105,9 +126,27 @@ export function createShellEventBridge(options) {
105
126
  };
106
127
  send(PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, detail);
107
128
  };
129
+ const relayItemPlayerEvent = (event) => {
130
+ if (mode() !== "section" || kind !== "item")
131
+ return;
132
+ const { itemId, canonicalItemId } = identity();
133
+ if (!itemId)
134
+ return;
135
+ const payload = {
136
+ itemId,
137
+ canonicalItemId: canonicalItemId || itemId,
138
+ type: event.type,
139
+ detail: event.detail,
140
+ };
141
+ dispatchCrossBoundaryEvent(host, PIE_ITEM_PLAYER_EVENT, payload);
142
+ };
108
143
  host.addEventListener("session-changed", onSessionChanged);
109
144
  host.addEventListener("load-complete", onLoadComplete);
145
+ host.addEventListener("backend-load-complete", onBackendLoadComplete);
110
146
  host.addEventListener("player-error", onPlayerError);
147
+ for (const type of RELAYED_ITEM_PLAYER_EVENTS) {
148
+ host.addEventListener(type, relayItemPlayerEvent);
149
+ }
111
150
  return {
112
151
  disconnect: () => {
113
152
  // An escape from the commit must not strand the listeners it needed, so
@@ -120,7 +159,11 @@ export function createShellEventBridge(options) {
120
159
  finally {
121
160
  host.removeEventListener("session-changed", onSessionChanged);
122
161
  host.removeEventListener("load-complete", onLoadComplete);
162
+ host.removeEventListener("backend-load-complete", onBackendLoadComplete);
123
163
  host.removeEventListener("player-error", onPlayerError);
164
+ for (const type of RELAYED_ITEM_PLAYER_EVENTS) {
165
+ host.removeEventListener(type, relayItemPlayerEvent);
166
+ }
124
167
  }
125
168
  },
126
169
  };
@@ -62,7 +62,7 @@ export interface ShellScope {
62
62
  * replays the request instead, once the toolkit's provider connects. A
63
63
  * different runtime answering later, a nearer toolkit taking over, moves the
64
64
  * registration: the old runtime is told to unregister by its id, and the new
65
- * one gets the registration and the content's last load, which only the old
66
- * one heard.
65
+ * one gets the registration and the content's last load and served identity,
66
+ * which only the old one heard.
67
67
  */
68
68
  export declare function createShellScope(): ShellScope;
@@ -1,7 +1,7 @@
1
1
  import { ContextProvider, } from "@pie-players/pie-context";
2
2
  import { assessmentToolkitRegionScopeContext, assessmentToolkitShellContext, } from "../context/assessment-toolkit-context.js";
3
3
  import { catalogSourceSignature, } from "../services/catalog-owner.js";
4
- import { PIE_INTERNAL_CONTENT_LOADED_EVENT } from "./registration-events.js";
4
+ import { PIE_INTERNAL_CONTENT_IDENTITY_EVENT, PIE_INTERNAL_CONTENT_LOADED_EVENT, } from "./registration-events.js";
5
5
  import { createShellRegistrationDispatcher, shellEventTarget, } from "./shell-registration.js";
6
6
  import { connectHostRuntimeContext, dispatchCrossBoundaryEvent, } from "./tool-host-contract.js";
7
7
  /**
@@ -65,8 +65,8 @@ const setAttributeIfChanged = (host, name, value) => {
65
65
  * replays the request instead, once the toolkit's provider connects. A
66
66
  * different runtime answering later, a nearer toolkit taking over, moves the
67
67
  * registration: the old runtime is told to unregister by its id, and the new
68
- * one gets the registration and the content's last load, which only the old
69
- * one heard.
68
+ * one gets the registration and the content's last load and served identity,
69
+ * which only the old one heard.
70
70
  */
71
71
  export function createShellScope() {
72
72
  const registration = createShellRegistrationDispatcher();
@@ -79,6 +79,7 @@ export function createShellScope() {
79
79
  let identity = null;
80
80
  let held = [];
81
81
  let loaded = null;
82
+ let served = null;
82
83
  const syncRegistration = () => {
83
84
  if (runtime !== null)
84
85
  registration.sync(identity, runtime);
@@ -90,6 +91,8 @@ export function createShellScope() {
90
91
  function send(type, detail) {
91
92
  if (type === PIE_INTERNAL_CONTENT_LOADED_EVENT)
92
93
  loaded = detail;
94
+ if (type === PIE_INTERNAL_CONTENT_IDENTITY_EVENT)
95
+ served = detail;
93
96
  if (host && runtime !== null && !runtimeDeparted(host, runtime)) {
94
97
  dispatchCrossBoundaryEvent(shellEventTarget(host, runtime), type, {
95
98
  ...detail,
@@ -113,8 +116,13 @@ export function createShellScope() {
113
116
  held = [];
114
117
  for (const event of pending)
115
118
  send(event.type, event.detail);
116
- const replayed = pending.some((event) => event.type === PIE_INTERNAL_CONTENT_LOADED_EVENT);
117
- if (moved && loaded && !replayed) {
119
+ if (!moved)
120
+ return;
121
+ const replayed = (type) => pending.some((event) => event.type === type);
122
+ if (served && !replayed(PIE_INTERNAL_CONTENT_IDENTITY_EVENT)) {
123
+ send(PIE_INTERNAL_CONTENT_IDENTITY_EVENT, served);
124
+ }
125
+ if (loaded && !replayed(PIE_INTERNAL_CONTENT_LOADED_EVENT)) {
118
126
  send(PIE_INTERNAL_CONTENT_LOADED_EVENT, loaded);
119
127
  }
120
128
  }
@@ -126,6 +134,7 @@ export function createShellScope() {
126
134
  runtime = null;
127
135
  held = [];
128
136
  loaded = null;
137
+ served = null;
129
138
  identity = null;
130
139
  shell = null;
131
140
  region = null;
@@ -14,6 +14,7 @@
14
14
  * This enables proper pronunciation, emphasis, and pacing for TTS without
15
15
  * requiring authors to maintain separate catalog files.
16
16
  */
17
+ import { isCloudMetadataHostname, isPrivateHostname, } from "@pie-players/pie-players-shared/security/private-hosts";
17
18
  /**
18
19
  * Allow-listed SSML elements (from the SSML 1.1 spec subset we forward to
19
20
  * TTS providers). Anything outside this list is unwrapped during
@@ -108,27 +109,11 @@ function expandSelfClosingVoidTags(input) {
108
109
  return next;
109
110
  }
110
111
  /**
111
- * Hosts that must never appear inside an SSML `<audio src>` because they
112
- * would cause the server-side TTS provider to fetch a private / metadata
113
- * endpoint on behalf of the request. Mirrors the SchoolCity provider's
114
- * defense-in-depth check; the SSML-side block is cheap and belt-and-braces.
112
+ * Whether an SSML `<audio src>` may stay. The server-side TTS provider fetches
113
+ * it on the request's behalf, so it must be http(s) and must not reach a cloud
114
+ * metadata or private host. The provider checks its own asset URLs against the
115
+ * same lists; this SSML-side block is cheap and belt-and-braces.
115
116
  */
116
- const SSML_PRIVATE_AUDIO_HOSTS = [
117
- /^localhost$/i,
118
- /^127(?:\.\d{1,3}){3}$/,
119
- /^0(?:\.\d{1,3}){3}$/,
120
- /^10(?:\.\d{1,3}){3}$/,
121
- /^192\.168(?:\.\d{1,3}){2}$/,
122
- /^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/,
123
- /^169\.254(?:\.\d{1,3}){2}$/,
124
- /^::1$/,
125
- /^fe80:/i,
126
- /^fc00:/i,
127
- /^fd[0-9a-f]{2}:/i,
128
- /^metadata\.google\.internal$/i,
129
- /^metadata\.azure\.internal$/i,
130
- /^metadata\.packet\.net$/i,
131
- ];
132
117
  function isSafeSsmlAudioSrc(raw) {
133
118
  if (typeof raw !== "string" || raw.length === 0)
134
119
  return false;
@@ -142,8 +127,8 @@ function isSafeSsmlAudioSrc(raw) {
142
127
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
143
128
  return false;
144
129
  }
145
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "");
146
- return !SSML_PRIVATE_AUDIO_HOSTS.some((pattern) => pattern.test(hostname));
130
+ return (!isCloudMetadataHostname(parsed.hostname) &&
131
+ !isPrivateHostname(parsed.hostname));
147
132
  }
148
133
  /**
149
134
  * Sanitize an SSML subtree in place: remove disallowed attributes and
@@ -173,7 +158,7 @@ function sanitizeSsmlElement(element) {
173
158
  }
174
159
  }
175
160
  // `src` survives the attribute allow-list, and a server-side provider
176
- // fetches it: keep it only for an http(s) URL off SSML_PRIVATE_AUDIO_HOSTS.
161
+ // fetches it: keep it only when isSafeSsmlAudioSrc allows it.
177
162
  if (element.tagName.toLowerCase() === "audio") {
178
163
  const src = element.getAttribute("src");
179
164
  if (src !== null && !isSafeSsmlAudioSrc(src)) {
@@ -545,8 +545,12 @@ export interface ToolRegistration {
545
545
  * Declare this only where the tool's own controls provably do nothing:
546
546
  * withdrawing a granted accommodation on a false negative is the more
547
547
  * expensive failure. Omitting it means "applicable".
548
+ *
549
+ * `config` is this tool's `tools.providers.<toolId>` entry, when the surface
550
+ * asking has one: host configuration can narrow what a tool acts on, and a
551
+ * tool that acts on nothing here is not applicable.
548
552
  */
549
- isApplicableToContent?(context: ToolContext): boolean;
553
+ isApplicableToContent?(context: ToolContext, config?: ToolProviderConfig): boolean;
550
554
  /**
551
555
  * Toolbar render contract. Required for `toolbar-toggle` and
552
556
  * `selection-gateway`; a region capability renders through
@@ -666,8 +670,10 @@ export declare class ToolRegistry {
666
670
  * @param contexts - Every context the tool could act on at this placement
667
671
  * @param onFailure - Receives the gate's throw; the tool then counts as
668
672
  * applicable. Logs when omitted.
673
+ * @param config - The tool's `tools.providers.<toolId>` entry, passed to the
674
+ * gate
669
675
  */
670
- isApplicableToAnyContext(toolId: string, contexts: readonly ToolContext[], onFailure?: ToolCallbackFailureHandler): boolean;
676
+ isApplicableToAnyContext(toolId: string, contexts: readonly ToolContext[], onFailure?: ToolCallbackFailureHandler, config?: ToolProviderConfig): boolean;
671
677
  /**
672
678
  * Passes 2 and 3 over a policy decision's tools: the ids a surface at `level`
673
679
  * shows against `contexts`, in decision order. This is the rule a toolbar
@@ -687,8 +693,10 @@ export declare class ToolRegistry {
687
693
  * @param contexts - Every context the tools could act on at that surface
688
694
  * @param onFailure - Receives a relevance or applicability check's throw.
689
695
  * Logs when omitted.
696
+ * @param getToolConfig - A tool's `tools.providers.<toolId>` entry, which
697
+ * applicability receives; a toolkit coordinator's `getToolConfig`
690
698
  */
691
- filterDecidedToolIds(entries: readonly Pick<ToolPolicyEntry, "toolId" | "required" | "alwaysAvailable">[], level: ToolLevel, contexts: readonly ToolContext[], onFailure?: ToolCallbackFailureHandler): string[];
699
+ filterDecidedToolIds(entries: readonly Pick<ToolPolicyEntry, "toolId" | "required" | "alwaysAvailable">[], level: ToolLevel, contexts: readonly ToolContext[], onFailure?: ToolCallbackFailureHandler, getToolConfig?: (toolId: string) => ToolProviderConfig | null): string[];
692
700
  /**
693
701
  * Plain metadata for every registration, in registration order, for UIs
694
702
  * such as a PNP configuration screen.
@@ -382,8 +382,10 @@ export class ToolRegistry {
382
382
  * @param contexts - Every context the tool could act on at this placement
383
383
  * @param onFailure - Receives the gate's throw; the tool then counts as
384
384
  * applicable. Logs when omitted.
385
+ * @param config - The tool's `tools.providers.<toolId>` entry, passed to the
386
+ * gate
385
387
  */
386
- isApplicableToAnyContext(toolId, contexts, onFailure = logToolCallbackFailure) {
388
+ isApplicableToAnyContext(toolId, contexts, onFailure = logToolCallbackFailure, config) {
387
389
  const tool = this.get(toolId);
388
390
  if (!tool?.isApplicableToContent)
389
391
  return true;
@@ -391,7 +393,7 @@ export class ToolRegistry {
391
393
  return true;
392
394
  return contexts.some((context) => {
393
395
  try {
394
- return tool.isApplicableToContent?.(context) ?? true;
396
+ return tool.isApplicableToContent?.(context, config) ?? true;
395
397
  }
396
398
  catch (error) {
397
399
  onFailure(toolId, "tool-applicability", error);
@@ -419,8 +421,10 @@ export class ToolRegistry {
419
421
  * @param contexts - Every context the tools could act on at that surface
420
422
  * @param onFailure - Receives a relevance or applicability check's throw.
421
423
  * Logs when omitted.
424
+ * @param getToolConfig - A tool's `tools.providers.<toolId>` entry, which
425
+ * applicability receives; a toolkit coordinator's `getToolConfig`
422
426
  */
423
- filterDecidedToolIds(entries, level, contexts, onFailure = logToolCallbackFailure) {
427
+ filterDecidedToolIds(entries, level, contexts, onFailure = logToolCallbackFailure, getToolConfig) {
424
428
  const granted = new Set();
425
429
  const decided = [];
426
430
  for (const entry of entries) {
@@ -447,7 +451,7 @@ export class ToolRegistry {
447
451
  }
448
452
  }
449
453
  return decided.filter((toolId) => relevant.has(toolId) &&
450
- this.isApplicableToAnyContext(toolId, contexts, onFailure));
454
+ this.isApplicableToAnyContext(toolId, contexts, onFailure, getToolConfig?.(toolId) ?? undefined));
451
455
  }
452
456
  /**
453
457
  * Plain metadata for every registration, in registration order, for UIs
@@ -35,13 +35,14 @@ import type { ToolCoordinatorApi } from "./interfaces.js";
35
35
  import { TTSService } from "./TTSService.js";
36
36
  import { ToolProviderRegistry } from "./tool-providers/index.js";
37
37
  import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
38
+ import { TTS_TOOL_ID } from "./tts/tts-lifecycle.js";
38
39
  import { ToolRegistry } from "./ToolRegistry.js";
39
40
  import type { ResolvedToolContext, ToolContextResolverContext, ToolContextResolverMap } from "./ToolRegistry.js";
40
41
  import type { ToolFailurePhase } from "./tool-failure.js";
41
42
  import type { ToolOpenRequest, ToolRequestTarget } from "./tool-request.js";
42
43
  import { type FeaturePolicyDecision, type PnpEnforcementMode, type PolicySource, type ResolvedEngineInputs, type ToolPolicyChangeListener, type ToolPolicyDecision, type ToolPolicyDecisionRequest, type ToolPolicyDiagnostic, type ToolScope } from "../policy/engine.js";
43
44
  import type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionControllerSessionState, SectionSessionPersistenceStrategy, SectionPersistenceFactoryDefaults } from "./section-controller-types.js";
44
- export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionControllerLoadedRenderable, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, } from "./section-controller-types.js";
45
+ export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionItemScore, SectionItemScorer, SectionScore, SectionControllerLoadedRenderable, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, } from "./section-controller-types.js";
45
46
  export type { FeaturePolicyDecision, PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest, } from "../policy/engine.js";
46
47
  export interface ToolkitToolsConfig extends CanonicalToolsConfig {
47
48
  policy: ToolPolicyConfig;
@@ -291,11 +292,7 @@ export declare class ToolkitCoordinator {
291
292
  readonly hooks: ToolkitCoordinatorHooks;
292
293
  readonly lazyInit: boolean;
293
294
  /** Track TTS initialization state */
294
- private ttsInitialized;
295
- /** Text-to-speech failed to start and policy does not grant it. */
296
- private ttsDegraded;
297
- private ttsInitPromise?;
298
- private ttsReconfigurePromise?;
295
+ private tts;
299
296
  /**
300
297
  * The loader ran, whether or not it returned state. A failed load is
301
298
  * reported once and the coordinator runs without saved tool state.
@@ -398,18 +395,6 @@ export declare class ToolkitCoordinator {
398
395
  * lifetime for no new information.
399
396
  */
400
397
  private reportedUnboundFeaturePolicy;
401
- /**
402
- * Whether {@link _initializeTTS} has already reported a non-browser backend
403
- * falling back to browser speech because no text-to-speech provider is registered.
404
- * Once per coordinator: a text-to-speech config change re-runs
405
- * initialization, and the missing provider is the same gap each time.
406
- */
407
- private reportedMissingTTSProvider;
408
- /**
409
- * The backend of a fallback {@link reportMissingTTSProvider} held back while
410
- * the registry was unsettled, reported if the bound toolkit has no registry.
411
- */
412
- private heldMissingTTSProviderBackend;
413
398
  /**
414
399
  * Whether {@link validateToolsConfig} has already reported validating with
415
400
  * no registry (`tools.registryUnavailable`). Once per coordinator: every
@@ -528,7 +513,6 @@ export declare class ToolkitCoordinator {
528
513
  private ensureStateLoaded;
529
514
  /** Whether the toolkit this coordinator binds to may still supply a registry. */
530
515
  private toolRegistryPending;
531
- private reportMissingTTSProvider;
532
516
  /**
533
517
  * Take the registry of the toolkit this coordinator is bound to, when the host
534
518
  * constructed it without one: the config is validated against it, its
@@ -694,27 +678,13 @@ export declare class ToolkitCoordinator {
694
678
  private disposeOwnedResources;
695
679
  private waitForAdmittedInitialization;
696
680
  /**
697
- * Initialize the TTS service from the `textToSpeech` tool config.
681
+ * Initialize the TTS service from the `textToSpeech` tool config. The
682
+ * lifecycle itself lives in `services/tts/tts-lifecycle.ts`; the coordinator
683
+ * keeps the public method hosts call.
698
684
  */
699
685
  ensureTTSReady(): Promise<void>;
700
- private waitForPendingTTSReconfigure;
701
- private _initializeTTS;
702
- private resolveTTSToolConfig;
703
- private initializeTTSService;
704
686
  waitUntilReady(): Promise<void>;
705
687
  isReady(): boolean;
706
- /**
707
- * Readiness waits for text-to-speech when it is enabled and either starts
708
- * eagerly or is granted: a granted read-aloud fails before the learner starts.
709
- */
710
- private ttsRequiredForReadiness;
711
- /**
712
- * Starts text-to-speech for readiness. A failure policy does not grant leaves
713
- * the tool degraded, reporting itself unavailable, and readiness settles; a
714
- * granted tool's failure rejects.
715
- */
716
- private startTTSForReadiness;
717
- private getTTSConfigFromProviders;
718
688
  private assertCanonicalToolId;
719
689
  private installToolContextResolvers;
720
690
  /**
@@ -725,14 +695,17 @@ export declare class ToolkitCoordinator {
725
695
  setToolContextResolvers(resolvers: ToolContextResolverMap | null | undefined): void;
726
696
  private notifyToolContextResolverChange;
727
697
  /**
728
- * Get tool configuration, or null if the tool is not configured.
698
+ * Get tool configuration, or null if the tool is not configured. The
699
+ * read-aloud overload takes its literal from `TTS_TOOL_ID` (the same type the
700
+ * runtime compares against), so the capability id is named once, where the
701
+ * capability lives.
729
702
  */
730
- getToolConfig(toolId: "textToSpeech"): TextToSpeechToolProviderConfig | null;
703
+ getToolConfig(toolId: typeof TTS_TOOL_ID): TextToSpeechToolProviderConfig | null;
731
704
  getToolConfig(toolId: string): ToolProviderConfig | null;
732
705
  /**
733
706
  * Update tool configuration and apply the change to the underlying services.
734
707
  */
735
- updateToolConfig(toolId: "textToSpeech", updates: Partial<TextToSpeechToolProviderConfig>): void;
708
+ updateToolConfig(toolId: typeof TTS_TOOL_ID, updates: Partial<TextToSpeechToolProviderConfig>): void;
736
709
  updateToolConfig(toolId: string, updates: Partial<ToolProviderConfig>): void;
737
710
  /**
738
711
  * Patch one or more placement levels in the canonical tools config.
@@ -899,12 +872,4 @@ export declare class ToolkitCoordinator {
899
872
  * so a check on the policy change the update dispatched finds the new provider.
900
873
  */
901
874
  private _registerChangedToolProvider;
902
- /**
903
- * Re-register the text-to-speech provider for the current config and registry, then
904
- * re-initialize unless initialization is lazy. An initialization already
905
- * running finishes first: it would otherwise mark its provider ready after
906
- * the reset.
907
- */
908
- private scheduleTTSReconfigure;
909
- private _reconfigureTTSProvider;
910
875
  }