/** * coordinator.js — Agente Coordinador de Zelin * ============================================= * Basado en arquitectura real de Manus AI (2025): * * "Manus uses multi-agent with a planner that assigns tasks, a knowledge manager * that reviews conversations and determines what should be saved in the filesystem, * and an executor sub-agent that performs tasks assigned by the planner." * — Lance Martin, LangChain (webinar con Peak Ji de Manus, Oct 2025) * * "Don't over-anthropomorphize your agents. You don't need an 'Org Chart' of agents * (Manager, Designer, Coder) that chat with each other. The primary goal of sub-agents * is to ISOLATE CONTEXT." * — Manus context engineering, Part 2 * * LO QUE HACE: * 1. PLANNER — Para tareas complejas: descompone en pasos antes de ejecutar * 2. ROUTER — Clasifica la intención y elige el handler más adecuado * 3. KNOWLEDGE MANAGER — Decide qué recordar de cada conversación (sin gastar tokens) * 4. ERROR PRESERVATION — Guarda errores en contexto (Manus: "erasing failure removes evidence") * * PRINCIPIO CLAVE: sub-agentes = AISLAMIENTO DE CONTEXTO, no org-chart de roles */ import { callAI, callAIBackground } from './ai.js'; import { isLocalAIReady, ollamaChatDirect } from './local-ai.js'; import * as db from './db.js'; // ═══════════════════════════════════════════════════════════════════════════════ // 1. PLANNER — Descomponer tareas complejas en pasos // ═══════════════════════════════════════════════════════════════════════════════ // Manus descubrió que gastar un 5% de tokens en planning ahorra 30%+ en ejecución // "A better pattern is a specific Planner sub-agent that returns a structured Plan object" export async function planTask(goal, context = '') { // El planner tiene su PROPIO contexto aislado — no hereda el historial de chat const plannerMessages = [ { role : 'system', content: `Eres un planificador de tareas para Zelin, bot de TomateSMP. Tu trabajo: descomponer una tarea compleja en pasos concretos y ejecutables. Reglas: - Máximo 5 pasos (más = complejidad innecesaria) - Cada paso debe ser verificable (¿cómo sé que terminó?) - Sé específico: no "buscar info" sino "buscar en la DB de jugadores por username X" - Identifica qué herramientas necesita cada paso - Si la tarea es simple (1 paso), responde directamente sin planificar Responde SOLO JSON: { "simple": true/false, "steps": [ {"id": 1, "action": "qué hacer exactamente", "tool": "qué tool usar o null", "verify": "cómo verificar"} ], "expected_output": "qué debería producir este plan" }`, }, { role : 'user', content: `Tarea: ${goal}\n${context ? `Contexto disponible: ${context}` : ''}`, }, ]; try { // Planner usa modelo fast con su propio contexto aislado let raw; if (isLocalAIReady()) { raw = await ollamaChatDirect(plannerMessages, 300, 10000); } else { raw = await callAIBackground(plannerMessages, 'fast', 300); } return JSON.parse(raw.replace(/```json|```/g, '').trim()); } catch { return { simple: true, steps: [] }; // si falla planning, ejecutar directo } } // ═══════════════════════════════════════════════════════════════════════════════ // 2. ROUTER — Clasificar intención y elegir el modo de respuesta óptimo // ═══════════════════════════════════════════════════════════════════════════════ const ROUTE_PATTERNS = { // Respuesta directa sin tool use — patterns de Manus "atomic" level DIRECT : /^(hola|hey|gg|xd|jaja|ok|gracias|sí|no|bien|mal|\?|!|👋|😂|lol)/i, // Necesita buscar en la DB de Zelin DB_QUERY : /cuántos|cuándo|historial|ban|sanción|jugador.+(fue|tiene|hizo|está)/i, // Tarea multi-paso que necesita planning COMPLEX : /crea|genera|analiza|investiga|recopila|haz un resumen|explica detalladamente|compara/i, // Pregunta sobre el servidor SERVER : /ip|cómo entro|cómo me registro|cómo juego|dónde está|qué es|para qué/i, }; export function routeRequest(message, hasTools = false) { const m = message?.trim() ?? ''; // Mensajes muy cortos → respuesta directa siempre if (m.length < 20) return { mode: 'direct', reason: 'short_message' }; // Patrones directos if (ROUTE_PATTERNS.DIRECT.test(m)) return { mode: 'direct', reason: 'casual' }; if (ROUTE_PATTERNS.DB_QUERY.test(m)) return { mode: 'tool_use', reason: 'needs_db' }; if (ROUTE_PATTERNS.COMPLEX.test(m) && hasTools) return { mode: 'planned', reason: 'complex_task' }; if (ROUTE_PATTERNS.SERVER.test(m)) return { mode: 'rag_enhanced', reason: 'server_info' }; return { mode: 'standard', reason: 'default' }; } // ═══════════════════════════════════════════════════════════════════════════════ // 3. KNOWLEDGE MANAGER — Decidir qué recordar de cada conversación // ═══════════════════════════════════════════════════════════════════════════════ // Manus: "a knowledge manager that reviews conversations and determines // what should be saved in the filesystem" // Para Zelin: determinar qué hechos del usuario guardar en memoria export async function extractKnowledge(userId, userMessage, zelinResponse) { // Solo extraer si hay algo potencialmente valioso const worthExtracting = /me llamo|soy|mi|tengo|juego|rango|casa|coords|discord/i.test(userMessage); if (!worthExtracting) return null; try { const raw = await callAIBackground([ { role : 'system', content: `Extrae hechos CONCRETOS sobre el usuario de esta conversación para recordarlos en el futuro. Solo hechos objetivos (no opiniones). Si no hay nada útil, devuelve null. Responde SOLO JSON o null: {"facts": ["hecho 1", "hecho 2"], "summary": "resumen en 1 frase"}`, }, { role : 'user', content: `Usuario dijo: "${userMessage.slice(0, 300)}"\nZelin respondió: "${zelinResponse.slice(0, 200)}"`, }, ], 'fast', 150); const clean = raw.replace(/```json|```/g, '').trim(); if (clean === 'null' || clean === '') return null; const knowledge = JSON.parse(clean); // Persistir en DB if (knowledge?.facts?.length) { const existing = await db.memGet(`knowledge.user.${userId}`) ?? { facts: [], updatedAt: null }; existing.facts = [...new Set([...existing.facts, ...knowledge.facts])].slice(-20); // max 20 hechos existing.updatedAt = new Date().toISOString(); await db.memSet(`knowledge.user.${userId}`, existing, 'user_knowledge'); } return knowledge; } catch { return null; } } export async function getUserKnowledge(userId) { return await db.memGet(`knowledge.user.${userId}`) ?? null; } // ═══════════════════════════════════════════════════════════════════════════════ // 4. ERROR PRESERVATION — Guardar errores en contexto (principio Manus) // ═══════════════════════════════════════════════════════════════════════════════ // "Erasing failure removes evidence. Retaining errors allows the model to reason // about why something failed and how to adapt." // — Manus AI context engineering blog const _errorLog = new Map(); // channelId → [{ error, context, ts }] export function preserveError(channelId, error, context = '') { const log = _errorLog.get(channelId) ?? []; log.push({ error: error.slice(0, 300), context: context.slice(0, 200), ts: Date.now() }); // Mantener últimos 3 errores por canal (no saturar) if (log.length > 3) log.shift(); _errorLog.set(channelId, log); } // Inyectar errores previos en el contexto para que la IA aprenda de ellos export function getErrorContext(channelId) { const log = _errorLog.get(channelId); if (!log?.length) return ''; const recent = log.filter(e => Date.now() - e.ts < 30 * 60_000); // últimos 30 min if (!recent.length) return ''; return '\n\n[Errores recientes en este canal — aprende de ellos]:\n' + recent.map(e => `- ${e.error}`).join('\n'); } // ═══════════════════════════════════════════════════════════════════════════════ // 5. GOAL RECITATION — Anti-lost-in-middle (técnica de Manus con todo.md) // ═══════════════════════════════════════════════════════════════════════════════ // Manus: "By constantly rewriting the todo list, Manus is reciting its objectives // into the END of the context. This pushes the global plan into the model's // recent attention span." // Para Zelin: en tareas multi-paso, añadir el objetivo al FINAL del último mensaje export function injectGoalRecitation(messages, currentGoal) { if (!currentGoal || !messages.length) return messages; const last = messages[messages.length - 1]; if (last.role !== 'user') return messages; // Añadir recordatorio del objetivo al final del último mensaje del usuario const enriched = [...messages]; enriched[enriched.length - 1] = { ...last, content: last.content + `\n\n[Recordatorio del objetivo actual]: ${currentGoal}`, }; return enriched; } export function coordinatorStats() { const channels = [..._errorLog.keys()].length; const totalErrors = [..._errorLog.values()].reduce((s, log) => s + log.length, 0); return { channels_tracked: channels, errors_preserved: totalErrors }; }