import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { optimizeMessagesWithStats, } from "../llm/message-optimizer.js";
import { safeSliceHead, safeSliceTail } from "../utils/safe-slice.js";
// Budgets in *characters* (≈ 1 token per 3.5–4 chars). Sized assuming the
// provider's input-prefix cache absorbs the bulk of the cost — see step #1
// of the context-optimization rollout. If you're running a model without
// prompt caching, divide these by ~3.
export const CONTEXT_BUDGETS = {
    chat: {
        softLimitChars: 60_000,
        hardLimitChars: 100_000,
        targetChars: 40_000,
        keepRatio: 0.6,
        memoryIndexMaxEntries: 30,
        memoryIndexMaxBytes: 6 * 1024,
        installedSkillsMaxChars: 4 * 1024,
    },
    task: {
        compactionCharBudget: 80_000,
        keepRatio: 0.55,
        memoryIndexMaxEntries: 20,
        memoryIndexMaxBytes: 4 * 1024,
        installedSkillsMaxChars: 4 * 1024,
        relevantMemoryMaxFiles: 5,
        relevantMemoryMaxTotalBytes: 8 * 1024,
        relevantMemoryPerFileBytes: 2 * 1024,
    },
    summaryInput: {
        assistantContentPreviewChars: 800,
        toolCallArgsPreviewChars: 300,
        toolResultPreviewChars: 800,
        messagePreviewChars: 1000,
        snapshotPreviewChars: 1600,
    },
};
const CONTEXT_STATE_LIMITS = {
    goals: { maxItems: 20, maxItemChars: 300 },
    constraints: { maxItems: 30, maxItemChars: 300 },
    facts: { maxItems: 80, maxItemChars: 500 },
    decisions: { maxItems: 50, maxItemChars: 400 },
    visitedUrls: { maxItems: 80, maxItemChars: 500 },
    completedActions: { maxItems: 80, maxItemChars: 400 },
    errors: { maxItems: 50, maxItemChars: 500 },
    pending: { maxItems: 50, maxItemChars: 400 },
    rawRefs: { maxItems: 200, maxItemChars: 80 },
    currentPage: {
        titleChars: 200,
        urlChars: 500,
        noteChars: 400,
    },
};
export function createEmptyContextState() {
    return {
        goals: [],
        constraints: [],
        facts: [],
        decisions: [],
        visitedUrls: [],
        completedActions: [],
        errors: [],
        pending: [],
        rawRefs: [],
    };
}
export function estimateMessagesChars(messages) {
    return messages.reduce((sum, msg) => {
        const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
        const toolCalls = msg.toolCalls ? JSON.stringify(msg.toolCalls) : "";
        return sum + content.length + toolCalls.length;
    }, 0);
}
function hashText(text) {
    return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
}
function truncateContextText(text, maxChars) {
    if (text.length <= maxChars)
        return text;
    const marker = "...[truncated]";
    const headBudget = Math.max(0, maxChars - marker.length);
    return `${safeSliceHead(text, headBudget)}${safeSliceHead(marker, maxChars)}`;
}
export function truncateContextSection(text, maxChars, note) {
    if (text.length <= maxChars)
        return text;
    const suffix = `\n... (${note})`;
    const headBudget = Math.max(0, maxChars - suffix.length);
    return headBudget > 0
        ? `${safeSliceHead(text, headBudget)}${suffix}`
        : safeSliceHead(suffix, maxChars);
}
function normalizedList(raw, limits) {
    if (!Array.isArray(raw))
        return [];
    const out = [];
    for (const item of raw) {
        const text = typeof item === "string"
            ? item
            : item == null
                ? ""
                : JSON.stringify(item);
        const trimmed = truncateContextText(text.trim(), limits.maxItemChars);
        if (trimmed && !out.includes(trimmed))
            out.push(trimmed);
    }
    return out.slice(-limits.maxItems);
}
function mergeLists(a, b, limits) {
    const out = [];
    for (const item of [...a, ...b]) {
        const trimmed = truncateContextText(item.trim(), limits.maxItemChars);
        if (trimmed && !out.includes(trimmed))
            out.push(trimmed);
    }
    return out.slice(-limits.maxItems);
}
function normalizeCurrentPage(raw) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw))
        return undefined;
    const cp = raw;
    const limits = CONTEXT_STATE_LIMITS.currentPage;
    const currentPage = {
        ...(typeof cp.title === "string" && cp.title.trim()
            ? { title: truncateContextText(cp.title.trim(), limits.titleChars) }
            : {}),
        ...(typeof cp.url === "string" && cp.url.trim()
            ? { url: truncateContextText(cp.url.trim(), limits.urlChars) }
            : {}),
        ...(typeof cp.note === "string" && cp.note.trim()
            ? { note: truncateContextText(cp.note.trim(), limits.noteChars) }
            : {}),
    };
    return Object.keys(currentPage).length > 0 ? currentPage : undefined;
}
function mergeCurrentPage(previous, next) {
    if (!previous)
        return normalizeCurrentPage(next);
    if (!next)
        return normalizeCurrentPage(previous);
    return normalizeCurrentPage({ ...previous, ...next });
}
export function limitContextState(state) {
    return {
        goals: mergeLists([], state.goals, CONTEXT_STATE_LIMITS.goals),
        constraints: mergeLists([], state.constraints, CONTEXT_STATE_LIMITS.constraints),
        facts: mergeLists([], state.facts, CONTEXT_STATE_LIMITS.facts),
        decisions: mergeLists([], state.decisions, CONTEXT_STATE_LIMITS.decisions),
        visitedUrls: mergeLists([], state.visitedUrls, CONTEXT_STATE_LIMITS.visitedUrls),
        completedActions: mergeLists([], state.completedActions, CONTEXT_STATE_LIMITS.completedActions),
        errors: mergeLists([], state.errors, CONTEXT_STATE_LIMITS.errors),
        pending: mergeLists([], state.pending, CONTEXT_STATE_LIMITS.pending),
        currentPage: normalizeCurrentPage(state.currentPage),
        rawRefs: mergeLists([], state.rawRefs, CONTEXT_STATE_LIMITS.rawRefs),
    };
}
export function normalizeContextState(raw) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw))
        return null;
    const obj = raw;
    const state = {
        goals: normalizedList(obj.goals, CONTEXT_STATE_LIMITS.goals),
        constraints: normalizedList(obj.constraints, CONTEXT_STATE_LIMITS.constraints),
        facts: normalizedList(obj.facts, CONTEXT_STATE_LIMITS.facts),
        decisions: normalizedList(obj.decisions, CONTEXT_STATE_LIMITS.decisions),
        visitedUrls: normalizedList(obj.visitedUrls, CONTEXT_STATE_LIMITS.visitedUrls),
        completedActions: normalizedList(obj.completedActions, CONTEXT_STATE_LIMITS.completedActions),
        errors: normalizedList(obj.errors, CONTEXT_STATE_LIMITS.errors),
        pending: normalizedList(obj.pending, CONTEXT_STATE_LIMITS.pending),
        rawRefs: normalizedList(obj.rawRefs, CONTEXT_STATE_LIMITS.rawRefs),
    };
    const currentPage = normalizeCurrentPage(obj.currentPage);
    if (currentPage)
        state.currentPage = currentPage;
    return state;
}
/**
 * Merge two ContextState snapshots. Two distinct policies coexist here:
 *
 *   Accumulative fields (goals, constraints, facts, decisions, visitedUrls,
 *   completedActions, errors, rawRefs) — UNION with previous. These are
 *   "things we have learned / done so far", so the safety-net behaviour of
 *   keeping previous items even when the LLM forgot to repeat them is the
 *   one we want.
 *
 *   Mutable workflow fields (pending) — REPLACE with next. The summarizer
 *   prompt asks the LLM for the COMPLETE updated state; a unioning merge
 *   re-introduces items that the LLM legitimately removed (e.g. "log in"
 *   after the agent has actually logged in). That keeps stale TODOs alive
 *   in the system prompt across compactions and misleads later turns.
 *
 *   currentPage — field-level merge: each field (title/url/note) is taken
 *   from next if present, else from previous. See mergeCurrentPage.
 */
