@johpaz/hive-sdk 0.4.6 → 0.4.7

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 CHANGED
@@ -17,9 +17,26 @@
17
17
  procedencia, hash y una prueba funcional del OOXML generado.
18
18
  - Añadidas las guías `docs/UPGRADING.md` y
19
19
  `docs/SECURITY-GUARDRAILS.md` para operación y auditoría.
20
+ - **Requiere `@johpaz/hive-db` ^0.5.1** (antes ^0.4.0). Trae las lecturas del
21
+ log causal acotadas por agente (`agents` en `causalThread`, `toolStats` y
22
+ `buildAgentContext`), de las que depende el aislamiento entre inquilinos del
23
+ log causal descrito en *Corregido*.
20
24
 
21
25
  ### Corregido
22
26
 
27
+ - **Con un tenant activo, el log causal se apagaba en lugar de acotarse.**
28
+ `causalThread`, `toolStats` y `buildAgentContext` recorrían todos los shards
29
+ de la base, así que con un tenant en scope `causalReadsEnabled()` las apagaba:
30
+ en un host multi-inquilino el reflector G9 y el contexto causal del
31
+ compilador no corrían nunca. Ahora las tres lecturas van siempre acotadas a
32
+ los agentes que corresponden —el agente del turno, o los del lote de trazas— y
33
+ el apagado desaparece. Además el shard de cada evento pasa a ser
34
+ `causalAgentKey(agentId)`: con tenant lleva el tenant delante (`t_…:agente`,
35
+ la misma forma que los ids del índice BM25), así que dos inquilinos con un
36
+ agente del mismo id ya no comparten shard. Una lista de agentes vacía se salta
37
+ la lectura en vez de pasarse al motor, que la trataría como "todos los
38
+ shards". Cubierto por `test/causal-tenant-scope.test.ts`.
39
+
23
40
  - **`browser_scrape` extraía con una tool que no ve lo que el navegador
24
41
  renderizó.** La skill existe para sitios dinámicos, y su paso de extracción
25
42
  usaba `web_fetch`, que vuelve a pedir la URL al servidor y recibe el HTML sin
@@ -194,6 +211,11 @@
194
211
 
195
212
  ### Cambiado
196
213
 
214
+ - **El `toolStats` del reflector se acota a los agentes del lote**, también sin
215
+ tenant. Antes sumaba el historial de la tool de todos los agentes de la base;
216
+ ahora el de los agentes cuyas trazas se están analizando. Es la misma
217
+ semántica con y sin tenant, y no recorre el log entero.
218
+
197
219
  - **Los tests que manejan un navegador real son opt-in (`BROWSER_TESTS=1`).**
198
220
  Su guarda era `isWebViewSupported()`, que sólo comprueba que exista un binario
199
221
  de Chromium — no que arranque. En un runner de CI (contenedor, a menudo root)
package/README.md CHANGED
@@ -271,4 +271,4 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
271
271
 
272
272
  ---
273
273
 
274
- *Hive SDK v0.4.6 — MIT*
274
+ *Hive SDK v0.4.7 — MIT*
@@ -444,8 +444,8 @@ unsubscribeCanvas(handler);
444
444
 
445
445
  ## Storage
446
446
 
447
- HiveDB (`@johpaz/hive-db`), un motor embebido con colecciones de documentos e
448
- índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
447
+ HiveDB (`@johpaz/hive-db` 0.5.1 o posterior), un motor embebido con colecciones
448
+ de documentos e índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
449
449
 
