Skip to main content

ChannelMagiBridge

ChannelMagiBridge is the bridge between the Channel system and the Agent processing layer, responsible for converting Channel messages into MagiIncomingMessage format, submitting them to MagiService for processing, and routing responses back to the original Channel.

Source location: packages/desktop/app/main/services/agent-core/magi/ChannelMagiBridge.ts

Data Flow

sequenceDiagram
participant Router as ChannelMessageRouter
participant Registry as ChannelPluginRegistry
participant Bridge as ChannelMagiBridge
participant Magi as MagiService
participant Plugin as Channel Plugin

Registry->>Bridge: emit('routeToAgent', payload)
Note over Bridge: Convert ChannelMessage → MagiIncomingMessage

Bridge->>Magi: handleMessage(incoming)

alt Agent generates intermediate message
Magi->>Bridge: onIntermediateMessage(text)
Bridge->>Router: sendResponse(channelId, chatId, text, type)
Router->>Plugin: sendMessage(chatId, chunk)
end

Magi-->>Bridge: { response, mode }

alt Final reply exists and no intermediate message was sent
Bridge->>Router: sendResponse(channelId, chatId, response, type)
Router->>Plugin: sendMessage(chatId, chunk)
end

Note over Bridge: Skip final reply if intermediate message<br/>was already sent (avoid duplication)

Lifecycle

class ChannelMagiBridge {
constructor(
registry: ChannelPluginRegistry,
channelRouter: ChannelMessageRouter,
magiService: MagiService,
logger: LoggerService,
)

/** Start listening to 'routeToAgent' events */
start(): void

/** Stop listening and cleanup */
stop(): void

/** Whether it is currently active */
isActive(): boolean
}

start() should be called after MagiService.start() to ensure the Agent service is ready.

Channel Source Mapping

Bridge maps Channel plugin types to MagiMessageSource:

Channel Plugin TypeMagiMessageSource
discorddiscord
telegramtelegram
slackslack
qqbotqqbot
whatsappwhatsapp
emailemail
wechatwechat
signalsignal
lineline
Other/Unmappedapi (fallback)

MagiMessageSource affects Agent behavior and context; different sources may trigger different prompt strategies.

RouteToAgentPayload

Event payload emitted by ChannelMessageRouter via registry.emit('routeToAgent', payload):

interface RouteToAgentPayload {
/** Formatted prompt (original text for DMs, XML for groups) */
prompt: string;
/** Original message that triggered the reply */
sourceMessage: ChannelMessage;
/** All relevant messages (buffer + trigger message) */
allMessages: ChannelMessage[];
/** Channel-specific system prompt returned by plugin */
channelSystemPrompt?: string;
/** Whether it is a one-on-one conversation (DM) */
isDirectConversation: boolean;
/** User permission information */
channelUserPermissions?: {
canUseTool: boolean;
requireConfirmation: boolean;
};
}

MagiIncomingMessage Construction

Bridge converts RouteToAgentPayload to MagiIncomingMessage:

const incoming: MagiIncomingMessage = {
content: prompt, // Groups: XML format; DMs: original text
source: CHANNEL_SOURCE_MAP[channelType], // Message source mapping
channelId: sourceMessage.channelId, // Channel instance ID
chatId: sourceMessage.chatId, // Chat/channel ID
userId: sourceMessage.senderId, // Platform user ID
messageId: sourceMessage.id, // Platform message ID
timestamp: Date.now(), // Processing timestamp
channelSystemPrompt, // Plugin-specific system prompt
isDirectConversation, // Whether one-on-one
attachments, // Attachment list
channelUserPermissions, // User permissions
onIntermediateMessage, // Intermediate message callback
};

Attachment Forwarding

Bridge collects all attachments from allMessages and forwards them to the Agent:

const attachments: MagiAttachment[] = [];
for (const msg of payload.allMessages) {
if (msg.attachments) {
for (const att of msg.attachments) {
if (!att.url && !att.localPath) continue; // Skip attachments without source
attachments.push({
type: att.type,
url: att.url,
localPath: att.localPath,
name: att.name || `attachment_${attachments.length + 1}`,
size: att.size,
});
}
}
}

Attachments come from all messages in the buffer (not just the trigger message), ensuring the Agent can see images and files shared in group conversations.

Intermediate Message Handling

When the Agent produces intermediate output during processing (such as step-by-step reasoning or tool call results), Bridge forwards them to the Channel in real-time via the onIntermediateMessage callback:

onIntermediateMessage: async (text: string) => {
intermediatesSent = true;
await this.channelRouter.sendResponse(
sourceMessage.channelId,
sourceMessage.chatId,
text,
sourceMessage.channelType,
);
}

Deduplication logic: If a reply has already been sent via intermediate messages (intermediatesSent === true), Bridge skips sending the final result.response to avoid duplication.

Error Handling

When message processing fails, Bridge sends error feedback to the original Channel:

const errorText = `[Elftia Error] ${error.message}`;
await this.channelRouter.sendResponse(
sourceMessage.channelId,
sourceMessage.chatId,
errorText,
sourceMessage.channelType,
);

If sending the error feedback itself fails, only log it and do not retry.

Next Steps