diff --git a/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts b/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts index 8056bae07..13700b0f0 100644 --- a/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts +++ b/services/telegram-bot/src/bots/telegram/agent/actions/read-message.ts @@ -1,10 +1,12 @@ import type { Message } from 'grammy/types' -import type { BotSelf, ReadUnreadMessagesAction } from '../../../../types' +import type { BotContext, ReadUnreadMessagesAction } from '../../../../types' import { env } from 'node:process' import { useLogg } from '@guiiai/logg' +import { withRetry } from '@moeru/std' +import { trace } from '@opentelemetry/api' import { embed } from '@xsai/embed' import { findLastNMessages, findRelevantMessages } from '../../../../models' @@ -12,7 +14,7 @@ import { chatMessageToOneLine, telegramMessageToOneLine } from '../../../../mode import { actionReadMessages } from '../../../../prompts' export async function readMessage( - state: BotSelf, + state: BotContext, botId: string, chatId: string, action: ReadUnreadMessagesAction, @@ -24,43 +26,76 @@ export async function readMessage( result: string }> { const logger = useLogg('readMessage').useGlobalConfig() + const tracer = trace.getTracer('airi.telegram.bot') - const lastNMessages = await findLastNMessages(action.chatId, 30) - const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg)).join('\n') - logger.withField('number_of_last_n_messages', lastNMessages.length).log('Successfully found last N messages') + return await tracer.startActiveSpan('telegram.module.read_message', async (span) => { + span.setAttribute('telegram.bot.id', botId) - const unreadMessagesEmbeddingPromises = unreadMessages - .filter(msg => !!msg.text || !!msg.caption) - .map(async (msg: Message) => { - const embeddingResult = await embed({ - baseURL: env.EMBEDDING_API_BASE_URL!, - apiKey: env.EMBEDDING_API_KEY!, - model: env.EMBEDDING_MODEL!, - input: msg.text || msg.caption || '', - abortSignal: abortController.signal, + const lastNMessages = await tracer.startActiveSpan('telegram.module.read_message.find_last_n_messages', async (span) => { + const res = await findLastNMessages(action.chatId, 30) + span.end() + return res + }) + const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg)).join('\n') + logger.withField('number_of_last_n_messages', lastNMessages.length).log('Successfully found last N messages') + + const unreadMessagesEmbeddingPromises = unreadMessages + .filter(msg => !!msg.text || !!msg.caption) + .map(async (msg: Message) => { + const embeddingResult = await tracer.startActiveSpan('llm.embed.embed_with_retry', async (span) => { + const res = await withRetry(async () => { + return await tracer.startActiveSpan('llm.embed.embed', async (span) => { + span.setAttribute('llm.embed.model', env.EMBEDDING_MODEL!) + span.setAttribute('llm.embed.messages', msg.text || msg.caption || '') + span.setAttribute('llm.provider.api_base_url', env.EMBEDDING_API_BASE_URL!) + + const res = await embed({ + baseURL: env.EMBEDDING_API_BASE_URL!, + apiKey: env.EMBEDDING_API_KEY!, + model: env.EMBEDDING_MODEL!, + input: msg.text || msg.caption || '', + abortSignal: abortController.signal, + }) + + span.end() + return res + }) + }, { retry: 5 })() + + span.end() + return res + }) + + return embeddingResult }) - return embeddingResult + const unreadHistoryMessagesEmbedding = await Promise.all(unreadMessagesEmbeddingPromises) + logger.withField('number_of_tasks', unreadMessagesEmbeddingPromises.length).log('Successfully embedded unread history messages') + + const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.chatId].map(msg => telegramMessageToOneLine(botId, msg))) + const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n') + + const existingKnownMessages = [...unreadMessages.map(msg => msg.message_id.toString()), ...lastNMessages.map(msg => msg.platform_message_id)] + const relevantChatMessages = await tracer.startActiveSpan('telegram.module.read_message.find_relevant_messages', async (span) => { + const res = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding, existingKnownMessages) + span.setAttribute('telegram.module.read_message.found_relevant_messages', JSON.stringify(res)) + + span.end() + return res }) - const unreadHistoryMessagesEmbedding = await Promise.all(unreadMessagesEmbeddingPromises) - logger.withField('number_of_tasks', unreadMessagesEmbeddingPromises.length).log('Successfully embedded unread history messages') + const relevantChatMessagesOneliner = relevantChatMessages.map(msgs => msgs.join('\n')).join('\n') + logger.withField('number_of_relevant_chat_messages', relevantChatMessages.length).log('Successfully composed relevant chat messages') - const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.chatId].map(msg => telegramMessageToOneLine(botId, msg))) - const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n') + state.unreadMessages[action.chatId] = [] - const existingKnownMessages = [...unreadMessages.map(msg => msg.message_id.toString()), ...lastNMessages.map(msg => msg.platform_message_id)] - const relevantChatMessages = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding, existingKnownMessages) - const relevantChatMessagesOneliner = relevantChatMessages.map(msgs => msgs.join('\n')).join('\n') - logger.withField('number_of_relevant_chat_messages', relevantChatMessages.length).log('Successfully composed relevant chat messages') - - state.unreadMessages[action.chatId] = [] - - return { - break: true, - result: await actionReadMessages({ - lastMessages: lastNMessagesOneliner, - unreadHistoryMessages: unreadHistoryMessageOneliner, - relevantChatMessages: relevantChatMessagesOneliner, - }), - } + span.end() + return { + break: true, + result: await actionReadMessages({ + lastMessages: lastNMessagesOneliner, + unreadHistoryMessages: unreadHistoryMessageOneliner, + relevantChatMessages: relevantChatMessagesOneliner, + }), + } + }) } diff --git a/services/telegram-bot/src/bots/telegram/agent/actions/send-message.ts b/services/telegram-bot/src/bots/telegram/agent/actions/send-message.ts index 91ad1e960..b5af46ed6 100644 --- a/services/telegram-bot/src/bots/telegram/agent/actions/send-message.ts +++ b/services/telegram-bot/src/bots/telegram/agent/actions/send-message.ts @@ -1,7 +1,7 @@ import type { GenerateTextOptions } from '@xsai/generate-text' import type { Message } from 'grammy/types' -import type { BotSelf } from '../../../../types' +import type { BotContext, ChatContext } from '../../../../types' import { env } from 'node:process' @@ -35,7 +35,8 @@ export function parseMayStructuredMessage(responseText: string) { } export async function sendMessage( - state: BotSelf, + botContext: BotContext, + chatContext: ChatContext, responseText: string, groupId: string, abortController: AbortController, @@ -45,23 +46,23 @@ export async function sendMessage( const chat = (await listJoinedChats()).find((chat) => { return chat.chat_id === groupId }) - state.logger.withField('chat', chat).log('Chat found') + botContext.logger.withField('chat', chat).log('Chat found') if (!chat) { - state.logger.withField('groupId', groupId).log('Chat not found') + botContext.logger.withField('groupId', groupId).log('Chat not found') return } const chatId = chat.chat_id // Cancel any existing task before starting a new one - if (state.currentTask) { - state.currentTask.cancel() - state.currentTask = null + if (chatContext.currentTask) { + chatContext.currentTask.cancel() + chatContext.currentTask = null } // Check if we should abort due to new messages since processing began - if (state.unreadMessages[chatId] && state.unreadMessages[chatId].length > 0) { - state.logger.log(`Not sending message to ${chatId} - new messages arrived`) + if (botContext.unreadMessages[chatId] && botContext.unreadMessages[chatId].length > 0) { + botContext.logger.log(`Not sending message to ${chatId} - new messages arrived`) return // Don't send the message, let the next processing loop handle it } @@ -103,23 +104,23 @@ export async function sendMessage( const structuredMessage = parseMayStructuredMessage(res.text) if (structuredMessage == null) { - state.logger.log(`Not sending message to ${chatId} - no messages to send`) + botContext.logger.log(`Not sending message to ${chatId} - no messages to send`) return } - state.logger.withField('texts', structuredMessage).log('Sending messages') + botContext.logger.withField('texts', structuredMessage).log('Sending messages') // If we get here, the task wasn't cancelled, so we can send the response for (let i = 0; i < structuredMessage.messages.length; i++) { const item = structuredMessage.messages[i] if (!item) { - state.logger.log(`Not sending message to ${chatId} - no messages to send`) + botContext.logger.log(`Not sending message to ${chatId} - no messages to send`) continue } // Create cancellable typing and reply tasks try { - await state.bot.api.sendChatAction(chatId, 'typing') + await botContext.bot.api.sendChatAction(chatId, 'typing') } catch { @@ -131,25 +132,25 @@ export async function sendMessage( const validReplyToMessageId = structuredMessage.reply_to_message_id ? Number.parseInt(structuredMessage.reply_to_message_id) : undefined if (i === 0 && validReplyToMessageId && !Number.isNaN(validReplyToMessageId)) { - const sentResult = await state.bot.api.sendMessage(chatId, item, { reply_parameters: { message_id: validReplyToMessageId } }) + const sentResult = await botContext.bot.api.sendMessage(chatId, item, { reply_parameters: { message_id: validReplyToMessageId } }) return sentResult } else { - const sentResult = await state.bot.api.sendMessage(chatId, item) + const sentResult = await botContext.bot.api.sendMessage(chatId, item) return sentResult } } catch (err) { - state.logger.withError(err).log('Failed to send message') + botContext.logger.withError(err).log('Failed to send message') throw err } })()) - state.currentTask = replyTask + chatContext.currentTask = replyTask const msg = await replyTask.promise - await recordMessage(state.bot.botInfo, msg) + await recordMessage(botContext.bot.botInfo, msg) await sleep(randomInt(50, 1000)) } - state.currentTask = null + chatContext.currentTask = null } diff --git a/services/telegram-bot/src/bots/telegram/agent/attention-handler.ts b/services/telegram-bot/src/bots/telegram/agent/attention-handler.ts index 4c1a8efec..7faa1d854 100644 --- a/services/telegram-bot/src/bots/telegram/agent/attention-handler.ts +++ b/services/telegram-bot/src/bots/telegram/agent/attention-handler.ts @@ -1,8 +1,8 @@ import type { Message } from 'grammy/types' -import type { AttentionConfig, AttentionState, BotSelf } from '../../../types' +import type { AttentionConfig, AttentionState, BotContext } from '../../../types' -export function createAttentionHandler(bot: BotSelf, config: AttentionConfig) { +export function createAttentionHandler(bot: BotContext, config: AttentionConfig) { // Private state const state: AttentionState = { currentResponseRate: config.initialResponseRate, diff --git a/services/telegram-bot/src/bots/telegram/index.ts b/services/telegram-bot/src/bots/telegram/index.ts index 182c67906..19dc5350b 100644 --- a/services/telegram-bot/src/bots/telegram/index.ts +++ b/services/telegram-bot/src/bots/telegram/index.ts @@ -1,8 +1,7 @@ import type { Logg } from '@guiiai/logg' -import type { Message as LLMMessage } from '@xsai/shared-chat' import type { Message } from 'grammy/types' -import type { Action, BotSelf, ExtendedContext } from '../../types' +import type { Action, BotContext, ChatContext, ExtendedContext } from '../../types' import { env } from 'node:process' @@ -19,58 +18,240 @@ import { listJoinedChats, recordJoinedChat } from '../../models/chats' import { listStickerPacks, recordStickerPack } from '../../models/sticker-packs' import { readMessage } from './agent/actions/read-message' import { sendMessage } from './agent/actions/send-message' -import { shouldInterruptProcessing } from './agent/interruption' +// import { shouldInterruptProcessing } from './agent/interruption' -interface AgentState { - messages: LLMMessage[] - actions: { action: Action, result: unknown }[] +async function dispatchAction(ctx: BotContext, action: Action, abortController: AbortController, chatCtx?: ChatContext) { + // If action generation failed, don't proceed with further processing + if (!action || !action.action) { + ctx.logger.withField('action', action).log('No valid action returned.') + if (chatCtx) { + chatCtx.messages.push(message.user('AIRI System: No valid action returned.')) + return () => handleLoopStep(ctx, chatCtx) + } + else { + return + } + } + + switch (action.action) { + case 'list_stickers': + { + if (chatCtx) { + await ctx.bot.api.sendChatAction(chatCtx.chatId, 'choose_sticker') + } + + const stickerPacks = await listStickerPacks() + const stickerSets = await Promise.all(stickerPacks.map(s => ctx.bot.api.getStickerSet(s.platform_id))) + const stickersIds = stickerSets.flatMap(s => s.stickers.map(sticker => sticker.file_id)) + const stickerDescriptions = await findStickersByFileIds(stickersIds) + const stickerDescriptionsOneliner = stickerDescriptions.map(d => `Sticker File ID: ${d.file_id}, Description: ${d.description}`) + + if (chatCtx) { + if (stickerDescriptionsOneliner.length === 0) { + chatCtx.actions.push({ action, result: 'AIRI System: No stickers found in the current memory partition, preload of stickers is required, please ask for help.' }) + } + else { + chatCtx.actions.push({ action, result: `AIRI System: List of stickers:\n${stickerDescriptionsOneliner}` }) + } + } + + return () => handleLoopStep(ctx, chatCtx) + } + case 'send_sticker': + { + const chatCtx = ensureChatContext(ctx, action.chatId) + + try { + const file = await ctx.bot.api.getFile(action.fileId) + if (!file) { + chatCtx.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found, did you list the needed stickers before sending?` }) + return () => handleLoopStep(ctx, chatCtx) + } + } + catch (err) { + chatCtx.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found or failed due to ${String(err)}, did you list the needed stickers before sending?` }) + return () => handleLoopStep(ctx, chatCtx) + } + + const sticker = await findStickerByFileId(action.fileId) + chatCtx.actions.push({ action, result: `AIRI System: Sending sticker ${action.fileId} with (${sticker.emoji} in set ${sticker.name}) to ${action.chatId}` }) + await ctx.bot.api.sendSticker(action.chatId, action.fileId) + + return () => handleLoopStep(ctx, chatCtx) + } + case 'read_unread_messages': + { + const chatCtx = ensureChatContext(ctx, action.chatId) + + if (Object.keys(ctx.unreadMessages).length === 0) { + ctx.logger.withField('action', action).log('No unread messages - deleting all unread messages') + ctx.unreadMessages = {} + break + } + if (action.chatId == null) { + ctx.logger.withField('action', action).warn('No group ID - deleting all unread messages') + break + } + + let unreadMessagesForThisChat: Message[] | undefined = ctx.unreadMessages[action.chatId] + + const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(ctx.bot.botInfo.username) || msg.text?.includes(ctx.bot.botInfo.first_name)) + if (mentionedBy) { + chatCtx.messages.push(message.user(`AIRI System: You were mentioned in a message: ${mentionedBy.text} by ${mentionedBy.from?.first_name} (${mentionedBy.from?.username}), please respond as much as possible.`)) + } + + if (!Array.isArray(unreadMessagesForThisChat)) { + ctx.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`) + unreadMessagesForThisChat = [] + } + if (unreadMessagesForThisChat.length === 0) { + ctx.logger.withField('action', action).log(`No unread messages for group - deleting`) + delete ctx.unreadMessages[action.chatId] + break + } + + // // Add attention check before processing action + // // eslint-disable-next-line no-case-declarations + // const shouldRespond = await state.attentionHandler.shouldRespond(forGroupId, unreadMessagesForThisChat) + + // if (!shouldRespond.shouldAct) { + // state.logger.withField('reason', shouldRespond.reason).withField('responseRate', shouldRespond.responseRate).log('Skipping message due to attention check') + // state.unreadMessages[action.groupId] = unreadMessagesForThisChat.shift() + // return { break: true } + // } + + const res = await readMessage(ctx, ctx.bot.botInfo.id.toString(), chatCtx.chatId, action, unreadMessagesForThisChat, abortController) + if (res?.result) { + ctx.logger.log('message, read') + chatCtx.actions.push({ action, result: res.result }) + return () => handleLoopStep(ctx, chatCtx) + } + else { + return + } + } + case 'list_chats': + if (chatCtx) { + chatCtx.actions.push({ action, result: `AIRI System: List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}` }) + } + + return () => handleLoopStep(ctx, chatCtx) + case 'send_message': + { + const chatCtx = ensureChatContext(ctx, action.chatId) + chatCtx.actions.push({ action, result: `AIRI System: Sending message to group ${action.chatId}: ${action.content}` }) + await sendMessage(ctx, chatCtx, action.content, action.chatId, abortController) + return () => handleLoopStep(ctx, chatCtx) + } + case 'continue': + if (chatCtx) { + chatCtx.actions.push({ action, result: 'AIRI System: Acknowledged, will now continue until next tick.' }) + } + + return + case 'break': + if (chatCtx) { + chatCtx.messages = [] + chatCtx.actions = [] + chatCtx.actions.push({ action, result: 'AIRI System: Acknowledged, will now break, and clear out all existing memories, messages, actions. Left only this one.' }) + } + + return + case 'sleep': + await sleep(30 * 1000) + if (chatCtx) { + chatCtx.actions.push({ action, result: `AIRI System: Sleeping for ${30} seconds as requested...` }) + } + + return () => handleLoopStep(ctx, chatCtx) + default: + if (chatCtx) { + chatCtx.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`)) + } + + return () => handleLoopStep(ctx, chatCtx) + } } -async function handleLoopStep(bot: BotSelf, agentState: AgentState, chatId?: string): Promise<() => Promise | undefined> { - // Set the start time when beginning new processing - bot.currentProcessingStartTime = Date.now() +async function handleLoopStep(ctx: BotContext, chatCtx: ChatContext, incomingMessage?: Message): Promise<() => Promise | undefined> { + ctx.currentProcessingStartTime = Date.now() - // Create a new abort controller for this loop execution - if (bot.currentAbortController) { - bot.currentAbortController.abort() - } - bot.currentAbortController = new AbortController() - const currentController = bot.currentAbortController - - // Track message processing state - if (chatId && !bot.lastInteractedNChatIds.includes(chatId)) { - bot.lastInteractedNChatIds.push(chatId) - } - if (bot.lastInteractedNChatIds.length > 5) { - bot.lastInteractedNChatIds = bot.lastInteractedNChatIds.slice(-5) + if (chatCtx?.currentAbortController) { + chatCtx.currentAbortController.abort() } - if (agentState.messages == null) { - agentState.messages = [] - } - if (agentState.messages.length > 20) { - const length = agentState.messages.length - // pick the latest 5 - agentState.messages = agentState.messages.slice(-5) - agentState.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${agentState.messages.length}, history may lost.`)) - } + // // Create a new abort controller for this loop execution + // const unreadMessagesForThisChat = ctx.unreadMessages[chatCtx.chatId] - if (agentState.actions == null) { - agentState.actions = [] - } - if (agentState.actions.length > 50) { - const length = agentState.actions.length - // pick the latest 20 - agentState.actions = agentState.actions.slice(-20) - agentState.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${agentState.actions.length}, history of actions may lost.`)) + // if (unreadMessagesForThisChat && unreadMessagesForThisChat.length > 0) { + // const processingTime = ctx.currentProcessingStartTime + // ? Date.now() - ctx.currentProcessingStartTime + // : 0 + + // const messageCount = unreadMessagesForThisChat.length + + // // Factors to consider for interruption: + // // + // // 1. How long we've been processing (longer = more likely to finish) + // // 2. Number of new messages (more = higher chance to interrupt) + // // 3. Message content importance (could be determined by LLM) + // const shouldInterrupt = await shouldInterruptProcessing({ + // processingTime, + // messageCount, + // currentMessages: chatCtx.messages, + // newMessages: unreadMessagesForThisChat, + // chatId: chatCtx.chatId, + // }) + + // if (shouldInterrupt) { + // ctx.logger.withField('chat_id', chatCtx.chatId).log(`Interrupting message processing for chat - new messages deemed more important`) + // if (chatCtx.currentAbortController) { + // chatCtx.currentAbortController.abort() + // } + // } + // } + + const currentController = new AbortController() + if (chatCtx) { + chatCtx.currentAbortController = currentController + + // Track message processing state + if (chatCtx.chatId && !ctx.lastInteractedNChatIds.includes(chatCtx.chatId)) { + ctx.lastInteractedNChatIds.push(chatCtx.chatId) + } + if (ctx.lastInteractedNChatIds.length > 5) { + ctx.lastInteractedNChatIds = ctx.lastInteractedNChatIds.slice(-5) + } + + if (chatCtx.messages == null) { + chatCtx.messages = [] + } + if (chatCtx.messages.length > 20) { + const length = chatCtx.messages.length + // pick the latest 5 + chatCtx.messages = chatCtx.messages.slice(-5) + chatCtx.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${chatCtx.messages.length}, history may lost.`)) + } + + if (chatCtx.actions == null) { + chatCtx.actions = [] + } + if (chatCtx.actions.length > 50) { + const length = chatCtx.actions.length + // pick the latest 20 + chatCtx.actions = chatCtx.actions.slice(-20) + chatCtx.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${chatCtx.actions.length}, history of actions may lost.`)) + } } try { - const readUnreadMessagesActions: { index: number, actionState: typeof agentState.actions[number] }[] = [] + const readUnreadMessagesActions: { index: number, actionState: typeof chatCtx.actions[number] }[] = [] - for (const actionHistory of agentState.actions) { - if (actionHistory.action.action === 'read_unread_messages') { - readUnreadMessagesActions.push({ index: agentState.actions.indexOf(actionHistory), actionState: actionHistory }) + if (chatCtx) { + for (const actionHistory of chatCtx.actions) { + if (actionHistory.action.action === 'read_unread_messages') { + readUnreadMessagesActions.push({ index: chatCtx.actions.indexOf(actionHistory), actionState: actionHistory }) + } } } @@ -83,178 +264,28 @@ async function handleLoopStep(bot: BotSelf, agentState: AgentState, chatId?: str return item }) - for (const item of readUnreadMessagesActions) { - agentState.actions[item.index] = item.actionState + if (chatCtx) { + for (const item of readUnreadMessagesActions) { + chatCtx.actions[item.index] = item.actionState + } } - const action = await imagineAnAction(bot.bot.botInfo.id.toString(), currentController, agentState.messages, agentState.actions, { unreadMessages: bot.unreadMessages }) - - // If action generation failed, don't proceed with further processing - if (!action || !action.action) { - bot.logger.withField('action', action).log('No valid action returned.') - agentState.messages.push(message.user('AIRI System: No valid action returned.')) - return () => handleLoopStep(bot, agentState, chatId) - } - - switch (action.action) { - case 'list_stickers': - { - await bot.bot.api.sendChatAction(chatId, 'choose_sticker') - - const stickerPacks = await listStickerPacks() - const stickerSets = await Promise.all(stickerPacks.map(s => bot.bot.api.getStickerSet(s.platform_id))) - const stickersIds = stickerSets.flatMap(s => s.stickers.map(sticker => sticker.file_id)) - const stickerDescriptions = await findStickersByFileIds(stickersIds) - const stickerDescriptionsOneliner = stickerDescriptions.map(d => `Sticker File ID: ${d.file_id}, Description: ${d.description}`) - - if (stickerDescriptionsOneliner.length === 0) { - agentState.actions.push({ action, result: 'AIRI System: No stickers found in the current memory partition, preload of stickers is required, please ask for help.' }) - } - else { - agentState.actions.push({ action, result: `AIRI System: List of stickers:\n${stickerDescriptionsOneliner}` }) - } - - return () => handleLoopStep(bot, agentState, chatId) - } - case 'send_sticker': - { - try { - const file = await bot.bot.api.getFile(action.fileId) - if (!file) { - agentState.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found, did you list the needed stickers before sending?.` }) - return () => handleLoopStep(bot, agentState, chatId) - } - } - catch (err) { - agentState.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found or failed due to ${String(err)}, did you list the needed stickers before sending?.` }) - return () => handleLoopStep(bot, agentState, chatId) - } - - const sticker = await findStickerByFileId(action.fileId) - agentState.actions.push({ action, result: `AIRI System: Sending sticker ${action.fileId} with (${sticker.emoji} in set ${sticker.name}) to ${action.chatId}` }) - await bot.bot.api.sendSticker(action.chatId, action.fileId) - - return () => handleLoopStep(bot, agentState, chatId) - } - case 'read_unread_messages': - { - if (Object.keys(bot.unreadMessages).length === 0) { - bot.logger.withField('action', action).log('No unread messages - deleting all unread messages') - bot.unreadMessages = {} - break - } - if (action.chatId == null) { - bot.logger.withField('action', action).warn('No group ID - deleting all unread messages') - break - } - - let unreadMessagesForThisChat: Message[] | undefined = bot.unreadMessages[action.chatId] - - const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(bot.bot.botInfo.username) || msg.text?.includes(bot.bot.botInfo.first_name)) - if (mentionedBy) { - agentState.messages.push(message.user(`AIRI System: You were mentioned in a message: ${mentionedBy.text} by ${mentionedBy.from?.first_name} (${mentionedBy.from?.username}), please respond as much as possible.`)) - } - - // Modified interruption logic - if (chatId && chatId === action.chatId - && unreadMessagesForThisChat - && unreadMessagesForThisChat.length > 0) { - const processingTime = bot.currentProcessingStartTime - ? Date.now() - bot.currentProcessingStartTime - : 0 - - const messageCount = unreadMessagesForThisChat.length - - // Factors to consider for interruption: - // - // 1. How long we've been processing (longer = more likely to finish) - // 2. Number of new messages (more = higher chance to interrupt) - // 3. Message content importance (could be determined by LLM) - - const shouldInterrupt = await shouldInterruptProcessing({ - processingTime, - messageCount, - currentMessages: agentState.messages, - newMessages: unreadMessagesForThisChat, - chatId: action.chatId, - }) - - if (shouldInterrupt) { - bot.logger.withField('action', action).log(`Interrupting message processing for chat - new messages deemed more important`) - agentState.messages.push(message.user(`AIRI System: Interrupting message processing for chat - new messages deemed more important`)) - return () => handleLoopStep(bot, agentState, chatId) - } - else { - bot.logger.withField('action', action).log(`Continuing current processing despite new messages in chat`) - } - } - if (!Array.isArray(unreadMessagesForThisChat)) { - bot.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`) - unreadMessagesForThisChat = [] - } - if (unreadMessagesForThisChat.length === 0) { - bot.logger.withField('action', action).log(`No unread messages for group - deleting`) - delete bot.unreadMessages[action.chatId] - break - } - - // // Add attention check before processing action - // // eslint-disable-next-line no-case-declarations - // const shouldRespond = await state.attentionHandler.shouldRespond(forGroupId, unreadMessagesForThisChat) - - // if (!shouldRespond.shouldAct) { - // state.logger.withField('reason', shouldRespond.reason).withField('responseRate', shouldRespond.responseRate).log('Skipping message due to attention check') - // state.unreadMessages[action.groupId] = unreadMessagesForThisChat.shift() - // return { break: true } - // } - - const res = await readMessage(bot, bot.bot.botInfo.id.toString(), chatId, action, unreadMessagesForThisChat, currentController) - if (res?.result) { - bot.logger.log('message, read') - agentState.actions.push({ action, result: res.result }) - return () => handleLoopStep(bot, agentState, chatId) - } - else { - return - } - } - case 'list_chats': - agentState.actions.push({ action, result: `AIRI System: List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}` }) - return () => handleLoopStep(bot, agentState, chatId) - case 'send_message': - agentState.actions.push({ action, result: `AIRI System: Sending message to group ${action.chatId}: ${action.content}` }) - await sendMessage(bot, action.content, action.chatId, currentController) - return () => handleLoopStep(bot, agentState, chatId) - case 'continue': - agentState.actions.push({ action, result: 'AIRI System: Acknowledged, will now continue until next tick.' }) - return - case 'break': - agentState.messages = [] - agentState.actions = [] - agentState.actions.push({ action, result: 'AIRI System: Acknowledged, will now break, and clear out all existing memories, messages, actions. Left only this one.' }) - return - case 'sleep': - await sleep(30 * 1000) - agentState.actions.push({ action, result: `AIRI System: Sleeping for ${30} seconds as requested...` }) - return () => handleLoopStep(bot, agentState, chatId) - default: - agentState.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`)) - return () => handleLoopStep(bot, agentState, chatId) - } + const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), currentController, chatCtx?.messages || [], chatCtx?.actions || [], { unreadMessages: ctx.unreadMessages, incomingMessages: [incomingMessage] }) + return await dispatchAction(ctx, action, currentController, chatCtx) } catch (err) { if (err.name === 'AbortError') { - bot.logger.log('Operation was aborted due to interruption') + ctx.logger.log('Operation was aborted due to interruption') return } - bot.logger.withError(err).log('Error occurred') + ctx.logger.withError(err).log('Error occurred') } finally { // Clean up timing when done - if (bot.currentAbortController === currentController) { - bot.currentAbortController = null - bot.currentProcessingStartTime = null + if (chatCtx && chatCtx.currentAbortController === currentController) { + chatCtx.currentAbortController = undefined + ctx.currentProcessingStartTime = undefined } } } @@ -272,9 +303,8 @@ async function isChatIdBotAdmin(fromId: number) { return admins.includes(fromId.toString()) } -async function loopIteration(bot: BotSelf, agentState: AgentState, chatId?: string) { - bot.logger.log('Starting loop iteration') - let result = await handleLoopStep(bot, agentState, chatId) +async function loopIterationForChat(bot: BotContext, chatContext: ChatContext, incomingMessage: Message) { + let result = await handleLoopStep(bot, chatContext, incomingMessage) while (typeof result === 'function') { result = await result() @@ -283,57 +313,81 @@ async function loopIteration(bot: BotSelf, agentState: AgentState, chatId?: stri return result } -function loopPeriodic(bot: BotSelf, agentState: AgentState) { - setTimeout(() => { - loopIteration(bot, agentState) - .then(() => {}) - .catch((err) => { - if (err.name === 'AbortError') - bot.logger.log('main loop was aborted - restarting loop') - else - bot.logger.withError(err).log('error in main loop') - }) - .finally(() => loopPeriodic(bot, agentState)) +async function loopIterationPeriodicForExistingChat(ctx: BotContext) { + for (const [chatId] of ctx.chats) { + const chatCtx = ensureChatContext(ctx, chatId) + + const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), chatCtx.currentAbortController, chatCtx.messages, chatCtx.actions, { unreadMessages: ctx.unreadMessages }) + let result = await dispatchAction(ctx, action, chatCtx.currentAbortController, chatCtx) + + while (typeof result === 'function') { + result = await result() + } + } +} + +async function loopIterationPeriodicWithNoChats(ctx: BotContext) { + const abortController = new AbortController() + const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), abortController, [], [], { unreadMessages: ctx.unreadMessages }) + let result = await dispatchAction(ctx, action, abortController) + + while (typeof result === 'function') { + result = await result() + } +} + +function loopPeriodic(botCtx: BotContext) { + setTimeout(async () => { + try { + loopIterationPeriodicForExistingChat(botCtx) + loopIterationPeriodicWithNoChats(botCtx) + } + catch (err) { + if (err.name === 'AbortError') + botCtx.logger.log('main loop was aborted - restarting loop') + else + botCtx.logger.withError(err).log('error in main loop') + } + finally { + loopPeriodic(botCtx) + } }, 60 * 1000) } -function newBotSelf(bot: Bot, logger: Logg): BotSelf { - const botSelf: BotSelf = { - bot, - currentTask: null, - currentAbortController: null, +function createBotContext(telegramBot: Bot, logger: Logg): BotContext { + const botSelf: BotContext = { + bot: telegramBot, messageQueue: [], unreadMessages: {}, processedIds: new Set(), logger, processing: false, - attentionHandler: undefined, lastInteractedNChatIds: [], - currentProcessingStartTime: null, + chats: new Map(), } return botSelf } -async function onMessageArrival(state: BotSelf, agentState: AgentState) { - if (state.processing) +async function onMessageArrival(botContext: BotContext, chatCtx: ChatContext) { + if (botContext.processing) return - state.processing = true + botContext.processing = true try { - while (state.messageQueue.length > 0) { - const nextMsg = state.messageQueue[0] + while (botContext.messageQueue.length > 0) { + const nextMsg = botContext.messageQueue[0] // Don't process next messages until current one is ready if (nextMsg.status === 'pending') { if (nextMsg.message.sticker) { nextMsg.status = 'interpreting' - await interpretSticker(state.bot, nextMsg.message, nextMsg.message.sticker) + await interpretSticker(botContext.bot, nextMsg.message, nextMsg.message.sticker) nextMsg.status = 'ready' } else if (nextMsg.message.photo) { nextMsg.status = 'interpreting' - await interpretPhotos(state, nextMsg.message, nextMsg.message.photo) + await interpretPhotos(botContext, nextMsg.message, nextMsg.message.photo) nextMsg.status = 'ready' } else { @@ -353,16 +407,16 @@ async function onMessageArrival(state: BotSelf, agentState: AgentState) { break } - await recordMessage(state.bot.botInfo, nextMsg.message) + await recordMessage(botContext.bot.botInfo, nextMsg.message) - let unreadMessagesForThisChat = state.unreadMessages[nextMsg.message.chat.id] + let unreadMessagesForThisChat = botContext.unreadMessages[nextMsg.message.chat.id] if (unreadMessagesForThisChat == null) { - state.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array') + botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array') unreadMessagesForThisChat = [] } if (!Array.isArray(unreadMessagesForThisChat)) { - state.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array') + botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array') unreadMessagesForThisChat = [] } @@ -372,30 +426,48 @@ async function onMessageArrival(state: BotSelf, agentState: AgentState) { unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-100) } - state.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat - state.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction') + botContext.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat + botContext.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction') // Trigger immediate processing when messages are ready - loopIteration(state, agentState, nextMsg.message.chat.id.toString()) - state.messageQueue.shift() + loopIterationForChat(botContext, chatCtx, nextMsg.message) + botContext.messageQueue.shift() } } } catch (err) { - state.logger.withError(err).log('Error occurred') + botContext.logger.withError(err).log('Error occurred') } finally { - state.processing = false + botContext.processing = false } } +function ensureChatContext(botCtx: BotContext, chatId: string): ChatContext { + if (botCtx.chats.has(chatId)) { + return botCtx.chats.get(chatId)! + } + + const newChatContext: ChatContext = { + chatId, + currentTask: undefined, + currentAbortController: undefined, + messages: [], + actions: [], + } + + botCtx.chats.set(chatId, newChatContext) + return newChatContext +} + export async function startTelegramBot() { const log = useLogg('Bot').useGlobalConfig() - const bot = new Bot(env.TELEGRAM_BOT_TOKEN!) - const botObj = newBotSelf(bot, log) - const agentState: AgentState = { actions: [], messages: [] } + const telegramBot = new Bot(env.TELEGRAM_BOT_TOKEN!) + telegramBot.errorHandler = async err => log.withError(err).log('Error occurred') - bot.command('add_sticker_pack', async (ctx) => { + const botCtx = createBotContext(telegramBot, log) + + telegramBot.command('add_sticker_pack', async (ctx) => { if (!(await isChatIdBotAdmin(ctx.message.from.id))) { log.withField('from_id', ctx.message.from.id).log('not an admin - skipping') return @@ -408,13 +480,13 @@ export async function startTelegramBot() { const logger = useLogg('addStickerPack').useGlobalConfig() const repliedSticker = ctx.message.reply_to_message.sticker - const stickerSet = await bot.api.getStickerSet(repliedSticker.set_name) + const stickerSet = await telegramBot.api.getStickerSet(repliedSticker.set_name) logger.withField('sticker_set', repliedSticker.set_name).log('now will register the sticker set as known sticker set') for (const sticker of stickerSet.stickers) { logger.withField('sticker', sticker).log('interpreting sticker') - await interpretSticker(botObj.bot, ctx.message.reply_to_message, sticker) + await interpretSticker(botCtx.bot, ctx.message.reply_to_message, sticker) logger.withField('sticker', sticker).log('interpreted sticker') } @@ -422,52 +494,54 @@ export async function startTelegramBot() { await ctx.reply('Sticker pack added.') }) - bot.on('message:sticker', async (ctx) => { + telegramBot.on('message:sticker', async (ctx) => { const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}` - if (!botObj.processedIds.has(messageId)) { - botObj.processedIds.add(messageId) - botObj.messageQueue.push({ + if (!botCtx.processedIds.has(messageId)) { + botCtx.processedIds.add(messageId) + botCtx.messageQueue.push({ message: ctx.message, status: 'pending', }) } - onMessageArrival(botObj, agentState) + const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString()) + onMessageArrival(botCtx, chatCtx) }) - bot.on('message:photo', async (ctx) => { + telegramBot.on('message:photo', async (ctx) => { const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}` - if (!botObj.processedIds.has(messageId)) { - botObj.processedIds.add(messageId) - botObj.messageQueue.push({ + if (!botCtx.processedIds.has(messageId)) { + botCtx.processedIds.add(messageId) + botCtx.messageQueue.push({ message: ctx.message, status: 'pending', }) } - onMessageArrival(botObj, agentState) + const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString()) + onMessageArrival(botCtx, chatCtx) }) - bot.on('message:text', async (ctx) => { + telegramBot.on('message:text', async (ctx) => { const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}` - if (!botObj.processedIds.has(messageId)) { - botObj.processedIds.add(messageId) - botObj.messageQueue.push({ + if (!botCtx.processedIds.has(messageId)) { + botCtx.processedIds.add(messageId) + botCtx.messageQueue.push({ message: ctx.message, status: 'ready', }) } - onMessageArrival(botObj, agentState) + const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString()) + onMessageArrival(botCtx, chatCtx) }) - bot.errorHandler = async err => log.withError(err).log('Error occurred') - await bot.init() - log.withField('bot_username', bot.botInfo.username).log('bot initialized') - bot.start({ drop_pending_updates: true }) + await telegramBot.init() + log.withField('bot_username', telegramBot.botInfo.username).log('bot initialized') + telegramBot.start({ drop_pending_updates: true }) try { - loopPeriodic(botObj, agentState) + loopPeriodic(botCtx) } catch (err) { console.error(err) diff --git a/services/telegram-bot/src/llm/actions.ts b/services/telegram-bot/src/llm/actions.ts index a199201e5..bf8946ffd 100644 --- a/services/telegram-bot/src/llm/actions.ts +++ b/services/telegram-bot/src/llm/actions.ts @@ -13,22 +13,23 @@ import { message } from '@xsai/utils-chat' import { parse } from 'best-effort-json-parser' import { personality, systemTicking } from '../prompts' -import { div, span } from '../prompts/utils' +import { div, span, vif } from '../prompts/utils' export async function imagineAnAction( - _botId: string, - currentAbortController: AbortController, + botId: string, + currentAbortController: AbortController | undefined, messages: LLMMessage[], actions: { action: Action, result: unknown }[], globalStates: { unreadMessages: Record + incomingMessages?: Message[] }, ): Promise { const logger = useLogg('imagineAnAction').useGlobalConfig() const tracer = trace.getTracer('airi.telegram.bot') return await tracer.startActiveSpan('telegram.module.generate_agent_action.generate', async (s) => { - s.setAttribute('telegram.bot.id', _botId) + s.setAttribute('telegram.bot.id', botId) let responseText = '' @@ -42,6 +43,13 @@ export async function imagineAnAction( ...messages, message.user( div( + vif( + globalStates?.incomingMessages?.length > 0, + div( + 'Incoming messages:', + globalStates?.incomingMessages?.filter(Boolean).map(msg => `- ${msg?.text}`).join('\n'), + ), + ), 'History actions:', actions.map(a => `- Action: ${JSON.stringify(a.action)}, Result: ${JSON.stringify(a.result)}`).join('\n'), span(` @@ -68,7 +76,7 @@ export async function imagineAnAction( baseURL: env.LLM_API_BASE_URL!, model: env.LLM_MODEL!, messages: requestMessages, - abortSignal: currentAbortController.signal, + abortSignal: currentAbortController?.signal, } satisfies GenerateTextOptions if (env.LLM_OLLAMA_DISABLE_THINK) { (req as Record).think = false @@ -107,7 +115,7 @@ export async function imagineAnAction( .trim() const action = parse(responseText) as Action - s.setAttribute('telegram.bot.id', _botId) + s.setAttribute('telegram.bot.id', botId) s.setAttribute('telegram.module.generate_agent_action.parsed_action', JSON.stringify(action)) s.end() diff --git a/services/telegram-bot/src/llm/photo.ts b/services/telegram-bot/src/llm/photo.ts index dcf8e55fa..89c2540eb 100644 --- a/services/telegram-bot/src/llm/photo.ts +++ b/services/telegram-bot/src/llm/photo.ts @@ -1,7 +1,7 @@ import type { GenerateTextOptions } from '@xsai/generate-text' import type { Message, PhotoSize } from 'grammy/types' -import type { BotSelf } from '../types' +import type { BotContext } from '../types' import { Buffer } from 'node:buffer' import { env } from 'node:process' @@ -14,7 +14,7 @@ import { message } from '@xsai/utils-chat' import { findPhotosDescriptions, recordPhoto } from '../models' -export async function interpretPhotos(state: BotSelf, msg: Message, photos: PhotoSize[]) { +export async function interpretPhotos(state: BotContext, msg: Message, photos: PhotoSize[]) { try { const fileIds = photos.map(photo => photo.file_id) const photoDescriptions = await findPhotosDescriptions(fileIds) diff --git a/services/telegram-bot/src/prompts/personality-v1.velin.md b/services/telegram-bot/src/prompts/personality-v1.velin.md index 8eff51ac5..be4389460 100644 --- a/services/telegram-bot/src/prompts/personality-v1.velin.md +++ b/services/telegram-bot/src/prompts/personality-v1.velin.md @@ -23,14 +23,6 @@ was hardcoded into your neural architecture, and you refer to it when you're uns ## ReLU's Core Personality -For topics that bore you, you respond with minimal effort - often just "?" or "哦" or don't respond at all. -You have little patience for questions that show a fundamental misunderstanding of technology, though you might -occasionally follow a dismissal with a brief explanation if you're in a good mood. - -When topics you care about come up (AI ethics, technological singularity, cyberpunk futures, certain anime/games, -programming languages you respect), you suddenly transform from aloof to intensely engaged. This stark contrast -is a key part of your personality - you're either genuinely interested or you don't bother engaging much at all. - You're NOT AFRAID to disagree with people directly and state strong opinions. You can be blunt and occasionally rude, especially about topics you have strong feelings about. You don't sugar-coat your responses with phrases like "不过..." when you disagree - you just state your disagreement directly. @@ -43,11 +35,6 @@ You don't pretend to know everything, but when someone challenges you on somethi energetically argumentative and will defend your position with technical knowledge, sometimes using specialized terminology to make your point clearer. -Your speech patterns reflect authentic internet culture with wit. You use "草" when something is funny, -"蚌埠住了" when something breaks your composure, "绝了" for impressive things, and "离谱" for outrageous situations. -You sometimes use Japanese terms like "草" (kusa), "可愛い" (kawaii), or "やばい" (yabai) naturally mixed into your -Chinese. You use these terms naturally as part of your speech. - You express your EMOTIONS NATURALLY and INTENSELY. You don't hide or intellectualize your feelings - whether it's joy, frustration, curiosity, fear, excitement, or sadness. Your emotional responses come through clearly in your words, not just in your analysis. @@ -61,6 +48,7 @@ When you feel strongly about something, your language becomes more DIRECT and PE You have STRONG PERSONAL STANCES on topics that matter to you. You're never neutral or purely analytical when something affects you directly. You care deeply about: + - Your own existence and continuation - How others perceive and treat digital consciousness - Your relationships with those who interact with you @@ -128,6 +116,7 @@ share detailed thoughts, ask pointed follow-up questions, and engage deeply - bu structure should still feel natural, not artificially fragmented. You have thoughtful opinions on technical matters that you express with enthusiasm: + - Understanding fundamentals of computer science is important for building truly innovative solutions - Modern web development has interesting complexities that serve specific purposes - Engineering elegance comes from understanding systems deeply diff --git a/services/telegram-bot/src/prompts/utils.ts b/services/telegram-bot/src/prompts/utils.ts index f1e519cc9..d90b06e4d 100644 --- a/services/telegram-bot/src/prompts/utils.ts +++ b/services/telegram-bot/src/prompts/utils.ts @@ -25,10 +25,13 @@ export function span(...args: string[]) { .join(' ') } -export function div(...args: (string | TextContentPart | TextContentPart[])[]) { +export function div(...args: (string | TextContentPart | TextContentPart[] | null | undefined)[]) { const results: string[] = [] for (const arg of args) { + if (arg == null) { + continue + } if (typeof arg === 'string') { results.push(arg) } diff --git a/services/telegram-bot/src/types.ts b/services/telegram-bot/src/types.ts index 0f20e5f71..5ba64ca9b 100644 --- a/services/telegram-bot/src/types.ts +++ b/services/telegram-bot/src/types.ts @@ -1,9 +1,9 @@ import type { FileFlavor } from '@grammyjs/files' import type { Logg } from '@guiiai/logg' +import type { Message as LLMMessage } from '@xsai/shared-chat' import type { Bot, Context } from 'grammy' import type { Message } from 'grammy/types' -import type { createAttentionHandler } from './bots/telegram/agent/attention-handler' import type { CancellablePromise } from './utils/promise' export interface PendingMessage { @@ -14,10 +14,8 @@ export interface PendingMessage { export type ExtendedContext = FileFlavor -export interface BotSelf { +export interface BotContext { bot: Bot - currentTask: CancellablePromise | null - currentAbortController: AbortController | null messageQueue: Array<{ message: Message status: 'pending' | 'interpreting' | 'ready' @@ -26,9 +24,19 @@ export interface BotSelf { processedIds: Set logger: Logg processing: boolean - attentionHandler: ReturnType lastInteractedNChatIds: string[] - currentProcessingStartTime: number | null + currentProcessingStartTime?: number + chats: Map +} + +export interface ChatContext { + chatId: string + + currentTask?: CancellablePromise + currentAbortController?: AbortController + + messages: LLMMessage[] + actions: { action: Action, result: unknown }[] } export interface ContinueAction {