package tech.easyflow.agent.runtime;

import com.easyagents.agent.runtime.AgentInitRequest;
import com.easyagents.agent.runtime.AgentRuntime;
import com.easyagents.agent.runtime.AgentRuntimeContext;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
import com.easyagents.agent.runtime.message.AgentMessage;
import com.easyagents.agent.runtime.message.AgentMessageRole;
import com.easyagents.agent.runtime.message.AgentMediaBlock;
import com.easyagents.agent.runtime.message.AgentTextBlock;
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.enums.AgentToolType;
import tech.easyflow.agent.distributed.AgentRuntimeCommandAction;
import tech.easyflow.agent.distributed.AgentRuntimeCommandProducer;
import tech.easyflow.agent.distributed.AgentRuntimeRoute;
import tech.easyflow.agent.distributed.AgentRuntimeRouteRegistry;
import tech.easyflow.agent.runtime.event.AgentRunEventRecorder;
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
import tech.easyflow.agent.runtime.lock.AgentRunLock;
import tech.easyflow.agent.runtime.session.EasyFlowAgentSessionStore;
import tech.easyflow.agent.runtime.media.AgentBoundMedia;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.runtime.media.AgentMediaUploadRecord;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.chat.protocol.ChatDomain;
import tech.easyflow.core.chat.protocol.ChatEnvelope;
import tech.easyflow.core.chat.protocol.ChatType;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.runtime.*;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

/**
 * Agent 管理端运行服务。
 */
@Service
public class AgentRunService {

    private static final Logger LOG = LoggerFactory.getLogger(AgentRunService.class);
    private static final String ASSISTANT_CODE = "AGENT";
    private static final String DRAFT_ASSISTANT_CODE = "AGENT_DRAFT";

    @Resource
    private AgentService agentService;
    @Resource
    private AgentRuntimeCompiler agentRuntimeCompiler;
    @Resource
    private AgentRuntimeFactory agentRuntimeFactory;
    @Resource
    private AgentChatCapabilityService agentChatCapabilityService;
    @Resource
    private EasyFlowAgentSessionStore easyFlowAgentSessionStore;
    @Resource
    private AgentSessionStore draftAgentSessionStore;
    @Resource
    private AgentRunRegistry agentRunRegistry;
    @Resource
    private AgentRuntimeRouteRegistry agentRuntimeRouteRegistry;
    @Resource
    private AgentRuntimeCommandProducer agentRuntimeCommandProducer;
    @Resource
    private AgentRunLock agentRunLock;
    @Resource
    private AgentHitlPendingService agentHitlPendingService;
    @Resource
    private AgentRunEventRecorder agentRunEventRecorder;
    @Resource
    private ChatRuntimeManager chatRuntimeManager;
    @Resource
    private ChatSessionQueryService chatSessionQueryService;
    @Resource(name = "sseThreadPool")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Resource
    private ResourceAccessService resourceAccessService;
    @Resource
    private WorkflowService workflowService;
    @Resource
    private PluginItemService pluginItemService;
    @Resource
    private McpService mcpService;
    @Resource
    private DocumentCollectionService documentCollectionService;
    @Resource
    private ModelService modelService;
    @Resource
    private AgentMediaService agentMediaService;

    /**
     * 启动 Agent 聊天。
     *
     * @param chatRequest 聊天请求
     * @return SSE Emitter
     */
    public SseEmitter chat(AgentChatRequest chatRequest) {
        // 判定Agent是否对当前用户可用
        validateChatRequest(chatRequest);
        LoginAccount account = requireCurrentLoginAccount();
        Agent liveAgent = agentService.getById(chatRequest.getAgentId());
        if (liveAgent == null) {
            throw new BusinessException("Agent 不存在");
        }
        resourceAccessService.assertAccess(CategoryResourceType.AGENT, liveAgent, ResourceAction.USE, "无权限运行该 Agent");
        assertAgentRunnable(liveAgent);
        // 创建会话 ID
        BigInteger sessionId = chatRequest.getSessionId() == null
                ? BigInteger.valueOf(new SnowFlakeIDKeyGenerator().nextId())
                : chatRequest.getSessionId();
        ChatSessionSummary existingSession = resolveExistingSession(account, sessionId, chatRequest.getAgentId());
        // 获取 Agent 发布快照
        Agent agent = agentService.getPublishedView(chatRequest.getAgentId());
        AgentChatCapabilityService.AgentChatCapabilityResolution capabilityResolution =
                agentChatCapabilityService.apply(agent, chatRequest.getCapabilities(), account);
        agent = capabilityResolution.agent();
        assertImageCapability(agent, chatRequest.getImageUploadIds());
        List<AgentMediaUploadRecord> mediaUploads = agentMediaService.requireUploads(
                chatRequest.getImageUploadIds(), AgentMediaService.MODE_FORMAL,
                chatRequest.getAgentId().toString(), sessionId.toString(), account);
        String requestId = UUID.randomUUID().toString();
        String traceId = UUID.randomUUID().toString();
        // 组建会话上下文必要信息
        ChatRuntimeContext chatContext = buildChatRuntimeContext(agent, sessionId, chatRequest.getPrompt(), account);
        if (capabilityResolution.knowledgeCapabilityProvided()) {
            chatContext.getExt().put(ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS, capabilityResolution.extraKnowledgeIds());
        }
        applyFormalSessionTitle(chatContext, chatRequest.getPrompt(), existingSession);
        // 执行对话
        return run(agent, chatRequest.getPrompt(), mediaUploads, account, requestId, traceId, sessionId.toString(),
                ASSISTANT_CODE, chatContext, true, easyFlowAgentSessionStore);
    }

    /**
     * 启动 Agent 草稿态纯文本试用。
     *
     * @param draftRequest 草稿试用请求
     * @return SSE Emitter
     */
    public SseEmitter chatDraft(AgentDraftChatRequest draftRequest) {
        validateDraftChatRequest(draftRequest);
        LoginAccount account = requireCurrentLoginAccount();
        Agent agent = buildDraftAgent(draftRequest, account);
        validateDraftResources(agent);

        String runtimeSessionId = draftRequest.getSessionId();
        if (runtimeSessionId == null || runtimeSessionId.isBlank()) {
            runtimeSessionId = "agent-draft-" + new SnowFlakeIDKeyGenerator().nextId();
        }
        assertImageCapability(agent, draftRequest.getImageUploadIds());
        List<AgentMediaUploadRecord> mediaUploads = agentMediaService.requireUploads(
                draftRequest.getImageUploadIds(), AgentMediaService.MODE_DRAFT,
                agent.getId().toString(), runtimeSessionId, account);
        BigInteger chatSessionId = BigInteger.valueOf(new SnowFlakeIDKeyGenerator().nextId());
        String requestId = UUID.randomUUID().toString();
        String traceId = UUID.randomUUID().toString();
        ChatRuntimeContext chatContext = buildChatRuntimeContext(agent, chatSessionId, draftRequest.getPrompt(), account, DRAFT_ASSISTANT_CODE);
        return run(agent, draftRequest.getPrompt(), mediaUploads, account, requestId, traceId, runtimeSessionId,
                DRAFT_ASSISTANT_CODE, chatContext, false, draftAgentSessionStore);
    }

    private SseEmitter run(Agent agent,
                           String prompt,
                           List<AgentMediaUploadRecord> mediaUploads,
                           LoginAccount account,
                           String requestId,
                           String traceId,
                           String runtimeSessionId,
                           String assistantCode,
                           ChatRuntimeContext chatContext,
                           boolean persistChatlog,
                           AgentSessionStore runtimeSessionStore) {
        ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
        // 获取会话锁
        AgentRunLock.Handle lockHandle = acquireRunLock(agent, runtimeSessionId);
        boolean submitted = false;
        try {
            List<AgentBoundMedia> boundMedia;
            if (persistChatlog) {
                // 持久化会话初始信息
                chatRuntimeManager.prepareSession(chatContext);
                if (!sendSessionCreated(chatSseEmitter, chatContext.getSessionId())) {
                    chatRuntimeManager.recordFailure(chatContext, new BusinessException("客户端连接已断开，Agent 运行已取消"));
                    return chatSseEmitter.getEmitter();
                }
                BigInteger messageId = BigInteger.valueOf(new SnowFlakeIDKeyGenerator().nextId());
                boundMedia = agentMediaService.bindFormal(mediaUploads, chatContext.getSessionId().toString(),
                        messageId.toString(), account);
                chatRuntimeManager.recordUserMessage(chatContext,
                        buildUserRuntimeMessage(chatContext, messageId, prompt, boundMedia));
                if (!sendInputAccepted(chatSseEmitter, chatContext.getSessionId(), messageId, boundMedia)) {
                    chatRuntimeManager.recordFailure(chatContext, new BusinessException("客户端连接已断开，Agent 运行已取消"));
                    return chatSseEmitter.getEmitter();
                }
            } else {
                boundMedia = agentMediaService.bindDraft(mediaUploads);
                if (!sendInputAccepted(chatSseEmitter, null, null, boundMedia)) {
                    return chatSseEmitter.getEmitter();
                }
            }
            AgentMessage userMessage = buildAgentMessage(prompt, boundMedia);
            threadPoolTaskExecutor.execute(() -> startRuntime(agent, userMessage, account, requestId, traceId, runtimeSessionId,
                    assistantCode, chatContext, chatSseEmitter, persistChatlog, runtimeSessionStore, lockHandle));
            submitted = true;
            return chatSseEmitter.getEmitter();
        } finally {
            // 释放锁
            if (!submitted && lockHandle != null) {
                lockHandle.release();
            }
        }
    }

