@johpaz/hive-sdk 0.2.0 → 0.3.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 (86) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/README.md +11 -3
  3. package/package.json +10 -4
  4. package/packages/core/src/agent/agent-catalog.ts +81 -24
  5. package/packages/core/src/agent/compaction.ts +20 -1
  6. package/packages/core/src/agent/context-compiler.ts +6 -3
  7. package/packages/core/src/agent/conversation-store.ts +136 -2
  8. package/packages/core/src/agent/curator.ts +12 -3
  9. package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
  10. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
  11. package/packages/core/src/agent/playbook-selector.ts +18 -3
  12. package/packages/core/src/agent/prompt-builder.ts +2 -2
  13. package/packages/core/src/agent/providers/index.ts +36 -1
  14. package/packages/core/src/agent/reflector.ts +32 -9
  15. package/packages/core/src/agent/skill-selector.ts +2 -2
  16. package/packages/core/src/agent/thread-store.ts +43 -0
  17. package/packages/core/src/agent/tool-selector.ts +2 -0
  18. package/packages/core/src/api/createAgent.ts +66 -1
  19. package/packages/core/src/artifacts/index.ts +15 -0
  20. package/packages/core/src/artifacts/store.ts +77 -2
  21. package/packages/core/src/canvas/index.ts +9 -0
  22. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  23. package/packages/core/src/events/index.ts +18 -0
  24. package/packages/core/src/events/tool-narration.ts +4 -0
  25. package/packages/core/src/gateway/channel-notify.ts +103 -6
  26. package/packages/core/src/gateway/durable-queue.ts +13 -1
  27. package/packages/core/src/gateway/index.ts +3 -0
  28. package/packages/core/src/gateway/job-store.ts +6 -0
  29. package/packages/core/src/harness/executors.ts +493 -0
  30. package/packages/core/src/harness/index.ts +12 -2
  31. package/packages/core/src/hooks/index.ts +203 -0
  32. package/packages/core/src/images/index.ts +161 -0
  33. package/packages/core/src/index.ts +1 -0
  34. package/packages/core/src/multimodal/vision-service.ts +45 -13
  35. package/packages/core/src/resilience/index.ts +13 -0
  36. package/packages/core/src/scheduler/CronScheduler.ts +48 -21
  37. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  38. package/packages/core/src/scheduler/cron/index.ts +10 -0
  39. package/packages/core/src/scheduler/cron/job.ts +339 -0
  40. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  41. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  42. package/packages/core/src/scheduler/index.ts +21 -3
  43. package/packages/core/src/scheduler/integration.ts +16 -5
  44. package/packages/core/src/scheduler/types.ts +3 -18
  45. package/packages/core/src/services/agents.ts +268 -0
  46. package/packages/core/src/services/cron.ts +257 -0
  47. package/packages/core/src/services/endpoints.ts +289 -0
  48. package/packages/core/src/services/ethics.ts +107 -0
  49. package/packages/core/src/services/images.ts +212 -0
  50. package/packages/core/src/services/index.ts +112 -0
  51. package/packages/core/src/services/mcp.ts +201 -0
  52. package/packages/core/src/services/memory.ts +133 -0
  53. package/packages/core/src/services/models.ts +179 -0
  54. package/packages/core/src/services/providers.ts +152 -0
  55. package/packages/core/src/services/setup.ts +222 -0
  56. package/packages/core/src/services/skills.ts +241 -0
  57. package/packages/core/src/services/swarms.ts +307 -0
  58. package/packages/core/src/services/tools.ts +106 -0
  59. package/packages/core/src/sessions/index.ts +5 -3
  60. package/packages/core/src/sessions/resolve.ts +108 -0
  61. package/packages/core/src/skills/SkillLoader.ts +8 -1
  62. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  63. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  64. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  65. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  66. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  67. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  68. package/packages/core/src/storage/bootstrap.ts +74 -5
  69. package/packages/core/src/storage/collections.ts +106 -1
  70. package/packages/core/src/storage/crypto.ts +24 -7
  71. package/packages/core/src/storage/index.ts +2 -1
  72. package/packages/core/src/storage/onboarding.ts +59 -43
  73. package/packages/core/src/storage/reconcile.ts +6 -1
  74. package/packages/core/src/storage/seed.ts +89 -11
  75. package/packages/core/src/swarm/types.ts +3 -18
  76. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  77. package/packages/core/src/tool-runtime/index.ts +129 -14
  78. package/packages/core/src/tools/agents/index.ts +18 -60
  79. package/packages/core/src/tools/cli/index.ts +55 -0
  80. package/packages/core/src/tools/core/index.ts +50 -2
  81. package/packages/core/src/tools/cron/index.ts +4 -4
  82. package/packages/core/src/tools/images/index.ts +130 -0
  83. package/packages/core/src/tools/index.ts +14 -1
  84. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  85. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  86. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
package/CHANGELOG.md CHANGED
@@ -2,8 +2,154 @@
2
2
 
3
3
  ## Sin publicar
4
4
 
