@johpaz/hive-sdk 0.4.4 → 0.4.5

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 (40) hide show
  1. package/CHANGELOG.md +20 -4
  2. package/README.md +48 -7
  3. package/SECURITY.md +17 -0
  4. package/docs/API-AGENTS.md +430 -0
  5. package/docs/API-ARTIFACTS.md +55 -0
  6. package/docs/API-CONTEXT-COMPILER.md +285 -0
  7. package/docs/API-CRON.md +188 -0
  8. package/docs/API-DAG-SCHEDULER.md +291 -0
  9. package/docs/API-HOOKS.md +147 -0
  10. package/docs/API-RESILIENCE.md +45 -0
  11. package/docs/API-SERVICES.md +458 -0
  12. package/docs/API-SESSIONS.md +146 -0
  13. package/docs/API-TOOLS-SKILLS-CHANNELS.md +499 -0
  14. package/docs/API-WORKERS-EVENTS.md +311 -0
  15. package/docs/HIVE-HARNESS.md +232 -0
  16. package/docs/INDEX.md +198 -0
  17. package/docs/SECURITY-GUARDRAILS.md +87 -0
  18. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  19. package/docs/UPGRADING.md +65 -0
  20. package/docs/assets/logoblack.png +0 -0
  21. package/docs/assets/logocolor-dark.png +0 -0
  22. package/docs/assets/logocolorbg.png +0 -0
  23. package/docs/plans/2026-09-05-office-dependency-hardening-design.md +28 -0
  24. package/docs/plans/2026-09-06-dependency-audit-remediation-design.md +25 -0
  25. package/docs/plans/2026-09-06-pptx-image-size-remediation-design.md +54 -0
  26. package/docs/plans/2026-09-06-typescript7-bun142-documentation-design.md +48 -0
  27. package/package.json +5 -4
  28. package/packages/core/src/agent/llm-providers/hiveagents.ts +2 -2
  29. package/packages/core/src/agent/providers/index.ts +17 -1
  30. package/packages/core/src/api/createAgent.ts +4 -2
  31. package/packages/core/src/gateway/server.ts +1 -1
  32. package/packages/core/src/mcp/transports/sse.ts +11 -3
  33. package/packages/core/src/mcp/transports/websocket.ts +11 -9
  34. package/packages/core/src/tool-runtime/tool-worker.ts +3 -1
  35. package/packages/core/src/tools/office/office-escribir-pptx.ts +3 -1
  36. package/packages/core/src/vendor/pptxgenjs/LICENSE +21 -0
  37. package/packages/core/src/vendor/pptxgenjs/README.md +17 -0
  38. package/packages/core/src/vendor/pptxgenjs/pptxgen.es.d.ts +17 -0
  39. package/packages/core/src/vendor/pptxgenjs/pptxgen.es.js +7368 -0
  40. package/packages/core/src/voice/index.ts +4 -4
