AionUi Multi-Agent Communication and Channels: Architecture Deep Dive
AionUi implements a layered channel subsystem that connects AI agents to instant messaging platforms through platform-agnostic plugins, unified message formats, and secure pairing workflows.
AionUi is an open-source AI desktop application that bridges large language models with external communication platforms. The AionUi multi-agent communication and channels architecture enables seamless interaction between AI workers (Gemini, Claude, Codex) and instant messaging clients like Telegram, Lark, and DingTalk through a sophisticated gateway pattern that isolates platform-specific details while maintaining unified session management.
Architecture Overview
The channel subsystem follows a layered architecture that separates concerns across six distinct layers. Each layer communicates through well-defined interfaces, allowing the system to support new messaging platforms without modifying core AI logic.
The Gateway layer handles plugin registration and message routing through PluginManager and ActionExecutor. The Core layer orchestrates lifecycle management via singletons like ChannelManager, SessionManager, and PairingService. The Agent layer manages AI communication through ChannelMessageService and ChannelEventBus. Platform-specific implementations reside in the Plugins and Adapters layers, while the Actions layer defines concrete commands triggered by UI buttons or slash-commands.
Core Components
ChannelManager Singleton
ChannelManager serves as the entry point for the entire subsystem. Located at src/channels/core/ChannelManager.ts, this singleton creates and holds four critical sub-components:
private pluginManager: PluginManager | null = null;
private sessionManager: SessionManager | null = null;
private pairingService: PairingService | null = null;
private actionExecutor: ActionExecutor | null = null;
During initialize(), the manager instantiates PairingService, SessionManager, and PluginManager, then creates an ActionExecutor and wires the message handler and tool-confirmation handler. The method loads all enabled plugins from the database and starts them. When the application shuts down, shutdown() stops every plugin, clears the pairing interval, and disposes of the AI service.
PluginManager and BasePlugin Lifecycle
PluginManager maintains a registry of platform plugins in src/channels/gateway/PluginManager.ts. Each plugin extends BasePlugin from src/channels/plugins/BasePlugin.ts, which defines a state-machine lifecycle: created → initializing → ready → starting → running → stopping → stopped.
The manager forwards incoming unified messages to ActionExecutor and routes tool-confirmation callbacks to ChannelMessageService. This abstraction allows the core system to treat Telegram, Lark, and DingTalk as interchangeable communication endpoints.
ActionExecutor Routing Logic
Located in src/channels/gateway/ActionExecutor.ts, this component receives IUnifiedIncomingMessage objects from plugins. It performs several critical functions:
- Authorization Check: Verifies pairing status via
PairingService - Session Resolution: Creates or retrieves sessions through
SessionManagerusing the composite keyuserId:chatId - Placeholder Messaging: Sends "Thinking..." indicators to the user
- AI Delegation: Routes the request to
ChannelMessageServicefor agent processing - Throttling: Implements a 500ms throttle timer to prevent flooding platforms with streaming updates
SessionManager Isolation
SessionManager in src/channels/core/SessionManager.ts ensures conversation isolation across different chats. It generates composite keys using buildKey(userId, chatId), guaranteeing that a user can maintain independent conversations in different group chats. Sessions are cached in-memory and persisted to the assistant_sessions SQLite table.
PairingService Security
The PairingService in src/channels/pairing/PairingService.ts implements a secure approval workflow. When a new user messages the bot, the service generates a 6-digit pairing code, stores it in assistant_pairing_codes, and displays it on the desktop UI via the IPC bridge. The user must approve the request in Settings → Channels. Approved users are recorded in assistant_users and can subsequently send messages without repeating the pairing process.
ChannelMessageService and Event Broadcasting
ChannelMessageService in src/channels/agent/ChannelMessageService.ts encapsulates calls to AI workers (Gemini, ACP, Codex). It emits events on the global ChannelEventBus from src/channels/agent/ChannelEventBus.ts, ensuring that both the desktop UI (via IPC) and the IM plugins receive identical AI output streams. This dual broadcast mechanism keeps the external channel and internal UI perfectly synchronized.
Platform Adapters and Plugins
Each messaging platform implements a plugin and adapter pair:
- Telegram:
TelegramPlugin.tshandles long-polling, message splitting, and inline keyboards.TelegramAdapter.tsconverts Telegram updates toIUnifiedIncomingMessageformat. - Lark:
LarkPlugin.tsmanages WebSocket connections, interactive cards, and event de-duplication.LarkAdapter.tshandles native SDK object conversion. - DingTalk:
DingTalkPlugin.tsimplements the Stream API, AI Card handling, and token management.
All plugins expose uniform sendMessage and editMessage APIs operating on the unified format, allowing the core system to remain platform-agnostic.
Code Examples
Initializing the Channel Subsystem
import { getChannelManager } from '@/channels';
// Called from the main process on app start
async function startChannels() {
const manager = getChannelManager();
await manager.initialize(); // loads plugins, pairing, sessions
console.log('Channel subsystem ready');
}
The initialize() method in src/channels/core/ChannelManager.ts instantiates all core services and starts enabled plugins.
Sending a Unified Message
import { getChannelManager } from '@/channels';
import { IUnifiedOutgoingMessage } from '@/channels/types';
async function sendHello(chatId: string) {
const manager = getChannelManager();
const pluginMgr = manager.getPluginManager();
if (!pluginMgr) return;
const msg: IUnifiedOutgoingMessage = {
type: 'text',
text: 'Hello from AionUi!',
};
// TelegramPlugin implements sendMessage(chatId, msg)
await pluginMgr.sendMessage('telegram', chatId, msg);
}
The plugin internally uses its adapter to convert the unified message into native Telegram API calls.
Handling Tool Confirmations
When an AI tool requires user approval, the confirmation flow works as follows:
// Inside a plugin's confirm handler (registered by ChannelManager)
async function onConfirm(
userId: string,
platform: string,
callId: string,
value: string
) {
const manager = getChannelManager();
// Route to the confirm handler defined in initialize()
await manager?.pluginManager?.confirmHandler?.(userId, platform, callId, value);
}
This handler:
- Looks up the user in
assistant_usersviadb.getChannelUserByPlatform - Retrieves the active session to obtain
conversationId - Calls
ChannelMessageService.confirm(conversationId, callId, value)to resume the AI task
Synchronizing Channel Settings
When users change AI models or agents in the UI:
await getChannelManager().syncChannelSettings('telegram', {
backend: 'gemini',
customAgentId: undefined,
name: 'Gemini 1.5',
}, { id: 'gemini-1.5', useModel: 'gemini-pro' });
The syncChannelSettings method updates stored conversation models and clears all active sessions, ensuring the next message uses the new configuration.
Summary
- AionUi multi-agent communication and channels rely on a layered architecture separating platform adapters from core AI logic
ChannelManageracts as the singleton orchestrator, managingPluginManager,SessionManager,PairingService, andActionExecutor- Unified message formats (
IUnifiedIncomingMessage,IUnifiedOutgoingMessage) enable platform-agnostic communication across Telegram, Lark, and DingTalk - Session isolation uses composite keys (
userId:chatId) to maintain independent conversations per chat - Secure pairing requires 6-digit code approval before users can interact with AI agents
- Dual broadcast via
ChannelEventBussynchronizes AI responses between the desktop UI and external messaging platforms
Frequently Asked Questions
How does AionUi handle message routing between different IM platforms?
AionUi routes messages through the ActionExecutor class in src/channels/gateway/ActionExecutor.ts. When a plugin receives a message from Telegram, Lark, or DingTalk, it converts the native format to IUnifiedIncomingMessage using its adapter. The ActionExecutor then checks authorization, resolves the session via SessionManager, and delegates to ChannelMessageService for AI processing. Responses follow the reverse path, ensuring platform-specific details never leak into core AI logic.
What security measures protect the multi-agent communication channels?
The architecture implements a pairing workflow managed by PairingService in src/channels/pairing/PairingService.ts. When a new user contacts the bot, the system generates a cryptographically random 6-digit code stored in assistant_pairing_codes. The desktop UI displays this code via IPC, and the user must explicitly approve the request in Settings → Channels. Only after approval is the user record created in assistant_users, granting persistent access. This prevents unauthorized access to AI agents through compromised messaging accounts.
How does AionUi maintain conversation context across multiple chats?
SessionManager in src/channels/core/SessionManager.ts implements composite key isolation using the buildKey(userId, chatId) method. This creates a unique identifier combining the user ID and chat ID, ensuring that conversations in different group chats remain independent. Sessions are cached in-memory for performance and persisted to the assistant_sessions SQLite table. When ActionExecutor processes a message, it retrieves or creates the specific session for that chat context, maintaining isolated conversation histories even when a single user participates in multiple channels simultaneously.
What happens when a user changes the AI model in the channel settings?
When users modify the default model or agent through the UI, the system calls ChannelManager.syncChannelSettings() in src/channels/core/ChannelManager.ts. This method updates the backend configuration for the specified channel (e.g., switching from Claude to Gemini) and clears all active sessions for that platform. Clearing sessions ensures that the next incoming message creates a fresh conversation context using the new model parameters, preventing context contamination between different AI backends. The change takes effect immediately for new messages while preserving historical conversation data in the database.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →