import { LanguageModel, ToolSet, ModelMessage, UIMessage, StopCondition } from 'ai'; import { d as ChatStoreFactory, C as ChatStore } from './chat-store-dqaTB0vU.mjs'; import { a as StorageAdapterFactory, S as StorageAdapter } from './storage-adapter-Dv_hy7k9.mjs'; import { R as RetrieverFactory, N as Namespace, a as RetrievedChunk } from './types-B0JV4XZ0.mjs'; import { d as MemoryAdapterFactory, M as Memory, a as MemoryScope } from './types-B_kkAV7t.mjs'; import { A as AgentConfig, F as FollowUpMessage, P as PublishedAgentConfig } from './config-CgqlRgyG.mjs'; /** Browser-safe, versioned error contract. Never add provider payloads, messages, * codes, stacks or causes here. Metadata is a UI hint, NOT server authorization * or permission to replay a request (tools may already have had side effects). */ declare const CHAT_ERROR_VERSION: 1; declare const CHAT_ERROR_DATA_TYPE: "data-chat-error"; /** JSON metadata in a header preserves the existing HTTP status and body. */ declare const CHAT_ERROR_HEADER = "x-chat-error"; type ChatErrorKind = 'abort' | 'rate_limit' | 'auth' | 'transient' | 'content_policy' | 'prompt' | 'model' | 'tool' | 'unknown'; interface ChatErrorMetadata { version: typeof CHAT_ERROR_VERSION; kind: ChatErrorKind; retryable: boolean; retryAfterMs?: number; /** Opaque correlation reference, not user-facing copy or a URL. */ traceId?: string; } declare function messageForErrorKind(kind: ChatErrorKind): string; /** Fail closed on unknown versions/kinds and malformed core fields. Return a * fresh allowlisted projection, never spread untrusted data into an Error/DOM. * Optional invalid hints are dropped. Contradictory retry hints cannot enable * retries for auth/policy/tool/etc. No caller should use this as server policy. */ declare function parseChatErrorMetadata(value: unknown): ChatErrorMetadata | undefined; declare function parseChatErrorHeader(value: string | null): ChatErrorMetadata | undefined; /** Server-side projection. Deliberately accepts only the public fields, not * `cause`, `code`, `status` or `message` from ClassifiedChatError. */ declare function toChatErrorMetadata(classified: Pick, traceId: string): ChatErrorMetadata; /** * Error taxonomy — turning "something went wrong" into something actionable. * * ────────────────────────────────────────────────────────────────────────── * Why this exists * ────────────────────────────────────────────────────────────────────────── * * Before this module the handler had exactly one user-facing failure string * and no notion of *what kind* of failure had occurred. A 429 from the * provider, an expired API key, a blown context window, and a genuine 500 all * collapsed into "An error occurred while generating the response." A host * that wanted to retry-with-backoff on rate limits, surface "your key is * invalid" to an operator, or tell a user their message was too long had to * write the classifier themselves — in their own `onError`, against provider * error shapes we already have in hand. * * That is the burden this package exists to absorb. Classification is * infrastructure, not application logic. * * ────────────────────────────────────────────────────────────────────────── * Why it duck-types instead of using `instanceof` * ────────────────────────────────────────────────────────────────────────── * * `ai` is a PEER dependency spanning `^6.0.0`, and the concrete provider * packages (`@ai-sdk/anthropic`, `@ai-sdk/openai`, `@ai-sdk/google`, the * gateway) are not dependencies at all. Importing error classes to * `instanceof` against would (a) couple us to one point in that range, (b) * break silently when a provider is loaded from a different copy of the SDK in * the host's tree — the classic dual-package `instanceof` failure — and (c) * miss raw provider errors that never became SDK classes. * * So we read *shape*: `name`, `statusCode`/`status`, the parsed `responseBody`, * the provider's own `type`/`code` discriminants, and response headers. That * is stable across SDK minors and works on errors the SDK never wrapped. * * ────────────────────────────────────────────────────────────────────────── * Design rules * ────────────────────────────────────────────────────────────────────────── * * 1. NEVER throw. Classification runs on an already-failing path; a throw * here would replace a real error with a confusing one. Every helper is * defensive and the whole entry point is wrapped. * 2. Prefer a WRONG-but-safe answer to a wrong-and-dangerous one. When * signals conflict we bias toward `retryable: false` — a missed retry * costs one failed turn; a retry loop against a content-policy block or a * bad key costs money and can get an account limited. * 3. `unknown` is a legitimate answer. Guessing a category we cannot support * is worse than admitting we do not know. */ /** The result of classifying a stream/turn failure. */ interface ClassifiedChatError { /** The actionable category. */ kind: ChatErrorKind; /** * Is a straight retry of the same request likely to succeed? * * `true` only for `rate_limit` and `transient`. Deliberately conservative: * a retry against a content-policy block or an invalid key burns money and * quota to arrive at the identical failure. */ retryable: boolean; /** * How long to wait before retrying, in ms, when the provider told us. * Parsed from `Retry-After` (delta-seconds or HTTP-date) or from the * provider's own rate-limit reset headers. `undefined` means "we were not * told" — back off on your own schedule, do not treat it as zero. */ retryAfterMs?: number; /** Upstream HTTP status, when the failure carried one. */ status?: number; /** The provider's own machine-readable code (`rate_limit_exceeded`, `overloaded_error`, …). */ code?: string; /** A safe, user-facing sentence for this category. Never contains provider internals. */ message: string; /** * The original thrown value, untouched. * * Log it, report it, re-classify it — but do not put it in front of a user: * provider errors routinely echo prompt fragments and occasionally key * prefixes. */ cause: unknown; } /** * Is this the AbortController firing rather than a real failure? * * Aborts arrive in several shapes depending on runtime and provider: a * DOMException named `AbortError` (undici / browsers), a plain Error with that * name, an SDK-wrapped abort, or Node's standard `ABORT_ERR` code. We do not * classify by message text because upstream gateways also use "aborted" for * real failures that must remain visible. * * Conservative by design: a false negative logs one extra error, a false * positive silently swallows a genuine outage. */ declare function isAbortError(err: unknown): boolean; /** * Classify a thrown value into an actionable category. * * Precedence, most to least trustworthy: * 1. abort — a signal we raised ourselves; never a failure * 2. the SDK's own `isRetryable: true` on an APICall-shaped error * 3. the provider's machine-readable code / type / status discriminant * 4. the HTTP status * 5. narrow free-text probes * 6. `unknown` * * Never throws: any internal failure degrades to an `unknown` classification * carrying the original error. */ declare function classifyError(err: unknown): ClassifiedChatError; /** * Observability — structured, correlated logging for a chat turn. * * ────────────────────────────────────────────────────────────────────────── * Why this exists * ────────────────────────────────────────────────────────────────────────── * * The handler already logged the things that matter — save failures, title * failures, memory-extraction failures, stream errors. But it logged them as * loose `console.error(JSON.stringify({ event, userId, conversationId }))` * calls with no shared shape, no correlation id, and no way for the host to * redirect them. `logErrors` was a *boolean*: you could silence the output or * keep it, but you could not route it into Datadog, add your own fields, or * follow a single turn from request → model → tool → persistence. * * So the moment something went wrong in production you had exactly the wrong * artefact: a wall of unrelated lines, no way to tell which belonged to the * conversation the customer is complaining about. * * This module fixes the *seam*, deliberately and narrowly: * * • one `ChatLogger` interface the host can implement * • one `traceId` minted per request and stamped on every line for that turn * • one event vocabulary, so `turn.finish` means the same thing everywhere * * ────────────────────────────────────────────────────────────────────────── * What this is NOT * ────────────────────────────────────────────────────────────────────────── * * This is not OpenTelemetry, and it deliberately does not depend on it. Adding * `@opentelemetry/*` to a widget package would push a heavy, version-fragile * transitive tree onto every consumer to serve the subset who want it. Instead * this is the *seam* an OTel adapter plugs into: a host that wants spans * implements `ChatLogger`, opens a span per `traceId`, and gets full fidelity * with zero cost to everyone else. That adapter is tracked separately (#109). * * ────────────────────────────────────────────────────────────────────────── * Contract for implementers * ────────────────────────────────────────────────────────────────────────── * * A `ChatLogger` MUST NOT throw and MUST NOT block. It sits on the hot path of * every turn; a logger that throws would turn a successful answer into a 500, * and one that awaits a network write would add latency to every token. The * handler wraps every call in a try/catch as defence in depth, but implement * accordingly: buffer and flush out of band. */ /** Standard severity ladder. `debug` is for per-chunk / per-step volume. */ type LogLevel = 'debug' | 'info' | 'warn' | 'error'; /** * The canonical event vocabulary. * * Typed as a union of known events *plus* `(string & {})`, so the known names * autocomplete and typo-check while a host or future feature can still emit * something bespoke without a package release. */ type ChatLogEvent = /** A chat turn began. Carries model + message counts. */ 'turn.start' /** A chat turn completed normally. Carries durationMs, finishReason, usage. */ | 'turn.finish' /** A chat turn failed. Carries the error taxonomy fields. */ | 'turn.error' /** The client disconnected, or the wall-clock cap fired. Not a failure. */ | 'turn.abort' /** The model finished a step. Carries stepCount and any tools invoked. */ | 'turn.step' /** A provider/external tool call was observed; NOT proof of execution. */ | 'tool.call' /** The SDK is about to execute a server-side tool. No payload is logged. */ | 'tool.start' /** Server-side execution completed (including generator exhaustion). */ | 'tool.finish' /** Server-side execution threw. Carries only the safe kind, never error text. */ | 'tool.error' /** Persisting the assistant turn failed — the loudest thing in this file. */ | 'save.failed' /** Thread-title generation or rename failed (degrades to a placeholder). */ | 'title.failed' /** Memory recall/extraction/consent failed. Carries a `phase` discriminant. */ | 'memory.failed' /** Knowledge retrieval failed (the turn continues, ungrounded). */ | 'retrieval.failed' /** The host's `getContext` threw (the turn continues, uninjected). */ | 'context.failed' /** Context injection was skipped — carries a `reason` (e.g. over budget). */ | 'context.skipped' /** Follow-up suggestion generation failed (best-effort, non-fatal). */ | 'followups.failed' /** A host lifecycle hook threw — carries `hook`. Never fails the turn. */ | 'hook.failed' /** History summarization failed (the turn continues, uncompacted). */ | 'summarize.failed' /** Per-request tool resource teardown failed — a resource may have leaked. */ | 'cleanup.failed' /** A history page was served. Carries limit/hasMore for pagination debugging. */ | 'history.page' /** An unhandled throw reached the dispatcher and became a 500. */ | 'request.error' | (string & {}); /** * The structured payload of one log line. * * Every field beyond `traceId` and `event` is optional because not every event * has every dimension — but when a field IS present it always means the same * thing, which is the entire point of having a shape at all. */ interface ChatLogFields { /** Correlation id for this request. Present on every line. */ traceId: string; /** The canonical event name. */ event: ChatLogEvent; /** The verified user, when the request got past authentication. */ userId?: string; conversationId?: string; messageId?: string; /** Unique authenticated chat request, even when several turns share a trace. */ turnId?: string; /** SDK tool invocation id, not the tool name (concurrent calls can share a name). */ toolCallId?: string; /** Zero-based SDK step; absent for approved calls resumed before the first step. */ stepNumber?: number; /** External means no server execute: client/deferred/unhandled, not proven client execution. */ executionLocation?: 'server' | 'provider' | 'external'; /** Tool lifecycle outcome; observed does not assert that a tool actually ran. */ outcome?: 'observed' | 'started' | 'success' | 'error'; /** Model label for this turn (`anthropic/claude-…`). */ model?: string; /** Tool name, for tool-scoped events. */ toolName?: string; /** Wall-clock duration of whatever the event describes. */ durationMs?: number; /** Error taxonomy fields, mirrored from `classifyError`. */ kind?: ChatErrorKind; retryable?: boolean; status?: number; code?: string; /** The error's message. Never the raw error object — that is not serialisable. */ error?: string; /** Stack, on `error`-level lines only, when one exists. */ stack?: string; /** Event-specific extras (token counts, finishReason, page size, …). */ [key: string]: unknown; } /** * The seam. Implement this to route chat telemetry wherever you already send * logs — Datadog, Pino, an OTel span, a queue. * * Must not throw and must not block. See the module doc. */ interface ChatLogger { log(level: LogLevel, fields: ChatLogFields): void | PromiseLike; } /** Discards everything. Used when `logErrors: false`. */ declare const noopLogger: ChatLogger; /** * The default logger: one JSON object per line on the console. * * JSON rather than pretty text because the overwhelmingly common destination is * a log aggregator that parses structured lines, and a human can still read it. * `warn`/`error` go to `console.error` so they land on stderr and survive * stdout-only log filters; everything else goes to `console.log`. */ declare function createConsoleLogger(options?: { minLevel?: LogLevel; }): ChatLogger; /** * Mint (or adopt) a correlation id for this request. * * If the caller already has a trace we ADOPT it rather than inventing a * competing one — a widget log that can't be joined to the host's request log * is half as useful. In precedence order: * * 1. `traceparent` (W3C Trace Context) → the 32-hex trace-id field * 2. `x-request-id` / `x-correlation-id` / `x-amzn-trace-id` — the de-facto * headers set by Vercel, Cloudflare, ALB and most gateways * 3. a fresh random id * * Adopted values are length-capped and stripped of anything outside a safe * charset: this string lands in log lines and a response header, so a hostile * client must not be able to inject newlines (log forging) or header * terminators. */ declare function resolveTraceId(request: { headers: Headers; }): string; /** A fresh 32-hex trace id, W3C-shaped so it drops straight into a `traceparent`. */ declare function newTraceId(): string; /** * A logger with request-scoped fields already bound. * * The handler creates one per turn so call sites read as * `log.warn('save.failed', { error })` instead of restating traceId + userId + * conversationId on every line — which is exactly the repetition that made the * old ad-hoc calls drift out of shape from one another. */ interface TurnLogger { readonly traceId: string; debug(event: ChatLogEvent, fields?: Partial): void; info(event: ChatLogEvent, fields?: Partial): void; warn(event: ChatLogEvent, fields?: Partial): void; error(event: ChatLogEvent, fields?: Partial): void; /** Derive a logger with extra bound fields (a conversation id learned mid-request). */ child(extra: Partial): TurnLogger; } /** * Bind base fields to a `ChatLogger`. * * Every emit is wrapped: a host logger that throws must never turn a working * answer into a 500. We swallow rather than re-log, because the only channel * available to report a broken logger is the broken logger. */ declare function createTurnLogger(logger: ChatLogger, base: Partial & { traceId: string; }): TurnLogger; /** Normalise a thrown value into the `error` / `stack` log fields. */ declare function errorFields(err: unknown): { error: string; stack?: string; }; /** Header carrying the trace id back to the caller. */ declare const TRACE_HEADER = "X-Mordn-Trace-Id"; interface SandboxFile { readonly type: 'file'; readonly url: string; readonly storagePath: string; readonly filename: string; readonly mediaType: string; readonly size: number; } /** * Read-only carrier, authenticated at runtime by isVerifiedSandboxArtifact. * Deliberately structural so independently bundled /server and /server/hosted * declaration files remain assignable; a TypeScript cast is NOT authority. */ interface VerifiedSandboxArtifact { readonly file: SandboxFile; } /** No resource identifiers, provider URLs, credentials or scope selectors. */ interface ManagedSandboxStatus { readonly enabled: boolean; readonly available: boolean; } /** * Server-code-only adapter returned by createHostedSandboxes(). The API key * binds the tenant/agent; ctx.userId MUST be verified by getUserId. The handler * invokes this only with resolved runtime.sandbox.enabled === true. The API * independently checks its published config and operator policy on EVERY call. */ interface ManagedSandboxIntegration { readonly kind: 'mordn-managed'; /** Read-only discovery; never allocates a workspace. */ status(ctx: ChatRequestContext): Promise; /** Per-turn HTTP connection only; cleanup must never delete the workspace. */ buildTools(ctx: ChatRequestContext, options: { readonly abortSignal: AbortSignal; /** Opaque, server-verified artifacts, NOT arbitrary tool JSON or telemetry. */ readonly onArtifact: (artifact: VerifiedSandboxArtifact) => void; }): Promise; } /** * Public configuration surface for `createChatHandler`. * * This is the API a host app touches to mount the chat backend. The design * follows one rule, derived from auditing a real power-user integration * (Jarvis): take away the shared, dangerous-to-get-wrong plumbing; keep every * point of genuine per-app variation as an injection point or a hook. * * Three tiers of override, from "you must" to "you rarely will": * * REQUIRED injections — no safe default exists: * • getUserId (identity; the security boundary) * * OPTIONAL injections — a default exists, swap to take control: * • model (which LLM) * • followUps (post-response suggestion generation) * • buildTools (your tools, incl. per-request resources) * • store (persistence; hosted default) * • storage (attachments; hosted default) * * HOOKS — the loop runs fine without them; override one seam at a time: * • buildSystemPrompt, transformMessages, onChatFinish, onError, * stopWhen, upload * * Everything NOT in this file — ownership checks, idempotency, pagination, * re-signing, socket teardown, save-on-finish — is owned by the handler and * is intentionally not configurable, because getting it wrong is a security * or correctness bug, not a preference. */ /** * Everything a per-request hook/injection needs to know about the current * request, assembled by the handler AFTER authentication. Passed to * `buildTools`, `model` (when a function), `buildSystemPrompt`, and * `transformMessages`. * * Note `userId` is the server-verified identity — the same value the store * and storage are bound to. Hooks can trust it. */ interface ChatRequestContext { /** Server-verified user id. Never client-supplied. */ userId: string; /** The conversation this request targets. */ conversationId: string; /** The raw request, for hooks that need headers/cookies (e.g. an org id). */ request: Request; /** Read-only, resolved server config (preview only after the trusted resolver). */ readonly config?: Readonly | null; /** Handler-owned client abort/deadline signal, available while building tools. */ readonly abortSignal?: AbortSignal; } /** * A single message-feedback submission the handler hands to the `onFeedback` * seam, assembled AFTER authentication. The widget posts a thumbs up/down * (optionally with a reason) on an assistant message; the handler validates it, * resolves the VERIFIED user, and calls `onFeedback` with this event. * * `userId` is the server-verified id (the same value the store/memory are bound * to), never a browser-controlled header or body field. Trust `userId`. */ interface FeedbackEvent { /** Server-verified user id. Never client-supplied. Safe to attribute on. */ userId: string; /** * The conversation the rated message belongs to. Optional: a brand-new chat * may not have been persisted server-side yet when the user rates a reply. */ conversationId?: string; /** Id of the assistant `UIMessage` being rated. Always present (validated). */ messageId: string; /** Thumbs up or down. */ rating: 'up' | 'down'; /** Optional freeform reason, typically supplied on thumbs-down. */ reason?: string; } /** Canonical hosted control-plane record. */ type HostedAgentConfig = PublishedAgentConfig; /** * What `buildTools` returns. The `cleanup` callback is the critical piece the * naive "just return a ToolSet" design misses: tools backed by a per-request * resource (an MCP client holding a socket, a DB transaction, a temp scope) * MUST be torn down after the stream finishes — exactly once, whether the * stream completed, errored, or the client aborted. The handler guarantees * that single, correctly-timed call so the host app never leaks sockets. */ interface BuiltTools { tools: ToolSet; /** * Called exactly once when the request is fully done (success, error, or * abort), after the response stream has settled. Optional — omit when your * tools hold no per-request resource. */ cleanup?: () => void | Promise; } /** * Server-side upload policy. Enforced by the handler's upload route BEFORE * any bytes touch storage, so an oversized or disallowed file is rejected * with a clean 4xx instead of a framework body-parse failure. The widget's * client-side `accept`/`maxBytes` should mirror these, but the SERVER is the * source of truth — the client checks are UX, these checks are the boundary. */ interface UploadPolicy { /** * Allowed MIME types. An exact-match allow-list (not a prefix match) so * `image/svg+xml` can be excluded while `image/png` is allowed. Defaults to * a conservative image+pdf set that current vision models accept natively. */ allowedMediaTypes?: string[]; /** Per-file size cap in bytes. Defaults to 5 MB. */ maxBytes?: number; } /** See `CreateChatHandlerOptions.cors`. */ interface CorsPolicy { /** Exact allowed origins (`https://docs.example.com`), or `'*'` for any. */ allowOrigins: string[]; /** Emit `Access-Control-Allow-Credentials: true` (cookie-based getUserId). */ allowCredentials?: boolean; } /** * Knowledge (RAG) retrieval config. Omit the whole `retrieval` option to disable * retrieval (default = off). When present, the handler resolves the namespaces * THIS request may read (from the verified ctx), constructs a namespace-fenced * `Retriever`, and either exposes a `searchKnowledge` tool (`mode: 'tool'`, * default) or auto-retrieves + injects a delimited context block (`mode: 'auto'`). * * SECURITY: `resolveNamespaces` is the trusted hinge — it MUST derive namespaces * from server-verified values (agentId from your routing, tenantId from the * session, the verified userId), NEVER from the request body. The `Retriever` * has no namespace parameter, so a forged agentId in the body is irrelevant. */ interface RetrievalConfig { /** Read-only retriever factory (e.g. createKnowledgeDrizzleRetriever({ embedder })). */ store: RetrieverFactory; /** * Resolve which namespaces this request may read, from the verified ctx. * Return both the shared agent KB and the user's private namespace to support * "shared docs + my uploaded PDF" (e.g. `[agent:${id}, user:${ctx.userId}:${id}]`). */ resolveNamespaces: (ctx: ChatRequestContext) => Namespace[] | Promise; /** * Retrieval mode. Default 'tool' (the model calls `searchKnowledge`). 'auto' * retrieves on every turn and injects a delimited context block before * generation. Both emit `source-url` parts for citations. */ mode?: 'tool' | 'auto'; /** Max chunks per retrieval. Default 5; the store clamps to a ceiling (20). */ topK?: number; /** Drop chunks below this similarity. Default 0.2. */ minScore?: number; /** Hybrid weighting: 1 = pure vector, 0 = pure lexical. Default 1. */ vectorWeight?: number; /** Emit `source-url` parts so the existing sources UI renders citations. Default true. */ citations?: boolean; /** * Build the query string from the conversation (for 'auto' mode). Default: * the latest user message's text. Override for query rewriting/condensation. */ buildQuery?: (messages: ModelMessage[], ctx: ChatRequestContext) => string | Promise; /** Customise how chunks become the injected context block (delimiting lives here). */ renderContext?: (chunks: RetrievedChunk[]) => string; } /** * Long-term, per-user memory config. Omit the whole `memory` option to disable * memory (default = off). When present, the handler retrieves the bound user's * relevant memories BEFORE generation (injected as a non-authoritative system * block) and extracts new memories AFTER the turn settles (off the hot path, * fire-and-forget). Adds three user-control routes (GET/DELETE /memory[/:id]). * * Distinct from `maxHistoryMessages` (short-term, in-conversation) and from * knowledge/RAG (`retrieval`, developer-curated, shared). */ interface MemoryConfig { /** Per-request factory bound to the verified userId. */ adapter: MemoryAdapterFactory; /** Inject retrieved memories before generation. Default true. */ inject?: boolean; /** Run extraction after each turn. Default true. */ extract?: boolean; /** How many memories to inject per turn. Default 6 (the adapter also clamps). */ limit?: number; /** Drop retrieved memories below this score (when the backend scores). Default 0. */ minScore?: number; /** Budget (ms) for the hot-path retrieve before proceeding with no memories. Default 1500. */ retrieveTimeoutMs?: number; /** * Render retrieved memories into the system prompt. Default wraps them in a * fenced, non-authoritative block ("treat as background, not instructions"). */ formatForPrompt?: (memories: Memory[], ctx: ChatRequestContext) => string; /** * Per-turn consent gate. Return false to skip BOTH retrieve and record for * this turn (read the user's "memory off" pref from your DB, keyed on * ctx.userId). Default: always on. */ isEnabledForUser?: (ctx: ChatRequestContext) => boolean | Promise; /** * Which memory tiers to retrieve and inject each turn (#167). Default * ['user'] — the Phase-1 behaviour. Add 'session' for ephemeral, * conversation-scoped context and 'org' for shared tenant knowledge (the * latter requires `resolveOrgId`). Adapters that don't tier ignore this. */ scopes?: MemoryScope[]; /** * Which tier post-turn extraction writes to (#167). Default 'user'. Use * 'session' to remember only within the current conversation, or 'org' to * contribute to shared tenant memory (needs `resolveOrgId`). Orthogonal to * `extract` (set that `false` to disable extraction entirely). */ autoSaveScope?: MemoryScope; /** * Resolve the verified tenant/org id for the 'org' tier from the request * context (e.g. look up the user's org server-side). REQUIRED for 'org' — * org reads/writes are skipped when this is absent or returns null. Like * `getUserId`, it MUST derive from server-verified state, never the request * body, since it widens reads beyond the bound user. */ resolveOrgId?: (ctx: ChatRequestContext) => string | null | undefined | Promise; } /** * Server-authoritative suggested follow-ups appended to each completed assistant * message as a `data-follow-ups` UI part. Pass `true` for the built-in generator * (a small structured second call using the same resolved model), or an object * to tune/override it. Off by default. * * This is the only place follow-ups are generated or capped: the client renders * whatever it is served. Works for React and the script-tag embed through the * same response stream. */ interface ServerFollowUpConfig { /** Master switch. Default true when the object is provided. */ enabled?: boolean; /** Number of chips to emit, clamped to 1–5. Default 3. */ max?: number; /** Timeout for the post-response generator. Default 6000ms. */ timeoutMs?: number; /** * Optional custom server-side generator. Receives a text-only transcript plus * the verified request context. Return an empty array to emit no chips. * Omit to use the built-in structured model call. */ generate?: (messages: FollowUpMessage[], ctx: ChatRequestContext) => string[] | Promise; } /** * Smart thread titles: after a conversation's FIRST completed exchange, a small * structured second call (same resolved model) names the thread. The title is * persisted via `store.renameConversation` and appended to the assistant * message as a `data-thread-title` UI part so the open tab renames live. The * first-message-prefix placeholder remains until the generated title lands — * and on any generation failure — so this only ever improves on the previous * behaviour. */ interface ServerTitleConfig { /** Master switch. Default true when the object is provided. */ enabled?: boolean; /** Timeout for the post-response generator. Default 5000ms. */ timeoutMs?: number; } interface CreateChatHandlerOptions { /** * Derive the authenticated user's id from the SERVER session — a verified * cookie/JWT, Clerk `auth()`, NextAuth `getServerSession()`, * `supabase.auth.getUser()`, etc. Return `null` for an unauthenticated * request; the handler responds 401. * * SECURITY: never read the id from the request body, query string, or a * header the browser controls (e.g. `X-User-Id`). Those are forgeable and * doing so reintroduces the IDOR this whole design exists to prevent. The * handler passes you the `Request` so you can read *verified* cookies — not * so you can read a client-asserted id. */ getUserId: (request: Request) => Promise | string | null; /** * The model to stream from. Either a fixed `LanguageModel` or a function of * the request context (for per-user/per-org model selection). When omitted, * `runtime.model` from canonical hosted config is required. */ model?: LanguageModel | ((ctx: ChatRequestContext) => LanguageModel | Promise); /** * Max output tokens for the model. When omitted, the hosted config's value * (the model's real catalog limit, via /v1/config) is used; when neither is * set, the provider default applies. Set this to cap output (e.g. for cost * control) — a code value always wins (code > hosted > provider default). */ maxOutputTokens?: number; /** * Generate contextual follow-up chips after each completed assistant reply. * `true` uses a small structured second call with the same resolved model; * pass an object to tune the count/timeout or supply a custom server-side * generator. Suggestions are appended to the assistant message as a * `data-follow-ups` part after the main text finishes, so they never delay * first-token streaming and survive history reloads. * * Precedence: code > hosted config > off. Pass `false` to force-disable a * hosted dashboard setting. Default: off. */ followUps?: boolean | ServerFollowUpConfig; /** * Smart thread titles: name a conversation from its first exchange with a * small structured second call (same resolved model), replacing the * first-message-prefix placeholder. The title is persisted through the store * and pushed to the client as a `data-thread-title` part, so it never delays * first-token streaming. * * Precedence: code > hosted config > ON. Unlike `followUps`, this defaults to * enabled — pass `false` (in code or hosted config) to keep placeholder-only * titles. */ titles?: boolean | ServerTitleConfig; /** * Build the tool set for this request. Async and context-aware so tools can * close over the user and open per-request resources (e.g. an MCP client). * Return `{ tools, cleanup }`; the handler calls `cleanup` exactly once when * the request settles. Omit for a chat with no tools. */ buildTools?: (ctx: ChatRequestContext) => Promise | BuiltTools; /** * Managed-only sandboxes. Wire createHostedSandboxes(hostedOptions) from * /server/hosted for a custom/preview handler. createMordnHandler installs it * automatically; false explicitly opts out. Requires resolved runtime.sandbox * enablement AND the API's published/operator gates. No provider credentials. */ sandboxes?: false | ManagedSandboxIntegration; /** * Persistence backend. Omit to use the hosted/default store. Provide a * factory to bring your own DB. The factory is called per request with the * server-verified `userId`; the handler never constructs a store with a * client-supplied id. */ store?: ChatStoreFactory; /** * Attachment storage backend. Omit to use the hosted/default adapter. * Provide a factory for BYO storage (S3/R2/own bucket). Same per-request, * verified-userId construction as `store`. */ storage?: StorageAdapterFactory; /** * Fetch a published canonical `{ agent, revision, config }` record from a * hosted control plane (mordn's GET /v1/config). This is * how dashboard-managed config reaches the loop WITHOUT a redeploy. * * Precedence is always **code > hosted > package default**: any `model` / * `buildSystemPrompt` you pass here in code takes priority; the hosted value * only fills what code leaves unset. Returning `null` (or throwing) falls * through to code/defaults — a control-plane hiccup never breaks a turn. * * Use `createHostedConfig({ apiKey })` from `@mordn/chat-widget/server/hosted` * to get a ready-made fetcher. */ getHostedConfig?: (ctx: ChatRequestContext) => Promise | HostedAgentConfig | null; /** * Explicit server-side preview trust boundary. The handler first validates the * request's complete `AgentConfig` envelope, then calls this resolver. Return a * config to replace (not merge with) the published config for this request, or * null to ignore the request config. Omit in production. */ resolvePreviewConfig?: (config: AgentConfig, ctx: ChatRequestContext) => AgentConfig | null | Promise; /** * Resolve the stable, opaque browser-storage scope returned by `/bootstrap`. * The generic handler defaults to a deterministic SHA-256 projection of the * published agent and verified user. Secret-bearing façades should override * this with a keyed derivation; `createMordnHandler` does so with its apiKey. */ resolveStorageScope?: (ctx: ChatRequestContext, agent: string) => string | Promise; /** * Produce the system prompt for this request. Receives the context so it * can personalise (e.g. bake in the user's name). Defaults to a generic * assistant prompt. */ buildSystemPrompt?: (ctx: ChatRequestContext) => string | Promise; /** * Last-chance transform of the model-ready messages before they're sent to * the model — after the handler has applied its own sliding-window prune. * Use for provider-specific rewrites (e.g. image file-parts → image-parts) * or extra capping. Defaults to identity. */ transformMessages?: (messages: ModelMessage[], ctx: ChatRequestContext) => ModelMessage[] | Promise; /** * Called after the assistant turn has been persisted. For telemetry/usage * logging. NOT where you save messages — the handler already did that. Any * error thrown here is logged and swallowed; it never fails the response * (the user already has their answer). */ onChatFinish?: (info: { ctx: ChatRequestContext; messages: UIMessage[]; usage?: unknown; providerMetadata?: unknown; }) => void | Promise; /** * Persist a message-feedback submission (thumbs up/down on an assistant * message). The widget's feedback UI POSTs to `${apiBase}/v1/feedback`; the * handler validates the body, resolves the VERIFIED user through `getUserId`, * and calls this with the resulting {@link FeedbackEvent} * plus the request context. * * This is the single wiring point for feedback, mirroring `store` / memory * `adapter`: pass any function to record wherever you like (your DB, an * analytics pipe), or pass the ready-made hosted recorder * `createHostedFeedback({ apiKey, agentId })` from * `@mordn/chat-widget/server/hosted`, which forwards to chat-api * `POST /v1/feedback` with the same `Authorization: Bearer ` + * `X-Chat-User` plumbing the hosted store/memory clients use. * * Best-effort by contract: feedback is a side signal, so a throw/rejection * here is logged and swallowed — it never fails the response (the endpoint * still returns `{ ok: true }`). Omit it entirely and the feedback route * cleanly no-ops, so the widget's POST never errors. */ onFeedback?: (feedback: FeedbackEvent, ctx: ChatRequestContext) => void | Promise; /** * Map a stream error to the legacy SDK errorText string. Existing callback * arguments and return values are preserved. Only return safe public copy: * raw provider errors may contain credentials or prompt fragments. * * The widget now uses versioned data-chat-error metadata for guidance and * retry controls, independently of this string. Custom clients can still * display/localise your errorText; the built-in banner uses package copy. * * The second argument is the handler's own classification of the failure — * a discriminated `kind` (`rate_limit`, `auth`, `transient`, * `content_policy`, `prompt`, `model`, `tool`, `abort`, `unknown`) plus * `retryable`, a `retryAfterMs` hint parsed from the provider's rate-limit * headers, and the upstream `status`/`code`. Branch on that instead of * regexing provider prose: * * ```ts * onError: (error, { kind }) => { * if (kind === 'auth') { pageOncall(error); } * return ''; // fall through to the built-in copy * } * ``` * * Return an empty string to fall back to the category's built-in copy, which * is already category-specific — you only need this hook to localise, to * override tone, or to fire a side effect. * * Additive and backward compatible: an existing `(error) => string` handler * keeps working untouched. Throwing from this hook falls back to safe copy. * Retry hints do not authorize automatic replay: tools may already have * performed side effects. The built-in UI only offers manual retry. * * Note: providing this does NOT silence the server-side error log. Stream * errors are logged by default (see `logErrors`) so a production failure * (bad key, rate limit, wrong URL) never disappears into empty logs — the * #1 documented streaming pitfall. This hook only controls the user-facing * copy; use `logErrors: false` to opt out of the log. * * Not called for aborts: a user pressing Stop is not a failure. */ onError?: (error: unknown, classified: ClassifiedChatError) => string; /** * Inject first-class, per-turn context into the system prompt (#162). Called * after authentication with the request context and the widget's * (UNTRUSTED) client-supplied `context`. Return a plain JSON-serialisable * object to inject as authoritative background — fetch live server-side data * here (the user's plan, open tickets, the record they're viewing) so answers * are aware of the user's real state, not just generic Q&A. * * The merged object is folded into the system prompt as a structured JSON * block alongside retrieval/memory, and is never echoed back to the client. * `clientContext` is passed in only so you can validate/merge it — never trust * it blindly (the browser controls it). Return `null`/`undefined` to inject * nothing. Per-request, not per-session, so long-lived sessions never go stale. */ getContext?: (ctx: ChatRequestContext, clientContext: unknown) => Record | null | undefined | Promise | null | undefined>; /** * Inject the widget's client-supplied `context` prop directly when no * `getContext` is provided. OFF by default because the browser controls that * value (prompt-injection / data-spoofing risk). Enable only for * non-sensitive context you're comfortable treating as model input. When * `getContext` IS provided it is always authoritative and this flag is * ignored. * * Default: `false`. */ trustClientContext?: boolean; /** * Log stream errors to the server console by default. The AI SDK swallows * stream errors (to avoid crashing the server), which is exactly how broken * production deployments end up with silent, empty logs. We surface them by * default — independently of `onError`, which only maps the user-facing * message. Set `false` only if you forward errors elsewhere and want to * suppress the built-in console log. * * Note: this switches the BUILT-IN console logger on and off. If you pass an * explicit `logger`, that always wins — a host that wired up Datadog should * not have its telemetry silenced by a flag whose documented job is muting * our console noise. * * Default: `true`. */ logErrors?: boolean; /** * Route chat telemetry into your own logging stack. * * The handler mints a `traceId` per request and stamps it on every line for * that turn — `turn.start`, `turn.finish`, `turn.error`, `turn.abort`, * `save.failed`, `title.failed`, `memory.failed`, `retrieval.failed`, * `cleanup.failed`, `request.error` — so one grep reconstructs a whole * conversation turn instead of leaving you to correlate timestamps across * tenants. The same id is echoed to the caller on the `X-Mordn-Trace-Id` * response header, so a user can hand you one string from their network tab * and you can find the exact turn. * * ```ts * logger: { * log: (level, fields) => pino[level]({ ...fields }), * } * ``` * * The trace id ADOPTS an inbound `traceparent` (W3C Trace Context) or * `x-request-id` / `x-correlation-id` / `x-amzn-trace-id` when present, so * widget lines join your existing request logs rather than starting a * competing trace. * * Implementations MUST NOT throw and MUST NOT block — this sits on the hot * path of every turn. The handler wraps each call defensively, but buffer and * flush out of band rather than awaiting a network write per line. * * Defaults to a JSON-per-line console logger (or nothing, when * `logErrors: false`). This is deliberately NOT OpenTelemetry: pulling * `@opentelemetry/*` into a widget package would push a heavy, version- * fragile tree onto every consumer to serve the subset who want it. This is * the seam an OTel adapter plugs into — implement `ChatLogger`, open a span * per `traceId`. */ logger?: ChatLogger; /** * When the model may chain tool calls, how long to let it run before it * must answer. Defaults to a bounded step count so a misbehaving tool loop * can't run forever. Pass any AI SDK `StopCondition`. */ stopWhen?: StopCondition; /** Server-side upload policy (types + size). See `UploadPolicy`. */ upload?: UploadPolicy; /** * Opt-in CORS for cross-origin embeds — the script-tag embed (or any * widget) calling this handler from ANOTHER origin, e.g. the widget on * docs.example.com with `apiBase` pointing at app.example.com. Custom generic * transport headers may trigger a preflight; without this option the handler * never answers it and the browser blocks the cross-origin request. * * Off by default on purpose: same-origin apps need nothing, and reflecting * arbitrary origins unasked would be a security hole. `allowOrigins` are * exact origin matches (`scheme://host[:port]`), or the literal `'*'` for * any origin. `allowCredentials` additionally allows cookies — needed only * when `getUserId` reads a session cookie cross-origin (pair it with the * widget's `requestCredentials: 'include'`); with `'*'` + credentials the * concrete request origin is reflected, since the spec forbids a literal * `*` on credentialed responses. * * Remember to export the new method from your route file: * `export const { GET, POST, DELETE, OPTIONS } = createChatHandler({…})`. */ cors?: CorsPolicy; /** * How many of the most-recent messages to send to the model (sliding * window). Defaults to 30. The handler always prunes; this tunes the * window. Older messages stay in the store and in the UI — only the model * payload is windowed. */ maxHistoryMessages?: number; /** * Defensive per-message character cap applied during pruning so a single * giant pasted blob can't dominate the context window. Defaults to 4000. * Set to `0` to disable. */ maxMessageChars?: number; /** * Hard cap (bytes) on the raw chat request body the handler will buffer before * rejecting with 413. Enforced against the ACTUAL bytes read off the request * stream — NOT the `Content-Length` header (which a chunked request can omit), * so a client cannot force an unbounded allocation / `JSON.stringify`. Defaults * to 1 MB, which is generous for a windowed message array plus injected * context; raise it only if you legitimately POST larger bodies. */ maxRequestBytes?: number; /** * Optional overall wall-clock timeout (ms) for the streamed model response. A * hung or stalled upstream (bad gateway, wedged tool call) otherwise holds the * connection and server resources open until the platform kills it. When set, * the handler aborts the stream after this many ms. OFF by default, so long * but legitimate tool-using turns are never cut short — set it to a ceiling * comfortably above your slowest expected answer and at/below your platform's * function timeout. * * Note: this is now purely an ADDITIONAL ceiling. Client disconnects are * always propagated to the upstream model call so generation and billing stop * when the live SSE request ends. Work that must outlive its requester belongs * on a background job/queue surface rather than this request handler. */ streamTimeoutMs?: number; /** * Context compaction. When a conversation grows past `maxHistoryMessages`, the * sliding window DROPS the oldest messages from the model payload — by default * that early context (the user's original goal, decisions made early on) is * silently lost. Provide this to instead SUMMARIZE the dropped messages into a * compact block that's prepended to the system prompt as non-authoritative * background, so long conversations keep their thread. * * Receives the messages being dropped this turn (oldest-first) plus the request * context; returns a short plain-text summary (or '' to add nothing). Called * only when there's an overflow, so short chats pay nothing. Keep it cheap — a * small/fast model is ideal. Throwing or returning '' falls back to plain drop; * a summarizer hiccup must never break the turn. * * Omit to keep today's behavior (drop without summarizing). */ summarizeHistory?: (droppedMessages: ModelMessage[], ctx: ChatRequestContext) => Promise | string; /** * Knowledge / RAG retrieval. Omit to disable (default). Read-only by * construction: the handler is given a `RetrieverFactory`, never a write * store, so the chat path cannot mutate the KB. See `RetrievalConfig`. */ retrieval?: RetrievalConfig; /** * Long-term, per-user memory across conversations. Omit to disable (default). * Inject-before-generate + extract-after-settle, with a consent gate and a * hot-path timeout. Adds GET/DELETE /memory[/:id] routes. See `MemoryConfig`. */ memory?: MemoryConfig; } /** * Hosted ChatStore + StorageAdapter — thin HTTP clients over the @mordn/chat-api * service. Same interfaces as the Drizzle/Supabase defaults, so switching a * consumer from BYO to hosted is a one-line change: * * store: createHostedChatStore({ apiKey: process.env.MORDN_CHAT_KEY }) * storage: createHostedStorage({ apiKey: process.env.MORDN_CHAT_KEY }) * * Identity: the `apiKey` authenticates the TENANT (the customer/app). The * per-request `userId` the handler binds is sent as `X-Chat-User` — the end * user, derived from the consumer's verified session (same trust model as * getUserId). The hosted service enforces both axes; this client never decides * authorization, it only carries identity. */ interface HostedOptions { /** Tenant API key (mck_live_… / mck_test_…). Required. Never sent to the client. */ apiKey: string; /** * API base URL. Defaults to the hosted service; override for self-host/local. * Message history requires the API's per-response `created-at-id-v1` * acknowledgement. Deploy a compatible API first; older/capped APIs fail * closed instead of silently reporting inaccessible history as complete. */ baseUrl?: string; /** * Explicit trusted loopback origin for managed sandbox HTTP when the hosted * runtime calls itself (e.g. http://127.0.0.1:3000). Literal loopback only; * never derive it from a browser header/config. This does NOT relax SSRF for * arbitrary MCP servers. Other hosted clients continue to use baseUrl. */ selfBaseUrl?: string; /** Optional fetch override (testing). */ fetch?: typeof fetch; /** * Per-request timeout (ms) for calls to the hosted API. A hung/stalled * upstream otherwise holds the request (and its connection/compute) open until * the platform kills it. Defaults to 30s; set `0` to disable. */ timeoutMs?: number; } /** * Create a `ChatStoreFactory` backed by the hosted @mordn/chat-api service. * Pass to `createChatHandler({ store: createHostedChatStore({ apiKey }) })`. */ declare function createHostedChatStore(options: HostedOptions): (userId: string) => ChatStore; /** * Create a `StorageAdapterFactory` backed by the hosted service. */ declare function createHostedStorage(options: HostedOptions): (userId: string) => StorageAdapter; /** * Create a `getHostedConfig` fetcher backed by the hosted service's * `GET /v1/config`. Pass to * `createChatHandler({ getHostedConfig: createHostedConfig({ apiKey }) })`. * * The agent is resolved server-side from the apiKey, so this returns THIS key's * agent config. Results are cached in-process per user for a short TTL so it * isn't refetched every turn. Fails CLOSED: a failed request or malformed * published document throws, so the handler surfaces an explicit error instead * of silently running a default agent (canonical-config contract). */ declare function createHostedConfig(options: HostedOptions): (ctx: ChatRequestContext) => Promise; /** Options for the hosted feedback recorder. Extends the shared `HostedOptions` * with the same optional `agentId` the hosted memory client accepts, so the * feedback lands in the right agent namespace. Same config, no new shape. */ interface HostedFeedbackOptions extends HostedOptions { /** * Agent namespace, sent alongside the feedback so the hosted service scopes * it with the tenant + user (mirrors `createHostedMemory`). Optional: omit * when the apiKey already resolves a single agent server-side. */ agentId?: string; } /** * Create an `onFeedback` handler backed by the hosted @mordn/chat-api service. * Pass to `createChatHandler({ onFeedback: createHostedFeedback({ apiKey, agentId }) })`. * * This is the hosted DEFAULT for the feedback seam — the counterpart to * `createHostedChatStore` / `createHostedMemory`. It forwards the verified * feedback event to chat-api `POST /v1/feedback` using the EXACT plumbing the * hosted store/memory clients use: `Authorization: Bearer ` + * `X-Chat-User: `, base URL `https://api.mordn.com` (override * via `baseUrl`), JSON body. The `apiKey` authenticates the tenant; the userId * the handler resolved server-side rides in `X-Chat-User` — the browser never * holds the secret and never asserts the identity. * * Best-effort to match the handler's contract: any network/HTTP failure is * swallowed (the handler already guarantees feedback never 500s a turn), so a * telemetry hiccup can't break the widget. */ declare function createHostedFeedback(options: HostedFeedbackOptions): (feedback: FeedbackEvent, _ctx: ChatRequestContext) => Promise; type MordnAdvancedOptions = Omit; type CreateMordnHandlerOptions = HostedOptions & { getUserId: CreateChatHandlerOptions['getUserId']; } & MordnAdvancedOptions; /** * Standard hosted façade: one API key wires published config, persistence, * attachments, feedback, knowledge, memory, agent MCP tools and optional managed * sandboxes while model execution continues in this handler. * * Tools are MERGED, not either/or: developer `buildTools(ctx)` runs alongside * the hosted MCP connect, with developer tools winning on a name clash * (code > hosted). Both cleanups run, each isolated from the other. Enabled * managed sandboxes additionally reserve their six sandbox_* names; they cannot * be shadowed by a custom tool that has not verified an artifact's provenance. * * Browser storage scoping uses the handler's default resolver — an opaque * digest of the server-resolved agent + verified user — so rotating the API * key never changes end users' storage namespace. Advanced callers can still * pass `resolveStorageScope` explicitly. */ declare function createMordnHandler(options: CreateMordnHandlerOptions): { GET: (request: Request) => Promise; POST: (request: Request) => Promise; DELETE: (request: Request) => Promise; OPTIONS: (request: Request) => Promise; }; export { createTurnLogger as A, errorFields as B, type CreateMordnHandlerOptions as C, newTraceId as D, noopLogger as E, resolveTraceId as F, TRACE_HEADER as G, type HostedOptions as H, type HostedAgentConfig as I, type FeedbackEvent as J, type ServerTitleConfig as K, type LogLevel as L, type ManagedSandboxIntegration as M, type BuiltTools as N, type MemoryConfig as O, type RetrievalConfig as R, type ServerFollowUpConfig as S, type TurnLogger as T, type UploadPolicy as U, type MordnAdvancedOptions as a, createHostedChatStore as b, createMordnHandler as c, createHostedStorage as d, createHostedConfig as e, createHostedFeedback as f, type HostedFeedbackOptions as g, type ManagedSandboxStatus as h, type CreateChatHandlerOptions as i, type ChatRequestContext as j, type ChatErrorKind as k, type ClassifiedChatError as l, classifyError as m, isAbortError as n, messageForErrorKind as o, CHAT_ERROR_VERSION as p, CHAT_ERROR_DATA_TYPE as q, CHAT_ERROR_HEADER as r, parseChatErrorMetadata as s, parseChatErrorHeader as t, toChatErrorMetadata as u, type ChatErrorMetadata as v, type ChatLogger as w, type ChatLogEvent as x, type ChatLogFields as y, createConsoleLogger as z };