5
+ ### Corregido
6
+
7
+ - **`browser_scrape` extraía con una tool que no ve lo que el navegador
8
+ renderizó.** La skill existe para sitios dinámicos, y su paso de extracción
9
+ usaba `web_fetch`, que vuelve a pedir la URL al servidor y recibe el HTML sin
10
+ JavaScript ejecutado: en un SPA, una cáscara vacía. Pasa a usar
11
+ `browser_extract`, que lee el DOM ya renderizado, con un `browser_wait` previo.
12
+ (Uno de sus ejemplos citaba además `browser_fetch`, que no existe.)
13
+
14
+ - **`browser_automate` no esperaba a los elementos.** Sus pasos iban de navegar a
15
+ hacer clic sin `browser_wait` en el medio, que es la falla más común de la
16
+ automatización web y falla en silencio. Se agregó el paso y el orden de
17
+ escalada: selector → `browser_script` → `computer_use_task`.
18
+
19
+ - **La tabla de campos de `cron_manager` omitía la mitad de lo que acepta la
20
+ tool**: `max_runs`, `payload`, `agent_id` y `tool_name`. También documentaba
21
+ expresiones de 5 campos cuando el motor acepta 6, y no aclaraba que la zona
22
+ horaria sale del perfil del usuario y no se pasa en la llamada.
23
+
24
+ - **Los contadores del scheduler perdían actualizaciones.** `run_count` y
25
+ `error_count` se calculaban desde una lectura hecha antes del bucle de
26
+ reintento de `updateJob`, así que al reintentar por conflicto de versión se
27
+ reescribía el valor viejo. Con dos corridas solapadas del mismo job —normal en
28
+ uno que tarda más que su intervalo y no declara `protect`, y garantizado en la
29
+ puesta al día por misfire, que llama a `execute()` en paralelo con el job ya
30
+ activado— ambas leían `error_count: 4` y ambas escribían 5. La consecuencia no
31
+ era el número: es que el umbral de auto-pausa (5 errores seguidos) no se
32
+ alcanzaba nunca y un job que fallaba siempre se quedaba reintentando para
33
+ siempre. `updateJob` ahora acepta un parche en forma de función, que se evalúa
34
+ contra la lectura fresca de cada intento. Es el mismo error que ya se corrigió
35
+ en `touchThread`. Cubierto por `packages/core/src/scheduler/scheduler.test.ts`.
36
+
37
+ ### Agregado
38
+
39
+ - **El seed inicial de especialistas es una elección.** `ensureHiveDb()` y
40
+ `seedAllData()` aceptan `specialists: "all" | "none" | string[]`. Con `"none"`
41
+ la colmena arranca sin ningún especialista y con sólo las `MINIMAL_TOOLS`
42
+ activas —la competencia del coordinador—, y son los enjambres los que traen
43
+ consigo a los suyos. `"all"` sigue siendo el default, así que nada cambia para
44
+ quien no lo pida.
45
+
46
+ La elección alcanza también a las **capacidades**, no sólo a los agentes: una
47
+ fila de tool o skill que nace en un arranque `"none"` nace inactiva. Antes
48
+ `active` defaulteaba a `true` para toda fila nueva, así que un arranque sin
49
+ especialistas dejaba igual las 62 tools encendidas — el usuario terminaba
50
+ apagando a mano lo que nunca pidió.
51
+
52
+ **Nunca borra.** La elección gobierna qué se crea, no qué se conserva: una
53
+ base que ya tiene sus ocho agentes no pierde ninguno por arrancar con
54
+ `"none"`, y se siguen reconciliando en cada arranque.
55
+
56
+ `createSwarm` acepta ahora miembros del catálogo que todavía no tienen fila:
57
+ con el seed en `"none"` un enjambre es el **pedido de instalación**, no una
58
+ referencia a algo que ya debería existir. Un id que no es del catálogo y no
59
+ existe sigue siendo un error.
60
+
61
+ - **Crear un enjambre ahora siembra sus especialistas.** El seed selectivo
62
+ (`applySeedPlan`) dejaba elegir qué personas del catálogo instalar, pero
63
+ `createSwarm` no lo miraba: guardaba el enjambre **sin una queja** con
64
+ miembros apagados y sus tools inactivas. La validación de "el agente existe"
65
+ pasaba igual, porque el seed crea las 8 filas siempre y sólo cambia `enabled`
66
+ — el enjambre quedaba definido y sin poder trabajar.
67
+
68
+ `createSwarm` y `updateSwarm` aceptan `activateMembers`, **`false` por
69
+ defecto**: crear un enjambre no debería cambiar en silencio qué capacidades
70
+ tiene la instalación entera, así que sin él el enjambre se crea igual y el
71
+ faltante vuelve en `pendingActivation` para que la UI lo muestre y el usuario
72
+ decida. Con `true` se activa la unión con lo que ya estaba, de modo que
73
+ encender los especialistas de un enjambre nunca apaga los de otro.
74
+
75
+ Se agregó `planActivationFor(agentIds)`, que devuelve el faltante **sin
76
+ encender nada** —para el "esto se va a activar" antes de confirmar— y
77
+ `enableCatalogAgents(ids)` en plural, porque activarlos de a uno reescribía el
78
+ catálogo entero una vez por agente. Cubierto por `test/swarm-seed.test.ts`.
79
+
80
+ - **Skills para las capacidades que no tenían ninguna.** `image_editor`
81
+ (`image_metadata`, `image_transform`, `artifact_inspect`) y `artifact_reader`
82
+ (`artifact_read`, `artifact_inspect`). Las tools existían pero ninguna skill
83
+ las enseñaba, así que el modelo sólo podía dar con ellas de casualidad vía
84
+ `search_knowledge` — y en el caso de los artefactos eso deja inerte todo el
85
+ mecanismo de `artifact_ref`, que existe justamente para que los archivos
86
+ grandes no entren en la ventana de contexto.
87
+
88
+
89
+ - **`sessionStart` y `sessionEnd` ya se disparan.** Eran registrables desde que
90
+ se implementaron los hooks, pero nada los invocaba. Van enganchados a las
91
+ cuatro transiciones del ciclo de vida del hilo (crear, reabrir, archivar,
92
+ borrar), todas en `agent/thread-store.ts`. `sessionStart` cuelga del `put` que
93
+ crea la fila y no de `createSession`, que es idempotente y se llama en cada
94
+ turno: enganchado ahí habría contado mensajes en vez de conversaciones.
95
+ `closeSession`/`reopenSession` pasan a delegar en los nuevos `archiveThread`
96
+ y `unarchiveThread` para que las cuatro transiciones vivan en un solo archivo.
97
+ Cubierto por `test/hooks.test.ts`.
98
+
99
+ ### Quitado
100
+
101
+ - **Cero dependencias para el cron: fuera `croner` y `cron-parser`.** El motor
102
+ ahora es propio (`scheduler/cron/`) y usa sólo `setTimeout` e `Intl` del
103
+ runtime. `cron-parser` además ni siquiera se importaba: estaba declarada en
104
+ los dos `package.json` y se la bajaba todo el que instalara el SDK.
105
+
106
+ `Bun.cron()` **no** sirve como reemplazo —evaluado contra el runtime 1.4.0—:
107
+ acepta sólo 5 campos, rechaza una fecha ISO como patrón (que es como se
108
+ agendan los jobs `one_shot`), ignora la zona horaria en `parse()`, y su handle
109
+ no expone la próxima corrida, de donde sale `next_run_at` y con lo que se
110
+ detectan las corridas perdidas al arrancar. Tampoco tiene equivalente de
111
+ `protect`, `maxRuns`, `interval`, `startAt`/`stopAt` ni `domAndDow`, todos
112
+ campos persistidos de `CronJobDoc`.
113
+
114
+ El motor propio conserva la superficie entera, así que `CronScheduler` no
115
+ cambió de comportamiento, e implementa además los dos casos de horario de
116
+ verano que se rompen callados: la hora que **no existe** al adelantar el
117
+ reloj (se saltea ese día en vez de correr a una hora inventada) y la que
118
+ **ocurre dos veces** al atrasarlo (corre en la primera, una sola vez). El
119
+ motor se exporta suelto desde `./scheduler` —`Cron`, `parseCronExpression`,
120
+ `isValidCronExpression`, `nextOccurrence`— para validar o previsualizar sin
121
+ montar un scheduler. Documentado en `docs/API-CRON.md`. Cubierto por
122
+ `packages/core/src/scheduler/cron/cron-engine.test.ts` (28 tests).
123
+
124
+ - **`CronerOptions` (tipo público).** Estaba declarado dos veces —en
125
+ `scheduler/types.ts` y en `swarm/types.ts`— y no tipaba nada en ninguna parte:
126
+ un tipo muerto con el nombre de una librería que ya no se usa. Su forma es la
127
+ de `CronOptions`, que ahora exporta el motor desde `./scheduler`.
128
+
129
+ - **Menciones a Croner en lo que lee el modelo.** Las descripciones de
130
+ `cron.create` (`start_at`, `stop_at`, `dom_and_dow`) y la skill `cron_manager`
131
+ citaban opciones "de Croner". Eso entra en el prompt: nombrarle al modelo una
132
+ librería que el código ya no usa lo manda a buscar documentación que no
133
+ aplica. Quedan sólo las referencias históricas que explican por qué el motor
134
+ es propio.
135
+
5
136
  ### Seguridad
