UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

962 lines (817 loc) 32.7 kB
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */ // Disable the auto sort key eslint rule to make the code more logic and readable import { GroupMemberInfo, buildGroupChatSystemPrompt, filterMessagesForAgent, } from '@lobechat/prompts'; import { ChatErrorType, CreateMessageParams, SendGroupMessageParams, UIChatMessage, } from '@lobechat/types'; import { produce } from 'immer'; import { StateCreator } from 'zustand/vanilla'; import { LOADING_FLAT } from '@/const/message'; import { DEFAULT_CHAT_GROUP_CHAT_CONFIG } from '@/const/settings'; import { ChatStore } from '@/store/chat/store'; import { messageMapKey } from '@/store/chat/utils/messageMapKey'; import { useSessionStore } from '@/store/session'; import { sessionSelectors } from '@/store/session/selectors'; import { userProfileSelectors } from '@/store/user/selectors'; import { getUserStoreState } from '@/store/user/store'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; import type { ChatStoreState } from '../../../initialState'; import { toggleBooleanList } from '../../../utils'; import { GroupChatSupervisor, SupervisorContext, SupervisorDecisionList, SupervisorTodoItem, } from '../../message/supervisor'; const n = setNamespace('aiGroupChat'); const supervisor = new GroupChatSupervisor(); /** * Delay between sequential agent responses in milliseconds */ const SEQUENTIAL_RESPONSE_DELAY = 1500; const getDebounceThreshold = (responseSpeed?: 'slow' | 'medium' | 'fast'): number => { switch (responseSpeed) { case 'fast': { return 1500; } case 'medium': { return 5000; } case 'slow': { return 8000; } default: { return 5000; } } }; const formatSupervisorTodoContent = (todos: SupervisorTodoItem[]): string => { // Pass todo data as JSON string for dedicated UI processing return JSON.stringify({ type: 'supervisor_todo', todos: todos || [], timestamp: Date.now(), }); }; /** * Extract mentioned agent IDs from message content * Looks for <mention id="agentId">Name</mention> tags */ const extractMentionsFromContent = ( content: string, groupMembers?: GroupMemberInfo[], ): string[] => { const mentionRegex = /<mention\s+[^>]*id="([^"]+)"[^>]*\/>/g; const mentions = new Set<string>(); let match; while ((match = mentionRegex.exec(content)) !== null) { const mentionId = match[1]; if (!mentionId) continue; if (mentionId === 'ALL_MEMBERS') { if (groupMembers?.length) { groupMembers.forEach((member) => { if (member.id) mentions.add(member.id); }); } continue; } mentions.add(mentionId); } return [...mentions]; }; /** * Check if a message is a tool calling message that requires a follow-up */ const isToolCallMessage = (message: UIChatMessage): boolean => { return message.role === 'assistant' && !!message.tools && message.tools.length > 0; }; /** * Count consecutive assistant messages from the end of the message list * This helps enforce maxResponseInRow limit */ const countConsecutiveAssistantMessages = (messages: UIChatMessage[]): number => { let count = 0; // Count from the end of the array backwards for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; // Stop counting if we hit a user message if (message.role === 'user') { break; } // Count assistant messages (including those from agents) if (message.role === 'assistant') { count++; } // Skip system and tool messages, continue counting } return count; }; /** * Check if we should avoid supervisor decisions based on recent messages * Returns true if the conversation flow should continue without supervisor intervention */ const shouldAvoidSupervisorDecision = ( messages: UIChatMessage[], maxResponseInRow?: number, isManualTrigger: boolean = false, ): boolean => { if (messages.length === 0) return true; const lastMessage = messages.at(-1); if (!lastMessage) return true; // Don't make decisions if the last message is a tool calling message // (it needs a follow-up assistant message) if (isToolCallMessage(lastMessage)) { return true; } // Don't make decisions if the last message is a tool response // (the conversation might still be in a tool calling sequence) if (lastMessage.role === 'tool') { return true; } // Only check maxResponseInRow limit for automatic triggers, not manual ones if (!isManualTrigger && maxResponseInRow && maxResponseInRow > 0) { const consecutiveCount = countConsecutiveAssistantMessages(messages); if (consecutiveCount >= maxResponseInRow) { console.log( `Avoiding automatic supervisor decision: ${consecutiveCount} consecutive assistant messages exceed limit of ${maxResponseInRow}`, ); return true; } } return false; }; export interface ChatGroupChatAction { /** * Sends a new message to a group chat and triggers agent responses */ sendGroupMessage: (params: SendGroupMessageParams) => Promise<void>; // ========= ↓ Internal Group Chat Methods ↓ ========== // /** * Triggers supervisor decision for group chat */ internal_triggerSupervisorDecision: ( groupId: string, topicId?: string | null, isManualTrigger?: boolean, ) => Promise<void>; /** * Triggers supervisor decision with debounce logic (dynamic threshold based on group responseSpeed setting) * Fast: 1s, Medium: 2s, Slow: 5s, Default: 3s * Cancels previous pending decisions and schedules a new one */ internal_triggerSupervisorDecisionDebounced: (groupId: string) => void; /** * Cancels pending supervisor decision for a group */ internal_cancelSupervisorDecision: (groupId: string) => void; /** * Cancels all pending supervisor decisions (cleanup method) */ internal_cancelAllSupervisorDecisions: () => void; /** * Update supervisor todo list for a group/topic combination */ internal_updateSupervisorTodos: ( groupId: string, topicId: string | null | undefined, todos: SupervisorTodoItem[], ) => void; /** * Executes agent responses for group chat based on supervisor decisions */ internal_executeAgentResponses: ( groupId: string, decisions: SupervisorDecisionList, ) => Promise<void>; /** * Processes a single agent message in group chat */ internal_processAgentMessage: ( groupId: string, agentId: string, targetId?: string, instruction?: string, ) => Promise<void>; /** * Sets the active group */ internal_setActiveGroup: (groupId: string) => void; /** * Toggles supervisor loading state for group chat */ internal_toggleSupervisorLoading: (loading: boolean, groupId?: string) => void; /** * Creates a supervisor error message for group chat */ internal_createSupervisorErrorMessage: ( groupId: string, error: Error | string, context?: string, ) => Promise<void>; } export const chatAiGroupChat: StateCreator< ChatStore, [['zustand/devtools', never]], [], ChatGroupChatAction > = (set, get) => { const selectGroupConfig = (groupId: string) => { const { groupMaps } = get(); const group = groupMaps[groupId]; return merge(DEFAULT_CHAT_GROUP_CHAT_CONFIG, group?.config || {}); }; return { sendGroupMessage: async ({ groupId, message, files, onlyAddUserMessage, targetMemberId }) => { const { internal_createMessage, internal_triggerSupervisorDecisionDebounced, internal_setActiveGroup, activeTopicId, } = get(); if (!message.trim() && (!files || files.length === 0)) return; internal_setActiveGroup(groupId); set({ isCreatingMessage: true }, false, n('creatingGroupMessage/start')); try { const userMessage: CreateMessageParams = { content: message, files: files?.map((f) => f.id), role: 'user', groupId, sessionId: useSessionStore.getState().activeId, topicId: activeTopicId, targetId: targetMemberId, }; const messageId = await internal_createMessage(userMessage); // if only add user message, then stop if (onlyAddUserMessage) { set({ isCreatingMessage: false }, false, n('creatingGroupMessage/onlyUser')); return; } if (messageId) { // Use the specific group's config rather than relying on active session const groupConfig = selectGroupConfig(groupId); // If supervisor is disabled, check for direct mentions and trigger them directly if (!groupConfig?.enableSupervisor) { const agents = sessionSelectors.currentGroupAgents(useSessionStore.getState()); const mentionableGroupAgents: GroupMemberInfo[] = agents.map((agent) => ({ id: agent.id, title: agent.title ?? agent.id, })); const mentionedAgentIds = extractMentionsFromContent(message, mentionableGroupAgents); const candidateAgentIds = new Set(mentionedAgentIds); if (targetMemberId && agents?.some((agent) => agent.id === targetMemberId)) { candidateAgentIds.add(targetMemberId); } if (candidateAgentIds.size > 0) { // Validate that mentioned agents exist in the group const validMentionedAgents = [...candidateAgentIds].filter((agentId) => agents?.some((agent) => agent.id === agentId), ); if (validMentionedAgents.length > 0) { console.log( 'Supervisor disabled, triggering direct mentions:', validMentionedAgents, ); // Process mentioned agents directly without supervisor decision const { internal_executeAgentResponses } = get(); const decisions = validMentionedAgents.map((agentId) => ({ id: agentId, target: targetMemberId && agentId === targetMemberId ? 'user' : undefined, })); await internal_executeAgentResponses(groupId, decisions); } else { console.log('Supervisor disabled, mentioned agents not found in group'); } } else { if (targetMemberId) { console.log( 'Supervisor disabled and DM target not found in group, no agent responses triggered', ); } else { console.log( 'Supervisor disabled and no mentions found, no agent responses triggered', ); } } } else { internal_triggerSupervisorDecisionDebounced(groupId); } } } catch (error) { console.error('Failed to send group message:', error); } finally { set({ isCreatingMessage: false }, false, n('creatingGroupMessage/end')); } }, // ========= ↓ Group Chat Internal Methods ↓ ========== // internal_triggerSupervisorDecision: async ( groupId: string, topicId?: string | null, isManualTrigger: boolean = false, ) => { const { messagesMap, internal_toggleSupervisorLoading, internal_createMessage, supervisorTodos, } = get(); // Capture topicId at invocation time to avoid leaking state after topic switches const currentTopicId = typeof topicId === 'undefined' ? get().activeTopicId : topicId; // Always read config for the provided groupId early so it can be used in createSupervisorTodoMessage const groupConfig = selectGroupConfig(groupId); const createSupervisorTodoMessage = async (todoList: SupervisorTodoItem[]) => { if (!groupId) return; const sessionId = useSessionStore.getState().activeId || groupId; if (!sessionId) return; const content = formatSupervisorTodoContent(todoList); const supervisorMessage: CreateMessageParams = { content, fromModel: groupConfig.orchestratorModel, fromProvider: groupConfig.orchestratorProvider, groupId, role: 'supervisor', sessionId, topicId: currentTopicId ?? undefined, }; console.log('Creating supervisor todo message:', supervisorMessage); await internal_createMessage(supervisorMessage); }; const messages = messagesMap[messageMapKey(groupId, currentTopicId)] || []; const agents = sessionSelectors.currentGroupAgents(useSessionStore.getState()); if (messages.length === 0) return; // If supervisor is disabled, skip supervisor decision if (!groupConfig?.enableSupervisor) { console.log('Supervisor is disabled for this group, skipping supervisor decision'); return; } // Skip supervisor decision if we're in the middle of tool calling sequence or exceeded maxResponseInRow (for automatic triggers only) if (shouldAvoidSupervisorDecision(messages, groupConfig?.maxResponseInRow, isManualTrigger)) { const reason = isManualTrigger ? 'waiting for tool calling sequence to complete' : 'waiting for tool calling sequence to complete or max responses exceeded'; console.log(`Skipping supervisor decision - ${reason}`); return; } // Create AbortController for this supervisor decision const abortController = new AbortController(); // Store the AbortController in state set( produce((state: ChatStoreState) => { state.supervisorDecisionAbortControllers[groupId] = abortController; }), false, n(`setSupervisorAbortController/${groupId}`), ); // Get real user name from user store const userStoreState = getUserStoreState(); const realUserName = userProfileSelectors.nickName(userStoreState) || 'User'; try { const todoKey = messageMapKey(groupId, currentTopicId); const context: SupervisorContext = { allowDM: groupConfig.allowDM, availableAgents: agents!, groupId, messages, model: groupConfig.orchestratorModel, provider: groupConfig.orchestratorProvider, scene: groupConfig.scene, userName: realUserName, systemPrompt: groupConfig.systemPrompt, abortController, todoList: supervisorTodos?.[todoKey] || [], }; internal_toggleSupervisorLoading(true, groupId); const { decisions, todos, todoUpdated } = await supervisor.makeDecision(context); // Turn off supervisor thinking immediately after decision is made internal_toggleSupervisorLoading(false, groupId); get().internal_updateSupervisorTodos(groupId, currentTopicId, todos); if (todoUpdated) { await createSupervisorTodoMessage(todos); } console.log('Supervisor decisions:', decisions); if (decisions.length > 0) { await get().internal_executeAgentResponses(groupId, decisions); } } catch (error) { // Turn off supervisor thinking on error internal_toggleSupervisorLoading(false, groupId); if ( (error instanceof Error && error.name === 'AbortError') || (error instanceof Error && error.message.includes('The operation was aborted')) ) { console.log('Supervisor decision was aborted for group:', groupId); // Don't create error message for intentional aborts } else { console.error('Supervisor decision failed:', error); // Create supervisor error message to show the error to users await get().internal_createSupervisorErrorMessage( groupId, 'Supervisor Decision Failed, Please check your configuration', ); } } finally { // Clean up AbortController from state set( produce((state: ChatStoreState) => { delete state.supervisorDecisionAbortControllers[groupId]; }), false, n(`cleanupSupervisorAbortController/${groupId}`), ); } }, internal_executeAgentResponses: async (groupId: string, decisions: SupervisorDecisionList) => { console.log('DEBUG: Executing agent responses with decisions:', decisions); const { internal_processAgentMessage, internal_triggerSupervisorDecisionDebounced } = get(); // Read the target group's config to respect per-group settings const groupConfig = selectGroupConfig(groupId); const agents = sessionSelectors.currentGroupAgents(useSessionStore.getState()); // Sort decisions by member order if response order is sequential const sortedDecisions = groupConfig?.responseOrder === 'sequential' ? [...decisions].sort((a, b) => { const agentA = agents?.find((agent) => agent.id === a.id); const agentB = agents?.find((agent) => agent.id === b.id); // Default to order 0 if not found or not set const orderA = agentA?.order ?? 0; const orderB = agentB?.order ?? 0; return orderA - orderB; }) : decisions; try { if (groupConfig?.responseOrder === 'sequential') { // Process agents sequentially with delay for (const [index, decision] of sortedDecisions.entries()) { // Add delay between agents (except for the first one) if (index > 0) { await new Promise((resolve) => { setTimeout(resolve, SEQUENTIAL_RESPONSE_DELAY); }); } await internal_processAgentMessage( groupId, decision.id, decision.target, decision.instruction, ); } } else { // Process agents in parallel for natural response order const responsePromises = sortedDecisions.map((decision) => internal_processAgentMessage( groupId, decision.id, decision.target, decision.instruction, ), ); await Promise.all(responsePromises); } // Only trigger next supervisor decision after ALL agents have completed their responses // This prevents rapid-fire agent responses and gives time for conversation to settle if (sortedDecisions.length > 0) { internal_triggerSupervisorDecisionDebounced(groupId); } } catch (error) { console.error('Failed to execute agent responses:', error); // Create supervisor error message to show the error to users await get().internal_createSupervisorErrorMessage( groupId, error instanceof Error ? error : new Error(String(error)), 'Agent Response Execution Failed', ); } }, // For group member responsing internal_processAgentMessage: async ( groupId: string, agentId: string, targetId?: string, instruction?: string, ) => { console.log('DEBUG: internal_processAgentMessage called with:', { groupId, agentId, targetId, instruction, }); const { messagesMap, internal_createMessage, internal_fetchAIChatMessage, refreshMessages, activeTopicId, internal_dispatchMessage, internal_toggleChatLoading, triggerToolCalls, } = get(); try { const allMessages = messagesMap[messageMapKey(groupId, activeTopicId)] || []; if (allMessages.length === 0) return; // Filter messages for this specific agent based on DM targeting rules const messages = filterMessagesForAgent(allMessages, agentId); // Get group agents and find the specific agent const agents = sessionSelectors.currentGroupAgents(useSessionStore.getState()); const agentData = agents?.find((agent) => agent.id === agentId); if (!agentData) { console.error(`Agent ${agentId} not found in group members`); return; } const agentProvider = agentData.provider || undefined; const agentModel = agentData.model || undefined; console.log('DEBUG: Group chat agent data:', agentData); if (!agentProvider || !agentModel) { console.error(`No provider or model configured for agent ${agentId}`); return; } // Get the individual agent's full configuration including temperature, top_p, etc. // const agentStoreState = getAgentStoreState(); // const agentConfig = agentSelectors.getAgentConfigById(agentId)(agentStoreState); // Get real user name from user store const userStoreState = getUserStoreState(); const realUserName = userProfileSelectors.nickName(userStoreState) || 'User'; const agentTitleMap: GroupMemberInfo[] = [ { id: 'user', title: realUserName }, ...(agents || []).map((agent) => ({ id: agent.id || '', title: agent.title || '' })), ]; const baseSystemRole = agentData.systemRole || ''; const members: GroupMemberInfo[] = agentTitleMap as GroupMemberInfo[]; const groupChatSystemPrompt = buildGroupChatSystemPrompt({ groupMembers: members, baseSystemRole, agentId, messages, targetId, instruction, }); // Create agent message using real agent config const agentMessage: CreateMessageParams = { role: 'assistant', fromModel: agentModel, groupId, content: LOADING_FLAT, fromProvider: agentProvider, agentId, sessionId: useSessionStore.getState().activeId, topicId: activeTopicId, targetId: targetId, // Use targetId when provided for DM messages }; console.log('DEBUG: Creating agent message with:', agentMessage); const assistantId = await internal_createMessage(agentMessage); const systemMessage: UIChatMessage = { id: 'group-system', role: 'system', content: groupChatSystemPrompt, createdAt: Date.now(), updatedAt: Date.now(), meta: {}, }; // Add author names to messages for better context const messagesWithAuthors = messages.map((msg) => { const authorInfo = agentTitleMap.find((member) => msg.role === 'user' ? member.id === 'user' : member.id === msg.agentId, ); const authorName = authorInfo?.title || (msg.role === 'user' ? realUserName : 'Unknown'); const authorId = msg.role === 'user' ? 'user' : msg.agentId || 'unknown'; // Keep user message as-is if (msg.role === 'user') { return { ...msg, content: msg.content, }; } return { ...msg, content: `<author_name_do_not_include_in_your_response name="${authorName}" id="${authorId}" />${msg.content}`, }; }); // TODO: Use context engineering const messagesForAPI = [systemMessage, ...messagesWithAuthors]; if (assistantId) { const { isFunctionCall } = await internal_fetchAIChatMessage({ messages: messagesForAPI, messageId: assistantId, model: agentModel, provider: agentProvider, params: { traceId: `group-${groupId}-agent-${agentId}`, agentConfig: agentData, }, }); // Handle tool calling in group chat like single chat if (isFunctionCall) { get().internal_toggleMessageInToolsCalling(true, assistantId); await refreshMessages(); await triggerToolCalls(assistantId, { threadId: undefined, inPortalThread: false, }); // Change: if an agent message is a tool call, make the same agent speak again // instead of asking supervisor for a decision. await get().internal_processAgentMessage(groupId, agentId, targetId, instruction); return; } } await refreshMessages(); // Don't trigger supervisor decision after individual agent responses // This prevents infinite loops of agent responses // Supervisor decisions should only be triggered after user messages or when all agents complete } catch (error) { console.error(`Failed to process message for agent ${agentId}:`, error); // Create supervisor error message to show the error to users await get().internal_createSupervisorErrorMessage( groupId, error instanceof Error ? error : new Error(String(error)), `Agent ${agentId} Response Failed`, ); // Also update error state if we have an assistant message (for consistency with single chat) const currentMessages = get().messagesMap[messageMapKey(groupId, activeTopicId)] || []; const errorMessage = currentMessages.find( (m) => m.role === 'assistant' && m.agentId === agentId && m.content === LOADING_FLAT, ); if (errorMessage) { internal_dispatchMessage({ id: errorMessage.id, type: 'updateMessage', value: { content: `Error: Failed to generate response. ${error instanceof Error ? error.message : 'Unknown error'}`, error: { type: ChatErrorType.CreateMessageError, message: error instanceof Error ? error.message : 'Unknown error', }, }, }); } } finally { internal_toggleChatLoading(false, undefined, n('processAgentMessage(end)')); } }, internal_setActiveGroup: () => { // Update the active session type to 'group' when setting an active group get().internal_updateActiveSessionType('group'); }, internal_toggleSupervisorLoading: (loading: boolean, groupId?: string) => { set( { supervisorDecisionLoading: groupId ? toggleBooleanList(get().supervisorDecisionLoading, groupId, loading) : loading ? get().supervisorDecisionLoading : [], }, false, n(`toggleSupervisorLoading/${loading ? 'start' : 'end'}`), ); }, internal_triggerSupervisorDecisionDebounced: (groupId: string) => { const { internal_cancelSupervisorDecision, internal_triggerSupervisorDecision } = get(); internal_cancelSupervisorDecision(groupId); // Use per-group config for debounce calculation const groupConfig = selectGroupConfig(groupId); const responseSpeed = groupConfig?.responseSpeed; const debounceThreshold = getDebounceThreshold(responseSpeed); console.log( `Using debounce threshold: ${debounceThreshold}ms for responseSpeed: ${responseSpeed}`, ); // Capture topicId at schedule time to decouple from future topic switches const scheduledTopicId = get().activeTopicId; // Set a new timer with dynamic debounce based on group settings const timerId = setTimeout(async () => { console.log(`Debounced supervisor decision triggered for group ${groupId}`); // Clean up the timer from state before executing set( produce((state: ChatStoreState) => { delete state.supervisorDebounceTimers[groupId]; }), false, n(`cleanupSupervisorTimer/${groupId}`), ); try { await internal_triggerSupervisorDecision(groupId, scheduledTopicId, false); // false = automatic trigger } catch (error) { console.error(`Failed to execute supervisor decision for group ${groupId}:`, error); } }, debounceThreshold); // Store the timer in state set( produce((state: ChatStoreState) => { state.supervisorDebounceTimers[groupId] = timerId as any; }), false, n(`setSupervisorTimer/${groupId}`), ); }, internal_cancelSupervisorDecision: (groupId: string) => { const { supervisorDebounceTimers, supervisorDecisionAbortControllers, internal_toggleSupervisorLoading, } = get(); const existingTimer = supervisorDebounceTimers[groupId]; const existingAbortController = supervisorDecisionAbortControllers[groupId]; // Cancel pending debounced timer if (existingTimer) { clearTimeout(existingTimer); console.log(`Cancelled pending supervisor decision timer for group ${groupId}`); // Remove timer from state set( produce((state: ChatStoreState) => { delete state.supervisorDebounceTimers[groupId]; }), false, n(`cancelSupervisorTimer/${groupId}`), ); } // Abort ongoing supervisor decision request if (existingAbortController) { existingAbortController.abort('User cancelled supervisor decision'); console.log(`Aborted ongoing supervisor decision request for group ${groupId}`); // Remove abort controller from state set( produce((state: ChatStoreState) => { delete state.supervisorDecisionAbortControllers[groupId]; }), false, n(`cancelSupervisorAbortController/${groupId}`), ); } // Stop the loading state internal_toggleSupervisorLoading(false, groupId); console.log(`Stopped supervisor loading state for group ${groupId}`); }, internal_cancelAllSupervisorDecisions: () => { const { supervisorDebounceTimers, supervisorDecisionAbortControllers } = get(); const timerGroupIds = Object.keys(supervisorDebounceTimers); const abortControllerGroupIds = Object.keys(supervisorDecisionAbortControllers); if (timerGroupIds.length > 0 || abortControllerGroupIds.length > 0) { console.log('Cancelling all pending supervisor decisions for session change/cleanup'); // Cancel all timers timerGroupIds.forEach((groupId) => { const timer = supervisorDebounceTimers[groupId]; if (timer) { clearTimeout(timer); } }); // Abort all ongoing requests abortControllerGroupIds.forEach((groupId) => { const abortController = supervisorDecisionAbortControllers[groupId]; if (abortController) { abortController.abort('Session cleanup'); } }); // Clear all timers and abort controllers from state set( { supervisorDebounceTimers: {}, supervisorDecisionAbortControllers: {}, }, false, n('cancelAllSupervisorDecisions'), ); } }, internal_updateSupervisorTodos: (groupId, topicId, todos) => { if (!groupId) return; const key = messageMapKey(groupId, topicId); set( produce((state: ChatStoreState) => { state.supervisorTodos[key] = todos; }), false, n(`internal_updateSupervisorTodos/${groupId}`), ); }, internal_createSupervisorErrorMessage: async (groupId: string, error: Error | string) => { const { internal_createTmpMessage, activeTopicId } = get(); try { const errorMessage = error instanceof Error ? error.message : error; const groupConfig = selectGroupConfig(groupId); const supervisorMessage: CreateMessageParams = { role: 'supervisor', fromModel: groupConfig.orchestratorModel, fromProvider: groupConfig.orchestratorProvider, groupId, sessionId: useSessionStore.getState().activeId || groupId, topicId: activeTopicId, error: { type: ChatErrorType.SupervisorDecisionFailed, message: errorMessage, }, content: LOADING_FLAT, }; // Create a temporary message that only exists in UI state, no API call internal_createTmpMessage(supervisorMessage); } catch (createError) { console.error('Failed to create supervisor error message:', createError); } }, }; };