"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIService = void 0;
exports.preloadAISDKProviders = preloadAISDKProviders;
const fs = __importStar(require("fs/promises"));
const os = __importStar(require("os"));
const events_1 = require("events");
const ai_1 = require("ai");
const applyToolOutputRedaction_1 = require("../utils/messages/applyToolOutputRedaction");
const sanitizeToolInput_1 = require("../utils/messages/sanitizeToolInput");
const result_1 = require("../types/result");
const message_1 = require("../types/message");
const streamManager_1 = require("./streamManager");
const tools_1 = require("../utils/tools/tools");
const runtimeFactory_1 = require("../runtime/runtimeFactory");
const secrets_1 = require("../types/secrets");
const log_1 = require("./log");
const modelMessageTransform_1 = require("../utils/messages/modelMessageTransform");
const cacheStrategy_1 = require("../utils/ai/cacheStrategy");
const systemMessage_1 = require("./systemMessage");
const tokenizer_1 = require("../utils/main/tokenizer");
const providerOptions_1 = require("../utils/ai/providerOptions");
const toolPolicy_1 = require("../utils/tools/toolPolicy");
const mockScenarioPlayer_1 = require("./mock/mockScenarioPlayer");
const undici_1 = require("undici");
// Export a standalone version of getToolsForModel for use in backend
// Create undici agent with unlimited timeouts for AI streaming requests.
// Safe because users control cancellation via AbortSignal from the UI.
const unlimitedTimeoutAgent = new undici_1.Agent({
    bodyTimeout: 0, // No timeout - prevents BodyTimeoutError on long reasoning pauses
    headersTimeout: 0, // No timeout for headers
});
/**
 * Default fetch function with unlimited timeouts for AI streaming.
 * Uses undici Agent to remove artificial timeout limits while still
 * respecting user cancellation via AbortSignal.
 *
 * Note: If users provide custom fetch in providers.jsonc, they are
 * responsible for configuring timeouts appropriately. Custom fetch
 * implementations using undici should set bodyTimeout: 0 and
 * headersTimeout: 0 to prevent BodyTimeoutError on long-running
 * reasoning models.
 */
const defaultFetchWithUnlimitedTimeout = (async (input, init) => {
    const requestInit = {
        ...(init ?? {}),
        dispatcher: unlimitedTimeoutAgent,
    };
    return fetch(input, requestInit);
});
const globalFetchWithExtras = fetch;
const defaultFetchWithExtras = defaultFetchWithUnlimitedTimeout;
if (typeof globalFetchWithExtras.preconnect === "function") {
    defaultFetchWithExtras.preconnect = globalFetchWithExtras.preconnect.bind(globalFetchWithExtras);
}
if (typeof globalFetchWithExtras.certificate === "function") {
    defaultFetchWithExtras.certificate =
        globalFetchWithExtras.certificate.bind(globalFetchWithExtras);
}
/**
 * Preload AI SDK provider modules to avoid race conditions in concurrent test environments.
 * This function loads @ai-sdk/anthropic and @ai-sdk/openai eagerly so that subsequent
 * dynamic imports in createModel() hit the module cache instead of racing.
 *
 * In production, providers are lazy-loaded on first use to optimize startup time.
 * In tests, we preload them once during setup to ensure reliable concurrent execution.
 */
