@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/README.md +9 -4
  3. package/docs/API-AGENTS.md +70 -0
  4. package/docs/INDEX.md +5 -1
  5. package/docs/UPGRADING.md +20 -0
  6. package/package.json +1 -1
  7. package/packages/core/src/agent/agent-loop.ts +82 -5
  8. package/packages/core/src/agent/context-compiler.ts +122 -23
  9. package/packages/core/src/agent/index.ts +2 -0
  10. package/packages/core/src/agent/jev-decisions.ts +197 -0
  11. package/packages/core/src/agent/jev-planner.ts +298 -0
  12. package/packages/core/src/agent/llm-client.ts +2 -2
  13. package/packages/core/src/agent/llm-providers/anthropic.ts +2 -1
  14. package/packages/core/src/agent/llm-providers/gemini.ts +3 -1
  15. package/packages/core/src/agent/realtime-providers/gemini-live.ts +2 -1
  16. package/packages/core/src/agent/tool-selector.ts +1 -0
  17. package/packages/core/src/canvas/emitter.ts +19 -0
  18. package/packages/core/src/events/channel-narration.ts +3 -0
  19. package/packages/core/src/multimodal/vision-service.ts +7 -7
  20. package/packages/core/src/services/swarms.ts +4 -0
  21. package/packages/core/src/storage/collections.ts +9 -2
  22. package/packages/core/src/storage/crypto.ts +51 -17
  23. package/packages/core/src/storage/index.ts +1 -0
  24. package/packages/core/src/storage/seed.ts +4 -0
  25. package/packages/core/src/storage/usage.ts +55 -2
  26. package/packages/core/src/swarm/RoleSwarm.ts +6 -0
  27. package/packages/core/src/tool-runtime/index.ts +20 -1
  28. package/packages/core/src/tools/agents/get-available-models.ts +3 -1
  29. package/packages/core/src/tools/core/index.ts +33 -2
  30. package/packages/core/src/tools/web/computer-use.ts +2 -2
  31. package/packages/core/src/voice/index.ts +13 -13
package/CHANGELOG.md CHANGED
@@ -1,6 +1,90 @@
1
1
  # Changelog
2
2
 
3
- ## Sin publicar
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.4.9 — MIT*
279
+ *Hive SDK v0.5.1 — MIT*
@@ -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.4.9",
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
- let injectedToolNames: string[] = []
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(messages) as LLMMessage[],
532
- tools: ctx.tools.length > 0 ? ctx.tools : undefined,
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