import { ReactNode } from 'react'; /** * Screenshot capture for WebAgent — feeds the LLM a visual snapshot of * the page alongside the indexed DOM dump. Disabled by default; opt in * via `WebAgentConfig.screenshot`. * * Two modes: * - `viewport` — only what the user can see right now (1 image). * - `full-page` — the entire scroll height, split into N segments * when taller than `maxSegmentHeight`. Each segment * becomes its own image, capped by `maxImages`. * * Implementation: dynamic-imports `html2canvas` so users who don't * enable screenshots pay zero bundle cost. When a host turns the * feature on without installing the peer, capture silently no-ops and * the agent continues with text-only input. */ type ScreenshotMode = 'viewport' | 'full-page'; interface ScreenshotConfig { /** Default capture mode. Default `'viewport'`. */ mode?: ScreenshotMode; /** Max pixel height per image segment. Full-page captures taller than * this split into multiple images. Default 4000. */ maxSegmentHeight?: number; /** Hard cap on the number of images sent per turn (after splitting). * Default 3 — keeps token cost predictable. */ maxImages?: number; /** JPEG quality 0–1. Default 0.75 (sweet spot for legibility vs. size). */ quality?: number; /** Downscale factor — 1 = full resolution, 0.5 = half. Lower = cheaper * tokens but blurrier text. Default 0.75. */ scale?: number; /** * Override the capture function entirely. Host returns one image * (viewport mode) or many (full-page mode), each as a data URL or * remote URL the LLM can fetch. When provided, html2canvas is not * loaded. */ capture?: (mode: ScreenshotMode) => Promise; } /** * Capture screenshot(s) of the current page according to `cfg`. Returns * an empty array when the host didn't install html2canvas (and didn't * provide a custom `capture`) — the agent loop continues without images. */ declare function captureScreenshots(cfg: ScreenshotConfig): Promise; /** * Types shared across the Plan module — todos artifact, markdown artifact, * planning input/output. The shape is deliberately conservative: every * artifact is in-memory by default; hosts that want persistence wire a * storage adapter (`{ load, save }`) at construction time. */ /** * A single planned step. The webagent reads this list as its master plan * for the run; `replace` / `remove` mutations happen via the per-turn * envelope, not the artifact API (so the LLM cannot mutate it freely from * arbitrary tool calls). */ interface TodoItem { /** Stable id assigned by Plan on create — `t1`, `t2`, ... */ id: string; /** * Coarse classification of what this todo does. The webagent loop uses * this hint to know whether a turn should be a navigate-only turn vs a * narrate turn. Non-exhaustive — hosts can introduce custom intents. */ intent: 'navigate' | 'narrate' | 'click' | 'fill' | 'ask' | 'finish' | string; /** Human-readable description — what the user will perceive happen. */ description: string; /** * Predicted turn index this todo will land on, starting at 1. Lets the * webagent recognise when the plan and reality diverge (a navigate todo * planned for turn 1 should be the only action of turn 1). */ expected_turn?: number; } /** Output of one planning call. `task_summary` is shown to the user when * `announcePlan` is on; `todos` is the master list the loop executes. */ interface TaskPlan { task_summary: string; todos: TodoItem[]; } /** What the planner needs to know to write a good plan. */ interface PlanInput { /** The user's original task text. */ task: string; /** Optional sitemap so the planner knows which pages exist. */ sitemap?: SitemapConfig; /** Optional brand context (productName / voice / constraints). */ brand?: BrandPrompt; /** Optional persona so plan task_summary speaks in the right voice. */ persona?: PersonaInput; /** BCP-47 locale hint for `task_summary` language. */ locale?: string; /** User selection (Dwell / drag) at invocation time. */ selection?: SelectionContext; /** Free-form host context (recent history, current page, etc). */ hostContext?: string; /** * v0.2.0 streaming hook — called synchronously for each new chunk of * `task_summary` chars as the LLM types it. When the host wires this * through, the planning announce can appear live in the subtitle bar * instead of one post-parse blast. Optional; omitting it keeps the * call non-visibly-streaming (the LLM body still uses `stream: true` * so the proxy doesn't buffer via cache-ttl, just no UI surfacing). */ onSummaryDelta?: (delta: string) => void; } /** An in-memory markdown document. Not used by the webagent — exposed for * hosts that want LLM-mediated authoring of meeting notes, drafts, etc. */ interface MarkdownDoc { /** Stable id assigned by Plan on create — `m1`, `m2`, ... */ id: string; title: string; content: string; /** ms epoch — set on create + every edit. */ updatedAt: number; } /** * Optional storage adapter. Same shape as ImmersiveTranslate's cache — * `load()` runs once at construct time and seeds in-memory state; * `save()` runs after every mutation. Hosts can plug IndexedDB, * localStorage, or a server endpoint behind this interface. */ interface PlanStorageAdapter { load(): Promise | PlanSnapshot | null; save(snapshot: PlanSnapshot): Promise | void; } interface PlanSnapshot { todos: TodoItem[]; markdownDocs: MarkdownDoc[]; } interface BrandPrompt { productName?: string; voice?: string; constraints?: string[]; } /** Persona — tells the agent who it speaks AS. Without this the agent * narrates in third-person observer voice. */ interface PersonaConfig { /** First-person identity statement. Required. */ identity: string; /** Voice / tone notes. */ voice?: string; /** Hard rules the persona must never break. */ constraints?: string[]; } type PersonaInput = string | PersonaConfig; interface PromptContext { locale?: string; agentName: string; siteName?: string; sitemap?: SitemapConfig; session: AgentSession; pageContext: string; selection?: SelectionContext; brand?: BrandPrompt; persona?: PersonaInput; appendSystemPrompt?: string; previousUrl?: string; /** When true, render the CoT-mode prompt (no procedural rules, schema * enforces structure). When false, render the classic narrator prompt. */ cotMode?: boolean; /** Pre-rendered tool reference (name + description + JSON Schema) for * every action exposed this turn. Pushed into the `# Tools` section * of the system prompt so the model sees the tool catalogue in one * place rather than buried inside the agent_turn tool description. */ toolReference?: string; } type SystemPromptOverride = string | ((ctx: PromptContext, defaultPrompt: string) => string); /** * Sitemap — tree/graph structure for agent navigation on multi-page sites. * * Why a tree? * Flat sitemap[] doesn't scale past ~10 pages. Agent has to scan everything * to figure out what's reachable. Tree gives: * - hierarchy (parent → children) → agent can drill down step-by-step * - per-node metadata (auth requirements, available actions, sub-routes) * - LLM-friendly serialization (indented outline matches how LLMs reason) * * Static-only in v1. Host writes it in config. Runtime learning deferred to v2. */ interface SitemapNode { /** * Display title for the page. Goes into prompts and palette search results. */ title: string; /** * One-line description. Agent uses this to match "I want to do X" intent * against the right page. */ description?: string; /** * Child routes. Key is the path segment (relative to this node). * Use `:id` / `:slug` for dynamic segments — agent knows it's a parameter. */ children?: Record; /** * Verbs the user can perform on this page. Agent uses these to plan. * Examples: ['view', 'edit', 'export', 'delete', 'refund', 'ship'] */ actions?: string[]; /** * If true, agent must ensure user is logged in (likely navigate to /login * first) before visiting. The login flow itself uses DOM operations (click * the login button, fill the form) — dddk does NOT handle OAuth. */ requiresAuth?: boolean; /** * If true, requires elevated permission. Agent should explain this to user * before attempting. */ requiresAdmin?: boolean; /** * Hidden from the LLM prompt (still navigable, just not advertised). Useful * for internal-only routes. */ hidden?: boolean; /** * Free-form notes the host wants the agent to remember about this page. * e.g., "Pagination is server-driven — use the [Next] button, not URL ?page=N" */ notes?: string; /** * Optional keywords the agent should match user intent against. Saves a * prompt token vs putting everything in description. */ keywords?: string[]; } interface SitemapResolution { /** Concrete URL path with parameters substituted. */ path: string; /** The matched node. */ node: SitemapNode; /** Parameters extracted from / supplied for the path. */ params: Record; } /** * LLM Provider interface — implement once, providers slot in. * See ../../docs/03-llm-providers.md for the full design. */ interface LLMProvider { readonly name: string; complete(opts: CompleteOptions): Promise; } interface CompleteOptions { messages: LLMMessage[]; tools?: ToolDefinition[]; temperature?: number; maxTokens?: number; model?: string; signal?: AbortSignal; /** * Reasoning intensity. Provider-specific mapping: * - OpenAI reasoning / gpt-5+ models → `reasoning_effort` * 'off' → 'minimal' * - Gemini 2.5+ models → `generationConfig.thinkingConfig.thinkingBudget` * 'off' → 0 * 'minimal' / 'low' → 64 / 512 * 'medium' / 'high' → 1024 / 4096 * - Other / non-reasoning models → field is ignored * * Use `'off'` for short, deterministic tasks (inline text edits, * translation, classification) — saves cost and prevents the model * from leaking chain-of-thought into the response. */ thinking?: 'off' | 'minimal' | 'low' | 'medium' | 'high'; /** * Force the model to respond with a JSON object that parses cleanly. * - OpenAI → `response_format: { type: 'json_object' }` * - Gemini → `generationConfig.responseMimeType = 'application/json'` * Providers that don't support structured output ignore this; the prompt * itself should still say "Reply with JSON only" as a fallback. */ jsonMode?: boolean; /** * Force which tool the model must call. * - `'auto'` (default) — model picks zero or one tool * - `'required'` — model must call exactly one tool (any tool in `tools`) * - `{ name: 'foo' }` — model must call the named tool * * Used by the WebAgent's CoT mode to force the wrapping `agent_turn` * tool every turn. Providers that don't support targeted tool_choice * fall back to `'required'` or `'auto'`. */ toolChoice?: 'auto' | 'required' | { name: string; }; } interface CompleteResult { content: string; toolCalls?: ToolCall[]; usage?: { promptTokens: number; completionTokens: number; }; finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter'; /** * Streaming-only timing. Present when the call went through * `streamComplete()` and the stream produced at least one delta. All * values are epoch ms (`Date.now()`). * - `startedAt` — first call to `produce()` * - `firstDeltaAt` — first non-empty text delta (TTFT = this - startedAt) * - `endedAt` — terminal chunk (last delta or finish) * Non-streaming `complete()` calls omit this field. */ streamMetrics?: { startedAt: number; firstDeltaAt?: number; endedAt: number; }; } type LLMRole = 'system' | 'user' | 'assistant' | 'tool'; interface LLMMessage { role: LLMRole; content: string | ContentPart[]; /** Set when role = 'tool' — the call this message answers. */ toolCallId?: string; /** Set when role = 'assistant' — tool calls emitted in this turn. */ toolCalls?: ToolCall[]; /** Display name (optional, for multi-agent traces). */ name?: string; } type ContentPart = { type: 'text'; text: string; } | { type: 'image'; image: string; }; interface ToolDefinition { name: string; description: string; parameters: Record; } interface ToolCall { id: string; name: string; arguments: Record; } /** * LLMRouter — pick a provider per role. * * 4 roles: * - `webagent` — main agent loop (text-only) * - `vision` — webagent with images / screenshot context. Falls back to webagent. * - `utility` — short single-shot calls (inline AI / voice cleanup / etc). Falls back to webagent. * - `plan` — pre-loop planner. Falls back to webagent. * * Legacy field names (`webagentWithSelection`, `inline`, `voiceCleanup`) * are still accepted as fallback sources so existing host configs keep * working; prefer the new names for new code. */ interface LLMRouter { /** Required. Default LLM for the webagent loop. All other roles fall back here. */ webagent: LLMProvider; /** Used when the agent has images / screenshots to reason about. */ vision?: LLMProvider; /** Short single-shot calls — inline AI, voice cleanup, immersive translate. */ utility?: LLMProvider; /** Pre-loop planner. */ plan?: LLMProvider; /** v0.2.0. TaskAgent — conversational + tool calling. */ task?: LLMProvider; /** @deprecated — use `vision`. */ webagentWithSelection?: LLMProvider; /** @deprecated — use `utility`. */ inline?: LLMProvider; /** @deprecated — use `utility`. */ voiceCleanup?: LLMProvider; } type LLMSource = LLMProvider | LLMRouter; type ActionFailureReason = 'not_found' | 'not_visible' | 'not_interactive' | 'timeout' | 'navigation' | 'cancelled' | 'user_declined' /** * the target is in scope the agent can read but * not act on — typically a cross-origin iframe or popup. The agent * narrates the boundary to the user and may offer to hand off. */ | 'cross_origin' /** * the route the agent navigated to requires * authentication; user must sign in before the agent can proceed. */ | 'auth_required' | 'unknown'; type ActionResult = { ok: true; data?: T; } | { ok: false; reason: ActionFailureReason; message?: string; }; interface ActionContext { session: AgentSession; signal: AbortSignal; /** * Resolve a selector argument (e.g. `selector`, `target`) to a live * element. Accepts either: * - A numeric index from the DOM dump (`"3"` or `"[3]"`) — looked * up in the per-turn index map. This is the preferred form. * - A CSS selector string — passed to `document.querySelector`. * * Returns `null` when neither resolves. Action handlers use this * helper instead of calling `document.querySelector` directly so * the indexed-tree contract works uniformly. */ resolveTarget(target: string | number): Element | null; /** * Visual / UX hints forwarded from `WebAgentConfig`. Actions read these * to apply human-paced overlays (synthetic cursor, link-click vs push- * state navigate, …) without each handler needing access to the full * config object. * * @since v0.2.0 */ uiHints?: { cursorTrail?: boolean; preferClickLinkOverNavigate?: boolean; }; } interface ActionDefinition

{ name: string; description: string; /** JSON Schema for params. */ parameters: Record; handler: (params: P, ctx: ActionContext) => Promise>; /** * If true (or a predicate returning true), the agent pauses before * invoking and emits a `confirm` event that the host must `decide()` * on. Use for destructive / external-side-effect actions (delete, * send email, place order, transfer money). The webagent also * auto-marks any action whose name matches a destructive-pattern * regex (see `WebAgentConfig.destructivePatterns`); per-action * `requireConfirmation: false` can opt out of the pattern match. */ requireConfirmation?: boolean | ((params: P, ctx: ActionContext) => boolean | Promise); /** Override the confirmation prompt. Return undefined to fall through * to host-level / SDK-default copy. */ confirmationMessage?: (params: P) => string | undefined; } interface SelectionContext { text?: string; images?: string[]; bbox?: { x: number; y: number; width: number; height: number; }; /** CSS selectors / DOM paths the user clicked or multi-selected. */ elements?: string[]; } interface ToolHandle { /** Unregister this tool. After this returns, the next agent step * no longer sees the action. Idempotent — calling twice is fine. */ remove(): void; } /** * Names the context provider supplies — they map to the slots the * runtime asks for when building a per-turn context. SDK ships * defaults for each; hosts can replace any * single slot without re-implementing the others. */ type ContextRole = 'url' | 'page_summary' | 'dom' | 'screenshot' | 'history' | 'selection'; interface ContextRequest { /** The agent's signal for the current turn (so providers can bail * cheaply when the host cancels). */ signal: AbortSignal; /** Most recent user task / sub-task the agent is operating on. */ task?: string; } /** * Pluggable producer for one slot of per-turn context. Returns a * string the runtime can splice into the prompt, or `null` to skip * the slot entirely (e.g. DOM dump bails on a cross-origin * iframe). Async — providers commonly snapshot from the live DOM * or call back to the host. */ type ContextProvider = (req: ContextRequest) => string | null | Promise; interface ContextProviderHandle { /** Unregister this context provider. Restores whatever was set * previously for this role (SDK default if no prior override). * Idempotent. */ remove(): void; } interface AgentSession { id: string; turns: AgentTurn[]; status: AgentStatus; currentPage: string; startedAt: number; updatedAt: number; /** Master plan produced by `WebAgentConfig.planner` at run start, if * any. The webagent's per-turn envelope reads `todos` from here; the * loop mutates via `todo_adjust` (remove / replace operations only). */ plan?: TaskPlan; } type AgentStatus = 'idle' | 'thinking' | 'executing' | 'waiting' | 'navigating' | 'done' | 'failed'; /** Discriminated union of everything that can land in `session.turns[]`. */ type AgentTurn = UserTurn | AgentStepTurn | AgentFinalTurn; interface UserTurn { kind: 'user'; ts: number; text: string; selection?: SelectionContext; } interface AgentStepTurn { kind: 'agent_step'; ts: number; /** Free-form text the model emitted *before* picking the tool (streaming). */ preText?: string; toolCall: { name: string; arguments: Record; }; toolCallId: string; result: ActionResult; } interface AgentFinalTurn { kind: 'agent_final'; ts: number; text: string; } type AgentEvent = { kind: 'thinking'; } | { kind: 'text-delta'; delta: string; } | { kind: 'tool-start'; name: string; args: Record; targetSelector?: string; toolCallId: string; } | { kind: 'tool-end'; name: string; result: ActionResult; toolCallId: string; } /** Fired right BEFORE the SPA router is invoked. The page is still * the old one; the host should show a "loading" indicator until * `navigated` arrives. */ | { kind: 'navigating'; from: string; to: string; } /** Fired AFTER the router Promise resolved AND the DOM has settled. * The page is now the new one and safe to introspect. */ | { kind: 'navigated'; from: string; to: string; } | { kind: 'confirm'; actionName: string; args: Record; message: string; decide: (approved: boolean) => void; } | { kind: 'final'; } | { kind: 'error'; error: Error; retrying: boolean; }; type OverlayType = 'highlight' | 'border' | 'spotlight' | 'inject'; interface OverlayItem { id: string; type: OverlayType; selector: string; color?: string; label?: string; text?: string; position?: 'before' | 'after'; } type PiecePlacement = 'center' | 'inline' | 'dock'; interface SitemapEntry { path: string; description: string; aliases?: string[]; } type SitemapConfig = SitemapEntry[] | SitemapNode; interface RunOptions { /** What the user had selected when they invoked the agent. */ selection?: SelectionContext; /** * Force a fresh session even if a live session exists within the * continuity window. Set this when the host's UX is "new conversation * button pressed" — default behavior (false) appends to the existing * session as a follow-up. */ freshSession?: boolean; } /** * Behaviour when the agent loop finishes (CoT `actions: []` / * classic-mode no-tool-call turn / maxSteps cap). Without an * `onLoopEnd` the subtitle bar disappears the moment the loop ends, * which reads as broken — there's no signal that the work completed. * * - `silent` — legacy: subtitle just disappears. * - `text` — stream a final closing line (e.g. "✓ Done") then * dismiss after `autoHide` ms. * - `feedback` — closing line + Space (satisfied) / double-tap (not) / * Esc (skipped) gestures, emitted as `agent_feedback` * on the intent stream. * - `ask_user` — option picker (e.g. 1-5 rating); the chosen value * flows into `agent_feedback.summary`. * * SDK default: `{ kind: 'text', text: i18n('agent.done'), autoHide: 3000 }`. * Hosts opt into `feedback` / `ask_user` for post-run satisfaction signal. */ type OnLoopEnd = { kind: 'silent'; } | { kind: 'text'; text: string; autoHide?: number; } | { kind: 'feedback'; text: string; } | { kind: 'ask_user'; question: string; options: Array<{ value: string; label: string; }>; }; interface WebAgentConfig { /** LLM source — single `LLMProvider` or `LLMRouter` (per-role). */ llm: LLMSource; locale?: string; /** * Optional pre-loop planning callback. When set, the webagent makes a * single planning call BEFORE entering the turn loop, expects a * `TaskPlan` ({ task_summary, todos[] }), stores it on the session, * and switches the per-turn envelope to the planned variant * (turn_planning + todo_adjust + actions instead of memory + * todos_remaining + actions). Typical wiring is `(input) => * dddk.plan.makeTodos(input)` so the Plan module's strategic * appendSystemPrompt drives the plan shape. */ planner?: (input: PlanInput) => Promise; /** When true (and `planner` is set), the resulting `task_summary` is * announced to the user via a subtitle bar narrate before the loop * begins. Default false — silent execution. */ announcePlan?: boolean; /** * Cap the size of the DOM snapshot fed to the planner. Default 8000 * chars — enough for a typical page's nav + headings + above-fold * content. The planner ALWAYS reads the current DOM (it needs to * know which page it's on and what nav links are reachable); this * just caps how much it sees. The webagent's per-turn DOM uses * `domMaxLength` (default 40000) separately so the loop's eyes * stay sharper than the planner's one-shot. */ plannerDomMaxLength?: number; /** Hard cap on tool-call iterations per task. Default 30. */ maxSteps?: number; /** Consecutive LLM-call failures before bailing. Default 3. */ maxErrors?: number; /** Single LLM call hard timeout (ms). Default 60_000. */ llmTimeoutMs?: number; /** Reasoning intensity (provider-specific). Default 'off'. */ thinking?: 'off' | 'low' | 'medium' | 'high'; /** System prompt override — string hard-replaces; function composes. */ systemPrompt?: SystemPromptOverride; /** Structured brand context layered into the default prompt. */ brand?: BrandPrompt; /** * First-person identity for the agent — who it IS and on whose behalf * it speaks. Without this, the agent narrates the page in third-person * observer voice ("the site states X"); with it, the agent speaks as * the site's representative ("we offer X"). * * Default: `undefined` (no persona section in the prompt). * * Pass a string for a quick identity line, or a structured * `PersonaConfig` for separate voice / constraint fields: * * persona: "You are the dotdotduck assistant, speaking on behalf * of perhapxin. Use 'we' for what perhapxin does." * * persona: { * identity: "You are Acme's support assistant...", * voice: "Warm, decisive, never speculative.", * constraints: ["Never promise refunds without confirming with the team."], * } */ persona?: PersonaInput; /** Plain text appended after the default prompt. */ appendSystemPrompt?: string; sitemap?: SitemapConfig; agentName?: string; siteName?: string; customActions?: ActionDefinition[]; /** * Per-action description overrides — applied at tool-build time so the * model sees host-customised wording without the SDK shipping rules for * every host. Keyed by action name (e.g. `'navigate'`, `'open_palette'`, * `'border'`). * * - `description` — hard replace; the SDK default is dropped entirely. * - `appendDescription` — appended after the SDK default with a newline. * * If both are set, `description` wins. Unknown action names are * ignored. Applies to BOTH CoT mode (where the override lands in the * system prompt's `# Tools` section) and classic mode (where it lands * on the OpenAI tool definition). */ actionOverrides?: Record; /** sessionStorage key. Default 'webagent.session'. */ sessionStorageKey?: string; /** * Inject a shared `AgentSession` instead of letting the agent * create + persist its own. Use to share conversation history + * memory across multiple WebAgent instances (e.g. different * personas per route) so cross-page continuity survives even when * the active agent changes. The agent reads + appends to the same * object; it does NOT replace the reference on continuity expiry * — that's the host's job (via `dddk.sessions.reset(name)`). * * When set, the agent skips the lazy `createSession` step and the * `loadSession` lookup. The host (or `dddk.sessions`) becomes * solely responsible for the session's lifecycle. v0.2. */ session?: AgentSession; /** Override default tool list (advanced). */ toolDefinitions?: ToolDefinition[]; /** * Multi-turn continuity. After a turn ends, follow-ups within this * window append to the current session as user turns. Older than this * and the next `runStream()` starts a fresh session. * * Default `0` (OFF) — most webagent usage is one-shot ("how do I X?", * answer, done); carrying prior turns into a new ask causes the LLM * to conflate unrelated questions. Hosts building conversational * agents (chat-style follow-ups) opt in by setting e.g. * `5 * 60 * 1000`. Cross-PAGE continuity (SPA navigation mid-run) * is independent and always on. */ sessionContinuityMs?: number; /** * Continuity scope — `'time'` (default) honors `sessionContinuityMs`; * `'palette'` ends continuity when the palette closes (host signals * via `dddk.agent.endContinuity()`). */ sessionScope?: 'time' | 'palette'; /** * Custom destructive-action patterns. An action whose name matches * any of these regexes auto-gates on `confirm`. Overrides built-in * defaults; pass an empty array to disable the default list and * rely only on per-action `requireConfirmation`. */ destructivePatterns?: RegExp[]; /** * When true, no tool call ever pauses for user confirmation — including * actions whose `requireConfirmation` is explicitly true. Use on sites * where the agent's surface is non-destructive (demos / docs / read-only * exploration) and the confirm-pause feels like friction. * * Per-action `requireConfirmation: true` is still respected for the * default destructive-pattern auto-gate when this flag is unset. */ disableConfirmations?: boolean; /** * Opt the agent in to the `present_surface` tool — lets the model * render a PieceSurface (image+text cards, option grids, etc.) and * await the user's pick. * * Default `false` because rich surfaces can leak host-private UI to * the model's planning context (the agent decides what image src to * use, what option labels to show). Enable when the host wants * recommendation-style flows and trusts the brand / persona / prompt * to keep the model on-rails. * * The host MUST ALSO call `agent.setSurfaceMounter(fn)` to wire the * actual mounting code — otherwise `present_surface` returns a * `not_wired` error and the model falls back to manual narration. */ allowPresent?: boolean; /** * Force every turn through a structured CoT envelope. The model is * required to call a single `agent_turn` tool whose args contain * `memory`, `next_goal`, and an ordered `actions[]` of narrations + * tool calls. The runtime parses the envelope and dispatches actions * in order — fixing the "border-then-skip" failure mode where the * classic per-turn-one-tool-call loop can short-circuit early. * * Default: `false` (classic streaming loop). When `true`, this also * disables the per-turn-pause pacing rules in the prompt — pause is * runtime-managed (auto-pause after each narration) rather than * model-managed. */ cotMode?: boolean; /** * Override the confirmation copy shown when a `requireConfirmation` * action is about to run. Receives the action name + its params and * the agent's locale. Return a string to use it; return `undefined` * to fall back to the SDK's built-in copy. * * The SDK ships en + zh-TW. Use this for `ja` / `es` / `fr` etc., or * when you want host-branded wording ("Approve the transfer? Press * space to confirm"). * * buildConfirmMessage: (action, params, locale) => { * if (locale === 'ja') { * if (action === 'navigate') return `${params.path} に移動します — スペースキーで確認`; * return undefined; // fall back to default English for the rest * } * return undefined; * } * * Per-action `ActionDefinition.confirmationMessage(params)` still * wins when set — that override is action-scoped and runs first. */ buildConfirmMessage?: (actionName: string, params: Record, locale: string) => string | undefined; /** * Subtraction filter applied to every visible element the DOM reader * considers. Return `false` to drop the element + its subtree from * the dump. Use this to keep the agent focused on the host's main * content area: filter out site chrome (nav / footer / cookie banner) * the agent should never narrate. * * domFilter: (el) => !el.matches('nav.global-nav, footer, [data-cookie]') * * Default: include everything visible. */ domFilter?: (el: Element) => boolean; /** * Hard cap on the DOM dump size sent to the LLM each turn (in * characters). Default ~12000 — large enough to capture a typical * marketing / pricing / docs page in full; small enough to keep the * per-turn token cost predictable. Hosts with denser pages can bump * this; hosts on a budget can shrink it. The reader truncates with a * `[...truncated]` marker rather than blowing the budget silently. */ domMaxLength?: number; /** * Cross-tab session sync via BroadcastChannel + localStorage mirror. * Default `false`. */ crossTabSync?: boolean; /** * Subtitle-bar hint shown when the agent calls `pause` without a * `note` argument. The SDK ships an English default — set this in * the host's UI language for a localised experience. */ defaultPauseNote?: string; /** * Hard cap on how many session turns get serialised into each LLM * prompt. When the session has more, the oldest turns are dropped * and only the most recent `maxTurnsInPrompt` are sent (the system * prompt is always kept). Default: undefined (no cap). */ maxTurnsInPrompt?: number; /** * Token budget for the per-turn LLM prompt. When the assembled * prompt exceeds this estimate, the SDK drops the OLDEST turns * first and keeps the most recent ones — the system prompt and the * env-block / latest-user message are always preserved. * * The estimate is a coarse char-count → token approximation * (mixed CJK + English ~ 3.5 chars per token); not a strict * count. Set it well below your model's true context window so * there's headroom for the model's own response. * * Default: undefined (no cap). */ maxPromptTokens?: number; /** * Attach a screenshot of the current page to every LLM turn alongside * the indexed DOM dump. Disabled by default — text-only mode is faster, * cheaper, and sufficient for most narration tasks. Turn on when the * page has visual content the DOM dump can't convey (charts, custom * canvases, complex visual layouts the agent should comment on). * * Two modes: * - `'viewport'` — one image of what the user currently sees. * - `'full-page'` — the full scroll height, auto-split into multiple * images when taller than `maxSegmentHeight`. * * Requires the `html2canvas` peer dependency (`pnpm add html2canvas`) * unless you provide a custom `capture` function. When neither is * available the agent runs text-only without erroring. */ screenshot?: boolean | ScreenshotConfig; /** * What happens when the agent loop ends. See `OnLoopEnd` for the * union; SDK default is `{ kind: 'text', text: i18n('agent.done'), * autoHide: 3000 }` so the subtitle bar doesn't just vanish. * * Pass `{ kind: 'silent' }` for the legacy "subtitle disappears" * behaviour; pass `{ kind: 'feedback', text: ... }` to collect a * satisfied / not-satisfied signal via `agent_feedback`. */ onLoopEnd?: OnLoopEnd; /** * Names of built-in actions to NOT expose to the agent. The default * builtin set is intentionally broad (navigate / scroll_to / wait / * click / fill_input / select_option / clear_input / border / pause / * ask_user / ask_user_choice) so any host works out of the box — * but most sites only need a subset. Listing names here removes them * from the agent's tool list entirely, which: * 1. Shrinks the schema the LLM has to read each turn (fewer tokens). * 2. Removes "wrong tool" failure modes (the agent can't pick a tool * that isn't relevant to your site). * * Example: a marketing demo with no ` behind the widget is hidden * or proxied through a custom handler, so the SDK's `fill_input` / * `select_option` defaults (set .value + dispatch input event) don't * actually update the widget's internal state. * * This registry lets a host plug in matchers + fillers for the * widget libraries their product actually uses. The runtime walks * registered widgets in priority order BEFORE the default fill path; * first match wins. * * import { registerFormWidget } from '@perhapxin/dddk'; * registerFormWidget({ * id: 'react-select', * priority: 100, * match: (el) => el.closest('.react-select__control') != null, * fill: async (el, value, ctx) => { * const control = el.closest('.react-select__control')!; * control.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); * await ctx.wait(60); * const opt = [...document.querySelectorAll('.react-select__option')] * .find((o) => o.textContent?.trim() === value); * opt?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); * return opt != null; * }, * }); * * The default fallback (set .value + dispatch input) is unchanged for * vanilla /