450
450
  ```typescript
451
451
  import { ensureHiveDb, col } from "@johpaz/hive-sdk";
package/docs/UPGRADING.md CHANGED
@@ -54,6 +54,26 @@ Estos adaptadores están en la frontera con el runtime. No deben reemplazarse po
54
54
  `any`, `@ts-ignore` o `@ts-expect-error`: hacerlo convertiría una incompatibilidad
55
55
  real de plataforma en un falso resultado verde.
56
56
 
57
+ ## hive-db 0.5.1 y log causal por tenant
58
+
59
+ Hive SDK requiere **`@johpaz/hive-db` 0.5.1 o posterior**. Llega como
60
+ dependencia del SDK, así que una aplicación consumidora no la declara. Lo que
61
+ sigue sólo importa con el log causal encendido (`HIVE_CAUSAL_LOG=true` o
62
+ `causalLog.enabled`):
63
+
64
+ - Con un tenant activo (`runInTenant`) el reflector y el contexto causal del
65
+ compilador vuelven a funcionar. Antes se apagaban; ahora leen acotado a los
66
+ agentes del turno o del lote de trazas.
67
+ - La clave de shard de cada evento es `causalAgentKey(agentId)`: sin tenant, el
68
+ id del agente tal cual; con tenant, `t_…:agentId`. Los eventos que un host
69
+ haya escrito con tenant antes de esta versión quedaron con el id crudo y las
70
+ lecturas acotadas ya no los ven. Sin tenant no cambia nada.
71
+ - El `toolStats` del reflector cuenta el historial de los agentes del lote, no
72
+ el de toda la base, con y sin tenant.
73
+ - `watchCausalEvents` con tenant sigue exigiendo `agentId`: se le pasa el id
74
+ crudo y el SDK lo califica. Los eventos que entrega traen en `agentId` la
75
+ clave del shard; `formatCausalEvent` la muestra sin el tenant.
76
+
57
77
  ## Compatibilidad y CI
58
78
 
59
79
  Los workflows fijan Bun 1.4.2, instalan con `--frozen-lockfile`, ejecutan el
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-sdk",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  import { logger } from "../utils/logger.ts"
17
17
  import { col, fromIndexable } from "../storage/hive.ts"
18
18
  import { getHiveDb } from "../storage/hivedb.ts"
19
+ import { causalAgentKey } from "../storage/causal-events.ts"
19
20
  import type { HiveDB, EventInput } from "@johpaz/hive-db"
20
21
  import type { AgentDoc, TurnSource } from "../storage/collections.ts"
21
22
  import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage, type ProviderCredentials } from "./llm-client.ts"
@@ -170,7 +171,8 @@ async function appendCausalEvent(
170
171
  ): Promise<number | undefined> {
171
172
  try {
172
173
  return await db.append({
173
- agentId: input.agentId,
174
+ // Shard calificado con el tenant: ver causalAgentKey en causal-events.ts.
175
+ agentId: causalAgentKey(input.agentId),
174
176
  streamId: input.streamId,
175
177
  kind: input.kind,
176
178
  payload: JSON.stringify(input.payload),
@@ -38,7 +38,7 @@ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
38
38
  import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync.ts"
39
39
  import { getUserDate, getUserTime } from "../utils/date.ts"
40
40
  import { getHiveDb } from "../storage/hivedb.ts"
41
- import { causalReadsEnabled } from "../storage/causal-events.ts"
41
+ import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
42
42
  import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector.ts"
43
43
  import { expandToolAllowlist } from "./delegation-runtime.ts"
44
44
  import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
@@ -588,7 +588,10 @@ export async function compileContext(opts: {
588
588
  // applies this turn (a real DB round-trip, not a per-turn cost) and
589
589
  // there's a causal stream to build it from. episodicSimilarity is omitted:
590
590
  // it requires embeddings hive doesn't generate anywhere yet.
591
- if (summaryApplies && opts.causalStreamId && causalReadsEnabled()) {
591
+ // Acotado al shard de este agente: el stream es de una sola invocación suya,
592
+ // así que el hilo es el mismo y no se recorre el log de nadie más.
593
+ const causalAgents = causalScope([opts.agentId])
594
+ if (summaryApplies && opts.causalStreamId && causalAgents && causalReadsEnabled()) {
592
595
  try {
593
596
  const causalDb = await getHiveDb()
594
597
  const objectiveSource = taskContext || userMessage
@@ -605,6 +608,7 @@ export async function compileContext(opts: {
605
608
  currentObjective: currentObjective.slice(0, 2000),
606
609
  maxTokens: causalMaxTokens,
607
610
  strategy: { causalAnchors: true, compressCompletedPhases: true },
611
+ agents: causalAgents,
608
612
  })) as AgentContextShape
609
613
 
610
614
  const causalLines = [...(causalCtx.items ?? []), ...(causalCtx.anomalies ?? [])]
@@ -14,7 +14,8 @@
14
14
  import { logger } from "../utils/logger.ts"
15
15
  import { col, nextId } from "../storage/hive.ts"
16
16
  import { getHiveDb } from "../storage/hivedb.ts"
17
- import { causalReadsEnabled } from "../storage/causal-events.ts"
17
+ import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
18
+ import { unqualifyDocId } from "../storage/tenant.ts"
18
19
  import type { HiveDB, ToolStats } from "@johpaz/hive-db"
19
20
  import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
20
21
  import { parseThreadId } from "./thread-id.ts"
@@ -183,10 +184,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
183
184
 
184
185
  for (const streamId of streamIds) {
185
186
  try {
186
- const thread = (await causalDb.causalThread(streamId)) as CausalThreadShape
187
+ // El stream es de una sola invocación, así que sus trazas nombran a los
188
+ // agentes que escribieron en él. Sin ninguno no hay con qué acotar, y una
189
+ // lectura sin acotar recorre los shards de todos los inquilinos.
190
+ const subset = traces.filter((t) => t.causal_stream_id === streamId)
191
+ const agents = causalScope(subset.map((t) => t.agent_id))
192
+ if (!agents) continue
193
+
194
+ const thread = (await causalDb.causalThread(streamId, agents)) as CausalThreadShape
187
195
  if (!thread.decisions?.length && !thread.toolCalls?.length) continue
188
196
 
189
- const subset = traces.filter((t) => t.causal_stream_id === streamId)
190
197
  const originalIntent = subset[0]?.input_summary ?? ""
191
198
  const success = subset.every((t) => t.success)
192
199
 
@@ -204,13 +211,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
204
211
  // underlying root cause mint a brand new playbook rule instead of
205
212
  // reinforcing one (confirmed via a local before/after canary run).
206
213
  if (evaluation.rootCause) {
214
+ // El log guarda la clave del shard (t_…:agente con tenant); a la regla de
215
+ // playbook y a affected_agents les llega el id que el host conoce.
216
+ const rootAgent = unqualifyDocId(evaluation.rootCause.agent)
207
217
  const decision = thread.decisions?.find((d) => d.seq === evaluation.rootCause!.seq)
208
218
  insights.push({
209
219
  type: "root_cause",
210
220
  description: decision
211
- ? `Root cause: decision "${decision.description}" (agent ${evaluation.rootCause.agent}) preceded a tool failure.`
212
- : `Root cause: a decision by agent ${evaluation.rootCause.agent} preceded a tool failure.`,
213
- affectedAgents: [evaluation.rootCause.agent],
221
+ ? `Root cause: decision "${decision.description}" (agent ${rootAgent}) preceded a tool failure.`
222
+ : `Root cause: a decision by agent ${rootAgent} preceded a tool failure.`,
223
+ affectedAgents: [rootAgent],
214
224
  confidence: 0.6,
215
225
  })
216
226
  }
@@ -248,14 +258,17 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
248
258
  async function analyzeTracesLocally(traces: TraceDoc[], causalDb: HiveDB | null): Promise<Insight[]> {
249
259
  const insights: Insight[] = []
250
260
 
251
- // G9: whole-history stats per tool touched by this batch (undefined when
252
- // disabled, or when the tool has no events in the log yet).
261
+ // G9: historial completo, por tool, de los agentes de este lote (undefined si
262
+ // el log está apagado o la tool todavía no tiene eventos). Acotado a esos
263
+ // agentes: con tenant, sin esto se sumarían llamadas de otros inquilinos; sin
264
+ // tenant, es el historial de estos agentes y no el de toda la base.
253
265
  const statsByTool = new Map<string, ToolStats>()
254
- if (causalDb) {
266
+ const agents = causalDb ? causalScope(traces.map((t) => t.agent_id)) : null
267
+ if (causalDb && agents) {
255
268
  const distinctTools = new Set(traces.map((t) => t.tool_used).filter((t): t is string => !!t))
256
269
  for (const tool of distinctTools) {
257
270
  try {
258
- const stats = await causalDb.toolStats(tool)
271
+ const stats = await causalDb.toolStats(tool, agents)
259
272
  if (stats) statsByTool.set(tool, stats)
260
273
  } catch (err) {
261
274
  log.warn(`[reflector] toolStats(${tool}) failed: ${(err as Error).message}`)
@@ -3,32 +3,63 @@
3
3
  *
4
4
  * Read-side, separate from agent-loop.ts's write-side appendCausalEvent()
5
5
  * (module-private there, write-only). This is the read/watch counterpart,
6
- * co-located with the DB singleton accessor.
6
+ * co-located with the DB singleton accessor. It also owns the shard key both
7
+ * sides use (causalAgentKey) and the agent scope of every aggregated read
8
+ * (causalScope).
7
9
  */
8
10
 
9
11
  import { getHiveDb } from "./hivedb.ts"
10
- import { currentTenant } from "./tenant.ts"
12
+ import { currentTenant, qualifyDocId, unqualifyDocId } from "./tenant.ts"
11
13
  import { loadConfig } from "../config/loader.ts"
12
14
  import type { Event, EventPattern } from "@johpaz/hive-db"
13
15
 
14
16
  export type { Event as CausalEvent, EventPattern as CausalEventPattern }
15
17
 
16
18
  /**
17
- * ¿Se pueden hacer lecturas AGREGADAS del log causal en este contexto?
19
+ * Clave de shard de un agente en el log causal.
18
20
  *
19
- * `causalThread`, `buildAgentContext` y `toolStats` recorren todos los shards
20
- * de la base y mezclan sus resultados (`log.rs` — `read_stream_all_agents` y
21
- * `project::<P>()` para proyecciones con scope Agent). Sobre una base
22
- * compartida eso devolvería hilos y estadísticas de otros inquilinos. Mientras
23
- * el motor no exponga las variantes acotadas por agente, con un tenant activo
24
- * estas lecturas se apagan: perder una señal de reflexión es aceptable, cruzar
25
- * datos entre agencias no.
21
+ * El log no tiene colecciones que prefijar: cada evento va al shard de su
22
+ * `agentId`, y ahí está todo el aislamiento. Con un tenant activo la clave
23
+ * lleva el tenant delante (`t_…:agentId`, la misma forma que usa el índice
24
+ * BM25 vía `qualifyDocId`), así que dos inquilinos con un agente del mismo id
25
+ * no comparten shard. Sin tenant es la identidad y un log de un solo dueño no
26
+ * cambia.
26
27
  *
27
- * La ESCRITURA (`append`) no entra aquí: va al shard del propio agente, y los
28
- * agentes de Hive Cloud ya llevan el enjambre en el id.
28
+ * Escritura y lecturas tienen que pasar por aquí: un evento escrito con una
29
+ * clave y leído con otra simplemente no aparece.
30
+ */
31
+ export function causalAgentKey(agentId: string): string {
32
+ return qualifyDocId(agentId)
33
+ }
34
+
35
+ /**
36
+ * Lista `agents` para `causalThread`, `toolStats` y `buildAgentContext`.
37
+ *
38
+ * Devuelve `null` si no queda ningún agente, y quien llama se salta la
39
+ * lectura: hive-db trata una lista vacía igual que la ausencia de filtro y
40
+ * recorre TODOS los shards, que sobre una base compartida es leer los eventos
41
+ * de otros inquilinos.
42
+ */
43
+ export function causalScope(agentIds: Iterable<string | null | undefined>): string[] | null {
44
+ const keys = new Set<string>()
45
+ for (const id of agentIds) {
46
+ if (id) keys.add(causalAgentKey(id))
47
+ }
48
+ return keys.size > 0 ? [...keys] : null
49
+ }
50
+
51
+ /**
52
+ * ¿Se lee el log causal (reflector, contexto causal del compilador)?
53
+ *
54
+ * Con hive-db 0.4, `causalThread`, `buildAgentContext` y `toolStats` recorrían
55
+ * todos los shards y mezclaban sus resultados, así que con un tenant activo
56
+ * había que apagarlas para no devolver hilos y estadísticas de otros
57
+ * inquilinos. hive-db 0.5.1 —la mínima que pide el SDK— acepta `agents` en las
58
+ * tres, y el SDK las llama siempre acotadas con {@link causalScope}: basta con
59
+ * que el log esté encendido.
29
60
  */
30
61
  export function causalReadsEnabled(): boolean {
31
- return !!loadConfig().causalLog?.enabled && !currentTenant()
62
+ return !!loadConfig().causalLog?.enabled
32
63
  }
33
64
 
34
65
  /**
@@ -52,10 +83,10 @@ export function causalReadsEnabled(): boolean {
52
83
  export async function watchCausalEvents(
53
84
  pattern: EventPattern
54
85
  ): Promise<AsyncIterable<Event> & { close(): void }> {
55
- // El log de eventos no tiene colecciones que prefijar: el aislamiento entre
56
- // inquilinos lo da el shard por `agentId`. Un patrón sin `agentId` sobre una
57
- // base compartida entregaría los eventos de todos los enjambres, así que se
58
- // exige explícitamente en vez de filtrar a medias.
86
+ // Un patrón sin `agentId` sobre una base compartida entregaría los eventos de
87
+ // todos los enjambres, así que con un tenant activo se exige explícitamente en
88
+ // vez de filtrar a medias. Se recibe el id crudo del agente y su shard se
89
+ // busca por la clave calificada.
59
90
  if (currentTenant() && !pattern.agentId) {
60
91
  throw new Error(
61
92
  "watchCausalEvents: con un tenant activo el patrón debe fijar agentId; " +
@@ -63,7 +94,7 @@ export async function watchCausalEvents(
63
94
  )
64
95
  }
65
96
  const db = await getHiveDb()
66
- return db.events(pattern)
97
+ return db.events(pattern.agentId ? { ...pattern, agentId: causalAgentKey(pattern.agentId) } : pattern)
67
98
  }
68
99
 
69
100
  /** One-line human-readable summary of a causal event, keyed by its kindTag. */
@@ -76,7 +107,7 @@ export function formatCausalEvent(event: Event): string {
76
107
  }
77
108
 
78
109
  const streamShort = event.streamId.slice(0, 8)
79
- const header = `[${event.seq}] ${kindIcon(event.kindTag)} ${event.kindTag.padEnd(16)} agent=${event.agentId} stream=${streamShort}…`
110
+ const header = `[${event.seq}] ${kindIcon(event.kindTag)} ${event.kindTag.padEnd(16)} agent=${unqualifyDocId(event.agentId)} stream=${streamShort}…`
80
111
 
81
112
  switch (event.kindTag) {
82
113
  case "IntentLogged":
@@ -128,4 +128,4 @@ export { reconcileOnBoot } from "./reconcile.ts";
128
128
 
129
129
  // ─── Log causal (G9) ─────────────────────────────────────────────────────────
130
130
  export type { CausalEvent, CausalEventPattern } from "./causal-events.ts";
131
- export { watchCausalEvents, formatCausalEvent } from "./causal-events.ts";
131
+ export { watchCausalEvents, formatCausalEvent, causalAgentKey, causalScope } from "./causal-events.ts";