/**
 * Tool-context condensation: collapse stale tool payloads out of the history
 * sent to the model, while leaving stored history untouched.
 *
 * The driver owns the registry (tool handlers populate it via `condenseWhen`)
 * and the cadence (this runs before every provider call). This module owns the
 * pure application: given the registry + the current loop iteration, produce a
 * rewritten copy of the history with stale args/results replaced by stubs. Kept
 * pure (no driver/session state, no logging) so it unit-tests in isolation —
 * the one-time `context.condensed` signal is delivered via an `onCondensed`
 * callback rather than reaching into the debug-event log directly.
 *
 * @packageDocumentation
 */
/**
 * Below this serialized size a tool payload isn't worth condensing — the stub
 * would save little and cost a breadcrumb. A driver-level floor so the public
 * `condenseWhen` API needs no per-policy threshold field.
 */
export const CONDENSE_MIN_CHARS = 1000;
/** Key the collapsed args are stored under — a tool-call's args must stay a Record. */
export const CONDENSED_ARGS_KEY = 'condensed';
/**
 * Rough chars-per-token divisor for the `tokensSaved` estimate. No tokenizer is
 * available in this stack; ~4 chars/token is the usual English approximation.
 */
const APPROX_CHARS_PER_TOKEN = 4;
/** Short human-readable reason for the stub text, per fired trigger. */
function triggerReason(trigger) {
    switch (trigger.kind) {
        case 'age':
            return 'aged out';
        case 'turnEnd':
            return 'turn ended';
        case 'agentEnd':
            return 'agent finished';
        case 'phaseEnd':
            return 'phase ended';
        default:
            return 'superseded';
    }
}
/** Framework-generated `pointer` stub — advertises that re-calling restores the content. */
function condenseStub(target, tool, trigger, origLen) {
    const what = target === 'args' ? 'args' : 'result';
    const key = trigger.kind === 'superseded' ? ` ${trigger.by}` : '';
    return `[${tool}${key} — ${what} elided, ~${origLen} chars (${triggerReason(trigger)}); re-call to restore]`;
}
/** Stable label for the `context.condensed` meta-event's `trigger` field. */
function triggerLabel(trigger) {
    switch (trigger.kind) {
        case 'age':
            return `age:${trigger.turns}`;
        case 'superseded':
            return `superseded:${trigger.by}`;
        default:
            return trigger.kind; // 'turnEnd' | 'agentEnd' | 'phaseEnd'
    }
}
function estimateTokensSaved(origLen, stubLen) {
    return Math.max(0, Math.ceil((origLen - stubLen) / APPROX_CHARS_PER_TOKEN));
}
/**
 * Collapse stale tool payloads (declared via `condenseWhen`) out of the
 * model-bound history. Pure over the `history` array — it emits new message
 * objects and never mutates the input messages (mirroring the agent-masking
 * transform). It DOES, however, flip the `reported*` flag on the matching
 * `policies` entry the first time a payload collapses — the report-once gate that
 * keeps `onCondensed` firing once per (tool call, payload) even though the
 * collapse itself re-runs before every provider call.
 *
 * Triggers (a policy may list several via `on: Trigger[]` — the FIRST to fire
 * collapses the payload; all are monotonic, so the collapse never reverts):
 * - `superseded` — among all registered calls sharing a `by` key, the LAST in
 *   history order survives; every earlier one's targeted payload collapses. A
 *   re-call with the same key becomes the new survivor and re-arms the rest.
 * - `age` — the result is first visible one model-call after the call, so `turns`
 *   is how many model-calls may see the full result before it collapses
 *   (fires when `modelCall − callModelCall > turns`): `turns: 1` is seen once,
 *   then collapses. The clock is monotonic across turns.
 * - `turnEnd` — collapses once the turn (`sendMessage`) that made the call has
 *   ended (fires when `turn > callTurn`): full for the rest of that request,
 *   gone on every later one.
 * - `agentEnd` — collapses once the agent activation that made the call has ended
 *   (a swap to another agent, or `releaseAgent`/`completeSubAgent`): kept across
 *   all of the agent's turns, dropped when its flow finishes.
 * - `phaseEnd` — collapses once the app advances its phase epoch via `endPhase()`
 *   after the call (a sub-`agentEnd` boundary the author declares), OR at
 *   `agentEnd` as a backstop if no boundary is ever declared.
 *
 * The tool-call/result envelope is never removed (providers reject orphaned
 * calls/results) — only the args object or the result content is replaced.
 *
 * @param history - The to-model history copy to rewrite (not mutated).
 * @param policies - The driver's registry, keyed by tool-call id. Its entries'
 *   `reported*` flags are flipped as payloads first collapse.
 * @param ctx - The driver's live clocks (model-call / turn / agent-activation).
 * @param onCondensed - Invoked once per payload at its full→stub transition.
 */
