Spaces:
Running
Running
File size: 6,082 Bytes
b30b7c5 | 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 | // Modo Objetivo (🎯 Goal): mismo patrón planificador/ejecutor que clonagent
// (~/work2026/clonagent) — un PLANIFICADOR descompone el objetivo en una
// lista de tareas con estado (pending/in-progress/done/failed/skipped), y un
// EJECUTOR las va ejecutando una a una hasta que no queda ninguna pendiente,
// marcando el resultado de cada una. Si una tarea falla, las siguientes se
// saltan (cascada) en vez de seguir a ciegas — igual que el fallo→skip de
// dependencias de clonagent.
//
// Aquí el "ejecutor" de cada tarea es el MISMO bucle de tool-calling que ya
// usa el chat normal (agent.handle): cada tarea del plan se manda como si
// fuera un mensaje de usuario, con todo el historial previo como contexto —
// así una tarea puede apoyarse en lo que hizo la anterior, igual que la
// sesión del ejecutor de clonagent tiene a la vista el fichero de tareas
// entero.
import { toolHelp, snapshot } from './tools/index.js';
const MAX_TASK_RETRIES = 2;
const TASK_PREFIX = '[tarea-objetivo] ';
function plannerPrompt(goalText, context) {
return `ROL: PLANIFICADOR
Objetivo del usuario: "${goalText}"
CONTEXTO AHORA (estado real del IDE):
${context}
Herramientas disponibles para la fase de EJECUCIÓN (aquí solo planificas, no las llames):
${toolHelp()}
Descompón el objetivo en una lista de tareas CONCRETAS, ORDENADAS y ejecutables una tras otra con esas herramientas. Entre 2 y 6 tareas normalmente basta — no fragmentes de más, cada tarea debe ser un paso real con sentido propio.
Responde SOLO con este JSON, nada de texto ni fences alrededor:
{"plan": "una frase resumiendo el enfoque", "tasks": [{"title": "título corto", "description": "qué hay que hacer, concreto y accionable"}]}`;
}
// Extrae el primer objeto JSON balanceado del texto (el modelo a veces mete
// prosa alrededor pese a la instrucción de "SOLO JSON").
function extractJson(text) {
const start = text.indexOf('{');
if (start < 0) return null;
let depth = 0, inStr = false, esc = false;
for (let i = start; i < text.length; i++) {
const ch = text[i];
if (esc) { esc = false; continue; }
if (ch === '\\') { esc = true; continue; }
if (ch === '"') inStr = !inStr;
else if (!inStr) {
if (ch === '{') depth++;
else if (ch === '}' && --depth === 0) {
try { return JSON.parse(text.slice(start, i + 1)); } catch { return null; }
}
}
}
return null;
}
// Llamada ÚNICA al modelo con el rol de planificador — no toca agent.history
// (el plan resultante se guarda aparte; el objetivo en sí SÍ se añade al
// historial en runGoal, para que las tareas puedan apoyarse en él).
export async function planGoal(agent, goalText) {
const context = await snapshot().catch(() => '');
const raw = await agent.provider.chat([{ role: 'user', content: goalText }], plannerPrompt(goalText, context), () => {});
const parsed = extractJson(raw);
if (!parsed || !Array.isArray(parsed.tasks) || !parsed.tasks.length) {
throw new Error('el planificador no devolvió una lista de tareas válida');
}
return {
planText: String(parsed.plan || '').trim(),
tasks: parsed.tasks.slice(0, 10).map(t => ({
title: String(t.title || 'tarea').trim(),
description: String(t.description || t.title || '').trim(),
})),
};
}
function clonePlan(plan) { return JSON.parse(JSON.stringify(plan)); }
// Orquesta plan → ejecución. onEvent es el MISMO canal que usa el chat
// normal (conversations.js lo conecta a onConvEvent) — los eventos de las
// tareas (token/tool/tool_result/text/error) se reenvían tal cual, y además
// se emiten 'plan' / 'plan_update' / 'plan_complete' para la tarjeta de plan.
export async function runGoal(conv, goalText, onEvent) {
conv.agent.history.push({ role: 'user', content: TASK_PREFIX + 'Objetivo: ' + goalText });
const plan = {
id: 'g' + Date.now().toString(36), goal: goalText, planText: '', status: 'planning',
tasks: [], createdAt: Date.now(), updatedAt: Date.now(),
};
conv.plan = plan;
onEvent({ type: 'plan', plan: clonePlan(plan) });
let parsed;
try {
parsed = await planGoal(conv.agent, goalText);
} catch (e) {
plan.status = 'failed';
plan.updatedAt = Date.now();
onEvent({ type: 'plan_update', plan: clonePlan(plan) });
onEvent({ type: 'error', text: 'No pude planificar el objetivo: ' + e.message });
return;
}
plan.planText = parsed.planText;
plan.tasks = parsed.tasks.map((t, i) => ({
id: 't' + (i + 1), title: t.title, description: t.description,
status: 'pending', retries: 0, result: '',
}));
plan.status = 'running';
plan.updatedAt = Date.now();
onEvent({ type: 'plan_update', plan: clonePlan(plan) });
let cascadeFail = false;
for (const task of plan.tasks) {
if (cascadeFail) { task.status = 'skipped'; plan.updatedAt = Date.now(); onEvent({ type: 'plan_update', plan: clonePlan(plan) }); continue; }
task.status = 'in-progress';
plan.updatedAt = Date.now();
onEvent({ type: 'plan_update', plan: clonePlan(plan) });
const taskPrompt = `${TASK_PREFIX}Tarea ${task.id} del objetivo «${goalText}»: ${task.title}\n${task.description}`;
let done = false;
while (!done && task.retries <= MAX_TASK_RETRIES) {
let sawError = false, errText = '';
await conv.agent.handle(taskPrompt, ev => {
if (ev.type === 'error') { sawError = true; errText = ev.text; }
onEvent(ev);
});
if (!sawError) { done = true; }
else {
task.retries++;
task.result = errText;
if (task.retries > MAX_TASK_RETRIES) break;
}
}
task.status = done ? 'done' : 'failed';
plan.updatedAt = Date.now();
onEvent({ type: 'plan_update', plan: clonePlan(plan) });
if (!done) cascadeFail = true; // el resto se saltan, igual que la cascada de dependencias fallidas en clonagent
}
plan.status = plan.tasks.some(t => t.status === 'failed') ? 'failed' : 'done';
plan.updatedAt = Date.now();
onEvent({ type: 'plan_complete', plan: clonePlan(plan) });
}
export { TASK_PREFIX };
|