/**
 * Agent Loop — reusable, streamable tool-calling loop.
 *
 * Extracted from chat.ts and enhanced with:
 *  - Input validation (catches bad LLM args immediately)
 *  - Parallel tool execution (independent calls run concurrently)
 *  - Retry for individual failed tool calls
 *  - Steering (inject guidance mid-loop)
 *  - Tool call queueing with priority
 *
 * Both the main chat route and the agent.spawn sub-agent tool use this.
 */
import { validateToolInput } from "./validate.js";
import { ASK_MODE_TOOLS, MUTATING_TOOLS } from "./chat-modes.js";
import { getReminderInstructions } from "./prompts/index.js";
import { computeContextUsage, estimateMessageTokens, estimateTokens } from "./token-estimator.js";
/** Format an LLM HTTP error into a user-friendly message, similar to VS Code Copilot. */
function formatLLMError(status, responseText) {
    let parsed;
    try {
        parsed = JSON.parse(responseText);
    }
    catch { /* not JSON */ }
    const serverMsg = parsed?.error?.message;
    const code = parsed?.error?.code;
    switch (status) {
        case 401:
            return "Your API key is invalid or expired. Please check your settings.";
        case 403:
            return "Access denied by the model provider. Please check your API key permissions.";
        case 429:
            if (code === "insufficient_quota" || serverMsg?.includes("quota"))
                return "You've exceeded your API quota. Please check your plan and billing details.";
            return `Rate limited by the model provider. Please wait a moment and try again.${serverMsg ? `\n${serverMsg}` : ""}`;
        case 500:
        case 502:
        case 503:
            return `The model provider is experiencing issues (${status}). Please try again later.`;
        default:
            return serverMsg
                ? `Request failed (${status}): ${serverMsg}`
                : `Request failed with status ${status}. Please try again.`;
    }
}
/** Priority levels for queued tool calls */
export var ToolCallPriority;
(function (ToolCallPriority) {
    /** Run before anything else (e.g. abort-checks, validation) */
    ToolCallPriority[ToolCallPriority["Critical"] = 0] = "Critical";
    /** Normal tool calls from the LLM */
    ToolCallPriority[ToolCallPriority["Normal"] = 1] = "Normal";
    /** Deferred / low-priority background work */
    ToolCallPriority[ToolCallPriority["Low"] = 2] = "Low";
})(ToolCallPriority || (ToolCallPriority = {}));
// ── Steering controller ──────────────────────────────────────────────
/**
 * Steering lets the user (or system) inject guidance into the agent
 * loop while it's running. The steered message gets appended to the
 * conversation as a system message before the next LLM call.
 */
export class SteeringController {
    queue = [];
    /** Inject a steering message into the loop */
    steer(message) {
        this.queue.push(message);
    }
    /** Drain all pending steering messages (called by the loop) */
    drain() {
        const msgs = this.queue.splice(0);
        return msgs;
    }
    get hasPending() {
        return this.queue.length > 0;
    }
}
// ── Tool call queue ──────────────────────────────────────────────────
/**
 * Priority queue for tool calls. Sorts by priority (lower = first),
 * then partitions into parallel-safe batches.
 */
export class ToolCallQueue {
    items = [];
    static MIN_PARALLEL_BATCH_SIZE = 3;
    /** Enqueue a tool call with optional priority and parallelism hint */
    enqueue(toolCall, priority = ToolCallPriority.Normal, parallelSafe = false) {
        this.items.push({ toolCall, priority, parallelSafe });
        // Keep sorted by priority
        this.items.sort((a, b) => a.priority - b.priority);
    }
    /** Enqueue multiple tool calls at the same priority */
    enqueueAll(toolCalls, priority = ToolCallPriority.Normal, parallelSafe = false) {
        for (const tc of toolCalls) {
            this.enqueue(tc, priority, parallelSafe);
        }
    }
    /**
     * Dequeue the next batch. If parallel execution is enabled, returns
     * all contiguous parallel-safe items at the same priority level, but
     * only when there are more than 2 consecutive calls to batch.
     * Otherwise returns one at a time.
     */
    dequeueBatch(allowParallel) {
        if (this.items.length === 0)
            return [];
        if (!allowParallel) {
            return [this.items.shift()];
        }
        const first = this.items[0];
        if (!first.parallelSafe) {
            return [this.items.shift()];
        }
        let contiguousParallelSafeCount = 0;
        while (contiguousParallelSafeCount < this.items.length &&
            this.items[contiguousParallelSafeCount].priority === first.priority &&
            this.items[contiguousParallelSafeCount].parallelSafe) {
            contiguousParallelSafeCount++;
        }
        if (contiguousParallelSafeCount < ToolCallQueue.MIN_PARALLEL_BATCH_SIZE) {
            return [this.items.shift()];
        }
        const batch = [];
        for (let i = 0; i < contiguousParallelSafeCount; i++) {
            batch.push(this.items.shift());
        }
        return batch;
    }
    get length() {
        return this.items.length;
    }
    get isEmpty() {
        return this.items.length === 0;
    }
}
// ── Tool name conversion ─────────────────────────────────────────────
/** OpenAI requires function names to match ^[a-zA-Z0-9_-]+$ — no dots */
export function toOpenAIName(name) {
    return name.replace(/\./g, "_");
}
export function fromOpenAIName(name) {
    if (name === "browser_sandbox_start")
        return "browser.sandbox.start";
    if (name === "ssh_session_start")
        return "ssh.session.start";
    if (name === "ssh_session_run")
        return "ssh.session.run";
    if (name === "ssh_session_close")
        return "ssh.session.close";
    const idx = name.indexOf("_");
    if (idx === -1)
        return name;
    return name.slice(0, idx) + "." + name.slice(idx + 1);
}
// ── Tools that are safe to run in parallel ───────────────────────────
/**
 * Read-only / side-effect-free tools that can safely execute concurrently.
 * Tools NOT in this set run sequentially to preserve ordering guarantees.
 */
