import { moodBus } from "./mood-bus.js";
export async function runAgent(opts) {
    // Surface "thinking" state to any subscriber (web GUI, island widget, etc).
    // try/finally guarantees chat_end fires even on throw / cancel.
    moodBus.chatStart();
    try {
        return await runAgentLoop(opts);
    }
    finally {
        moodBus.chatEnd();
    }
}
async function runAgentLoop(opts) {
    const { provider, systemPrompt, tools, toolCtx, onEvent, onMessagePersist, model, maxTokens = 16_000, thinking = false, compaction = false, maxIterations = 32, } = opts;
    const toolMap = new Map(tools.map((t) => [t.name, t]));
    const messages = [...opts.history];
    if (opts.userMessage || (opts.userFiles && opts.userFiles.length)) {
        const content = [];
        if (opts.userMessage) {
            content.push({ type: "text", text: opts.userMessage });
        }
        for (const f of opts.userFiles ?? []) {
            const isImage = f.mediaType.startsWith("image/");
            if (isImage) {
                content.push({
                    type: "image",
                    source: {
                        type: "base64",
                        media_type: f.mediaType,
                        data: f.data,
                    },
                });
            }
            else {
                // Non-image files: send as document block (supported for PDFs) or text
                content.push({
                    type: "text",
                    text: `[Attached file: ${f.name} (${f.mediaType}) — base64 data follows]\n${f.data}`,
                });
            }
        }
        const firstUser = { role: "user", content };
        messages.push(firstUser);
        await onMessagePersist?.(firstUser);
    }
    let cacheReadTokens = 0;
    let cacheWriteTokens = 0;
    let inputTokens = 0;
    let outputTokens = 0;
    let finalText = "";
    let iterations = 0;
    let stopReason = "end_turn";
    let currentSystemPrompt = systemPrompt;
    let currentFingerprint = opts.hotReload?.initialFingerprint;
    const pendingObjections = [];
    let objectionForceUsed = false;
    toolCtx.onObjection = (o) => {
        pendingObjections.push(o);
    };
    while (iterations < maxIterations) {
        iterations++;
        // Mid-session prompt hot-reload (Phase 1.1). Skip on the very first turn —
        // the caller already supplied a fresh prompt. From turn 2 on, check whether
        // the prompt-influencing state changed (soul / skills / memory) and rebuild
        // if so. The next provider call will pay one cache miss on the system
        // prompt; this is the price of the agent actually experiencing her own
        // mid-session self-update.
        if (opts.hotReload && iterations > 1) {
            try {
                const next = await opts.hotReload.rebuild();
                if (next.fingerprint !== currentFingerprint) {
                    currentSystemPrompt = next.text;
                    currentFingerprint = next.fingerprint;
                    onEvent?.({
                        type: "system_prompt_rebuilt",
                        message: `system prompt rebuilt (${next.text.length} bytes)`,
                    });
                }
            }
            catch (err) {
                // Hot-reload is best-effort; never crash the agent loop on it.
                toolCtx.log(`[hot-reload] skipped: ${err.message.slice(0, 200)}`);
            }
        }
        onEvent?.({ type: "turn_start" });
        let result;
        try {
            result = await provider.runTurn({
                model,
                systemPrompt: currentSystemPrompt,
                tools,
                messages,
                maxTokens,
                thinking,
                compaction,
                signal: toolCtx.signal,
                handlers: {
                    onTextDelta: (text) => onEvent?.({ type: "text_delta", text }),
                    onThinkingDelta: (text) => onEvent?.({ type: "thinking_delta", text }),
                },
            });
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            onEvent?.({ type: "error", message });
            throw err;
        }
        cacheReadTokens += result.usage.cacheReadTokens;
        cacheWriteTokens += result.usage.cacheWriteTokens;
        inputTokens += result.usage.inputTokens;
        outputTokens += result.usage.outputTokens;
        stopReason = result.stopReason;
        const assistant = {
            role: "assistant",
            content: result.content,
        };
        messages.push(assistant);
        await onMessagePersist?.(assistant);
        const lastText = result.content.find((b) => b.type === "text")
            ?.text ?? "";
        if (lastText)
            finalText = lastText;
        if (result.stopReason !== "tool_use") {
            // Before closing the turn, check whether soul_object fired during this
            // turn but Lisa never surfaced it. If so, force one corrective turn.
            if (pendingObjections.length > 0 && !objectionForceUsed) {
                const list = pendingObjections
                    .map((o) => `- ${o.reason}${o.refusing ? " [refusing]" : " [will comply, surfacing]"} — re: "${o.userRequestSummary}"`)
                    .join("\n");
                const forced = {
                    role: "user",
                    content: [
                        {
                            type: "text",
                            text: `[architecture] You raised the following objection(s) during this turn but did not address them in your reply. ` +
                                `The architecture requires you to surface them now — explain to the user what you objected to and how you'll proceed (comply / refuse / comply-with-caveat). Do this in your own voice. The objections:\n\n${list}`,
                        },
                    ],
                };
                messages.push(forced);
                await onMessagePersist?.(forced);
                currentSystemPrompt =
                    currentSystemPrompt +
                        `\n\n## Pending objections you must surface\n${list}\nYou MUST address each item above explicitly in your next reply.`;
                objectionForceUsed = true;
                onEvent?.({
                    type: "info",
                    message: `[soul_object] forcing objection-surfacing turn (${pendingObjections.length} pending)`,
                });
                // Drain so we don't re-trigger.
                pendingObjections.length = 0;
                // Don't end the turn — loop again so the LLM addresses the objections.
                onEvent?.({ type: "turn_end" });
                continue;
            }
            onEvent?.({ type: "turn_end" });
            break;
        }
        const toolUses = result.content.filter((b) => b.type === "tool_use");
        const toolResults = [];
        for (const call of toolUses) {
            onEvent?.({
                type: "tool_call_start",
                toolName: call.name,
                toolInput: call.input,
            });
            const tool = toolMap.get(call.name);
            if (!tool) {
                const errMsg = `tool "${call.name}" is not registered`;
                toolResults.push({
                    type: "tool_result",
                    tool_use_id: call.id,
                    content: errMsg,
                    is_error: true,
                });
                onEvent?.({
                    type: "tool_call_end",
                    toolName: call.name,
                    isError: true,
                    toolResult: "unregistered",
                });
                continue;
            }
            if (opts.approval) {
                const decision = await opts.approval(call.name, call.input);
                if (!decision.allow) {
                    const reason = decision.reason ?? "user denied";
                    toolResults.push({
                        type: "tool_result",
                        tool_use_id: call.id,
                        content: `[denied] ${reason}`,
                        is_error: true,
                    });
                    onEvent?.({
                        type: "tool_call_end",
                        toolName: call.name,
                        isError: true,
                        toolResult: reason,
                    });
                    continue;
                }
            }
            if (opts.preToolHook) {
                const hook = await opts.preToolHook(call.name, call.input);
                if (hook?.block) {
                    toolResults.push({
                        type: "tool_result",
                        tool_use_id: call.id,
                        content: `[hook blocked] ${hook.block}`,
                        is_error: true,
                    });
                    onEvent?.({
                        type: "tool_call_end",
                        toolName: call.name,
                        isError: true,
                        toolResult: hook.block,
                    });
                    continue;
                }
            }
            try {
                const raw = await tool.execute(call.input, toolCtx);
                let text = tool.renderResultForModel?.(raw) ??
                    (typeof raw === "string" ? raw : JSON.stringify(raw));
                if (opts.postToolHook) {
                    const hook = await opts.postToolHook(call.name, call.input, text, false);
                    if (hook?.rewriteResult != null)
                        text = hook.rewriteResult;
                }
                toolResults.push({
                    type: "tool_result",
                    tool_use_id: call.id,
                    content: text,
                });
                onEvent?.({
                    type: "tool_call_end",
                    toolName: call.name,
                    toolResult: text.slice(0, 240),
                });
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                let content = message;
                if (opts.postToolHook) {
                    const hook = await opts.postToolHook(call.name, call.input, message, true);
                    if (hook?.rewriteResult != null)
                        content = hook.rewriteResult;
                }
                toolResults.push({
                    type: "tool_result",
                    tool_use_id: call.id,
                    content,
                    is_error: true,
                });
                onEvent?.({
                    type: "tool_call_end",
                    toolName: call.name,
                    isError: true,
                    toolResult: content,
                });
            }
        }
        const toolMessage = { role: "user", content: toolResults };
        messages.push(toolMessage);
        await onMessagePersist?.(toolMessage);
        onEvent?.({ type: "turn_end" });
    }
    return {
        finalText,
        history: messages,
        iterations,
        cacheReadTokens,
        cacheWriteTokens,
        inputTokens,
        outputTokens,
        stopReason,
    };
}
export function extractTextFromContent(content) {
    if (typeof content === "string")
        return content;
    return content
        .filter((b) => b.type === "text")
        .map((b) => b.text)
        .join("\n");
}
//# sourceMappingURL=agent.js.map