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