const PARALLEL_SAFE_TOOLS = new Set([
    "file.read",
    "file.list",
    "file.stat",
    "os.query",
    "memory.search",
    "session.search",
    "web.fetch",
    "web.search",
    "gateway.status",
    "browser.snapshot",
    "browser.inspect",
]);
function isParallelSafe(toolName) {
    return PARALLEL_SAFE_TOOLS.has(toolName);
}
// ── Serialize messages for OpenAI API ────────────────────────────────
function syntheticMissingToolResult(toolCall) {
    return {
        role: "tool",
        content: JSON.stringify({
            ok: false,
            message: "Tool call did not complete before the next turn. Continue from the available context.",
        }),
        tool_call_id: toolCall.id,
        name: toolCall.function.name,
    };
}
export function repairToolCallHistory(messages) {
    const repaired = [];
    let pendingToolCalls = [];
    const flushMissingToolResults = () => {
        for (const toolCall of pendingToolCalls) {
            repaired.push(syntheticMissingToolResult(toolCall));
        }
        pendingToolCalls = [];
    };
    for (const message of messages) {
        if (message.role === "tool") {
            const toolCallId = message.tool_call_id;
            const pendingIndex = pendingToolCalls.findIndex((toolCall) => toolCall.id === toolCallId);
            if (pendingIndex === -1) {
                continue;
            }
            repaired.push(message);
            pendingToolCalls.splice(pendingIndex, 1);
            continue;
        }
        if (pendingToolCalls.length > 0) {
            flushMissingToolResults();
        }
        repaired.push(message);
        pendingToolCalls = message.role === "assistant" && message.tool_calls?.length
            ? [...message.tool_calls]
            : [];
    }
    if (pendingToolCalls.length > 0) {
        flushMissingToolResults();
    }
    messages.splice(0, messages.length, ...repaired);
}
export function serializeMessages(messages) {
    return messages.map((m) => {
        const msg = { role: m.role, content: m.content };
        if (m.tool_calls)
            msg.tool_calls = m.tool_calls;
        if (m.tool_call_id)
            msg.tool_call_id = m.tool_call_id;
        if (m.name)
            msg.name = m.name;
        return msg;
    });
}
// ── Build OpenAI tool schemas ────────────────────────────────────────
export function buildToolSchemas(registry, allowedTools) {
    let tools = registry.list();
    if (allowedTools) {
        tools = tools.filter((t) => allowedTools.has(t.name));
    }
    return tools.map((t) => ({
        type: "function",
        function: {
            name: toOpenAIName(t.name),
            description: t.description,
            parameters: t.parameters,
        },
    }));
}
/**
 * Build schemas respecting tiers and user disabled tools.
 *
 * Only "core" and "standard" (non-disabled) tools are included in the
 * initial payload. External / MCP tools must be discovered via tools.search.
 */
export function buildTieredToolSchemas(registry, disabledTools, options) {
    let tools = registry.listForLLM(disabledTools);
    if (options?.ollamaEssentials) {
        const keep = new Set(["read", "edit", "execute", "search", "web"]);
        tools = tools.filter((t) => keep.has(t.name));
    }
    return tools.map((t) => ({
        type: "function",
        function: {
            name: toOpenAIName(t.name),
            description: t.description,
            parameters: t.parameters,
        },
    }));
}
/**
 * Convert individual tool definitions into OpenAI schemas.
 * Used to dynamically inject schemas discovered via tools.search.
 */
