@aparte/provider-scenario 0.19.3 → 0.20.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,4SAA4S;AAC1S;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;;;;;;;;;;;OAWG;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;AAiCD;;;;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,
|
|
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,4SAA4S;AAC1S;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;;;;;;;;;;;OAWG;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;AAiCD;;;;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,CAsDnC;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAqG9F"}
|
package/dist/index.js
CHANGED
|
@@ -82,11 +82,10 @@ function playTurn(turn, request, pacing = {}, signal) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
if (!signal?.aborted) {
|
|
85
|
-
const
|
|
86
|
-
const inputTokens = estimateTokens(inputChars);
|
|
85
|
+
const merged = { inputTokens: estimateTokens(inputChars), outputTokens: estimateTokens(outputChars), durationMs: Date.now() - turnBegan, ...usage };
|
|
87
86
|
controller.enqueue({
|
|
88
87
|
type: "done",
|
|
89
|
-
usage: {
|
|
88
|
+
usage: { totalTokens: (merged.inputTokens ?? 0) + (merged.outputTokens ?? 0), ...merged }
|
|
90
89
|
});
|
|
91
90
|
}
|
|
92
91
|
controller.close();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\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. Rendered as markdown once a markdown plugin is installed (plain text otherwise — core ships no markdown renderer); a tagged block (`<artifact>`…) becomes its segment once a grammar for it is registered (`@aparte/plugin-artifacts` does). */\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 * The cursor belongs to the PROVIDER, not to a chat: a request carries no\n * conversation identity, so two chats registered against one provider take turns\n * from the same script and interleave it (chat A gets `turns[0]`, chat B\n * `turns[1]`). On a page with more than one chat, build a provider per chat —\n * `createScenarioProvider` is cheap and takes an `id` — or use `scenarios`, which\n * answers from the request itself and has no cursor at all.\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/**\n * The name of the tool a `tool` message answers, when the message does not carry it:\n * the call with that id, in the assistant turn before.\n */\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') {\n const tool = last.toolName ?? 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 // A `when` scenario that calls a tool needs its `after` counterpart, or the\n // default match routes the tool RESULT back through the same `when` and the\n // conversation eats its own tail — identical rounds until the client's\n // maxTurns error. Plausible to write, silent to read; the hole is visible at\n // creation, so it is said at creation. Ordered `turns` advance on their own, so\n // they are the one exemption.\n //\n // A custom `match` is NOT exempt, and used to be. It does not replace the default\n // rule, it precedes it — `pick` reads `match(...) ?? defaultMatch(...)` — so a\n // `match` that returns `undefined` for a tool result, the shape the docs and this\n // repo's own examples write, falls straight into the loop this warning describes.\n // The exemption silenced the one case most likely to need the line; a consumer who\n // really owns the routing can ignore a single console line at creation — and the\n // message tells them so, since for THEM the sentence about looping may be false.\n if (options.scenarios) {\n // A record value is a Scenario OR a bare turn (string / steps) — a tool in a\n // BARE turn loops the same way, through the default/first-entry fallback.\n const values = Object.values(options.scenarios);\n const asScenario = (v: Scenario | ScenarioTurn): Scenario | null =>\n typeof v === 'object' && !Array.isArray(v) ? v : null;\n const routed = new Set(values.map((v) => asScenario(v)?.after).filter(Boolean));\n const orphans = new Set<string>();\n for (const v of values) {\n const turn = asScenario(v)?.turn ?? (v as ScenarioTurn);\n const steps = Array.isArray(turn) ? turn : [];\n for (const step of steps) {\n if (typeof step === 'object' && step !== null && 'tool' in step\n && typeof step.tool === 'string' && !routed.has(step.tool)) {\n orphans.add(step.tool);\n }\n }\n }\n if (orphans.size > 0) {\n console.warn(\n `[scenario] No \\`after\\` route for: ${[...orphans].map((t) => `\\`${t}\\``).join(', ')}. `\n + 'A scenario calls the tool, but nothing answers its result — the default match '\n + 'sends it back through the same `when`, and the conversation loops until the '\n + \"client's maxTurns stops it. Declare a scenario with `after: '<tool>'` for each.\"\n // Only said when there IS a `match`, because only then can the sentence above be\n // false: routing the result by value is the documented \"Branching on what the user\n // answered\" shape, and it does not loop. Without a `match` the loop is certain, and\n // an escape hatch named there would only muddy the one instruction that helps.\n + (options.match\n ? ' Your own `match` runs before that rule: if it already routes this result'\n + ' — routing by the value the tool returned is a documented shape — this line'\n + ' is the one to ignore.'\n : ''),\n );\n }\n }\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 if (!scenario) {\n // Said, because the alternative was \"Typing…\" forever with an empty reply\n // and nothing in the console (issue #29). The usual cause: `match()`\n // returned the scenario OBJECT — the second argument hands you the objects,\n // so it is an easy slip — where the loop needs its KEY.\n const got = key === undefined ? 'nothing' : typeof key === 'string' ? `\"${key}\"` : `a ${typeof key}`;\n console.warn(\n `[provider-scenario] no scenario for this call: match() returned ${got}, and the keys are `\n + `${Object.keys(scenarios).map((k) => `\"${k}\"`).join(', ') || '(none)'}. Return a key, or `\n + '`undefined` to fall back to the default rule. Streaming an empty turn.',\n );\n }\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"],"names":[],"mappings":"AA8GA,MAAM,SAAS,CAAC,YAAmD;AAC/D,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,EAAE,YAAY;AACpB,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QAAQ,IAAI,CAAC,SAAU,KAAK,SAAS,SAAS,KAAK,OAAO,EAAG,EAAE,KAAK,GAAG;AAClF;AAEA,MAAM,UAAU,CAAC,SAAwC,OAAO,SAAS,WAAW,CAAC,EAAE,MAAM,KAAA,CAAM,IAAI;AAEvG,MAAM,SAAS,CAAC,UAA0D,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAE1H,MAAM,YAAY,CAAC,cAAiF;AAChG,QAAM,MAAgC,CAAA;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,EAAG,KAAI,GAAG,IAAI,OAAO,KAAK,IAAI,EAAE,MAAM,UAAU;AACnG,SAAO;AACX;AAMA,MAAM,aAAa,CAAC,UAA+B,eAAuD;AACtG,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAM,OAAO,SAAS,CAAC,EAAG,WAAW,KAAK,CAAC,OAAO,GAAG,OAAO,UAAU;AACtE,QAAI,aAAa,KAAK;AAAA,EAC1B;AACA,SAAO;AACX;AAOO,SAAS,aAAa,SAA4B,WAAyD;AAC9G,QAAM,UAAU,OAAO,QAAQ,SAAS;AACxC,QAAM,OAAO,QAAQ,SAAS,QAAQ,SAAS,SAAS,CAAC;AACzD,MAAI,MAAM,SAAS,QAAQ;AACvB,UAAM,OAAO,KAAK,YAAY,WAAW,QAAQ,UAAU,KAAK,UAAU;AAC1E,UAAM,MAAM,QAAQ,KAAK,CAAC,CAAA,EAAG,CAAC,MAAM,EAAE,UAAU,UAAa,EAAE,UAAU,IAAI;AAC7E,QAAI,IAAK,QAAO,IAAI,CAAC;AAAA,EACzB;AACA,QAAM,OAAO,OAAO,IAAI;AACxB,aAAW,CAAC,KAAK,CAAC,KAAK,SAAS;AAC5B,QAAI,EAAE,SAAS,OAAW;AAC1B,QAAI,OAAO,EAAE,SAAS,WAAW,KAAK,YAAA,EAAc,SAAS,EAAE,KAAK,YAAA,CAAa,IAAI,EAAE,KAAK,KAAK,IAAI,EAAG,QAAO;AAAA,EACnH;AACA,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO,QAAQ,CAAC,IAAI,CAAC;AACzB;AAEA,MAAM,QAAQ,CAAC,IAAY,WAAwC,IAAI,QAAQ,CAAC,YAAY;AACxF,MAAI,MAAM,KAAK,QAAQ,gBAAgB,QAAA;AACvC,QAAM,QAAQ,WAAW,MAAM,EAAE;AACjC,WAAS,OAAa;AAClB,iBAAa,KAAK;AAClB,YAAQ,oBAAoB,SAAS,IAAI;AACzC,YAAA;AAAA,EACJ;AACA,UAAQ,iBAAiB,SAAS,MAAM,EAAE,MAAM,MAAM;AAC1D,CAAC;AAGD,MAAM,iBAAiB,CAAC,UAA0B,KAAK,KAAK,QAAQ,CAAC;AAErE,IAAI,WAAW;AAOR,SAAS,SACZ,MACA,SACA,SAAqC,CAAA,GACrC,QACiC;AACjC,QAAM,QAAQ,WAAW,YAAY,OAAO,oBAAoB,KAAK,IAAI,GAAG,OAAO,SAAS,EAAE;AAC9F,QAAM,QAAQ,WAAW,YAAY,IAAI,KAAK,IAAI,GAAG,OAAO,SAAS,EAAE;AACvE,QAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAM,aAAa,QAAQ,SAAS,OAAO,CAAC,GAAG,MAAM,IAAI,OAAO,CAAC,EAAE,QAAQ,CAAC;AAE5E,SAAO,IAAI,eAAkC;AAAA,IACzC,MAAM,MAAM,YAAY;AACpB,UAAI,cAAc;AAClB,UAAI,QAA8B,CAAA;AAGlC,YAAM,YAAY,KAAK,IAAA;AACvB,UAAI;AACA,mBAAW,QAAQ,OAAO;AACtB,cAAI,QAAQ,QAAS;AACrB,cAAI,UAAU,MAAM;AAChB,kBAAM,MAAM,KAAK,MAAM,MAAM;AAAA,UACjC,WAAW,UAAU,QAAQ,cAAc,MAAM;AAC7C,kBAAM,OAAO,UAAU,OAAO,SAAS;AACvC,kBAAM,OAAO,UAAU,OAAO,KAAK,OAAO,KAAK;AAC/C,qBAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,OAAO;AACzC,kBAAI,QAAQ,QAAS;AACrB,oBAAM,QAAQ,KAAK,MAAM,GAAG,IAAI,KAAK;AACrC,yBAAW,QAAQ,EAAE,MAAM,MAAA,CAAO;AAClC,6BAAe,MAAM;AACrB,kBAAI,QAAQ,KAAK,IAAI,QAAQ,KAAK,OAAQ,OAAM,MAAM,OAAO,MAAM;AAAA,YACvE;AAAA,UACJ,WAAW,UAAU,MAAM;AACvB,uBAAW,QAAQ,EAAE,MAAM,YAAY,IAAI,KAAK,MAAM,YAAY,EAAE,QAAQ,IAAI,MAAM,KAAK,MAAM,OAAO,KAAK,SAAS,CAAA,GAAI;AAAA,UAC9H,WAAW,WAAW,MAAM;AACxB,uBAAW,QAAQ,EAAE,MAAM,SAAS,SAAS,KAAK,OAAO;AACzD,uBAAW,MAAA;AACX;AAAA,UACJ,WAAW,WAAW,MAAM;AACxB,oBAAQ,EAAE,GAAG,OAAO,GAAG,KAAK,MAAA;AAAA,UAChC;AAAA,QACJ;AACA,YAAI,CAAC,QAAQ,SAAS;AAClB,gBAAM,eAAe,eAAe,WAAW;AAC/C,gBAAM,cAAc,eAAe,UAAU;AAC7C,qBAAW,QAAQ;AAAA,YACf,MAAM;AAAA,YACN,OAAO,EAAE,aAAa,cAAc,aAAa,cAAc,cAAc,YAAY,KAAK,QAAQ,WAAW,GAAG,MAAA;AAAA,UAAM,CAC7H;AAAA,QACL;AACA,mBAAW,MAAA;AAAA,MACf,SAAS,KAAK;AACV,mBAAW,MAAM,GAAG;AAAA,MACxB;AAAA,IACJ;AAAA,EAAA,CACH;AACL;AAkBO,SAAS,uBAAuB,UAAmC,IAAsB;AAe5F,MAAI,QAAQ,WAAW;AAGnB,UAAM,SAAS,OAAO,OAAO,QAAQ,SAAS;AAC9C,UAAM,aAAa,CAAC,MAChB,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAAI,IAAI;AACrD,UAAM,SAAS,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,WAAW,CAAC,GAAG,KAAK,EAAE,OAAO,OAAO,CAAC;AAC9E,UAAM,8BAAc,IAAA;AACpB,eAAW,KAAK,QAAQ;AACpB,YAAM,OAAO,WAAW,CAAC,GAAG,QAAS;AACrC,YAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAA;AAC3C,iBAAW,QAAQ,OAAO;AACtB,YAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,UAAU,QACpD,OAAO,KAAK,SAAS,YAAY,CAAC,OAAO,IAAI,KAAK,IAAI,GAAG;AAC5D,kBAAQ,IAAI,KAAK,IAAI;AAAA,QACzB;AAAA,MACJ;AAAA,IACJ;AACA,QAAI,QAAQ,OAAO,GAAG;AAClB,cAAQ;AAAA,QACJ,sCAAsC,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,qPAQjF,QAAQ,QACL,+KAGA;AAAA,MAAA;AAAA,IAEd;AAAA,EACJ;AACA,QAAM,KAAK,QAAQ,MAAM;AACzB,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,SAA0B,QAAQ,UAAU,CAAC;AAAA,IAC/C,IAAI;AAAA,IACJ;AAAA,IACA,aAAa;AAAA,IACb,cAAc,CAAC,aAAa,kBAAkB;AAAA,EAAA,CACjD;AACD,QAAM,YAAY,UAAU,QAAQ,aAAa,CAAA,CAAE;AACnD,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,QAAQ;AAEZ,QAAM,OAAO,CAAC,YAA6C;AACvD,QAAI,QAAQ,OAAO,QAAQ;AACvB,YAAM,OAAO,QAAQ,MAAM,KAAK,IAAI,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC;AACpE;AACA,aAAO;AAAA,IACX;AACA,UAAM,MAAM,MAAM,SAAS,SAAS,KAAK,aAAa,SAAS,SAAS;AACxE,UAAM,WAAW,QAAQ,SAAY,UAAU,GAAG,IAAI;AACtD,QAAI,CAAC,UAAU;AAKX,YAAM,MAAM,QAAQ,SAAY,YAAY,OAAO,QAAQ,WAAW,IAAI,GAAG,MAAM,KAAK,OAAO,GAAG;AAClG,cAAQ;AAAA,QACJ,mEAAmE,GAAG,sBACjE,OAAO,KAAK,SAAS,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,KAAK,QAAQ;AAAA,MAAA;AAAA,IAG/E;AACA,WAAO,UAAU,QAAQ;AAAA,EAC7B;AAEA,SAAO;AAAA,IACH;AAAA,IACA,aAAa,OAAO;AAAA,MAChB;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb,SAAS;AAAA,MACT,cAAc,EAAE,QAAQ,CAAA,EAAC;AAAA,IAAE;AAAA,IAE/B,WAAW,MAAM;AAAA,IACjB,aAAa,YAAY;AAAA,IACzB,MAAM,KAAK,SAA4B,SAAmB,KAAiF;AACvI,aAAO,SAAS,KAAK,OAAO,GAAG,SAAS,QAAQ,QAAQ,KAAK,MAAM;AAAA,IACvE;AAAA,EAAA;AAER;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\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. Rendered as markdown once a markdown plugin is installed (plain text otherwise — core ships no markdown renderer); a tagged block (`<artifact>`…) becomes its segment once a grammar for it is registered (`@aparte/plugin-artifacts` does). */\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 * The cursor belongs to the PROVIDER, not to a chat: a request carries no\n * conversation identity, so two chats registered against one provider take turns\n * from the same script and interleave it (chat A gets `turns[0]`, chat B\n * `turns[1]`). On a page with more than one chat, build a provider per chat —\n * `createScenarioProvider` is cheap and takes an `id` — or use `scenarios`, which\n * answers from the request itself and has no cursor at all.\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/**\n * The name of the tool a `tool` message answers, when the message does not carry it:\n * the call with that id, in the assistant turn before.\n */\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') {\n const tool = last.toolName ?? 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 merged = { inputTokens: estimateTokens(inputChars), outputTokens: estimateTokens(outputChars), durationMs: Date.now() - turnBegan, ...usage };\n // The total follows the parts it is the sum of, unless the script set it\n // too: a step overriding `inputTokens` alone left the estimate's total in\n // place, and the context gauge reads the total first.\n controller.enqueue({\n type: 'done',\n usage: { totalTokens: (merged.inputTokens ?? 0) + (merged.outputTokens ?? 0), ...merged },\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 // A `when` scenario that calls a tool needs its `after` counterpart, or the\n // default match routes the tool RESULT back through the same `when` and the\n // conversation eats its own tail — identical rounds until the client's\n // maxTurns error. Plausible to write, silent to read; the hole is visible at\n // creation, so it is said at creation. Ordered `turns` advance on their own, so\n // they are the one exemption.\n //\n // A custom `match` is NOT exempt, and used to be. It does not replace the default\n // rule, it precedes it — `pick` reads `match(...) ?? defaultMatch(...)` — so a\n // `match` that returns `undefined` for a tool result, the shape the docs and this\n // repo's own examples write, falls straight into the loop this warning describes.\n // The exemption silenced the one case most likely to need the line; a consumer who\n // really owns the routing can ignore a single console line at creation — and the\n // message tells them so, since for THEM the sentence about looping may be false.\n if (options.scenarios) {\n // A record value is a Scenario OR a bare turn (string / steps) — a tool in a\n // BARE turn loops the same way, through the default/first-entry fallback.\n const values = Object.values(options.scenarios);\n const asScenario = (v: Scenario | ScenarioTurn): Scenario | null =>\n typeof v === 'object' && !Array.isArray(v) ? v : null;\n const routed = new Set(values.map((v) => asScenario(v)?.after).filter(Boolean));\n const orphans = new Set<string>();\n for (const v of values) {\n const turn = asScenario(v)?.turn ?? (v as ScenarioTurn);\n const steps = Array.isArray(turn) ? turn : [];\n for (const step of steps) {\n if (typeof step === 'object' && step !== null && 'tool' in step\n && typeof step.tool === 'string' && !routed.has(step.tool)) {\n orphans.add(step.tool);\n }\n }\n }\n if (orphans.size > 0) {\n console.warn(\n `[scenario] No \\`after\\` route for: ${[...orphans].map((t) => `\\`${t}\\``).join(', ')}. `\n + 'A scenario calls the tool, but nothing answers its result — the default match '\n + 'sends it back through the same `when`, and the conversation loops until the '\n + \"client's maxTurns stops it. Declare a scenario with `after: '<tool>'` for each.\"\n // Only said when there IS a `match`, because only then can the sentence above be\n // false: routing the result by value is the documented \"Branching on what the user\n // answered\" shape, and it does not loop. Without a `match` the loop is certain, and\n // an escape hatch named there would only muddy the one instruction that helps.\n + (options.match\n ? ' Your own `match` runs before that rule: if it already routes this result'\n + ' — routing by the value the tool returned is a documented shape — this line'\n + ' is the one to ignore.'\n : ''),\n );\n }\n }\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 if (!scenario) {\n // Said, because the alternative was \"Typing…\" forever with an empty reply\n // and nothing in the console (issue #29). The usual cause: `match()`\n // returned the scenario OBJECT — the second argument hands you the objects,\n // so it is an easy slip — where the loop needs its KEY.\n const got = key === undefined ? 'nothing' : typeof key === 'string' ? `\"${key}\"` : `a ${typeof key}`;\n console.warn(\n `[provider-scenario] no scenario for this call: match() returned ${got}, and the keys are `\n + `${Object.keys(scenarios).map((k) => `\"${k}\"`).join(', ') || '(none)'}. Return a key, or `\n + '`undefined` to fall back to the default rule. Streaming an empty turn.',\n );\n }\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"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/provider-scenario",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"homepage": "https://apartejs.dev/providers/ai/scenario/",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@aparte/core": ">=0.
|
|
34
|
+
"@aparte/core": ">=0.20.0 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.4.0",
|
|
38
38
|
"vite": "^6.0.0",
|
|
39
|
-
"@aparte/core": "0.
|
|
39
|
+
"@aparte/core": "0.20.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"aparte",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"dev": "vite",
|
|
63
|
-
"build": "vite build && tsc -b --emitDeclarationOnly --force",
|
|
63
|
+
"build": "vite build && tsc -b --emitDeclarationOnly --force && node ../../../../scripts/stamp-dist.mjs",
|
|
64
64
|
"preview": "vite preview",
|
|
65
65
|
"test": "vitest",
|
|
66
66
|
"test:run": "vitest run",
|