Skip to content

Commit 89ba9b3

Browse files
Add channel connector (#53)
* feat: add channel connector planning * feat: add channel connector package * feat: add channel command in cli * fix: filter user message
1 parent 2ba5c25 commit 89ba9b3

22 files changed

Lines changed: 2113 additions & 0 deletions
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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

Comments
 (0)