/** * Replay Executor - Core execution engine for command sequences */ import type { CommandRecorder, RecordedCommand, CommandSequence, ActiveSequenceState } from '../command-recorder.js'; import type { ExecuteToolCall } from '../types.js'; import { ClickValidationConfig } from '../config.js'; export { injectReplayCursor, showClickEffect, showKeyPress, removeReplayCursor } from '../replay-cursor.js'; interface ReplayCursorCallbacks { onClickBefore?: (x: number, y: number, isRightClick: boolean) => Promise; onKeyPress?: (key: string) => Promise; } export declare function setReplayCursorCallbacks(callbacks: ReplayCursorCallbacks): void; export interface ExecutionContext { executeToolCall: ExecuteToolCall; commandRecorder: CommandRecorder; connectionReason: string; logPrefix?: string; /** Current nesting depth for conditional commands (used for recursion protection) */ conditionalDepth?: number; /** Call stack of sequence names for circular reference detection */ conditionalCallStack?: string[]; /** Per-run variable store for {{var:name.path}} interpolation. Populated by * { saveAs } steps (see CAPTURE_SOURCES), consumed by later steps' param * interpolation. Shared BY REFERENCE with per-step ctx clones and nested * sequences, so a capture anywhere is visible everywhere in the run. */ variableStore?: Record; /** {{timestamp}} value for this run, computed once and cached (not per-step). */ runTimestamp?: number; /** * Maps a per-step `connectionReason` as RECORDED onto a reference that exists * in THIS session (`{ 'duo-member-two': 'my-second-browser' }`). Connection * references are per-session, so a multi-connection sequence recorded elsewhere * needs its references rebound before it can run here. Both sides are expected * pre-sanitized (see sanitizeConnectionMap). Inherited by nested sequences. */ connectionMap?: Record; /** * References this run CAUSED to be launched, filled in as `launchChrome` * steps succeed with `reused: false`. Shared by reference with per-step ctx * clones and nested sequences, so ownership survives every early return the * executor has - a paused, failed or aborted run knows what it created just * as well as a completed one. `killChromeOnFinish` kills exactly this set * plus the run's own connection, and nothing else (issue #103). */ launchedConnections?: Set; } export interface StepResult { step: number; tool: string; success: boolean; error?: string; substeps?: StepResult[]; sequenceName?: string; conditionMet?: boolean; /** forEach: how many items the source yielded, before `where` filtering. */ itemsFound?: number; /** forEach: how many items actually ran `do` (post-filter, post-maxItems). */ iterations?: number; } export interface BreakpointHitInfo { url: string; lineNumber: number; columnNumber?: number; functionName?: string; } export interface ClickValidationFailure { step: number; selector: string; errors: string[]; warnings: string[]; info: string[]; } export interface ExecutionResult { results: StepResult[]; totalCommands: number; durationMs: number; pausedAtStep?: number; activeSequenceState?: ActiveSequenceState; breakpointHit?: BreakpointHitInfo; /** Click validation failure - sequence paused for inspection/retry */ clickValidationFailure?: ClickValidationFailure; /** * Results of the sequence's `teardown` steps, when it has any and the run * reached a terminal state. Deliberately NOT merged into `results`: teardown * outcomes must never change the run's verdict, or a broken cleanup would * mask the failure it was cleaning up after. */ teardownResults?: StepResult[]; /** True when teardown ran but at least one of its steps failed. */ teardownFailed?: boolean; } export interface ConnectionAnalysis { launchChromeIndex: number; firstConnectionToolIndex: number; hasLaunchBeforeConnection: boolean; } /** * Resolve what a step's `saveAs` should write to the variable store. * A `saveAs` that cannot be honoured is an error, not a silent no-op: the * later {{var:...}} step would otherwise fail somewhere far away with a * confusing "no variable named" message. */ export declare function captureVariable(tool: string, params: Record, result: any): { ok: true; value: unknown; } | { ok: false; error: string; }; /** * Tools that can only run against a *browser*. Used to decide whether a sequence * needs Chrome auto-launched (analyzeSequenceConnections / sequenceNeedsConnection * and the auto-launch paths in replay-tools). * * Deliberately excludes tools that are equally valid against a Node target * (`inspect`, `execution`, `breakpoint`, `getSourceCode`, `request`) - listing * those here would make a Node-only sequence spuriously launch Chrome. */ export declare const TOOLS_NEEDING_CONNECTION: string[]; /** * Tools whose params accept a `connectionReason` and should therefore have the * run-level connection injected when the step doesn't name one itself. Superset of * TOOLS_NEEDING_CONNECTION: it adds the target-agnostic (Chrome *or* Node) debugging * tools, which need to be pinned to the run's target but must NOT drag a browser * launch in with them. * * `request` is handled separately - only `destination: 'browser'` takes a connection. */ export declare const TOOLS_ACCEPTING_CONNECTION: string[]; /** * Whether a single step requires a *browser* connection (drives Chrome * auto-launch). Param-aware variant of `TOOLS_NEEDING_CONNECTION.includes(tool)`: * `wait` is browser-bound only in its selector/selectorGone forms. * - `wait({ ms })` is a plain sleep and must not drag a Chrome launch in. * - `wait({ expression })` is target-agnostic (valid against Node too), so it * behaves like `inspect`: the run connection is injected, but it never * forces a browser launch on its own. */ export declare function commandNeedsBrowserConnection(cmd: { tool: string; params?: Record; }): boolean; /** * Whether a bare step will have the run-level connection injected into it - * i.e. whether leaving it bare is AMBIGUOUS about which browser it belongs to. * * Deliberately wider than `commandNeedsBrowserConnection`, which answers a * different question (does this drag a Chrome launch in?). `inspect`, * `execution`, `storage` and friends take an optional connectionReason, so a * recording made without one captures nothing about which browser it ran * against - and on replay it silently lands wherever the run-level connection * points. Measuring ambiguity with the narrower predicate missed exactly those * tools, which are the ones people actually leave bare. */ export declare function commandTakesInjectedConnection(cmd: { tool: string; params?: Record; }): boolean; /** * Result of condition evaluation * - met: true - condition matched * - met: false, isError: undefined - condition legitimately not met * - met: false, isError: true - evaluation FAILED (should stop sequence) */ export type ConditionResult = { met: true; } | { met: false; reason?: string; } | { met: false; reason: string; isError: true; }; /** The condition types `evaluateCondition` knows how to answer. */ export declare const CONDITION_TYPES: readonly ["selector", "url", "cookie", "localStorage", "indexedDB"]; /** * Check a condition at authoring time: shape, type, and the `url`/`indexedDB` * sub-forms. A value holding a `{{var:...}}` token is skipped - it is * substituted at run time, so its final shape is unknowable here. */ export declare function validateConditionSyntax(condition: string, maxRegexLength?: number): { ok: true; } | { ok: false; reason: string; }; /** * Evaluate a handlebar-style condition * Supported patterns: * {{selector:CSS_SELECTOR}} - true if element exists * {{!selector:CSS_SELECTOR}} - true if element does NOT exist * {{url:contains:STRING}} - true if URL contains string * {{url:matches:REGEX}} - true if URL matches regex * {{cookie:NAME}} - true if cookie exists * {{!cookie:NAME}} - true if cookie does NOT exist * {{localStorage:KEY}} - true if localStorage key exists * {{!localStorage:KEY}} - true if localStorage key does NOT exist * {{indexedDB:DB/STORE/KEY}} - true if that IndexedDB record exists * {{indexedDB:DB/STORE}} - true if that object store holds any record * {{!indexedDB:...}} - negation of either form */ export declare function evaluateCondition(condition: string, ctx: ExecutionContext): Promise; export interface ConditionalFlowResult { success: boolean; executed: boolean; sequenceName: string; substeps?: StepResult[]; error?: string; durationMs?: number; } /** * Execute a conditional flow - runs a sequence if condition is met */ export declare function executeConditionalFlow(condition: string, sequenceName: string, ctx: ExecutionContext, recorder: CommandRecorder, /** * The parent run's timeout budget, so substeps are bounded the way the * caller asked rather than silently falling back to the defaults. * * `totalTimeout` must be the parent's REMAINING budget, not a fresh copy of * its original value - otherwise wrapping steps in a conditional becomes a * way to extend the total, and a caller who set a tight bound to fail fast * would not get it. */ budget?: { stepTimeout?: number; totalTimeout?: number; }, /** * The parent RUN's signal. Without it a nested sequence is deaf to * `replay cancel` even at its own step boundaries - the substep loop would * run to completion after the user cancelled. */ abortSignal?: AbortSignal): Promise; export interface ForEachFlowResult { success: boolean; sequenceName: string; /** Items the source yielded, before `where` filtering. */ itemsFound: number; /** Items that actually ran `do`. */ iterations: number; substeps?: StepResult[]; error?: string; durationMs?: number; } /** * Resolve a `forEach` source to the array it enumerates. * * Two forms, deliberately no more. `{{var:name}}` reads an array a previous * `saveAs` step captured - which is how anything non-DOM is enumerated, since * `inspect({ action: 'evaluateExpression' })` can already return exactly the * list the caller wants and is a recordable step. `{{selectorAll:CSS}}` covers * the DOM case without making the caller hand-write an evaluate for it. * * Note the asymmetry with `conditional`'s conditions: those ask whether one * named thing exists, so they can't express "give me every X". That gap is the * whole reason this step exists. */ export declare function resolveForEachItems(source: unknown, ctx: ExecutionContext): Promise<{ ok: true; items: unknown[]; } | { ok: false; error: string; }>; /** * Run a sequence once per item of an enumerated source. * * Each iteration binds the item to `as` in the run's variable store (and its * position to `Index`), so the body addresses it with {{var:.field}} * exactly like any captured variable. The binding is REPLACED per iteration * rather than scoped, because the variable store is shared by reference across * nested runs - which also means a body's own `saveAs` captures survive into * the next iteration, and a caller relying on that should say so. */ export declare function executeForEachFlow(params: { in: unknown; as: string; do: string; where?: string; maxItems?: number; }, ctx: ExecutionContext, recorder: CommandRecorder, /** The parent's REMAINING budget - a loop must not extend the total. */ budget?: { stepTimeout?: number; totalTimeout?: number; }, abortSignal?: AbortSignal): Promise; export interface LoadSequenceArgs { name?: string; sequenceId?: string; } export type LoadSequenceResult = { success: true; sequence: CommandSequence; } | { success: false; error: string; errorCode: string; /** Template variables for error response (e.g., action, missing for MISSING_PARAMETER) */ templateVars?: Record; }; /** * Load a sequence from memory (by sequenceId) or disk (by name) */ export declare function loadSequence(args: LoadSequenceArgs, recorder: CommandRecorder): Promise; /** * Return a deep copy of the sequence retargeted at another deployment: * every absolute http(s) URL — the startUrl and any string param in any * command (navigate goto, request url, ...) — keeps its path/query/hash but * takes `baseUrl`'s origin. Relative URLs are untouched (they already follow * the page origin). An explicit `startUrl` replaces the sequence's startUrl * wholesale, after rebasing, for runs whose entry point differs per target * (e.g. a freshly minted share link). The stored sequence is never mutated — * loadSequence can return the recorder's in-memory object. */ export declare function rebaseSequence(sequence: CommandSequence, overrides: { baseUrl?: string; startUrl?: string; }): CommandSequence; /** * Analyze sequence commands to find launchChrome and determine connection requirements */ export declare function analyzeSequenceConnections(commands: RecordedCommand[]): ConnectionAnalysis; /** * Extract connectionReason from sequence's launchChrome command if present */ export declare function extractConnectionFromSequence(commands: RecordedCommand[], analysis: ConnectionAnalysis): string | undefined; export interface RecordedConnectionAnalysis { /** Distinct per-step connection references, in first-seen order. */ references: string[]; /** The one reference every connection-bearing step shares, if there is one. */ uniform?: string; /** * True when some steps name a connection and other BROWSER steps don't - the * recording was driven partly through the active connection, so we cannot tell * which browser the bare steps belonged to. Such a sequence is not hoisted * (that could pin every step to the one named reference) and `create` says so. */ mixed: boolean; /** More than one distinct per-step reference: a genuinely multi-connection sequence. */ multiConnection: boolean; } /** * What connections a recorded/stored sequence's steps name. */ export declare function analyzeRecordedStepConnections(commands: RecordedCommand[]): RecordedConnectionAnalysis; /** * Hoist a uniform per-step connection back off the steps so the sequence stays * portable: `replay({ action: 'run', connectionReason: 'other' })` can then * retarget the whole thing. Steps keep their own connection only where the * sequence genuinely spans connections (or where it is ambiguous - see * `mixed`), which is the case a run-level connection cannot express. * * Returns a new command array; the input is never mutated. */ export declare function normalizeStepConnections(commands: RecordedCommand[]): { commands: RecordedCommand[]; /** The reference that was hoisted off every step, if any. */ hoisted?: string; analysis: RecordedConnectionAnalysis; }; /** * Normalize a recorded-reference -> session-reference map (both sides sanitized, * so `{ 'Duo Member Two': 'My Second Browser' }` works the same as the * hyphenated form). Returns undefined for an empty/absent map. */ export declare function sanitizeConnectionMap(map?: Record): Record | undefined; /** * Check if sequence needs a connection */ export declare function sequenceNeedsConnection(commands: RecordedCommand[]): boolean; /** * The connection references live in this session, as `listConnections` reports * them. Returns null when that cannot be determined (probe failed, or a stubbed * executeToolCall returned nothing parseable) - callers must treat null as * "unknown" and NOT as "empty", or every per-step connection would be rejected. */ export declare function probeLiveConnectionReferences(executeToolCall: ExecuteToolCall): Promise | null>; /** * The `connections` array out of a `listConnections` response, or null when the * response carries no parseable JSON block (a stub, or a future format) - null * means "unknown", never "empty". */ export declare function parseConnectionList(text: string): Array<{ reference: string; port?: number; connected?: boolean; }> | null; /** * Resolve a step's RECORDED connectionReason onto this session, and refuse to * proceed if it doesn't exist here (bug-018). * * Falling back to the run-level connection with a warning is exactly the failure * this exists to prevent: a sequence whose purpose is proving something crosses * a browser boundary would run entirely in one browser and still pass. */ export declare function formatMissingStepConnection(opts: { step: number; tool: string; recorded: string; resolved: string; mapped: boolean; runConnection?: string; live: string[]; }): string; /** * Check if debugger is paused and return breakpoint info if so */ export declare function checkIfPaused(ctx: ExecutionContext): Promise; /** * Check if debugger is paused and auto-resume if so */ export declare function resumeIfPaused(ctx: ExecutionContext): Promise; export type AutoLaunchResult = { success: true; } | { success: false; error: string; errorType: 'INVALID_REFERENCE' | 'LAUNCH_FAILED'; }; /** * Validate reference and auto-launch Chrome if needed. * This is the shared helper for all auto-launch scenarios. */ export declare function autoLaunchChrome(executeToolCall: ExecuteToolCall, connectionReason: string, logPrefix?: string, forceNewInstance?: boolean): Promise; /** * Ensure a connection is available, auto-launching Chrome if needed */ export declare function ensureConnection(ctx: ExecutionContext, needsConnection: boolean, hasLaunchBeforeConnection: boolean): Promise<{ success: true; didAutoLaunch: boolean; } | { success: false; error: string; }>; /** * Check if a URL's port is open (for localhost URLs only) * Returns success if port is open or URL is not localhost */ export declare function checkPortBeforeNavigation(url: string, logPrefix?: string): Promise<{ success: true; } | { success: false; error: string; }>; /** * Navigate to startUrl if sequence has one and doesn't start with navigate */ export declare function navigateToStartUrl(ctx: ExecutionContext, sequence: CommandSequence, analysis: ConnectionAnalysis): Promise<{ success: true; } | { success: false; error: string; }>; /** * Execute a single command with retry logic for element not found errors */ export declare function executeCommandWithRetry(executeToolCall: ExecuteToolCall, tool: string, params: Record, logPrefix?: string, /** * The RUN's signal, forwarded to the tool handler so handlers that honour * it (currently `wait`) are interrupted mid-step by `replay cancel`. Note * this helper still RESOLVES `{ success: false }` when a handler throws an * abort - executeSteps consults the signal on the failure path to classify * it as "Replay aborted by user" rather than a genuine step failure. */ abortSignal?: AbortSignal): Promise<{ success: boolean; result?: any; error?: string; }>; /** * Validate that navigation succeeded (page loaded correctly) */ export declare function validateNavigation(ctx: ExecutionContext, expectedUrl?: string): Promise<{ success: boolean; error?: string; }>; /** * Wait for an element to appear */ export declare function waitForElement(ctx: ExecutionContext, selector: string): Promise; /** * Validate typed text was entered correctly */ export declare function validateTypedText(ctx: ExecutionContext, selector: string, expectedText: string, append?: boolean): Promise; export interface PreClickState { consoleErrorCount: number; consoleWarnCount: number; consoleTotalCount: number; networkRequestCount: number; url: string; } export interface ClickValidationResult { valid: boolean; errors: string[]; warnings: string[]; info: string[]; } /** * Capture pre-click state for delta comparison */ export declare function capturePreClickState(ctx: ExecutionContext): Promise; /** * Validate click action results */ export declare function validateClickAction(ctx: ExecutionContext, preState: PreClickState, clickResult: any, config: ClickValidationConfig): Promise; export interface ExecuteStepsOptions { /** * Teardown's own total budget, independent of `totalTimeout`. * * It has to be independent: the commonest reason a run needs cleaning up * after is that it ran out of time, and a teardown drawing on the exhausted * parent budget would be skipped in exactly that case. */ teardownTimeout?: number; sequence: CommandSequence; startStep: number; endStep?: number; ctx: ExecutionContext; variables?: Record; record?: boolean; stepTimeout?: number; totalTimeout?: number; overrideConnectionReason?: string; abortSignal?: AbortSignal; /** * Called as each TOP-LEVEL step starts executing, so a background run can * report live progress. Deliberately not propagated into nested sequences * (conditional flows): substeps report through their parent step only. */ onProgress?: (ev: { step: number; totalSteps: number; tool: string; }) => void; } /** * Execute a range of steps from a sequence */ export declare function executeSteps(options: ExecuteStepsOptions): Promise; /** * Execute a sequence with pause support (stepTo) */ export declare function executeSequenceWithPause(options: ExecuteStepsOptions & { stepTo?: number; }): Promise; export interface DebugState { isPaused: boolean; pauseLocation?: string; breakpointCount: number; } /** * Get current debug state (breakpoints, pause status) */ export declare function getDebugState(ctx: ExecutionContext): Promise; //# sourceMappingURL=replay-executor.d.ts.map