    /**
     * 清理草稿试运行会话。
     *
     * @param sessionId 草稿会话 ID
     */
    public void clearDraftSession(String sessionId) {
        if (sessionId == null || sessionId.isBlank()) {
            throw new BusinessException("Agent 草稿会话 ID 不能为空");
        }
        if (!sessionId.startsWith("agent-draft-")) {
            throw new BusinessException("仅允许清理 Agent 草稿试运行会话");
        }
        LoginAccount account = requireCurrentLoginAccount();
        clearDraftSessionInternal(sessionId, account.getId() == null ? null : account.getId().toString());
    }

    private void clearDraftSessionInternal(String sessionId, String userId) {
        agentRunRegistry.cancelSession(sessionId, userId);
        draftAgentSessionStore.delete(sessionId);
    }

    /**
     * 批准工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     */
    public void approve(String requestId, String resumeToken) {
        LoginAccount account = requireCurrentLoginAccount();
        approveRuntime(requestId, resumeToken, account.getId(), account.getId() == null ? null : account.getId().toString());
    }

    private void approveRuntime(String requestId, String resumeToken, BigInteger operatorId, String userId) {
        if (!agentRunRegistry.containsResumeTarget(requestId, resumeToken)) {
            dispatchRemoteRuntimeCommand(requestId, resumeToken, AgentRuntimeCommandAction.APPROVE, null, operatorId, userId);
            return;
        }
        approveRuntimeLocal(requestId, resumeToken, operatorId, userId);
    }

    /**
     * 在当前节点批准工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     * @param operatorId 操作人 ID
     * @param userId 用户 ID
     */
    public void approveRuntimeLocal(String requestId, String resumeToken, BigInteger operatorId, String userId) {
        if (agentRunRegistry.isDraftResumeTarget(requestId, resumeToken)) {
            agentRunRegistry.approve(requestId, resumeToken, userId);
            return;
        }
        agentRunRegistry.approve(requestId, resumeToken, userId,
                () -> agentHitlPendingService.approve(resumeToken, operatorId));
    }

    /**
     * 拒绝工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     * @param reason 拒绝原因
     */
    public void reject(String requestId, String resumeToken, String reason) {
        LoginAccount account = requireCurrentLoginAccount();
        rejectRuntime(requestId, resumeToken, reason, account.getId(), account.getId() == null ? null : account.getId().toString());
    }

    private void rejectRuntime(String requestId, String resumeToken, String reason, BigInteger operatorId, String userId) {
        if (!agentRunRegistry.containsResumeTarget(requestId, resumeToken)) {
            dispatchRemoteRuntimeCommand(requestId, resumeToken, AgentRuntimeCommandAction.REJECT, reason, operatorId, userId);
            return;
        }
        rejectRuntimeLocal(requestId, resumeToken, reason, operatorId, userId);
    }

    /**
     * 在当前节点拒绝工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     * @param reason 拒绝原因
     * @param operatorId 操作人 ID
     * @param userId 用户 ID
     */
    public void rejectRuntimeLocal(String requestId, String resumeToken, String reason, BigInteger operatorId, String userId) {
        if (agentRunRegistry.isDraftResumeTarget(requestId, resumeToken)) {
            agentRunRegistry.reject(requestId, resumeToken, userId, reason);
            return;
        }
        agentRunRegistry.reject(requestId, resumeToken, userId, reason,
                () -> agentHitlPendingService.reject(resumeToken, operatorId, reason));
    }

    private void dispatchRemoteRuntimeCommand(String requestId,
                                              String resumeToken,
                                              AgentRuntimeCommandAction action,
                                              String reason,
                                              BigInteger operatorId,
                                              String userId) {
        String resolvedRequestId = resolveRequestIdForRemoteDispatch(requestId, resumeToken);
        AgentRuntimeRoute ownerRoute = agentRuntimeRouteRegistry.findOwnerRoute(resolvedRequestId);
        String ownerNodeId = ownerRoute == null ? null : ownerRoute.getNodeId();
        if (ownerNodeId == null || ownerNodeId.isBlank()) {
            throw new BusinessException("Agent 运行节点不可用，请重新发起对话");
        }
        if (ownerNodeId.equals(agentRuntimeRouteRegistry.currentNodeId())) {
            throw new BusinessException("Agent 运行节点不可用，请重新发起对话");
        }
        if (!agentRuntimeRouteRegistry.isNodeAlive(ownerNodeId)) {
            throw new BusinessException("Agent 运行节点不可用，请重新发起对话");
        }
        String currentOwnerBootId = agentRuntimeRouteRegistry.currentNodeBootId(ownerNodeId);
        if (ownerRoute.getBootId() == null || !ownerRoute.getBootId().equals(currentOwnerBootId)) {
            throw new BusinessException("Agent 运行节点不可用，请重新发起对话");
        }
        if (action == AgentRuntimeCommandAction.APPROVE) {
            agentRuntimeCommandProducer.sendApprove(ownerNodeId, resolvedRequestId, resumeToken, operatorId, userId);
            return;
        }
        agentRuntimeCommandProducer.sendReject(ownerNodeId, resolvedRequestId, resumeToken, reason, operatorId, userId);
    }

    private String resolveRequestIdForRemoteDispatch(String requestId, String resumeToken) {
        if (requestId != null && !requestId.isBlank()) {
            return requestId;
        }
        String resolvedRequestId = agentRuntimeRouteRegistry.findRequestIdByResumeToken(resumeToken);
        if (resolvedRequestId == null || resolvedRequestId.isBlank()) {
            throw new BusinessException("Agent 运行节点不可用，请重新发起对话");
        }
        return resolvedRequestId;
    }

