# 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:<chat_id>` (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_<BOT>` 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.
