feat: add channel adapter abstraction with Telegram and WebChat adapters
Implement Phase 3 channel adapters that decouple message sources from the agent via a uniform ChannelAdapter interface and ChannelRegistry. - Add ChannelAdapter/InboundMessage/OutboundMessage types - Add ChannelRegistry for adapter lifecycle and message routing - Add TelegramAdapter (grammy bot, auth middleware, confirmations, chunking) - Add WebChatAdapter (thin shim over GatewayServer) - Refactor daemon to use ChannelRegistry with per-channel-per-user agents - Add config.get/config.patch gateway handlers (Phase 2 loose end) - Add system.restart gateway handler (Phase 2 loose end) - Add implementation plans and design docs Tests: 225 passing (33 new channel adapter + gateway handler tests)
This commit is contained in:
+111
-36
@@ -1,13 +1,13 @@
|
||||
import { Bot } from 'grammy';
|
||||
import { Lifecycle } from './lifecycle.js';
|
||||
import type { Config } from '../config/index.js';
|
||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter } from '../models/index.js';
|
||||
import { NativeAgent } from '../backends/index.js';
|
||||
import { createTelegramBot } from '../frontends/telegram/index.js';
|
||||
import { SessionStore, SessionManager } from '../session/index.js';
|
||||
import { HookEngine } from '../hooks/index.js';
|
||||
import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js';
|
||||
import { GatewayServer } from '../gateway/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter } from '../channels/index.js';
|
||||
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
|
||||
import { resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { mkdirSync, readFileSync, existsSync } from 'fs';
|
||||
@@ -15,8 +15,6 @@ import { mkdirSync, readFileSync, existsSync } from 'fs';
|
||||
export interface DaemonContext {
|
||||
config: Config;
|
||||
lifecycle: Lifecycle;
|
||||
bot: Bot;
|
||||
agent: NativeAgent;
|
||||
sessionStore: SessionStore;
|
||||
sessionManager: SessionManager;
|
||||
hookEngine: HookEngine;
|
||||
@@ -24,6 +22,7 @@ export interface DaemonContext {
|
||||
toolRegistry: ToolRegistry;
|
||||
toolExecutor: ToolExecutor;
|
||||
gateway: GatewayServer;
|
||||
channelRegistry: ChannelRegistry;
|
||||
}
|
||||
|
||||
function loadSystemPrompt(): string {
|
||||
@@ -106,6 +105,59 @@ function createModelRouter(config: Config): ModelRouter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the unified message handler for the channel registry.
|
||||
* Each channel+sender pair gets its own NativeAgent backed by a persistent session.
|
||||
*/
|
||||
function createMessageRouter(deps: {
|
||||
sessionManager: SessionManager;
|
||||
modelRouter: ModelRouter;
|
||||
systemPrompt: string;
|
||||
toolRegistry: ToolRegistry;
|
||||
toolExecutor: ToolExecutor;
|
||||
}) {
|
||||
// Cache agents by session ID to avoid recreating on every message
|
||||
const agents = new Map<string, NativeAgent>();
|
||||
|
||||
function getOrCreateAgent(channel: string, senderId: string): NativeAgent {
|
||||
const sessionId = `${channel}:${senderId}`;
|
||||
let agent = agents.get(sessionId);
|
||||
if (!agent) {
|
||||
const session = deps.sessionManager.getSession(channel, senderId);
|
||||
agent = new NativeAgent({
|
||||
modelClient: deps.modelRouter,
|
||||
systemPrompt: deps.systemPrompt,
|
||||
session,
|
||||
toolRegistry: deps.toolRegistry,
|
||||
toolExecutor: deps.toolExecutor,
|
||||
});
|
||||
agents.set(sessionId, agent);
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
return async (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>): Promise<void> => {
|
||||
const agent = getOrCreateAgent(msg.channel, msg.senderId);
|
||||
|
||||
// Handle special commands
|
||||
if (msg.metadata?.isCommand && msg.metadata.command === 'reset') {
|
||||
agent.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await agent.process(msg.text);
|
||||
await reply({ text: response, replyTo: msg.id });
|
||||
} catch (error) {
|
||||
console.error(`Error processing message from ${msg.channel}:${msg.senderId}:`, error);
|
||||
await reply({
|
||||
text: 'Sorry, an error occurred while processing your message.',
|
||||
replyTo: msg.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
const lifecycle = new Lifecycle();
|
||||
|
||||
@@ -138,26 +190,6 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
// Load system prompt once for reuse
|
||||
const systemPrompt = loadSystemPrompt();
|
||||
|
||||
// Get Telegram session
|
||||
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
|
||||
const session = sessionManager.getSession('telegram', telegramUserId);
|
||||
|
||||
// Initialize native agent with session and tools
|
||||
const agent = new NativeAgent({
|
||||
modelClient: modelRouter,
|
||||
systemPrompt,
|
||||
session,
|
||||
toolRegistry,
|
||||
toolExecutor,
|
||||
});
|
||||
|
||||
// Initialize Telegram bot with hook engine
|
||||
const bot = createTelegramBot({
|
||||
telegram: config.telegram,
|
||||
agent,
|
||||
hookEngine,
|
||||
});
|
||||
|
||||
// Initialize gateway WebSocket server
|
||||
const gateway = new GatewayServer({
|
||||
port: config.server.port,
|
||||
@@ -167,9 +199,43 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
systemPrompt,
|
||||
toolRegistry,
|
||||
toolExecutor,
|
||||
uiDir: resolve(import.meta.dirname, '../gateway/ui'),
|
||||
config,
|
||||
restart: async () => {
|
||||
console.log('Restart requested via gateway');
|
||||
await lifecycle.shutdown();
|
||||
// Exit with code 75 (EX_TEMPFAIL) — process supervisor should restart
|
||||
process.exit(75);
|
||||
},
|
||||
});
|
||||
|
||||
// Register signal handlers
|
||||
// ── Channel Registry ──────────────────────────────────────────
|
||||
|
||||
const channelRegistry = new ChannelRegistry();
|
||||
|
||||
// Set up the unified message handler
|
||||
channelRegistry.setMessageHandler(createMessageRouter({
|
||||
sessionManager,
|
||||
modelRouter,
|
||||
systemPrompt,
|
||||
toolRegistry,
|
||||
toolExecutor,
|
||||
}));
|
||||
|
||||
// Register Telegram adapter
|
||||
const telegramAdapter = new TelegramAdapter({
|
||||
botToken: config.telegram.bot_token,
|
||||
allowedChatIds: config.telegram.allowed_chat_ids,
|
||||
hookEngine,
|
||||
});
|
||||
channelRegistry.register(telegramAdapter);
|
||||
|
||||
// Register WebChat adapter (wraps the gateway)
|
||||
const webChatAdapter = new WebChatAdapter({ gateway });
|
||||
channelRegistry.register(webChatAdapter);
|
||||
|
||||
// ── Signal Handlers ───────────────────────────────────────────
|
||||
|
||||
const signalHandler = () => {
|
||||
lifecycle.shutdown().then(() => process.exit(0));
|
||||
};
|
||||
@@ -182,20 +248,18 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
process.off('SIGTERM', signalHandler);
|
||||
});
|
||||
|
||||
// Start bot
|
||||
// ── Start Services ────────────────────────────────────────────
|
||||
|
||||
// Register shutdown handler for channels (stops Telegram bot etc.)
|
||||
lifecycle.onShutdown(async () => {
|
||||
await bot.stop();
|
||||
console.log('Telegram bot stopped');
|
||||
await channelRegistry.stopAll();
|
||||
console.log('Channel adapters stopped');
|
||||
});
|
||||
|
||||
// Use long polling (no webhook, no internet exposure)
|
||||
bot.start({
|
||||
onStart: (botInfo) => {
|
||||
console.log(`Telegram bot started: @${botInfo.username}`);
|
||||
},
|
||||
});
|
||||
// Start all channel adapters (Telegram long polling, WebChat status)
|
||||
await channelRegistry.startAll();
|
||||
|
||||
// Start gateway
|
||||
// Start gateway (HTTP + WS server)
|
||||
lifecycle.onShutdown(async () => {
|
||||
await gateway.stop();
|
||||
console.log('Gateway server stopped');
|
||||
@@ -205,7 +269,18 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
|
||||
console.log('Flynn daemon started');
|
||||
|
||||
return { config, lifecycle, bot, agent, sessionStore, sessionManager, hookEngine, modelRouter, toolRegistry, toolExecutor, gateway };
|
||||
return {
|
||||
config,
|
||||
lifecycle,
|
||||
sessionStore,
|
||||
sessionManager,
|
||||
hookEngine,
|
||||
modelRouter,
|
||||
toolRegistry,
|
||||
toolExecutor,
|
||||
gateway,
|
||||
channelRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
export { Lifecycle } from './lifecycle.js';
|
||||
|
||||
Reference in New Issue
Block a user