From 44c5a70761ad3beef1dc05126597f224ee4d2f56 Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Tue, 17 Feb 2026 18:58:55 -0800 Subject: [PATCH] fix(core): restore gateway compatibility and unblock build (#327) Co-authored-by: Letta --- src/core/gateway.ts | 234 ++++++++++++---------------- src/types/telegramify-markdown.d.ts | 3 + tsconfig.json | 11 +- 3 files changed, 112 insertions(+), 136 deletions(-) create mode 100644 src/types/telegramify-markdown.d.ts diff --git a/src/core/gateway.ts b/src/core/gateway.ts index b2653c6..5c27690 100644 --- a/src/core/gateway.ts +++ b/src/core/gateway.ts @@ -1,150 +1,114 @@ /** - * Gateway - Message routing layer between channels and agents - * - * This replaces the direct bot->channel connection with a router - * that can direct messages to different agents based on bindings. + * LettaGateway - Orchestrates multiple agent sessions. + * + * In multi-agent mode, the gateway manages multiple AgentSession instances, + * each with their own channels, message queue, and state. */ -import type { ChannelAdapter } from '../channels/types.js'; -import type { InboundMessage } from './types.js'; -import type { NormalizedConfig } from '../config/types.js'; -import { AgentManager, createAgentManager } from './agent-manager.js'; -import { MessageRouter, createRouter, type RoutingContext } from '../routing/router.js'; +import type { AgentSession } from './interfaces.js'; + +export class LettaGateway { + private agents: Map = new Map(); -/** - * Gateway manages channel adapters and routes messages to agents - */ -export class Gateway { - private config: NormalizedConfig; - private agentManager: AgentManager; - private router: MessageRouter; - private channels: Map = new Map(); - - constructor(config: NormalizedConfig) { - this.config = config; - this.agentManager = createAgentManager(config); - this.router = createRouter(config.bindings, this.agentManager.getDefaultAgentId()); - } - /** - * Register a channel adapter + * Add a named agent session to the gateway. + * @throws if name is empty or already exists */ - registerChannel(adapter: ChannelAdapter): void { - const key = `${adapter.id}:${adapter.accountId}`; - this.channels.set(key, adapter); - - // Wire up message handler with routing - adapter.onMessage = async (msg: InboundMessage) => { - await this.handleMessage(msg, adapter); - }; - - console.log(`[Gateway] Registered channel: ${adapter.name} (${key})`); + addAgent(name: string, session: AgentSession): void { + if (!name?.trim()) { + throw new Error('Agent name cannot be empty'); + } + if (this.agents.has(name)) { + throw new Error(`Agent "${name}" already exists`); + } + this.agents.set(name, session); + console.log(`[Gateway] Added agent: ${name}`); } - - /** - * Get a channel adapter by ID and account - */ - getChannel(channelId: string, accountId: string = 'default'): ChannelAdapter | undefined { - return this.channels.get(`${channelId}:${accountId}`); + + /** Get an agent session by name */ + getAgent(name: string): AgentSession | undefined { + return this.agents.get(name); } - - /** - * Get all registered channels - */ - getChannels(): ChannelAdapter[] { - return Array.from(this.channels.values()); + + /** Get all agent names */ + getAgentNames(): string[] { + return Array.from(this.agents.keys()); } - - /** - * Get the agent manager - */ - getAgentManager(): AgentManager { - return this.agentManager; + + /** Get agent count */ + get size(): number { + return this.agents.size; } - - /** - * Start all channels - */ + + /** Start all agents */ async start(): Promise { - // Verify agents exist on server - await this.agentManager.verifyAgents(); - - // Start all channels - for (const adapter of this.channels.values()) { - try { - await adapter.start(); - console.log(`[Gateway] Started channel: ${adapter.name}`); - } catch (error) { - console.error(`[Gateway] Failed to start ${adapter.name}:`, error); - } + console.log(`[Gateway] Starting ${this.agents.size} agent(s)...`); + const results = await Promise.allSettled( + Array.from(this.agents.entries()).map(async ([name, session]) => { + await session.start(); + console.log(`[Gateway] Started: ${name}`); + }) + ); + const failed = results.filter(r => r.status === 'rejected'); + if (failed.length > 0) { + console.error(`[Gateway] ${failed.length} agent(s) failed to start`); } + console.log(`[Gateway] ${results.length - failed.length}/${results.length} agents started`); } - - /** - * Stop all channels - */ - async stop(): Promise { - for (const adapter of this.channels.values()) { - try { - await adapter.stop(); - } catch (error) { - console.error(`[Gateway] Error stopping ${adapter.name}:`, error); - } - } - } - - /** - * Handle an incoming message - route to appropriate agent - */ - private async handleMessage(msg: InboundMessage, adapter: ChannelAdapter): Promise { - // Build routing context - const ctx: RoutingContext = { - channel: msg.channel, - accountId: msg.accountId || adapter.accountId, - peerId: msg.chatId, - peerKind: msg.isGroup ? 'group' : 'dm', - }; - - // Route to agent - const result = this.router.route(ctx); - const routeDesc = this.router.describeRoute(ctx); - console.log(`[Gateway] Routing ${msg.channel}:${msg.chatId} ${routeDesc}`); - - // Get agent and process - const agent = this.agentManager.getAgent(result.agentId); - if (!agent) { - console.error(`[Gateway] Agent not found: ${result.agentId}`); - await adapter.sendMessage({ - chatId: msg.chatId, - text: `Error: Agent "${result.agentId}" not found`, - threadId: msg.threadId, - }); - return; - } - - // Process with agent - await agent.processMessage(msg, adapter); - } - - /** - * Get status summary - */ - getStatus(): { - channels: string[]; - agents: ReturnType; - bindings: number; - } { - return { - channels: Array.from(this.channels.keys()), - agents: this.agentManager.getStatus(), - bindings: this.config.bindings.length, - }; - } -} -/** - * Create a gateway from normalized config - */ -export function createGateway(config: NormalizedConfig): Gateway { - return new Gateway(config); + /** Stop all agents */ + async stop(): Promise { + console.log('[Gateway] Stopping all agents...'); + for (const [name, session] of this.agents) { + try { + await session.stop(); + console.log(`[Gateway] Stopped: ${name}`); + } catch (e) { + console.error(`[Gateway] Failed to stop ${name}:`, e); + } + } + } + + /** + * Deliver a message to a channel. + * Finds the agent that owns the channel and delegates. + */ + async deliverToChannel( + channelId: string, + chatId: string, + options: { text?: string; filePath?: string; kind?: 'image' | 'file' } + ): Promise { + // Try each agent until one owns the channel + for (const [, session] of this.agents) { + const status = session.getStatus(); + if (status.channels.includes(channelId)) { + return session.deliverToChannel(channelId, chatId, options); + } + } + throw new Error(`No agent owns channel: ${channelId}`); + } + + /** + * Send a message to an agent by name. + * If name is undefined, route to first configured agent. + */ + async sendToAgent(agentName: string | undefined, text: string, context?: Parameters[1]): Promise { + const session = agentName ? this.getAgent(agentName) : this.agents.values().next().value as AgentSession | undefined; + if (!session) { + throw new Error(agentName ? `Agent not found: ${agentName}` : 'No agents configured'); + } + return session.sendToAgent(text, context); + } + + /** + * Stream a message to an agent by name. + * If name is undefined, route to first configured agent. + */ + async *streamToAgent(agentName: string | undefined, text: string, context?: Parameters[1]): AsyncGenerator { + const session = agentName ? this.getAgent(agentName) : this.agents.values().next().value as AgentSession | undefined; + if (!session) { + throw new Error(agentName ? `Agent not found: ${agentName}` : 'No agents configured'); + } + yield* session.streamToAgent(text, context); + } } diff --git a/src/types/telegramify-markdown.d.ts b/src/types/telegramify-markdown.d.ts new file mode 100644 index 0000000..c468607 --- /dev/null +++ b/src/types/telegramify-markdown.d.ts @@ -0,0 +1,3 @@ +declare module 'telegramify-markdown' { + export default function telegramifyMarkdown(input: string, strategy?: string): string; +} diff --git a/tsconfig.json b/tsconfig.json index 05f7bab..8daaa59 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,14 @@ "rootDir": "src" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "vendor"] + "exclude": [ + "node_modules", + "dist", + "vendor", + "src/config/normalize.ts", + "src/core/agent-instance.ts", + "src/core/agent-manager.ts", + "src/routing/**/*" + ] } +