import { dispatchBrowserActionDetailed, isFailureText, } from "./dispatch.js";
import { optimizeMessagesWithStats } from "../llm/message-optimizer.js";
import { browserTool, storeTool } from "../llm/tools.js";
import { executeStoreAction } from "../agent/executors/store-executor.js";
/** Actions that only observe page state — never count as "step goal achieved" in fallback. */
const OBSERVATION_ACTIONS = new Set(["snapshot", "screenshot", "console"]);
/**
 * Wrap a string result from the CSS-selector replay path into a
 * BrowserActionResult. These calls bypass dispatch (and therefore can't
 * run an `expect` block), so the leading-prefix isFailureText check is
 * exact — `[verification failed]` cannot appear in their output.
 */
function wrapDirect(text, action) {
    return { ok: !isFailureText(text), text, action };
}
/**
 * Runs a skill script step-by-step.
 * Each step tries direct execution first; on failure, falls back to LLM.
 */
export class ScriptRunner {
    fallbackInstructions;
    store;
    actions;
    llmClient;
    logger;
    constructor(actions, llmClient, logger, fallbackInstructions = "", 
    /**
     * Optional store handle. When present, the fallback LLM gets the
     * `storeTool` exposed so it can read and record structured cross-run
     * state while recovering a failed step. Tasks
     * that don't go through TaskRunner (e.g. legacy callers) can leave it
     * undefined; fallback then runs browser-only as before.
     *
     * Note: this only helps the *fallback* path. The deterministic script
     * step loop (executeStepInner) still routes everything through the
     * browser dispatch — adding native `store.*` step types is a separate
     * change that needs skill-generator support and isn't required until
     * skill auto-generation is re-enabled (currently TODO'd in task-runner).
     */
    store) {
        this.fallbackInstructions = fallbackInstructions;
        this.store = store;
        this.actions = actions;
        this.llmClient = llmClient;
        this.logger = logger;
    }
    async run(script, variables = {}) {
        const stepResults = [];
        this.logger.log("script_start", {
            skill: script.name,
            totalSteps: script.steps.length,
        });
        let failedCount = 0;
        for (let i = 0; i < script.steps.length; i++) {
            const step = script.steps[i];
            const resolvedParams = this.resolveVariables(step.params, variables);
            console.log(`  Step ${i + 1}/${script.steps.length}: ${step.action} ${step.description || ""}`);
            try {
                await this.executeStep(step.action, resolvedParams);
                stepResults.push({
                    index: i,
                    action: step.action,
                    success: true,
                    usedFallback: false,
                });
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                console.log(`  Step ${i + 1} failed, falling back to LLM: ${msg}`);
                try {
                    const fallbackResult = await this.llmFallback(step, resolvedParams, msg);
                    stepResults.push({
                        index: i,
                        action: step.action,
                        success: true,
                        usedFallback: true,
                        fallbackAction: fallbackResult,
                    });
                }
                catch (fallbackErr) {
                    failedCount++;
                    stepResults.push({
                        index: i,
                        action: step.action,
                        success: false,
                        usedFallback: true,
                        error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr),
                    });
                }
            }
        }
        const report = {
            skillName: script.name,
            totalSteps: script.steps.length,
            successCount: stepResults.filter((s) => s.success).length,
            fallbackCount: stepResults.filter((s) => s.usedFallback).length,
            failedCount,
            steps: stepResults,
            completed: failedCount === 0,
        };
        this.logger.log("script_end", {
            skill: script.name,
            completed: report.completed,
            fallbackCount: report.fallbackCount,
            failedCount: report.failedCount,
        });
        return report;
    }
    /**
     * Detect BrowserActions failure strings via the centralized predicate
     * shared with dispatch.ts so prefix changes never drift between modules.
     * Used only for the CSS-selector replay path that calls BrowserActions
     * methods directly (no dispatch, so no `expect` block can run, so the
     * leading-prefix predicate is sufficient there).
     */
    isFailureResult(result) {
        return isFailureText(result);
    }
    /**
     * Execute a step and throw on failure.
     *
     * Failure detection runs against the structured `BrowserActionResult.ok`
     * so post-action `expect` mismatches (which appear as
     * "[verification failed]" mid-string, NOT as a leading "Error:" prefix)
     * are surfaced. Otherwise a click that succeeded at the Playwright level
     * but failed its expectation would be miscounted as success and pollute
     * skill stats.
     */
    async executeStep(action, params) {
        const r = await this.executeStepInner(action, params);
        if (!r.ok)
            throw new Error(r.text);
        return r.text;
    }
    /**
     * Inner dispatch. Routes through `dispatchBrowserActionDetailed` for
     * everything except the skill-only `sleep` action, so:
     *   - CSS-shaped refs persisted in `ref` are auto-routed to the
     *     selector slot by dispatch's splitRefOrSelector.
     *   - `expect` blocks attached to step params are honored via
     *     dispatchActKind's verification step (covers both modern
     *     {action:"act", kind:"click"} and legacy {action:"click"} shapes).
     *
     * Earlier versions of this file kept hand-written CSS-replay wrappers
     * (clickBySelector et al.) that called BrowserActions directly. That
     * path bypassed the `expect` machinery, so old skills with mid-step
     * verification were silently unverified. Removed in favor of the
     * unified dispatch path.
     */
    async executeStepInner(action, params) {
        if (action === "sleep") {
            const text = await this.actions.sleep(params.ms);
            return wrapDirect(text, action);
        }
        if (action === "store") {
            // Native store step: lets a deterministic skill record cross-run state
            // (typically a `store.put` after a successful action). The step shape
            // mirrors the LLM tool call: action: "store", params: { action: "put",
            // collection, id, value?, meta? }.
            //
            // KNOWN GAP (acknowledged): pre-action storage reads via store.has/get
            // would need (a) branching ("if has == yes, terminate script with
            // success") and (b) variables extracted from page state (e.g. the
            // candidate's stable id). Neither exists in the current script schema.
            // Today this is enforced indirectly by the storage-sensitive guard in
            // skill-generator: tasks whose prompts require structured storage stay
            // LLM-driven. If skill auto-gen is ever re-enabled for those tasks,
            // that guard must be strengthened or the script schema extended for
            // guards/variables.
            if (!this.store) {
                return {
                    ok: false,
                    text: `Error: store step requested but no StoreManager wired into ` +
                        `ScriptRunner. This is an internal wiring bug — task-runner ` +
                        `should always pass store. Action ignored.`,
                    action: "store",
                };
            }
            const subAction = params.action;
            const text = executeStoreAction(this.store, subAction, params);
            const isMutationSuccess = (subAction === "put" && (text === "ok" || text === "updated")) ||
                (subAction === "delete" && text === "ok");
            // Observations (has/get/list/count) are not "successful steps" in the
            // executeStep sense; they don't change page state and they don't fail
            // the script either. Mark ok=true so the linear script keeps going.
            const isObservation = subAction === "has" ||
                subAction === "get" ||
                subAction === "list" ||
                subAction === "count";
            const isError = text.startsWith("store rejected") ||
                text.startsWith("store.put rejected") ||
                text.startsWith("store error:") ||
                text.startsWith("Unknown store action");
            return {
                ok: !isError && (isMutationSuccess || isObservation),
                text,
                action: "store",
            };
        }
        return dispatchBrowserActionDetailed(this.actions, action, params);
    }
    /**
     * LLM fallback: ask LLM to accomplish the step goal, then actually execute
     * the tool calls it returns.
     */
    async llmFallback(step, params, error) {
        // Get current page state for LLM context
        const snapshot = await this.actions.snapshot("full");
        const messages = [
            {
                role: "user",
                content: `I'm executing a script step that failed. Here's the context:

Step: ${step.action} with params: ${JSON.stringify(params)}
Description: ${step.description || "N/A"}
Error: ${error}

Current page state:
${snapshot}

Please use the browser tool to accomplish what this step was trying to do.`,
            },
        ];
        const systemPrompt = "You are a browser automation assistant. A script step failed and you need to accomplish the same goal using the browser tool. Be concise." +
            (this.fallbackInstructions.trim()
                ? `\n\n${this.fallbackInstructions.trim()}`
                : "");
        // Mini chat loop: execute tool calls until LLM is done
        const maxIterations = 10;
        let lastSuccessAction = "";
        const fallbackTools = this.store
            ? [browserTool, storeTool]
            : [browserTool];
        for (let i = 0; i < maxIterations; i++) {
            const request = optimizeMessagesWithStats(messages, { preserveFirst: true });
            const response = await this.llmClient.chat(request.messages, fallbackTools, systemPrompt, {
                systemStaticChars: systemPrompt.length,
                cacheTools: true,
                cacheMessages: request.messages.length > 0,
            });
            // No tool calls — LLM is done
            if (response.toolCalls.length === 0 || response.stopReason === "stop") {
                if (lastSuccessAction)
                    return lastSuccessAction;
                // LLM gave up without any successful action
                throw new Error(`LLM fallback failed: ${response.content || "no browser action succeeded"}`);
            }
            // Record assistant message
            messages.push({
                role: "assistant",
                content: response.content,
                toolCalls: response.toolCalls,
            });
            // Execute each tool call via the detailed dispatch — we read `ok`
            // directly instead of re-parsing the result string for failure
            // prefixes.
            for (const tc of response.toolCalls) {
                const args = tc.arguments;
                const action = args.action;
                let resultText;
                let tcAction;
                if (tc.name === "store" && this.store) {
                    resultText = executeStoreAction(this.store, action, args);
                    tcAction = `store.${action}`;
                    // Only mutations advance lastSuccessAction. store.put returns
                    // exactly "ok" (insert) or "updated" (overwrite); store.delete
                    // returns "ok" on actual deletion. Everything else (including
                    // delete no-op, rejection, unknown action) does not advance.
                    const isStoreSuccess = (action === "put" && (resultText === "ok" || resultText === "updated")) ||
                        (action === "delete" && resultText === "ok");
                    if (isStoreSuccess) {
                        lastSuccessAction = `${tcAction}: ${resultText}`;
                    }
                }
                else {
                    const r = await dispatchBrowserActionDetailed(this.actions, action, args);
                    resultText = r.text;
                    tcAction = args.kind || action;
                    if (r.ok && !OBSERVATION_ACTIONS.has(tcAction)) {
                        lastSuccessAction = `${tcAction}: ${r.text.slice(0, 100)}`;
                    }
                }
                messages.push({
                    role: "tool",
                    content: resultText,
                    toolCallId: tc.id,
                });
            }
        }
        if (lastSuccessAction)
            return lastSuccessAction;
        throw new Error("LLM fallback failed: max iterations with no successful action");
    }
    resolveVariables(params, variables) {
        const resolved = {};
        for (const [key, value] of Object.entries(params)) {
            if (typeof value === "string") {
                resolved[key] = value.replace(/\{\{(\w+)\}\}/g, (_, varName) => variables[varName] || `{{${varName}}}`);
            }
            else {
                resolved[key] = value;
            }
        }
        return resolved;
    }
}
//# sourceMappingURL=script-runner.js.map