export function mergeContextState(previous, next) {
    return {
        goals: mergeLists(previous.goals, next.goals, CONTEXT_STATE_LIMITS.goals),
        constraints: mergeLists(previous.constraints, next.constraints, CONTEXT_STATE_LIMITS.constraints),
        facts: mergeLists(previous.facts, next.facts, CONTEXT_STATE_LIMITS.facts),
        decisions: mergeLists(previous.decisions, next.decisions, CONTEXT_STATE_LIMITS.decisions),
        visitedUrls: mergeLists(previous.visitedUrls, next.visitedUrls, CONTEXT_STATE_LIMITS.visitedUrls),
        completedActions: mergeLists(previous.completedActions, next.completedActions, CONTEXT_STATE_LIMITS.completedActions),
        errors: mergeLists(previous.errors, next.errors, CONTEXT_STATE_LIMITS.errors),
        // pending is workflow state — replace (still apply limits + uniqueness).
        pending: mergeLists([], next.pending, CONTEXT_STATE_LIMITS.pending),
        currentPage: mergeCurrentPage(previous.currentPage, next.currentPage),
        rawRefs: mergeLists(previous.rawRefs, next.rawRefs, CONTEXT_STATE_LIMITS.rawRefs),
    };
}
export function isContextStateEmpty(state) {
    return (state.goals.length === 0 &&
        state.constraints.length === 0 &&
        state.facts.length === 0 &&
        state.decisions.length === 0 &&
        state.visitedUrls.length === 0 &&
        state.completedActions.length === 0 &&
        state.errors.length === 0 &&
        state.pending.length === 0 &&
        state.rawRefs.length === 0 &&
        !state.currentPage);
}
export function formatContextState(state) {
    const limited = limitContextState(state);
    if (isContextStateEmpty(limited))
        return "";
    return JSON.stringify(limited, null, 2);
}
export function parseContextStateFromText(text) {
    const trimmed = text.trim();
    const candidates = [
        trimmed,
        trimmed.match(/```json\s*([\s\S]*?)```/i)?.[1],
        trimmed.match(/\{[\s\S]*\}/)?.[0],
    ].filter((v) => !!v);
    for (const candidate of candidates) {
        try {
            return normalizeContextState(JSON.parse(candidate));
        }
        catch {
            // Try next extraction shape.
        }
    }
    return null;
}
export class ContextTranscript {
    seq = 0;
    filePath;
    refs = new WeakMap();
    constructor(workspace, scope) {
        try {
            const dir = path.join(workspace, "context");
            fs.mkdirSync(dir, { recursive: true });
            const ts = new Date().toISOString().replace(/[:.]/g, "-");
            const safeScope = scope.replace(/[^a-zA-Z0-9_-]/g, "_");
            const nonce = crypto.randomBytes(3).toString("hex");
            this.filePath = path.join(dir, `${ts}_${nonce}_${safeScope}.jsonl`);
        }
        catch {
            this.filePath = undefined;
        }
    }
    append(message, meta) {
        const existing = this.refs.get(message);
        if (existing)
            return existing;
        const id = `ctx_${(++this.seq).toString().padStart(5, "0")}`;
        this.refs.set(message, id);
        const contentText = typeof message.content === "string"
            ? message.content
            : JSON.stringify(message.content);
        const record = {
            id,
            timestamp: new Date().toISOString(),
            role: message.role,
            ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
            ...(message.toolCalls ? { toolCalls: message.toolCalls } : {}),
            ...(meta ? { meta } : {}),
            content: message.content,
            contentHash: hashText(contentText),
            contentChars: contentText.length,
        };
        if (this.filePath) {
            try {
                fs.appendFileSync(this.filePath, JSON.stringify(record) + "\n", "utf-8");
            }
            catch {
                // Context should never fail a user task because transcript persistence failed.
            }
        }
        return id;
    }
    getRef(message) {
        return this.refs.get(message);
    }
    getFilePath() {
        return this.filePath;
    }
}
function contentToText(content) {
    if (typeof content === "string")
        return content;
    return content.map((b) => (b.type === "image" ? "[image]" : b.text || "")).join("");
}
function formatToolCallArgs(args, maxChars) {
    const json = JSON.stringify(args);
    return json.length > maxChars
        ? safeSliceHead(json, maxChars) + `...[${json.length - maxChars} chars omitted]`
        : json;
}
function formatPreview(text, maxChars) {
    if (text.length <= maxChars)
        return text;
    const headBudget = Math.floor(maxChars * 0.65);
    const tailBudget = maxChars - headBudget;
    const head = safeSliceHead(text, headBudget);
    const tail = safeSliceTail(text, tailBudget);
    const omitted = text.length - head.length - tail.length;
    return `${head}\n...[${omitted} chars omitted]...\n${tail}`;
}
function formatSnapshotForState(ref, content) {
    const title = content.match(/^Page: (.+)$/m)?.[1] ?? "unknown";
    const url = content.match(/^URL: (.+)$/m)?.[1] ?? "unknown";
    return [
        `[${ref} tool] browser snapshot preview; page="${title}" url="${url}" chars=${content.length}`,
        formatPreview(content, CONTEXT_BUDGETS.summaryInput.snapshotPreviewChars),
    ].join("\n");
}
function formatMessagesForState(messages, transcript) {
    const snapshotToolCallIds = new Set();
    for (const msg of messages) {
        if (msg.role === "assistant" && msg.toolCalls) {
            for (const tc of msg.toolCalls) {
                if (tc.name === "browser" && tc.arguments.action === "snapshot") {
                    snapshotToolCallIds.add(tc.id);
                }
            }
        }
    }
    return messages
        .map((msg, idx) => {
        const ref = transcript?.getRef(msg) ?? `message_${idx + 1}`;
        const content = contentToText(msg.content);
        if (msg.role === "assistant" && msg.toolCalls?.length) {
            const calls = msg.toolCalls
                .map((tc) => {
                const action = tc.arguments.action || tc.name;
                return `${tc.name}(${String(action)}) ${formatToolCallArgs(tc.arguments, CONTEXT_BUDGETS.summaryInput.toolCallArgsPreviewChars)}`;
            })
                .join("\n");
            return `[${ref} assistant]\n${content
                ? safeSliceHead(content, CONTEXT_BUDGETS.summaryInput.assistantContentPreviewChars) + "\n"
                : ""}${calls}`;
        }
        if (msg.role === "tool") {
            if (msg.toolCallId && snapshotToolCallIds.has(msg.toolCallId)) {
                return formatSnapshotForState(ref, content);
            }
            const preview = content.length > CONTEXT_BUDGETS.summaryInput.toolResultPreviewChars
                ? formatPreview(content, CONTEXT_BUDGETS.summaryInput.toolResultPreviewChars)
                : content;
            return `[${ref} tool]\n${preview}`;
        }
        const preview = content.length > CONTEXT_BUDGETS.summaryInput.messagePreviewChars
            ? formatPreview(content, CONTEXT_BUDGETS.summaryInput.messagePreviewChars)
            : content;
        return `[${ref} ${msg.role}]\n${preview}`;
    })
        .join("\n\n");
}
async function summarizeMessagesToState(llmClient, messages, previousState, transcript, sessionId) {
    const previous = formatContextState(previousState) || "{}";
    const session = formatMessagesForState(messages, transcript);
    const prompt = `Update the browser automation context state from the session fragment.

Return ONLY valid JSON with this base shape:
{
  "goals": [],
  "constraints": [],
  "facts": [],
  "decisions": [],
  "visitedUrls": [],
  "completedActions": [],
  "errors": [],
  "pending": [],
  "rawRefs": []
}

When page information is known, also include:
{ "currentPage": { "title": "...", "url": "...", "note": "..." } }

Rules:
- Return the COMPLETE updated state, not a prose summary.
- Preserve user goals, hard constraints, collected identifiers, decisions, errors, current progress, and what remains.
- Keep entries concise and factual.
- Preserve previous currentPage unless the fragment clearly shows a newer page state.
- Omit currentPage when no title, URL, or note is known. Never return empty strings just to match the template.
- Add rawRefs for any important fact using message ids shown in the fragment, such as ctx_00001 or message_1. Do not use ephemeral browser element refs as rawRefs.
- Do not invent facts.

Previous state:
${previous}

Session fragment:
${session}`;
    try {
        const response = await llmClient.chat([{ role: "user", content: prompt }], undefined, "You maintain compact machine-readable context state. Output only valid JSON.", sessionId ? { sessionId } : undefined);
        const parsed = parseContextStateFromText(response.content);
        return parsed ? mergeContextState(previousState, parsed) : null;
    }
    catch {
        return null;
    }
}
export async function compactMessagesWithState(opts) {
    const totalChars = estimateMessagesChars(opts.messages);
    if (totalChars <= opts.charBudget) {
        return {
            messages: opts.messages,
            state: opts.state,
            changed: false,
            summarizedCount: 0,
            retainedCount: opts.messages.length,
            reason: "within_budget",
        };
    }
    const keepRatio = opts.keepRatio ?? 0.6;
    const firstContentIndex = opts.preserveFirst ? 1 : 0;
    let keptChars = 0;
    let cutIndex = opts.messages.length;
    for (let i = opts.messages.length - 1; i >= firstContentIndex; i--) {
        const msg = opts.messages[i];
        keptChars += estimateMessagesChars([msg]);
        if (keptChars >= opts.charBudget * keepRatio) {
            cutIndex = i;
            while (cutIndex > firstContentIndex) {
                const role = opts.messages[cutIndex].role;
                const safeRole = opts.preserveFirst
                    ? role === "user" || role === "assistant"
                    : role === "user";
                if (safeRole)
                    break;
                cutIndex--;
            }
            break;
        }
    }
    if (cutIndex <= firstContentIndex) {
        return {
            messages: opts.messages,
            state: opts.state,
            changed: false,
            summarizedCount: 0,
            retainedCount: opts.messages.length,
            reason: "no_safe_cut",
        };
    }
    const toSummarize = opts.messages.slice(firstContentIndex, cutIndex);
    const nextState = await summarizeMessagesToState(opts.llmClient, toSummarize, opts.state, opts.transcript, opts.sessionId);
    if (!nextState) {
        return {
            messages: opts.messages,
            state: opts.state,
            changed: false,
            summarizedCount: 0,
            retainedCount: opts.messages.length,
            reason: "summarization_failed",
        };
    }
    const kept = opts.preserveFirst
        ? [opts.messages[0], ...opts.messages.slice(cutIndex)]
        : opts.messages.slice(cutIndex);
    return {
        messages: kept,
        state: nextState,
        changed: true,
        summarizedCount: toSummarize.length,
        retainedCount: kept.length,
    };
}
export function buildLlmContext(opts) {
    const limitedState = limitContextState(opts.state);
    const stateText = formatContextState(limitedState);
    // Static = cacheable; only the base prompt the caller assembled once per
    // user turn. Dynamic = the per-turn ContextState section, appended for
    // continuity but explicitly kept out of the cacheable prefix so that
    // compaction-driven state changes don't bust the cache.
    const systemPromptStatic = opts.baseSystemPrompt;
    const systemPromptDynamic = stateText
        ? `\n\n## ${opts.contextTitle}\n${stateText}`
        : "";
    const systemPrompt = systemPromptStatic + systemPromptDynamic;
    const optimizedResult = optimizeMessagesWithStats(opts.messages, opts.optimizeOptions);
    const optimized = optimizedResult.messages;
    const inputChars = estimateMessagesChars(opts.messages);
    const outputChars = estimateMessagesChars(optimized);
    return {
        messages: optimized,
        systemPrompt,
        systemPromptStatic,
        systemPromptDynamic,
        manifest: {
            cacheablePrefixHash: hashText(systemPromptStatic),
            stateHash: hashText(stateText),
            transcriptFile: opts.transcript?.getFilePath(),
            messageCount: optimized.length,
            charCount: outputChars,
            systemPromptChars: systemPrompt.length,
            systemPromptStaticChars: systemPromptStatic.length,
            systemPromptDynamicChars: systemPromptDynamic.length,
            stateChars: stateText.length,
            totalChars: outputChars + systemPrompt.length,
            rawRefCount: limitedState.rawRefs.length,
            optimized: {
                inputMessageCount: opts.messages.length,
                outputMessageCount: optimized.length,
                inputChars,
                outputChars,
                liveToolResultGuard: optimizedResult.liveToolResultGuard,
            },
        },
    };
}
//# sourceMappingURL=context.js.map