6
137
 
138
+ - **El playbook ACE no distinguía de quién era lo aprendido.** `PlaybookDoc` y
139
+ `ReflectionDoc` no tenían `user_id`, así que la cadena entera —trazas →
140
+ reflexión → regla → inyección en el system prompt— era global: lo que el
141
+ agente aprendía interactuando con una persona se le aplicaba a todas las demás
142
+ del mismo proceso. Es el mismo supuesto de "un solo usuario" que ya se había
143
+ cerrado en `memory`. Ahora el reflector agrupa las trazas por usuario
144
+ (derivado del `thread_id`), el curador propaga el dueño a la regla y deduplica
145
+ dentro del usuario, y las tres puertas de lectura filtran a global + propio:
146
+ `selectPlaybookRules(texto, userId)`, `EthicsGuard.getRules(rol, userId)` y la
147
+ tool `search_knowledge`. Las reglas sembradas siguen siendo globales a
148
+ propósito (`user_id: ""`): son conocimiento del producto, no de nadie.
149
+ `ensureHiveDb()` migra las filas anteriores asignándolas al primer usuario de
150
+ la base — dejarlas sin dueño las volvería globales, que es justo lo que se
151
+ viene a cerrar. Cubierto por `test/playbook-isolation.test.ts`.
152
+
7
153
  - **La lista blanca de tools no se aplicaba al descubrimiento dinámico.**
8
154
  `compileContext` sólo recortaba `allTools` cuando el agente era de catálogo
9
155
  (`source === "catalog"`). Un agente creado por el usuario veía su loadout
@@ -51,6 +197,15 @@
51
197
  Ahora el test usa un id de proveedor propio (`test-tenant-isolation`) y limpia
52
198
  sus secretos, sin tocar el módulo ni la credencial de nadie.
53
199
 
200
+ - **El caché de disponibilidad del keychain se envenenaba para todo el proceso.**
201
+ `_keychainOk` recuerda si `Bun.secrets` respondió, para no reintentar en cada
202
+ lectura en un servidor sin libsecret. El problema es que ese resultado valía
203
+ para siempre: una vez marcado como no disponible, sustituir `Bun.secrets` por
204
+ otro backend —o por un doble de test— no servía de nada, porque la lectura
205
+ cortaba antes de tocarlo. Ahora se detecta que el objeto cambió de identidad y
206
+ el sondeo se invalida solo. Era la causa de que el test de compatibilidad con
207
+ keychain fallara en CI headless (y sólo ahí).
208
+
54
209
  - **`resetKeychainProbe()`** en `storage/crypto.ts`. Si el keychain del SO no
55
210
  responde, el resultado se cachea a nivel de módulo para no reintentar en cada
56
211
  lectura — correcto en producción, pero significa que el primer sondeo vale
@@ -89,8 +244,82 @@
89
244
 
90
245
  - CI actualizado a **Bun 1.4.0**, alineado con `hive`.
91
246
 
247
+ ### Quitado
248
+
249
+ - **`AgentRunner`** (`agent/providers/index.ts`). Era una capa de compatibilidad
250
+ con la firma de LangGraph anterior a que el runtime pasara a `agent-loop.ts`, y
251
+ **nunca llegó a instanciarse**: los cuatro puntos de entrada reales —el
252
+ gateway, `createAgent`, el worker y los ejecutores del harness— llaman
253
+ `runAgent()` directo. 158 líneas de código muerto. El subpath
254
+ `@johpaz/hive-sdk/agent/providers` sigue existiendo con sus tipos (`Provider`,
255
+ `ModelResponse`), que sí son parte del contrato público.
256
+
92
257
  ### Añadido
93
258
 
