/** * Provider ports (#148/#149) — typed contracts for host-supplied backends. * * The engine never ships network code or API keys. A host wires a backend via * the `providers` prop on ; the engine calls its typed methods and * falls back gracefully when no provider is mounted. * * The three ports: * - `ChatProvider` — gives QQ buddies an LLM brain (#148) * - `ModerationProvider` — screens inbound LLM text before render (#148) * - `WebContentProvider` — generates fake 2000s-era web pages for IE (#149) * * Key constraint: these types intentionally CANNOT express an `apiKey` field. * The provider is a pure function boundary — the host owns the credential on * the other side (a BFF, a serverless function, etc.). */ /** * One turn in a buddy conversation, tagged by role. The engine assembles this * from the QQ thread history — at most `maxHistory` turns (default 20). */ export interface ChatTurn { role: 'user' | 'buddy'; text: string; } /** * A provenance-labeled item in the world context snapshot. Only items matching * the buddy's declared `contextSelectors` are included, so what leaves the * browser is an explicit, auditable subset. */ export interface WorldContextItem { /** What kind of data this is. */ kind: 'flag' | 'event' | 'file'; /** Display key (flag name / event type / file path). */ key: string; /** The value or summary. */ value: string | number | boolean | null; /** Who produced this data point. */ author: 'system' | 'player' | 'llm'; } /** * The context passed to {@link ChatProvider.reply}. The engine assembles it; * the host's BFF forwards it to its LLM. */ export interface ChatContext { /** The buddy being talked to. */ buddyId: string; /** The buddy's display name. */ nickname: string; /** Persona prompt from the buddy's data (freeform string). */ persona?: string; /** Bounded conversation history (most recent last). */ history: ChatTurn[]; /** Declared world-context items, when the buddy opts in via selectors. */ worldContext?: WorldContextItem[]; } /** * The host-supplied chat backend. Returns either a complete string or an * async iterable of chunks (mapped to the QQ typing effect). */ export interface ChatProvider { reply(ctx: ChatContext, signal: AbortSignal): Promise | AsyncIterable; } /** The result of a moderation check. */ export interface ModerationResult { /** Whether the text is allowed to be rendered. */ allowed: boolean; /** Human-readable reason when blocked (not shown to the player). */ reason?: string; } /** * Screens inbound LLM text before it reaches the player. Applied to both * chat replies and generated web content (#149). */ export interface ModerationProvider { check(text: string, signal: AbortSignal): Promise; } /** Context passed to the web content generator. */ export interface WebContentContext { /** The URL the player navigated to. */ url: string; /** Culture id (e.g. 'en', 'zh') for era-appropriate styling. */ culture: string; /** Optional era-prompt template from the culture package. */ eraPrompt?: string; } /** The result of generating a fake web page. */ export interface GeneratedPage { /** The HTML body to render (will be sanitized by the strict tier). */ html: string; /** Browser title; falls back to the URL when omitted. */ title?: string; } /** * Generates period-appropriate web pages for URLs that aren't in the * authorized-site registry. Like websim.ai but constrained to the XP era. */ export interface WebContentProvider { generatePage(url: string, ctx: WebContentContext, signal: AbortSignal): Promise; } /** * The `providers` prop on . Every field is optional — the engine * works identically without any provider (scripted fallback for chat, error * page for unknown URLs). */ export interface XPProviders { chat?: ChatProvider; moderation?: ModerationProvider; webContent?: WebContentProvider; } /** * Declares which world-state items a buddy's provider may see. Only matching * items are assembled into {@link ChatContext.worldContext} — nothing else * leaves the browser. */ export interface ContextSelector { /** Include scenario flag values matching these names. */ flags?: string[]; /** Include recent events matching these type prefixes. */ recentEvents?: string[]; /** Include file summaries (name + exists + locked) for these paths. */ fileSummary?: string[][]; } /** * Extended fields for a `QQReply` of `kind: 'provider'`. These live on the * buddy data (culture package / scenario JSON), not on the provider itself. */ export interface ProviderReplyConfig { /** Freeform persona prompt forwarded as {@link ChatContext.persona}. */ persona?: string; /** Which world-state items the provider may see. */ contextSelectors?: ContextSelector; /** Fallback scripted lines played when the provider is missing or fails. */ fallbackLines?: string[]; /** Max conversation turns to include in context (default 20). */ maxHistory?: number; }