zelin-bot / src /coordinator.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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 };
}