/**
 * Runtime controller — the worker-side translator between `AgentCommand`s
 * coming from the main thread and the embedded `EmbeddedAgentRuntime`.
 * Owns per-request `AbortController`s and the request-generation counter
 * that detaches late host tool results after an abort.
 */
import { AgentCommandKind, AgentEventKind, AgentProtocolErrorKind } from '../../contracts/agent-api.js';
import { createFailedHostToolResult, errorToMessage } from '../../contracts/host-tool-api.js';
function createRuntimeError(kind, message, requestId) {
    return {
        kind: AgentEventKind.FatalError,
        error: {
            kind,
            message,
            requestId
        }
    };
}
/** Build a controller bound to one runtime + client adapter pair. */
export function createEmbeddedAgentDispatch(options) {
    const listeners = new Set();
    const activeRequests = new Map();
    let requestGeneration = 0;
    const emit = (event) => {
        for (const listener of listeners) {
            listener(event);
        }
    };
    const isRequestActive = (requestId, generation) => generation === requestGeneration && activeRequests.has(requestId);
    const clearActiveRequests = () => {
        for (const controller of activeRequests.values()) {
            controller.abort();
        }
        activeRequests.clear();
    };
    const handleHostToolRequest = async (request, generation, abortController) => {
        let result;
        try {
            result = await options.hostToolExecutor.executeHostTool(request, {
                signal: abortController.signal
            });
        }
        catch (error) {
            result = createFailedHostToolResult(request.kind, request.requestId, errorToMessage(error));
        }
        // Abort invariant: a result that finishes after `Abort` (which bumps
        // `requestGeneration`) is silently dropped instead of being plumbed back
        // into the runtime as if the turn were still live. The bridge itself is
        // also idempotent for late/duplicate/unknown ids, so this generation
        // check is belt-and-suspenders for the *event-emission* side: it stops
        // us from emitting a `HostToolResultApplied` event for a stale request.
        if (!isRequestActive(request.requestId, generation)) {
            return;
        }
        await options.runtime.resumeWithHostToolResult(result);
        if (!isRequestActive(request.requestId, generation)) {
            return;
        }
        emit({
            kind: AgentEventKind.HostToolResultApplied,
            result
        });
    };
    const runtimeUnsubscribe = options.runtime.subscribe((event) => {
        emit(event);
        if (event.kind !== AgentEventKind.HostToolRequest) {
            return;
        }
        const { requestId } = event.request;
        if (activeRequests.has(requestId)) {
            emit(createRuntimeError(AgentProtocolErrorKind.ControllerProtocol, `Duplicate active host tool request: ${requestId}`, requestId));
            return;
        }
        const abortController = new AbortController();
        activeRequests.set(requestId, abortController);
        const requestGenerationAtStart = requestGeneration;
        void handleHostToolRequest(event.request, requestGenerationAtStart, abortController).finally(() => {
            activeRequests.delete(requestId);
        });
    });
    const controller = {
        async handleCommand(command) {
            switch (command.kind) {
                case AgentCommandKind.Configure:
                    await options.runtime.configure(command.configuration);
                    return;
                case AgentCommandKind.SubmitPrompt:
                    await options.runtime.submit(command.prompt, command.submitOptions);
                    return;
                case AgentCommandKind.Abort:
                    // Bumping the generation is the canonical detachment point: any
                    // in-flight host tool request belongs to a stale generation now and
                    // its eventual result cannot resume the runtime.
                    requestGeneration += 1;
                    clearActiveRequests();
                    await options.runtime.abort();
                    return;
                case AgentCommandKind.Dispose:
                    controller.dispose();
                    return;
            }
        },
        subscribe(listener) {
            listeners.add(listener);
            return () => {
                listeners.delete(listener);
            };
        },
        getSnapshot() {
            return options.runtime.getSnapshot();
        },
        dispose() {
            requestGeneration += 1;
            runtimeUnsubscribe();
            listeners.clear();
            clearActiveRequests();
            options.runtime.dispose();
        }
    };
    return controller;
}
//# sourceMappingURL=dispatch.js.map