chore: renumerar tasks a 3 dígitos + añadir nuevas + config tweaks
Renumera todos los archivos de tasks de 2 dígitos (01-, 02-...) a 3 dígitos (001-, 002-...) para mejor ordenación. Añade tres nuevas tasks pendientes: 012-threads, 013-hot-reload, 014-template-agent. Deshabilita memory temporalmente en assistant-bot config mientras se estabiliza el sistema de memoria. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Task 011 — Matrix Thread Support
|
||||
|
||||
## Objetivo
|
||||
|
||||
Permitir que los agentes mantengan conversaciones en threads de Matrix (`m.thread`),
|
||||
de forma que cada interaccion con un usuario pueda vivir en un hilo separado
|
||||
en lugar de la timeline principal del room.
|
||||
|
||||
## Contexto
|
||||
|
||||
Matrix soporta threads via `m.relates_to` con `rel_type: "m.thread"`.
|
||||
Un thread siempre referencia un **evento raiz** y opcionalmente incluye
|
||||
`m.in_reply_to` como fallback para clientes sin soporte de threads.
|
||||
|
||||
```json
|
||||
{
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.thread",
|
||||
"event_id": "$rootEventId",
|
||||
"is_falling_back": true,
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$lastEventInThread"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisito
|
||||
|
||||
- Task: Reply simple (`m.in_reply_to`) ya implementado.
|
||||
|
||||
## Plan de implementacion
|
||||
|
||||
### 1. Detectar threads entrantes en el Listener
|
||||
|
||||
- En `shell/matrix/listener.go`, al parsear el evento, extraer `m.relates_to`
|
||||
- Si `rel_type == "m.thread"`, capturar `event_id` como `ThreadRootID`
|
||||
- Propagar `ThreadRootID` en `MessageContext`
|
||||
|
||||
### 2. Extender MessageContext
|
||||
|
||||
- `pkg/decision/types.go`: anadir `ThreadRootID string` (el evento raiz del thread)
|
||||
- Esto es dato puro, no rompe la arquitectura
|
||||
|
||||
### 3. Extender ReplyAction
|
||||
|
||||
- `pkg/decision/types.go`: anadir `ThreadRootID string` a `ReplyAction`
|
||||
- El runner usara esto para decidir si enviar como thread o como mensaje normal
|
||||
|
||||
### 4. SendThreadMarkdown en Client
|
||||
|
||||
- `shell/matrix/client.go`: nuevo metodo `SendThreadMarkdown(ctx, roomID, threadRootID, inReplyTo, markdown)`
|
||||
- Construye el `m.relates_to` con `rel_type: "m.thread"` + fallback `m.in_reply_to`
|
||||
|
||||
### 5. Actualizar effects/Runner
|
||||
|
||||
- `shell/effects/runner.go`: si `ReplyAction.ThreadRootID != ""`, usar `SendThreadMarkdown`
|
||||
- Actualizar interfaz `MatrixSender` con el nuevo metodo
|
||||
|
||||
### 6. Propagacion en runtime.go
|
||||
|
||||
- Cuando el mensaje entrante ya esta en un thread (`msgCtx.ThreadRootID != ""`),
|
||||
las respuestas del bot deben continuar en ese thread
|
||||
- Cuando el usuario inicia una conversacion nueva, decidir segun config si crear thread o no
|
||||
|
||||
### 7. Configuracion por agente
|
||||
|
||||
- `internal/config/schema.go`: anadir opcion `matrix.threads.enabled: bool` y
|
||||
`matrix.threads.auto_thread: bool` (crear thread automatico por cada conversacion nueva)
|
||||
- Default: `enabled: true`, `auto_thread: false`
|
||||
|
||||
### 8. Memory por thread
|
||||
|
||||
- La window de conversacion deberia poder ser por thread en vez de por room
|
||||
- Si `ThreadRootID != ""`, usar `threadRootID` como key de la window en vez de `roomID`
|
||||
- Esto permite conversaciones paralelas en threads distintos sin mezclarse
|
||||
|
||||
### 9. Tests
|
||||
|
||||
- Unit tests para `SendThreadMarkdown` (verificar estructura JSON)
|
||||
- Test de integracion: listener detecta thread entrante y propaga ThreadRootID
|
||||
- Test: respuesta dentro de thread mantiene el thread root correcto
|
||||
|
||||
## Notas
|
||||
|
||||
- `is_falling_back: true` siempre debe estar cuando se usa thread + in_reply_to fallback
|
||||
- El `event_id` de `m.relates_to` (nivel top) siempre apunta al root del thread, nunca cambia
|
||||
- El `m.in_reply_to` dentro del thread apunta al ultimo mensaje respondido
|
||||
- Clientes sin soporte de threads ven el fallback como un reply normal
|
||||
@@ -0,0 +1,265 @@
|
||||
# Task 013 — Hot-Reload de Agentes Individuales
|
||||
|
||||
## Objetivo
|
||||
|
||||
Permitir reiniciar (recrear) un agente individual dentro del launcher sin detener
|
||||
los demas agentes. El bus y el orquestador permanecen intactos porque todo sigue
|
||||
en el mismo proceso.
|
||||
|
||||
## Contexto
|
||||
|
||||
Actualmente el launcher ejecuta todos los agentes como goroutines dentro de un
|
||||
unico proceso. No hay forma de reiniciar un solo agente — hay que matar y
|
||||
re-arrancar el launcher entero, lo que desconecta a todos los bots de Matrix
|
||||
y rompe conversaciones en curso.
|
||||
|
||||
### Por que no un proceso por agente
|
||||
|
||||
El sistema de orquestacion multi-bot depende de:
|
||||
|
||||
- **Bus in-process** (`shell/bus/bus.go`): Go channels, solo funciona dentro del mismo proceso.
|
||||
- **Orquestador** (`shell/orchestration/`): usa el bus para `dispatchAndWait()` (request-response).
|
||||
- **Deduplicacion** (`seen map`): estado compartido en memoria para evitar que multiples bots
|
||||
en el mismo room procesen el mismo mensaje.
|
||||
- **Interceptor**: callback sincrono que el listener de cada bot llama al orquestador.
|
||||
|
||||
Separar en procesos romperia todo lo anterior. El hot-reload mantiene el proceso unico
|
||||
pero recrea el agente internamente.
|
||||
|
||||
## Mecanismo propuesto
|
||||
|
||||
### Signal: SIGHUP + archivo de control
|
||||
|
||||
1. El launcher escucha `SIGHUP` ademas de SIGINT/SIGTERM.
|
||||
2. Al recibir SIGHUP, lee un archivo `run/reload.txt` que contiene el ID del agente a recargar.
|
||||
3. Si el archivo no existe o esta vacio, recarga TODOS los agentes.
|
||||
4. Alternativa: un comando via bus (`bus.KindReload`) enviado desde el TUI/agentctl.
|
||||
|
||||
### Flujo de hot-reload
|
||||
|
||||
```
|
||||
SIGHUP recibido (o comando reload via bus/TUI)
|
||||
|
|
||||
v
|
||||
Launcher lee run/reload.txt -> agentID (o "*" para todos)
|
||||
|
|
||||
v
|
||||
Para cada agente a recargar:
|
||||
1. Cancelar su context (ctx.cancel) -> Agent.Run() termina gracefully
|
||||
2. Esperar a que la goroutine termine (via WaitGroup o done channel)
|
||||
3. Desuscribir del bus (bus.Unsubscribe(agentID))
|
||||
4. Re-leer config.yaml del agente
|
||||
5. Re-crear Agent con agents.New(cfg, rules, logger)
|
||||
6. Re-suscribir al bus (agent.SetBus)
|
||||
7. Re-conectar interceptor y membership notify si orquestador activo
|
||||
8. Re-registrar participante en orquestador
|
||||
9. Lanzar nueva goroutine con agent.Run(newCtx)
|
||||
|
|
||||
v
|
||||
Log: "agent <id> reloaded successfully"
|
||||
```
|
||||
|
||||
## Plan de implementacion
|
||||
|
||||
### 1. Hacer Agent cancelable individualmente
|
||||
|
||||
**Archivo**: `agents/runtime.go`
|
||||
|
||||
- Actualmente `Agent.Run(ctx)` recibe el context del launcher (compartido).
|
||||
- Cambiar para que cada agente tenga su propio `context.WithCancel(parentCtx)`.
|
||||
- Exponer un metodo `Agent.Stop()` que cancela el context hijo.
|
||||
- Exponer un canal o metodo `Agent.Done() <-chan struct{}` para saber cuando termino.
|
||||
|
||||
```go
|
||||
type Agent struct {
|
||||
// ... campos existentes ...
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (a *Agent) Run(ctx context.Context) error {
|
||||
ctx, a.cancel = context.WithCancel(ctx)
|
||||
defer close(a.done)
|
||||
// ... resto del Run existente ...
|
||||
}
|
||||
|
||||
func (a *Agent) Stop() {
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) Done() <-chan struct{} {
|
||||
return a.done
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Anadir Unsubscribe al bus
|
||||
|
||||
**Archivo**: `shell/bus/bus.go`
|
||||
|
||||
- Nuevo metodo `Unsubscribe(id AgentID)` que elimina el canal del mapa y lo cierra.
|
||||
- `listenBus()` en runtime.go debe manejar canal cerrado sin panic.
|
||||
|
||||
```go
|
||||
func (b *Bus) Unsubscribe(id AgentID) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if ch, ok := b.channels[id]; ok {
|
||||
close(ch)
|
||||
delete(b.channels, id)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Tracker de agentes en el launcher
|
||||
|
||||
**Archivo**: `cmd/launcher/main.go`
|
||||
|
||||
- Reemplazar el `sync.WaitGroup` actual por un registry de agentes vivos:
|
||||
|
||||
```go
|
||||
type runningAgent struct {
|
||||
agent *agents.Agent
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
type agentRegistry struct {
|
||||
mu sync.Mutex
|
||||
agents map[string]*runningAgent
|
||||
}
|
||||
```
|
||||
|
||||
- Metodos: `register(id, agent)`, `stop(id)`, `reload(id, parentCtx)`, `stopAll()`.
|
||||
- `reload(id)` ejecuta el flujo descrito arriba: stop -> wait -> recreate -> start.
|
||||
|
||||
### 4. Handler de SIGHUP
|
||||
|
||||
**Archivo**: `cmd/launcher/main.go`
|
||||
|
||||
- Escuchar SIGHUP en un canal separado (no en el mismo NotifyContext de SIGINT/SIGTERM).
|
||||
- Al recibir SIGHUP:
|
||||
- Leer `run/reload.txt` (si existe)
|
||||
- Llamar `registry.reload(id, ctx)` o `registry.reloadAll(ctx)` si es "*"
|
||||
|
||||
```go
|
||||
sighup := make(chan os.Signal, 1)
|
||||
signal.Notify(sighup, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for range sighup {
|
||||
id := readReloadTarget("run/reload.txt")
|
||||
if id == "" || id == "*" {
|
||||
registry.reloadAll(ctx)
|
||||
} else {
|
||||
registry.reload(id, ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
### 5. Integracion con el orquestador
|
||||
|
||||
**Archivo**: `cmd/launcher/main.go` (dentro de `reload()`)
|
||||
|
||||
Al recrear un agente que participa en orquestacion:
|
||||
|
||||
1. El orquestador no necesita "desregistrar" al participante — basta con re-registrar
|
||||
con la misma info (sobreescribe).
|
||||
2. Re-llamar `SetInterceptor` y `SetMembershipNotify` en el nuevo Agent.
|
||||
3. El bus.Subscribe del nuevo agente devuelve un canal nuevo — el orquestador usa
|
||||
`bus.Send(agentID)` que resuelve el nuevo canal automaticamente.
|
||||
|
||||
**Caso critico**: si el agente esta en medio de un `dispatchAndWait()` cuando se cancela:
|
||||
- El context se cancela -> SendAndWait retorna error
|
||||
- El orquestador recibe timeout/error para esa iteracion
|
||||
- La respuesta parcial se pierde pero no hay corrupcion
|
||||
- El orquestador puede reintentar o pasar al siguiente bot
|
||||
|
||||
### 6. Integracion con el TUI
|
||||
|
||||
**Archivos**: `pkg/tui/update.go`, `shell/tui/adapter.go`, `shell/process/manager.go`
|
||||
|
||||
El boton "Restart" del TUI (task actual) debe cambiar de "kill+start launcher" a:
|
||||
|
||||
1. Escribir el agentID en `run/reload.txt`
|
||||
2. Enviar SIGHUP al proceso del launcher (`kill -HUP <pid>`)
|
||||
3. Esperar un momento y refrescar estado
|
||||
|
||||
```go
|
||||
func (a *Adapter) restartAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Escribir target en reload file
|
||||
os.WriteFile("run/reload.txt", []byte(id), 0644)
|
||||
// Enviar SIGHUP al launcher
|
||||
pid := a.mgr.UnifiedPID()
|
||||
if pid > 0 {
|
||||
syscall.Kill(pid, syscall.SIGHUP)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: nil}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Integracion con agentctl CLI
|
||||
|
||||
**Archivo**: `cmd/agentctl/main.go`
|
||||
|
||||
- Nuevo subcomando: `agentctl reload <agent-id>`
|
||||
- Escribe `run/reload.txt` + envia SIGHUP
|
||||
- Mismo mecanismo que el TUI
|
||||
|
||||
### 8. Graceful shutdown del agente
|
||||
|
||||
**Archivo**: `agents/runtime.go`
|
||||
|
||||
Al cancelar el context individual de un agente:
|
||||
|
||||
1. El sync loop de Matrix debe detenerse limpiamente (mautrix tiene `StopSync()`)
|
||||
2. Las llamadas LLM en curso deben cancelarse via context
|
||||
3. La tool execution en curso debe respetar context cancellation
|
||||
4. Memory/knowledge stores deben flush antes de cerrar
|
||||
5. El canal del bus se cierra — `listenBus` sale del loop
|
||||
|
||||
Verificar que `runtime.go:Run()` ya maneja todo esto con el context actual.
|
||||
Si no, anadir cleanup explicicto.
|
||||
|
||||
### 9. Tests
|
||||
|
||||
- **Unit test**: `bus.Unsubscribe` no causa panic, mensajes posteriores al unsubscribe
|
||||
no se pierden (retornan error).
|
||||
- **Unit test**: `agentRegistry.reload()` — stop + recreate funciona.
|
||||
- **Integration test**: enviar SIGHUP y verificar que solo el agente target se reinicia.
|
||||
- **Orchestrator test**: agente en medio de task, se cancela, orquestador maneja el error.
|
||||
|
||||
## Orden de implementacion sugerido
|
||||
|
||||
1. `Agent.Stop()` + `Agent.Done()` (runtime.go)
|
||||
2. `Bus.Unsubscribe()` (bus.go)
|
||||
3. `agentRegistry` en launcher (main.go)
|
||||
4. Handler SIGHUP (main.go)
|
||||
5. Graceful shutdown verification (runtime.go)
|
||||
6. Actualizar TUI adapter (adapter.go)
|
||||
7. Actualizar agentctl (agentctl/main.go)
|
||||
8. Tests
|
||||
|
||||
## Riesgos y mitigaciones
|
||||
|
||||
| Riesgo | Mitigacion |
|
||||
|--------|------------|
|
||||
| Race condition al cerrar canal del bus | Mutex en Unsubscribe, recover en Send |
|
||||
| Crypto store de mautrix queda locked | Cerrar store explicitamente en cleanup |
|
||||
| Orquestador en medio de dispatch | Context cancellation + timeout ya existente |
|
||||
| Config invalido al recargar | Validar config antes de destruir agente viejo |
|
||||
| Matrix sync no para limpio | Llamar StopSync() explicitamente antes de cancel |
|
||||
|
||||
## Notas
|
||||
|
||||
- SIGHUP es la convencion Unix para "recargar configuracion" (nginx, haproxy, etc.)
|
||||
- El archivo `run/reload.txt` es efimero — se puede borrar despues de leer
|
||||
- Si el launcher no esta corriendo, el TUI debe caer al comportamiento actual (start launcher)
|
||||
- El orquestador NO se recarga — solo los agentes. Para recargar el orquestador
|
||||
hay que reiniciar el launcher entero.
|
||||
@@ -0,0 +1,104 @@
|
||||
# 014 — Agente plantilla + estandarización de config.yaml
|
||||
|
||||
## Objetivo
|
||||
Crear un agente plantilla (no lanzable) que sirva como referencia canónica para la configuración de todos los agentes. Enriquecer `!info` para mostrar metadata completa. Estandarizar los config.yaml existentes.
|
||||
|
||||
## Contexto
|
||||
- El launcher descubre agentes via `agents/*/config.yaml` (glob en cmd/launcher/main.go:55)
|
||||
- `!info` existe como built-in en `pkg/command/builtins.go` pero solo muestra: nombre, ID, versión, descripción
|
||||
- No hay herencia de configs ni template base — cada config.yaml es autocontenido
|
||||
- Agentes actuales: assistant-bot, asistente-2, meteorologo
|
||||
|
||||
---
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Agente plantilla
|
||||
|
||||
- [ ] **1.1** Decidir mecanismo de exclusión del launcher. Opciones:
|
||||
- (A) Campo `agent.template: true` en config.yaml → launcher lo ignora
|
||||
- (B) Directorio especial fuera de `agents/` (ej: `agents/_template/`)
|
||||
- (C) Convención de nombre con prefijo `_` que el glob excluya
|
||||
- **Recomendación**: opción (A) es la más explícita y extensible. Añadir campo `Template bool` a `AgentMeta` en schema.go y filtrar en launcher.
|
||||
|
||||
- [ ] **1.2** Añadir campo `template: true` a `internal/config/schema.go` → `AgentMeta`
|
||||
|
||||
- [ ] **1.3** Filtrar agentes template en `cmd/launcher/main.go` — skip si `cfg.Agent.Template == true`
|
||||
|
||||
- [ ] **1.4** Crear `agents/_template/config.yaml` con TODAS las secciones documentadas y valores por defecto comentados. Este archivo es la referencia canónica. Incluir:
|
||||
- `agent:` — id, name, description, version, tags, template: true
|
||||
- `personality:` — valores estándar (tone, verbosity, language, emoji_style, prefix, templates, behavior)
|
||||
- `llm:` — primary con placeholders, tool_use config, fallback
|
||||
- `tools:` — todas las subsecciones (memory, knowledge, ssh, http, scripts, file_ops, mcp)
|
||||
- `matrix:` — homeserver, user_id, encryption, rooms, filters
|
||||
- `agents:` — peers, delegation
|
||||
- `security:` — roles, audit
|
||||
- `schedules:` — ejemplo de cron
|
||||
- `observability:` — logging, metrics, health
|
||||
- `resilience:` — circuit breaker, retry, shutdown
|
||||
- `storage:` — state, cache, history
|
||||
- `memory:` — window config
|
||||
|
||||
- [ ] **1.5** Crear `agents/_template/agent.go` mínimo con `Rules()` que retorna slice vacío (para que compile, aunque nunca se lance)
|
||||
|
||||
- [ ] **1.6** Actualizar `dev-scripts/agent/new-agent.sh` para copiar desde `_template/` en lugar de generar inline
|
||||
|
||||
### Fase 2: Enriquecer `!info`
|
||||
|
||||
- [ ] **2.1** Modificar el handler de `!info` en `agents/commands.go` para que devuelva:
|
||||
- Nombre (`agent.name`)
|
||||
- Descripción (`agent.description`)
|
||||
- Versión (`agent.version`)
|
||||
- Personalidad: tone, verbosity, language, emoji_style
|
||||
- LLM: provider + modelo del primary
|
||||
- Nº de tools registradas (del toolRegistry)
|
||||
- Memoria habilitada (sí/no + window size)
|
||||
- Knowledge habilitado (sí/no)
|
||||
- Nº de docs en knowledge (si habilitado, contar archivos en knowledge dir)
|
||||
- Uptime del agente
|
||||
|
||||
- [ ] **2.2** Formatear la respuesta como bloque legible (markdown con secciones o tabla)
|
||||
|
||||
- [ ] **2.3** Asegurar que `!info` no exponga datos sensibles (tokens, API keys, paths internos)
|
||||
|
||||
### Fase 3: Estandarizar configs existentes
|
||||
|
||||
- [ ] **3.1** Definir convenciones estándar obligatorias para todo config.yaml:
|
||||
- `agent.version` siempre presente (semver)
|
||||
- `agent.tags` siempre presente (al menos un tag de categoría)
|
||||
- `personality.language` y `personality.languages_supported` siempre explícitos
|
||||
- `personality.behavior` siempre con las 6 claves (proactive, ask_confirmation, show_reasoning, thread_replies, typing_indicator, acknowledge_receipt)
|
||||
- `llm.tool_use` siempre explícito (enabled true/false, max_iterations)
|
||||
- `tools.memory` y `tools.knowledge` siempre presentes (enabled true/false)
|
||||
- `matrix.homeserver` y `matrix.encryption` siempre presentes
|
||||
- `observability.logging.level` siempre explícito
|
||||
|
||||
- [ ] **3.2** Actualizar `agents/assistant-bot/config.yaml` según convenciones
|
||||
|
||||
- [ ] **3.3** Actualizar `agents/asistente-2/config.yaml` según convenciones
|
||||
|
||||
- [ ] **3.4** Actualizar `agents/meteorologo/config.yaml` según convenciones
|
||||
|
||||
- [ ] **3.5** Validar que los tres agentes arrancan correctamente tras los cambios
|
||||
|
||||
### Fase 4: Documentación y tooling
|
||||
|
||||
- [ ] **4.1** Añadir validación en `internal/config/loader.go` que emita warnings si faltan secciones recomendadas (no bloquear, solo log)
|
||||
|
||||
- [ ] **4.2** Actualizar `.claude/policies/create_agent.md` para referenciar el template
|
||||
|
||||
- [ ] **4.3** Actualizar `docs/creating-agents.md` con la nueva referencia del template
|
||||
|
||||
---
|
||||
|
||||
## Orden de ejecución recomendado
|
||||
1. Fase 1 (template) → base para todo lo demás
|
||||
2. Fase 2 (info) → independiente, puede hacerse en paralelo
|
||||
3. Fase 3 (estandarizar) → requiere Fase 1 como referencia
|
||||
4. Fase 4 (docs) → última, cuando todo esté estable
|
||||
|
||||
## Decisiones de diseño pendientes
|
||||
- ¿El template debería ser un config.yaml real parseable o solo documentación con comentarios?
|
||||
→ Recomendación: real parseable con `template: true`, así sirve como test de que el schema está completo
|
||||
- ¿Añadir un comando `agentctl validate <config.yaml>` para verificar conformidad?
|
||||
→ Nice-to-have, se puede añadir en una fase posterior
|
||||
@@ -123,7 +123,7 @@ tools:
|
||||
tools: []
|
||||
|
||||
memory:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
||||
knowledge:
|
||||
enabled: true
|
||||
@@ -132,7 +132,7 @@ tools:
|
||||
# MEMORIA — ventana de conversación + hechos
|
||||
# ============================================
|
||||
memory:
|
||||
enabled: true
|
||||
enabled: false
|
||||
window_size: 30
|
||||
|
||||
# ============================================
|
||||
|
||||
Reference in New Issue
Block a user