@johpaz/hive-sdk 0.4.5 → 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 +22 -0
- package/README.md +1 -1
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +2 -2
- package/docs/UPGRADING.md +20 -0
- package/package.json +2 -2
- package/packages/core/src/agent/agent-loop.ts +5 -1
- package/packages/core/src/agent/capability-search.ts +2 -1
- package/packages/core/src/agent/context-compiler.ts +6 -2
- package/packages/core/src/agent/llm-client.ts +15 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +3 -3
- package/packages/core/src/agent/llm-providers/opencode-go.ts +37 -0
- package/packages/core/src/agent/reflector.ts +23 -10
- package/packages/core/src/storage/causal-events.ts +50 -19
- package/packages/core/src/storage/index.ts +1 -1
- package/packages/core/src/storage/seed.ts +7 -3
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
|
@@ -444,8 +444,8 @@ unsubscribeCanvas(handler);
|
|
|
444
444
|
|
|
445
445
|
## Storage
|
|
446
446
|
|
|
447
|
-
HiveDB (`@johpaz/hive-db`), un motor embebido con colecciones
|
|
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.
|
|
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",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"drift": "bun scripts/check-drift.ts"
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@johpaz/hive-db": "^0.
|
|
102
|
+
"@johpaz/hive-db": "^0.5.1",
|
|
103
103
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
104
104
|
"@google/genai": "^1.43.0",
|
|
105
105
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -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
|
-
|
|
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),
|
|
@@ -521,6 +523,7 @@ export async function* runAgent(
|
|
|
521
523
|
messages: clearOldToolResults(messages) as LLMMessage[],
|
|
522
524
|
tools: ctx.tools.length > 0 ? ctx.tools : undefined,
|
|
523
525
|
signal: opts.signal,
|
|
526
|
+
sessionId: opts.threadId,
|
|
524
527
|
onToken: opts.onToken && !delegationGroupAtCall
|
|
525
528
|
? (token: string) => {
|
|
526
529
|
streamedThisCall = true
|
|
@@ -1133,6 +1136,7 @@ export async function* runAgent(
|
|
|
1133
1136
|
...providerCfg,
|
|
1134
1137
|
messages: clearOldToolResults(messages) as LLMMessage[],
|
|
1135
1138
|
tools: undefined, // no tools — force text response
|
|
1139
|
+
sessionId: opts.threadId,
|
|
1136
1140
|
})
|
|
1137
1141
|
if (synthesis.usage) {
|
|
1138
1142
|
totalInputTokens += synthesis.usage.input_tokens
|
|
@@ -81,7 +81,8 @@ export async function searchCapabilities(
|
|
|
81
81
|
const k = opts.k ?? 10;
|
|
82
82
|
const types = opts.types?.length ? opts.types : undefined;
|
|
83
83
|
const trimmed = query.trim();
|
|
84
|
-
|
|
84
|
+
// hive-db >= 0.4 rejects k <= 0 instead of returning no hits.
|
|
85
|
+
if (!trimmed || k <= 0) return [];
|
|
85
86
|
|
|
86
87
|
const startTime = performance.now();
|
|
87
88
|
const db = await getHiveDb();
|
|
@@ -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
|
-
|
|
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 ?? [])]
|
|
@@ -97,6 +97,13 @@ export interface LLMCallOptions {
|
|
|
97
97
|
signal?: AbortSignal
|
|
98
98
|
/** Enable extended thinking for supported models (Anthropic Claude 3.7+). */
|
|
99
99
|
thinking?: { enabled: boolean; budget_tokens?: number }
|
|
100
|
+
/**
|
|
101
|
+
* Stable id of the conversation this call belongs to (the agent loop's
|
|
102
|
+
* threadId). Providers that route or cache per session derive their own
|
|
103
|
+
* header from it — OpenCode Go's `x-opencode-session` — and never send it
|
|
104
|
+
* verbatim, since it carries user, channel and peer ids.
|
|
105
|
+
*/
|
|
106
|
+
sessionId?: string
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
export interface LLMResponse {
|
|
@@ -214,6 +221,14 @@ export function describeProviderFailure(
|
|
|
214
221
|
provider: string,
|
|
215
222
|
cleanModel: string,
|
|
216
223
|
): string {
|
|
224
|
+
// NVIDIA usa 404 también para modelos que siguen en su catálogo público pero
|
|
225
|
+
// no están habilitados para esa key ("Function '…': Not found for account
|
|
226
|
+
// '…'"). Decir que "se retiró" mandaba a buscar un modelo que sí existe.
|
|
227
|
+
if (status === 404 && /not found for account/i.test((err as Error)?.message ?? "")) {
|
|
228
|
+
return `Tu cuenta de ${provider} no tiene habilitado el modelo "${cleanModel}" (HTTP 404): `
|
|
229
|
+
+ `figura en su catálogo, pero no está disponible para esta API key. `
|
|
230
|
+
+ `Elige otro modelo en Ajustes → Proveedores.`
|
|
231
|
+
}
|
|
217
232
|
if (status === 404 || status === 410) {
|
|
218
233
|
return `El modelo "${cleanModel}" ya no existe en ${provider} (HTTP ${status}). `
|
|
219
234
|
+ `El proveedor lo retiró de su catálogo; reintentar no sirve. `
|
|
@@ -82,8 +82,8 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
82
82
|
/** Override to true for providers running on localhost. */
|
|
83
83
|
protected isLocalProvider(): boolean { return false }
|
|
84
84
|
|
|
85
|
-
/** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch). */
|
|
86
|
-
protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined): Promise<any> {
|
|
85
|
+
/** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch, per-call headers). */
|
|
86
|
+
protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined, _options?: LLMCallOptions): Promise<any> {
|
|
87
87
|
const { default: OpenAI } = await import("openai")
|
|
88
88
|
return new OpenAI({ apiKey, baseURL })
|
|
89
89
|
}
|
|
@@ -135,7 +135,7 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
135
135
|
throw new Error(`API key missing for provider: ${this.providerName}. Configure it in Settings → Providers.`)
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const client = await this.resolveOpenAIClient(apiKey, baseURL)
|
|
138
|
+
const client = await this.resolveOpenAIClient(apiKey, baseURL, options)
|
|
139
139
|
|
|
140
140
|
const sanitized = sanitizeMessages(options.messages)
|
|
141
141
|
const rawMessages = this.needsReasoningRoundtrip()
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
import pkg from "../../../../../package.json"
|
|
1
3
|
import { OpenAICompatBase } from "./openai-compat-base.ts"
|
|
4
|
+
import type { LLMCallOptions } from "../llm-client.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode Go rechaza con 400 `MissingSessionID` toda petición sin
|
|
8
|
+
* `x-opencode-session` (verificado 2026-09-10 con una key real: sin el header
|
|
9
|
+
* 400, con él 200). Su documentación pide además que el cliente se identifique
|
|
10
|
+
* con su propio User-Agent en vez del genérico del SDK:
|
|
11
|
+
* https://opencode.ai/docs/go/#where-can-i-use-it
|
|
12
|
+
*/
|
|
13
|
+
const USER_AGENT = `hive-sdk/${pkg.version}`
|
|
14
|
+
|
|
15
|
+
/** Sesión de respaldo para llamadas sin conversación (compactación, metas). */
|
|
16
|
+
const PROCESS_SESSION = randomUUID()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* La sesión tiene que ser estable por conversación —OpenCode la usa para rutear
|
|
20
|
+
* y cachear el prompt—, pero el threadId lleva usuario, canal y contacto: a
|
|
21
|
+
* OpenCode sólo le llega un hash.
|
|
22
|
+
*/
|
|
23
|
+
function sessionHeader(sessionId: string | undefined): string {
|
|
24
|
+
if (!sessionId) return PROCESS_SESSION
|
|
25
|
+
return createHash("sha256").update(`hive:${sessionId}`).digest("hex").slice(0, 32)
|
|
26
|
+
}
|
|
2
27
|
|
|
3
28
|
export class OpenCodeGoProvider extends OpenAICompatBase {
|
|
4
29
|
static readonly secretKey = "OPENCODE_GO_API_KEY"
|
|
@@ -6,4 +31,16 @@ export class OpenCodeGoProvider extends OpenAICompatBase {
|
|
|
6
31
|
constructor() {
|
|
7
32
|
super("opencode-go")
|
|
8
33
|
}
|
|
34
|
+
|
|
35
|
+
protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined, options?: LLMCallOptions): Promise<any> {
|
|
36
|
+
const { default: OpenAI } = await import("openai")
|
|
37
|
+
return new OpenAI({
|
|
38
|
+
apiKey,
|
|
39
|
+
baseURL,
|
|
40
|
+
defaultHeaders: {
|
|
41
|
+
"x-opencode-session": sessionHeader(options?.sessionId),
|
|
42
|
+
"User-Agent": USER_AGENT,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
9
46
|
}
|
|
@@ -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
|
-
|
|
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 ${
|
|
212
|
-
: `Root cause: a decision by agent ${
|
|
213
|
-
affectedAgents: [
|
|
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:
|
|
252
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
19
|
+
* Clave de shard de un agente en el log causal.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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";
|
|
@@ -266,7 +266,9 @@ export const SEED_DATA: SeedData = {
|
|
|
266
266
|
{ id: "z-ai/glm-5.3-flash", providerId: "openrouter", name: "GLM 5.3 Flash (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
|
|
267
267
|
{ id: "z-ai/glm-5.2", providerId: "openrouter", name: "GLM 5.2 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.966, outputPer1M: 3.036 },
|
|
268
268
|
// Qwen
|
|
269
|
-
|
|
269
|
+
// OpenRouter renombró qwen/qwen3.8-max a su versión fechada: mismo contexto
|
|
270
|
+
// y precio, ahora también con imagen. Verificado 2026-09-10.
|
|
271
|
+
{ id: "qwen/qwen3.8-max-0902", providerId: "openrouter", name: "Qwen3.8 Max (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
|
|
270
272
|
{ id: "qwen/qwen3.8-flash", providerId: "openrouter", name: "Qwen3.8 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.15, outputPer1M: 0.47 },
|
|
271
273
|
{ id: "qwen/qwen3.7-flash", providerId: "openrouter", name: "Qwen3.7 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.03, outputPer1M: 0.13 },
|
|
272
274
|
// xAI
|
|
@@ -334,8 +336,10 @@ export const SEED_DATA: SeedData = {
|
|
|
334
336
|
// provider `z-ai` directo, o el enrutado de OpenRouter.
|
|
335
337
|
{ id: "moonshotai/kimi-k3", providerId: "nvidia", name: "Kimi K3 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
336
338
|
{ id: "nvidia/nemotron-3.5-lightning-30b-a3b", providerId: "nvidia", name: "Nemotron 3.5 Lightning 30B", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
// MiniMax M3 (minimaxai/minimax-m3) se sacó: NVIDIA lo retiró (410 Gone, ya
|
|
340
|
+
// no figura en /v1/models). Kimi K2.6 (moonshotai/kimi-k2.6) se sacó por lo
|
|
341
|
+
// mismo que DeepSeek V4 Pro: 404 "Function not found for account" en dos
|
|
342
|
+
// cuentas distintas. Verificado 2026-09-10.
|
|
339
343
|
{ id: "nvidia/nemotron-3-ultra-550b-a55b", providerId: "nvidia", name: "Nemotron 3 Ultra 550B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
340
344
|
{ id: "nvidia/nemotron-3-super-120b-a12b", providerId: "nvidia", name: "Nemotron 3 Super 120B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
341
345
|
// DeepSeek V4 Pro (deepseek-ai/deepseek-v4-pro) se sacó: devuelve 404
|