    private void startRuntime(Agent agent,
                              AgentMessage userMessage,
                              LoginAccount account,
                              String requestId,
                              String traceId,
                              String runtimeSessionId,
                              String assistantCode,
                              ChatRuntimeContext chatContext,
                              ChatSseEmitter chatSseEmitter,
                              boolean persistChatlog,
                              AgentSessionStore runtimeSessionStore,
                              AgentRunLock.Handle initialLockHandle) {
        AtomicBoolean finished = new AtomicBoolean(false);
        StringBuilder answer = new StringBuilder();
        ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
        LegacyThinkingTagParser legacyThinkingTagParser = new LegacyThinkingTagParser();
        // 注册 emit 服务
        registerEmitterCancellation(requestId, chatSseEmitter, chatContext, answer,
                assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
        AgentRunLock.Handle lockHandle = initialLockHandle;
        try {
            if (persistChatlog) {
                bindAgentSession(agent, runtimeSessionId, chatContext);
            }
            AgentRuntimeBundle bundle = agentRuntimeCompiler.compile(agent);
            AgentRuntime runtime = agentRuntimeFactory.create();
            // 会话初始化请求
            AgentInitRequest request = new AgentInitRequest();
            request.setSessionId(runtimeSessionId);
            request.setAgentDefinition(bundle.getDefinition());
            request.setRuntimeContext(buildAgentRuntimeContext(chatContext, traceId, runtimeSessionId));
            request.setToolInvokers(bundle.getToolInvokers());
            request.setKnowledgeRetrievers(bundle.getKnowledgeRetrievers());
            request.setSessionStore(runtimeSessionStore);
            request.setMediaResolver(agentMediaService.runtimeResolver(account));
            request.getMetadata().put("assistantCode", assistantCode);
            runtime.init(request);
            // 注册会话运行时管理
            AgentRunRegistry.RunOwner owner = new AgentRunRegistry.RunOwner(
                    agent.getId() == null ? null : agent.getId().toString(),
                    runtimeSessionId,
                    chatContext.getUserId() == null ? null : chatContext.getUserId().toString()
            );
            AgentRunRegistry.AgentRunContext runContext = new AgentRunRegistry.AgentRunContext(
                    requestId,
                    runtimeSessionId,
                    runtime,
                    chatSseEmitter,
                    chatContext,
                    answer,
                    assistantAccumulator,
                    finished,
                    persistChatlog,
                    owner,
                    lockHandle,
                    event -> handleRuntimeEvent(event, requestId, chatSseEmitter, answer,
                            assistantAccumulator, legacyThinkingTagParser, chatContext, finished, persistChatlog),
                    error -> handleRuntimeStreamError(error, requestId, chatSseEmitter, chatContext, answer,
                            assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog),
                    () -> finishRuntimeStream(requestId, chatSseEmitter, chatContext, answer,
                            assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)
            );
            agentRunRegistry.register(runContext);
            lockHandle = null;
            if (finished.get()) {
                runContext.cancel();
                agentRunRegistry.remove(requestId);
                return;
            }
            agentRunRegistry.bindSubscription(requestId,
                    runtime.stream(userMessage).subscribe(
                            runContext.eventConsumer(),
                            runContext.errorConsumer(),
                            runContext.completionHandler()
                    ));
        } catch (Exception e) {
            AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
            if (runContext != null) {
                runContext.cancel();
            }
            agentRunRegistry.remove(requestId);
            if (lockHandle != null) {
                lockHandle.release();
            }
            handleRuntimeError(e, requestId, chatSseEmitter, chatContext, finished, persistChatlog);
        }
    }

    private void bindAgentSession(Agent agent, String runtimeSessionId, ChatRuntimeContext chatContext) {
        if (easyFlowAgentSessionStore == null || runtimeSessionId == null || runtimeSessionId.isBlank()) {
            return;
        }
        easyFlowAgentSessionStore.bindSession(new EasyFlowAgentSessionStore.AgentSessionMetadata(
                runtimeSessionId,
                runtimeSessionId,
                agent == null ? null : agent.getId(),
                chatContext == null ? null : chatContext.getSessionId(),
                chatContext == null ? null : chatContext.getTenantId(),
                chatContext == null ? null : chatContext.getUserId()
        ));
    }

    private AgentRunLock.Handle acquireRunLock(Agent agent, String runtimeSessionId) {
        if (agentRunLock == null) {
            return null;
        }
        return agentRunLock.acquire(agent == null ? null : agent.getId(), runtimeSessionId);
    }

    private void recordRuntimeEvent(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event, boolean persistChatlog) {
        if (persistChatlog && agentRunEventRecorder != null) {
            agentRunEventRecorder.record(requestId, chatContext, event);
        }
    }

    private void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event, boolean persistChatlog) {
        if (persistChatlog && agentHitlPendingService != null) {
            agentHitlPendingService.recordApprovalRequired(requestId, chatContext, event);
        }
    }

    private void cancelPending(String requestId, String reason, boolean persistChatlog) {
        if (persistChatlog && agentHitlPendingService != null) {
            agentHitlPendingService.cancelByRequestId(requestId, reason);
        }
    }

