import { EventEmitter } from "node:events";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { createBuiltInFileTools, } from "./built-in-file-tools.js";
import { loadAcodeCommands } from "./commands/commandLoader.js";
import { ACodeCommandRegistry } from "./commands/commandRegistry.js";
import { loadAcodeConfig, loadAcodeSkills } from "./config.js";
import { ACodeHookBus, findDeniedHookResult } from "./hooks/hookBus.js";
function nowIso() {
    return new Date().toISOString();
}
function isRecord(value) {
    return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function isJsonObject(value) {
    return isRecord(value);
}
function normalizeText(value) {
    const text = String(value ?? "").trim();
    return text || undefined;
}
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 60_000;
const DEFAULT_PROVIDER_REQUEST_RETRY_COUNT = 2;
const DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS = 1000;
const DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS = 60_000;
const DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS = 30 * 60_000;
const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000;
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
async function withTimeout(operation, timeoutMs, label, onTimeout) {
    if (timeoutMs <= 0) {
        return await operation;
    }
    let timeout;
    try {
        return await new Promise((resolve, reject) => {
            timeout = setTimeout(() => {
                onTimeout?.();
                reject(new Error(`${label} timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            operation.then(resolve, reject);
        });
    }
    finally {
        if (timeout) {
            clearTimeout(timeout);
        }
    }
}
async function runBestEffort(label, operation, onError, timeoutMs = DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS) {
    try {
        await withTimeout(operation, timeoutMs, label);
    }
    catch (error) {
        onError(error instanceof Error ? error.message : String(error));
    }
}
function normalizeNonNegativeInteger(value, fallback) {
    if (value === 0 || value === "0") {
        return 0;
    }
    const numeric = typeof value === "number" ? value : Number(value);
    return Number.isFinite(numeric) && numeric > 0
        ? Math.floor(numeric)
        : fallback;
}
function normalizePositiveInteger(value, fallback) {
    const numeric = normalizeNonNegativeInteger(value, fallback);
    return numeric > 0 ? numeric : fallback;
}
async function delay(ms) {
    await new Promise((resolve) => setTimeout(resolve, ms));
}
function isMcpPollingToolName(toolName) {
    const normalized = toolName.trim().toLowerCase();
    return (normalized === "poll_message" ||
        normalized === "get_message" ||
        normalized.includes("poll_message"));
}
function isBoolean(value) {
    return typeof value === "boolean";
}
function modelSupportsImageInput(model, defaultProvider, providerOptions) {
    if (isBoolean(providerOptions?.supportsImageInput)) {
        return providerOptions.supportsImageInput;
    }
    const models = isRecord(defaultProvider) ? defaultProvider.models : undefined;
    const modelConfig = isRecord(models) ? models[model] : undefined;
    if (!isRecord(modelConfig)) {
        return false;
    }
    const modalities = modelConfig.modalities;
    if (!isRecord(modalities) || !Array.isArray(modalities.input)) {
        return false;
    }
    return modalities.input.some((item) => String(item || "")
        .trim()
        .toLowerCase() === "image");
}
/**
 * 解析 ACode 当前会话模型：优先显式启动参数，其次兼容 cc-switch 写入的
 * OpenCode-style `provider.default.model`，最后回落到旧版 top-level model。
 */
function resolveProviderConfig(config, acodeConfig) {
    const defaultProvider = acodeConfig.provider?.default;
    const providerOptions = defaultProvider?.options;
    const model = config.model ||
        defaultProvider?.model ||
        acodeConfig.model ||
        "acode-default";
    return {
        model,
        baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
        apiKey: normalizeText(providerOptions?.apiKey),
        systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
        stream: providerOptions?.stream !== false,
        requestTimeoutMs: normalizeNonNegativeInteger(providerOptions?.requestTimeoutMs ?? providerOptions?.timeoutMs, DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS),
        requestRetryCount: normalizeNonNegativeInteger(providerOptions?.requestRetryCount, DEFAULT_PROVIDER_REQUEST_RETRY_COUNT),
        mcpToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpToolTimeoutMs, DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS),
        mcpPollingToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpPollingToolTimeoutMs, DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS),
        supportsImageInput: modelSupportsImageInput(model, defaultProvider, providerOptions),
    };
}
function appendTextPart(parts, text) {
    const normalized = text.replace(/\s+/g, " ").trim();
    if (normalized) {
        parts.push({ type: "text", text: normalized });
    }
}
function toDataImageUrl(mediaType, rawBase64) {
    const trimmed = rawBase64.trim();
    if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(trimmed)) {
        return trimmed;
    }
    return `data:${mediaType || "image/png"};base64,${trimmed}`;
}
function isDataImageUrl(url) {
    return /^data:image\/[a-z0-9.+-]+;base64,/i.test(url.trim());
}
function assertFetchableImageUrl(url) {
    let parsedUrl;
    try {
        parsedUrl = new URL(url);
    }
    catch {
        throw new Error(`ACode image URL is invalid: ${url}`);
    }
    if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
        throw new Error(`ACode image URL protocol is not supported: ${parsedUrl.protocol}`);
    }
}
async function responseToBase64(response) {
    const arrayBuffer = await response.arrayBuffer();
    if (arrayBuffer.byteLength > MAX_INLINE_IMAGE_BYTES) {
        throw new Error(`ACode image is too large to inline: ${arrayBuffer.byteLength} bytes exceeds ${MAX_INLINE_IMAGE_BYTES} bytes`);
    }
    return Buffer.from(arrayBuffer).toString("base64");
}
async function materializeImageUrl(url) {
    const trimmedUrl = url.trim();
    if (isDataImageUrl(trimmedUrl)) {
        return trimmedUrl;
    }
    assertFetchableImageUrl(trimmedUrl);
    const response = await fetch(trimmedUrl);
    if (!response.ok) {
        throw new Error(`ACode image fetch failed: ${response.status} ${await response.text()}`);
    }
    const contentType = response.headers.get("content-type") || "";
    const mediaType = contentType.split(";")[0]?.trim().toLowerCase() || "";
    if (!mediaType.startsWith("image/")) {
        throw new Error(`ACode image URL did not return an image content type: ${contentType || "unknown"}`);
    }
    return `data:${mediaType};base64,${await responseToBase64(response)}`;
}
function normalizeImageAttachmentUrl(attachment) {
    const base64 = normalizeText(attachment.base64Data ?? attachment.base64 ?? attachment.data);
    if (base64) {
        return toDataImageUrl(normalizeText(attachment.mediaType ?? attachment.mimeType) || "image/png", base64);
    }
    return normalizeText(attachment.url ?? attachment.serverUrl);
}
function fileAttachmentText(name, url, mediaType, size) {
    const details = [
        name ? `name=${name}` : undefined,
        url ? `url=${url}` : undefined,
        mediaType ? `mediaType=${mediaType}` : undefined,
        size ? `size=${size}` : undefined,
    ].filter((item) => Boolean(item));
    return `[Attached file: ${details.join(", ")}]`;
}
function parseMarkerAttachmentMessage(message) {
    const markerRegex = /\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi;
    const parts = [];
    let hasImages = false;
    let lastIndex = 0;
    let match = markerRegex.exec(message);
    while (match !== null) {
        appendTextPart(parts, message.slice(lastIndex, match.index));
        const markerType = match[1];
        const markerValue = (match[2] || "").trim();
        if (markerType === "图片" && markerValue) {
            parts.push({ type: "image_url", image_url: { url: markerValue } });
            hasImages = true;
        }
        else if (markerType === "文件" && markerValue) {
            const [name = "", url = "", mediaType = "", size = ""] = markerValue
                .split("|")
                .map((item) => item.trim());
            appendTextPart(parts, fileAttachmentText(name, url, mediaType, size));
        }
        else {
            appendTextPart(parts, match[0]);
        }
        lastIndex = match.index + match[0].length;
        match = markerRegex.exec(message);
    }
    if (parts.length === 0) {
        return { content: message, hasImages: false };
    }
    appendTextPart(parts, message.slice(lastIndex));
    return { content: parts, hasImages };
}
function appendPolledMessagesContent(parts, parsed) {
    let hasImages = false;
    const messageGroups = [parsed.directMessages, parsed.groupMessages];
    for (const group of messageGroups) {
        if (!Array.isArray(group)) {
            continue;
        }
        for (const item of group) {
            if (!isRecord(item)) {
                continue;
            }
            const content = normalizeText(item.content);
            if (!content) {
                continue;
            }
            const senderName = normalizeText(item.senderName);
            appendTextPart(parts, senderName ? `Message from ${senderName}:` : "Message:");
            const parsedContent = parseAttachmentMessage(content);
            if (Array.isArray(parsedContent.content)) {
                parts.push(...parsedContent.content);
            }
            else {
                appendTextPart(parts, parsedContent.content);
            }
            hasImages = hasImages || parsedContent.hasImages;
        }
    }
    return hasImages;
}
function parseJsonAttachmentMessage(message) {
    let parsed;
    try {
        parsed = JSON.parse(message);
    }
    catch {
        return undefined;
    }
    if (!isRecord(parsed)) {
        return undefined;
    }
    const parts = [];
    let hasImages = appendPolledMessagesContent(parts, parsed);
    const text = normalizeText(parsed.text) || "";
    const rawAttachments = Array.isArray(parsed.attachments)
        ? parsed.attachments
        : [];
    appendTextPart(parts, text);
    for (const item of rawAttachments) {
        if (!isRecord(item)) {
            continue;
        }
        const attachment = item;
        const kind = String(attachment.type ?? attachment.kind ?? "").toLowerCase();
        if (kind === "image") {
            const imageUrl = normalizeImageAttachmentUrl(attachment);
            if (imageUrl) {
                parts.push({ type: "image_url", image_url: { url: imageUrl } });
                hasImages = true;
            }
            continue;
        }
        const fileUrl = normalizeText(attachment.url ?? attachment.serverUrl) || "";
        appendTextPart(parts, fileAttachmentText(normalizeText(attachment.name) || "", fileUrl, normalizeText(attachment.mediaType ?? attachment.mimeType) || "", normalizeText(attachment.size) || ""));
    }
    if (parts.length === 0) {
        return { content: message, hasImages: false };
    }
    return { content: parts, hasImages };
}
function parseAttachmentMessage(message) {
    return (parseJsonAttachmentMessage(message) ?? parseMarkerAttachmentMessage(message));
}
async function materializeImageContent(content) {
    if (!Array.isArray(content)) {
        return content;
    }
    const materializedParts = [];
    for (const part of content) {
        if (part.type === "text") {
            materializedParts.push(part);
            continue;
        }
        materializedParts.push({
            type: "image_url",
            image_url: { url: await materializeImageUrl(part.image_url.url) },
        });
    }
    return materializedParts;
}
function contentPreview(content) {
    if (typeof content === "string") {
        return content;
    }
    if (!Array.isArray(content)) {
        return "";
    }
    return content
        .map((part) => (part.type === "text" ? part.text : "[Image]"))
        .join(" ")
        .trim();
}
/**
 * 解析 ACode 工具权限配置。
 * 主流程：运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
 */
function resolvePermissionConfig(config, acodeConfig) {
    const configured = config.permissions ?? acodeConfig.permissions ?? {};
    const legacyAllow = config.permissions
        ? []
        : (acodeConfig.allowedTools ?? []);
    const legacyDeny = config.permissions ? [] : (acodeConfig.deniedTools ?? []);
    return {
        mode: configured.mode ?? "default",
        allow: [...(configured.allow ?? []), ...legacyAllow]
            .map((rule) => rule.trim())
            .filter(Boolean),
        deny: [...(configured.deny ?? []), ...legacyDeny]
            .map((rule) => rule.trim())
            .filter(Boolean),
    };
}
function wildcardMatches(pattern, value) {
    if (pattern === "*") {
        return true;
    }
    if (pattern.endsWith("*")) {
        return value.startsWith(pattern.slice(0, -1));
    }
    return pattern === value;
}
function toolPermissionNames(tool) {
    if (tool.kind === "builtin") {
        return [tool.exposedName, tool.toolName, `builtin.${tool.toolName}`];
    }
    return [
        tool.exposedName,
        tool.toolName,
        `${tool.serverName}.${tool.toolName}`,
        `mcp.${tool.serverName}.${tool.toolName}`,
    ];
}
function matchesToolRule(rule, tool) {
    return toolPermissionNames(tool).some((name) => wildcardMatches(rule, name));
}
function assertToolAllowedByRules(tool, rules, message) {
    if (rules.length > 0 && !rules.some((rule) => matchesToolRule(rule, tool))) {
        throw new Error(message);
    }
}
function assertToolPermission(tool, permissions, commandAllowedTools) {
    if (permissions.mode === "bypassPermissions") {
        return;
    }
    if (permissions.deny.some((rule) => matchesToolRule(rule, tool))) {
        throw new Error(`ACode tool denied by permission rule: ${tool.exposedName}`);
    }
    assertToolAllowedByRules(tool, permissions.allow, `ACode tool is not in allowed permission rules: ${tool.exposedName}`);
    assertToolAllowedByRules(tool, commandAllowedTools ?? [], `ACode slash command does not allow tool: ${tool.exposedName}`);
}
function isToolAllowed(tool, permissions, commandAllowedTools) {
    try {
        assertToolPermission(tool, permissions, commandAllowedTools);
        return true;
    }
    catch {
        return false;
    }
}
function normalizeChatCompletionsUrl(baseURL) {
    const trimmed = baseURL.replace(/\/+$/g, "");
    if (/\/chat\/completions$/i.test(trimmed)) {
        return trimmed;
    }
    return `${trimmed}/chat/completions`;
}
function summarizeProviderUrl(baseURL) {
    try {
        const url = new URL(baseURL);
        url.search = "";
        url.hash = "";
        const normalizedUrl = new URL(normalizeChatCompletionsUrl(url.toString()));
        return `${normalizedUrl.origin}${normalizedUrl.pathname}`;
    }
    catch {
        return normalizeChatCompletionsUrl(baseURL).replace(/([?&](?:api[_-]?key|token|authorization)=)[^&]+/gi, "$1[redacted]");
    }
}
function summarizeLastUserMessage(messages) {
    const lastUserMessage = [...messages]
        .reverse()
        .find((message) => message.role === "user");
    const content = contentPreview(lastUserMessage?.content ?? null).trim();
    if (!content)
        return undefined;
    return content.length > 300 ? `${content.slice(0, 300)}...` : content;
}
function coerceArguments(rawArguments) {
    try {
        const parsed = JSON.parse(rawArguments);
        return isJsonObject(parsed) ? parsed : {};
    }
    catch {
        return {};
    }
}
function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
    if (tool.toolName !== "Bash") {
        return fallbackTimeoutMs;
    }
    const rawTimeout = args.timeout_ms ?? args.timeoutMs;
    const numericTimeout = typeof rawTimeout === "number"
        ? rawTimeout
        : typeof rawTimeout === "string"
            ? Number(rawTimeout)
            : NaN;
    if (!Number.isFinite(numericTimeout) || numericTimeout <= 0) {
        return fallbackTimeoutMs;
    }
    return Math.min(Math.floor(numericTimeout), 10 * 60_000) + 2000;
}
function createFallbackResponse(message, state) {
    const trimmed = message.trim();
    return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要：${trimmed.slice(0, 200)}` : ""}`;
}
function toToolResultText(result) {
    if (!isRecord(result)) {
        return String(result ?? "");
    }
    const content = result.content;
    if (Array.isArray(content)) {
        const parts = content
            .map((item) => {
            if (!isRecord(item)) {
                return "";
            }
            if (typeof item.text === "string") {
                return item.text;
            }
            return JSON.stringify(item);
        })
            .filter(Boolean);
        if (parts.length > 0) {
            return parts.join("\n");
        }
    }
    return JSON.stringify(result);
}
function getErrorMessage(error) {
    return error instanceof Error ? error.message : String(error);
}
function isRetryableProviderRequestError(error) {
    const message = getErrorMessage(error).toLowerCase();
    return (message.includes("fetch failed") ||
        message.includes("timed out") ||
        message.includes("econnreset") ||
        message.includes("econnrefused") ||
        message.includes("etimedout") ||
        message.includes("enotfound") ||
        message.includes("502") ||
        message.includes("503") ||
        message.includes("504"));
}
function getErrorType(error) {
    if (typeof error === "object" && error !== null) {
        const code = Reflect.get(error, "code");
        if (typeof code === "string" && code) {
            return code;
        }
        const name = Reflect.get(error, "name");
        if (typeof name === "string" && name) {
            return name;
        }
    }
    return "tool_error";
}
function toToolErrorResultText(tool, error) {
    return JSON.stringify({
        ok: false,
        tool: tool.exposedName,
        error: {
            type: getErrorType(error),
            message: getErrorMessage(error),
        },
    });
}
function summarizeToolArguments(toolArguments) {
    const serialized = JSON.stringify(toolArguments);
    return serialized.length > 500 ? `${serialized.slice(0, 500)}…` : serialized;
}
function extractReasoningText(delta) {
    const candidates = [
        delta?.reasoning_content,
        delta?.reasoning_text,
        delta?.reasoning,
    ];
    for (const candidate of candidates) {
        if (typeof candidate === "string" && candidate) {
            return candidate;
        }
        if (isRecord(candidate)) {
            const text = candidate.text ?? candidate.content;
            if (typeof text === "string" && text) {
                return text;
            }
        }
    }
    return "";
}
function buildSkillPrompt(skills) {
    if (skills.length === 0) {
        return "";
    }
    return [
        "Available skills:",
        ...skills.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` : ""}`),
        "Use these skills as guidance when they match the user task.",
    ].join("\n");
}
function buildCommandPrompt(commands) {
    if (commands.length === 0) {
        return "";
    }
    return [
        "Available slash commands:",
        ...commands.map((command) => `- /${command.name}${command.description ? `: ${command.description}` : ""}`),
        "When the user invokes a slash command, ACode expands it before model execution.",
    ].join("\n");
}
function toOpenAiTools(tools) {
    return tools.map((tool) => ({
        type: "function",
        function: {
            name: tool.exposedName,
            description: tool.description || tool.toolName,
            parameters: tool.inputSchema,
        },
    }));
}
function normalizeToolName(serverName, toolName) {
    const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g, "_");
    return normalized.slice(0, 64) || "mcp_tool";
}
function buildRequestHeaders(provider) {
    const headers = {
        "Content-Type": "application/json",
    };
    if (provider.apiKey) {
        headers.Authorization = `Bearer ${provider.apiKey}`;
    }
    return headers;
}
function parseSseDataLines(chunk) {
    return chunk
        .split(/\r?\n/)
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice("data:".length).trim())
        .filter(Boolean);
}
function updateToolCallAccumulator(accumulators, delta) {
    const index = Number.isFinite(delta.index) ? Number(delta.index) : 0;
    const current = accumulators.get(index) ?? {
        id: delta.id || `call-${index}`,
        type: "function",
        function: { name: "", arguments: "" },
    };
    if (delta.id) {
        current.id = delta.id;
    }
    if (delta.function?.name) {
        current.function.name += delta.function.name;
    }
    if (delta.function?.arguments) {
        current.function.arguments += delta.function.arguments;
    }
    accumulators.set(index, current);
}
function toAccumulatedToolCalls(accumulators) {
    return [...accumulators.entries()]
        .sort(([left], [right]) => left - right)
        .map(([, value]) => value)
        .filter((toolCall) => toolCall.function.name.trim().length > 0);
}
export class ACodeRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
        this.state = {
            started: false,
            terminated: false,
            configMcpCount: 0,
            skills: [],
            provider: {
                model: "acode-default",
                stream: false,
                requestTimeoutMs: DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
                requestRetryCount: DEFAULT_PROVIDER_REQUEST_RETRY_COUNT,
                mcpToolTimeoutMs: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS,
                mcpPollingToolTimeoutMs: DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS,
                supportsImageInput: false,
            },
            permissions: { mode: "default", allow: [], deny: [] },
            mcpConnections: [],
            exposedTools: [],
            commands: [],
            commandRegistry: new ACodeCommandRegistry([]),
            conversation: [],
        };
        this.abortController = null;
        this.submitQueue = Promise.resolve();
        this.activeTurn = false;
        this.abortRequested = false;
        this.sessionCompleteEmitted = false;
        this.hookBus = new ACodeHookBus({
            sessionId: config.sessionId,
            onError: (event, error) => {
                this.emitAcodeEvent("error", {
                    message: `ACode hook failed: ${event.name}: ${error instanceof Error ? error.message : String(error)}`,
                });
            },
        });
        for (const handler of config.hooks ?? []) {
            this.hookBus.register(handler);
        }
    }
    emitAcodeEvent(type, data) {
        const event = {
            type,
            sessionId: this.config.sessionId,
            timestamp: nowIso(),
            data,
        };
        this.emit("event", event);
    }
    /**
     * 启动 ACode headless 会话。
     * 主流程：加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
     * 如有初始 prompt 则执行一轮响应。
     */
    async start() {
        const acodeConfig = await loadAcodeConfig(this.config.configPath);
        const skills = await loadAcodeSkills(this.config.skillsDir);
        const provider = resolveProviderConfig(this.config, acodeConfig);
        const permissions = resolvePermissionConfig(this.config, acodeConfig);
        const mcpConnections = await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
        const builtInTools = createBuiltInFileTools(this.config.workingDirectory);
        const exposedTools = [
            ...builtInTools,
            ...mcpConnections.flatMap((connection) => connection.tools),
        ];
        const commandDirs = this.resolveCommandDirs(acodeConfig);
        const commands = await loadAcodeCommands(commandDirs);
        const commandRegistry = new ACodeCommandRegistry(commands);
        const skillPrompt = buildSkillPrompt(skills);
        const commandPrompt = buildCommandPrompt(commands);
        const systemPrompt = [provider.systemPrompt, skillPrompt, commandPrompt]
            .map((item) => item?.trim())
            .filter((item) => Boolean(item))
            .join("\n\n");
        this.state = {
            started: true,
            terminated: false,
            configMcpCount: Object.keys(acodeConfig.mcpServers ?? {}).length,
            skills,
            provider,
            permissions,
            mcpConnections,
            exposedTools,
            commands,
            commandRegistry,
            conversation: systemPrompt
                ? [{ role: "system", content: systemPrompt }]
                : [],
        };
        await this.hookBus.dispatch("SessionStart", {
            workingDirectory: this.config.workingDirectory,
            model: provider.model,
            commandCount: commands.length,
        });
        this.emitAcodeEvent("session_started", {
            workingDirectory: this.config.workingDirectory,
            model: provider.model,
            commandCount: commands.length,
        });
        this.emitAcodeEvent("mcp_status", {
            configPath: this.config.configPath,
            installedCount: this.state.configMcpCount,
            servers: Object.keys(acodeConfig.mcpServers ?? {}),
            connectedCount: mcpConnections.length,
            builtInToolCount: builtInTools.length,
            toolCount: exposedTools.length,
            permissionMode: permissions.mode,
            allowedToolRules: permissions.allow,
            deniedToolRules: permissions.deny,
            tools: exposedTools.map((tool) => ({
                name: tool.exposedName,
                source: tool.kind === "mcp"
                    ? `${tool.serverName}.${tool.toolName}`
                    : `builtin.${tool.toolName}`,
            })),
        });
        this.emitAcodeEvent("skill_status", {
            skillsDir: this.config.skillsDir,
            installedCount: skills.length,
            skills: skills.map((skill) => ({
                name: skill.name,
                description: skill.description,
            })),
        });
        this.emitAcodeEvent("command_status", {
            action: "commands_loaded",
            installedCommandCount: commands.length,
            commandDirs,
            commands: commands.map((command) => ({
                name: command.name,
                description: command.description,
            })),
        });
        if (this.config.initialPrompt) {
            await this.submit(this.config.initialPrompt);
        }
    }
    /**
     * 处理一轮用户输入。
     * 主流程：将用户输入追加到会话 -> 调用 OpenAI-compatible 模型 -> 如模型请求
     * MCP tool call 则执行工具并继续同一轮，最后输出 assistant 文本与 usage。
     */
    async submit(message) {
        const queued = this.submitQueue.then(() => this.executeSubmit(message));
        this.submitQueue = queued.catch(() => undefined);
        return queued;
    }
    async executeSubmit(message) {
        if (!this.state.started || this.state.terminated) {
            throw new Error("ACode runtime is not active");
        }
        this.activeTurn = true;
        this.abortRequested = false;
        const originalPrompt = message.trim();
        try {
            const promptHookResults = await this.hookBus.dispatch("UserPromptSubmit", { prompt: originalPrompt });
            const promptDeny = findDeniedHookResult(promptHookResults);
            if (promptDeny) {
                this.emitAcodeEvent("error", {
                    message: promptDeny.message || "ACode prompt was denied by hook",
                });
                this.emitAcodeEvent("turn_complete", { failed: true });
                return;
            }
            const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
            const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
            const commandAllowedTools = expandedCommand?.command.allowedTools;
            if (expandedCommand) {
                this.emitAcodeEvent("command_status", {
                    action: "slash_command",
                    command: expandedCommand.command.name,
                    arguments: expandedCommand.argumentsText,
                    sourcePath: expandedCommand.command.sourcePath,
                });
            }
            if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
                const text = createFallbackResponse(trimmed, this.state);
                this.emitAcodeEvent("assistant_delta", { text });
                this.emitAcodeEvent("turn_complete", {
                    usage: { inputTokens: trimmed.length, outputTokens: text.length },
                });
                return;
            }
            const parsedContent = parseAttachmentMessage(trimmed);
            if (parsedContent.hasImages && !this.state.provider.supportsImageInput) {
                throw new Error("ACode provider/model is not configured for image input. Enable provider.default.models[model].modalities.input including 'image' or options.supportsImageInput=true.");
            }
            const userContent = await materializeImageContent(parsedContent.content);
            this.state.conversation.push({ role: "user", content: userContent });
            const result = await this.runModelTurn(commandAllowedTools);
            if (result.text && !this.state.provider.stream) {
                this.emitAcodeEvent("assistant_delta", { text: result.text });
            }
            this.emitAcodeEvent("turn_complete", {
                usage: result.usage ?? {
                    inputTokens: trimmed.length,
                    outputTokens: result.text.length,
                },
                ...(result.responseBody ? { responseBody: result.responseBody } : {}),
                ...(result.finishReason ? { finishReason: result.finishReason } : {}),
            });
        }
        catch (error) {
            if (this.abortRequested) {
                this.emitAcodeEvent("turn_complete", { aborted: true });
                return;
            }
            this.emitAcodeEvent("error", {
                message: error instanceof Error ? error.message : String(error),
            });
            this.emitAcodeEvent("turn_complete", { failed: true });
        }
        finally {
            this.activeTurn = false;
            this.abortRequested = false;
        }
    }
    /** 中止当前轮次；由执行轮次统一发出一个终态事件，避免 aborted/failed 双终态。 */
    async abort() {
        this.abortRequested = this.activeTurn;
        this.abortController?.abort();
        this.abortController = null;
    }
    /** 终止 ACode 会话并发出 session_complete。 */
    async stop() {
        if (this.state.terminated) {
            return;
        }
        this.state.terminated = true;
        this.abortRequested = this.activeTurn;
        this.abortController?.abort();
        this.abortController = null;
        await runBestEffort("ACode active turn shutdown", this.submitQueue, (message) => {
            this.emitAcodeEvent("error", { message });
        });
        await this.disconnectMcpServers();
        await runBestEffort("ACode Stop hooks", this.hookBus.dispatch("Stop", { reason: "session_stop" }), (message) => {
            this.emitAcodeEvent("error", { message });
        });
        this.state.terminated = true;
        if (!this.sessionCompleteEmitted) {
            this.sessionCompleteEmitted = true;
            this.emitAcodeEvent("session_complete", { exitCode: 0 });
        }
    }
    resolveCommandDirs(acodeConfig) {
        const configured = [
            ...(this.config.commandDirs ?? []),
            ...(acodeConfig.commandDirs ?? []),
            ...(acodeConfig.commands?.directories ?? []),
        ];
        return [
            ...new Set(configured.map((directory) => directory.trim()).filter(Boolean)),
        ];
    }
    async runModelTurn(commandAllowedTools) {
        let finalText = "";
        let finalUsage;
        let finalResponseBody;
        let finalFinishReason;
        for (let step = 0; step < 5; step += 1) {
            const response = await this.requestChatCompletion(commandAllowedTools);
            if (this.state.terminated) {
                throw new Error("ACode runtime was stopped during the active turn");
            }
            const choice = response.choices?.[0];
            finalResponseBody = JSON.stringify(response, null, 2);
            finalFinishReason =
                typeof choice?.finish_reason === "string"
                    ? choice.finish_reason
                    : undefined;
            const message = choice?.message;
            const content = typeof message?.content === "string" ? message.content : "";
            const toolCalls = Array.isArray(message?.tool_calls)
                ? message.tool_calls
                : [];
            finalUsage = {
                inputTokens: response.usage?.prompt_tokens ?? response.usage?.input_tokens ?? 0,
                outputTokens: response.usage?.completion_tokens ??
                    response.usage?.output_tokens ??
                    0,
            };
            this.state.conversation.push({
                role: "assistant",
                content: content || null,
                tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
            });
            if (content) {
                finalText = content;
            }
            if (toolCalls.length === 0) {
                return {
                    text: finalText,
                    usage: finalUsage,
                    responseBody: finalResponseBody,
                    finishReason: finalFinishReason,
                };
            }
            await this.handleToolCalls(toolCalls, commandAllowedTools);
        }
        return {
            text: finalText || "ACode 已达到本轮 MCP 工具调用上限。",
            usage: finalUsage,
            responseBody: finalResponseBody,
            finishReason: finalFinishReason,
        };
    }
    async requestChatCompletion(commandAllowedTools) {
        const provider = this.state.provider;
        const baseURL = provider.baseURL;
        if (!baseURL) {
            throw new Error("ACode provider baseURL is not configured");
        }
        const body = {
            model: provider.model,
            messages: this.state.conversation,
            stream: provider.stream,
        };
        const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
        if (allowedTools.length > 0) {
            body.tools = toOpenAiTools(allowedTools);
            body.tool_choice = "auto";
        }
        this.emitAcodeEvent("ai_request", {
            model: provider.model,
            endpoint: summarizeProviderUrl(baseURL),
            stream: provider.stream,
            messageCount: this.state.conversation.length,
            toolCount: allowedTools.length,
            lastUserMessagePreview: summarizeLastUserMessage(this.state.conversation),
            requestBody: JSON.stringify(body, null, 2),
        });
        const maxAttempts = provider.requestRetryCount + 1;
        for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
            this.abortController = new AbortController();
            let timedOut = false;
            try {
                return await withTimeout((async () => {
                    const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
                        method: "POST",
                        headers: buildRequestHeaders(provider),
                        body: JSON.stringify(body),
                        signal: this.abortController?.signal,
                    }), provider.requestTimeoutMs, "ACode provider request", () => {
                        timedOut = true;
                        this.abortController?.abort();
                    });
                    if (!response.ok) {
                        throw new Error(`ACode provider request failed: ${response.status} ${await response.text()}`);
                    }
                    const contentType = response.headers?.get?.("content-type") || "";
                    if (provider.stream &&
                        contentType.toLowerCase().includes("text/event-stream")) {
                        return await this.readChatCompletionStream(response, provider.requestTimeoutMs);
                    }
                    const payload = (await withTimeout(response.json(), provider.requestTimeoutMs, "ACode provider response body", () => {
                        timedOut = true;
                        this.abortController?.abort();
                    }));
                    return isRecord(payload) ? payload : {};
                })(), 0, "ACode provider request");
            }
            catch (error) {
                let requestError;
                if (error instanceof Error && error.name === "AbortError") {
                    requestError = timedOut
                        ? new Error(`ACode provider request timed out after ${provider.requestTimeoutMs}ms`)
                        : new Error("ACode provider request aborted");
                }
                else {
                    requestError =
                        error instanceof Error ? error : new Error(String(error));
                }
                const shouldRetry = attempt < maxAttempts &&
                    !this.abortRequested &&
                    !this.state.terminated &&
                    isRetryableProviderRequestError(requestError);
                if (!shouldRetry) {
                    throw requestError;
                }
                this.emitAcodeEvent("error", {
                    message: `ACode provider request attempt ${attempt}/${maxAttempts} failed; retrying: ${requestError.message}`,
                });
                await delay(DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS * attempt);
            }
            finally {
                this.abortController = null;
            }
        }
        throw new Error("ACode provider request failed after all retries");
    }
    async readChatCompletionStream(response, idleTimeoutMs) {
        if (!response.body) {
            throw new Error("ACode provider stream response has no body");
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        const toolCallAccumulators = new Map();
        let text = "";
        let buffer = "";
        const consumeFrame = (frame) => {
            for (const data of parseSseDataLines(frame)) {
                if (data === "[DONE]") {
                    continue;
                }
                const payload = JSON.parse(data);
                if (!isRecord(payload)) {
                    continue;
                }
                const choice = payload.choices?.[0];
                const delta = choice?.delta;
                const reasoningText = extractReasoningText(delta);
                if (reasoningText) {
                    this.emitAcodeEvent("thinking_delta", { text: reasoningText });
                }
                const content = typeof delta?.content === "string" ? delta.content : "";
                if (content) {
                    text += content;
                    this.emitAcodeEvent("assistant_delta", { text: content });
                }
                const toolDeltas = Array.isArray(delta?.tool_calls)
                    ? delta.tool_calls
                    : [];
                for (const toolDelta of toolDeltas) {
                    updateToolCallAccumulator(toolCallAccumulators, toolDelta);
                }
            }
        };
        while (true) {
            const { done, value } = await withTimeout(reader.read(), idleTimeoutMs, "ACode provider stream idle", () => this.abortController?.abort());
            if (done) {
                break;
            }
            buffer += decoder.decode(value, { stream: true });
            const frames = buffer.split(/\r?\n\r?\n/);
            buffer = frames.pop() ?? "";
            for (const frame of frames) {
                consumeFrame(frame);
            }
        }
        buffer += decoder.decode();
        if (buffer.trim()) {
            consumeFrame(buffer);
        }
        return {
            choices: [
                {
                    message: {
                        content: text || null,
                        tool_calls: toAccumulatedToolCalls(toolCallAccumulators),
                    },
                },
            ],
        };
    }
    async handleToolCalls(toolCalls, commandAllowedTools) {
        for (const toolCall of toolCalls) {
            const tool = this.state.exposedTools.find((item) => item.exposedName === toolCall.function.name);
            if (!tool) {
                this.state.conversation.push({
                    role: "tool",
                    tool_call_id: toolCall.id,
                    content: `ACode MCP tool not found: ${toolCall.function.name}`,
                });
                continue;
            }
            try {
                if (tool.kind === "builtin") {
                    const toolArguments = coerceArguments(toolCall.function.arguments);
                    assertToolPermission(tool, this.state.permissions, commandAllowedTools);
                    const preResults = await this.hookBus.dispatch("PreToolUse", {
                        source: "builtin",
                        toolName: tool.toolName,
                        exposedName: tool.exposedName,
                        arguments: toolArguments,
                    });
                    const denied = findDeniedHookResult(preResults);
                    if (denied) {
                        throw new Error(denied.message ||
                            `ACode tool denied by hook: ${tool.exposedName}`);
                    }
                    this.emitAcodeEvent("mcp_status", {
                        action: "call_tool",
                        server: "builtin",
                        tool: tool.toolName,
                        exposedName: tool.exposedName,
                        toolUseId: toolCall.id,
                        arguments: toolArguments,
                        argumentsPreview: summarizeToolArguments(toolArguments),
                    });
                    const result = await withTimeout(tool.execute(toolArguments), getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs), `ACode built-in tool ${tool.toolName}`);
                    await this.hookBus.dispatch("PostToolUse", {
                        source: "builtin",
                        toolName: tool.toolName,
                        exposedName: tool.exposedName,
                        result: toToolResultText(result),
                    });
                    const resultText = toToolResultText(result);
                    this.emitAcodeEvent("mcp_status", {
                        action: "tool_result",
                        server: "builtin",
                        tool: tool.toolName,
                        result: resultText,
                    });
                    this.state.conversation.push({
                        role: "tool",
                        tool_call_id: toolCall.id,
                        content: resultText,
                    });
                    await this.appendSyntheticMultimodalUserMessage(resultText);
                    continue;
                }
                const connection = this.state.mcpConnections.find((item) => item.serverName === tool.serverName);
                if (!connection) {
                    this.state.conversation.push({
                        role: "tool",
                        tool_call_id: toolCall.id,
                        content: `ACode MCP server not connected: ${tool.serverName}`,
                    });
                    continue;
                }
                const toolArguments = coerceArguments(toolCall.function.arguments);
                assertToolPermission(tool, this.state.permissions, commandAllowedTools);
                const preResults = await this.hookBus.dispatch("PreToolUse", {
                    source: "mcp",
                    serverName: tool.serverName,
                    toolName: tool.toolName,
                    exposedName: tool.exposedName,
                    arguments: toolArguments,
                });
                const denied = findDeniedHookResult(preResults);
                if (denied) {
                    throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
                }
                this.emitAcodeEvent("mcp_status", {
                    action: "call_tool",
                    server: tool.serverName,
                    tool: tool.toolName,
                    exposedName: tool.exposedName,
                    toolUseId: toolCall.id,
                    arguments: toolArguments,
                    argumentsPreview: summarizeToolArguments(toolArguments),
                });
                const toolTimeoutMs = isMcpPollingToolName(tool.toolName)
                    ? this.state.provider.mcpPollingToolTimeoutMs
                    : this.state.provider.mcpToolTimeoutMs;
                const result = await withTimeout(connection.client.callTool({
                    name: tool.toolName,
                    arguments: toolArguments,
                }, undefined, { timeout: toolTimeoutMs }), toolTimeoutMs, `ACode MCP tool ${tool.serverName}/${tool.toolName}`);
                await this.hookBus.dispatch("PostToolUse", {
                    source: "mcp",
                    serverName: tool.serverName,
                    toolName: tool.toolName,
                    exposedName: tool.exposedName,
                    result: toToolResultText(result),
                });
                const resultText = toToolResultText(result);
                this.emitAcodeEvent("mcp_status", {
                    action: "tool_result",
                    server: tool.serverName,
                    tool: tool.toolName,
                    result: resultText,
                });
                this.state.conversation.push({
                    role: "tool",
                    tool_call_id: toolCall.id,
                    content: resultText,
                });
                await this.appendSyntheticMultimodalUserMessage(resultText);
            }
            catch (error) {
                this.appendToolFailureResult(toolCall, tool, error);
            }
        }
    }
    async appendSyntheticMultimodalUserMessage(toolResultText) {
        const parsedContent = parseAttachmentMessage(toolResultText);
        if (!parsedContent.hasImages) {
            return;
        }
        if (!this.state.provider.supportsImageInput) {
            this.emitAcodeEvent("error", {
                message: "ACode skipped image content from tool result because the provider/model is not configured for image input.",
            });
            return;
        }
        const content = await materializeImageContent(parsedContent.content);
        this.state.conversation.push({ role: "user", content });
    }
    appendToolFailureResult(toolCall, tool, error) {
        const resultText = toToolErrorResultText(tool, error);
        this.emitAcodeEvent("error", {
            message: `ACode tool failed: ${tool.exposedName}: ${getErrorMessage(error)}`,
        });
        this.emitAcodeEvent("mcp_status", {
            action: "tool_result",
            server: tool.kind === "mcp" ? tool.serverName : "builtin",
            tool: tool.toolName,
            result: resultText,
            failed: true,
        });
        this.state.conversation.push({
            role: "tool",
            tool_call_id: toolCall.id,
            content: resultText,
        });
    }
    async connectMcpServers(servers, timeoutMs) {
        const connections = [];
        for (const [serverName, serverSpec] of Object.entries(servers)) {
            try {
                const client = new Client({ name: "acode", version: "0.1.0" });
                const transport = this.createMcpTransport(serverSpec);
                await withTimeout(client.connect(transport), timeoutMs, `ACode MCP server ${serverName} connect`);
                const { tools } = await withTimeout(client.listTools(), timeoutMs, `ACode MCP server ${serverName} listTools`);
                connections.push({
                    serverName,
                    client,
                    transport,
                    tools: tools.map((tool) => ({
                        serverName,
                        toolName: tool.name,
                        kind: "mcp",
                        exposedName: normalizeToolName(serverName, tool.name),
                        description: tool.description,
                        inputSchema: isJsonObject(tool.inputSchema)
                            ? tool.inputSchema
                            : { type: "object", properties: {} },
                    })),
                });
            }
            catch (error) {
                this.emitAcodeEvent("error", {
                    message: `ACode MCP server failed to connect: ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
                });
            }
        }
        return connections;
    }
    createMcpTransport(serverSpec) {
        if (serverSpec.type === "stdio" || serverSpec.command) {
            if (!serverSpec.command) {
                throw new Error("stdio MCP server requires command");
            }
            return new StdioClientTransport({
                command: serverSpec.command,
                args: serverSpec.args,
                cwd: serverSpec.cwd || this.config.workingDirectory,
                env: { ...(this.config.env ?? {}), ...(serverSpec.env ?? {}) },
                stderr: "pipe",
            });
        }
        if (!serverSpec.url) {
            throw new Error("remote MCP server requires url");
        }
        const requestInit = serverSpec.headers
            ? { headers: serverSpec.headers }
            : undefined;
        if (serverSpec.type === "sse") {
            return new SSEClientTransport(new URL(serverSpec.url), { requestInit });
        }
        return new StreamableHTTPClientTransport(new URL(serverSpec.url), {
            requestInit,
        });
    }
    async disconnectMcpServers() {
        const connections = this.state.mcpConnections;
        this.state.mcpConnections = [];
        this.state.exposedTools = [];
        await Promise.all(connections.map((connection) => runBestEffort(`ACode MCP server ${connection.serverName} close`, connection.transport.close(), (message) => {
            this.emitAcodeEvent("error", { message });
        }, this.state.provider.mcpToolTimeoutMs)));
    }
}
