@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.
- package/CHANGELOG.md +20 -4
- package/README.md +48 -7
- package/SECURITY.md +17 -0
- package/docs/API-AGENTS.md +430 -0
- package/docs/API-ARTIFACTS.md +55 -0
- package/docs/API-CONTEXT-COMPILER.md +285 -0
- package/docs/API-CRON.md +188 -0
- package/docs/API-DAG-SCHEDULER.md +291 -0
- package/docs/API-HOOKS.md +147 -0
- package/docs/API-RESILIENCE.md +45 -0
- package/docs/API-SERVICES.md +458 -0
- package/docs/API-SESSIONS.md +146 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +499 -0
- package/docs/API-WORKERS-EVENTS.md +311 -0
- package/docs/HIVE-HARNESS.md +232 -0
- package/docs/INDEX.md +198 -0
- package/docs/SECURITY-GUARDRAILS.md +87 -0
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/docs/UPGRADING.md +65 -0
- package/docs/assets/logoblack.png +0 -0
- package/docs/assets/logocolor-dark.png +0 -0
- package/docs/assets/logocolorbg.png +0 -0
- package/docs/plans/2026-09-05-office-dependency-hardening-design.md +28 -0
- package/docs/plans/2026-09-06-dependency-audit-remediation-design.md +25 -0
- package/docs/plans/2026-09-06-pptx-image-size-remediation-design.md +54 -0
- package/docs/plans/2026-09-06-typescript7-bun142-documentation-design.md +48 -0
- package/package.json +5 -4
- package/packages/core/src/agent/llm-providers/hiveagents.ts +2 -2
- package/packages/core/src/agent/providers/index.ts +17 -1
- package/packages/core/src/api/createAgent.ts +4 -2
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/mcp/transports/sse.ts +11 -3
- package/packages/core/src/mcp/transports/websocket.ts +11 -9
- package/packages/core/src/tool-runtime/tool-worker.ts +3 -1
- package/packages/core/src/tools/office/office-escribir-pptx.ts +3 -1
- package/packages/core/src/vendor/pptxgenjs/LICENSE +21 -0
- package/packages/core/src/vendor/pptxgenjs/README.md +17 -0
- package/packages/core/src/vendor/pptxgenjs/pptxgen.es.d.ts +17 -0
- package/packages/core/src/vendor/pptxgenjs/pptxgen.es.js +7368 -0
- 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*
|