|
| 1 | +--- |
| 2 | +phase: design |
| 3 | +title: "Channel Connector: System Design" |
| 4 | +description: Technical architecture for the channel-connector package as a generic messaging bridge |
| 5 | +--- |
| 6 | + |
| 7 | +# System Design: Channel Connector |
| 8 | + |
| 9 | +## Architecture Overview |
| 10 | + |
| 11 | +The key architectural principle: **channel-connector is a pure message pipe**. It knows nothing about agents. CLI is the orchestrator that connects channel-connector with agent-manager. |
| 12 | + |
| 13 | +```mermaid |
| 14 | +graph TD |
| 15 | + subgraph Telegram |
| 16 | + TG_USER[Developer on Phone] |
| 17 | + TG_API[Telegram Bot API] |
| 18 | + end |
| 19 | +
|
| 20 | + subgraph "@ai-devkit/channel-connector" |
| 21 | + CM[ChannelManager] |
| 22 | + TA[TelegramAdapter] |
| 23 | + CA[ChannelAdapter Interface] |
| 24 | + CS[ConfigStore] |
| 25 | + end |
| 26 | +
|
| 27 | + subgraph "@ai-devkit/cli" |
| 28 | + CMD[channel commands] |
| 29 | + INPUT_HANDLER[Input Handler] |
| 30 | + OUTPUT_LOOP[Output Polling Loop] |
| 31 | + end |
| 32 | +
|
| 33 | + subgraph "@ai-devkit/agent-manager" |
| 34 | + AM[AgentManager] |
| 35 | + TW[TtyWriter] |
| 36 | + GC[getConversation] |
| 37 | + end |
| 38 | +
|
| 39 | + subgraph Running Agent |
| 40 | + A1[Claude Code] |
| 41 | + SF[Session JSONL File] |
| 42 | + end |
| 43 | +
|
| 44 | + TG_USER -->|send message| TG_API |
| 45 | + TG_API -->|long polling| TA |
| 46 | + TA -->|incoming msg| INPUT_HANDLER |
| 47 | + INPUT_HANDLER -->|fire-and-forget| TW |
| 48 | + TW -->|keyboard input| A1 |
| 49 | +
|
| 50 | + A1 -->|writes output| SF |
| 51 | + OUTPUT_LOOP -->|poll getConversation| GC |
| 52 | + GC -->|read| SF |
| 53 | + OUTPUT_LOOP -->|new assistant msgs| TA |
| 54 | + TA -->|sendMessage| TG_API |
| 55 | + TG_API -->|push| TG_USER |
| 56 | +
|
| 57 | + CMD -->|configure| CS |
| 58 | + CMD -->|create & start| CM |
| 59 | +``` |
| 60 | + |
| 61 | +### Key Separation of Concerns |
| 62 | + |
| 63 | +| Layer | Package | Responsibility | |
| 64 | +|-------|---------|---------------| |
| 65 | +| **Channel** | `channel-connector` | Connect to messaging platforms, receive/send text. No agent knowledge. | |
| 66 | +| **Orchestration** | `cli` | Wire channel-connector to agent-manager. Provide message handler. | |
| 67 | +| **Agent** | `agent-manager` | Detect agents, send input (TtyWriter), read conversation history. | |
| 68 | + |
| 69 | +### Key Components (channel-connector only) |
| 70 | + |
| 71 | +| Component | Responsibility | |
| 72 | +|-----------|---------------| |
| 73 | +| `ChannelManager` | Registers adapters, manages lifecycle, routes messages to/from handler callback | |
| 74 | +| `ChannelAdapter` | Interface for messaging platforms (Telegram, future: Slack, WhatsApp) | |
| 75 | +| `TelegramAdapter` | Telegram Bot API integration via long polling | |
| 76 | +| `ConfigStore` | Persists channel configurations (tokens, chat IDs, preferences) | |
| 77 | + |
| 78 | +### Technology Choices |
| 79 | +- **Telegram library**: `telegraf` (mature, TypeScript-native, active maintenance) |
| 80 | +- **Config storage**: JSON file at `~/.ai-devkit/channels.json` |
| 81 | +- **Process model**: Foreground process via `ai-devkit channel start` |
| 82 | + |
| 83 | +## Data Models |
| 84 | + |
| 85 | +### ChannelConfig |
| 86 | +```typescript |
| 87 | +interface ChannelConfig { |
| 88 | + channels: Record<string, ChannelEntry>; |
| 89 | +} |
| 90 | + |
| 91 | +interface ChannelEntry { |
| 92 | + type: 'telegram' | 'slack' | 'whatsapp'; |
| 93 | + enabled: boolean; |
| 94 | + createdAt: string; |
| 95 | + config: TelegramConfig; // extend with union for future channel types |
| 96 | +} |
| 97 | + |
| 98 | +interface TelegramConfig { |
| 99 | + botToken: string; |
| 100 | + botUsername: string; |
| 101 | + authorizedChatId?: number; // auto-set from first user to message the bot |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +### Message Types (generic, no agent concepts) |
| 106 | +```typescript |
| 107 | +interface IncomingMessage { |
| 108 | + channelType: string; |
| 109 | + chatId: string; |
| 110 | + userId: string; |
| 111 | + text: string; |
| 112 | + timestamp: Date; |
| 113 | + metadata?: Record<string, unknown>; |
| 114 | +} |
| 115 | + |
| 116 | +/** Handler function provided by the consumer (CLI). Fire-and-forget — returns void. */ |
| 117 | +type MessageHandler = (message: IncomingMessage) => Promise<void>; |
| 118 | +``` |
| 119 | + |
| 120 | +### Authorization Model |
| 121 | +- First user to message the bot is auto-authorized: their `chatId` is stored in config via `ConfigStore` |
| 122 | +- Subsequent messages from other chat IDs are rejected |
| 123 | +- CLI tracks the authorized `chatId` in-memory for the output polling loop (captured from first incoming message) |
| 124 | + |
| 125 | +## API Design |
| 126 | + |
| 127 | +### ChannelAdapter Interface |
| 128 | +```typescript |
| 129 | +interface ChannelAdapter { |
| 130 | + readonly type: string; |
| 131 | + |
| 132 | + /** Start listening for messages */ |
| 133 | + start(): Promise<void>; |
| 134 | + |
| 135 | + /** Stop listening */ |
| 136 | + stop(): Promise<void>; |
| 137 | + |
| 138 | + /** Send a message to a specific chat. Automatically chunks messages exceeding platform limits (e.g., 4096 chars for Telegram), splitting at newline boundaries. */ |
| 139 | + sendMessage(chatId: string, text: string): Promise<void>; |
| 140 | + |
| 141 | + /** Register handler for incoming text messages (fire-and-forget, responses sent via sendMessage) */ |
| 142 | + onMessage(handler: (msg: IncomingMessage) => Promise<void>): void; |
| 143 | + |
| 144 | + /** Check if adapter is connected and healthy */ |
| 145 | + isHealthy(): Promise<boolean>; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +### ChannelManager API |
| 150 | +```typescript |
| 151 | +class ChannelManager { |
| 152 | + registerAdapter(adapter: ChannelAdapter): void; |
| 153 | + getAdapter(type: string): ChannelAdapter | undefined; |
| 154 | + startAll(): Promise<void>; |
| 155 | + stopAll(): Promise<void>; |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +### ConfigStore API |
| 160 | +```typescript |
| 161 | +class ConfigStore { |
| 162 | + constructor(configPath?: string); // defaults to ~/.ai-devkit/channels.json |
| 163 | + |
| 164 | + getConfig(): Promise<ChannelConfig>; |
| 165 | + saveChannel(name: string, entry: ChannelEntry): Promise<void>; |
| 166 | + removeChannel(name: string): Promise<void>; |
| 167 | + getChannel(name: string): Promise<ChannelEntry | undefined>; |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +### CLI Integration Pattern |
| 172 | +```typescript |
| 173 | +// In CLI's `channel start --agent <name>` command |
| 174 | +import { ChannelManager, TelegramAdapter, ConfigStore } from '@ai-devkit/channel-connector'; |
| 175 | +import { AgentManager, ClaudeCodeAdapter, CodexAdapter, TtyWriter } from '@ai-devkit/agent-manager'; |
| 176 | + |
| 177 | +// 1. Set up agent-manager and resolve target agent by name |
| 178 | +const agentManager = new AgentManager(); |
| 179 | +agentManager.registerAdapter(new ClaudeCodeAdapter()); |
| 180 | +agentManager.registerAdapter(new CodexAdapter()); |
| 181 | +const agents = await agentManager.listAgents(); |
| 182 | +const agent = agents.find(a => a.name === targetAgentName); |
| 183 | + |
| 184 | +// 2. Set up channel-connector |
| 185 | +const config = await configStore.getChannel('telegram'); |
| 186 | +const telegram = new TelegramAdapter({ botToken: config.botToken }); |
| 187 | + |
| 188 | +// 3. Track chatId from first incoming message |
| 189 | +let activeChatId: string | null = null; |
| 190 | + |
| 191 | +// 4. Register message handler (fire-and-forget to agent, capture chatId) |
| 192 | +telegram.onMessage(async (msg) => { |
| 193 | + if (!activeChatId) { |
| 194 | + activeChatId = msg.chatId; // auto-authorize first user |
| 195 | + } |
| 196 | + if (msg.chatId !== activeChatId) return; // reject unauthorized |
| 197 | + await ttyWriter.write(agent.pid, msg.text); // send to agent, don't wait |
| 198 | +}); |
| 199 | + |
| 200 | +// 5. Start agent output observation loop (polls and pushes to Telegram) |
| 201 | +let lastMessageCount = 0; |
| 202 | +const pollInterval = setInterval(() => { |
| 203 | + if (!activeChatId) return; // no user connected yet |
| 204 | + const conversation = agentAdapter.getConversation(agent.sessionFilePath); |
| 205 | + const newMessages = conversation.slice(lastMessageCount) |
| 206 | + .filter(m => m.role === 'assistant'); |
| 207 | + for (const msg of newMessages) { |
| 208 | + telegram.sendMessage(activeChatId, msg.content); // auto-chunks at 4096 chars |
| 209 | + } |
| 210 | + lastMessageCount = conversation.length; |
| 211 | +}, 2000); |
| 212 | + |
| 213 | +// 5. Start channel |
| 214 | +const manager = new ChannelManager(); |
| 215 | +manager.registerAdapter(telegram); |
| 216 | +await manager.startAll(); |
| 217 | +``` |
| 218 | + |
| 219 | +### CLI Commands |
| 220 | +``` |
| 221 | +ai-devkit channel connect telegram # Interactive setup (bot token prompt) |
| 222 | +ai-devkit channel list # Show configured channels |
| 223 | +ai-devkit channel disconnect telegram # Remove channel config |
| 224 | +ai-devkit channel start --agent <agent-name> # Start bridge to specific agent |
| 225 | +ai-devkit channel stop # Stop running bridge |
| 226 | +ai-devkit channel status # Show bridge process status |
| 227 | +``` |
| 228 | + |
| 229 | +## Component Breakdown |
| 230 | + |
| 231 | +### Package: `@ai-devkit/channel-connector` |
| 232 | +``` |
| 233 | +packages/channel-connector/ |
| 234 | + src/ |
| 235 | + index.ts # Public exports |
| 236 | + ChannelManager.ts # Adapter registry and lifecycle |
| 237 | + ConfigStore.ts # Config persistence (~/.ai-devkit/) |
| 238 | + adapters/ |
| 239 | + ChannelAdapter.ts # Interface definition |
| 240 | + TelegramAdapter.ts # Telegram Bot API implementation |
| 241 | + types.ts # Shared type definitions |
| 242 | + __tests__/ |
| 243 | + ChannelManager.test.ts |
| 244 | + ConfigStore.test.ts |
| 245 | + adapters/ |
| 246 | + TelegramAdapter.test.ts |
| 247 | + package.json |
| 248 | + tsconfig.json |
| 249 | + tsconfig.build.json |
| 250 | + jest.config.ts |
| 251 | +``` |
| 252 | + |
| 253 | +### CLI Integration (in `@ai-devkit/cli`) |
| 254 | +``` |
| 255 | +packages/cli/src/commands/ |
| 256 | + channel.ts # channel connect/list/disconnect/start/stop/status |
| 257 | +``` |
| 258 | + |
| 259 | +## Design Decisions |
| 260 | + |
| 261 | +### 1. Channel-connector has zero agent knowledge |
| 262 | +**Decision**: No dependency on agent-manager. The package is a pure messaging bridge. |
| 263 | +**Why**: Clean separation of concerns. Channel-connector can be used independently of agents (e.g., for notifications, other integrations). CLI is the natural integration point since it already depends on both packages. |
| 264 | + |
| 265 | +### 2. Async fire-and-forget message handling |
| 266 | +**Decision**: Consumer registers a `MessageHandler` callback that returns `void`. Responses are sent separately via `sendMessage()`. |
| 267 | +**Why**: Agent responses are inherently async (can take seconds to minutes). Decoupling input and output avoids blocking. CLI runs a separate polling loop that observes agent conversation via `getConversation()` and pushes new assistant messages to the channel. This also naturally handles unsolicited agent output (errors, completions). |
| 268 | + |
| 269 | +### 3. One agent per session (v1) |
| 270 | +**Decision**: `channel start --agent <id>` binds one channel to one agent. |
| 271 | +**Why**: Simplest mental model. No agent-routing logic needed in channel-connector. Developer explicitly chooses which agent to bridge. Can evolve to multi-agent in CLI later without changing channel-connector. |
| 272 | + |
| 273 | +### 4. Adapter pattern (consistent with agent-manager) |
| 274 | +**Decision**: Use pluggable adapter interface for channel implementations. |
| 275 | +**Why**: Proven pattern in this codebase. Makes adding Slack/WhatsApp straightforward. |
| 276 | + |
| 277 | +### 5. Long polling for Telegram (not webhooks) |
| 278 | +**Decision**: Use Telegram's long polling via telegraf. |
| 279 | +**Why**: No need for a public server/URL. Works behind firewalls and NAT. Simpler for CLI tool users. |
| 280 | + |
| 281 | +### 6. Single config file at `~/.ai-devkit/` |
| 282 | +**Decision**: Store channel configs globally, not per-project. |
| 283 | +**Why**: A Telegram bot bridges to agents across projects. Global config avoids re-setup per project. |
| 284 | + |
| 285 | +## Non-Functional Requirements |
| 286 | + |
| 287 | +### Performance |
| 288 | +- Message delivery latency: < 3 seconds (Telegram → handler → Telegram) |
| 289 | +- Memory footprint: < 50MB for the bridge process |
| 290 | + |
| 291 | +### Reliability |
| 292 | +- Auto-reconnect on network failure with exponential backoff |
| 293 | +- Graceful shutdown on SIGINT/SIGTERM |
| 294 | +- Message queue for offline/reconnecting scenarios (in-memory, bounded) |
| 295 | + |
| 296 | +### Security |
| 297 | +- Bot token stored with file permissions 0600 |
| 298 | +- Only authorized chat IDs can interact with the bot |
| 299 | +- No sensitive data logged or exposed in error messages |
| 300 | +- Token validation on connect before persisting |
| 301 | + |
| 302 | +### Scalability |
| 303 | +- v1 targets single-user, single-machine use |
| 304 | +- Adapter interface supports future multi-channel, multi-user scenarios |
| 305 | +- Handler pattern allows CLI to evolve routing without channel-connector changes |
0 commit comments