xonecode 0.8.46 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/apps/web/dist/assets/index-BAyybSz-.css +1 -0
- package/apps/web/dist/assets/index-BNCMVjWv.js +485 -0
- package/apps/web/dist/assets/{php-Bx1jVads.js → php-DPbDT8rX.js} +1 -1
- package/apps/web/dist/assets/{ruby-L0I9KR6K.js → ruby-Bfvws9R8.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/openui/visor.css +1 -0
- package/apps/web/dist/openui/visor.js +9113 -0
- package/dist/agent/exportarPdf.js +5 -1
- package/dist/agent/exportarPdf.js.map +1 -1
- package/dist/agent/grafo/arbolDeProyecto.js +67 -2
- package/dist/agent/grafo/arbolDeProyecto.js.map +1 -1
- package/dist/agent/grafo/memoriaDeProyecto.js +3 -2
- package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -1
- package/dist/agent/grafo/navegacionXone.js +8 -3
- package/dist/agent/grafo/navegacionXone.js.map +1 -1
- package/dist/agent/grafo/opcionesDeFicheros.js +24 -0
- package/dist/agent/grafo/opcionesDeFicheros.js.map +1 -0
- package/dist/agent/grafo/perfiles.js +1 -1
- package/dist/agent/grafo/perfiles.js.map +1 -1
- package/dist/agent/grafo/proyecto.js +31 -15
- package/dist/agent/grafo/proyecto.js.map +1 -1
- package/dist/agent/grafo/unirSecciones.js +128 -0
- package/dist/agent/grafo/unirSecciones.js.map +1 -0
- package/dist/agent/grafo/xoneAgent.js +56 -51
- package/dist/agent/grafo/xoneAgent.js.map +1 -1
- package/dist/agent/motores/trueforge/capacidades.js +179 -0
- package/dist/agent/motores/trueforge/capacidades.js.map +1 -0
- package/dist/agent/motores/trueforge/eventosTrueforge.js +143 -0
- package/dist/agent/motores/trueforge/eventosTrueforge.js.map +1 -0
- package/dist/agent/motores/trueforge/memoriaTrueforge.js +336 -0
- package/dist/agent/motores/trueforge/memoriaTrueforge.js.map +1 -0
- package/dist/agent/motores/trueforge/metricasTrueforge.js +55 -0
- package/dist/agent/motores/trueforge/metricasTrueforge.js.map +1 -0
- package/dist/agent/motores/trueforge/modeloExterno.js +46 -0
- package/dist/agent/motores/trueforge/modeloExterno.js.map +1 -0
- package/dist/agent/motores/trueforge/modeloLangchain.js +152 -0
- package/dist/agent/motores/trueforge/modeloLangchain.js.map +1 -0
- package/dist/agent/motores/trueforge/recortes.js +121 -0
- package/dist/agent/motores/trueforge/recortes.js.map +1 -0
- package/dist/agent/motores/trueforge/sesionTrueforge.js +1062 -0
- package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -0
- package/dist/agent/motores/trueforge/skillsTrueforge.js +14 -0
- package/dist/agent/motores/trueforge/skillsTrueforge.js.map +1 -0
- package/dist/agent/motores/trueforge/toolsDeFichero.js +265 -0
- package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -0
- package/dist/agent/motores/trueforge/toolsPropias.js +65 -0
- package/dist/agent/motores/trueforge/toolsPropias.js.map +1 -0
- package/dist/agent/motores/trueforge/trueforge.js +45 -0
- package/dist/agent/motores/trueforge/trueforge.js.map +1 -0
- package/dist/agent/navegacion/indiceEnDisco.js +10 -1
- package/dist/agent/navegacion/indiceEnDisco.js.map +1 -1
- package/dist/agent/planesEnDisco.js +64 -0
- package/dist/agent/planesEnDisco.js.map +1 -0
- package/dist/agent/sesiones/memoriaDeHilo.js +24 -0
- package/dist/agent/sesiones/memoriaDeHilo.js.map +1 -0
- package/dist/agent/sesiones/modeloDelCambio.js +70 -0
- package/dist/agent/sesiones/modeloDelCambio.js.map +1 -0
- package/dist/agent/sesiones/sesionGit.js +14 -0
- package/dist/agent/sesiones/sesionGit.js.map +1 -1
- package/dist/agent/subagentes/actividadDeCodex.js +60 -0
- package/dist/agent/subagentes/actividadDeCodex.js.map +1 -0
- package/dist/agent/subagentes/agentesEnDisco.js +95 -5
- package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
- package/dist/agent/subagentes/escrituraExterna.js +10 -2
- package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
- package/dist/agent/subagentes/subagenteCodex.js +20 -0
- package/dist/agent/subagentes/subagenteCodex.js.map +1 -1
- package/dist/agent/subagentes/subagenteExterno.js +269 -202
- package/dist/agent/subagentes/subagenteExterno.js.map +1 -1
- package/dist/agent/subagentes/subagenteOpencode.js +19 -2
- package/dist/agent/subagentes/subagenteOpencode.js.map +1 -1
- package/dist/agent/turno/diagnosticoDeTools.js +3 -0
- package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
- package/dist/agent/turno/ficherosDelProyecto.js +62 -0
- package/dist/agent/turno/ficherosDelProyecto.js.map +1 -0
- package/dist/agent/turno/informeDeTraza.js +28 -1
- package/dist/agent/turno/informeDeTraza.js.map +1 -1
- package/dist/agent/turno/puente.js +17 -1
- package/dist/agent/turno/puente.js.map +1 -1
- package/dist/agent/turno/resumenDeContexto.js +17 -4
- package/dist/agent/turno/resumenDeContexto.js.map +1 -1
- package/dist/agent/turno/resumenDeTool.js +3 -0
- package/dist/agent/turno/resumenDeTool.js.map +1 -1
- package/dist/agent/turno/sesionReal.js +2 -0
- package/dist/agent/turno/sesionReal.js.map +1 -0
- package/dist/agent/turno/turnoReal.js +55 -164
- package/dist/agent/turno/turnoReal.js.map +1 -1
- package/dist/agent/turno/verificacion.js +142 -0
- package/dist/agent/turno/verificacion.js.map +1 -0
- package/dist/cli/config.js +19 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/consola.js.map +1 -1
- package/dist/cli/main.js +35 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui/correrTui.js +1 -1
- package/dist/cli/tui/correrTui.js.map +1 -1
- package/dist/cli/tui/transcript.js +9 -0
- package/dist/cli/tui/transcript.js.map +1 -1
- package/dist/core/actos.js.map +1 -1
- package/dist/core/agentes.js +17 -0
- package/dist/core/agentes.js.map +1 -1
- package/dist/core/artefactos.js +22 -0
- package/dist/core/artefactos.js.map +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/diffDeColecciones.js +84 -0
- package/dist/core/diffDeColecciones.js.map +1 -0
- package/dist/core/exportacion.js +6 -1
- package/dist/core/exportacion.js.map +1 -1
- package/dist/core/fotoDeColecciones.js +48 -0
- package/dist/core/fotoDeColecciones.js.map +1 -0
- package/dist/core/imagenesDeDocumento.js +88 -0
- package/dist/core/imagenesDeDocumento.js.map +1 -0
- package/dist/core/memoria.js +24 -0
- package/dist/core/memoria.js.map +1 -0
- package/dist/core/motor.js +41 -0
- package/dist/core/motor.js.map +1 -0
- package/dist/core/navegacion.js.map +1 -1
- package/dist/core/notify.js +23 -5
- package/dist/core/notify.js.map +1 -1
- package/dist/core/ports.js.map +1 -1
- package/dist/core/serieDeEscrituras.js +13 -0
- package/dist/core/serieDeEscrituras.js.map +1 -1
- package/dist/core/settings.js +17 -1
- package/dist/core/settings.js.map +1 -1
- package/dist/core/tareasDelPlan.js +103 -0
- package/dist/core/tareasDelPlan.js.map +1 -0
- package/dist/core/trazaDeErrores.js +42 -3
- package/dist/core/trazaDeErrores.js.map +1 -1
- package/dist/core/turno.js +44 -4
- package/dist/core/turno.js.map +1 -1
- package/dist/web/servidor/arranque.js +204 -5
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/pielWeb.js +69 -4
- package/dist/web/servidor/pielWeb.js.map +1 -1
- package/dist/web/servidor/sesiones.js +11 -0
- package/dist/web/servidor/sesiones.js.map +1 -1
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +23 -2
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/dist/web/servidor/visorOpenui.js +63 -0
- package/dist/web/servidor/visorOpenui.js.map +1 -0
- package/package.json +7 -2
- package/skills/openui-builder/SKILL.md +48 -0
- package/apps/web/dist/assets/index-GRwyotMV.js +0 -478
- package/apps/web/dist/assets/index-_cBVqq54.css +0 -1
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Una `SesionReal` con el motor de TrueForge debajo — la Fase 0 de
|
|
3
|
+
* `docs/VARIANTE-TRUEFORGE-HARNESS.md`.
|
|
4
|
+
*
|
|
5
|
+
* Cumple el MISMO contrato que la de deepagents (`turno/sesionReal.ts`), así que la
|
|
6
|
+
* consola —web, terminal, tareas— no sabe con cuál habla: los dos pintan eventos de dominio en la
|
|
7
|
+
* misma `Piel`, aprueban por el mismo `pedirAprobacion` y devuelven los mismos `cambios`.
|
|
8
|
+
*
|
|
9
|
+
* **Lo que hay, y lo que NO, dicho entero**:
|
|
10
|
+
* - SÍ: el raíz como ORQUESTADOR de solo lectura y sin skills, y cada especialista como hijo
|
|
11
|
+
* sacado de su `.md` (prompt, skills, permisos, modelo; la shell solo quien la declara), sobre
|
|
12
|
+
* nuestro backend (`toolsDeFichero.ts`) y nuestro modelo (`modeloLangchain.ts`); las tools
|
|
13
|
+
* propias adaptadas (`toolsPropias.ts`: `xone_navegacion`, `regex_search`, la crítica visual…);
|
|
14
|
+
* los recortes de deepagents y la compactación del raíz (`recortes.ts`); la aprobación de cada
|
|
15
|
+
* escritura con su diff, devuelta al hilo que la pidió; el verificador con su reparación, con
|
|
16
|
+
* las reglas compartidas de `turno/verificacion.ts`; el modo autónomo, los artefactos sin
|
|
17
|
+
* preguntar, la cancelación, los tokens y los cambios del turno.
|
|
18
|
+
* Y la memoria en disco: la foto del raíz se guarda al final de cada turno y reabrir la sesión
|
|
19
|
+
* continúa la conversación (`memoriaTrueforge.ts`).
|
|
20
|
+
* Y además: los hechos del proyecto delante de cada turno, el juez del turno y el crítico de
|
|
21
|
+
* pantalla enganchados al final, la pregunta del orquestador también como dato para la tarjeta,
|
|
22
|
+
* y Claude Code, Codex y OpenCode como hijos por el mismo puerto que deepagents
|
|
23
|
+
* (`modeloExterno.ts`).
|
|
24
|
+
* - NO: un presupuesto GLOBAL por turno (la suma de todos los hilos), que deepagents tampoco tiene.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import winston from "winston";
|
|
29
|
+
import { AgentThread, AgentThreadOrchestrator, EventType, NOOP_AGENT_TRACING, askUserQuestion, contextCompaction, dynamicSubAgents } from "./trueforge.js";
|
|
30
|
+
import { TOPE_DE_LLAMADAS_DEL_CONDUCTOR, TOPE_DE_LLAMADAS_DEL_ESPECIALISTA, UMBRAL_RESUMEN_TOKENS } from "../../turno/resumenDeContexto.js";
|
|
31
|
+
import { crearSubagenteExterno } from "../../subagentes/subagenteExterno.js";
|
|
32
|
+
import { inventarioDelProyecto, opcionesDeSubagenteExterno } from "../../subagentes/escrituraExterna.js";
|
|
33
|
+
import { sumarConsumo, SIN_CONSUMO } from "../../subagentes/consumoExterno.js";
|
|
34
|
+
import { ColaDeEventos, entrelazar } from "../../../core/entrelazar.js";
|
|
35
|
+
import { modeloExternoParaTrueforge } from "./modeloExterno.js";
|
|
36
|
+
import { diferenciasDelContraste, metricasDeTrueforge } from "./metricasTrueforge.js";
|
|
37
|
+
import { cambiosQueSeVerifican, huellaDeErrores, repartirHallazgos, textoDeReparacion, tocaCriticarPantalla, TOPE_REPARACIONES } from "../../turno/verificacion.js";
|
|
38
|
+
import { correrTurno } from "../../../core/turno.js";
|
|
39
|
+
import { createTokenTracker } from "../../../vendor/tokenTracking.js";
|
|
40
|
+
import { MAX_APPROVAL_ROUNDS } from "../../../vendor/hitl.js";
|
|
41
|
+
import { backendDeAgente, entornoDeLaShellDelProyecto } from "../../grafo/proyecto.js";
|
|
42
|
+
import { cargarAgentes } from "../../subagentes/agentesEnDisco.js";
|
|
43
|
+
import { fichaDeAgente, promptDeAgente, repartirSkills } from "../../../core/agentes.js";
|
|
44
|
+
import { PERFIL_DEL_ORQUESTADOR, promptOrquestador } from "../../grafo/xoneAgent.js";
|
|
45
|
+
import { permisosDe, seDetieneEn, TEXTO_HITL } from "../../grafo/perfiles.js";
|
|
46
|
+
import { cambioDe } from "../../turno/interrupts.js";
|
|
47
|
+
import { tomarInstantanea } from "../../turno/instantanea.js";
|
|
48
|
+
import { ficherosDelProyecto } from "../../turno/ficherosDelProyecto.js";
|
|
49
|
+
import { accionDelJuez } from "../../../core/juezDelTurno.js";
|
|
50
|
+
import { modeloParaTrueforge } from "./modeloLangchain.js";
|
|
51
|
+
import { TOOLS_DE_LECTURA } from "./toolsDeFichero.js";
|
|
52
|
+
import { topeAgotadoDe, traducirEvento } from "./eventosTrueforge.js";
|
|
53
|
+
import { anuncioDeSkills } from "./skillsTrueforge.js";
|
|
54
|
+
import { capabilitiesDe, capacidadDeFecha, capacidadDeFicheros, capacidadDeInstrucciones, capacidadDePropias, capacidadDeRecortes, capacidadesDelEspecialista, clasesDeTools, toolsDe, } from "./capacidades.js";
|
|
55
|
+
import { crearDiagnosticoDeTools } from "../../turno/diagnosticoDeTools.js";
|
|
56
|
+
import { detalleDe, parametrosDe } from "../../turno/resumenDeTool.js";
|
|
57
|
+
import { apartarMemoria, cargarMemoria, fotoSaneada, guardarMemoria, textoDeMemoriaDescartada } from "./memoriaTrueforge.js";
|
|
58
|
+
import { crearNavegacionXone } from "../../grafo/navegacionXone.js";
|
|
59
|
+
import { hechosDelProyectoDe } from "../../navegacion/hechosEnDisco.js";
|
|
60
|
+
import { conHechosDelProyecto } from "../../../core/hechosDelProyecto.js";
|
|
61
|
+
import { crearBusquedaRegex } from "../../grafo/busquedaRegex.js";
|
|
62
|
+
import { crearCopiarArtefacto } from "../../grafo/copiarArtefacto.js";
|
|
63
|
+
import { crearUnirSecciones } from "../../grafo/unirSecciones.js";
|
|
64
|
+
import { crearCriticaVisual } from "../../grafo/criticaVisual.js";
|
|
65
|
+
import { crearTraerDeLaMaquina } from "../../grafo/traerDeLaMaquina.js";
|
|
66
|
+
import { invocarVisualConModelos } from "../../dispositivos/juezVisual.js";
|
|
67
|
+
import { estilosDeDisco, indiceEnDisco } from "../../navegacion/indiceEnDisco.js";
|
|
68
|
+
/** El hilo raíz de TrueForge. Se llama así en la librería y no se elige. */
|
|
69
|
+
const HILO_RAIZ = "main";
|
|
70
|
+
/**
|
|
71
|
+
* Lo que el orquestador tiene que saber para DELEGAR en ESTE motor. Su prompt es el de siempre
|
|
72
|
+
* (`promptOrquestador`), que habla de `task` y de la ficha que va en su descripción: aquí la
|
|
73
|
+
* delegación es `create_sub_agent` y no tiene descripción por especialista, así que se traduce
|
|
74
|
+
* el nombre de la tool y las fichas van escritas aquí, con la MISMA función que deepagents.
|
|
75
|
+
*/
|
|
76
|
+
export function notaDeDelegacion(agentes) {
|
|
77
|
+
if (agentes.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
return [
|
|
80
|
+
"NOTA DEL HARNESS: en este entorno NO existe la tool `task`. Se delega con `create_sub_agent`:",
|
|
81
|
+
"`name` es EXACTAMENTE el nombre del especialista y `input` el encargo, autosuficiente —el",
|
|
82
|
+
"especialista no ve esta conversación—. Donde estas instrucciones dicen `task` o `subagent_type`,",
|
|
83
|
+
"entiende `create_sub_agent` y `name`. Las fichas de los especialistas:",
|
|
84
|
+
...agentes.map((a) => `- ${a.nombre}: ${fichaDeAgente(a)}`),
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
/** Lo que cada clase NO puede hacer, que la lista de tools no dice sola. */
|
|
88
|
+
const LIMITES_DE = {
|
|
89
|
+
ejecuta: "No puedes escribir ficheros del proyecto.",
|
|
90
|
+
lee: "Solo puedes escribir en `/artefactos/` y `/planes/`: el proyecto no lo puedes tocar, y no puedes ejecutar comandos.",
|
|
91
|
+
escribe: "Cada escritura la aprueba una persona viendo su diff, y no puedes ejecutar comandos.",
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* La frase de las tools que TIENE, sacada de los nombres que de verdad se le montan: escrita a
|
|
95
|
+
* mano se quedaba vieja en cuanto se añadía una —la nota decía cuatro y el modelo veía seis—.
|
|
96
|
+
*/
|
|
97
|
+
export function notaDeTools(nombres, limite) {
|
|
98
|
+
return `NOTA DEL HARNESS: aunque tu identidad diga lo contrario, NO tienes las mismas tools que quien te delega. Tienes ${nombres
|
|
99
|
+
.map((n) => `\`${n}\``)
|
|
100
|
+
.join(", ")}. ${limite}`;
|
|
101
|
+
}
|
|
102
|
+
/** Un nombre que no es de ningún especialista: lectura a secas, sin escribir en ningún sitio. */
|
|
103
|
+
const LIMITE_DEL_GENERICO = "No puedes escribir ficheros ni ejecutar comandos.";
|
|
104
|
+
/**
|
|
105
|
+
* La pregunta del orquestador, como TEXTO del chat: la pregunta y sus opciones numeradas. Se pinta
|
|
106
|
+
* en la respuesta y no en un diálogo, a propósito: así llega igual a la web, a la TUI y al terminal
|
|
107
|
+
* sin tocar ninguna piel, y la persona contesta como contesta siempre, escribiendo.
|
|
108
|
+
*/
|
|
109
|
+
export function textoDePregunta(args) {
|
|
110
|
+
const { pregunta, opciones } = consultaDe(args);
|
|
111
|
+
return [
|
|
112
|
+
"\n\n" + pregunta,
|
|
113
|
+
...(opciones.length === 0 ? [] : ["", ...opciones.map((o, i) => `${i + 1}. ${o}`), "", "Contesta con el número o con tus palabras."]),
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* La pregunta y sus opciones como DATO (`events.ts#consulta`), de los argumentos que mandó el
|
|
118
|
+
* modelo: una sola lectura para el texto, la tarjeta y la respuesta, o las tres divergirían.
|
|
119
|
+
*/
|
|
120
|
+
export function consultaDe(args) {
|
|
121
|
+
return {
|
|
122
|
+
pregunta: typeof args.question === "string" ? args.question.trim() : "",
|
|
123
|
+
opciones: Array.isArray(args.options) ? args.options.filter((o) => typeof o === "string") : [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Lo que escribió la persona, como respuesta: un número de opción se traduce a su texto. */
|
|
127
|
+
export function respuestaAPregunta(args, escrito) {
|
|
128
|
+
const { opciones } = consultaDe(args);
|
|
129
|
+
const n = /^\s*(\d+)\s*[.)]?\s*$/.exec(escrito);
|
|
130
|
+
const elegida = n === null ? undefined : opciones[Number(n[1]) - 1];
|
|
131
|
+
return elegida ?? escrito;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* El tope de llamadas del ORQUESTADOR en un turno. deepagents no le pone ninguno —no puede quedarse
|
|
135
|
+
* a medias— y TrueForge exige un número (su omisión, 25, es la del core); 100 es la omisión de su
|
|
136
|
+
* propio `AgentSpec`. Es POR TURNO porque el raíz se rehace desde su foto al acabar cada uno.
|
|
137
|
+
*/
|
|
138
|
+
export const LIMITE_DE_LLAMADAS_DEL_RAIZ = 100;
|
|
139
|
+
/** La pregunta guardada en la foto, en la forma con que la espera el turno. */
|
|
140
|
+
function preguntaDeLaFoto(foto) {
|
|
141
|
+
const p = foto?.pregunta_pendiente;
|
|
142
|
+
if (p === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
return {
|
|
145
|
+
clave: claveDe(p.hilo, p.id),
|
|
146
|
+
hilo: p.hilo,
|
|
147
|
+
id: p.id,
|
|
148
|
+
nombre: "ask_user_question",
|
|
149
|
+
args: p.args,
|
|
150
|
+
...(p.encargo === undefined ? {} : { encargo: p.encargo }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const claveDe = (hilo, id) => `${hilo}:${id}`;
|
|
154
|
+
export async function abrirSesionTrueforge(opciones) {
|
|
155
|
+
const { raiz } = opciones;
|
|
156
|
+
let modelos = opciones.modelos;
|
|
157
|
+
const logger = winston.createLogger({ silent: true, transports: [] });
|
|
158
|
+
/**
|
|
159
|
+
* La traza de tools (`XONECODE_TRACE_TOOLS=1`), la MISMA de deepagents: mismo fichero y mismo
|
|
160
|
+
* formato, así que `xonecode traza` compara los dos motores. La omisión es la real —entrar por
|
|
161
|
+
* parámetro solo sirve para doblarla—, que es lo que evita dejarla escrita y sin montar.
|
|
162
|
+
*/
|
|
163
|
+
const diagnostico = opciones.diagnostico ?? crearDiagnosticoDeTools(raiz);
|
|
164
|
+
const tracker = createTokenTracker();
|
|
165
|
+
const oyentes = new Set();
|
|
166
|
+
let aborto;
|
|
167
|
+
/** Se pidió cancelar ESTE turno. Hace falta además del `aborto`: entre dos rondas —mientras
|
|
168
|
+
* se espera una aprobación— no hay llamada en curso que abortar, y la ronda siguiente
|
|
169
|
+
* abriría un control nuevo y se llevaría la cancelación por delante. */
|
|
170
|
+
let cancelado = false;
|
|
171
|
+
let cerrada = false;
|
|
172
|
+
let hilo = opciones.hilo ?? `tf-${Date.now()}`;
|
|
173
|
+
const catalogo = opciones.skills;
|
|
174
|
+
const disponibles = new Set(catalogo.map((s) => s.nombre));
|
|
175
|
+
/** De quién es cada hilo, para que la tarjeta de aprobación diga QUIÉN quiere escribir. */
|
|
176
|
+
const quienEs = new Map([[HILO_RAIZ, PERFIL_DEL_ORQUESTADOR.nombre]]);
|
|
177
|
+
/**
|
|
178
|
+
* El especialista CARGADO de cada hilo hijo, para el `origen` de sus tools. No es `quienEs`:
|
|
179
|
+
* ése guarda también el nombre que el modelo puso al delegar aunque no sea de nadie, y lo que
|
|
180
|
+
* viaja por el cable como nombre de un especialista tiene que serlo de verdad.
|
|
181
|
+
*/
|
|
182
|
+
const especialistaDeHilo = new Map();
|
|
183
|
+
/** Lo que el agente dejó en `/artefactos/` y aún no se ha anunciado: un artefacto se escribe
|
|
184
|
+
* SIN aprobación, así que tiene que ANUNCIARSE, como en deepagents. */
|
|
185
|
+
const artefactosPorAnunciar = [];
|
|
186
|
+
/** Las IMÁGENES que dejó el turno en curso, para el crítico de pantalla. Se vacía al empezar
|
|
187
|
+
* cada turno: una captura de antes enseña la pantalla de antes. */
|
|
188
|
+
let capturasDelTurno = [];
|
|
189
|
+
const anotarArtefacto = (a) => {
|
|
190
|
+
artefactosPorAnunciar.push(a);
|
|
191
|
+
if (a.mime !== undefined && a.mime.startsWith("image/"))
|
|
192
|
+
capturasDelTurno.push(a);
|
|
193
|
+
};
|
|
194
|
+
const montarBackend = (ejecucion) => backendDeAgente({
|
|
195
|
+
raiz,
|
|
196
|
+
ficheros: ficherosDelProyecto(raiz),
|
|
197
|
+
...(opciones.artefactos === undefined
|
|
198
|
+
? {}
|
|
199
|
+
: { artefactos: { carpeta: opciones.artefactos, alEscribir: anotarArtefacto } }),
|
|
200
|
+
...(ejecucion === undefined ? {} : { ejecucion }),
|
|
201
|
+
});
|
|
202
|
+
const backend = montarBackend();
|
|
203
|
+
/**
|
|
204
|
+
* **Los motores EXTERNOS**, por el MISMO puerto que deepagents (`subagenteExterno.ts`), con sus
|
|
205
|
+
* guardas de ruta, su política —la de la sesión, traducida (`politicaExternaDeSesion`)— y su
|
|
206
|
+
* consumo. Lo que el hijo hace MIENTRAS trabaja entra por `eventosExternos` y se entrelaza con el
|
|
207
|
+
* flujo del turno; las rutas que aplica sin preguntar van al aviso del turno por
|
|
208
|
+
* `apuntarAplicadasSinPreguntar`, un puntero de la sesión a una lista del turno: la política se
|
|
209
|
+
* compone una vez y el aviso es de cada turno.
|
|
210
|
+
*/
|
|
211
|
+
const eventosExternos = new ColaDeEventos();
|
|
212
|
+
let consumoExterno = SIN_CONSUMO;
|
|
213
|
+
let apuntarAplicadasSinPreguntar;
|
|
214
|
+
const externo = (opciones.subagenteExterno ?? crearSubagenteExterno)(opcionesDeSubagenteExterno({
|
|
215
|
+
...(opciones.pedirAprobacion === undefined ? {} : { pedirAprobacion: opciones.pedirAprobacion }),
|
|
216
|
+
ficherosDelProyecto: () => ficherosDelProyecto(raiz),
|
|
217
|
+
eventos: eventosExternos,
|
|
218
|
+
alConsumir: (c) => {
|
|
219
|
+
consumoExterno = sumarConsumo(consumoExterno, c);
|
|
220
|
+
avisar();
|
|
221
|
+
},
|
|
222
|
+
modo: {
|
|
223
|
+
sinPreguntar: () => opciones.sinAprobacion?.() === true,
|
|
224
|
+
alAplicarSinPreguntar: (rutas) => apuntarAplicadasSinPreguntar?.(rutas),
|
|
225
|
+
},
|
|
226
|
+
}));
|
|
227
|
+
/**
|
|
228
|
+
* Qué motores externos se pueden usar DE VERDAD, preguntado UNA vez al abrir —como deepagents al
|
|
229
|
+
* construir el grafo—: un especialista que el orquestador elige y que revienta en cuanto lo
|
|
230
|
+
* elige es un botón muerto que pulsa el modelo, y se lo cree.
|
|
231
|
+
*/
|
|
232
|
+
const motoresDisponibles = new Set();
|
|
233
|
+
for (const motor of new Set(cargarAgentes(raiz).agentes.map((a) => a.motor).filter((m) => m !== "modelo"))) {
|
|
234
|
+
if (await externo.disponible(motor))
|
|
235
|
+
motoresDisponibles.add(motor);
|
|
236
|
+
}
|
|
237
|
+
/** Los hilos de un hijo EXTERNO: su «llamada al modelo» no es una llamada, no se cuenta. */
|
|
238
|
+
const hilosExternos = new Set();
|
|
239
|
+
/** Cuántos hijos externos lanzó el turno en curso, para el contraste con las métricas del motor. */
|
|
240
|
+
let externosDelTurno = 0;
|
|
241
|
+
/**
|
|
242
|
+
* **El raíz es el ORQUESTADOR, de solo lectura y SIN skills** — la regla de deepagents
|
|
243
|
+
* (`xoneAgent.ts`): los subagentes no heredan las skills del orquestador, cada uno recibe
|
|
244
|
+
* las de su `.md`. Lee para orientarse y contestar lo que se contesta mirando; todo lo que
|
|
245
|
+
* escribe o ejecuta lo delega. Los de motor externo, solo si su motor está disponible: el
|
|
246
|
+
* prompt del orquestador y la factoría de hijos salen de ESTA lista, así que no puede ofrecer
|
|
247
|
+
* uno que luego no se monte.
|
|
248
|
+
*/
|
|
249
|
+
const especialistas = () => cargarAgentes(raiz).agentes.filter((a) => a.motor === "modelo" || motoresDisponibles.has(a.motor));
|
|
250
|
+
/**
|
|
251
|
+
* **Las tools PROPIAS, con el MISMO reparto que deepagents** (`xoneAgent.ts`): son las mismas
|
|
252
|
+
* funciones, adaptadas (`toolsPropias.ts`), no una copia.
|
|
253
|
+
* - `xone_navegacion` va a TODOS, el orquestador incluido: medido en deepagents, el
|
|
254
|
+
* orquestador no delegaba la pregunta de estructura y sin ella se orientaba con once
|
|
255
|
+
* llamadas de `ls`/`grep`/`read_file`.
|
|
256
|
+
* - `regex_search` a los especialistas.
|
|
257
|
+
* - `copiar_artefacto` solo a quien declara `escribeEn` y con carpeta de artefactos.
|
|
258
|
+
* - La crítica visual y traer de la máquina, solo al ORQUESTADOR y solo con carpeta: es
|
|
259
|
+
* quien reparte y quien lee la ruta que nombra la persona.
|
|
260
|
+
*/
|
|
261
|
+
const ficheros = ficherosDelProyecto(raiz);
|
|
262
|
+
const cargarIndice = opciones.navegacion ?? indiceEnDisco(raiz);
|
|
263
|
+
const cargarEstilos = estilosDeDisco(raiz);
|
|
264
|
+
const navegacion = () => crearNavegacionXone(cargarIndice, ficheros, cargarEstilos);
|
|
265
|
+
const carpeta = opciones.artefactos;
|
|
266
|
+
const propiasDelRaiz = [
|
|
267
|
+
navegacion(),
|
|
268
|
+
...(carpeta === undefined
|
|
269
|
+
? []
|
|
270
|
+
: [
|
|
271
|
+
crearCriticaVisual({
|
|
272
|
+
leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)),
|
|
273
|
+
invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
|
|
274
|
+
}),
|
|
275
|
+
crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto }),
|
|
276
|
+
]),
|
|
277
|
+
];
|
|
278
|
+
const propiasDe = (agente) => [
|
|
279
|
+
crearBusquedaRegex(backend),
|
|
280
|
+
navegacion(),
|
|
281
|
+
...(carpeta !== undefined && (agente.escribeEn ?? []).length > 0
|
|
282
|
+
? [crearCopiarArtefacto({ raiz, carpetaDeArtefactos: carpeta, perfil: agente })]
|
|
283
|
+
: []),
|
|
284
|
+
// El mismo reparto que deepagents (`xoneAgent.ts`): a quien declara `escribeEn`.
|
|
285
|
+
...((agente.escribeEn ?? []).length > 0 ? [crearUnirSecciones({ raiz, perfil: agente })] : []),
|
|
286
|
+
];
|
|
287
|
+
/**
|
|
288
|
+
* **Un cliente de modelo por papel, modelo y esfuerzo, que dura la SESIÓN** —hasta `/modelo`—, y
|
|
289
|
+
* no uno por llamada como era.
|
|
290
|
+
*
|
|
291
|
+
* No es por ahorro, es por el ECO de DeepSeek (`config/ecoDeRazonamiento.ts`): repone el
|
|
292
|
+
* `reasoning_content` que `@langchain/openai` tira, emparejándolo por el id de la tool call, y su
|
|
293
|
+
* memoria vive DENTRO del cliente. Con un cliente nuevo en cada llamada la memoria nacía vacía y el
|
|
294
|
+
* eco no emparejaba nunca. Medido en el cable con DeepSeek real: deepagents devolvía el
|
|
295
|
+
* razonamiento en 7 de 7 mensajes con tool calls, y TrueForge en 0 de 5. Hoy la API lo acepta
|
|
296
|
+
* igual, pero su documentación exige devolverlo, y con esto TrueForge lo hace a nivel HTTP haga lo
|
|
297
|
+
* que haga su contexto —que es justo lo que cambia en 0.3—
|
|
298
|
+
* (`deepseekEnTrueforge.test.ts`).
|
|
299
|
+
*/
|
|
300
|
+
const clientes = new Map();
|
|
301
|
+
const clienteDe = (clave, crear) => {
|
|
302
|
+
let cliente = clientes.get(clave);
|
|
303
|
+
if (cliente === undefined) {
|
|
304
|
+
cliente = crear();
|
|
305
|
+
clientes.set(clave, cliente);
|
|
306
|
+
}
|
|
307
|
+
return cliente;
|
|
308
|
+
};
|
|
309
|
+
const llm = modeloParaTrueforge({ modelo: () => clienteDe("papel:trabajo:", () => modelos.paraPapel("trabajo")), senal: () => aborto?.signal });
|
|
310
|
+
/**
|
|
311
|
+
* El hilo de un hijo EXTERNO: un `AgentThread` normal con UNA llamada, cuyo «modelo» es el
|
|
312
|
+
* producto (`modeloExterno.ts`). Sin tools, sin capabilities y sin compactación: el bucle vive
|
|
313
|
+
* dentro de Claude Code, Codex u OpenCode. La petición es la de deepagents (`xoneAgent.ts`), y
|
|
314
|
+
* se compone al DELEGAR —el inventario, fresco—; su vida es la del encargo, y no se persiste
|
|
315
|
+
* nada del producto: la foto del raíz guarda el encargo y la respuesta, nunca un proceso.
|
|
316
|
+
*/
|
|
317
|
+
const hijoExterno = (agente, params) => {
|
|
318
|
+
hilosExternos.add(params.threadId);
|
|
319
|
+
externosDelTurno += 1;
|
|
320
|
+
const motor = agente.motor;
|
|
321
|
+
return new AgentThread({
|
|
322
|
+
definition: {
|
|
323
|
+
modelClient: modeloExternoParaTrueforge({
|
|
324
|
+
puerto: externo,
|
|
325
|
+
peticion: () => ({
|
|
326
|
+
motor,
|
|
327
|
+
cwd: raiz,
|
|
328
|
+
instrucciones: `${promptDeAgente(agente, repartirSkills(agente, disponibles))}\n\n${inventarioDelProyecto(ficherosDelProyecto(raiz))}`,
|
|
329
|
+
tarea: params.request.input,
|
|
330
|
+
...(agente.modelo === undefined ? {} : { modelo: agente.modelo }),
|
|
331
|
+
permitirEscritura: !agente.soloLectura,
|
|
332
|
+
agente: agente.nombre,
|
|
333
|
+
}),
|
|
334
|
+
senal: () => aborto?.signal,
|
|
335
|
+
}),
|
|
336
|
+
messages: [{ role: "user", content: params.request.input }],
|
|
337
|
+
iterationLimit: 1,
|
|
338
|
+
},
|
|
339
|
+
threadId: params.threadId,
|
|
340
|
+
title: params.request.name,
|
|
341
|
+
parent: params.parent,
|
|
342
|
+
agentInfo: { type: "dynamic", ...params.request },
|
|
343
|
+
tracing: NOOP_AGENT_TRACING,
|
|
344
|
+
logger,
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* El hilo de un subagente, sacado de SU `.md`: su prompt, SUS skills, sus permisos y su
|
|
349
|
+
* modelo. TrueForge no tiene subagentes con nombre ni deja poner prompt propio a un hijo —su
|
|
350
|
+
* identidad fija dice además que tiene «las mismas tools que el padre», que aquí no es
|
|
351
|
+
* verdad—, así que las instrucciones van en su primer mensaje, CORRIGIENDO esa frase, y el
|
|
352
|
+
* encargo detrás. Un nombre que no es de ningún especialista da un hijo genérico de SOLO
|
|
353
|
+
* LECTURA y sin shell: un nombre inventado no tumba el turno ni se lleva la escritura.
|
|
354
|
+
*/
|
|
355
|
+
const crearHijo = async (params) => {
|
|
356
|
+
const agente = especialistas().find((a) => a.nombre === params.request.name);
|
|
357
|
+
quienEs.set(params.threadId, agente?.nombre ?? params.request.name);
|
|
358
|
+
if (agente !== undefined)
|
|
359
|
+
especialistaDeHilo.set(params.threadId, agente.nombre);
|
|
360
|
+
if (agente !== undefined && agente.motor !== "modelo")
|
|
361
|
+
return hijoExterno(agente, params);
|
|
362
|
+
const clase = agente === undefined ? "lee" : clasesDeTools(agente);
|
|
363
|
+
// Las piezas por lo que declara su `.md` (`capacidades.ts`); la nota sale de SUS tools.
|
|
364
|
+
const piezas = capacidadesDelEspecialista(agente, params.request.name, {
|
|
365
|
+
backend: backend,
|
|
366
|
+
propias: propiasDe,
|
|
367
|
+
conShell: () => montarBackend({ entorno: entornoDeLaShellDelProyecto(raiz, opciones.artefactos) }),
|
|
368
|
+
});
|
|
369
|
+
const nombres = toolsDe(piezas);
|
|
370
|
+
const nota = notaDeTools(nombres, agente === undefined ? LIMITE_DEL_GENERICO : LIMITES_DE[clase]);
|
|
371
|
+
const instrucciones = agente === undefined
|
|
372
|
+
? `${nota} Contesta con lo que encuentres y dónde.`
|
|
373
|
+
: [promptDeAgente(agente, repartirSkills(agente, disponibles)), "", nota, anuncioDeSkills(agente, catalogo)]
|
|
374
|
+
.filter((l) => l !== undefined)
|
|
375
|
+
.join("\n")
|
|
376
|
+
.trimEnd();
|
|
377
|
+
const papel = agente?.soloLectura === true ? "rapido" : "trabajo";
|
|
378
|
+
const definicionDelHijo = {
|
|
379
|
+
modelClient: modeloParaTrueforge({
|
|
380
|
+
modelo: () => agente?.modelo === undefined
|
|
381
|
+
? clienteDe(`papel:${papel}:${agente?.esfuerzo ?? ""}`, () => modelos.paraPapel(papel, agente?.esfuerzo))
|
|
382
|
+
: clienteDe(`modelo:${agente.modelo}:${agente.esfuerzo ?? ""}`, () => modelos.paraModelo(agente.modelo, agente.esfuerzo)),
|
|
383
|
+
senal: () => aborto?.signal,
|
|
384
|
+
}),
|
|
385
|
+
messages: [{ role: "user", content: params.request.input }],
|
|
386
|
+
// Los topes MEDIDOS de deepagents: el conductor más, porque cada paso suyo es un comando.
|
|
387
|
+
iterationLimit: agente?.ejecucion === true ? TOPE_DE_LLAMADAS_DEL_CONDUCTOR : TOPE_DE_LLAMADAS_DEL_ESPECIALISTA,
|
|
388
|
+
};
|
|
389
|
+
return new AgentThread({
|
|
390
|
+
definition: definicionDelHijo,
|
|
391
|
+
threadId: params.threadId,
|
|
392
|
+
title: params.request.name,
|
|
393
|
+
parent: params.parent,
|
|
394
|
+
agentInfo: { type: "dynamic", ...params.request },
|
|
395
|
+
/**
|
|
396
|
+
* **El prompt del especialista va en el prompt de SISTEMA, por una capability**, y no en
|
|
397
|
+
* su primer mensaje como al principio. Medido en la librería: `buildInstruction` IGNORA el
|
|
398
|
+
* `instruction` de un hijo (`!this.parent`), y lo único que llega a su sistema son los
|
|
399
|
+
* `instructionBuilders`. En el primer mensaje, además, una compactación —que sustituye el
|
|
400
|
+
* contexto entero— se lo llevaría por delante.
|
|
401
|
+
*
|
|
402
|
+
* **Y el hijo NO se compacta, por una medida.** Se activó al mismo umbral que el raíz y la
|
|
403
|
+
* traza lo tumbó («lanza la app en el emulador», MyAllXOne): el `device-controller` llegó a
|
|
404
|
+
* 32.694 a un par de llamadas de terminar, y el resumen de TrueForge —un prompt largo que
|
|
405
|
+
* pide fragmentos de código enteros— costó 26.010 de entrada SIN caché y 5.916 de salida, y
|
|
406
|
+
* las dos llamadas siguientes volvieron a calentar la caché. Un encargo de hijo es corto y va
|
|
407
|
+
* cacheado al 85-95 %: reenviar su contexto sale más barato que resumirlo. El de deepagents
|
|
408
|
+
* acabó a 22.508 sin llegar al umbral.
|
|
409
|
+
*/
|
|
410
|
+
capabilities: capabilitiesDe([...piezas, capacidadDeInstrucciones(instrucciones)]),
|
|
411
|
+
tracing: NOOP_AGENT_TRACING,
|
|
412
|
+
logger,
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
/**
|
|
416
|
+
* El árbol de hilos de UNA conversación. Se rehace con `nuevoHilo`, al reabrir una sesión
|
|
417
|
+
* —desde la foto guardada— y tras un turno cortado, desde la foto SANEADA del raíz: los hijos
|
|
418
|
+
* que se quedaron esperando no pueden seguir, y con ellos vivos el siguiente mensaje del usuario
|
|
419
|
+
* lo rechazaba la librería («Cannot process user messages while sub agents are running»).
|
|
420
|
+
*/
|
|
421
|
+
let raizActual;
|
|
422
|
+
/** La pregunta del orquestador que espera respuesta: el siguiente mensaje la contesta. */
|
|
423
|
+
let preguntaEnEspera;
|
|
424
|
+
const nuevoOrquestador = (foto) => {
|
|
425
|
+
const definicion = {
|
|
426
|
+
modelClient: llm,
|
|
427
|
+
instruction: [promptOrquestador(especialistas()), notaDeDelegacion(especialistas())].filter((l) => l !== "").join("\n\n"),
|
|
428
|
+
// Por TURNO, porque el raíz se rehace desde su foto al final de cada uno (ver `turno`).
|
|
429
|
+
iterationLimit: LIMITE_DE_LLAMADAS_DEL_RAIZ,
|
|
430
|
+
};
|
|
431
|
+
const raizDelArbol = new AgentThread({
|
|
432
|
+
definition: definicion,
|
|
433
|
+
threadId: HILO_RAIZ,
|
|
434
|
+
title: HILO_RAIZ,
|
|
435
|
+
capabilities: [
|
|
436
|
+
// Las piezas del orquestador (`capacidades.ts`): leer, sus tools propias, el presupuesto
|
|
437
|
+
// del paso y la fecha. Lo que necesita su definición —compactar— y delegar van aquí.
|
|
438
|
+
...capabilitiesDe([
|
|
439
|
+
capacidadDeFicheros({ backend, reglas: permisosDe(PERFIL_DEL_ORQUESTADOR), tools: TOOLS_DE_LECTURA, conAprobacion: false }),
|
|
440
|
+
capacidadDePropias(propiasDelRaiz, backend),
|
|
441
|
+
capacidadDeRecortes(backend),
|
|
442
|
+
capacidadDeFecha(),
|
|
443
|
+
]),
|
|
444
|
+
/**
|
|
445
|
+
* **La conversación se RESUME al mismo umbral que deepagents** (`UMBRAL_RESUMEN_TOKENS`):
|
|
446
|
+
* sin esto no se compactaba nunca y cada turno reenviaba la sesión entera. La
|
|
447
|
+
* compactación de TrueForge sustituye el contexto ENTERO por el resumen, y por eso las
|
|
448
|
+
* instrucciones van en el prompt de sistema —aquí `instruction`, en un hijo su
|
|
449
|
+
* capability—, que no se compacta.
|
|
450
|
+
*/
|
|
451
|
+
contextCompaction({ definition: definicion, compactionThresholdTokens: UMBRAL_RESUMEN_TOKENS }),
|
|
452
|
+
// `ask_user_question`, SOLO en el raíz —la librería tampoco se la da a un hijo—: es el
|
|
453
|
+
// único que tiene a una persona delante.
|
|
454
|
+
askUserQuestion(),
|
|
455
|
+
// `create_sub_agent`: la delegación de TrueForge, un nivel y cinco a la vez como mucho.
|
|
456
|
+
dynamicSubAgents({ sandboxAvailable: false, tracing: NOOP_AGENT_TRACING }),
|
|
457
|
+
],
|
|
458
|
+
tracing: NOOP_AGENT_TRACING,
|
|
459
|
+
logger,
|
|
460
|
+
...(foto === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: {
|
|
463
|
+
context: foto.context,
|
|
464
|
+
...(foto.current_context_usage === undefined ? {} : { currentContextUsage: foto.current_context_usage }),
|
|
465
|
+
...(foto.capability_state == null ? {} : { capabilityState: foto.capability_state }),
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
raizActual = raizDelArbol;
|
|
469
|
+
return new AgentThreadOrchestrator({
|
|
470
|
+
agentThreads: new Map([[HILO_RAIZ, raizDelArbol]]),
|
|
471
|
+
createDynamicSubAgentThread: async (params) => crearHijo(params),
|
|
472
|
+
tracing: NOOP_AGENT_TRACING,
|
|
473
|
+
logger,
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Solo se persiste con un id de sesión DADO: es lo que tiene índice donde reanudar. La consola
|
|
478
|
+
* de terminal no lo pasa, la misma regla que el checkpoint de deepagents.
|
|
479
|
+
*/
|
|
480
|
+
const persistir = opciones.hilo !== undefined;
|
|
481
|
+
/** El aviso de una memoria que existía y no se entendió: sale en el PRIMER turno y se gasta. */
|
|
482
|
+
let avisoDeMemoria;
|
|
483
|
+
/**
|
|
484
|
+
* La foto de la sesión `id`, si hay y se entiende. Una que no se entiende se APARTA —el guardado
|
|
485
|
+
* de este mismo turno la pisaría— y se DICE en el turno siguiente (`memoriaTrueforge.ts`).
|
|
486
|
+
*/
|
|
487
|
+
const fotoDeLaSesion = (id) => {
|
|
488
|
+
avisoDeMemoria = undefined;
|
|
489
|
+
if (!persistir)
|
|
490
|
+
return undefined;
|
|
491
|
+
const lectura = cargarMemoria(raiz, id);
|
|
492
|
+
if (lectura.estado === "incompatible")
|
|
493
|
+
avisoDeMemoria = textoDeMemoriaDescartada(lectura.motivo, apartarMemoria(raiz, id));
|
|
494
|
+
return lectura.estado === "ok" ? lectura.foto : undefined;
|
|
495
|
+
};
|
|
496
|
+
const fotoInicial = fotoDeLaSesion(hilo);
|
|
497
|
+
let orquestador = nuevoOrquestador(fotoInicial);
|
|
498
|
+
// La pregunta que se quedó sin contestar al cerrar: la respuesta de la persona es para ELLA.
|
|
499
|
+
preguntaEnEspera = preguntaDeLaFoto(fotoInicial);
|
|
500
|
+
const avisar = () => {
|
|
501
|
+
for (const o of oyentes)
|
|
502
|
+
o();
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Lo que el orquestador dice al ejecutar, traducido; y lo que pidió aprobar al terminar.
|
|
506
|
+
*
|
|
507
|
+
* **Una aprobación se devuelve al hilo que la PIDIÓ.** Con subagentes que escriben, la
|
|
508
|
+
* `write_file` la pide el hijo, y el orquestador de TrueForge reparte las decisiones por
|
|
509
|
+
* `thread_id`: mandarla al raíz no la contestaría —o la rechazaría por hilo desconocido—.
|
|
510
|
+
* Por eso las tool calls se apuntan por hilo Y por id: dos hijos pueden repetir id.
|
|
511
|
+
*/
|
|
512
|
+
async function* paso(lote, senal, pendientes, preguntas = []) {
|
|
513
|
+
for await (const _ of orquestador.send(lote))
|
|
514
|
+
void _;
|
|
515
|
+
const llamadas = new Map();
|
|
516
|
+
// El razonamiento de los especialistas, juntado por hilo hasta que su mensaje se completa.
|
|
517
|
+
const pensamientos = new Map();
|
|
518
|
+
const it = orquestador.execute({ signal: senal });
|
|
519
|
+
let r = await it.next();
|
|
520
|
+
while (!r.done) {
|
|
521
|
+
const evento = r.value;
|
|
522
|
+
const deHilo = evento.thread_id ?? HILO_RAIZ;
|
|
523
|
+
if (evento.type === "internal.agent.context.append" && Array.isArray(evento.output)) {
|
|
524
|
+
for (const m of evento.output) {
|
|
525
|
+
for (const t of m.tool_calls ?? []) {
|
|
526
|
+
let args = {};
|
|
527
|
+
try {
|
|
528
|
+
args = JSON.parse(t.function.arguments);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
args = {};
|
|
532
|
+
}
|
|
533
|
+
llamadas.set(claveDe(deHilo, t.id), { nombre: t.function.name, args });
|
|
534
|
+
// La traza: la MISMA lista blanca que el evento (`detalleDe`, `parametrosDe`), nunca
|
|
535
|
+
// los argumentos crudos.
|
|
536
|
+
diagnostico?.herramienta(t.function.name, detalleDe(t.function.name, args), parametrosDe(t.function.name, args), tracker, deHilo === HILO_RAIZ ? "orquestador" : "especialista");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (evento.type === "tool.response" && evento.tool_call_id !== undefined) {
|
|
541
|
+
// Cuánto METIÓ en el contexto lo que devolvió: los caracteres, nunca el contenido.
|
|
542
|
+
const llamada = llamadas.get(claveDe(deHilo, evento.tool_call_id));
|
|
543
|
+
const chars = typeof evento.content === "string" ? evento.content.length : 0;
|
|
544
|
+
diagnostico?.resultado?.(llamada?.nombre, llamada === undefined ? undefined : detalleDe(llamada.nombre, llamada.args), chars);
|
|
545
|
+
}
|
|
546
|
+
// Un hilo que agotó su tope se ANOTA en la traza, con quién era: el raíz y cualquier hijo.
|
|
547
|
+
const tope = topeAgotadoDe(evento);
|
|
548
|
+
if (tope !== undefined)
|
|
549
|
+
diagnostico?.corte?.(quienEs.get(deHilo) ?? deHilo, tope);
|
|
550
|
+
const { eventos, uso } = traducirEvento(evento, (h) => especialistaDeHilo.get(h), pensamientos);
|
|
551
|
+
// La «llamada» de un hijo externo es el producto entero: su consumo llega por su propio
|
|
552
|
+
// callback (`externo`), y contarla aquí sumaría una llamada de modelo a ceros por delegación.
|
|
553
|
+
if (uso !== undefined && !hilosExternos.has(deHilo)) {
|
|
554
|
+
// Los tokens de TODOS los hilos se gastaron, así que todos cuentan. La VENTANA es otra
|
|
555
|
+
// pregunta —cuánto ocupa la conversación—, y esa es la del raíz: la de un hijo es la de
|
|
556
|
+
// un encargo que muere con él.
|
|
557
|
+
tracker.input += uso.input;
|
|
558
|
+
tracker.output += uso.output;
|
|
559
|
+
tracker.cache += uso.cache;
|
|
560
|
+
tracker.calls += 1;
|
|
561
|
+
// Y solo de una llamada NORMAL: la de la compactación mide lo de ANTES de resumir.
|
|
562
|
+
if (deHilo === HILO_RAIZ && evento.type === "internal.agent.context.append")
|
|
563
|
+
tracker.contexto = uso.input;
|
|
564
|
+
// Por ORIGEN, como deepagents: el orquestador y cada especialista por su nombre.
|
|
565
|
+
diagnostico?.modelo(quienEs.get(deHilo) ?? deHilo, {
|
|
566
|
+
input: uso.input,
|
|
567
|
+
output: uso.output,
|
|
568
|
+
cache: uso.cache,
|
|
569
|
+
llamadas: tracker.calls,
|
|
570
|
+
contexto: uso.input,
|
|
571
|
+
});
|
|
572
|
+
avisar();
|
|
573
|
+
}
|
|
574
|
+
yield* eventos;
|
|
575
|
+
while (artefactosPorAnunciar.length > 0)
|
|
576
|
+
yield { tipo: "artefacto", artefacto: artefactosPorAnunciar.shift() };
|
|
577
|
+
r = await it.next();
|
|
578
|
+
}
|
|
579
|
+
const resultado = r.value;
|
|
580
|
+
for (const accion of resultado.required_actions ?? []) {
|
|
581
|
+
// Dos interrupciones distintas, y no se mezclan: una APROBACIÓN decide sobre una escritura
|
|
582
|
+
// con su diff; una RESPUESTA es lo que pide `ask_user_question`, que no escribe nada.
|
|
583
|
+
const destino = accion.type === "tool.approval_required" ? pendientes : accion.type === "tool.response_required" ? preguntas : undefined;
|
|
584
|
+
if (destino === undefined)
|
|
585
|
+
continue;
|
|
586
|
+
const deHilo = accion.thread_id ?? HILO_RAIZ;
|
|
587
|
+
for (const { id } of accion.tool_calls ?? []) {
|
|
588
|
+
const clave = claveDe(deHilo, id);
|
|
589
|
+
const llamada = llamadas.get(clave);
|
|
590
|
+
if (llamada !== undefined)
|
|
591
|
+
destino.push({ clave, hilo: deHilo, id, ...llamada });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
get tracker() {
|
|
597
|
+
return tracker;
|
|
598
|
+
},
|
|
599
|
+
get hilo() {
|
|
600
|
+
return hilo;
|
|
601
|
+
},
|
|
602
|
+
async turno(peticion, piel) {
|
|
603
|
+
if (cerrada)
|
|
604
|
+
throw new Error("la sesión está cerrada");
|
|
605
|
+
const t0 = Date.now();
|
|
606
|
+
cancelado = false;
|
|
607
|
+
const instantanea = await tomarInstantanea(raiz, opciones.entorno.git);
|
|
608
|
+
const tope = opciones.topeDeRondas ?? MAX_APPROVAL_ROUNDS;
|
|
609
|
+
const aplicadasSinPreguntar = [];
|
|
610
|
+
apuntarAplicadasSinPreguntar = (rutas) => void aplicadasSinPreguntar.push(...rutas);
|
|
611
|
+
// Lo que llevaba el tracker al empezar: el contraste con el motor es del TURNO.
|
|
612
|
+
const trackerAlEmpezar = { input: tracker.input, output: tracker.output, cache: tracker.cache, calls: tracker.calls };
|
|
613
|
+
externosDelTurno = 0;
|
|
614
|
+
capturasDelTurno = [];
|
|
615
|
+
/** El encargo de ESTE turno tal cual se pidió, y el mismo con la última pregunta y su
|
|
616
|
+
* respuesta al lado, que es lo que se juzga y se repara (ver `flujo`). Ausente = no consta. */
|
|
617
|
+
let encargoDelTurno;
|
|
618
|
+
let objetivoDelTurno;
|
|
619
|
+
// Se dice UNA vez: el turno que lo lleva es el primero que corre sin la conversación de antes.
|
|
620
|
+
const memoriaDescartada = avisoDeMemoria;
|
|
621
|
+
avisoDeMemoria = undefined;
|
|
622
|
+
let cortadoPorTope = false;
|
|
623
|
+
let sinResolver = 0;
|
|
624
|
+
// El veredicto del turno, con las MISMAS reglas que deepagents (`verificacion.ts`).
|
|
625
|
+
let veredicto = "no-corrio";
|
|
626
|
+
let hallazgosDelTurno = [];
|
|
627
|
+
let preexistentesDelTurno;
|
|
628
|
+
let motivoSinVerificar;
|
|
629
|
+
let escribioProyecto = false;
|
|
630
|
+
/**
|
|
631
|
+
* Las RONDAS de una petición: una pausa termina la ronda y se reanuda con las decisiones.
|
|
632
|
+
* Devuelve si acabó LIMPIA —sin escrituras en la mesa—, que es lo único que se verifica:
|
|
633
|
+
* con algo sin resolver el turno no ha terminado su trabajo, y mirar a medias daría un
|
|
634
|
+
* veredicto sobre algo que no es lo que quedó.
|
|
635
|
+
*/
|
|
636
|
+
async function* rondasDe(loteInicial, salida) {
|
|
637
|
+
let lote = loteInicial;
|
|
638
|
+
let rondas = 0;
|
|
639
|
+
while (true) {
|
|
640
|
+
if (cancelado || cerrada)
|
|
641
|
+
return;
|
|
642
|
+
aborto = new AbortController();
|
|
643
|
+
const senal = aborto.signal;
|
|
644
|
+
const pendientes = [];
|
|
645
|
+
const preguntas = [];
|
|
646
|
+
yield* paso(lote, senal, pendientes, preguntas);
|
|
647
|
+
if (pendientes.length === 0) {
|
|
648
|
+
// El orquestador PREGUNTA: el turno acaba aquí con la pregunta a la vista, y lo que la
|
|
649
|
+
// persona escriba después vuelve como la respuesta de esa tool (`turno`, arriba).
|
|
650
|
+
const pregunta = preguntas[0];
|
|
651
|
+
if (pregunta !== undefined) {
|
|
652
|
+
// El encargo CRUDO, no el objetivo con su nota: si no, cada pregunta encadenada
|
|
653
|
+
// arrastraría las notas de las anteriores y el objetivo crecería turno a turno.
|
|
654
|
+
preguntaEnEspera = { ...pregunta, ...(encargoDelTurno === undefined ? {} : { encargo: encargoDelTurno }) };
|
|
655
|
+
yield { tipo: "token", texto: textoDePregunta(pregunta.args) };
|
|
656
|
+
// Y la misma pregunta como DATO, para la piel que pinta un botón por opción.
|
|
657
|
+
const consulta = consultaDe(pregunta.args);
|
|
658
|
+
if (consulta.opciones.length > 0)
|
|
659
|
+
yield { tipo: "consulta", ...consulta };
|
|
660
|
+
}
|
|
661
|
+
salida.limpia = true;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const leer = (ruta) => {
|
|
665
|
+
try {
|
|
666
|
+
return readFileSync(join(raiz, ruta), "utf8");
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return "";
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
// Todo va por la CLAVE (hilo + id): es lo que viaja a la tarjeta y lo que vuelve.
|
|
673
|
+
const decisiones = new Map();
|
|
674
|
+
const humanos = [];
|
|
675
|
+
const ficheros = new Map();
|
|
676
|
+
const diffs = new Map();
|
|
677
|
+
const autonomo = opciones.sinAprobacion?.() === true;
|
|
678
|
+
for (const p of pendientes) {
|
|
679
|
+
const ruta = typeof p.args.file_path === "string" ? p.args.file_path : "";
|
|
680
|
+
// Lo que NO es el proyecto —artefactos y planes— y lo que el backend va a rechazar de
|
|
681
|
+
// todas formas no se pregunta: la MISMA función que el HITL de deepagents.
|
|
682
|
+
if (!seDetieneEn({ toolCall: { args: p.args } })) {
|
|
683
|
+
decisiones.set(p.clave, { type: "approve" });
|
|
684
|
+
}
|
|
685
|
+
else if (autonomo) {
|
|
686
|
+
decisiones.set(p.clave, { type: "approve" });
|
|
687
|
+
aplicadasSinPreguntar.push(ruta);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
humanos.push({
|
|
691
|
+
id: p.clave,
|
|
692
|
+
origen: quienEs.get(p.hilo) ?? p.hilo,
|
|
693
|
+
descripcion: `quiere ${TEXTO_HITL[p.nombre] ?? p.nombre}`,
|
|
694
|
+
decisionesPermitidas: ["approve", "reject"],
|
|
695
|
+
});
|
|
696
|
+
ficheros.set(p.clave, ruta);
|
|
697
|
+
const vista = cambioDe({ id: p.clave, tool: p.nombre, args: p.args, description: "", allowedDecisions: [] }, leer);
|
|
698
|
+
if (vista !== undefined)
|
|
699
|
+
diffs.set(p.clave, vista.lineas);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (humanos.length > 0) {
|
|
703
|
+
rondas += 1;
|
|
704
|
+
yield { tipo: "pausa", pendientes: humanos };
|
|
705
|
+
if (opciones.pedirAprobacion === undefined || rondas > tope) {
|
|
706
|
+
// Nadie a quien preguntar, o se agotó el tope: lo que no se decide NO se aplica.
|
|
707
|
+
cortadoPorTope = rondas > tope;
|
|
708
|
+
sinResolver = humanos.length;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const respuesta = await opciones.pedirAprobacion(humanos, ficheros, diffs);
|
|
712
|
+
if (cancelado || cerrada) {
|
|
713
|
+
// Cancelado mientras se decidía: no se aplica nada de lo que estaba en la mesa.
|
|
714
|
+
sinResolver = humanos.length;
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
for (const h of humanos)
|
|
718
|
+
decisiones.set(h.id, respuesta.get(h.id) ?? { type: "reject" });
|
|
719
|
+
}
|
|
720
|
+
lote = pendientes.map((p) => ({ p, d: decisiones.get(p.clave) ?? { type: "reject" } })).map(({ p, d }) => ({
|
|
721
|
+
type: "user.tool_approval",
|
|
722
|
+
thread_id: p.hilo,
|
|
723
|
+
tool_call_id: p.id,
|
|
724
|
+
approval: d.type === "approve" ? { status: "allow" } : { status: "deny", reason: d.message ?? "rechazado por el usuario" },
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* **El verificador, cosido al FINAL del flujo y no después del turno** —la regla de
|
|
730
|
+
* deepagents—: `correrTurno` cierra en cuanto el flujo se agota, así que verificar después
|
|
731
|
+
* pintaría el veredicto fuera del turno. Un rojo se REPARA en el mismo hilo, como mensaje
|
|
732
|
+
* de USUARIO con el objetivo delante (`textoDeReparacion`), hasta `TOPE_REPARACIONES`, y
|
|
733
|
+
* corta antes si un intento deja la MISMA huella de errores.
|
|
734
|
+
*/
|
|
735
|
+
async function* flujo() {
|
|
736
|
+
// Si el orquestador dejó una pregunta, ESTE mensaje es su respuesta: vuelve a su hilo como
|
|
737
|
+
// `user.tool_response`, y no como un mensaje nuevo que la dejaría sin contestar.
|
|
738
|
+
const enEspera = preguntaEnEspera;
|
|
739
|
+
preguntaEnEspera = undefined;
|
|
740
|
+
// El ENCARGO del turno: lo que se repara y lo que se juzga. En un turno que contesta una
|
|
741
|
+
// pregunta no es lo tecleado —un «2» no es un objetivo—, sino el encargo que la provocó.
|
|
742
|
+
//
|
|
743
|
+
// Y con lo que se preguntó y se contestó al lado. Medido en el navegador: con el encargo
|
|
744
|
+
// a secas, el juez leía solo la respuesta de ESTE turno y concluía que la pregunta que
|
|
745
|
+
// se pidió no se había hecho — se hizo, en el turno anterior, y él no lo veía.
|
|
746
|
+
// Solo la ÚLTIMA pregunta va en la nota: las de antes están en la conversación, y el
|
|
747
|
+
// encargo de una pregunta encadenada es el CRUDO (`rondasDe`), así que no se acumulan.
|
|
748
|
+
encargoDelTurno = enEspera === undefined ? peticion : enEspera.encargo;
|
|
749
|
+
objetivoDelTurno =
|
|
750
|
+
enEspera === undefined || encargoDelTurno === undefined
|
|
751
|
+
? encargoDelTurno
|
|
752
|
+
: `${encargoDelTurno}\n\n[En un turno anterior el agente preguntó «${consultaDe(enEspera.args).pregunta}» y la persona contestó «${respuestaAPregunta(enEspera.args, peticion)}».]`;
|
|
753
|
+
// Los hechos baratos del proyecto van DELANTE (`core/hechosDelProyecto.ts`), la misma
|
|
754
|
+
// foto y el MISMO cargador que `xone_navegacion`, rehecha en cada turno —la regla de
|
|
755
|
+
// deepagents: en el prompt de sistema envejecería dentro de la sesión—. Solo en la
|
|
756
|
+
// petición ORIGINAL: la respuesta a una pregunta no es un encargo nuevo, y el mensaje de
|
|
757
|
+
// una reparación ya lleva sus hallazgos. Un índice que no carga deja la petición sola.
|
|
758
|
+
const loteInicial = enEspera === undefined
|
|
759
|
+
? [
|
|
760
|
+
{
|
|
761
|
+
type: EventType.USER_MESSAGE,
|
|
762
|
+
content: conHechosDelProyecto(peticion, await hechosDelProyectoDe(cargarIndice, ficherosDelProyecto(raiz))),
|
|
763
|
+
},
|
|
764
|
+
]
|
|
765
|
+
: [{ type: "user.tool_response", thread_id: enEspera.hilo, tool_call_id: enEspera.id, content: respuestaAPregunta(enEspera.args, peticion) }];
|
|
766
|
+
yield* pasadas(loteInicial);
|
|
767
|
+
yield* juzgar();
|
|
768
|
+
}
|
|
769
|
+
/** Lo que el turno CONTESTÓ en su última pasada: es lo que lee el juez. */
|
|
770
|
+
let respuestaDeLaPasada = "";
|
|
771
|
+
/**
|
|
772
|
+
* Las PASADAS del turno: la de la petición y las de reparación. Cada `return` de aquí es un
|
|
773
|
+
* final del turno —verde, sin escribir, sin verificador, bloqueado…— y el juez va DETRÁS de
|
|
774
|
+
* todos (`flujo`); la única salida que no cierra es la que sigue a otra pasada.
|
|
775
|
+
*/
|
|
776
|
+
async function* pasadas(loteInicial) {
|
|
777
|
+
let lote = loteInicial;
|
|
778
|
+
let intento = 0;
|
|
779
|
+
let huellaPrevia;
|
|
780
|
+
let visualYaDisparo = false;
|
|
781
|
+
while (true) {
|
|
782
|
+
if (intento > 0)
|
|
783
|
+
yield { tipo: "reparacion", intento, tope: TOPE_REPARACIONES };
|
|
784
|
+
const salida = { limpia: false };
|
|
785
|
+
respuestaDeLaPasada = "";
|
|
786
|
+
for await (const evento of rondasDe(lote, salida)) {
|
|
787
|
+
if (evento.tipo === "token")
|
|
788
|
+
respuestaDeLaPasada += evento.texto;
|
|
789
|
+
yield evento;
|
|
790
|
+
}
|
|
791
|
+
const cambios = cambiosQueSeVerifican(await instantanea.cambios());
|
|
792
|
+
escribioProyecto = cambios.length > 0;
|
|
793
|
+
if (!salida.limpia) {
|
|
794
|
+
if (sinResolver > 0)
|
|
795
|
+
motivoSinVerificar = "quedaron escrituras sin resolver";
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
veredicto = "no-corrio";
|
|
799
|
+
if (!escribioProyecto) {
|
|
800
|
+
motivoSinVerificar = "el turno no escribió ningún fichero del proyecto";
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (opciones.verifier === undefined) {
|
|
804
|
+
motivoSinVerificar = "esta ejecución no tiene verificador";
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
yield { tipo: "fase", fase: "verificando" };
|
|
808
|
+
let informe;
|
|
809
|
+
try {
|
|
810
|
+
informe = await opciones.verifier.verificar(raiz);
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
// Que no esté el binario NO es un fallo del proyecto, y se dice como tal.
|
|
814
|
+
motivoSinVerificar = e instanceof Error ? e.message : String(e);
|
|
815
|
+
yield { tipo: "aviso", texto: `⚠ no se pudo verificar: ${motivoSinVerificar}`, severidad: "aviso" };
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
motivoSinVerificar = undefined;
|
|
819
|
+
const { hallazgos, preexistentes, errores } = repartirHallazgos(raiz, informe, cambios.map((c) => c.ruta));
|
|
820
|
+
veredicto = errores === 0 ? "verde" : "rojo";
|
|
821
|
+
hallazgosDelTurno = hallazgos;
|
|
822
|
+
preexistentesDelTurno = preexistentes;
|
|
823
|
+
yield {
|
|
824
|
+
tipo: "verificacion",
|
|
825
|
+
verde: errores === 0,
|
|
826
|
+
errores,
|
|
827
|
+
avisos: hallazgos.length - errores,
|
|
828
|
+
hallazgos,
|
|
829
|
+
...(preexistentes > 0 ? { preexistentes } : {}),
|
|
830
|
+
};
|
|
831
|
+
/**
|
|
832
|
+
* **El crítico de pantalla**, con las reglas de deepagents (`turnoReal.ts`, junto a
|
|
833
|
+
* `tocaCriticarPantalla`): mira la ÚLTIMA captura que dejó este turno, una vez por
|
|
834
|
+
* turno. Con el simulador en VERDE solo REPORTA —medido allí: una ronda disparada por
|
|
835
|
+
* defectos de otra pantalla se iba a rediseñar lo que nadie pidió—; en ROJO sus
|
|
836
|
+
* observaciones se suman a la reparación que iba a salir igualmente. Que no se pueda
|
|
837
|
+
* preguntar es un aviso, no un rojo.
|
|
838
|
+
*/
|
|
839
|
+
let observacionesVisuales = [];
|
|
840
|
+
if (carpeta !== undefined &&
|
|
841
|
+
tocaCriticarPantalla({ hayCritico: opciones.criticaVisual !== undefined, capturas: capturasDelTurno.length, yaDisparo: visualYaDisparo, intento })) {
|
|
842
|
+
visualYaDisparo = true;
|
|
843
|
+
const captura = capturasDelTurno[capturasDelTurno.length - 1];
|
|
844
|
+
try {
|
|
845
|
+
const bytes = readFileSync(join(carpeta, captura.nombre));
|
|
846
|
+
const visual = await opciones.criticaVisual({ base64: bytes.toString("base64"), mime: captura.mime ?? "image/png" }, captura.nombre);
|
|
847
|
+
if (visual.veredicto === "rojo" && visual.observaciones.length > 0) {
|
|
848
|
+
observacionesVisuales = visual.observaciones;
|
|
849
|
+
yield {
|
|
850
|
+
tipo: "aviso",
|
|
851
|
+
texto: `⚠ la captura de esta sesión enseña ${visual.observaciones.length} defecto(s) de pantalla`,
|
|
852
|
+
severidad: "aviso",
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
yield { tipo: "aviso", texto: `⚠ no se pudo criticar la pantalla: ${error instanceof Error ? error.name : "error"}`, severidad: "aviso" };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (errores === 0)
|
|
861
|
+
return;
|
|
862
|
+
const huella = huellaDeErrores(hallazgos);
|
|
863
|
+
if (huella === huellaPrevia) {
|
|
864
|
+
yield {
|
|
865
|
+
tipo: "bloqueado",
|
|
866
|
+
motivo: "no-progreso",
|
|
867
|
+
explicacion: `el intento ${intento} dejó los mismos ${errores} error(es) en los mismos sitios`,
|
|
868
|
+
};
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (intento >= TOPE_REPARACIONES) {
|
|
872
|
+
yield {
|
|
873
|
+
tipo: "bloqueado",
|
|
874
|
+
motivo: "tope-reparaciones",
|
|
875
|
+
explicacion: `tras ${intento} intento(s) siguen ${errores} error(es); se deja como está para que lo mires`,
|
|
876
|
+
};
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
huellaPrevia = huella;
|
|
880
|
+
intento += 1;
|
|
881
|
+
lote = [{ type: EventType.USER_MESSAGE, content: textoDeReparacion(hallazgos, observacionesVisuales, objetivoDelTurno ?? peticion) }];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* **¿Hizo lo que se le pidió?** El juez del turno, con el reparto de deepagents: los HECHOS
|
|
886
|
+
* los mide el código y al modelo solo se le pregunta el juicio (`core/juezDelTurno.ts`). Va
|
|
887
|
+
* DETRÁS de todas las pasadas, así que alcanza cada final del turno —«no escribió nada» es
|
|
888
|
+
* el candidato número uno a no haber cumplido—. Tres casos en que NO se pregunta, y los tres
|
|
889
|
+
* serían ruido: un turno CANCELADO (alguien pulsó Parar; juzgarlo es gastar una llamada
|
|
890
|
+
* contra su decisión), uno que acaba PREGUNTANDO (su «respuesta» es la pregunta, y saldría
|
|
891
|
+
* dudoso siempre), y la respuesta a una pregunta cuyo encargo no consta —una sesión
|
|
892
|
+
* reabierta: el encargo vive en memoria del proceso, no en la foto—. Un juez que falla es
|
|
893
|
+
* un aviso: es una opinión sobre trabajo ya hecho.
|
|
894
|
+
*/
|
|
895
|
+
async function* juzgar() {
|
|
896
|
+
if (opciones.juezDelTurno === undefined || cancelado || cerrada)
|
|
897
|
+
return;
|
|
898
|
+
if (preguntaEnEspera !== undefined || objetivoDelTurno === undefined)
|
|
899
|
+
return;
|
|
900
|
+
try {
|
|
901
|
+
const v = await opciones.juezDelTurno({
|
|
902
|
+
objetivo: objetivoDelTurno,
|
|
903
|
+
respuesta: respuestaDeLaPasada,
|
|
904
|
+
hechos: {
|
|
905
|
+
// Ausente es «no corrió», que no es lo mismo que «salió mal»: el prompt lo dice.
|
|
906
|
+
...(veredicto === "verde" || veredicto === "rojo" ? { verificador: veredicto } : {}),
|
|
907
|
+
...(cortadoPorTope ? { escriturasSinResolver: true } : {}),
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
const accion = accionDelJuez(v, { hayHumano: false });
|
|
911
|
+
if (accion.tipo !== "nada")
|
|
912
|
+
yield { tipo: "aviso", texto: `⚠ ${accion.texto}`, severidad: "aviso" };
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
yield { tipo: "aviso", texto: `⚠ no se pudo consultar al juez del turno: ${error instanceof Error ? error.name : "error"}`, severidad: "aviso" };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
let bitacora;
|
|
919
|
+
try {
|
|
920
|
+
// El flujo del turno MÁS lo que un hijo externo va haciendo en su proceso: sin esto, sus
|
|
921
|
+
// minutos de trabajo no cruzan hasta que contesta (`core/entrelazar.ts`).
|
|
922
|
+
bitacora = await correrTurno(entrelazar(flujo(), eventosExternos), piel, {
|
|
923
|
+
// Solo si el turno ESCRIBIÓ y aun así no se verificó, y con el motivo: la regla de
|
|
924
|
+
// deepagents — un aviso que salta cuando no ha pasado nada enseña a ignorarlo.
|
|
925
|
+
avisos: (b) => [
|
|
926
|
+
...(memoriaDescartada === undefined ? [] : [memoriaDescartada]),
|
|
927
|
+
...(b.corrio("verify") || !escribioProyecto
|
|
928
|
+
? []
|
|
929
|
+
: [`⚠ el verificador no ha corrido en este turno${motivoSinVerificar === undefined ? "" : ` (${motivoSinVerificar})`}`]),
|
|
930
|
+
...(aplicadasSinPreguntar.length === 0
|
|
931
|
+
? []
|
|
932
|
+
: [`⚠ ${aplicadasSinPreguntar.length} escritura(s) aplicadas SIN aprobación: ${aplicadasSinPreguntar.join(", ")} — esta sesión va en modo autónomo`]),
|
|
933
|
+
],
|
|
934
|
+
desde: t0,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
finally {
|
|
938
|
+
aborto = undefined;
|
|
939
|
+
/**
|
|
940
|
+
* **El contraste con las métricas del motor**, ANTES de rehacer el raíz: el árbol que se
|
|
941
|
+
* mide es el de este turno, y rehecho ya no lo sería (`metricasTrueforge.ts`). Solo con la
|
|
942
|
+
* traza puesta, y sin poder tumbar nada: es diagnóstico.
|
|
943
|
+
*/
|
|
944
|
+
if (diagnostico?.contraste !== undefined) {
|
|
945
|
+
try {
|
|
946
|
+
const motor = metricasDeTrueforge(orquestador.getMetrics());
|
|
947
|
+
const nuestras = {
|
|
948
|
+
entrada: tracker.input - trackerAlEmpezar.input,
|
|
949
|
+
salida: tracker.output - trackerAlEmpezar.output,
|
|
950
|
+
cache: tracker.cache - trackerAlEmpezar.cache,
|
|
951
|
+
llamadas: tracker.calls - trackerAlEmpezar.calls,
|
|
952
|
+
};
|
|
953
|
+
diagnostico.contraste({
|
|
954
|
+
nuestras,
|
|
955
|
+
motor: { ...motor },
|
|
956
|
+
externos: externosDelTurno,
|
|
957
|
+
diferencias: diferenciasDelContraste(nuestras, motor, externosDelTurno),
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
// Una métrica que no se pudo leer no es un turno que falló.
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Lo que un hijo escribiera fuera de un turno no tiene dónde contarse (ver arriba).
|
|
965
|
+
apuntarAplicadasSinPreguntar = undefined;
|
|
966
|
+
/**
|
|
967
|
+
* La foto del raíz, al acabar CADA turno —en el `finally`, como el commit del turno—:
|
|
968
|
+
* saneada, para que las tool calls que el corte dejó sin respuesta no rompan la
|
|
969
|
+
* siguiente llamada. Se guarda si hay sesión que reanudar, y si el turno no acabó limpio
|
|
970
|
+
* se rehace el árbol desde ella. Guardar no puede tumbar un turno ya terminado.
|
|
971
|
+
*/
|
|
972
|
+
if (raizActual !== undefined) {
|
|
973
|
+
const foto = fotoSaneada({
|
|
974
|
+
...raizActual.toSnapshot(),
|
|
975
|
+
...(preguntaEnEspera === undefined
|
|
976
|
+
? {}
|
|
977
|
+
: {
|
|
978
|
+
pregunta_pendiente: {
|
|
979
|
+
hilo: preguntaEnEspera.hilo,
|
|
980
|
+
id: preguntaEnEspera.id,
|
|
981
|
+
args: preguntaEnEspera.args,
|
|
982
|
+
...(preguntaEnEspera.encargo === undefined ? {} : { encargo: preguntaEnEspera.encargo }),
|
|
983
|
+
},
|
|
984
|
+
}),
|
|
985
|
+
});
|
|
986
|
+
if (persistir) {
|
|
987
|
+
try {
|
|
988
|
+
guardarMemoria(raiz, hilo, foto);
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
// Sin memoria en disco la sesión sigue viva; al reabrirla, `hayMemoria` dirá la verdad.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* **El raíz se rehace desde su foto al final de CADA turno**, y no solo tras uno cortado.
|
|
996
|
+
* El contador de llamadas de TrueForge (`metrics.iterations`) vive con el HILO y no se
|
|
997
|
+
* reinicia entre ejecuciones: con el raíz vivo toda la conversación, su tope se agotaba
|
|
998
|
+
* sumando TODOS los turnos y una sesión larga dejaba de contestar aunque cada turno fuera
|
|
999
|
+
* corto. Rehacerlo reinicia el contador y conserva el contexto: el tope pasa a ser por
|
|
1000
|
+
* turno, que es lo que promete.
|
|
1001
|
+
*/
|
|
1002
|
+
orquestador = nuevoOrquestador(foto);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const cambios = await instantanea.cambios();
|
|
1006
|
+
return {
|
|
1007
|
+
bitacora,
|
|
1008
|
+
cambios,
|
|
1009
|
+
cortadoPorTope,
|
|
1010
|
+
verificador: veredicto,
|
|
1011
|
+
pendientes: sinResolver,
|
|
1012
|
+
...(hallazgosDelTurno.length === 0 ? {} : { hallazgos: hallazgosDelTurno }),
|
|
1013
|
+
...(preexistentesDelTurno === undefined ? {} : { preexistentes: preexistentesDelTurno }),
|
|
1014
|
+
...(motivoSinVerificar === undefined ? {} : { motivoSinVerificar }),
|
|
1015
|
+
};
|
|
1016
|
+
},
|
|
1017
|
+
async cambiarModelos(nuevos) {
|
|
1018
|
+
// El ILLM pide el modelo en cada llamada, así que basta con cambiar a quién se lo pide:
|
|
1019
|
+
// el hilo sigue entero.
|
|
1020
|
+
modelos = nuevos;
|
|
1021
|
+
// Los clientes eran del modelo de antes: el siguiente se construye con el nuevo.
|
|
1022
|
+
clientes.clear();
|
|
1023
|
+
},
|
|
1024
|
+
nuevoHilo(id) {
|
|
1025
|
+
hilo = id ?? `tf-${Date.now()}`;
|
|
1026
|
+
// Un hilo NUEVO: lo que hubiera guardado con ese id no se pisa ni se carga a medias. Y una
|
|
1027
|
+
// pregunta de la conversación de antes no la contesta el primer mensaje de la nueva.
|
|
1028
|
+
const foto = fotoDeLaSesion(hilo);
|
|
1029
|
+
orquestador = nuevoOrquestador(foto);
|
|
1030
|
+
preguntaEnEspera = preguntaDeLaFoto(foto);
|
|
1031
|
+
// Y las DOS cuentas vuelven a cero, como en deepagents: lo que gastó la conversación de antes
|
|
1032
|
+
// no es de ésta, y sin esto `/nuevo` arrastraba la cifra entera a una sesión vacía.
|
|
1033
|
+
tracker.input = 0;
|
|
1034
|
+
tracker.output = 0;
|
|
1035
|
+
tracker.cache = 0;
|
|
1036
|
+
tracker.calls = 0;
|
|
1037
|
+
tracker.contexto = 0;
|
|
1038
|
+
consumoExterno = SIN_CONSUMO;
|
|
1039
|
+
avisar();
|
|
1040
|
+
},
|
|
1041
|
+
cancelar() {
|
|
1042
|
+
cancelado = true;
|
|
1043
|
+
aborto?.abort(new Error("turno cancelado por el usuario"));
|
|
1044
|
+
},
|
|
1045
|
+
consumo() {
|
|
1046
|
+
return {
|
|
1047
|
+
modelo: { entrada: tracker.input, salida: tracker.output, cache: tracker.cache },
|
|
1048
|
+
externo: consumoExterno,
|
|
1049
|
+
contexto: tracker.contexto,
|
|
1050
|
+
};
|
|
1051
|
+
},
|
|
1052
|
+
alCambiarConsumo(oyente) {
|
|
1053
|
+
oyentes.add(oyente);
|
|
1054
|
+
return () => oyentes.delete(oyente);
|
|
1055
|
+
},
|
|
1056
|
+
cerrar() {
|
|
1057
|
+
cerrada = true;
|
|
1058
|
+
aborto?.abort(new Error("sesión cerrada"));
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
//# sourceMappingURL=sesionTrueforge.js.map
|