/**
 * Main-thread runtime client — sits opposite the worker, forwards
 * `AgentCommand`s, fields host tool requests + model-stream requests from the
 * worker, and exposes a small imperative API to the host. Owns the abort
 * generation that detaches late client/model-stream results after a cancel.
 */
import { AgentCommandKind, AgentEventKind } from '../../contracts/agent-api.js';
import { createFailedHostToolResult, errorToMessage } from '../../contracts/host-tool-api.js';
import { ShortcutXLModelStreamEventKind, ShortcutXLModelStreamStopReason } from '../../contracts/model-stream.js';
import { WorkerMessageKind, isWorkerMessage } from '../../contracts/wire.js';
/** Trace-event tags used by `onModelStreamTrace`. */
export const ShortcutXLModelStreamTraceKind = {
    HostToolRequestReceived: 'hostToolRequestReceived',
    HostToolResultPosted: 'hostToolResultPosted',
    HostToolResultSuppressed: 'hostToolResultSuppressed',
    CancelReceived: 'cancelReceived',
    EventSent: 'eventSent',
    HandlerCompleted: 'handlerCompleted',
    HandlerFailed: 'handlerFailed',
    RequestReceived: 'requestReceived'
};
const MODEL_STREAM_DELTA_FLUSH_MS = 16;
function createModelStreamErrorEvent(request, error, reason = ShortcutXLModelStreamStopReason.Error) {
    const message = errorToMessage(error);
    return {
        kind: ShortcutXLModelStreamEventKind.Error,
        error: {
            api: 'shortcut-llm',
            content: [],
            errorMessage: message,
            model: request.model.id,
            provider: 'shortcut',
            role: 'assistant',
            stopReason: reason,
            timestamp: Date.now(),
            usage: {
                cacheRead: 0,
                cacheWrite: 0,
                cost: {
                    cacheRead: 0,
                    cacheWrite: 0,
                    input: 0,
                    output: 0,
                    total: 0
                },
                input: 0,
                output: 0,
                totalTokens: 0
            }
        },
        partial: {
            api: 'shortcut-llm',
            content: [],
            errorMessage: message,
            model: request.model.id,
            provider: 'shortcut',
            role: 'assistant',
            stopReason: reason,
            timestamp: Date.now(),
            usage: {
                cacheRead: 0,
                cacheWrite: 0,
                cost: {
                    cacheRead: 0,
                    cacheWrite: 0,
                    input: 0,
                    output: 0,
                    total: 0
                },
                input: 0,
                output: 0,
                totalTokens: 0
            }
        },
        reason
    };
}
function isCoalescibleModelStreamEvent(event) {
    return (event.kind === ShortcutXLModelStreamEventKind.TextDelta ||
        event.kind === ShortcutXLModelStreamEventKind.ThinkingDelta ||
        event.kind === ShortcutXLModelStreamEventKind.ToolCallDelta);
}
function canMergeModelStreamEvents(current, next) {
    return current.kind === next.kind && current.contentIndex === next.contentIndex;
}
function mergeModelStreamEvents(current, next) {
    return {
        ...next,
        delta: current.delta + next.delta
    };
}
function createModelStreamEventCoalescer(input) {
    let pendingDelta = null;
    let flushTimer = null;
    const clearFlushTimer = () => {
        if (flushTimer == null) {
            return;
        }
        clearTimeout(flushTimer);
        flushTimer = null;
    };
    const flush = () => {
        clearFlushTimer();
        const event = pendingDelta;
        if (!event) {
            return;
        }
        pendingDelta = null;
        input.onDeltaFlushed?.();
        input.emitNow(event);
    };
    const scheduleFlush = () => {
        if (flushTimer != null) {
            return;
        }
        flushTimer = setTimeout(flush, MODEL_STREAM_DELTA_FLUSH_MS);
    };
    return {
        dispose() {
            clearFlushTimer();
            pendingDelta = null;
        },
        emit(event) {
            if (!isCoalescibleModelStreamEvent(event)) {
                flush();
                input.emitNow(event);
                return;
            }
            if (pendingDelta && canMergeModelStreamEvents(pendingDelta, event)) {
                pendingDelta = mergeModelStreamEvents(pendingDelta, event);
                input.onDeltaMerged?.();
                scheduleFlush();
                return;
            }
            flush();
            pendingDelta = event;
            scheduleFlush();
        },
        flush
    };
}
/** Build a main-thread client bound to one worker port. */
export function createEmbeddedAgentPortClient(port, options) {
    const { registry } = options;
    const listeners = new Set();
    const activeHostToolRequestControllers = new Map();
    const activeModelStreams = new Map();
    const modelStreamBridgeMetrics = {
        coalescedModelStreamDeltaCount: 0,
        forwardedModelStreamDeltaCount: 0,
        forwardedModelStreamEventCount: 0,
        generatedModelStreamErrorCount: 0,
        maxActiveModelStreams: 0,
        modelStreamCancelCount: 0,
        modelStreamDeltaFlushCount: 0,
        modelStreamRequestCount: 0,
        rawModelStreamDeltaCount: 0,
        rawModelStreamEventCount: 0
    };
    const pendingRuntimeCommands = new Map();
    let hostToolRequestGeneration = 0;
    let nextRuntimeCommandId = 0;
    let disposed = false;
    let snapshot = null;
    const emit = (event) => {
        if (disposed) {
            return;
        }
        if (event.kind === AgentEventKind.SnapshotUpdated) {
            snapshot = event.snapshot;
        }
        for (const listener of listeners) {
            listener(event);
        }
    };
    const abortActiveHostToolRequests = () => {
        // Bumping the generation is the canonical detachment point: any client
        // request whose handler resolves after this returns belongs to a stale
        // generation and its result must not be posted back to the worker.
        hostToolRequestGeneration += 1;
        for (const controller of activeHostToolRequestControllers.values()) {
            controller.abort();
        }
        activeHostToolRequestControllers.clear();
    };
    const isHostToolRequestActive = (requestId, generation) => generation === hostToolRequestGeneration && activeHostToolRequestControllers.has(requestId);
    const isModelStreamEventTerminal = (event) => event.kind === ShortcutXLModelStreamEventKind.Done ||
        event.kind === ShortcutXLModelStreamEventKind.Error;
    const shouldForwardModelStreamEvent = (requestId, event, forwardOptions) => {
        const activeRequest = activeModelStreams.get(requestId);
        if (!activeRequest || activeRequest.terminalForwarded) {
            return false;
        }
        return (!activeRequest.cancelled ||
            (forwardOptions?.allowCancelledError === true &&
                event.kind === ShortcutXLModelStreamEventKind.Error));
    };
    const markModelStreamCancelled = (requestId) => {
        const activeRequest = activeModelStreams.get(requestId);
        if (!activeRequest) {
            return undefined;
        }
        if (!activeRequest.cancelled) {
            modelStreamBridgeMetrics.modelStreamCancelCount += 1;
            activeRequest.cancelled = true;
        }
        return activeRequest;
    };
    const recordRawModelStreamEvent = (event) => {
        modelStreamBridgeMetrics.rawModelStreamEventCount += 1;
        if (isCoalescibleModelStreamEvent(event)) {
            modelStreamBridgeMetrics.rawModelStreamDeltaCount += 1;
        }
    };
    const recordForwardedModelStreamEvent = (event) => {
        modelStreamBridgeMetrics.forwardedModelStreamEventCount += 1;
        if (isCoalescibleModelStreamEvent(event)) {
            modelStreamBridgeMetrics.forwardedModelStreamDeltaCount += 1;
        }
    };
    const postModelStreamEvent = (requestId, event, forwardOptions) => {
        if (disposed || !shouldForwardModelStreamEvent(requestId, event, forwardOptions)) {
            return;
        }
        recordForwardedModelStreamEvent(event);
        const activeRequest = activeModelStreams.get(requestId);
        if (activeRequest && isModelStreamEventTerminal(event)) {
            activeRequest.terminalForwarded = true;
        }
        options.onModelStreamTrace?.({
            eventKind: event.kind,
            kind: ShortcutXLModelStreamTraceKind.EventSent,
            requestId,
            timestamp: Date.now()
        });
        port.postMessage({
            kind: WorkerMessageKind.ModelStreamEvent,
            requestId,
            event
        });
    };
    const postGeneratedModelStreamError = (request, error) => {
        const activeRequest = activeModelStreams.get(request.requestId);
        const reason = activeRequest?.cancelled
            ? ShortcutXLModelStreamStopReason.Aborted
            : ShortcutXLModelStreamStopReason.Error;
        const errorEvent = createModelStreamErrorEvent(request, error, reason);
        modelStreamBridgeMetrics.generatedModelStreamErrorCount += 1;
        postModelStreamEvent(request.requestId, errorEvent, { allowCancelledError: true });
    };
    const rejectPendingRuntimeCommands = (error) => {
        for (const pendingCommand of pendingRuntimeCommands.values()) {
            pendingCommand.reject(error);
        }
        pendingRuntimeCommands.clear();
    };
    const postRuntimeCommand = (command, options) => {
        if (disposed) {
            return Promise.reject(new Error('ShortcutXL worker runtime client was disposed.'));
        }
        const commandId = `runtime-command-${++nextRuntimeCommandId}`;
        if (!options?.awaitAck) {
            try {
                port.postMessage({
                    kind: WorkerMessageKind.AgentCommand,
                    commandId,
                    command,
                    ...(options?.fatalOnError === false ? { fatalOnError: false } : {})
                });
                return Promise.resolve();
            }
            catch (error) {
                return Promise.reject(error instanceof Error ? error : new Error(String(error)));
            }
        }
        return new Promise((resolve, reject) => {
            pendingRuntimeCommands.set(commandId, { resolve, reject });
            try {
                port.postMessage({
                    kind: WorkerMessageKind.AgentCommand,
                    commandId,
                    command,
                    ...(options?.fatalOnError === false ? { fatalOnError: false } : {})
                });
            }
            catch (error) {
                pendingRuntimeCommands.delete(commandId);
                reject(error instanceof Error ? error : new Error(String(error)));
            }
        });
    };
    const listener = (event) => {
        if (disposed) {
            return;
        }
        if (!isWorkerMessage(event.data, registry)) {
            return;
        }
        const message = event.data;
        switch (message.kind) {
            case WorkerMessageKind.AgentCommandAck: {
                const pendingCommand = pendingRuntimeCommands.get(message.commandId);
                if (!pendingCommand) {
                    return;
                }
                pendingRuntimeCommands.delete(message.commandId);
                if (message.ok) {
                    pendingCommand.resolve();
                    return;
                }
                pendingCommand.reject(new Error(message.error ?? `Runtime command ${message.commandId} failed.`));
                return;
            }
            case WorkerMessageKind.AgentEvent:
                emit(message.event);
                return;
            case WorkerMessageKind.HostToolRequest: {
                const requestController = new AbortController();
                const requestGenerationAtStart = hostToolRequestGeneration;
                options.onModelStreamTrace?.({
                    kind: ShortcutXLModelStreamTraceKind.HostToolRequestReceived,
                    message: message.request.kind,
                    requestId: message.request.requestId,
                    timestamp: Date.now()
                });
                activeHostToolRequestControllers.set(message.request.requestId, requestController);
                void options.hostToolExecutor
                    .executeHostTool(message.request, {
                    signal: requestController.signal
                })
                    .catch((error) => createFailedHostToolResult(message.request.kind, message.request.requestId, errorToMessage(error)))
                    .then((result) => {
                    // Abort invariant: results that resolve after a dispose or after
                    // `abort()` invalidates the generation are dropped (logged as
                    // "suppressed") rather than posted back to the worker.
                    if (disposed ||
                        !isHostToolRequestActive(message.request.requestId, requestGenerationAtStart)) {
                        options.onModelStreamTrace?.({
                            kind: ShortcutXLModelStreamTraceKind.HostToolResultSuppressed,
                            message: result.status,
                            requestId: message.request.requestId,
                            timestamp: Date.now()
                        });
                        return;
                    }
                    const transportResult = registry.normalizeHostToolResult(result);
                    port.postMessage({
                        kind: WorkerMessageKind.HostToolResult,
                        result: transportResult
                    });
                    options.onModelStreamTrace?.({
                        kind: ShortcutXLModelStreamTraceKind.HostToolResultPosted,
                        message: transportResult.status,
                        requestId: message.request.requestId,
                        timestamp: Date.now()
                    });
                })
                    .finally(() => {
                    if (activeHostToolRequestControllers.get(message.request.requestId) === requestController) {
                        activeHostToolRequestControllers.delete(message.request.requestId);
                    }
                });
                return;
            }
            case WorkerMessageKind.ModelStreamRequest: {
                const streamController = new AbortController();
                const streamRequest = message.aiRequest;
                modelStreamBridgeMetrics.modelStreamRequestCount += 1;
                modelStreamBridgeMetrics.maxActiveModelStreams = Math.max(modelStreamBridgeMetrics.maxActiveModelStreams, activeModelStreams.size + 1);
                const streamCoalescer = createModelStreamEventCoalescer({
                    emitNow: (streamEvent) => {
                        postModelStreamEvent(streamRequest.requestId, streamEvent);
                    },
                    onDeltaFlushed: () => {
                        modelStreamBridgeMetrics.modelStreamDeltaFlushCount += 1;
                    },
                    onDeltaMerged: () => {
                        modelStreamBridgeMetrics.coalescedModelStreamDeltaCount += 1;
                    }
                });
                activeModelStreams.set(streamRequest.requestId, {
                    cancelled: false,
                    coalescer: streamCoalescer,
                    controller: streamController,
                    terminalForwarded: false
                });
                options.onModelStreamTrace?.({
                    kind: ShortcutXLModelStreamTraceKind.RequestReceived,
                    requestId: streamRequest.requestId,
                    timestamp: Date.now()
                });
                void (options.modelStreamRequestHandler?.handleRequest(streamRequest, (streamEvent) => {
                    if (disposed ||
                        !shouldForwardModelStreamEvent(streamRequest.requestId, streamEvent)) {
                        return;
                    }
                    recordRawModelStreamEvent(streamEvent);
                    streamCoalescer.emit(streamEvent);
                }, streamController.signal) ?? Promise.reject(new Error('ShortcutXL host runtime has no model stream handler.')))
                    .then(() => {
                    const activeRequest = activeModelStreams.get(streamRequest.requestId);
                    if (activeRequest?.cancelled && !activeRequest.terminalForwarded) {
                        postGeneratedModelStreamError(streamRequest, new Error('ShortcutXL runtime stream was aborted.'));
                    }
                    else {
                        streamCoalescer.flush();
                    }
                    options.onModelStreamTrace?.({
                        kind: ShortcutXLModelStreamTraceKind.HandlerCompleted,
                        requestId: streamRequest.requestId,
                        timestamp: Date.now()
                    });
                })
                    .catch((error) => {
                    if (disposed) {
                        return;
                    }
                    const activeRequest = activeModelStreams.get(streamRequest.requestId);
                    if (!activeRequest || activeRequest.terminalForwarded) {
                        return;
                    }
                    if (activeRequest.cancelled) {
                        streamCoalescer.dispose();
                    }
                    else {
                        streamCoalescer.flush();
                    }
                    options.onModelStreamTrace?.({
                        kind: ShortcutXLModelStreamTraceKind.HandlerFailed,
                        message: errorToMessage(error),
                        requestId: streamRequest.requestId,
                        timestamp: Date.now()
                    });
                    postGeneratedModelStreamError(streamRequest, error);
                })
                    .finally(() => {
                    activeModelStreams.delete(streamRequest.requestId);
                    streamCoalescer.dispose();
                });
                return;
            }
            case WorkerMessageKind.ModelStreamCancel:
                {
                    const activeRequest = markModelStreamCancelled(message.requestId);
                    if (activeRequest) {
                        activeRequest.coalescer.dispose();
                        activeRequest.controller.abort();
                    }
                }
                options.onModelStreamTrace?.({
                    kind: ShortcutXLModelStreamTraceKind.CancelReceived,
                    requestId: message.requestId,
                    timestamp: Date.now()
                });
                return;
            case WorkerMessageKind.HostToolResult:
            case WorkerMessageKind.ModelStreamEvent:
            case WorkerMessageKind.AgentCommand:
                return;
        }
    };
    port.addEventListener('message', listener);
    port.start?.();
    return {
        abort() {
            if (disposed) {
                return Promise.resolve();
            }
            abortActiveHostToolRequests();
            for (const requestId of activeModelStreams.keys()) {
                const activeRequest = markModelStreamCancelled(requestId);
                if (!activeRequest) {
                    continue;
                }
                activeRequest.coalescer.dispose();
                activeRequest.controller.abort();
            }
            return postRuntimeCommand({
                kind: AgentCommandKind.Abort
            }, { awaitAck: true });
        },
        configure(configuration) {
            if (disposed) {
                throw new Error('ShortcutXL worker runtime client was disposed.');
            }
            return postRuntimeCommand({
                kind: AgentCommandKind.Configure,
                configuration
            }, { awaitAck: true });
        },
        dispose() {
            disposed = true;
            try {
                port.postMessage({
                    kind: WorkerMessageKind.AgentCommand,
                    commandId: `runtime-command-${++nextRuntimeCommandId}`,
                    command: {
                        kind: AgentCommandKind.Dispose
                    }
                });
            }
            catch {
                // Disposal is best-effort; teardown below must still release listeners
                // and reject command waiters owned by this client.
            }
            abortActiveHostToolRequests();
            for (const requestId of activeModelStreams.keys()) {
                const activeRequest = markModelStreamCancelled(requestId);
                if (!activeRequest) {
                    continue;
                }
                activeRequest.controller.abort();
                activeRequest.coalescer.dispose();
            }
            activeModelStreams.clear();
            rejectPendingRuntimeCommands(new Error('ShortcutXL worker runtime client disposed before command acknowledgement.'));
            port.removeEventListener('message', listener);
            listeners.clear();
        },
        getModelStreamBridgeMetrics() {
            return {
                ...modelStreamBridgeMetrics,
                activeModelStreams: activeModelStreams.size,
                coalescingWindowMs: MODEL_STREAM_DELTA_FLUSH_MS
            };
        },
        getSnapshot() {
            return snapshot;
        },
        submit(prompt, submitOptions, options) {
            if (disposed) {
                return Promise.reject(new Error('ShortcutXL worker runtime client was disposed.'));
            }
            return postRuntimeCommand({
                kind: AgentCommandKind.SubmitPrompt,
                prompt,
                submitOptions
            }, options?.awaitAck === true ? { awaitAck: true, fatalOnError: false } : undefined);
        },
        subscribe(runtimeEventListener) {
            listeners.add(runtimeEventListener);
            return () => {
                listeners.delete(runtimeEventListener);
            };
        }
    };
}
//# sourceMappingURL=host-client.js.map