export function toolDefsToSchemas(defs) {
    return defs.map((t) => ({
        type: "function",
        function: {
            name: toOpenAIName(t.name),
            description: t.description,
            parameters: t.parameters,
        },
    }));
}
export async function parseOpenAIStream(reader, onEvent) {
    const decoder = new TextDecoder();
    let buffer = "";
    let contentText = "";
    let thinkingText = "";
    let finishReason = null;
    let usage;
    const toolCallMap = new Map();
    const processLine = (line) => {
        const trimmed = line.trim();
        if (!trimmed || !trimmed.startsWith("data: "))
            return;
        const payload = trimmed.slice(6);
        if (payload === "[DONE]")
            return;
        try {
            const chunk = JSON.parse(payload);
            const choice = chunk.choices?.[0];
            if (!choice)
                return;
            const delta = choice.delta;
            if (!delta)
                return;
            // Reasoning / thinking tokens (e.g. Gemma 4, DeepSeek R1 via Ollama)
            const reasoningToken = delta.reasoning ?? delta.reasoning_content;
            if (reasoningToken) {
                thinkingText += reasoningToken;
                onEvent?.({ type: "thinking", content: reasoningToken });
            }
            // Text content
            if (delta.content) {
                contentText += delta.content;
                onEvent?.({ type: "token", content: delta.content });
            }
            // Tool calls (streamed incrementally)
            if (delta.tool_calls) {
                for (const tc of delta.tool_calls) {
                    const idx = tc.index ?? 0;
                    const isNew = !toolCallMap.has(idx);
                    if (isNew) {
                        toolCallMap.set(idx, {
                            id: tc.id ?? "",
                            type: "function",
                            function: { name: tc.function?.name ?? "", arguments: "" },
                        });
                    }
                    const existing = toolCallMap.get(idx);
                    if (tc.id)
                        existing.id = tc.id;
                    if (!isNew && tc.function?.name)
                        existing.function.name += tc.function.name;
                    if (tc.function?.arguments)
                        existing.function.arguments += tc.function.arguments;
                    const callId = existing.id || `pending-${idx}`;
                    onEvent?.({
                        type: "tool_call_delta",
                        call_id: callId,
                        index: idx,
                        name_delta: tc.function?.name || undefined,
                        args_delta: tc.function?.arguments || undefined,
                    });
                }
            }
            if (choice.finish_reason) {
                finishReason = choice.finish_reason;
            }
            // Usage stats (sent in the final chunk by OpenAI-compatible APIs)
            if (chunk.usage && typeof chunk.usage === "object") {
                const u = chunk.usage;
                if (typeof u.prompt_tokens === "number" && typeof u.completion_tokens === "number") {
                    usage = {
                        prompt_tokens: u.prompt_tokens,
                        completion_tokens: u.completion_tokens,
                        total_tokens: u.total_tokens ?? (u.prompt_tokens + u.completion_tokens),
                    };
                }
            }
        }
        catch {
            // partial JSON chunk — ignore
        }
    };
    while (true) {
        let readResult;
        try {
            readResult = await reader.read();
        }
        catch {
            // Abort or network error — return whatever we've accumulated so far
            break;
        }
        const { done, value } = readResult;
        if (done) {
            buffer += decoder.decode();
            if (buffer.trim()) {
                processLine(buffer);
            }
            break;
        }
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";
        for (const line of lines) {
            processLine(line);
        }
    }
    const toolCalls = [...toolCallMap.entries()]
        .sort(([a], [b]) => a - b)
        .map(([, tc]) => tc);
    return { contentText, thinkingText, toolCalls, finishReason, usage };
}
async function executeOneToolCall(opts) {
    const { tc, sessionId, auth, signal, toolRegistry, maxRetries, onEvent, executeTool } = opts;
    const startedAt = Date.now();
    let args;
    try {
        args = JSON.parse(tc.function.arguments);
    }
    catch {
        args = {};
    }
    const internalName = fromOpenAIName(tc.function.name);
    // ── Input validation (fast reject bad LLM args) ──
    if (toolRegistry) {
        const toolDef = toolRegistry.get(internalName);
        if (toolDef) {
            const validation = validateToolInput(toolDef.parameters, args);
            if (!validation.valid) {
                onEvent?.({
                    type: "tool_validation_error",
                    call_id: tc.id,
                    tool: internalName,
                    errors: validation.errors,
                });
                // Return the validation error as a tool result so the LLM can self-correct
                const errorMsg = `INPUT VALIDATION ERROR: ${validation.errors.join("; ")}`;
                const result = { ok: false, message: errorMsg };
                return {
                    result,
                    executed: {
                        callId: tc.id,
                        tool: internalName,
                        args,
                        ok: false,
                        message: errorMsg,
                        startedAt,
                        completedAt: Date.now(),
                        retryCount: 0,
                    },
                    historyEntry: {
                        role: "tool",
                        content: JSON.stringify({ ok: false, message: errorMsg }),
                        tool_call_id: tc.id,
                        name: tc.function.name,
                    },
                };
            }
        }
    }
    // ── Execute with retries ──
    onEvent?.({ type: "tool_start", tool: internalName, args, call_id: tc.id });
    let result = { ok: false, message: "Not executed" };
    let retryCount = 0;
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        if (signal?.aborted) {
            result = { ok: false, message: "Cancelled" };
            break;
        }
        if (attempt > 0) {
            onEvent?.({ type: "tool_retry", call_id: tc.id, attempt, maxAttempts: maxRetries });
            // Exponential backoff: 500ms, 1s, 2s
            await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** (attempt - 1), 4000)));
        }
        result = await executeTool(internalName, args, sessionId, auth, (chunk) => {
            onEvent?.({ type: "tool_output", call_id: tc.id, content: chunk });
        }, signal);
        if (result.ok)
            break;
        // Only retry transient failures, not logical errors
        if (!isTransientFailure(result.message))
            break;
        if (attempt >= maxRetries)
            break;
        retryCount = attempt + 1;
    }
    const completedAt = Date.now();
    onEvent?.({
        type: "tool_result",
        call_id: tc.id,
        tool: internalName,
        ok: result.ok,
        message: result.message,
        data: result.data,
    });
    // If this was a todo tool call, emit todo_list event for the UI
    if (internalName === "todo" && result.ok && result.data) {
        const items = result.data.items;
        if (Array.isArray(items)) {
            onEvent?.({ type: "todo_list", items });
        }
    }
    // If this was a file-modifying tool, emit file_changed event for cross-client sync
    if (result.ok && (internalName === "file.write" || internalName === "file.patch" || internalName === "edit")) {
        const filePath = String(args?.path ?? "");
        if (filePath) {
            onEvent?.({
                type: "file_changed",
                path: filePath,
                name: filePath.split("/").pop() ?? filePath,
            });
        }
    }
    return {
        result,
        executed: {
            callId: tc.id,
            tool: internalName,
            args,
            ok: result.ok,
            message: result.message,
            data: result.data,
            startedAt,
            completedAt,
            retryCount,
        },
        historyEntry: {
            role: "tool",
            content: JSON.stringify({
                ok: result.ok,
                message: result.message.length > TOOL_RESULT_MAX_CHARS
                    ? result.message.slice(0, TOOL_RESULT_MAX_CHARS) + `\n\n[truncated — ${result.message.length} chars total, showing first ${TOOL_RESULT_MAX_CHARS}]`
                    : result.message,
                data: result.data,
            }),
            tool_call_id: tc.id,
            name: tc.function.name,
        },
    };
}
/** Heuristic: is this error transient and worth retrying? */
function isTransientFailure(message) {
    const lower = message.toLowerCase();
    return (lower.includes("timeout") ||
        lower.includes("econnrefused") ||
        lower.includes("econnreset") ||
        lower.includes("socket hang up") ||
        lower.includes("rate limit") ||
        lower.includes("429") ||
        lower.includes("503") ||
        lower.includes("502") ||
        lower.includes("network") ||
        lower.includes("unavailable"));
}
export const __testUtils = {
    buildStructuredConversationSummary,
    executeOneToolCall,
    isTransientFailure,
    pruneHistory,
    repairToolCallHistory,
};
// ── Main agent loop ──────────────────────────────────────────────────
const DEFAULT_MAX_ROUNDS = 15;
const DEFAULT_MAX_RETRIES = 2;
/** Max times the same tool+args signature can be called before being rejected as a duplicate. */
const DUPLICATE_CALL_THRESHOLD = 2;
/** Max characters for a single tool result message before truncation (~8k tokens). */
const TOOL_RESULT_MAX_CHARS = 30_000;
/** Consecutive rounds with zero successful tool calls before the loop bails out. */
const MAX_UNPRODUCTIVE_ROUNDS = 3;
function latestUserContent(history) {
    for (let i = history.length - 1; i >= 0; i--) {
        const message = history[i];
        if (message?.role === "user" && message.content.trim())
            return message.content.trim();
    }
    return "Complete the user's swarm task.";
}
function inferSwarmPattern(objective) {
    const lower = objective.toLowerCase();
    if (/\b(fix|bug|test|typecheck|lint|refactor|implement|code|component|api|route|css|ui|backend|frontend)\b/.test(lower))
        return "code";
    if (/\b(research|compare|investigate|sources|docs|documentation|latest|market|evaluate)\b/.test(lower))
        return "research";
    if (/\b(write|draft|copy|article|post|email|slides|document|proposal|narrative)\b/.test(lower))
        return "content";
    return "general";
}
function buildSwarmCreateManyArgs(objective) {
    const pattern = inferSwarmPattern(objective);
    const common = [
        `Original user objective:\n${objective}`,
        "",
        "Return a concise result with findings, actions taken, changed files if any, verification evidence, and unresolved risks.",
        "Stay within the active project and do not duplicate work assigned to other specialists.",
    ].join("\n");
    const specsByPattern = {
        code: [
            {
                title: "Swarm Implementation Specialist",
                role: "Implementation Specialist",
                task: "Inspect the relevant code path, implement the smallest correct change for the objective, and run focused verification where practical.",
            },
            {
                title: "Swarm Verification Specialist",
                role: "Verification Specialist",
                task: "Independently inspect likely failure modes, check tests or missing coverage, and identify regressions or integration risks.",
            },
            {
                title: "Swarm Review Specialist",
                role: "Code Review Specialist",
                task: "Review the intended change for correctness, maintainability, UI/UX impact, and consistency with repository patterns.",
            },
        ],
        research: [
            {
                title: "Swarm Research Specialist",
                role: "Deep Research Specialist",
                task: "Gather source-backed facts, compare options, and return concise evidence with citations or file references.",
            },
            {
                title: "Swarm Analysis Specialist",
                role: "Analysis Specialist",
                task: "Evaluate tradeoffs, constraints, risks, and decision criteria for the objective.",
            },
            {
                title: "Swarm Synthesis Reviewer",
                role: "Synthesis Reviewer",
                task: "Check the research for gaps, contradictions, stale assumptions, and missing verification.",
            },
        ],
        content: [
            {
                title: "Swarm Drafting Specialist",
                role: "Drafting Specialist",
                task: "Create the primary deliverable or proposed content structure for the objective.",
            },
            {
                title: "Swarm Editing Specialist",
                role: "Editing Specialist",
                task: "Improve clarity, flow, tone, and audience fit while preserving technical accuracy.",
            },
            {
                title: "Swarm QA Specialist",
                role: "QA Specialist",
                task: "Check completeness, consistency, formatting, and likely user expectations.",
            },
        ],
        general: [
            {
                title: "Swarm Planner Specialist",
                role: "Planner Specialist",
                task: "Decompose the objective, identify dependencies, and propose a practical execution path.",
            },
            {
                title: "Swarm Execution Specialist",
                role: "Execution Specialist",
                task: "Carry out the most concrete actionable parts of the objective and report results.",
            },
            {
                title: "Swarm Review Specialist",
                role: "Review Specialist",
                task: "Validate the work, identify gaps, and recommend next steps.",
            },
        ],
    };
    return {
        action: "create_many",
        start: true,
        kind: "delegation",
        detach: false,
        autoStopAfterTurn: true,
        threads: specsByPattern[pattern].map((spec) => ({
            title: spec.title,
            kind: "delegation",
            prompt: [
                `Role: ${spec.role}`,
                `Task: ${spec.task}`,
                "",
                common,
            ].join("\n"),
        })),
    };
}
/**
 * Run the agentic tool-calling loop.
 *
 * This is the core reusable loop used by both the main chat route and
 * the agent.spawn sub-agent tool. It streams LLM responses, executes
 * tool calls (with validation, retry, parallel batching, and steering),
 * and returns the accumulated result.
 */
