/**
 * How one agent log accounts for the work it consumed.
 *
 * The turn and step vocabulary alone cannot answer this. A turn that stops
 * before its first step leaves a `turn/end` shaped exactly like the balanced
 * no-op turns a rejection or an empty claim produces, so reading turns in
 * isolation either credits cut-short work as finished or convicts every no-op.
 * The missing fact is the inbox's own record: {@link Inbox} logs each mutation
 * with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
 * separates a turn claiming its input from work being dropped unrun.
 *
 * @module @deepseek-ai/dsh-agent/consumed-work
 */
/**
 * Whether a turn that consumed input but never reached a step ends in a way
 * that accounts for that input. Only a `completed` end does not: it had
 * nothing left to run once its claim was rewritten away. A `blocked` end is
 * that input's ending too — the pre-step rejection that produced it discarded
 * the claimed messages, so the work it took will never run.
 * @param reason - the turn's recorded ending.
 * @returns whether the ending accounts for the input the turn took.
 */
function accountsForClaim(reason) {
    switch (reason.kind) {
        case 'completed':
            return false;
        case 'blocked':
        case 'aborted':
        case 'interrupted':
        case 'error':
            return true;
        /* v8 ignore next 4 -- unreachable: the one unnamed built-in, `max-tokens`, requires a step,
         * so its turn short-circuits as stepped before this call, and `TurnEndReasonMap` is
         * merge-extensible, so a backend-added variant cannot be listed; an unnameable ending over
         * consumed input must not read as success. */
        default:
            return true;
    }
}
/**
 * Fold one agent log, or an owned suffix of one, into its account of consumed
 * work. Single pass, and every input is the log itself: no caller has to sample
 * live state before cancelling, so a cancellation issued by anyone — the owner's
 * teardown, an ancestor's interrupt, an unloading plugin — reads the same.
 * @param events - the log, or an owned suffix, to fold.
 * @returns the accounting turn when one closed, and whether work was dropped unrun after it.
 */
export function foldConsumedWork(events) {
    const stepped = new Set();
    const claimed = new Set();
    let open;
    let end;
    let droppedUnrun = false;
    for (const event of events) {
        switch (event.type) {
            case 'turn/start':
                open = event.data.turn;
                break;
            case 'step/start':
                stepped.add(event.data.turn);
                break;
            case 'agent/inbox/spliced': {
                const { removedCount, outcome, inserted } = event.data;
                if (removedCount === undefined)
                    break;
                // A replacement keeps the work pending under a new identity, so only a
                // cancellation that leaves nothing behind drops it.
                if (outcome === 'canceled')
                    droppedUnrun ||= inserted.length === 0;
                // Claims are the loop's own step-boundary reads, always inside a turn.
                else if (open !== undefined)
                    claimed.add(open);
                break;
            }
            case 'turn/end': {
                const { turn, reason } = event.data;
                open = undefined;
                if (stepped.delete(turn) || (claimed.delete(turn) && accountsForClaim(reason))) {
                    end = event;
                    // Anything dropped before this turn closed is what its own ending
                    // reports; only a later drop is still unaccounted for.
                    droppedUnrun = false;
                }
                break;
            }
            default:
                break;
        }
    }
    return { ...end === undefined ? {} : { end }, droppedUnrun };
}
//# sourceMappingURL=consumed-work.js.map