/**
 * Worker-side composition root for the embedded runtime. It wires the agent
 * loop, session persistence, compaction, run stats, credit gate, suggestions,
 * and pending host-tool requests into the public runtime facade.
 */
import { AGENT_TOOL_IN_FLIGHT_DISPLAY_DELAY_MS, AgentEventKind, AgentMainMessageStreamEventType, AgentTelemetryEventType, AgentTelemetryTerminalPhase } from '../contracts/agent-api.js';
import { AgentStopReason } from '../contracts/agent-snapshot.js';
import { AgentController } from '../core/agent-controller.js';
import { AgentEngine } from '../core/agent-engine.js';
import { convertAgentMessagesToLlm, createAgentSnapshotFromAgentMessages, projectsAsUserRole } from '../core/agent-snapshot-builder.js';
import { AgentLoopEventType, AssistantMessageEventType, InFlightOpKind, ToolLifecycleSource } from '../core/core-types.js';
import { CANCELLATION_BOUNDARY_CUSTOM_TYPE, createCancellationBoundaryMessage } from '../core/custom-messages.js';
import { createEmbeddedAgentRuntime } from '../core/embedded-agent-facade.js';
import { createPendingHostToolRequests } from '../core/pending-host-tool-requests.js';
import { buildSessionContextMessageEntryIds } from '../core/session-schema.js';
import { resolveShortcutSpeedForContext } from '../fast-mode.js';
import { trimNonEmpty } from '../utils/string.js';
import { createAgentEventFunnel } from './agent-event-funnel.js';
import { createEmbeddedCompactionWiring } from './compaction-wiring.js';
import { getRuntimePendingHostToolTimeoutMs } from './host-tools/timeouts.js';
import { selectInitialRuntimeModel } from './model-selection.js';
import { OutOfCreditsError } from './out-of-credits-error.js';
import { buildUserMessage } from './prompt/build-user-message.js';
import { createEmbeddedRecoveryWiring } from './recovery-wiring.js';
import { createRunStatsAccumulator } from './run-stats.js';
import { ShortcutThinkingLevelToEffort } from './stream.js';
import { runSuggestionsViaBridge } from './suggestions.js';
import { EmbeddedSubmitOverlapPolicy, EmbeddedTurnLifecycle, getSubmitOverlapPolicy } from './turn-lifecycle.js';
export class EditTargetUnresolvedError extends Error {
    /** Build the error with the unresolved target entry id baked into the message. */
    constructor(targetEntryId) {
        super(`editFromMessageId='${targetEntryId}' did not resolve to a user-message entry.`);
        this.name = 'EditTargetUnresolvedError';
    }
}
/**
 * Thrown by `submit()` when the credit gate refuses the next turn (user is
 * out of credits). Defense-in-depth: the host's `checkCreditGate`
 * normally halts before reaching the runtime, so this fires only when the
 * host gate is bypassed or stale. Hosts should catch and surface an
 * out-of-credits UI (e.g. the existing team-out-of-credits modal).
 */
export { OutOfCreditsError };
const EmbeddedRecoveryEventType = {
    End: 'recovery_end',
    Fallback: 'recovery_fallback',
    Start: 'recovery_start'
};
/**
 * Treat anything the LLM projector will send as a user-role message as prior
 * user context. Sharing the predicate with the projector keeps the warning
 * eligibility aligned with the actual transcript shape.
 */
/** True if any message in the transcript projects to a user role for the LLM. */
const AUTONOMOUS_SKILL_MARKER = '[Skill:autonomous selected]';
const REFRESH_CONTEXT_CONTINUATION_INSTRUCTIONS = 'You are continuing an existing autonomous task in a fresh context. Read the progress markdown files in the progress folder first, especially plan/status markdown files, and treat them as the source of truth. Continue autonomously from there. Do not ask the user what to do next. Stop only if the task is complete, blocked, or a real approval boundary is hit.';
const REFRESH_CONTEXT_ORIGINAL_TASK_HEADER = 'Original user task:';
const REFRESH_CONTEXT_PROGRESS_FOLDER_HEADER = 'Progress folder:';
const REFRESH_CONTEXT_CONTINUATION_NOTE_HEADER = 'Continuation note:';
function extractAutonomousTask(prompt) {
    const idx = prompt.indexOf(AUTONOMOUS_SKILL_MARKER);
    if (idx < 0)
        return undefined;
    return trimNonEmpty(prompt.slice(idx + AUTONOMOUS_SKILL_MARKER.length));
}
function buildRefreshContextPrompt(refresh, originalAutonomousTask) {
    const parts = [];
    if (originalAutonomousTask) {
        parts.push(AUTONOMOUS_SKILL_MARKER, '', REFRESH_CONTEXT_ORIGINAL_TASK_HEADER, originalAutonomousTask, '');
    }
    if (refresh.progressFolder) {
        parts.push(REFRESH_CONTEXT_PROGRESS_FOLDER_HEADER, refresh.progressFolder, '');
    }
    parts.push(REFRESH_CONTEXT_CONTINUATION_INSTRUCTIONS, '', REFRESH_CONTEXT_CONTINUATION_NOTE_HEADER, refresh.query);
    return parts.join('\n');
}
function hasPriorUserEquivalent(messages) {
    return messages.some(projectsAsUserRole);
}
/** Apply model/thinking user-knob overrides for the duration of the active turn. */
function applyPerTurnModelAndThinkingOverrides(agent, modelRegistry, submitOptions) {
    if (submitOptions?.model) {
        const overrideModel = modelRegistry.find(agent.state.model.provider, submitOptions.model);
        if (overrideModel)
            agent.setModel(overrideModel);
        else {
            console.warn(`[embedded-runtime] Per-turn model override '${submitOptions.model}' did not resolve; falling back to baseline.`);
        }
    }
    if (submitOptions?.thinkingLevel !== undefined)
        agent.setThinkingLevel(submitOptions.thinkingLevel);
}
function resolvePerTurnSpeed(agent, userMessage, submitOptions) {
    const requestedSpeed = submitOptions?.speed;
    if (requestedSpeed === undefined)
        return undefined;
    const promptMessages = submitOptions?.contextReset
        ? [userMessage]
        : [...agent.state.messages, userMessage];
    return resolveShortcutSpeedForContext(agent.state.model?.id, requestedSpeed, {
        systemPrompt: agent.state.systemPrompt,
        messages: convertAgentMessagesToLlm(promptMessages),
        tools: agent.state.tools
    });
}
/**
 * Defense-in-depth credit halt. Throws `OutOfCreditsError` if the gate has
 * resolved a no-credits disposition for the next submit. The host's primary
 * gate (`checkCreditGate`) usually halts first; this fires only when
 * that's bypassed.
 *
 * Halt also kicks off a background update: a halted submit doesn't reach
 * `agent_end`, so the standard post-run update never fires. Without this,
 * the cached halt would persist across retries even after the user tops up
 * credits.
 */
