import path from "node:path";
import { AiEntityType, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
import { classifyAttachmentDelivery } from "@superblocksteam/shared";
import { safeJsonStringify } from "../../../util/log-sanitizer.js";
import { getErrorMeta, getLogger } from "../../../util/logger.js";
import { buildBaseSystemPrompt } from "../../agent/prompts/build-base-system-prompt.js";
import { buildTools } from "../../agent/tools.js";
import { processStreamChunk } from "../../agent/utils.js";
import { AppSkillsManager } from "../../app-skills/manager.js";
import { AttachmentsStore } from "../../attachments/index.js";
import { isUploadedAttachment, toUploadedAttachmentContentPart, toUploadedAttachmentReferenceTextPart, } from "../../attachments/uploaded-content-part.js";
import { normalizeTextAttachmentForModel } from "../../chat/utils.js";
import { getActiveContext } from "../../llm/context-v2/utils/index.js";
import { isNoOutputError, LLMProviderError } from "../../llm/error.js";
import { wrapError } from "../../llm/stream/errors.js";
import { GRAPHQL_SKILL_INTEGRATION_TYPES, REST_SKILL_INTEGRATION_TYPES, SQL_SKILL_INTEGRATION_TYPES, SYSTEM_SKILL_REFERENCES, } from "../../skills/types.js";
import { buildModeMessage } from "../../util/mode-message.js";
import { parseJwt } from "../../util/parse-jwt.js";
import { hasToolSuccess, isAwaitingUser, needsPermissionToExecute, } from "../../util/stop-condition.js";
import { AGENT_CANCELED, AGENT_NEEDS_USER_INPUT, AGENT_PLANNED, APP_RUNTIME_UPDATED_WITHOUT_EDITS, LLM_ERRORED, USER_CANCELED, V3_AGENT_FINISHED, } from "../clark-fsm.js";
import { getContextId } from "../helpers/context-id.js";
import { sendInteractiveMessageChannel, sendUserMessageChannel, } from "../helpers/peer.js";
import { transitionFrom } from "../helpers/transition.js";
/**
 * Get the skill reference files that should be read based on the tagged integrations.
 * Returns an array of skill file paths.
 */
function getRequiredSkillReferences(integrations) {
    const refs = new Set();
    for (const integration of integrations) {
        const pluginType = integration.type?.toLowerCase() ?? "";
        if (SQL_SKILL_INTEGRATION_TYPES.has(pluginType)) {
            refs.add(SYSTEM_SKILL_REFERENCES.SQL_DATABASES);
        }
        if (REST_SKILL_INTEGRATION_TYPES.has(pluginType)) {
            refs.add(SYSTEM_SKILL_REFERENCES.REST_APIS);
        }
        if (GRAPHQL_SKILL_INTEGRATION_TYPES.has(pluginType)) {
            refs.add(SYSTEM_SKILL_REFERENCES.GRAPHQL);
        }
    }
    // If any integration-specific reference is needed, also require the main API skill
    if (refs.size > 0) {
        refs.add(SYSTEM_SKILL_REFERENCES.API_MAIN);
    }
    return Array.from(refs);
}
/**
 * Parse a data URL and extract the media type and base64 data.
 * Returns null if it's not a valid data URL.
 */
const parseDataUrl = (dataUrl) => {
    const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
    if (match && match[1] && match[2]) {
        return { mediaType: match[1], data: match[2] };
    }
    return null;
};
const toBase64Data = (value) => Buffer.from(value, "utf8").toString("base64");
const getToolResultOutputObject = (output) => {
    if (!output || typeof output !== "object") {
        return null;
    }
    if ("value" in output && output.value && typeof output.value === "object") {
        return output.value;
    }
    return output;
};
/**
 * Maps attachments to LLM content parts.
 * Text attachments (CSV, JSON, YAML, CSS, TXT) are wrapped in XML tags.
 */
const mapAttachmentsToContentParts = (attachments, options) => {
    return attachments.map((attachment) => {
        if (attachment.type === "image") {
            return {
                type: attachment.type,
                image: attachment.image,
            };
        }
        else if (attachment.type === "archive") {
            // Archives are reference-only: upstream providers reject
            // application/zip, application/gzip, etc. as inline FileParts.
            // The normal flow force-uploads archives in
            // `maybeUploadPromptContextAttachments` so they arrive here as
            // `type: "uploaded"`; this branch is a defensive fallback for the
            // narrow case where that upload was skipped (e.g. missing jwt or
            // applicationId) so the generation degrades to a reference rather
            // than blowing up the whole LLM request.
            const parsed = parseDataUrl(attachment.data);
            const mediaType = parsed?.mediaType ?? "application/octet-stream";
            const fileName = resolveAttachmentFileName({
                fileName: attachment.fileName,
                mediaType,
                type: attachment.type,
            });
            return {
                type: "text",
                text: `<attachment_reference>
fileName: ${fileName}
mediaType: ${mediaType}
note: This archive was not uploaded to persistent storage, so downloadAttachments cannot fetch it. Ask the user to retry; do not attempt to infer its contents.
</attachment_reference>`,
            };
        }
        else if (attachment.type === "pdf") {
            const parsed = parseDataUrl(attachment.data);
            const mediaType = parsed?.mediaType ?? "application/pdf";
            return {
                type: "file",
                data: parsed?.data ?? attachment.data,
                mediaType,
                filename: resolveAttachmentFileName({
                    fileName: attachment.fileName,
                    mediaType,
                    type: attachment.type,
                }),
            };
        }
        else if (isUploadedAttachment(attachment)) {
            const delivery = classifyAttachmentDelivery({
                fileName: attachment.fileName,
                mimeType: attachment.mediaType,
            });
            if (delivery === "reference_only") {
                return toUploadedAttachmentReferenceTextPart(attachment, {
                    downloadEnabled: options.attachmentDownloadEnabled,
                });
            }
            return toUploadedAttachmentContentPart(attachment);
        }
        const normalizedTextAttachment = normalizeTextAttachmentForModel(attachment);
        if (normalizedTextAttachment) {
            // File parts must use bytes, base64 data, or a URL. Inline text files
            // therefore need to be encoded before we send them as text/plain files.
            return {
                type: "file",
                data: toBase64Data(normalizedTextAttachment.content),
                filename: normalizedTextAttachment.fileName,
                mediaType: "text/plain",
            };
        }
        throw new Error("Unsupported attachment type");
    });
};
const buildUserMessage = ({ preferredAppInferenceIntegration, userPrompt, promptContext, planContext, mode, services, userChanges, userFactsSection, pendingKnowledgeFreeTextTurn, pendingKnowledgeEntries, skillsNeedReload, useSystemSkills, }) => {
    const content = [];
    // If context was recently summarized, remind Clark to re-read skills
    if (skillsNeedReload && useSystemSkills) {
        const sdkApiEnabled = services.features?.sdkApiEnabled ?? false;
        const apiSkillReminder = sdkApiEnabled
            ? ""
            : "\n- For API work: `skills/system/superblocks-api/SKILL.md` and the appropriate reference file for your integration type";
        content.push({
            type: "text",
            text: `<skill_reload_reminder>
**IMPORTANT:** The conversation context was recently summarized. Any skill files you previously read may no longer be in context.

If you are working on tasks that require skill guidance, you MUST re-read the relevant skill files using \`build_readFile\` before continuing:${apiSkillReminder}
- For frontend work: \`skills/system/superblocks-frontend/SKILL.md\`

Do not rely on prior knowledge from skills that may have been summarized away.
</skill_reload_reminder>\n\n`,
        });
    }
    // Get entities from promptContext, but also include planContext entities for approved plans
    const promptEntities = promptContext?.entities ?? [];
    const planEntities = planContext?.entities ?? [];
    const planApproved = planContext?.approved === true;
    // Merge entities: use promptContext entities, but if plan is approved include plan entities too
    const focusedEntities = planApproved && planEntities.length > 0
        ? [
            ...promptEntities,
            ...planEntities.filter((pe) => !promptEntities.some((e) => e.id === pe.id)),
        ]
        : promptEntities.length > 0
            ? promptEntities
            : undefined;
    const focusedEntitiesWithSemanticInfo = focusedEntities?.map((entity) => {
        if (entity.type === AiEntityType.COMPONENT) {
            // add semantic information to components so clark can use it to understand which
            // element is targeted
            const component = services.appContextStore.getComponent(entity.id) ?? {};
            return {
                ...entity,
                ...component,
            };
        }
        return entity;
    });
    const selectedIntegrations = promptContext?.integrations;
    const planSummary = planContext?.plan;
    // Add user changes if present
    if (userChanges && userChanges.length > 0) {
        const appRootDirPath = services.appShell.appRootDirPath;
        const changeDescriptions = userChanges.map((change) => {
            switch (change.type) {
                case "addPage":
                    return `- Created a new page at ${path.relative(appRootDirPath, change.path)}`;
                case "deletePage":
                    return `- Deleted page at ${path.relative(appRootDirPath, change.path)}`;
                case "apiUpdate":
                    return `- Modified API "${change.apiName}"`;
                case "apiDelete":
                    return `- Deleted API "${change.apiName}"`;
                case "fileChanged":
                    return `- Edited code in ${path.relative(appRootDirPath, change.filePath)}`;
            }
        });
        content.push({
            type: "text",
            text: `<recent_user_changes>\nSince your last interaction, the user has made the following changes:\n${changeDescriptions.join("\n")}\n\nThe current state of the application reflects all of these changes.\n</recent_user_changes>\n\n`,
        });
    }
    // Add integration setup state
    if (promptContext?.integrationSetup) {
        const { pluginId, mode, integrationName } = promptContext.integrationSetup;
        const nameLabel = integrationName ? ` ("${integrationName}")` : "";
        content.push({
            type: "text",
            text: `<integration_setup_state>\nThe integration setup form is already open for ${pluginId}${nameLabel} in ${mode} mode. Do not call openIntegrationSetup or openExistingIntegration — the user can already see the form.\n</integration_setup_state>\n\n`,
        });
    }
    if (promptContext?.currentView) {
        const viewDescriptions = {
            preview: "the Preview, where the user sees a live preview of their application as it would appear in a browser.",
            "code-viewer": "the Code view. The Code view is a read-only file viewer — the user cannot edit files directly in this view. Do not suggest editing files through the Code view UI. To make code changes, use your own tools instead.",
            "integration-setup": "the Integration Setup view, where the user configures an integration (e.g. database credentials, API keys, or connection settings).",
        };
        const desc = viewDescriptions[promptContext.currentView];
        content.push({
            type: "text",
            text: `<current_view>\nThe user is currently in ${desc}\n</current_view>\n\n`,
        });
    }
    // Add preview device context (only relevant in Preview)
    if (promptContext &&
        (!promptContext.currentView || promptContext.currentView === "preview")) {
        content.push({
            type: "text",
            text: `<preview_device>\nThe user is currently viewing the app in ${promptContext.previewDevice === "mobile" ? "mobile preview mode (viewport width: 375px)" : "desktop preview mode (full width)"}.\n</preview_device>\n\n`,
        });
    }
    // Add current page context
    if (promptContext?.currentPageName) {
        content.push({
            type: "text",
            text: `<current_page>\nThe user is currently viewing the page named "${promptContext.currentPageName}".\n</current_page>\n\n`,
        });
    }
    // Add current route context
    if (promptContext) {
        const routeLines = [];
        if (promptContext.currentPathname) {
            routeLines.push(`pathname: "${promptContext.currentPathname}"`);
        }
        if (promptContext.currentRoutePattern) {
            routeLines.push(`routePattern: "${promptContext.currentRoutePattern}"`);
        }
        if (promptContext.currentRouteParams &&
            Object.keys(promptContext.currentRouteParams).length > 0) {
            routeLines.push(`routeParams: ${safeJsonStringify(promptContext.currentRouteParams)}`);
        }
        if (routeLines.length > 0) {
            content.push({
                type: "text",
                text: `<current_route>\n${routeLines.join("\n")}\n</current_route>\n\n`,
            });
        }
    }
    // Add current API context
    if (promptContext?.currentApiName) {
        content.push({
            type: "text",
            text: `<current_api>\nThe user is currently viewing the API named "${promptContext.currentApiName}" in the bottom panel.\n</current_api>\n\n`,
        });
    }
    // Add available data tags context
    if (promptContext?.dataTags) {
        const { available, selected, default: defaultDataTag, } = promptContext.dataTags;
        const dataTagLines = [];
        dataTagLines.push(`Available data tags (${available.length}):`);
        available.forEach((dataTag) => {
            const isSelected = selected?.id === dataTag.id;
            const isDefault = defaultDataTag.id === dataTag.id;
            let line = `- ${dataTag.displayName} (key: "${dataTag.key}")`;
            if (isSelected)
                line += " [CURRENTLY SELECTED]";
            if (isDefault)
                line += " [DEFAULT]";
            if (dataTag.description)
                line += ` - ${dataTag.description}`;
            dataTagLines.push(line);
        });
        content.push({
            type: "text",
            text: `<available_data_tags>\n${dataTagLines.join("\n")}\n</available_data_tags>\n\n`,
        });
    }
    // Add current user context
    if (promptContext?.user) {
        const user = promptContext.user;
        content.push({
            type: "text",
            text: `<current_user>\nName: ${user.name}\nEmail: ${user.email}\nUsername: ${user.username}\n</current_user>\n\n`,
        });
    }
    // Add organization groups context
    if (promptContext?.groups && promptContext.groups.length > 0) {
        const allGroups = promptContext.groups
            .map((g) => `- ${g.name} (${g.size} members)`)
            .join("\n");
        content.push({
            type: "text",
            text: `<organization_groups>\nAll groups in the organization (${promptContext.groups.length}):\n${allGroups}\n</organization_groups>\n\n`,
        });
    }
    if (promptContext?.conflictedFiles?.length) {
        const fileList = promptContext.conflictedFiles
            .map((f) => `- ${f}`)
            .join("\n");
        content.push({
            type: "text",
            text: `<conflicted_files>\nThe following files have unresolved git merge conflicts:\n${fileList}\nRead each file, resolve the conflict markers, and stage them.\nAfter resolving all conflicts, run \`build_debug\` and \`build_captureScreenshot\` to verify the app still works. If any issues are found, fix them before finishing.\n</conflicted_files>\n\n`,
        });
    }
    // Add focused entities if present
    if (focusedEntities?.length) {
        content.push({
            type: "text",
            text: `<focused_entities>\nThe user has focused the editor on the following entities corresponding to the current app state. Attempt to constrain your actions to affect only these entities.\n${safeJsonStringify(focusedEntitiesWithSemanticInfo)}\n\n`,
        });
    }
    // Add selected integrations if present.
    //
    // Integration `displayName` / `name` are user-controlled (org admins set them), so
    // anything we interpolate into a tag-delimited prompt block must be escaped or a
    // crafted name (e.g. `</selected_integrations>...`) could close the block and inject
    // attacker-controlled instructions into the system prompt. We do that by escaping
    // `<` and `>` in the JSON payload before interpolation.
    const escapeTagDelimiters = (json) => json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
    if (selectedIntegrations?.length) {
        // Omit metadata to reduce token usage - only include essential identifying information
        // Send displayName as "name" so message shows original name
        const integrationsWithoutMetadata = selectedIntegrations.map(({ id, name, displayName, type }) => ({
            id,
            name: displayName || name,
            tagName: name,
            type,
        }));
        content.push({
            type: "text",
            text: `<selected_integrations>\nThe user has explicitly selected the following integration configurations to use. You MUST use these specific integrations when creating or modifying APIs.\n${escapeTagDelimiters(safeJsonStringify(integrationsWithoutMetadata))}\n</selected_integrations>\n\n`,
        });
        // Add required skill reads based on integration types (YAML APIs only)
        const requiredSkillRefs = getRequiredSkillReferences(selectedIntegrations);
        const sdkApiEnabledForSkills = services.features?.sdkApiEnabled ?? false;
        if (requiredSkillRefs.length > 0 &&
            useSystemSkills &&
            !sdkApiEnabledForSkills) {
            content.push({
                type: "text",
                text: `<required_skill_reads>\n**MANDATORY:** Before creating or modifying APIs with the tagged integrations, you MUST read the following skill files using \`build_readFile\`:\n${requiredSkillRefs.map((f) => `- ${f}`).join("\n")}\n\nDo NOT proceed with API work until you have read these files. They contain integration-specific patterns and syntax that differ from general knowledge.\n</required_skill_reads>\n\n`,
            });
        }
    }
    if (preferredAppInferenceIntegration) {
        const defaultPayload = {
            id: preferredAppInferenceIntegration.id,
            name: preferredAppInferenceIntegration.displayName ||
                preferredAppInferenceIntegration.name,
            tagName: preferredAppInferenceIntegration.name,
            type: preferredAppInferenceIntegration.type,
        };
        content.push({
            type: "text",
            text: `<default_ai_integration>\nThe organization has configured the following integration as the default for AI inference in customer-built apps:\n${escapeTagDelimiters(safeJsonStringify(defaultPayload))}\nWhen the user asks for AI-powered functionality (chatbots, summarization, classification, content generation, semantic search, etc.), DIRECTLY use this integration for AI inference. Do NOT use askMultiChoice or searchIntegrations to ask which AI provider to use — this integration is already the organization's chosen default. Include its ID in integrationIds when calling exitPlanMode for plans that involve AI-powered features. Only pick a different AI integration if the user explicitly names another provider.\n</default_ai_integration>\n\n`,
        });
    }
    // Add integration-specific knowledge facts
    if (userFactsSection) {
        content.push({
            type: "text",
            text: `${userFactsSection}\n\n`,
        });
    }
    if (pendingKnowledgeFreeTextTurn &&
        pendingKnowledgeEntries &&
        pendingKnowledgeEntries.length > 0) {
        const candidateSummary = pendingKnowledgeEntries
            .map((entry, idx) => `${idx + 1}. ${entry.name}${entry.description ? ` — ${entry.description}` : ""}`)
            .join("\n");
        content.push({
            type: "text",
            text: `<pending_knowledge_approval_context>
The user is replying in free text while there is a pending app knowledge approval draft.

Prioritize resolving this pending knowledge approval interaction:
- Interpret the user's message as feedback on the pending knowledge candidates first.
- Refine or consolidate candidates as needed, then call \`rememberKnowledge\` with updated candidate(s).
- Ask one focused clarifying question only if the feedback is genuinely ambiguous.
- Do not pivot into unrelated implementation work until this pending knowledge interaction is resolved.

Pending candidates:
${candidateSummary}
</pending_knowledge_approval_context>\n\n`,
        });
    }
    if (promptContext?.attachments?.length) {
        // Log attachments with their label/insight metadata for debugging
        getLogger().info("[llm-generating] Received attachments:", promptContext.attachments.map((a) => ({
            type: a.type,
            fileName: a.fileName,
            label: a.label,
            insight: a.insight,
            imageLength: a.type === "image" ? a.image?.length : undefined,
        })));
        content.push(...mapAttachmentsToContentParts(promptContext.attachments, {
            attachmentDownloadEnabled: services.features?.clarkAttachmentDownloadEnabled === true,
        }));
    }
    if (mode !== undefined) {
        content.push({
            type: "text",
            text: `${buildModeMessage(mode, planApproved)}\n\n`,
        });
    }
    // Only include <approved_plan> when the plan is actually approved.
    if (planApproved && planSummary) {
        content.push({
            type: "text",
            text: `<approved_plan>Reference this previously approved plan when executing. Do not deviate without user confirmation.\n${planSummary}\n</approved_plan>\n\n`,
        });
    }
    else if (mode === AiMode.PLAN && planSummary) {
        // Keep refining the plan until it is approved.
        content.push({
            type: "text",
            text: `<proposed_plan>This is the current proposed plan. It is NOT approved yet. Revise it as needed based on the user's feedback, and present an updated plan via exitPlanMode.\n${planSummary}\n</proposed_plan>\n\n`,
        });
    }
    // Add testing instructions if enableTesting is set on the plan context
    const shouldTest = planContext?.enableTesting === true;
    if (shouldTest && mode === AiMode.BUILD) {
        content.push({
            type: "text",
            text: `<testing_required>
IMPORTANT: Testing is enabled for this plan. After building the features, you MUST call the \`startTestRun\` tool to invoke the Testing Agent.
The Testing Agent will verify your implementation works correctly before you can finalize.
Do NOT call \`build_finalize\` until testing is complete.
</testing_required>\n\n`,
        });
    }
    // Include attachments from approved plan for execution.
    // Duplicate filenames are handled downstream by uniquifyFilePartFilenames()
    // in getMessages(), which appends stable suffixes (_2, _3, …) to collisions
    // rather than dropping any attachments.
    if (planApproved && planContext?.attachments?.length) {
        content.push(...mapAttachmentsToContentParts(planContext.attachments, {
            attachmentDownloadEnabled: services.features?.clarkAttachmentDownloadEnabled === true,
        }));
    }
    content.push({
        type: "text",
        text: userPrompt,
    });
    return {
        role: "user",
        content,
    };
};
export const doLLMGenerating = (clark, services) => {
    const transitionTo = transitionFrom(clark);
    return async ({ event }) => {
        switch (event.type) {
            case AGENT_PLANNED: {
                const { userPrompt, promptContext, isUserPrompt = true, knowledgeMessageId, } = event;
                const runPromptId = clark.context.promptId;
                const isStaleRun = () => clark.context.promptId !== runPromptId;
                const provider = services.llmProvider.name;
                const isKnowledgeEnabled = clark.context.llmConfig?.flags?.isFactsEnabled ?? false;
                const mode = clark.context.currentMode;
                const planContext = clark.context.planContext;
                const planApproved = planContext?.approved;
                const pendingKnowledgeFreeTextTurn = clark.context.pendingKnowledgeFreeTextTurn === true;
                const pendingKnowledgeEntries = clark.context.pendingAppKnowledge?.knowledgeEntries?.map((entry) => ({
                    name: entry.name,
                    description: entry.description,
                }));
                const context = getActiveContext(clark, services);
                const skillsNeedReload = context?.checkAndClearSkillsNeedReload?.() ?? false;
                const userId = parseJwt(clark.context.jwt)?.userId ?? "";
                let netNewKnowledge = {
                    org: [],
                    user: [],
                    integrations: {},
                };
                let knowledgeSections = {
                    systemPromptSection: "",
                    userMessageSection: "",
                    onDemandSkillIndex: "",
                    facts: netNewKnowledge,
                };
                if (isKnowledgeEnabled) {
                    knowledgeSections =
                        await services.knowledgeManager.loadFactsPromptSections({
                            rpcClient: services.rpcClient,
                            organizationId: services.organizationId,
                            userId,
                            logger: services.logger,
                            promptIntegrations: promptContext?.integrations ?? [],
                            planIntegrations: planContext?.integrations,
                        });
                    // Get net-new facts (this marks ALL facts as sent, including on-demand)
                    const allNetNewKnowledge = services.knowledgeManager.getNetNewFacts(knowledgeSections.facts);
                    // Only show always-inject facts in the initial knowledge UI message
                    // On-demand facts will be shown when they are read via build_readFile
                    netNewKnowledge =
                        services.knowledgeManager.filterAlwaysInjectFacts(allNetNewKnowledge);
                }
                // Load app knowledge entries from disk (user's persistent app preferences)
                let appKnowledgeSection = "";
                try {
                    const appSkillsManager = new AppSkillsManager(services.appRootDirPath);
                    appKnowledgeSection = await appSkillsManager.loadFactsForPrompt();
                }
                catch (err) {
                    clark.logger.warn("[llm-generating] Failed to load app skills facts (non-blocking)", { err });
                }
                const system = buildBaseSystemPrompt(services, mode, knowledgeSections.systemPromptSection, appKnowledgeSection, knowledgeSections.onDemandSkillIndex);
                void services.logger.info("[llm-generating] System prompt:", {
                    system,
                });
                const preferredAppInferenceIntegrationId = promptContext?.aiConfiguration?.preferredAppInferenceIntegrationId;
                const preferredAppInferenceIntegration = preferredAppInferenceIntegrationId
                    ? await services.integrationStore.availableIntegration(preferredAppInferenceIntegrationId)
                    : undefined;
                const user = buildUserMessage({
                    preferredAppInferenceIntegration,
                    userPrompt,
                    promptContext,
                    planContext,
                    mode,
                    services,
                    userChanges: clark.context.userChanges,
                    userFactsSection: knowledgeSections.userMessageSection,
                    pendingKnowledgeFreeTextTurn,
                    pendingKnowledgeEntries,
                    skillsNeedReload,
                    useSystemSkills: services.features?.useSystemSkills ?? false,
                });
                void services.logger.info("[llm-generating] User message:", {
                    user,
                });
                if (isKnowledgeEnabled) {
                    // Persist validated knowledge message to chatSessionStore so it survives message reloads
                    const knowledgePayload = {
                        id: knowledgeMessageId,
                        type: "knowledge",
                        role: "assistant",
                        content: "",
                        status: "validated",
                        facts: netNewKnowledge,
                    };
                    const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
                    await sendUserMessage(knowledgePayload);
                    await sendUserMessage({
                        type: "reasoning",
                        role: "assistant",
                        content: "",
                    });
                }
                const tools = await buildTools(clark, services);
                const result = await clark.streamText(services, {
                    user,
                    system,
                    tools,
                    observers: services.streamObservers,
                    context: {
                        contextId: getContextId(clark, services),
                    },
                    stopWhen: [
                        hasToolSuccess("build_finalize"),
                        hasToolSuccess("exitPlanMode"),
                        hasToolSuccess("askMultiChoice"),
                        hasToolSuccess("askSearchableDropdown"),
                        hasToolSuccess("openExistingIntegration"),
                        hasToolSuccess("openIntegrationSetup"),
                        needsPermissionToExecute(clark),
                        isAwaitingUser(clark),
                    ],
                    onError: (error) => {
                        clark.logger.error("LLM stream processing failed", getErrorMeta(error));
                    },
                    // Only apply safety features for original user prompts, not responses to interactive messages
                    safety: isUserPrompt
                        ? {
                            scope: "last", // Unified scope: only scan user prompt, not context parts
                            classification: {
                                enabled: services.features.safetyClassificationEnabled ?? false,
                                // Only resolve model if classification is enabled to avoid unnecessary work
                                model: services.features.safetyClassificationEnabled
                                    ? services.llmProvider.modelForTask("fast")
                                    : undefined,
                                failOpen: true,
                            },
                            secretRedaction: {
                                level: services.features.secretRedactionLevel ?? "full",
                            },
                        }
                        : undefined,
                });
                // Flush any attachments that were buffered before context was loaded.
                // At this point streamText has created the context, so getActiveContext will succeed.
                const pendingAttachments = clark.context.pendingAttachments;
                if (pendingAttachments) {
                    const ctx = getActiveContext(clark, services);
                    if (ctx) {
                        const store = new AttachmentsStore();
                        const stored = store.appendAttachments(ctx, pendingAttachments.attachments, pendingAttachments.originalMessage);
                        clark.updateContext({ pendingAttachments: undefined });
                        getLogger().info(`[llm-generating] Flushed ${stored.length} deferred attachment(s) to context`, stored.map((a) => ({
                            id: a.id,
                            type: a.type,
                            fileName: a.fileName,
                        })));
                    }
                    else {
                        getLogger().error(`[llm-generating] Failed to flush deferred attachments: context still not loaded`);
                    }
                }
                let shouldTransitionToAwaitingUser;
                clark.logger.info("[llm-generating] Starting to process stream chunks");
                try {
                    for await (const chunk of result.fullStream) {
                        if (isStaleRun()) {
                            clark.logger.warn("[llm-generating] Ignoring stale stream chunk from prior run", {
                                runPromptId,
                                currentPromptId: clark.context.promptId,
                                chunkType: chunk.type,
                            });
                            return;
                        }
                        if (chunk.type === "tool-result") {
                            const isOpenIntegrationTool = chunk.toolName === "openExistingIntegration" ||
                                chunk.toolName === "openIntegrationSetup";
                            const isUpdateIntegrationTool = chunk.toolName === "updateIntegrationFields";
                            if (chunk.toolName === "askMultiChoice" ||
                                chunk.toolName === "askSearchableDropdown") {
                                shouldTransitionToAwaitingUser = true;
                            }
                            if (isOpenIntegrationTool || isUpdateIntegrationTool) {
                                const output = getToolResultOutputObject(chunk.output);
                                const success = output?.success === true;
                                if (success) {
                                    shouldTransitionToAwaitingUser = true;
                                    const sendInteractiveMessage = sendInteractiveMessageChannel(clark, services.chatSessionStore);
                                    const payload = {
                                        type: "multi_choice",
                                        role: "assistant",
                                        content: "Integration drafted",
                                        group: "clark",
                                        choices: ["Discard integration setup", "Save integration"],
                                        selectionType: "single",
                                    };
                                    let result = await sendInteractiveMessage(payload);
                                    // Recover from stale active interactive prompts that can block
                                    // the integration save/discard card from being emitted.
                                    if (!result.success &&
                                        clark.context.activeInteractiveMessage) {
                                        clark.updateContext({
                                            activeInteractiveMessage: undefined,
                                        });
                                        result = await sendInteractiveMessage(payload);
                                    }
                                    if (result.success &&
                                        clark.context.activeInteractiveMessage?.id) {
                                        clark.updateContext({
                                            pendingIntegrationActionPrompt: {
                                                messageId: clark.context.activeInteractiveMessage.id,
                                            },
                                        });
                                    }
                                }
                            }
                        }
                        await processStreamChunk(chunk, clark, services);
                    }
                    clark.logger.info("[llm-generating] Stream processing completed successfully");
                }
                catch (err) {
                    let error;
                    if (LLMProviderError.isInstance(err)) {
                        error = err;
                    }
                    else {
                        error = wrapError(err, provider);
                    }
                    if (error.type === "aborted") {
                        clark.logger.info("LLM generation aborted by user");
                        if (isStaleRun()) {
                            clark.logger.info("[llm-generating] Ignoring aborted transition from stale run", {
                                runPromptId,
                                currentPromptId: clark.context.promptId,
                            });
                            return;
                        }
                        void transitionTo({
                            type: USER_CANCELED,
                        });
                        return;
                    }
                    if (error.type === "validation_error") {
                        clark.logger.warn("Validation error (possibly invalid tool input)", getErrorMeta(error));
                        return;
                    }
                    // Handle AI_NoOutputGeneratedError - transition to idle since no output was generated
                    // The retry logic will have already tried fallback providers/models if enabled
                    if (isNoOutputError(err)) {
                        getLogger().warn("AI stream completed without output (retries exhausted or not retryable)");
                        if (isStaleRun()) {
                            clark.logger.info("[llm-generating] Ignoring no-output transition from stale run", {
                                runPromptId,
                                currentPromptId: clark.context.promptId,
                            });
                            return;
                        }
                        void transitionTo({
                            type: AGENT_CANCELED,
                            advice: "Clark completed without output. Try again.",
                        });
                        return;
                    }
                    clark.logger.error("LLM stream processing failed", getErrorMeta(error));
                    if (isStaleRun()) {
                        clark.logger.info("[llm-generating] Ignoring errored transition from stale run", {
                            runPromptId,
                            currentPromptId: clark.context.promptId,
                        });
                        return;
                    }
                    void clark.send({
                        type: LLM_ERRORED,
                        error,
                    });
                    return;
                }
                // In PLAN mode, only proceed to draft state if the user has approved the plan
                if (mode === AiMode.PLAN && !planApproved) {
                    if (isStaleRun()) {
                        clark.logger.info("[llm-generating] Ignoring plan-awaiting transition from stale run", {
                            runPromptId,
                            currentPromptId: clark.context.promptId,
                        });
                        return;
                    }
                    clark.logger.info("[llm-generating] Early return: PLAN mode without approval - transitioning to AGENT_NEEDS_USER_INPUT", {
                        hasSuggestions: clark.context.hasSuggestions,
                        mode,
                        planApproved,
                    });
                    void transitionTo({
                        type: AGENT_NEEDS_USER_INPUT,
                    });
                    return;
                }
                if (shouldTransitionToAwaitingUser ||
                    clark.context.pendingToolPermissionRequest) {
                    if (isStaleRun()) {
                        clark.logger.info("[llm-generating] Ignoring awaiting-user transition from stale run", {
                            runPromptId,
                            currentPromptId: clark.context.promptId,
                        });
                        return;
                    }
                    clark.logger.info("[llm-generating] Early return: transitioning to AGENT_NEEDS_USER_INPUT", {
                        hasSuggestions: clark.context.hasSuggestions,
                        shouldTransitionToAwaitingUser,
                        pendingToolPermissionRequest: !!clark.context.pendingToolPermissionRequest,
                    });
                    void transitionTo({
                        type: AGENT_NEEDS_USER_INPUT,
                    });
                    return;
                }
                clark.logger.info("[llm-generating] Checking local draft changes", {
                    currentHasSuggestions: clark.context.hasSuggestions,
                });
                const hasLocalDraft = await services.draftInterface.hasLocalDraftChanges();
                if (isStaleRun()) {
                    clark.logger.info("[llm-generating] Ignoring local-draft result from stale run", {
                        runPromptId,
                        currentPromptId: clark.context.promptId,
                    });
                    return;
                }
                clark.logger.info("[llm-generating] hasLocalDraft check completed", {
                    hasLocalDraft,
                    currentHasSuggestions: clark.context.hasSuggestions,
                });
                if (hasLocalDraft) {
                    clark.updateContext({ hasSuggestions: true });
                    clark.logger.info("[llm-generating] Transitioning to V3_AGENT_FINISHED with hasSuggestions=true");
                    void transitionTo({
                        type: V3_AGENT_FINISHED,
                    });
                }
                else {
                    clark.logger.warn("[llm-generating] No local drafts found, transitioning to Idle (generationCompleted will NOT be emitted)");
                    void transitionTo({
                        type: APP_RUNTIME_UPDATED_WITHOUT_EDITS,
                    });
                }
            }
        }
    };
};
//# sourceMappingURL=llm-generating.js.map