@happyvertical/smrt-chat 0.42.6 → 0.43.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/AGENTS.md +7 -0
- package/README.md +19 -0
- package/dist/chunks/data-surface-bridge-BJomMxjS.js +740 -0
- package/dist/chunks/data-surface-bridge-BJomMxjS.js.map +1 -0
- package/dist/data-surface-bridge.d.ts +186 -0
- package/dist/data-surface-bridge.d.ts.map +1 -0
- package/dist/data-surface-bridge.js +2 -0
- package/dist/data-surface-normalizer.d.ts +11 -0
- package/dist/data-surface-normalizer.d.ts.map +1 -0
- package/dist/data-surface-tools.d.ts +9 -0
- package/dist/data-surface-tools.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +106 -22
- package/dist/smrt-knowledge.json +7 -5
- package/package.json +19 -11
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/chat-feedback.ts","../src/tool-loop.ts","../src/persona-conversation.ts","../src/chat-stream.ts","../src/models/VoiceGatewayTurn.ts","../src/collections/VoiceGatewayTurnCollection.ts","../src/voice.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// During library builds, smrtPlugin replaces this entire module with generated\n// code that embeds the scanned manifest inline (#1506/#1507) — published dists\n// never resolve this URL, so downstream bundlers cannot break registration by\n// relocating the compiled module away from dist/manifest.json. The runtime\n// lookup below is the fallback for source-mode runs without that transform.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Chat feedback capture — turn an in-conversation judgement into a first-class\n * learning signal (L3 of the learning-agents epic, #1891).\n *\n * A tenant end-user accepting or rejecting an applied change, giving a\n * thumbs-up/down, or typing an inline correction produces a {@link Feedback}\n * row — carrying the conversation's **correlation-id** back to the turn it\n * judges — and (by default) immediately reinforces the persona's learning\n * memory. Because recall draws on that same memory next turn, captured feedback\n * *influences subsequent behaviour*: a rejected strategy decays below the reuse\n * floor and stops resurfacing; a correction supersedes it with the corrected\n * value.\n *\n * This is the human-signal half of the loop the personas package already models\n * ({@link reinforceFromFeedback}); the gated half — rewriting a persona's\n * instructions — stays in the directive-proposal flow.\n *\n * @module\n */\n\nimport type {\n LearningMemoryRecord,\n LearningSemanticSearch,\n SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n type Feedback,\n FeedbackCollection,\n type FeedbackSignalType,\n feedbackSourceFor,\n personaLearningMemory,\n personaMemoryScope,\n reinforceFromFeedback,\n} from '@happyvertical/smrt-personas';\nimport { getDatabase } from '@happyvertical/sql';\n\n/** The minimal persona shape chat feedback needs to route the signal. */\nexport interface ChatFeedbackPersona {\n /** Persona id — required (a signal always judges a specific persona). */\n id?: string | null;\n /** Owning tenant. */\n tenantId?: string | null;\n /** Canonical agent class the persona configures (denormalised onto the row). */\n agentClass?: string;\n /** Learning memory partition key. */\n memoryScope?: string;\n}\n\n/**\n * Options for {@link captureChatFeedback}.\n */\nexport interface CaptureChatFeedbackOptions {\n /** Database handle. */\n db: SmrtClassOptions['db'];\n /** The persona the signal judges. */\n persona: ChatFeedbackPersona;\n /** The kind of signal. */\n signalType: FeedbackSignalType;\n /** Correlation-id of the conversation turn this signal judges. */\n correlationId: string;\n /** What {@link correlationId} names. Default `'chat_message'`. */\n correlationType?: string;\n /** Learning episode scope the signal reinforces (matches recall/capture). */\n scope: string;\n /** Learning episode key the signal reinforces. */\n key: string;\n /** The user id that authored the signal (null for autonomous). */\n actorId?: string | null;\n /** Numeric rating for a `rating` signal. */\n rating?: number | null;\n /** Corrected value for a `correction` signal. */\n correction?: string | null;\n /** Freeform note. */\n comment?: string | null;\n /** Structured metadata persisted on the row. */\n metadata?: Record<string, unknown>;\n /** Apply the signal to memory immediately. Default `true`. */\n reinforce?: boolean;\n /** Optional embedding search wired into the reinforced memory. */\n semanticSearch?: LearningSemanticSearch;\n /** Neutral point of a `rating` scale (see `FeedbackOutcomeOptions`). Default 0. */\n ratingNeutral?: number;\n}\n\n/** The outcome of capturing chat feedback. */\nexport interface ChatFeedbackResult {\n /** The persisted feedback row. */\n feedback: Feedback;\n /** The memory record the signal reinforced, or `null` when it carried none. */\n reinforced: LearningMemoryRecord | null;\n}\n\n/**\n * Capture one in-chat feedback signal as a {@link Feedback} row and (by default)\n * reinforce the persona's learning memory from it.\n *\n * @throws when the persona has no id (a signal must name a persisted persona).\n */\nexport async function captureChatFeedback(\n options: CaptureChatFeedbackOptions,\n): Promise<ChatFeedbackResult> {\n if (!options.persona.id) {\n throw new Error(\n 'captureChatFeedback requires a persisted persona (missing id)',\n );\n }\n const memoryScope = personaMemoryScope(options.persona);\n\n const feedbacks = await FeedbackCollection.create({ db: options.db });\n const feedback = await feedbacks.create({\n tenantId: options.persona.tenantId ?? null,\n personaId: options.persona.id,\n agentClass: options.persona.agentClass ?? '',\n memoryScope,\n scope: options.scope,\n key: options.key,\n signalType: options.signalType,\n source: feedbackSourceFor(options.signalType),\n correlationId: options.correlationId,\n correlationType: options.correlationType ?? 'chat_message',\n rating: options.rating ?? null,\n correction: options.correction ?? null,\n comment: options.comment ?? null,\n actorId: options.actorId ?? null,\n });\n if (options.metadata) {\n feedback.setMetadata(options.metadata);\n }\n await feedback.save();\n\n let reinforced: LearningMemoryRecord | null = null;\n if (options.reinforce !== false) {\n // LearningMemory operates on a resolved DB handle; `getDatabase` accepts a\n // config or a handle and returns a handle (idempotent for a handle).\n const memory = personaLearningMemory({\n db: await getDatabase(options.db as Parameters<typeof getDatabase>[0]),\n persona: options.persona,\n semanticSearch: options.semanticSearch,\n });\n reinforced = await reinforceFromFeedback(memory, feedback, {\n ratingNeutral: options.ratingNeutral,\n });\n // Gate exactly-once reinforcement so a later reflection pass never\n // re-applies this signal (mirrors the personas reflection runner).\n feedback.reinforcedAt = new Date();\n await feedback.save();\n }\n\n return { feedback, reinforced };\n}\n\n/** Shared options for the signal-typed convenience wrappers. */\nexport type ChatFeedbackBase = Omit<\n CaptureChatFeedbackOptions,\n 'signalType' | 'rating' | 'correction'\n>;\n\n/**\n * Accept an applied change — reinforces the judged strategy as a success.\n */\nexport function acceptAppliedChange(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'accept' });\n}\n\n/**\n * Reject an applied change — decays the judged strategy toward the failure floor\n * so it stops being recalled.\n */\nexport function rejectAppliedChange(\n options: ChatFeedbackBase & { comment?: string | null },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'reject' });\n}\n\n/**\n * Record an inline correction — decays the wrong strategy AND supersedes its\n * stored value with the corrected one, so the next recall returns the fix.\n */\nexport function correctResponse(\n options: ChatFeedbackBase & { correction: string; comment?: string | null },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({\n ...options,\n signalType: 'correction',\n correction: options.correction,\n });\n}\n\n/**\n * Record a numeric rating for a response (scale is caller-defined; pass\n * `ratingNeutral` for a mid-point).\n */\nexport function rateResponse(\n options: ChatFeedbackBase & { rating: number },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({\n ...options,\n signalType: 'rating',\n rating: options.rating,\n });\n}\n\n/** Thumbs-up — a `+1` rating (reinforces as a success against neutral 0). */\nexport function thumbsUp(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'rating', rating: 1 });\n}\n\n/** Thumbs-down — a `-1` rating (decays as a failure against neutral 0). */\nexport function thumbsDown(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'rating', rating: -1 });\n}\n","/**\n * ToolLoop — a bounded `tool_call → observe → respond` agentic loop over the\n * SMRT manifest operation surface (L3 of the learning-agents epic, #1891).\n *\n * The universe of tools is **closed**: every tool is a manifest operation of an\n * installed SMRT package — an object's CRUD or *public custom action*, each a\n * `(collection, action)` in the manifest-derived permission catalog\n * ({@link PermissionCatalogService}). The loop invokes them **in-process\n * (\"side door\")** — no HTTP/MCP round-trip — inside the persona's\n * session-permission context ({@link executeAsPrincipal}), so tenant isolation\n * and per-operation authority (Postgres RLS, or the catalog assert when RLS is\n * off) apply through any door.\n *\n * Two independent gates make the loop fail-closed:\n *\n * 1. **Offer gate** — the available tools are exactly the manifest operations\n * filtered by the persona's `allowedTools`. A tool outside the allow-list is\n * never offered to the model, and a hallucinated tool name is rejected\n * without execution.\n * 2. **Execution gate** — every executed tool additionally re-asserts the\n * fail-closed allow-list ({@link PrincipalRun.assertToolAllowed}) and the\n * catalog permission for its `(collection, action)`\n * ({@link PrincipalRun.assertOperation}), so even a bug in the offer gate\n * cannot run an un-permitted operation.\n *\n * The loop is bounded by a max-steps ceiling: after `maxSteps` tool-executing\n * rounds it disables tools for one final completion, guaranteeing termination\n * with a text answer.\n *\n * Beyond manifest operations, the loop accepts a small set of **extra tools**\n * ({@link ToolLoopOptions.extraTools}) — non-CRUD `PrincipalTool`s such as the\n * agent-orchestration `invoke-agent` tool (#1892). Each is gated by the *same*\n * fail-closed allow-list (the caller only passes an allow-listed tool, and the\n * tool's `execute` re-asserts `assertToolAllowed`), so the closed-universe,\n * fail-closed property holds for them too.\n *\n * @module\n */\n\nimport type {\n AIInterface,\n AIMessage,\n AIResponse,\n AITool,\n ChatOptions,\n} from '@happyvertical/ai';\nimport {\n executeAsPrincipal,\n type PrincipalAuditSink,\n type PrincipalBinding,\n type PrincipalRun,\n type PrincipalTool,\n PrincipalToolNotAllowedError,\n} from '@happyvertical/smrt-agents';\nimport {\n ObjectRegistry,\n type SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n OperationPermissionError,\n PermissionCatalogService,\n type PermissionDefinition,\n} from '@happyvertical/smrt-users';\n\n/** Default ceiling on tool-executing rounds before the loop force-terminates. */\nexport const DEFAULT_MAX_STEPS = 8;\n\n/**\n * A transcript message that may carry an OpenAI-style `tool_call_id` on a tool\n * observation. A structural superset of {@link AIMessage}, so the working\n * transcript stays assignable to `AIMessage[]` for `ai.chat()`.\n */\ntype LoopMessage = AIMessage & { tool_call_id?: string };\n\n/**\n * A single manifest operation the loop can offer and execute. Its {@link slug}\n * is simultaneously the tool's stable name AND its permission-catalog slug — one\n * source of truth for both what the model may call and what the principal must\n * be permitted to do.\n */\nexport interface ManifestTool {\n /** Catalog slug (`collection.action`) — the tool name and the permission slug. */\n slug: string;\n /** Collection (permission resource), e.g. `articles`. */\n collection: string;\n /** Registry class name used to resolve the backing collection, e.g. `Article`. */\n className: string;\n /** Catalog action: `read` / `create` / `update` / `delete`, or a public custom method name. */\n action: string;\n /** Qualified class name, when known. */\n qualifiedName?: string;\n /** Human-readable description surfaced to the model. */\n description?: string;\n}\n\n/**\n * The record of one tool invocation attempt in a loop turn.\n */\nexport interface ToolInvocation {\n /** The tool name the model asked for. */\n slug: string;\n /** Parsed arguments (best-effort JSON parse of the model's raw arguments). */\n args: Record<string, unknown>;\n /** Whether the operation executed successfully. */\n ok: boolean;\n /** The JSON-serializable observation fed back to the model. */\n observation: unknown;\n /** True when the call was denied (not on the allow-list / not permitted). */\n rejected: boolean;\n /** Error summary when `ok` is false. */\n error?: string;\n}\n\n/** Why {@link runToolLoop} returned. */\nexport type ToolLoopStopReason = 'stop' | 'max_steps' | 'no_tools';\n\n/** The outcome of a {@link runToolLoop} turn. */\nexport interface ToolLoopResult {\n /** The model's final assistant text. */\n content: string;\n /** Number of tool-executing rounds completed. */\n steps: number;\n /** Why the loop stopped. */\n stoppedReason: ToolLoopStopReason;\n /** Every tool invocation attempted this turn, in order. */\n invocations: ToolInvocation[];\n /** The full working transcript (input messages + assistant/tool turns). */\n messages: AIMessage[];\n /** Total tokens reported by the AI boundary, when available. */\n totalTokens: number;\n}\n\n/** Context handed to a custom {@link ToolLoopOptions.executeTool} implementation. */\nexport interface ToolExecutionContext {\n /** The principal run whose context bounds this execution. */\n run: PrincipalRun;\n /** The manifest operation to execute. */\n tool: ManifestTool;\n /** Parsed tool arguments. */\n args: Record<string, unknown>;\n /** The database handle to operate against (already the RLS-bound tx when on). */\n db?: SmrtClassOptions['db'];\n}\n\n/**\n * Options for {@link runToolLoop}.\n */\nexport interface ToolLoopOptions {\n /** The AI boundary (the only thing mocked in tests). */\n ai: AIInterface;\n /** The initial conversation messages (system / history / user). */\n messages: AIMessage[];\n /** The manifest operations available this turn (already allow-list-filtered). */\n tools: ManifestTool[];\n /**\n * Non-manifest tools offered alongside the manifest operations — e.g. the\n * agent-orchestration `invoke-agent` tool (#1892). Each is gated by the same\n * fail-closed allow-list: only pass a tool whose `slug` is on the persona's\n * `allowedTools`, and its `execute` re-asserts the gate. Offered to the model\n * with its own `aiTool` definition and routed to its own handler.\n */\n extraTools?: PrincipalTool[];\n /** The persona principal every tool call runs as. */\n principal: PrincipalBinding;\n /** Database handle the side-door operations run against. */\n db?: SmrtClassOptions['db'];\n /** Max tool-executing rounds before force-termination. Default {@link DEFAULT_MAX_STEPS}. */\n maxSteps?: number;\n /** Model id passed to the AI boundary. */\n model?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Max tokens per completion. */\n maxTokens?: number;\n /** Tool-choice behaviour while tools are offered. Default `'auto'`. */\n toolChoice?: ChatOptions['toolChoice'];\n /**\n * Override the side-door executor. The default\n * ({@link invokeManifestTool}) enforces the allow-list + catalog gate and\n * dispatches through the ObjectRegistry. Tests inject a stub to exercise loop\n * mechanics without a backing object.\n */\n executeTool?: (ctx: ToolExecutionContext) => Promise<unknown>;\n /** Notified after each tool invocation (for streaming/telemetry). */\n onInvocation?: (invocation: ToolInvocation) => void | Promise<void>;\n /**\n * Token sink for live streaming (#1936). When set, each `ai.chat` round is run\n * with `stream: true` and the model's text deltas are forwarded here as they\n * arrive. It is best-effort: a provider that cannot stream (or streams no text\n * on a tool-call round) simply never calls it, and the fully-resolved response\n * is still returned. Deltas across ALL rounds are forwarded — a tool-call\n * round may narrate before calling a tool — so the emitted tokens are a live\n * PREVIEW; the loop's final `content` (persisted + surfaced by the caller as\n * the authoritative message) is the source of truth.\n */\n onToken?: (chunk: string) => void;\n /** The originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Canonical agent class, recorded in the audit entry. */\n agentClass?: string;\n /** Audit sink forwarded to {@link executeAsPrincipal}. */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/**\n * Best-effort parse of the model's raw tool-call arguments (a JSON string).\n * A non-object or malformed payload yields `{}` so a bad-argument call still\n * flows through the permission gate rather than throwing before it.\n */\nfunction parseToolArguments(raw: string | undefined): Record<string, unknown> {\n if (!raw) {\n return {};\n }\n try {\n return asRecord(JSON.parse(raw));\n } catch {\n return {};\n }\n}\n\n/**\n * Derive the catalog action for a permission definition — the slug segment(s)\n * after the `collection.` prefix. Catalog slugs are built as\n * `${collection}.${action}`, so this recovers `read` / `create` / a custom\n * method name unambiguously.\n */\nfunction actionFromDefinition(def: PermissionDefinition): string | null {\n const collection = def.collection;\n if (!collection || !def.slug.startsWith(`${collection}.`)) {\n return null;\n }\n const action = def.slug.slice(collection.length + 1);\n return action.length > 0 ? action : null;\n}\n\n/**\n * Build the closed catalog of manifest operations available as tools.\n *\n * Reads the manifest-derived {@link PermissionCatalog} and keeps only the\n * entries that name a dispatchable operation (a `(collection, action)` with a\n * resolvable backing class). Pass `allowedTools` to narrow the catalog to a\n * persona's least-privilege allow-list — this is the **offer gate**: a slug not\n * in `allowedTools` is never returned, so it is neither offered to the model nor\n * executed. A missing, `null`, or empty `allowedTools` yields **no tools**\n * (fail-closed) — the same whitelist semantics as `AgentSession`/\n * `PrincipalBinding` (S5 #1392), so forgetting the allow-list can only tighten,\n * never widen, the offered surface. Pass `all: true` to deliberately enumerate\n * the full manifest operation surface (e.g. an admin tool picker) — that is the\n * one explicit escape hatch, never the default.\n */\nexport function buildManifestToolCatalog(\n options: SmrtClassOptions & {\n /** Least-privilege allow-list to narrow the catalog by (fail-closed). */\n allowedTools?: string[] | null;\n /** Explicitly enumerate the ENTIRE manifest operation surface (no narrowing). */\n all?: boolean;\n /** Supply a pre-built catalog (skips the manifest walk). */\n catalog?: PermissionDefinition[];\n } = {},\n): ManifestTool[] {\n const definitions =\n options.catalog ??\n PermissionCatalogService.create(options).getCatalog().permissions;\n\n // Fail-closed: an absent / `null` / empty allow-list permits NOTHING. Only an\n // explicit `all: true` disables the narrowing and returns the full surface, so\n // a caller that forgets `allowedTools` gets zero tools rather than every one.\n const filter =\n options.all === true ? null : new Set(options.allowedTools ?? []);\n\n const tools: ManifestTool[] = [];\n for (const def of definitions) {\n if (!def.className || !def.collection) {\n continue;\n }\n if (filter && !filter.has(def.slug)) {\n continue;\n }\n const action = actionFromDefinition(def);\n if (!action) {\n continue;\n }\n tools.push({\n slug: def.slug,\n collection: def.collection,\n className: def.className,\n action,\n qualifiedName: def.qualifiedName,\n description: def.description,\n });\n }\n return tools;\n}\n\n/**\n * JSON-schema parameters for a manifest operation, keyed off its action. Create\n * / update schemas are enriched with the object's declared field names (tool arg\n * schemas come from field metadata) when the registry can supply them.\n */\nfunction toolParameters(tool: ManifestTool): Record<string, unknown> {\n const fieldProps = (): Record<string, unknown> => {\n const props: Record<string, unknown> = {};\n try {\n for (const [name] of ObjectRegistry.getFields(tool.className)) {\n if (typeof name === 'string') {\n props[name] = { type: 'string' };\n }\n }\n } catch {\n // No field metadata available — fall back to a free-form object.\n }\n return props;\n };\n\n switch (tool.action) {\n case 'read':\n return {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Fetch one row by id (omit to list).',\n },\n where: {\n type: 'object',\n description: 'Equality filters for a list.',\n },\n limit: { type: 'number' },\n offset: { type: 'number' },\n },\n };\n case 'create':\n return { type: 'object', properties: fieldProps() };\n case 'update':\n return {\n type: 'object',\n required: ['id'],\n properties: { id: { type: 'string' }, ...fieldProps() },\n };\n case 'delete':\n return {\n type: 'object',\n required: ['id'],\n properties: { id: { type: 'string' } },\n };\n default:\n return {\n type: 'object',\n required: ['id'],\n properties: {\n id: { type: 'string', description: 'Target row id for the action.' },\n },\n };\n }\n}\n\n/**\n * A provider-safe function name for a catalog slug.\n *\n * Catalog slugs are `collection.action` and routinely contain a `.`, but many\n * providers (OpenAI) restrict function names to `[A-Za-z0-9_-]{1,64}`. This maps\n * the slug into that charset (dots → `-`) for the wire; {@link runToolLoop} maps\n * the returned name back to the tool, and `tool.slug` remains the internal\n * permission id. Distinct slugs stay distinct (the only substituted char is the\n * single `.` separator).\n */\nexport function toolFunctionName(slug: string): string {\n return slug.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 64);\n}\n\n/**\n * Project a manifest operation into an AI function-tool definition. The function\n * name is the provider-safe rendering of the catalog slug\n * ({@link toolFunctionName}), so the model can only ever name a real operation.\n */\nexport function manifestToolToAITool(tool: ManifestTool): AITool {\n return {\n type: 'function',\n function: {\n name: toolFunctionName(tool.slug),\n description:\n tool.description ??\n `Manifest operation '${tool.action}' on '${tool.collection}'.`,\n parameters: toolParameters(tool),\n },\n };\n}\n\ninterface OperableItem {\n toJSON: () => Record<string, unknown>;\n save: () => Promise<unknown>;\n delete: () => Promise<unknown>;\n}\n\nfunction itemToObservation(item: unknown): unknown {\n const candidate = item as { toJSON?: () => unknown } | null;\n return typeof candidate?.toJSON === 'function' ? candidate.toJSON() : item;\n}\n\n/**\n * Execute a manifest operation in-process (\"side door\") under the principal.\n *\n * Enforces both authority dimensions before touching data: the fail-closed tool\n * allow-list ({@link PrincipalRun.assertToolAllowed}) and the catalog permission\n * for the `(collection, action)` ({@link PrincipalRun.assertOperation}) — the\n * door-agnostic teeth that hold on RLS-off adapters and are a redundant second\n * gate under Postgres RLS. Data operations run against the principal context's\n * database (the RLS-bound transaction when RLS is on), so tenant + per-operation\n * enforcement apply exactly as they would through REST or MCP.\n */\nexport async function invokeManifestTool(\n run: PrincipalRun,\n tool: ManifestTool,\n args: Record<string, unknown>,\n options: { db?: SmrtClassOptions['db'] } = {},\n): Promise<unknown> {\n // Gate 1: fail-closed allow-list (defense-in-depth behind the offer gate).\n run.assertToolAllowed(tool.slug);\n // Gate 2: door-agnostic catalog authority (the RLS-off teeth; redundant under RLS).\n await run.assertOperation(tool.collection, tool.action);\n\n // Operate against the principal context's database so RLS (when on) and tenant\n // auto-filtering bound the query; fall back to the supplied handle otherwise.\n const db = (run.context.database ?? options.db) as SmrtClassOptions['db'];\n const collection = await ObjectRegistry.getCollection(\n tool.className,\n db ? { db } : {},\n );\n\n switch (tool.action) {\n case 'read': {\n const id = typeof args.id === 'string' ? args.id : undefined;\n if (id) {\n const item = await collection.get(id);\n return item ? itemToObservation(item) : { found: false };\n }\n const items = await collection.list({\n where: asRecord(args.where),\n limit: typeof args.limit === 'number' ? args.limit : 50,\n offset: typeof args.offset === 'number' ? args.offset : 0,\n });\n return items.map(itemToObservation);\n }\n case 'create': {\n const item = (await collection.create(args)) as unknown as OperableItem;\n await item.save();\n return itemToObservation(item);\n }\n case 'update': {\n const { id, ...rest } = args;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(`'${tool.slug}' requires an 'id' to update.`);\n }\n const item = (await collection.get(id)) as unknown as OperableItem | null;\n if (!item) {\n return { found: false };\n }\n Object.assign(item, rest);\n await item.save();\n return itemToObservation(item);\n }\n case 'delete': {\n const id = typeof args.id === 'string' ? args.id : undefined;\n if (!id) {\n throw new Error(`'${tool.slug}' requires an 'id' to delete.`);\n }\n const item = (await collection.get(id)) as unknown as OperableItem | null;\n if (!item) {\n return { found: false };\n }\n await item.delete();\n return { success: true, id };\n }\n default: {\n // Public custom action: invoke the named method on the row.\n const { id, ...rest } = args;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(`'${tool.slug}' requires an 'id' for a custom action.`);\n }\n const item = await collection.get(id);\n if (!item) {\n return { found: false };\n }\n const method = (item as unknown as Record<string, unknown>)[tool.action];\n if (typeof method !== 'function') {\n throw new Error(\n `Method '${tool.action}' not found on '${tool.className}'.`,\n );\n }\n const result = await (\n method as (input: Record<string, unknown>) => Promise<unknown>\n ).call(item, rest);\n return result === undefined\n ? { success: true }\n : itemToObservation(result);\n }\n }\n}\n\n/**\n * Run a bounded `tool_call → observe → respond` loop over the manifest operation\n * surface, as the persona's bound principal.\n *\n * The whole turn runs inside a single {@link executeAsPrincipal} context, so\n * every tool call shares one published permission snapshot (matching what a\n * Postgres RLS session enforces) and the turn audits once as on-behalf-of the\n * originating user.\n *\n * @param options - The AI boundary, seed messages, allow-list-filtered tools,\n * principal, and ceiling.\n * @returns The final assistant text plus the invocation log and transcript.\n */\nexport async function runToolLoop(\n options: ToolLoopOptions,\n): Promise<ToolLoopResult> {\n const {\n ai,\n messages,\n tools,\n extraTools = [],\n principal,\n db,\n maxSteps = DEFAULT_MAX_STEPS,\n model,\n temperature,\n maxTokens,\n toolChoice = 'auto',\n executeTool,\n onInvocation,\n onToken,\n onBehalfOfUserId,\n agentClass,\n audit,\n postgresRls,\n } = options;\n\n const aiTools = [\n ...tools.map(manifestToolToAITool),\n ...extraTools.map((tool) => tool.aiTool),\n ];\n // Resolve the tool by EITHER the internal slug (a mock/pass-through provider)\n // OR the provider-safe function name the model actually receives, so the offer\n // gate holds regardless of how the provider renders the name.\n const offered = new Map<string, ManifestTool>();\n for (const tool of tools) {\n offered.set(tool.slug, tool);\n offered.set(toolFunctionName(tool.slug), tool);\n }\n // Extra (non-manifest) tools resolve by their slug OR the function name their\n // own `aiTool` definition advertises to the model.\n const offeredExtra = new Map<string, PrincipalTool>();\n for (const tool of extraTools) {\n offeredExtra.set(tool.slug, tool);\n offeredExtra.set(tool.aiTool.function.name, tool);\n }\n\n return executeAsPrincipal(\n {\n db,\n principal,\n onBehalfOfUserId,\n agentClass,\n action: 'chat.tool_loop',\n postgresRls,\n audit,\n },\n async (run): Promise<ToolLoopResult> => {\n // `LoopMessage` carries `tool_call_id` on tool observations (OpenAI's tool\n // message shape needs it to correlate an observation to its call); it is a\n // structural superset of `AIMessage`, so the transcript stays chat-compatible.\n const working: LoopMessage[] = [...messages];\n const invocations: ToolInvocation[] = [];\n let executedRounds = 0;\n let totalTokens = 0;\n let response: AIResponse;\n\n for (;;) {\n const offerTools = aiTools.length > 0 && executedRounds < maxSteps;\n response = await ai.chat(working, {\n model,\n temperature,\n maxTokens,\n tools: offerTools ? aiTools : undefined,\n toolChoice: offerTools ? toolChoice : 'none',\n // Live token streaming (#1936). Best-effort: providers that don't\n // stream ignore these and still resolve the full response below.\n ...(onToken ? { stream: true, onProgress: onToken } : {}),\n });\n totalTokens += response.usage?.totalTokens ?? 0;\n\n const toolCalls = offerTools ? (response.toolCalls ?? []) : [];\n if (toolCalls.length === 0) {\n return {\n content: response.content ?? '',\n steps: executedRounds,\n stoppedReason:\n aiTools.length === 0\n ? 'no_tools'\n : offerTools\n ? 'stop'\n : 'max_steps',\n invocations,\n messages: working,\n totalTokens,\n };\n }\n\n // Record the assistant's tool-call turn before appending observations.\n working.push({\n role: 'assistant',\n content: response.content ?? '',\n tool_calls: toolCalls,\n });\n\n for (const call of toolCalls) {\n const requestedName = call.function.name;\n const args = parseToolArguments(call.function.arguments);\n const tool = offered.get(requestedName);\n // An extra (non-manifest) tool only when no manifest tool matched.\n const extraTool = tool ? undefined : offeredExtra.get(requestedName);\n // Record the canonical slug (the permission id) for a resolved tool;\n // for a rejected/hallucinated call, echo whatever the model named.\n const slug = tool?.slug ?? extraTool?.slug ?? requestedName;\n\n let invocation: ToolInvocation;\n if (!tool && !extraTool) {\n // Offer gate: a tool the persona was not offered (not on the\n // allow-list, or hallucinated) is rejected without execution.\n invocation = {\n slug,\n args,\n ok: false,\n rejected: true,\n observation: {\n error: `Tool '${slug}' is not permitted for this persona.`,\n },\n error: 'not_permitted',\n };\n } else {\n try {\n const observation = await (tool\n ? executeTool\n ? executeTool({ run, tool, args, db })\n : invokeManifestTool(run, tool, args, { db })\n : // biome-ignore lint/style/noNonNullAssertion: extraTool is defined in this branch (tool is falsy).\n extraTool!.execute({ run, args, db }));\n invocation = {\n slug,\n args,\n ok: true,\n rejected: false,\n observation,\n };\n } catch (error) {\n const rejected =\n error instanceof PrincipalToolNotAllowedError ||\n error instanceof OperationPermissionError;\n invocation = {\n slug,\n args,\n ok: false,\n rejected,\n observation: {\n error: error instanceof Error ? error.message : String(error),\n },\n error: rejected ? 'not_permitted' : 'execution_error',\n };\n }\n }\n\n invocations.push(invocation);\n await onInvocation?.(invocation);\n working.push({\n role: 'tool',\n name: requestedName,\n // Correlate the observation to the exact call the model made — many\n // providers (OpenAI) require `tool_call_id` on a tool message and\n // mis-associate observations without it when several calls occur.\n tool_call_id: call.id,\n content: JSON.stringify(invocation.observation),\n });\n }\n\n executedRounds += 1;\n }\n },\n );\n}\n","/**\n * Persona-bound conversation — the bridge from an {@link AgentSession} (a\n * conversation) to an `AgentPersona`/`TenantAgent` (a tenant-scoped, principal-\n * bound behavioural profile) (L3 of the learning-agents epic, #1891).\n *\n * This is the new, acyclic `chat → personas` edge. A conversation bound this way\n * runs under the persona's **principal** (its `runAsUserId`, via\n * {@link runToolLoop} → `executeAsPrincipal`), offers only the persona's\n * **tools** (its `allowedTools`, narrowing the manifest operation surface),\n * speaks with the persona's **instructions**, and draws on its **recalled\n * learning memory** — so the assistant behaves like it knows the tenant's job.\n *\n * The persona's `allowedTools` is mirrored onto the `AgentSession` so the chat\n * layer's own fail-closed tool gate (S5 #1392) agrees with the loop's — one\n * allow-list, enforced at both the loop's side door and the message-authoring\n * seam.\n *\n * @module\n */\n\nimport type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type {\n PrincipalAuditSink,\n PrincipalBinding,\n PrincipalTool,\n} from '@happyvertical/smrt-agents';\nimport type {\n LearningMemoryRecord,\n LearningSemanticSearch,\n SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n personaLearningMemory,\n resolvePersonaInstructions,\n} from '@happyvertical/smrt-personas';\nimport { getDatabase } from '@happyvertical/sql';\nimport type { AgentSession } from './models/AgentSession.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport {\n buildManifestToolCatalog,\n type ManifestTool,\n runToolLoop,\n type ToolLoopResult,\n} from './tool-loop.js';\n\n/**\n * The structural persona shape the conversation binding needs. Both a\n * `ResolvedPersona` (from `PersonaResolver.resolve()`) and a raw `AgentPersona`\n * satisfy it via the adapters below.\n */\nexport interface ConversationPersona {\n /** Persona id — required to scope learning memory and prompt overrides. */\n id?: string | null;\n /** Owning tenant. */\n tenantId: string | null;\n /** Canonical agent class the persona configures. */\n agentClass?: string;\n /** The user whose live permissions bound the conversation. */\n runAsUserId: string;\n /** Optional acting `Bot` profile id (identity/audit). */\n actsAsProfileId?: string | null;\n /** The persona's tool allow-list (already capped by the class ceiling). */\n allowedTools: string[];\n /** Behavioural instructions / system prompt. */\n instructions?: string;\n /** Learning memory partition key. */\n memoryScope?: string;\n}\n\n/** Adapt a `PersonaResolver.resolve()` result into a {@link ConversationPersona}. */\nexport function conversationPersonaFromResolved(resolved: {\n personaId?: string;\n tenantId: string;\n agentClass: string;\n runAsUserId?: string;\n actsAsProfileId?: string | null;\n allowedTools: string[];\n instructions: string;\n memoryScope: string;\n}): ConversationPersona {\n return {\n id: resolved.personaId ?? null,\n tenantId: resolved.tenantId,\n agentClass: resolved.agentClass,\n runAsUserId: resolved.runAsUserId ?? '',\n actsAsProfileId: resolved.actsAsProfileId ?? null,\n allowedTools: resolved.allowedTools,\n instructions: resolved.instructions,\n memoryScope: resolved.memoryScope,\n };\n}\n\n/** Adapt a raw `AgentPersona` row into a {@link ConversationPersona}. */\nexport function conversationPersonaFromAgentPersona(persona: {\n id?: string | null;\n tenantId: string;\n agentClass: string;\n runAsUserId: string;\n actsAsProfileId?: string | null;\n instructions: string;\n memoryScope?: string;\n getAllowedTools: () => string[];\n}): ConversationPersona {\n return {\n id: persona.id ?? null,\n tenantId: persona.tenantId,\n agentClass: persona.agentClass,\n runAsUserId: persona.runAsUserId,\n actsAsProfileId: persona.actsAsProfileId ?? null,\n allowedTools: persona.getAllowedTools(),\n instructions: persona.instructions,\n memoryScope: persona.memoryScope,\n };\n}\n\n/**\n * Project a {@link ConversationPersona} into the {@link PrincipalBinding} the\n * tool loop runs as. The persona's `allowedTools` is the fail-closed whitelist\n * (absent/empty ⇒ no tools).\n */\nexport function principalBindingFor(\n persona: ConversationPersona,\n): PrincipalBinding {\n return {\n runAsUserId: persona.runAsUserId,\n tenantId: persona.tenantId,\n allowedTools: persona.allowedTools,\n actsAsProfileId: persona.actsAsProfileId ?? null,\n };\n}\n\n/** How to recall a persona's learning memory into the conversation context. */\nexport interface PersonaRecallOptions {\n /** Learning scope to recall (defaults to `'chat'`). */\n scope?: string;\n /** Exact episode key within the scope (omit for a scope-wide recall). */\n key?: string;\n /** Free-text query for the semantic arm (needs a `semanticSearch`). */\n query?: string;\n /** Max recalled records injected into context. Default 5. */\n limit?: number;\n /** Override the reuse floor for this recall. */\n minConfidence?: number;\n /** Optional embedding search for the semantic recall arm. */\n semanticSearch?: LearningSemanticSearch;\n}\n\n/**\n * Recall the persona's confidence-filtered learning memory.\n *\n * Isolated per persona by `memoryScope`, so what the \"Support\" persona learned\n * never bleeds into \"Sales\". Returns `[]` for a persona with no memory scope /\n * id (nothing to partition on).\n */\nexport async function recallPersonaMemory(\n db: SmrtClassOptions['db'],\n persona: ConversationPersona,\n options: PersonaRecallOptions = {},\n): Promise<LearningMemoryRecord[]> {\n if (!persona.memoryScope && !persona.id) {\n return [];\n }\n // LearningMemory operates on a resolved DB handle; `getDatabase` accepts a\n // config or a handle and returns a handle (idempotent for a handle).\n const memory = personaLearningMemory({\n db: await getDatabase(db as Parameters<typeof getDatabase>[0]),\n persona,\n semanticSearch: options.semanticSearch,\n });\n return memory.recall(options.scope ?? 'chat', {\n key: options.key,\n query: options.query,\n limit: options.limit ?? 5,\n minConfidence: options.minConfidence,\n });\n}\n\n/**\n * Format recalled memory into a system-context block. Empty string when there\n * is nothing to inject (so it can be unconditionally concatenated).\n */\nexport function formatRecalledMemory(records: LearningMemoryRecord[]): string {\n if (records.length === 0) {\n return '';\n }\n const lines = records.map((record) => {\n const value =\n typeof record.value === 'string'\n ? record.value\n : JSON.stringify(record.value);\n return `- [confidence ${record.confidence.toFixed(2)}] ${record.key}: ${value}`;\n });\n return `What you have learned about this organisation:\\n${lines.join('\\n')}`;\n}\n\n/**\n * Resolve the persona's effective instructions.\n *\n * Prefers the prompt-system resolution (`resolvePersonaInstructions`, which\n * layers any approved learned-directive override) when the persona is persisted;\n * falls back to the inline `persona.instructions`. This is how a conversation\n * \"uses its instructions (`applyPersonaInstructions`)\".\n */\nexport async function resolveConversationInstructions(\n db: SmrtClassOptions['db'],\n persona: ConversationPersona,\n): Promise<string> {\n if (persona.id) {\n try {\n const resolved = await resolvePersonaInstructions({\n persona: { id: persona.id, tenantId: persona.tenantId },\n db: db as Parameters<typeof resolvePersonaInstructions>[0]['db'],\n });\n if (resolved) {\n return resolved;\n }\n } catch {\n // Fall through to the inline instructions.\n }\n }\n return persona.instructions ?? '';\n}\n\n/** The minimal AgentSession surface the turn needs. */\ntype SessionLike = Pick<AgentSession, 'id' | 'chatRoomId' | 'systemPrompt'>;\n\n/** The minimal ChatService surface the turn needs to author the reply. */\nexport interface ConversationReplyService {\n initialize(): Promise<void>;\n}\n\n/**\n * Options for {@link runPersonaConversationTurn}.\n */\nexport interface PersonaConversationTurnOptions {\n /** The AI boundary. */\n ai: AIInterface;\n /** The database handle side-door operations run against. */\n db: SmrtClassOptions['db'];\n /** The persona the conversation is bound to. */\n persona: ConversationPersona;\n /** The user's message this turn. */\n userMessage: string;\n /** Tenant the turn runs within. */\n tenantId: string;\n /** Prior conversation turns (assistant/user), oldest first. */\n history?: AIMessage[];\n /**\n * The bound agent session. When provided together with `chatService`, the\n * agent reply is authored into the session's room and each executed tool is\n * recorded as a `tool_result` message (gated by the session allow-list).\n */\n session?: SessionLike | null;\n /** Chat service used to author the agent reply. */\n chatService?: ConversationReplyService | null;\n /** Thread to attach authored messages to. */\n threadId?: string | null;\n /** Recall configuration, or `false` to skip memory recall. */\n recall?: PersonaRecallOptions | false;\n /** Pre-built tool catalog (else derived from the persona's `allowedTools`). */\n tools?: ManifestTool[];\n /**\n * Non-manifest tools to offer this turn — e.g. the agent-orchestration\n * `invoke-agent` tool (#1892). Each is filtered by the persona's\n * `allowedTools` before being offered, so orchestration is gated exactly like\n * any other tool: a persona that does not allow-list `agents.invoke` never\n * sees it.\n */\n extraTools?: PrincipalTool[];\n /** Max tool-executing rounds. */\n maxSteps?: number;\n /** Model id. */\n model?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Max tokens per completion. */\n maxTokens?: number;\n /** Originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Audit sink for the on-behalf-of entry (forwarded to `executeAsPrincipal`). */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n /** Correlation id for the turn (feedback ties back to it). Auto-generated when omitted. */\n correlationId?: string;\n /**\n * Token sink for live streaming (#1936). Forwarded to {@link runToolLoop}: the\n * model's text deltas stream here as they arrive. Best-effort (see\n * `ToolLoopOptions.onToken`); the authored assistant message remains the\n * authoritative final content.\n */\n onToken?: (chunk: string) => void;\n}\n\n/** The outcome of a persona-bound conversation turn. */\nexport interface PersonaConversationTurnResult {\n /** The tool-loop result (final text, invocations, transcript). */\n result: ToolLoopResult;\n /** The correlation id feedback on this turn should reference. */\n correlationId: string;\n /** The memory recalled into the turn's context. */\n recalled: LearningMemoryRecord[];\n /** The system prompt assembled for the turn. */\n systemPrompt: string;\n /** Persisted messages authored by this turn when a chat service was supplied. */\n authoredMessages?: AuthoredConversationMessages;\n}\n\n/** Persisted assistant/tool messages emitted for a persona turn. */\nexport interface AuthoredConversationMessages {\n toolMessages: ChatMessage[];\n assistantMessage: ChatMessage | null;\n}\n\nfunction assembleSystemPrompt(\n instructions: string,\n memoryBlock: string,\n sessionPrompt: string | undefined,\n): string {\n // De-duplicate identical blocks: `bindPersonaToSession()` sets\n // `session.systemPrompt` to the persona instructions, so without this the\n // instruction block would appear twice (wasted tokens + confusion) once a\n // conversation runs on a bound session.\n const blocks = [sessionPrompt, instructions, memoryBlock]\n .map((part) => part?.trim())\n .filter((part): part is string => Boolean(part));\n return [...new Set(blocks)].join('\\n\\n');\n}\n\n/**\n * Run one turn of a persona-bound conversation.\n *\n * Binds the conversation to the persona: recalls its learning memory, resolves\n * its instructions, offers only its allow-listed manifest operations, and runs\n * the bounded tool loop as its principal. When a `chatService` + `session` are\n * given the assistant reply (and each executed tool) is authored into the room,\n * exercising the chat layer's own fail-closed tool gate.\n *\n * @returns The loop result, the turn's correlation id, and the recalled memory.\n */\nexport async function runPersonaConversationTurn(\n options: PersonaConversationTurnOptions,\n): Promise<PersonaConversationTurnResult> {\n const { ai, db, persona, userMessage, tenantId } = options;\n // A conversation must run as a concrete principal. A default/unbound persona\n // (e.g. a `PersonaResolver` default fallback) has no `runAsUserId`; fail fast\n // with a clear error rather than building an empty-string PrincipalBinding\n // that would silently resolve to zero permissions downstream.\n if (!persona.runAsUserId) {\n throw new Error(\n 'runPersonaConversationTurn requires a persona bound to a run-as user ' +\n '(runAsUserId); an unbound/default persona cannot operate the app.',\n );\n }\n const correlationId = options.correlationId ?? crypto.randomUUID();\n\n const recalled =\n options.recall === false\n ? []\n : await recallPersonaMemory(db, persona, options.recall ?? {});\n\n const instructions = await resolveConversationInstructions(db, persona);\n const memoryBlock = formatRecalledMemory(recalled);\n const systemPrompt = assembleSystemPrompt(\n instructions,\n memoryBlock,\n options.session?.systemPrompt,\n );\n\n const messages: AIMessage[] = [];\n if (systemPrompt) {\n messages.push({ role: 'system', content: systemPrompt });\n }\n if (options.history) {\n messages.push(...options.history);\n }\n messages.push({ role: 'user', content: userMessage });\n\n const tools =\n options.tools ??\n buildManifestToolCatalog({ db, allowedTools: persona.allowedTools });\n\n // Offer gate for non-manifest tools: only those the persona allow-lists (e.g.\n // `agents.invoke`) are offered, mirroring how the manifest catalog is\n // narrowed by `allowedTools`.\n const extraTools = (options.extraTools ?? []).filter((tool) =>\n persona.allowedTools.includes(tool.slug),\n );\n\n const result = await runToolLoop({\n ai,\n messages,\n tools,\n extraTools,\n principal: principalBindingFor(persona),\n db,\n maxSteps: options.maxSteps,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n onBehalfOfUserId: options.onBehalfOfUserId,\n agentClass: persona.agentClass,\n postgresRls: options.postgresRls,\n audit: options.audit,\n onToken: options.onToken,\n });\n\n let authoredMessages: AuthoredConversationMessages | undefined;\n if (options.chatService && options.session?.id) {\n authoredMessages = await authorConversationReply({\n chatService: options.chatService,\n session: options.session,\n tenantId,\n threadId: options.threadId ?? null,\n result,\n });\n }\n\n return { result, correlationId, recalled, systemPrompt, authoredMessages };\n}\n\n/**\n * Options for {@link bindPersonaToSession}.\n */\nexport interface BindPersonaToSessionOptions {\n /** Chat service exposing the owner-checked `updateAgentSessionConfig`. */\n chatService: {\n updateAgentSessionConfig(params: {\n agentSessionId: string;\n actorProfileId: string;\n tenantId: string | null;\n allowedTools?: string[];\n systemPrompt?: string;\n }): Promise<AgentSession>;\n };\n /** The session to bind. */\n session: Pick<AgentSession, 'id'>;\n /** The session owner (the update is owner-checked, S5 #1392). */\n actorProfileId: string;\n /** Tenant the session belongs to. */\n tenantId: string | null;\n /** The persona to bind the session to. */\n persona: ConversationPersona;\n /** Instructions to set as the session system prompt (else resolved). */\n instructions?: string;\n /** Database handle used to resolve instructions when not supplied. */\n db?: SmrtClassOptions['db'];\n}\n\n/**\n * Bind an {@link AgentSession} to a persona: mirror the persona's `allowedTools`\n * and instructions onto the session so the chat layer's own fail-closed tool\n * gate (S5 #1392) agrees with the loop's, and the session's system prompt speaks\n * the persona's voice. This is the durable side of the `chat → personas` bridge:\n * once bound, the session's authoring gate and the loop's offer gate share one\n * allow-list.\n */\nexport async function bindPersonaToSession(\n options: BindPersonaToSessionOptions,\n): Promise<AgentSession> {\n const instructions =\n options.instructions ??\n (options.db\n ? await resolveConversationInstructions(options.db, options.persona)\n : (options.persona.instructions ?? ''));\n return options.chatService.updateAgentSessionConfig({\n agentSessionId: options.session.id as string,\n actorProfileId: options.actorProfileId,\n tenantId: options.tenantId,\n allowedTools: options.persona.allowedTools,\n systemPrompt: instructions,\n });\n}\n\n/**\n * Author the agent's turn into the chat room: one `tool_result` message per\n * executed tool (gated fail-closed by the session allow-list), then the final\n * assistant text. Uses the trusted in-package agent-reply bridge, so messages\n * are authored AS the session's agent.\n */\nasync function authorConversationReply(input: {\n chatService: ConversationReplyService;\n session: SessionLike;\n tenantId: string;\n threadId: string | null;\n result: ToolLoopResult;\n}): Promise<AuthoredConversationMessages> {\n const { sendAgentReply } = await import('./services/ChatService.js');\n const toolMessages: ChatMessage[] = [];\n for (const invocation of input.result.invocations) {\n if (!invocation.ok) {\n continue;\n }\n const message = await sendAgentReply(input.chatService, {\n tenantId: input.tenantId,\n agentSessionId: input.session.id as string,\n threadId: input.threadId,\n content: JSON.stringify(invocation.observation),\n kind: 'tool',\n messageType: 'tool_result',\n toolCallData: { name: invocation.slug, args: invocation.args },\n });\n toolMessages.push(message);\n }\n const assistantMessage = await sendAgentReply(input.chatService, {\n tenantId: input.tenantId,\n agentSessionId: input.session.id as string,\n threadId: input.threadId,\n content: input.result.content,\n kind: 'assistant',\n });\n return { toolMessages, assistantMessage };\n}\n","/**\n * Token-streaming conversation endpoint (SSE) for embeddable chat clients\n * (issue #1936).\n *\n * The seam that joins the conversational harness to an embeddable widget: a\n * client (first consumer: the Happy chat widget, `happyvertical/animation#5`)\n * POSTs the conversation so far and receives a `text/event-stream` of\n * `data: <json>` frames — token deltas as the model generates them, then a\n * final `done` frame carrying the persisted message. A floating character can\n * stream the reply into a bubble and lip-sync each sentence as it completes,\n * instead of waiting for the whole JSON reply the way `POST /api/dev-chat` does.\n *\n * Two modes, dispatched by the resolved {@link ChatStreamContext}:\n * - **persona-bound** (`context.binding` present): runs the full\n * {@link runPersonaConversationTurn} — persona principal, allow-listed tools,\n * recalled memory — with a token sink wired through the tool loop, then\n * persists via `ChatService` and emits the persisted message as `done`.\n * - **plain / unbound** (`context.binding` absent): streams `ai.stream()`\n * directly and emits a synthesized (unpersisted) `done` message.\n *\n * Security posture (mirrors the voice gateway, #1910): this module NEVER\n * authorizes from the request's `session` metadata. `createChatStreamHandler`\n * takes an injected `authorize(request, body)` that the app implements to\n * validate the caller (bearer session id / cookie) and resolve an already-\n * authorized `ChatStreamContext`. The streaming engine enforces whatever that\n * returns; no authorizer ⇒ the handler cannot run. The persona path reuses the\n * harness's own fail-closed principal + allow-list gates unchanged.\n */\n\nimport type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type {\n PrincipalAuditSink,\n PrincipalTool,\n} from '@happyvertical/smrt-agents';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport type { AgentSession } from './models/AgentSession.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport {\n type ConversationPersona,\n type PersonaRecallOptions,\n runPersonaConversationTurn,\n} from './persona-conversation.js';\nimport type { ChatService } from './services/index.js';\nimport type { VoiceGatewayTurnMetadata } from './voice.js';\n\n/** Max conversation messages accepted on one streaming request. */\nexport const MAX_CHAT_STREAM_MESSAGES = 50;\n/** Max characters per message (matches the voice gateway text cap). */\nexport const MAX_CHAT_STREAM_CONTENT_LENGTH = 12_000;\n/**\n * Default SSE keep-alive interval (ms). A persona turn can go quiet for tens of\n * seconds during a silent tool-calling round (an LLM round-trip + an in-process\n * tool op emit no tokens), and idle intermediaries (nginx / ALB / Cloudflare —\n * the expected home for an embedded widget backend) drop a connection with no\n * traffic. A periodic SSE comment line keeps it warm, mirroring the core\n * `_events` route's `DEFAULT_EVENTS_HEARTBEAT_MS`.\n */\nexport const DEFAULT_CHAT_STREAM_HEARTBEAT_MS = 15_000;\n\n/** Roles carried on the wire (a subset of the internal `ChatMessageRole`). */\nexport type ChatStreamRole = 'user' | 'assistant' | 'system';\n\n/**\n * A conversation message on the wire — the shape the client sends in `messages`\n * and the shape the final `done` frame carries back. Kept self-contained (not\n * the internal `ChatMessage` model) so the contract is stable and JSON-only.\n */\nexport interface ChatStreamMessage {\n id?: string;\n role: ChatStreamRole;\n content: string;\n /** ISO-8601 timestamp. */\n createdAt?: string;\n}\n\n/**\n * Session metadata the client attaches to a turn — `VoiceGatewayTurnMetadata`-\n * shaped so voice and chat share one binding vocabulary. It is UNTRUSTED input:\n * the handler's `authorize` callback is responsible for validating any of these\n * ids against the authenticated principal before they reach a chat write or the\n * tool loop.\n */\nexport type ChatStreamSession = VoiceGatewayTurnMetadata;\n\n/**\n * A host-page control command (#1921, smrt-ui `control-interaction.ts`) carried\n * on the optional `control` lane. Kept structural here so the streaming\n * contract does not hard-couple to `@happyvertical/smrt-ui/forms`' exact union:\n * the client adapter (the Happy widget) executes it against its own control\n * registry, where sensitivity gating and the stage→apply consent split stay\n * enforced registry-side.\n */\nexport interface ChatStreamControlCommand {\n action: string;\n [key: string]: unknown;\n}\n\n/**\n * One frame of the stream. `token`/`done`/`error` are emitted today; `emotion`\n * and `control` are part of the wire contract (so clients can rely on the union\n * and a future server hook can emit them without a breaking change) but are not\n * produced by the v1 engine.\n */\nexport type ChatStreamEvent =\n | { type: 'token'; text: string }\n | { type: 'emotion'; name: string }\n | { type: 'control'; command: ChatStreamControlCommand }\n | { type: 'done'; message: ChatStreamMessage }\n | { type: 'error'; error: string };\n\n/** The JSON body of a streaming request. */\nexport interface ChatStreamRequestBody {\n messages?: unknown;\n session?: ChatStreamSession;\n}\n\n/** The minimal `AgentSession` surface the persona turn needs. */\ntype StreamSessionLike = Pick<\n AgentSession,\n 'id' | 'chatRoomId' | 'systemPrompt'\n>;\n\n/**\n * A resolved, ALREADY-AUTHORIZED persona binding. The `authorize` callback\n * produces this after validating the request against the authenticated\n * principal; nothing here is taken from untrusted request metadata.\n */\nexport interface ChatStreamPersonaBinding {\n chatService: ChatService;\n /** Database handle the persona turn's side-door operations run against. */\n db: SmrtClassOptions['db'];\n persona: ConversationPersona;\n session: StreamSessionLike;\n tenantId: string;\n /** Thread within the bound session room to author into. */\n threadId?: string | null;\n /** Originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Recall configuration, or `false` to skip memory recall. */\n recall?: PersonaRecallOptions | false;\n /**\n * Non-manifest custom tools to offer this streamed turn — e.g. the persona\n * messaging tool (`messages.send`) or an assistance-request/lead-ticket tool\n * backed by a `@smrt({ api:false, mcp:false })` service, which can only reach\n * the loop as `extraTools` (never as a generated manifest tool). Forwarded to\n * {@link runPersonaConversationTurn} exactly like the non-streaming persona\n * path, so a streamed persona chat can *act*, not just answer.\n *\n * This is resolved by the app's server-side `authorize` callback (trusted),\n * never taken from untrusted request input. Offering a tool is NOT authorizing\n * it: each entry is still filtered by the persona's `allowedTools` (the offer\n * gate) and its `execute` re-asserts the bound principal's RBAC + the\n * fail-closed `assertToolAllowed` (the execution gate), unchanged.\n */\n extraTools?: PrincipalTool[];\n /** Audit sink forwarded to the principal execution. */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n}\n\n/**\n * The trusted context a turn runs in. `binding` present ⇒ persona-bound; absent\n * ⇒ plain `ai.stream()`. Generation caps live here (server-resolved), never on\n * the request, so a caller cannot widen `maxTokens`/`maxSteps`.\n */\nexport interface ChatStreamContext {\n ai: AIInterface;\n binding?: ChatStreamPersonaBinding;\n /** System prompt for the PLAIN path. Ignored when `binding` is set. */\n systemPrompt?: string;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n maxSteps?: number;\n}\n\n/** Options for {@link runChatConversationStream}. */\nexport interface RunChatConversationStreamOptions {\n context: ChatStreamContext;\n /** The conversation so far; the last user message is this turn's prompt. */\n messages: ChatStreamMessage[];\n}\n\n/** Base error carrying an HTTP status + code for the handler to render. */\nexport class ChatStreamError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = 'ChatStreamError';\n this.status = status;\n this.code = code;\n }\n}\n\n/** 400 — malformed request body. */\nexport class ChatStreamBadRequestError extends ChatStreamError {\n constructor(message = 'Invalid chat stream request') {\n super(message, 400, 'chat_stream_bad_request');\n this.name = 'ChatStreamBadRequestError';\n }\n}\n\n/** 401 — the request could not be authorized. */\nexport class ChatStreamUnauthorizedError extends ChatStreamError {\n constructor(message = 'Unauthorized') {\n super(message, 401, 'chat_stream_unauthorized');\n this.name = 'ChatStreamUnauthorizedError';\n }\n}\n\n/**\n * Run one streaming conversation turn, yielding SSE events. Dispatches on\n * `context.binding`: persona-bound turns run the full harness; unbound turns\n * stream `ai.stream()`. Errors surface as an in-band `error` event (the HTTP\n * response has already committed to 200 once streaming starts), never a throw.\n */\nexport async function* runChatConversationStream(\n options: RunChatConversationStreamOptions,\n): AsyncGenerator<ChatStreamEvent> {\n const { context } = options;\n const messages = normalizeMessages(options.messages);\n const { history, userMessage } = splitConversation(messages);\n if (!userMessage) {\n yield { type: 'error', error: 'No user message to respond to' };\n return;\n }\n\n if (context.binding) {\n yield* streamPersonaConversation(\n context,\n context.binding,\n history,\n userMessage,\n );\n } else {\n yield* streamPlainConversation(context, history, userMessage);\n }\n}\n\n/**\n * Persona-bound turn. Bridges the tool loop's callback-based token stream\n * (`onToken`) into this async generator via a small producer/consumer queue,\n * then emits the persisted assistant message as `done`.\n */\nasync function* streamPersonaConversation(\n context: ChatStreamContext,\n binding: ChatStreamPersonaBinding,\n history: AIMessage[],\n userMessage: string,\n): AsyncGenerator<ChatStreamEvent> {\n // The producer (the turn's onToken) enqueues without awaiting the consumer, so\n // a slow client lets `queue` grow — but only up to ONE turn's emitted tokens,\n // which is bounded by the server-set `maxSteps`×`maxTokens` ceiling (the turn\n // runs to completion and then stops emitting). Tokens are intentionally NOT\n // coalesced: the widget lip-syncs each sentence as it streams, so per-delta\n // granularity is the contract. The browser-facing delivery is still\n // backpressured by the `ReadableStream` in `sseBody` (pull-based).\n const queue: ChatStreamEvent[] = [];\n let notify: (() => void) | null = null;\n let finished = false;\n\n // Wake a parked consumer (if any) exactly once.\n const wake = () => {\n const resume = notify;\n notify = null;\n resume?.();\n };\n const emit = (event: ChatStreamEvent) => {\n queue.push(event);\n wake();\n };\n\n const turnPromise = (async () => {\n try {\n const turn = await runPersonaConversationTurn({\n ai: context.ai,\n db: binding.db,\n persona: binding.persona,\n tenantId: binding.tenantId,\n userMessage,\n history,\n chatService: binding.chatService,\n session: binding.session,\n threadId: binding.threadId,\n recall: binding.recall,\n // Custom tools resolved server-side by `authorize`; still offer-gated by\n // the persona's `allowedTools` inside the turn (a tool being offered is\n // not the same as authorized).\n extraTools: binding.extraTools,\n model: context.model,\n temperature: context.temperature,\n maxTokens: context.maxTokens,\n maxSteps: context.maxSteps,\n onBehalfOfUserId: binding.onBehalfOfUserId,\n audit: binding.audit,\n postgresRls: binding.postgresRls,\n onToken: (chunk) => {\n if (chunk) emit({ type: 'token', text: chunk });\n },\n });\n emit({ type: 'done', message: resolvePersonaDoneMessage(turn) });\n } catch (error) {\n emit({ type: 'error', error: toErrorMessage(error) });\n } finally {\n finished = true;\n wake();\n }\n })();\n\n try {\n for (;;) {\n if (queue.length > 0) {\n yield queue.shift() as ChatStreamEvent;\n continue;\n }\n if (finished) break;\n await new Promise<void>((resolve) => {\n notify = resolve;\n });\n }\n } finally {\n // Never leave the turn dangling — it persists + reinforces memory even if\n // the client disconnected and stopped consuming.\n await turnPromise;\n }\n}\n\n/** Unbound turn: stream `ai.stream()` directly, then a synthesized `done`. */\nasync function* streamPlainConversation(\n context: ChatStreamContext,\n history: AIMessage[],\n userMessage: string,\n): AsyncGenerator<ChatStreamEvent> {\n const messages: AIMessage[] = [];\n if (context.systemPrompt) {\n messages.push({ role: 'system', content: context.systemPrompt });\n }\n messages.push(...history, { role: 'user', content: userMessage });\n\n let content = '';\n try {\n for await (const chunk of context.ai.stream(messages, {\n model: context.model,\n temperature: context.temperature,\n maxTokens: context.maxTokens,\n })) {\n if (chunk) {\n content += chunk;\n yield { type: 'token', text: chunk };\n }\n }\n } catch (error) {\n yield { type: 'error', error: toErrorMessage(error) };\n return;\n }\n yield { type: 'done', message: synthesizeAssistantMessage(content) };\n}\n\n/**\n * Options for {@link createChatStreamHandler}.\n */\nexport interface ChatStreamHandlerOptions {\n /**\n * Resolve an ALREADY-AUTHORIZED context from the request. This is the sole\n * trust boundary: validate the caller (bearer session id / cookie /\n * same-origin) and the claimed `body.session` ids against the authenticated\n * principal here, and return the context the turn runs in. Throw a\n * {@link ChatStreamError} (or any error ⇒ 500) to reject before any byte is\n * streamed.\n */\n authorize: (\n request: Request,\n body: ChatStreamRequestBody,\n ) => ChatStreamContext | Promise<ChatStreamContext>;\n /**\n * Cross-origin allowlist for the embedded widget (#1861 posture): the request\n * `Origin` is echoed only when a member (never `*`). Empty/omitted ⇒\n * same-origin only.\n */\n allowedOrigins?: string[];\n /** Emit `Access-Control-Allow-Credentials: true` for an allow-listed origin. */\n allowCredentials?: boolean;\n /**\n * SSE keep-alive interval (ms). Defaults to\n * {@link DEFAULT_CHAT_STREAM_HEARTBEAT_MS}. `0` disables the heartbeat.\n */\n heartbeatMs?: number;\n}\n\n/**\n * Build a Fetch-compatible SSE handler for the streaming contract (mirrors\n * `createVoiceGatewayTurnHandler`). Returns `text/event-stream`; wire it into a\n * SvelteKit `+server.ts`, a Bun/Node server, or any Fetch host.\n */\nexport function createChatStreamHandler(\n options: ChatStreamHandlerOptions,\n): (request: Request) => Promise<Response> {\n const allowedOrigins = normalizeAllowedOrigins(options.allowedOrigins);\n const allowCredentials = options.allowCredentials === true;\n const cors = (request: Request): Record<string, string> =>\n corsHeaders(request, allowedOrigins, allowCredentials);\n\n return async (request: Request): Promise<Response> => {\n if (request.method === 'OPTIONS') {\n const headers = cors(request);\n if (!('Access-Control-Allow-Origin' in headers)) {\n return new Response(null, { status: 403 });\n }\n return new Response(null, {\n status: 204,\n headers: {\n ...headers,\n 'Access-Control-Allow-Methods': 'POST,OPTIONS',\n 'Access-Control-Allow-Headers': 'Authorization,Content-Type',\n 'Access-Control-Max-Age': '86400',\n },\n });\n }\n\n if (request.method !== 'POST') {\n return jsonResponse(\n { error: 'Method not allowed', code: 'method_not_allowed' },\n 405,\n cors(request),\n );\n }\n\n let body: ChatStreamRequestBody;\n try {\n body = (await request.json()) as ChatStreamRequestBody;\n } catch {\n return jsonResponse(\n { error: 'Request body must be JSON', code: 'chat_stream_bad_request' },\n 400,\n cors(request),\n );\n }\n if (!body || typeof body !== 'object') {\n return jsonResponse(\n {\n error: 'Request body must be an object',\n code: 'chat_stream_bad_request',\n },\n 400,\n cors(request),\n );\n }\n\n let context: ChatStreamContext;\n try {\n context = await options.authorize(request, body);\n } catch (error) {\n const status = error instanceof ChatStreamError ? error.status : 500;\n const code =\n error instanceof ChatStreamError ? error.code : 'chat_stream_error';\n return jsonResponse(\n { error: toErrorMessage(error), code },\n status,\n cors(request),\n );\n }\n\n const messages = Array.isArray(body.messages)\n ? (body.messages as ChatStreamMessage[])\n : [];\n const events = runChatConversationStream({ context, messages });\n\n return new Response(sseBody(events, options.heartbeatMs), {\n status: 200,\n headers: {\n ...cors(request),\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n });\n };\n}\n\nconst encoder = new TextEncoder();\n\n/** SSE serialization of one event: a single `data:` frame. */\nexport function encodeChatStreamEvent(event: ChatStreamEvent): string {\n return `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\n/** SSE comment line — a keep-alive `EventSource` ignores natively. */\nconst HEARTBEAT_FRAME = encoder.encode(': heartbeat\\n\\n');\n\n/**\n * Adapt the event generator into a backpressure-aware `ReadableStream`. `pull`\n * advances one event at a time (so browser-facing delivery stays backpressured);\n * a `start()` heartbeat interval enqueues an SSE comment while `events.next()`\n * is pending, so a quiet tool-calling round doesn't let an idle intermediary\n * drop the connection mid-turn. `cancel` (client disconnect) clears the\n * heartbeat and returns the generator so its `finally` runs (the persona turn is\n * still awaited to completion inside it).\n */\nfunction sseBody(\n events: AsyncGenerator<ChatStreamEvent>,\n heartbeatMs = DEFAULT_CHAT_STREAM_HEARTBEAT_MS,\n): ReadableStream<Uint8Array> {\n let heartbeat: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n const stopHeartbeat = () => {\n if (heartbeat) {\n clearInterval(heartbeat);\n heartbeat = null;\n }\n };\n return new ReadableStream<Uint8Array>({\n start(controller) {\n if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) return;\n heartbeat = setInterval(() => {\n if (closed) return;\n try {\n controller.enqueue(HEARTBEAT_FRAME);\n } catch {\n // Controller already closed — stop pinging a dead stream.\n closed = true;\n stopHeartbeat();\n }\n }, heartbeatMs);\n // Don't keep the event loop alive solely for heartbeats.\n (heartbeat as { unref?: () => void }).unref?.();\n },\n async pull(controller) {\n try {\n const { value, done } = await events.next();\n if (done) {\n closed = true;\n stopHeartbeat();\n controller.close();\n return;\n }\n controller.enqueue(encoder.encode(encodeChatStreamEvent(value)));\n } catch (error) {\n closed = true;\n stopHeartbeat();\n controller.enqueue(\n encoder.encode(\n encodeChatStreamEvent({\n type: 'error',\n error: toErrorMessage(error),\n }),\n ),\n );\n controller.close();\n }\n },\n async cancel() {\n closed = true;\n stopHeartbeat();\n await events.return?.(undefined);\n },\n });\n}\n\n/** Trim + cap the incoming messages, dropping empty/invalid entries. */\nfunction normalizeMessages(messages: ChatStreamMessage[]): ChatStreamMessage[] {\n if (!Array.isArray(messages)) return [];\n const out: ChatStreamMessage[] = [];\n for (const message of messages.slice(-MAX_CHAT_STREAM_MESSAGES)) {\n const role = message?.role;\n if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;\n const content =\n typeof message.content === 'string' ? message.content.trim() : '';\n if (!content) continue;\n out.push({\n ...(typeof message.id === 'string' ? { id: message.id } : {}),\n role,\n content: content.slice(0, MAX_CHAT_STREAM_CONTENT_LENGTH),\n ...(typeof message.createdAt === 'string'\n ? { createdAt: message.createdAt }\n : {}),\n });\n }\n return out;\n}\n\n/**\n * Split the conversation into prior turns (`history`) and this turn's prompt\n * (`userMessage`, the last user message). Anything after the last user message\n * is dropped — the turn responds to the user's newest input.\n */\nfunction splitConversation(messages: ChatStreamMessage[]): {\n history: AIMessage[];\n userMessage: string;\n} {\n let lastUserIndex = -1;\n for (let i = messages.length - 1; i >= 0; i -= 1) {\n if (messages[i].role === 'user') {\n lastUserIndex = i;\n break;\n }\n }\n if (lastUserIndex === -1) return { history: [], userMessage: '' };\n const history = messages.slice(0, lastUserIndex).map(\n (message): AIMessage => ({\n role: message.role,\n content: message.content,\n }),\n );\n return { history, userMessage: messages[lastUserIndex].content };\n}\n\n/** Map a persona turn's persisted reply to the wire `done` message. */\nfunction resolvePersonaDoneMessage(turn: {\n result: { content: string };\n authoredMessages?: { assistantMessage: ChatMessage | null };\n}): ChatStreamMessage {\n const persisted = turn.authoredMessages?.assistantMessage;\n if (persisted) {\n return toStreamMessage(persisted);\n }\n return synthesizeAssistantMessage(turn.result.content);\n}\n\n/**\n * Coerce a persisted `ChatMessageRole` (which also includes `'tool'`) into the\n * narrower wire role. A `done` message is always the authored assistant reply,\n * so any non-conversational role collapses to `'assistant'` rather than leaking\n * an invalid value onto the contract.\n */\nfunction toWireRole(role: unknown): ChatStreamRole {\n return role === 'user' || role === 'system' ? role : 'assistant';\n}\n\n/** Project a persisted `ChatMessage` onto the stable wire shape. */\nfunction toStreamMessage(message: ChatMessage): ChatStreamMessage {\n const createdAt = (message as { createdAt?: unknown }).createdAt;\n return {\n id: (message.id as string | undefined) ?? crypto.randomUUID(),\n role: toWireRole(message.role),\n content: message.content ?? '',\n createdAt: toIsoString(createdAt),\n };\n}\n\n/** A `done` message for the unbound path, which persists nothing. */\nfunction synthesizeAssistantMessage(content: string): ChatStreamMessage {\n return {\n id: crypto.randomUUID(),\n role: 'assistant',\n content,\n createdAt: new Date().toISOString(),\n };\n}\n\nfunction toIsoString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string' && value) return value;\n return new Date().toISOString();\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction jsonResponse(\n body: unknown,\n status: number,\n extraHeaders: Record<string, string> = {},\n): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json', ...extraHeaders },\n });\n}\n\n/** Trim/de-dupe an origin allowlist; empty ⇒ same-origin only. */\nfunction normalizeAllowedOrigins(\n origins: string[] | undefined,\n): string[] | undefined {\n if (!Array.isArray(origins)) return undefined;\n const cleaned = [\n ...new Set(\n origins\n .filter((o): o is string => typeof o === 'string')\n .map((o) => o.trim())\n .filter((o) => o.length > 0),\n ),\n ];\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/**\n * CORS headers for an embedded cross-origin widget. The `Origin` is echoed only\n * when allow-listed (never `*`); credentials are added only when opted in — the\n * same fail-closed posture as the core `_events` route (#1861).\n */\nfunction corsHeaders(\n request: Request,\n allowedOrigins: string[] | undefined,\n allowCredentials: boolean,\n): Record<string, string> {\n if (!allowedOrigins) return {};\n const origin = request.headers.get('origin');\n if (!origin || !allowedOrigins.includes(origin)) return {};\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': origin,\n Vary: 'Origin',\n };\n if (allowCredentials) {\n headers['Access-Control-Allow-Credentials'] = 'true';\n }\n return headers;\n}\n","import { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n VoiceGatewayTurnOptions,\n VoiceGatewayTurnStatus,\n} from '../types.js';\n\n/**\n * Durable reservation for a gateway turn. The natural key makes `turn_id`\n * replay checks concurrency-safe before transcript messages are written.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'voice_gateway_turns',\n conflictColumns: ['voice_session_id', 'gateway_turn_id'],\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: false,\n})\nexport class VoiceGatewayTurn extends SmrtObject {\n @tenantId()\n tenantId: string = '';\n\n @foreignKey('VoiceSession', { required: true })\n voiceSessionId: string = '';\n\n @field({ required: true })\n gatewaySessionId: string = '';\n\n @field({ required: true })\n gatewayTurnId: string = '';\n\n @field({ required: true })\n target: string = 'smrt:chat';\n\n @field({ required: true })\n status: VoiceGatewayTurnStatus = 'processing';\n\n @field()\n completedAt: Date | null = null;\n\n @field()\n failedAt: Date | null = null;\n\n constructor(options: VoiceGatewayTurnOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.voiceSessionId !== undefined)\n this.voiceSessionId = options.voiceSessionId;\n if (options.gatewaySessionId !== undefined)\n this.gatewaySessionId = options.gatewaySessionId;\n if (options.gatewayTurnId !== undefined)\n this.gatewayTurnId = options.gatewayTurnId;\n if (options.target !== undefined) this.target = options.target;\n if (options.status !== undefined) this.status = options.status;\n if (options.completedAt !== undefined)\n this.completedAt = options.completedAt;\n if (options.failedAt !== undefined) this.failedAt = options.failedAt;\n }\n\n async complete(now: Date = new Date()): Promise<void> {\n this.status = 'completed';\n this.completedAt = now;\n this.failedAt = null;\n await this.save();\n }\n\n async fail(now: Date = new Date()): Promise<void> {\n this.status = 'failed';\n this.failedAt = now;\n await this.save();\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { VoiceGatewayTurn } from '../models/VoiceGatewayTurn.js';\n\nexport class VoiceGatewayTurnCollection extends SmrtCollection<VoiceGatewayTurn> {\n static readonly _itemClass = VoiceGatewayTurn;\n\n async reserveTurn(input: {\n tenantId: string;\n voiceSessionId: string;\n gatewaySessionId: string;\n gatewayTurnId: string;\n target: string;\n }): Promise<VoiceGatewayTurn> {\n return this.create({\n ...input,\n status: 'processing',\n _insertOnly: true,\n });\n }\n}\n","import type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type { PrincipalAuditSink } from '@happyvertical/smrt-agents';\nimport {\n type SmrtClassOptions,\n ValidationError,\n} from '@happyvertical/smrt-core';\nimport { VoiceGatewayTurnCollection } from './collections/VoiceGatewayTurnCollection.js';\nimport { VoiceSessionCollection } from './collections/VoiceSessionCollection.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport type { VoiceGatewayTurn } from './models/VoiceGatewayTurn.js';\nimport type { VoiceSession } from './models/VoiceSession.js';\nimport {\n bindPersonaToSession,\n type ConversationPersona,\n type PersonaRecallOptions,\n runPersonaConversationTurn,\n} from './persona-conversation.js';\nimport type { ChatService } from './services/index.js';\n\nexport const SMRT_CHAT_VOICE_TARGET = 'smrt:chat';\nexport const MAX_VOICE_GATEWAY_TEXT_LENGTH = 12_000;\nconst DEFAULT_VOICE_SESSION_TTL_SECONDS = 10 * 60;\nconst DEFAULT_HISTORY_LIMIT = 24;\n\nexport interface VoiceGatewayTurnMetadata {\n tenantId?: string;\n actorProfileId?: string;\n chatRoomId?: string;\n threadId?: string;\n agentSessionId?: string;\n personaId?: string;\n voiceSessionId?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface VoiceGatewayTurnPayload {\n session_id: string;\n turn_id: string;\n target: string;\n actor?: string;\n text: string;\n metadata?: VoiceGatewayTurnMetadata;\n}\n\nexport interface VoiceGatewayTurnResponseMetadata {\n tenantId: string;\n chatRoomId: string;\n threadId: string | null;\n agentSessionId: string;\n userMessageId: string;\n assistantMessageId: string | null;\n personaId: string;\n correlationId: string;\n voiceSessionId: string;\n source: string;\n}\n\nexport interface VoiceGatewayTurnResponse {\n session_id: string;\n turn_id: string;\n text: string;\n metadata: VoiceGatewayTurnResponseMetadata;\n}\n\nexport interface CreateVoiceChatSessionOptions {\n chatService: ChatService;\n db: SmrtClassOptions['db'];\n tenantId: string;\n actorProfileId: string;\n actorUserId?: string | null;\n persona: ConversationPersona;\n /** Existing agent session to bind voice to. Omit to create/reuse one. */\n agentSessionId?: string;\n /** Agent profile/session author id when creating a new agent session. */\n agentId?: string;\n /** Optional existing thread within the bound session room. */\n threadId?: string | null;\n /** Stable gateway-level `session_id`. Generated when omitted. */\n gatewaySessionId?: string;\n /** Subject key forwarded to ChatService.createAgentSession(). */\n sessionKey?: string | null;\n /** Voice binding TTL. Ignored when `expiresAt` is supplied. */\n ttlSeconds?: number;\n expiresAt?: Date;\n metadata?: Record<string, unknown>;\n target?: string;\n maxTokens?: number;\n maxMessages?: number;\n instructions?: string;\n}\n\nexport interface VoiceChatSessionCreationResult {\n voiceSession: VoiceSession;\n voiceSessionId: string;\n gatewaySessionId: string;\n expiresAt: Date;\n tenantId: string;\n actorProfileId: string;\n personaId: string;\n agentSessionId: string;\n chatRoomId: string;\n threadId: string | null;\n metadata: VoiceGatewayTurnMetadata;\n}\n\nexport interface HandleVoiceGatewayTurnOptions {\n chatService: ChatService;\n db: SmrtClassOptions['db'];\n ai: AIInterface;\n payload: VoiceGatewayTurnPayload;\n now?: Date;\n historyLimit?: number;\n recall?: PersonaRecallOptions | false;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n maxSteps?: number;\n postgresRls?: boolean;\n audit?: PrincipalAuditSink;\n}\n\nexport interface VoiceGatewayTurnHandlerOptions\n extends Omit<HandleVoiceGatewayTurnOptions, 'payload'> {\n gatewayToken: string | (() => string | Promise<string>);\n}\n\nexport class VoiceGatewayError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = 'VoiceGatewayError';\n this.status = status;\n this.code = code;\n }\n}\n\nexport class VoiceGatewayBadRequestError extends VoiceGatewayError {\n constructor(message: string) {\n super(message, 400, 'voice_gateway_bad_request');\n this.name = 'VoiceGatewayBadRequestError';\n }\n}\n\nexport class VoiceGatewayUnauthorizedError extends VoiceGatewayError {\n constructor(message = 'Voice gateway authorization failed') {\n super(message, 401, 'voice_gateway_unauthorized');\n this.name = 'VoiceGatewayUnauthorizedError';\n }\n}\n\nexport class VoiceSessionRejectedError extends VoiceGatewayError {\n constructor(message: string) {\n super(message, 403, 'voice_session_rejected');\n this.name = 'VoiceSessionRejectedError';\n }\n}\n\nexport class VoiceSessionExpiredError extends VoiceGatewayError {\n constructor(message = 'Voice session is expired') {\n super(message, 410, 'voice_session_expired');\n this.name = 'VoiceSessionExpiredError';\n }\n}\n\nexport class VoiceGatewayReplayError extends VoiceGatewayError {\n constructor(message = 'Voice gateway turn has already been processed') {\n super(message, 409, 'voice_gateway_replay');\n this.name = 'VoiceGatewayReplayError';\n }\n}\n\nexport async function createVoiceChatSession(\n options: CreateVoiceChatSessionOptions,\n): Promise<VoiceChatSessionCreationResult> {\n const target = options.target ?? SMRT_CHAT_VOICE_TARGET;\n const personaId = requireNonEmptyString(\n options.persona.id,\n 'createVoiceChatSession requires a persisted persona id',\n );\n if (\n options.persona.tenantId !== null &&\n options.persona.tenantId !== options.tenantId\n ) {\n throw new VoiceSessionRejectedError(\n 'Persona tenant does not match the voice session tenant',\n );\n }\n\n const persona: ConversationPersona = {\n ...options.persona,\n id: personaId,\n tenantId: options.tenantId,\n };\n\n let agentSession = options.agentSessionId\n ? await options.chatService.getAgentSession({\n agentSessionId: options.agentSessionId,\n tenantId: options.tenantId,\n })\n : null;\n\n if (options.agentSessionId) {\n if (!agentSession) {\n throw new VoiceSessionRejectedError('Agent session not found');\n }\n assertAgentSessionMatchesActor(agentSession, options.actorProfileId);\n assertActiveAgentSession(agentSession);\n } else {\n const agentId =\n options.agentId ??\n persona.actsAsProfileId ??\n persona.id ??\n persona.agentClass;\n if (!agentId) {\n throw new VoiceGatewayBadRequestError(\n 'createVoiceChatSession requires agentId when the persona has no acting profile or id',\n );\n }\n const created = await options.chatService.createAgentSession({\n tenantId: options.tenantId,\n agentId,\n actorProfileId: options.actorProfileId,\n allowedTools: persona.allowedTools,\n systemPrompt: options.instructions ?? persona.instructions,\n maxTokens: options.maxTokens,\n maxMessages: options.maxMessages,\n sessionKey: options.sessionKey,\n });\n agentSession = created.session;\n }\n\n const boundSession = await bindPersonaToSession({\n chatService: options.chatService,\n session: agentSession,\n actorProfileId: options.actorProfileId,\n tenantId: options.tenantId,\n persona,\n instructions: options.instructions,\n db: options.db,\n });\n assertActiveAgentSession(boundSession);\n\n const chatRoomId = requireNonEmptyString(\n boundSession.chatRoomId,\n 'Agent session has no chat room',\n );\n await options.chatService.getRoomForMember(\n chatRoomId,\n options.actorProfileId,\n options.tenantId,\n );\n\n const threadId = options.threadId ?? null;\n if (threadId) {\n const thread = await options.chatService.getThread({\n threadId,\n tenantId: options.tenantId,\n });\n if (!thread || thread.roomId !== chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'threadId does not belong to the bound agent session room',\n );\n }\n }\n\n const voiceSessions = await VoiceSessionCollection.create({ db: options.db });\n const gatewaySessionId = options.gatewaySessionId ?? crypto.randomUUID();\n const expiresAt =\n options.expiresAt ??\n new Date(\n Date.now() +\n (options.ttlSeconds ?? DEFAULT_VOICE_SESSION_TTL_SECONDS) * 1000,\n );\n\n const voiceSession = await voiceSessions.create({\n tenantId: options.tenantId,\n gatewaySessionId,\n actorProfileId: options.actorProfileId,\n actorUserId: options.actorUserId ?? null,\n personaId,\n agentSessionId: boundSession.id as string,\n chatRoomId,\n threadId,\n target,\n status: 'active',\n expiresAt,\n personaSnapshot: JSON.stringify(persona),\n metadata: JSON.stringify(options.metadata ?? {}),\n });\n\n const voiceSessionId = voiceSession.id as string;\n return {\n voiceSession,\n voiceSessionId,\n gatewaySessionId,\n expiresAt,\n tenantId: options.tenantId,\n actorProfileId: options.actorProfileId,\n personaId,\n agentSessionId: boundSession.id as string,\n chatRoomId,\n threadId,\n metadata: {\n tenantId: options.tenantId,\n actorProfileId: options.actorProfileId,\n chatRoomId,\n threadId: threadId ?? undefined,\n agentSessionId: boundSession.id as string,\n personaId,\n voiceSessionId,\n source: 'smrt-chat',\n },\n };\n}\n\nexport async function handleVoiceGatewayTurn(\n options: HandleVoiceGatewayTurnOptions,\n): Promise<VoiceGatewayTurnResponse> {\n const payload = normalizeGatewayPayload(options.payload);\n const metadata = payload.metadata ?? {};\n const voiceSessionId = requireNonEmptyString(\n metadata.voiceSessionId,\n 'Voice gateway payload metadata.voiceSessionId is required',\n );\n\n const voiceSessions = await VoiceSessionCollection.create({ db: options.db });\n const voiceSession = await voiceSessions.get({\n id: voiceSessionId,\n target: payload.target,\n });\n if (!voiceSession) {\n throw new VoiceSessionRejectedError('Voice session not found');\n }\n if (voiceSession.isExpired(options.now)) {\n if (voiceSession.status === 'active') {\n await voiceSession.expire();\n }\n throw new VoiceSessionExpiredError();\n }\n if (!voiceSession.isActive(options.now)) {\n throw new VoiceSessionRejectedError('Voice session is not active');\n }\n if (voiceSession.hasProcessedTurn(payload.turn_id)) {\n throw new VoiceGatewayReplayError();\n }\n\n validateGatewayPayloadAgainstBinding(payload, voiceSession);\n const persona = requireVoiceSessionPersona(voiceSession);\n\n const agentSession = await options.chatService.getAgentSession({\n agentSessionId: voiceSession.agentSessionId,\n tenantId: voiceSession.tenantId,\n });\n if (!agentSession) {\n throw new VoiceSessionRejectedError('Bound agent session not found');\n }\n assertAgentSessionMatchesActor(agentSession, voiceSession.actorProfileId);\n assertActiveAgentSession(agentSession);\n if (agentSession.chatRoomId !== voiceSession.chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'Bound agent session no longer belongs to the voice session room',\n );\n }\n\n if (voiceSession.threadId) {\n const thread = await options.chatService.getThread({\n threadId: voiceSession.threadId,\n tenantId: voiceSession.tenantId,\n });\n if (!thread || thread.roomId !== voiceSession.chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'Bound thread no longer belongs to the voice session room',\n );\n }\n }\n\n const gatewayTurn = await reserveVoiceGatewayTurn(\n options.db,\n voiceSession,\n payload,\n );\n\n try {\n const history = await loadConversationHistory({\n chatService: options.chatService,\n tenantId: voiceSession.tenantId,\n actorProfileId: voiceSession.actorProfileId,\n roomId: voiceSession.chatRoomId,\n threadId: voiceSession.threadId,\n limit: options.historyLimit ?? DEFAULT_HISTORY_LIMIT,\n });\n const correlationId = crypto.randomUUID();\n const commonMetadata = {\n source: 'voice-gateway',\n target: payload.target,\n voiceSessionId,\n gatewaySessionId: payload.session_id,\n gatewayTurnId: payload.turn_id,\n correlationId,\n };\n\n const userMessage = await options.chatService.sendAgentUserMessage({\n tenantId: voiceSession.tenantId,\n agentSessionId: voiceSession.agentSessionId,\n actorProfileId: voiceSession.actorProfileId,\n content: payload.text,\n });\n await mergeAndSaveMetadata(userMessage, {\n ...commonMetadata,\n voiceRole: 'transcript',\n });\n\n const turn = await runPersonaConversationTurn({\n ai: options.ai,\n db: options.db,\n persona,\n tenantId: voiceSession.tenantId,\n userMessage: payload.text,\n history,\n chatService: options.chatService,\n session: agentSession,\n threadId: voiceSession.threadId,\n recall: options.recall,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n maxSteps: options.maxSteps,\n postgresRls: options.postgresRls,\n audit: options.audit,\n onBehalfOfUserId: voiceSession.actorUserId ?? undefined,\n correlationId,\n });\n\n for (const toolMessage of turn.authoredMessages?.toolMessages ?? []) {\n await mergeAndSaveMetadata(toolMessage, {\n ...commonMetadata,\n voiceRole: 'tool',\n });\n }\n const assistantMessage = turn.authoredMessages?.assistantMessage ?? null;\n if (assistantMessage) {\n await mergeAndSaveMetadata(assistantMessage, {\n ...commonMetadata,\n voiceRole: 'assistant',\n });\n }\n\n voiceSession.recordGatewayTurn(payload.turn_id);\n await voiceSession.save();\n await gatewayTurn.complete();\n\n return {\n session_id: payload.session_id,\n turn_id: payload.turn_id,\n text: turn.result.content,\n metadata: {\n tenantId: voiceSession.tenantId,\n chatRoomId: voiceSession.chatRoomId,\n threadId: voiceSession.threadId,\n agentSessionId: voiceSession.agentSessionId,\n userMessageId: userMessage.id as string,\n assistantMessageId:\n (assistantMessage?.id as string | undefined) ?? null,\n personaId: voiceSession.personaId,\n correlationId: turn.correlationId,\n voiceSessionId,\n source: 'voice-gateway',\n },\n };\n } catch (error) {\n await markVoiceGatewayTurnFailed(gatewayTurn);\n throw error;\n }\n}\n\nexport function createVoiceGatewayTurnHandler(\n options: VoiceGatewayTurnHandlerOptions,\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n try {\n if (request.method !== 'POST') {\n return jsonResponse({ error: 'Method not allowed' }, 405);\n }\n await assertVoiceGatewayBearer(\n request.headers,\n await resolveGatewayToken(options.gatewayToken),\n );\n const payload = normalizeGatewayPayload(await request.json());\n const response = await handleVoiceGatewayTurn({ ...options, payload });\n return jsonResponse(response, 200);\n } catch (error) {\n const status = error instanceof VoiceGatewayError ? error.status : 500;\n const message =\n error instanceof Error ? error.message : 'Voice gateway turn failed';\n const code =\n error instanceof VoiceGatewayError ? error.code : 'voice_gateway_error';\n return jsonResponse({ error: message, code }, status);\n }\n };\n}\n\nexport async function assertVoiceGatewayBearer(\n headers: Headers,\n expectedToken: string,\n): Promise<void> {\n if (!expectedToken) {\n throw new VoiceGatewayUnauthorizedError(\n 'Voice gateway token is not configured',\n );\n }\n const authorization = headers.get('authorization') ?? '';\n const match = authorization.match(/^Bearer\\s+(.+)$/i);\n const actual = match?.[1] ?? '';\n if (!constantTimeEquals(actual, expectedToken)) {\n throw new VoiceGatewayUnauthorizedError();\n }\n}\n\nasync function resolveGatewayToken(\n token: string | (() => string | Promise<string>),\n): Promise<string> {\n return typeof token === 'function' ? token() : token;\n}\n\nfunction normalizeGatewayPayload(input: unknown): VoiceGatewayTurnPayload {\n if (!isRecord(input)) {\n throw new VoiceGatewayBadRequestError('Voice gateway payload must be JSON');\n }\n const sessionId = requireNonEmptyString(\n input.session_id,\n 'Voice gateway payload session_id is required',\n );\n const turnId = requireNonEmptyString(\n input.turn_id,\n 'Voice gateway payload turn_id is required',\n );\n const target = requireNonEmptyString(\n input.target,\n 'Voice gateway payload target is required',\n );\n if (target !== SMRT_CHAT_VOICE_TARGET) {\n throw new VoiceGatewayBadRequestError(\n `Unsupported voice gateway target '${target}'`,\n );\n }\n const text = requireNonEmptyString(\n input.text,\n 'Voice gateway payload text is required',\n );\n if (text.length > MAX_VOICE_GATEWAY_TEXT_LENGTH) {\n throw new VoiceGatewayBadRequestError(\n `Voice gateway payload text must be ${MAX_VOICE_GATEWAY_TEXT_LENGTH} characters or fewer`,\n );\n }\n const metadata = input.metadata;\n if (metadata !== undefined && !isRecord(metadata)) {\n throw new VoiceGatewayBadRequestError(\n 'Voice gateway payload metadata must be an object',\n );\n }\n\n return {\n session_id: sessionId,\n turn_id: turnId,\n target,\n actor:\n typeof input.actor === 'string' && input.actor.length > 0\n ? input.actor\n : undefined,\n text,\n metadata: metadata as VoiceGatewayTurnMetadata | undefined,\n };\n}\n\nfunction validateGatewayPayloadAgainstBinding(\n payload: VoiceGatewayTurnPayload,\n voiceSession: VoiceSession,\n): void {\n if (payload.session_id !== voiceSession.gatewaySessionId) {\n throw new VoiceSessionRejectedError(\n 'Gateway session_id does not match the voice session binding',\n );\n }\n const metadata = payload.metadata ?? {};\n assertMetadataMatches(\n metadata,\n 'tenantId',\n voiceSession.tenantId,\n 'tenantId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'actorProfileId',\n voiceSession.actorProfileId,\n 'actorProfileId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'chatRoomId',\n voiceSession.chatRoomId,\n 'chatRoomId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'threadId',\n voiceSession.threadId,\n 'threadId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'agentSessionId',\n voiceSession.agentSessionId,\n 'agentSessionId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'personaId',\n voiceSession.personaId,\n 'personaId does not match the voice session binding',\n );\n}\n\nfunction requireVoiceSessionPersona(\n voiceSession: VoiceSession,\n): ConversationPersona {\n const persona = voiceSession.getPersonaSnapshot();\n if (!hasNonEmptyString(persona.id)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has no persona id',\n );\n }\n if (persona.id !== voiceSession.personaId) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot does not match the voice session persona',\n );\n }\n if (persona.tenantId !== voiceSession.tenantId) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot tenant does not match the voice session tenant',\n );\n }\n if (!hasNonEmptyString(persona.runAsUserId)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has no runnable user',\n );\n }\n if (!Array.isArray(persona.allowedTools)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has an invalid tool allow-list',\n );\n }\n return persona;\n}\n\nasync function reserveVoiceGatewayTurn(\n db: SmrtClassOptions['db'],\n voiceSession: VoiceSession,\n payload: VoiceGatewayTurnPayload,\n): Promise<VoiceGatewayTurn> {\n const gatewayTurns = await VoiceGatewayTurnCollection.create({ db });\n try {\n return await gatewayTurns.reserveTurn({\n tenantId: voiceSession.tenantId,\n voiceSessionId: voiceSession.id as string,\n gatewaySessionId: payload.session_id,\n gatewayTurnId: payload.turn_id,\n target: payload.target,\n });\n } catch (error) {\n if (isUniqueConstraintError(error)) {\n throw new VoiceGatewayReplayError();\n }\n throw error;\n }\n}\n\nasync function markVoiceGatewayTurnFailed(\n gatewayTurn: VoiceGatewayTurn,\n): Promise<void> {\n try {\n await gatewayTurn.fail();\n } catch {\n // Preserve the original turn failure for the gateway response.\n }\n}\n\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof ValidationError &&\n error.code === 'VALIDATION_UNIQUE_CONSTRAINT'\n );\n}\n\nfunction assertMetadataMatches(\n metadata: VoiceGatewayTurnMetadata,\n key: keyof VoiceGatewayTurnMetadata,\n expected: string | null,\n message: string,\n): void {\n const actual = metadata[key];\n if (actual === undefined || actual === null || actual === '') {\n return;\n }\n if (typeof actual !== 'string' || actual !== expected) {\n throw new VoiceSessionRejectedError(message);\n }\n}\n\nfunction assertAgentSessionMatchesActor(\n session: { participantProfileId: string },\n actorProfileId: string,\n): void {\n if (session.participantProfileId !== actorProfileId) {\n throw new VoiceSessionRejectedError(\n 'Agent session participant does not match the voice session actor',\n );\n }\n}\n\nfunction assertActiveAgentSession(session: {\n isActive: () => boolean;\n chatRoomId: string | null;\n}): void {\n if (!session.isActive()) {\n throw new VoiceSessionRejectedError('Agent session is not active');\n }\n if (!session.chatRoomId) {\n throw new VoiceSessionRejectedError('Agent session has no chat room');\n }\n}\n\nasync function loadConversationHistory(input: {\n chatService: ChatService;\n tenantId: string;\n actorProfileId: string;\n roomId: string;\n threadId: string | null;\n limit: number;\n}): Promise<AIMessage[]> {\n const messages = input.threadId\n ? await input.chatService.getThreadMessages({\n threadId: input.threadId,\n actorProfileId: input.actorProfileId,\n tenantId: input.tenantId,\n limit: input.limit,\n })\n : (\n await input.chatService.getRoomMessages({\n roomId: input.roomId,\n actorProfileId: input.actorProfileId,\n tenantId: input.tenantId,\n limit: input.limit,\n })\n ).reverse();\n\n return messages\n .map(chatMessageToAIMessage)\n .filter((message): message is AIMessage => message !== null);\n}\n\nfunction chatMessageToAIMessage(message: ChatMessage): AIMessage | null {\n if (\n message.role !== 'user' &&\n message.role !== 'assistant' &&\n message.role !== 'system'\n ) {\n return null;\n }\n return { role: message.role, content: message.content } as AIMessage;\n}\n\nasync function mergeAndSaveMetadata(\n message: ChatMessage,\n metadata: Record<string, unknown>,\n): Promise<void> {\n message.setMetadata({ ...message.getMetadata(), ...metadata });\n await message.save();\n}\n\nfunction requireNonEmptyString(value: unknown, message: string): string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new VoiceGatewayBadRequestError(message);\n }\n return value;\n}\n\nfunction hasNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction constantTimeEquals(actual: string, expected: string): boolean {\n let diff = actual.length ^ expected.length;\n const length = Math.max(actual.length, expected.length);\n for (let index = 0; index < length; index++) {\n const actualCode = index < actual.length ? actual.charCodeAt(index) : 0;\n const expectedCode =\n index < expected.length ? expected.charCodeAt(index) : 0;\n diff |= actualCode ^ expectedCode;\n }\n return diff === 0;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/chat-feedback.ts","../src/tool-loop.ts","../src/persona-conversation.ts","../src/chat-stream.ts","../src/models/VoiceGatewayTurn.ts","../src/collections/VoiceGatewayTurnCollection.ts","../src/voice.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// During library builds, smrtPlugin replaces this entire module with generated\n// code that embeds the scanned manifest inline (#1506/#1507) — published dists\n// never resolve this URL, so downstream bundlers cannot break registration by\n// relocating the compiled module away from dist/manifest.json. The runtime\n// lookup below is the fallback for source-mode runs without that transform.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Chat feedback capture — turn an in-conversation judgement into a first-class\n * learning signal (L3 of the learning-agents epic, #1891).\n *\n * A tenant end-user accepting or rejecting an applied change, giving a\n * thumbs-up/down, or typing an inline correction produces a {@link Feedback}\n * row — carrying the conversation's **correlation-id** back to the turn it\n * judges — and (by default) immediately reinforces the persona's learning\n * memory. Because recall draws on that same memory next turn, captured feedback\n * *influences subsequent behaviour*: a rejected strategy decays below the reuse\n * floor and stops resurfacing; a correction supersedes it with the corrected\n * value.\n *\n * This is the human-signal half of the loop the personas package already models\n * ({@link reinforceFromFeedback}); the gated half — rewriting a persona's\n * instructions — stays in the directive-proposal flow.\n *\n * @module\n */\n\nimport type {\n LearningMemoryRecord,\n LearningSemanticSearch,\n SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n type Feedback,\n FeedbackCollection,\n type FeedbackSignalType,\n feedbackSourceFor,\n personaLearningMemory,\n personaMemoryScope,\n reinforceFromFeedback,\n} from '@happyvertical/smrt-personas';\nimport { getDatabase } from '@happyvertical/sql';\n\n/** The minimal persona shape chat feedback needs to route the signal. */\nexport interface ChatFeedbackPersona {\n /** Persona id — required (a signal always judges a specific persona). */\n id?: string | null;\n /** Owning tenant. */\n tenantId?: string | null;\n /** Canonical agent class the persona configures (denormalised onto the row). */\n agentClass?: string;\n /** Learning memory partition key. */\n memoryScope?: string;\n}\n\n/**\n * Options for {@link captureChatFeedback}.\n */\nexport interface CaptureChatFeedbackOptions {\n /** Database handle. */\n db: SmrtClassOptions['db'];\n /** The persona the signal judges. */\n persona: ChatFeedbackPersona;\n /** The kind of signal. */\n signalType: FeedbackSignalType;\n /** Correlation-id of the conversation turn this signal judges. */\n correlationId: string;\n /** What {@link correlationId} names. Default `'chat_message'`. */\n correlationType?: string;\n /** Learning episode scope the signal reinforces (matches recall/capture). */\n scope: string;\n /** Learning episode key the signal reinforces. */\n key: string;\n /** The user id that authored the signal (null for autonomous). */\n actorId?: string | null;\n /** Numeric rating for a `rating` signal. */\n rating?: number | null;\n /** Corrected value for a `correction` signal. */\n correction?: string | null;\n /** Freeform note. */\n comment?: string | null;\n /** Structured metadata persisted on the row. */\n metadata?: Record<string, unknown>;\n /** Apply the signal to memory immediately. Default `true`. */\n reinforce?: boolean;\n /** Optional embedding search wired into the reinforced memory. */\n semanticSearch?: LearningSemanticSearch;\n /** Neutral point of a `rating` scale (see `FeedbackOutcomeOptions`). Default 0. */\n ratingNeutral?: number;\n}\n\n/** The outcome of capturing chat feedback. */\nexport interface ChatFeedbackResult {\n /** The persisted feedback row. */\n feedback: Feedback;\n /** The memory record the signal reinforced, or `null` when it carried none. */\n reinforced: LearningMemoryRecord | null;\n}\n\n/**\n * Capture one in-chat feedback signal as a {@link Feedback} row and (by default)\n * reinforce the persona's learning memory from it.\n *\n * @throws when the persona has no id (a signal must name a persisted persona).\n */\nexport async function captureChatFeedback(\n options: CaptureChatFeedbackOptions,\n): Promise<ChatFeedbackResult> {\n if (!options.persona.id) {\n throw new Error(\n 'captureChatFeedback requires a persisted persona (missing id)',\n );\n }\n const memoryScope = personaMemoryScope(options.persona);\n\n const feedbacks = await FeedbackCollection.create({ db: options.db });\n const feedback = await feedbacks.create({\n tenantId: options.persona.tenantId ?? null,\n personaId: options.persona.id,\n agentClass: options.persona.agentClass ?? '',\n memoryScope,\n scope: options.scope,\n key: options.key,\n signalType: options.signalType,\n source: feedbackSourceFor(options.signalType),\n correlationId: options.correlationId,\n correlationType: options.correlationType ?? 'chat_message',\n rating: options.rating ?? null,\n correction: options.correction ?? null,\n comment: options.comment ?? null,\n actorId: options.actorId ?? null,\n });\n if (options.metadata) {\n feedback.setMetadata(options.metadata);\n }\n await feedback.save();\n\n let reinforced: LearningMemoryRecord | null = null;\n if (options.reinforce !== false) {\n // LearningMemory operates on a resolved DB handle; `getDatabase` accepts a\n // config or a handle and returns a handle (idempotent for a handle).\n const memory = personaLearningMemory({\n db: await getDatabase(options.db as Parameters<typeof getDatabase>[0]),\n persona: options.persona,\n semanticSearch: options.semanticSearch,\n });\n reinforced = await reinforceFromFeedback(memory, feedback, {\n ratingNeutral: options.ratingNeutral,\n });\n // Gate exactly-once reinforcement so a later reflection pass never\n // re-applies this signal (mirrors the personas reflection runner).\n feedback.reinforcedAt = new Date();\n await feedback.save();\n }\n\n return { feedback, reinforced };\n}\n\n/** Shared options for the signal-typed convenience wrappers. */\nexport type ChatFeedbackBase = Omit<\n CaptureChatFeedbackOptions,\n 'signalType' | 'rating' | 'correction'\n>;\n\n/**\n * Accept an applied change — reinforces the judged strategy as a success.\n */\nexport function acceptAppliedChange(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'accept' });\n}\n\n/**\n * Reject an applied change — decays the judged strategy toward the failure floor\n * so it stops being recalled.\n */\nexport function rejectAppliedChange(\n options: ChatFeedbackBase & { comment?: string | null },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'reject' });\n}\n\n/**\n * Record an inline correction — decays the wrong strategy AND supersedes its\n * stored value with the corrected one, so the next recall returns the fix.\n */\nexport function correctResponse(\n options: ChatFeedbackBase & { correction: string; comment?: string | null },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({\n ...options,\n signalType: 'correction',\n correction: options.correction,\n });\n}\n\n/**\n * Record a numeric rating for a response (scale is caller-defined; pass\n * `ratingNeutral` for a mid-point).\n */\nexport function rateResponse(\n options: ChatFeedbackBase & { rating: number },\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({\n ...options,\n signalType: 'rating',\n rating: options.rating,\n });\n}\n\n/** Thumbs-up — a `+1` rating (reinforces as a success against neutral 0). */\nexport function thumbsUp(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'rating', rating: 1 });\n}\n\n/** Thumbs-down — a `-1` rating (decays as a failure against neutral 0). */\nexport function thumbsDown(\n options: ChatFeedbackBase,\n): Promise<ChatFeedbackResult> {\n return captureChatFeedback({ ...options, signalType: 'rating', rating: -1 });\n}\n","/**\n * ToolLoop — a bounded `tool_call → observe → respond` agentic loop over the\n * SMRT manifest operation surface (L3 of the learning-agents epic, #1891).\n *\n * The universe of tools is **closed**: every tool is a manifest operation of an\n * installed SMRT package — an object's CRUD or *public custom action*, each a\n * `(collection, action)` in the manifest-derived permission catalog\n * ({@link PermissionCatalogService}). The loop invokes them **in-process\n * (\"side door\")** — no HTTP/MCP round-trip — inside the persona's\n * session-permission context ({@link executeAsPrincipal}), so tenant isolation\n * and per-operation authority (Postgres RLS, or the catalog assert when RLS is\n * off) apply through any door.\n *\n * Two independent gates make the loop fail-closed:\n *\n * 1. **Offer gate** — the available tools are exactly the manifest operations\n * filtered by the persona's `allowedTools`. A tool outside the allow-list is\n * never offered to the model, and a hallucinated tool name is rejected\n * without execution.\n * 2. **Execution gate** — every executed tool additionally re-asserts the\n * fail-closed allow-list ({@link PrincipalRun.assertToolAllowed}) and the\n * catalog permission for its `(collection, action)`\n * ({@link PrincipalRun.assertOperation}), so even a bug in the offer gate\n * cannot run an un-permitted operation.\n *\n * The loop is bounded by a max-steps ceiling: after `maxSteps` tool-executing\n * rounds it disables tools for one final completion, guaranteeing termination\n * with a text answer.\n *\n * Beyond manifest operations, the loop accepts a small set of **extra tools**\n * ({@link ToolLoopOptions.extraTools}) — non-CRUD `PrincipalTool`s such as the\n * agent-orchestration `invoke-agent` tool (#1892). Each is gated by the *same*\n * fail-closed allow-list (the caller only passes an allow-listed tool, and the\n * tool's `execute` re-asserts `assertToolAllowed`), so the closed-universe,\n * fail-closed property holds for them too.\n *\n * @module\n */\n\nimport type {\n AIInterface,\n AIMessage,\n AIResponse,\n AITool,\n ChatOptions,\n} from '@happyvertical/ai';\nimport {\n executeAsPrincipal,\n type PrincipalAuditSink,\n type PrincipalBinding,\n type PrincipalRun,\n type PrincipalTool,\n PrincipalToolNotAllowedError,\n} from '@happyvertical/smrt-agents';\nimport {\n ObjectRegistry,\n type SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n OperationPermissionError,\n PermissionCatalogService,\n type PermissionDefinition,\n} from '@happyvertical/smrt-users';\n\n/** Default ceiling on tool-executing rounds before the loop force-terminates. */\nexport const DEFAULT_MAX_STEPS = 8;\n\n/**\n * A transcript message that may carry an OpenAI-style `tool_call_id` on a tool\n * observation. A structural superset of {@link AIMessage}, so the working\n * transcript stays assignable to `AIMessage[]` for `ai.chat()`.\n */\ntype LoopMessage = AIMessage & { tool_call_id?: string };\n\n/**\n * A single manifest operation the loop can offer and execute. Its {@link slug}\n * is simultaneously the tool's stable name AND its permission-catalog slug — one\n * source of truth for both what the model may call and what the principal must\n * be permitted to do.\n */\nexport interface ManifestTool {\n /** Catalog slug (`collection.action`) — the tool name and the permission slug. */\n slug: string;\n /** Collection (permission resource), e.g. `articles`. */\n collection: string;\n /** Registry class name used to resolve the backing collection, e.g. `Article`. */\n className: string;\n /** Catalog action: `read` / `create` / `update` / `delete`, or a public custom method name. */\n action: string;\n /** Qualified class name, when known. */\n qualifiedName?: string;\n /** Human-readable description surfaced to the model. */\n description?: string;\n}\n\n/**\n * The record of one tool invocation attempt in a loop turn.\n */\nexport interface ToolInvocation {\n /** The tool name the model asked for. */\n slug: string;\n /** Parsed arguments (best-effort JSON parse of the model's raw arguments). */\n args: Record<string, unknown>;\n /** Whether the operation executed successfully. */\n ok: boolean;\n /** The JSON-serializable observation fed back to the model. */\n observation: unknown;\n /** True when the call was denied (not on the allow-list / not permitted). */\n rejected: boolean;\n /** Error summary when `ok` is false. */\n error?: string;\n}\n\n/** Why {@link runToolLoop} returned. */\nexport type ToolLoopStopReason = 'stop' | 'max_steps' | 'no_tools';\n\n/** The outcome of a {@link runToolLoop} turn. */\nexport interface ToolLoopResult {\n /** The model's final assistant text. */\n content: string;\n /** Number of tool-executing rounds completed. */\n steps: number;\n /** Why the loop stopped. */\n stoppedReason: ToolLoopStopReason;\n /** Every tool invocation attempted this turn, in order. */\n invocations: ToolInvocation[];\n /** The full working transcript (input messages + assistant/tool turns). */\n messages: AIMessage[];\n /** Total tokens reported by the AI boundary, when available. */\n totalTokens: number;\n}\n\n/** Context handed to a custom {@link ToolLoopOptions.executeTool} implementation. */\nexport interface ToolExecutionContext {\n /** The principal run whose context bounds this execution. */\n run: PrincipalRun;\n /** The manifest operation to execute. */\n tool: ManifestTool;\n /** Parsed tool arguments. */\n args: Record<string, unknown>;\n /** The database handle to operate against (already the RLS-bound tx when on). */\n db?: SmrtClassOptions['db'];\n}\n\n/**\n * Options for {@link runToolLoop}.\n */\nexport interface ToolLoopOptions {\n /** The AI boundary (the only thing mocked in tests). */\n ai: AIInterface;\n /** The initial conversation messages (system / history / user). */\n messages: AIMessage[];\n /** The manifest operations available this turn (already allow-list-filtered). */\n tools: ManifestTool[];\n /**\n * Non-manifest tools offered alongside the manifest operations — e.g. the\n * agent-orchestration `invoke-agent` tool (#1892). Each is gated by the same\n * fail-closed allow-list: only pass a tool whose `slug` is on the persona's\n * `allowedTools`, and its `execute` re-asserts the gate. Offered to the model\n * with its own `aiTool` definition and routed to its own handler.\n */\n extraTools?: PrincipalTool[];\n /** The persona principal every tool call runs as. */\n principal: PrincipalBinding;\n /** Database handle the side-door operations run against. */\n db?: SmrtClassOptions['db'];\n /** Max tool-executing rounds before force-termination. Default {@link DEFAULT_MAX_STEPS}. */\n maxSteps?: number;\n /** Model id passed to the AI boundary. */\n model?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Max tokens per completion. */\n maxTokens?: number;\n /** Tool-choice behaviour while tools are offered. Default `'auto'`. */\n toolChoice?: ChatOptions['toolChoice'];\n /**\n * Override the side-door executor. The default\n * ({@link invokeManifestTool}) enforces the allow-list + catalog gate and\n * dispatches through the ObjectRegistry. Tests inject a stub to exercise loop\n * mechanics without a backing object.\n */\n executeTool?: (ctx: ToolExecutionContext) => Promise<unknown>;\n /** Notified after each tool invocation (for streaming/telemetry). */\n onInvocation?: (invocation: ToolInvocation) => void | Promise<void>;\n /**\n * Token sink for live streaming (#1936). When set, each `ai.chat` round is run\n * with `stream: true` and the model's text deltas are forwarded here as they\n * arrive. It is best-effort: a provider that cannot stream (or streams no text\n * on a tool-call round) simply never calls it, and the fully-resolved response\n * is still returned. Deltas across ALL rounds are forwarded — a tool-call\n * round may narrate before calling a tool — so the emitted tokens are a live\n * PREVIEW; the loop's final `content` (persisted + surfaced by the caller as\n * the authoritative message) is the source of truth.\n */\n onToken?: (chunk: string) => void;\n /** The originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Canonical agent class, recorded in the audit entry. */\n agentClass?: string;\n /** Audit sink forwarded to {@link executeAsPrincipal}. */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/**\n * Best-effort parse of the model's raw tool-call arguments (a JSON string).\n * A non-object or malformed payload yields `{}` so a bad-argument call still\n * flows through the permission gate rather than throwing before it.\n */\nfunction parseToolArguments(raw: string | undefined): Record<string, unknown> {\n if (!raw) {\n return {};\n }\n try {\n return asRecord(JSON.parse(raw));\n } catch {\n return {};\n }\n}\n\n/**\n * Derive the catalog action for a permission definition — the slug segment(s)\n * after the `collection.` prefix. Catalog slugs are built as\n * `${collection}.${action}`, so this recovers `read` / `create` / a custom\n * method name unambiguously.\n */\nfunction actionFromDefinition(def: PermissionDefinition): string | null {\n const collection = def.collection;\n if (!collection || !def.slug.startsWith(`${collection}.`)) {\n return null;\n }\n const action = def.slug.slice(collection.length + 1);\n return action.length > 0 ? action : null;\n}\n\n/**\n * Build the closed catalog of manifest operations available as tools.\n *\n * Reads the manifest-derived {@link PermissionCatalog} and keeps only the\n * entries that name a dispatchable operation (a `(collection, action)` with a\n * resolvable backing class). Pass `allowedTools` to narrow the catalog to a\n * persona's least-privilege allow-list — this is the **offer gate**: a slug not\n * in `allowedTools` is never returned, so it is neither offered to the model nor\n * executed. A missing, `null`, or empty `allowedTools` yields **no tools**\n * (fail-closed) — the same whitelist semantics as `AgentSession`/\n * `PrincipalBinding` (S5 #1392), so forgetting the allow-list can only tighten,\n * never widen, the offered surface. Pass `all: true` to deliberately enumerate\n * the full manifest operation surface (e.g. an admin tool picker) — that is the\n * one explicit escape hatch, never the default.\n */\nexport function buildManifestToolCatalog(\n options: SmrtClassOptions & {\n /** Least-privilege allow-list to narrow the catalog by (fail-closed). */\n allowedTools?: string[] | null;\n /** Explicitly enumerate the ENTIRE manifest operation surface (no narrowing). */\n all?: boolean;\n /** Supply a pre-built catalog (skips the manifest walk). */\n catalog?: PermissionDefinition[];\n } = {},\n): ManifestTool[] {\n const definitions =\n options.catalog ??\n PermissionCatalogService.create(options).getCatalog().permissions;\n\n // Fail-closed: an absent / `null` / empty allow-list permits NOTHING. Only an\n // explicit `all: true` disables the narrowing and returns the full surface, so\n // a caller that forgets `allowedTools` gets zero tools rather than every one.\n const filter =\n options.all === true ? null : new Set(options.allowedTools ?? []);\n\n const tools: ManifestTool[] = [];\n for (const def of definitions) {\n if (!def.className || !def.collection) {\n continue;\n }\n if (filter && !filter.has(def.slug)) {\n continue;\n }\n const action = actionFromDefinition(def);\n if (!action) {\n continue;\n }\n tools.push({\n slug: def.slug,\n collection: def.collection,\n className: def.className,\n action,\n qualifiedName: def.qualifiedName,\n description: def.description,\n });\n }\n return tools;\n}\n\n/**\n * JSON-schema parameters for a manifest operation, keyed off its action. Create\n * / update schemas are enriched with the object's declared field names (tool arg\n * schemas come from field metadata) when the registry can supply them.\n */\nfunction toolParameters(tool: ManifestTool): Record<string, unknown> {\n const fieldProps = (): Record<string, unknown> => {\n const props: Record<string, unknown> = {};\n try {\n for (const [name] of ObjectRegistry.getFields(tool.className)) {\n if (typeof name === 'string') {\n props[name] = { type: 'string' };\n }\n }\n } catch {\n // No field metadata available — fall back to a free-form object.\n }\n return props;\n };\n\n switch (tool.action) {\n case 'read':\n return {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Fetch one row by id (omit to list).',\n },\n where: {\n type: 'object',\n description: 'Equality filters for a list.',\n },\n limit: { type: 'number' },\n offset: { type: 'number' },\n },\n };\n case 'create':\n return { type: 'object', properties: fieldProps() };\n case 'update':\n return {\n type: 'object',\n required: ['id'],\n properties: { id: { type: 'string' }, ...fieldProps() },\n };\n case 'delete':\n return {\n type: 'object',\n required: ['id'],\n properties: { id: { type: 'string' } },\n };\n default:\n return {\n type: 'object',\n required: ['id'],\n properties: {\n id: { type: 'string', description: 'Target row id for the action.' },\n },\n };\n }\n}\n\n/**\n * A provider-safe function name for a catalog slug.\n *\n * Catalog slugs are `collection.action` and routinely contain a `.`, but many\n * providers (OpenAI) restrict function names to `[A-Za-z0-9_-]{1,64}`. This maps\n * the slug into that charset (dots → `-`) for the wire; {@link runToolLoop} maps\n * the returned name back to the tool, and `tool.slug` remains the internal\n * permission id. Distinct slugs stay distinct (the only substituted char is the\n * single `.` separator).\n */\nexport function toolFunctionName(slug: string): string {\n return slug.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 64);\n}\n\n/**\n * Project a manifest operation into an AI function-tool definition. The function\n * name is the provider-safe rendering of the catalog slug\n * ({@link toolFunctionName}), so the model can only ever name a real operation.\n */\nexport function manifestToolToAITool(tool: ManifestTool): AITool {\n return {\n type: 'function',\n function: {\n name: toolFunctionName(tool.slug),\n description:\n tool.description ??\n `Manifest operation '${tool.action}' on '${tool.collection}'.`,\n parameters: toolParameters(tool),\n },\n };\n}\n\ninterface OperableItem {\n toJSON: () => Record<string, unknown>;\n save: () => Promise<unknown>;\n delete: () => Promise<unknown>;\n}\n\nfunction itemToObservation(item: unknown): unknown {\n const candidate = item as { toJSON?: () => unknown } | null;\n return typeof candidate?.toJSON === 'function' ? candidate.toJSON() : item;\n}\n\n/**\n * Execute a manifest operation in-process (\"side door\") under the principal.\n *\n * Enforces both authority dimensions before touching data: the fail-closed tool\n * allow-list ({@link PrincipalRun.assertToolAllowed}) and the catalog permission\n * for the `(collection, action)` ({@link PrincipalRun.assertOperation}) — the\n * door-agnostic teeth that hold on RLS-off adapters and are a redundant second\n * gate under Postgres RLS. Data operations run against the principal context's\n * database (the RLS-bound transaction when RLS is on), so tenant + per-operation\n * enforcement apply exactly as they would through REST or MCP.\n */\nexport async function invokeManifestTool(\n run: PrincipalRun,\n tool: ManifestTool,\n args: Record<string, unknown>,\n options: { db?: SmrtClassOptions['db'] } = {},\n): Promise<unknown> {\n // Gate 1: fail-closed allow-list (defense-in-depth behind the offer gate).\n run.assertToolAllowed(tool.slug);\n // Gate 2: door-agnostic catalog authority (the RLS-off teeth; redundant under RLS).\n await run.assertOperation(tool.collection, tool.action);\n\n // Operate against the principal context's database so RLS (when on) and tenant\n // auto-filtering bound the query; fall back to the supplied handle otherwise.\n const db = (run.context.database ?? options.db) as SmrtClassOptions['db'];\n const collection = await ObjectRegistry.getCollection(\n tool.className,\n db ? { db } : {},\n );\n\n switch (tool.action) {\n case 'read': {\n const id = typeof args.id === 'string' ? args.id : undefined;\n if (id) {\n const item = await collection.get(id);\n return item ? itemToObservation(item) : { found: false };\n }\n const items = await collection.list({\n where: asRecord(args.where),\n limit: typeof args.limit === 'number' ? args.limit : 50,\n offset: typeof args.offset === 'number' ? args.offset : 0,\n });\n return items.map(itemToObservation);\n }\n case 'create': {\n const item = (await collection.create(args)) as unknown as OperableItem;\n await item.save();\n return itemToObservation(item);\n }\n case 'update': {\n const { id, ...rest } = args;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(`'${tool.slug}' requires an 'id' to update.`);\n }\n const item = (await collection.get(id)) as unknown as OperableItem | null;\n if (!item) {\n return { found: false };\n }\n Object.assign(item, rest);\n await item.save();\n return itemToObservation(item);\n }\n case 'delete': {\n const id = typeof args.id === 'string' ? args.id : undefined;\n if (!id) {\n throw new Error(`'${tool.slug}' requires an 'id' to delete.`);\n }\n const item = (await collection.get(id)) as unknown as OperableItem | null;\n if (!item) {\n return { found: false };\n }\n await item.delete();\n return { success: true, id };\n }\n default: {\n // Public custom action: invoke the named method on the row.\n const { id, ...rest } = args;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(`'${tool.slug}' requires an 'id' for a custom action.`);\n }\n const item = await collection.get(id);\n if (!item) {\n return { found: false };\n }\n const method = (item as unknown as Record<string, unknown>)[tool.action];\n if (typeof method !== 'function') {\n throw new Error(\n `Method '${tool.action}' not found on '${tool.className}'.`,\n );\n }\n const result = await (\n method as (input: Record<string, unknown>) => Promise<unknown>\n ).call(item, rest);\n return result === undefined\n ? { success: true }\n : itemToObservation(result);\n }\n }\n}\n\n/**\n * Run a bounded `tool_call → observe → respond` loop over the manifest operation\n * surface, as the persona's bound principal.\n *\n * The whole turn runs inside a single {@link executeAsPrincipal} context, so\n * every tool call shares one published permission snapshot (matching what a\n * Postgres RLS session enforces) and the turn audits once as on-behalf-of the\n * originating user.\n *\n * @param options - The AI boundary, seed messages, allow-list-filtered tools,\n * principal, and ceiling.\n * @returns The final assistant text plus the invocation log and transcript.\n */\nexport async function runToolLoop(\n options: ToolLoopOptions,\n): Promise<ToolLoopResult> {\n const {\n ai,\n messages,\n tools,\n extraTools = [],\n principal,\n db,\n maxSteps = DEFAULT_MAX_STEPS,\n model,\n temperature,\n maxTokens,\n toolChoice = 'auto',\n executeTool,\n onInvocation,\n onToken,\n onBehalfOfUserId,\n agentClass,\n audit,\n postgresRls,\n } = options;\n\n const aiTools = [\n ...tools.map(manifestToolToAITool),\n ...extraTools.map((tool) => tool.aiTool),\n ];\n // Resolve the tool by EITHER the internal slug (a mock/pass-through provider)\n // OR the provider-safe function name the model actually receives, so the offer\n // gate holds regardless of how the provider renders the name.\n const offered = new Map<string, ManifestTool>();\n for (const tool of tools) {\n offered.set(tool.slug, tool);\n offered.set(toolFunctionName(tool.slug), tool);\n }\n // Extra (non-manifest) tools resolve by their slug OR the function name their\n // own `aiTool` definition advertises to the model.\n const offeredExtra = new Map<string, PrincipalTool>();\n for (const tool of extraTools) {\n offeredExtra.set(tool.slug, tool);\n offeredExtra.set(tool.aiTool.function.name, tool);\n }\n\n return executeAsPrincipal(\n {\n db,\n principal,\n onBehalfOfUserId,\n agentClass,\n action: 'chat.tool_loop',\n postgresRls,\n audit,\n },\n async (run): Promise<ToolLoopResult> => {\n // `LoopMessage` carries `tool_call_id` on tool observations (OpenAI's tool\n // message shape needs it to correlate an observation to its call); it is a\n // structural superset of `AIMessage`, so the transcript stays chat-compatible.\n const working: LoopMessage[] = [...messages];\n const invocations: ToolInvocation[] = [];\n let executedRounds = 0;\n let totalTokens = 0;\n let response: AIResponse;\n\n for (;;) {\n const offerTools = aiTools.length > 0 && executedRounds < maxSteps;\n response = await ai.chat(working, {\n model,\n temperature,\n maxTokens,\n tools: offerTools ? aiTools : undefined,\n toolChoice: offerTools ? toolChoice : 'none',\n // Live token streaming (#1936). Best-effort: providers that don't\n // stream ignore these and still resolve the full response below.\n ...(onToken ? { stream: true, onProgress: onToken } : {}),\n });\n totalTokens += response.usage?.totalTokens ?? 0;\n\n const toolCalls = offerTools ? (response.toolCalls ?? []) : [];\n if (toolCalls.length === 0) {\n return {\n content: response.content ?? '',\n steps: executedRounds,\n stoppedReason:\n aiTools.length === 0\n ? 'no_tools'\n : offerTools\n ? 'stop'\n : 'max_steps',\n invocations,\n messages: working,\n totalTokens,\n };\n }\n\n // Record the assistant's tool-call turn before appending observations.\n working.push({\n role: 'assistant',\n content: response.content ?? '',\n tool_calls: toolCalls,\n });\n\n for (const call of toolCalls) {\n const requestedName = call.function.name;\n const args = parseToolArguments(call.function.arguments);\n const tool = offered.get(requestedName);\n // An extra (non-manifest) tool only when no manifest tool matched.\n const extraTool = tool ? undefined : offeredExtra.get(requestedName);\n // Record the canonical slug (the permission id) for a resolved tool;\n // for a rejected/hallucinated call, echo whatever the model named.\n const slug = tool?.slug ?? extraTool?.slug ?? requestedName;\n\n let invocation: ToolInvocation;\n if (!tool && !extraTool) {\n // Offer gate: a tool the persona was not offered (not on the\n // allow-list, or hallucinated) is rejected without execution.\n invocation = {\n slug,\n args,\n ok: false,\n rejected: true,\n observation: {\n error: `Tool '${slug}' is not permitted for this persona.`,\n },\n error: 'not_permitted',\n };\n } else {\n try {\n const observation = await (tool\n ? executeTool\n ? executeTool({ run, tool, args, db })\n : invokeManifestTool(run, tool, args, { db })\n : // biome-ignore lint/style/noNonNullAssertion: extraTool is defined in this branch (tool is falsy).\n extraTool!.execute({ run, args, db }));\n invocation = {\n slug,\n args,\n ok: true,\n rejected: false,\n observation,\n };\n } catch (error) {\n const rejected =\n error instanceof PrincipalToolNotAllowedError ||\n error instanceof OperationPermissionError;\n invocation = {\n slug,\n args,\n ok: false,\n rejected,\n observation: {\n error: error instanceof Error ? error.message : String(error),\n },\n error: rejected ? 'not_permitted' : 'execution_error',\n };\n }\n }\n\n invocations.push(invocation);\n await onInvocation?.(invocation);\n working.push({\n role: 'tool',\n name: requestedName,\n // Correlate the observation to the exact call the model made — many\n // providers (OpenAI) require `tool_call_id` on a tool message and\n // mis-associate observations without it when several calls occur.\n tool_call_id: call.id,\n content: JSON.stringify(invocation.observation),\n });\n }\n\n executedRounds += 1;\n }\n },\n );\n}\n","/**\n * Persona-bound conversation — the bridge from an {@link AgentSession} (a\n * conversation) to an `AgentPersona`/`TenantAgent` (a tenant-scoped, principal-\n * bound behavioural profile) (L3 of the learning-agents epic, #1891).\n *\n * This is the new, acyclic `chat → personas` edge. A conversation bound this way\n * runs under the persona's **principal** (its `runAsUserId`, via\n * {@link runToolLoop} → `executeAsPrincipal`), offers only the persona's\n * **tools** (its `allowedTools`, narrowing the manifest operation surface),\n * speaks with the persona's **instructions**, and draws on its **recalled\n * learning memory** — so the assistant behaves like it knows the tenant's job.\n *\n * The persona's `allowedTools` is mirrored onto the `AgentSession` so the chat\n * layer's own fail-closed tool gate (S5 #1392) agrees with the loop's — one\n * allow-list, enforced at both the loop's side door and the message-authoring\n * seam.\n *\n * @module\n */\n\nimport type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type {\n PrincipalAuditSink,\n PrincipalBinding,\n PrincipalTool,\n} from '@happyvertical/smrt-agents';\nimport type {\n LearningMemoryRecord,\n LearningSemanticSearch,\n SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport {\n personaLearningMemory,\n resolvePersonaInstructions,\n} from '@happyvertical/smrt-personas';\nimport { getDatabase } from '@happyvertical/sql';\nimport type { AgentSession } from './models/AgentSession.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport {\n buildManifestToolCatalog,\n type ManifestTool,\n runToolLoop,\n type ToolLoopResult,\n} from './tool-loop.js';\n\n/**\n * The structural persona shape the conversation binding needs. Both a\n * `ResolvedPersona` (from `PersonaResolver.resolve()`) and a raw `AgentPersona`\n * satisfy it via the adapters below.\n */\nexport interface ConversationPersona {\n /** Persona id — required to scope learning memory and prompt overrides. */\n id?: string | null;\n /** Owning tenant. */\n tenantId: string | null;\n /** Canonical agent class the persona configures. */\n agentClass?: string;\n /** The user whose live permissions bound the conversation. */\n runAsUserId: string;\n /** Optional acting `Bot` profile id (identity/audit). */\n actsAsProfileId?: string | null;\n /** The persona's tool allow-list (already capped by the class ceiling). */\n allowedTools: string[];\n /** Behavioural instructions / system prompt. */\n instructions?: string;\n /** Learning memory partition key. */\n memoryScope?: string;\n}\n\n/** Adapt a `PersonaResolver.resolve()` result into a {@link ConversationPersona}. */\nexport function conversationPersonaFromResolved(resolved: {\n personaId?: string;\n tenantId: string;\n agentClass: string;\n runAsUserId?: string;\n actsAsProfileId?: string | null;\n allowedTools: string[];\n instructions: string;\n memoryScope: string;\n}): ConversationPersona {\n return {\n id: resolved.personaId ?? null,\n tenantId: resolved.tenantId,\n agentClass: resolved.agentClass,\n runAsUserId: resolved.runAsUserId ?? '',\n actsAsProfileId: resolved.actsAsProfileId ?? null,\n allowedTools: resolved.allowedTools,\n instructions: resolved.instructions,\n memoryScope: resolved.memoryScope,\n };\n}\n\n/** Adapt a raw `AgentPersona` row into a {@link ConversationPersona}. */\nexport function conversationPersonaFromAgentPersona(persona: {\n id?: string | null;\n tenantId: string;\n agentClass: string;\n runAsUserId: string;\n actsAsProfileId?: string | null;\n instructions: string;\n memoryScope?: string;\n getAllowedTools: () => string[];\n}): ConversationPersona {\n return {\n id: persona.id ?? null,\n tenantId: persona.tenantId,\n agentClass: persona.agentClass,\n runAsUserId: persona.runAsUserId,\n actsAsProfileId: persona.actsAsProfileId ?? null,\n allowedTools: persona.getAllowedTools(),\n instructions: persona.instructions,\n memoryScope: persona.memoryScope,\n };\n}\n\n/**\n * Project a {@link ConversationPersona} into the {@link PrincipalBinding} the\n * tool loop runs as. The persona's `allowedTools` is the fail-closed whitelist\n * (absent/empty ⇒ no tools).\n */\nexport function principalBindingFor(\n persona: ConversationPersona,\n): PrincipalBinding {\n return {\n runAsUserId: persona.runAsUserId,\n tenantId: persona.tenantId,\n allowedTools: persona.allowedTools,\n actsAsProfileId: persona.actsAsProfileId ?? null,\n };\n}\n\n/** How to recall a persona's learning memory into the conversation context. */\nexport interface PersonaRecallOptions {\n /** Learning scope to recall (defaults to `'chat'`). */\n scope?: string;\n /** Exact episode key within the scope (omit for a scope-wide recall). */\n key?: string;\n /** Free-text query for the semantic arm (needs a `semanticSearch`). */\n query?: string;\n /** Max recalled records injected into context. Default 5. */\n limit?: number;\n /** Override the reuse floor for this recall. */\n minConfidence?: number;\n /** Optional embedding search for the semantic recall arm. */\n semanticSearch?: LearningSemanticSearch;\n}\n\n/**\n * Recall the persona's confidence-filtered learning memory.\n *\n * Isolated per persona by `memoryScope`, so what the \"Support\" persona learned\n * never bleeds into \"Sales\". Returns `[]` for a persona with no memory scope /\n * id (nothing to partition on).\n */\nexport async function recallPersonaMemory(\n db: SmrtClassOptions['db'],\n persona: ConversationPersona,\n options: PersonaRecallOptions = {},\n): Promise<LearningMemoryRecord[]> {\n if (!persona.memoryScope && !persona.id) {\n return [];\n }\n // LearningMemory operates on a resolved DB handle; `getDatabase` accepts a\n // config or a handle and returns a handle (idempotent for a handle).\n const memory = personaLearningMemory({\n db: await getDatabase(db as Parameters<typeof getDatabase>[0]),\n persona,\n semanticSearch: options.semanticSearch,\n });\n return memory.recall(options.scope ?? 'chat', {\n key: options.key,\n query: options.query,\n limit: options.limit ?? 5,\n minConfidence: options.minConfidence,\n });\n}\n\n/**\n * Format recalled memory into a system-context block. Empty string when there\n * is nothing to inject (so it can be unconditionally concatenated).\n */\nexport function formatRecalledMemory(records: LearningMemoryRecord[]): string {\n if (records.length === 0) {\n return '';\n }\n const lines = records.map((record) => {\n const value =\n typeof record.value === 'string'\n ? record.value\n : JSON.stringify(record.value);\n return `- [confidence ${record.confidence.toFixed(2)}] ${record.key}: ${value}`;\n });\n return `What you have learned about this organisation:\\n${lines.join('\\n')}`;\n}\n\n/**\n * Resolve the persona's effective instructions.\n *\n * Prefers the prompt-system resolution (`resolvePersonaInstructions`, which\n * layers any approved learned-directive override) when the persona is persisted;\n * falls back to the inline `persona.instructions`. This is how a conversation\n * \"uses its instructions (`applyPersonaInstructions`)\".\n */\nexport async function resolveConversationInstructions(\n db: SmrtClassOptions['db'],\n persona: ConversationPersona,\n): Promise<string> {\n if (persona.id) {\n try {\n const resolved = await resolvePersonaInstructions({\n persona: { id: persona.id, tenantId: persona.tenantId },\n db: db as Parameters<typeof resolvePersonaInstructions>[0]['db'],\n });\n if (resolved) {\n return resolved;\n }\n } catch {\n // Fall through to the inline instructions.\n }\n }\n return persona.instructions ?? '';\n}\n\n/** The minimal AgentSession surface the turn needs. */\ntype SessionLike = Pick<AgentSession, 'id' | 'chatRoomId' | 'systemPrompt'>;\n\n/** The minimal ChatService surface the turn needs to author the reply. */\nexport interface ConversationReplyService {\n initialize(): Promise<void>;\n}\n\n/**\n * Options for {@link runPersonaConversationTurn}.\n */\nexport interface PersonaConversationTurnOptions {\n /** The AI boundary. */\n ai: AIInterface;\n /** The database handle side-door operations run against. */\n db: SmrtClassOptions['db'];\n /** The persona the conversation is bound to. */\n persona: ConversationPersona;\n /** The user's message this turn. */\n userMessage: string;\n /** Tenant the turn runs within. */\n tenantId: string;\n /** Prior conversation turns (assistant/user), oldest first. */\n history?: AIMessage[];\n /**\n * The bound agent session. When provided together with `chatService`, the\n * agent reply is authored into the session's room and each executed tool is\n * recorded as a `tool_result` message (gated by the session allow-list).\n */\n session?: SessionLike | null;\n /** Chat service used to author the agent reply. */\n chatService?: ConversationReplyService | null;\n /** Thread to attach authored messages to. */\n threadId?: string | null;\n /** Recall configuration, or `false` to skip memory recall. */\n recall?: PersonaRecallOptions | false;\n /** Pre-built tool catalog (else derived from the persona's `allowedTools`). */\n tools?: ManifestTool[];\n /**\n * Non-manifest tools to offer this turn — e.g. the agent-orchestration\n * `invoke-agent` tool (#1892). Each is filtered by the persona's\n * `allowedTools` before being offered, so orchestration is gated exactly like\n * any other tool: a persona that does not allow-list `agents.invoke` never\n * sees it.\n */\n extraTools?: PrincipalTool[];\n /** Max tool-executing rounds. */\n maxSteps?: number;\n /** Model id. */\n model?: string;\n /** Sampling temperature. */\n temperature?: number;\n /** Max tokens per completion. */\n maxTokens?: number;\n /** Originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Audit sink for the on-behalf-of entry (forwarded to `executeAsPrincipal`). */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n /** Correlation id for the turn (feedback ties back to it). Auto-generated when omitted. */\n correlationId?: string;\n /**\n * Token sink for live streaming (#1936). Forwarded to {@link runToolLoop}: the\n * model's text deltas stream here as they arrive. Best-effort (see\n * `ToolLoopOptions.onToken`); the authored assistant message remains the\n * authoritative final content.\n */\n onToken?: (chunk: string) => void;\n}\n\n/** The outcome of a persona-bound conversation turn. */\nexport interface PersonaConversationTurnResult {\n /** The tool-loop result (final text, invocations, transcript). */\n result: ToolLoopResult;\n /** The correlation id feedback on this turn should reference. */\n correlationId: string;\n /** The memory recalled into the turn's context. */\n recalled: LearningMemoryRecord[];\n /** The system prompt assembled for the turn. */\n systemPrompt: string;\n /** Persisted messages authored by this turn when a chat service was supplied. */\n authoredMessages?: AuthoredConversationMessages;\n}\n\n/** Persisted assistant/tool messages emitted for a persona turn. */\nexport interface AuthoredConversationMessages {\n toolMessages: ChatMessage[];\n assistantMessage: ChatMessage | null;\n}\n\nfunction assembleSystemPrompt(\n instructions: string,\n memoryBlock: string,\n sessionPrompt: string | undefined,\n): string {\n // De-duplicate identical blocks: `bindPersonaToSession()` sets\n // `session.systemPrompt` to the persona instructions, so without this the\n // instruction block would appear twice (wasted tokens + confusion) once a\n // conversation runs on a bound session.\n const blocks = [sessionPrompt, instructions, memoryBlock]\n .map((part) => part?.trim())\n .filter((part): part is string => Boolean(part));\n return [...new Set(blocks)].join('\\n\\n');\n}\n\n/**\n * Run one turn of a persona-bound conversation.\n *\n * Binds the conversation to the persona: recalls its learning memory, resolves\n * its instructions, offers only its allow-listed manifest operations, and runs\n * the bounded tool loop as its principal. When a `chatService` + `session` are\n * given the assistant reply (and each executed tool) is authored into the room,\n * exercising the chat layer's own fail-closed tool gate.\n *\n * @returns The loop result, the turn's correlation id, and the recalled memory.\n */\nexport async function runPersonaConversationTurn(\n options: PersonaConversationTurnOptions,\n): Promise<PersonaConversationTurnResult> {\n const { ai, db, persona, userMessage, tenantId } = options;\n // A conversation must run as a concrete principal. A default/unbound persona\n // (e.g. a `PersonaResolver` default fallback) has no `runAsUserId`; fail fast\n // with a clear error rather than building an empty-string PrincipalBinding\n // that would silently resolve to zero permissions downstream.\n if (!persona.runAsUserId) {\n throw new Error(\n 'runPersonaConversationTurn requires a persona bound to a run-as user ' +\n '(runAsUserId); an unbound/default persona cannot operate the app.',\n );\n }\n const correlationId = options.correlationId ?? crypto.randomUUID();\n\n const recalled =\n options.recall === false\n ? []\n : await recallPersonaMemory(db, persona, options.recall ?? {});\n\n const instructions = await resolveConversationInstructions(db, persona);\n const memoryBlock = formatRecalledMemory(recalled);\n const systemPrompt = assembleSystemPrompt(\n instructions,\n memoryBlock,\n options.session?.systemPrompt,\n );\n\n const messages: AIMessage[] = [];\n if (systemPrompt) {\n messages.push({ role: 'system', content: systemPrompt });\n }\n if (options.history) {\n messages.push(...options.history);\n }\n messages.push({ role: 'user', content: userMessage });\n\n const tools =\n options.tools ??\n buildManifestToolCatalog({ db, allowedTools: persona.allowedTools });\n\n // Offer gate for non-manifest tools: only those the persona allow-lists (e.g.\n // `agents.invoke`) are offered, mirroring how the manifest catalog is\n // narrowed by `allowedTools`.\n const extraTools = (options.extraTools ?? []).filter((tool) =>\n persona.allowedTools.includes(tool.slug),\n );\n\n const result = await runToolLoop({\n ai,\n messages,\n tools,\n extraTools,\n principal: principalBindingFor(persona),\n db,\n maxSteps: options.maxSteps,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n onBehalfOfUserId: options.onBehalfOfUserId,\n agentClass: persona.agentClass,\n postgresRls: options.postgresRls,\n audit: options.audit,\n onToken: options.onToken,\n });\n\n let authoredMessages: AuthoredConversationMessages | undefined;\n if (options.chatService && options.session?.id) {\n authoredMessages = await authorConversationReply({\n chatService: options.chatService,\n session: options.session,\n tenantId,\n threadId: options.threadId ?? null,\n result,\n });\n }\n\n return { result, correlationId, recalled, systemPrompt, authoredMessages };\n}\n\n/**\n * Options for {@link bindPersonaToSession}.\n */\nexport interface BindPersonaToSessionOptions {\n /** Chat service exposing the owner-checked `updateAgentSessionConfig`. */\n chatService: {\n updateAgentSessionConfig(params: {\n agentSessionId: string;\n actorProfileId: string;\n tenantId: string | null;\n allowedTools?: string[];\n systemPrompt?: string;\n }): Promise<AgentSession>;\n };\n /** The session to bind. */\n session: Pick<AgentSession, 'id'>;\n /** The session owner (the update is owner-checked, S5 #1392). */\n actorProfileId: string;\n /** Tenant the session belongs to. */\n tenantId: string | null;\n /** The persona to bind the session to. */\n persona: ConversationPersona;\n /** Instructions to set as the session system prompt (else resolved). */\n instructions?: string;\n /** Database handle used to resolve instructions when not supplied. */\n db?: SmrtClassOptions['db'];\n}\n\n/**\n * Bind an {@link AgentSession} to a persona: mirror the persona's `allowedTools`\n * and instructions onto the session so the chat layer's own fail-closed tool\n * gate (S5 #1392) agrees with the loop's, and the session's system prompt speaks\n * the persona's voice. This is the durable side of the `chat → personas` bridge:\n * once bound, the session's authoring gate and the loop's offer gate share one\n * allow-list.\n */\nexport async function bindPersonaToSession(\n options: BindPersonaToSessionOptions,\n): Promise<AgentSession> {\n const instructions =\n options.instructions ??\n (options.db\n ? await resolveConversationInstructions(options.db, options.persona)\n : (options.persona.instructions ?? ''));\n return options.chatService.updateAgentSessionConfig({\n agentSessionId: options.session.id as string,\n actorProfileId: options.actorProfileId,\n tenantId: options.tenantId,\n allowedTools: options.persona.allowedTools,\n systemPrompt: instructions,\n });\n}\n\n/**\n * Author the agent's turn into the chat room: one `tool_result` message per\n * executed tool (gated fail-closed by the session allow-list), then the final\n * assistant text. Uses the trusted in-package agent-reply bridge, so messages\n * are authored AS the session's agent.\n */\nasync function authorConversationReply(input: {\n chatService: ConversationReplyService;\n session: SessionLike;\n tenantId: string;\n threadId: string | null;\n result: ToolLoopResult;\n}): Promise<AuthoredConversationMessages> {\n const { sendAgentReply } = await import('./services/ChatService.js');\n const toolMessages: ChatMessage[] = [];\n for (const invocation of input.result.invocations) {\n if (!invocation.ok) {\n continue;\n }\n const message = await sendAgentReply(input.chatService, {\n tenantId: input.tenantId,\n agentSessionId: input.session.id as string,\n threadId: input.threadId,\n content: JSON.stringify(invocation.observation),\n kind: 'tool',\n messageType: 'tool_result',\n toolCallData: { name: invocation.slug, args: invocation.args },\n });\n toolMessages.push(message);\n }\n const assistantMessage = await sendAgentReply(input.chatService, {\n tenantId: input.tenantId,\n agentSessionId: input.session.id as string,\n threadId: input.threadId,\n content: input.result.content,\n kind: 'assistant',\n });\n return { toolMessages, assistantMessage };\n}\n","/**\n * Token-streaming conversation endpoint (SSE) for embeddable chat clients\n * (issue #1936).\n *\n * The seam that joins the conversational harness to an embeddable widget: a\n * client (first consumer: the Happy chat widget, `happyvertical/animation#5`)\n * POSTs the conversation so far and receives a `text/event-stream` of\n * `data: <json>` frames — token deltas as the model generates them, then a\n * final `done` frame carrying the persisted message. A floating character can\n * stream the reply into a bubble and lip-sync each sentence as it completes,\n * instead of waiting for the whole JSON reply the way `POST /api/dev-chat` does.\n *\n * Two modes, dispatched by the resolved {@link ChatStreamContext}:\n * - **persona-bound** (`context.binding` present): runs the full\n * {@link runPersonaConversationTurn} — persona principal, allow-listed tools,\n * recalled memory — with a token sink wired through the tool loop, then\n * persists via `ChatService` and emits the persisted message as `done`.\n * - **plain / unbound** (`context.binding` absent): streams `ai.stream()`\n * directly and emits a synthesized (unpersisted) `done` message.\n *\n * Security posture (mirrors the voice gateway, #1910): this module NEVER\n * authorizes from the request's `session` metadata. `createChatStreamHandler`\n * takes an injected `authorize(request, body)` that the app implements to\n * validate the caller (bearer session id / cookie) and resolve an already-\n * authorized `ChatStreamContext`. The streaming engine enforces whatever that\n * returns; no authorizer ⇒ the handler cannot run. The persona path reuses the\n * harness's own fail-closed principal + allow-list gates unchanged.\n */\n\nimport type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type {\n PrincipalAuditSink,\n PrincipalTool,\n} from '@happyvertical/smrt-agents';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport type { AgentSession } from './models/AgentSession.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport {\n type ConversationPersona,\n type PersonaRecallOptions,\n runPersonaConversationTurn,\n} from './persona-conversation.js';\nimport type { ChatService } from './services/index.js';\nimport type { VoiceGatewayTurnMetadata } from './voice.js';\n\n/** Max conversation messages accepted on one streaming request. */\nexport const MAX_CHAT_STREAM_MESSAGES = 50;\n/** Max characters per message (matches the voice gateway text cap). */\nexport const MAX_CHAT_STREAM_CONTENT_LENGTH = 12_000;\n/**\n * Default SSE keep-alive interval (ms). A persona turn can go quiet for tens of\n * seconds during a silent tool-calling round (an LLM round-trip + an in-process\n * tool op emit no tokens), and idle intermediaries (nginx / ALB / Cloudflare —\n * the expected home for an embedded widget backend) drop a connection with no\n * traffic. A periodic SSE comment line keeps it warm, mirroring the core\n * `_events` route's `DEFAULT_EVENTS_HEARTBEAT_MS`.\n */\nexport const DEFAULT_CHAT_STREAM_HEARTBEAT_MS = 15_000;\n\n/** Roles carried on the wire (a subset of the internal `ChatMessageRole`). */\nexport type ChatStreamRole = 'user' | 'assistant' | 'system';\n\n/**\n * A conversation message on the wire — the shape the client sends in `messages`\n * and the shape the final `done` frame carries back. Kept self-contained (not\n * the internal `ChatMessage` model) so the contract is stable and JSON-only.\n */\nexport interface ChatStreamMessage {\n id?: string;\n role: ChatStreamRole;\n content: string;\n /** ISO-8601 timestamp. */\n createdAt?: string;\n}\n\n/**\n * Session metadata the client attaches to a turn — `VoiceGatewayTurnMetadata`-\n * shaped so voice and chat share one binding vocabulary. It is UNTRUSTED input:\n * the handler's `authorize` callback is responsible for validating any of these\n * ids against the authenticated principal before they reach a chat write or the\n * tool loop.\n */\nexport type ChatStreamSession = VoiceGatewayTurnMetadata;\n\n/**\n * A host-page control command (#1921, smrt-ui `control-interaction.ts`) carried\n * on the optional `control` lane. Kept structural here so the streaming\n * contract does not hard-couple to `@happyvertical/smrt-ui/forms`' exact union:\n * the client adapter (the Happy widget) executes it against its own control\n * registry, where sensitivity gating and the stage→apply consent split stay\n * enforced registry-side.\n */\nexport interface ChatStreamControlCommand {\n action: string;\n [key: string]: unknown;\n}\n\n/**\n * One frame of the stream. `token`/`done`/`error` are emitted today; `emotion`\n * and `control` are part of the wire contract (so clients can rely on the union\n * and a future server hook can emit them without a breaking change) but are not\n * produced by the v1 engine.\n */\nexport type ChatStreamEvent =\n | { type: 'token'; text: string }\n | { type: 'emotion'; name: string }\n | { type: 'control'; command: ChatStreamControlCommand }\n | { type: 'done'; message: ChatStreamMessage }\n | { type: 'error'; error: string };\n\n/** The JSON body of a streaming request. */\nexport interface ChatStreamRequestBody {\n messages?: unknown;\n session?: ChatStreamSession;\n}\n\n/** The minimal `AgentSession` surface the persona turn needs. */\ntype StreamSessionLike = Pick<\n AgentSession,\n 'id' | 'chatRoomId' | 'systemPrompt'\n>;\n\n/**\n * A resolved, ALREADY-AUTHORIZED persona binding. The `authorize` callback\n * produces this after validating the request against the authenticated\n * principal; nothing here is taken from untrusted request metadata.\n */\nexport interface ChatStreamPersonaBinding {\n chatService: ChatService;\n /** Database handle the persona turn's side-door operations run against. */\n db: SmrtClassOptions['db'];\n persona: ConversationPersona;\n session: StreamSessionLike;\n tenantId: string;\n /** Thread within the bound session room to author into. */\n threadId?: string | null;\n /** Originating user the turn runs on behalf of (audited). */\n onBehalfOfUserId?: string | null;\n /** Recall configuration, or `false` to skip memory recall. */\n recall?: PersonaRecallOptions | false;\n /**\n * Non-manifest custom tools to offer this streamed turn — e.g. the persona\n * messaging tool (`messages.send`) or an assistance-request/lead-ticket tool\n * backed by a `@smrt({ api:false, mcp:false })` service, which can only reach\n * the loop as `extraTools` (never as a generated manifest tool). Forwarded to\n * {@link runPersonaConversationTurn} exactly like the non-streaming persona\n * path, so a streamed persona chat can *act*, not just answer.\n *\n * This is resolved by the app's server-side `authorize` callback (trusted),\n * never taken from untrusted request input. Offering a tool is NOT authorizing\n * it: each entry is still filtered by the persona's `allowedTools` (the offer\n * gate) and its `execute` re-asserts the bound principal's RBAC + the\n * fail-closed `assertToolAllowed` (the execution gate), unchanged.\n */\n extraTools?: PrincipalTool[];\n /** Audit sink forwarded to the principal execution. */\n audit?: PrincipalAuditSink;\n /** Opt into Postgres RLS transaction wrapping. */\n postgresRls?: boolean;\n}\n\n/**\n * The trusted context a turn runs in. `binding` present ⇒ persona-bound; absent\n * ⇒ plain `ai.stream()`. Generation caps live here (server-resolved), never on\n * the request, so a caller cannot widen `maxTokens`/`maxSteps`.\n */\nexport interface ChatStreamContext {\n ai: AIInterface;\n binding?: ChatStreamPersonaBinding;\n /** System prompt for the PLAIN path. Ignored when `binding` is set. */\n systemPrompt?: string;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n maxSteps?: number;\n}\n\n/** Options for {@link runChatConversationStream}. */\nexport interface RunChatConversationStreamOptions {\n context: ChatStreamContext;\n /** The conversation so far; the last user message is this turn's prompt. */\n messages: ChatStreamMessage[];\n}\n\n/** Base error carrying an HTTP status + code for the handler to render. */\nexport class ChatStreamError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = 'ChatStreamError';\n this.status = status;\n this.code = code;\n }\n}\n\n/** 400 — malformed request body. */\nexport class ChatStreamBadRequestError extends ChatStreamError {\n constructor(message = 'Invalid chat stream request') {\n super(message, 400, 'chat_stream_bad_request');\n this.name = 'ChatStreamBadRequestError';\n }\n}\n\n/** 401 — the request could not be authorized. */\nexport class ChatStreamUnauthorizedError extends ChatStreamError {\n constructor(message = 'Unauthorized') {\n super(message, 401, 'chat_stream_unauthorized');\n this.name = 'ChatStreamUnauthorizedError';\n }\n}\n\n/**\n * Run one streaming conversation turn, yielding SSE events. Dispatches on\n * `context.binding`: persona-bound turns run the full harness; unbound turns\n * stream `ai.stream()`. Errors surface as an in-band `error` event (the HTTP\n * response has already committed to 200 once streaming starts), never a throw.\n */\nexport async function* runChatConversationStream(\n options: RunChatConversationStreamOptions,\n): AsyncGenerator<ChatStreamEvent> {\n const { context } = options;\n const messages = normalizeMessages(options.messages);\n const { history, userMessage } = splitConversation(messages);\n if (!userMessage) {\n yield { type: 'error', error: 'No user message to respond to' };\n return;\n }\n\n if (context.binding) {\n yield* streamPersonaConversation(\n context,\n context.binding,\n history,\n userMessage,\n );\n } else {\n yield* streamPlainConversation(context, history, userMessage);\n }\n}\n\n/**\n * Persona-bound turn. Bridges the tool loop's callback-based token stream\n * (`onToken`) into this async generator via a small producer/consumer queue,\n * then emits the persisted assistant message as `done`.\n */\nasync function* streamPersonaConversation(\n context: ChatStreamContext,\n binding: ChatStreamPersonaBinding,\n history: AIMessage[],\n userMessage: string,\n): AsyncGenerator<ChatStreamEvent> {\n // The producer (the turn's onToken) enqueues without awaiting the consumer, so\n // a slow client lets `queue` grow — but only up to ONE turn's emitted tokens,\n // which is bounded by the server-set `maxSteps`×`maxTokens` ceiling (the turn\n // runs to completion and then stops emitting). Tokens are intentionally NOT\n // coalesced: the widget lip-syncs each sentence as it streams, so per-delta\n // granularity is the contract. The browser-facing delivery is still\n // backpressured by the `ReadableStream` in `sseBody` (pull-based).\n const queue: ChatStreamEvent[] = [];\n let notify: (() => void) | null = null;\n let finished = false;\n\n // Wake a parked consumer (if any) exactly once.\n const wake = () => {\n const resume = notify;\n notify = null;\n resume?.();\n };\n const emit = (event: ChatStreamEvent) => {\n queue.push(event);\n wake();\n };\n\n const turnPromise = (async () => {\n try {\n const turn = await runPersonaConversationTurn({\n ai: context.ai,\n db: binding.db,\n persona: binding.persona,\n tenantId: binding.tenantId,\n userMessage,\n history,\n chatService: binding.chatService,\n session: binding.session,\n threadId: binding.threadId,\n recall: binding.recall,\n // Custom tools resolved server-side by `authorize`; still offer-gated by\n // the persona's `allowedTools` inside the turn (a tool being offered is\n // not the same as authorized).\n extraTools: binding.extraTools,\n model: context.model,\n temperature: context.temperature,\n maxTokens: context.maxTokens,\n maxSteps: context.maxSteps,\n onBehalfOfUserId: binding.onBehalfOfUserId,\n audit: binding.audit,\n postgresRls: binding.postgresRls,\n onToken: (chunk) => {\n if (chunk) emit({ type: 'token', text: chunk });\n },\n });\n emit({ type: 'done', message: resolvePersonaDoneMessage(turn) });\n } catch (error) {\n emit({ type: 'error', error: toErrorMessage(error) });\n } finally {\n finished = true;\n wake();\n }\n })();\n\n try {\n for (;;) {\n if (queue.length > 0) {\n yield queue.shift() as ChatStreamEvent;\n continue;\n }\n if (finished) break;\n await new Promise<void>((resolve) => {\n notify = resolve;\n });\n }\n } finally {\n // Never leave the turn dangling — it persists + reinforces memory even if\n // the client disconnected and stopped consuming.\n await turnPromise;\n }\n}\n\n/** Unbound turn: stream `ai.stream()` directly, then a synthesized `done`. */\nasync function* streamPlainConversation(\n context: ChatStreamContext,\n history: AIMessage[],\n userMessage: string,\n): AsyncGenerator<ChatStreamEvent> {\n const messages: AIMessage[] = [];\n if (context.systemPrompt) {\n messages.push({ role: 'system', content: context.systemPrompt });\n }\n messages.push(...history, { role: 'user', content: userMessage });\n\n let content = '';\n try {\n for await (const chunk of context.ai.stream(messages, {\n model: context.model,\n temperature: context.temperature,\n maxTokens: context.maxTokens,\n })) {\n if (chunk) {\n content += chunk;\n yield { type: 'token', text: chunk };\n }\n }\n } catch (error) {\n yield { type: 'error', error: toErrorMessage(error) };\n return;\n }\n yield { type: 'done', message: synthesizeAssistantMessage(content) };\n}\n\n/**\n * Options for {@link createChatStreamHandler}.\n */\nexport interface ChatStreamHandlerOptions {\n /**\n * Resolve an ALREADY-AUTHORIZED context from the request. This is the sole\n * trust boundary: validate the caller (bearer session id / cookie /\n * same-origin) and the claimed `body.session` ids against the authenticated\n * principal here, and return the context the turn runs in. Throw a\n * {@link ChatStreamError} (or any error ⇒ 500) to reject before any byte is\n * streamed.\n */\n authorize: (\n request: Request,\n body: ChatStreamRequestBody,\n ) => ChatStreamContext | Promise<ChatStreamContext>;\n /**\n * Cross-origin allowlist for the embedded widget (#1861 posture): the request\n * `Origin` is echoed only when a member (never `*`). Empty/omitted ⇒\n * same-origin only.\n */\n allowedOrigins?: string[];\n /** Emit `Access-Control-Allow-Credentials: true` for an allow-listed origin. */\n allowCredentials?: boolean;\n /**\n * SSE keep-alive interval (ms). Defaults to\n * {@link DEFAULT_CHAT_STREAM_HEARTBEAT_MS}. `0` disables the heartbeat.\n */\n heartbeatMs?: number;\n}\n\n/**\n * Build a Fetch-compatible SSE handler for the streaming contract (mirrors\n * `createVoiceGatewayTurnHandler`). Returns `text/event-stream`; wire it into a\n * SvelteKit `+server.ts`, a Bun/Node server, or any Fetch host.\n */\nexport function createChatStreamHandler(\n options: ChatStreamHandlerOptions,\n): (request: Request) => Promise<Response> {\n const allowedOrigins = normalizeAllowedOrigins(options.allowedOrigins);\n const allowCredentials = options.allowCredentials === true;\n const cors = (request: Request): Record<string, string> =>\n corsHeaders(request, allowedOrigins, allowCredentials);\n\n return async (request: Request): Promise<Response> => {\n if (request.method === 'OPTIONS') {\n const headers = cors(request);\n if (!('Access-Control-Allow-Origin' in headers)) {\n return new Response(null, { status: 403 });\n }\n return new Response(null, {\n status: 204,\n headers: {\n ...headers,\n 'Access-Control-Allow-Methods': 'POST,OPTIONS',\n 'Access-Control-Allow-Headers': 'Authorization,Content-Type',\n 'Access-Control-Max-Age': '86400',\n },\n });\n }\n\n if (request.method !== 'POST') {\n return jsonResponse(\n { error: 'Method not allowed', code: 'method_not_allowed' },\n 405,\n cors(request),\n );\n }\n\n let body: ChatStreamRequestBody;\n try {\n body = (await request.json()) as ChatStreamRequestBody;\n } catch {\n return jsonResponse(\n { error: 'Request body must be JSON', code: 'chat_stream_bad_request' },\n 400,\n cors(request),\n );\n }\n if (!body || typeof body !== 'object') {\n return jsonResponse(\n {\n error: 'Request body must be an object',\n code: 'chat_stream_bad_request',\n },\n 400,\n cors(request),\n );\n }\n\n let context: ChatStreamContext;\n try {\n context = await options.authorize(request, body);\n } catch (error) {\n const status = error instanceof ChatStreamError ? error.status : 500;\n const code =\n error instanceof ChatStreamError ? error.code : 'chat_stream_error';\n return jsonResponse(\n { error: toErrorMessage(error), code },\n status,\n cors(request),\n );\n }\n\n const messages = Array.isArray(body.messages)\n ? (body.messages as ChatStreamMessage[])\n : [];\n const events = runChatConversationStream({ context, messages });\n\n return new Response(sseBody(events, options.heartbeatMs), {\n status: 200,\n headers: {\n ...cors(request),\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n });\n };\n}\n\nconst encoder = new TextEncoder();\n\n/** SSE serialization of one event: a single `data:` frame. */\nexport function encodeChatStreamEvent(event: ChatStreamEvent): string {\n return `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\n/** SSE comment line — a keep-alive `EventSource` ignores natively. */\nconst HEARTBEAT_FRAME = encoder.encode(': heartbeat\\n\\n');\n\n/**\n * Adapt the event generator into a backpressure-aware `ReadableStream`. `pull`\n * advances one event at a time (so browser-facing delivery stays backpressured);\n * a `start()` heartbeat interval enqueues an SSE comment while `events.next()`\n * is pending, so a quiet tool-calling round doesn't let an idle intermediary\n * drop the connection mid-turn. `cancel` (client disconnect) clears the\n * heartbeat and returns the generator so its `finally` runs (the persona turn is\n * still awaited to completion inside it).\n */\nfunction sseBody(\n events: AsyncGenerator<ChatStreamEvent>,\n heartbeatMs = DEFAULT_CHAT_STREAM_HEARTBEAT_MS,\n): ReadableStream<Uint8Array> {\n let heartbeat: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n const stopHeartbeat = () => {\n if (heartbeat) {\n clearInterval(heartbeat);\n heartbeat = null;\n }\n };\n return new ReadableStream<Uint8Array>({\n start(controller) {\n if (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0) return;\n heartbeat = setInterval(() => {\n if (closed) return;\n try {\n controller.enqueue(HEARTBEAT_FRAME);\n } catch {\n // Controller already closed — stop pinging a dead stream.\n closed = true;\n stopHeartbeat();\n }\n }, heartbeatMs);\n // Don't keep the event loop alive solely for heartbeats.\n (heartbeat as { unref?: () => void }).unref?.();\n },\n async pull(controller) {\n try {\n const { value, done } = await events.next();\n if (done) {\n closed = true;\n stopHeartbeat();\n controller.close();\n return;\n }\n controller.enqueue(encoder.encode(encodeChatStreamEvent(value)));\n } catch (error) {\n closed = true;\n stopHeartbeat();\n controller.enqueue(\n encoder.encode(\n encodeChatStreamEvent({\n type: 'error',\n error: toErrorMessage(error),\n }),\n ),\n );\n controller.close();\n }\n },\n async cancel() {\n closed = true;\n stopHeartbeat();\n await events.return?.(undefined);\n },\n });\n}\n\n/** Trim + cap the incoming messages, dropping empty/invalid entries. */\nfunction normalizeMessages(messages: ChatStreamMessage[]): ChatStreamMessage[] {\n if (!Array.isArray(messages)) return [];\n const out: ChatStreamMessage[] = [];\n for (const message of messages.slice(-MAX_CHAT_STREAM_MESSAGES)) {\n const role = message?.role;\n if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;\n const content =\n typeof message.content === 'string' ? message.content.trim() : '';\n if (!content) continue;\n out.push({\n ...(typeof message.id === 'string' ? { id: message.id } : {}),\n role,\n content: content.slice(0, MAX_CHAT_STREAM_CONTENT_LENGTH),\n ...(typeof message.createdAt === 'string'\n ? { createdAt: message.createdAt }\n : {}),\n });\n }\n return out;\n}\n\n/**\n * Split the conversation into prior turns (`history`) and this turn's prompt\n * (`userMessage`, the last user message). Anything after the last user message\n * is dropped — the turn responds to the user's newest input.\n */\nfunction splitConversation(messages: ChatStreamMessage[]): {\n history: AIMessage[];\n userMessage: string;\n} {\n let lastUserIndex = -1;\n for (let i = messages.length - 1; i >= 0; i -= 1) {\n if (messages[i].role === 'user') {\n lastUserIndex = i;\n break;\n }\n }\n if (lastUserIndex === -1) return { history: [], userMessage: '' };\n const history = messages.slice(0, lastUserIndex).map(\n (message): AIMessage => ({\n role: message.role,\n content: message.content,\n }),\n );\n return { history, userMessage: messages[lastUserIndex].content };\n}\n\n/** Map a persona turn's persisted reply to the wire `done` message. */\nfunction resolvePersonaDoneMessage(turn: {\n result: { content: string };\n authoredMessages?: { assistantMessage: ChatMessage | null };\n}): ChatStreamMessage {\n const persisted = turn.authoredMessages?.assistantMessage;\n if (persisted) {\n return toStreamMessage(persisted);\n }\n return synthesizeAssistantMessage(turn.result.content);\n}\n\n/**\n * Coerce a persisted `ChatMessageRole` (which also includes `'tool'`) into the\n * narrower wire role. A `done` message is always the authored assistant reply,\n * so any non-conversational role collapses to `'assistant'` rather than leaking\n * an invalid value onto the contract.\n */\nfunction toWireRole(role: unknown): ChatStreamRole {\n return role === 'user' || role === 'system' ? role : 'assistant';\n}\n\n/** Project a persisted `ChatMessage` onto the stable wire shape. */\nfunction toStreamMessage(message: ChatMessage): ChatStreamMessage {\n const createdAt = (message as { createdAt?: unknown }).createdAt;\n return {\n id: (message.id as string | undefined) ?? crypto.randomUUID(),\n role: toWireRole(message.role),\n content: message.content ?? '',\n createdAt: toIsoString(createdAt),\n };\n}\n\n/** A `done` message for the unbound path, which persists nothing. */\nfunction synthesizeAssistantMessage(content: string): ChatStreamMessage {\n return {\n id: crypto.randomUUID(),\n role: 'assistant',\n content,\n createdAt: new Date().toISOString(),\n };\n}\n\nfunction toIsoString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string' && value) return value;\n return new Date().toISOString();\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction jsonResponse(\n body: unknown,\n status: number,\n extraHeaders: Record<string, string> = {},\n): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json', ...extraHeaders },\n });\n}\n\n/** Trim/de-dupe an origin allowlist; empty ⇒ same-origin only. */\nfunction normalizeAllowedOrigins(\n origins: string[] | undefined,\n): string[] | undefined {\n if (!Array.isArray(origins)) return undefined;\n const cleaned = [\n ...new Set(\n origins\n .filter((o): o is string => typeof o === 'string')\n .map((o) => o.trim())\n .filter((o) => o.length > 0),\n ),\n ];\n return cleaned.length > 0 ? cleaned : undefined;\n}\n\n/**\n * CORS headers for an embedded cross-origin widget. The `Origin` is echoed only\n * when allow-listed (never `*`); credentials are added only when opted in — the\n * same fail-closed posture as the core `_events` route (#1861).\n */\nfunction corsHeaders(\n request: Request,\n allowedOrigins: string[] | undefined,\n allowCredentials: boolean,\n): Record<string, string> {\n if (!allowedOrigins) return {};\n const origin = request.headers.get('origin');\n if (!origin || !allowedOrigins.includes(origin)) return {};\n const headers: Record<string, string> = {\n 'Access-Control-Allow-Origin': origin,\n Vary: 'Origin',\n };\n if (allowCredentials) {\n headers['Access-Control-Allow-Credentials'] = 'true';\n }\n return headers;\n}\n","import { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n VoiceGatewayTurnOptions,\n VoiceGatewayTurnStatus,\n} from '../types.js';\n\n/**\n * Durable reservation for a gateway turn. The natural key makes `turn_id`\n * replay checks concurrency-safe before transcript messages are written.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'voice_gateway_turns',\n conflictColumns: ['voice_session_id', 'gateway_turn_id'],\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: false,\n})\nexport class VoiceGatewayTurn extends SmrtObject {\n @tenantId()\n tenantId: string = '';\n\n @foreignKey('VoiceSession', { required: true })\n voiceSessionId: string = '';\n\n @field({ required: true })\n gatewaySessionId: string = '';\n\n @field({ required: true })\n gatewayTurnId: string = '';\n\n @field({ required: true })\n target: string = 'smrt:chat';\n\n @field({ required: true })\n status: VoiceGatewayTurnStatus = 'processing';\n\n @field()\n completedAt: Date | null = null;\n\n @field()\n failedAt: Date | null = null;\n\n constructor(options: VoiceGatewayTurnOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.voiceSessionId !== undefined)\n this.voiceSessionId = options.voiceSessionId;\n if (options.gatewaySessionId !== undefined)\n this.gatewaySessionId = options.gatewaySessionId;\n if (options.gatewayTurnId !== undefined)\n this.gatewayTurnId = options.gatewayTurnId;\n if (options.target !== undefined) this.target = options.target;\n if (options.status !== undefined) this.status = options.status;\n if (options.completedAt !== undefined)\n this.completedAt = options.completedAt;\n if (options.failedAt !== undefined) this.failedAt = options.failedAt;\n }\n\n async complete(now: Date = new Date()): Promise<void> {\n this.status = 'completed';\n this.completedAt = now;\n this.failedAt = null;\n await this.save();\n }\n\n async fail(now: Date = new Date()): Promise<void> {\n this.status = 'failed';\n this.failedAt = now;\n await this.save();\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { VoiceGatewayTurn } from '../models/VoiceGatewayTurn.js';\n\nexport class VoiceGatewayTurnCollection extends SmrtCollection<VoiceGatewayTurn> {\n static readonly _itemClass = VoiceGatewayTurn;\n\n async reserveTurn(input: {\n tenantId: string;\n voiceSessionId: string;\n gatewaySessionId: string;\n gatewayTurnId: string;\n target: string;\n }): Promise<VoiceGatewayTurn> {\n return this.create({\n ...input,\n status: 'processing',\n _insertOnly: true,\n });\n }\n}\n","import type { AIInterface, AIMessage } from '@happyvertical/ai';\nimport type { PrincipalAuditSink } from '@happyvertical/smrt-agents';\nimport {\n type SmrtClassOptions,\n ValidationError,\n} from '@happyvertical/smrt-core';\nimport { VoiceGatewayTurnCollection } from './collections/VoiceGatewayTurnCollection.js';\nimport { VoiceSessionCollection } from './collections/VoiceSessionCollection.js';\nimport type { ChatMessage } from './models/ChatMessage.js';\nimport type { VoiceGatewayTurn } from './models/VoiceGatewayTurn.js';\nimport type { VoiceSession } from './models/VoiceSession.js';\nimport {\n bindPersonaToSession,\n type ConversationPersona,\n type PersonaRecallOptions,\n runPersonaConversationTurn,\n} from './persona-conversation.js';\nimport type { ChatService } from './services/index.js';\n\nexport const SMRT_CHAT_VOICE_TARGET = 'smrt:chat';\nexport const MAX_VOICE_GATEWAY_TEXT_LENGTH = 12_000;\nconst DEFAULT_VOICE_SESSION_TTL_SECONDS = 10 * 60;\nconst DEFAULT_HISTORY_LIMIT = 24;\n\nexport interface VoiceGatewayTurnMetadata {\n tenantId?: string;\n actorProfileId?: string;\n chatRoomId?: string;\n threadId?: string;\n agentSessionId?: string;\n personaId?: string;\n voiceSessionId?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface VoiceGatewayTurnPayload {\n session_id: string;\n turn_id: string;\n target: string;\n actor?: string;\n text: string;\n metadata?: VoiceGatewayTurnMetadata;\n}\n\nexport interface VoiceGatewayTurnResponseMetadata {\n tenantId: string;\n chatRoomId: string;\n threadId: string | null;\n agentSessionId: string;\n userMessageId: string;\n assistantMessageId: string | null;\n personaId: string;\n correlationId: string;\n voiceSessionId: string;\n source: string;\n}\n\nexport interface VoiceGatewayTurnResponse {\n session_id: string;\n turn_id: string;\n text: string;\n metadata: VoiceGatewayTurnResponseMetadata;\n}\n\nexport interface CreateVoiceChatSessionOptions {\n chatService: ChatService;\n db: SmrtClassOptions['db'];\n tenantId: string;\n actorProfileId: string;\n actorUserId?: string | null;\n persona: ConversationPersona;\n /** Existing agent session to bind voice to. Omit to create/reuse one. */\n agentSessionId?: string;\n /** Agent profile/session author id when creating a new agent session. */\n agentId?: string;\n /** Optional existing thread within the bound session room. */\n threadId?: string | null;\n /** Stable gateway-level `session_id`. Generated when omitted. */\n gatewaySessionId?: string;\n /** Subject key forwarded to ChatService.createAgentSession(). */\n sessionKey?: string | null;\n /** Voice binding TTL. Ignored when `expiresAt` is supplied. */\n ttlSeconds?: number;\n expiresAt?: Date;\n metadata?: Record<string, unknown>;\n target?: string;\n maxTokens?: number;\n maxMessages?: number;\n instructions?: string;\n}\n\nexport interface VoiceChatSessionCreationResult {\n voiceSession: VoiceSession;\n voiceSessionId: string;\n gatewaySessionId: string;\n expiresAt: Date;\n tenantId: string;\n actorProfileId: string;\n personaId: string;\n agentSessionId: string;\n chatRoomId: string;\n threadId: string | null;\n metadata: VoiceGatewayTurnMetadata;\n}\n\nexport interface HandleVoiceGatewayTurnOptions {\n chatService: ChatService;\n db: SmrtClassOptions['db'];\n ai: AIInterface;\n payload: VoiceGatewayTurnPayload;\n now?: Date;\n historyLimit?: number;\n recall?: PersonaRecallOptions | false;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n maxSteps?: number;\n postgresRls?: boolean;\n audit?: PrincipalAuditSink;\n}\n\nexport interface VoiceGatewayTurnHandlerOptions\n extends Omit<HandleVoiceGatewayTurnOptions, 'payload'> {\n gatewayToken: string | (() => string | Promise<string>);\n}\n\nexport class VoiceGatewayError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = 'VoiceGatewayError';\n this.status = status;\n this.code = code;\n }\n}\n\nexport class VoiceGatewayBadRequestError extends VoiceGatewayError {\n constructor(message: string) {\n super(message, 400, 'voice_gateway_bad_request');\n this.name = 'VoiceGatewayBadRequestError';\n }\n}\n\nexport class VoiceGatewayUnauthorizedError extends VoiceGatewayError {\n constructor(message = 'Voice gateway authorization failed') {\n super(message, 401, 'voice_gateway_unauthorized');\n this.name = 'VoiceGatewayUnauthorizedError';\n }\n}\n\nexport class VoiceSessionRejectedError extends VoiceGatewayError {\n constructor(message: string) {\n super(message, 403, 'voice_session_rejected');\n this.name = 'VoiceSessionRejectedError';\n }\n}\n\nexport class VoiceSessionExpiredError extends VoiceGatewayError {\n constructor(message = 'Voice session is expired') {\n super(message, 410, 'voice_session_expired');\n this.name = 'VoiceSessionExpiredError';\n }\n}\n\nexport class VoiceGatewayReplayError extends VoiceGatewayError {\n constructor(message = 'Voice gateway turn has already been processed') {\n super(message, 409, 'voice_gateway_replay');\n this.name = 'VoiceGatewayReplayError';\n }\n}\n\nexport async function createVoiceChatSession(\n options: CreateVoiceChatSessionOptions,\n): Promise<VoiceChatSessionCreationResult> {\n const target = options.target ?? SMRT_CHAT_VOICE_TARGET;\n const personaId = requireNonEmptyString(\n options.persona.id,\n 'createVoiceChatSession requires a persisted persona id',\n );\n if (\n options.persona.tenantId !== null &&\n options.persona.tenantId !== options.tenantId\n ) {\n throw new VoiceSessionRejectedError(\n 'Persona tenant does not match the voice session tenant',\n );\n }\n\n const persona: ConversationPersona = {\n ...options.persona,\n id: personaId,\n tenantId: options.tenantId,\n };\n\n let agentSession = options.agentSessionId\n ? await options.chatService.getAgentSession({\n agentSessionId: options.agentSessionId,\n tenantId: options.tenantId,\n })\n : null;\n\n if (options.agentSessionId) {\n if (!agentSession) {\n throw new VoiceSessionRejectedError('Agent session not found');\n }\n assertAgentSessionMatchesActor(agentSession, options.actorProfileId);\n assertActiveAgentSession(agentSession);\n } else {\n const agentId =\n options.agentId ??\n persona.actsAsProfileId ??\n persona.id ??\n persona.agentClass;\n if (!agentId) {\n throw new VoiceGatewayBadRequestError(\n 'createVoiceChatSession requires agentId when the persona has no acting profile or id',\n );\n }\n const created = await options.chatService.createAgentSession({\n tenantId: options.tenantId,\n agentId,\n actorProfileId: options.actorProfileId,\n allowedTools: persona.allowedTools,\n systemPrompt: options.instructions ?? persona.instructions,\n maxTokens: options.maxTokens,\n maxMessages: options.maxMessages,\n sessionKey: options.sessionKey,\n });\n agentSession = created.session;\n }\n\n const boundSession = await bindPersonaToSession({\n chatService: options.chatService,\n session: agentSession,\n actorProfileId: options.actorProfileId,\n tenantId: options.tenantId,\n persona,\n instructions: options.instructions,\n db: options.db,\n });\n assertActiveAgentSession(boundSession);\n\n const chatRoomId = requireNonEmptyString(\n boundSession.chatRoomId,\n 'Agent session has no chat room',\n );\n await options.chatService.getRoomForMember(\n chatRoomId,\n options.actorProfileId,\n options.tenantId,\n );\n\n const threadId = options.threadId ?? null;\n if (threadId) {\n const thread = await options.chatService.getThread({\n threadId,\n tenantId: options.tenantId,\n });\n if (!thread || thread.roomId !== chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'threadId does not belong to the bound agent session room',\n );\n }\n }\n\n const voiceSessions = await VoiceSessionCollection.create({ db: options.db });\n const gatewaySessionId = options.gatewaySessionId ?? crypto.randomUUID();\n const expiresAt =\n options.expiresAt ??\n new Date(\n Date.now() +\n (options.ttlSeconds ?? DEFAULT_VOICE_SESSION_TTL_SECONDS) * 1000,\n );\n\n const voiceSession = await voiceSessions.create({\n tenantId: options.tenantId,\n gatewaySessionId,\n actorProfileId: options.actorProfileId,\n actorUserId: options.actorUserId ?? null,\n personaId,\n agentSessionId: boundSession.id as string,\n chatRoomId,\n threadId,\n target,\n status: 'active',\n expiresAt,\n personaSnapshot: JSON.stringify(persona),\n metadata: JSON.stringify(options.metadata ?? {}),\n });\n\n const voiceSessionId = voiceSession.id as string;\n return {\n voiceSession,\n voiceSessionId,\n gatewaySessionId,\n expiresAt,\n tenantId: options.tenantId,\n actorProfileId: options.actorProfileId,\n personaId,\n agentSessionId: boundSession.id as string,\n chatRoomId,\n threadId,\n metadata: {\n tenantId: options.tenantId,\n actorProfileId: options.actorProfileId,\n chatRoomId,\n threadId: threadId ?? undefined,\n agentSessionId: boundSession.id as string,\n personaId,\n voiceSessionId,\n source: 'smrt-chat',\n },\n };\n}\n\nexport async function handleVoiceGatewayTurn(\n options: HandleVoiceGatewayTurnOptions,\n): Promise<VoiceGatewayTurnResponse> {\n const payload = normalizeGatewayPayload(options.payload);\n const metadata = payload.metadata ?? {};\n const voiceSessionId = requireNonEmptyString(\n metadata.voiceSessionId,\n 'Voice gateway payload metadata.voiceSessionId is required',\n );\n\n const voiceSessions = await VoiceSessionCollection.create({ db: options.db });\n const voiceSession = await voiceSessions.get({\n id: voiceSessionId,\n target: payload.target,\n });\n if (!voiceSession) {\n throw new VoiceSessionRejectedError('Voice session not found');\n }\n if (voiceSession.isExpired(options.now)) {\n if (voiceSession.status === 'active') {\n await voiceSession.expire();\n }\n throw new VoiceSessionExpiredError();\n }\n if (!voiceSession.isActive(options.now)) {\n throw new VoiceSessionRejectedError('Voice session is not active');\n }\n if (voiceSession.hasProcessedTurn(payload.turn_id)) {\n throw new VoiceGatewayReplayError();\n }\n\n validateGatewayPayloadAgainstBinding(payload, voiceSession);\n const persona = requireVoiceSessionPersona(voiceSession);\n\n const agentSession = await options.chatService.getAgentSession({\n agentSessionId: voiceSession.agentSessionId,\n tenantId: voiceSession.tenantId,\n });\n if (!agentSession) {\n throw new VoiceSessionRejectedError('Bound agent session not found');\n }\n assertAgentSessionMatchesActor(agentSession, voiceSession.actorProfileId);\n assertActiveAgentSession(agentSession);\n if (agentSession.chatRoomId !== voiceSession.chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'Bound agent session no longer belongs to the voice session room',\n );\n }\n\n if (voiceSession.threadId) {\n const thread = await options.chatService.getThread({\n threadId: voiceSession.threadId,\n tenantId: voiceSession.tenantId,\n });\n if (!thread || thread.roomId !== voiceSession.chatRoomId) {\n throw new VoiceSessionRejectedError(\n 'Bound thread no longer belongs to the voice session room',\n );\n }\n }\n\n const gatewayTurn = await reserveVoiceGatewayTurn(\n options.db,\n voiceSession,\n payload,\n );\n\n try {\n const history = await loadConversationHistory({\n chatService: options.chatService,\n tenantId: voiceSession.tenantId,\n actorProfileId: voiceSession.actorProfileId,\n roomId: voiceSession.chatRoomId,\n threadId: voiceSession.threadId,\n limit: options.historyLimit ?? DEFAULT_HISTORY_LIMIT,\n });\n const correlationId = crypto.randomUUID();\n const commonMetadata = {\n source: 'voice-gateway',\n target: payload.target,\n voiceSessionId,\n gatewaySessionId: payload.session_id,\n gatewayTurnId: payload.turn_id,\n correlationId,\n };\n\n const userMessage = await options.chatService.sendAgentUserMessage({\n tenantId: voiceSession.tenantId,\n agentSessionId: voiceSession.agentSessionId,\n actorProfileId: voiceSession.actorProfileId,\n content: payload.text,\n });\n await mergeAndSaveMetadata(userMessage, {\n ...commonMetadata,\n voiceRole: 'transcript',\n });\n\n const turn = await runPersonaConversationTurn({\n ai: options.ai,\n db: options.db,\n persona,\n tenantId: voiceSession.tenantId,\n userMessage: payload.text,\n history,\n chatService: options.chatService,\n session: agentSession,\n threadId: voiceSession.threadId,\n recall: options.recall,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n maxSteps: options.maxSteps,\n postgresRls: options.postgresRls,\n audit: options.audit,\n onBehalfOfUserId: voiceSession.actorUserId ?? undefined,\n correlationId,\n });\n\n for (const toolMessage of turn.authoredMessages?.toolMessages ?? []) {\n await mergeAndSaveMetadata(toolMessage, {\n ...commonMetadata,\n voiceRole: 'tool',\n });\n }\n const assistantMessage = turn.authoredMessages?.assistantMessage ?? null;\n if (assistantMessage) {\n await mergeAndSaveMetadata(assistantMessage, {\n ...commonMetadata,\n voiceRole: 'assistant',\n });\n }\n\n voiceSession.recordGatewayTurn(payload.turn_id);\n await voiceSession.save();\n await gatewayTurn.complete();\n\n return {\n session_id: payload.session_id,\n turn_id: payload.turn_id,\n text: turn.result.content,\n metadata: {\n tenantId: voiceSession.tenantId,\n chatRoomId: voiceSession.chatRoomId,\n threadId: voiceSession.threadId,\n agentSessionId: voiceSession.agentSessionId,\n userMessageId: userMessage.id as string,\n assistantMessageId:\n (assistantMessage?.id as string | undefined) ?? null,\n personaId: voiceSession.personaId,\n correlationId: turn.correlationId,\n voiceSessionId,\n source: 'voice-gateway',\n },\n };\n } catch (error) {\n await markVoiceGatewayTurnFailed(gatewayTurn);\n throw error;\n }\n}\n\nexport function createVoiceGatewayTurnHandler(\n options: VoiceGatewayTurnHandlerOptions,\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n try {\n if (request.method !== 'POST') {\n return jsonResponse({ error: 'Method not allowed' }, 405);\n }\n await assertVoiceGatewayBearer(\n request.headers,\n await resolveGatewayToken(options.gatewayToken),\n );\n const payload = normalizeGatewayPayload(await request.json());\n const response = await handleVoiceGatewayTurn({ ...options, payload });\n return jsonResponse(response, 200);\n } catch (error) {\n const status = error instanceof VoiceGatewayError ? error.status : 500;\n const message =\n error instanceof Error ? error.message : 'Voice gateway turn failed';\n const code =\n error instanceof VoiceGatewayError ? error.code : 'voice_gateway_error';\n return jsonResponse({ error: message, code }, status);\n }\n };\n}\n\nexport async function assertVoiceGatewayBearer(\n headers: Headers,\n expectedToken: string,\n): Promise<void> {\n if (!expectedToken) {\n throw new VoiceGatewayUnauthorizedError(\n 'Voice gateway token is not configured',\n );\n }\n const authorization = headers.get('authorization') ?? '';\n const match = authorization.match(/^Bearer\\s+(.+)$/i);\n const actual = match?.[1] ?? '';\n if (!constantTimeEquals(actual, expectedToken)) {\n throw new VoiceGatewayUnauthorizedError();\n }\n}\n\nasync function resolveGatewayToken(\n token: string | (() => string | Promise<string>),\n): Promise<string> {\n return typeof token === 'function' ? token() : token;\n}\n\nfunction normalizeGatewayPayload(input: unknown): VoiceGatewayTurnPayload {\n if (!isRecord(input)) {\n throw new VoiceGatewayBadRequestError('Voice gateway payload must be JSON');\n }\n const sessionId = requireNonEmptyString(\n input.session_id,\n 'Voice gateway payload session_id is required',\n );\n const turnId = requireNonEmptyString(\n input.turn_id,\n 'Voice gateway payload turn_id is required',\n );\n const target = requireNonEmptyString(\n input.target,\n 'Voice gateway payload target is required',\n );\n if (target !== SMRT_CHAT_VOICE_TARGET) {\n throw new VoiceGatewayBadRequestError(\n `Unsupported voice gateway target '${target}'`,\n );\n }\n const text = requireNonEmptyString(\n input.text,\n 'Voice gateway payload text is required',\n );\n if (text.length > MAX_VOICE_GATEWAY_TEXT_LENGTH) {\n throw new VoiceGatewayBadRequestError(\n `Voice gateway payload text must be ${MAX_VOICE_GATEWAY_TEXT_LENGTH} characters or fewer`,\n );\n }\n const metadata = input.metadata;\n if (metadata !== undefined && !isRecord(metadata)) {\n throw new VoiceGatewayBadRequestError(\n 'Voice gateway payload metadata must be an object',\n );\n }\n\n return {\n session_id: sessionId,\n turn_id: turnId,\n target,\n actor:\n typeof input.actor === 'string' && input.actor.length > 0\n ? input.actor\n : undefined,\n text,\n metadata: metadata as VoiceGatewayTurnMetadata | undefined,\n };\n}\n\nfunction validateGatewayPayloadAgainstBinding(\n payload: VoiceGatewayTurnPayload,\n voiceSession: VoiceSession,\n): void {\n if (payload.session_id !== voiceSession.gatewaySessionId) {\n throw new VoiceSessionRejectedError(\n 'Gateway session_id does not match the voice session binding',\n );\n }\n const metadata = payload.metadata ?? {};\n assertMetadataMatches(\n metadata,\n 'tenantId',\n voiceSession.tenantId,\n 'tenantId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'actorProfileId',\n voiceSession.actorProfileId,\n 'actorProfileId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'chatRoomId',\n voiceSession.chatRoomId,\n 'chatRoomId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'threadId',\n voiceSession.threadId,\n 'threadId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'agentSessionId',\n voiceSession.agentSessionId,\n 'agentSessionId does not match the voice session binding',\n );\n assertMetadataMatches(\n metadata,\n 'personaId',\n voiceSession.personaId,\n 'personaId does not match the voice session binding',\n );\n}\n\nfunction requireVoiceSessionPersona(\n voiceSession: VoiceSession,\n): ConversationPersona {\n const persona = voiceSession.getPersonaSnapshot();\n if (!hasNonEmptyString(persona.id)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has no persona id',\n );\n }\n if (persona.id !== voiceSession.personaId) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot does not match the voice session persona',\n );\n }\n if (persona.tenantId !== voiceSession.tenantId) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot tenant does not match the voice session tenant',\n );\n }\n if (!hasNonEmptyString(persona.runAsUserId)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has no runnable user',\n );\n }\n if (!Array.isArray(persona.allowedTools)) {\n throw new VoiceSessionRejectedError(\n 'Voice session persona snapshot has an invalid tool allow-list',\n );\n }\n return persona;\n}\n\nasync function reserveVoiceGatewayTurn(\n db: SmrtClassOptions['db'],\n voiceSession: VoiceSession,\n payload: VoiceGatewayTurnPayload,\n): Promise<VoiceGatewayTurn> {\n const gatewayTurns = await VoiceGatewayTurnCollection.create({ db });\n try {\n return await gatewayTurns.reserveTurn({\n tenantId: voiceSession.tenantId,\n voiceSessionId: voiceSession.id as string,\n gatewaySessionId: payload.session_id,\n gatewayTurnId: payload.turn_id,\n target: payload.target,\n });\n } catch (error) {\n if (isUniqueConstraintError(error)) {\n throw new VoiceGatewayReplayError();\n }\n throw error;\n }\n}\n\nasync function markVoiceGatewayTurnFailed(\n gatewayTurn: VoiceGatewayTurn,\n): Promise<void> {\n try {\n await gatewayTurn.fail();\n } catch {\n // Preserve the original turn failure for the gateway response.\n }\n}\n\nfunction isUniqueConstraintError(error: unknown): boolean {\n return (\n error instanceof ValidationError &&\n error.code === 'VALIDATION_UNIQUE_CONSTRAINT'\n );\n}\n\nfunction assertMetadataMatches(\n metadata: VoiceGatewayTurnMetadata,\n key: keyof VoiceGatewayTurnMetadata,\n expected: string | null,\n message: string,\n): void {\n const actual = metadata[key];\n if (actual === undefined || actual === null || actual === '') {\n return;\n }\n if (typeof actual !== 'string' || actual !== expected) {\n throw new VoiceSessionRejectedError(message);\n }\n}\n\nfunction assertAgentSessionMatchesActor(\n session: { participantProfileId: string },\n actorProfileId: string,\n): void {\n if (session.participantProfileId !== actorProfileId) {\n throw new VoiceSessionRejectedError(\n 'Agent session participant does not match the voice session actor',\n );\n }\n}\n\nfunction assertActiveAgentSession(session: {\n isActive: () => boolean;\n chatRoomId: string | null;\n}): void {\n if (!session.isActive()) {\n throw new VoiceSessionRejectedError('Agent session is not active');\n }\n if (!session.chatRoomId) {\n throw new VoiceSessionRejectedError('Agent session has no chat room');\n }\n}\n\nasync function loadConversationHistory(input: {\n chatService: ChatService;\n tenantId: string;\n actorProfileId: string;\n roomId: string;\n threadId: string | null;\n limit: number;\n}): Promise<AIMessage[]> {\n const messages = input.threadId\n ? await input.chatService.getThreadMessages({\n threadId: input.threadId,\n actorProfileId: input.actorProfileId,\n tenantId: input.tenantId,\n limit: input.limit,\n })\n : (\n await input.chatService.getRoomMessages({\n roomId: input.roomId,\n actorProfileId: input.actorProfileId,\n tenantId: input.tenantId,\n limit: input.limit,\n })\n ).reverse();\n\n return messages\n .map(chatMessageToAIMessage)\n .filter((message): message is AIMessage => message !== null);\n}\n\nfunction chatMessageToAIMessage(message: ChatMessage): AIMessage | null {\n if (\n message.role !== 'user' &&\n message.role !== 'assistant' &&\n message.role !== 'system'\n ) {\n return null;\n }\n return { role: message.role, content: message.content } as AIMessage;\n}\n\nasync function mergeAndSaveMetadata(\n message: ChatMessage,\n metadata: Record<string, unknown>,\n): Promise<void> {\n message.setMetadata({ ...message.getMetadata(), ...metadata });\n await message.save();\n}\n\nfunction requireNonEmptyString(value: unknown, message: string): string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new VoiceGatewayBadRequestError(message);\n }\n return value;\n}\n\nfunction hasNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction constantTimeEquals(actual: string, expected: string): boolean {\n let diff = actual.length ^ expected.length;\n const length = Math.max(actual.length, expected.length);\n for (let index = 0; index < length; index++) {\n const actualCode = index < actual.length ? actual.charCodeAt(index) : 0;\n const expectedCode =\n index < expected.length ? expected.charCodeAt(index) : 0;\n diff |= actualCode ^ expectedCode;\n }\n return diff === 0;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":"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"}
|