@aparte/provider-scenario 0.14.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aparté
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @aparte/provider-scenario
2
+
3
+ A **scripted model** for aparté: it replays turns you wrote — text at a typing pace,
4
+ thinking, tool calls, errors, pauses — instead of calling a model. No key, no network,
5
+ the same answer every time. For demos and docs that should stream something real, and
6
+ for **your own tests**, which get a deterministic model.
7
+
8
+ ```bash
9
+ npm install @aparte/provider-scenario @aparte/core
10
+ ```
11
+
12
+ `@aparte/core` is the only **peer dependency**; this package has none of its own.
13
+
14
+ ```ts
15
+ import { aparteGlobalConfig, AparteClient, AparteDirectTransport } from '@aparte/core';
16
+ import { createScenarioProvider } from '@aparte/provider-scenario';
17
+
18
+ aparteGlobalConfig.registerAIProvider(createScenarioProvider({
19
+ turns: [
20
+ 'Hello! Ask me anything.',
21
+ [{ thinking: 'Let me see…' }, { text: 'Here is my **answer**.' }],
22
+ ],
23
+ }));
24
+ aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
25
+ new AparteClient().start();
26
+ ```
27
+
28
+ `turns` answers calls in order and repeats the last one. For a model that answers
29
+ *according to what was asked*, name scenarios and let `when` / `after` pick:
30
+
31
+ ```ts
32
+ createScenarioProvider({
33
+ scenarios: {
34
+ default: 'Ask me for a haiku, or the weather.',
35
+ haiku: { when: /haiku/i, turn: 'Web components hum — \nno framework in the wind, \njust the page, alive.' },
36
+ weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },
37
+ forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },
38
+ },
39
+ });
40
+ ```
41
+
42
+ A tool step calls the tool you registered on the config; the real loop runs its handler
43
+ and calls the provider again with the result, which `after` answers. `pacing:
44
+ { chunk, delay }` sets the typing speed; `pacing: 'instant'` gives a test the whole
45
+ reply at once. `showcase` is a ready-made set covering markdown, code, reasoning, a tool
46
+ round-trip, a typed question (`ask_user`), an artifact and an error.
47
+
48
+ Full guide: [apartejs.dev/providers/ai/scenario](https://apartejs.dev/providers/ai/scenario/).
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @aparte/provider-scenario — a scripted model.
3
+ *
4
+ * An `AparteAIProvider` that REPLAYS turns you wrote instead of calling a model: text at
5
+ * a typing pace, thinking, tool calls (the real loop runs your handler and calls back
6
+ * with the result), errors, pauses. No key, no network, no randomness.
7
+ *
8
+ * Three things in this repository had written it by hand before this package existed:
9
+ * the e2e mock of the wire, the screenshot harness of the UI audit, and the docs frames
10
+ * that seed content. It is also the piece nobody ships for CONSUMERS: a deterministic
11
+ * model for their own tests, and a demo that streams without a backend.
12
+ *
13
+ * It owns its I/O — `chat()` — so `AparteDirectTransport` hands the request straight to
14
+ * it; there is no wire format here to adapt. (This repository's own e2e keeps its
15
+ * network mock on purpose: that suite tests the real wire path, which this provider
16
+ * bypasses by construction.)
17
+ *
18
+ * ```ts
19
+ * import { createScenarioProvider } from '@aparte/provider-scenario';
20
+ * aparteGlobalConfig.registerAIProvider(createScenarioProvider({
21
+ * turns: ['Hello! Ask me anything.', [{ thinking: 'Let me see…' }, { text: 'Here is my answer.' }]],
22
+ * }));
23
+ * ```
24
+ */
25
+ import type { AparteAIModel, AparteAIProvider, AparteChatRequest, AparteStreamEvent, AparteUsage } from '@aparte/core';
26
+ /** One thing the scripted model does, in order. */
27
+ export type ScenarioStep =
28
+ /** Stream this text, chunk by chunk at the configured pace. Markdown and `<artifact>` tags are fine: core parses them. */
29
+ {
30
+ text: string;
31
+ }
32
+ /** Stream this as reasoning (the thinking block). */
33
+ | {
34
+ thinking: string;
35
+ }
36
+ /** Call a tool. The loop runs the registered handler, then calls the provider again with the result. */
37
+ | {
38
+ tool: string;
39
+ input?: Record<string, unknown>;
40
+ id?: string;
41
+ }
42
+ /** Fail the turn with this message — what a provider error looks like to the UI. */
43
+ | {
44
+ error: string;
45
+ }
46
+ /** Pause, in milliseconds — a slow model, a long tool. */
47
+ | {
48
+ wait: number;
49
+ }
50
+ /** Override the usage reported at the end (merged over the estimate). */
51
+ | {
52
+ usage: Partial<AparteUsage>;
53
+ };
54
+ /** What the model does for ONE call. A bare string is one text step. */
55
+ export type ScenarioTurn = string | ScenarioStep[];
56
+ /** A turn with the conditions under which it answers. */
57
+ export interface Scenario {
58
+ /**
59
+ * Answers when the last user message matches: a string is a case-insensitive
60
+ * substring, a RegExp is tested as is.
61
+ */
62
+ when?: string | RegExp;
63
+ /** Answers the call that follows this tool's result — the second half of a tool round-trip. */
64
+ after?: string;
65
+ turn: ScenarioTurn;
66
+ }
67
+ /** How fast text streams. */
68
+ export interface ScenarioPacing {
69
+ /** Characters per chunk. Default 12. */
70
+ chunk?: number;
71
+ /** Milliseconds between chunks. Default 24. */
72
+ delay?: number;
73
+ }
74
+ export interface ScenarioProviderOptions {
75
+ /** Provider id, as seen by core (key resolution, model picker, events). Default `scenario`. */
76
+ id?: string;
77
+ /** Display name. Default `Scripted model`. */
78
+ name?: string;
79
+ /**
80
+ * Calls answered in order; the last one repeats. The simplest form — a demo that
81
+ * always goes the same way, a test that needs three replies. Every call to the
82
+ * model advances, a tool round-trip included.
83
+ */
84
+ turns?: ScenarioTurn[];
85
+ /**
86
+ * Named scenarios, one of which answers each call — picked by `match`, or by the
87
+ * default: the `after` of a tool result, else the first `when` that matches the
88
+ * last user message, else `default`, else the first one. A bare turn value is
89
+ * `{ turn }`.
90
+ */
91
+ scenarios?: Record<string, Scenario | ScenarioTurn>;
92
+ /** Picks the scenario for a call, by name. Return `undefined` to fall back to the default rule. */
93
+ match?: (request: AparteChatRequest, scenarios: Record<string, Scenario>) => string | undefined;
94
+ /** Typing pace, or `'instant'` for a test that wants the whole reply at once. */
95
+ pacing?: ScenarioPacing | 'instant';
96
+ /** The models the picker offers. Default: one, `scripted`, declaring streaming and tools. */
97
+ models?: AparteAIModel[];
98
+ }
99
+ /**
100
+ * The default rule: a tool result goes to the scenario that declared `after` for that
101
+ * tool; otherwise the first scenario whose `when` matches the last user message;
102
+ * otherwise `default`; otherwise the first scenario declared.
103
+ */
104
+ export declare function defaultMatch(request: AparteChatRequest, scenarios: Record<string, Scenario>): string | undefined;
105
+ /**
106
+ * Play one turn as the event stream the loop reads.
107
+ *
108
+ * Exported for a test or a tool that wants the events without a provider around them.
109
+ */
110
+ export declare function playTurn(turn: ScenarioTurn, request: AparteChatRequest, pacing?: ScenarioPacing | 'instant', signal?: AbortSignal): ReadableStream<AparteStreamEvent>;
111
+ /**
112
+ * Build the scripted provider. Register it like any other:
113
+ *
114
+ * ```ts
115
+ * aparteGlobalConfig.registerAIProvider(createScenarioProvider({
116
+ * scenarios: {
117
+ * default: 'Ask me for a haiku, a table, or the weather.',
118
+ * haiku: { when: /haiku/i, turn: 'Web components hum — \nno framework in the wind, \njust the page, alive.' },
119
+ * weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },
120
+ * forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },
121
+ * },
122
+ * }));
123
+ * ```
124
+ */
125
+ export declare function createScenarioProvider(options?: ScenarioProviderOptions): AparteAIProvider;
126
+ export { showcase } from './presets.js';
127
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAEhB,iBAAiB,EAEjB,iBAAiB,EACjB,WAAW,EACd,MAAM,cAAc,CAAC;AAItB,mDAAmD;AACnD,MAAM,MAAM,YAAY;AACpB,0HAA0H;AACxH;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,qDAAqD;GACnD;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE;AACtB,wGAAwG;GACtG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE;AAChE,oFAAoF;GAClF;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE;AACnB,0DAA0D;GACxD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,yEAAyE;GACvE;IAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;CAAE,CAAC;AAEtC,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,EAAE,CAAC;AAEnD,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,YAAY,CAAC;CACtB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC3B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACpC,+FAA+F;IAC/F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAC;IACpD,mGAAmG;IACnG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IAChG,iFAAiF;IACjF,MAAM,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACpC,6FAA6F;IAC7F,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AA8BD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,GAAG,SAAS,CAehH;AAkBD;;;;GAIG;AACH,wBAAgB,QAAQ,CACpB,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,iBAAiB,EAC1B,MAAM,GAAE,cAAc,GAAG,SAAc,EACvC,MAAM,CAAC,EAAE,WAAW,GACrB,cAAc,CAAC,iBAAiB,CAAC,CAoDnC;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAuC9F;AAED,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,214 @@
1
+ const showcase = {
2
+ default: {
3
+ turn: "Hi — I am a **scripted** model: no key, no network, the same answer every time.\n\nAsk me for a *haiku*, a *table*, some *code*, the *weather*, or to *ask you a question*."
4
+ },
5
+ haiku: {
6
+ when: /haiku/i,
7
+ turn: "Web components hum — \nno framework in the wind, \njust the page, alive."
8
+ },
9
+ table: {
10
+ when: /table/i,
11
+ turn: "Two ways to reach a model:\n\n| Transport | Where the key lives | Use it for |\n|---|---|---|\n| `AparteDirectTransport` | in the browser | prototypes, local models |\n| `AparteBackendTransport` | on your server | production |\n\nThe component does not change between the two."
12
+ },
13
+ code: {
14
+ when: /code|example|snippet/i,
15
+ turn: [
16
+ { thinking: "The shortest useful wiring is three lines: register a provider, set a transport, start the client." },
17
+ { text: "The minimal wiring:\n\n```ts\nimport { AparteClient, aparteGlobalConfig, AparteDirectTransport } from '@aparte/core';\nimport { createScenarioProvider } from '@aparte/provider-scenario';\n\naparteGlobalConfig.registerAIProvider(createScenarioProvider({ turns: ['Hello!'] }));\naparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));\nnew AparteClient().start();\n```\n\nThree lines that matter." }
18
+ ]
19
+ },
20
+ weather: {
21
+ when: /weather|forecast/i,
22
+ turn: [
23
+ { text: "Let me check that for you." },
24
+ { tool: "get_weather", input: { city: "Lille" } }
25
+ ]
26
+ },
27
+ forecast: {
28
+ after: "get_weather",
29
+ turn: "Cloudy, 14 °C, a little wind from the west — bring a jacket."
30
+ },
31
+ question: {
32
+ when: /question|ask me|colou?r scheme/i,
33
+ turn: [
34
+ { text: "One thing first." },
35
+ {
36
+ tool: "ask_user",
37
+ input: {
38
+ questions: [{
39
+ question: "Which mood for your landing page?",
40
+ header: "Mood",
41
+ options: [
42
+ { title: "Dark and quiet", description: "ink, brass accents", recommended: true },
43
+ { title: "Light and warm", description: "cream, soft contrast" },
44
+ { title: "Maximum contrast", description: "strict black and white" }
45
+ ]
46
+ }]
47
+ }
48
+ }
49
+ ]
50
+ },
51
+ answered: {
52
+ after: "ask_user",
53
+ turn: "Noted — I will go with that."
54
+ },
55
+ artifact: {
56
+ when: /artifact|document|write me/i,
57
+ turn: 'Here is a first draft:\n\n<artifact type="text/markdown" title="Launch note">## aparté\n\nA chat in Web Components, with the agent loop inside.\n\n- any framework\n- zero third-party dependencies\n</artifact>\n\nTell me what to change.'
58
+ },
59
+ slow: {
60
+ when: /slow|think hard|take your time/i,
61
+ turn: [
62
+ { thinking: "This deserves a moment." },
63
+ { wait: 2500 },
64
+ { text: "Done thinking. The answer is: it depends on the transport." }
65
+ ]
66
+ },
67
+ failure: {
68
+ when: /fail|error|crash/i,
69
+ turn: [{ error: "The scripted model exploded, as requested." }]
70
+ }
71
+ };
72
+ const textOf = (message) => {
73
+ if (!message) return "";
74
+ const { content } = message;
75
+ if (typeof content === "string") return content;
76
+ return content.map((part) => part.type === "text" ? part.text : "").join(" ");
77
+ };
78
+ const stepsOf = (turn) => typeof turn === "string" ? [{ text: turn }] : turn;
79
+ const isTurn = (value) => typeof value === "string" || Array.isArray(value);
80
+ const normalize = (scenarios) => {
81
+ const out = {};
82
+ for (const [key, value] of Object.entries(scenarios)) out[key] = isTurn(value) ? { turn: value } : value;
83
+ return out;
84
+ };
85
+ const toolNameOf = (messages, toolCallId) => {
86
+ for (let i = messages.length - 1; i >= 0; i--) {
87
+ const call = messages[i].toolCalls?.find((tc) => tc.id === toolCallId);
88
+ if (call) return call.name;
89
+ }
90
+ return void 0;
91
+ };
92
+ function defaultMatch(request, scenarios) {
93
+ const entries = Object.entries(scenarios);
94
+ const last = request.messages[request.messages.length - 1];
95
+ if (last?.role === "tool_result") {
96
+ const tool = toolNameOf(request.messages, last.toolCallId);
97
+ const hit = entries.find(([, s]) => s.after !== void 0 && s.after === tool);
98
+ if (hit) return hit[0];
99
+ }
100
+ const text = textOf(last);
101
+ for (const [key, s] of entries) {
102
+ if (s.when === void 0) continue;
103
+ if (typeof s.when === "string" ? text.toLowerCase().includes(s.when.toLowerCase()) : s.when.test(text)) return key;
104
+ }
105
+ if ("default" in scenarios) return "default";
106
+ return entries[0]?.[0];
107
+ }
108
+ const sleep = (ms, signal) => new Promise((resolve) => {
109
+ if (ms <= 0 || signal?.aborted) return resolve();
110
+ const timer = setTimeout(done, ms);
111
+ function done() {
112
+ clearTimeout(timer);
113
+ signal?.removeEventListener("abort", done);
114
+ resolve();
115
+ }
116
+ signal?.addEventListener("abort", done, { once: true });
117
+ });
118
+ const estimateTokens = (chars) => Math.ceil(chars / 4);
119
+ let sequence = 0;
120
+ function playTurn(turn, request, pacing = {}, signal) {
121
+ const chunk = pacing === "instant" ? Number.POSITIVE_INFINITY : Math.max(1, pacing.chunk ?? 12);
122
+ const delay = pacing === "instant" ? 0 : Math.max(0, pacing.delay ?? 24);
123
+ const steps = stepsOf(turn);
124
+ const inputChars = request.messages.reduce((n, m) => n + textOf(m).length, 0);
125
+ return new ReadableStream({
126
+ async start(controller) {
127
+ let outputChars = 0;
128
+ let usage = {};
129
+ const turnBegan = Date.now();
130
+ try {
131
+ for (const step of steps) {
132
+ if (signal?.aborted) break;
133
+ if ("wait" in step) {
134
+ await sleep(step.wait, signal);
135
+ } else if ("text" in step || "thinking" in step) {
136
+ const type = "text" in step ? "text" : "thinking";
137
+ const body = "text" in step ? step.text : step.thinking;
138
+ for (let i = 0; i < body.length; i += chunk) {
139
+ if (signal?.aborted) break;
140
+ const delta = body.slice(i, i + chunk);
141
+ controller.enqueue({ type, delta });
142
+ outputChars += delta.length;
143
+ if (delay > 0 && i + chunk < body.length) await sleep(delay, signal);
144
+ }
145
+ } else if ("tool" in step) {
146
+ controller.enqueue({ type: "tool_use", id: step.id ?? `scenario_${++sequence}`, name: step.tool, input: step.input ?? {} });
147
+ } else if ("error" in step) {
148
+ controller.enqueue({ type: "error", message: step.error });
149
+ controller.close();
150
+ return;
151
+ } else if ("usage" in step) {
152
+ usage = { ...usage, ...step.usage };
153
+ }
154
+ }
155
+ if (!signal?.aborted) {
156
+ const outputTokens = estimateTokens(outputChars);
157
+ const inputTokens = estimateTokens(inputChars);
158
+ controller.enqueue({
159
+ type: "done",
160
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, durationMs: Date.now() - turnBegan, ...usage }
161
+ });
162
+ }
163
+ controller.close();
164
+ } catch (err) {
165
+ controller.error(err);
166
+ }
167
+ }
168
+ });
169
+ }
170
+ function createScenarioProvider(options = {}) {
171
+ const id = options.id ?? "scenario";
172
+ const name = options.name ?? "Scripted model";
173
+ const models = options.models ?? [{
174
+ id: "scripted",
175
+ name,
176
+ description: "Replays scripted turns — no key, no network.",
177
+ capabilities: ["streaming", "function_calling"]
178
+ }];
179
+ const scenarios = normalize(options.scenarios ?? {});
180
+ const match = options.match ?? defaultMatch;
181
+ let calls = 0;
182
+ const pick = (request) => {
183
+ if (options.turns?.length) {
184
+ const turn = options.turns[Math.min(calls, options.turns.length - 1)];
185
+ calls++;
186
+ return turn;
187
+ }
188
+ const key = match(request, scenarios) ?? defaultMatch(request, scenarios);
189
+ const scenario = key !== void 0 ? scenarios[key] : void 0;
190
+ return scenario?.turn ?? "";
191
+ };
192
+ return {
193
+ id,
194
+ getMetadata: () => ({
195
+ id,
196
+ name,
197
+ description: "Replays scripted turns — no key, no network.",
198
+ isLocal: true,
199
+ configSchema: { fields: [] }
200
+ }),
201
+ getModels: () => models,
202
+ fetchModels: async () => models,
203
+ async chat(request, _config, ctx) {
204
+ return playTurn(pick(request), request, options.pacing, ctx?.signal);
205
+ }
206
+ };
207
+ }
208
+ export {
209
+ createScenarioProvider,
210
+ defaultMatch,
211
+ playTurn,
212
+ showcase
213
+ };
214
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/presets.ts","../src/index.ts"],"sourcesContent":["import type { Scenario } from './index.js';\n\n/**\n * A ready-made set of scenarios that shows the whole surface of a chat: markdown, a\n * code block, reasoning, a tool round-trip, a typed question, an artifact, an error.\n * The docs' live frames and a demo page can register it as is:\n *\n * ```ts\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));\n * ```\n *\n * The tool names it calls — `get_weather`, `ask_user` — only do something if the app\n * registered a tool of that name; `@aparte/plugin-ask-user` provides the second.\n */\nexport const showcase: Record<string, Scenario> = {\n default: {\n turn: 'Hi — I am a **scripted** model: no key, no network, the same answer every time.\\n\\nAsk me for a *haiku*, a *table*, some *code*, the *weather*, or to *ask you a question*.',\n },\n haiku: {\n when: /haiku/i,\n turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.',\n },\n table: {\n when: /table/i,\n turn: 'Two ways to reach a model:\\n\\n| Transport | Where the key lives | Use it for |\\n|---|---|---|\\n| `AparteDirectTransport` | in the browser | prototypes, local models |\\n| `AparteBackendTransport` | on your server | production |\\n\\nThe component does not change between the two.',\n },\n code: {\n when: /code|example|snippet/i,\n turn: [\n { thinking: 'The shortest useful wiring is three lines: register a provider, set a transport, start the client.' },\n { text: 'The minimal wiring:\\n\\n```ts\\nimport { AparteClient, aparteGlobalConfig, AparteDirectTransport } from \\'@aparte/core\\';\\nimport { createScenarioProvider } from \\'@aparte/provider-scenario\\';\\n\\naparteGlobalConfig.registerAIProvider(createScenarioProvider({ turns: [\\'Hello!\\'] }));\\naparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));\\nnew AparteClient().start();\\n```\\n\\nThree lines that matter.' },\n ],\n },\n weather: {\n when: /weather|forecast/i,\n turn: [\n { text: 'Let me check that for you.' },\n { tool: 'get_weather', input: { city: 'Lille' } },\n ],\n },\n forecast: {\n after: 'get_weather',\n turn: 'Cloudy, 14 °C, a little wind from the west — bring a jacket.',\n },\n question: {\n when: /question|ask me|colou?r scheme/i,\n turn: [\n { text: 'One thing first.' },\n {\n tool: 'ask_user',\n input: {\n questions: [{\n question: 'Which mood for your landing page?',\n header: 'Mood',\n options: [\n { title: 'Dark and quiet', description: 'ink, brass accents', recommended: true },\n { title: 'Light and warm', description: 'cream, soft contrast' },\n { title: 'Maximum contrast', description: 'strict black and white' },\n ],\n }],\n },\n },\n ],\n },\n answered: {\n after: 'ask_user',\n turn: 'Noted — I will go with that.',\n },\n artifact: {\n when: /artifact|document|write me/i,\n turn: 'Here is a first draft:\\n\\n<artifact type=\"text/markdown\" title=\"Launch note\">## aparté\\n\\nA chat in Web Components, with the agent loop inside.\\n\\n- any framework\\n- zero third-party dependencies\\n</artifact>\\n\\nTell me what to change.',\n },\n slow: {\n when: /slow|think hard|take your time/i,\n turn: [\n { thinking: 'This deserves a moment.' },\n { wait: 2500 },\n { text: 'Done thinking. The answer is: it depends on the transport.' },\n ],\n },\n failure: {\n when: /fail|error|crash/i,\n turn: [{ error: 'The scripted model exploded, as requested.' }],\n },\n};\n","/**\n * @aparte/provider-scenario — a scripted model.\n *\n * An `AparteAIProvider` that REPLAYS turns you wrote instead of calling a model: text at\n * a typing pace, thinking, tool calls (the real loop runs your handler and calls back\n * with the result), errors, pauses. No key, no network, no randomness.\n *\n * Three things in this repository had written it by hand before this package existed:\n * the e2e mock of the wire, the screenshot harness of the UI audit, and the docs frames\n * that seed content. It is also the piece nobody ships for CONSUMERS: a deterministic\n * model for their own tests, and a demo that streams without a backend.\n *\n * It owns its I/O — `chat()` — so `AparteDirectTransport` hands the request straight to\n * it; there is no wire format here to adapt. (This repository's own e2e keeps its\n * network mock on purpose: that suite tests the real wire path, which this provider\n * bypasses by construction.)\n *\n * ```ts\n * import { createScenarioProvider } from '@aparte/provider-scenario';\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * turns: ['Hello! Ask me anything.', [{ thinking: 'Let me see…' }, { text: 'Here is my answer.' }]],\n * }));\n * ```\n */\n\nimport type {\n AparteAIModel,\n AparteAIProvider,\n AparteChatMessage,\n AparteChatRequest,\n AparteChatResponse,\n AparteStreamEvent,\n AparteUsage,\n} from '@aparte/core';\n\n// ─── What a turn is made of ──────────────────────────────────────────────────\n\n/** One thing the scripted model does, in order. */\nexport type ScenarioStep =\n /** Stream this text, chunk by chunk at the configured pace. Markdown and `<artifact>` tags are fine: core parses them. */\n | { text: string }\n /** Stream this as reasoning (the thinking block). */\n | { thinking: string }\n /** Call a tool. The loop runs the registered handler, then calls the provider again with the result. */\n | { tool: string; input?: Record<string, unknown>; id?: string }\n /** Fail the turn with this message — what a provider error looks like to the UI. */\n | { error: string }\n /** Pause, in milliseconds — a slow model, a long tool. */\n | { wait: number }\n /** Override the usage reported at the end (merged over the estimate). */\n | { usage: Partial<AparteUsage> };\n\n/** What the model does for ONE call. A bare string is one text step. */\nexport type ScenarioTurn = string | ScenarioStep[];\n\n/** A turn with the conditions under which it answers. */\nexport interface Scenario {\n /**\n * Answers when the last user message matches: a string is a case-insensitive\n * substring, a RegExp is tested as is.\n */\n when?: string | RegExp;\n /** Answers the call that follows this tool's result — the second half of a tool round-trip. */\n after?: string;\n turn: ScenarioTurn;\n}\n\n/** How fast text streams. */\nexport interface ScenarioPacing {\n /** Characters per chunk. Default 12. */\n chunk?: number;\n /** Milliseconds between chunks. Default 24. */\n delay?: number;\n}\n\nexport interface ScenarioProviderOptions {\n /** Provider id, as seen by core (key resolution, model picker, events). Default `scenario`. */\n id?: string;\n /** Display name. Default `Scripted model`. */\n name?: string;\n /**\n * Calls answered in order; the last one repeats. The simplest form — a demo that\n * always goes the same way, a test that needs three replies. Every call to the\n * model advances, a tool round-trip included.\n */\n turns?: ScenarioTurn[];\n /**\n * Named scenarios, one of which answers each call — picked by `match`, or by the\n * default: the `after` of a tool result, else the first `when` that matches the\n * last user message, else `default`, else the first one. A bare turn value is\n * `{ turn }`.\n */\n scenarios?: Record<string, Scenario | ScenarioTurn>;\n /** Picks the scenario for a call, by name. Return `undefined` to fall back to the default rule. */\n match?: (request: AparteChatRequest, scenarios: Record<string, Scenario>) => string | undefined;\n /** Typing pace, or `'instant'` for a test that wants the whole reply at once. */\n pacing?: ScenarioPacing | 'instant';\n /** The models the picker offers. Default: one, `scripted`, declaring streaming and tools. */\n models?: AparteAIModel[];\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst textOf = (message: AparteChatMessage | undefined): string => {\n if (!message) return '';\n const { content } = message;\n if (typeof content === 'string') return content;\n return content.map((part) => (part.type === 'text' ? part.text : '')).join(' ');\n};\n\nconst stepsOf = (turn: ScenarioTurn): ScenarioStep[] => (typeof turn === 'string' ? [{ text: turn }] : turn);\n\nconst isTurn = (value: Scenario | ScenarioTurn): value is ScenarioTurn => typeof value === 'string' || Array.isArray(value);\n\nconst normalize = (scenarios: Record<string, Scenario | ScenarioTurn>): Record<string, Scenario> => {\n const out: Record<string, Scenario> = {};\n for (const [key, value] of Object.entries(scenarios)) out[key] = isTurn(value) ? { turn: value } : value;\n return out;\n};\n\n/** The name of the tool a `tool_result` answers: the call with that id, in the turn before. */\nconst toolNameOf = (messages: AparteChatMessage[], toolCallId: string | undefined): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const call = messages[i]!.toolCalls?.find((tc) => tc.id === toolCallId);\n if (call) return call.name;\n }\n return undefined;\n};\n\n/**\n * The default rule: a tool result goes to the scenario that declared `after` for that\n * tool; otherwise the first scenario whose `when` matches the last user message;\n * otherwise `default`; otherwise the first scenario declared.\n */\nexport function defaultMatch(request: AparteChatRequest, scenarios: Record<string, Scenario>): string | undefined {\n const entries = Object.entries(scenarios);\n const last = request.messages[request.messages.length - 1];\n if (last?.role === 'tool_result') {\n const tool = toolNameOf(request.messages, last.toolCallId);\n const hit = entries.find(([, s]) => s.after !== undefined && s.after === tool);\n if (hit) return hit[0];\n }\n const text = textOf(last);\n for (const [key, s] of entries) {\n if (s.when === undefined) continue;\n if (typeof s.when === 'string' ? text.toLowerCase().includes(s.when.toLowerCase()) : s.when.test(text)) return key;\n }\n if ('default' in scenarios) return 'default';\n return entries[0]?.[0];\n}\n\nconst sleep = (ms: number, signal?: AbortSignal): Promise<void> => new Promise((resolve) => {\n if (ms <= 0 || signal?.aborted) return resolve();\n const timer = setTimeout(done, ms);\n function done(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n signal?.addEventListener('abort', done, { once: true });\n});\n\n/** Four characters per token — the usual rough cut, enough for a gauge to move. */\nconst estimateTokens = (chars: number): number => Math.ceil(chars / 4);\n\nlet sequence = 0;\n\n/**\n * Play one turn as the event stream the loop reads.\n *\n * Exported for a test or a tool that wants the events without a provider around them.\n */\nexport function playTurn(\n turn: ScenarioTurn,\n request: AparteChatRequest,\n pacing: ScenarioPacing | 'instant' = {},\n signal?: AbortSignal,\n): ReadableStream<AparteStreamEvent> {\n const chunk = pacing === 'instant' ? Number.POSITIVE_INFINITY : Math.max(1, pacing.chunk ?? 12);\n const delay = pacing === 'instant' ? 0 : Math.max(0, pacing.delay ?? 24);\n const steps = stepsOf(turn);\n const inputChars = request.messages.reduce((n, m) => n + textOf(m).length, 0);\n\n return new ReadableStream<AparteStreamEvent>({\n async start(controller) {\n let outputChars = 0;\n let usage: Partial<AparteUsage> = {};\n // Not `startedAt`: that name is a segment stamp core writes in one place, and\n // `check:segment-stamp` reads any write to it as a third stamping site.\n const turnBegan = Date.now();\n try {\n for (const step of steps) {\n if (signal?.aborted) break;\n if ('wait' in step) {\n await sleep(step.wait, signal);\n } else if ('text' in step || 'thinking' in step) {\n const type = 'text' in step ? 'text' : 'thinking';\n const body = 'text' in step ? step.text : step.thinking;\n for (let i = 0; i < body.length; i += chunk) {\n if (signal?.aborted) break;\n const delta = body.slice(i, i + chunk);\n controller.enqueue({ type, delta });\n outputChars += delta.length;\n if (delay > 0 && i + chunk < body.length) await sleep(delay, signal);\n }\n } else if ('tool' in step) {\n controller.enqueue({ type: 'tool_use', id: step.id ?? `scenario_${++sequence}`, name: step.tool, input: step.input ?? {} });\n } else if ('error' in step) {\n controller.enqueue({ type: 'error', message: step.error });\n controller.close();\n return;\n } else if ('usage' in step) {\n usage = { ...usage, ...step.usage };\n }\n }\n if (!signal?.aborted) {\n const outputTokens = estimateTokens(outputChars);\n const inputTokens = estimateTokens(inputChars);\n controller.enqueue({\n type: 'done',\n usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, durationMs: Date.now() - turnBegan, ...usage },\n });\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n}\n\n// ─── The provider ────────────────────────────────────────────────────────────\n\n/**\n * Build the scripted provider. Register it like any other:\n *\n * ```ts\n * aparteGlobalConfig.registerAIProvider(createScenarioProvider({\n * scenarios: {\n * default: 'Ask me for a haiku, a table, or the weather.',\n * haiku: { when: /haiku/i, turn: 'Web components hum — \\nno framework in the wind, \\njust the page, alive.' },\n * weather: { when: 'weather', turn: [{ text: 'Let me check.' }, { tool: 'get_weather', input: { city: 'Lille' } }] },\n * forecast: { after: 'get_weather', turn: 'Cloudy, 14 °C. Bring a jacket.' },\n * },\n * }));\n * ```\n */\nexport function createScenarioProvider(options: ScenarioProviderOptions = {}): AparteAIProvider {\n const id = options.id ?? 'scenario';\n const name = options.name ?? 'Scripted model';\n const models: AparteAIModel[] = options.models ?? [{\n id: 'scripted',\n name,\n description: 'Replays scripted turns — no key, no network.',\n capabilities: ['streaming', 'function_calling'],\n }];\n const scenarios = normalize(options.scenarios ?? {});\n const match = options.match ?? defaultMatch;\n let calls = 0;\n\n const pick = (request: AparteChatRequest): ScenarioTurn => {\n if (options.turns?.length) {\n const turn = options.turns[Math.min(calls, options.turns.length - 1)]!;\n calls++;\n return turn;\n }\n const key = match(request, scenarios) ?? defaultMatch(request, scenarios);\n const scenario = key !== undefined ? scenarios[key] : undefined;\n return scenario?.turn ?? '';\n };\n\n return {\n id,\n getMetadata: () => ({\n id,\n name,\n description: 'Replays scripted turns — no key, no network.',\n isLocal: true,\n configSchema: { fields: [] },\n }),\n getModels: () => models,\n fetchModels: async () => models,\n async chat(request: AparteChatRequest, _config?: unknown, ctx?: { providerId: string; signal?: AbortSignal }): Promise<AparteChatResponse> {\n return playTurn(pick(request), request, options.pacing, ctx?.signal);\n },\n };\n}\n\nexport { showcase } from './presets.js';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Scenario } from './index.js';
2
+ /**
3
+ * A ready-made set of scenarios that shows the whole surface of a chat: markdown, a
4
+ * code block, reasoning, a tool round-trip, a typed question, an artifact, an error.
5
+ * The docs' live frames and a demo page can register it as is:
6
+ *
7
+ * ```ts
8
+ * aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));
9
+ * ```
10
+ *
11
+ * The tool names it calls — `get_weather`, `ask_user` — only do something if the app
12
+ * registered a tool of that name; `@aparte/plugin-ask-user` provides the second.
13
+ */
14
+ export declare const showcase: Record<string, Scenario>;
15
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAsE7C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@aparte/provider-scenario",
3
+ "version": "0.14.0",
4
+ "description": "A scripted model for aparté — replays turns you wrote (text at a typing pace, thinking, tool calls, errors) so demos, docs and your own tests stream real-looking conversations with no key and no network.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "@aparte-workspace/source": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "peerDependencies": {
27
+ "@aparte/core": ">=0.14.0 <1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.4.0",
31
+ "vite": "^6.0.0",
32
+ "@aparte/core": "0.14.0"
33
+ },
34
+ "keywords": [
35
+ "aparte",
36
+ "ai",
37
+ "provider",
38
+ "chat",
39
+ "scenario",
40
+ "mock",
41
+ "testing",
42
+ "demo",
43
+ "llm"
44
+ ],
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/apartejs/aparte.git",
49
+ "directory": "packages/providers/ai/scenario"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/apartejs/aparte/issues"
53
+ },
54
+ "scripts": {
55
+ "dev": "vite",
56
+ "build": "vite build && tsc -b --emitDeclarationOnly --force",
57
+ "preview": "vite preview",
58
+ "test": "vitest",
59
+ "test:run": "vitest run",
60
+ "test:coverage": "vitest run --coverage"
61
+ }
62
+ }