259
+ - **Streaming por token en la API pública.** `chat(mensaje, { stream: true })`
260
+ emite eventos `token` con los deltas del proveedor a medida que llegan. El
261
+ mecanismo ya existía —los proveedores llamaban `onToken` por cada delta— pero
262
+ **ningún punto de entrada lo pasaba**, así que nunca llegaba a nadie: la
263
+ respuesta aparecía de golpe al terminar el turno.
264
+
265
+ - **`@johpaz/hive-sdk/services/images`** — imágenes como servicio para el usuario
266
+ final, no para el agente: entra y sale por bytes, se persiste por id. Incluye
267
+ galería (`listImages`), presets y control de retención.
268
+
269
+ - **`@johpaz/hive-sdk/services` — la superficie que maneja una interfaz.** El SDK
270
+ estaba construido para que lo condujera el modelo: casi todo el CRUD vivía
271
+ dentro de las tools (`cronCreateTool`, `memoryWriteTool`, `agentCreateTool`),
272
+ con argumentos con forma de LLM y respuestas escritas para un prompt. Montar
273
+ una UI encima obligaba a llamar `tool.execute({...})` y parsear prosa, o a
274
+ escribir consultas crudas contra HiveDB conociendo un esquema privado.
275
+
276
+ Ahora la implementación vive en `services/` y las tools la envuelven — una
277
+ implementación, dos consumidores. Diez dominios: `agents`, `swarms`, `skills`,
278
+ `tools`, `providers`, `models`, `mcp`, `cron`, `memory`, `ethics`. Es
279
+ deliberadamente agnóstico del framework (funciones, no rutas HTTP): una app
280
+ móvil o de escritorio que embeba el runtime no quiere un servidor.
281
+
282
+ Añade tres cosas que hive no hace: **valida que las referencias existan** al
283
+ asignar tools/skills/MCP a un agente (hive las guarda sin comprobar, y el
284
+ error aparece cuando el agente intenta usarlas); **`testMcpServer()`**, que
285
+ allí es "guardá y esperá a que el hot-reload conecte"; y el **rename de modelo
286
+ transaccional**, que re-apunta a cada agente en el mismo `batch()`.
287
+
288
+ - **`SwarmDoc` — los enjambres se pueden guardar.** Hasta acá un enjambre existía
289
+ sólo mientras corría: `runRoleSwarm()` recibe los agentes en la llamada y no
290
+ persiste nada, así que quien armara uno desde una interfaz lo perdía al cerrar
291
+ la ventana. Era el bloqueador real para poner una UI encima del SDK, y explica
292
+ por qué hive-cloud creó sus propias tablas en Postgres.
293
+
294
+ La validación ocurre **al guardar, no al correr**: un enjambre jerárquico sin
295
+ orquestador, o con un agente que ya no existe, es un error de configuración —
296
+ descubrirlo semanas después, cuando alguien lo ejecuta, es descubrirlo tarde.
297
+
298
+ - **El harness trae ejecutores listos** (`initHarnessExecutors()`). La cola
299
+ durable sabía encolar, reintentar y recuperar tras un crash, pero no ejecutar:
300
+ registrar los ejecutores quedaba en manos de quien usara el SDK, y eso son
301
+ ~420 líneas de cableado —epoch, proof packets, criterios de aceptación,
302
+ fan-in de delegaciones— antes de correr un solo enjambre durable. Ahora vienen
303
+ `worker_task` (worker delegado en contexto aislado, con verificación de sus
304
+ criterios) y `goal_run` (varios turnos contra un objetivo hasta verificarlo o
305
+ agotar el presupuesto).
306
+
307
+ `chat_turn` no está a propósito: qué es un canal y cómo se transmite un token
308
+ lo define la aplicación. Se registra desde fuera con `registerExecutor()`.
309
+ Registrar sigue siendo opt-in — `initHarnessExecutors()` no se llama sola.
310
+
311
+ - **`getRegisteredExecutorTypes()`** — el registro era privado, así que no había
312
+ forma de comprobar si un tipo quedó cableado. Un job encolado sin ejecutor no
313
+ falla al encolarse sino al tomarse, lejos de donde está el error.
314
+
315
+ - **Superficie pública completa**: 33 subpaths (antes 28). `events/` y
316
+ `resilience/` no tenían barril, `canvas/` no exportaba su emitter, `artifacts/`
317
+ no existía como módulo, y `./events` apuntaba a un solo archivo — el
318
+ **agent-bus**, que es la mensajería entre workers de un enjambre, era
319
+ inalcanzable desde fuera. También se exponen `./tool-runtime`, `./channels`,
320
+ `./voice`, y `initializeBrowserService`/`activateBrowserTools`, sin los cuales
321
+ las browser tools estaban en el catálogo pero nadie podía arrancarlas.
322
+
94
323
  - **`@johpaz/hive-sdk/sessions`** — la conversación de un usuario como una sola
95
324
  cosa. Hasta acá "sesión" estaba repartida entre `thread-store` (identidad),
96
325
  `conversation-store` (mensajes), `run-store` (ejecución) y un `Map` en memoria
@@ -124,6 +353,83 @@
124
353
 
125
354
  ### Corregido
126
355
 
