@johpaz/hive-sdk 0.4.9 → 0.5.1
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 +85 -1
- package/README.md +9 -4
- package/docs/API-AGENTS.md +70 -0
- package/docs/INDEX.md +5 -1
- package/docs/UPGRADING.md +20 -0
- package/package.json +1 -1
- package/packages/core/src/agent/agent-loop.ts +82 -5
- package/packages/core/src/agent/context-compiler.ts +122 -23
- package/packages/core/src/agent/index.ts +2 -0
- package/packages/core/src/agent/jev-decisions.ts +197 -0
- package/packages/core/src/agent/jev-planner.ts +298 -0
- package/packages/core/src/agent/llm-client.ts +2 -2
- package/packages/core/src/agent/llm-providers/anthropic.ts +2 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +3 -1
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +2 -1
- package/packages/core/src/agent/tool-selector.ts +1 -0
- package/packages/core/src/canvas/emitter.ts +19 -0
- package/packages/core/src/events/channel-narration.ts +3 -0
- package/packages/core/src/multimodal/vision-service.ts +7 -7
- package/packages/core/src/services/swarms.ts +4 -0
- package/packages/core/src/storage/collections.ts +9 -2
- package/packages/core/src/storage/crypto.ts +51 -17
- package/packages/core/src/storage/index.ts +1 -0
- package/packages/core/src/storage/seed.ts +4 -0
- package/packages/core/src/storage/usage.ts +55 -2
- package/packages/core/src/swarm/RoleSwarm.ts +6 -0
- package/packages/core/src/tool-runtime/index.ts +20 -1
- package/packages/core/src/tools/agents/get-available-models.ts +3 -1
- package/packages/core/src/tools/core/index.ts +33 -2
- package/packages/core/src/tools/web/computer-use.ts +2 -2
- package/packages/core/src/voice/index.ts +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Seguridad — claves aisladas por inquilino
|
|
6
|
+
|
|
7
|
+
- **Secretos filtrados entre inquilinos por la caché en memoria.** El
|
|
8
|
+
almacén de secretos cacheaba cada valor descifrado en un `Map` del proceso
|
|
9
|
+
indexado sólo por nombre (`provider:openai:api_key`), y el llavero del SO
|
|
10
|
+
también es de toda la máquina. La colección `secrets` sí está particionada,
|
|
11
|
+
pero la caché la tapaba: después de que un inquilino leyera o guardara su
|
|
12
|
+
clave, `loadProviderApiKey` le devolvía esa misma clave a cualquier otro
|
|
13
|
+
inquilino del proceso, y `resolveProviderConfig` la usaba para cobrarle a
|
|
14
|
+
la cuenta equivocada cuando la llamada no traía `credentials`. Ahora la
|
|
15
|
+
caché se indexa por inquilino y, con un inquilino activo, el llavero del SO
|
|
16
|
+
no se lee, no se escribe ni se borra. Sin inquilino (escritorio) todo sigue
|
|
17
|
+
igual.
|
|
18
|
+
- **La clave del entorno ya no se usa en nombre de un inquilino.** Con un
|
|
19
|
+
inquilino activo y sin `credentials` ni clave guardada, `resolveProviderConfig`
|
|
20
|
+
caía en `<PROVEEDOR>_API_KEY` del proceso, que es la cuenta de la plataforma;
|
|
21
|
+
lo mismo OCR (`vision-service`), voz (STT/TTS), `computer_use` y Jev. Ahora
|
|
22
|
+
todos pasan por `envSecret(nombre)`, que dentro de un inquilino devuelve
|
|
23
|
+
`undefined`. Los adaptadores de Anthropic, Gemini y Gemini Live pasan siempre
|
|
24
|
+
una cadena al SDK del proveedor, porque con `undefined` esos SDK leían la
|
|
25
|
+
variable de entorno por su cuenta. **Cambio de comportamiento:** un host
|
|
26
|
+
multi-inquilino que dependía de ese respaldo tiene que pasar la clave en
|
|
27
|
+
`credentials` o guardarla en los secretos del inquilino; sin eso la llamada
|
|
28
|
+
falla por falta de clave en vez de cobrarse a la plataforma. Sin inquilino
|
|
29
|
+
(escritorio) el entorno sigue siendo el último respaldo.
|
|
30
|
+
- Nuevo `envSecret(nombre)` en `@johpaz/hive-sdk/storage`: la variable de
|
|
31
|
+
entorno sólo fuera de un inquilino. Úsalo en tus propias tools en lugar de
|
|
32
|
+
`process.env.X_API_KEY`.
|
|
33
|
+
- Quitado `loadDurableProviderApiKey` (interno, agregado en 0.5.0 y nunca
|
|
34
|
+
exportado por un barrel): `loadProviderApiKey` ya es seguro por inquilino.
|
|
35
|
+
|
|
36
|
+
## 0.5.0
|
|
37
|
+
|
|
38
|
+
### Jev — plano de decisión (OpenRouter Decisions)
|
|
39
|
+
|
|
40
|
+
Portado de hive 1.1.0. Jev decide por turno qué historial, herramientas,
|
|
41
|
+
skills, notas y reglas del playbook entran al contexto; entre iteraciones poda
|
|
42
|
+
resultados viejos de herramientas y sugiere la siguiente acción; decide si un
|
|
43
|
+
lote de herramientas corre en paralelo, y conoce el mapa del enjambre
|
|
44
|
+
(especialistas y estado de cada MCP) para recomendar a quién delegar.
|
|
45
|
+
**Sin clave de OpenRouter, Jev no existe y todo corre igual que antes.**
|
|
46
|
+
|
|
47
|
+
- **Clave inyectable por llamada**: `jev?: { apiKey, mcpSettingsPath? } | false`
|
|
48
|
+
en `AgentLoopOptions`, `compileContext`, `IsolatedAgentOptions`,
|
|
49
|
+
`runRoleSwarm` y `runSwarm`, igual que `credentials`. `false` lo apaga;
|
|
50
|
+
sin la opción decide la fila `openrouter` del inquilino actual. Con un
|
|
51
|
+
inquilino activo **nunca** se usa `OPENROUTER_API_KEY` ni la caché de
|
|
52
|
+
secretos del proceso: la clave de la plataforma no se usa en nombre de un
|
|
53
|
+
cliente.
|
|
54
|
+
- **Estado por inquilino**: fallos, cooldown y totales se llevan por
|
|
55
|
+
`currentTenant()`; una clave inválida de un cliente no pone en fallback a
|
|
56
|
+
los demás.
|
|
57
|
+
- **Evento para el host**: cada decisión llega por `onStep` como
|
|
58
|
+
`StepEvent` `jev_decision` (`jev`: agente, tipo, resumen, tokens ahorrados,
|
|
59
|
+
latencia, costo, especialista recomendado, MCP apagados). También se emite
|
|
60
|
+
`canvas:jev_decision` / `canvas:jev_status` para hosts tipo hive.
|
|
61
|
+
- **Uso y costo**: `recordJevDecision` y los campos `jev*` de
|
|
62
|
+
`UsageRollupDoc`; `getUsageStats()` devuelve `jev` con el total y el
|
|
63
|
+
desglose por agente. El ahorro se estima (caracteres/4) y se cotiza con el
|
|
64
|
+
modelo del agente asesorado.
|
|
65
|
+
- **Catálogo**: modelo `openrouter/typesafe/jev-1.13` con `modelType:
|
|
66
|
+
"decision"`, excluido de `get_available_models` (y de `getDefaultLLM`, que
|
|
67
|
+
sólo toma modelos `llm`).
|
|
68
|
+
- Herramienta nueva `conversation_read`: recupera mensajes o notas que Jev
|
|
69
|
+
dejó fuera del contexto, siempre dentro del hilo actual.
|
|
70
|
+
- `executeToolBatch` acepta `parallelToolCalls` por lote.
|
|
71
|
+
- `NarrationEventDoc.kind` suma `"decision"`: un host puede anotar las
|
|
72
|
+
decisiones de Jev en `narrationEvents` para sus vistas de actividad.
|
|
73
|
+
`shouldDeliverToChannel` nunca lo entrega a un canal.
|
|
74
|
+
- `loadDurableProviderApiKey(id)`: lee la clave sólo de la colección
|
|
75
|
+
`secrets` (particionada por inquilino), sin pasar por la caché de proceso ni
|
|
76
|
+
el llavero del SO.
|
|
77
|
+
|
|
78
|
+
**Privacidad.** Con Jev activo se envían a la API de decisiones de OpenRouter
|
|
79
|
+
(`https://openrouter.ai/api/alpha/decisions`), con la clave del workspace:
|
|
80
|
+
el objetivo del turno (hasta 3 500 caracteres), extractos de hasta 450
|
|
81
|
+
caracteres de mensajes previos del hilo, nombre y descripción de herramientas
|
|
82
|
+
y skills candidatas, notas del scratchpad y reglas del playbook (hasta 350
|
|
83
|
+
caracteres cada una), extractos de resultados de herramientas (hasta 650
|
|
84
|
+
caracteres), argumentos de llamadas en lote (hasta 700) y el mapa del enjambre
|
|
85
|
+
(ids, nombres y descripciones de especialistas, nombres y estado de los MCP).
|
|
86
|
+
No se envían ids de MCP con prefijo de inquilino, credenciales ni adjuntos
|
|
87
|
+
binarios. Un host que no quiera enviar nada pasa `jev: false`.
|
|
4
88
|
|
|
5
89
|
### Plataforma y seguridad
|
|
6
90
|
|
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ bun add @johpaz/hive-sdk
|
|
|
25
25
|
- **Runtime**: ejecución paralela de tools vía Bun Workers.
|
|
26
26
|
- **Gateway**: servidor HTTP/WebSocket para exponer agentes como API.
|
|
27
27
|
- **Memoria y estado**: HiveDB (colecciones + índice BM25), scratchpad, context compiler con compactación.
|
|
28
|
+
- **Jev (opcional)**: plano de decisión sobre la API Decisions de OpenRouter. Por turno elige qué historial, tools, skills, notas y reglas entran al contexto, poda resultados viejos entre iteraciones y decide si un lote de tools corre en paralelo. Sin clave de OpenRouter no existe y todo corre igual. Ver [API-AGENTS.md](./docs/API-AGENTS.md#jev-plano-de-decisión).
|
|
29
|
+
- **Multi-inquilino**: varios enjambres en una sola HiveDB con `runInTenant`; credenciales y clave de Jev por llamada (`credentials`, `jev`), sin que la clave de un inquilino ni la de la plataforma se usen en nombre de otro.
|
|
28
30
|
- **Servicios**: CRUD tipado de agentes, enjambres, skills, modelos, MCP y cron para montarle **la interfaz que quieras** — móvil, web o escritorio. Ver [API-SERVICES.md](./docs/API-SERVICES.md).
|
|
29
31
|
- **Sesiones**: un hilo por canal y por contacto, con historial, resumen y reanudación tras un corte.
|
|
30
32
|
- **Imágenes**: redimensionar y convertir con `Bun.Image`, sin dependencias nativas. Las imágenes entrantes se normalizan antes de llegar al modelo — una foto de cámara pasa de 217 KB a 4 KB.
|
|
@@ -197,7 +199,10 @@ capturar. El SDK lo resuelve con `resolvePort`: avisa y sigue con el default.
|
|
|
197
199
|
|
|
198
200
|
La API key de cada provider se guarda cifrada en la base. Como alternativa, el
|
|
199
201
|
SDK cae a `<PROVIDER>_API_KEY` del entorno, en mayúsculas y con el id del
|
|
200
|
-
provider tal cual
|
|
202
|
+
provider tal cual. **Sólo sin inquilino** (app de escritorio, un proceso por
|
|
203
|
+
instalación): dentro de `runInTenant` el entorno es de la plataforma, no del
|
|
204
|
+
cliente, y no se usa nunca — la clave llega en `credentials` o desde los
|
|
205
|
+
secretos del inquilino. Ver [UPGRADING.md](./docs/UPGRADING.md#051-claves-aisladas-por-inquilino).
|
|
201
206
|
|
|
202
207
|
```bash
|
|
203
208
|
OPENAI_API_KEY=sk-...
|
|
@@ -205,7 +210,7 @@ ANTHROPIC_API_KEY=sk-ant-...
|
|
|
205
210
|
GOOGLE_API_KEY=... # provider "gemini"
|
|
206
211
|
MODELSCOPE_API_KEY=ms-...
|
|
207
212
|
NVIDIA_API_KEY=nvapi-...
|
|
208
|
-
OPENROUTER_API_KEY=sk-or-...
|
|
213
|
+
OPENROUTER_API_KEY=sk-or-... # también activa Jev (con el provider openrouter habilitado)
|
|
209
214
|
```
|
|
210
215
|
|
|
211
216
|
## Tests
|
|
@@ -260,7 +265,7 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
|
|
|
260
265
|
|
|
261
266
|
| Documento | Descripción |
|
|
262
267
|
|-----------|-------------|
|
|
263
|
-
| [API-AGENTS.md](docs/API-AGENTS.md) | createAgent, AgentLoop, Tool/Skill Selector, los 16 LLM Providers |
|
|
268
|
+
| [API-AGENTS.md](docs/API-AGENTS.md) | createAgent, AgentLoop, Tool/Skill Selector, los 16 LLM Providers, multi-inquilino y Jev |
|
|
264
269
|
| [API-CONTEXT-COMPILER.md](docs/API-CONTEXT-COMPILER.md) | Context Compiler, historial, Scratchpad, EthicsGuard, ACE |
|
|
265
270
|
| [API-TOOLS-SKILLS-CHANNELS.md](docs/API-TOOLS-SKILLS-CHANNELS.md) | Tools, Skills, MCP, Gateway, Channels, Tool Runtime, Storage |
|
|
266
271
|
| [API-DAG-SCHEDULER.md](docs/API-DAG-SCHEDULER.md) | DAGScheduler, TaskGraph, TaskNode, estrategias, presets |
|
|
@@ -271,4 +276,4 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
|
|
|
271
276
|
|
|
272
277
|
---
|
|
273
278
|
|
|
274
|
-
*Hive SDK v0.
|
|
279
|
+
*Hive SDK v0.5.1 — MIT*
|
package/docs/API-AGENTS.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
3. [Tool Selector](#tool-selector)
|
|
8
8
|
4. [Skill Selector](#skill-selector)
|
|
9
9
|
5. [LLM Providers](#llm-providers)
|
|
10
|
+
6. [Multi-inquilino: credenciales y Jev](#multi-inquilino-credenciales-y-jev)
|
|
11
|
+
7. [Jev: plano de decisión](#jev-plano-de-decisión)
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -240,6 +242,74 @@ const result = await runAgentIsolated({
|
|
|
240
242
|
});
|
|
241
243
|
```
|
|
242
244
|
|
|
245
|
+
### Multi-inquilino: credenciales y Jev
|
|
246
|
+
|
|
247
|
+
Un host que sirve a varios clientes desde un mismo proceso corre cada turno
|
|
248
|
+
dentro de `runInTenant(tenantKey, …)` y pasa las claves **por llamada**:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { runAgent } from "@johpaz/hive-sdk";
|
|
252
|
+
import { runInTenant } from "@johpaz/hive-sdk/storage";
|
|
253
|
+
|
|
254
|
+
await runInTenant(tenantKey, async () => {
|
|
255
|
+
for await (const chunk of runAgent({
|
|
256
|
+
agentId, threadId, userMessage,
|
|
257
|
+
credentials: { apiKey: claveDelModelo, baseUrl }, // gana y corta ahí
|
|
258
|
+
jev: claveOpenRouter ? { apiKey: claveOpenRouter } : false,
|
|
259
|
+
onStep: async (paso) => { /* text · tool_call · tool_result · jev_decision */ },
|
|
260
|
+
})) { /* … */ }
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Reglas con un inquilino activo (desde 0.5.1):
|
|
265
|
+
|
|
266
|
+
- `credentials.apiKey` gana. Sin ella, la clave sale de los secretos **de ese
|
|
267
|
+
inquilino** (`storeProviderApiKey` dentro de su `runInTenant`).
|
|
268
|
+
- Nunca se usa `<PROVIDER>_API_KEY` del entorno ni el llavero del SO: son de la
|
|
269
|
+
máquina, no del cliente. Si no hay clave, la llamada falla en vez de cobrarse
|
|
270
|
+
a la plataforma. Para tus propias tools usa `envSecret(nombre)`
|
|
271
|
+
(`@johpaz/hive-sdk/storage`), que aplica la misma regla.
|
|
272
|
+
- `credentials` y `jev` se propagan a `runAgentIsolated`, `runRoleSwarm` y
|
|
273
|
+
`runSwarm`.
|
|
274
|
+
|
|
275
|
+
### Jev: plano de decisión
|
|
276
|
+
|
|
277
|
+
Jev hace preguntas acotadas a la API Decisions de OpenRouter
|
|
278
|
+
(`typesafe/jev-1.13`) y usa las respuestas para recortar lo que recibe el
|
|
279
|
+
modelo principal. **Es opcional**: sin clave no existe, no se hace ninguna
|
|
280
|
+
llamada y el turno corre exactamente igual.
|
|
281
|
+
|
|
282
|
+
| Momento | Qué decide |
|
|
283
|
+
|---|---|
|
|
284
|
+
| Al compilar el contexto | Qué mensajes previos, tools, skills, notas del scratchpad y reglas del playbook entran; a qué especialista conviene delegar, y si depende de un MCP apagado. Los últimos 4 mensajes siempre quedan. |
|
|
285
|
+
| Entre iteraciones | Qué resultados viejos de tools se omiten y la siguiente acción (continuar, delegar, descubrir, cerrar). Sólo si hay ≥ 4 000 caracteres podables. |
|
|
286
|
+
| Antes de ejecutar tools | Si un lote de lecturas independientes, o de delegaciones a workers distintos, corre en paralelo. |
|
|
287
|
+
|
|
288
|
+
Lo omitido se puede recuperar: el prompt lista los ids y el coordinador tiene
|
|
289
|
+
`conversation_read`.
|
|
290
|
+
|
|
291
|
+
**Activación**, por orden:
|
|
292
|
+
|
|
293
|
+
1. `jev: { apiKey, mcpSettingsPath? }` en la llamada. `mcpSettingsPath` es el
|
|
294
|
+
texto con el que el coordinador le dice al usuario dónde encender un MCP
|
|
295
|
+
(por defecto «Ajustes → Entorno → MCP Servers»).
|
|
296
|
+
2. `jev: false` lo apaga.
|
|
297
|
+
3. Sin la opción: el provider `openrouter` habilitado y activo, con su clave
|
|
298
|
+
guardada; sin inquilino, también `OPENROUTER_API_KEY`.
|
|
299
|
+
|
|
300
|
+
**Qué ve el host.** Cada decisión llega por `onStep` como
|
|
301
|
+
`{ type: "jev_decision", message, jev }`, con agente, tipo (`context`,
|
|
302
|
+
`iteration`, `parallel`), resumen, tokens ahorrados estimados, latencia, costo,
|
|
303
|
+
especialista recomendado y MCP apagados. También se emite
|
|
304
|
+
`canvas:jev_decision`. `getUsageStats().jev` suma decisiones, costo y ahorro
|
|
305
|
+
(total y por agente). Fallos y cooldown se llevan por inquilino.
|
|
306
|
+
|
|
307
|
+
**Privacidad.** Se envían a OpenRouter, con la clave de la llamada, extractos
|
|
308
|
+
acotados: el objetivo del turno, fragmentos de mensajes previos, nombres y
|
|
309
|
+
descripciones de tools y skills, notas y reglas, fragmentos de resultados de
|
|
310
|
+
tools y el mapa del enjambre (nombres, no ids con prefijo de inquilino). Nunca
|
|
311
|
+
credenciales ni adjuntos. El detalle está en el [CHANGELOG](../CHANGELOG.md).
|
|
312
|
+
|
|
243
313
|
---
|
|
244
314
|
|
|
245
315
|
## Tool Selector
|
package/docs/INDEX.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| [API-RESILIENCE.md](./API-RESILIENCE.md) | Reintentos con backoff y circuit breakers |
|
|
13
13
|
| [API-SESSIONS.md](./API-SESSIONS.md) | Sesiones por canal, historial, reanudación tras un corte |
|
|
14
14
|
| [API-SERVICES.md](./API-SERVICES.md) | **La superficie para una UI** — CRUD de agentes, enjambres, skills, modelos, MCP, cron |
|
|
15
|
-
| [API-AGENTS.md](./API-AGENTS.md) | createAgent, AgentLoop, Tool/Skill Selector, LLM Providers |
|
|
15
|
+
| [API-AGENTS.md](./API-AGENTS.md) | createAgent, AgentLoop, Tool/Skill Selector, LLM Providers, multi-inquilino y Jev |
|
|
16
16
|
| [API-DAG-SCHEDULER.md](./API-DAG-SCHEDULER.md) | DAGScheduler, TaskGraph, Estrategias, Presets |
|
|
17
17
|
| [API-CRON.md](./API-CRON.md) | Tareas programadas: expresiones, zona horaria, misfires, motor sin dependencias |
|
|
18
18
|
| [API-WORKERS-EVENTS.md](./API-WORKERS-EVENTS.md) | **Bun Workers**, createWorker, WorkerPool, AgentBus, EventBus, Canvas |
|
|
@@ -181,6 +181,10 @@ ANTHROPIC_API_KEY=sk-ant-... # Anthropic
|
|
|
181
181
|
LOG_LEVEL=info # debug | info | warn | error
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
Las `*_API_KEY` del entorno sólo se usan sin inquilino; dentro de
|
|
185
|
+
`runInTenant` la clave llega en `credentials` o desde los secretos del
|
|
186
|
+
inquilino (ver [UPGRADING.md](./UPGRADING.md#051-claves-aisladas-por-inquilino)).
|
|
187
|
+
|
|
184
188
|
---
|
|
185
189
|
|
|
186
190
|
## Tests
|
package/docs/UPGRADING.md
CHANGED
|
@@ -74,6 +74,26 @@ sigue sólo importa con el log causal encendido (`HIVE_CAUSAL_LOG=true` o
|
|
|
74
74
|
crudo y el SDK lo califica. Los eventos que entrega traen en `agentId` la
|
|
75
75
|
clave del shard; `formatCausalEvent` la muestra sin el tenant.
|
|
76
76
|
|
|
77
|
+
## 0.5.1: claves aisladas por inquilino
|
|
78
|
+
|
|
79
|
+
Sólo cambia algo si corres turnos dentro de `runInTenant` (un host
|
|
80
|
+
multi-inquilino). Sin inquilino todo sigue igual.
|
|
81
|
+
|
|
82
|
+
- **La caché de secretos y el llavero del SO ya no se comparten.** Antes, en
|
|
83
|
+
cuanto un inquilino leía o guardaba `provider:<id>:api_key`, los demás del
|
|
84
|
+
mismo proceso recibían esa clave. Ahora la caché es por inquilino y el
|
|
85
|
+
llavero no se toca dentro de un inquilino. No hay que cambiar código.
|
|
86
|
+
- **El entorno ya no es respaldo dentro de un inquilino.** Si un turno no
|
|
87
|
+
trae `credentials` y el inquilino no tiene clave guardada, antes se usaba
|
|
88
|
+
`<PROVIDER>_API_KEY` del proceso (la cuenta de la plataforma); ahora la
|
|
89
|
+
llamada falla por falta de clave. Aplica al modelo principal, OCR, voz,
|
|
90
|
+
`computer_use` y Jev. Qué hacer: pasar la clave en `credentials`, o
|
|
91
|
+
guardarla con `storeProviderApiKey` dentro del `runInTenant` del cliente. Si
|
|
92
|
+
quieres que un cliente use la cuenta de la plataforma, pásala tú en
|
|
93
|
+
`credentials`, a propósito.
|
|
94
|
+
- Para tus propias tools: `envSecret("MI_API_KEY")` en lugar de
|
|
95
|
+
`process.env.MI_API_KEY`.
|
|
96
|
+
|
|
77
97
|
## Compatibilidad y CI
|
|
78
98
|
|
|
79
99
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -23,9 +23,12 @@ import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage, type Pr
|
|
|
23
23
|
import { addMessage } from "./conversation-store.ts"
|
|
24
24
|
import { saveTrace, recordLLMUsage } from "./tracer.ts"
|
|
25
25
|
import { maybeCompact, clearOldToolResults } from "./compaction.ts"
|
|
26
|
-
import { emitCanvas } from "../canvas/emitter.ts"
|
|
26
|
+
import { emitCanvas, type CanvasJevDecision } from "../canvas/emitter.ts"
|
|
27
27
|
import type { MCPClientManager } from "../mcp/index.ts"
|
|
28
28
|
import { compileContext } from "./context-compiler.ts"
|
|
29
|
+
import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
|
|
30
|
+
import { jevWantsParallel, planJevIteration } from "./jev-planner.ts"
|
|
31
|
+
import { emitJevDecision, type JevOption } from "./jev-decisions.ts"
|
|
29
32
|
import { formatToolResult } from "../utils/toon.ts"
|
|
30
33
|
import { redactBinaryStrings } from "../utils/redact-binary.ts"
|
|
31
34
|
import { resolveUserId, resolveAgentId } from "../storage/onboarding.ts"
|
|
@@ -52,6 +55,10 @@ import { getNarration } from "../events/tool-narration.ts"
|
|
|
52
55
|
|
|
53
56
|
const log = logger.child("agent-loop")
|
|
54
57
|
|
|
58
|
+
const JEV_ACTION_LABELS: Record<string, string> = {
|
|
59
|
+
continue: "Continuar", delegate: "Delegar", discover: "Descubrir", finish: "Cerrar",
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Per-operation budget for a single LLM call — NOT an aggregate deadline for the
|
|
56
63
|
// whole turn. Each call gets its own fresh window; a slow-but-healthy multi-step
|
|
57
64
|
// turn (many quick operations) is never killed just for taking a while overall.
|
|
@@ -226,6 +233,12 @@ export interface AgentLoopOptions {
|
|
|
226
233
|
* dos inquilinos concurrentes en el mismo proceso compartían credencial.
|
|
227
234
|
*/
|
|
228
235
|
credentials?: ProviderCredentials
|
|
236
|
+
/**
|
|
237
|
+
* Jev (OpenRouter Decisions) for this run. `{ apiKey }` uses that key,
|
|
238
|
+
* `false` turns it off, undefined reads the current tenant's `openrouter`
|
|
239
|
+
* provider row. Travels with the run like `credentials`.
|
|
240
|
+
*/
|
|
241
|
+
jev?: JevOption
|
|
229
242
|
/** Whether to resume from a previously saved checkpoint */
|
|
230
243
|
resume?: boolean
|
|
231
244
|
/** Run budget — overrides agent.max_iterations when set */
|
|
@@ -258,12 +271,17 @@ export interface AgentLoopOptions {
|
|
|
258
271
|
export type { StepEvent as AgentStepEvent }
|
|
259
272
|
|
|
260
273
|
export interface StepEvent {
|
|
261
|
-
type: "text" | "tool_call" | "tool_result"
|
|
274
|
+
type: "text" | "tool_call" | "tool_result" | "jev_decision"
|
|
262
275
|
message: string
|
|
263
276
|
toolName?: string
|
|
264
277
|
isError?: boolean
|
|
278
|
+
/** Present on `jev_decision`: what Jev decided for this run, its cost and the estimated savings. */
|
|
279
|
+
jev?: JevStepDecision
|
|
265
280
|
}
|
|
266
281
|
|
|
282
|
+
/** One Jev decision as the host receives it through `onStep`. */
|
|
283
|
+
export type JevStepDecision = CanvasJevDecision
|
|
284
|
+
|
|
267
285
|
// ─── Stream chunk types (compatible with providers/index.ts) ─────────────────
|
|
268
286
|
|
|
269
287
|
export interface StreamChunk {
|
|
@@ -385,8 +403,25 @@ export async function* runAgent(
|
|
|
385
403
|
taskContext: opts.taskContext,
|
|
386
404
|
userId: opts.userId,
|
|
387
405
|
causalStreamId,
|
|
406
|
+
skipJev: !!opts.resume,
|
|
407
|
+
jev: opts.jev,
|
|
388
408
|
})
|
|
389
409
|
|
|
410
|
+
// Every decision goes to the canvas (hosts like hive) and to onStep (hosts
|
|
411
|
+
// that drive runAgent themselves, like hive-cloud).
|
|
412
|
+
const publishJev = async (decision: Parameters<typeof emitJevDecision>[0]): Promise<void> => {
|
|
413
|
+
const event = emitJevDecision(decision)
|
|
414
|
+
if (!opts.onStep) return
|
|
415
|
+
try {
|
|
416
|
+
await opts.onStep({ type: "jev_decision", message: event.summary, jev: event })
|
|
417
|
+
} catch (err) {
|
|
418
|
+
log.warn(`[agent-loop] onStep(jev_decision) failed: ${(err as Error).message}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (ctx.jevDecision) {
|
|
422
|
+
await publishJev({ ...ctx.jevDecision, agentId: opts.agentId, kind: "context", provider: providerCfg.provider, model: providerCfg.model })
|
|
423
|
+
}
|
|
424
|
+
|
|
390
425
|
// Force extra tools into the loadout (tests/evals)
|
|
391
426
|
if (opts.extraTools?.length) {
|
|
392
427
|
const existingNames = new Set(ctx.tools.map((t: any) => t.function?.name))
|
|
@@ -417,9 +452,12 @@ export async function* runAgent(
|
|
|
417
452
|
if (opts.isolated) {
|
|
418
453
|
messages.push({ role: "user", content: opts.userMessage })
|
|
419
454
|
}
|
|
455
|
+
const jevObjective = typeof opts.userMessage === "string" ? opts.userMessage :
|
|
456
|
+
opts.userMessage.filter((part) => part.type === "text").map((part) => (part as { text: string }).text).join("\n")
|
|
420
457
|
|
|
421
458
|
// ── Resume from checkpoint ─────────────────────────────────────────────────
|
|
422
|
-
|
|
459
|
+
// Seeded with the compiled loadout so a checkpoint records the tools Jev chose.
|
|
460
|
+
let injectedToolNames: string[] = ctx.tools.map(t => t.function.name).filter(name => !MINIMAL_TOOLS.has(name))
|
|
423
461
|
let systemPromptSkillSections: string[] = []
|
|
424
462
|
let resumedFromPending = false
|
|
425
463
|
let iterations = 0
|
|
@@ -440,6 +478,15 @@ export async function* runAgent(
|
|
|
440
478
|
if (restored) {
|
|
441
479
|
messages = restored.messages
|
|
442
480
|
injectedToolNames = restored.injectedToolNames ?? []
|
|
481
|
+
// A resume skips Jev: restore the loadout the checkpoint recorded.
|
|
482
|
+
const currentTools = new Set(ctx.tools.map(t => t.function.name))
|
|
483
|
+
for (const name of injectedToolNames) {
|
|
484
|
+
const tool = ctx.allTools.find(t => t.name === name)
|
|
485
|
+
if (tool && !currentTools.has(name)) {
|
|
486
|
+
ctx.tools.push({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })
|
|
487
|
+
currentTools.add(name)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
443
490
|
systemPromptSkillSections = restored.systemPromptSkillSections ?? []
|
|
444
491
|
iterations = restored.iterations ?? 0
|
|
445
492
|
totalInputTokens = restored.totalInputTokens ?? 0
|
|
@@ -525,11 +572,27 @@ export async function* runAgent(
|
|
|
525
572
|
: null
|
|
526
573
|
let streamedThisCall = false
|
|
527
574
|
let response: Awaited<ReturnType<typeof callLLM>>
|
|
575
|
+
const jevIteration = await planJevIteration({ objective: jevObjective, messages, tools: ctx.tools, jev: opts.jev })
|
|
576
|
+
.catch((err) => { log.warn(`[agent-loop] Jev iteration fallback: ${(err as Error).message}`); return null })
|
|
577
|
+
const callMessages = jevIteration?.messages ?? messages
|
|
578
|
+
const callTools = jevIteration?.tools ?? ctx.tools
|
|
579
|
+
if (jevIteration) {
|
|
580
|
+
log.info(`[agent-loop] Jev action=${jevIteration.action} omitted_results=${jevIteration.omittedResults} tools=${callTools.map(t => t.function.name).join(",")}`)
|
|
581
|
+
// Measured on what the provider actually receives, after the usual truncation.
|
|
582
|
+
const payloadChars = (msgs: LLMMessage[], tools: typeof ctx.tools) =>
|
|
583
|
+
JSON.stringify(clearOldToolResults(msgs)).length + JSON.stringify(tools).length
|
|
584
|
+
await publishJev({
|
|
585
|
+
agentId: opts.agentId, kind: "iteration", provider: providerCfg.provider, model: providerCfg.model,
|
|
586
|
+
summary: `${JEV_ACTION_LABELS[jevIteration.action] ?? jevIteration.action} · ${jevIteration.omittedResults} resultado(s) omitido(s) · ${callTools.length}/${ctx.tools.length} herramientas`,
|
|
587
|
+
savedTokens: Math.round((payloadChars(messages, ctx.tools) - payloadChars(callMessages, callTools)) / 4),
|
|
588
|
+
latencyMs: jevIteration.decision.latencyMs, costUsd: jevIteration.decision.costUsd,
|
|
589
|
+
})
|
|
590
|
+
}
|
|
528
591
|
try {
|
|
529
592
|
response = await withTimeout(() => callLLM({
|
|
530
593
|
...providerCfg,
|
|
531
|
-
messages: clearOldToolResults(
|
|
532
|
-
tools:
|
|
594
|
+
messages: clearOldToolResults(callMessages) as LLMMessage[],
|
|
595
|
+
tools: callTools.length > 0 ? callTools : undefined,
|
|
533
596
|
signal: opts.signal,
|
|
534
597
|
sessionId: opts.threadId,
|
|
535
598
|
onToken: opts.onToken && !delegationGroupAtCall
|
|
@@ -689,6 +752,16 @@ export async function* runAgent(
|
|
|
689
752
|
}
|
|
690
753
|
}
|
|
691
754
|
|
|
755
|
+
const jevParallel = await jevWantsParallel(response.tool_calls, opts.jev)
|
|
756
|
+
.catch((err) => { log.warn(`[agent-loop] Jev parallel fallback: ${(err as Error).message}`); return null })
|
|
757
|
+
if (jevParallel?.decision) {
|
|
758
|
+
log.info(`[agent-loop] Jev parallel=${jevParallel.parallel} calls=${response.tool_calls.length}`)
|
|
759
|
+
await publishJev({
|
|
760
|
+
agentId: opts.agentId, kind: "parallel", provider: providerCfg.provider, model: providerCfg.model,
|
|
761
|
+
summary: `${response.tool_calls.length} herramientas ${jevParallel.parallel ? "en paralelo" : "en secuencia"}`,
|
|
762
|
+
savedTokens: 0, latencyMs: jevParallel.decision.latencyMs, costUsd: jevParallel.decision.costUsd,
|
|
763
|
+
})
|
|
764
|
+
}
|
|
692
765
|
const toolResults = await executeToolBatch({
|
|
693
766
|
toolCalls: response.tool_calls,
|
|
694
767
|
allTools: ctx.allTools,
|
|
@@ -709,6 +782,7 @@ export async function* runAgent(
|
|
|
709
782
|
},
|
|
710
783
|
hiveConfig,
|
|
711
784
|
workerPool: hiveConfig.tools?.workerPool,
|
|
785
|
+
parallelToolCalls: jevParallel?.parallel,
|
|
712
786
|
signal: opts.signal,
|
|
713
787
|
})
|
|
714
788
|
|
|
@@ -1305,6 +1379,8 @@ export interface IsolatedAgentOptions {
|
|
|
1305
1379
|
* reabría justo en el camino de delegación.
|
|
1306
1380
|
*/
|
|
1307
1381
|
credentials?: ProviderCredentials
|
|
1382
|
+
/** Jev for the worker, inherited from the delegating turn like `credentials`. */
|
|
1383
|
+
jev?: JevOption
|
|
1308
1384
|
}
|
|
1309
1385
|
|
|
1310
1386
|
export async function runAgentIsolatedDetailed(
|
|
@@ -1329,6 +1405,7 @@ export async function runAgentIsolatedDetailed(
|
|
|
1329
1405
|
channel: opts.channel,
|
|
1330
1406
|
sessionId: opts.sessionId,
|
|
1331
1407
|
credentials: opts.credentials,
|
|
1408
|
+
jev: opts.jev,
|
|
1332
1409
|
})) {
|
|
1333
1410
|
if (chunk.agent?.messages?.[0]?.content) {
|
|
1334
1411
|
lastContent = chunk.agent.messages[0].content
|