import type { AgentWidgetPlugin } from "./plugins/types"; import type { DeepPartial, PersonaTheme } from "./types/theme"; import type { RuntypeClientChatRequest, RuntypeClientFeedbackRequest, RuntypeStopReasonKind, } from "./generated/runtype-openapi-contract"; import type { TargetResolver } from "./utils/target"; export type { TargetResolver, ResolvedTarget } from "./utils/target"; // ============================================================================ // Multi-Modal Content Types // ============================================================================ /** * Text content part for multi-modal messages */ export type TextContentPart = { type: 'text'; text: string; }; /** * Image content part for multi-modal messages * Supports base64 data URIs or URLs */ export type ImageContentPart = { type: 'image'; image: string; // base64 data URI or URL mimeType?: string; alt?: string; // optional alt text for accessibility }; /** * File content part for multi-modal messages * Supports PDF, TXT, DOCX, and other document types */ export type FileContentPart = { type: 'file'; data: string; // base64 data URI or URL mimeType: string; filename: string; }; /** * Audio content part for multi-modal messages * Supports base64 data URIs or URLs */ export type AudioContentPart = { type: 'audio'; audio: string; // base64 data URI or URL mimeType?: string; }; /** * Video content part for multi-modal messages * Supports base64 data URIs or URLs */ export type VideoContentPart = { type: 'video'; video: string; // base64 data URI or URL mimeType?: string; }; /** * Union type for all content part types */ export type ContentPart = | TextContentPart | ImageContentPart | FileContentPart | AudioContentPart | VideoContentPart; /** * Message content can be a simple string or an array of content parts */ export type MessageContent = string | ContentPart[]; // ============================================================================ // Context and Middleware Types // ============================================================================ export type AgentWidgetContextProviderContext = { messages: AgentWidgetMessage[]; config: AgentWidgetConfig; }; export type AgentWidgetContextProvider = ( context: AgentWidgetContextProviderContext ) => | Record | void | Promise | void>; export type AgentWidgetRequestPayloadMessage = { role: AgentWidgetMessageRole; content: MessageContent; createdAt: string; }; export type AgentWidgetRequestPayload = { messages: AgentWidgetRequestPayloadMessage[]; flowId?: string; agent?: { agentId: string }; context?: Record; metadata?: Record; /** Per-turn template variables for /v1/client/chat (merged as root-level {{var}} in Runtype). */ inputs?: Record; /** * Per-turn page-discovered tools (WebMCP). Sent to Runtype's dispatch so the * agent can call them as `webmcp:`. The widget snapshots * `document.modelContext.__getRegisteredTools()` each turn and ships only * the JSON-serializable surface (no `execute`). */ clientTools?: ClientToolDefinition[]; }; // ============================================================================ // Agent Execution Types // ============================================================================ /** * Configuration for agent loop behavior. */ export type AgentLoopConfig = { /** Maximum number of agent turns (1-100). The loop continues while the model calls tools. */ maxTurns: number; /** Maximum cost budget in USD. Agent stops when exceeded. */ maxCost?: number; /** Enable periodic reflection during execution */ enableReflection?: boolean; /** Number of iterations between reflections (1-50) */ reflectionInterval?: number; }; /** * Configuration for agent tools (search, code execution, MCP servers, etc.) */ export type AgentToolsConfig = { /** Tool IDs to enable (e.g., "builtin:exa", "builtin:dalle", "builtin:openai_web_search") */ toolIds?: string[]; /** Per-tool configuration overrides keyed by tool ID */ toolConfigs?: Record>; /** Inline tool definitions for runtime-defined tools */ runtimeTools?: Array>; /** Custom MCP server connections */ mcpServers?: Array>; /** Maximum number of tool invocations per execution */ maxToolCalls?: number; /** How the model is steered toward tools: let it decide, force a call, or disable */ toolCallStrategy?: "auto" | "required" | "none"; /** Per-tool invocation limits / requirements keyed by tool name */ perToolLimits?: Record; /** Tool approval configuration for human-in-the-loop workflows */ approval?: { /** Tool names/patterns to require approval for, or true for all tools */ require: string[] | boolean; /** Approval timeout in milliseconds (default: 300000 / 5 minutes) */ timeout?: number; /** Ask the agent to state its intent alongside approval requests (default: true) */ requestReason?: boolean; }; /** * Enables the synthesized `spawn_subagent` tool: the model can spin up * ad-hoc child agents at runtime, restricted to `toolPool` (tool IDs / * runtime-tool names already granted to the parent agent). */ subagentConfig?: { toolPool: string[]; defaultMaxTurns?: number; maxTurnsLimit?: number; maxSpawnsPerRun?: number; defaultModel?: string; allowNesting?: boolean; defaultTimeoutMs?: number; }; /** * Enables the synthesized `code_mode` tool: the model writes JS that calls * pool tools inside a sandbox instead of issuing individual tool calls. */ codeModeConfig?: { toolPool: string[]; description?: string; timeoutMs?: number; }; }; /** Artifact kinds for the Persona sidebar and dispatch payload */ export type PersonaArtifactKind = "markdown" | "component"; /** * Agent configuration for agent execution mode. * When provided in the widget config, enables agent loop execution instead of flow dispatch. */ export type ArtifactConfigPayload = { enabled: true; types: PersonaArtifactKind[]; }; export type AgentConfig = { /** Agent display name */ name: string; /** Model identifier (e.g., 'openai:gpt-4o-mini', 'qwen/qwen3-8b') */ model: string; /** System prompt for the agent */ systemPrompt: string; /** Temperature for model responses */ temperature?: number; /** Tool configuration for the agent */ tools?: AgentToolsConfig; /** Persona artifacts: sibling of tools (virtual agent / API parity) */ artifacts?: ArtifactConfigPayload; /** Loop configuration for multi-turn execution */ loopConfig?: AgentLoopConfig; }; export type SavedAgentConfig = { /** Persisted Runtype agent ID to execute. */ agentId: string; }; /** * Options for agent execution requests. */ export type AgentRequestOptions = { /** Whether to stream the response (should be true for widget usage) */ streamResponse?: boolean; /** Record mode: 'virtual' for no persistence, 'existing'/'create' for database records */ recordMode?: 'virtual' | 'existing' | 'create'; /** Whether to store results server-side */ storeResults?: boolean; /** Enable debug mode for additional event data */ debugMode?: boolean; }; /** * Request payload for agent execution mode. */ export type AgentWidgetAgentRequestPayload = { agent: AgentConfig | SavedAgentConfig; messages: AgentWidgetRequestPayloadMessage[]; options: AgentRequestOptions; context?: Record; metadata?: Record; /** * Per-turn page-discovered tools (WebMCP): same shape as * `AgentWidgetRequestPayload.clientTools`. */ clientTools?: ClientToolDefinition[]; }; // ============================================================================ // WebMCP Types (page-discovered tools shipped per dispatch) // ============================================================================ /** * Wire shape for a single client-discovered tool sent on `dispatch.clientTools[]`. * * Mirrors the SDK's `ClientToolDefinition` in `@runtypelabs/sdk`. Only the * JSON-serializable surface of a WebMCP tool: the `execute` function stays * client-side; the server merges these into the agent's tool catalog under * the `webmcp:` namespace. */ export type ClientToolDefinition = { /** Bare tool name; the server prepends `webmcp:` on the wire. */ name: string; description: string; /** JSON Schema (per WebMCP spec): passed through as-is. */ parametersSchema?: object; /** * `'webmcp'` for tools discovered via the polyfill (server prepends the * `webmcp:` wire prefix); `'sdk'` for widget/SDK-provided tools (name stays * bare on the wire). Matches the server's accepted enum: any other value * fails dispatch validation. */ origin?: 'webmcp' | 'sdk'; /** Origin of the page that registered the tool: for server-side audit. */ pageOrigin?: string; /** * WebMCP `Tool.annotations` (spec). Not used for gating server-side; the * widget reads these client-side. Forwarded so traces/dashboards can show * `readOnlyHint` / `untrustedContentHint` on tool-call records. */ annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean; }; }; /** * Information passed to the confirm-bubble handler before a `webmcp:*` tool * call executes. Every WebMCP tool routes through this single gate. */ export type WebMcpConfirmInfo = { /** Bare tool name (no `webmcp:` prefix). */ toolName: string; args: unknown; description?: string; /** * Display title the tool declared via the WebMCP spec's * `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool * didn't declare one. */ title?: string; annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean; }; /** * Why the confirm was requested. Currently always `'gate'`: the default * confirm-by-default gate that fires before every `webmcp:*` call. (The * `@mcp-b/webmcp-polyfill` owns the spec's `requestUserInteraction` callback * internally, so Persona no longer surfaces a nested in-tool confirm.) */ reason: 'gate'; }; /** * Resolves to `true` if the user approves the tool call; `false` to decline. */ export type WebMcpConfirmHandler = (info: WebMcpConfirmInfo) => Promise; /** * Persona's normalized tool-result shape sent back to the agent on `/resume`. * Mirrors the MCP `CallToolResult` content shape; arbitrary `execute()` return * values are wrapped as a single text block at the bridge boundary. */ export type WebMcpToolResult = { content: Array< | { type: 'text'; text: string } | { type: string;[key: string]: unknown } >; isError?: boolean; /** Pass-through of the tool's `annotations.untrustedContentHint`. */ annotations?: { untrustedContentHint?: boolean; }; }; /** * Widget-level WebMCP configuration. Set `enabled: true` to opt in. The * surface's server-side `webmcp` policy is the source of truth for which * tools are accepted: these client-side options are convenience filters. */ export type AgentWidgetWebMcpConfig = { /** Master switch. Default: `false` (widget never installs the polyfill). */ enabled?: boolean; /** * Glob-ish name patterns to include client-side. `'*'` matches any chars * except `:`. Patterns are matched against the bare tool name (no `webmcp:` * prefix). If unset, all registered tools are included. */ allowlist?: string[]; /** * Per-tool gate policy. Called before the confirm gate for every * `webmcp:*` call; return `true` to approve immediately and skip the * confirmation UI entirely. Use this to auto-allow read-only tools (e.g. * a catalog search) while still gating mutating ones. Only consulted on * the default-UI path: a custom `onConfirm` takes full control instead. */ autoApprove?: (info: WebMcpConfirmInfo) => boolean; /** * Confirm gate handler. When omitted, Persona renders its native in-panel * approval bubble (the same chrome used for server-driven tool approvals) * and resolves on the user's Approve/Deny click. Supply this to override * with a custom confirmer (e.g. a route-level modal). The legacy * `window.confirm` fallback only applies when no widget UI is attached. */ onConfirm?: WebMcpConfirmHandler; }; /** * Agent execution state tracking. */ export type AgentExecutionState = { executionId: string; agentId: string; agentName: string; status: 'running' | 'complete' | 'error'; currentIteration: number; maxTurns: number; startedAt?: number; completedAt?: number; stopReason?: 'complete' | 'end_turn' | 'max_turns' | 'max_cost' | 'timeout' | 'error'; }; /** * Metadata attached to messages created during agent execution. */ export type AgentMessageMetadata = { executionId?: string; iteration?: number; turnId?: string; agentName?: string; /** * When this message was produced by a step inside a nested flow executed * as a tool, identifies the parent tool call id. Enables renderers to * visually group or indent nested-flow output under its parent tool. */ parentToolId?: string; /** * Nested flow step id that produced this message (e.g. a `send-stream` * or `prompt` step inside the nested flow). Stable key for that step. */ parentStepId?: string; /** * Set to `true` on a tool-variant message produced from a `step_await` * event (`awaitReason: "local_tool_required"`). Signals to UI code that * the tool call is a LOCAL tool and the server is paused waiting for a * `POST /v1/dispatch/resume` with the user's answer keyed by tool name. */ awaitingLocalTool?: boolean; /** * The provider per-call id (`toolu_…`) carried on the `step_await` / * `flow_await` events for a LOCAL tool (core#3878). Present only when the * server emits it. Two PARALLEL calls to the same tool in one turn share a * `toolName` (and a collapsed `toolId`) but get DISTINCT `webMcpToolCallId`s, * so this is the key the widget batches a single `/resume` on: preferred * over tool name, which collides for same-tool parallel calls. Absent → * fall back to the legacy name-keyed resume contract. */ webMcpToolCallId?: string; /** * Set to `true` once the user has picked / typed / dismissed an answer for * an `ask_user_question` tool call, so renderers stop re-mounting the * answer-pill sheet for this tool call on subsequent render passes. */ askUserQuestionAnswered?: boolean; /** * In-progress answers for a multi-question `ask_user_question` payload, * keyed by question text. Persisted across refresh so the user lands back * where they were if the page reloads mid-flow. Cleared once * `askUserQuestionAnswered` flips to `true`. */ askUserQuestionAnswers?: Record; /** * Current page index for a multi-question `ask_user_question` payload's * paginated stepper. Persists alongside `askUserQuestionAnswers`. */ askUserQuestionIndex?: number; /** * Set to `true` once a `suggest_replies` tool call's fire-and-forget * `/resume` has been accepted by the server. Persisted belt-and-suspenders * mirror of the in-memory resolved-key dedupe, so hydration/re-emit paths * never re-resume the call. */ suggestRepliesResolved?: boolean; }; export type AgentWidgetRequestMiddlewareContext = { payload: AgentWidgetRequestPayload; config: AgentWidgetConfig; }; export type AgentWidgetRequestMiddleware = ( context: AgentWidgetRequestMiddlewareContext ) => AgentWidgetRequestPayload | void | Promise; export type AgentWidgetParsedAction = { type: string; payload: Record; raw?: unknown; }; export type AgentWidgetActionParserInput = { text: string; message: AgentWidgetMessage; }; export type AgentWidgetActionParser = ( input: AgentWidgetActionParserInput ) => AgentWidgetParsedAction | null | undefined; export type AgentWidgetActionHandlerResult = { handled?: boolean; displayText?: string; persistMessage?: boolean; // If false, prevents message from being saved to history resubmit?: boolean; // If true, automatically triggers another model call after handler completes }; export type AgentWidgetActionContext = { message: AgentWidgetMessage; metadata: Record; updateMetadata: ( updater: (prev: Record) => Record ) => void; document: Document | null; /** * Trigger automatic model continuation. * Call this AFTER completing async operations (e.g., injecting search results) * to have the model analyze the injected data. * * Use this instead of returning `resubmit: true` for handlers that do async work, * as it ensures the continuation happens after the data is available in context. * * @example * // In an action handler * const results = await fetchProducts(query); * session.injectAssistantMessage({ content: formatResults(results) }); * context.triggerResubmit(); */ triggerResubmit: () => void; }; export type AgentWidgetActionHandler = ( action: AgentWidgetParsedAction, context: AgentWidgetActionContext ) => AgentWidgetActionHandlerResult | void; export type AgentWidgetStoredState = { messages?: AgentWidgetMessage[]; metadata?: Record; artifacts?: PersonaArtifactRecord[]; selectedArtifactId?: string | null; }; export interface AgentWidgetStorageAdapter { load?: () => | AgentWidgetStoredState | null | Promise; save?: (state: AgentWidgetStoredState) => void | Promise; clear?: () => void | Promise; } export type AgentWidgetVoiceStateEvent = { active: boolean; source: "user" | "auto" | "restore" | "system"; timestamp: number; }; /** * Fired on every voice `VoiceStatus` transition (listening / processing / * speaking / idle / …). Unlike `voice:state` (a coarse active on/off), this * exposes the granular status so consumers can render their own per-state UI * (e.g. a listening/speaking status dock). */ export type AgentWidgetVoiceStatusEvent = { status: VoiceStatus; timestamp: number; }; export type AgentWidgetActionEventPayload = { action: AgentWidgetParsedAction; message: AgentWidgetMessage; }; /** * Fired on every "Read aloud" (text-to-speech) state transition for a message: * `loading` (press) → `playing` → `paused`/`playing` → `idle` (finished or * stopped). On the terminal `idle` transition, `messageId`/`message` still * identify the message that just stopped. */ export type AgentWidgetReadAloudEvent = { /** The message being read aloud (or that just finished/stopped), if known. */ messageId: string | null; /** The message object, when still present in the thread. */ message: AgentWidgetMessage | null; /** The new playback state for this message. */ state: ReadAloudState; timestamp: number; }; /** * Feedback event payload for upvote/downvote actions on messages */ export type AgentWidgetMessageFeedback = { type: "upvote" | "downvote"; messageId: string; message: AgentWidgetMessage; }; /** * Configuration for message action buttons (copy, upvote, downvote) * * **Client Token Mode**: When using `clientToken`, feedback is automatically * sent to your Runtype backend. Just enable the buttons and you're done! * The `onFeedback` and `onCopy` callbacks are optional for additional local handling. * * @example * ```typescript * // With clientToken - feedback is automatic! * config: { * clientToken: 'ct_live_...', * messageActions: { * showUpvote: true, * showDownvote: true, * // No onFeedback needed - sent to backend automatically * } * } * ``` */ export type AgentWidgetMessageActionsConfig = { /** * Enable/disable message actions entirely * @default true */ enabled?: boolean; /** * Show copy button * @default true */ showCopy?: boolean; /** * Show upvote button. * When using `clientToken`, feedback is sent to the backend automatically. * @default false */ showUpvote?: boolean; /** * Show downvote button. * When using `clientToken`, feedback is sent to the backend automatically. * @default false */ showDownvote?: boolean; /** * Show a "Read aloud" button that speaks the assistant message via * text-to-speech (play / pause / resume). Uses the browser Web Speech API by * default, or a hosted engine supplied via `textToSpeech.createEngine`. * Voice, rate and pitch are read from the `textToSpeech` config. * @default false */ showReadAloud?: boolean; /** * Visibility mode: 'always' shows buttons always, 'hover' shows on hover only * @default 'hover' */ visibility?: "always" | "hover"; /** * Horizontal alignment of action buttons * @default 'right' */ align?: "left" | "center" | "right"; /** * Layout style for action buttons * - 'pill-inside': Compact floating pill around just the buttons (default for hover) * - 'row-inside': Full-width row at the bottom of the message * @default 'pill-inside' */ layout?: "pill-inside" | "row-inside"; /** * Callback when user submits feedback (upvote/downvote). * * **Note**: When using `clientToken`, feedback is AUTOMATICALLY sent to your * backend via `/v1/client/feedback`. This callback is called IN ADDITION to * the automatic submission, useful for updating local UI or analytics. */ onFeedback?: (feedback: AgentWidgetMessageFeedback) => void; /** * Callback when user copies a message. * * **Note**: When using `clientToken`, copy events are AUTOMATICALLY tracked * via `/v1/client/feedback`. This callback is called IN ADDITION to the * automatic tracking. */ onCopy?: (message: AgentWidgetMessage) => void; }; export type AgentWidgetStateEvent = { open: boolean; source: "user" | "auto" | "api" | "system"; timestamp: number; }; export type AgentWidgetStateSnapshot = { open: boolean; launcherEnabled: boolean; voiceActive: boolean; streaming: boolean; }; export type AgentWidgetControllerEventMap = { "user:message": AgentWidgetMessage; "assistant:message": AgentWidgetMessage; "assistant:complete": AgentWidgetMessage; "voice:state": AgentWidgetVoiceStateEvent; "voice:status": AgentWidgetVoiceStatusEvent; "action:detected": AgentWidgetActionEventPayload; "action:resubmit": AgentWidgetActionEventPayload; "widget:opened": AgentWidgetStateEvent; "widget:closed": AgentWidgetStateEvent; "widget:state": AgentWidgetStateSnapshot; "message:feedback": AgentWidgetMessageFeedback; "message:copy": AgentWidgetMessage; "message:read-aloud": AgentWidgetReadAloudEvent; "eventStream:opened": { timestamp: number }; "eventStream:closed": { timestamp: number }; "approval:requested": { approval: AgentWidgetApproval; message: AgentWidgetMessage }; "approval:resolved": { approval: AgentWidgetApproval; decision: string }; }; /** * Layout for the artifact split / drawer (CSS lengths unless noted). * * **Close behavior:** In desktop split mode, the artifact chrome `Close` control uses the same * dismiss path as the mobile drawer (`onDismiss` on the artifact pane): the pane is hidden until * new artifact content arrives or the host calls `showArtifacts()` on the widget handle. */ export type AgentWidgetArtifactsLayoutConfig = { /** Flex gap between chat column and artifact pane. @default 0.5rem */ splitGap?: string; /** Artifact column width in split mode. @default 40% */ paneWidth?: string; /** Max width of artifact column. @default 28rem */ paneMaxWidth?: string; /** Min width of artifact column (optional). */ paneMinWidth?: string; /** * When the floating panel is at most this wide (px), use in-panel drawer for artifacts * instead of a side-by-side split (viewport can still be wide). * @default 520 */ narrowHostMaxWidth?: number; /** * When true (default), widen the launcher panel while artifacts are visible and not user-dismissed. * No-op for inline embed (`launcher.enabled === false`). */ expandLauncherPanelWhenOpen?: boolean; /** Panel width when expanded (launcher + artifacts visible). @default min(720px, calc(100vw - 24px)) */ expandedPanelWidth?: string; /** * When true, shows a drag handle between chat and artifact columns in desktop split mode only * (hidden in narrow-host drawer and viewport ≤640px). Width is not persisted across reloads. */ resizable?: boolean; /** Min artifact column width while resizing. Only `px` strings are supported. @default 200px */ resizableMinWidth?: string; /** Optional max artifact width cap while resizing (`px` only). Layout still bounds by chat min width. */ resizableMaxWidth?: string; /** * Visual treatment for the artifact column in split mode. * - `'panel'`: bordered sidebar with left border, gap, and shadow (default). * - `'seamless'`: flush with chat: no border or shadow, container background, zero gap. * @default 'panel' */ paneAppearance?: "panel" | "seamless"; /** Border radius on the artifact pane (CSS length). Works with any `paneAppearance`. */ paneBorderRadius?: string; /** CSS `box-shadow` on the artifact pane. Set `"none"` to suppress the default shadow. */ paneShadow?: string; /** * Full `border` shorthand for the artifact `