@johpaz/hive-sdk 0.1.6 → 0.3.0

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 (238) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +21 -3
  3. package/package.json +27 -5
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +82 -25
  6. package/packages/core/src/agent/agent-loop.ts +115 -26
  7. package/packages/core/src/agent/capability-search.ts +2 -2
  8. package/packages/core/src/agent/catalog-selector.ts +4 -4
  9. package/packages/core/src/agent/compaction.ts +30 -10
  10. package/packages/core/src/agent/context-compiler.ts +63 -36
  11. package/packages/core/src/agent/conversation-store.ts +167 -9
  12. package/packages/core/src/agent/curator.ts +16 -7
  13. package/packages/core/src/agent/delegation-runtime.ts +5 -5
  14. package/packages/core/src/agent/goal-runner.ts +9 -9
  15. package/packages/core/src/agent/index.ts +1 -0
  16. package/packages/core/src/agent/llm-client.ts +98 -37
  17. package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
  18. package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
  19. package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
  20. package/packages/core/src/agent/llm-providers/groq.ts +1 -1
  21. package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
  22. package/packages/core/src/agent/llm-providers/interface.ts +2 -2
  23. package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
  24. package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
  25. package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
  26. package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
  27. package/packages/core/src/agent/llm-providers/nvidia.ts +40 -1
  28. package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
  29. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +47 -7
  30. package/packages/core/src/agent/llm-providers/openai.ts +1 -1
  31. package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
  32. package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
  33. package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
  34. package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
  35. package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
  36. package/packages/core/src/agent/playbook-selector.ts +22 -7
  37. package/packages/core/src/agent/prompt-builder.ts +5 -5
  38. package/packages/core/src/agent/proof-packet.ts +5 -5
  39. package/packages/core/src/agent/providers/index.ts +39 -4
  40. package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
  41. package/packages/core/src/agent/realtime-providers/index.ts +29 -0
  42. package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
  43. package/packages/core/src/agent/reflector.ts +38 -15
  44. package/packages/core/src/agent/run-store.ts +8 -8
  45. package/packages/core/src/agent/service.ts +10 -10
  46. package/packages/core/src/agent/skill-selector.ts +6 -6
  47. package/packages/core/src/agent/thread-id.ts +71 -0
  48. package/packages/core/src/agent/thread-store.ts +293 -0
  49. package/packages/core/src/agent/tool-selector.ts +9 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +67 -2
  52. package/packages/core/src/artifacts/index.ts +15 -0
  53. package/packages/core/src/artifacts/store.ts +161 -5
  54. package/packages/core/src/canvas/emitter.ts +2 -2
  55. package/packages/core/src/canvas/index.ts +9 -0
  56. package/packages/core/src/channels/telegram.ts +1 -1
  57. package/packages/core/src/channels/webchat.ts +1 -1
  58. package/packages/core/src/config/loader.ts +14 -5
  59. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  60. package/packages/core/src/events/agent-bus.ts +3 -3
  61. package/packages/core/src/events/channel-narration.ts +3 -3
  62. package/packages/core/src/events/event-bus.ts +1 -1
  63. package/packages/core/src/events/index.ts +18 -0
  64. package/packages/core/src/events/narration.ts +3 -3
  65. package/packages/core/src/events/tool-narration.ts +4 -0
  66. package/packages/core/src/gateway/channel-notify.ts +103 -6
  67. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  68. package/packages/core/src/gateway/durable-queue.ts +18 -6
  69. package/packages/core/src/gateway/index.ts +3 -0
  70. package/packages/core/src/gateway/job-store.ts +11 -5
  71. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  72. package/packages/core/src/gateway/server.ts +2 -2
  73. package/packages/core/src/harness/executors.ts +493 -0
  74. package/packages/core/src/harness/index.ts +12 -2
  75. package/packages/core/src/hooks/index.ts +203 -0
  76. package/packages/core/src/images/index.ts +161 -0
  77. package/packages/core/src/index.ts +1 -0
  78. package/packages/core/src/mcp/MCPClient.ts +3 -3
  79. package/packages/core/src/mcp/hot-reload.ts +5 -5
  80. package/packages/core/src/mcp/tool-sync.ts +5 -5
  81. package/packages/core/src/mcp/transports/index.ts +2 -2
  82. package/packages/core/src/mcp/transports/sse.ts +1 -1
  83. package/packages/core/src/models/index.ts +36 -0
  84. package/packages/core/src/multimodal/index.ts +2 -2
  85. package/packages/core/src/multimodal/vision-service.ts +51 -19
  86. package/packages/core/src/plugins/loader.ts +4 -1
  87. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  88. package/packages/core/src/resilience/index.ts +13 -0
  89. package/packages/core/src/resilience/retry.ts +1 -1
  90. package/packages/core/src/scheduler/CronScheduler.ts +54 -27
  91. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  92. package/packages/core/src/scheduler/cron/index.ts +10 -0
  93. package/packages/core/src/scheduler/cron/job.ts +339 -0
  94. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  95. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  96. package/packages/core/src/scheduler/index.ts +21 -3
  97. package/packages/core/src/scheduler/integration.ts +25 -14
  98. package/packages/core/src/scheduler/types.ts +3 -18
  99. package/packages/core/src/services/agents.ts +268 -0
  100. package/packages/core/src/services/cron.ts +257 -0
  101. package/packages/core/src/services/endpoints.ts +289 -0
  102. package/packages/core/src/services/ethics.ts +107 -0
  103. package/packages/core/src/services/images.ts +212 -0
  104. package/packages/core/src/services/index.ts +112 -0
  105. package/packages/core/src/services/mcp.ts +201 -0
  106. package/packages/core/src/services/memory.ts +133 -0
  107. package/packages/core/src/services/models.ts +179 -0
  108. package/packages/core/src/services/providers.ts +152 -0
  109. package/packages/core/src/services/setup.ts +222 -0
  110. package/packages/core/src/services/skills.ts +241 -0
  111. package/packages/core/src/services/swarms.ts +307 -0
  112. package/packages/core/src/services/tools.ts +106 -0
  113. package/packages/core/src/sessions/index.ts +268 -0
  114. package/packages/core/src/sessions/resolve.ts +108 -0
  115. package/packages/core/src/skills/SkillLoader.ts +8 -1
  116. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  117. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  118. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  119. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  120. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  121. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  122. package/packages/core/src/storage/bootstrap.ts +107 -12
  123. package/packages/core/src/storage/causal-events.ts +1 -1
  124. package/packages/core/src/storage/collections.ts +138 -2
  125. package/packages/core/src/storage/crypto.ts +35 -4
  126. package/packages/core/src/storage/hive.ts +1 -1
  127. package/packages/core/src/storage/hivedb.ts +10 -1
  128. package/packages/core/src/storage/index.ts +2 -1
  129. package/packages/core/src/storage/onboarding.ts +61 -45
  130. package/packages/core/src/storage/reconcile.ts +11 -6
  131. package/packages/core/src/storage/seed.ts +191 -23
  132. package/packages/core/src/storage/usage.ts +3 -3
  133. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  134. package/packages/core/src/swarm/Coordinator.ts +8 -8
  135. package/packages/core/src/swarm/EventBridge.ts +2 -2
  136. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  137. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  138. package/packages/core/src/swarm/index.ts +7 -0
  139. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  140. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  141. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  142. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  143. package/packages/core/src/swarm/types.ts +3 -18
  144. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  145. package/packages/core/src/tool-runtime/index.ts +129 -14
  146. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  147. package/packages/core/src/tools/agents/index.ts +18 -60
  148. package/packages/core/src/tools/cli/index.ts +55 -0
  149. package/packages/core/src/tools/core/index.ts +52 -4
  150. package/packages/core/src/tools/cron/index.ts +8 -8
  151. package/packages/core/src/tools/images/index.ts +130 -0
  152. package/packages/core/src/tools/index.ts +14 -1
  153. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  154. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  155. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
  156. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  157. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  158. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  159. package/packages/core/src/tools/web/browser-click.ts +2 -2
  160. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  161. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  162. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  163. package/packages/core/src/tools/web/browser-script.ts +2 -2
  164. package/packages/core/src/tools/web/browser-service.ts +63 -384
  165. package/packages/core/src/tools/web/browser-session.ts +125 -0
  166. package/packages/core/src/tools/web/browser-type.ts +2 -2
  167. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  168. package/packages/core/src/tools/web/computer-use.ts +553 -0
  169. package/packages/core/src/tools/web/index.ts +8 -1
  170. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  171. package/packages/core/src/utils/index.ts +1 -0
  172. package/packages/core/src/utils/logger.ts +12 -4
  173. package/packages/core/src/utils/redact-binary.ts +17 -0
  174. package/packages/core/src/utils/toon.ts +1 -1
  175. package/packages/core/src/voice/index.ts +6 -6
  176. package/bun.lock +0 -859
  177. package/bunfig.toml +0 -9
  178. package/docs/API-AGENTS.md +0 -367
  179. package/docs/API-CONTEXT-COMPILER.md +0 -249
  180. package/docs/API-DAG-SCHEDULER.md +0 -273
  181. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  182. package/docs/API-WORKERS-EVENTS.md +0 -299
  183. package/docs/HIVE-HARNESS.md +0 -113
  184. package/docs/INDEX.md +0 -190
  185. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  186. package/packages/cli/package.json +0 -17
  187. package/packages/cli/src/commands/create-app.test.ts +0 -180
  188. package/packages/core/package.json +0 -70
  189. package/packages/core/src/api/createAgent.test.ts +0 -160
  190. package/packages/core/src/canvas/canvas.test.ts +0 -36
  191. package/packages/core/src/channels/channels.test.ts +0 -18
  192. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  193. package/packages/core/src/gateway/gateway.test.ts +0 -38
  194. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  195. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  196. package/packages/core/src/skills/skills.test.ts +0 -62
  197. package/packages/core/src/swarm/swarm.test.ts +0 -24
  198. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  199. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  200. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  201. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  202. package/packages/core/src/workers/workers.test.ts +0 -41
  203. package/scripts/bump-version.ts +0 -248
  204. package/scripts/generate-skill-bundle.ts +0 -108
  205. package/test/acceptance-checks.test.ts +0 -403
  206. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  207. package/test/browser-backend.test.ts +0 -308
  208. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  209. package/test/causal-events.test.ts +0 -117
  210. package/test/compaction.test.ts +0 -105
  211. package/test/context-compiler.test.ts +0 -269
  212. package/test/curator.test.ts +0 -130
  213. package/test/durable-queue.test.ts +0 -114
  214. package/test/harness-barrel.test.ts +0 -64
  215. package/test/hive-helpers.test.ts +0 -130
  216. package/test/hivedb-search.test.ts +0 -189
  217. package/test/internal-turns.test.ts +0 -166
  218. package/test/job-idempotency.test.ts +0 -68
  219. package/test/job-retry-backoff.test.ts +0 -184
  220. package/test/job-store.test.ts +0 -381
  221. package/test/llm-retry.test.ts +0 -97
  222. package/test/memory-perf.test.ts +0 -774
  223. package/test/minimal-loadout.test.ts +0 -78
  224. package/test/model-catalog.test.ts +0 -105
  225. package/test/preload.ts +0 -12
  226. package/test/reflector.test.ts +0 -320
  227. package/test/retention-cap.test.ts +0 -91
  228. package/test/retired-capabilities-pruned.test.ts +0 -192
  229. package/test/run-store.test.ts +0 -355
  230. package/test/scratchpad.test.ts +0 -74
  231. package/test/secrets-durability.test.ts +0 -119
  232. package/test/seed-model-reseed.test.ts +0 -155
  233. package/test/setup-agent-seed.test.ts +0 -264
  234. package/test/tool-inventory.test.ts +0 -65
  235. package/test/tool-runtime.test.ts +0 -258
  236. package/test/tool-selector-runtime-tools.test.ts +0 -117
  237. package/test/toon.test.ts +0 -429
  238. package/tsconfig.json +0 -42
