/**
 * ReactLoop — Reason + Act tool-calling loop for the act phase.
 *
 * Thought → Action → Observation loop with maxStep guard. Model-agnostic:
 * accepts an `LLMCompletionFn` that decouples from any specific provider.
 *
 * Used within the `act` phase of an OODA cycle when the agent needs multi-step
 * reasoning with tool calls (ReAct pattern).
 *
 * Loop stops on:
 *   - Final answer (no tool call in response)
 *   - maxSteps reached
 *   - LLM error
 *   - AbortSignal fired
 *
 * @public
 */
// ─── Constants ──────────────────────────────────────────────────────────────────
const DEFAULT_MAX_STEPS = 10;
const DEFAULT_MAX_TOOL_CALL_SIZE = 10_240; // 10KB
// ─── Helpers ────────────────────────────────────────────────────────────────────
/**
 * Invoke the user-supplied `onStep` callback with try/catch. Errors are
 * logged via `opts.logger?.warn` and never propagate to the caller.
 *
 * Handles both sync void and async returns.
 */
async function safeOnStep(cb, step, meta, logger) {
    try {
        const ret = cb(step, meta);
        if (ret && typeof ret.then === "function") {
            await ret;
        }
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        logger?.warn?.("reactLoop.onStep threw — error swallowed", {
            iteration: meta.iteration,
            loopId: meta.loopId,
            error: message,
        });
    }
}
// ─── Loop ───────────────────────────────────────────────────────────────────────
/**
 * Run a ReAct (Reasoning + Acting) loop.
 *
 * ```
 * Thought → Action → Observation → Thought → ... → Final Answer
 * ```
 */
export async function reactLoop(opts) {
    const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
    const maxToolCallSize = opts.maxToolCallSize ?? DEFAULT_MAX_TOOL_CALL_SIZE;
    const allowedTools = opts.allowedTools;
    const steps = [];
    const messages = [
        { role: "system", content: opts.systemPrompt },
        { role: "user", content: opts.task },
    ];
    const { onStep, loopId, logger } = opts;
    const finalizeAsTool = opts.finalizeAsTool === true;
    const finalizeToolName = opts.finalizeToolName ?? "finalize";
    /** Build telemetry meta for iteration `i`, given its start timestamp. */
    const buildMeta = (i, iterationStart) => ({
        iteration: i,
        durationMs: Date.now() - iterationStart,
        loopId,
    });
    for (let i = 0; i < maxSteps; i++) {
        if (opts.signal?.aborted) {
            // Aborted iteration — step is not finalized, onStep is NOT called.
            return { answer: "", steps, totalSteps: i, truncated: true };
        }
        const iterationStart = Date.now();
        // Thought → Action
        const response = await opts.llm(messages);
        // finalizeAsTool path : when isFinal AND a matching finalize tool
        // call is present, invoke executeTool and use its return value as
        // the answer. Allows side-effects (logging, validation, persistence)
        // to hang off finalization.
        const finalizeCall = finalizeAsTool && response.isFinal
            ? response.toolCalls?.find((c) => c.tool === finalizeToolName)
            : undefined;
        steps.push({
            index: i,
            thought: response.content,
            // Record the finalize tool call as the step's action when invoked
            // as a tool — otherwise mirror the legacy behaviour (first tool
            // call, if any).
            action: finalizeCall ?? response.toolCalls?.[0],
        });
        if (finalizeCall) {
            let answer = response.content;
            try {
                const observation = await opts.executeTool(finalizeCall);
                steps[i].observation = observation;
                answer = observation;
            }
            catch (err) {
                steps[i].error = err instanceof Error ? err.message : String(err);
                steps[i].observation = `Error executing tool "${finalizeCall.tool}": ${steps[i].error}`;
                if (onStep) {
                    await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
                }
                // Match the loop's existing error semantics — propagate so the
                // caller learns finalization failed.
                throw err;
            }
            if (onStep) {
                await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
            }
            return {
                answer,
                steps,
                totalSteps: i + 1,
                truncated: false,
            };
        }
        // Final answer — no tool call (legacy short-circuit)
        if (response.isFinal ||
            !response.toolCalls ||
            response.toolCalls.length === 0) {
            // finalizeAsTool mode but the LLM emitted isFinal without a
            // matching finalize tool call — warn and fall back to the legacy
            // short-circuit so a misbehaving LLM does not crash the loop.
            if (finalizeAsTool && response.isFinal) {
                logger?.warn?.("reactLoop.finalizeAsTool: isFinal=true but no matching finalize tool call — falling back to LLM content as answer", {
                    iteration: i,
                    loopId,
                    expectedTool: finalizeToolName,
                    toolCallsSeen: response.toolCalls?.map((c) => c.tool) ?? [],
                });
            }
            if (onStep) {
                await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
            }
            return {
                answer: response.content,
                steps,
                totalSteps: i + 1,
                truncated: false,
            };
        }
        const toolCall = response.toolCalls[0];
        // Validate tool call size
        const toolJson = JSON.stringify(toolCall);
        if (toolJson.length > maxToolCallSize) {
            steps[i].error = `Tool call exceeds ${maxToolCallSize}B limit`;
            steps[i].observation = `Error: tool call too large (${toolJson.length}B > ${maxToolCallSize}B limit)`;
            messages.push({ role: "assistant", content: response.content, name: toolCall.tool }, {
                role: "tool",
                content: steps[i].observation,
                tool_call_id: `call_${i}`,
            });
            if (onStep) {
                await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
            }
            continue;
        }
        // Check allowed tools
        if (allowedTools && !allowedTools.includes(toolCall.tool)) {
            steps[i].error = `Tool "${toolCall.tool}" not in allowed list`;
            steps[i].observation = `Error: tool "${toolCall.tool}" is not allowed. Available tools: ${allowedTools.join(", ")}`;
            messages.push({ role: "assistant", content: response.content, name: toolCall.tool }, {
                role: "tool",
                content: steps[i].observation,
                tool_call_id: `call_${i}`,
            });
            if (onStep) {
                await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
            }
            continue;
        }
        // Observation — execute tool
        try {
            const observation = await opts.executeTool(toolCall);
            steps[i].observation = observation;
        }
        catch (err) {
            steps[i].error = err instanceof Error ? err.message : String(err);
            steps[i].observation = `Error executing tool "${toolCall.tool}": ${steps[i].error}`;
        }
        // Feed observation back into the conversation
        messages.push({ role: "assistant", content: response.content, name: toolCall.tool }, {
            role: "tool",
            content: steps[i].observation,
            tool_call_id: `call_${i}`,
        });
        // Step is fully finalized (thought + action + observation/error).
        if (onStep) {
            await safeOnStep(onStep, steps[i], buildMeta(i, iterationStart), logger);
        }
    }
    // Truncated — max steps reached
    const lastStep = steps[steps.length - 1];
    return {
        answer: lastStep?.thought ?? "",
        steps,
        totalSteps: maxSteps,
        truncated: true,
    };
}
//# sourceMappingURL=react-loop.js.map