@@ -0,0 +1,458 @@
1
+ # API-SERVICES — la superficie que maneja una interfaz
2
+
3
+ ## Por qué existe
4
+
5
+ El SDK nació para que lo condujera un modelo. Casi todo el CRUD vivía **dentro
6
+ de las tools** —`cronCreateTool`, `memoryWriteTool`, `agentCreateTool`— con
7
+ argumentos con forma de LLM y respuestas escritas para un prompt. Montar una UI
8
+ encima obligaba a una de dos cosas, ambas malas:
9
+
10
+ - llamar `tool.execute({...})` y parsear prosa pensada para un prompt, o
11
+ - escribir consultas crudas contra HiveDB conociendo un esquema que no es
12
+ contrato público.
13
+
14
+ `@johpaz/hive-sdk/services` es la respuesta: la implementación vive acá y **las
15
+ tools la envuelven**. Una sola implementación, dos consumidores — el modelo y tu
16
+ aplicación.
17
+
18
+ ```typescript
19
+ import { agents, skills, swarms } from "@johpaz/hive-sdk/services";
20
+
21
+ const a = await agents.createAgent({ name: "Investigador", toolPatterns: ["web_*"] });
22
+ const s = await swarms.createSwarm({
23
+ name: "Equipo de research",
24
+ strategy: "sequential",
25
+ members: [{ agentId: a.id }],
26
+ });
27
+ await swarms.runSwarm(s.id, "Investigá el mercado de X");
28
+ ```
29
+
30
+ ## Dos decisiones de diseño
31
+
32
+ **Es agnóstico del framework: funciones, no rutas HTTP.** Una app móvil o de
33
+ escritorio que embeba el runtime no quiere un servidor. Quien haga una UI web
34
+ monta sus rutas encima en unas pocas líneas — es exactamente lo que hace hive,
35
+ cuya ruta de conversaciones es delgadísima porque toda la lógica está en el
36
+ módulo, no en el handler.
37
+
38
+ **Los servicios lanzan excepciones**, no devuelven `{ok:false}`. Quien construye
39
+ una interfaz quiere `try/catch`; inspeccionar un campo en cada llamada es ruido.
40
+ La traducción al formato que espera el modelo la hace el envoltorio de la tool.
41
+
42
+ ## Dominios
43
+
44
+ | Módulo | Qué resuelve |
45
+ |---|---|
46
+ | `agents` | CRUD + `assignTools` / `assignSkills` / `assignMcpServers` |
47
+ | `swarms` | CRUD de enjambres guardados + `runSwarm` |
48
+ | `skills` | CRUD + `importSkillFromDisk` |
49
+ | `tools` | Listar, encender/apagar, editar metadatos |
50
+ | `providers` | CRUD, API keys cifradas, cascada hacia modelos |
51
+ | `models` | CRUD, `renameModel` transaccional, protección de borrado |
52
+ | `mcp` | CRUD + `testMcpServer` |
53
+ | `cron` | CRUD sobre el scheduler, con respaldo directo a BD |
54
+ | `memory` | CRUD de la memoria de largo plazo |
55
+ | `ethics` | CRUD del código de ética |
56
+ | `endpoints` | Endpoints HTTP registrados como herramientas |
57
+ | `setup` | Seed selectivo: qué agentes quiere el usuario |
58
+
59
+ ---
60
+
61
+ ## Reglas que no son obvias
62
+
63
+ Estas son las que, si se pierden, dejan la colmena inconsistente sin avisar.
64
+
65
+ ### Las referencias se validan al escribir
66
+
67
+ `createAgent` y `updateAgent` comprueban que cada tool, skill y servidor MCP
68
+ exista antes de guardar. Los patrones se expanden primero: un `zzz_*` que no
69
+ case con nada es un error, no una lista vacía silenciosa.
70
+
71
+ ```typescript
72
+ await agents.createAgent({ name: "X", skills: ["no-existe"] });
73
+ // Error: skills inexistentes: no-existe
74
+ ```
75
+
76
+ Sin esto, un id mal escrito no falla al guardar sino más tarde, cuando el agente
77
+ intenta usar una capacidad que no existe — lejos de donde está el error.
78
+
79
+ ### Borrar un agente no borra sus tools ni sus skills
80
+
81
+ Son colecciones **globales compartidas**: `web_fetch` lo usan a la vez el
82
+ investigador web y el operador de navegador. Borrar las de un agente rompería al
83
+ otro. Lo mismo al desactivarlo.
84
+
85
+ ### Desactivar un proveedor arrastra sus modelos
86
+
87
+ Un modelo activo de un proveedor apagado aparece en el selector y falla al
88
+ llamarse, porque no hay credencial ni endpoint que lo atienda.
89
+
90
+ ### Renombrar un modelo re-apunta a sus agentes
91
+
92
+ Cambiar el nombre cambia el id, así que mover la fila y actualizar a cada agente
93
+ que la referenciaba ocurre en un solo `batch()`. A medias dejaría agentes
94
+ apuntando a la nada.
95
+
96
+ ```typescript
97
+ await models.renameModel("openai/gpt-viejo", "gpt-nuevo"); // los agentes siguen apuntando bien
98
+ ```
99
+
100
+ ### Las API keys nunca salen
101
+
102
+ Se guardan cifradas y hacia afuera sólo hay `hasApiKey` y una versión
103
+ enmascarada. Una UI necesita mostrar "hay clave configurada" sin poder leerla.
104
+
105
+ ### No existe `createTool`
106
+
107
+ Una tool es **código con un `execute`**, y desde una interfaz no hay dónde
108
+ ponerlo. Las vías reales para sumar capacidades son tres:
109
+
110
+ 1. `registerAppTool()` — código propio, para quien construye sobre el SDK.
111
+ 2. Un **servidor MCP** (`mcp.createMcpServer`) — un proceso externo expone sus
112
+ tools por el protocolo.
113
+ 3. Un **endpoint HTTP declarativo**, donde el ejecutor es genérico y lo que el
114
+ usuario aporta son datos, no código.
115
+
116
+ `services/tools.ts` sólo cubre lo que una UI necesita a diario: ver el catálogo,
117
+ encender y apagar, y corregir un nombre o una descripción.
118
+
119
+ ---
120
+
121
+ ## Enjambres
122
+
123
+ Hasta `SwarmDoc` un enjambre existía **sólo mientras corría**: `runRoleSwarm()`
124
+ recibe los agentes en la llamada y no persiste nada. Quien armara uno desde una
125
+ interfaz lo perdía al cerrar la ventana. Era el bloqueador real para poner una
126
+ UI encima del SDK.
127
+
128
+ ```typescript
129
+ const s = await swarms.createSwarm({
130
+ name: "Revisión en cadena",
131
+ strategy: "sequential", // "parallel" | "hierarchical"
132
+ members: [
133
+ { agentId: "redactor", orderIndex: 0 },
134
+ { agentId: "revisor", orderIndex: 1 },
135
+ ],
136
+ });
137
+
138
+ await swarms.runSwarm(s.id, "Escribí el informe de agosto", {
139
+ credentials: { apiKey: keyDelInquilino }, // multi-tenant
140
+ onMessage: (m) => guardarPaso(m), // acá persiste tu app si quiere
141
+ });
142
+ ```
143
+
144
+ **La validación ocurre al guardar, no al correr.** Un enjambre jerárquico sin
145
+ orquestador, o con un agente que ya no existe, es un error de configuración:
146
+ descubrirlo cuando alguien lo ejecuta —posiblemente semanas después— es
147
+ descubrirlo tarde.
148
+
149
+ Un enjambre deshabilitado no corre. Si corriera igual, el interruptor de la UI
150
+ sería decorativo.
151
+
152
+ ---
153
+
154
+ ## Skills: disco y base de datos
155
+
156
+ Conviven dos orígenes y no compiten:
157
+
158
+ - **Disco** — carpetas con `SKILL.md` (frontmatter YAML + cuerpo markdown), que
159
+ `SkillLoader` lee del bundle, `~/.hive/skills`, `extraDirs` y el workspace. Es
160
+ la vía de `hives add-skill`, versionable con git.
161
+ - **Base de datos** — lo que el runtime consulta y lo que una UI edita.
162
+
163
+ `importSkillFromDisk()` es el puente:
164
+
165
+ ```bash
166
+ hives add-skill mi-skill # genera el andamiaje
167
+ ```
168
+ ```typescript
169
+ await skills.importSkillFromDisk("./skills/mi-skill"); // lo materializa como fila editable
170
+ ```
171
+
172
+ Es idempotente por id: editás el archivo, reimportás, y actualiza en vez de
173
+ duplicar. Reutiliza el mismo `parseFrontmatter` que `SkillLoader` — con dos
174
+ parsers distintos, un día aceptarían formatos distintos.
175
+
176
+ A diferencia de una tool, una skill **es instruccional**: metadatos más un cuerpo
177
+ markdown que se le inyecta al agente, sin `execute`. Por eso sí puede crearla un
178
+ usuario desde una interfaz sin abrir la puerta a ejecutar código arbitrario.
179
+
180
+ Cada alta, edición o borrado re-sincroniza el índice BM25: una skill que no está
181
+ indexada es una skill que el modelo no encuentra.
182
+
183
+ ---
184
+
185
+ ## Cron: híbrido a propósito
186
+
187
+ Si hay un `CronScheduler` corriendo, se delega en él — es quien sabe calcular la
188
+ próxima ejecución y rearmar los timers. Si no, se opera directo sobre la
189
+ colección, para que un proceso que sólo administra tareas (una UI, un script) no
190
+ necesite levantar el scheduler entero. Una tarea creada sin scheduler queda
191
+ persistida y la recoge el próximo arranque.
192
+
193
+ `triggerCronJob()` es la excepción: **exige** scheduler, porque sin él no hay
194
+ nada que la ejecute y devolver `true` sería mentir.
195
+
196
+ ---
197
+
198
+ ## MCP: probar antes de guardar
199
+
200
+ `testMcpServer()` intenta la conexión y responde si funcionó. Requiere un
201
+ `MCPClientManager` activo: sin él no hay quién hable el protocolo.
202
+
203
+ ```typescript
204
+ const r = await mcp.testMcpServer("mi-servidor");
205
+ if (!r.ok) mostrarError(r.error);
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Endpoints HTTP como herramientas
211
+
212
+ Es lo más cerca de "crear una tool desde la UI" sin abrir la puerta a ejecutar
213
+ código arbitrario: el usuario aporta **datos** —URL, método, cabeceras, qué
214
+ parámetros acepta— y el ejecutor es genérico.
215
+
216
+ ```typescript
217
+ await endpoints.createEndpoint({
218
+ name: "Clima",
219
+ description: "Consulta el clima de una ciudad", // esto es lo que lee el modelo
220
+ method: "GET",
221
+ url: "https://api.example.com/clima",
222
+ query: { ciudad: "{{ciudad}}" },
223
+ secretHeaders: { Authorization: "Bearer sk-..." },
224
+ paramSchema: {
225
+ type: "object",
226
+ properties: { ciudad: { type: "string", description: "Nombre de la ciudad" } },
227
+ required: ["ciudad"],
228
+ },
229
+ });
230
+ ```
231
+
232
+ Queda disponible como `endpoint_clima`. Al registrarse hace tres cosas: guarda
233
+ la definición, **cifra las credenciales aparte**, y escribe su fila en `tools`
234
+ más el reíndice — sin ese último paso el modelo nunca sabría que existe, porque
235
+ el loadout inicial es mínimo por diseño.
236
+
237
+ **La credencial no vuelve a salir.** `getEndpoint` y `listEndpoints` devuelven
238
+ `secretHeaderNames` (qué cabeceras hay configuradas) pero nunca sus valores, y
239
+ tampoco aparecen en el resultado de una ejecución. Es lo que hace que un
240
+ endpoint sea más seguro que darle al modelo una `api_request` con la clave
241
+ escrita en el prompt.
242
+
243
+ `testEndpoint(id, params)` lo llama sin pasar por el modelo, para probarlo antes
244
+ de dejárselo a un agente. Tras un reinicio, `registerEndpointTools()` rearma las
245
+ tools en memoria desde la base.
246
+
247
+ ---
248
+
249
+ ## Seed selectivo: elegir los agentes
250
+
251
+ Históricamente el seed era todo o nada: las 8 personas del catálogo en cada
252
+ arranque, con las 62 tools activas. Para un producto donde cada quien arma su
253
+ enjambre eso es demasiado — y contradictorio, porque el usuario termina apagando
254
+ a mano lo que nunca pidió.
255
+
256
+ ### La colmena arranca vacía si querés
257
+
258
+ ```typescript
259
+ await ensureHiveDb({ specialists: "none" }); // ningún especialista
260
+ await ensureHiveDb({ specialists: ["web_researcher"] }); // sólo ése
261
+ await ensureHiveDb(); // los 8 (default, sin cambios)
262
+ ```
263
+
264
+ Con `"none"`, una instalación limpia queda así:
265
+
266
+ | | |
267
+ |---|---|
268
+ | Especialistas | 0 |
269
+ | Tools activas | las 8 `MINIMAL_TOOLS` |
270
+ | Skills activas | 0 |
271
+ | Filas de tools/skills | **todas**, apagadas |
272
+
273
+ Las mínimas quedan prendidas porque son la competencia del coordinador —delegar,
274
+ buscar, avisar— y sin ellas no hay colmena a la que agregarle especialistas. Y
275
+ las filas existen todas aunque estén apagadas: activarlas después no requiere
276
+ volver a sembrar nada desde el código.
277
+
278
+ **La elección gobierna qué se crea, nunca qué se conserva.** Arrancar con
279
+ `"none"` en una base que ya tiene sus ocho agentes **no borra ninguno**: se
280
+ siguen reconciliando en cada arranque como siempre. Cambiar de modo es seguro.
281
+
282
+ ```typescript
283
+ setup.listCatalogPersonas(); // qué ofrecer en la UI
284
+ const plan = setup.planSeedFor(["web_researcher"]); // qué se instalaría, sin tocar nada
285
+ await setup.applySeedPlan(["web_researcher"]); // aplicarlo
286
+ ```
287
+
288
+ **Se siembra la unión, no "lo de cada agente".** Es el error obvio y no
289
+ funciona, porque las tools se comparten: `web_fetch` lo declaran el investigador
290
+ web y el operador de navegador; `fs_*`, el operador de archivos y el ingeniero.
291
+ A eso se suman las `MINIMAL_TOOLS` —delegar, buscar, avisar— que el coordinador
292
+ necesita siempre, haya los agentes que haya. Y los globs se expanden: `fs_*` no
293
+ es una tool, son varias.
294
+
295
+ **Desactivar no borra.** Las filas de `tools` y `skills` son globales y
296
+ compartidas; borrar las de un agente rompería a otro. Se marca el agente y se
297
+ recalcula la unión:
298
+
299
+ ```typescript
300
+ await setup.disableCatalogAgent("web_researcher");
301
+ // browser_operator sigue activo, así que web_fetch sigue activa
302
+ ```
303
+
304
+ **Y sobrevive al reinicio.** El reseed del arranque reescribe las filas de tools
305
+ y skills desde el código —descripción y categoría son la fuente de verdad— pero
306
+ **preserva `active`**, que es la elección del usuario. Sin eso, el seed
307
+ selectivo duraría hasta el próximo reinicio.
308
+
309
+ ### Armar un enjambre también siembra
310
+
311
+ El mismo mecanismo funciona al crear un enjambre: los especialistas que el
312
+ enjambre nombra son los que definen qué tools y skills hacen falta.
313
+
314
+ ```typescript
315
+ // 1. Ver qué se encendería, sin encender nada
316
+ const gap = await setup.planActivationFor(["web_researcher", "software_engineer"]);
317
+ // → { agents: ["software_engineer"], tools: ["cli_exec", …], skills: […], nonCatalog: [] }
318
+
319
+ // 2. Crear el enjambre activando sus especialistas
320
+ const enjambre = await createSwarm({
321
+ name: "Equipo mixto",
322
+ strategy: "sequential",
323
+ members: [{ agentId: "web_researcher" }, { agentId: "software_engineer" }],
324
+ activateMembers: true,
325
+ });
326
+ ```
327
+
328
+ **`activateMembers` es `false` por defecto, a propósito.** Crear un enjambre no
329
+ debería cambiar en silencio qué capacidades tiene la instalación entera: si el
330
+ usuario apagó `cli_exec`, guardar un enjambre no es motivo suficiente para
331
+ volver a encenderla. Con `false` el enjambre se crea igual y el faltante vuelve
332
+ en `pendingActivation`, para que la UI lo muestre y el usuario decida.
333
+
334
+ Sin esto, un enjambre se guardaba **sin una queja** con especialistas apagados y
335
+ sus tools inactivas: la fila del agente existe siempre —el seed las crea todas y
336
+ sólo cambia `enabled`—, así que la validación de "el agente existe" pasaba
337
+ igual. El enjambre quedaba definido y sin poder trabajar.
338
+
339
+ **Activar es siempre la unión.** Encender los especialistas de un enjambre nunca
340
+ apaga los de otro: se recalcula sobre lo que ya estaba activo. `updateSwarm`
341
+ pasa por el mismo camino, así que agregar un especialista a un enjambre que ya
342
+ existe se comporta igual que crearlo con él.
343
+
344
+ `planActivationFor` devuelve el **faltante**, no el conjunto entero: una tool
345
+ que ya está activa porque la usa otro agente no aparece, porque prometerle a la
346
+ UI un cambio que no va a ocurrir es peor que no decir nada. Los miembros que no
347
+ son del catálogo salen aparte en `nonCatalog` —traen sus propias tools— y no
348
+ hacen fallar el plan.
349
+
350
+ ---
351
+
352
+ ## Skills declaradas en código
353
+
354
+ `createAgent({ skills })` ahora **sí** hace algo. Estaba tipado y se descartaba
355
+ en silencio: declarar una skill no tenía ningún efecto. Ahora se materializa
356
+ como fila y se indexa, igual que ya ocurría con `tools`. Es idempotente —
357
+ declarar la misma skill dos veces la actualiza.
358
+
359
+ ---
360
+
361
+ ## Imágenes
362
+
363
+ `@johpaz/hive-sdk/images` usa `Bun.Image` — sharp integrado en el runtime, sin
364
+ dependencias nativas ni bindings que compilar. El SDK ya exige Bun ≥ 1.4.2, así
365
+ que no agrega ningún requisito.
366
+
367
+ Cubre dos cosas que conviene no confundir:
368
+
369
+ **1. Tools activables** (`image_metadata`, `image_transform`), como cualquier
370
+ otra del catálogo. Trabajan sobre **artefactos**, no sobre base64 suelto:
371
+ entra un `artifact_id` y sale otro. Devolverle una imagen en base64 al modelo es
372
+ exactamente lo que llena la ventana de contexto.
373
+
374
+ ```typescript
375
+ // El modelo maneja referencias; sólo mira la imagen si de verdad la necesita.
376
+ { ok: true, artifact_id: "art_...", width: 64, height: 64, format: "webp" }
377
+ ```
378
+
379
+ **2. Normalización de lo que entra.** Una foto de teléfono son varios megabytes
380
+ y unos cuantos miles de tokens. `multimodalService.processImage()` la achica
381
+ antes de mandarla al modelo:
382
+
383
+ ```
384
+ 3024x4032 JPEG, 217 KB → 768x1024 WebP, 4 KB (98% menos)
385
+ ```
386
+
387
+ Y el ahorro se multiplica: esa imagen no viaja una sola vez, queda en el
388
+ historial y se reenvía en cada turno siguiente.
389
+
390
+ Es best-effort. Si el runtime no puede procesarla o la imagen está corrupta, se
391
+ manda tal cual — perderla sería peor que mandarla grande. Una URL no se toca:
392
+ no ocupa contexto, porque la descarga la hace el proveedor.
393
+
394
+ ```typescript
395
+ import { normalizeForModel, transformImage, measureImage } from "@johpaz/hive-sdk/images";
396
+ ```
397
+
398
+ > Detalle de `Bun.Image`: las transformaciones son **diferidas**. `metadata()`
399
+ > sobre una cadena sin materializar devuelve las dimensiones del origen, no las
400
+ > del resultado — hay que pedir los bytes y releerlos. `measureImage()` y
401
+ > `transformImage()` ya lo hacen.
402
+
403
+ ---
404
+
405
+ ## Referencia
406
+
407
+ Todo desde `@johpaz/hive-sdk/services`, disponible suelto o por dominio
408
+ (`import { agents } from "..."` → `agents.createAgent`).
409
+
410
+ ### `agents`
411
+ `createAgent` · `getAgent` · `listAgents` · `updateAgent` · `deleteAgent`
412
+ `assignTools` · `assignSkills` · `assignMcpServers` · `enableAgent` · `disableAgent`
413
+ `slugify` — el id se deriva del nombre normalizando acentos: "Diseño" → `diseno`, no `dise_o`.
414
+
415
+ ### `swarms`
416
+ `createSwarm` · `getSwarm` · `listSwarms` · `updateSwarm` · `deleteSwarm` · `toggleSwarm` · `runSwarm`
417
+
418
+ ### `skills`
419
+ `createSkill` · `getSkill` · `listSkills` · `updateSkill` · `deleteSkill` · `toggleSkill`
420
+ `importSkillFromDisk` — el puente entre `hives add-skill` y la base.
421
+
422
+ ### `tools`
423
+ `listTools` · `getTool` · `toggleTool` · `updateToolMetadata`
424
+ No hay `createTool`: una tool es código. Ver arriba las tres vías reales.
425
+
426
+ ### `endpoints`
427
+ `createEndpoint` · `getEndpoint` · `listEndpoints` · `updateEndpoint` · `deleteEndpoint`
428
+ `toggleEndpoint` · `testEndpoint` · `registerEndpointTools` · `buildEndpointTool` · `toolNameFor`
429
+
430
+ ### `providers` y `models`
431
+ `listProviders` · `getProvider` · `createProvider` · `updateProvider` · `toggleProvider` · `deleteProvider`
432
+ `listModels` · `getModel` · `createModel` · `toggleModel` · `deleteModel` · `renameModel` · `agentsUsingModel`
433
+
434
+ ### `mcp`
435
+ `listMcpServers` · `getMcpServer` · `createMcpServer` · `updateMcpServer`
436
+ `testMcpServer` · `toggleMcpServer` · `deleteMcpServer`
437
+
438
+ ### `cron`
439
+ `createCronJob` · `getCronJob` · `listCronJobs` · `updateCronJob` · `deleteCronJob`
440
+ `pauseCronJob` · `resumeCronJob` · `triggerCronJob` · `getCronHistory` · `hasScheduler`
441
+
442
+ ### `memory`
443
+ `writeMemory` · `readMemory` · `listMemories` · `searchMemories` · `deleteMemory`
444
+ Aisladas por usuario: el `userId` es opcional y se resuelve del contexto.
445
+
446
+ ### `ethics`
447
+ `listEthics` · `getEthics` · `createEthics` · `updateEthics` · `toggleEthics` · `deleteEthics`
448
+
449
+ ### `images`
450
+ `uploadImage` · `transformStoredImage` · `getImageBytes` · `listImages`
451
+ `setImageRetention` · `deleteImage` · `applyPreset` · `IMAGE_PRESETS`
452
+
453
+ ### `setup`
454
+ `listCatalogPersonas` · `planSeedFor` · `applySeedPlan` · `planActivationFor`
455
+ `enableCatalogAgent` · `enableCatalogAgents` · `disableCatalogAgent`
456
+ `listEnabledCatalogAgents` · `CATALOG_AGENT_IDS`
457
+
458
+ *Documentación Hive SDK — ver `version` en package.json*
@@ -0,0 +1,146 @@
1
+ # API-SESSIONS — la conversación de un usuario, como una sola cosa
2
+
3
+ ## Por qué existe
4
+
5
+ "Sesión" estaba repartida en cuatro capas que nadie unía:
6
+
7
+ | Pieza | Qué guardaba |
8
+ |---|---|
9
+ | `agent/thread-store.ts` | Identidad y catálogo del hilo |
10
+ | `agent/conversation-store.ts` | Los mensajes |
11
+ | `agent/run-store.ts` | La ejecución: checkpoint, lease, reanudación |
12
+ | `state/store.ts` | Un `Map` en memoria que moría con el proceso |
13
+
14
+ El resultado eran dos identificadores para lo mismo —`thread_id` para la
15
+ conversación, `run_id` para la ejecución— y **ninguna forma de preguntar "qué
16
+ sesiones tiene este usuario"** sin escanear mensajes.
17
+
18
+ ```typescript
19
+ import { createSession, listSessions, appendMessage, resumeSession } from "@johpaz/hive-sdk/sessions";
20
+ ```
21
+
22
+ `Session` es una **vista compuesta** sobre las colecciones que ya existían: no
23
+ agrega una tercera persistencia. Agregar una colección propia habría recreado
24
+ exactamente la duplicación que este módulo viene a cerrar. `Session.id` **es** el
25
+ `threadId`.
26
+
27
+ ## Un hilo por canal y por contacto
28
+
29
+ ```typescript
30
+ const s = await createSession({ userId: "u1", channel: "telegram", peerId: "12345" });
31
+ s.id; // "u1/telegram/12345"
32
+ ```
33
+
34
+ La web es un caso más: `createWebSession()` abre una conversación nueva con su
35
+ propio id, y `mostRecentWebSession()` devuelve en la que el usuario seguiría
36
+ escribiendo.
37
+
38
+ Antes de la separación por canal todos compartían un único hilo por usuario. Esa
39
+ fila legacy sigue siendo legible: se registra como una conversación más sin
40
+ mover un solo mensaje.
41
+
42
+ ## Listar, que es lo que no se podía
43
+
44
+ ```typescript
45
+ await listSessions("u1"); // activas, de la más reciente a la más vieja
46
+ await listSessions("u1", { channel: "webchat" });
47
+ await listSessions("u1", { includeArchived: true });
48
+ await listSessions("u1", { withRuns: true }); // adjunta la última ejecución
49
+ ```
50
+
51
+ `withRuns` cuesta una consulta por sesión, así que está apagado por defecto: la
52
+ lista de conversaciones de una UI no lo necesita.
53
+
54
+ ## Retomar lo que quedó a medias
55
+
56
+ ```typescript
57
+ const pendiente = await resumeSession(s.id);
58
+ if (pendiente) {
59
+ pendiente.run.runId; // la ejecución interrumpida
60
+ pendiente.checkpoint.messages; // dónde se quedó
61
+ }
62
+ ```
63
+
64
+ Devuelve `null` cuando no hay nada que retomar, que es el caso normal. Una
65
+ ejecución `running` sin checkpoint tampoco sirve: murió antes de guardar estado.
66
+
67
+ ## Cerrar no es borrar
68
+
69
+ ```typescript
70
+ await closeSession(s.id); // sale de la lista, el historial queda
71
+ await reopenSession(s.id);
72
+ await deleteSession(s.id); // borra mensajes, resumen, notas y la fila
73
+ ```
74
+
75
+ ## Identidad entre canales
76
+
77
+ Un mensaje de Telegram trae un id de Telegram, no un usuario de la colmena.
78
+ `resolveContext` traduce: busca la identidad en `userIdentities` y devuelve el
79
+ usuario, el hilo y el agente que deben atenderlo, creando el hilo si hace falta.
80
+
81
+ ```typescript
82
+ import { resolveContext } from "@johpaz/hive-sdk/sessions";
83
+
84
+ const { userId, threadId, agentId, isNewUser } = await resolveContext({
85
+ channel: "telegram",
86
+ channelUserId: "12345",
87
+ accountId: "mi-bot",
88
+ });
89
+ ```
90
+
91
+ > **Ojo con la auto-vinculación.** Si la identidad no existe, se asocia al
92
+ > **único usuario existente** — coherente con hive, que es mono-usuario, pero en
93
+ > un despliegue con varios significa que el primer desconocido que escriba por
94
+ > un canal quedaría vinculado a quien estuviera. Para eso está
95
+ > `security/pairing.ts`, que exige aprobación antes de crear la identidad: un
96
+ > host multi-usuario debe ponerlo delante.
97
+
98
+ ## La memoria está aislada por usuario
99
+
100
+ ```typescript
101
+ import { writeMemory, listMemories } from "@johpaz/hive-sdk/services";
102
+
103
+ await writeMemory("presupuesto", "5000", "ana");
104
+ await writeMemory("presupuesto", "900", "beto"); // otra memoria, no un pisotón
105
+ ```
106
+
107
+ La colección era **global al proceso**: el id era sólo el título, así que dos
108
+ usuarios no podían tener una memoria con el mismo nombre y cualquiera veía la
109
+ del otro. Ahora el id es `${userId}:${title}` y toda lectura filtra por dueño.
110
+
111
+ `userId` es opcional: sin él se resuelve el del contexto, para que las tools del
112
+ modelo sigan funcionando igual. Las filas anteriores se migran al arrancar
113
+ asignándolas al usuario existente — no se pierde ninguna.
114
+
115
+ ## Consistencia eventual del catálogo
116
+
117
+ `appendMessage` persiste el mensaje **al instante**, pero la actualización del
118
+ catálogo (título, contador, orden) es deliberadamente *fire-and-forget*: nunca
119
+ se bloquea la escritura de un mensaje detrás de un contador.
120
+
121
+ En la práctica, `messageCount` y `title` son de consistencia eventual — leer la
122
+ sesión inmediatamente después de escribir puede devolver el valor anterior.
123
+ `getSessionHistory()` sí es consistente al instante.
124
+
125
+ ## El identificador del hilo
126
+
127
+ `Session.id` **es** el `threadId`, con forma `${userId}/${canal}/${peer}`.
128
+
129
+ | | |
130
+ |---|---|
131
+ | `makeThreadId(userId, canal, peer)` | Construirlo. |
132
+ | `parseThreadId(id)` | Devuelve las tres partes, o `null` si no tiene esa forma. |
133
+ | `isStructuredThreadId(id)` | Si sigue el formato. La fila del hilo legacy —anterior a la separación por canal— no lo sigue: su id es el `userId` pelado. |
134
+ | `sanitizeSegment(v)` | Limpia un segmento; el separador es `/`, así que un `:` en el peer rompería el parseo. |
135
+ | `newWebConversationId()` | Un id de conversación web nuevo. |
136
+
137
+ ## Resto de la superficie
138
+
139
+ `renameSession(id, título)` — el título se deriva solo del primer mensaje del
140
+ usuario, pero se puede fijar a mano.
141
+
142
+ `sessionForChannel(userId, canal)` — a qué hilo escribirle a alguien cuando no
143
+ venimos de un mensaje suyo: el aviso de una tarea programada, por ejemplo.
144
+ Devuelve `null` si no hay ninguno, y quien llame decide el respaldo.
145
+
146
+ *Documentación Hive SDK — ver `version` en package.json*