@@ -7,15 +7,15 @@
7
7
  * folding, lenient parsing — raw user text never throws).
8
8
  */
9
9
 
10
- import { col } from "../storage/hive"
11
- import type { PlaybookDoc } from "../storage/collections"
12
- import { logger } from "../utils/logger"
10
+ import { col } from "../storage/hive.ts"
11
+ import type { PlaybookDoc } from "../storage/collections.ts"
12
+ import { logger } from "../utils/logger.ts"
13
13
  import {
14
14
  searchCapabilities,
15
15
  applyRelativeCutoff,
16
16
  replaceCapabilityDocs,
17
17
  type CapabilityDoc,
18
- } from "./capability-search"
18
+ } from "./capability-search.ts"
19
19
 
20
20
  const log = logger.child("playbook-selector")
21
21
 
@@ -39,20 +39,33 @@ const MAX_RULES_PER_TURN = 5
39
39
  */
40
40
  const RELEVANCE_RATIO = 0.3
41
41
 
42
+ /** Cuántos candidatos se piden de más para sobrevivir al filtro por usuario. */
43
+ const OVERFETCH_FACTOR = 4
44
+
42
45
  // ─── Selection Logic ───────────────────────────────────────────────────────────
43
46
 
44
47
  /**
45
- * Select relevant rules from the Playbook based on semantic matching
48
+ * Select relevant rules from the Playbook based on semantic matching.
49
+ *
50
+ * `userId` acota lo aprendido a quien corresponde: una regla se aplica si es
51
+ * global (`user_id === ""`, el conocimiento sembrado con el producto) o si
52
+ * salió de las trazas de este mismo usuario. Sin este filtro, lo que el agente
53
+ * aprende hablando con una persona termina inyectado en el prompt de otra.
54
+ * Omitirlo devuelve sólo las reglas globales.
46
55
  */
