/** Turn driver: wraps `agentLoop` with per-session config and reduces stream events into AgentState. */
import { agentLoop, agentLoopContinue, streamAssistantResponse } from './agent-loop.js';
import { createTerminalErrorMessage, finalizePartialAssistantMessage, reduceAgentLoopEvent } from './agent-state-reducer.js';
import { AgentLoopEventType } from './core-types.js';
import { classifyAgentError } from './error-classification.js';
import { createHeartbeatEmitter } from './heartbeat.js';
function defaultConvertToLlm(messages) {
    return messages.filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'toolResult');
}
export class AgentEngine {
    _convertToLlm;
    _transformContext;
    _streamFn;
    _requestAssistantResponse;
    _sessionId;
    _getApiKey;
    _transport;
    _maxRetryDelayMs;
    _getToolContext;
    _heartbeatClock;
    _heartbeatIntervalMs;
    /** Active turn's heartbeat wrap; bridge / long-lived components read via getHeartbeatWrap(). */
    _activeWrap;
    /**
     * Per-call speed mode. Set by the embedded runtime's submit() from
     * `AgentSubmitOptions.speed` and cleared after the run. Forwarded
     * through `AgentLoopConfig.speed` into `streamFn` (see core-types.ts).
     */
    _speed;
    _streamObservabilityContext;
    constructor(options = {}) {
        this._convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
        this._transformContext = options.transformContext;
        this._streamFn = options.streamFn;
        this._requestAssistantResponse = options.requestAssistantResponse ?? streamAssistantResponse;
        this._sessionId = options.sessionId;
        this._getApiKey = options.getApiKey;
        this._transport = options.transport ?? 'sse';
        this._maxRetryDelayMs = options.maxRetryDelayMs;
        this._getToolContext = options.getToolContext;
        this._heartbeatClock = options.heartbeatClock;
        this._heartbeatIntervalMs = options.heartbeatIntervalMs;
    }
    /** Returns the current turn's wrap, or undefined when idle. */
    getHeartbeatWrap() {
        return this._activeWrap;
    }
    get streamFn() {
        return this._streamFn;
    }
    set streamFn(value) {
        this._streamFn = value;
    }
    get sessionId() {
        return this._sessionId;
    }
    set sessionId(value) {
        this._sessionId = value;
    }
    get transport() {
        return this._transport;
    }
    set transport(value) {
        this._transport = value;
    }
    get maxRetryDelayMs() {
        return this._maxRetryDelayMs;
    }
    set maxRetryDelayMs(value) {
        this._maxRetryDelayMs = value;
    }
    get speed() {
        return this._speed;
    }
    set speed(value) {
        this._speed = value;
    }
    get streamObservabilityContext() {
        return this._streamObservabilityContext;
    }
    set streamObservabilityContext(value) {
        this._streamObservabilityContext = value;
    }
    async runTurn(options) {
        const { state, messageQueue, signal, messages, onEvent } = options;
        const model = state.model;
        const reasoning = state.thinkingLevel === 'off' ? undefined : state.thinkingLevel;
        const context = {
            systemPrompt: state.systemPrompt,
            messages: state.messages.slice(),
            tools: state.tools
        };
        let skipInitialSteeringPoll = options.skipInitialSteeringPoll === true;
        /** Heartbeat events (op_start/heartbeat_tick/op_end) bypass the
         *  agentLoop stream's reducer pass below. Apply the reducer inline so
         *  `state.inFlight` reflects every wrap, not just stream events.
         *
         * The loop producer can reach the first heartbeat wrap before the async
         * iterator drains the already-enqueued initial run envelope. Buffer those
         * early heartbeat events so subscribers keep the public event invariant:
         * `agent_start` opens the run, then the initial `turn_start` and prompt
         * messages appear before LLM in-flight events.
         */
        let partial = null;
        let emittedAgentStart = false;
        let emittedInitialTurnStart = false;
        let emittedInitialPromptMessageEnds = 0;
        let initialEnvelopeDrained = false;
        const initialPromptMessagesToDrain = messages?.length ?? 0;
        const pendingHeartbeatEvents = [];
        const emitReduced = (event) => {
            partial = reduceAgentLoopEvent(state, event, partial);
            onEvent(event);
        };
        const canFlushHeartbeatEvents = () => emittedAgentStart &&
            emittedInitialTurnStart &&
            emittedInitialPromptMessageEnds >= initialPromptMessagesToDrain;
        const flushPendingHeartbeatEvents = () => {
            for (const event of pendingHeartbeatEvents.splice(0)) {
                emitReduced(event);
            }
        };
        const onLoopEventReduced = (event) => {
            emitReduced(event);
            if (event.type === AgentLoopEventType.AgentStart) {
                emittedAgentStart = true;
            }
            else if (event.type === AgentLoopEventType.TurnStart && !emittedInitialTurnStart) {
                emittedInitialTurnStart = true;
            }
            else if (event.type === AgentLoopEventType.MessageEnd &&
                !initialEnvelopeDrained &&
                emittedInitialPromptMessageEnds < initialPromptMessagesToDrain) {
                emittedInitialPromptMessageEnds++;
            }
            if (!initialEnvelopeDrained && canFlushHeartbeatEvents()) {
                initialEnvelopeDrained = true;
                flushPendingHeartbeatEvents();
            }
        };
        const onHeartbeatEvent = (event) => {
            if (!initialEnvelopeDrained) {
                pendingHeartbeatEvents.push(event);
                return;
            }
            emitReduced(event);
        };
        const heartbeat = createHeartbeatEmitter(onHeartbeatEvent, {
            clock: this._heartbeatClock,
            intervalMs: this._heartbeatIntervalMs
        });
        const wrap = heartbeat.wrap.bind(heartbeat);
        this._activeWrap = wrap;
        const baseGetToolContext = this._getToolContext;
        const getToolContextWithHeartbeat = () => {
            const base = baseGetToolContext?.();
            return base ? { ...base, heartbeatWrap: wrap } : { heartbeatWrap: wrap };
        };
        const config = {
            model,
            maxTokens: model.maxTokens,
            reasoning,
            sessionId: this._sessionId,
            transport: this._transport,
            maxRetryDelayMs: this._maxRetryDelayMs,
            convertToLlm: this._convertToLlm,
            transformContext: this._transformContext,
            getApiKey: this._getApiKey,
            getToolContext: getToolContextWithHeartbeat,
            requestAssistantResponse: this._requestAssistantResponse,
            heartbeatWrap: wrap,
            getActiveConfiguration: () => ({
                agentMode: this._streamObservabilityContext?.agentMode,
                systemPrompt: state.systemPrompt,
                tools: state.tools
            }),
            ...(this._streamObservabilityContext?.agentMode
                ? { agentMode: this._streamObservabilityContext.agentMode }
                : {}),
            ...(this._speed !== undefined ? { speed: this._speed } : {}),
            ...(this._streamObservabilityContext?.initialFileId
                ? { initialFileId: this._streamObservabilityContext.initialFileId }
                : {}),
            ...(this._streamObservabilityContext?.fileAttachmentIds?.length
                ? { fileAttachmentIds: this._streamObservabilityContext.fileAttachmentIds }
                : {}),
            ...(this._streamObservabilityContext?.interactionMode
                ? { interactionMode: this._streamObservabilityContext.interactionMode }
                : {}),
            ...(this._streamObservabilityContext?.llmOperation
                ? { llmOperation: this._streamObservabilityContext.llmOperation }
                : {}),
            getSteeringMessages: async () => {
                if (skipInitialSteeringPoll) {
                    skipInitialSteeringPoll = false;
                    return [];
                }
                return messageQueue.dequeueSteeringMessages();
            },
            getFollowUpMessages: async () => messageQueue.dequeueFollowUpMessages()
        };
        try {
            const stream = messages
                ? agentLoop(messages, context, config, signal, this._streamFn)
                : agentLoopContinue(context, config, signal, this._streamFn);
            for await (const event of stream)
                onLoopEventReduced(event);
            const finalPartial = finalizePartialAssistantMessage(partial, signal);
            if (finalPartial) {
                state.messages = [...state.messages, finalPartial];
            }
        }
        catch (error) {
            const errorMessage = createTerminalErrorMessage(model, error, signal);
            state.messages = [...state.messages, errorMessage];
            state.error = errorMessage.errorMessage;
            if (errorMessage.stopReason !== 'aborted') {
                state.errorKind = classifyAgentError(error);
            }
            onEvent({ type: AgentLoopEventType.AgentEnd, messages: [errorMessage] });
        }
        finally {
            flushPendingHeartbeatEvents();
            heartbeat.dispose();
            this._activeWrap = undefined;
        }
    }
}
//# sourceMappingURL=agent-engine.js.map