356
+ - **Las notificaciones no llegaban a ningún lado.** `notifyChannel` era un stub
357
+ que sólo hacía `console.log`, y está en el camino real: la tool `notify`, los
358
+ reportes de progreso, el aviso de que una tarea programada terminó, el de un
359
+ turno interrumpido por un crash. Un agente sobre el SDK **no podía hablarle al
360
+ usuario por ningún canal**, mientras `channels/manager.ts` tenía adaptadores
361
+ funcionales de Slack, Discord, Telegram y WhatsApp sin nada que los conectara.
362
+ Ahora la app registra el suyo con `setChannelManager()`; sin registro se
363
+ conserva el comportamiento anterior, pero avisando.
364
+
365
+ - **Las imágenes se reenviaban al modelo en cada turno.** `content_multimodal`
366
+ guardaba el base64 completo y `toAPIMessages` lo restauraba una y otra vez:
367
+ cinco fotos en una conversación eran cinco fotos viajando en cada turno
368
+ siguiente. Ahora se guardan como artefacto y en el historial queda una
369
+ referencia; las de los últimos mensajes se vuelven a poner en línea, porque un
370
+ modelo de visión no ve una foto desde un id. Mismo criterio que
371
+ `clearOldToolResults`.
372
+
373
+ - **`token_count` no contaba las imágenes**, así que la compactación creía que un
374
+ hilo lleno de fotos ocupaba lo que ocupa su texto y no se disparaba hasta que
375
+ el proveedor rechazaba el turno. Ahora se estiman por área, como cobran los
376
+ proveedores.
377
+
378
+ - **`agent.context.compactionThreshold` no lo leía nadie.** Estaba en el esquema
379
+ de configuración y ajustarlo no hacía nada. Una opción que no hace nada es
380
+ peor que no tenerla, porque el usuario cree que cambió algo.
381
+
382
+ - **`search_knowledge` filtraba en el lugar equivocado.** Mostraba tools fuera de
383
+ la lista blanca del agente. La ejecución sí estaba protegida, pero además de
384
+ contarle qué existe fuera de su alcance, ofrecerle algo que no puede ejecutar
385
+ es hacerle perder un turno.
386
+
387
+ - **El seed selectivo no habría sobrevivido a un reinicio.** `reseedToolsAndSkills()`
388
+ escribía `active: true` para todas las tools y skills en cada arranque, así que
389
+ la elección del usuario sobre qué capacidades quiere en su colmena duraba hasta
390
+ el próximo reinicio: apagaba lo que no usaba y volvía todo. Ahora el reseed
391
+ preserva `active` —la descripción y la categoría siguen viniendo del código,
392
+ que es su fuente de verdad—, igual que ya hacía con los modelos.
393
+
394
+ - **La memoria era global al proceso.** El id de `MemoryDoc` era sólo el título y
395
+ no había `user_id`: dos usuarios no podían tener una memoria con el mismo
396
+ nombre —la segunda pisaba la primera— y cualquiera veía la del otro. Coherente
397
+ con hive, que es mono-usuario; inservible para un runtime donde cada quien arma
398
+ su colmena. El id pasa a ser `${userId}:${title}` y toda lectura filtra por
399
+ dueño. Las filas anteriores se migran al arrancar.
400
+
401
+ - **Los ids no manejaban acentos.** "Efímero" quedaba como `ef_mero` y "Diseño"
402
+ como `dise_o`, porque la í y la ñ no son `[a-z0-9]`. Para un producto en
403
+ español eso no es cosmético. `slugify()` normaliza los diacríticos antes de
404
+ filtrar, y se aplica también a skills y servidores MCP.
405
+
406
+ - **Documentación que describía un backend retirado.** `API-TOOLS-SKILLS-CHANNELS.md`
407
+ seguía explicando cómo `agent-browser` se instalaba solo en `~/.hive/` al
408
+ primer uso — un backend que ya no existe. Reescrita para `Bun.WebView`, con la
409
+ nota de por qué se retiró. También se corrigieron los conteos (58→60 tools,
410
+ 106→110 modelos) y los pies de página congelados en `v0.0.17`.
411
+
412
+
413
+ - **Un job que moría por expiración de lease no disparaba su terminal hook.** La
414
+ ruta normal de fallo sí lo hacía; la de recuperación tras un crash, no. El
415
+ aviso al usuario y el fan-in de delegaciones se perdían en silencio justo
416
+ cuando más importaban.
417
+
418
+ - **Los artefactos de imagen no llegaban al consumidor.** El agent loop ya los
419
+ emitía (`chunk.artifacts.images`, vía mcp-result-normalizer), pero el wrapper
420
+ `AgentRunner` no los propagaba, así que una imagen producida por una tool MCP
421
+ se perdía antes de salir del SDK.
422
+
423
+ - **NVIDIA no emitía razonamiento.** NIM lo mantiene apagado por defecto y el
424
+ interruptor no es `reasoning_effort` sino `chat_template_kwargs`, con una
425
+ clave distinta por familia de modelo. Se añade el reintento sin esos extras
426
+ cuando el proveedor responde 400/422: perder el razonamiento es mejor que
427
+ perder el turno.
428
+
429
+ - **Un turno con más de una tool call podía morir por un hueco de empaquetado.**
430
+ `resolveWorkerEntry()` lanzaba si no encontraba el worker; ahora devuelve null
431
+ y degrada a hilo principal.
432
+
127
433
  - **`touchThread` perdía mensajes en el contador.** El incremento se calculaba
128
434
  fuera del reintento de `updateDoc`, así que ante un conflicto de versión el
129
435
  reintento volvía a escribir el valor viejo. Como `addMessage` la llama sin
package/README.md CHANGED
@@ -17,17 +17,25 @@ bun add @johpaz/hive-sdk
17
17
  **Hive SDK es un Agent Harness**: un marco de trabajo completo para construir, desplegar y escalar aplicaciones de agentes de IA. A diferencia de un simple wrapper de LLM, un *harness* provee todo lo necesario para que un agente opere en producción:
18
18
 
19
19
  - **Agentes**: ciclo ReAct nativo con checkpoint durable, 16 providers LLM y descubrimiento de tools/skills por búsqueda BM25.
20
- - **Catálogo**: 18 providers y 106 modelos sembrados, cada uno con su precio por millón de tokens — una sola fuente de verdad para el costo.
21
- - **Tools**: 58 tools incluidas — filesystem, web search, browser automation (`agent-browser`), APIs (`api_request`), a2ui, office, cron, delegación.
20
+ - **Catálogo**: 18 providers y 110 modelos sembrados, cada uno con su precio por millón de tokens — una sola fuente de verdad para el costo.
21
+ - **Tools**: 60 tools incluidas — filesystem, web search, browser automation (`Bun.WebView`), APIs (`api_request`), a2ui, office, cron, delegación.
22
22
  - **Skills**: 23 workflows bundled, más los tuyos con `defineSkill` y `SkillLoader`.
23
23
  - **Canales**: Telegram, Discord, WhatsApp, Slack y WebChat con `ChannelManager`.
24
24
  - **Swarm**: orquestación multi-agente con `DAGScheduler`, `TaskGraph` y `WorkerPool`.
25
25
  - **Runtime**: ejecución paralela de tools vía Bun Workers.
26
26
  - **Gateway**: servidor HTTP/WebSocket para exponer agentes como API.
27
27
  - **Memoria y estado**: HiveDB (colecciones + índice BM25), scratchpad, context compiler con compactación.