    private void registerEmitterCancellation(String requestId,
                                             ChatSseEmitter chatSseEmitter,
                                             ChatRuntimeContext chatContext,
                                             StringBuilder answer,
                                             ChatAssistantAccumulator assistantAccumulator,
                                             LegacyThinkingTagParser legacyThinkingTagParser,
                                             AtomicBoolean finished,
                                             boolean persistChatlog) {
        Runnable cancelTask = () -> cancelDisconnectedRun(requestId, chatContext, answer,
                assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
        SseEmitter emitter = chatSseEmitter.getEmitter();
        emitter.onCompletion(cancelTask);
        emitter.onTimeout(cancelTask);
        emitter.onError(error -> cancelTask.run());
    }

    private void cancelDisconnectedRun(String requestId,
                                       ChatRuntimeContext chatContext,
                                       StringBuilder answer,
                                       ChatAssistantAccumulator assistantAccumulator,
                                       LegacyThinkingTagParser legacyThinkingTagParser,
                                       AtomicBoolean finished,
                                       boolean persistChatlog) {
        if (!finished.compareAndSet(false, true)) {
            return;
        }
        AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
        if (runContext != null) {
            try {
                runContext.cancel();
            } catch (Exception e) {
                LOG.warn("Cancel disconnected Agent run failed, requestId={}, message={}", requestId, e.getMessage(), e);
            }
        }
        agentRunRegistry.remove(requestId);
        cancelPending(requestId, "客户端连接已断开，Agent 运行已取消", persistChatlog);
        appendAssistantSegments(legacyThinkingTagParser.finish(), answer, assistantAccumulator);
        if (!persistChatlog) {
            return;
        }
        try {
            recordPartialAssistantIfPresent(chatContext, answer, assistantAccumulator, "客户端连接已断开，Agent 运行已取消");
            chatRuntimeManager.recordFailure(chatContext, new BusinessException("客户端连接已断开，Agent 运行已取消"));
        } catch (Exception e) {
            LOG.warn("Record disconnected Agent run failure failed, requestId={}, message={}", requestId, e.getMessage(), e);
        }
    }

    private void handleRuntimeEvent(AgentRuntimeEvent event,
                                    String requestId,
                                    ChatSseEmitter chatSseEmitter,
                                    StringBuilder answer,
                                    ChatAssistantAccumulator assistantAccumulator,
                                    ChatRuntimeContext chatContext,
                                    AtomicBoolean finished,
                                    boolean persistChatlog) {
        handleRuntimeEvent(event, requestId, chatSseEmitter, answer, assistantAccumulator,
                new LegacyThinkingTagParser(), chatContext, finished, persistChatlog);
    }

    private void handleRuntimeEvent(AgentRuntimeEvent event,
                                    String requestId,
                                    ChatSseEmitter chatSseEmitter,
                                    StringBuilder answer,
                                    ChatAssistantAccumulator assistantAccumulator,
                                    LegacyThinkingTagParser legacyThinkingTagParser,
                                    ChatRuntimeContext chatContext,
                                    AtomicBoolean finished,
                                    boolean persistChatlog) {
        if (event == null || event.getEventType() == null) {
            return;
        }
        recordRuntimeEvent(requestId, chatContext, event, persistChatlog);
        if (event.getEventType() == AgentRuntimeEventType.REASONING_STARTED) {
            emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
            legacyThinkingTagParser.reset();
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) {
            String text = stringPayload(event, "text");
            emitAssistantSegments(legacyThinkingTagParser.acceptContent(text), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.REASONING_DELTA) {
            String reasoning = firstText(stringPayload(event, "reasoning"), stringPayload(event, "text"));
            emitAssistantSegments(legacyThinkingTagParser.acceptReasoning(reasoning), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
            String resumeToken = stringPayload(event, "resumeToken");
            agentRunRegistry.registerResumeToken(requestId, resumeToken);
            recordApprovalRequired(requestId, chatContext, event, persistChatlog);
            if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.FORM_REQUEST, buildToolHitlPayload(requestId, event))) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
            }
            return;
        }
        if (isAsyncToolEvent(event.getEventType())) {
            if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, asyncToolChatType(event), buildAsyncToolEventPayload(event))) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
            }
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.TOOL_CALL) {
            if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
                return;
            }
            LOG.info("Agent runtime tool call, requestId={}, toolCallId={}, payload={}, metadata={}",
                    requestId, event.getToolCallId(), event.getPayload(), event.getMetadata());
            Map<String, Object> toolPayload = buildToolEventPayload(event);
            assistantAccumulator.appendToolCall(
                    firstText(stringValue(toolPayload, "toolCallId"), event.getToolCallId()),
                    firstText(stringValue(toolPayload, "toolName"), stringValue(toolPayload, "name")),
                    stringValue(toolPayload, "toolDisplayName"),
                    firstNonNull(toolPayload.get("input"), toolPayload.get("toolInput"))
            );
            if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.TOOL_CALL, toolPayload)) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
            }
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.TOOL_RESULT) {
            LOG.info("Agent runtime tool result, requestId={}, toolCallId={}, payload={}, metadata={}",
                    requestId, event.getToolCallId(), event.getPayload(), event.getMetadata());
            Map<String, Object> toolPayload = buildToolEventPayload(event);
            assistantAccumulator.appendToolResult(
                    firstText(stringValue(toolPayload, "toolCallId"), event.getToolCallId()),
                    firstText(stringValue(toolPayload, "toolName"), stringValue(toolPayload, "name")),
                    stringValue(toolPayload, "toolDisplayName"),
                    firstNonNull(firstNonNull(toolPayload.get("output"), toolPayload.get("result")),
                            toolPayload.get("text"))
            );
            if (!sendEnvelope(chatSseEmitter, ChatDomain.TOOL, ChatType.TOOL_RESULT, toolPayload)) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
                return;
            }
            legacyThinkingTagParser.reset();
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
            LOG.info("Agent runtime knowledge retrieval, requestId={}, payload={}, metadata={}",
                    requestId, event.getPayload(), event.getMetadata());
            if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, buildKnowledgeRetrievalStatusPayload(event))) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
            }
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED
                || event.getEventType() == AgentRuntimeEventType.MEMORY_COMPRESSION_COMPLETED) {
            LOG.info("Agent runtime memory compression, requestId={}, eventType={}, payload={}, metadata={}",
                    requestId, event.getEventType(), event.getPayload(), event.getMetadata());
            if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, event.getPayload())) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
            }
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.SUSPENDED) {
            LOG.info("Agent runtime suspended, requestId={}, payload={}, metadata={}",
                    requestId, event.getPayload(), event.getMetadata());
            if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, buildSuspendedStatusPayload(event))) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
                return;
            }
            AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
            if (runContext != null) {
                runContext.markSuspended();
            }
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.COMPLETED) {
            if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
                return;
            }
            String finalText = stringPayload(event, "text");
            if (!legacyThinkingTagParser.isLegacyFormatDetected() && finalText != null && !finalText.isBlank()) {
                answer.setLength(0);
                answer.append(finalText);
            }
            List<Map<String, Object>> citations = buildKnowledgeCitationPayload(event);
            if (!citations.isEmpty()) {
                if (!sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.CITATIONS, Map.of("items", citations))) {
                    cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                            legacyThinkingTagParser, finished, persistChatlog);
                    return;
                }
            }
            finishIfNeeded(requestId, chatSseEmitter, chatContext, answer,
                    assistantAccumulator, finished, persistChatlog, citations);
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.CANCELLED) {
            if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
                return;
            }
            handleRuntimeCancelled(event, requestId, chatSseEmitter, chatContext, answer,
                    assistantAccumulator, finished, persistChatlog);
            return;
        }
        if (event.getEventType() == AgentRuntimeEventType.FAILED) {
            if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                    answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
                return;
            }
            handleRuntimeError(new BusinessException(errorMessage(event)), requestId, chatSseEmitter, chatContext, finished, persistChatlog);
        }
    }

    /**
     * 将解析后的助手片段累计、持久化并发送到前端。
     *
     * @param segments 解析片段
     * @param requestId 运行请求 ID
     * @param chatSseEmitter SSE 发送器
     * @param chatContext 聊天上下文
     * @param answer 最终正文缓冲
     * @param assistantAccumulator 结构化消息缓冲
     * @param legacyThinkingTagParser 旧思考标签解析器
     * @param finished 完成标记
     * @param persistChatlog 是否持久化聊天记录
     * @return 全部片段发送成功时为 {@code true}
     */
    private boolean emitAssistantSegments(List<LegacyThinkingTagParser.Segment> segments,
                                          String requestId,
                                          ChatSseEmitter chatSseEmitter,
                                          ChatRuntimeContext chatContext,
                                          StringBuilder answer,
                                          ChatAssistantAccumulator assistantAccumulator,
                                          LegacyThinkingTagParser legacyThinkingTagParser,
                                          AtomicBoolean finished,
                                          boolean persistChatlog) {
        for (LegacyThinkingTagParser.Segment segment : segments) {
            String text = segment.getText();
            ChatType chatType;
            Map<String, Object> payload = new LinkedHashMap<>();
            if (segment.getType() == LegacyThinkingTagParser.SegmentType.REASONING) {
                assistantAccumulator.appendReasoning(text);
                payload.put("reasoning", text);
                payload.put("delta", text);
                chatType = ChatType.THINKING;
            } else {
                answer.append(text);
                assistantAccumulator.appendContent(text);
                payload.put("delta", text);
                payload.put("role", "assistant");
                chatType = ChatType.MESSAGE;
                LOG.debug("Agent runtime message delta, requestId={}, deltaLength={}, answerLength={}, delta={}",
                        requestId, text.length(), answer.length(), toVisibleLogText(text));
            }
            if (!sendEnvelope(chatSseEmitter, ChatDomain.LLM, chatType, payload)) {
                cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
                        legacyThinkingTagParser, finished, persistChatlog);
                return false;
            }
        }
        return true;
    }

    /**
     * 仅累计解析片段，用于连接已断开后的部分消息持久化。
     *
     * @param segments 解析片段
     * @param answer 最终正文缓冲
     * @param assistantAccumulator 结构化消息缓冲
     */
    private void appendAssistantSegments(List<LegacyThinkingTagParser.Segment> segments,
                                         StringBuilder answer,
                                         ChatAssistantAccumulator assistantAccumulator) {
        for (LegacyThinkingTagParser.Segment segment : segments) {
            if (segment.getType() == LegacyThinkingTagParser.SegmentType.REASONING) {
                assistantAccumulator.appendReasoning(segment.getText());
            } else {
                answer.append(segment.getText());
                assistantAccumulator.appendContent(segment.getText());
            }
        }
    }

    /**
     * 在运行时自然结束但未显式发出完成事件时收口兼容解析器。
     *
     * @param requestId 运行请求 ID
     * @param chatSseEmitter SSE 发送器
     * @param chatContext 聊天上下文
     * @param answer 最终正文缓冲
     * @param assistantAccumulator 结构化消息缓冲
     * @param legacyThinkingTagParser 旧思考标签解析器
     * @param finished 完成标记
     * @param persistChatlog 是否持久化聊天记录
     */
    private void finishRuntimeStream(String requestId,
                                     ChatSseEmitter chatSseEmitter,
                                     ChatRuntimeContext chatContext,
                                     StringBuilder answer,
                                     ChatAssistantAccumulator assistantAccumulator,
                                     LegacyThinkingTagParser legacyThinkingTagParser,
                                     AtomicBoolean finished,
                                     boolean persistChatlog) {
        if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
            return;
        }
        finishIfNeeded(requestId, chatSseEmitter, chatContext, answer,
                assistantAccumulator, finished, persistChatlog);
    }

    /**
     * 在运行时异常结束前发送兼容解析器中尚未收口的片段。
     *
     * @param error 运行异常
     * @param requestId 运行请求 ID
     * @param chatSseEmitter SSE 发送器
     * @param chatContext 聊天上下文
     * @param answer 最终正文缓冲
     * @param assistantAccumulator 结构化消息缓冲
     * @param legacyThinkingTagParser 旧思考标签解析器
     * @param finished 完成标记
     * @param persistChatlog 是否持久化聊天记录
     */
    private void handleRuntimeStreamError(Throwable error,
                                          String requestId,
                                          ChatSseEmitter chatSseEmitter,
                                          ChatRuntimeContext chatContext,
                                          StringBuilder answer,
                                          ChatAssistantAccumulator assistantAccumulator,
                                          LegacyThinkingTagParser legacyThinkingTagParser,
                                          AtomicBoolean finished,
                                          boolean persistChatlog) {
        if (!emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, chatSseEmitter, chatContext,
                answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog)) {
            return;
        }
        handleRuntimeError(error, requestId, chatSseEmitter, chatContext, finished, persistChatlog);
    }

    private void finishIfNeeded(String requestId,
                                ChatSseEmitter chatSseEmitter,
                                ChatRuntimeContext chatContext,
                                StringBuilder answer,
                                ChatAssistantAccumulator assistantAccumulator,
                                AtomicBoolean finished,
                                boolean persistChatlog) {
        finishIfNeeded(requestId, chatSseEmitter, chatContext, answer,
                assistantAccumulator, finished, persistChatlog, List.of());
    }

    private void finishIfNeeded(String requestId,
                                ChatSseEmitter chatSseEmitter,
                                ChatRuntimeContext chatContext,
                                StringBuilder answer,
                                ChatAssistantAccumulator assistantAccumulator,
                                AtomicBoolean finished,
                                boolean persistChatlog,
                                List<Map<String, Object>> citations) {
        AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
        if (runContext != null && runContext.isSuspended()) {
            LOG.info("Agent runtime stream suspended, keep SSE and runtime active, requestId={}", requestId);
            return;
        }
        if (!finished.compareAndSet(false, true)) {
            return;
        }
        agentRunRegistry.remove(requestId);
        String finalAnswer = answer.toString();
        LOG.info("Agent runtime final answer, requestId={}, sessionId={}, answerLength={}, answer={}",
                requestId, chatContext.getSessionId(), finalAnswer.length(), toVisibleLogText(finalAnswer));
        if (persistChatlog) {
            chatRuntimeManager.recordAssistantCompleted(chatContext,
                    buildAssistantRuntimeMessage(chatContext, finalAnswer, assistantAccumulator, citations));
            chatRuntimeManager.recordCompleted(chatContext);
        }
        sendDone(chatSseEmitter);
    }

    private void handleRuntimeError(Throwable error,
                                    String requestId,
                                    ChatSseEmitter chatSseEmitter,
                                    ChatRuntimeContext chatContext,
                                    AtomicBoolean finished,
                                    boolean persistChatlog) {
        if (!finished.compareAndSet(false, true)) {
            return;
        }
        agentRunRegistry.remove(requestId);
        cancelPending(requestId, safeErrorMessage(error), persistChatlog);
        Throwable safeError = error == null ? new BusinessException("Agent 运行失败") : error;
        LOG.error("Agent run failed, requestId={}, message={}, exception={}", requestId,
                safeError.getMessage(), safeError.toString(), safeError);
        if (persistChatlog) {
            chatRuntimeManager.recordFailure(chatContext, safeError);
        }
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("message", safeError.getMessage() == null ? "Agent 运行失败" : safeError.getMessage());
        payload.put("code", "AGENT_RUN_FAILED");
        sendEnvelope(chatSseEmitter, ChatDomain.SYSTEM, ChatType.ERROR, payload);
        chatSseEmitter.complete();
    }

    private String safeErrorMessage(Throwable error) {
        if (error == null || error.getMessage() == null || error.getMessage().isBlank()) {
            return "Agent 运行失败";
        }
        return error.getMessage();
    }

    private void handleRuntimeCancelled(AgentRuntimeEvent event,
                                        String requestId,
                                        ChatSseEmitter chatSseEmitter,
                                        ChatRuntimeContext chatContext,
                                        StringBuilder answer,
                                        ChatAssistantAccumulator assistantAccumulator,
                                        AtomicBoolean finished,
                                        boolean persistChatlog) {
        if (!finished.compareAndSet(false, true)) {
            return;
        }
        agentRunRegistry.remove(requestId);
        String reason = errorMessage(event);
        cancelPending(requestId, reason, persistChatlog);
        LOG.info("Agent run cancelled, requestId={}, reason={}", requestId, reason);
        if (persistChatlog) {
            recordPartialAssistantIfPresent(chatContext, answer, assistantAccumulator, reason);
            chatRuntimeManager.recordFailure(chatContext, new BusinessException(reason));
        }
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("statusKey", "agent-cancelled");
        payload.put("status", "cancelled");
        payload.put("label", "已取消");
        payload.put("message", reason);
        sendEnvelope(chatSseEmitter, ChatDomain.BUSINESS, ChatType.STATUS, payload);
        sendDone(chatSseEmitter);
    }

    /**
     * 保存 Agent 取消前已经生成的 assistant 内容。
     *
     * <p>用户主动中止或连接断开时，运行不会进入正常 COMPLETED 分支，但前端历史恢复仍需要看到
     * 中止前已经流出的模型输出。没有正文、reasoning 或工具链内容时不写空 assistant 消息。</p>
     *
     * @param context 聊天运行上下文
     * @param answer 已累计的 assistant 正文
     * @param assistantAccumulator assistant 结构化累计器
     * @param reason 取消原因
     */
    private void recordPartialAssistantIfPresent(ChatRuntimeContext context,
                                                StringBuilder answer,
                                                ChatAssistantAccumulator assistantAccumulator,
                                                String reason) {
        String partialAnswer = answer == null ? "" : answer.toString();
        if (partialAnswer.isBlank() && !hasAssistantPayload(assistantAccumulator)) {
            return;
        }
        chatRuntimeManager.recordAssistantCompleted(context,
                buildAssistantRuntimeMessage(context, partialAnswer, assistantAccumulator, List.of()));
        LOG.info("Agent partial answer persisted after cancellation, sessionId={}, answerLength={}, reason={}",
                context == null ? null : context.getSessionId(), partialAnswer.length(), reason);
    }

    /**
     * 判断 assistant 累计器中是否存在可恢复的展示内容。
     *
     * @param assistantAccumulator assistant 结构化累计器
     * @return true 表示存在正文、思考过程或工具链内容
     */
    private boolean hasAssistantPayload(ChatAssistantAccumulator assistantAccumulator) {
        if (assistantAccumulator == null) {
            return false;
        }
        Map<String, Object> payload = assistantAccumulator.buildPayload("");
        if (payload == null || payload.isEmpty()) {
            return false;
        }
        return payload.values().stream().anyMatch(value -> {
            if (value instanceof List<?> list) {
                return !list.isEmpty();
            }
            if (value instanceof Map<?, ?> map) {
                return !map.isEmpty();
            }
            return value != null && !String.valueOf(value).isBlank();
        });
    }

    private ChatRuntimeContext buildChatRuntimeContext(Agent agent, BigInteger sessionId, String prompt, LoginAccount account) {
        return buildChatRuntimeContext(agent, sessionId, prompt, account, ASSISTANT_CODE);
    }

    /**
     * 为正式 Agent 聊天设置会话默认标题。
     *
     * <p>正式聊天只在新会话首轮使用用户输入作为默认标题，后续轮次不再自动覆盖，避免用户已重命名的标题被后续消息改写。</p>
     *
     * @param context 聊天运行上下文
     * @param prompt 当前用户输入
     * @param existingSession 已存在的会话摘要
     */
    private void applyFormalSessionTitle(ChatRuntimeContext context, String prompt, ChatSessionSummary existingSession) {
        if (context == null) {
            return;
        }
        if (existingSession != null) {
            context.setSessionTitle(null);
            return;
        }
        context.setSessionTitle(toSessionTitle(prompt));
    }

    private ChatRuntimeContext buildChatRuntimeContext(Agent agent,
                                                       BigInteger sessionId,
                                                       String prompt,
                                                       LoginAccount account,
                                                       String assistantCode) {
        ChatRuntimeContext context = new ChatRuntimeContext();
        context.setChannel(ChatChannel.ADMIN);
        context.setSessionId(sessionId);
        context.setTenantId(account.getTenantId());
        context.setDeptId(account.getDeptId());
        context.setUserId(account.getId());
        context.setUserAccount(account.getLoginName());
        context.setUserName(account.getNickname() == null || account.getNickname().isBlank() ? account.getLoginName() : account.getNickname());
        context.setAssistantId(agent.getId());
        context.setAssistantCode(assistantCode);
        context.setAssistantName(agent.getName());
        context.setSessionTitle(toSessionTitle(prompt));
        return context;
    }

    /**
     * 将用户输入裁剪为会话标题。
     *
     * @param prompt 用户输入内容
     * @return 最长 200 字符的会话标题
     */
    private String toSessionTitle(String prompt) {
        if (prompt == null || prompt.isBlank()) {
            return "图片对话";
        }
        return prompt.length() > 200 ? prompt.substring(0, 200) : prompt;
    }

    private AgentRuntimeContext buildAgentRuntimeContext(ChatRuntimeContext chatContext, String traceId, String sessionId) {
        AgentRuntimeContext context = new AgentRuntimeContext();
        context.setTenantId(chatContext.getTenantId() == null ? null : chatContext.getTenantId().toString());
        context.setUserId(chatContext.getUserId() == null ? null : chatContext.getUserId().toString());
        context.setUserName(chatContext.getUserName());
        context.setSessionId(sessionId);
        context.setTraceId(traceId);
        return context;
    }

    private ChatRuntimeMessage buildUserRuntimeMessage(ChatRuntimeContext context,
                                                       BigInteger messageId,
                                                       String prompt,
                                                       List<AgentBoundMedia> media) {
        ChatRuntimeMessage message = new ChatRuntimeMessage();
        message.setMessageId(messageId);
        message.setRole("user");
        message.setContentType(media == null || media.isEmpty() ? "TEXT" : "MULTIMODAL");
        message.setContentText(prompt);
        if (media != null && !media.isEmpty()) {
            message.getContentPayload().put("images", media.stream().map(AgentBoundMedia::payload).toList());
        }
        message.setCreatedAt(new Date());
        message.setSenderId(context.getUserId());
        message.setSenderName(context.getUserName());
        return message;
    }

    private AgentMessage buildAgentMessage(String prompt, List<AgentBoundMedia> media) {
        AgentMessage message = new AgentMessage();
        message.setRole(AgentMessageRole.USER);
        List<com.easyagents.agent.runtime.message.AgentContentBlock> blocks = new ArrayList<>();
        if (prompt != null && !prompt.isBlank()) {
            blocks.add(new AgentTextBlock(prompt));
        }
        if (media != null) {
            for (AgentBoundMedia item : media) {
                AgentMediaBlock image = new AgentMediaBlock("image");
                image.setReference(item.reference());
                image.setMimeType(item.mimeType());
                blocks.add(image);
            }
        }
        message.setContentBlocks(blocks);
        return message;
    }

    private ChatRuntimeMessage buildAssistantRuntimeMessage(ChatRuntimeContext context, String content) {
        return buildAssistantRuntimeMessage(context, content, new ChatAssistantAccumulator(), List.of());
    }

    private ChatRuntimeMessage buildAssistantRuntimeMessage(ChatRuntimeContext context,
                                                            String content,
                                                            ChatAssistantAccumulator assistantAccumulator,
                                                            List<Map<String, Object>> citations) {
        ChatRuntimeMessage message = new ChatRuntimeMessage();
        message.setRole("assistant");
        message.setContentType("TEXT");
        message.setContentText(content);
        Map<String, Object> contentPayload = assistantAccumulator == null
                ? new LinkedHashMap<>()
                : assistantAccumulator.buildPayload(content);
        if (citations != null && !citations.isEmpty()) {
            contentPayload.put("knowledgeCitations", citations);
        }
        Map<String, Object> agentResult = new LinkedHashMap<>();
        agentResult.put("text", content);
        agentResult.put("reasoning", contentPayload.get("reasoningContent"));
        agentResult.put("knowledgeReferences", citations == null ? List.of() : citations);
        contentPayload.put("agentResult", agentResult);
        message.setContentPayload(contentPayload);
        message.setCreatedAt(new Date());
        message.setSenderId(context.getAssistantId());
        message.setSenderName(context.getAssistantName());
        return message;
    }

    private boolean sendEnvelope(ChatSseEmitter chatSseEmitter, ChatDomain domain, ChatType type, Object payload) {
        ChatEnvelope<Object> envelope = new ChatEnvelope<>();
        envelope.setDomain(domain);
        envelope.setType(type);
        envelope.setPayload(payload);
        return chatSseEmitter.send(envelope);
    }

    private boolean sendDone(ChatSseEmitter chatSseEmitter) {
        ChatEnvelope<Map<String, Object>> envelope = new ChatEnvelope<>();
        envelope.setDomain(ChatDomain.SYSTEM);
        envelope.setType(ChatType.DONE);
        return chatSseEmitter.sendDone(envelope);
    }

    private boolean sendSessionCreated(ChatSseEmitter chatSseEmitter, BigInteger sessionId) {
        if (sessionId == null) {
            return true;
        }
        return sendEnvelope(chatSseEmitter, ChatDomain.SYSTEM, ChatType.SESSION_CREATED,
                Map.of("sessionId", sessionId.toString()));
    }

    private boolean sendInputAccepted(ChatSseEmitter chatSseEmitter,
                                      BigInteger sessionId,
                                      BigInteger messageId,
                                      List<AgentBoundMedia> boundMedia) {
        Map<String, Object> payload = new LinkedHashMap<>();
        if (sessionId != null) {
            payload.put("sessionId", sessionId.toString());
        }
        if (messageId != null) {
            payload.put("messageId", messageId.toString());
        }
        if (boundMedia != null && !boundMedia.isEmpty()) {
            payload.put("images", boundMedia.stream().map(AgentBoundMedia::payload).toList());
        }
        return sendEnvelope(chatSseEmitter, ChatDomain.SYSTEM, ChatType.INPUT_ACCEPTED, payload);
    }

    private void validateChatRequest(AgentChatRequest request) {
        if (request == null || request.getAgentId() == null) {
            throw new BusinessException("Agent ID 不能为空");
        }
        if ((request.getPrompt() == null || request.getPrompt().isBlank())
                && (request.getImageUploadIds() == null || request.getImageUploadIds().isEmpty())) {
            throw new BusinessException("Agent 输入不能为空");
        }
    }

    private void validateDraftChatRequest(AgentDraftChatRequest request) {
        if (request == null || request.getAgent() == null) {
            throw new BusinessException("Agent 草稿不能为空");
        }
        if (request.getAgent().getModelId() == null) {
            throw new BusinessException("Agent 模型不能为空");
        }
        if ((request.getPrompt() == null || request.getPrompt().isBlank())
                && (request.getImageUploadIds() == null || request.getImageUploadIds().isEmpty())) {
            throw new BusinessException("Agent 输入不能为空");
        }
    }

    private void assertImageCapability(Agent agent, List<String> imageUploadIds) {
        if (imageUploadIds == null || imageUploadIds.isEmpty()) {
            return;
        }
        tech.easyflow.ai.entity.Model model = agent == null || agent.getModelId() == null
                ? null
                : modelService.getModelInstance(agent.getModelId());
        if (model == null || !Boolean.TRUE.equals(model.getSupportImage())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力");
        }
    }

    private LoginAccount requireCurrentLoginAccount() {
        try {
            return SaTokenUtil.getLoginAccount();
        } catch (Exception e) {
            throw new BusinessException("当前登录状态失效，请重新登录后再试");
        }
    }

    /**
     * 解析并校验当前用户可继续的 Agent 会话。
     *
     * <p>前端可能先生成稳定 sessionId 再发送首条消息。若该 ID 尚未落库，视为新会话并返回 null；若已落库，则校验归属与 Agent 绑定。</p>
     *
     * @param account 当前登录账号
     * @param sessionId 会话 ID
     * @param agentId Agent ID
     * @return 已存在的会话摘要；不存在时返回 null
     */
    private ChatSessionSummary resolveExistingSession(LoginAccount account, BigInteger sessionId, BigInteger agentId) {
        if (sessionId == null) {
            return null;
        }
        ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
        if (summary == null) {
            return null;
        }
        if (Integer.valueOf(1).equals(summary.getIsDeleted())) {
            throw new BusinessException("Agent 会话不存在或已删除");
        }
        if (!ASSISTANT_CODE.equals(summary.getAssistantCode())) {
            throw new BusinessException("当前会话不是 Agent 会话");
        }
        if (!Objects.equals(summary.getUserId(), account.getId())) {
            throw new BusinessException("无权访问该 Agent 会话");
        }
        if (agentId != null && summary.getAssistantId() != null && !Objects.equals(summary.getAssistantId(), agentId)) {
            throw new BusinessException("当前会话与所选 Agent 不匹配");
        }
        return summary;
    }

    private void assertAgentRunnable(Agent agent) {
        if (!Integer.valueOf(1).equals(agent.getStatus())
                || PublishStatus.from(agent.getPublishStatus()) != PublishStatus.PUBLISHED) {
            throw new BusinessException("当前 Agent 已下线或不可继续会话");
        }
    }

    private Agent buildDraftAgent(AgentDraftChatRequest request, LoginAccount account) {
        Agent incoming = request.getAgent();
        Agent agent = new Agent();
        agent.setId(incoming.getId());
        agent.setTenantId(account.getTenantId());
        agent.setDeptId(account.getDeptId());
        agent.setCreatedBy(account.getId());
        agent.setName(incoming.getName() == null || incoming.getName().isBlank() ? "草稿 Agent" : incoming.getName());
        agent.setDescription(incoming.getDescription());
        agent.setAvatar(incoming.getAvatar());
        agent.setCategoryId(incoming.getCategoryId());
        agent.setModelId(incoming.getModelId());
        agent.setModelConfigJson(incoming.getModelConfigJson());
        agent.setGenerationConfigJson(incoming.getGenerationConfigJson());
        agent.setPromptConfigJson(incoming.getPromptConfigJson());
        agent.setMemoryConfigJson(incoming.getMemoryConfigJson());
        agent.setExecutionConfigJson(incoming.getExecutionConfigJson());
        agent.setStatus(incoming.getStatus() == null ? 1 : incoming.getStatus());
        agent.setVisibilityScope(incoming.getVisibilityScope());
        agent.setPublishStatus(incoming.getPublishStatus());
        if (agent.getId() == null) {
            agent.setId(BigInteger.valueOf(new SnowFlakeIDKeyGenerator().nextId()));
        } else {
            Agent liveAgent = agentService.getById(agent.getId());
            if (liveAgent == null) {
                throw new BusinessException("Agent 不存在");
            }
            resourceAccessService.assertAccess(CategoryResourceType.AGENT, liveAgent, ResourceAction.MANAGE, "无权限试用该 Agent 草稿");
        }
        agent.setToolBindings(copyDraftToolBindings(request.getToolBindings(), agent, account));
        agent.setKnowledgeBindings(copyDraftKnowledgeBindings(request.getKnowledgeBindings(), agent, account));
        return agent;
    }

    private List<AgentToolBinding> copyDraftToolBindings(List<AgentToolBinding> bindings, Agent agent, LoginAccount account) {
        List<AgentToolBinding> result = new ArrayList<>();
        if (bindings == null || bindings.isEmpty()) {
            return result;
        }
        for (int i = 0; i < bindings.size(); i++) {
            AgentToolBinding incoming = bindings.get(i);
            if (incoming == null) {
                continue;
            }
            AgentToolBinding binding = new AgentToolBinding();
            binding.setId(incoming.getId());
            binding.setTenantId(account.getTenantId());
            binding.setAgentId(agent.getId());
            binding.setToolType(incoming.getToolType());
            binding.setTargetId(incoming.getTargetId());
            binding.setToolName(incoming.getToolName());
            binding.setEnabled(incoming.getEnabled() == null || incoming.getEnabled());
            binding.setHitlEnabled(Boolean.TRUE.equals(incoming.getHitlEnabled()));
            binding.setHitlConfigJson(incoming.getHitlConfigJson());
            binding.setOptionsJson(incoming.getOptionsJson());
            binding.setResourceSnapshot(new LinkedHashMap<>());
            binding.setResourceSummary(new LinkedHashMap<>());
            binding.setSortNo(incoming.getSortNo() == null ? i : incoming.getSortNo());
            binding.setCreatedBy(account.getId());
            binding.setModifiedBy(account.getId());
            result.add(binding);
        }
        return result;
    }

    private List<AgentKnowledgeBinding> copyDraftKnowledgeBindings(List<AgentKnowledgeBinding> bindings, Agent agent, LoginAccount account) {
        List<AgentKnowledgeBinding> result = new ArrayList<>();
        if (bindings == null || bindings.isEmpty()) {
            return result;
        }
        for (int i = 0; i < bindings.size(); i++) {
            AgentKnowledgeBinding incoming = bindings.get(i);
            if (incoming == null) {
                continue;
            }
            AgentKnowledgeBinding binding = new AgentKnowledgeBinding();
            binding.setId(incoming.getId());
            binding.setTenantId(account.getTenantId());
            binding.setAgentId(agent.getId());
            binding.setKnowledgeId(incoming.getKnowledgeId());
            binding.setRetrievalMode(incoming.getRetrievalMode());
            binding.setEnabled(incoming.getEnabled() == null || incoming.getEnabled());
            binding.setOptionsJson(incoming.getOptionsJson());
            binding.setResourceSnapshot(new LinkedHashMap<>());
            binding.setResourceSummary(new LinkedHashMap<>());
            binding.setSortNo(incoming.getSortNo() == null ? i : incoming.getSortNo());
            binding.setCreatedBy(account.getId());
            binding.setModifiedBy(account.getId());
            result.add(binding);
        }
        return result;
    }

    private void validateDraftResources(Agent agent) {
        if (agent.getToolBindings() != null) {
            for (AgentToolBinding binding : agent.getToolBindings()) {
                if (binding != null && Boolean.TRUE.equals(binding.getEnabled())) {
                    validateDraftToolBinding(binding);
                }
            }
        }
        if (agent.getKnowledgeBindings() != null) {
            for (AgentKnowledgeBinding binding : agent.getKnowledgeBindings()) {
                if (binding != null && Boolean.TRUE.equals(binding.getEnabled())) {
                    validateDraftKnowledgeBinding(binding);
                }
            }
        }
    }

    private void validateDraftToolBinding(AgentToolBinding binding) {
        if (binding.getTargetId() == null || binding.getToolType() == null) {
            throw new BusinessException("工具绑定参数不完整");
        }
        AgentToolType type = parseToolType(binding.getToolType());
        if (type == AgentToolType.WORKFLOW) {
            Workflow workflow = workflowService.getById(binding.getTargetId());
            if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
                throw new BusinessException("绑定工作流不存在或未发布");
            }
            resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限试用该工作流工具");
            return;
        }
        if (type == AgentToolType.PLUGIN) {
            PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
            if (pluginItem == null || pluginItem.getStatus() == null || pluginItem.getStatus() != 1) {
                throw new BusinessException("绑定插件不存在或未启用");
            }
            return;
        }
        Mcp mcp = mcpService.getById(binding.getTargetId());
        if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
            throw new BusinessException("绑定 MCP 不存在或未启用");
        }
    }

    private void validateDraftKnowledgeBinding(AgentKnowledgeBinding binding) {
        if (binding.getKnowledgeId() == null) {
            throw new BusinessException("知识库绑定参数不完整");
        }
        KnowledgeRetrievalModes.parse(binding.getRetrievalMode());
        DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
        if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
            throw new BusinessException("绑定知识库不存在或未发布");
        }
        resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限试用该知识库");
    }

    private AgentToolType parseToolType(String toolType) {
        try {
            return AgentToolType.from(toolType);
        } catch (IllegalArgumentException e) {
            throw new BusinessException("不支持的工具绑定类型：" + (toolType == null ? "" : toolType));
        }
    }

    private AgentToolHitlPayload buildToolHitlPayload(String requestId, AgentRuntimeEvent event) {
        Map<String, Object> rawPayload = event.getPayload() == null ? Map.of() : event.getPayload();
        AgentToolHitlPayload payload = new AgentToolHitlPayload();
        payload.setRequestId(requestId);
        payload.setResumeToken(stringValue(rawPayload, "resumeToken"));
        payload.setSessionId(stringValue(rawPayload, "sessionId"));
        payload.setAgentId(stringValue(rawPayload, "agentId"));
        payload.setToolCallId(firstText(stringValue(rawPayload, "toolCallId"), event.getToolCallId()));
        payload.setToolName(stringValue(rawPayload, "toolName"));
        payload.setToolDisplayName(firstText(stringValue(rawPayload, "toolDisplayName"), stringValue(rawPayload, "toolName")));
        payload.setInput(mapPayload(rawPayload.get("toolInput")));
        if (payload.getInput().isEmpty()) {
            payload.setInput(mapPayload(rawPayload.get("input")));
        }
        payload.setExpiresAt(stringValue(rawPayload, "expiresAt"));
        Map<String, Object> metadata = buildHitlMetadata(rawPayload);
        String toolType = firstText(stringValue(rawPayload, "toolType"), stringValue(metadata, "toolType"));
        payload.setToolType(toolType);
        if (toolType != null && !toolType.isBlank()) {
            metadata.put("toolType", toolType);
        }
        payload.setMetadata(metadata);
        return payload;
    }

    /**
     * 构建发送给聊天时间线的工具事件载荷。
     *
     * @param event 运行时工具事件
     * @return 包含稳定工具调用 ID 的前端载荷
     */
    private Map<String, Object> buildToolEventPayload(AgentRuntimeEvent event) {
        Map<String, Object> payload = new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload());
        String toolCallId = firstText(event.getToolCallId(), stringValue(payload, "toolCallId"));
        if (toolCallId != null && !toolCallId.isBlank()) {
            payload.put("toolCallId", toolCallId);
        }
        if (Boolean.TRUE.equals(event.getMetadata().get("asyncTool"))) {
            enrichAsyncToolPayload(payload, event.getMetadata(), toolCallId);
            String taskId = stringValue(payload, "taskId");
            if (taskId != null && !taskId.isBlank()) {
                payload.put("toolCallId", taskId);
            }
        }
        return payload;
    }

    private boolean isAsyncToolEvent(AgentRuntimeEventType type) {
        return type == AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED
                || type == AgentRuntimeEventType.ASYNC_TOOL_OBSERVED
                || type == AgentRuntimeEventType.ASYNC_TOOL_RESULT
                || type == AgentRuntimeEventType.ASYNC_TOOL_CANCELLED
                || type == AgentRuntimeEventType.ASYNC_TOOL_LISTED
                || type == AgentRuntimeEventType.ASYNC_TOOL_FAILED;
    }

    private ChatType asyncToolChatType(AgentRuntimeEvent event) {
        String status = stringPayload(event, "status");
        if ("SUCCEEDED".equalsIgnoreCase(status)
                || "FAILED".equalsIgnoreCase(status)
                || "CANCELLED".equalsIgnoreCase(status)
                || "TIMEOUT".equalsIgnoreCase(status)
                || event.getEventType() == AgentRuntimeEventType.ASYNC_TOOL_RESULT
                || event.getEventType() == AgentRuntimeEventType.ASYNC_TOOL_FAILED
                || event.getEventType() == AgentRuntimeEventType.ASYNC_TOOL_CANCELLED) {
            return ChatType.TOOL_RESULT;
        }
        return ChatType.TOOL_CALL;
    }

    private Map<String, Object> buildAsyncToolEventPayload(AgentRuntimeEvent event) {
        Map<String, Object> payload = new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload());
        String taskId = stringValue(payload, "taskId");
        String toolCallId = firstText(taskId, event.getToolCallId());
        if (toolCallId != null && !toolCallId.isBlank()) {
            payload.put("toolCallId", toolCallId);
        }
        enrichAsyncToolPayload(payload, event.getMetadata(), toolCallId);
        return payload;
    }

    private void enrichAsyncToolPayload(Map<String, Object> payload, Map<String, Object> metadata, String fallbackId) {
        Map<String, Object> safeMetadata = metadata == null ? Map.of() : metadata;
        payload.put("asyncTool", true);
        putIfPresent(payload, "asyncToolName", firstText(stringValue(payload, "asyncToolName"), stringValue(safeMetadata, "asyncToolName")));
        putIfPresent(payload, "phase", firstText(stringValue(payload, "phase"), stringValue(safeMetadata, "asyncToolPhase")));
        putIfPresent(payload, "taskId", firstText(stringValue(payload, "taskId"), stringValue(safeMetadata, "taskId")));
        putIfPresent(payload, "status", firstText(stringValue(payload, "status"), stringValue(safeMetadata, "status")));
        String displayName = firstText(stringValue(payload, "toolDisplayName"),
                firstText(stringValue(safeMetadata, "toolDisplayName"), stringValue(payload, "asyncToolName")));
        putIfPresent(payload, "toolDisplayName", displayName);
        putIfPresent(payload, "toolName", displayName);
        putIfPresent(payload, "name", displayName);
        String statusKey = "async-tool:" + firstText(stringValue(payload, "taskId"), fallbackId);
        payload.put("statusKey", statusKey);
        payload.put("label", asyncToolLabel(stringValue(payload, "status"), stringValue(payload, "phase"), displayName));
    }

    private String asyncToolLabel(String status, String phase, String displayName) {
        String name = displayName == null || displayName.isBlank() ? "异步工具" : displayName;
        if ("SUCCEEDED".equalsIgnoreCase(status)) {
            return name + "已完成";
        }
        if ("FAILED".equalsIgnoreCase(status) || "TIMEOUT".equalsIgnoreCase(status)) {
            return name + "执行失败";
        }
        if ("PENDING".equalsIgnoreCase(status) || "submit".equalsIgnoreCase(phase)) {
            return name + "已提交";
        }
        return name + "执行中";
    }

    /**
     * 构建知识库检索状态载荷，确保前端可按稳定 key 合并同一轮状态行。
     *
     * @param event 知识库检索运行时事件
     * @return 知识库检索状态载荷
     */
    private Map<String, Object> buildKnowledgeRetrievalStatusPayload(AgentRuntimeEvent event) {
        Map<String, Object> payload = new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload());
        payload.put("statusKey", "knowledge-retrieval");
        payload.put("status", "done");
        payload.put("label", "已检索知识库");
        return payload;
    }

    /**
     * 构建挂起状态载荷。
     *
     * @param event 挂起事件
     * @return 前端状态载荷
     */
    private Map<String, Object> buildSuspendedStatusPayload(AgentRuntimeEvent event) {
        Map<String, Object> payload = new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload());
        payload.put("statusKey", "agent-suspended");
        payload.put("status", "waiting");
        payload.put("label", "等待人工确认");
        return payload;
    }

    /**
     * 构建发送给前端的最终知识库引用载荷。
     *
     * @param event 运行完成事件
     * @return 知识库引用列表
     */
    private List<Map<String, Object>> buildKnowledgeCitationPayload(AgentRuntimeEvent event) {
        AgentMessage message = event == null ? null : event.getMessage();
        if (message == null || message.getKnowledgeReferences() == null || message.getKnowledgeReferences().isEmpty()) {
            return List.of();
        }
        List<Map<String, Object>> citations = new ArrayList<>();
        for (AgentKnowledgeReference reference : message.getKnowledgeReferences()) {
            if (reference == null) {
                continue;
            }
            Map<String, Object> item = new LinkedHashMap<>();
            putIfPresent(item, "knowledgeId", reference.getKnowledgeId());
            putIfPresent(item, "knowledgeName", reference.getKnowledgeName());
            putIfPresent(item, "documentId", reference.getDocumentId());
            putIfPresent(item, "documentName", reference.getDocumentName());
            putIfPresent(item, "chunkId", reference.getChunkId());
            putIfPresent(item, "chunkContent", reference.getChunkContent());
            putIfPresent(item, "sourceUri", reference.getSourceUri());
            putIfPresent(item, "score", reference.getScore());
            if (reference.getMetadata() != null && !reference.getMetadata().isEmpty()) {
                item.put("metadata", new LinkedHashMap<>(reference.getMetadata()));
                putIfPresent(item, "sourceFileName", reference.getMetadata().get("sourceFileName"));
                putIfPresent(item, "knowledgeType", reference.getMetadata().get("knowledgeType"));
                putIfPresent(item, "faqCollection", reference.getMetadata().get("faqCollection"));
            }
            citations.add(item);
        }
        return citations;
    }

    private void putIfPresent(Map<String, Object> target, String key, Object value) {
        if (value != null) {
            target.put(key, value);
        }
    }

    private Map<String, Object> buildHitlMetadata(Map<String, Object> rawPayload) {
        Map<String, Object> metadata = new LinkedHashMap<>();
        mapPayload(rawPayload.get("approvalMetadata")).forEach((key, value) -> {
            if (!isHitlPromptKey(key)) {
                metadata.put(key, value);
            }
        });
        for (String key : List.of("toolType", "skillId", "skillName", "bindingId", "targetId")) {
            Object value = rawPayload.get(key);
            if (value != null) {
                metadata.put(key, value);
            }
        }
        return metadata;
    }

    private boolean isHitlPromptKey(String key) {
        if (key == null) {
            return false;
        }
        String normalized = key.trim();
        return "prompt".equalsIgnoreCase(normalized)
                || "question".equalsIgnoreCase(normalized)
                || "approvalPrompt".equalsIgnoreCase(normalized);
    }

    private Map<String, Object> mapPayload(Object value) {
        if (!(value instanceof Map<?, ?> rawMap)) {
            return new LinkedHashMap<>();
        }
        Map<String, Object> result = new LinkedHashMap<>();
        rawMap.forEach((rawKey, rawValue) -> result.put(String.valueOf(rawKey), rawValue));
        return result;
    }

    private String stringValue(Map<String, Object> payload, String key) {
        Object value = payload == null ? null : payload.get(key);
        return value == null ? null : String.valueOf(value);
    }

    /**
     * 将模型输出转换为适合日志排查的可见文本。
     *
     * @param value 原始模型输出
     * @return 已转义换行、制表符与控制字符的日志文本
     */
    private String toVisibleLogText(String value) {
        if (value == null) {
            return "";
        }
        StringBuilder builder = new StringBuilder(value.length());
        for (int index = 0; index < value.length(); index++) {
            char current = value.charAt(index);
            if (current == '\n') {
                builder.append("\\n");
            } else if (current == '\r') {
                builder.append("\\r");
            } else if (current == '\t') {
                builder.append("\\t");
            } else if (Character.isISOControl(current)) {
                builder.append(String.format("\\u%04x", (int) current));
            } else {
                builder.append(current);
            }
        }
        return builder.toString();
    }

    private String firstText(String first, String second) {
        return first == null || first.isBlank() ? second : first;
    }

    private Object firstNonNull(Object first, Object second) {
        return first != null ? first : second;
    }

    private String stringPayload(AgentRuntimeEvent event, String key) {
        Object value = event.getPayload() == null ? null : event.getPayload().get(key);
        return value == null ? null : String.valueOf(value);
    }

    private String errorMessage(AgentRuntimeEvent event) {
        String message = stringPayload(event, "message");
        if (message != null && !message.isBlank()) {
            return message;
        }
        String reason = stringPayload(event, "reason");
        return reason == null || reason.isBlank() ? "Agent 运行失败" : reason;
    }
}
