
Security News
US Government Forces Anthropic to Pull Claude Fable Days After Launch
Anthropic says the directive cited national security concerns over a narrow jailbreak, but offered no specific technical details.
@pie-players/pie-assessment-toolkit
Advanced tools
PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination
Independent, composable services for coordinating tools, accommodations, and item players in assessment applications.
This is not an opinionated framework or monolithic "player" - it's a toolkit that solves specific problems through centralized service management.
✨ Centralized Service Management: The new ToolkitCoordinator provides a single entry point for all toolkit services, simplifying initialization and configuration.
Before (scattered services):
// Create 5+ services independently
const ttsService = new TTSService();
const toolCoordinator = new ToolCoordinator();
const highlightCoordinator = new HighlightCoordinator();
const catalogResolver = new AccessibilityCatalogResolver([...]);
// Missing: ElementToolStateStore
await ttsService.initialize(new BrowserTTSProvider());
ttsService.setCatalogResolver(catalogResolver);
// Pass all services separately
player.ttsService = ttsService;
player.toolCoordinator = toolCoordinator;
// ...
After (coordinator orchestrates):
// Create one coordinator with configuration
const toolkitCoordinator = new ToolkitCoordinator({
assessmentId: 'my-assessment',
tools: {
providers: {
textToSpeech: { enabled: true, backend: 'browser' },
calculator: { enabled: true }
},
placement: {
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech']
}
}
});
// Pass single coordinator to section-player through runtime
player.runtime = { ...(player.runtime ?? {}), coordinator: toolkitCoordinator };
Toolkit instrumentation is provider-agnostic and additive. It uses the shared
InstrumentationProvider contract from @pie-players/pie-players-shared.
When toolkit is hosted by section/assessment player flows, the canonical provider path is the item-player loader config:
runtime.player.loaderConfig.instrumentationProvidertrackPageActions: true, missing/undefined providers use the default New Relic provider path.instrumentationProvider: null explicitly disables instrumentation.item-player behavior remains the compatibility anchor.CompositeInstrumentationProvider (for example New Relic + debug panel).pie-toolkit-runtime-ownedpie-toolkit-runtime-inheritedpie-toolkit-readypie-toolkit-section-readypie-toolkit-framework-errorToolkit tool/backend operational stream:
pie-tool-init-start|success|errorpie-tool-backend-call-start|success|errorpie-tool-library-load-start|success|errorOwnership boundary: toolkit emits toolkit lifecycle semantics only. Section and assessment semantic streams stay in their own layers to avoid overlap. Bridge dedupe is a safety net, not a substitute for clear ownership.
See the ToolkitCoordinator section in the architecture overview for the current design documentation.
runtimeThis package and @pie-players/pie-section-player follow a deliberate
two-tier configuration model. The same knob can usually be set in either
tier; the choice is about ergonomics, not capability.
Easy tier — top-level CE attributes / properties. Use these for the common cases that are static for the lifetime of the player or that hosts want to set declaratively in HTML / templating frameworks. Example:
<pie-assessment-toolkit
assessment-id="my-assessment"
section-id="s-1"
tool-config-strictness="warn"
></pie-assessment-toolkit>
Sophisticated tier — passing a constructed ToolkitCoordinator (or a
runtime object on consumer CEs). Use this for advanced cases: composed
configuration, dynamic overrides, runtime mutation, fields without a
tier-1 attribute, or anything that benefits from being a single typed
object passed by reference. Example:
const coordinator = new ToolkitCoordinator({
assessmentId: "my-assessment",
toolConfigStrictness: "warn",
tools: {
providers: { calculator: { enabled: true } },
placement: { item: ["calculator", "textToSpeech"] },
},
});
el.runtime = { ...(el.runtime ?? {}), coordinator };
Top-level attributes use kebab-case (assessment-id,
tool-config-strictness). Section-player runtime configuration is grouped
under the runtime object instead of duplicated as top-level props.
The configuration object owns runtime fields. Section-player layout attributes cover identity, layout, diagnostics, and callback/event convenience:
runtime.<key> for player, tool, accessibility, coordinator, env,
isolation, and runtime factories.The tier-1 attribute set is the same shape across
pie-assessment-toolkit, pie-section-player-base, and the
pie-section-player-* layout elements (locked in M5). Every tier-1
surface obeys the strict mirror rule:
kebab-attribute ↔ camelCaseProp ↔ runtime.<sameCamelCaseKey>
Common members include:
assessment-id, section-id, attempt-idruntimetools, tool-registry, coordinator,
accessibilitytool-config-strictness, debug. Framework-error
delivery is via the canonical onFrameworkError callback prop and the
framework-error DOM event dispatched on the layout CE host.Documented exceptions to the mirror rule:
section-id, attempt-id, section): per-attempt host
state, not configuration.show-toolbar, toolbar-position, narrow-layout-breakpoint,
split-pane-collapse-strategy): layout-CE rendering concerns.tools.placement.item / tools.placement.passage (object form) or
runtime.tools.placement.{item,passage} directly.createSectionController, isolation): accepted only via
runtime.<key>. <pie-assessment-toolkit>
itself keeps createSectionController and isolation as JS-only
props (no kebab-attribute surface): section-player layouts forward
runtime.isolation and runtime.createSectionController to the
wrapped toolkit via property bindings; standalone hosts that need
to override coordinator inheritance should pass an explicit
coordinator={...} instead.Add a tier-1 attribute only if all of the following hold:
ToolkitCoordinator
/ runtime object.Otherwise expose it through the configuration object only.
import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
// Create coordinator with configuration
const coordinator = new ToolkitCoordinator({
assessmentId: 'demo-assessment',
tools: {
providers: {
textToSpeech: { enabled: true, backend: 'browser', defaultVoice: 'en-US' },
calculator: { enabled: true }
},
placement: {
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech']
}
},
accessibility: {
catalogs: assessment.accessibilityCatalogs || [],
language: 'en-US'
}
});
// Pass to section player
const player = document.getElementById('player');
player.runtime = { ...(player.runtime ?? {}), coordinator };
// Access services directly if needed
const ttsService = coordinator.ttsService;
const toolState = coordinator.elementToolStateStore.getAllState();
For host-side session/progress logic, prefer helper subscriptions over the generic filter API. Subscriptions follow the toolkit's active section cohort automatically — a single subscribe* call survives navigation between sections without re-wiring:
const unsubscribeItem = coordinator.subscribeItemEvents({
itemIds: ['item-1', 'item-2'],
listener: (event) => {
// item-selected, item-session-data-changed, item-complete-changed, ...
}
});
const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
listener: (event) => {
// section-loading-complete, section-items-complete-changed, section-error, ...
}
});
// cleanup
unsubscribeItem?.();
unsubscribeSection?.();
Behavior pins (PIE-512 Phase D):
getOrCreateSectionController(...) resolves; calling subscribe before any cohort exists throws.getOrCreateSectionController for a new section), the listener is automatically migrated to the new controller and receives a snapshot replay (content-loaded × N then section-loading-complete) in the same order a fresh subscriber would have seen.console.warn-logged; the throw does not interrupt fan-out to other listeners.Use subscribeSectionEvents(...) when you need advanced/custom filtering mixes. Section-scoped events do not carry item IDs, so pairing them with itemIds filters will not match.
To persist or snapshot an inactive section, use coordinator.getSectionController({ sectionId, attemptId }) — that lookup is by id and is unaffected by the active-cohort behavior described above.
<0.3.35 (BREAKING — pre-1.0)0.3.35 is the first release where subscribeSectionEvents (and its two helper wrappers subscribeItemEvents / subscribeSectionLifecycleEvents) follows the toolkit's active section cohort automatically. The on-the-wire shape of the subscription args object changed.
If your host code looked like this:
const unsub = coordinator.subscribeItemEvents({
sectionId: 'section-1',
attemptId: 'attempt-1',
listener: handleEvent,
});
Update it to drop sectionId / attemptId:
const unsub = coordinator.subscribeItemEvents({
listener: handleEvent,
});
What this means in practice for typed integrations:
SectionEventSubscriptionArgs, SectionItemEventSubscriptionArgs, and SectionScopedEventSubscriptionArgs no longer declare sectionId? / attemptId? properties. Any host that imports these arg types directly and passes those keys will fail to compile after upgrade. Action required.sectionId / attemptId continues to work without source changes. The args have no effect at runtime — the subscription always follows the active cohort.subscribe* now throws if no active section cohort exists. Subscribe after the first getOrCreateSectionController(...) resolves. Subscribing on toolkit-ready alone is no longer sufficient — though in practice the section player emits toolkit-ready after its first getOrCreateSectionController(...) resolves, so a toolkit-ready anchor is safe in section-player hosts.toolkit-ready. Hosts that detached and re-subscribed on every toolkit-ready event (the correct pre-Phase D pattern, since each subscription was pinned to a sectionId) will now observe two snapshot replays per navigation: one delivered automatically when Phase D migrates the existing listener to the new active cohort, and a second when the manual re-subscribe attaches a fresh listener that replays again. Listener handlers that are not strictly idempotent will fire twice — analytics pageActions, non-Set counters, side-effecting hydration. The fix is a one-line guard (if (this.controllerUnsubscribe) return;) so the subscribe runs only on the first toolkit-ready.coordinator.getSectionController({ sectionId, attemptId }) and subscribe directly on the controller handle (controller.subscribe?.(...)) — that binding is pinned to one controller instance and does not migrate.If your local types were hand-rolled structural copies of the public arg types (e.g. an Angular wrapper duplicating the shape rather than importing the package types), sectionId / attemptId keys will compile but are dead code at runtime — recommend dropping them as part of the upgrade.
// BEFORE (pre-Phase D): rebind for every section change because the
// subscription was pinned to a sectionId.
public handleToolkitReady(event: Event): void {
const coordinator = (event as CustomEvent).detail?.coordinator;
if (!coordinator) return;
this.controllerUnsubscribe?.(); // detach prior pin
const itemUnsub = coordinator.subscribeItemEvents({
sectionId: this.sectionId,
listener: handleItemEvent,
});
const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
sectionId: this.sectionId,
listener: handleSectionEvent,
});
this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
}
// AFTER (Phase D): subscribe once; the listener follows the active
// cohort across all subsequent navigation.
public handleToolkitReady(event: Event): void {
const coordinator = (event as CustomEvent).detail?.coordinator;
if (!coordinator) return;
this.toolkitCoordinator = coordinator;
if (this.controllerUnsubscribe) return; // already subscribed; do nothing on re-fire
const itemUnsub = coordinator.subscribeItemEvents({
listener: handleItemEvent,
});
const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
listener: handleSectionEvent,
});
this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
}
import {
TTSService,
BrowserTTSProvider,
ToolCoordinator,
HighlightCoordinator,
AccessibilityCatalogResolver,
ElementToolStateStore
} from '@pie-players/pie-assessment-toolkit';
// Initialize each service independently
const ttsService = new TTSService();
const toolCoordinator = new ToolCoordinator();
const highlightCoordinator = new HighlightCoordinator();
const elementToolStateStore = new ElementToolStateStore();
const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
await ttsService.initialize(new BrowserTTSProvider());
ttsService.setCatalogResolver(catalogResolver);
// Pass services individually
player.ttsService = ttsService;
player.toolCoordinator = toolCoordinator;
// ...
The toolkit uses one canonical tools model with three concerns:
policy: allow/block constraints (global gates)placement: where tools appear (assessment, section, item, passage, rubric, plus custom registered levels)providers: provider/runtime options (calculator, textToSpeech, etc.)Example:
tools: {
policy: {
allowed: ['calculator', 'textToSpeech', 'answerEliminator', 'graph', 'periodicTable'],
blocked: ['graph']
},
placement: {
assessment: [],
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech'],
rubric: []
},
providers: {
calculator: { authFetcher: async () => ({ apiKey: '...' }) },
textToSpeech: { enabled: true, backend: 'browser', defaultVoice: 'en-US' }
}
}
The runtime still distinguishes between contextual (item/passage) and section-wide tools:
Tool instances use structured IDs so scope is explicit:
<toolId>:<scopeLevel>:<scopeId>[:inline]
Examples:
calculator:section:section-1calculator:item:item-42textToSpeech:passage:passage-1highlighter:rubric:rubric-3tools.placement.item)Tools that operate within the context of a specific question/item:
tools: {
placement: {
item: ['calculator', 'textToSpeech', 'answerEliminator']
}
}
Characteristics:
Available Item-Level Tools:
Example Use Case: A student uses answer eliminator on Question 3 to cross out choices B and D. When they navigate to Question 4, they see fresh, uneliminated choices. When they return to Question 3, their eliminations are restored.
tools.placement.section)Tools that float above the entire assessment and persist across questions:
tools: {
placement: {
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
},
providers: {
calculator: {
enabled: true,
authFetcher: async () => { /* ... */ }
}
}
}
Characteristics:
Available Floating Tools:
Example Use Case: A student opens the calculator on Question 2, computes 45 × 12 = 540. They navigate to Question 7, and the calculator still shows their computation history. They can reference previous calculations across multiple questions without losing context.
Use item-level tools when:
Use floating tools when:
Complete example showing both types:
const coordinator = new ToolkitCoordinator({
assessmentId: 'math-exam',
tools: {
placement: {
// Contextual placement
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech'],
// Section-level utilities
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
},
providers: {
calculator: {
enabled: true,
authFetcher: async () => {
const response = await fetch('/api/tools/desmos/auth');
return response.json();
}
},
textToSpeech: { enabled: true, backend: 'browser' }
}
},
accessibility: {
catalogs: [],
language: 'en-US'
}
});
Simple Default (All Tools Enabled):
For most use cases, simply enable all available tools:
const coordinator = new ToolkitCoordinator({
assessmentId: 'my-assessment',
tools: {
placement: {
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech']
},
providers: {
calculator: { enabled: true },
textToSpeech: { enabled: true, backend: 'browser' }
}
}
});
The ToolkitCoordinator handles all internal complexity (service initialization, provider management, state coordination). The only special configuration is authFetcher for Desmos calculator (optional - falls back to local calculator if not provided).
For Polly/Google server-backed TTS, the provider config supports a minimal form. Common options are defaulted so you can start with:
tools: {
providers: {
textToSpeech: {
enabled: true,
backend: 'polly'
}
}
}
By default, server-backed TTS resolves:
apiEndpoint: '/api/tts'transportMode: 'pie'endpointValidationMode: 'voices'You can still set apiEndpoint explicitly when your host route is not /api/tts.
Inline TTS speed buttons are configurable via speedOptions in provider settings.
tools: {
providers: {
textToSpeech: {
enabled: true,
backend: "browser",
settings: {
speedOptions: [2, 1.25, 1.5] // rendered in this order
}
}
}
}
speedOptions semantics:
0.8x, 1.25x).[]): hide all speed buttons.["fast", -1, 1]): fall back to defaults.1 is excluded (normal speed is already available by toggling active speed off).When server-backed playback fails at runtime (for example 503, network outage,
or synthesized asset fetch failure), TTSService now performs a one-time
runtime fallback for that session:
speak() request once.This keeps the inline/passage TTS controls usable during transient backend incidents without requiring host-side reconfiguration.
Telemetry emitted for observability:
pie-tool-runtime-fallback (fallback switch succeeded)pie-tool-runtime-fallback-error (fallback switch failed)provider.runtime.authFetcher is optional. Add it only when your host environment
requires runtime auth material for TTS requests:
tools: {
providers: {
textToSpeech: {
enabled: true,
backend: 'polly',
apiEndpoint: '/api/tts',
provider: {
runtime: {
authFetcher: async () => {
const response = await fetch('/api/tts/auth');
return response.json();
}
}
}
}
}
}
For custom backends that return URL assets (for example { audioContent, word }),
prefer a host-owned proxy endpoint so secrets never ship to the browser.
tools: {
providers: {
textToSpeech: {
enabled: true,
backend: "server",
serverProvider: "custom",
transportMode: "custom",
endpointMode: "rootPost",
endpointValidationMode: "none",
apiEndpoint: "/api/tts/sc",
speedRate: "medium",
lang_id: "en-US",
cache: true
}
}
}
Recommended host boundary:
/api/tts/sc) only.SchoolCity is used as a host-configured integration example for custom transport. Toolkit defaults still remain browser/standard providers unless the host explicitly configures custom server-backed TTS.
The toolkit exposes a canonical TestAttemptSession runtime and a deterministic adapter for pie backend activity payloads from ../../kds/pie-api-aws.
import {
mapActivityToTestAttemptSession,
toItemSessionsRecord,
buildActivitySessionPatchFromTestAttemptSession
} from "@pie-players/pie-assessment-toolkit";
const testAttemptSession = mapActivityToTestAttemptSession({
activityDefinition,
activitySession
});
// Use in section-player handoff (same item session shape as item players expect)
const itemSessions = toItemSessionsRecord(testAttemptSession);
// Host-owned backend persistence payload
const patch = buildActivitySessionPatchFromTestAttemptSession(testAttemptSession);
@pie-players/pie-section-player stays backend-agnostic and emits session/state changes.../../kds/pie-api-aws).For section-level session flows, the toolkit supports two complementary APIs:
createSectionSessionPersistence(context, defaults) for load/save/clear orchestrationgetSession(), applySession(session, { mode }), updateItemSession(itemId, detail)The persistence strategy works with the same SectionControllerSessionState shape exposed by the controller, so hosts can choose bulk restore (applySession) and fine-grained updates (updateItemSession) without internal runtime coupling.
<speak> tagsThe toolkit integrates seamlessly with the PIE Section Player:
<speak> tags from passages and itemsexport interface ToolkitCoordinatorConfig {
assessmentId: string; // Required: unique assessment identifier
tools?: {
policy?: {
allowed?: string[];
blocked?: string[];
};
placement?: {
assessment?: string[];
section?: string[];
item?: string[];
passage?: string[];
rubric?: string[];
};
providers?: {
textToSpeech?: {
enabled?: boolean;
backend?: 'browser' | 'polly' | 'google' | 'server';
defaultVoice?: string;
rate?: number;
};
calculator?: {
enabled?: boolean;
authFetcher?: () => Promise<Record<string, unknown>>;
};
};
};
toolRegistry?: ToolRegistry | null;
accessibility?: {
catalogs?: any[];
language?: string;
};
}
// Get all services as a bundle
const services = coordinator.getServiceBundle();
// Returns: { ttsService, toolCoordinator, highlightCoordinator, elementToolStateStore, catalogResolver }
// Tool configuration
coordinator.isToolEnabled('textToSpeech'); // Check if tool is enabled
coordinator.getToolConfig('textToSpeech'); // Get tool-specific config
coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // Update tool config
All services are public properties for direct access:
coordinator.ttsService // TTSService instance
coordinator.toolCoordinator // ToolCoordinator instance
coordinator.highlightCoordinator // HighlightCoordinator instance
coordinator.elementToolStateStore // ElementToolStateStore instance
coordinator.catalogResolver // AccessibilityCatalogResolver instance
The ElementToolStateStore manages ephemeral tool state at the element level using globally unique composite keys.
${assessmentId}:${sectionId}:${itemId}:${elementId}// Generate global element ID
const globalElementId = store.getGlobalElementId(
'demo-assessment',
'section-1',
'question-1',
'mc1'
);
// Returns: "demo-assessment:section-1:question-1:mc1"
// Parse global element ID
const components = store.parseGlobalElementId(globalElementId);
// Returns: { assessmentId, sectionId, itemId, elementId }
// Set state for a tool on an element
store.setState(globalElementId, 'answerEliminator', {
eliminatedChoices: ['choice-a', 'choice-c']
});
// Get state for a specific tool
const state = store.getState(globalElementId, 'answerEliminator');
// Get all tool states for an element
const elementState = store.getElementState(globalElementId);
// Get all states across all elements
const allState = store.getAllState();
// Clear state for a specific element
store.clearElement(globalElementId);
// Clear state for a specific tool across all elements
store.clearTool('answerEliminator');
// Clear all elements in a specific section
store.clearSection('demo-assessment', 'section-1');
// Clear all state
store.clearAll();
// Set callback for persistence (e.g., localStorage)
store.setOnStateChange((state) => {
localStorage.setItem('tool-state', JSON.stringify(state));
});
// Load state from persistence
const saved = localStorage.getItem('tool-state');
if (saved) {
store.loadState(JSON.parse(saved));
}
// Subscribe to state changes
const unsubscribe = store.subscribe((state) => {
console.log('State changed:', state);
});
// Unsubscribe when done
unsubscribe();
const ttsService = new TTSService();
// Initialize with provider
await ttsService.initialize(new BrowserTTSProvider());
// Set catalog resolver for SSML support
ttsService.setCatalogResolver(catalogResolver);
// Playback
await ttsService.speak('Read this text', {
catalogId: 'prompt-001',
language: 'en-US'
});
// Controls
ttsService.pause();
ttsService.resume();
ttsService.stop();
// Settings
await ttsService.updateSettings({
rate: 1.5,
voice: 'Matthew'
});
const toolCoordinator = new ToolCoordinator();
// Register tools
toolCoordinator.registerTool('calculator', 'Calculator', element);
// Manage visibility
toolCoordinator.showTool('calculator');
toolCoordinator.hideTool('calculator');
toolCoordinator.toggleTool('calculator');
// Z-index management
toolCoordinator.bringToFront(element);
// Check state
const isVisible = toolCoordinator.isToolVisible('calculator');
const highlightCoordinator = new HighlightCoordinator();
// TTS highlights (temporary)
highlightCoordinator.highlightTTSWord(textNode, start, end);
highlightCoordinator.highlightTTSSentence([range1, range2]);
highlightCoordinator.clearTTS();
// Annotation highlights (persistent)
const id = highlightCoordinator.addAnnotation(range, 'yellow');
highlightCoordinator.removeAnnotation(id);
const resolver = new AccessibilityCatalogResolver(
assessment.accessibilityCatalogs,
'en-US'
);
// Add item-level catalogs
resolver.addItemCatalogs(item.accessibilityCatalogs);
// Get alternative representation
const alternative = resolver.getAlternative('prompt-001', {
type: 'spoken',
language: 'en-US'
});
// Clear item catalogs when navigating away
resolver.clearItemCatalogs();
const extractor = new SSMLExtractor();
// Extract from item config
const result = extractor.extractFromItemConfig(item.config);
// Update item with cleaned config
item.config = result.cleanedConfig;
item.config.extractedCatalogs = result.catalogs;
// Register with catalog resolver
catalogResolver.addItemCatalogs(result.catalogs);
The section player provides automatic ToolkitCoordinator integration:
<pie-section-player id="player"></pie-section-player>
<script type="module">
import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
// Create coordinator
const coordinator = new ToolkitCoordinator({
assessmentId: 'my-assessment',
tools: {
providers: { textToSpeech: { enabled: true, backend: 'browser' } },
placement: {
section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
item: ['calculator', 'textToSpeech', 'answerEliminator'],
passage: ['textToSpeech']
}
}
});
// Pass to player
const player = document.getElementById('player');
player.runtime = { ...(player.runtime ?? {}), coordinator };
player.section = mySection;
// Player automatically:
// - Extracts services from coordinator
// - Generates section ID
// - Provides runtime context to child components
// - Manages SSML extraction
// - Handles catalog lifecycle
</script>
The toolkit now exports a shared context key used by section-player and toolkit components:
import {
assessmentToolkitRuntimeContext,
type AssessmentToolkitRuntimeContext
} from "@pie-players/pie-assessment-toolkit";
AssessmentToolkitRuntimeContext carries ambient orchestration dependencies
that should not be prop-drilled through intermediate components:
toolkitCoordinatortoolCoordinatorttsServicehighlightCoordinatorcatalogResolverelementToolStateStoreassessmentIdsectionIdThese runtime fields are expected to be present once the section-player provider is established (host-supplied coordinator or lazily created by section-player). Use explicit props/events for direct content contracts, and use runtime context for cross-cutting orchestration scope.
If no coordinator is provided, the section player creates a default one:
// No coordinator provided - section player creates default
player.section = mySection;
// Internally creates:
// new ToolkitCoordinator({
// assessmentId: 'anon_...', // auto-generated
// tools: {
// providers: { textToSpeech: { enabled: true, backend: 'browser' }, calculator: { enabled: true } },
// placement: {
// section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
// item: ['calculator', 'textToSpeech', 'answerEliminator'],
// passage: ['textToSpeech']
// }
// }
// })
Default behavior is now framework-owned: invalid tools/runtime initialization is handled in pie-assessment-toolkit without host try/catch.
[pie-framework:<kind>:<source>]framework-error eventkind: "coordinator-init" when the owned coordinator construction path throws.Use createToolsConfig() when you want to pre-validate and inspect diagnostics before mounting:
import {
createPackagedToolRegistry,
createToolsConfig,
ToolkitCoordinator
} from "@pie-players/pie-assessment-toolkit";
const toolRegistry = createPackagedToolRegistry();
const { config, diagnostics } = createToolsConfig({
source: "host.bootstrap",
strictness: "error",
toolRegistry,
tools: {
providers: {
textToSpeech: { enabled: true, backend: "browser" },
calculator: { enabled: true }
},
placement: {
item: ["calculator", "textToSpeech"]
}
}
});
// Fail-fast default: invalid config throws at the boundary.
const coordinator = new ToolkitCoordinator({
assessmentId: "demo-assessment",
toolRegistry,
tools: config,
toolConfigStrictness: "error"
});
Notes:
providers.textToSpeech is the canonical TTS provider key.providers.tts is rejected by the validation contract.sanitizeConfig and validateConfig hooks.framework-error DOM event,
the onFrameworkError(model) callback prop, or by subscribing directly
to the package-internal bus via
ToolkitCoordinator.subscribeFrameworkErrors(listener). The callback
prop fires exactly once per error, regardless of wrapper depth. Filter
by model.kind (e.g. "tts-init", "provider-init",
"provider-register") for tool- or provider-specific handling.docs/tools-and-accomodations/framework-owned-error-handling.md for event payload and error-kind mapping details.The toolkit exposes a layered section runtime engine that consolidates
runtime resolution, FSM-driven stage progression, framework-error reporting,
DOM-event fan-out, and instrumentation into a single object hosts can mount
and dispose. The engine is what <pie-section-player-…> and
<pie-assessment-toolkit> use internally, and it is also the surface
custom hosts (or alternate layout shells) consume directly.
The engine ships with two deliberately separate entry points so consumers pick the stability surface that matches their use case:
@pie-players/pie-assessment-toolkit/runtime/engine.
Narrow, semver-stable surface for hosts that want to mount, drive, and
dispose a section runtime. Re-exports SectionRuntimeEngine,
SECTION_RUNTIME_ENGINE_KEY (Svelte context), the cross-CE host
context (sectionRuntimeEngineHostContext), and the consumer-side
helper for that bridge (connectSectionRuntimeEngineHostContext).
The cross-CE host context exposes only a lifecycle handle; controller
methods stay on SectionRuntimeEngine.@pie-players/pie-assessment-toolkit/runtime/internal.
Wider, evolving surface for advanced hosts that need to construct an
engine manually, inspect FSM state, or build alternate fan-out paths.
Exposes SectionEngineCore, the four adapter bridges
(createDomEventBridge, createFrameworkErrorBridge,
createCoordinatorBridge, createInstrumentationBridge),
FrameworkErrorBus, cohort helpers,
and the resolveRuntime / resolveToolsConfig /
resolveSectionEngineRuntimeState helpers. Symbols here may change
between minor versions with a changeset note.When <pie-assessment-toolkit> is nested inside a section-player layout,
the layout kernel publishes a lifecycle handle via
sectionRuntimeEngineHostContext. The toolkit detects that host
lifecycle owner and suppresses its own external lifecycle DOM emits
and onStageChange callback in favor of the layout CE host. From the
outside, one cohort yields one pie-stage-change /
pie-loading-complete chain on the layout CE host regardless of wrapper
depth. Controller-side
registration, content loading, session propagation, and persistence
remain toolkit-local through its own SectionRuntimeEngine instance.
A standalone <pie-assessment-toolkit> (no host context) emits from
its own engine.
Detection. If a custom layout shell emits two pie-stage-change
events per stage transition (or two pie-loading-complete per cohort)
on the same layout CE — typically with two distinct detail.runtimeId
values — the shell has not published its engine via
sectionRuntimeEngineHostContext, so the wrapped
<pie-assessment-toolkit> falls back to its standalone lifecycle emit
path. Wire the bridge as shown below.
Most hosts never construct the engine directly — the section-player layout CE and the toolkit CE handle it. Use the facade only when building an alternate layout shell (e.g. a custom kernel host). The shape mirrors what the section-player kernel does internally:
import { ContextProvider } from "@pie-players/pie-context";
import {
SectionRuntimeEngine,
sectionRuntimeEngineHostContext,
} from "@pie-players/pie-assessment-toolkit/runtime/engine";
import {
FrameworkErrorBus,
makeCohort,
} from "@pie-players/pie-assessment-toolkit/runtime/internal";
const bus = new FrameworkErrorBus();
const engine = new SectionRuntimeEngine();
// 1. Attach to the layout CE host. `sourceCe` is stamped onto every
// DOM event the engine dispatches and is required.
engine.attachHost({
host: layoutHostElement,
sourceCe: "my-custom-layout",
frameworkErrorBus: bus,
coordinator: toolkitCoordinator,
});
// 2. Publish a lifecycle handle on the layout CE host so any wrapped
// <pie-assessment-toolkit> suppresses duplicate external lifecycle
// emits. The toolkit still owns its controller registration/session
// plumbing locally.
const engineProvider = new ContextProvider(layoutHostElement, {
context: sectionRuntimeEngineHostContext,
initialValue: {
engine: {
getRuntimeId: () => engine.getRuntimeId(),
},
},
});
engineProvider.connect();
// 3. (Optional) Subscribe to the structured output stream — same set
// of outputs the DOM-event bridge fans out to the host element.
engine.subscribe((output) => {
// tap stage transitions, readiness updates, framework errors,
// instrumentation events
});
// 4. Drive the engine. Use real `SectionEngineInput` shapes:
const cohort = makeCohort({ sectionId, attemptId });
engine.dispatchInput({
kind: "initialize",
cohort,
effectiveRuntime,
effectiveToolsConfig,
itemCount,
});
// On loading-progress / readiness signal updates:
engine.dispatchInput({
kind: "update-readiness-signals",
signals: {
sectionReady,
interactionReady,
allLoadingComplete,
runtimeError,
},
loadedCount,
itemCount,
mode: "progressive",
});
// On unmount:
engineProvider.disconnect();
engine.dispose();
The DOM events pie-stage-change, pie-loading-complete, and
framework-error are dispatched on host automatically by the
adapter's dom-event-bridge. The canonical onFrameworkError callback
prop and the package-internal FrameworkErrorBus deliver each error
exactly once regardless of wrapper depth. The framework-error DOM
event on the layout CE host also delivers each error exactly once: the
section-player kernel intercepts the toolkit's bubbled emit at
<pie-section-player-base> and calls event.stopPropagation(), so the
layout host sees only the canonical engine-bridge emit. Direct
listeners on <pie-assessment-toolkit> itself still see the toolkit's
own emit (the toolkit dispatch reaches them before the kernel listener
runs). The single-emit contract is pinned by
packages/section-player/tests/section-player-framework-error-dual-emit.test.ts.
The layout host emits one framework-error DOM event per framework error.
Hosts should listen to pie-stage-change (with the readiness detail also
available via the kernel's selectReadiness()) and pie-loading-complete.
The toolkit enforces a clear separation between ephemeral tool state and persistent session data:
Client-only, never sent to server for scoring:
{
"demo-assessment:section-1:question-1:mc1": {
"answerEliminator": {
"eliminatedChoices": ["choice-b", "choice-d"]
},
"highlighter": {
"annotations": [...]
}
}
}
Use for:
Sent to server for scoring:
{
"question-1": {
"id": "session-123",
"data": [
{ "id": "mc1", "element": "multiple-choice", "value": ["choice-a"] }
]
}
}
Use for:
See the section-demos for complete examples:
Full TypeScript definitions included:
import type {
IToolkitCoordinator,
IElementToolStateStore,
ToolkitCoordinatorConfig,
ToolkitServiceBundle
} from '@pie-players/pie-assessment-toolkit';
The toolkit embeds item content via the underlying pie-item-player
custom element and renders tool icons / SSML fragments that originate
from tool configuration. Two sanitization layers apply:
pie-item-player. See
pie-item-player README
for the trust-markup opt-out and the sanitizeMarkup override.
As a post-sanitization step, every authored <img> outside a pie-*
custom element is wrapped in <span class="pie-image-scroll"> so
overwide images surface a horizontal scrollbar instead of being
clipped by the section layout's overflow-x: hidden ancestors
(PIE-94 / WCAG 1.4.10 Reflow at 400% zoom). The wrapper is
keyboard-scrollable (tabindex="0", role="region") and carries
the image's alt text in its aria-label; matching CSS lives in
@pie-players/pie-theme (components.css). Authored <table>
elements outside a pie-* custom element get the same treatment via
<div class="pie-table-scroll"> so wide data grids reflow into a
horizontally scrollable region; the wrapper's aria-label is derived
from the table's aria-label / aria-labelledby / <caption>.<script> or event-handler attributes in their icon strings.MIT
FAQs
PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination
We found that @pie-players/pie-assessment-toolkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Anthropic says the directive cited national security concerns over a narrow jailbreak, but offered no specific technical details.

Security News
A network of 152 Chrome live wallpaper extensions hid ad tracking and made extension-driven traffic look like Google search clicks.

Company News
Socket’s first CISO brings deep experience securing high-growth SaaS companies as open source supply chain threats accelerate.