47
- export async function selectPlaybookRules(message: string): Promise<PlaybookRule[]> {
56
+ export async function selectPlaybookRules(message: string, userId?: string): Promise<PlaybookRule[]> {
48
57
  const startTime = performance.now()
49
58
 
50
59
  if (!message.trim()) return []
51
60
 
52
61
  try {
62
+ // El índice BM25 es uno solo para todos los usuarios, así que pedir k
63
+ // resultados y filtrar después dejaría a un usuario sin reglas cuando
64
+ // las mejor puntuadas son de otro. Se pide un pozo más ancho y se
65
+ // recorta a MAX_RULES_PER_TURN recién después de filtrar.
53
66
  const hits = await searchCapabilities(message, {
54
67
  types: ["playbook"],
55
- k: MAX_RULES_PER_TURN,
68
+ k: MAX_RULES_PER_TURN * OVERFETCH_FACTOR,
56
69
  })
57
70
 
58
71
  const relevantIds = applyRelativeCutoff(hits, RELEVANCE_RATIO).map(h => h.rawId)
@@ -64,6 +77,8 @@ export async function selectPlaybookRules(message: string): Promise<PlaybookRule
64
77
  const entries = await Promise.all(relevantIds.map(id => playbookCol.get(id)))
65
78
  const rules: PlaybookRule[] = entries
66
79
  .filter((e): e is NonNullable<typeof e> => !!e && e.doc.active)
80
+ .filter(e => e.doc.user_id === "" || e.doc.user_id === (userId ?? ""))
81
+ .slice(0, MAX_RULES_PER_TURN)
67
82
  .map(e => ({
68
83
  id: e.id,
69
84
  rule: e.doc.rule,
@@ -13,11 +13,11 @@
13
13
  * - Skills activos
14
14
  */
15
15
 
16
- import { col } from "../storage/hive"
16
+ import { col } from "../storage/hive.ts"
17
17
  import type { EthicsDoc, AgentDoc, UserDoc } from "../storage/collections"
18
- import { logger } from "../utils/logger"
19
- import { formatContext } from "../utils/toon"
20
- import { resolveUserId } from "../storage/onboarding"
18
+ import { logger } from "../utils/logger.ts"
19
+ import { formatContext } from "../utils/toon.ts"
20
+ import { resolveUserId } from "../storage/onboarding.ts"
21
21
 
22
22
  const log = logger.child("prompt-builder")
23
23
 
@@ -136,7 +136,7 @@ export async function buildSystemPrompt(opts: BuildSystemPromptOpts): Promise<st
136
136
  if (Object.keys(userData).length > 0) {
137
137
  userSection += formatContext(userData) + "\n\n"
138
138
  if (agent.role === "coordinator" && user.email) {
139
- userSection += `Cuando el usuario diga "envíame", "mándame" o "a mi correo" sin indicar otro destinatario, usá CorreoPropio. Para terceras personas, resolvé su dirección por separado.\n\n`
139
+ userSection += `Cuando el usuario diga "envíame", "mándame" o "a mi correo" sin indicar otro destinatario, usa CorreoPropio. Para terceras personas, resuelve su dirección por separado.\n\n`
140
140
  }
141
141
  } else {
142
142
  userSection += `Usuario ID: ${userId}\n\n`
@@ -6,11 +6,11 @@
6
6
  * reviewer doesn't have to replay the whole run to trust its outcome.
7
7
  */
8
8
 
9
- import { col, nextId, toIndexable } from "../storage/hive";
10
- import type { ProofPacketDoc } from "../storage/collections";
11
- import type { AcceptanceResult } from "./goal-runner";
12
- import type { RunEpoch } from "./run-epoch";
13
- import { logger } from "../utils/logger";
9
+ import { col, nextId, toIndexable } from "../storage/hive.ts";
10
+ import type { ProofPacketDoc } from "../storage/collections.ts";
11
+ import type { AcceptanceResult } from "./goal-runner.ts";
12
+ import type { RunEpoch } from "./run-epoch.ts";
13
+ import { logger } from "../utils/logger.ts";
14
14
 
15
15
  const log = logger.child("proof-packet");
16
16
 
@@ -7,10 +7,11 @@
7
7
 
8
8
  import type { Config } from "../../config/loader.ts"
9
9
  import { logger } from "../../utils/logger.ts"
10
- import { getAgentLoop } from "../agent-loop"
11
- import { resolveUserId, resolveAgentId } from "../../storage/onboarding"
12
- import type { ContentPart } from "../../multimodal/types"
13
- import type { TurnSource } from "../../storage/collections"
10
+ import { getAgentLoop, buildAgentLoop } from "../agent-loop.ts"
11
+ import { resolveUserId, resolveAgentId } from "../../storage/onboarding.ts"
12
+ import type { ContentPart } from "../../multimodal/types.ts"
13
+ import type { TurnSource } from "../../storage/collections.ts"
14
+ import type { MCPClientManager } from "../../mcp/index.ts"
14
15
 
15
16
  export type Provider = "openai" | "anthropic" | "gemini" | "mistral" | "kimi" | "ollama" | "openrouter" | "deepseek" | "nvidia" | "hiveagents" | "z-ai" | "modelscope" | "minimax" | "qwen" | "groq" | "opencode-go"
16
17
 
@@ -62,6 +63,34 @@ export interface ModelResponse {
62
63
  totalTokens: number
63
64
  }
64
65
  finishReason?: string
66
+ /** Image artifacts (mcp-result-normalizer.ts, agent-loop.ts's turnImageArtifacts) produced by tools this turn. */
67
+ imageArtifacts?: Array<{ artifactId: string; mimeType: string }>
68
+ }
69
+
70
+ /**
71
+ * Crea un `AgentRunner` listo para usar.
72
+ *
73
+ * `AgentRunner.generate()` necesita que el loop global exista
74
+ * (`getAgentLoop()`), y construirlo es un paso aparte que hay que recordar:
75
+ * `new AgentRunner(config)` a secas compila, se instancia sin quejarse y falla
76
+ * recién en la primera llamada con "AgentLoop not initialized". En hive ese paso
77
+ * lo hace su initializer; en el SDK no lo hacía nadie, así que la clase estaba
78
+ * exportada pero no era utilizable.
79
+ *
80
+ * Esta fábrica hace las dos cosas en el orden correcto. Es idempotente: llamarla
81
+ * dos veces reconstruye el loop con el manager que se le pase.
82
+ *
83
+ * ```ts
84
+ * const runner = await createAgentRunner(loadConfig(), { mcpManager });
85
+ * const res = await runner.generate({ messages: [...] });
86
+ * ```
87
+ */
88
+ export async function createAgentRunner(
89
+ config: Config,
90
+ opts: { mcpManager?: MCPClientManager | null } = {},
91
+ ): Promise<AgentRunner> {
92
+ buildAgentLoop({ mcpManager: opts.mcpManager ?? null })
93
+ return new AgentRunner(config)
65
94
  }
66
95
 
67
96
  export class AgentRunner {
@@ -87,6 +116,7 @@ export class AgentRunner {
87
116
  throw new Error("AgentLoop not initialized")
88
117
  }
89
118
 
119
+ let imageArtifacts: ModelResponse["imageArtifacts"]
90
120
  let lastAgentContent = ""
91
121
  let accumulatedAgentContent = "" // Accumulate content from all agent chunks
92
122
  let toolCalls: ModelResponse["toolCalls"] = []
@@ -190,6 +220,10 @@ export class AgentRunner {
190
220
  totalInputTokens += chunk.usage.input_tokens
191
221
  totalOutputTokens += chunk.usage.output_tokens
192
222
  }
223
+
224
+ if (chunk.artifacts?.images?.length) {
225
+ imageArtifacts = [...(imageArtifacts ?? []), ...chunk.artifacts.images]
226
+ }
193
227
  }
194
228
 
195
229
  logger.debug(`[STREAM] done. totalChunks=${chunkCount} lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
@@ -208,6 +242,7 @@ export class AgentRunner {
208
242
  totalTokens: totalInputTokens + totalOutputTokens,
209
243
  },
210
244
  finishReason: "stop",
245
+ imageArtifacts,
211
246
  }
212
247
  } catch (error) {
213
248
  logger.error("AgentRunner error:", error)
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Adaptador de la Live API de Gemini (`bidiGenerateContent`).
3
+ *
4
+ * Sobre `ai.live.connect()` de @google/genai. El import es dinámico igual que en
5
+ * ../llm-providers/gemini.ts: el SDK arrastra dependencias de Vertex que no hacen
6
+ * falta con API key, y cargarlo perezosamente evita pagarlas en el arranque.
7
+ *
8
+ * Verificado contra gemini-3.1-flash-live-preview (2026-08): la inyección de
9
+ * texto va por `sendRealtimeInput({text})`; `sendClientContent` quedó restringido
10
+ * al contexto inicial en 3.x, así que no se usa acá.
11
+ */
12
+
13
+ import { logger } from "../../utils/logger.ts";
14
+ import { ensureArrayItems } from "../llm-providers/interface.ts";
15
+ import type {
16
+ RealtimeProvider,
17
+ RealtimeSession,
18
+ RealtimeSessionOptions,
19
+ RealtimeToolCall,
20
+ } from "./interface.ts";
21
+
22
+ const log = logger.child("realtime:gemini");
23
+
24
+ /** Modelos con `bidiGenerateContent`. El id vive en la BD; esto es sólo el fallback. */
25
+ export const DEFAULT_GEMINI_LIVE_MODEL = "gemini-3.1-flash-live-preview";
26
+
27
+ class GeminiLiveSession implements RealtimeSession {
28
+ closed = false;
29
+
30
+ readonly model: string;
31
+ private readonly session: any;
32
+ private readonly callbacks: RealtimeSessionOptions["callbacks"];
33
+
34
+ constructor(
35
+ model: string,
36
+ session: any,
37
+ callbacks: RealtimeSessionOptions["callbacks"],
38
+ ) {
39
+ this.model = model;
40
+ this.session = session;
41
+ this.callbacks = callbacks;
42
+ }
43
+
44
+ sendAudio(pcm: ArrayBufferLike): void {
45
+ if (this.closed) return;
46
+ try {
47
+ this.session.sendRealtimeInput({
48
+ audio: {
49
+ data: Buffer.from(pcm as ArrayBuffer).toString("base64"),
50
+ mimeType: "audio/pcm;rate=16000",
51
+ },
52
+ });
53
+ } catch (error) {
54
+ this.fail(error, "sendAudio");
55
+ }
56
+ }
57
+
58
+ sendVideoFrame(base64: string, mimeType: string): void {
59
+ if (this.closed) return;
60
+ try {
61
+ this.session.sendRealtimeInput({ video: { data: base64, mimeType } });
62
+ } catch (error) {
63
+ this.fail(error, "sendVideoFrame");
64
+ }
65
+ }
66
+
67
+ sendText(text: string): void {
68
+ if (this.closed || !text.trim()) return;
69
+ try {
70
+ this.session.sendRealtimeInput({ text });
71
+ } catch (error) {
72
+ this.fail(error, "sendText");
73
+ }
74
+ }
75
+
76
+ sendToolResult(id: string, name: string, result: Record<string, unknown>): void {
77
+ if (this.closed) return;
78
+ try {
79
+ this.session.sendToolResponse({ functionResponses: [{ id, name, response: result }] });
80
+ } catch (error) {
81
+ this.fail(error, "sendToolResult");
82
+ }
83
+ }
84
+
85
+ close(): void {
86
+ if (this.closed) return;
87
+ this.closed = true;
88
+ try {
89
+ this.session.close();
90
+ } catch {
91
+ /* el socket ya estaba cerrado */
92
+ }
93
+ }
94
+
95
+ /** Marca la sesión como muerta: seguir escribiendo en un socket roto sólo genera ruido. */
96
+ private fail(error: unknown, op: string): void {
97
+ this.closed = true;
98
+ const err = error instanceof Error ? error : new Error(String(error));
99
+ log.warn(`${op} falló: ${err.message}`);
100
+ this.callbacks.onError?.(err);
101
+ }
102
+ }
103
+
104
+ export class GeminiLiveProvider implements RealtimeProvider {
105
+ readonly id = "gemini";
106
+
107
+ async connect(options: RealtimeSessionOptions): Promise<RealtimeSession> {
108
+ const { GoogleGenAI } = await import("@google/genai");
109
+ const ai = new GoogleGenAI({ apiKey: options.apiKey });
110
+ const cb = options.callbacks;
111
+
112
+ const config: Record<string, unknown> = {
113
+ responseModalities: ["AUDIO"],
114
+ systemInstruction: options.systemInstruction,
115
+ // Necesarias para persistir el hilo y pintarlo en el chat: sin esto sólo
116
+ // llegan bytes de audio y la conversación hablada no deja rastro escrito.
117
+ inputAudioTranscription: {},
118
+ outputAudioTranscription: {},
119
+ // Sin compresión la sesión muere a los 15 min de audio.
120
+ contextWindowCompression: { slidingWindow: {} },
121
+ // Handle para reanudar tras un goAway sin perder el hilo.
122
+ sessionResumption: options.resumptionHandle ? { handle: options.resumptionHandle } : {},
123
+ };
124
+
125
+ if (options.voice || options.language) {
126
+ const speechConfig: Record<string, unknown> = {};
127
+ if (options.voice) {
128
+ speechConfig.voiceConfig = { prebuiltVoiceConfig: { voiceName: options.voice } };
129
+ }
130
+ if (options.language) speechConfig.languageCode = options.language;
131
+ config.speechConfig = speechConfig;
132
+ }
133
+ if (options.silenceDurationMs || options.startOfSpeechSensitivity) {
134
+ const deteccion: Record<string, unknown> = {};
135
+ if (options.silenceDurationMs) deteccion.silenceDurationMs = options.silenceDurationMs;
136
+ if (options.startOfSpeechSensitivity) {
137
+ deteccion.startOfSpeechSensitivity = options.startOfSpeechSensitivity;
138
+ }
139
+ config.realtimeInputConfig = { automaticActivityDetection: deteccion };
140
+ }
141
+ if (options.tools?.length) {
142
+ config.tools = [
143
+ {
144
+ functionDeclarations: options.tools.map((t) => ({
145
+ name: t.function.name,
146
+ description: t.function.description,
147
+ parameters: ensureArrayItems(t.function.parameters),
148
+ })),
149
+ },
150
+ ];
151
+ }
152
+
153
+ let wrapper: GeminiLiveSession | null = null;
154
+
155
+ const session = await ai.live.connect({
156
+ model: options.model,
157
+ config: config as any,
158
+ callbacks: {
159
+ onopen: () => log.info(`sesión abierta (${options.model})`),
160
+ onmessage: (msg: any) => {
161
+ try {
162
+ handleMessage(msg, cb);
163
+ } catch (error) {
164
+ cb.onError?.(error instanceof Error ? error : new Error(String(error)));
165
+ }
166
+ },
167
+ onerror: (e: any) => {
168
+ cb.onError?.(new Error(e?.message ?? String(e)));
169
+ },
170
+ onclose: (e: any) => {
171
+ if (wrapper) wrapper.closed = true;
172
+ cb.onClose?.(e?.reason || "");
173
+ },
174
+ },
175
+ });
176
+
177
+ wrapper = new GeminiLiveSession(options.model, session, cb);
178
+ return wrapper;
179
+ }
180
+ }
181
+
182
+ function handleMessage(msg: any, cb: RealtimeSessionOptions["callbacks"]): void {
183
+ const content = msg.serverContent;
184
+ if (content) {
185
+ for (const part of content.modelTurn?.parts ?? []) {
186
+ const data = part.inlineData?.data;
187
+ if (data) cb.onAudio?.(Buffer.from(data, "base64"));
188
+ }
189
+ if (content.inputTranscription?.text) cb.onInputTranscript?.(content.inputTranscription.text);
190
+ if (content.outputTranscription?.text) cb.onOutputTranscript?.(content.outputTranscription.text);
191
+ if (content.interrupted) cb.onInterrupted?.();
192
+ if (content.turnComplete) cb.onTurnComplete?.();
193
+ }
194
+
195
+ if (msg.toolCall?.functionCalls?.length) {
196
+ const calls: RealtimeToolCall[] = msg.toolCall.functionCalls.map((fc: any) => ({
197
+ id: fc.id ?? "",
198
+ name: fc.name ?? "",
199
+ args: (fc.args ?? {}) as Record<string, unknown>,
200
+ }));
201
+ cb.onToolCall?.(calls);
202
+ }
203
+
204
+ if (msg.toolCallCancellation?.ids?.length) {
205
+ cb.onToolCallCancellation?.(msg.toolCallCancellation.ids);
206
+ }
207
+
208
+ if (msg.usageMetadata) {
209
+ const u = msg.usageMetadata;
210
+ cb.onUsage?.({
211
+ inputTokens: Number(u.promptTokenCount ?? 0),
212
+ outputTokens: Number(u.responseTokenCount ?? 0),
213
+ totalTokens: Number(u.totalTokenCount ?? 0),
214
+ });
215
+ }
216
+
217
+ if (msg.sessionResumptionUpdate?.newHandle) {
218
+ cb.onResumptionHandle?.(msg.sessionResumptionUpdate.newHandle);
219
+ }
220
+
221
+ if (msg.goAway) {
222
+ cb.onGoAway?.(parseDuration(msg.goAway.timeLeft));
223
+ }
224
+ }
225
+
226
+ /** `timeLeft` llega como duración protobuf ("9.5s") o como objeto {seconds,nanos}. */
227
+ function parseDuration(value: unknown): number {
228
+ if (typeof value === "string") {
229
+ const seconds = parseFloat(value.replace(/s$/, ""));
230
+ return Number.isFinite(seconds) ? Math.round(seconds * 1000) : 0;
231
+ }
232
+ if (value && typeof value === "object") {
233
+ const v = value as { seconds?: number | string; nanos?: number };
234
+ const seconds = Number(v.seconds ?? 0);
235
+ return Math.round(seconds * 1000 + (v.nanos ?? 0) / 1e6);
236
+ }
237
+ return 0;
238
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Registro de proveedores de voz en tiempo real.
3
+ *
4
+ * Hoy sólo Gemini Live. La interfaz existe desacoplada para que añadir OpenAI
5
+ * Realtime o Nova Sonic no obligue a tocar el gateway ni el cliente: cambia el
6
+ * adaptador, no el transporte.
7
+ */
8
+
9
+ import { GeminiLiveProvider } from "./gemini-live.ts";
10
+ import type { RealtimeProvider } from "./interface.ts";
11
+
12
+ export * from "./interface.ts";
13
+ export { DEFAULT_GEMINI_LIVE_MODEL } from "./gemini-live.ts";
14
+
15
+ const providers = new Map<string, RealtimeProvider>([["gemini", new GeminiLiveProvider()]]);
16
+
17
+ export function getRealtimeProvider(providerId: string): RealtimeProvider {
18
+ const provider = providers.get(providerId);
19
+ if (!provider) {
20
+ throw new Error(
21
+ `El proveedor "${providerId}" no soporta voz en tiempo real. Disponibles: ${[...providers.keys()].join(", ")}`,
22
+ );
23
+ }
24
+ return provider;
25
+ }
26
+
27
+ export function isRealtimeProvider(providerId: string): boolean {
28
+ return providers.has(providerId);
29
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * realtime-providers — sesiones de voz full-duplex (speech-to-speech).
3
+ *
4
+ * `LLMProvider` (../llm-providers/interface.ts) es one-shot: `call()` recibe un
5
+ * historial y devuelve una respuesta. Una sesión Live no encaja ahí — es un
6
+ * socket que vive minutos, recibe audio continuo y emite audio continuo. De ahí
7
+ * esta interfaz hermana en vez de una subclase.
8
+ *
9
+ * El modelo realtime NO es el cerebro de Hive: sólo oye, habla e interrumpe. El
10
+ * trabajo real lo hace el agent-loop de siempre (Bee → especialistas → tools).
11
+ * Por eso las herramientas que se declaran acá son un puñado fijo de funciones
12
+ * puente (gateway/realtime/bridge-tools.ts) y no el catálogo del agente: la Live
13
+ * API congela las tools en el `setup` y no admite cambiarlas a mitad de sesión.
14
+ */
15
+
16
+ import type { LLMToolDef } from "../llm-client.ts";
17
+
18
+ /** Lo que el micrófono debe entregar: PCM16 mono little-endian. */
19
+ export const REALTIME_INPUT_SAMPLE_RATE = 16_000;
20
+ /** Lo que el modelo devuelve: PCM16 mono. */
21
+ export const REALTIME_OUTPUT_SAMPLE_RATE = 24_000;
22
+
23
+ export interface RealtimeToolCall {
24
+ id: string;
25
+ name: string;
26
+ args: Record<string, unknown>;
27
+ }
28
+
29
+ export interface RealtimeCallbacks {
30
+ /** Audio del modelo, PCM16 mono a REALTIME_OUTPUT_SAMPLE_RATE. */
31
+ onAudio?: (pcm: Buffer) => void;
32
+ /** Transcripción de lo que dijo el usuario (fragmentos incrementales). */
33
+ onInputTranscript?: (text: string) => void;
34
+ /** Transcripción de lo que dijo el modelo (fragmentos incrementales). */
35
+ onOutputTranscript?: (text: string) => void;
36
+ onToolCall?: (calls: RealtimeToolCall[]) => void;
37
+ /** El modelo canceló tool calls ya emitidas (el usuario cambió de tema). */
38
+ onToolCallCancellation?: (ids: string[]) => void;
39
+ /** Barge-in: el usuario habló encima. Hay que vaciar la cola de reproducción YA. */
40
+ onInterrupted?: () => void;
41
+ onTurnComplete?: () => void;
42
+ /** Consumo acumulado que reporta el proveedor. */
43
+ onUsage?: (usage: { inputTokens: number; outputTokens: number; totalTokens: number }) => void;
44
+ /** Handle para reanudar la sesión tras un corte (válido ~2 h). */
45
+ onResumptionHandle?: (handle: string) => void;
46
+ /** El servidor va a cerrar la conexión; hay que reconectar con el handle. */
47
+ onGoAway?: (timeLeftMs: number) => void;
48
+ onError?: (error: Error) => void;
49
+ onClose?: (reason: string) => void;
50
+ }
51
+
52
+ export interface RealtimeSessionOptions {
53
+ model: string;
54
+ apiKey: string;
55
+ /** Prompt de la sesión de voz (no es el system prompt del agent-loop). */
56
+ systemInstruction: string;
57
+ /** Funciones puente. Congeladas: la Live API no permite cambiarlas después. */
58
+ tools?: LLMToolDef[];
59
+ /** Voz del proveedor (ej. "Kore"). Ver voiceService.getGeminiVoices(). */
60
+ voice?: string;
61
+ /**
62
+ * Idioma con acento regional, en BCP-47 (es-CO, es-MX, en-US…).
63
+ *
64
+ * Los modelos de audio nativo cambian de idioma solos siguiendo al usuario,
65
+ * pero sin esto eligen el acento por su cuenta: un usuario colombiano puede
66
+ * terminar escuchando español rioplatense.
67
+ */
68
+ language?: string;
69
+ /** Silencio en ms que cierra el turno del usuario (VAD automático). */
70
+ silenceDurationMs?: number;
71
+ /**
72
+ * Cuánto cuesta que el modelo dé por empezada una intervención del usuario.
73
+ * `START_SENSITIVITY_LOW` sube el listón: hace falta voz clara para
74
+ * interrumpirle, en vez de cualquier ruido. Es lo que salva las manos libres,
75
+ * donde lo que entra por el micrófono incluye su propia voz rebotando.
76
+ */
77
+ startOfSpeechSensitivity?: "START_SENSITIVITY_LOW" | "START_SENSITIVITY_HIGH";
78
+ /** Handle devuelto por una sesión anterior, para retomar el hilo. */
79
+ resumptionHandle?: string;
80
+ callbacks: RealtimeCallbacks;
81
+ }
82
+
83
+ export interface RealtimeSession {
84
+ readonly model: string;
85
+ /** PCM16 mono a REALTIME_INPUT_SAMPLE_RATE. */
86
+ sendAudio(pcm: ArrayBufferLike): void;
87
+ /**
88
+ * Un fotograma de la cámara, ya codificado (JPEG en base64).
89
+ *
90
+ * Va a baja cadencia a propósito: con vídeo, la Live API recorta la sesión de
91
+ * 15 minutos a unos 2 y cada imagen cuesta cientos de tokens frente a los 25
92
+ * por segundo del audio.
93
+ */
94
+ sendVideoFrame(base64: string, mimeType: string): void;
95
+ /** Inyecta texto en la conversación (narración de la colmena, avisos). */
96
+ sendText(text: string): void;
97
+ sendToolResult(id: string, name: string, result: Record<string, unknown>): void;
98
+ close(): void;
99
+ readonly closed: boolean;
100
+ }
101
+
102
+ export interface RealtimeProvider {
103
+ readonly id: string;
104
+ connect(options: RealtimeSessionOptions): Promise<RealtimeSession>;
105
+ }
106
+
107
+ /** Toolset fijo de una sesión de voz, en el formato canónico de Hive. */
108
+ export type RealtimeToolset = LLMToolDef[];
@@ -11,12 +11,13 @@
11
11
  * trick the SQLite version used — HiveDB has no equivalent primitive.
12
12
  */
13
13
 
14
- import { logger } from "../utils/logger"
15
- import { col, nextId } from "../storage/hive"
16
- import { getHiveDb } from "../storage/hivedb"
17
- import { loadConfig } from "../config/loader"
14
+ import { logger } from "../utils/logger.ts"
15
+ import { col, nextId } from "../storage/hive.ts"
16
+ import { getHiveDb } from "../storage/hivedb.ts"
17
+ import { loadConfig } from "../config/loader.ts"
18
18
  import type { HiveDB, ToolStats } from "@johpaz/hive-db"
19
- import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections"
19
+ import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
20
+ import { parseThreadId } from "./thread-id.ts"
20
21
 
21
22
  const log = logger.child("reflector")
22
23
 
@@ -51,25 +52,42 @@ export async function runReflector(): Promise<void> {
51
52
 
52
53
  log.info(`[reflector] Analyzing ${traces.length} traces...`)
53
54
 
55
+ // Las trazas se agrupan por usuario antes de analizarlas.
56
+ //
57
+ // Antes se analizaba el lote entero de una vez, así que una reflexión —y la
58
+ // regla de playbook que sale de ella— mezclaba lo aprendido de varias
59
+ // personas. En una instalación de un solo usuario da igual; en un host
60
+ // multi-inquilino significa que lo aprendido de un workspace se le aplica a
61
+ // otro. El usuario sale del `thread_id`, que ya lo lleva dentro.
62
+ const porUsuario = new Map<string, typeof traceEntries>()
63
+ for (const entry of traceEntries) {
64
+ const usuario = parseThreadId(entry.doc.thread_id)?.userId ?? ""
65
+ const grupo = porUsuario.get(usuario) ?? []
66
+ grupo.push(entry)
67
+ porUsuario.set(usuario, grupo)
68
+ }
69
+
54
70
  // G9: when enabled, per-tool insights use whole-history stats from HiveDB's
55
71
  // event log (toolStats) instead of counters built from just this batch, and
56
72
  // causal threads add root-cause/learning-proposal insights on top.
57
73
  const causalDb = loadConfig().causalLog?.enabled ? await getHiveDb() : null
58
- const localInsights = await analyzeTracesLocally(traces, causalDb)
59
- const causalInsights = await analyzeCausalThreads(traces, causalDb)
60
- const insights = [...localInsights, ...causalInsights]
74
+ const reflectionsCol = await col<ReflectionDoc>("reflections")
75
+ let totalInsights = 0
61
76
 
62
- if (insights.length === 0) {
63
- log.debug("[reflector] No insights generated")
64
- } else {
65
- const traceIds = JSON.stringify(traceEntries.map(e => e.id))
66
- const reflectionsCol = await col<ReflectionDoc>("reflections")
77
+ for (const [usuario, entradas] of porUsuario) {
78
+ const delGrupo = entradas.map(e => e.doc)
79
+ const localInsights = await analyzeTracesLocally(delGrupo, causalDb)
80
+ const causalInsights = await analyzeCausalThreads(delGrupo, causalDb)
81
+ const insights = [...localInsights, ...causalInsights]
82
+ if (insights.length === 0) continue
67
83
 
84
+ const traceIds = JSON.stringify(entradas.map(e => e.id))
68
85
  for (const insight of insights) {
69
86
  const id = await nextId("reflections")
70
87
  await reflectionsCol.put(id, {
71
88
  id,
72
89
  trace_ids: traceIds,
90
+ user_id: usuario,
73
91
  insight_type: insight.type,
74
92
  description: insight.description,
75
93
  affected_tools: insight.affectedTools ? JSON.stringify(insight.affectedTools) : null,
@@ -78,8 +96,13 @@ export async function runReflector(): Promise<void> {
78
96
  created_at: Date.now(),
79
97
  })
80
98
  }
99
+ totalInsights += insights.length
100
+ }
81
101
 
82
- log.info(`[reflector] Generated ${insights.length} insights`)
102
+ if (totalInsights === 0) {
103
+ log.debug("[reflector] No insights generated")
104
+ } else {
105
+ log.info(`[reflector] Generated ${totalInsights} insights across ${porUsuario.size} user(s)`)
83
106
  }
84
107
 
85
108
  // Advance the cursor regardless of whether insights were generated —
@@ -88,7 +111,7 @@ export async function runReflector(): Promise<void> {
88
111
  await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
89
112
 
90
113
  // Trigger curator
91
- const { runCurator } = await import("./curator")
114
+ const { runCurator } = await import("./curator.ts")
92
115
  await runCurator()
93
116
 
94
117
  log.info(`[reflector] Reflection cycle completed successfully`)