@aparte/provider-scenario
A scripted model for aparté: it replays turns you wrote — text at a typing pace, thinking, tool calls, errors, pauses — instead of calling a model. No key, no network, the same answer every time. For demos and docs that should stream something real, and for your own tests, which get a deterministic model.
npm install @aparte/provider-scenario @aparte/core
@aparte/core is the only peer dependency; this package has none of its own.
It stands in for the whole provider: everything above it — the loop, tool execution and approval, the transcript, your renderers — runs for real, and nothing below it runs at all. So it tests an app that talks to a model over HTTP (BYOK, a backend, a local server). It does not test a provider you wrote yourself — a worker, a wire format, a tool-call parser — since that is exactly the part it replaces; keep a fake of your own for that layer.
import { aparteGlobalConfig, AparteClient, AparteDirectTransport } from '@aparte/core';
import { createScenarioProvider } from '@aparte/provider-scenario';
aparteGlobalConfig.registerAIProvider(createScenarioProvider({
turns: [
'Hello! Ask me anything.',
[{ thinking: 'Let me see…' }, { text: 'Here is my **answer**.' }],
],
}));
aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
new AparteClient().start();
turns answers calls in order and repeats the last one. For a model that answers
according to what was asked, name scenarios and let when / after pick:
createScenarioProvider({
scenarios: {
default: 'Ask me for a haiku, or the weather.',
haiku: { when: /haiku/i, turn: 'Web components hum — \nno framework in the wind, \njust the page, alive.' },
weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },
forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },
},
});
A tool step calls the tool you registered on the config; the real loop runs its handler
and calls the provider again with the result, which after answers. pacing: { chunk, delay } sets the typing speed; pacing: 'instant' gives a test the whole
reply at once. showcase is a ready-made set covering markdown, code, reasoning, a
tool round-trip, a typed question (ask_user), an answer with its sources, an artifact (a
card once @aparte/plugin-artifacts is set up, prose otherwise) and an error; it is imported
from @aparte/provider-scenario/showcase, its own entry point, so your own scenarios
do not pay for it.
Sources and citations
A sources step lists the sources a reply draws on, and its citations say where in the text
each one is cited. Put it after the text it cites: a citation's start and end are UTF-16
offsets into the text the turn has streamed so far (its text steps joined, end exclusive),
and a chip goes at the end of the passage. Source ids are unique within the turn, and a citation
names them.
const first = 'The sky is blue because air scatters short wavelengths.';
const second = ' Sunsets are red for the opposite reason.';
createScenarioProvider({
turns: [[
{ text: first + second },
{
sources: [
{ id: 'a', url: 'https://en.wikipedia.org/wiki/Rayleigh_scattering', title: 'Rayleigh scattering', snippet: 'Scattering by particles smaller than the wavelength.' },
{ id: 'b', url: 'https://www.nasa.gov/', title: 'NASA' },
],
// One passage citing two sources shows as "+1". `end` is exclusive.
citations: [{ start: 0, end: first.length, sources: ['a', 'b'] }],
},
]],
});
Compute the offsets from the strings (text.indexOf(passage)) rather than counting by hand, so
an edit to a sentence cannot leave a chip on the wrong words. Ask the showcase for "why is
the sky blue?" or "with sources" to see a three-source answer.
Full guide: apartejs.dev/providers/ai/scenario.