File size: 10,783 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
 * 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 };
}