// ── Context pruning ──────────────────────────────────────────────────
/** Target ratio after pruning — leave headroom for the next LLM response */
const PRUNE_TARGET_RATIO = 0.65;
const SUMMARY_ITEM_LIMIT = 5;
const SUMMARY_TEXT_LIMIT = 220;
function compactSummaryText(value, max = SUMMARY_TEXT_LIMIT) {
    const compact = value.replace(/\s+/g, " ").trim();
    if (compact.length <= max)
        return compact;
    return `${compact.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
}
function pushSummaryItem(items, value, limit = SUMMARY_ITEM_LIMIT) {
    const item = compactSummaryText(value);
    if (!item || items.includes(item) || items.length >= limit)
        return;
    items.push(item);
}
function addFileReferences(value, files) {
    const text = typeof value === "string" ? value : JSON.stringify(value);
    if (!text)
        return;
    const filePathPattern = /(?:^|[\s([{'"`])((?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+(?:\.[A-Za-z0-9_.-]+)?)/g;
    for (const match of text.matchAll(filePathPattern)) {
        pushSummaryItem(files, match[1] ?? "", 10);
    }
}
function summarizeToolMessage(message) {
    const toolName = fromOpenAIName(message.name ?? "tool");
    let detail = message.content;
    try {
        const parsed = JSON.parse(message.content);
        if (typeof parsed.message === "string" && parsed.message.trim()) {
            detail = parsed.message;
        }
        else if (typeof parsed.ok === "boolean") {
            detail = parsed.ok ? "completed successfully" : "failed";
        }
    }
    catch {
        // Keep raw tool content.
    }
    return `Tool ${toolName}: ${detail}`;
}
function formatSummarySection(title, items) {
    const values = items.length > 0 ? items : ["Not captured in pruned turns."];
    return [title, ...values.map((item) => `- ${item}`)];
}
function buildStructuredConversationSummary(removedMessages) {
    const goals = [];
    const constraints = [];
    const decisions = [];
    const files = [];
    const progress = [];
    const nextSteps = [];
    const constraintPattern = /\b(must|never|always|prefer|avoid|require|required|constraint|do not|don't|keep|only)\b/i;
    const decisionPattern = /\b(decision|decided|chose|choose|selected|approach|instead|use|using|will|should)\b/i;
    const nextStepPattern = /\b(next|todo|follow up|remaining|continue|verify|test|run|implement|fix|later)\b/i;
    for (const message of removedMessages) {
        addFileReferences(message.content, files);
        if (message.tool_calls)
            addFileReferences(message.tool_calls, files);
        const content = compactSummaryText(message.content);
        if (!content && !message.tool_calls?.length)
            continue;
        if (message.role === "user") {
            pushSummaryItem(goals, content, 3);
            if (constraintPattern.test(content))
                pushSummaryItem(constraints, content);
            if (decisionPattern.test(content))
                pushSummaryItem(decisions, content);
            if (nextStepPattern.test(content))
                pushSummaryItem(nextSteps, content);
            continue;
        }
        if (message.role === "system") {
            if (constraintPattern.test(content))
                pushSummaryItem(constraints, content);
            continue;
        }
        if (message.role === "assistant") {
            if (content) {
                if (decisionPattern.test(content))
                    pushSummaryItem(decisions, content);
                pushSummaryItem(progress, `Assistant: ${content}`);
                if (nextStepPattern.test(content))
                    pushSummaryItem(nextSteps, content);
            }
            if (message.tool_calls?.length) {
                const tools = message.tool_calls.map((call) => fromOpenAIName(call.function.name)).join(", ");
                pushSummaryItem(progress, `Assistant requested tools: ${tools}`);
            }
            continue;
        }
        if (message.role === "tool") {
            pushSummaryItem(progress, summarizeToolMessage(message));
        }
    }
    if (nextSteps.length === 0) {
        nextSteps.push("Continue from the remaining messages below; the latest user turn is preserved verbatim.");
    }
    return [
        "[conversation-summary]",
        `Earlier messages pruned: ${removedMessages.length}.`,
        ...formatSummarySection("Goal:", goals),
        ...formatSummarySection("Constraints:", constraints),
        ...formatSummarySection("Decisions:", decisions),
        ...formatSummarySection("Files:", files),
        ...formatSummarySection("Progress:", progress),
        ...formatSummarySection("Next steps:", nextSteps),
        "[/conversation-summary]",
    ].join("\n");
}
/**
 * Prune oldest conversation turns to bring context usage below the target.
 *
 * Strategy (similar to Copilot):
 *  1. Never remove the system prompt (index 0) or the last user message.
 *  2. Remove oldest user/assistant/tool turn groups first.
 *  3. Insert a structured `[conversation-summary]` so the model keeps the
 *     pruned goal, constraints, decisions, files, progress, and next steps.
 *
 * Mutates `history` in place. Returns true if anything was pruned.
 */
function pruneHistory(history, contextWindow, toolSchemas) {
    const usage = computeContextUsage(history, toolSchemas, contextWindow);
    const targetTokens = Math.floor(contextWindow * PRUNE_TARGET_RATIO);
    if (usage.total <= targetTokens)
        return false;
    let tokensToFree = usage.total - targetTokens;
    let pruned = false;
    // Find removable range: skip system messages at the start and keep
    // the last user message + everything after it.
    let firstRemovable = 0;
    while (firstRemovable < history.length && history[firstRemovable].role === "system") {
        firstRemovable++;
    }
    // Find last user message index
    let lastUserIdx = history.length - 1;
    while (lastUserIdx >= 0 && history[lastUserIdx].role !== "user") {
        lastUserIdx--;
    }
    // Keep the last user message and everything after it
    const safeEnd = Math.max(lastUserIdx, firstRemovable);
    // Remove messages from firstRemovable forward until we've freed enough
    const removedIndices = [];
    const removedMessages = [];
    for (let i = firstRemovable; i < safeEnd && tokensToFree > 0; i++) {
        const msg = history[i];
        const cost = estimateMessageTokens(msg);
        tokensToFree -= cost;
        removedIndices.push(i);
        removedMessages.push(msg);
        pruned = true;
    }
    if (removedIndices.length > 0) {
        // Remove in reverse order to keep indices stable
        for (let i = removedIndices.length - 1; i >= 0; i--) {
            history.splice(removedIndices[i], 1);
        }
        // Insert a structured summary after the system messages.
        const insertAt = firstRemovable;
        history.splice(insertAt, 0, {
            role: "system",
            content: buildStructuredConversationSummary(removedMessages),
        });
    }
    return pruned;
}
export async function runAgentLoop(options, executeTool, steering) {
    const { llm, history, toolSchemas: initialToolSchemas, hasTools, sessionId, auth, abort, maxRounds = DEFAULT_MAX_ROUNDS, maxRetries = DEFAULT_MAX_RETRIES, parallel = true, toolRegistry, disabledTools, mode = "agent", onEvent, onContext, onPersist, log = console, } = options;
    let fullContent = "";
    const executedToolCalls = [];
    const segments = [];
    const queue = new ToolCallQueue();
    // ── Loop health tracking ──
    /** Fingerprint → call count. Detects the model calling the same tool with identical args. */
    const toolCallFingerprints = new Map(options.priorFingerprints ?? []);
    /** Consecutive rounds where no tool call succeeded — triggers early bail-out. */
    let consecutiveUnproductiveRounds = 0;
    // ── Plan mode state ──
    const plannedActions = [];
    const planId = mode === "plan" ? `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` : "";
    // ── Mode-aware schema filtering ──
    // Ask mode: only read-only tools. Plan/Agent/Swarm mode: full set.
    let modeFilteredSchemas = initialToolSchemas;
    if (mode === "ask") {
        modeFilteredSchemas = initialToolSchemas.filter((s) => ASK_MODE_TOOLS.has(fromOpenAIName(s.function.name)));
        onEvent?.({ type: "mode_notice", mode: "ask", message: "Running in Ask mode — read-only tools only." });
    }
    else if (mode === "plan") {
        onEvent?.({ type: "mode_notice", mode: "plan", message: "Running in Plan mode — mutating actions will be proposed, not executed." });
    }
    else if (mode === "swarm") {
        onEvent?.({ type: "mode_notice", mode: "swarm", message: "Running in Swarm mode — starting visible specialist threads for this task." });
    }
    // Dynamic schema set — starts with filtered schemas, grows when tools.search
    // discovers additional tools (e.g. external/MCP tools).
    const activeSchemas = [...modeFilteredSchemas];
    const activeSchemaNames = new Set(activeSchemas.map((s) => s.function.name));
    if (mode === "swarm") {
        const callId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const swarmArgs = buildSwarmCreateManyArgs(latestUserContent(history));
        const swarmCall = {
            id: callId,
            type: "function",
            function: {
                name: toOpenAIName("thread.control"),
                arguments: JSON.stringify(swarmArgs),
            },
        };
        segments.push({ type: "toolGroup", callIds: [callId] });
        const { executed, historyEntry } = await executeOneToolCall({
            tc: swarmCall,
            sessionId,
            auth,
            signal: abort.signal,
            toolRegistry,
            maxRetries,
            onEvent,
            executeTool,
        });
        executedToolCalls.push(executed);
        history.push({
            role: "assistant",
            content: "",
            tool_calls: [swarmCall],
        });
        history.push(historyEntry);
    }
    for (let round = 0; round < maxRounds; round++) {
        // ── Check abort ──
        if (abort.signal.aborted) {
            log.info(`Agent loop cancelled for session ${sessionId} — stopping before round ${round}`);
            return { content: fullContent, executedToolCalls, segments, rounds: round, aborted: true, hitMaxRounds: false, fingerprints: toolCallFingerprints };
        }
        // ── Apply steering messages ──
        if (steering) {
            const steered = steering.drain();
            for (const msg of steered) {
                history.push({ role: "system", content: `[STEERING] ${msg}` });
                onEvent?.({ type: "steering", message: msg });
                log.info(`Steering injected for session ${sessionId}: ${msg.slice(0, 100)}`);
            }
        }
        // ── Periodic reminder (every 4 rounds) to reinforce good agent behaviour ──
        if (round > 0 && round % 4 === 0 && mode !== "ask") {
            const modelEndpoint = { model: llm.openaiModel, baseUrl: "" };
            const reminder = getReminderInstructions(mode, modelEndpoint);
            if (reminder) {
                history.push({ role: "system", content: reminder });
            }
        }
        // ── Context budget tracking & pruning ──────────────────────────────
        const contextWindow = llm.contextWindow;
        if (contextWindow > 0) {
            let usage = computeContextUsage(history, activeSchemas, contextWindow);
            onEvent?.({ type: "context_usage", ...usage });
            // If context usage ≥ 85%, prune oldest conversation turns
            if (usage.ratio >= 0.85) {
                const pruned = pruneHistory(history, contextWindow, activeSchemas);
                if (pruned) {
                    usage = computeContextUsage(history, activeSchemas, contextWindow);
                    onEvent?.({ type: "context_usage", ...usage, pruned: true });
                    log.info(`Context pruned: ${usage.total}/${contextWindow} tokens (${(usage.ratio * 100).toFixed(0)}%)`);
                }
            }
        }
        repairToolCallHistory(history);
        // ── LLM request ──
        const reqBody = {
            model: llm.openaiModel,
            messages: serializeMessages(history),
            stream: true,
            stream_options: { include_usage: true },
        };
        if (hasTools) {
            reqBody.tools = activeSchemas;
            reqBody.tool_choice = "auto";
        }
        // Snapshot context_usage for inclusion in round metrics
        let roundContextUsage;
        if (contextWindow > 0) {
            const snap = computeContextUsage(history, activeSchemas, contextWindow);
            roundContextUsage = snap;
        }
        // Track the current round object so we can attach metrics after the LLM call
        const currentRound = {
            round: round + 1,
            createdAt: new Date().toISOString(),
            model: llm.openaiModel,
            messages: reqBody.messages,
            ...(hasTools ? { tools: activeSchemas, tool_choice: "auto" } : {}),
        };
        onContext?.(currentRound);
        const llmStartTime = Date.now();
        // Debug: log payload size so we can diagnose slow local model requests
        {
            const bodyJson = JSON.stringify(reqBody);
            const sysMsgLen = history.find(m => m.role === "system")?.content?.length ?? 0;
            log.info(`LLM request round=${round + 1} model=${llm.openaiModel} bodySize=${bodyJson.length} sysPromptChars=${sysMsgLen} tools=${activeSchemas.length} → ${llm.openaiBaseUrl}`);
        }
        let contentText = "";
        let thinkingText = "";
        let toolCalls = [];
        let finishReason = null;
        try {
            // Use a generous timeout (10 min) for LLM streaming — slow local models
            // (e.g. Ollama on CPU) can take several minutes for the first token.
            // Combine with the user-cancel abort signal so either can stop the request.
            const llmSignal = AbortSignal.any([
                abort.signal,
                AbortSignal.timeout(10 * 60 * 1000),
            ]);
            const response = await fetch(`${llm.openaiBaseUrl}/chat/completions`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Authorization: `Bearer ${llm.openaiApiKey}`,
                },
                body: JSON.stringify(reqBody),
                signal: llmSignal,
            });
            if (!response.ok) {
                const errText = await response.text();
                log.error(`LLM error ${response.status}: ${errText}`);
                const friendlyMessage = formatLLMError(response.status, errText);
                onEvent?.({ type: "error", message: friendlyMessage });
                return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: false, hitMaxRounds: false, fingerprints: toolCallFingerprints };
            }
            const reader = response.body?.getReader();
            if (!reader) {
                onEvent?.({ type: "error", message: "No response body from LLM" });
                return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: false, hitMaxRounds: false, fingerprints: toolCallFingerprints };
            }
            const parsed = await parseOpenAIStream(reader, onEvent);
            contentText = parsed.contentText;
            thinkingText = parsed.thinkingText;
            toolCalls = parsed.toolCalls;
            finishReason = parsed.finishReason;
            // Attach metrics to the round now that streaming is complete
            const durationMs = Date.now() - llmStartTime;
            const metrics = { durationMs };
            if (parsed.usage) {
                metrics.promptTokens = parsed.usage.prompt_tokens;
                metrics.completionTokens = parsed.usage.completion_tokens;
                metrics.totalTokens = parsed.usage.total_tokens;
                if (parsed.usage.completion_tokens > 0 && durationMs > 0) {
                    metrics.tokensPerSecond = Math.round((parsed.usage.completion_tokens / (durationMs / 1000)) * 10) / 10;
                }
            }
            else {
                // Estimate completion tokens from text length when provider doesn't report usage
                const estimatedCompletionTokens = estimateTokens(contentText + thinkingText);
                if (estimatedCompletionTokens > 0 && durationMs > 0) {
                    metrics.completionTokens = estimatedCompletionTokens;
                    metrics.tokensPerSecond = Math.round((estimatedCompletionTokens / (durationMs / 1000)) * 10) / 10;
                }
            }
            if (roundContextUsage)
                metrics.contextUsage = roundContextUsage;
            currentRound.metrics = metrics;
        }
        catch (fetchErr) {
            if (abort.signal.aborted) {
                log.info(`Agent loop cancelled during LLM streaming (round ${round})`);
                return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: true, hitMaxRounds: false, fingerprints: toolCallFingerprints };
            }
            throw fetchErr;
        }
        fullContent += contentText;
        // ── Track segments for interleaved rendering ──
        if (thinkingText) {
            const last = segments[segments.length - 1];
            if (last?.type === "thinking") {
                segments[segments.length - 1] = { type: "thinking", content: last.content + thinkingText };
            }
            else {
                segments.push({ type: "thinking", content: thinkingText });
            }
        }
        if (contentText) {
            const last = segments[segments.length - 1];
            if (last?.type === "text") {
                segments[segments.length - 1] = { type: "text", content: last.content + contentText };
            }
            else {
                segments.push({ type: "text", content: contentText });
            }
        }
        // ── Model returned tool calls → queue & execute ──
        if (toolCalls.length > 0) {
            if (finishReason && finishReason !== "tool_calls") {
                log.warn(`LLM returned ${toolCalls.length} tool call(s) with finish_reason="${finishReason}" — executing anyway`);
            }
            // Push assistant message with tool_calls to history
            history.push({
                role: "assistant",
                content: contentText || "",
                tool_calls: toolCalls,
            });
            // Enqueue tool calls, rejecting duplicates
            for (const tc of toolCalls) {
                const internalName = fromOpenAIName(tc.function.name);
                const fingerprint = `${internalName}:${tc.function.arguments}`;
                const prevCount = toolCallFingerprints.get(fingerprint) ?? 0;
                toolCallFingerprints.set(fingerprint, prevCount + 1);
                if (prevCount >= DUPLICATE_CALL_THRESHOLD) {
                    // Duplicate detected — skip execution, tell the model to try something else
                    log.warn(`Duplicate tool call detected: ${internalName} (${prevCount + 1}x) — skipping`);
                    const dupMsg = `DUPLICATE CALL: You have already called "${internalName}" with these exact arguments ${prevCount + 1} times. The result will not change. Try a different approach or tool.`;
                    history.push({
                        role: "tool",
                        content: JSON.stringify({ ok: false, message: dupMsg }),
                        tool_call_id: tc.id,
                        name: tc.function.name,
                    });
                    executedToolCalls.push({
                        callId: tc.id,
                        tool: internalName,
                        args: (() => { try {
                            return JSON.parse(tc.function.arguments);
                        }
                        catch {
                            return {};
                        } })(),
                        ok: false,
                        message: dupMsg,
                        startedAt: Date.now(),
                        completedAt: Date.now(),
                    });
                    continue;
                }
                queue.enqueue(tc, ToolCallPriority.Normal, isParallelSafe(internalName));
            }
            // Track tool calls as a segment group for interleaved rendering
            const callIds = toolCalls.map(tc => tc.id);
            const lastSeg = segments[segments.length - 1];
            if (lastSeg?.type === "toolGroup") {
                // Extend existing group (shouldn't normally happen, but defensive)
                const merged = new Set([...lastSeg.callIds, ...callIds]);
                segments[segments.length - 1] = { type: "toolGroup", callIds: [...merged] };
            }
            else {
                segments.push({ type: "toolGroup", callIds });
            }
            // ── Plan-mode & Ask-mode interception ──
            // In plan mode, mutating tools are captured as plan actions.
            // In ask mode, any tool that slipped through is blocked.
            if (mode === "plan" || mode === "ask") {
                const intercepted = [];
                const passthrough = [];
                while (!queue.isEmpty) {
                    const batch = queue.dequeueBatch(false);
                    for (const item of batch) {
                        const name = fromOpenAIName(item.toolCall.function.name);
                        const isMutating = MUTATING_TOOLS.has(name);
                        if (mode === "ask" && !ASK_MODE_TOOLS.has(name)) {
                            // Ask mode: block non-read tools, return error to LLM
                            intercepted.push(item);
                        }
                        else if (mode === "plan" && isMutating) {
                            // Plan mode: capture mutating tools as planned actions
                            intercepted.push(item);
                        }
                        else {
                            passthrough.push(item);
                        }
                    }
                }
                // Handle intercepted tool calls
                for (const item of intercepted) {
                    const tc = item.toolCall;
                    const name = fromOpenAIName(tc.function.name);
                    let args;
                    try {
                        args = JSON.parse(tc.function.arguments);
                    }
                    catch {
                        args = {};
                    }
                    if (mode === "ask") {
                        // Return an error to the LLM
                        const msg = `Tool "${name}" is not available in Ask mode. Only read-only tools can be used. Suggest the user switch to Agent or Plan mode for this action.`;
                        history.push({
                            role: "tool",
                            content: JSON.stringify({ ok: false, message: msg }),
                            tool_call_id: tc.id,
                            name: tc.function.name,
                        });
                        executedToolCalls.push({
                            callId: tc.id,
                            tool: name,
                            args,
                            ok: false,
                            message: msg,
                            startedAt: Date.now(),
                            completedAt: Date.now(),
                        });
                    }
                    else {
                        // Plan mode: capture as planned action
                        const action = {
                            id: tc.id,
                            tool: name,
                            args,
                            description: `${name}(${JSON.stringify(args).slice(0, 200)})`,
                            order: plannedActions.length,
                            status: "pending",
                        };
                        plannedActions.push(action);
                        onEvent?.({ type: "plan_action", action });
                        // Tell the LLM the action was captured
                        const msg = `[PLANNED] Action "${name}" has been added to the plan (step ${action.order + 1}). It will execute after user approval. Continue analyzing and propose more actions if needed.`;
                        history.push({
                            role: "tool",
                            content: JSON.stringify({ ok: true, message: msg }),
                            tool_call_id: tc.id,
                            name: tc.function.name,
                        });
                        executedToolCalls.push({
                            callId: tc.id,
                            tool: name,
                            args,
                            ok: true,
                            message: msg,
                            startedAt: Date.now(),
                            completedAt: Date.now(),
                        });
                    }
                }
                // Re-enqueue passthrough items
                for (const item of passthrough) {
                    queue.enqueue(item.toolCall, item.priority, item.parallelSafe);
                }
            }
            // ── Process the queue ──
            while (!queue.isEmpty) {
                if (abort.signal.aborted) {
                    // Mark remaining queued calls as cancelled
                    while (!queue.isEmpty) {
                        const batch = queue.dequeueBatch(false);
                        for (const item of batch) {
                            let rArgs;
                            try {
                                rArgs = JSON.parse(item.toolCall.function.arguments);
                            }
                            catch {
                                rArgs = {};
                            }
                            executedToolCalls.push({
                                callId: item.toolCall.id,
                                tool: fromOpenAIName(item.toolCall.function.name),
                                args: rArgs,
                                ok: false,
                                message: "Cancelled",
                                startedAt: Date.now(),
                                completedAt: Date.now(),
                            });
                        }
                    }
                    return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: true, hitMaxRounds: false, fingerprints: toolCallFingerprints };
                }
                const batch = queue.dequeueBatch(parallel);
                if (batch.length === 1) {
                    // Sequential execution (single item or non-parallel-safe)
                    const item = batch[0];
                    const { executed, historyEntry } = await executeOneToolCall({
                        tc: item.toolCall,
                        sessionId,
                        auth,
                        signal: abort.signal,
                        toolRegistry,
                        maxRetries,
                        onEvent,
                        executeTool,
                    });
                    executedToolCalls.push(executed);
                    history.push(historyEntry);
                }
                else {
                    // ── Parallel execution ──
                    log.info(`Executing ${batch.length} tool calls in parallel`);
                    const results = await Promise.all(batch.map((item) => executeOneToolCall({
                        tc: item.toolCall,
                        sessionId,
                        auth,
                        signal: abort.signal,
                        toolRegistry,
                        maxRetries,
                        onEvent,
                        executeTool,
                    })));
                    for (const { executed, historyEntry } of results) {
                        executedToolCalls.push(executed);
                        history.push(historyEntry);
                    }
                }
            }
            // ── Dynamic schema expansion ──
            // If any tool call was tools.search/tools.list, check if the result
            // contains new tool schemas that should be injected for subsequent rounds.
            for (const exec of executedToolCalls) {
                if (exec.tool === "tools.search" && exec.ok && exec.data) {
                    const data = exec.data;
                    if (Array.isArray(data.matches)) {
                        for (const match of data.matches) {
                            if (match.name && match.description && match.parameters) {
                                const oaiName = toOpenAIName(match.name);
                                // In ask mode, only add read-only tools
                                if (mode === "ask" && !ASK_MODE_TOOLS.has(match.name))
                                    continue;
                                if (!activeSchemaNames.has(oaiName) && !disabledTools?.has(match.name)) {
                                    activeSchemas.push({
                                        type: "function",
                                        function: {
                                            name: oaiName,
                                            description: match.description,
                                            parameters: match.parameters,
                                        },
                                    });
                                    activeSchemaNames.add(oaiName);
                                    log.info(`Dynamic schema expansion: added ${match.name}`);
                                }
                            }
                        }
                    }
                }
            }
            // ── Stuck-loop detection ──
            // If every tool call in this round failed, increment the unproductive counter.
            const roundStart = executedToolCalls.length - toolCalls.length;
            const roundCalls = executedToolCalls.slice(roundStart < 0 ? 0 : roundStart);
            const anySuccess = roundCalls.some(c => c.ok);
            if (anySuccess) {
                consecutiveUnproductiveRounds = 0;
            }
            else {
                consecutiveUnproductiveRounds++;
                if (consecutiveUnproductiveRounds >= MAX_UNPRODUCTIVE_ROUNDS) {
                    log.warn(`${MAX_UNPRODUCTIVE_ROUNDS} consecutive unproductive rounds — stopping loop for session ${sessionId}`);
                    const bailMsg = "\n\n[Stopped: multiple consecutive rounds produced no successful results. The current approach isn't working — please try a different strategy.]";
                    onEvent?.({ type: "token", content: bailMsg });
                    fullContent += bailMsg;
                    const tcJson = executedToolCalls.length > 0 ? JSON.stringify(executedToolCalls) : undefined;
                    const segJson = segments.length > 0 ? JSON.stringify(segments) : undefined;
                    onPersist?.(sessionId, "assistant", fullContent, tcJson, segJson, undefined);
                    return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: false, hitMaxRounds: false, fingerprints: toolCallFingerprints };
                }
            }
            // Loop continues — LLM sees results and decides next
            continue;
        }
        // ── Normal text response — done ──
        if (contentText) {
            history.push({ role: "assistant", content: contentText });
        }
        if (fullContent || thinkingText || segments.length > 0 || executedToolCalls.length > 0) {
            const tcJson = executedToolCalls.length > 0 ? JSON.stringify(executedToolCalls) : undefined;
            const segJson = segments.length > 0 ? JSON.stringify(segments) : undefined;
            onPersist?.(sessionId, "assistant", fullContent, tcJson, segJson, thinkingText || undefined);
        }
        // ── Emit plan completion in plan mode ──
        if (mode === "plan" && plannedActions.length > 0) {
            onEvent?.({
                type: "plan_complete",
                planId,
                summary: contentText || "Plan ready for review.",
                actions: plannedActions,
            });
        }
        const planResult = mode === "plan" && plannedActions.length > 0
            ? { id: planId, summary: contentText || "Plan ready for review.", actions: plannedActions }
            : undefined;
        return { content: fullContent, executedToolCalls, segments, rounds: round + 1, aborted: false, hitMaxRounds: false, fingerprints: toolCallFingerprints, plan: planResult };
    }
    // Hit max rounds
    log.warn(`Agent loop hit max rounds (${maxRounds}) for session ${sessionId}`);
    const msg = "\n\n[Reached maximum tool execution rounds. Stopping.]";
    onEvent?.({ type: "token", content: msg });
    fullContent += msg;
    {
        const tcJson = executedToolCalls.length > 0 ? JSON.stringify(executedToolCalls) : undefined;
        const segJson = segments.length > 0 ? JSON.stringify(segments) : undefined;
        onPersist?.(sessionId, "assistant", fullContent, tcJson, segJson, undefined);
    }
    const planResultMaxRounds = mode === "plan" && plannedActions.length > 0
        ? { id: planId, summary: fullContent, actions: plannedActions }
        : undefined;
    return { content: fullContent, executedToolCalls, segments, rounds: maxRounds, aborted: false, hitMaxRounds: true, fingerprints: toolCallFingerprints, plan: planResultMaxRounds };
}
// ── Retry API ────────────────────────────────────────────────────────
/**
 * Retry a specific failed tool call by its callId.
 *
 * This re-executes the tool with its original arguments, updates the
 * conversation history in-place (replaces the old tool result message),
 * and returns the new result.
 *
 * Designed to be called from a REST endpoint like:
 *   POST /api/sessions/:sessionId/retry-tool
 *   { callId: "call_abc123" }
 */