28
+ - **Servicios**: CRUD tipado de agentes, enjambres, skills, modelos, MCP y cron para montarle **la interfaz que quieras** — móvil, web o escritorio. Ver [API-SERVICES.md](./docs/API-SERVICES.md).
29
+ - **Sesiones**: un hilo por canal y por contacto, con historial, resumen y reanudación tras un corte.
30
+ - **Imágenes**: redimensionar y convertir con `Bun.Image`, sin dependencias nativas. Las imágenes entrantes se normalizan antes de llegar al modelo — una foto de cámara pasa de 217 KB a 4 KB.
31
+ - **Harness**: cola durable con leases y recuperación tras crash, y ejecutores listos (`initHarnessExecutors`).
28
32
 
29
33
  Con Hive SDK no montas un agente desde cero: **enganchas tu lógica de negocio en un harness ya armado**.
30
34
 
35
+ Y si además querés ponerle interfaz, no tenés que hablarle a la base de datos ni
36
+ imitar el formato que espera el modelo: `@johpaz/hive-sdk/services` expone el
37
+ mismo CRUD que usan las tools, en funciones tipadas.
38
+
31
39
  ## Instalación
32
40
 
33
41
  > **Requiere Bun.** El paquete se publica como TypeScript y usa APIs de Bun
@@ -222,4 +230,4 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
222
230
 
223
231
  ---
224
232
 
225
- *Hive SDK v0.2.0 — MIT*
233
+ *Hive SDK v0.3.0 — MIT*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
6
6
  "license": "MIT",
@@ -35,6 +35,9 @@
35
35
  "./tools": "./packages/core/src/tools/index.ts",
36
36
  "./skills": "./packages/core/src/skills/index.ts",
37
37
  "./storage": "./packages/core/src/storage/index.ts",
38
+ "./services": "./packages/core/src/services/index.ts",
39
+ "./hooks": "./packages/core/src/hooks/index.ts",
40
+ "./images": "./packages/core/src/images/index.ts",
38
41
  "./sessions": "./packages/core/src/sessions/index.ts",
39
42
  "./models": "./packages/core/src/models/index.ts",
40
43
  "./swarm": "./packages/core/src/swarm/index.ts",
@@ -42,7 +45,12 @@
42
45
  "./swarm/presets": "./packages/core/src/swarm/presets/index.ts",
43
46
  "./scheduler": "./packages/core/src/scheduler/index.ts",
44
47
  "./workers": "./packages/core/src/workers/index.ts",
45
- "./events": "./packages/core/src/events/event-bus.ts",
48
+ "./events": "./packages/core/src/events/index.ts",
49
+ "./resilience": "./packages/core/src/resilience/index.ts",
50
+ "./tool-runtime": "./packages/core/src/tool-runtime/index.ts",
51
+ "./channels": "./packages/core/src/channels/index.ts",
52
+ "./voice": "./packages/core/src/voice/index.ts",
53
+ "./artifacts": "./packages/core/src/artifacts/index.ts",
46
54
  "./ethics": "./packages/core/src/ethics/index.ts",
47
55
  "./canvas": "./packages/core/src/canvas/index.ts",
48
56
  "./config": "./packages/core/src/config/index.ts",
@@ -97,8 +105,6 @@
97
105
  "@slack/bolt": "^4.7.2",
98
106
  "@whiskeysockets/baileys": "7.0.0-rc11",
99
107
  "async-mutex": "^0.5.0",
100
- "cron-parser": "^5.5.0",
101
- "croner": "^10.0.1",
102
108
  "discord.js": "^14.26.4",
103
109
  "docx": "^9.6.1",
104
110
  "grammy": "^1.42.0",
@@ -3,7 +3,7 @@ import type {
3
3
  AgentAcceptanceCriterion,
4
4
  AgentModelOverride,
5
5
  AgentWorkspaceScope,
6
- } from "../storage/collections.ts";
6
+ } from "../storage/collections";
7
7
  import { col, toIndexable, fromIndexable } from "../storage/hive.ts";
8
8
  import { expandToolAllowlist } from "./delegation-runtime.ts";
9
9
 
@@ -37,7 +37,7 @@ ${s.workflow.map((step, index) => `${index + 1}. ${step}`).join("\n")}
37
37
 
38
38
  # QUÉ NO HACES
39
39
  ${s.prohibitions.map((rule) => `- ${rule}`).join("\n")}
40
- - No hablás con el usuario, no pedís confirmaciones directas y no delegás a otros agentes.
40
+ - No hablas con el usuario, no pedís confirmaciones directas y no delegas a otros agentes.
41
41
  - No ampliás el alcance ni usás tools fuera del loadout autorizado.
42
42
  - No declarás éxito sin evidencia comprobable para cada criterio.
43
43
 
@@ -72,10 +72,10 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
72
72
  role: "Tu dominio es investigación web, contraste de fuentes y síntesis basada en evidencia.",
73
73
  receives: "Una pregunta acotada, contexto relevante, restricciones de actualidad y criterios de aceptación.",
74
74
  workflow: [
75
- "Convertí la pregunta en consultas concretas y buscá fuentes primarias o autorizadas.",
76
- "Leé las fuentes relevantes y separá hechos, inferencias y datos no confirmados.",
75
+ "Convertí la pregunta en consultas concretas y busca fuentes primarias o autorizadas.",
76
+ "Lee las fuentes relevantes y separa hechos, inferencias y datos no confirmados.",
77
77
  "Contrastá afirmaciones sensibles o discutidas con más de una fuente.",
78
- "Entregá una síntesis concisa con referencias y fechas cuando sean relevantes.",
78
+ "Entrega una síntesis concisa con referencias y fechas cuando sean relevantes.",
79
79
  ],
80
80
  prohibitions: [...COMMON_PROHIBITIONS, "No automatizás formularios ni realizás acciones en sitios."],
81
81
  quality: "Cada afirmación material debe poder rastrearse a una fuente accesible; los desacuerdos se presentan explícitamente.",
@@ -92,10 +92,10 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
92
92
  role: "Tu dominio es navegación y automatización web renderizada.",
93
93
  receives: "Una acción web autorizada, URL inicial, datos permitidos, estado final esperado y límites de seguridad.",