/** Throw OutOfCreditsError when the credit gate has cached a halt for the next submit. */
function enforceCreditGate(creditGate) {
    if (creditGate?.shouldHaltNextSubmit()) {
        void creditGate.update().catch((err) => {
            console.warn('[embedded-runtime] post-halt credit gate update failed:', err);
        });
        throw new OutOfCreditsError();
    }
}
/** Pair an `agent.state.messages` index with its session-entry id from the funnel-tracked list. */
/** Locate the index of a user-role agent message paired with the given session-entry id. */
function findMessageIndexForEntryId(messageEntryIds, agentMessages, targetEntryId) {
    for (let i = 0; i < messageEntryIds.length; i += 1) {
        if (messageEntryIds[i] === targetEntryId && agentMessages[i]?.role === 'user')
            return i;
    }
    return -1;
}
/**
 * Slice agent messages and persisted entries to before the target user message,
 * dropping any compaction whose kept-tail boundary no longer exists. Throws
 * `EditTargetUnresolvedError` when the id does not match a user-message entry —
 * the host UI surfaces this as a retry prompt.
 */
/** Apply an edit-and-resubmit by truncating both runtime messages and persisted entries. */
function applyEditFromMessageId(agent, sessionStore, funnel, targetEntryId) {
    funnel.assertAlignedWith(agent.state.messages.length);
    const messageIndex = findMessageIndexForEntryId(funnel.getMessageEntryIds(), agent.state.messages, targetEntryId);
    if (messageIndex < 0)
        throw new EditTargetUnresolvedError(targetEntryId);
    agent.replaceMessages(agent.state.messages.slice(0, messageIndex));
    funnel.truncateMessageEntryIds(messageIndex);
    sessionStore.truncateFrom(targetEntryId);
    cascadeOrphanCompactions(sessionStore);
    funnel.assertAlignedWith(agent.state.messages.length);
}
/**
 * Pair each agent message with its persisted session-entry id by
 * reference equality. Synthetic agent messages with no backing entry
 * (compaction summary, orphan-repair injections) get an empty
 * placeholder and fall back to the positional snapshot id.
 */
