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 Type | MagiMessageSource |
|---|---|
discord | discord |
telegram | telegram |
slack | slack |
qqbot | qqbot |
whatsapp | whatsapp |
email | email |
wechat | wechat |
signal | signal |
line | line |
| Other/Unmapped | api (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
- Message Routing and Security Pipeline — Processing flow before messages reach Bridge
- Writing a Channel Plugin — Write a custom Channel plugin from scratch