export async function retryToolCall(callId, history, executedToolCalls, executeTool, sessionId, auth, onEvent, signal) {
    // Find the original call
    const original = executedToolCalls.find((tc) => tc.callId === callId);
    if (!original) {
        return { ok: false, message: `Tool call ${callId} not found` };
    }
    // Find and update history entry
    const histIdx = history.findIndex((m) => m.role === "tool" && m.tool_call_id === callId);
    const startedAt = Date.now();
    onEvent?.({
        type: "tool_start",
        tool: original.tool,
        args: original.args,
        call_id: callId,
    });
    const result = await executeTool(original.tool, original.args, sessionId, auth, (chunk) => onEvent?.({ type: "tool_output", call_id: callId, content: chunk }), signal);
    const completedAt = Date.now();
    onEvent?.({
        type: "tool_result",
        call_id: callId,
        tool: original.tool,
        ok: result.ok,
        message: result.message,
        data: result.data,
    });
    // Update the executed tool call record
    original.ok = result.ok;
    original.message = result.message;
    original.data = result.data;
    original.startedAt = startedAt;
    original.completedAt = completedAt;
    original.retryCount = (original.retryCount ?? 0) + 1;
    // Update conversation history so the LLM sees the new result
    if (histIdx !== -1) {
        history[histIdx] = {
            role: "tool",
            content: JSON.stringify({ ok: result.ok, message: result.message, data: result.data }),
            tool_call_id: callId,
            name: toOpenAIName(original.tool),
        };
    }
    return result;
}
//# sourceMappingURL=agent-loop.js.map