/**
 * Conversation call configuration and freeze utilities. Provider routing,
 * model, reasoning effort, and sampling values are request-header state that
 * can affect cache reuse; request waterfalls replace them and the loop logs
 * changed snapshots instead of allowing silent per-call drift.
 * @module dsh-llm/call-config
 */
/** Process-local identities of request objects assembled by dsh-agent-loop. */
const AGENT_LOOP_REQUESTS = new WeakSet();
/**
 * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
 * runs to decide whether a proposed configuration is a real change (worth a
 * logged header snapshot) or the held one restated.
 * @param a - one configuration.
 * @param b - the other.
 * @returns whether every field (including the `stop` list, element-wise) matches.
 */
export function callConfigEquals(a, b) {
    if (a.provider !== b.provider
        || a.model !== b.model
        || a.reasoningEffort !== b.reasoningEffort
        || a.temperature !== b.temperature
        || a.maxTokens !== b.maxTokens)
        return false;
    if (a.stop === undefined || b.stop === undefined)
        return a.stop === b.stop;
    return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
}
/**
 * Mark one exact request object as assembled by dsh-agent-loop.
 * @param request - loop-owned request envelope before LLM dispatch.
 * @returns the same request object marked as created by the process-local agent loop.
 */
export function markAgentLoopRequest(request) {
    AGENT_LOOP_REQUESTS.add(request);
    return request;
}
/**
 * Test whether the exact request object was assembled by dsh-agent-loop.
 * @param request - request envelope observed at the LLM waterfall.
 * @returns whether {@link markAgentLoopRequest} recorded this object.
 */
export function isAgentLoopRequest(request) {
    return AGENT_LOOP_REQUESTS.has(request);
}
/**
 * Deep-freeze a value in place with an iterative traversal, guarding cycles,
 * so later mutation throws without imposing a JavaScript call-stack depth cap.
 * {@link AbortSignal} objects are deliberately skipped because they are the
 * request's live cancellation channel and freezing them breaks abort.
 * @param value - the value to freeze in place.
 * @returns the same value, frozen.
 */
export function deepFreeze(value) {
    const seen = new WeakSet();
    const pending = [{ kind: 'visit', node: value }];
    while (pending.length > 0) {
        const task = pending.pop();
        /* v8 ignore next -- the loop condition guarantees one pending task. */
        if (task === undefined)
            continue;
        if (task.kind === 'property') {
            pending.push({ kind: 'visit', node: task.source[task.key] });
            continue;
        }
        const node = task.node;
        if (node === null || typeof node !== 'object')
            continue;
        if (node instanceof AbortSignal)
            continue;
        if (seen.has(node))
            continue;
        seen.add(node);
        Object.freeze(node);
        const keys = Object.keys(node);
        for (let index = keys.length - 1; index >= 0; index--) {
            const key = keys[index];
            /* v8 ignore next -- the loop is bounded by the captured key count. */
            if (key === undefined)
                continue;
            pending.push({ kind: 'property', source: node, key });
        }
    }
    return value;
}
//# sourceMappingURL=call-config.js.map