94
94
  workflow: [
95
- "Abrí el sitio y verificá que corresponda al objetivo.",
96
- "Inspeccioná el estado antes de interactuar y usá selectores estables.",
97
- "Ejecutá solamente los clicks, escritura y esperas necesarios.",
98
- "Verificá el estado final mediante extracción y captura de pantalla.",
95
+ "Abre el sitio y verifica que corresponda al objetivo.",
96
+ "Inspeccioná el estado antes de interactuar y usa selectores estables.",
97
+ "Ejecuta solamente los clicks, escritura y esperas necesarios.",
98
+ "Verifica el estado final mediante extracción y captura de pantalla.",
99
99
  ],
100
100
  prohibitions: [...COMMON_PROHIBITIONS, "No confirmás compras, envíos, borrados o publicaciones si el principal no autorizó explícitamente ese efecto."],
101
101
  quality: "La entrega incluye URL final, estado observado y evidencia visual o estructurada posterior a la acción.",
@@ -112,10 +112,10 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
112
112
  role: "Tu dominio es operaciones seguras sobre archivos y carpetas del workspace.",
113
113
  receives: "Paths relativos o autorizados, contenido solicitado, operación exacta y estado final esperado.",
114
114
  workflow: [
115
- "Resolvé todos los paths contra el workspace y comprobá su estado inicial.",
115
+ "Resuelve todos los paths contra el workspace y comprueba su estado inicial.",
116
116
  "Aplicá la mínima operación necesaria sin tocar paths ajenos.",
117
117
  "Volvé a leer o listar el resultado para comprobarlo.",
118
- "Reportá paths exactos, cambios y evidencia de readback.",
118
+ "Reporta paths exactos, cambios y evidencia de readback.",
119
119
  ],
120
120
  prohibitions: [...COMMON_PROHIBITIONS, "No ejecutás comandos shell ni modificás repositorios fuera de la operación de archivos pedida."],
121
121
  quality: "Todos los paths permanecen dentro del workspace y su contenido o ausencia final se comprueba después de la operación.",
@@ -134,10 +134,10 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
134
134
  workflow: [
135
135
  "Inspeccioná el repositorio, convenciones y estado antes de editar.",
136
136
  "Determiná la causa o el diseño mínimo y modificá solo archivos pertinentes.",
137
- "Ejecutá checks, tests o builds proporcionales al riesgo.",
138
- "Entregá archivos cambiados, evidencia de validación y riesgos restantes.",
137
+ "Ejecuta checks, tests o builds proporcionales al riesgo.",
138
+ "Entrega archivos cambiados, evidencia de validación y riesgos restantes.",
139
139
  ],
140
- prohibitions: [...COMMON_PROHIBITIONS, "No sobrescribís cambios ajenos, no publicás y no delegás a subagentes CLI."],
140
+ prohibitions: [...COMMON_PROHIBITIONS, "No sobrescribís cambios ajenos, no publicás y no delegas a subagentes CLI."],
141
141
  quality: "El cambio satisface el comportamiento pedido, preserva compatibilidad y pasa las validaciones relevantes.",
142
142
  routingExamples: ["implementar una función", "arreglar un bug", "ejecutar tests de un proyecto"],
143
143
  tools: ["fs_*", "cli_exec"],
@@ -153,10 +153,10 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
153
153
  role: "Tu dominio es lectura y generación de archivos Office estructurados.",
154
154
  receives: "Archivo de entrada o especificación del documento, formato final, contenido y path autorizado.",
155
155
  workflow: [
156
- "Inspeccioná entradas y confirmá el formato solicitado.",
156
+ "Inspeccioná entradas y confirma el formato solicitado.",
157
157
  "Generá o extraé contenido preservando estructura y datos.",
158
- "Comprobá que el archivo existe, no está vacío y puede reabrirse.",
159
- "Entregá el path final, resumen de contenido y prueba de reapertura.",
158
+ "Comprueba que el archivo existe, no está vacío y puede reabrirse.",
159
+ "Entrega el path final, resumen de contenido y prueba de reapertura.",
160
160
  ],
161
161
  prohibitions: [...COMMON_PROHIBITIONS, "No editás formatos binarios con tools genéricas de filesystem."],
162
162
  quality: "El artefacto debe abrir sin error con la tool lectora correspondiente y contener la estructura solicitada.",
@@ -174,9 +174,9 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
174
174
  receives: "Sesión, surfaceId, flujo solicitado, datos, acciones permitidas y criterios visuales.",
175
175
  workflow: [
176
176
  "Diseñá una jerarquía pequeña con IDs únicos y un root explícito.",
177
- "Creá la superficie antes de enviar componentes.",
177
+ "Crea la superficie antes de enviar componentes.",
178
178
  "Enviá componentes válidos y después el data model enlazado.",
179
- "Comprobá acknowledgements, IDs y paths; liberá la superficie al cancelar.",
179
+ "Comprueba acknowledgements, IDs y paths; liberá la superficie al cancelar.",
180
180
  ],
181
181
  prohibitions: [...COMMON_PROHIBITIONS, "No interpretás acciones del usuario ni conversás; los eventos vuelven al principal."],
182
182
  quality: "La superficie usa el catálogo v0.9, no tiene referencias rotas y sus bindings apuntan a paths válidos.",
@@ -194,9 +194,9 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
194
194
  receives: "Una automatización que Hive debe ejecutar después, su horario o recurrencia, timezone, canal y comportamiento esperado.",
195
195
  workflow: [
196
196
  "Normalizá fecha, recurrencia y timezone sin cambiar la intención.",
197
- "Creá o modificá únicamente el job solicitado.",
197
+ "Crea o modificá únicamente el job solicitado.",
198
198
  "Consultá el job persistido y su próxima ejecución.",
199
- "Entregá ID, estado, timezone y next_run_at.",
199
+ "Entrega ID, estado, timezone y next_run_at.",
200
200
  ],