export function applyCondensation(history, policies, ctx, onCondensed) {
    var _a;
    if (policies.size === 0)
        return history;
    const triggersOf = (entry) => Array.isArray(entry.policy.on) ? entry.policy.on : [entry.policy.on];
    // One pass: resolve supersession survivors and a toolCallId → tool-name
    // lookup. For each `superseded` key the LAST call in history order survives.
    // The name lookup lets a tool message (which carries only an id) name its tool
    // in stubs and events.
    const latestByKey = new Map();
    const nameById = new Map();
    for (const msg of history) {
        if (!((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length))
            continue;
        for (const tc of msg.toolCalls) {
            nameById.set(tc.id, tc.name);
            const entry = policies.get(tc.id);
            if (!entry)
                continue;
            for (const t of triggersOf(entry)) {
                if (t.kind === 'superseded')
                    latestByKey.set(t.by, tc.id);
            }
        }
    }
    const triggerFires = (toolCallId, entry, trig) => {
        switch (trig.kind) {
            // Result first visible one model-call after the call, so collapse one call
            // past the last allowed view: turns:1 → seen once, then gone. Monotonic
            // clock, so this holds across turn boundaries too.
            case 'age':
                return ctx.modelCall - entry.iteration > trig.turns;
            case 'turnEnd':
                return ctx.turn > entry.turn;
            case 'agentEnd':
                return ctx.activationEnded(entry.activation);
            // Fires when the app advanced the phase epoch since the call — OR, as a
            // backstop, when the whole activation has ended, so a phase boundary that
            // is never declared still collapses at `agentEnd` rather than never.
            case 'phaseEnd':
                return ctx.phaseEnded(entry.phaseEpoch) || ctx.activationEnded(entry.activation);
            default:
                return latestByKey.get(trig.by) !== toolCallId; // superseded by a newer call
        }
    };
    // The first listed trigger that fires — drives the collapse, the stub reason,
    // and the event label. `undefined` means the payload stays full.
    const firstFired = (toolCallId, entry) => triggersOf(entry).find((t) => triggerFires(toolCallId, entry, t));
    return history.map((msg) => {
        var _a, _b, _c, _d;
        // Tool-call ARGS live on the assistant message.
        if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
            let changed = false;
            const toolCalls = msg.toolCalls.map((tc) => {
                var _a;
                const entry = policies.get(tc.id);
                if (!(entry === null || entry === void 0 ? void 0 : entry.policy.args))
                    return tc;
                const fired = firstFired(tc.id, entry);
                if (!fired)
                    return tc;
                const origLen = JSON.stringify((_a = tc.args) !== null && _a !== void 0 ? _a : {}).length;
                if (origLen < CONDENSE_MIN_CHARS)
                    return tc;
                changed = true;
                const result = entry.policy.args;
                // Args must stay a Record; `drop` is an empty object, otherwise stash the
                // stub under a single key. Spread `tc` so providerMetadata (e.g. the
                // Gemini reasoning signature that must round-trip) and UI fields survive.
                const args = result === 'drop'
                    ? {}
                    : {
                        [CONDENSED_ARGS_KEY]: result === 'pointer'
                            ? condenseStub('args', tc.name, fired, origLen)
                            : result.replaceWith,
                    };
                if (!entry.reportedArgs) {
                    entry.reportedArgs = true;
                    const stubLen = JSON.stringify(args).length;
                    onCondensed({
                        turn: entry.iteration,
                        toolCallId: tc.id,
                        tool: tc.name,
                        target: 'args',
                        trigger: triggerLabel(fired),
                        stubLen,
                        tokensSaved: estimateTokensSaved(origLen, stubLen),
                    });
                }
                return Object.assign(Object.assign({}, tc), { args });
            });
            return changed ? Object.assign(Object.assign({}, msg), { toolCalls }) : msg;
        }
        // The tool RESULT lives on the tool message.
        if (msg.toolResult) {
            const entry = policies.get(msg.toolResult.toolCallId);
            // Defensive `?? 0` for restored/malformed history — `content` is typed
            // `string`, but a 0-length payload is simply below the floor and skipped.
            const origLen = (_c = (_b = msg.toolResult.content) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
            const fired = (entry === null || entry === void 0 ? void 0 : entry.policy.response)
                ? firstFired(msg.toolResult.toolCallId, entry)
                : undefined;
            if (entry && fired && origLen >= CONDENSE_MIN_CHARS) {
                const tool = (_d = nameById.get(msg.toolResult.toolCallId)) !== null && _d !== void 0 ? _d : msg.toolResult.toolCallId;
                const result = entry.policy.response; // `fired` is only set when response is declared
                // Kept non-empty — Anthropic rejects empty tool_result content.
                const content = result === 'drop'
                    ? '[elided]'
                    : result === 'pointer'
                        ? condenseStub('response', tool, fired, origLen)
                        : result.replaceWith;
                if (!entry.reportedResponse) {
                    entry.reportedResponse = true;
                    onCondensed({
                        turn: entry.iteration,
                        toolCallId: msg.toolResult.toolCallId,
                        tool,
                        target: 'response',
                        trigger: triggerLabel(fired),
                        stubLen: content.length,
                        tokensSaved: estimateTokensSaved(origLen, content.length),
                    });
                }
                return Object.assign(Object.assign({}, msg), { toolResult: Object.assign(Object.assign({}, msg.toolResult), { content }) });
            }
        }
        return msg;
    });
}
