import { setTimeout as sleep } from 'node:timers/promises';
import { HeddleEventType } from '../../../core/event-types.js';
import { isAbortError } from '../../../core/agent/utils/index.js';
import { STREAM_UPDATE_INTERVAL_MS } from '../constants.js';
import { AgentRunFinisher } from '../finish/index.js';
import { AgentModelTurnRetryService } from './model-turn-retry-service.js';
/**
 * Owns one LLM request/stream step inside an agent run.
 */
export class AgentModelTurnService {
    static async request(args) {
        const { context } = args;
        if (context.abortSignal?.aborted) {
            return AgentRunFinisher.finishInterrupted(context, 'Agent run interrupted before LLM call');
        }
        return AgentModelTurnService.requestWithRetries(args);
    }
    static async requestWithRetries(args) {
        const { context } = args;
        let attempt = 1;
        while (true) {
            try {
                const response = await AgentModelTurnService.requestOnce(args);
                context.state.usage = AgentModelTurnService.accumulateUsage({
                    current: context.state.usage,
                    next: response.usage,
                });
                const retry = AgentModelTurnRetryService.resolve({ kind: 'response', response });
                if (!retry.retryable || attempt >= retry.maxAttempts) {
                    return retry.retryable
                        ? AgentRunFinisher.finish(context, 'error', `${retry.message} after ${attempt} attempts`)
                        : response;
                }
                await AgentModelTurnService.recordRetryAndWait({ context, attempt, retry });
                attempt += 1;
            }
            catch (error) {
                if (isAbortError(error) || context.abortSignal?.aborted || context.shouldStop?.()) {
                    return AgentRunFinisher.finishInterrupted(context, 'Agent run interrupted during LLM call');
                }
                const retry = AgentModelTurnRetryService.resolve({ kind: 'error', error });
                if (!retry.retryable || attempt >= retry.maxAttempts) {
                    context.log.error({ step: context.state.step, error: retry.message, attempts: attempt }, 'LLM call failed');
                    return AgentRunFinisher.finish(context, 'error', attempt > 1 ? `LLM error after ${attempt} attempts: ${retry.message}` : `LLM error: ${retry.message}`);
                }
                await AgentModelTurnService.recordRetryAndWait({ context, attempt, retry });
                attempt += 1;
            }
        }
    }
    static async requestOnce(args) {
        const { context } = args;
        const streamState = {
            content: '',
            reasoningSummary: '',
            lastStreamEmitAt: 0,
        };
        return context.llm.chat(context.messages, context.registry.list(), context.abortSignal, (event) => AgentModelTurnService.handleStreamEvent({ context, event, streamState }));
    }
    static async recordRetryAndWait(args) {
        const { context, attempt, retry } = args;
        const retryAfterMs = AgentModelTurnRetryService.nextDelayMs(attempt);
        context.live.trace({
            type: HeddleEventType.modelRetry,
            reason: retry.reason ?? 'transport_error',
            attempt,
            maxAttempts: retry.maxAttempts,
            retryAfterMs,
            message: retry.message,
            step: context.state.step,
            timestamp: context.now(),
        });
        context.log.warn({ step: context.state.step, attempt, maxAttempts: retry.maxAttempts, retryAfterMs, reason: retry.reason, message: retry.message }, 'Retrying LLM call');
        try {
            await sleep(retryAfterMs, undefined, { signal: context.abortSignal });
        }
        catch (error) {
            if (isAbortError(error) || context.abortSignal?.aborted) {
                throw error;
            }
        }
    }
    static handleStreamEvent(args) {
        const { context, event, streamState } = args;
        if (event.type === 'content.delta') {
            const nowMs = Date.now();
            streamState.content += event.delta;
            if (!AgentModelTurnService.shouldEmitStreamUpdate(streamState, nowMs)) {
                return;
            }
            // Stream the accumulated assistant text through the live event path. This
            // is intentionally in-memory; durable session files are updated later by
            // turn persistence, not once per LLM delta.
            context.live.activity({ type: HeddleEventType.assistantStream, step: context.state.step, text: streamState.content, done: false });
            return;
        }
        if (event.type === 'content.done') {
            streamState.content = event.content;
            streamState.lastStreamEmitAt = Date.now();
            context.live.activity({ type: HeddleEventType.assistantStream, step: context.state.step, text: streamState.content, done: true });
            return;
        }
        if (event.type === 'reasoning_summary.delta') {
            streamState.reasoningSummary += event.delta;
            if (streamState.content || !AgentModelTurnService.shouldEmitStreamUpdate(streamState, Date.now())) {
                return;
            }
            context.live.activity({
                type: HeddleEventType.assistantStream,
                step: context.state.step,
                text: AgentModelTurnService.formatReasoningSummary(streamState.reasoningSummary),
                done: false,
            });
            return;
        }
        if (event.type === 'reasoning_summary.done') {
            streamState.reasoningSummary = event.text;
            if (!streamState.content) {
                streamState.lastStreamEmitAt = Date.now();
                context.live.activity({
                    type: HeddleEventType.assistantStream,
                    step: context.state.step,
                    text: AgentModelTurnService.formatReasoningSummary(streamState.reasoningSummary),
                    done: false,
                });
            }
        }
    }
    static shouldEmitStreamUpdate(streamState, nowMs) {
        if (streamState.lastStreamEmitAt && nowMs - streamState.lastStreamEmitAt < STREAM_UPDATE_INTERVAL_MS) {
            return false;
        }
        streamState.lastStreamEmitAt = nowMs;
        return true;
    }
    static accumulateUsage(args) {
        if (!args.next) {
            return args.current;
        }
        if (!args.current) {
            return { ...args.next };
        }
        const cachedInputTokens = (args.current.cachedInputTokens ?? 0) + (args.next.cachedInputTokens ?? 0);
        const reasoningTokens = (args.current.reasoningTokens ?? 0) + (args.next.reasoningTokens ?? 0);
        const requests = (args.current.requests ?? 0) + (args.next.requests ?? 0);
        return {
            inputTokens: args.current.inputTokens + args.next.inputTokens,
            outputTokens: args.current.outputTokens + args.next.outputTokens,
            totalTokens: args.current.totalTokens + args.next.totalTokens,
            cachedInputTokens: cachedInputTokens || undefined,
            reasoningTokens: reasoningTokens || undefined,
            requests: requests || undefined,
        };
    }
    static formatReasoningSummary(text) {
        const trimmed = AgentModelTurnService.stripReasoningSummaryMarkdown(text.trim());
        return trimmed ? `Thinking: ${trimmed}` : 'Thinking...';
    }
    static stripReasoningSummaryMarkdown(text) {
        return text
            .replace(/\*\*([^*]+)\*\*/g, '$1')
            .replace(/__([^_]+)__/g, '$1')
            .replace(/([.!?])([A-Z][A-Za-z ]{2,}:?)/g, '$1 $2');
    }
}
//# sourceMappingURL=model-turn-service.js.map