@johpaz/hive-sdk 0.4.6 → 0.4.8
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/CHANGELOG.md +90 -0
- package/README.md +1 -1
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +83 -4
- package/docs/SECURITY-GUARDRAILS.md +21 -1
- package/docs/UPGRADING.md +20 -0
- package/package.json +1 -1
- package/packages/cli/templates/hive-app/hive.config.ts +7 -0
- package/packages/cli/templates/hive-app/src/main.ts +3 -0
- package/packages/core/src/agent/agent-loop.ts +3 -1
- package/packages/core/src/agent/capability-search.ts +72 -31
- package/packages/core/src/agent/context-compiler.ts +6 -2
- package/packages/core/src/agent/reflector.ts +23 -10
- package/packages/core/src/channels/index.ts +1 -0
- package/packages/core/src/channels/manager.ts +40 -0
- package/packages/core/src/channels/whatsapp-cloud/channel.ts +334 -0
- package/packages/core/src/channels/whatsapp-cloud/client.ts +368 -0
- package/packages/core/src/channels/whatsapp-cloud/index.ts +3 -0
- package/packages/core/src/channels/whatsapp-cloud/webhook.ts +235 -0
- package/packages/core/src/channels/whatsapp.ts +58 -28
- package/packages/core/src/config/loader.ts +3 -1
- package/packages/core/src/gateway/server.ts +24 -0
- package/packages/core/src/index.ts +17 -0
- package/packages/core/src/mcp/MCPClient.ts +9 -0
- package/packages/core/src/mcp/config.ts +2 -1
- package/packages/core/src/mcp/transports/index.ts +44 -1
- package/packages/core/src/storage/catalog.ts +354 -0
- package/packages/core/src/storage/causal-events.ts +50 -19
- package/packages/core/src/storage/hive.ts +6 -1
- package/packages/core/src/storage/index.ts +14 -1
- package/packages/core/src/storage/seed.ts +161 -128
- package/packages/core/src/tools/cron/index.ts +2 -2
- package/packages/core/src/voice/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,94 @@
|
|
|
17
17
|
procedencia, hash y una prueba funcional del OOXML generado.
|
|
18
18
|
- Añadidas las guías `docs/UPGRADING.md` y
|
|
19
19
|
`docs/SECURITY-GUARDRAILS.md` para operación y auditoría.
|
|
20
|
+
- **Requiere `@johpaz/hive-db` ^0.5.1** (antes ^0.4.0). Trae las lecturas del
|
|
21
|
+
log causal acotadas por agente (`agents` en `causalThread`, `toolStats` y
|
|
22
|
+
`buildAgentContext`), de las que depende el aislamiento entre inquilinos del
|
|
23
|
+
log causal descrito en *Corregido*.
|
|
24
|
+
|
|
25
|
+
### WhatsApp por la API oficial de Meta
|
|
26
|
+
|
|
27
|
+
- **Canal nuevo `whatsapp_cloud`**: WhatsApp Business por la Cloud API, que es
|
|
28
|
+
lo que puede usar una empresa. Incluye `WhatsAppCloudClient` (texto con
|
|
29
|
+
partido automático en 4096 caracteres, plantillas, audio, marcar leído con
|
|
30
|
+
"escribiendo…" y descarga de medios), las funciones de webhook
|
|
31
|
+
(`verifyWhatsAppSignature`, `verifyWhatsAppChallenge`, `parseWhatsAppWebhook`)
|
|
32
|
+
y el canal `WhatsAppCloudChannel`. Sin dependencias nuevas: sólo `fetch`.
|
|
33
|
+
- El gateway enruta `GET|POST /webhooks/whatsapp-cloud/:accountId` cuando se le
|
|
34
|
+
pasa `channelManager`. Meta exige HTTPS público, así que hace falta un proxy
|
|
35
|
+
inverso o un túnel por delante.
|
|
36
|
+
- La versión del Graph se resuelve en un solo lugar (`META_GRAPH_API_VERSION`,
|
|
37
|
+
por defecto **v26.0**). Cada versión caduca a los ~2 años y Meta redirige en
|
|
38
|
+
silencio a la más vieja que siga viva; tenerla centralizada es lo que evita
|
|
39
|
+
enterarse tarde.
|
|
40
|
+
- Fuera de la ventana de 24 h el canal manda la plantilla de
|
|
41
|
+
`windowFallbackTemplate`, y si no hay ninguna configurada lanza un error que
|
|
42
|
+
lo dice (131047). La narración de progreso no se envía por defecto
|
|
43
|
+
(`sendProgress: false`): desde el 1/10/2026 Meta cobra cada mensaje de
|
|
44
|
+
servicio dentro de la ventana.
|
|
45
|
+
- El parser recorre todas las `entry` y `changes` del webhook, y el canal
|
|
46
|
+
descarta los reintentos de Meta por id de mensaje.
|
|
47
|
+
- **Baileys se carga recién al conectar el canal `whatsapp`.** Antes se
|
|
48
|
+
importaba al cargar el índice del SDK, así que cualquier consumidor —aunque
|
|
49
|
+
no usara canales— se traía Baileys entero y su parche de
|
|
50
|
+
`process.stderr.write` en cada proceso. Quien usa el canal por código QR no
|
|
51
|
+
ve ningún cambio.
|
|
52
|
+
|
|
53
|
+
### Catálogo compartido entre inquilinos
|
|
54
|
+
|
|
55
|
+
- **El catálogo se instala una sola vez.** `tools`, `skills` y `ethics` dejan de
|
|
56
|
+
copiarse en la partición de cada inquilino: su contenido vive en la colección
|
|
57
|
+
sin prefijo —la misma que ve una instalación local— y cada inquilino guarda
|
|
58
|
+
únicamente lo que activó, en `catalogActivations`. Un enjambre nuevo arranca
|
|
59
|
+
con **cero escrituras** de catálogo, y encender una tool guarda una fila de
|
|
60
|
+
elección en lugar de una copia de la fila entera.
|
|
61
|
+
- **Providers y modelos ya no se resiembran dentro de un inquilino.** En un host
|
|
62
|
+
multi-inquilino el catálogo que vale es el del host, así que `seedAllData()`
|
|
63
|
+
saltea el estático cuando hay tenant activo. Antes borraba y recreaba los 139
|
|
64
|
+
modelos de `SEED_DATA` en la partición de cada enjambre, en cada arranque, para
|
|
65
|
+
que un instante después los pisara el host.
|
|
66
|
+
- Lo que un inquilino **crea** sigue siendo suyo y privado: las tools de un
|
|
67
|
+
endpoint de API, o una skill o un código de ética propios, se escriben en su
|
|
68
|
+
partición como siempre. Editar el contenido de una fila del catálogo también
|
|
69
|
+
deja una copia privada, y borrarla la oculta sólo para él.
|
|
70
|
+
- API nueva en `@johpaz/hive-sdk/storage`: `setCatalogActivation`,
|
|
71
|
+
`clearCatalogActivation`, `listCatalogActivations`, `sharedCatalogCol`,
|
|
72
|
+
`CATALOG_COLLECTIONS` y el tipo `DocStore`, que es lo que ahora devuelve
|
|
73
|
+
`col()` — la clase `Collection` de hive-db lo satisface tal cual, así que no
|
|
74
|
+
cambia nada para quien la recibe.
|
|
75
|
+
- **Sin inquilino en scope no cambia nada**: una instalación local sigue viendo
|
|
76
|
+
una sola partición, con el catálogo y su `active` en la misma fila.
|
|
77
|
+
- **El índice de capacidades también se comparte, y eso destapa dos fallas que
|
|
78
|
+
ya existían.** Todo documento declara ahora su ámbito —el inquilino que lo
|
|
79
|
+
escribió, o `_` si es del catálogo— y `searchCapabilities()` consulta los dos
|
|
80
|
+
cuando hay inquilino activo:
|
|
81
|
+
- Antes el catálogo se indexaba **sin** filtro de inquilino y la búsqueda
|
|
82
|
+
desde un enjambre filtraba **por** su inquilino, así que dentro de un
|
|
83
|
+
enjambre no se encontraba NADA del catálogo: ni una tool ni una skill. El
|
|
84
|
+
agente sólo descubría sus tools de MCP y las de sus endpoints.
|
|
85
|
+
- Y un reindexado del catálogo —el que corre en cada arranque del gateway—
|
|
86
|
+
borraba por `type` a secas, llevándose por delante lo que cada inquilino
|
|
87
|
+
tenía indexado. Ahora el borrado va acotado a su ámbito.
|
|
88
|
+
- Encima de la búsqueda manda la elección: una capacidad que el inquilino
|
|
89
|
+
apagó no se le ofrece, aunque esté en el catálogo y puntúe primero.
|
|
90
|
+
Cubierto por `packages/core/src/agent/capability-search.test.ts` y
|
|
91
|
+
`packages/core/src/storage/catalog.test.ts`.
|
|
20
92
|
|
|
21
93
|
### Corregido
|
|
22
94
|
|
|
95
|
+
- **Con un tenant activo, el log causal se apagaba en lugar de acotarse.**
|
|
96
|
+
`causalThread`, `toolStats` y `buildAgentContext` recorrían todos los shards
|
|
97
|
+
de la base, así que con un tenant en scope `causalReadsEnabled()` las apagaba:
|
|
98
|
+
en un host multi-inquilino el reflector G9 y el contexto causal del
|
|
99
|
+
compilador no corrían nunca. Ahora las tres lecturas van siempre acotadas a
|
|
100
|
+
los agentes que corresponden —el agente del turno, o los del lote de trazas— y
|
|
101
|
+
el apagado desaparece. Además el shard de cada evento pasa a ser
|
|
102
|
+
`causalAgentKey(agentId)`: con tenant lleva el tenant delante (`t_…:agente`,
|
|
103
|
+
la misma forma que los ids del índice BM25), así que dos inquilinos con un
|
|
104
|
+
agente del mismo id ya no comparten shard. Una lista de agentes vacía se salta
|
|
105
|
+
la lectura en vez de pasarse al motor, que la trataría como "todos los
|
|
106
|
+
shards". Cubierto por `test/causal-tenant-scope.test.ts`.
|
|
107
|
+
|
|
23
108
|
- **`browser_scrape` extraía con una tool que no ve lo que el navegador
|
|
24
109
|
renderizó.** La skill existe para sitios dinámicos, y su paso de extracción
|
|
25
110
|
usaba `web_fetch`, que vuelve a pedir la URL al servidor y recibe el HTML sin
|
|
@@ -194,6 +279,11 @@
|
|
|
194
279
|
|
|
195
280
|
### Cambiado
|
|
196
281
|
|
|
282
|
+
- **El `toolStats` del reflector se acota a los agentes del lote**, también sin
|
|
283
|
+
tenant. Antes sumaba el historial de la tool de todos los agentes de la base;
|
|
284
|
+
ahora el de los agentes cuyas trazas se están analizando. Es la misma
|
|
285
|
+
semántica con y sin tenant, y no recorre el log entero.
|
|
286
|
+
|
|
197
287
|
- **Los tests que manejan un navegador real son opt-in (`BROWSER_TESTS=1`).**
|
|
198
288
|
Su guarda era `isWebViewSupported()`, que sólo comprueba que exista un binario
|
|
199
289
|
de Chromium — no que arranque. En un runner de CI (contenedor, a menudo root)
|
package/README.md
CHANGED
|
@@ -353,6 +353,7 @@ await manager.initialize();
|
|
|
353
353
|
import {
|
|
354
354
|
TelegramChannel,
|
|
355
355
|
DiscordChannel,
|
|
356
|
+
WhatsAppCloudChannel,
|
|
356
357
|
WhatsAppChannel,
|
|
357
358
|
SlackChannel,
|
|
358
359
|
WebChatChannel,
|
|
@@ -364,8 +365,20 @@ const telegram = new TelegramChannel({ botToken: process.env.TELEGRAM_BOT_TOKEN!
|
|
|
364
365
|
// Discord
|
|
365
366
|
const discord = new DiscordChannel({ botToken: process.env.DISCORD_BOT_TOKEN! });
|
|
366
367
|
|
|
367
|
-
// WhatsApp
|
|
368
|
-
const whatsapp = new
|
|
368
|
+
// WhatsApp por la API oficial de Meta — el camino para un negocio
|
|
369
|
+
const whatsapp = new WhatsAppCloudChannel({
|
|
370
|
+
enabled: true,
|
|
371
|
+
accountId: "ventas",
|
|
372
|
+
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
|
|
373
|
+
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
|
|
374
|
+
appSecret: process.env.WHATSAPP_APP_SECRET!,
|
|
375
|
+
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
|
|
376
|
+
dmPolicy: "open",
|
|
377
|
+
allowFrom: [],
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// WhatsApp por código QR (Baileys). No es oficial: uso personal.
|
|
381
|
+
const whatsappQr = new WhatsAppChannel();
|
|
369
382
|
|
|
370
383
|
// Slack
|
|
371
384
|
const slack = new SlackChannel({ botToken: process.env.SLACK_BOT_TOKEN! });
|
|
@@ -374,6 +387,45 @@ const slack = new SlackChannel({ botToken: process.env.SLACK_BOT_TOKEN! });
|
|
|
374
387
|
const webchat = new WebChatChannel();
|
|
375
388
|
```
|
|
376
389
|
|
|
390
|
+
### WhatsApp: cuál de los dos
|
|
391
|
+
|
|
392
|
+
| | `whatsapp_cloud` | `whatsapp` |
|
|
393
|
+
|---|---|---|
|
|
394
|
+
| Qué es | La API oficial de Meta (Cloud API) | Baileys, WhatsApp Web por código QR |
|
|
395
|
+
| Para quién | Un negocio: número de WhatsApp Business propio, plantillas de marketing y atribución de los anuncios que abren la conversación | Uso personal |
|
|
396
|
+
| Riesgo | Ninguno: es el camino soportado | Va contra los términos de WhatsApp; el número puede terminar bloqueado |
|
|
397
|
+
| Cómo recibe | Webhook: hace falta una URL HTTPS pública | Conexión propia, sin URL pública |
|
|
398
|
+
|
|
399
|
+
El canal oficial no abre ninguna conexión: Meta empuja los mensajes a una URL.
|
|
400
|
+
El gateway la sirve en `/webhooks/whatsapp-cloud/<cuenta>` si se le pasa el
|
|
401
|
+
manager, y esa es la que se registra en el panel de Meta junto con el
|
|
402
|
+
`verifyToken`.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
const channelManager = new ChannelManager(await loadConfig());
|
|
406
|
+
await channelManager.initialize();
|
|
407
|
+
|
|
408
|
+
// Sin esto, el gateway no tiene a quién entregarle lo que manda Meta.
|
|
409
|
+
await startGateway({ channelManager });
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Tres cosas que conviene saber antes de ponerlo en producción:
|
|
413
|
+
|
|
414
|
+
- **La ventana de 24 h.** Pasado ese tiempo desde el último mensaje del cliente,
|
|
415
|
+
Meta sólo acepta plantillas aprobadas (error 131047). Con
|
|
416
|
+
`windowFallbackTemplate` el canal manda esa plantilla; sin ella, lanza un
|
|
417
|
+
error que lo explica en vez de fallar en silencio.
|
|
418
|
+
- **Cada mensaje se cobra.** Desde el 1 de octubre de 2026 los mensajes de
|
|
419
|
+
servicio dentro de la ventana también se pagan, así que la narración de
|
|
420
|
+
progreso del agente no se envía salvo que se encienda `sendProgress`. En su
|
|
421
|
+
lugar se renueva el "escribiendo…", que es gratis.
|
|
422
|
+
- **4096 caracteres por mensaje.** El cliente parte las respuestas largas solo,
|
|
423
|
+
cortando entre párrafos.
|
|
424
|
+
|
|
425
|
+
La versión del Graph sale de `META_GRAPH_API_VERSION` (por defecto la estable
|
|
426
|
+
más reciente). Conviene fijarla a conciencia: cada versión caduca a los ~2 años
|
|
427
|
+
y Meta redirige las llamadas a la más vieja que siga viva, sin avisar.
|
|
428
|
+
|
|
377
429
|
---
|
|
378
430
|
|
|
379
431
|
## Tool Runtime
|
|
@@ -444,8 +496,8 @@ unsubscribeCanvas(handler);
|
|
|
444
496
|
|
|
445
497
|
## Storage
|
|
446
498
|
|
|
447
|
-
HiveDB (`@johpaz/hive-db`), un motor embebido con colecciones
|
|
448
|
-
índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
|
|
499
|
+
HiveDB (`@johpaz/hive-db` 0.5.1 o posterior), un motor embebido con colecciones
|
|
500
|
+
de documentos e índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
|
|
449
501
|
|
|
450
502
|
```typescript
|
|
451
503
|
import { ensureHiveDb, col } from "@johpaz/hive-sdk";
|
|
@@ -483,6 +535,33 @@ const skills = await selectSkills("investigar en la web");
|
|
|
483
535
|
Una tool declarada con `defineTool` y pasada a `createAgent` queda indexada
|
|
484
536
|
automáticamente, así que el modelo puede descubrirla igual que a las nativas.
|
|
485
537
|
|
|
538
|
+
### Catálogo compartido entre inquilinos
|
|
539
|
+
|
|
540
|
+
En un host multi-inquilino (varios enjambres en una sola HiveDB, ver
|
|
541
|
+
`runInTenant`) el catálogo —`tools`, `skills` y `ethics`— es contenido de la
|
|
542
|
+
INSTALACIÓN, no de cada inquilino: su contenido se instala una sola vez en la
|
|
543
|
+
colección sin prefijo, y cada inquilino guarda únicamente lo que activó.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { col, runInTenant, setCatalogActivation } from "@johpaz/hive-sdk";
|
|
547
|
+
|
|
548
|
+
// Sin inquilino: esto instala el catálogo, una vez para todos.
|
|
549
|
+
await ensureHiveDb();
|
|
550
|
+
|
|
551
|
+
await runInTenant(tenantKey, async () => {
|
|
552
|
+
const tools = await col<ToolDoc>("tools");
|
|
553
|
+
await tools.scan({}); // ve el catálogo entero
|
|
554
|
+
|
|
555
|
+
// Encender una tool para ESTE inquilino: guarda su elección, no una copia.
|
|
556
|
+
await setCatalogActivation("tools", "web_search", { active: true });
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Lo que el inquilino crea —la tool de un endpoint de API, una skill propia— se
|
|
561
|
+
escribe en su partición y no lo ve nadie más. Editar el contenido de una fila del
|
|
562
|
+
catálogo deja una copia privada; borrarla la oculta sólo para él. Sin inquilino
|
|
563
|
+
en scope nada de esto se activa y `col()` se comporta como siempre.
|
|
564
|
+
|
|
486
565
|
---
|
|
487
566
|
|
|
488
567
|
## Config
|
|
@@ -14,13 +14,33 @@ de proceso.
|
|
|
14
14
|
- SheetJS CE se instala desde la distribución oficial 0.20.3; el paquete npm
|
|
15
15
|
abandonado en 0.18.5 no forma parte del grafo.
|
|
16
16
|
- Baileys está fijado exactamente en `7.0.0-rc14` para evitar retroceder a una
|
|
17
|
-
release candidate vulnerable.
|
|
17
|
+
release candidate vulnerable. Además se **carga bajo demanda**: entra en el
|
|
18
|
+
proceso sólo si alguien conecta el canal `whatsapp` por código QR. Un
|
|
19
|
+
consumidor que no lo use —hive-cloud, por ejemplo, que usa el canal oficial—
|
|
20
|
+
no carga su grafo ni su parche de `process.stderr.write`.
|
|
18
21
|
- PptxGenJS 4.0.1 se conserva como artefacto ESM vendorizado, con licencia,
|
|
19
22
|
procedencia y SHA-256. No se instala su dependencia muerta `image-size`.
|
|
20
23
|
|
|
21
24
|
Toda actualización del artefacto PPTX debe verificar su origen y hash, ejecutar
|
|
22
25
|
la prueba OOXML y volver a ejecutar el audit.
|
|
23
26
|
|
|
27
|
+
## Webhooks de WhatsApp (Cloud API)
|
|
28
|
+
|
|
29
|
+
El canal `whatsapp_cloud` recibe por una URL pública, así que todo lo que llega
|
|
30
|
+
es no confiable hasta comprobar dos cosas:
|
|
31
|
+
|
|
32
|
+
- **La firma.** `verifySignature` recalcula el HMAC-SHA256 del cuerpo **crudo**
|
|
33
|
+
con el secreto de la app de Meta y lo compara con `X-Hub-Signature-256`
|
|
34
|
+
usando `timingSafeEqual`. Sin cabecera, sin secreto o con una firma que no
|
|
35
|
+
coincide, el webhook responde 401 y no se procesa nada. El cuerpo se lee como
|
|
36
|
+
texto antes de parsearlo: firmar el JSON reserializado daría distinto.
|
|
37
|
+
- **El token de verificación.** El `hub.verify_token` del alta se compara
|
|
38
|
+
también con `timingSafeEqual`; si no coincide, 403.
|
|
39
|
+
|
|
40
|
+
El canal además sólo atiende eventos de su propio `phone_number_id` —una misma
|
|
41
|
+
cuenta de WhatsApp Business puede tener varios números apuntando a la misma
|
|
42
|
+
URL— y descarta por id los reintentos de Meta.
|
|
43
|
+
|
|
24
44
|
## Archivos PDF
|
|
25
45
|
|
|
26
46
|
Antes de leer un PDF, `office_leer_pdf` exige un archivo regular y limita el
|
package/docs/UPGRADING.md
CHANGED
|
@@ -54,6 +54,26 @@ Estos adaptadores están en la frontera con el runtime. No deben reemplazarse po
|
|
|
54
54
|
`any`, `@ts-ignore` o `@ts-expect-error`: hacerlo convertiría una incompatibilidad
|
|
55
55
|
real de plataforma en un falso resultado verde.
|
|
56
56
|
|
|
57
|
+
## hive-db 0.5.1 y log causal por tenant
|
|
58
|
+
|
|
59
|
+
Hive SDK requiere **`@johpaz/hive-db` 0.5.1 o posterior**. Llega como
|
|
60
|
+
dependencia del SDK, así que una aplicación consumidora no la declara. Lo que
|
|
61
|
+
sigue sólo importa con el log causal encendido (`HIVE_CAUSAL_LOG=true` o
|
|
62
|
+
`causalLog.enabled`):
|
|
63
|
+
|
|
64
|
+
- Con un tenant activo (`runInTenant`) el reflector y el contexto causal del
|
|
65
|
+
compilador vuelven a funcionar. Antes se apagaban; ahora leen acotado a los
|
|
66
|
+
agentes del turno o del lote de trazas.
|
|
67
|
+
- La clave de shard de cada evento es `causalAgentKey(agentId)`: sin tenant, el
|
|
68
|
+
id del agente tal cual; con tenant, `t_…:agentId`. Los eventos que un host
|
|
69
|
+
haya escrito con tenant antes de esta versión quedaron con el id crudo y las
|
|
70
|
+
lecturas acotadas ya no los ven. Sin tenant no cambia nada.
|
|
71
|
+
- El `toolStats` del reflector cuenta el historial de los agentes del lote, no
|
|
72
|
+
el de toda la base, con y sin tenant.
|
|
73
|
+
- `watchCausalEvents` con tenant sigue exigiendo `agentId`: se le pasa el id
|
|
74
|
+
crudo y el SDK lo califica. Los eventos que entrega traen en `agentId` la
|
|
75
|
+
clave del shard; `formatCausalEvent` la muestra sin el tenant.
|
|
76
|
+
|
|
57
77
|
## Compatibilidad y CI
|
|
58
78
|
|
|
59
79
|
Los workflows fijan Bun 1.4.2, instalan con `--frozen-lockfile`, ejecutan el
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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",
|
|
@@ -16,6 +16,13 @@ export default {
|
|
|
16
16
|
webchat: { enabled: true },
|
|
17
17
|
telegram: { enabled: false },
|
|
18
18
|
discord: { enabled: false },
|
|
19
|
+
// WhatsApp por la API oficial de Meta. Es el camino para un negocio:
|
|
20
|
+
// número propio de WhatsApp Business, plantillas y atribución de los
|
|
21
|
+
// anuncios que abren la conversación. Recibe por webhook, así que hace
|
|
22
|
+
// falta exponer `/webhooks/whatsapp-cloud/<cuenta>` con HTTPS público.
|
|
23
|
+
whatsapp_cloud: { enabled: false },
|
|
24
|
+
// WhatsApp por código QR (Baileys). No es oficial: sirve para uso
|
|
25
|
+
// personal, no para un negocio, y el número puede terminar bloqueado.
|
|
19
26
|
whatsapp: { enabled: false },
|
|
20
27
|
slack: { enabled: false },
|
|
21
28
|
},
|
|
@@ -31,6 +31,9 @@ async function main() {
|
|
|
31
31
|
host: config.gateway?.host,
|
|
32
32
|
port: config.gateway?.port,
|
|
33
33
|
agentId: coordinatorAgent.id,
|
|
34
|
+
// Los canales que reciben por webhook —WhatsApp por la API oficial de
|
|
35
|
+
// Meta— entran por el gateway, así que necesita el manager.
|
|
36
|
+
channelManager,
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
log.info(`{{APP_NAME}} is running at http://${gateway.hostname}:${gateway.port}`);
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { logger } from "../utils/logger.ts"
|
|
17
17
|
import { col, fromIndexable } from "../storage/hive.ts"
|
|
18
18
|
import { getHiveDb } from "../storage/hivedb.ts"
|
|
19
|
+
import { causalAgentKey } from "../storage/causal-events.ts"
|
|
19
20
|
import type { HiveDB, EventInput } from "@johpaz/hive-db"
|
|
20
21
|
import type { AgentDoc, TurnSource } from "../storage/collections.ts"
|
|
21
22
|
import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage, type ProviderCredentials } from "./llm-client.ts"
|
|
@@ -170,7 +171,8 @@ async function appendCausalEvent(
|
|
|
170
171
|
): Promise<number | undefined> {
|
|
171
172
|
try {
|
|
172
173
|
return await db.append({
|
|
173
|
-
|
|
174
|
+
// Shard calificado con el tenant: ver causalAgentKey en causal-events.ts.
|
|
175
|
+
agentId: causalAgentKey(input.agentId),
|
|
174
176
|
streamId: input.streamId,
|
|
175
177
|
kind: input.kind,
|
|
176
178
|
payload: JSON.stringify(input.payload),
|
|
@@ -21,10 +21,27 @@
|
|
|
21
21
|
import type { IndexDoc } from "@johpaz/hive-db";
|
|
22
22
|
import { getHiveDb } from "../storage/hivedb.ts";
|
|
23
23
|
import { currentTenant, qualifyDocId, unqualifyDocId, scopedFilterValue } from "../storage/tenant.ts";
|
|
24
|
+
import { listCatalogActivations } from "../storage/catalog.ts";
|
|
24
25
|
import { logger } from "../utils/logger.ts";
|
|
25
26
|
|
|
26
27
|
const log = logger.child("capability-search");
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Ámbito de los documentos que no son de ningún inquilino: el catálogo.
|
|
31
|
+
*
|
|
32
|
+
* Es el mismo `"_"` que `scopedFilterValue()` usa cuando no hay tenant, y por
|
|
33
|
+
* eso sirve de las dos maneras: como valor del filtro `tenant` para poder
|
|
34
|
+
* BUSCAR el catálogo desde dentro de un inquilino, y como parte de
|
|
35
|
+
* `tenant__type` para que un reindexado del catálogo BORRE sólo lo suyo.
|
|
36
|
+
*
|
|
37
|
+
* Antes el catálogo se indexaba sin filtro `tenant` alguno, y eso rompía las dos
|
|
38
|
+
* cosas: desde un enjambre la búsqueda filtraba por su tenant y no encontraba
|
|
39
|
+
* NADA del catálogo —ni una tool, ni una skill—, y un reindexado desde fuera
|
|
40
|
+
* borraba por `type` a secas, llevándose por delante los documentos de todos los
|
|
41
|
+
* inquilinos (las tools de sus endpoints, sus tools de MCP).
|
|
42
|
+
*/
|
|
43
|
+
const CATALOGO = "_";
|
|
44
|
+
|
|
28
45
|
export type CapabilityType = "tool" | "skill" | "playbook" | "mcp" | "agent";
|
|
29
46
|
|
|
30
47
|
export interface CapabilityHit {
|
|
@@ -91,17 +108,22 @@ export async function searchCapabilities(
|
|
|
91
108
|
// type; the single-type and all-types cases are one engine call.
|
|
92
109
|
//
|
|
93
110
|
// El índice semántico es UNO solo para todos los inquilinos (no hay
|
|
94
|
-
// "colección" que prefijar), así que el
|
|
95
|
-
//
|
|
96
|
-
//
|
|
111
|
+
// "colección" que prefijar), así que el ámbito entra como un filtro más que el
|
|
112
|
+
// motor AND-ea con el resto.
|
|
113
|
+
//
|
|
114
|
+
// Con inquilino activo se consulta DOS veces: lo suyo —las tools de sus
|
|
115
|
+
// endpoints, sus tools de MCP— y el catálogo compartido, que se instala una
|
|
116
|
+
// sola vez y es de todos (ver storage/catalog.ts). Sin inquilino se consulta
|
|
117
|
+
// sin filtro de ámbito: una instalación local tiene una sola partición y así
|
|
118
|
+
// un índice ya construido sigue respondiendo igual.
|
|
97
119
|
const tenant = currentTenant();
|
|
98
|
-
const
|
|
99
|
-
const queries =
|
|
100
|
-
?
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
120
|
+
const ambitos = tenant ? [tenant, CATALOGO] : [null];
|
|
121
|
+
const queries = ambitos.flatMap((ambito) => {
|
|
122
|
+
const filtroAmbito = ambito ? [{ field: "tenant", value: ambito }] : [];
|
|
123
|
+
return types
|
|
124
|
+
? types.map((t) => ({ filters: [...filtroAmbito, { field: "type", value: t }] }))
|
|
125
|
+
: [{ filters: filtroAmbito.length ? filtroAmbito : undefined }];
|
|
126
|
+
});
|
|
105
127
|
|
|
106
128
|
const merged = new Map<string, CapabilityHit>();
|
|
107
129
|
for (const q of queries) {
|
|
@@ -129,7 +151,10 @@ export async function searchCapabilities(
|
|
|
129
151
|
}
|
|
130
152
|
}
|
|
131
153
|
|
|
154
|
+
const apagados = tenant && merged.size > 0 ? await apagadosParaElInquilino() : null;
|
|
155
|
+
|
|
132
156
|
const results = Array.from(merged.values())
|
|
157
|
+
.filter((hit) => !apagados?.has(hit.id))
|
|
133
158
|
.sort((a, b) => b.score - a.score)
|
|
134
159
|
.slice(0, k);
|
|
135
160
|
|
|
@@ -140,6 +165,27 @@ export async function searchCapabilities(
|
|
|
140
165
|
return results;
|
|
141
166
|
}
|
|
142
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Lo que este inquilino apagó del catálogo compartido.
|
|
170
|
+
*
|
|
171
|
+
* El índice del catálogo es uno solo, así que no puede llevar la elección de
|
|
172
|
+
* nadie: se filtra al leer. Sólo se descarta lo que el inquilino decidió
|
|
173
|
+
* explícitamente —apagar u ocultar—; lo que nunca tocó hereda lo que diga el
|
|
174
|
+
* catálogo, igual que en la colección (ver storage/catalog.ts).
|
|
175
|
+
*
|
|
176
|
+
* Ofrecerle al modelo una capacidad que su workspace apagó es hacerle perder un
|
|
177
|
+
* turno, además de contarle que existe algo que no puede usar.
|
|
178
|
+
*/
|
|
179
|
+
async function apagadosParaElInquilino(): Promise<Set<string>> {
|
|
180
|
+
const apagados = new Set<string>();
|
|
181
|
+
for (const [tipo, coleccion] of [["tool", "tools"], ["skill", "skills"]] as const) {
|
|
182
|
+
for (const eleccion of await listCatalogActivations(coleccion)) {
|
|
183
|
+
if (!eleccion.active || eleccion.hidden) apagados.add(`${tipo}:${eleccion.itemId}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return apagados;
|
|
187
|
+
}
|
|
188
|
+
|
|
143
189
|
/**
|
|
144
190
|
* Keep only hits scoring at least `ratio` of the top hit. This replaces the
|
|
145
191
|
* old absolute negative-bm25 thresholds: relevance is relative to the best
|
|
@@ -169,12 +215,12 @@ export async function replaceCapabilityDocs(
|
|
|
169
215
|
// `deleteByFilter` acepta UN SOLO filtro, así que en una base compartida
|
|
170
216
|
// borrar por `type` se llevaría por delante los documentos de todos los
|
|
171
217
|
// inquilinos. El campo sintético `tenant__type` (ver scopedFilterValue)
|
|
172
|
-
// mantiene el borrado en un filtro y acotado a
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
218
|
+
// mantiene el borrado en un filtro y acotado a un ámbito.
|
|
219
|
+
//
|
|
220
|
+
// Sin inquilino el ámbito es el catálogo (`_`), no "todo": reindexar el
|
|
221
|
+
// catálogo al arrancar borraba las tools de los endpoints y las de MCP de cada
|
|
222
|
+
// inquilino, que nadie volvía a escribir hasta que ese enjambre se reconectara.
|
|
223
|
+
await db.deleteByFilter({ field: "tenant__type", value: scopedFilterValue(type) });
|
|
178
224
|
if (docs.length === 0) return;
|
|
179
225
|
await db.upsertBatch(docs.map(toIndexDoc));
|
|
180
226
|
}
|
|
@@ -198,7 +244,6 @@ export async function deleteCapabilitiesByServer(serverId: string): Promise<void
|
|
|
198
244
|
|
|
199
245
|
function toIndexDoc(doc: CapabilityDoc): IndexDoc {
|
|
200
246
|
const extra = doc.extraFilters ?? [];
|
|
201
|
-
const tenant = currentTenant();
|
|
202
247
|
return {
|
|
203
248
|
id: qualifyDocId(`${doc.type}:${doc.rawId}`),
|
|
204
249
|
name: doc.name,
|
|
@@ -207,20 +252,16 @@ function toIndexDoc(doc: CapabilityDoc): IndexDoc {
|
|
|
207
252
|
filters: [
|
|
208
253
|
{ field: "type", value: doc.type },
|
|
209
254
|
...extra,
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
value: scopedFilterValue(f.value),
|
|
221
|
-
})),
|
|
222
|
-
]
|
|
223
|
-
: []),
|
|
255
|
+
// Todo documento declara su ámbito —el inquilino que lo escribió, o `_` si
|
|
256
|
+
// es del catálogo—, y cada filtro lleva además su gemelo
|
|
257
|
+
// `tenant__<campo>`. Con eso la búsqueda puede pedir un ámbito concreto y
|
|
258
|
+
// los borrados masivos —que aceptan un solo filtro— quedan acotados a él.
|
|
259
|
+
{ field: "tenant", value: currentTenant() ?? CATALOGO },
|
|
260
|
+
{ field: "tenant__type", value: scopedFilterValue(doc.type) },
|
|
261
|
+
...extra.map((f) => ({
|
|
262
|
+
field: `tenant__${f.field}`,
|
|
263
|
+
value: scopedFilterValue(f.value),
|
|
264
|
+
})),
|
|
224
265
|
],
|
|
225
266
|
};
|
|
226
267
|
}
|
|
@@ -38,7 +38,7 @@ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
|
|
|
38
38
|
import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync.ts"
|
|
39
39
|
import { getUserDate, getUserTime } from "../utils/date.ts"
|
|
40
40
|
import { getHiveDb } from "../storage/hivedb.ts"
|
|
41
|
-
import { causalReadsEnabled } from "../storage/causal-events.ts"
|
|
41
|
+
import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
|
|
42
42
|
import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector.ts"
|
|
43
43
|
import { expandToolAllowlist } from "./delegation-runtime.ts"
|
|
44
44
|
import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
|
|
@@ -588,7 +588,10 @@ export async function compileContext(opts: {
|
|
|
588
588
|
// applies this turn (a real DB round-trip, not a per-turn cost) and
|
|
589
589
|
// there's a causal stream to build it from. episodicSimilarity is omitted:
|
|
590
590
|
// it requires embeddings hive doesn't generate anywhere yet.
|
|
591
|
-
|
|
591
|
+
// Acotado al shard de este agente: el stream es de una sola invocación suya,
|
|
592
|
+
// así que el hilo es el mismo y no se recorre el log de nadie más.
|
|
593
|
+
const causalAgents = causalScope([opts.agentId])
|
|
594
|
+
if (summaryApplies && opts.causalStreamId && causalAgents && causalReadsEnabled()) {
|
|
592
595
|
try {
|
|
593
596
|
const causalDb = await getHiveDb()
|
|
594
597
|
const objectiveSource = taskContext || userMessage
|
|
@@ -605,6 +608,7 @@ export async function compileContext(opts: {
|
|
|
605
608
|
currentObjective: currentObjective.slice(0, 2000),
|
|
606
609
|
maxTokens: causalMaxTokens,
|
|
607
610
|
strategy: { causalAnchors: true, compressCompletedPhases: true },
|
|
611
|
+
agents: causalAgents,
|
|
608
612
|
})) as AgentContextShape
|
|
609
613
|
|
|
610
614
|
const causalLines = [...(causalCtx.items ?? []), ...(causalCtx.anomalies ?? [])]
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import { logger } from "../utils/logger.ts"
|
|
15
15
|
import { col, nextId } from "../storage/hive.ts"
|
|
16
16
|
import { getHiveDb } from "../storage/hivedb.ts"
|
|
17
|
-
import { causalReadsEnabled } from "../storage/causal-events.ts"
|
|
17
|
+
import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
|
|
18
|
+
import { unqualifyDocId } from "../storage/tenant.ts"
|
|
18
19
|
import type { HiveDB, ToolStats } from "@johpaz/hive-db"
|
|
19
20
|
import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
|
|
20
21
|
import { parseThreadId } from "./thread-id.ts"
|
|
@@ -183,10 +184,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
|
|
|
183
184
|
|
|
184
185
|
for (const streamId of streamIds) {
|
|
185
186
|
try {
|
|
186
|
-
|
|
187
|
+
// El stream es de una sola invocación, así que sus trazas nombran a los
|
|
188
|
+
// agentes que escribieron en él. Sin ninguno no hay con qué acotar, y una
|
|
189
|
+
// lectura sin acotar recorre los shards de todos los inquilinos.
|
|
190
|
+
const subset = traces.filter((t) => t.causal_stream_id === streamId)
|
|
191
|
+
const agents = causalScope(subset.map((t) => t.agent_id))
|
|
192
|
+
if (!agents) continue
|
|
193
|
+
|
|
194
|
+
const thread = (await causalDb.causalThread(streamId, agents)) as CausalThreadShape
|
|
187
195
|
if (!thread.decisions?.length && !thread.toolCalls?.length) continue
|
|
188
196
|
|
|
189
|
-
const subset = traces.filter((t) => t.causal_stream_id === streamId)
|
|
190
197
|
const originalIntent = subset[0]?.input_summary ?? ""
|
|
191
198
|
const success = subset.every((t) => t.success)
|
|
192
199
|
|
|
@@ -204,13 +211,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
|
|
|
204
211
|
// underlying root cause mint a brand new playbook rule instead of
|
|
205
212
|
// reinforcing one (confirmed via a local before/after canary run).
|
|
206
213
|
if (evaluation.rootCause) {
|
|
214
|
+
// El log guarda la clave del shard (t_…:agente con tenant); a la regla de
|
|
215
|
+
// playbook y a affected_agents les llega el id que el host conoce.
|
|
216
|
+
const rootAgent = unqualifyDocId(evaluation.rootCause.agent)
|
|
207
217
|
const decision = thread.decisions?.find((d) => d.seq === evaluation.rootCause!.seq)
|
|
208
218
|
insights.push({
|
|
209
219
|
type: "root_cause",
|
|
210
220
|
description: decision
|
|
211
|
-
? `Root cause: decision "${decision.description}" (agent ${
|
|
212
|
-
: `Root cause: a decision by agent ${
|
|
213
|
-
affectedAgents: [
|
|
221
|
+
? `Root cause: decision "${decision.description}" (agent ${rootAgent}) preceded a tool failure.`
|
|
222
|
+
: `Root cause: a decision by agent ${rootAgent} preceded a tool failure.`,
|
|
223
|
+
affectedAgents: [rootAgent],
|
|
214
224
|
confidence: 0.6,
|
|
215
225
|
})
|
|
216
226
|
}
|
|
@@ -248,14 +258,17 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
|
|
|
248
258
|
async function analyzeTracesLocally(traces: TraceDoc[], causalDb: HiveDB | null): Promise<Insight[]> {
|
|
249
259
|
const insights: Insight[] = []
|
|
250
260
|
|
|
251
|
-
// G9:
|
|
252
|
-
//
|
|
261
|
+
// G9: historial completo, por tool, de los agentes de este lote (undefined si
|
|
262
|
+
// el log está apagado o la tool todavía no tiene eventos). Acotado a esos
|
|
263
|
+
// agentes: con tenant, sin esto se sumarían llamadas de otros inquilinos; sin
|
|
264
|
+
// tenant, es el historial de estos agentes y no el de toda la base.
|
|
253
265
|
const statsByTool = new Map<string, ToolStats>()
|
|
254
|
-
|
|
266
|
+
const agents = causalDb ? causalScope(traces.map((t) => t.agent_id)) : null
|
|
267
|
+
if (causalDb && agents) {
|
|
255
268
|
const distinctTools = new Set(traces.map((t) => t.tool_used).filter((t): t is string => !!t))
|
|
256
269
|
for (const tool of distinctTools) {
|
|
257
270
|
try {
|
|
258
|
-
const stats = await causalDb.toolStats(tool)
|
|
271
|
+
const stats = await causalDb.toolStats(tool, agents)
|
|
259
272
|
if (stats) statsByTool.set(tool, stats)
|
|
260
273
|
} catch (err) {
|
|
261
274
|
log.warn(`[reflector] toolStats(${tool}) failed: ${(err as Error).message}`)
|