"use strict";
/**
 * Module: llm/gen/mock
 *
 * Mock LLM generator that uses a static response database for testing and development.
 *
 * === MOCK DATABASE FORMAT (YAML) ===
 *
 * ```yaml
 * responses:
 *   # Comments are supported! Explain which test step uses each entry.
 *   # E.g., # docs/e2e-story-test/basics/reminders.md: Step 1
 *   - message: "hello"
 *     role: "user"
 *     response: "Hi there!"
 *     delayMs: 200
 *     chunkDelayMs: 20
 *   - message: "error text"
 *     role: "tool"
 *     response: "corrected response"
 * ```
 *
 * === ROLE-BASED MATCHING (EXACT ONLY) ===
 *
 * The mock looks at the last *input-like* message in context (no fallback):
 * - role='user' from prompting_msg → matches entries with role='user'
 * - role='tool' from func_result_msg / tellask_result_msg → matches entries with role='tool'
 *
 * Notes:
 * - Context tails may include reminders / transient guides; these are ignored for matching.
 *
 * Matching rules (strict exact matching, NO fallback):
 * 1. Exact: "tool:error text" matches response with message="error text" AND role="tool"
 * 2. Exact: "user:hello" matches response with message="hello" AND role="user"
 * 3. No match: returns fallback response explaining how to add mocks
 *
 * === BENEFITS OF YAML FORMAT ===
 *
 * - Comments: Document which test step uses each entry
 * - Easy cleanup: Identify unused entries by checking test step references
 * - Maintainability: Update comments when tests change, never lose referenced entries
 *
 * === EXAMPLE: TOOL ERROR RECOVERY ===
 *
 * // Turn 1: User asks, LLM makes syntax error
 * # docs/e2e-story-test/reminders.md: User creates reminders
 * - message: "create reminders"
 *   role: "user"
 *   response: "Call the function tool `add_reminder` with {\"content\":\"goals...\"}"
 *
 * // Turn 2: Tool error (func_result_msg with role='tool'), LLM self-corrects
 * # docs/e2e-story-test/reminders.md: Tool syntax error recovery
 * - message: "Error: Invalid args. Use: add_reminder({ content: string, scope?: \"dialog\" | \"task\" | \"agent\" }) (omitting scope means task)."
 *   role: "tool"
 *   response: "Call the function tool `add_reminder` with {\"content\":\"Goals...\",\"scope\":\"task\"}"
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockGen = void 0;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const yaml_1 = __importDefault(require("yaml"));
const log_1 = require("../../log");
const reply_prompt_copy_1 = require("../../runtime/reply-prompt-copy");
const gen_1 = require("../gen");
const stop_reason_i18n_1 = require("../stop-reason-i18n");
const RUNTIME_PROMPT_WRAPPER_PREFIXES = [
    '[System notice] Context state: 🔴 critical; user interjection received',
    '【系统提示】 上下文状态：🔴 告急；收到用户插话',
    reply_prompt_copy_1.ACTIVE_REPLY_TOOL_PREFIX_EN,
    reply_prompt_copy_1.ACTIVE_REPLY_TOOL_PREFIX_ZH,
    reply_prompt_copy_1.ANSWERING_REPLY_REMINDER_PREFIX_EN,
    reply_prompt_copy_1.ANSWERING_REPLY_REMINDER_PREFIX_ZH,
    reply_prompt_copy_1.NO_ACTIVE_REPLY_PREFIX_EN,
    reply_prompt_copy_1.NO_ACTIVE_REPLY_PREFIX_ZH,
    reply_prompt_copy_1.REPLY_SUPPRESSION_PREFIX_EN,
    reply_prompt_copy_1.REPLY_SUPPRESSION_PREFIX_ZH,
];
function normalizeDelayMs(value) {
    if (typeof value !== 'number' || !Number.isFinite(value))
        return 0;
    if (value <= 0)
        return 0;
    return Math.floor(value);
}
async function delayWithAbort(ms, abortSignal) {
    const normalized = normalizeDelayMs(ms);
    if (normalized <= 0)
        return;
    if (abortSignal?.aborted) {
        throw new Error('AbortError');
    }
    await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
            if (abortSignal) {
                abortSignal.removeEventListener('abort', onAbort);
            }
            resolve();
        }, normalized);
        const onAbort = () => {
            clearTimeout(timeout);
            if (abortSignal) {
                abortSignal.removeEventListener('abort', onAbort);
            }
            reject(new Error('AbortError'));
        };
        if (abortSignal) {
            abortSignal.addEventListener('abort', onAbort, { once: true });
        }
    });
}
class MockGen {
    get apiType() {
        return 'mock';
    }
    databaseCache = new Map();
    resolveInputForMatching(context) {
        for (let i = context.length - 1; i >= 0; i--) {
            const msg = context[i];
            if (!msg)
                continue;
            switch (msg.type) {
                case 'prompting_msg':
                case 'func_result_msg':
                case 'tellask_result_msg':
                case 'tellask_carryover_msg':
                    return { content: msg.content, role: msg.role };
                case 'environment_msg':
                case 'transient_guide_msg':
                case 'saying_msg':
                case 'thinking_msg':
                case 'func_call_msg':
                    break;
                default: {
                    const _exhaustive = msg;
                    throw new Error(`Unsupported chat message while selecting mock input: ${_exhaustive}`);
                }
            }
        }
        const lastMsg = context[context.length - 1];
        if (!lastMsg) {
            return { content: '', role: '' };
        }
        const content = (() => {
            switch (lastMsg.type) {
                case 'environment_msg':
                case 'transient_guide_msg':
                case 'prompting_msg':
                case 'saying_msg':
                case 'thinking_msg':
                case 'func_result_msg':
                case 'tellask_result_msg':
                case 'tellask_carryover_msg':
                    return lastMsg.content;
                case 'func_call_msg':
                    return '';
                default: {
                    const _exhaustive = lastMsg;
                    throw new Error(`Unsupported chat message while selecting mock input: ${_exhaustive}`);
                }
            }
        })();
        return { content, role: lastMsg.role };
    }
    async loadResponseDatabase(dbPath, modelName) {
        const cacheKey = `${dbPath}:${modelName}`;
        const dbFilePath = path_1.default.join(dbPath, `${modelName}.yaml`);
        const cached = this.databaseCache.get(cacheKey);
        if (cached) {
            try {
                const stats = await promises_1.default.stat(dbFilePath);
                if (stats.mtimeMs === cached.lastModified) {
                    return cached;
                }
            }
            catch {
                // Continue to reload
            }
        }
        try {
            const content = await promises_1.default.readFile(dbFilePath, 'utf-8');
            const rawDatabase = yaml_1.default.parse(content);
            // EXACT MATCHING ONLY: Store entries with role prefix
            // No fallback to message-only entries
            const lookupMap = new Map();
            for (const resp of rawDatabase.responses) {
                if (typeof resp.message !== 'string') {
                    throw new Error('message is required for mock response matching');
                }
                if (!resp.role) {
                    log_1.log.warn(`⚠️  Mock response without role: "${resp.message.substring(0, 50)}..."`);
                    log_1.log.warn('💡 All mock responses should have a "role" field (e.g., "user" or "tool")');
                }
                const key = resp.role
                    ? `${resp.role}:${resp.message.trim().toLowerCase()}`
                    : resp.message.trim().toLowerCase();
                const existing = lookupMap.get(key);
                if (existing) {
                    existing.push(resp);
                }
                else {
                    lookupMap.set(key, [resp]);
                }
            }
            const stats = await promises_1.default.stat(dbFilePath);
            const cachedDb = {
                filePath: dbFilePath,
                lastModified: stats.mtimeMs,
                lookupMap,
            };
            this.databaseCache.set(cacheKey, cachedDb);
            return cachedDb;
        }
        catch {
            log_1.log.warn(`⚠️  Mock database not found: ${dbFilePath}`);
            log_1.log.warn('💡 Create the file with: responses: [{ message: "...", role: "...", response: "..." }]');
            const emptyDb = {
                filePath: dbFilePath,
                lastModified: Date.now(),
                lookupMap: new Map(),
            };
            this.databaseCache.set(cacheKey, emptyDb);
            return emptyDb;
        }
    }
    responseMatchesContext(resp, context) {
        if (!resp.contextContains || resp.contextContains.length === 0) {
            return true;
        }
        const availableContents = [];
        for (const msg of context) {
            switch (msg.type) {
                case 'environment_msg':
                case 'transient_guide_msg':
                case 'prompting_msg':
                case 'saying_msg':
                case 'thinking_msg':
                case 'func_result_msg':
                case 'tellask_result_msg':
                case 'tellask_carryover_msg':
                    availableContents.push(msg.content);
                    break;
                case 'func_call_msg':
                    break;
                default: {
                    const _exhaustive = msg;
                    throw new Error(`Unsupported chat message while matching mock context: ${_exhaustive}`);
                }
            }
        }
        return resp.contextContains.every((required) => availableContents.some((content) => content.includes(required)));
    }
    normalizeFuncCallArgs(args) {
        if (typeof args === 'string') {
            return args;
        }
        if (args === undefined) {
            return '{}';
        }
        return JSON.stringify(args);
    }
    extractLastAssistantSaying(context) {
        for (let i = context.length - 1; i >= 0; i -= 1) {
            const msg = context[i];
            if (msg?.type !== 'saying_msg') {
                continue;
            }
            const trimmed = msg.content.trim();
            if (trimmed !== '') {
                return trimmed;
            }
        }
        return null;
    }
    extractCurrentPromptMsgId(context) {
        for (let index = context.length - 1; index >= 0; index -= 1) {
            const msg = context[index];
            if (msg?.type === 'prompting_msg') {
                const trimmed = msg.msgId.trim();
                return trimmed === '' ? undefined : trimmed;
            }
        }
        return undefined;
    }
    buildReplyToolReminderAutoResponse(input, role, context) {
        if (role !== 'user' || !(0, reply_prompt_copy_1.isReplyToolReminderPromptContent)(input)) {
            return null;
        }
        const toolMatch = input.match(/`(replyTellask(?:Sessionless|Back)?)`/);
        if (!toolMatch) {
            return null;
        }
        const replyContent = this.extractLastAssistantSaying(context);
        if (!replyContent) {
            return null;
        }
        const promptMsgId = this.extractCurrentPromptMsgId(context);
        return {
            message: input,
            role,
            response: '',
            funcCalls: [
                {
                    id: promptMsgId ? `mock_reply_${promptMsgId}` : undefined,
                    name: toolMatch[1],
                    arguments: { replyContent },
                },
            ],
        };
    }
    /**
     * Find matching response using EXACT last message only.
     * NO fallback to message-only matching - if exact match fails, return null.
     */
    findMatchingResponse(database, input, role, context) {
        if (!role) {
            throw new Error('role is required for mock response matching');
        }
        const normalizedInputs = (() => {
            const variants = new Set();
            const trimmed = input.trim();
            if (trimmed !== '') {
                variants.add(trimmed.toLowerCase());
            }
            const tellaskStatusPrefixes = [
                '\n[Dominds 诉请状态]\n',
                '\n[Dominds tellask status]\n',
                '\n[Dominds 等待人类回复]\n',
                '\n[Dominds waiting for human answer]\n',
                '\n[Dominds 等待队友回贴]\n',
                '\n[Dominds waiting for teammate reply]\n',
                '\n[Dominds 队友回贴状态]\n',
                '\n[Dominds teammate reply status]\n',
            ];
            const tellaskStatusIndex = Math.max(...tellaskStatusPrefixes.map((prefix) => trimmed.lastIndexOf(prefix)));
            if (tellaskStatusIndex >= 0) {
                const strippedTellaskStatus = trimmed.slice(0, tellaskStatusIndex).trim();
                if (strippedTellaskStatus !== '') {
                    variants.add(strippedTellaskStatus.toLowerCase());
                }
            }
            const hasRuntimeWrapper = RUNTIME_PROMPT_WRAPPER_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
            if (!hasRuntimeWrapper) {
                return [...variants];
            }
            const separatorIndex = trimmed.indexOf('\n\n');
            if (separatorIndex < 0) {
                return [...variants];
            }
            const stripped = trimmed.slice(separatorIndex + 2).trim();
            if (stripped !== '') {
                variants.add(stripped.toLowerCase());
            }
            return [...variants];
        })();
        for (const normalizedInput of normalizedInputs) {
            const exactKey = `${role}:${normalizedInput}`;
            const candidates = database.lookupMap.get(exactKey);
            if (!candidates || candidates.length === 0) {
                continue;
            }
            for (let i = candidates.length - 1; i >= 0; i--) {
                const candidate = candidates[i];
                if (candidate && this.responseMatchesContext(candidate, context)) {
                    return candidate;
                }
            }
        }
        return null;
    }
    makeFallbackResponse(dbPath, input, role, modelName) {
        return `🤖 **Mock Response Not Found**

No mock response for the specified message with role=${JSON.stringify(role)} !!

Database: \`${path_1.default.join(dbPath, `${modelName}.yaml`)}\`

Add to database:
\`\`\`yaml
responses:
  - message: ${JSON.stringify(input.trim())}
    role: ${JSON.stringify(role)}
    response: "Your response here"
\`\`\``;
    }
    async genToReceiver(providerConfig, agent, systemPrompt, _funcTools, _requestContext, context, receiver, _genseq, abortSignal) {
        const dbPath = providerConfig.baseUrl;
        if (!agent.model) {
            throw new Error('Model undefined for agent: ' + agent.id);
        }
        const modelName = agent.model;
        const { content, role } = this.resolveInputForMatching(context);
        if (abortSignal?.aborted) {
            throw new Error('AbortError');
        }
        const db = await this.loadResponseDatabase(dbPath, modelName);
        const matched = this.findMatchingResponse(db, content, role, context) ??
            this.buildReplyToolReminderAutoResponse(content, role, context);
        await delayWithAbort(matched?.delayMs ?? 0, abortSignal);
        const thinkingText = matched?.omitDefaultThinking === true
            ? (matched.thinkingResponse ?? '')
            : (matched?.thinkingResponse ?? `[${modelName}] ${content.substring(0, 50) || '(empty)'}`);
        if (thinkingText !== '') {
            await receiver.thinkingStart();
            await receiver.thinkingChunk(thinkingText);
            await receiver.thinkingFinish();
        }
        if (matched?.streamError) {
            if (matched.emitStreamErrorBeforeThrow && receiver.streamError) {
                await receiver.streamError(matched.streamError);
                throw new gen_1.LlmStreamErrorEmittedError({
                    detail: matched.streamError,
                    i18nStopReason: (0, stop_reason_i18n_1.buildHumanSystemStopReasonTextI18n)({
                        detail: matched.streamError,
                        fallbackKind: 'request_failed',
                    }),
                });
            }
            throw new Error(matched.streamError);
        }
        const responseText = matched?.response ?? this.makeFallbackResponse(dbPath, content, role, modelName);
        let usage = { kind: 'unavailable' };
        if (matched && matched.usageUnavailable === true) {
            usage = { kind: 'unavailable' };
        }
        else if (matched && matched.usage) {
            const promptTokens = matched.usage.promptTokens;
            const completionTokens = typeof matched.usage.completionTokens === 'number' ? matched.usage.completionTokens : 0;
            const totalTokens = typeof matched.usage.totalTokens === 'number'
                ? matched.usage.totalTokens
                : promptTokens + completionTokens;
            usage = {
                kind: 'available',
                promptTokens,
                completionTokens,
                totalTokens,
            };
        }
        else {
            const promptChars = systemPrompt.length +
                context.reduce((acc, msg) => {
                    switch (msg.type) {
                        case 'environment_msg':
                        case 'transient_guide_msg':
                        case 'prompting_msg':
                        case 'saying_msg':
                        case 'thinking_msg':
                        case 'func_result_msg':
                        case 'tellask_result_msg':
                        case 'tellask_carryover_msg':
                            return acc + msg.content.length;
                        case 'func_call_msg':
                            return acc + msg.name.length + msg.arguments.length;
                        default: {
                            const _exhaustive = msg;
                            throw new Error(`Unsupported chat message: ${_exhaustive}`);
                        }
                    }
                }, 0);
            const promptTokens = Math.ceil(promptChars / 4);
            const completionTokens = Math.ceil(responseText.length / 4);
            usage = {
                kind: 'available',
                promptTokens,
                completionTokens,
                totalTokens: promptTokens + completionTokens,
            };
        }
        if (responseText !== '') {
            await receiver.sayingStart();
            const chunkDelayMs = normalizeDelayMs(matched?.chunkDelayMs ?? 0);
            const words = responseText.split(/(\s+)/);
            for (const word of words) {
                if (abortSignal?.aborted) {
                    throw new Error('AbortError');
                }
                await receiver.sayingChunk(word);
                if (chunkDelayMs > 0) {
                    await delayWithAbort(chunkDelayMs, abortSignal);
                }
            }
            await receiver.sayingFinish();
        }
        if (matched?.answeringResponse !== undefined && matched.answeringResponse.trim() !== '') {
            await receiver.answering?.(matched.answeringResponse);
        }
        const funcCalls = matched?.funcCalls ?? [];
        for (let i = 0; i < funcCalls.length; i++) {
            const call = funcCalls[i];
            if (!call || typeof call.name !== 'string' || call.name.trim() === '') {
                throw new Error(`Invalid mock funcCalls[${String(i)}].name`);
            }
            const callId = typeof call.id === 'string' && call.id.trim() !== ''
                ? call.id
                : `mock_func_${String(i + 1)}_${call.name}`;
            await receiver.funcCall(callId, call.name, this.normalizeFuncCallArgs(call.arguments));
        }
        const invalidFuncCalls = matched?.invalidFuncCalls ?? [];
        for (const call of invalidFuncCalls) {
            await receiver.invalidFuncCall?.(call);
        }
        return { usage, llmGenModel: modelName };
    }
    async genMoreMessages(providerConfig, agent, systemPrompt, _funcTools, _requestContext, context, genseq, abortSignal) {
        if (abortSignal?.aborted) {
            throw new Error('AbortError');
        }
        const dbPath = providerConfig.baseUrl;
        if (!agent.model) {
            throw new Error('Model undefined for agent: ' + agent.id);
        }
        const modelName = agent.model;
        const { content, role } = this.resolveInputForMatching(context);
        try {
            const db = await this.loadResponseDatabase(dbPath, modelName);
            const matched = this.findMatchingResponse(db, content, role, context) ??
                this.buildReplyToolReminderAutoResponse(content, role, context);
            await delayWithAbort(matched?.delayMs ?? 0, abortSignal);
            const responseText = matched?.response ?? this.makeFallbackResponse(dbPath, content, role, modelName);
            const thinkingText = matched?.omitDefaultThinking === true
                ? (matched.thinkingResponse ?? '')
                : (matched?.thinkingResponse ?? `[${modelName}] ${content.substring(0, 100)}`);
            const thinking = thinkingText === ''
                ? undefined
                : {
                    type: 'thinking_msg',
                    role: 'assistant',
                    genseq,
                    content: thinkingText,
                };
            const saying = matched?.streamError || responseText !== ''
                ? {
                    type: 'saying_msg',
                    role: 'assistant',
                    genseq,
                    content: matched?.streamError
                        ? `❌ Mock Error: ${matched.streamError}`
                        : responseText,
                }
                : undefined;
            let usage = { kind: 'unavailable' };
            if (matched && matched.usageUnavailable === true) {
                usage = { kind: 'unavailable' };
            }
            else if (matched && matched.usage) {
                const promptTokens = matched.usage.promptTokens;
                const completionTokens = typeof matched.usage.completionTokens === 'number' ? matched.usage.completionTokens : 0;
                const totalTokens = typeof matched.usage.totalTokens === 'number'
                    ? matched.usage.totalTokens
                    : promptTokens + completionTokens;
                usage = {
                    kind: 'available',
                    promptTokens,
                    completionTokens,
                    totalTokens,
                };
            }
            else {
                const promptChars = systemPrompt.length +
                    context.reduce((acc, msg) => {
                        switch (msg.type) {
                            case 'environment_msg':
                            case 'transient_guide_msg':
                            case 'prompting_msg':
                            case 'saying_msg':
                            case 'thinking_msg':
                            case 'func_result_msg':
                            case 'tellask_result_msg':
                            case 'tellask_carryover_msg':
                                return acc + msg.content.length;
                            case 'func_call_msg':
                                return acc + msg.name.length + msg.arguments.length;
                            default: {
                                const _exhaustive = msg;
                                throw new Error(`Unsupported chat message: ${_exhaustive}`);
                            }
                        }
                    }, 0);
                const promptTokens = Math.ceil(promptChars / 4);
                const completionTokens = Math.ceil(responseText.length / 4);
                usage = {
                    kind: 'available',
                    promptTokens,
                    completionTokens,
                    totalTokens: promptTokens + completionTokens,
                };
            }
            const funcMsgs = matched?.funcCalls?.map((call, idx) => {
                if (!call || typeof call.name !== 'string' || call.name.trim() === '') {
                    throw new Error(`Invalid mock funcCalls[${String(idx)}].name`);
                }
                return {
                    type: 'func_call_msg',
                    role: 'assistant',
                    genseq,
                    id: typeof call.id === 'string' && call.id.trim() !== ''
                        ? call.id
                        : `mock_func_${String(idx + 1)}_${call.name}`,
                    name: call.name,
                    arguments: this.normalizeFuncCallArgs(call.arguments),
                };
            }) ?? [];
            const invalidFuncCalls = matched?.invalidFuncCalls ?? [];
            const invalidFuncCallOutputs = invalidFuncCalls.map((call) => ({
                kind: 'invalid_func_call',
                call,
            }));
            const answeringOutputs = matched?.answeringResponse !== undefined && matched.answeringResponse.trim() !== ''
                ? [{ kind: 'answering', content: matched.answeringResponse }]
                : [];
            const messages = thinking !== undefined
                ? saying
                    ? [thinking, saying, ...funcMsgs]
                    : [thinking, ...funcMsgs]
                : saying
                    ? [saying, ...funcMsgs]
                    : [...funcMsgs];
            return {
                messages,
                ...(invalidFuncCallOutputs.length > 0 || answeringOutputs.length > 0
                    ? {
                        outputs: [
                            ...messages.map((message) => ({ kind: 'message', message })),
                            ...answeringOutputs,
                            ...invalidFuncCallOutputs,
                        ],
                    }
                    : {}),
                usage,
                llmGenModel: modelName,
            };
        }
        catch (err) {
            const errMsg = err instanceof Error ? err.message : String(err);
            const saying = {
                type: 'saying_msg',
                role: 'assistant',
                genseq,
                content: `❌ Mock Error: ${errMsg}`,
            };
            const thinking = {
                type: 'thinking_msg',
                role: 'assistant',
                content: `[${modelName}] error: ${errMsg}`,
                genseq,
            };
            return {
                messages: [thinking, saying],
                usage: { kind: 'unavailable' },
                llmGenModel: modelName,
            };
        }
    }
}
exports.MockGen = MockGen;
