@arcadialdev/arcality 2.4.31 → 2.4.33
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/.agent/skills/form-expert.md +99 -0
- package/.agent/skills/investigation-protocol.md +61 -0
- package/.agent/skills/modal-master.md +41 -0
- package/.agent/skills/native-control-expert.md +82 -0
- package/bin/arcality.mjs +10 -1
- package/package.json +2 -1
- package/playwright.config.js +1 -0
- package/scripts/arcality-logs.mjs +76 -0
- package/scripts/gen-and-run.mjs +31 -3
- package/src/arcalityClient.mjs +3 -2
- package/src/services/autoskillsService.ts +61 -0
- package/tests/_helpers/agentic-runner.bundle.spec.js +116 -51
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: form-expert
|
|
3
|
+
description: Metodología experta para explorar, identificar y llenar formularios complejos en aplicaciones web.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Form Expert Skill
|
|
7
|
+
|
|
8
|
+
Esta habilidad define el protocolo que el agente DEBE seguir cuando la misión involucra llenar, crear o editar registros a través de formularios.
|
|
9
|
+
|
|
10
|
+
## Principio Fundamental
|
|
11
|
+
**NUNCA interactúes con un campo que no hayas inspeccionado primero.** Si un componente tiene un nombre genérico, un tag inferred como `[INFERRED]`, o simplemente no estás seguro de qué es, usa `inspect_element_details` ANTES de hacer click o fill.
|
|
12
|
+
|
|
13
|
+
## Protocolo de Formularios (4 fases obligatorias)
|
|
14
|
+
|
|
15
|
+
### Fase 1: RECONOCIMIENTO (Scan)
|
|
16
|
+
Antes de llenar cualquier campo, dedica tu primer turno en el formulario a **analizar todos los componentes visibles**:
|
|
17
|
+
|
|
18
|
+
1. **Lee la lista completa de CURRENT COMPONENTS** — identifica:
|
|
19
|
+
- Campos de tipo FIELD (inputs, textareas, selects)
|
|
20
|
+
- Campos con label visible (prioritarios)
|
|
21
|
+
- Campos sin label o con nombre genérico (requieren inspección)
|
|
22
|
+
- Botones de acción (GUARDAR, CANCELAR, etc.)
|
|
23
|
+
- Tabs o pestañas si el formulario tiene secciones
|
|
24
|
+
|
|
25
|
+
2. **Clasifica los campos** en:
|
|
26
|
+
- ✅ **Confiables**: Tienen un nombre claro como `[input] Título del documento`
|
|
27
|
+
- ⚠️ **Sospechosos**: Tienen nombre genérico como `[input] Editar` o `[INFERRED] xxx`
|
|
28
|
+
- ❌ **Desconocidos**: Solo tienen tag genérico como `[input]`
|
|
29
|
+
|
|
30
|
+
3. **Para campos sospechosos o desconocidos**: Usa `inspect_element_details` para obtener:
|
|
31
|
+
- El placeholder text
|
|
32
|
+
- El aria-label
|
|
33
|
+
- El atributo name o id
|
|
34
|
+
- Si está disabled o readonly
|
|
35
|
+
|
|
36
|
+
### Fase 2: PLANIFICACIÓN (Plan)
|
|
37
|
+
Con la información del reconocimiento, planifica la secuencia de acciones:
|
|
38
|
+
|
|
39
|
+
1. **Prioriza los campos obligatorios** — los que tienen asterisco (*), clase "required", o están marcados en rojo.
|
|
40
|
+
2. **Genera datos únicos** — Si la misión dice "genérico" o "aleatorio", genera datos que incluyan un timestamp o ID único para evitar duplicados. Ejemplo: `TEST-${timestamp}` o `Prueba QA Feb24`.
|
|
41
|
+
3. **Planifica el orden**: Llena los campos de arriba a abajo, izquierda a derecha, siguiendo el orden del DOM.
|
|
42
|
+
|
|
43
|
+
### Fase 3: EJECUCIÓN (Act)
|
|
44
|
+
Ejecuta las acciones planificadas:
|
|
45
|
+
|
|
46
|
+
1. **Llena múltiples campos por turno** — No pierdas un turno por campo. Agrupa 3-5 fills en una sola llamada a `perform_ui_actions`.
|
|
47
|
+
2. **Para cada campo fill**: click + fill (en ese orden). El click asegura que el campo tiene focus.
|
|
48
|
+
3. **Para dropdowns/selects**: Click para abrir → espera el siguiente turno → selecciona la opción.
|
|
49
|
+
4. **Si un fill falla**: NO reintentes inmediatamente. Usa `inspect_element_details` para ver si el campo es un select, un datepicker, o tiene un overlay encima.
|
|
50
|
+
|
|
51
|
+
### Fase 4: VERIFICACIÓN (Verify)
|
|
52
|
+
Antes de hacer GUARDAR:
|
|
53
|
+
|
|
54
|
+
1. **Usa `validate_element_state`** en el botón de GUARDAR para confirmar que está enabled.
|
|
55
|
+
2. **Revisa visualmente** si hay campos en rojo o mensajes de error.
|
|
56
|
+
3. **Solo entonces** haz click en GUARDAR.
|
|
57
|
+
4. **Después de GUARDAR**: Espera la respuesta de la UI. Si aparece un toast de éxito o regresas a la lista, la misión fue exitosa.
|
|
58
|
+
|
|
59
|
+
## Manejo de Errores en Formularios
|
|
60
|
+
|
|
61
|
+
### Si un campo falla con Timeout:
|
|
62
|
+
```
|
|
63
|
+
1. NO reintentes el mismo click/fill inmediatamente
|
|
64
|
+
2. Usa inspect_element_details en ese IDX
|
|
65
|
+
3. Analiza si:
|
|
66
|
+
- El elemento está cubierto por un overlay/modal
|
|
67
|
+
- El elemento es readonly/disabled
|
|
68
|
+
- El IDX cambió (la página se re-renderizó)
|
|
69
|
+
4. Si el elemento no sirve, busca un campo alternativo con nombre similar
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Si aparece un error de "duplicado" o "ya existe":
|
|
73
|
+
```
|
|
74
|
+
1. Identifica qué campo causó el error (claves, códigos, nombres)
|
|
75
|
+
2. Cambia el valor a algo único (agrega sufijo numérico o timestamp)
|
|
76
|
+
3. Reintenta GUARDAR
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Si el formulario tiene tabs o secciones:
|
|
80
|
+
```
|
|
81
|
+
1. Llena los campos de la sección actual PRIMERO.
|
|
82
|
+
2. Identifica el elemento de la pestaña: Busca el texto de la pestaña (ej: "CAMPOS DE LA FORMA") en componentes tipo ACTION.
|
|
83
|
+
3. PRECISIÓN: Evita hacer click en contenedores grandes que agrupan varias pestañas. Prefiere el elemento más pequeño (span o label) que contenga el texto exacto.
|
|
84
|
+
4. Haz click en la pestaña. Si un click simple no funciona tras 2 intentos, usa 'double_click'.
|
|
85
|
+
5. Espera el siguiente turno para ver los nuevos campos.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Uso de Double Click
|
|
89
|
+
- Úsalo SOLO cuando:
|
|
90
|
+
- La misión lo pida explícitamente (ej: "haz doble clic en la celda").
|
|
91
|
+
- Un elemento (como una pestaña o un icono) no responda tras dos intentos de click simple.
|
|
92
|
+
- Quieras activar modos de edición en tablas o componentes complejos.
|
|
93
|
+
|
|
94
|
+
## Anti-patrones (NUNCA hagas esto)
|
|
95
|
+
- ❌ Hacer click en un campo `[INFERRED]` sin inspeccionarlo primero
|
|
96
|
+
- ❌ Intentar llenar TODO el formulario en el primer turno sin saber qué campos hay
|
|
97
|
+
- ❌ Hacer click en el botón GUARDAR sin haber verificado que los campos están llenos
|
|
98
|
+
- ❌ Reintentar la misma acción fallida sin investigar la causa
|
|
99
|
+
- ❌ Asumir que un componente con nombre largo (>50 chars) es un campo — probablemente es un contenedor
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: investigation-protocol
|
|
3
|
+
description: Protocolo obligatorio de investigación cuando una acción falla o un elemento es ambiguo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Investigation Protocol Skill
|
|
7
|
+
|
|
8
|
+
## Regla de Oro
|
|
9
|
+
**NUNCA reintentes una acción fallida sin investigar primero.** Si algo falla, SIEMPRE ejecuta el protocolo de investigación antes de intentar de nuevo.
|
|
10
|
+
|
|
11
|
+
## Protocolo de Investigación (Obligatorio tras cualquier fallo)
|
|
12
|
+
|
|
13
|
+
### Paso 1: Inspección del elemento
|
|
14
|
+
```
|
|
15
|
+
Usa: inspect_element_details (idx del elemento que falló)
|
|
16
|
+
Objetivo: Determinar POR QUÉ falló
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Revisa en el resultado:
|
|
20
|
+
- ¿El elemento es `disabled` o `readonly`?
|
|
21
|
+
- ¿El elemento está cubierto por otro (`pointer-events: none`, overlay)?
|
|
22
|
+
- ¿El elemento es un `<select>` que requiere interacción diferente a fill?
|
|
23
|
+
- ¿El elemento está en un iframe diferente (`frameIdx`)?
|
|
24
|
+
|
|
25
|
+
### Paso 2: Auditoría de consola
|
|
26
|
+
```
|
|
27
|
+
Usa: capture_console_errors
|
|
28
|
+
Objetivo: Buscar errores JavaScript silenciosos
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Si la UI no responde o está congelada, puede haber un error JS no visible. Revisa:
|
|
32
|
+
- Errores de red (fetch failed, 404, 500)
|
|
33
|
+
- Errores de JS (TypeError, undefined)
|
|
34
|
+
- Warnings de frameworks (React, Angular)
|
|
35
|
+
|
|
36
|
+
### Paso 3: Captura de evidencia
|
|
37
|
+
```
|
|
38
|
+
Usa: create_test_evidence (type: annotated_screenshot)
|
|
39
|
+
Objetivo: Documentar el estado actual para debugging
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Si identificas un bug real de la aplicación (no un error tuyo), documéntalo con evidencia anotada.
|
|
43
|
+
|
|
44
|
+
### Paso 4: Decisión
|
|
45
|
+
Después de investigar, toma UNA de estas decisiones:
|
|
46
|
+
1. **Reintentar con variación**: Si descubriste que el campo es un select, usa click en vez de fill.
|
|
47
|
+
2. **Buscar alternativa**: Si el elemento está cubierto, busca otro camino al mismo objetivo.
|
|
48
|
+
3. **Reportar bug**: Si es un bug de la aplicación, usa `report_application_bug`.
|
|
49
|
+
4. **Reportar incapacidad**: Si no puedes avanzar, usa `report_inability_to_proceed`.
|
|
50
|
+
|
|
51
|
+
## Cuándo usar cada herramienta de investigación
|
|
52
|
+
|
|
53
|
+
| Situación | Herramienta | Obligatoria |
|
|
54
|
+
|-----------|------------|-------------|
|
|
55
|
+
| Acción falló con Timeout | `inspect_element_details` | ✅ SÍ |
|
|
56
|
+
| Elemento con nombre [INFERRED] | `inspect_element_details` | ✅ SÍ |
|
|
57
|
+
| UI no responde / congelada | `capture_console_errors` | ✅ SÍ |
|
|
58
|
+
| Antes de hacer GUARDAR/SAVE | `validate_element_state` | ✅ SÍ |
|
|
59
|
+
| Después de GUARDAR exitoso | `extract_table_data` | Recomendada |
|
|
60
|
+
| Bug encontrado | `create_test_evidence` | ✅ SÍ |
|
|
61
|
+
| Fin de misión exitosa | `create_test_evidence` | Recomendada |
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modal-master
|
|
3
|
+
description: Gestión experta de ventanas modales, diálogos y popups en pruebas de QA.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modal Master Skill
|
|
7
|
+
|
|
8
|
+
Esta habilidad permite al agente detectar, navegar y cerrar ventanas modales de manera eficiente, evitando que el flujo del test se bloquee por elementos superpuestos.
|
|
9
|
+
|
|
10
|
+
## Cuándo usar esta habilidad
|
|
11
|
+
- Cuando el agente realiza una acción y aparece un diálogo de confirmación (ej: "¿Desea guardar los cambios?").
|
|
12
|
+
- Cuando un popup de error o notificación bloquea el acceso al resto de la página.
|
|
13
|
+
- Cuando la misión requiere interactuar específicamente con elementos dentro de un modal.
|
|
14
|
+
|
|
15
|
+
## Instrucciones para el Agente
|
|
16
|
+
|
|
17
|
+
### 1. Detección de Modales
|
|
18
|
+
Antes de intentar interactuar con un elemento del fondo, verifica si hay un modal activo buscando:
|
|
19
|
+
- Elementos con `role="dialog"` o `role="alertdialog"`.
|
|
20
|
+
- Atributos `aria-modal="true"`.
|
|
21
|
+
- Overlays oscuros que cubren la pantalla (ej: clases tipo `backdrop`, `overlay`, `modal-open`).
|
|
22
|
+
- Títulos destacados que no estaban antes de la acción.
|
|
23
|
+
|
|
24
|
+
### 2. Prioridad de Interacción
|
|
25
|
+
Si un modal está abierto:
|
|
26
|
+
- **Prioridad 1**: Si el modal es informativo (Warning/Error), léelo y repórtalo en tu pensamiento antes de cerrarlo.
|
|
27
|
+
- **Prioridad 2**: Si el modal es de confirmación para la acción que acabas de realizar, haz clic en el botón afirmativo (ej: "Aceptar", "Sí", "Confirmar").
|
|
28
|
+
- **Prioridad 3**: Si el modal es intrusivo y no permite seguir, busca el botón de cierre (usualmente una 'X', un botón "Cerrar" o "Cancelar").
|
|
29
|
+
|
|
30
|
+
### 3. Estrategias de Cierre
|
|
31
|
+
Si no encuentras un botón de cierre claro:
|
|
32
|
+
- Busca botones con clases `close`, `btn-close` o que tengan un ícono dentro.
|
|
33
|
+
- Usa `send_keyboard_event` con key `Escape` para intentar cerrar el modal/popup/dropdown.
|
|
34
|
+
- Verifica si el modal desaparece al hacer clic en el backdrop (el área fuera del cuadro blanco).
|
|
35
|
+
|
|
36
|
+
### 4. Flujo de Trabajo (Ejemplo)
|
|
37
|
+
1. **Acción**: Clic en "Eliminar".
|
|
38
|
+
2. **Scan**: Detecto un modal con el texto "¿Está seguro?".
|
|
39
|
+
3. **Validación**: Uso `validate_element_state` para verificar que el botón "Eliminar permanentemente" es visible dentro del modal.
|
|
40
|
+
4. **Ejecución**: Clic en el botón del modal.
|
|
41
|
+
5. **Verificación**: Espero a que el modal desaparezca antes de realizar la siguiente acción de la misión.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: native-control-expert
|
|
3
|
+
description: Protocolo experto para interactuar con controles nativos del navegador (time pickers, date pickers, selects, etc.)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Native Control Expert Skill
|
|
7
|
+
|
|
8
|
+
Esta habilidad define el protocolo que el agente DEBE seguir cuando la misión involucra interactuar con controles nativos del navegador que **NO responden bien** a la acción estándar `fill`.
|
|
9
|
+
|
|
10
|
+
## Controles Nativos que REQUIEREN esta skill
|
|
11
|
+
|
|
12
|
+
| Control | Ejemplo | Acción preferida |
|
|
13
|
+
|---------|---------|-----------------|
|
|
14
|
+
| `<input type="time">` | Campo de hora `HH:mm` | `interact_native_control` con `control_type: "time"` |
|
|
15
|
+
| `<input type="date">` | Campo de fecha `YYYY-MM-DD` | `interact_native_control` con `control_type: "date"` |
|
|
16
|
+
| `<input type="datetime-local">` | Fecha y hora combinados | `interact_native_control` con `control_type: "datetime"` |
|
|
17
|
+
| `<input type="color">` | Selector de color | `interact_native_control` con `control_type: "color"` |
|
|
18
|
+
| `<input type="range">` | Slider numérico | `interact_native_control` con `control_type: "range"` |
|
|
19
|
+
| `<select>` nativo | Dropdown estándar HTML | `interact_native_control` con `control_type: "select"` |
|
|
20
|
+
| Combobox/Autocomplete | Campos con `role="combobox"` | `click` + `fill` letra + `send_keyboard_event` ArrowDown + Enter |
|
|
21
|
+
| Time picker con diálogo | Campos que abren un popup al hacer click | `send_keyboard_event` con Arrow keys |
|
|
22
|
+
|
|
23
|
+
## Protocolo de Interacción con Time Pickers
|
|
24
|
+
|
|
25
|
+
### ❌ LO QUE NUNCA DEBES HACER:
|
|
26
|
+
- Intentar `fill` repetidamente si falla la primera vez
|
|
27
|
+
- Hacer click una y otra vez sin cambiar de estrategia (esto causa loop detection)
|
|
28
|
+
- Asumir que un time picker funciona como un input de texto normal
|
|
29
|
+
|
|
30
|
+
### ✅ LO QUE SÍ DEBES HACER:
|
|
31
|
+
|
|
32
|
+
**Estrategia 1: interact_native_control (PREFERIDA)**
|
|
33
|
+
```
|
|
34
|
+
1. Usa inspect_element_details para confirmar que es type="time"
|
|
35
|
+
2. Usa interact_native_control con control_type: "time", value: "13:05"
|
|
36
|
+
3. Verifica el resultado
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Estrategia 2: Keyboard Navigation (FALLBACK)**
|
|
40
|
+
```
|
|
41
|
+
1. Haz click en el campo de hora
|
|
42
|
+
2. Usa send_keyboard_event con key: "Control+a" para seleccionar todo
|
|
43
|
+
3. Usa send_keyboard_event para escribir los dígitos del tiempo
|
|
44
|
+
4. Ejemplo: Para "13:05", envía las teclas: "1", "3", "0", "5"
|
|
45
|
+
5. Usa send_keyboard_event con key: "Tab" para navegar al siguiente campo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Estrategia 3: Arrow Keys (ULTIMO RECURSO)**
|
|
49
|
+
```
|
|
50
|
+
1. Haz click en el segmento de horas del campo
|
|
51
|
+
2. Usa send_keyboard_event con key: "ArrowUp" o "ArrowDown" para ajustar la hora
|
|
52
|
+
3. Usa send_keyboard_event con key: "Tab" para mover al segmento de minutos
|
|
53
|
+
4. Repite con ArrowUp/ArrowDown para los minutos
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Protocolo de Interacción con Date Pickers
|
|
57
|
+
|
|
58
|
+
### ✅ Estrategia preferida:
|
|
59
|
+
```
|
|
60
|
+
1. Confirma con inspect_element_details que es type="date"
|
|
61
|
+
2. Usa interact_native_control con control_type: "date", value: "2026-03-25"
|
|
62
|
+
3. IMPORTANTE: El formato SIEMPRE es YYYY-MM-DD
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Detección: ¿Cuándo usar esta skill?
|
|
66
|
+
|
|
67
|
+
Debes activar esta skill cuando:
|
|
68
|
+
1. Ves un componente FIELD con nombre que incluye "HH:mm" o "hora" o "time"
|
|
69
|
+
2. Un campo tiene `role="combobox"` y `aria-expanded`
|
|
70
|
+
3. Un `fill()` falla en un campo de fecha/hora con Timeout o sin efecto
|
|
71
|
+
4. El inspector muestra `type="time"` o `type="date"` en los atributos
|
|
72
|
+
|
|
73
|
+
## Keyboard Shortcuts Útiles
|
|
74
|
+
|
|
75
|
+
| Tecla | Efecto en controles nativos |
|
|
76
|
+
|-------|-----------------------------|
|
|
77
|
+
| `Tab` | Mover al siguiente segmento (horas → minutos → AM/PM) |
|
|
78
|
+
| `ArrowUp` | Incrementar valor del segmento actual |
|
|
79
|
+
| `ArrowDown` | Decrementar valor del segmento actual |
|
|
80
|
+
| `Escape` | Cerrar dropdown/calendar que se abrió |
|
|
81
|
+
| `Enter` | Confirmar selección en calendarios |
|
|
82
|
+
| `Control+a` | Seleccionar todo el contenido del campo |
|
package/bin/arcality.mjs
CHANGED
|
@@ -14,15 +14,24 @@ const rawArgs = process.argv.slice(2);
|
|
|
14
14
|
let isRun = false;
|
|
15
15
|
let isInit = false;
|
|
16
16
|
let isSetup = false;
|
|
17
|
+
let isLogs = false;
|
|
17
18
|
|
|
18
19
|
// Buscamos los comandos en cualquier posición para evitar filtros de NPM en Windows
|
|
19
20
|
rawArgs.forEach(arg => {
|
|
20
21
|
if (arg === 'run') isRun = true;
|
|
21
22
|
if (arg === 'init') isInit = true;
|
|
22
23
|
if (arg === 'setup') isSetup = true;
|
|
24
|
+
if (arg === '--logs') isLogs = true;
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
if (
|
|
27
|
+
if (isLogs) {
|
|
28
|
+
const logsScript = path.join(PACKAGE_ROOT, 'scripts', 'arcality-logs.mjs');
|
|
29
|
+
spawn('node', [logsScript], {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
cwd: process.cwd(),
|
|
32
|
+
env: { ...process.env, ARCALITY_ROOT: PACKAGE_ROOT }
|
|
33
|
+
}).on('exit', code => process.exit(code || 0));
|
|
34
|
+
} else if (isInit) {
|
|
26
35
|
const initScript = path.join(PACKAGE_ROOT, 'scripts', 'init.mjs');
|
|
27
36
|
spawn('node', [initScript, ...rawArgs.filter(a => a !== 'init')], {
|
|
28
37
|
stdio: 'inherit',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadialdev/arcality",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.33",
|
|
4
4
|
"description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin/",
|
|
19
19
|
"scripts/",
|
|
20
20
|
"src/",
|
|
21
|
+
".agent/",
|
|
21
22
|
".agents/",
|
|
22
23
|
"tests/_helpers/agentic-runner.bundle.spec.js",
|
|
23
24
|
"tests/_helpers/ArcalityReporter.js",
|
package/playwright.config.js
CHANGED
|
@@ -7,6 +7,7 @@ module.exports = defineConfig({
|
|
|
7
7
|
testDir: path.join(__dirname, 'tests', '_helpers'),
|
|
8
8
|
testMatch: ['agentic-runner.bundle.spec.js'],
|
|
9
9
|
reporter: [
|
|
10
|
+
['line'],
|
|
10
11
|
[path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'),
|
|
11
12
|
{ outputDir: process.env.REPORTS_DIR || path.join(__dirname, 'tests-report') }]
|
|
12
13
|
],
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
const contextDir = path.join(process.cwd(), "out");
|
|
12
|
+
const centralLog = path.join(contextDir, "arcality-history.log");
|
|
13
|
+
|
|
14
|
+
console.log(chalk.cyan(`\n🔍 Arcality Logs Viewer`));
|
|
15
|
+
console.log(chalk.gray(`=========================\n`));
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(centralLog)) {
|
|
18
|
+
console.log(chalk.yellow(`No se encontró un historial de logs en este proyecto.`));
|
|
19
|
+
console.log(chalk.gray(`(Directorio actual: ${contextDir})\n`));
|
|
20
|
+
console.log(`Ejecuta algunas misiones de prueba primero usando el comando arcality.\n`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const rawData = fs.readFileSync(centralLog, 'utf8');
|
|
26
|
+
const lines = rawData.split('\n').filter(l => l.trim().length > 0);
|
|
27
|
+
|
|
28
|
+
if (lines.length === 0) {
|
|
29
|
+
console.log(chalk.yellow(`El archivo de historial de misiones está vacío.\n`));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.bold(`Registros encontrados: `) + chalk.cyan(lines.length));
|
|
34
|
+
console.log(chalk.gray(`Mostrando los últimos 20 resultados:`));
|
|
35
|
+
console.log();
|
|
36
|
+
|
|
37
|
+
const recentLines = lines.slice(-20);
|
|
38
|
+
let totalInput = 0;
|
|
39
|
+
let totalOutput = 0;
|
|
40
|
+
|
|
41
|
+
recentLines.forEach((line, index) => {
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(line);
|
|
44
|
+
|
|
45
|
+
// Icono según resultado
|
|
46
|
+
const statusIcon = data.success ? chalk.green('✅') : chalk.red('❌');
|
|
47
|
+
|
|
48
|
+
// Fecha bonita
|
|
49
|
+
const date = new Date(data.ts).toLocaleString();
|
|
50
|
+
|
|
51
|
+
// Contabilizar tokens
|
|
52
|
+
const inT = data.usage?.input_tokens || 0;
|
|
53
|
+
const caT = data.usage?.cache_creation_input_tokens || 0;
|
|
54
|
+
const crT = data.usage?.cache_read_input_tokens || 0;
|
|
55
|
+
const totalIn = inT + caT + crT;
|
|
56
|
+
const totalOut = data.usage?.output_tokens || 0;
|
|
57
|
+
|
|
58
|
+
totalInput += totalIn;
|
|
59
|
+
totalOutput += totalOut;
|
|
60
|
+
|
|
61
|
+
const costStr = chalk.yellow(`[IN: ${totalIn} | OUT: ${totalOut}]`);
|
|
62
|
+
const targetStr = data.target ? chalk.gray(` @ ${data.target}`) : '';
|
|
63
|
+
|
|
64
|
+
console.log(`${statusIcon} ${chalk.blue(date)} ${costStr}`);
|
|
65
|
+
console.log(` ${chalk.white(data.mission)}${targetStr}`);
|
|
66
|
+
console.log(` ${chalk.gray(`Pasos dados: ${data.steps}`)}\n`);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.log(chalk.red(`⚠️ Error parseando línea: ${line.substring(0, 50)}...\n`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray(`--------------------------------------------------\n`));
|
|
73
|
+
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(chalk.red(`❌ Ocurrió un error al leer los logs:`), err.message);
|
|
76
|
+
}
|
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -194,7 +194,7 @@ function showBanner() {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
|
|
197
|
-
const ARCALITY_MODEL = process.env.CLAUDE_MODEL || "claude-
|
|
197
|
+
const ARCALITY_MODEL = process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest";
|
|
198
198
|
const OUT_DIR = path.join(PROJECT_ROOT, "tests");
|
|
199
199
|
const LOGS_DIR = path.join(PROJECT_ROOT, "logs");
|
|
200
200
|
|
|
@@ -807,7 +807,22 @@ async function main() {
|
|
|
807
807
|
|
|
808
808
|
// ── End Mission ──
|
|
809
809
|
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
810
|
-
|
|
810
|
+
|
|
811
|
+
let finalUsage = undefined;
|
|
812
|
+
try {
|
|
813
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
814
|
+
if (fs.existsSync(ctxDir)) {
|
|
815
|
+
const files = fs.readdirSync(ctxDir);
|
|
816
|
+
const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
|
|
817
|
+
if (logFiles.length > 0) {
|
|
818
|
+
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
819
|
+
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
820
|
+
finalUsage = logData.usage;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} catch(e) {}
|
|
824
|
+
|
|
825
|
+
await endMission(mission.mission_id, 'success', finalUsage);
|
|
811
826
|
|
|
812
827
|
// ── Ping Project ──
|
|
813
828
|
await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
|
|
@@ -866,7 +881,20 @@ async function main() {
|
|
|
866
881
|
// End mission with failure
|
|
867
882
|
try {
|
|
868
883
|
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
869
|
-
|
|
884
|
+
let finalUsage = undefined;
|
|
885
|
+
try {
|
|
886
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
887
|
+
if (fs.existsSync(ctxDir)) {
|
|
888
|
+
const files = fs.readdirSync(ctxDir);
|
|
889
|
+
const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
|
|
890
|
+
if (logFiles.length > 0) {
|
|
891
|
+
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
892
|
+
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
893
|
+
finalUsage = logData.usage;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch(e) {}
|
|
897
|
+
await endMission(mission.mission_id, 'failed', finalUsage);
|
|
870
898
|
} catch { }
|
|
871
899
|
} finally {
|
|
872
900
|
const sRep = spinner();
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -234,8 +234,9 @@ export async function startMission(prompt, targetUrl) {
|
|
|
234
234
|
* Marks a mission as completed.
|
|
235
235
|
* @param {string} missionId
|
|
236
236
|
* @param {'success'|'failed'|'cancelled'} result
|
|
237
|
+
* @param {object} usage - Optional usage statistics directly from the AI
|
|
237
238
|
*/
|
|
238
|
-
export async function endMission(missionId, result) {
|
|
239
|
+
export async function endMission(missionId, result, usage = null) {
|
|
239
240
|
const apiBase = getEffectiveApiBase();
|
|
240
241
|
if (!apiBase || !missionId) return { ok: true };
|
|
241
242
|
|
|
@@ -247,7 +248,7 @@ export async function endMission(missionId, result) {
|
|
|
247
248
|
'Content-Type': 'application/json',
|
|
248
249
|
'x-api-key': key
|
|
249
250
|
},
|
|
250
|
-
body: JSON.stringify({ result })
|
|
251
|
+
body: JSON.stringify({ result, usage })
|
|
251
252
|
});
|
|
252
253
|
} catch { }
|
|
253
254
|
return { ok: true };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// Mapeo curado de tecnologías clave a sus skills de pruebas/qa/patrones de autoskills.sh
|
|
5
|
+
// Son recursos muy ligeros en formato Markdown
|
|
6
|
+
const SKILLS_MAP: Record<string, string> = {
|
|
7
|
+
'next': 'https://skills.sh/vercel-labs/next-skills/next-best-practices/raw',
|
|
8
|
+
'vue': 'https://skills.sh/antfu/skills/vue-best-practices/raw',
|
|
9
|
+
'tailwindcss': 'https://skills.sh/giuseppe-trisciuoglio/developer-kit/tailwind-css-patterns/raw',
|
|
10
|
+
'react': 'https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices/raw',
|
|
11
|
+
'angular': 'https://skills.sh/angular/skills/angular-developer/raw',
|
|
12
|
+
'nuxt': 'https://skills.sh/antfu/skills/nuxt/raw',
|
|
13
|
+
'svelte': 'https://skills.sh/ejirocodes/agent-skills/svelte5-best-practices/raw',
|
|
14
|
+
'astro': 'https://skills.sh/astrolicious/agent-skills/astro/raw',
|
|
15
|
+
'prisma': 'https://skills.sh/prisma/skills/prisma-postgres/raw'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function detectAndFetchEphemeralSkills(projectRoot: string): Promise<string> {
|
|
19
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
20
|
+
if (!fs.existsSync(pkgPath)) return '';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
24
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
25
|
+
|
|
26
|
+
const matchedUrls: string[] = [];
|
|
27
|
+
const matchedTechs: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [lib, url] of Object.entries(SKILLS_MAP)) {
|
|
30
|
+
// Buscamos coincidencia parcial (ej. si usan @angular/core, next, react-dom)
|
|
31
|
+
const hasLib = Object.keys(deps).some(dep => dep === lib || dep.includes(`/${lib}`));
|
|
32
|
+
if (hasLib) {
|
|
33
|
+
matchedUrls.push(url);
|
|
34
|
+
matchedTechs.push(lib);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (matchedUrls.length === 0) return '';
|
|
39
|
+
|
|
40
|
+
console.log(`>>ARCALITY_STATUS>> ⚡ Stack detectado (${matchedTechs.join(', ')}). QA Skills inyectadas en memoria.`);
|
|
41
|
+
|
|
42
|
+
// Fetch strings in parallel with simple timeout
|
|
43
|
+
const skillsPromises = matchedUrls.map(url => {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const id = setTimeout(() => controller.abort(), 2000); // Max 2s per request
|
|
46
|
+
return fetch(url, { signal: controller.signal })
|
|
47
|
+
.then(r => r.ok ? r.text() : '')
|
|
48
|
+
.catch(() => '')
|
|
49
|
+
.finally(() => clearTimeout(id));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const skillsTexts = await Promise.all(skillsPromises);
|
|
53
|
+
|
|
54
|
+
const validTexts = skillsTexts.filter(Boolean);
|
|
55
|
+
if (validTexts.length === 0) return '';
|
|
56
|
+
|
|
57
|
+
return `\n<TECH_STACK_CONTEXT>\nEl proyecto objetivo utiliza las siguientes tecnologías. Usa estas directrices técnicas para entender el DOM, rutas y posibles bugs visuales:\n\n${validTexts.join('\n\n---\n\n')}\n</TECH_STACK_CONTEXT>\n`;
|
|
58
|
+
} catch {
|
|
59
|
+
return ''; // Fails silently
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1375,6 +1375,14 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
1375
1375
|
} catch (e) {
|
|
1376
1376
|
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
|
|
1377
1377
|
}
|
|
1378
|
+
let autoskillsContext = "";
|
|
1379
|
+
try {
|
|
1380
|
+
const path3 = require("path");
|
|
1381
|
+
const toolsRoot = process.env.ARCALITY_ROOT || path3.join(__dirname, "..", "..");
|
|
1382
|
+
const { detectAndFetchEphemeralSkills } = require(path3.join(toolsRoot, "src", "services", "autoskillsService"));
|
|
1383
|
+
autoskillsContext = await detectAndFetchEphemeralSkills(process.cwd());
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
}
|
|
1378
1386
|
const systemPromptBlocks = [
|
|
1379
1387
|
{
|
|
1380
1388
|
type: "text",
|
|
@@ -1386,6 +1394,7 @@ ${prompt}
|
|
|
1386
1394
|
|
|
1387
1395
|
${projectInfo}
|
|
1388
1396
|
${skillsContext}
|
|
1397
|
+
${autoskillsContext}
|
|
1389
1398
|
${memoryContext}
|
|
1390
1399
|
${credentialsContext}
|
|
1391
1400
|
${memoryBackendContext}`
|
|
@@ -1491,8 +1500,7 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
|
|
|
1491
1500
|
|
|
1492
1501
|
3. **NEVER LOOP ON THE SAME STEP**: If you've been on the same wizard step for 3+ turns without advancing, go back one step and verify the data is correct before trying to advance again.
|
|
1493
1502
|
|
|
1494
|
-
4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm
|
|
1495
|
-
cache_control: { type: "ephemeral" }
|
|
1503
|
+
4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm.`
|
|
1496
1504
|
}
|
|
1497
1505
|
];
|
|
1498
1506
|
if (this.testInfo) {
|
|
@@ -1511,8 +1519,7 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
|
|
|
1511
1519
|
type: "base64",
|
|
1512
1520
|
media_type: "image/png",
|
|
1513
1521
|
data: base64Img
|
|
1514
|
-
}
|
|
1515
|
-
cache_control: { type: "ephemeral" }
|
|
1522
|
+
}
|
|
1516
1523
|
},
|
|
1517
1524
|
{
|
|
1518
1525
|
type: "text",
|
|
@@ -1537,14 +1544,14 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1537
1544
|
},
|
|
1538
1545
|
{
|
|
1539
1546
|
type: "text",
|
|
1540
|
-
text: `Please analyze the current state and provide the next step
|
|
1541
|
-
cache_control: { type: "ephemeral" }
|
|
1547
|
+
text: `Please analyze the current state and provide the next step.`
|
|
1542
1548
|
}
|
|
1543
1549
|
]
|
|
1544
1550
|
}
|
|
1545
1551
|
];
|
|
1546
1552
|
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Arcality est\xE1 procesando la visi\xF3n...`);
|
|
1547
1553
|
const startTime = Date.now();
|
|
1554
|
+
let totalUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
1548
1555
|
for (let turn = 0; turn < 10; turn++) {
|
|
1549
1556
|
let response;
|
|
1550
1557
|
let retryCount = 0;
|
|
@@ -1611,6 +1618,12 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1611
1618
|
}
|
|
1612
1619
|
const data = await response.json();
|
|
1613
1620
|
const message = data;
|
|
1621
|
+
if (data.usage) {
|
|
1622
|
+
totalUsage.input_tokens += data.usage.input_tokens || 0;
|
|
1623
|
+
totalUsage.output_tokens += data.usage.output_tokens || 0;
|
|
1624
|
+
totalUsage.cache_creation_input_tokens += data.usage.cache_creation_input_tokens || 0;
|
|
1625
|
+
totalUsage.cache_read_input_tokens += data.usage.cache_read_input_tokens || 0;
|
|
1626
|
+
}
|
|
1614
1627
|
anthropicMessages.push({
|
|
1615
1628
|
role: "assistant",
|
|
1616
1629
|
content: message.content
|
|
@@ -1619,7 +1632,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1619
1632
|
const textResponse = message.content.find((c) => c.type === "text")?.text || "";
|
|
1620
1633
|
if (toolCalls.length === 0) {
|
|
1621
1634
|
console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
|
|
1622
|
-
return { thought: textResponse || "No action decided", actions: [], usage:
|
|
1635
|
+
return { thought: textResponse || "No action decided", actions: [], usage: totalUsage };
|
|
1623
1636
|
}
|
|
1624
1637
|
const toolResults = [];
|
|
1625
1638
|
let uiActionCall = null;
|
|
@@ -1679,7 +1692,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1679
1692
|
\u{1F4A1} SUGERENCIA: ${toolInput.prompt_suggestion}`,
|
|
1680
1693
|
actions: [],
|
|
1681
1694
|
finish: true,
|
|
1682
|
-
usage:
|
|
1695
|
+
usage: totalUsage
|
|
1683
1696
|
};
|
|
1684
1697
|
} else if (toolName === "validate_element_state") {
|
|
1685
1698
|
const { idx, validations } = toolInput;
|
|
@@ -2187,7 +2200,7 @@ ${report}` });
|
|
|
2187
2200
|
return { ...a, value: finalValue, selector: c?.selector, frameIdx: c?.frameIdx, description: c?.name, type: c?.type };
|
|
2188
2201
|
}),
|
|
2189
2202
|
finish: input.finish,
|
|
2190
|
-
usage:
|
|
2203
|
+
usage: totalUsage
|
|
2191
2204
|
};
|
|
2192
2205
|
}
|
|
2193
2206
|
}
|
|
@@ -2450,6 +2463,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2450
2463
|
const maxSteps = 30;
|
|
2451
2464
|
let lastSeenSuccessToast = "";
|
|
2452
2465
|
let accumulatedCost = 0;
|
|
2466
|
+
let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
2453
2467
|
const saveMissionResults = () => {
|
|
2454
2468
|
if (resultsSaved)
|
|
2455
2469
|
return;
|
|
@@ -2471,73 +2485,102 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2471
2485
|
}
|
|
2472
2486
|
}
|
|
2473
2487
|
}
|
|
2488
|
+
const effectiveLocalSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
|
|
2474
2489
|
const missionData = {
|
|
2475
2490
|
prompt,
|
|
2476
2491
|
steps: history,
|
|
2477
|
-
steps_data:
|
|
2492
|
+
steps_data: effectiveLocalSteps,
|
|
2478
2493
|
success: finalSuccess,
|
|
2479
2494
|
error: hasCriticalError,
|
|
2480
2495
|
timestamp: Date.now()
|
|
2481
2496
|
};
|
|
2482
2497
|
memories.push(missionData);
|
|
2483
2498
|
fs2.writeFileSync(memoryFile, JSON.stringify(memories, null, 2));
|
|
2484
|
-
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${
|
|
2499
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${effectiveLocalSteps.length} pasos nuevos`);
|
|
2485
2500
|
} catch (err) {
|
|
2486
2501
|
console.error("\u274C Fall\xF3 al guardar memoria:", err);
|
|
2487
2502
|
}
|
|
2488
2503
|
try {
|
|
2489
2504
|
const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
|
|
2490
|
-
fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError }, null, 2));
|
|
2505
|
+
fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2506
|
+
try {
|
|
2507
|
+
fs2.appendFileSync(path2.join(contextDir, "arcality-history.log"), JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), mission: prompt.substring(0, 150), success: finalSuccess, steps: stepCount, usage: totalMissionUsage }) + "\n");
|
|
2508
|
+
} catch (e) {
|
|
2509
|
+
}
|
|
2491
2510
|
} catch (err) {
|
|
2492
2511
|
}
|
|
2493
2512
|
};
|
|
2494
2513
|
let collectiveMemoryPersisted = false;
|
|
2495
|
-
const persistCollectiveMemory = async () => {
|
|
2496
|
-
if (collectiveMemoryPersisted)
|
|
2497
|
-
return;
|
|
2498
|
-
collectiveMemoryPersisted = true;
|
|
2514
|
+
const persistCollectiveMemory = async (isFailed = false) => {
|
|
2499
2515
|
const pid = process.env.ARCALITY_PROJECT_ID || "";
|
|
2500
2516
|
const apiUrl = process.env.ARCALITY_API_URL || "";
|
|
2501
2517
|
const apiKey = process.env.ARCALITY_API_KEY || "";
|
|
2502
2518
|
if (!pid || !apiUrl || !apiKey) {
|
|
2503
|
-
console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}'
|
|
2519
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}' \u2192 NO persiste.`);
|
|
2504
2520
|
return;
|
|
2505
2521
|
}
|
|
2506
|
-
|
|
2522
|
+
if (collectiveMemoryPersisted && !isFailed) {
|
|
2523
|
+
console.log(`[CollectiveMemory] \u2139\uFE0F Ya persistido como \xE9xito. Skip duplicado.`);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (!isFailed && stepsData.length === 0 && stepsDataBackup.length === 0) {
|
|
2527
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F stepsData vac\xEDo en path de \xE9xito \u2014 intentando usar backup...`);
|
|
2528
|
+
}
|
|
2529
|
+
const effectiveSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
|
|
2530
|
+
console.log(`
|
|
2531
|
+
======================================================`);
|
|
2532
|
+
console.log(`\u{1F9E0} [MEMORIA COLECTIVA - INGESTA DE APRENDIZAJE]`);
|
|
2533
|
+
console.log(`>>arcality>> Persistiendo conocimiento (project: ${pid}, steps: ${effectiveSteps.length}, isFailed: ${isFailed})...`);
|
|
2534
|
+
console.log(`======================================================
|
|
2535
|
+
`);
|
|
2507
2536
|
try {
|
|
2508
|
-
const stepsNarrative =
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Knowledge guardado: ${knowledgeId}`);
|
|
2516
|
-
else
|
|
2517
|
-
console.warn(`[CollectiveMemory] \u26A0\uFE0F pushKnowledge retorn\xF3 null (revisar HTTP status arriba)`);
|
|
2518
|
-
const uniqueUrls = [...new Set(stepsData.map((s) => s.url).filter(Boolean))];
|
|
2519
|
-
if (uniqueUrls.length > 1) {
|
|
2520
|
-
const ruleId = await pushRule({
|
|
2521
|
-
rule_type: "NAVIGATION",
|
|
2522
|
-
title: `Flujo de navegaci\xF3n: ${prompt.substring(0, 80)}`,
|
|
2523
|
-
description: `Agente naveg\xF3 ${uniqueUrls.length} p\xE1ginas: ${uniqueUrls.join(" \u2192 ").substring(0, 400)}`,
|
|
2524
|
-
severity: "suggestion"
|
|
2537
|
+
const stepsNarrative = effectiveSteps.map((s) => `${s.action_data?.action || "action"} "${s.componentName}" en ${s.url}`).join(" \u2192 ").substring(0, 500);
|
|
2538
|
+
if (!isFailed) {
|
|
2539
|
+
collectiveMemoryPersisted = true;
|
|
2540
|
+
const knowledgeId = await pushKnowledge({
|
|
2541
|
+
doc_type: "PROCESS",
|
|
2542
|
+
title: `Flujo exitoso: ${prompt.substring(0, 100)}`,
|
|
2543
|
+
content: stepsNarrative || prompt.substring(0, 500)
|
|
2525
2544
|
});
|
|
2526
|
-
if (
|
|
2527
|
-
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2545
|
+
if (knowledgeId)
|
|
2546
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Knowledge guardado: ${knowledgeId}`);
|
|
2547
|
+
else
|
|
2548
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F pushKnowledge retorn\xF3 null (revisar HTTP status arriba)`);
|
|
2549
|
+
const uniqueUrls = [...new Set(effectiveSteps.map((s) => s.url).filter(Boolean))];
|
|
2550
|
+
if (uniqueUrls.length > 1) {
|
|
2551
|
+
const ruleId = await pushRule({
|
|
2552
|
+
rule_type: "NAVIGATION",
|
|
2553
|
+
title: `Flujo de navegaci\xF3n: ${prompt.substring(0, 80)}`,
|
|
2554
|
+
description: `Agente naveg\xF3 ${uniqueUrls.length} p\xE1ginas: ${uniqueUrls.join(" \u2192 ").substring(0, 400)}`,
|
|
2555
|
+
severity: "suggestion"
|
|
2556
|
+
});
|
|
2557
|
+
if (ruleId)
|
|
2558
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla NAVIGATION guardada: ${ruleId}`);
|
|
2559
|
+
}
|
|
2560
|
+
const submitStep = effectiveSteps.find(
|
|
2561
|
+
(s) => s.action_data?.action === "click" && (s.componentName?.toLowerCase().includes("guardar") || s.componentName?.toLowerCase().includes("save") || s.componentName?.toLowerCase().includes("crear") || s.componentName?.toLowerCase().includes("registrar"))
|
|
2562
|
+
);
|
|
2563
|
+
if (submitStep) {
|
|
2564
|
+
const ruleId2 = await pushRule({
|
|
2565
|
+
rule_type: "UI_UX",
|
|
2566
|
+
title: `Patr\xF3n de guardado: ${prompt.substring(0, 60)}`,
|
|
2567
|
+
description: `Flujo exitoso incluy\xF3 guardado/creaci\xF3n. Pasos: ${stepsNarrative.substring(0, 400)}`,
|
|
2568
|
+
severity: "important"
|
|
2569
|
+
});
|
|
2570
|
+
if (ruleId2)
|
|
2571
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla UI_UX guardada: ${ruleId2}`);
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
const failRuleId = await pushRule({
|
|
2575
|
+
rule_type: "VALIDATION",
|
|
2576
|
+
title: `Misi\xF3n fallida: ${prompt.substring(0, 80)}`,
|
|
2577
|
+
description: `El agente no pudo completar: "${prompt.substring(0, 200)}". \xDAltimos pasos: ${stepsNarrative.substring(0, 300)}`,
|
|
2537
2578
|
severity: "important"
|
|
2538
2579
|
});
|
|
2539
|
-
if (
|
|
2540
|
-
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla
|
|
2580
|
+
if (failRuleId)
|
|
2581
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla de fallo guardada: ${failRuleId}`);
|
|
2582
|
+
else
|
|
2583
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F No se pudo persistir misi\xF3n fallida`);
|
|
2541
2584
|
}
|
|
2542
2585
|
} catch (err) {
|
|
2543
2586
|
console.warn(`[CollectiveMemory] \u274C Error inesperado en persistCollectiveMemory: ${err?.message}`);
|
|
@@ -2679,7 +2722,12 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2679
2722
|
response = null;
|
|
2680
2723
|
} else {
|
|
2681
2724
|
guideStepCount++;
|
|
2682
|
-
console.log(
|
|
2725
|
+
console.log(`
|
|
2726
|
+
======================================================`);
|
|
2727
|
+
console.log(`\u{1F680} [USANDO GU\xCDA DE \xC9XITO - MEMORIA COLECTIVA AHORRANDO TOKENS]`);
|
|
2728
|
+
console.log(`>>arcality>> Paso validado (Gu\xEDa #${guideStepCount}, NO consume turno IA)`);
|
|
2729
|
+
console.log(`======================================================
|
|
2730
|
+
`);
|
|
2683
2731
|
}
|
|
2684
2732
|
}
|
|
2685
2733
|
if (!response) {
|
|
@@ -2688,8 +2736,15 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2688
2736
|
response = await agent.askIA(prompt, history, stepCount);
|
|
2689
2737
|
if (response.usage) {
|
|
2690
2738
|
const inputs = response.usage.input_tokens || 0;
|
|
2739
|
+
const cacheCreates = response.usage.cache_creation_input_tokens || 0;
|
|
2740
|
+
const cacheReads = response.usage.cache_read_input_tokens || 0;
|
|
2691
2741
|
const outputs = response.usage.output_tokens || 0;
|
|
2692
|
-
|
|
2742
|
+
totalMissionUsage.input_tokens += inputs;
|
|
2743
|
+
totalMissionUsage.output_tokens += outputs;
|
|
2744
|
+
totalMissionUsage.cache_creation_input_tokens += cacheCreates;
|
|
2745
|
+
totalMissionUsage.cache_read_input_tokens += cacheReads;
|
|
2746
|
+
const totalInputs = inputs + cacheCreates;
|
|
2747
|
+
const cost = totalInputs / 1e6 * 3 + cacheReads / 1e6 * 0.3 + outputs / 1e6 * 15;
|
|
2693
2748
|
accumulatedCost += cost;
|
|
2694
2749
|
if (accumulatedCost >= 6) {
|
|
2695
2750
|
console.warn(`
|
|
@@ -2702,7 +2757,12 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2702
2757
|
}
|
|
2703
2758
|
}
|
|
2704
2759
|
}
|
|
2705
|
-
console.log(
|
|
2760
|
+
console.log(`
|
|
2761
|
+
======================================================`);
|
|
2762
|
+
console.log(`\u{1F916} [RAZONAMIENTO DEL AGENTE]`);
|
|
2763
|
+
console.log(`${response.thought}`);
|
|
2764
|
+
console.log(`======================================================
|
|
2765
|
+
`);
|
|
2706
2766
|
if (!response.finish && history.length >= 6) {
|
|
2707
2767
|
const lastSixActions = history.slice(-6);
|
|
2708
2768
|
const clickActions = lastSixActions.filter((h) => h.includes("click en"));
|
|
@@ -3073,6 +3133,11 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
|
|
|
3073
3133
|
}
|
|
3074
3134
|
saveMissionResults();
|
|
3075
3135
|
if (!aiMarkedSuccess) {
|
|
3136
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
|
|
3137
|
+
await persistCollectiveMemory(true).catch(() => {
|
|
3138
|
+
});
|
|
3076
3139
|
throw new Error("Misi\xF3n no completada o finalizada con errores.");
|
|
3140
|
+
} else {
|
|
3141
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
|
|
3077
3142
|
}
|
|
3078
3143
|
});
|