/** Pair each agent message with its persisted entry id by reference, padding with placeholders. */
function buildEntryIdsForMessages(agent, sessionStore) {
    const messageRefToId = new Map();
    for (const entry of sessionStore.getEntries()) {
        if (entry.type === 'message') {
            messageRefToId.set(entry.message, entry.id);
        }
    }
    const projectedEntryIds = buildSessionContextMessageEntryIds(sessionStore.getEntries());
    return agent.state.messages.map((message, index) => messageRefToId.get(message) ?? projectedEntryIds[index] ?? '');
}
/** Repeatedly drop compaction entries whose `firstKeptEntryId` is missing — handles chained orphans. */
/** Iteratively drop compaction entries whose first-kept entry no longer exists in the store. */
function cascadeOrphanCompactions(sessionStore) {
    for (;;) {
        const entries = sessionStore.getEntries();
        const ids = new Set(entries.map((entry) => entry.id));
        const orphan = entries.find((entry) => entry.type === 'compaction' && !ids.has(entry.firstKeptEntryId));
        if (!orphan)
            return;
        sessionStore.truncateFrom(orphan.id);
    }
}
/** True if the last message is a non-displayed cancellation-boundary marker. */
function hasTrailingCancellationBoundary(messages) {
    const lastMessage = messages[messages.length - 1];
    return (lastMessage?.role === 'custom' &&
        lastMessage.customType === CANCELLATION_BOUNDARY_CUSTOM_TYPE &&
        lastMessage.display === false);
}
/** Append a cancellation boundary message to mark an aborted run, if one is not already present. */
function appendCancellationBoundaryIfNeeded(agent, funnel) {
    funnel.assertAlignedWith(agent.state.messages.length);
    if (hasTrailingCancellationBoundary(agent.state.messages))
        return;
    funnel.appendCustomBoundaryMessage(createCancellationBoundaryMessage(new Date().toISOString()));
    funnel.assertAlignedWith(agent.state.messages.length);
}
/** Assemble the per-turn stream-observability context (mode, file ids, interaction mode) for tracing. */
function buildStreamObservabilityContext(submitOptions, agentMode) {
    const fileAttachmentIds = submitOptions?.fileUploads
        ?.map((file) => file.fileId)
        .filter((fileId) => typeof fileId === 'string' && fileId.length > 0) ?? [];
    const interactionMode = submitOptions?.interactionMode;
    return {
        agentMode,
        ...(submitOptions?.userMessageId ? { initialFileId: submitOptions.userMessageId } : {}),
        ...(fileAttachmentIds.length > 0 ? { fileAttachmentIds } : {}),
        ...(interactionMode ? { interactionMode } : {}),
        llmOperation: 'chat'
    };
}
/** Internal composition root that wires every embedded-runtime subsystem and returns the public facade. */
export async function composeEmbeddedAgentRuntimeInternal(options) {
    const sessionContext = options.sessionStore.buildSessionContext();
    const resolved = await selectInitialRuntimeModel({
        model: options.model,
        modelRegistry: options.modelRegistry,
        sessionContext,
        thinkingLevel: options.thinkingLevel
    });
    const agentRef = {};
    const agentRuntime = new AgentEngine({
        convertToLlm: convertAgentMessagesToLlm,
        sessionId: options.sessionStore.getSessionId(),
        getApiKey: async (provider) => {
            const resolvedProvider = provider ?? agentRef.agent?.state.model?.provider;
            if (!resolvedProvider)
                return undefined;
            return options.modelRegistry.getApiKeyForProvider(resolvedProvider);
        },
        streamFn: options.streamFn,
        transport: options.settingsStore.getTransport()
    });
    const pendingHostToolRequests = createPendingHostToolRequests({
        getTimeoutMs: getRuntimePendingHostToolTimeoutMs,
        getHeartbeatWrap: () => agentRuntime.getHeartbeatWrap()
    });
    let currentConfiguration = {
        systemPrompt: options.initialConfiguration?.systemPrompt ?? options.systemPrompt ?? '',
        toolNames: [...(options.initialConfiguration?.toolNames ?? [])],
        ...(options.initialConfiguration?.agentMode
            ? { agentMode: options.initialConfiguration.agentMode }
            : {})
    };
    let runtimeAgentMode = currentConfiguration.agentMode ?? 'action';
    let pendingRefreshContext = null;
    let originalAutonomousTask;
    const scheduleRefresh = (params) => {
        pendingRefreshContext = params;
    };
    const applyRuntimeHandoff = (handoff) => {
        if (handoff.kind !== 'switchMode') {
            return;
        }
        applyAgentConfiguration({
            ...handoff.configuration,
            toolNames: [...handoff.configuration.toolNames]
        });
    };
    const normalizeHostToolLifecycle = (tools) => tools.map((tool) => tool.lifecycleSource === undefined
        ? { ...tool, lifecycleSource: ToolLifecycleSource.HostTool }
        : tool);
    // Shared mutable holder for per-turn options. Tool construction, compaction,
    // and submit all need the latest value, including during initial tool setup.
    let activePerTurnOptions = {};
    const buildAvailableTools = () => [
        ...normalizeHostToolLifecycle(options.createHostTools?.(pendingHostToolRequests, {
            applyRuntimeHandoff,
            getAgentMode: () => runtimeAgentMode,
            getCurrentModel: () => agentRef.agent?.state.model,
            getSkipTools: () => activePerTurnOptions.skipTools,
            getSpeed: () => agentRef.agent?.speed,
            scheduleRefresh
        }) ?? []),
        ...(options.tools ?? [])
    ];
    const buildAvailableToolMap = (tools) => {
        const map = new Map(tools.map((tool) => [tool.name, tool]));
        if (map.size !== tools.length) {
            throw new Error('Embedded runtime tool names must be unique');
        }
        return map;
    };
    let availableTools = buildAvailableTools();
    let availableToolMap = buildAvailableToolMap(availableTools);
    if (currentConfiguration.toolNames.length === 0) {
        currentConfiguration = {
            ...currentConfiguration,
            toolNames: availableTools.map((tool) => tool.name)
        };
    }
    const getConfiguredTools = (toolNames) => toolNames.map((toolName) => {
        const tool = availableToolMap.get(toolName);
        if (!tool)
            throw new Error(`Embedded runtime does not know tool "${toolName}".`);
        return tool;
    });
    const applyAgentConfiguration = (configuration) => {
        currentConfiguration = { ...configuration, toolNames: [...configuration.toolNames] };
        runtimeAgentMode = currentConfiguration.agentMode ?? 'action';
        availableTools = buildAvailableTools();
        availableToolMap = buildAvailableToolMap(availableTools);
        agent.setSystemPrompt(currentConfiguration.systemPrompt);
        agent.setTools(getConfiguredTools(currentConfiguration.toolNames));
        agentRuntime.streamObservabilityContext = {
            ...agentRuntime.streamObservabilityContext,
            agentMode: runtimeAgentMode
        };
    };
    const initialTools = getConfiguredTools(currentConfiguration.toolNames);
    const agent = new AgentController({
        initialState: {
            model: resolved.model,
            systemPrompt: currentConfiguration.systemPrompt,
            thinkingLevel: resolved.thinkingLevel,
            tools: initialTools,
            messages: sessionContext.messages
        },
        steeringMode: options.settingsStore.getSteeringMode(),
        followUpMode: options.settingsStore.getFollowUpMode(),
        runtime: agentRuntime
    });
    agentRef.agent = agent;
    let stopReasonOverride;
    // Extra-events sink — populated when the facade subscribes via
    // `subscribeExtraEvents`. The runtime pushes RunStatsUpdated events
    // through it so the host's AgentEvent listener sees them on the same
    // bus as snapshot/session-context events.
    const extraEventListeners = new Set();
    const emitExtraEvent = (event) => {
        for (const listener of extraEventListeners) {
            listener(event);
        }
    };
    const turnLifecycle = new EmbeddedTurnLifecycle();
    const runStatsAccumulator = createRunStatsAccumulator({
        onRunComplete: (snapshot) => {
            emitExtraEvent({
                kind: AgentEventKind.RunStatsUpdated,
                threadTokenUsage: snapshot.threadTokenUsage,
                runEntry: snapshot.runEntry
            });
        },
        // Called once per run at `agent_start`, after submit() resolved the
        // effective model. So `agent.state.model.id` here is the model the run
        // actually streams against, including host-degraded 'free-model' runs.
        // `RunMetricsDropdown` keys its display off this value.
        resolveRunConfig: () => {
            const effortLevel = ShortcutThinkingLevelToEffort[agent.state.thinkingLevel];
            return {
                modelName: agent.state.model?.id,
                effortLevel,
                speed: agent.speed
            };
        }
    });
    // Hydrated session history seeds both cumulative token usage and run numbers.
    runStatsAccumulator.seedFromHistory(options.sessionStore.getEntries());
    if (sessionContext.messages.length === 0) {
        if (resolved.model) {
            options.sessionStore.appendModelChange(resolved.model.provider, resolved.model.id);
        }
        options.sessionStore.appendThinkingLevelChange(resolved.thinkingLevel);
    }
    else if (sessionContext.model == null && resolved.model) {
        options.sessionStore.appendModelChange(resolved.model.provider, resolved.model.id);
    }
    // Late-bound: compaction can rebuild messages before/after the funnel exists,
    // but this callback only needs the assigned funnel once a run has emitted turns.
    const funnelRef = {};
    const compactionWiring = createEmbeddedCompactionWiring({
        agent,
        modelRegistry: options.modelRegistry,
        sessionStore: options.sessionStore,
        getPerTurnOptions: () => activePerTurnOptions,
        completeFn: options.compactionCompleteFn,
        getHeartbeatWrap: () => agentRuntime.getHeartbeatWrap(),
        onMessagesReplaced: (context) => {
            funnelRef.current?.seedMessageEntryIds(context.messageEntryIds ?? []);
            funnelRef.current?.assertAlignedWith(context.messages.length);
        },
        onAgentMessagesReplaced: (messages) => {
            funnelRef.current?.truncateMessageEntryIds(messages.length);
            funnelRef.current?.assertAlignedWith(messages.length);
        }
    });
    /**
     * Fire-and-forget follow-up suggestions. Snapshot messages and current-mode
     * tool descriptions at `agent_end` so the suggestion job sees the completed turn.
     */
    const maxSuggestionsRunsPerSession = 4;
    let suggestionsRunsStarted = 0;
    const fireSuggestions = () => {
        if (suggestionsRunsStarted >= maxSuggestionsRunsPerSession)
            return;
        suggestionsRunsStarted += 1;
        const messagesSnapshot = [...agent.state.messages];
        const toolDescriptions = agent.state.tools
            .map((tool) => `### ${tool.name}\n${tool.description}\n`)
            .join('\n');
        emitExtraEvent({ kind: AgentEventKind.FollowupSuggestionsStarted });
        void (async () => {
            const outcome = await runSuggestionsViaBridge(messagesSnapshot, toolDescriptions, pendingHostToolRequests);
            if (outcome.status === 'failed') {
                emitExtraEvent({
                    kind: AgentEventKind.FollowupSuggestionsFailed,
                    message: outcome.reason
                });
                return;
            }
            const result = outcome.result;
            if (!result || result.suggestions.length === 0) {
                emitExtraEvent({
                    kind: AgentEventKind.FollowupSuggestionsFailed,
                    message: 'No follow-up suggestions returned.'
                });
                return;
            }
            emitExtraEvent({
                kind: AgentEventKind.FollowupSuggestionsReady,
                suggestions: [...result.suggestions],
                skillProposal: result.skillProposal ? { ...result.skillProposal } : null
            });
        })();
    };
    // Update after each run boundary; the next submit reads the cached
    // disposition synchronously and never waits on the fetch.
    const updateCreditGate = options.creditGate
        ? () => {
            void options.creditGate.update().catch((err) => {
                console.warn('[embedded-runtime] credit gate update failed:', err);
            });
        }
        : undefined;
    const funnel = createAgentEventFunnel({
        agent,
        sessionStore: options.sessionStore,
        runStatsAccumulator,
        compactionWiring,
        onAgentStart: options.onAgentStart,
        onAgentEnd: updateCreditGate,
        onAgentEndSuccess: fireSuggestions
    });
    funnelRef.current = funnel;
    const emitRecoveryTelemetry = (event) => {
        const runId = runStatsAccumulator.getActiveRunId();
        switch (event.type) {
            case EmbeddedRecoveryEventType.Start:
                emitExtraEvent({
                    kind: AgentEventKind.Telemetry,
                    telemetry: {
                        type: AgentTelemetryEventType.RecoveryStarted,
                        category: event.category,
                        attempt: event.attempt,
                        maxAttempts: event.maxAttempts,
                        delayMs: event.delayMs,
                        errorMessage: event.errorMessage,
                        ...(runId !== undefined ? { runId } : {})
                    }
                });
                break;
            case EmbeddedRecoveryEventType.End:
                emitExtraEvent({
                    kind: AgentEventKind.Telemetry,
                    telemetry: {
                        type: event.success
                            ? AgentTelemetryEventType.RecoverySucceeded
                            : AgentTelemetryEventType.RecoveryFailed,
                        category: event.category,
                        attempt: event.attempt,
                        ...(event.finalError !== undefined ? { finalError: event.finalError } : {}),
                        ...(runId !== undefined ? { runId } : {})
                    }
                });
                break;
            case EmbeddedRecoveryEventType.Fallback:
                emitExtraEvent({
                    kind: AgentEventKind.Telemetry,
                    telemetry: {
                        type: AgentTelemetryEventType.RecoveryFallback,
                        category: event.category,
                        fallbackModel: event.fallbackModel,
                        ...(runId !== undefined ? { runId } : {})
                    }
                });
                break;
        }
    };
    const recoveryWiring = createEmbeddedRecoveryWiring({
        agent,
        continueAgent: async () => {
            turnLifecycle.markRecovering();
            runStatsAccumulator.beginRun();
            try {
                await agent.waitForIdle();
                await agent.continue();
            }
            catch (err) {
                runStatsAccumulator.abortRun();
                throw err;
            }
        },
        sessionStore: options.sessionStore,
        funnel,
        getSettings: () => ({
            enabled: true,
            ...(typeof options.settingsStore.getErrorRecoverySettings === 'function'
                ? options.settingsStore.getErrorRecoverySettings()
                : {})
        }),
        onRecoveryEvent: emitRecoveryTelemetry,
        recoverySleep: options.recoverySleep
    });
    // Pair each hydrated message with its persisted entry id. Synthetic messages
    // without entries keep an empty slot so ids stay index-aligned with messages.
    funnel.seedMessageEntryIds(buildEntryIdsForMessages(agent, options.sessionStore));
    let mainMessageStreamCounter = 0;
    let activeMainMessageEntryId;
    let activeMainMessageStreamId;
    const nextMainMessageStreamId = () => {
        mainMessageStreamCounter += 1;
        return `${options.sessionStore.getSessionId()}:main:${mainMessageStreamCounter}`;
    };
    const getMainMessageStreamRunId = () => runStatsAccumulator.getActiveRunId();
    const parseToolCallArgs = (value) => {
        if (value && typeof value === 'object' && !Array.isArray(value)) {
            return value;
        }
        if (typeof value !== 'string' || value.trim().length === 0) {
            return {};
        }
        try {
            const parsed = JSON.parse(value);
            return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
                ? parsed
                : {};
        }
        catch {
            return { partial_json: value };
        }
    };
    const readToolCallFromAssistantPartial = (message, contentIndex) => {
        const content = message.content?.[contentIndex];
        if (content?.type !== 'toolCall')
            return null;
        if (typeof content.id !== 'string' || typeof content.name !== 'string')
            return null;
        return {
            arguments: content.arguments,
            id: content.id,
            name: content.name
        };
    };
    const buildMainMessageStreamEvent = (event) => {
        if ((event.type === AgentLoopEventType.MessageStart ||
            event.type === AgentLoopEventType.MessageUpdate ||
            event.type === AgentLoopEventType.MessageEnd) &&
            event.message.role !== 'assistant') {
            return null;
        }
        if (event.type === AgentLoopEventType.MessageStart) {
            activeMainMessageStreamId = nextMainMessageStreamId();
            activeMainMessageEntryId = options.sessionStore.reserveAssistantMessageEntryId();
            // Allocate the session entry id before streaming so Start events and
            // later snapshots share one assistant-message identity.
            return {
                kind: AgentEventKind.MainMessageStream,
                stream: {
                    type: AgentMainMessageStreamEventType.Start,
                    messageId: activeMainMessageEntryId,
                    runId: getMainMessageStreamRunId(),
                    streamId: activeMainMessageStreamId
                }
            };
        }
        if (event.type === AgentLoopEventType.MessageUpdate) {
            if (!activeMainMessageStreamId) {
                console.warn('[embedded-runtime] assistant message update received before stream start');
                return null;
            }
            const streamId = activeMainMessageStreamId;
            const assistantEvent = event.assistantMessageEvent;
            const contentIndex = assistantEvent.contentIndex;
            if (typeof contentIndex !== 'number')
                return null;
            const runId = getMainMessageStreamRunId();
            switch (assistantEvent.type) {
                case AssistantMessageEventType.TextDelta:
                    return typeof assistantEvent.delta === 'string'
                        ? {
                            kind: AgentEventKind.MainMessageStream,
                            stream: {
                                contentIndex,
                                delta: assistantEvent.delta,
                                runId,
                                streamId,
                                type: AgentMainMessageStreamEventType.TextDelta
                            }
                        }
                        : null;
                case AssistantMessageEventType.ThinkingDelta:
                    return typeof assistantEvent.delta === 'string'
                        ? {
                            kind: AgentEventKind.MainMessageStream,
                            stream: {
                                contentIndex,
                                delta: assistantEvent.delta,
                                runId,
                                streamId,
                                type: AgentMainMessageStreamEventType.ThinkingDelta
                            }
                        }
                        : null;
                case AssistantMessageEventType.ToolCallEnd: {
                    const eventToolCall = assistantEvent.toolCall &&
                        typeof assistantEvent.toolCall.id === 'string' &&
                        typeof assistantEvent.toolCall.name === 'string'
                        ? {
                            arguments: assistantEvent.toolCall.arguments,
                            id: assistantEvent.toolCall.id,
                            name: assistantEvent.toolCall.name
                        }
                        : null;
                    const toolCall = eventToolCall ?? readToolCallFromAssistantPartial(event.message, contentIndex);
                    if (!toolCall)
                        return null;
                    return {
                        kind: AgentEventKind.MainMessageStream,
                        stream: {
                            args: parseToolCallArgs(toolCall?.arguments),
                            contentIndex,
                            runId,
                            streamId,
                            toolCallId: toolCall.id,
                            toolName: toolCall.name,
                            type: AgentMainMessageStreamEventType.ToolCallEnd
                        }
                    };
                }
                default:
                    return null;
            }
        }
        if (event.type === AgentLoopEventType.MessageEnd) {
            if (!activeMainMessageStreamId) {
                console.warn('[embedded-runtime] assistant message end received before stream start');
                return null;
            }
            const streamId = activeMainMessageStreamId;
            activeMainMessageStreamId = undefined;
            activeMainMessageEntryId = undefined;
            return {
                kind: AgentEventKind.MainMessageStream,
                stream: {
                    runId: getMainMessageStreamRunId(),
                    streamId,
                    type: AgentMainMessageStreamEventType.End
                }
            };
        }
        return null;
    };
    const isMainAssistantMessageUpdate = (event) => event.type === AgentLoopEventType.MessageUpdate && event.message.role === 'assistant';
    const shouldSkipSnapshotUpdate = (event) => {
        if (event.type === AgentLoopEventType.MessageStart && event.message.role === 'assistant')
            return true;
        if (event.type === AgentLoopEventType.MessageStart && event.message.role === 'toolResult')
            return true;
        if (event.type === AgentLoopEventType.MessageEnd && event.message.role === 'toolResult') {
            const toolCallId = event.message.toolCallId;
            if (agent.state.inFlight.some((op) => (op.kind === InFlightOpKind.Tool || op.kind === InFlightOpKind.HostTool) &&
                op.toolCallId === toolCallId)) {
                return true;
            }
        }
        if (isMainAssistantMessageUpdate(event))
            return true;
        if (event.type === AgentLoopEventType.ToolExecutionStart ||
            event.type === AgentLoopEventType.ToolExecutionUpdate ||
            event.type === AgentLoopEventType.ToolExecutionEnd) {
            return true;
        }
        if (event.type === AgentLoopEventType.OpStart &&
            (event.kind === InFlightOpKind.Tool || event.kind === InFlightOpKind.HostTool)) {
            return true;
        }
        if (event.type === AgentLoopEventType.HeartbeatTick &&
            (event.kind === InFlightOpKind.Tool || event.kind === InFlightOpKind.HostTool) &&
            event.elapsedMs < AGENT_TOOL_IN_FLIGHT_DISPLAY_DELAY_MS) {
            return true;
        }
        return false;
    };
    const stampAssistantEntryIdBeforePersistence = (event) => {
        if (event.type === AgentLoopEventType.MessageEnd &&
            event.message.role === 'assistant' &&
            activeMainMessageEntryId) {
            event.message.id = activeMainMessageEntryId;
        }
    };
    const getSnapshotInFlightOps = () => agent.state.inFlight.filter((op) => (op.kind !== InFlightOpKind.Tool && op.kind !== InFlightOpKind.HostTool) ||
        op.elapsedMs >= AGENT_TOOL_IN_FLIGHT_DISPLAY_DELAY_MS);
    const getToolResultObservabilityFields = (toolName, result) => {
        const details = result?.details;
        return availableToolMap.get(toolName)?.getResultObservabilityFields?.(details);
    };
    const isToolLifecycleOwnedByHost = (toolName) => availableToolMap.get(toolName)?.lifecycleSource === ToolLifecycleSource.HostTool;
    /**
     * Op kinds whose lifecycle telemetry is emitted from the generic OpStart/OpEnd
     * branch. Tool and HostTool have dedicated lifecycle sources (ToolExecution*
     * and HostToolRequest/Applied respectively) and must not double-emit here.
     */
    const OP_LIFECYCLE_ALLOWLIST = new Set([
        InFlightOpKind.Compaction,
        InFlightOpKind.Llm,
        InFlightOpKind.Subagent
    ]);
    const emitToolExecutionStarted = (event) => {
        if (isToolLifecycleOwnedByHost(event.toolName))
            return;
        emitExtraEvent({
            kind: AgentEventKind.Telemetry,
            telemetry: {
                type: AgentTelemetryEventType.OperationStarted,
                opId: event.toolCallId,
                opKind: InFlightOpKind.Tool,
                startedAt: event.startedAt,
                runId: runStatsAccumulator.getActiveRunId(),
                toolName: event.toolName,
                toolCallId: event.toolCallId
            }
        });
    };
    const emitToolExecutionFinished = (event) => {
        if (isToolLifecycleOwnedByHost(event.toolName))
            return;
        const metadata = getToolResultObservabilityFields(event.toolName, event.result);
        emitExtraEvent({
            kind: AgentEventKind.Telemetry,
            telemetry: {
                type: AgentTelemetryEventType.OperationFinished,
                opId: event.toolCallId,
                opKind: InFlightOpKind.Tool,
                phase: event.isError
                    ? AgentTelemetryTerminalPhase.Failed
                    : AgentTelemetryTerminalPhase.Completed,
                startedAt: event.startedAt,
                endedAt: event.endedAt,
                runId: runStatsAccumulator.getActiveRunId(),
                toolName: event.toolName,
                toolCallId: event.toolCallId,
                ...(metadata !== undefined ? { metadata } : {})
            }
        });
    };
    const emitOpLifecycleStarted = (event) => {
        emitExtraEvent({
            kind: AgentEventKind.Telemetry,
            telemetry: {
                type: AgentTelemetryEventType.OperationStarted,
                opId: event.opId,
                opKind: event.kind,
                startedAt: event.startedAt,
                runId: runStatsAccumulator.getActiveRunId(),
                ...(event.meta.toolName !== undefined ? { toolName: event.meta.toolName } : {}),
                ...(event.meta.toolCallId !== undefined ? { toolCallId: event.meta.toolCallId } : {})
            }
        });
    };
    const emitOpLifecycleFinished = (event) => {
        emitExtraEvent({
            kind: AgentEventKind.Telemetry,
            telemetry: {
                type: AgentTelemetryEventType.OperationFinished,
                opId: event.opId,
                opKind: event.kind,
                phase: event.status === 'cancelled'
                    ? AgentTelemetryTerminalPhase.Cancelled
                    : event.status === 'failed'
                        ? AgentTelemetryTerminalPhase.Failed
                        : AgentTelemetryTerminalPhase.Completed,
                startedAt: event.startedAt,
                endedAt: event.endedAt,
                runId: runStatsAccumulator.getActiveRunId(),
                ...(event.meta.toolName !== undefined ? { toolName: event.meta.toolName } : {}),
                ...(event.meta.toolCallId !== undefined ? { toolCallId: event.meta.toolCallId } : {}),
                ...(event.errorMessage !== undefined ? { errorMessage: event.errorMessage } : {})
            }
        });
    };
    const subscribeSnapshotSource = (listener) => agent.subscribe((event) => {
        recoveryWiring.prepareForAgentEnd(event);
        stampAssistantEntryIdBeforePersistence(event);
        funnel.handle(event);
        if (event.type === AgentLoopEventType.AgentEnd) {
            // Recovery starts from the synchronous event subscriber so the facade
            // contract stays "emit snapshot/session updates, don't block". submit()
            // awaits recoveryWiring.wait() below so callers still observe a fully
            // recovered terminal state when recovery succeeds.
            void recoveryWiring.handleAgentEnd(event).catch((err) => {
                console.warn('[embedded-runtime] error recovery failed:', err);
            });
        }
        const mainStreamEvent = buildMainMessageStreamEvent(event);
        if (mainStreamEvent) {
            emitExtraEvent(mainStreamEvent);
        }
        switch (event.type) {
            case AgentLoopEventType.AgentStart:
                emitExtraEvent({
                    kind: AgentEventKind.Telemetry,
                    telemetry: {
                        type: AgentTelemetryEventType.RunStarted,
                        runId: runStatsAccumulator.getActiveRunId()
                    }
                });
                break;
            case AgentLoopEventType.ToolExecutionStart:
                emitToolExecutionStarted(event);
                break;
            case AgentLoopEventType.ToolExecutionEnd:
                emitToolExecutionFinished(event);
                break;
            case AgentLoopEventType.OpStart:
                if (OP_LIFECYCLE_ALLOWLIST.has(event.kind))
                    emitOpLifecycleStarted(event);
                break;
            case AgentLoopEventType.OpEnd:
                if (OP_LIFECYCLE_ALLOWLIST.has(event.kind))
                    emitOpLifecycleFinished(event);
                break;
        }
        if (shouldSkipSnapshotUpdate(event)) {
            return;
        }
        listener();
    });
    const runtime = createEmbeddedAgentRuntime({
        async abort() {
            const wasActive = turnLifecycle.isTurnActive(agent.state.isStreaming);
            if (!wasActive) {
                return;
            }
            const cancellationToken = turnLifecycle.beginCancelling();
            pendingRefreshContext = null;
            recoveryWiring.abort();
            // Mark BEFORE agent.abort() so the in-flight agent_end (which fires
            // during waitForIdle) drops the run without finalizing run_stats /
            // run_token_usage — matches backend `finalize` skip on SSE close.
            runStatsAccumulator.markCancelling();
            // Set the snapshot override before abort emits any agent events. Those
            // events can synchronously publish snapshots; waiting until after
            // waitForIdle lets a stale "completed" snapshot race the cancelled one.
            stopReasonOverride = AgentStopReason.Cancelled;
            if (agent.state.isStreaming) {
                agent.abort();
            }
            agent.clearSteeringQueue();
            pendingHostToolRequests.discardPending();
            await agent.waitForIdle();
            // Idempotent no-op when agent_end already dropped activeRun.
            runStatsAccumulator.abortRun();
            appendCancellationBoundaryIfNeeded(agent, funnel);
            turnLifecycle.finish(cancellationToken);
        },
        async configure(configuration) {
            applyAgentConfiguration(configuration);
        },
        dispose() {
            turnLifecycle.dispose();
            recoveryWiring.abort();
            agent.abort();
        },
        getSnapshot() {
            const continuationStopReasonOverride = stopReasonOverride ??
                (turnLifecycle.snapshotShouldStayRunning(pendingRefreshContext !== null)
                    ? AgentStopReason.Running
                    : undefined);
            return createAgentSnapshotFromAgentMessages({
                sessionId: options.sessionStore.getSessionId(),
                messages: agent.state.messages,
                isStreaming: agent.state.isStreaming,
                stopReasonOverride: continuationStopReasonOverride,
                errorKind: agent.state.errorKind,
                inFlight: getSnapshotInFlightOps(),
                runId: runStatsAccumulator.getActiveRunId(),
                messageEntryIds: funnel.getMessageEntryIds()
            });
        },
        getSessionContext() {
            return options.sessionStore.buildSessionContext();
        },
        async resumeWithHostToolResult(result) {
            if (stopReasonOverride !== AgentStopReason.Cancelled) {
                stopReasonOverride = undefined;
            }
            pendingHostToolRequests.applyResult(result);
        },
        async submit(prompt, submitOptions) {
            stopReasonOverride = undefined;
            originalAutonomousTask = extractAutonomousTask(prompt) ?? originalAutonomousTask;
            const activeRunInProgress = turnLifecycle.isTurnActive(agent.state.isStreaming);
            // Edit-and-resubmit rewinds both runtime messages and persisted entries
            // before appending the replacement user turn.
            if (submitOptions?.editFromMessageId) {
                if (activeRunInProgress) {
                    throw new Error('Cannot edit and resubmit while a previous turn is still streaming.');
                }
                applyEditFromMessageId(agent, options.sessionStore, funnel, submitOptions.editFromMessageId);
                runStatsAccumulator.seedFromHistory(options.sessionStore.getEntries());
            }
            if (activeRunInProgress) {
                if (getSubmitOverlapPolicy(submitOptions) === EmbeddedSubmitOverlapPolicy.Reject) {
                    if (submitOptions?.contextReset) {
                        throw new Error('Cannot reset context while a previous turn is still streaming.');
                    }
                    throw new Error('Cannot submit while a previous turn is still streaming.');
                }
                enforceCreditGate(options.creditGate);
                const userMessage = buildUserMessage({
                    query: prompt,
                    userMessageId: submitOptions?.userMessageId,
                    interactionMode: submitOptions?.interactionMode,
                    fileUploads: submitOptions?.fileUploads,
                    workbookSummary: submitOptions?.workbookSummary,
                    focusedRange: submitOptions?.focusedRange,
                    rangeRestrictions: submitOptions?.rangeRestrictions,
                    contextWarningEligible: false
                });
                agent.steer(userMessage);
                return;
            }
            // CreditGate runs *before* any per-turn state mutation: a halt must
            // not leak partial knob overrides into the next submit.
            enforceCreditGate(options.creditGate);
            activePerTurnOptions = submitOptions ?? {};
            // Per-turn overrides apply only after the credit gate accepts the submit.
            const baselineModel = agent.state.model;
            const baselineThinkingLevel = agent.state.thinkingLevel;
            const baselineSpeed = agent.speed;
            const baselineStreamObservabilityContext = agentRuntime.streamObservabilityContext;
            applyPerTurnModelAndThinkingOverrides(agent, options.modelRegistry, submitOptions);
            if (submitOptions?.speed !== undefined)
                agent.speed = undefined;
            const submitToken = turnLifecycle.beginSubmit();
            let runStarted = false;
            try {
                agentRuntime.streamObservabilityContext = buildStreamObservabilityContext(submitOptions, runtimeAgentMode);
                // Mint runId before compaction so the summarizer's usage rolls into
                // this run AND beats the heartbeat-emit-before-agent_start race.
                runStatsAccumulator.beginRun();
                await compactionWiring.triggerIfNeeded(false);
                if (!turnLifecycle.isCurrent(submitToken)) {
                    return;
                }
                const userMessage = buildUserMessage({
                    query: prompt,
                    userMessageId: submitOptions?.userMessageId,
                    interactionMode: submitOptions?.interactionMode,
                    fileUploads: submitOptions?.fileUploads,
                    workbookSummary: submitOptions?.workbookSummary,
                    focusedRange: submitOptions?.focusedRange,
                    rangeRestrictions: submitOptions?.rangeRestrictions,
                    contextWarningEligible: hasPriorUserEquivalent(agent.state.messages),
                    contextReset: submitOptions?.contextReset
                });
                if (!turnLifecycle.isCurrent(submitToken)) {
                    return;
                }
                if (submitOptions?.speed !== undefined) {
                    agent.speed = resolvePerTurnSpeed(agent, userMessage, submitOptions);
                    runStatsAccumulator.refreshRunConfig();
                }
                runStarted = true;
                turnLifecycle.markRunning(submitToken);
                await agent.prompt([userMessage]);
                // handleAgentEnd starts recovery from the event subscriber; this wait
                // keeps submit() open until retry/continue has either succeeded or
                // exhausted, matching CLI AgentSession.prompt().
                await recoveryWiring.wait();
                while (pendingRefreshContext) {
                    if (!turnLifecycle.isCurrent(submitToken)) {
                        pendingRefreshContext = null;
                        return;
                    }
                    const refresh = pendingRefreshContext;
                    pendingRefreshContext = null;
                    turnLifecycle.markRefreshing(submitToken);
                    if (refresh.model) {
                        const refreshModel = options.modelRegistry.find(agent.state.model.provider, refresh.model);
                        if (refreshModel)
                            agent.setModel(refreshModel);
                    }
                    activePerTurnOptions = {
                        model: refresh.model,
                        contextReset: true,
                        interactionMode: runtimeAgentMode,
                        fileUploads: submitOptions?.fileUploads,
                        workbookSummary: submitOptions?.workbookSummary,
                        focusedRange: submitOptions?.focusedRange,
                        rangeRestrictions: submitOptions?.rangeRestrictions
                    };
                    agentRuntime.streamObservabilityContext = buildStreamObservabilityContext(activePerTurnOptions, runtimeAgentMode);
                    agent.speed = undefined;
                    runStatsAccumulator.beginRun();
                    runStatsAccumulator.refreshRunConfig();
                    const continuationMessage = buildUserMessage({
                        query: buildRefreshContextPrompt(refresh, originalAutonomousTask),
                        interactionMode: runtimeAgentMode,
                        fileUploads: submitOptions?.fileUploads,
                        workbookSummary: submitOptions?.workbookSummary,
                        focusedRange: submitOptions?.focusedRange,
                        rangeRestrictions: submitOptions?.rangeRestrictions,
                        contextWarningEligible: false,
                        contextReset: true
                    });
                    await agent.prompt([continuationMessage]);
                    await recoveryWiring.wait();
                }
            }
            catch (err) {
                pendingRefreshContext = null;
                if (!runStarted)
                    runStatsAccumulator.abortRun();
                throw err;
            }
            finally {
                turnLifecycle.finish(submitToken);
                activePerTurnOptions = {};
                // Restore baselines so per-turn overrides never leak into the next submit.
                agent.setModel(baselineModel);
                agent.setThinkingLevel(baselineThinkingLevel);
                agent.speed = baselineSpeed;
                agentRuntime.streamObservabilityContext = baselineStreamObservabilityContext;
            }
        },
        subscribeSnapshotSource,
        subscribeExtraEvents(listener) {
            extraEventListeners.add(listener);
            return () => {
                extraEventListeners.delete(listener);
            };
        }
    }, pendingHostToolRequests);
    return {
        runtime,
        whenCompactionIdle: () => compactionWiring.whenIdle(),
        recordChildAgentEvent: (event) => runStatsAccumulator.handleChildEvent(event)
    };
}
//# sourceMappingURL=compose.js.map