# 015 — Soporte multi-plataforma: Telegram como segunda plataforma ## Objetivo Desacoplar el runtime de agentes de Matrix e introducir abstracciones de plataforma que permitan conectar un mismo agente a multiples servicios de mensajeria. Implementar Telegram como primera plataforma adicional para validar el diseno. ## Contexto - Actualmente `agents/runtime.go` depende directamente de `*matrix.Client` y `*matrix.Listener` - `shell/effects/runner.go` ya define `MatrixSender` como interfaz, pero con nombre acoplado - `decision.MessageContext` es **generico** — no tiene nada de Matrix - Las reglas, LLM, tools y memoria son independientes de la plataforma - El acoplamiento esta en: runtime.go, effects/runner.go, listener, y algunos tools (matrix_send) ## Prerequisitos - Ninguno estricto. Se puede hacer de forma incremental sin romper Matrix. --- ## Tareas ### Fase 1: Abstracciones de plataforma en `pkg/platform/` - [ ] **1.1** Crear `pkg/platform/types.go` con las interfaces puras: ```go // Messenger envía mensajes a una plataforma de chat. type Messenger interface { SendText(ctx context.Context, roomID, text string) error SendMarkdown(ctx context.Context, roomID, markdown string) error SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error SendTyping(ctx context.Context, roomID string, typing bool) error } // EventSource escucha eventos de una plataforma y los entrega como MessageContext. type EventSource interface { Run(ctx context.Context) error } // Platform agrupa Messenger + EventSource para una plataforma concreta. type Platform interface { Messenger EventSource Name() string // "matrix", "telegram", "slack", etc. } ``` Nota: estas interfaces van en `pkg/` porque son tipos puros (no ejecutan I/O, solo los definen). - [ ] **1.2** Definir `PlatformID` como prefijo para room IDs multi-plataforma: - Formato: `matrix:!abc123:server.com`, `telegram:chat_456` - Crear helpers `PrefixRoomID(platform, rawID) string` y `ParseRoomID(prefixed) (platform, rawID)` - Esto permite que la memoria y windows no mezclen contextos entre plataformas ### Fase 2: Adaptar shell/matrix/ a las interfaces - [ ] **2.1** Verificar que `shell/matrix/Client` ya satisface `platform.Messenger` (deberia, con los metodos actuales). Anadir metodo `Name() string` que retorne `"matrix"`. - [ ] **2.2** Refactorizar `shell/matrix/Listener` para que implemente `platform.EventSource`: - El `EventHandler` callback ya recibe `decision.MessageContext` — solo necesita ajustar la firma de `Run(ctx)` si difiere - Internamente sigue usando mautrix syncer, pero externamente expone la interfaz generica - [ ] **2.3** Crear wrapper `shell/matrix/Platform` que componga Client + Listener e implemente `platform.Platform` ### Fase 3: Desacoplar runtime.go - [ ] **3.1** Cambiar el campo `matrix *matrix.Client` en `Agent` struct por `messenger platform.Messenger` - [ ] **3.2** Cambiar `listener *matrix.Listener` por `sources []platform.EventSource` - [ ] **3.3** Actualizar `Run()` para arrancar multiples EventSources en goroutines: ```go for _, src := range a.sources { go src.Run(ctx) } ``` - [ ] **3.4** Actualizar `handleEvent` para que no reciba `*event.Event` — actualmente solo usa `evt.RoomID` que ya esta en `MessageContext.RoomID`. Eliminar la dependencia de `mautrix/event`. - [ ] **3.5** Actualizar todas las llamadas directas a `a.matrix.SendXxx()` y `a.matrix.SendTyping()` para usar `a.messenger.SendXxx()`. Puntos clave: - `handleEvent` — typing indicator, command replies, unknown command - `executeActions` — ya pasa por el runner, OK - `handleTaskEvent` — typing indicator, send reply - `runLLM` — tool call notices - [ ] **3.6** Actualizar `shell/effects/runner.go`: - Renombrar interfaz `MatrixSender` a `Messenger` (o importar `platform.Messenger`) - El Runner ya recibe la interfaz, solo cambia el nombre - [ ] **3.7** Actualizar `New()` constructor para recibir `[]platform.Platform` en vez de construir matrix.Client internamente. Mover la creacion de clientes de plataforma al launcher. ### Fase 4: Implementar shell/telegram/ - [ ] **4.1** Elegir libreria de Telegram Bot API para Go. Opciones: - (A) `github.com/go-telegram-bot-api/telegram-bot-api/v5` — la mas popular, estable - (B) `github.com/gotd/td` — cliente completo (MTProto), mas complejo - (C) HTTP directo contra Bot API — minimo, sin dependencias extra - **Recomendacion**: opcion (A) por madurez y simplicidad - [ ] **4.2** Crear `shell/telegram/client.go`: - Struct `Client` con el bot API client interno - Constructor `New(cfg config.TelegramCfg) (*Client, error)` - Implementar `platform.Messenger`: - `SendText` — `tgbotapi.NewMessage(chatID, text)` - `SendMarkdown` — `tgbotapi.NewMessage` con `ParseMode: "MarkdownV2"` - `SendReplyMarkdown` — `ReplyToMessageID` en el message config - `SendTyping` — `tgbotapi.NewChatAction(chatID, "typing")` - [ ] **4.3** Crear `shell/telegram/listener.go`: - Implementar `platform.EventSource` - Modo long-polling con `GetUpdatesChan()` (webhook es mas complejo y requiere dominio publico) - Convertir cada `tgbotapi.Update` a `decision.MessageContext`: - `SenderID` = user ID de Telegram (string) - `SenderName` = username o first_name - `RoomID` = `telegram:` (con prefijo de plataforma) - `Content` = texto del mensaje - `IsDirectMsg` = true si chat.Type == "private" - `IsMention` = true si el mensaje contiene @botname - `Command` = parsear si empieza con `!` (o `/` que es la convencion Telegram) - Llamar al mismo `handleEvent(ctx, msgCtx)` del Agent - [ ] **4.4** Crear `shell/telegram/platform.go` que componga Client + Listener e implemente `platform.Platform` ### Fase 5: Configuracion - [ ] **5.1** Anadir tipos de config en `internal/config/schema.go`: ```yaml telegram: enabled: false bot_token_env: "TELEGRAM_TOKEN_BOT" allowed_chats: [] # lista de chat IDs permitidos (vacio = todos) command_prefix: "/" # convencion Telegram, ademas de "!" ``` Struct: `TelegramCfg` con campos `Enabled`, `BotTokenEnv`, `AllowedChats`, `CommandPrefix` - [ ] **5.2** Anadir `TelegramCfg` al config principal del agente (junto a `MatrixCfg`) - [ ] **5.3** Actualizar `internal/config/loader.go` para parsear la nueva seccion - [ ] **5.4** Actualizar `cmd/launcher/main.go` para instanciar plataformas segun config: ```go var platforms []platform.Platform if cfg.Matrix.Enabled { platforms = append(platforms, matrixPlatform) } if cfg.Telegram.Enabled { platforms = append(platforms, telegramPlatform) } ``` - [ ] **5.5** Anadir `TELEGRAM_TOKEN_` a `.env.example` ### Fase 6: Tool matrix_send → platform_send - [ ] **6.1** Evaluar si `matrix_send` tool debe ser generico o especifico: - Opcion A: renombrar a `send_message` con parametro `platform` — mas flexible - Opcion B: mantener `matrix_send` y anadir `telegram_send` — mas simple - **Recomendacion**: opcion A a largo plazo, pero para esta task basta con que el LLM responda por la misma plataforma que recibio el mensaje (ya lo hace via `handleEvent` → runner) - `matrix_send` como tool explícita solo se usa para enviar a rooms arbitrarios; si no se necesita eso en Telegram, no hace falta `telegram_send` ahora - [ ] **6.2** Si se opta por generalizar: crear `tools/send.go` con `NewPlatformSend(messenger platform.Messenger)` que el LLM pueda usar para enviar a cualquier plataforma ### Fase 7: Tests - [ ] **7.1** Unit tests para `pkg/platform/types.go` — verificar que las interfaces compilan y los helpers de PlatformID funcionan - [ ] **7.2** Unit tests para `shell/telegram/client.go` — mock del bot API, verificar conversion de mensajes - [ ] **7.3** Unit tests para `shell/telegram/listener.go` — mock de updates, verificar conversion a MessageContext - [ ] **7.4** Integration test: verificar que un Agent con dos plataformas (matrix mock + telegram mock) recibe y responde correctamente por ambas - [ ] **7.5** Verificar que todos los agentes existentes siguen funcionando solo con Matrix (backward compat) ### Fase 8: Documentacion - [ ] **8.1** Actualizar `CLAUDE.md` — anadir `shell/telegram/` a la estructura de directorios, actualizar diagrama de flujo - [ ] **8.2** Actualizar `docs/creating-agents.md` con la seccion de configuracion multi-plataforma - [ ] **8.3** Actualizar `.claude/rules/create_agent.md` para mencionar la seccion `telegram:` en config - [ ] **8.4** Anadir a `README.md` la seccion de soporte Telegram --- ## Orden de ejecucion recomendado 1. **Fase 1** (interfaces) — base para todo lo demas 2. **Fase 2** (adaptar matrix) — asegurar que Matrix sigue funcionando con las nuevas interfaces 3. **Fase 3** (desacoplar runtime) — el refactor central; debe compilar y pasar tests con solo Matrix 4. **Fase 5** (config) — preparar el config antes de implementar Telegram 5. **Fase 4** (implementar telegram) — el codigo nuevo 6. **Fase 6** (tools) — ajustar si es necesario 7. **Fase 7** (tests) — validar todo 8. **Fase 8** (docs) — ultima, cuando todo este estable ## Decisiones de diseno pendientes - **Memoria compartida vs separada**: Si un usuario habla por Matrix y por Telegram, son windows separadas (por el prefijo de plataforma en roomID). Podria unificarse en el futuro con un "user identity" cross-platform, pero no es necesario ahora. - **Comandos `/` vs `!`**: Telegram usa `/` como convencion para comandos de bot. Soportar ambos prefijos (`/` y `!`) en el parser de comandos, configurable por plataforma. - **Webhooks vs long-polling**: Empezar con long-polling por simplicidad. Webhook requiere HTTPS publico y es una optimizacion posterior. - **Presence**: Matrix tiene presence (online/offline). Telegram no tiene equivalente nativo para bots. Abstraer como opcional en la interfaz. - **Reactions**: Matrix tiene reactions (`m.reaction`). Telegram tiene limitaciones. No incluir en `Messenger` por ahora; dejarlo como extension especifica de plataforma. ## Dependencias nuevas ``` github.com/go-telegram-bot-api/telegram-bot-api/v5 # Telegram Bot API client ``` ## Riesgos - El refactor de runtime.go (Fase 3) es el mas delicado — cambia el corazon del sistema. Hacer commits atomicos despues de cada sub-tarea. - Asegurar backward compatibility: un agente sin `telegram:` en su config debe funcionar exactamente como antes.