async function preloadAISDKProviders() {
    // Preload providers to ensure they're in the module cache before concurrent tests run
    await Promise.all([Promise.resolve().then(() => __importStar(require("@ai-sdk/anthropic"))), Promise.resolve().then(() => __importStar(require("@ai-sdk/openai")))]);
}
class AIService extends events_1.EventEmitter {
    streamManager;
    historyService;
    partialService;
    config;
    initStateManager;
    mockModeEnabled;
    mockScenarioPlayer;
    constructor(config, historyService, partialService, initStateManager) {
        super();
        // Increase max listeners to accommodate multiple concurrent workspace listeners
        // Each workspace subscribes to stream events, and we expect >10 concurrent workspaces
        this.setMaxListeners(50);
        this.config = config;
        this.historyService = historyService;
        this.partialService = partialService;
        this.initStateManager = initStateManager;
        this.streamManager = new streamManager_1.StreamManager(historyService, partialService);
        void this.ensureSessionsDir();
        this.setupStreamEventForwarding();
        this.mockModeEnabled = process.env.CMUX_MOCK_AI === "1";
        if (this.mockModeEnabled) {
            log_1.log.info("AIService running in CMUX_MOCK_AI mode");
            this.mockScenarioPlayer = new mockScenarioPlayer_1.MockScenarioPlayer({
                aiService: this,
                historyService,
            });
        }
    }
    /**
     * Forward all stream events from StreamManager to AIService consumers
     */
    setupStreamEventForwarding() {
        this.streamManager.on("stream-start", (data) => this.emit("stream-start", data));
        this.streamManager.on("stream-delta", (data) => this.emit("stream-delta", data));
        this.streamManager.on("stream-end", (data) => this.emit("stream-end", data));
        // Handle stream-abort: commit partial to history before forwarding
        // Note: If abandonPartial option was used, partial is already deleted by IPC handler
        this.streamManager.on("stream-abort", (data) => {
            void (async () => {
                // Check if partial still exists (not abandoned)
                const partial = await this.partialService.readPartial(data.workspaceId);
                if (partial) {
                    // Commit interrupted message to history with partial:true metadata
                    // This ensures /clear and /truncate can clean up interrupted messages
                    await this.partialService.commitToHistory(data.workspaceId);
                    await this.partialService.deletePartial(data.workspaceId);
                }
                // Forward abort event to consumers
                this.emit("stream-abort", data);
            })();
        });
        this.streamManager.on("error", (data) => this.emit("error", data));
        // Forward tool events
        this.streamManager.on("tool-call-start", (data) => this.emit("tool-call-start", data));
        this.streamManager.on("tool-call-delta", (data) => this.emit("tool-call-delta", data));
        this.streamManager.on("tool-call-end", (data) => this.emit("tool-call-end", data));
        // Forward reasoning events
        this.streamManager.on("reasoning-delta", (data) => this.emit("reasoning-delta", data));
        this.streamManager.on("reasoning-end", (data) => this.emit("reasoning-end", data));
    }
    async ensureSessionsDir() {
        try {
            await fs.mkdir(this.config.sessionsDir, { recursive: true });
        }
        catch (error) {
            log_1.log.error("Failed to create sessions directory:", error);
        }
    }
    isMockModeEnabled() {
        return this.mockModeEnabled;
    }
    getWorkspaceMetadata(workspaceId) {
        try {
            // Read from config.json (single source of truth)
            // getAllWorkspaceMetadata() handles migration from legacy metadata.json files
            const allMetadata = this.config.getAllWorkspaceMetadata();
            const metadata = allMetadata.find((m) => m.id === workspaceId);
            if (!metadata) {
                return (0, result_1.Err)(`Workspace metadata not found for ${workspaceId}. Workspace may not be properly initialized.`);
            }
            return (0, result_1.Ok)(metadata);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            return (0, result_1.Err)(`Failed to read workspace metadata: ${message}`);
        }
    }
    /**
     * Split assistant messages that have text after tool calls with results.
  
    /**
     * Create an AI SDK model from a model string (e.g., "anthropic:claude-opus-4-1")
     *
     * IMPORTANT: We ONLY use providers.jsonc as the single source of truth for provider configuration.
     * We DO NOT use environment variables or default constructors that might read them.
     * This ensures consistent, predictable configuration management.
     *
     * Provider configuration from providers.jsonc is passed verbatim to the provider
     * constructor, ensuring automatic parity with Vercel AI SDK - any configuration options
     * supported by the provider will work without modification.
     */
    async createModel(modelString, cmuxProviderOptions) {
        try {
            // Parse model string (format: "provider:model-id")
            const [providerName, modelId] = modelString.split(":");
            if (!providerName || !modelId) {
                return (0, result_1.Err)({
                    type: "invalid_model_string",
                    message: `Invalid model string format: "${modelString}". Expected "provider:model-id"`,
                });
            }
            // Load providers configuration - the ONLY source of truth
            const providersConfig = this.config.loadProvidersConfig();
            const providerConfig = providersConfig?.[providerName] ?? {};
            // Handle Anthropic provider
            if (providerName === "anthropic") {
                // Check for API key in config
                if (!providerConfig.apiKey) {
                    return (0, result_1.Err)({
                        type: "api_key_not_found",
                        provider: providerName,
                    });
                }
                // Add 1M context beta header if requested
                const use1MContext = cmuxProviderOptions?.anthropic?.use1MContext;
                const existingHeaders = providerConfig.headers;
                const headers = use1MContext && existingHeaders
                    ? { ...existingHeaders, "anthropic-beta": "context-1m-2025-08-07" }
                    : use1MContext
                        ? { "anthropic-beta": "context-1m-2025-08-07" }
                        : existingHeaders;
                // Lazy-load Anthropic provider to reduce startup time
                const { createAnthropic } = await Promise.resolve().then(() => __importStar(require("@ai-sdk/anthropic")));
                const provider = createAnthropic({ ...providerConfig, headers });
                return (0, result_1.Ok)(provider(modelId));
            }
            // Handle OpenAI provider (using Responses API)
            if (providerName === "openai") {
                if (!providerConfig.apiKey) {
                    return (0, result_1.Err)({
                        type: "api_key_not_found",
                        provider: providerName,
                    });
                }
                // Use custom fetch if provided, otherwise default with unlimited timeout
                const baseFetch = typeof providerConfig.fetch === "function"
                    ? providerConfig.fetch
                    : defaultFetchWithUnlimitedTimeout;
                // Wrap fetch to force truncation: "auto" for OpenAI Responses API calls.
                // This is a temporary override until @ai-sdk/openai supports passing
                // truncation via providerOptions. Safe because it only targets the
                // OpenAI Responses endpoint and leaves other providers untouched.
                // Can be disabled via cmuxProviderOptions for testing purposes.
                const disableAutoTruncation = cmuxProviderOptions?.openai?.disableAutoTruncation ?? false;
                const fetchWithOpenAITruncation = Object.assign(async (input, init) => {
                    try {
                        const urlString = (() => {
                            if (typeof input === "string") {
                                return input;
                            }
                            if (input instanceof URL) {
                                return input.toString();
                            }
                            if (typeof input === "object" && input !== null && "url" in input) {
                                const possibleUrl = input.url;
                                if (typeof possibleUrl === "string") {
                                    return possibleUrl;
                                }
                            }
                            return "";
                        })();
                        const method = (init?.method ?? "GET").toUpperCase();
                        const isOpenAIResponses = /\/v1\/responses(\?|$)/.test(urlString);
                        const body = init?.body;
                        if (!disableAutoTruncation &&
                            isOpenAIResponses &&
                            method === "POST" &&
                            typeof body === "string") {
                            // Clone headers to avoid mutating caller-provided objects
                            const headers = new Headers(init?.headers);
                            // Remove content-length if present, since body will change
                            headers.delete("content-length");
                            try {
                                const json = JSON.parse(body);
                                // Only set if not already present
                                if (json.truncation === undefined) {
                                    json.truncation = "auto";
                                }
                                const newBody = JSON.stringify(json);
                                const newInit = { ...init, headers, body: newBody };
                                return baseFetch(input, newInit);
                            }
                            catch {
                                // If body isn't JSON, fall through to normal fetch
                                return baseFetch(input, init);
                            }
                        }
                        // Default passthrough
                        return baseFetch(input, init);
                    }
                    catch {
                        // On any unexpected error, fall back to original fetch
                        return baseFetch(input, init);
                    }
                }, "preconnect" in baseFetch && typeof baseFetch.preconnect === "function"
                    ? {
                        preconnect: baseFetch.preconnect.bind(baseFetch),
                    }
                    : {});
                // Lazy-load OpenAI provider to reduce startup time
                const { createOpenAI } = await Promise.resolve().then(() => __importStar(require("@ai-sdk/openai")));
                const provider = createOpenAI({
                    ...providerConfig,
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
                    fetch: fetchWithOpenAITruncation,
                });
                // Use Responses API for persistence and built-in tools
                // OpenAI manages reasoning state via previousResponseId - no middleware needed
                const model = provider.responses(modelId);
                return (0, result_1.Ok)(model);
            }
            return (0, result_1.Err)({
                type: "provider_not_supported",
                provider: providerName,
            });
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return (0, result_1.Err)({ type: "unknown", raw: `Failed to create model: ${errorMessage}` });
        }
    }
    /**
     * Stream a message conversation to the AI model
     * @param messages Array of conversation messages
     * @param workspaceId Unique identifier for the workspace
     * @param modelString Model string (e.g., "anthropic:claude-opus-4-1") - required from frontend
     * @param thinkingLevel Optional thinking/reasoning level for AI models
     * @param toolPolicy Optional policy to filter available tools
     * @param abortSignal Optional signal to abort the stream
     * @param additionalSystemInstructions Optional additional system instructions to append
     * @param maxOutputTokens Optional maximum tokens for model output
     * @param cmuxProviderOptions Optional provider-specific options
     * @param mode Optional mode name - affects system message via Mode: sections in AGENTS.md
     * @returns Promise that resolves when streaming completes or fails
     */
    async streamMessage(messages, workspaceId, modelString, thinkingLevel, toolPolicy, abortSignal, additionalSystemInstructions, maxOutputTokens, cmuxProviderOptions, mode) {
        try {
            if (this.mockModeEnabled && this.mockScenarioPlayer) {
                return await this.mockScenarioPlayer.play(messages, workspaceId);
            }
            // DEBUG: Log streamMessage call
            const lastMessage = messages[messages.length - 1];
            log_1.log.debug(`[STREAM MESSAGE] workspaceId=${workspaceId} messageCount=${messages.length} lastRole=${lastMessage?.role}`);
            // Before starting a new stream, commit any existing partial to history
            // This is idempotent - won't double-commit if already in chat.jsonl
            await this.partialService.commitToHistory(workspaceId);
            // Create model instance with early API key validation
            const modelResult = await this.createModel(modelString, cmuxProviderOptions);
            if (!modelResult.success) {
                return (0, result_1.Err)(modelResult.error);
            }
            // Dump original messages for debugging
            log_1.log.debug_obj(`${workspaceId}/1_original_messages.json`, messages);
            // Extract provider name from modelString (e.g., "anthropic:claude-opus-4-1" -> "anthropic")
            const [providerName] = modelString.split(":");
            // Get tool names early for mode transition sentinel (stub config, no workspace context needed)
            const earlyRuntime = (0, runtimeFactory_1.createRuntime)({ type: "local", srcBaseDir: process.cwd() });
            const earlyAllTools = await (0, tools_1.getToolsForModel)(modelString, {
                cwd: process.cwd(),
                runtime: earlyRuntime,
                runtimeTempDir: os.tmpdir(),
                secrets: {},
            }, "", // Empty workspace ID for early stub config
            this.initStateManager);
            const earlyTools = (0, toolPolicy_1.applyToolPolicy)(earlyAllTools, toolPolicy);
            const toolNamesForSentinel = Object.keys(earlyTools);
            // Filter out assistant messages with only reasoning (no text/tools)
            // EXCEPTION: When extended thinking is enabled, preserve reasoning-only messages
            // to comply with Extended Thinking API requirements
            const preserveReasoningOnly = Boolean(thinkingLevel);
            const filteredMessages = (0, modelMessageTransform_1.filterEmptyAssistantMessages)(messages, preserveReasoningOnly);
            log_1.log.debug(`Filtered ${messages.length - filteredMessages.length} empty assistant messages`);
            log_1.log.debug_obj(`${workspaceId}/1a_filtered_messages.json`, filteredMessages);
            // OpenAI-specific: Keep reasoning parts in history
            // OpenAI manages conversation state via previousResponseId
            if (providerName === "openai") {
                log_1.log.debug("Keeping reasoning parts for OpenAI (managed via previousResponseId)");
            }
            // Add [CONTINUE] sentinel to partial messages (for model context)
            const messagesWithSentinel = (0, modelMessageTransform_1.addInterruptedSentinel)(filteredMessages);
            // Inject mode transition context if mode changed from last assistant message
            // Include tool names so model knows what tools are available in the new mode
            const messagesWithModeContext = (0, modelMessageTransform_1.injectModeTransition)(messagesWithSentinel, mode, toolNamesForSentinel);
            // Apply centralized tool-output redaction BEFORE converting to provider ModelMessages
            // This keeps the persisted/UI history intact while trimming heavy fields for the request
            const redactedForProvider = (0, applyToolOutputRedaction_1.applyToolOutputRedaction)(messagesWithModeContext);
            log_1.log.debug_obj(`${workspaceId}/2a_redacted_messages.json`, redactedForProvider);
            // Sanitize tool inputs to ensure they are valid objects (not strings or arrays)
            // This fixes cases where corrupted data in history has malformed tool inputs
            // that would cause API errors like "Input should be a valid dictionary"
            const sanitizedMessages = (0, sanitizeToolInput_1.sanitizeToolInputs)(redactedForProvider);
            log_1.log.debug_obj(`${workspaceId}/2b_sanitized_messages.json`, sanitizedMessages);
            // Convert CmuxMessage to ModelMessage format using Vercel AI SDK utility
            // Type assertion needed because CmuxMessage has custom tool parts for interrupted tools
            // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
            const modelMessages = (0, ai_1.convertToModelMessages)(sanitizedMessages);
            log_1.log.debug_obj(`${workspaceId}/2_model_messages.json`, modelMessages);
            // Apply ModelMessage transforms based on provider requirements
            const transformedMessages = (0, modelMessageTransform_1.transformModelMessages)(modelMessages, providerName);
            // Apply cache control for Anthropic models AFTER transformation
            const finalMessages = (0, cacheStrategy_1.applyCacheControl)(transformedMessages, modelString);
            log_1.log.debug_obj(`${workspaceId}/3_final_messages.json`, finalMessages);
            // Validate the messages meet Anthropic requirements (Anthropic only)
            if (providerName === "anthropic") {
                const validation = (0, modelMessageTransform_1.validateAnthropicCompliance)(finalMessages);
                if (!validation.valid) {
                    log_1.log.error(`Anthropic compliance validation failed: ${validation.error ?? "unknown error"}`);
                    // Continue anyway, as the API might be more lenient
                }
            }
            // Get workspace metadata to retrieve workspace path
            const metadataResult = this.getWorkspaceMetadata(workspaceId);
            if (!metadataResult.success) {
                return (0, result_1.Err)({ type: "unknown", raw: metadataResult.error });
            }
            const metadata = metadataResult.data;
            // Get actual workspace path from config (handles both legacy and new format)
            const workspace = this.config.findWorkspace(workspaceId);
            if (!workspace) {
                return (0, result_1.Err)({ type: "unknown", raw: `Workspace ${workspaceId} not found in config` });
            }
            // Get workspace path - handle both worktree and in-place modes
            const runtime = (0, runtimeFactory_1.createRuntime)(metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir });
            // In-place workspaces (CLI/benchmarks) have projectPath === name
            // Use path directly instead of reconstructing via getWorkspacePath
            const isInPlace = metadata.projectPath === metadata.name;
            const workspacePath = isInPlace
                ? metadata.projectPath
                : runtime.getWorkspacePath(metadata.projectPath, metadata.name);
            // Build system message from workspace metadata
            const systemMessage = await (0, systemMessage_1.buildSystemMessage)(metadata, runtime, workspacePath, mode, additionalSystemInstructions);
            // Count system message tokens for cost tracking
            const tokenizer = (0, tokenizer_1.getTokenizerForModel)(modelString);
            const systemMessageTokens = tokenizer.countTokens(systemMessage);
            // Load project secrets
            const projectSecrets = this.config.getProjectSecrets(metadata.projectPath);
            // Generate stream token and create temp directory for tools
            const streamToken = this.streamManager.generateStreamToken();
            const runtimeTempDir = await this.streamManager.createTempDirForStream(streamToken, runtime);
            // Get model-specific tools with workspace path (correct for local or remote)
            const allTools = await (0, tools_1.getToolsForModel)(modelString, {
                cwd: workspacePath,
                runtime,
                secrets: (0, secrets_1.secretsToRecord)(projectSecrets),
                runtimeTempDir,
            }, workspaceId, this.initStateManager);
            // Apply tool policy to filter tools (if policy provided)
            const tools = (0, toolPolicy_1.applyToolPolicy)(allTools, toolPolicy);
            log_1.log.info("AIService.streamMessage: tool configuration", {
                workspaceId,
                model: modelString,
                toolNames: Object.keys(tools),
                hasToolPolicy: Boolean(toolPolicy),
            });
            // Create assistant message placeholder with historySequence from backend
            const assistantMessageId = `assistant-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
            const assistantMessage = (0, message_1.createCmuxMessage)(assistantMessageId, "assistant", "", {
                timestamp: Date.now(),
                model: modelString,
                systemMessageTokens,
                mode, // Track the mode for this assistant response
            });
            // Append to history to get historySequence assigned
            const appendResult = await this.historyService.appendToHistory(workspaceId, assistantMessage);
            if (!appendResult.success) {
                return (0, result_1.Err)({ type: "unknown", raw: appendResult.error });
            }
            // Get the assigned historySequence
            const historySequence = assistantMessage.metadata?.historySequence ?? 0;
            const forceContextLimitError = modelString.startsWith("openai:") &&
                cmuxProviderOptions?.openai?.forceContextLimitError === true;
            const simulateToolPolicyNoop = modelString.startsWith("openai:") &&
                cmuxProviderOptions?.openai?.simulateToolPolicyNoop === true;
            if (forceContextLimitError) {
                const errorMessage = "Context length exceeded: the conversation is too long to send to this OpenAI model. Please shorten the history and try again.";
                const errorPartialMessage = {
                    id: assistantMessageId,
                    role: "assistant",
                    metadata: {
                        historySequence,
                        timestamp: Date.now(),
                        model: modelString,
                        systemMessageTokens,
                        partial: true,
                        error: errorMessage,
                        errorType: "context_exceeded",
                    },
                    parts: [],
                };
                await this.partialService.writePartial(workspaceId, errorPartialMessage);
                const streamStartEvent = {
                    type: "stream-start",
                    workspaceId,
                    messageId: assistantMessageId,
                    model: modelString,
                    historySequence,
                };
                this.emit("stream-start", streamStartEvent);
                this.emit("error", {
                    type: "error",
                    workspaceId,
                    messageId: assistantMessageId,
                    error: errorMessage,
                    errorType: "context_exceeded",
                });
                return (0, result_1.Ok)(undefined);
            }
            if (simulateToolPolicyNoop) {
                const noopMessage = (0, message_1.createCmuxMessage)(assistantMessageId, "assistant", "", {
                    timestamp: Date.now(),
                    model: modelString,
                    systemMessageTokens,
                    toolPolicy,
                });
                const parts = [
                    {
                        type: "text",
                        text: "Tool execution skipped because the requested tool is disabled by policy.",
                    },
                ];
                const streamStartEvent = {
                    type: "stream-start",
                    workspaceId,
                    messageId: assistantMessageId,
                    model: modelString,
                    historySequence,
                };
                this.emit("stream-start", streamStartEvent);
                const textParts = parts.filter((part) => part.type === "text");
                if (textParts.length === 0) {
                    throw new Error("simulateToolPolicyNoop requires at least one text part");
                }
                for (const textPart of textParts) {
                    if (textPart.text.length === 0) {
                        continue;
                    }
                    const streamDeltaEvent = {
                        type: "stream-delta",
                        workspaceId,
                        messageId: assistantMessageId,
                        delta: textPart.text,
                        tokens: 0, // Mock scenario - actual tokenization happens in streamManager
                        timestamp: Date.now(),
                    };
                    this.emit("stream-delta", streamDeltaEvent);
                }
                const streamEndEvent = {
                    type: "stream-end",
                    workspaceId,
                    messageId: assistantMessageId,
                    metadata: {
                        model: modelString,
                        systemMessageTokens,
                    },
                    parts,
                };
                this.emit("stream-end", streamEndEvent);
                const finalAssistantMessage = {
                    ...noopMessage,
                    metadata: {
                        ...noopMessage.metadata,
                        historySequence,
                    },
                    parts,
                };
                await this.partialService.deletePartial(workspaceId);
                await this.historyService.updateHistory(workspaceId, finalAssistantMessage);
                return (0, result_1.Ok)(undefined);
            }
            // Build provider options based on thinking level and message history
            // Pass filtered messages so OpenAI can extract previousResponseId for persistence
            const providerOptions = (0, providerOptions_1.buildProviderOptions)(modelString, thinkingLevel ?? "off", filteredMessages);
            // Delegate to StreamManager with model instance, system message, tools, historySequence, and initial metadata
            const streamResult = await this.streamManager.startStream(workspaceId, finalMessages, modelResult.data, modelString, historySequence, systemMessage, runtime, abortSignal, tools, {
                systemMessageTokens,
                timestamp: Date.now(),
                mode, // Pass mode so it persists in final history entry
            }, providerOptions, maxOutputTokens, toolPolicy, streamToken // Pass the pre-generated stream token
            );
            if (!streamResult.success) {
                // StreamManager already returns SendMessageError
                return (0, result_1.Err)(streamResult.error);
            }
            // StreamManager now handles history updates directly on stream-end
            // No need for event listener here
            return (0, result_1.Ok)(undefined);
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            log_1.log.error("Stream message error:", error);
            // Return as unknown error type
            return (0, result_1.Err)({ type: "unknown", raw: `Failed to stream message: ${errorMessage}` });
        }
    }
    async stopStream(workspaceId) {
        if (this.mockModeEnabled && this.mockScenarioPlayer) {
            this.mockScenarioPlayer.stop(workspaceId);
            return (0, result_1.Ok)(undefined);
        }
        return this.streamManager.stopStream(workspaceId);
    }
    /**
     * Check if a workspace is currently streaming
     */
    isStreaming(workspaceId) {
        if (this.mockModeEnabled && this.mockScenarioPlayer) {
            return this.mockScenarioPlayer.isStreaming(workspaceId);
        }
        return this.streamManager.isStreaming(workspaceId);
    }
    /**
     * Get the current stream state for a workspace
     */
    getStreamState(workspaceId) {
        if (this.mockModeEnabled && this.mockScenarioPlayer) {
            return this.mockScenarioPlayer.isStreaming(workspaceId) ? "streaming" : "idle";
        }
        return this.streamManager.getStreamState(workspaceId);
    }
    /**
     * Get the current stream info for a workspace if actively streaming
     * Used to re-establish streaming context on frontend reconnection
     */
    getStreamInfo(workspaceId) {
        if (this.mockModeEnabled && this.mockScenarioPlayer) {
            return undefined;
        }
        return this.streamManager.getStreamInfo(workspaceId);
    }
    /**
     * Replay stream events
     * Emits the same events that would be emitted during live streaming
     */
    replayStream(workspaceId) {
        if (this.mockModeEnabled && this.mockScenarioPlayer) {
            this.mockScenarioPlayer.replayStream(workspaceId);
            return;
        }
        this.streamManager.replayStream(workspaceId);
    }
    async deleteWorkspace(workspaceId) {
        try {
            const workspaceDir = this.config.getSessionDir(workspaceId);
            await fs.rm(workspaceDir, { recursive: true, force: true });
            return (0, result_1.Ok)(undefined);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            return (0, result_1.Err)(`Failed to delete workspace: ${message}`);
        }
    }
}
exports.AIService = AIService;
//# sourceMappingURL=aiService.js.map