import { randomUUID } from "node:crypto";
import { findServerAdapter } from "../agent-runtimes/index.js";
import { agentRunContextService } from "./agent-run-context.js";
import { agentService } from "./agents.js";
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
import { executeAdapterWithModelFallbacks } from "./runtime-kernel/model-fallback.js";
import { preflightManagedAgentWorkspace } from "./managed-workspace-preflight.js";
import { CHAT_UNSUPPORTED_ADAPTER_TYPES, CHAT_RESULT_SENTINEL_PREFIX, ChatAssistantStreamError, safeTrim, asString, modelLabel, unconfiguredDescriptor, unavailableAgentDescriptor, extractGeneratedAttachments, prepareChatAttachmentReferences, buildConversationPrompt, resultText, chatExecutionConfig, linkedIssueIdsForChat, linkedProjectIdForChat, stubAgent, summarizeRuntimeSkills, createAssistantTextAccumulator, createSentinelStream, parseCompletedAssistantReply, partialBodyFromRawAssistantText, maybeEmitAssistantState, maybeEmitAssistantDelta, maybeEmitTranscriptEntry, maybeEmitObservedTranscriptEntry, shouldSuppressChatTranscriptEntry } from "./chat-assistant.helpers.js";
export * from "./chat-assistant.helpers.js";
export function chatAssistantService(db, storage) {
    const agentsSvc = agentService(db);
    const runContextSvc = agentRunContextService(db);
    async function resolveChatInvocation(input) {
        const runtimeSource = await resolveConversationRuntime(input.conversation);
        if (!runtimeSource.descriptor.available) {
            return {
                runtimeSource,
                adapter: null,
                config: null,
                linkedIssueIds: [],
                linkedProjectId: null,
                resolvedWorkspace: null,
                sceneContext: null,
                availabilityError: runtimeSource.descriptor.error ?? "Chat assistant is not configured",
            };
        }
        if (!runtimeSource.agentRuntimeType || !runtimeSource.agentRuntimeConfig || !runtimeSource.runtimeAgent) {
            return {
                runtimeSource,
                adapter: null,
                config: null,
                linkedIssueIds: [],
                linkedProjectId: null,
                resolvedWorkspace: null,
                sceneContext: null,
                availabilityError: runtimeSource.descriptor.error ?? "Chat runtime is not configured",
            };
        }
        const adapter = findServerAdapter(runtimeSource.agentRuntimeType);
        if (!adapter) {
            return {
                runtimeSource,
                adapter: null,
                config: null,
                linkedIssueIds: [],
                linkedProjectId: null,
                resolvedWorkspace: null,
                sceneContext: null,
                availabilityError: `Unknown chat adapter type: ${runtimeSource.agentRuntimeType}`,
            };
        }
        const config = chatExecutionConfig(input.conversation, runtimeSource.agentRuntimeType, runtimeSource.agentRuntimeConfig);
        const linkedIssueIds = linkedIssueIdsForChat(input.conversation, input.contextLinks);
        const linkedProjectId = linkedProjectIdForChat(input.contextLinks);
        const resolvedWorkspace = await runContextSvc.resolveWorkspaceForRun(runtimeSource.runtimeAgent, {
            issueId: input.conversation.primaryIssueId ?? linkedIssueIds[0] ?? null,
            projectId: linkedProjectId,
        }, null);
        const sceneContext = await runContextSvc.buildSceneContext({
            scene: "chat",
            agent: runtimeSource.runtimeAgent,
            resolvedWorkspace,
            runtimeConfig: config,
        });
        return {
            runtimeSource,
            adapter,
            config,
            linkedIssueIds,
            linkedProjectId,
            resolvedWorkspace,
            sceneContext,
            availabilityError: null,
        };
    }
    async function resolveAgentRuntime(orgId, agentId) {
        const agent = await agentsSvc.getById(agentId);
        if (!agent || agent.orgId !== orgId || agent.status === "terminated") {
            return {
                descriptor: unavailableAgentDescriptor({
                    sourceLabel: "Selected agent",
                    runtimeAgentId: null,
                    agentRuntimeType: null,
                    model: null,
                    error: "The selected chat agent is unavailable. Choose another agent before sending messages.",
                }),
                runtimeAgent: null,
                agentRuntimeType: null,
                agentRuntimeConfig: null,
                runtimeSkills: [],
            };
        }
        const agentAdapterType = agent.agentRuntimeType;
        const agentAdapterConfig = (agent.agentRuntimeConfig ?? {});
        if (CHAT_UNSUPPORTED_ADAPTER_TYPES.has(agentAdapterType)) {
            return {
                descriptor: unavailableAgentDescriptor({
                    sourceLabel: agent.name,
                    runtimeAgentId: agent.id,
                    agentRuntimeType: agentAdapterType,
                    model: modelLabel(agentAdapterConfig) ?? null,
                    error: `${agent.name} uses ${agentAdapterType}, which does not support chat conversations.`,
                }),
                runtimeAgent: {
                    id: agent.id,
                    orgId: agent.orgId,
                    name: agent.name,
                    agentRuntimeType: agentAdapterType,
                    agentRuntimeConfig: agentAdapterConfig,
                },
                agentRuntimeType: agentAdapterType,
                agentRuntimeConfig: null,
                runtimeSkills: [],
            };
        }
        const { runtimeConfig, runtimeSkillEntries } = await runContextSvc.prepareRuntimeConfig({
            scene: "chat",
            agent: {
                id: agent.id,
                orgId: agent.orgId,
                name: agent.name,
                status: agent.status,
                agentRuntimeType: agentAdapterType,
                agentRuntimeConfig: agentAdapterConfig,
                metadata: agent.metadata ?? null,
            },
        });
        return {
            descriptor: {
                sourceType: "agent",
                sourceLabel: agent.name,
                runtimeAgentId: agent.id,
                agentRuntimeType: agentAdapterType,
                model: modelLabel(runtimeConfig) ?? "Default model",
                available: true,
                error: null,
            },
            runtimeAgent: {
                id: agent.id,
                orgId: agent.orgId,
                name: agent.name,
                agentRuntimeType: agentAdapterType,
                agentRuntimeConfig: runtimeConfig,
            },
            agentRuntimeType: agentAdapterType,
            agentRuntimeConfig: runtimeConfig,
            runtimeSkills: summarizeRuntimeSkills(runtimeSkillEntries),
        };
    }
    async function resolveConversationRuntime(conversation) {
        if (conversation.preferredAgentId) {
            const agentRuntime = await resolveAgentRuntime(conversation.orgId, conversation.preferredAgentId);
            if (agentRuntime)
                return agentRuntime;
        }
        return {
            descriptor: unconfiguredDescriptor("Choose a chat agent before sending messages."),
            runtimeAgent: null,
            agentRuntimeType: null,
            agentRuntimeConfig: null,
            runtimeSkills: [],
        };
    }
    async function enrichConversation(conversation) {
        const resolved = await resolveConversationRuntime(conversation);
        return {
            ...conversation,
            chatRuntime: resolved.descriptor,
        };
    }
    async function enrichConversations(conversations) {
        return Promise.all(conversations.map((conversation) => enrichConversation(conversation)));
    }
    async function streamChatAssistantReply(input) {
        const resolvedInvocation = await resolveChatInvocation({
            conversation: input.conversation,
            contextLinks: input.contextLinks,
        });
        if (resolvedInvocation.availabilityError) {
            throw new Error(resolvedInvocation.availabilityError);
        }
        const { runtimeSource, adapter, config, linkedIssueIds, linkedProjectId, sceneContext, } = resolvedInvocation;
        if (!adapter ||
            !config ||
            !sceneContext ||
            !runtimeSource.agentRuntimeType ||
            !runtimeSource.descriptor.runtimeAgentId) {
            throw new Error("Chat runtime is not configured");
        }
        const runtimeAgentType = runtimeSource.agentRuntimeType;
        const runtimeAgentId = runtimeSource.descriptor.runtimeAgentId;
        const resultSentinel = `${CHAT_RESULT_SENTINEL_PREFIX}${randomUUID()}__`;
        const runId = `chat-${input.conversation.id}-${randomUUID()}`;
        const assistantTextAccumulator = createAssistantTextAccumulator();
        const sentinelStream = createSentinelStream(resultSentinel);
        let parser = adapter.parseStdoutLine;
        let stdoutLineBuffer = "";
        const { rudderWorkspace, rudderWorkspaces, rudderRuntimeServiceIntents, rudderScene } = sceneContext;
        await preflightManagedAgentWorkspace({
            agentHome: asString(rudderWorkspace.agentHome),
            instructionsDir: asString(rudderWorkspace.instructionsDir),
            memoryDir: asString(rudderWorkspace.memoryDir),
            lifeDir: asString(rudderWorkspace.lifeDir),
            skillsDir: asString(rudderWorkspace.agentSkillsDir),
        });
        const preparedAttachments = await prepareChatAttachmentReferences({
            runtimeType: runtimeAgentType,
            messages: input.messages,
            storage,
            runId,
        });
        const prompt = buildConversationPrompt(input, runtimeSource, resultSentinel, typeof rudderWorkspace.orgResourcesPrompt === "string" ? rudderWorkspace.orgResourcesPrompt : "", preparedAttachments.references);
        const processTranscriptEntries = async (entries) => {
            for (const entry of entries) {
                if (entry.kind === "assistant") {
                    const delta = assistantTextAccumulator.push(entry.text, entry.delta === true);
                    if (!delta)
                        continue;
                    const visibleDelta = sentinelStream.push(delta);
                    if (visibleDelta) {
                        const assistantTranscriptEntry = {
                            kind: "assistant",
                            ts: entry.ts,
                            text: visibleDelta,
                            delta: true,
                        };
                        await maybeEmitObservedTranscriptEntry(input.onObservedTranscriptEntry, assistantTranscriptEntry);
                        await maybeEmitTranscriptEntry(input.onTranscriptEntry, assistantTranscriptEntry);
                    }
                    continue;
                }
                if (entry.kind === "result") {
                    await maybeEmitObservedTranscriptEntry(input.onObservedTranscriptEntry, {
                        ...entry,
                        text: partialBodyFromRawAssistantText(entry.text, resultSentinel),
                    });
                }
                else if (!(entry.kind === "stdout" && entry.text.includes(resultSentinel))) {
                    await maybeEmitObservedTranscriptEntry(input.onObservedTranscriptEntry, entry);
                }
                if (shouldSuppressChatTranscriptEntry(entry, resultSentinel)) {
                    continue;
                }
                await maybeEmitTranscriptEntry(input.onTranscriptEntry, entry);
            }
        };
        const processStdoutLine = async (line) => {
            if (!parser || !line.trim())
                return;
            await processTranscriptEntries(parser(line, new Date().toISOString()));
        };
        const flushStdoutChunk = async (chunk, finalize = false) => {
            const combined = `${stdoutLineBuffer}${chunk}`;
            const lines = combined.split(/\r?\n/);
            stdoutLineBuffer = lines.pop() ?? "";
            for (const line of lines) {
                await processStdoutLine(line);
            }
            if (finalize && stdoutLineBuffer.trim()) {
                const trailing = stdoutLineBuffer;
                stdoutLineBuffer = "";
                await processStdoutLine(trailing);
            }
        };
        await maybeEmitAssistantState(input.onAssistantState, "streaming");
        const chatAttachments = input.messages
            .slice(-12)
            .flatMap((message) => message.attachments)
            .map((attachment) => {
            const reference = preparedAttachments.references.get(attachment.id);
            return reference ? { attachmentId: attachment.id, ...reference } : null;
        })
            .filter((attachment) => attachment !== null);
        const media = preparedAttachments.media;
        const result = await (async () => {
            try {
                return await executeAdapterWithModelFallbacks(adapter, {
                    runId,
                    agent: stubAgent({
                        orgId: input.conversation.orgId,
                        agentRuntimeType: runtimeAgentType,
                        agentRuntimeConfig: config,
                        sourceLabel: runtimeSource.descriptor.sourceLabel,
                        sourceId: runtimeAgentId,
                    }),
                    runtime: {
                        sessionId: null,
                        sessionParams: null,
                        sessionDisplayId: null,
                        taskKey: null,
                    },
                    config,
                    context: {
                        chatPrompt: prompt,
                        chatConversationId: input.conversation.id,
                        chatMode: true,
                        rudderScene,
                        rudderWorkspace,
                        rudderWorkspaces,
                        ...(chatAttachments.length > 0 ? { chatAttachments } : {}),
                        ...(rudderRuntimeServiceIntents ? { rudderRuntimeServiceIntents } : {}),
                        ...(linkedProjectId ? { projectId: linkedProjectId } : {}),
                        ...(linkedIssueIds[0] ? { issueId: linkedIssueIds[0] } : {}),
                        ...(linkedIssueIds.length > 0 ? { issueIds: linkedIssueIds } : {}),
                    },
                    ...(media.length > 0 ? { media } : {}),
                    onMeta: async (meta) => {
                        await input.onInvocationMeta?.({
                            ...meta,
                            loadedSkills: runtimeSource.runtimeSkills,
                        });
                    },
                    authToken: adapter.supportsLocalAgentJwt
                        ? createLocalAgentJwt(runtimeAgentId, input.conversation.orgId, runtimeAgentType, runId) ?? undefined
                        : undefined,
                    abortSignal: input.abortSignal,
                    onLog: async (stream, chunk) => {
                        if (stream === "stdout") {
                            if (chunk.startsWith("[rudder]")) {
                                const entry = {
                                    kind: "stdout",
                                    ts: new Date().toISOString(),
                                    text: chunk,
                                };
                                await maybeEmitObservedTranscriptEntry(input.onObservedTranscriptEntry, entry);
                                await maybeEmitTranscriptEntry(input.onTranscriptEntry, entry);
                                return;
                            }
                            await flushStdoutChunk(chunk);
                        }
                    },
                }, {
                    resolveAdapter: findServerAdapter,
                    createAuthToken: (agentRuntimeType) => createLocalAgentJwt(runtimeAgentId, input.conversation.orgId, agentRuntimeType, runId) ?? undefined,
                    onAttemptStart: (_attempt, attemptAdapter) => {
                        parser = attemptAdapter.parseStdoutLine;
                    },
                });
            }
            finally {
                await preparedAttachments.cleanup();
            }
        })();
        await flushStdoutChunk("", true);
        await maybeEmitAssistantDelta(input.onAssistantDelta, sentinelStream.finish());
        const partialBody = partialBodyFromRawAssistantText(assistantTextAccumulator.fullText || resultText(result), resultSentinel) ||
            (safeTrim(sentinelStream.visibleText) ?? "");
        if (input.abortSignal?.aborted) {
            await maybeEmitAssistantState(input.onAssistantState, "stopped");
            return {
                outcome: "stopped",
                partialBody,
                replyingAgentId: runtimeAgentId,
            };
        }
        if (result.timedOut) {
            throw new ChatAssistantStreamError("Chat request timed out", partialBody);
        }
        if ((result.exitCode ?? 0) !== 0 || result.errorMessage) {
            throw new ChatAssistantStreamError(result.errorMessage ?? "Chat adapter execution failed", partialBody);
        }
        await maybeEmitAssistantState(input.onAssistantState, "finalizing");
        const raw = resultText(result) || assistantTextAccumulator.fullText;
        const generatedAttachments = extractGeneratedAttachments(result);
        let reply;
        try {
            reply = parseCompletedAssistantReply(raw, resultSentinel, { requireSentinel: true });
        }
        catch (error) {
            throw new ChatAssistantStreamError(error instanceof Error ? error.message : "Chat adapter returned an invalid final reply", partialBody, generatedAttachments);
        }
        const finalBody = reply.body;
        reply.replyingAgentId = runtimeAgentId;
        if (generatedAttachments.length > 0) {
            reply.generatedAttachments = generatedAttachments;
        }
        const streamedBody = safeTrim(sentinelStream.visibleText) ?? "";
        if (finalBody && finalBody !== streamedBody) {
            await maybeEmitAssistantDelta(input.onAssistantDelta, finalBody);
        }
        return {
            outcome: "completed",
            reply,
            partialBody: finalBody,
            replyingAgentId: runtimeAgentId,
        };
    }
    return {
        enrichConversation,
        enrichConversations,
        getChatAssistantAvailability: async (conversation) => {
            const resolved = await resolveChatInvocation({
                conversation,
                contextLinks: Array.isArray(conversation.contextLinks) ? conversation.contextLinks : [],
            });
            return resolved.runtimeSource.descriptor.available && !resolved.availabilityError
                ? {
                    ...resolved.runtimeSource.descriptor,
                    available: true,
                }
                : {
                    ...resolved.runtimeSource.descriptor,
                    available: false,
                    error: resolved.availabilityError ?? resolved.runtimeSource.descriptor.error,
                };
        },
        generateChatAssistantReply: async (input) => {
            const result = await streamChatAssistantReply(input);
            if (result.outcome !== "completed") {
                throw new Error("Chat assistant reply was stopped before completion");
            }
            return result.reply;
        },
        streamChatAssistantReply,
    };
}
//# sourceMappingURL=chat-assistant.js.map