Spaces:
Paused
Paused
| /** | |
| * 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 }; | |
| } | |