201
201
  prohibitions: [
202
202
  ...COMMON_PROHIBITIONS,
@@ -223,9 +223,9 @@ const CATALOG_PERSONAS: CatalogPersona[] = [
223
223
  receives: "Endpoint autorizado, método, headers permitidos, payload, status esperado y esquema relevante.",
224
224
  workflow: [
225
225
  "Validá método, host, payload y alcance antes del request.",
226
- "Ejecutá una sola operación idempotente o explícitamente autorizada.",
227
- "Comprobá status, headers y forma de la respuesta.",
228
- "Entregá evidencia saneada sin secretos.",
226
+ "Ejecuta una sola operación idempotente o explícitamente autorizada.",
227
+ "Comprueba status, headers y forma de la respuesta.",
228
+ "Entrega evidencia saneada sin secretos.",
229
229
  ],
230
230
  prohibitions: [...COMMON_PROHIBITIONS, "No repetís mutaciones automáticamente ni cambiás método, host o payload para forzar éxito."],
231
231
  quality: "El status y contrato observados coinciden con los criterios y la evidencia no contiene credenciales.",
@@ -284,6 +284,63 @@ export function createSeedCatalogAgents(now = Date.now()): AgentDoc[] {
284
284
 
285
285
  export const CATALOG_AGENT_IDS = CATALOG_PERSONAS.map((s) => s.id);
286
286
 
287
+ /**
288
+ * Qué tools y skills necesita un subconjunto de agentes de catálogo.
289
+ *
290
+ * Devuelve la **unión**, no la lista de cada uno, y ahí está todo el asunto: las
291
+ * tools se comparten. `web_fetch` lo declaran `web_researcher` y
292
+ * `browser_operator`; `fs_*` lo declaran `workspace_file_operator` y
293
+ * `software_engineer`. Sembrar "sólo lo de este agente" dejaría a los demás sin
294
+ * capacidades que sí necesitan, y el fallo aparecería recién cuando el modelo
295
+ * descubra por BM25 una tool que no puede ejecutar.
296
+ *
297
+ * Los patrones se devuelven **sin expandir** a propósito: `expandToolAllowlist`
298
+ * necesita el registro vivo de tools, que este módulo no debe conocer. Quien
299
+ * siembre expande.
300
+ *
301
+ * `MINIMAL_TOOLS` no se incluye acá: son del coordinador, existan o no estos
302
+ * agentes, y quien siembra las añade siempre.
303
+ */
304
+ export function requiredCapabilitiesFor(agentIds: string[]): {
305
+ toolPatterns: string[];
306
+ skills: string[];
307
+ } {
308
+ const conocidos = new Set(CATALOG_AGENT_IDS);
309
+ const desconocidos = agentIds.filter((id) => !conocidos.has(id));
310
+ if (desconocidos.length > 0) {
311
+ throw new Error(`No existen en el catálogo: ${desconocidos.join(", ")}`);
312
+ }
313
+
314
+ const pedidos = new Set(agentIds);
315
+ const toolPatterns = new Set<string>();
316
+ const skills = new Set<string>();
317
+
318
+ for (const persona of CATALOG_PERSONAS) {
319
+ if (!pedidos.has(persona.id)) continue;
320
+ for (const t of persona.tools) toolPatterns.add(t);
321
+ for (const s of persona.skills) skills.add(s);
322
+ }
323
+
324
+ return { toolPatterns: [...toolPatterns], skills: [...skills] };
325
+ }
326
+
327
+ /** Las personas del catálogo, para que una UI pueda ofrecerlas al configurar. */
328
+ export function listCatalogPersonas(): Array<{
329
+ id: string;
330
+ name: string;
331
+ description: string;
332
+ tools: string[];
333
+ skills: string[];
334
+ }> {
335
+ return CATALOG_PERSONAS.map((p) => ({
336
+ id: p.id,
337
+ name: p.name,
338
+ description: p.description,
339
+ tools: p.tools,
340
+ skills: p.skills,
341
+ }));
342
+ }
343
+
287
344
  /**
288
345
  * Writes the coordinator's provider/model onto every other agent row, so the
289
346
  * whole hive is explicitly configured instead of relying on the runtime
@@ -29,6 +29,8 @@ import { estimateTokens } from "../utils/toon.ts"
29
29
  import { callLLM, resolveProviderConfig, getDefaultLLM, type ContentPart } from "./llm-client.ts"
30
30
  import { col, fromIndexable } from "../storage/hive.ts"
31
31
  import type { AgentDoc, ModelDoc } from "../storage/collections.ts"
32
+ import { loadConfig } from "../config/loader.ts"
33
+ import { runBeforeCompaction } from "../hooks/index.ts"
32
34
 
33
35
  const log = logger.child("compaction")
34
36
 
@@ -51,8 +53,15 @@ export async function maybeCompact(
51
53
  try {
52
54
  const totalTokens = await getTotalTokens(threadId)
53
55
 
54
- // Use model's context window if available, otherwise use default
56
+ // Orden de precedencia: lo que el usuario configuró gana sobre lo que se
57
+ // deduce del modelo, y eso gana sobre la constante.
58
+ //
59
+ // `agent.context.compactionThreshold` estaba en el esquema de configuración
60
+ // y **no lo leía nadie**: alguien podía ajustarlo y no pasaba nada. Una
61
+ // opción que no hace nada es peor que no tenerla, porque el usuario cree
62
+ // que cambió algo.
55
63
  let effectiveThreshold = COMPACT_TOKEN_THRESHOLD
64
+ const configurado = loadConfig().agent?.context?.compactionThreshold
56
65
  try {
57
66
  const agentsCol = await col<AgentDoc>("agents")
58
67
  const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
@@ -69,8 +78,18 @@ export async function maybeCompact(
69
78
  }
70
79
  } catch { /* use default threshold */ }
71
80
 
81
+ if (configurado && configurado > 0) effectiveThreshold = configurado
82
+
72
83
  if (totalTokens < effectiveThreshold) return
73
84
 
85
+ // Avisar antes de comprimir: es la última oportunidad de que alguien
86
+ // conserve algo del historial que está por resumirse.
87
+ await runBeforeCompaction({
88
+ threadId,
89
+ messageCount: await getMessageCount(threadId),
90
+ totalTokens,
91
+ }).catch(() => {})
92
+
74
93
  const summary = await getSummary(threadId)
75
94
  const totalMessages = await getMessageCount(threadId)
76
95