@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
@@ -21,27 +21,28 @@
21
21
  * TODOS los datos se formatean en TOON para ahorro de tokens.
22
22
  */
23
23
 
24
- import { col, fromIndexable } from "../storage/hive"
25
- import type { AgentDoc, ModelDoc } from "../storage/collections"
26
- import { logger } from "../utils/logger"
27
- import type { LLMMessage, LLMToolDef, ContentPart } from "./llm-client"
24
+ import { col, fromIndexable } from "../storage/hive.ts"
25
+ import type { AgentDoc, ModelDoc } from "../storage/collections.ts"
26
+ import { logger } from "../utils/logger.ts"
27
+ import type { LLMMessage, LLMToolDef, ContentPart } from "./llm-client.ts"
28
28
  import type { MCPClientManager } from "../mcp/index.ts"
29
- import { syncToolCatalogToIndex, mcpToolFullName } from "./tool-selector"
30
- import { syncSkillsToIndex, getMinimalSkills, selectSkills, getSkillByName, type SkillDescriptor } from "./skill-selector"
31
- import { syncPlaybookToIndex, selectPlaybookRules } from "./playbook-selector"
32
- import { getRecentMessages, getSummary, getScratchpad, toAPIMessages } from "./conversation-store"
33
- import { formatContext, estimateTokens } from "../utils/toon"
34
- import { buildSystemPromptWithProjects } from "./prompt-builder"
29
+ import { syncToolCatalogToIndex, mcpToolFullName } from "./tool-selector.ts"
30
+ import { syncSkillsToIndex, getMinimalSkills, selectSkills, getSkillByName, type SkillDescriptor } from "./skill-selector.ts"
31
+ import { syncPlaybookToIndex, selectPlaybookRules } from "./playbook-selector.ts"
32
+ import { getRecentMessages, getSummary, getScratchpad, toAPIMessages, inflateRecentImages } from "./conversation-store.ts"
33
+ import { formatContext, estimateTokens } from "../utils/toon.ts"
34
+ import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
35
35
  import { createAllTools } from "../tools/index.ts"
36
- import { resolveUserId } from "../storage/onboarding"
37
- import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton"
38
- import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync"
39
- import { getUserDate, getUserTime } from "../utils/date"
40
- import { loadConfig } from "../config/loader"
41
- import { getHiveDb } from "../storage/hivedb"
42
- import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector"
43
- import { expandToolAllowlist } from "./delegation-runtime"
44
- import { MINIMAL_TOOLS } from "./minimal-loadout"
36
+ import { resolveUserId } from "../storage/onboarding.ts"
37
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
38
+ import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync.ts"
39
+ import { getUserDate, getUserTime } from "../utils/date.ts"
40
+ import { loadConfig } from "../config/loader.ts"
41
+ import { getHiveDb } from "../storage/hivedb.ts"
42
+ import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector.ts"
43
+ import { expandToolAllowlist } from "./delegation-runtime.ts"
44
+ import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
45
+ import { normalizeMcpResult } from "./mcp-result-normalizer.ts"
45
46
 
46
47
  const log = logger.child("context-compiler")
47
48
 
@@ -238,7 +239,7 @@ export async function compileContext(opts: {
238
239
 
239
240
  if (effectiveMcpManager) {
240
241
  try {
241
- const mcpServersCol = await col<import("../storage/collections").McpServerDoc>("mcpServers")
242
+ const mcpServersCol = await col<import("../storage/collections.ts").McpServerDoc>("mcpServers")
242
243
  const assignedMcpIds = new Set<string>([
243
244
  ...(agent.mcp_server_ids_json ? JSON.parse(agent.mcp_server_ids_json) : []),
244
245
  ...(agent.active_mcp_json ? JSON.parse(agent.active_mcp_json) : []),
@@ -293,10 +294,20 @@ export async function compileContext(opts: {
293
294
  name: fullName,
294
295
  description: mcpTool.description || `Tool from ${server.name}`,
295
296
  parameters: mcpTool.inputSchema || { type: "object", properties: {} },
296
- execute: async (params: Record<string, unknown>) => {
297
+ execute: async (params: Record<string, unknown>, config?: { configurable?: Record<string, unknown> }) => {
297
298
  // Return raw JS value — agent-loop will TOON-encode via formatToolResult.
298
299
  // Never pre-stringify here: formatToolResult(string) double-encodes.
299
- return await effectiveMcpManager.callTool(resolvedServerKey, mcpTool.name, params)
300
+ const raw = await effectiveMcpManager.callTool(resolvedServerKey, mcpTool.name, params)
301
+ // MCP results can carry base64 image/audio/blob content blocks —
302
+ // normalize those into artifact_ref pointers so a large binary
303
+ // result never gets serialized whole into the LLM context (see
304
+ // mcp-result-normalizer.ts for the incident this prevents).
305
+ const configurable = config?.configurable ?? {}
306
+ return await normalizeMcpResult(raw, {
307
+ userId: configurable.user_id ? String(configurable.user_id) : undefined,
308
+ runId: configurable.run_id ? String(configurable.run_id) : null,
309
+ taskId: configurable.task_id ? String(configurable.task_id) : null,
310
+ })
300
311
  },
301
312
  })
302
313
 
@@ -352,17 +363,30 @@ export async function compileContext(opts: {
352
363
  // Only native minimal tools in LLM context
353
364
  // MCP tools are discovered dynamically via search_knowledge(type="mcp")
354
365
  let filteredNativeTools: ContextTool[] = nativeTools.filter(t => MINIMAL_TOOLS.has(t.name))
355
- if (isCatalogAgent) {
356
- const allowedNames = new Set<string>(
357
- agent.tool_allowlist_json
358
- ? expandToolAllowlist(
359
- JSON.parse(agent.tool_allowlist_json),
360
- nativeTools.map((tool) => tool.name),
361
- )
362
- : agent.tools_json
363
- ? JSON.parse(agent.tools_json)
364
- : [],
366
+
367
+ // La lista blanca se aplica a `allTools`, no sólo al prompt inicial. Filtrar
368
+ // únicamente el loadout de arranque no restringe nada: `search_knowledge`
369
+ // busca contra el índice completo y agent-loop.ts inyecta lo que encuentre
370
+ // resolviéndolo contra `allTools`, así que un agente restringido llegaba
371
+ // igual a cualquier tool nativa por descubrimiento dinámico. Sacarla de
372
+ // `allTools` la vuelve irresoluble: se puede encontrar en el índice, pero no
373
+ // se puede cargar ni llamar.
374
+ //
375
+ // Antes esto dependía de `isCatalogAgent`, y los agentes creados por el
376
+ // usuario —que son los que un host multi-tenant define— quedaban sin límite.
377
+ // Ahora depende de que el agente declare una lista: el coordinador, que no
378
+ // declara ninguna, conserva el descubrimiento abierto.
379
+ const declaredAllowlist = agent.tool_allowlist_json
380
+ ? expandToolAllowlist(
381
+ JSON.parse(agent.tool_allowlist_json),
382
+ nativeTools.map((tool) => tool.name),
365
383
  )
384
+ : agent.tools_json
385
+ ? (JSON.parse(agent.tools_json) as string[])
386
+ : null
387
+
388
+ if (declaredAllowlist) {
389
+ const allowedNames = new Set<string>(declaredAllowlist)
366
390
  filteredNativeTools = nativeTools.filter((tool) => allowedNames.has(tool.name))
367
391
  allTools = [...filteredNativeTools, ...mcpToolExecutors]
368
392
  }
@@ -494,7 +518,10 @@ export async function compileContext(opts: {
494
518
  // or — on a context-overflow retry that keeps only the LAST system message
495
519
  // (openai-compat-base.ts) — silently replaces the real prompt. The summary
496
520
  // lives in `systemPrompt` instead (see conversationSummarySection below).
497
- const messages: LLMMessage[] = toAPIMessages(recentMessages)
521
+ // En el historial las imágenes son referencias, para no reenviarlas enteras en
522
+ // cada turno. Las de los últimos mensajes se vuelven a poner en línea: el
523
+ // modelo todavía puede necesitar mirarlas, y una referencia no se mira.
524
+ const messages: LLMMessage[] = await inflateRecentImages(toAPIMessages(recentMessages))
498
525
 
499
526
  // [STEP-10] STRATEGY 4: ISOLATE — Build context based on agent role
500
527
  log.info(`[context-compiler] [STEP-10] Building system prompt...`)
@@ -508,7 +535,7 @@ export async function compileContext(opts: {
508
535
  }
509
536
 
510
537
  // [STEP-10b] Inject current date/time (ENTORNO ACTUAL)
511
- const usersCol = await col<import("../storage/collections").UserDoc>("users")
538
+ const usersCol = await col<import("../storage/collections.ts").UserDoc>("users")
512
539
  const userRow = await usersCol.get(userId)
513
540
  const userTimezone = userRow?.doc.timezone || "UTC"
514
541
  const now = new Date()
@@ -538,7 +565,7 @@ export async function compileContext(opts: {
538
565
  : Array.isArray(playbookInput)
539
566
  ? playbookInput.filter((part) => part.type === "text").map((part) => (part as any).text).join("\n")
540
567
  : String(playbookInput)
541
- const playbookRules = (await selectPlaybookRules(playbookText)).filter((rule) => {
568
+ const playbookRules = (await selectPlaybookRules(playbookText, userId)).filter((rule) => {
542
569
  if (!rule.applicable_to || !rule.applicable_to.includes("agent:")) return true
543
570
  return isCatalogAgent ? rule.applicable_to.includes(`agent:${agent.id}`) : false
544
571
  })
@@ -585,7 +612,7 @@ export async function compileContext(opts: {
585
612
  .filter((line): line is string => !!line)
586
613
 
587
614
  if (causalLines.length > 0) {
588
- systemPrompt += `\n\n# CAUSAL CONTEXT (decisiones y tool calls de este turno, previos a la compactación — priorizá la conversación actual; usalo solo para no repetir algo que ya funcionó o ya falló)\n${causalLines.join("\n")}\n`
615
+ systemPrompt += `\n\n# CAUSAL CONTEXT (decisiones y tool calls de este turno, previos a la compactación — prioriza la conversación actual; úsalo solo para no repetir algo que ya funcionó o ya falló)\n${causalLines.join("\n")}\n`
589
616
  log.info(`[context-compiler] [STEP-9d] ✅ Injected ${causalLines.length} causal context item(s)`)
590
617
  }
591
618
  } catch (err) {
@@ -5,12 +5,13 @@
5
5
  * Also manages: summaries and scratchpad, both HiveDB document collections.
6
6
  */
7
7
 
8
- import { col, nextId, bumpRollup } from "../storage/hive"
9
- import { getHiveDb } from "../storage/hivedb"
10
- import { logger } from "../utils/logger"
11
- import type { LLMMessage, ContentPart } from "./llm-client"
12
- import { estimateTokens } from "../utils/toon"
13
- import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections"
8
+ import { col, nextId, bumpRollup } from "../storage/hive.ts"
9
+ import { getHiveDb } from "../storage/hivedb.ts"
10
+ import { logger } from "../utils/logger.ts"
11
+ import type { LLMMessage, ContentPart } from "./llm-client.ts"
12
+ import { estimateTokens } from "../utils/toon.ts"
13
+ import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections.ts"
14
+ import { touchThread } from "./thread-store.ts"
14
15
 
15
16
  const log = logger.child("conv-store")
16
17
 
@@ -45,13 +46,26 @@ export interface StoredMessage {
45
46
  // stays clean and the wording can evolve without a migration.
46
47
 
47
48
  export const INTERNAL_SOURCES: ReadonlySet<string> =
48
- new Set(["task_complete", "delegation_summary", "legacy_internal"])
49
+ new Set(["task_complete", "delegation_summary", "legacy_internal", "realtime_chat"])
49
50
 
50
51
  export function isInternalSource(source: string | null | undefined): boolean {
51
52
  return !!source && INTERNAL_SOURCES.has(source)
52
53
  }
53
54
 
54
55
  export function formatInternalEvent(source: string, content: string): string {
56
+ // La charla hablada sí salió del usuario, pero la sesión de voz ya la
57
+ // respondió: es contexto, no trabajo por hacer. Sin esta distinción el
58
+ // coordinador leía cada frase suelta de una llamada como un pedido nuevo y
59
+ // delegaba una tarea por cada una.
60
+ if (source === "realtime_chat") {
61
+ return `<hive:voice_context>\n` +
62
+ `Fragmento de una conversación hablada que la voz de Hive YA respondió en el momento. ` +
63
+ `Es contexto de lo que vinieron hablando, NO un pedido pendiente: no ejecutes ni delegues nada por esto. ` +
64
+ `El trabajo real llega siempre como un mensaje aparte y explícito.\n\n` +
65
+ `${content}\n` +
66
+ `</hive:voice_context>`
67
+ }
68
+
55
69
  return `<hive:internal_event source="${source}">\n` +
56
70
  `Evento interno del sistema — NO es un mensaje del usuario. No lo cites literalmente, no expongas IDs internos (task_id, worker_id) ni JSON crudo. Respondé al usuario de forma natural y breve.\n\n` +
57
71
  `${content}\n` +
@@ -97,6 +111,66 @@ export function getRecentMessageCount(windowMs = 5 * 60_000): number {
97
111
  return recentMessageTimestamps.length
98
112
  }
99
113
 
114
+ /** El threadId es `${userId}/${canal}/${peer}`, así que el dueño ya está ahí. */
115
+ async function resolveOwnerId(threadId: string): Promise<string> {
116
+ const { parseThreadId } = await import("./thread-id.ts")
117
+ return parseThreadId(threadId)?.userId ?? threadId
118
+ }
119
+
120
+ /**
121
+ * Cuánto ocupa una imagen en la ventana de contexto.
122
+ *
123
+ * Los proveedores cobran por área, no por bytes: la fórmula es la de los
124
+ * modelos de visión más comunes (~750 px² por token). Es una estimación, pero
125
+ * cualquier estimación es infinitamente mejor que la anterior, que era cero: la
126
+ * compactación creía que un hilo con diez fotos ocupaba lo que ocupa su texto,
127
+ * y no se disparaba hasta que el proveedor rechazaba el turno.
128
+ */
129
+ export function estimateImageTokens(width?: number | null, height?: number | null): number {
130
+ if (!width || !height) return 1_500; // desconocida: el promedio de una foto
131
+ return Math.ceil((width * height) / 750);
132
+ }
133
+
134
+ /**
135
+ * Cambia las imágenes en línea por referencias a un artefacto.
136
+ *
137
+ * El base64 se guardaba entero en `content_multimodal` y `toAPIMessages` lo
138
+ * devolvía al modelo **en cada turno siguiente**: cinco fotos en una
139
+ * conversación eran cinco fotos reenviadas una y otra vez. Guardar el archivo y
140
+ * dejar una referencia corta ese crecimiento de raíz.
141
+ *
142
+ * La imagen no se pierde: `inflateRecentImages` la vuelve a poner en línea para
143
+ * los últimos turnos, que es donde el modelo todavía puede necesitar mirarla.
144
+ */
145
+ async function imagesToRefs(content: ContentPart[], userId: string): Promise<ContentPart[]> {
146
+ const { createArtifact } = await import("../artifacts/store.ts")
147
+ const out: ContentPart[] = []
148
+
149
+ for (const part of content) {
150
+ if (part.type !== "image_base64") { out.push(part); continue }
151
+ try {
152
+ const bytes = Uint8Array.from(Buffer.from((part as { base64: string }).base64, "base64"))
153
+ const mimeType = (part as { mimeType?: string }).mimeType || "image/jpeg"
154
+ // Si no se puede medir, no es una imagen. Guardarla igual crearía un
155
+ // artefacto de tipo "image" con basura adentro, que aparecería en la
156
+ // galería del usuario; es mejor dejarla como venía.
157
+ const { measureImage } = await import("../images/index.ts")
158
+ const meta = await measureImage(bytes)
159
+
160
+ const art = await createArtifact({
161
+ bytes, mimeType, kind: "image", userId,
162
+ width: meta.width, height: meta.height, expiresAt: null,
163
+ })
164
+ out.push({ type: "artifact_ref", artifact_id: art.id, mime_type: mimeType, width: meta.width, height: meta.height } as unknown as ContentPart)
165
+ } catch {
166
+ // No es una imagen medible, o no se pudo guardar: viaja como venía.
167
+ // Perderla sería peor que no optimizarla.
168
+ out.push(part)
169
+ }
170
+ }
171
+ return out
172
+ }
173
+
100
174
  export async function addMessage(
101
175
  threadId: string,
102
176
  role: StoredMessage["role"],
@@ -107,6 +181,8 @@ export async function addMessage(
107
181
  tool_call_id?: string
108
182
  reasoning_content?: string
109
183
  source?: MessageSource
184
+ /** Dueño de los artefactos que se creen para este mensaje (imágenes). */
185
+ userId?: string
110
186
  }
111
187
  ): Promise<number> {
112
188
  // Handle multimodal content by extracting text for the content column
@@ -116,7 +192,12 @@ export async function addMessage(
116
192
  ? content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
117
193
  : String(content)
118
194
 
119
- const content_multimodal = Array.isArray(content) ? JSON.stringify(content) : null
195
+ // Las imágenes se guardan como archivo y en el historial queda una
196
+ // referencia: el base64 entero se reenviaba al modelo en cada turno.
197
+ const partes = Array.isArray(content)
198
+ ? await imagesToRefs(content, opts?.userId ?? (await resolveOwnerId(threadId)))
199
+ : null
200
+ const content_multimodal = partes ? JSON.stringify(partes) : null
120
201
  const tool_calls_json = opts?.tool_calls ? JSON.stringify(opts.tool_calls) : null
121
202
 
122
203
  const paddedSeq = await nextId(`conversations:${threadId}`)
@@ -136,7 +217,19 @@ export async function addMessage(
136
217
  reasoning_content: opts?.reasoning_content ?? null,
137
218
  source: opts?.source ?? "message",
138
219
  // Estimate tokens: content + tool_calls JSON
139
- token_count: Math.max(1, estimateTokens(textContent) + estimateTokens(tool_calls_json ?? "")),
220
+ // Las imágenes cuentan: antes sumaban cero y la compactación creía que un
221
+ // hilo lleno de fotos ocupaba lo que ocupa su texto.
222
+ token_count: Math.max(
223
+ 1,
224
+ estimateTokens(textContent) +
225
+ estimateTokens(tool_calls_json ?? "") +
226
+ (partes ?? []).reduce((n, p) => {
227
+ const q = p as { type: string; width?: number | null; height?: number | null }
228
+ return q.type === "artifact_ref" || q.type === "image_base64" || q.type === "image_url"
229
+ ? n + estimateImageTokens(q.width, q.height)
230
+ : n
231
+ }, 0),
232
+ ),
140
233
  created_at: now,
141
234
  updated_at: now,
142
235
  }, { expectedVersion: 0 })
@@ -146,6 +239,16 @@ export async function addMessage(
146
239
  bumpRollup("activityRollups", hour, { messageCount: 1 }).catch(() => {})
147
240
  recentMessageTimestamps.push(now)
148
241
 
242
+ // Igual de opcional: el registro de conversaciones es el catálogo que alimenta la
243
+ // lista de la web (título, orden, contador). Se actualiza acá y no en cada llamador
244
+ // para que todo camino que escriba un mensaje —canales, webchat, API, voz— lo
245
+ // mantenga al día sin repetir la llamada.
246
+ touchThread(threadId, {
247
+ role,
248
+ text: textContent,
249
+ internal: isInternalSource(opts?.source),
250
+ }).catch(() => {})
251
+
149
252
  return seq
150
253
  }
151
254
 
@@ -228,6 +331,61 @@ export async function getMessagesAfter(threadId: string, afterId: number): Promi
228
331
 
229
332
  // ─── Convert stored messages → LLMMessage array ───────────────────────────────
230
333
 
334
+ /**
335
+ * Cuántos mensajes del final conservan sus imágenes en línea.
336
+ *
337
+ * Mismo criterio que `clearOldToolResults` (compaction.ts), que ya poda
338
+ * resultados viejos y deja intactos los recientes. Es el compromiso: el modelo
339
+ * puede volver a mirar una imagen de hace un rato —"¿qué decía la factura?"—
340
+ * sin que una conversación larga arrastre todas las fotos para siempre.
341
+ */
342
+ export const KEEP_IMAGES_LAST_N = 6
343
+
344
+ /**
345
+ * Vuelve a poner en línea las imágenes de los últimos mensajes.
346
+ *
347
+ * En el historial las imágenes son referencias (ver `imagesToRefs`), que no
348
+ * ocupan contexto pero tampoco se pueden mirar: un modelo de visión no ve una
349
+ * foto desde un id. Para los últimos `keepLastN` mensajes se leen del disco y
350
+ * se devuelven como base64; los anteriores quedan como referencia, con sus
351
+ * dimensiones, para que el modelo sepa que hubo una imagen y cuál.
352
+ */
353
+ export async function inflateRecentImages(
354
+ messages: LLMMessage[],
355
+ keepLastN = KEEP_IMAGES_LAST_N,
356
+ ): Promise<LLMMessage[]> {
357
+ const desde = Math.max(0, messages.length - keepLastN)
358
+ const tieneRefs = messages.slice(desde).some((m) =>
359
+ Array.isArray(m.content) && m.content.some((p) => (p as { type?: string }).type === "artifact_ref"))
360
+ if (!tieneRefs) return messages
361
+
362
+ const { readArtifactBytes } = await import("../artifacts/store.ts")
363
+
364
+ return Promise.all(messages.map(async (msg, i) => {
365
+ if (i < desde || !Array.isArray(msg.content)) return msg
366
+
367
+ const partes = await Promise.all(msg.content.map(async (part) => {
368
+ const p = part as { type: string; artifact_id?: string; mime_type?: string }
369
+ if (p.type !== "artifact_ref" || !p.artifact_id) return part
370
+ if (!String(p.mime_type ?? "").startsWith("image/")) return part
371
+
372
+ try {
373
+ const datos = await readArtifactBytes(p.artifact_id)
374
+ if (!datos) return part // caducó o se borró: queda la referencia
375
+ return {
376
+ type: "image_base64",
377
+ base64: Buffer.from(datos.bytes).toString("base64"),
378
+ mimeType: datos.mimeType,
379
+ } as unknown as ContentPart
380
+ } catch {
381
+ return part
382
+ }
383
+ }))
384
+
385
+ return { ...msg, content: partes }
386
+ }))
387
+ }
388
+
231
389
  export function toAPIMessages(rows: StoredMessage[]): LLMMessage[] {
232
390
  return rows.map((r) => {
233
391
  let content: string | ContentPart[] = r.content
@@ -13,8 +13,8 @@
13
13
  * (id="curator:lastReflection") instead of SQL's MAX(source_reflection_id).
14
14
  */
15
15
 
16
- import { logger } from "../utils/logger"
17
- import { col, nextId, toIndexable, fromIndexable } from "../storage/hive"
16
+ import { logger } from "../utils/logger.ts"
17
+ import { col, nextId, toIndexable, fromIndexable } from "../storage/hive.ts"
18
18
  import type {
19
19
  ReflectionDoc,
20
20
  PlaybookDoc,
@@ -22,7 +22,7 @@ import type {
22
22
  CursorDoc,
23
23
  AgentProposalDoc,
24
24
  TraceDoc,
25
- } from "../storage/collections"
25
+ } from "../storage/collections.ts"
26
26
 
27
27
  const log = logger.child("curator")
28
28
 
@@ -149,7 +149,7 @@ async function curateAgentStructure(): Promise<void> {
149
149
  })
150
150
  }
151
151
 
152
- const { syncCatalogAgentsToIndex } = await import("./catalog-selector")
152
+ const { syncCatalogAgentsToIndex } = await import("./catalog-selector.ts")
153
153
  await syncCatalogAgentsToIndex()
154
154
  }
155
155
 
@@ -201,7 +201,13 @@ async function processReflection(
201
201
 
202
202
  // Check if a similar rule already exists (fuzzy check by first 60 chars)
203
203
  const prefix = reflection.description.substring(0, 60)
204
- const existing = allPlaybook.find(e => e.doc.active && e.doc.rule.startsWith(prefix))
204
+ // La deduplicación también va por usuario: si dos personas producen la misma
205
+ // observación, son dos reglas. Buscando sólo por texto, lo aprendido de la
206
+ // segunda reforzaría la regla de la primera y la haría pesar más en un
207
+ // playbook que no es suyo.
208
+ const existing = allPlaybook.find(
209
+ e => e.doc.active && e.doc.user_id === reflection.user_id && e.doc.rule.startsWith(prefix)
210
+ )
205
211
 
206
212
  if (existing) {
207
213
  // Reinforce existing rule
@@ -216,6 +222,7 @@ async function processReflection(
216
222
  id,
217
223
  rule: reflection.description,
218
224
  category,
225
+ user_id: reflection.user_id,
219
226
  applicable_to: applicableTo,
220
227
  helpful_count: 1,
221
228
  harmful_count: 0,
@@ -224,7 +231,7 @@ async function processReflection(
224
231
  created_at: now,
225
232
  updated_at: now,
226
233
  }, { expectedVersion: 0 })
227
- allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
234
+ allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, user_id: reflection.user_id, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
228
235
  }
229
236
 
230
237
  function mapInsightTypeToCategory(
@@ -248,12 +255,13 @@ async function addOrUpdateRule(
248
255
  opts: {
249
256
  rule: string
250
257
  category: string
258
+ user_id: string
251
259
  applicable_to: string | null
252
260
  sourceReflectionId: string | null
253
261
  }
254
262
  ): Promise<void> {
255
263
  const prefix = opts.rule.substring(0, 60)
256
- const existing = allPlaybook.find(e => e.doc.rule.startsWith(prefix))
264
+ const existing = allPlaybook.find(e => e.doc.user_id === opts.user_id && e.doc.rule.startsWith(prefix))
257
265
 
258
266
  if (existing) {
259
267
  await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
@@ -264,6 +272,7 @@ async function addOrUpdateRule(
264
272
  id,
265
273
  rule: opts.rule,
266
274
  category: opts.category as PlaybookDoc["category"],
275
+ user_id: opts.user_id,
267
276
  applicable_to: opts.applicable_to,
268
277
  helpful_count: 1,
269
278
  harmful_count: 0,
@@ -1,4 +1,4 @@
1
- import { col, toIndexable, fromIndexable, updateDoc } from "../storage/hive";
1
+ import { col, toIndexable, fromIndexable, updateDoc } from "../storage/hive.ts";
2
2
  import type {
3
3
  AgentDoc,
4
4
  McpServerDoc,
@@ -6,11 +6,11 @@ import type {
6
6
  ProviderDoc,
7
7
  SkillDoc,
8
8
  AgentModelOverride,
9
- } from "../storage/collections";
10
- import { createAllTools } from "../tools";
11
- import { loadConfig } from "../config/loader";
9
+ } from "../storage/collections.ts";
10
+ import { createAllTools } from "../tools/index.ts";
11
+ import { loadConfig } from "../config/loader.ts";
12
12
  import type { MCPClientManager } from "../mcp/index.ts";
13
- import { logger } from "../utils/logger";
13
+ import { logger } from "../utils/logger.ts";
14
14
 
15
15
  const log = logger.child("delegation-runtime");
16
16
  const MCP_IDLE_TTL_MS = 2 * 60_000;
@@ -14,15 +14,15 @@
14
14
  * entire run (not per-turn). This prevents endless loops.
15
15
  */
16
16
 
17
- import { logger } from "../utils/logger";
18
- import { callLLM, type LLMMessage } from "./llm-client";
19
- import { createRun, type AcceptanceCriterion } from "./run-store";
20
- import { clearOldToolResults } from "./compaction";
21
- import { loadConfig } from "../config/loader";
17
+ import { logger } from "../utils/logger.ts";
18
+ import { callLLM, type LLMMessage } from "./llm-client.ts";
19
+ import { createRun, type AcceptanceCriterion } from "./run-store.ts";
20
+ import { clearOldToolResults } from "./compaction.ts";
21
+ import { loadConfig } from "../config/loader.ts";
22
22
  import { getDurableQueue } from "../gateway/durable-queue.ts";
23
- import { recordLLMUsage } from "./tracer";
23
+ import { recordLLMUsage } from "./tracer.ts";
24
24
 
25
- export type { AcceptanceCriterion } from "./run-store";
25
+ export type { AcceptanceCriterion } from "./run-store.ts";
26
26
 
27
27
  export interface AcceptanceResult {
28
28
  id: string;
@@ -126,8 +126,8 @@ export async function runGoal(opts: GoalRunOptions): Promise<GoalRunResult> {
126
126
  /** Runs a deterministic goal_check_tool, no LLM involved. */
127
127
  async function runDeterministicCheck(checkTool: string, goal: string): Promise<{ met: boolean; reason: string } | null> {
128
128
  try {
129
- const { executeToolBatch } = await import("../tool-runtime");
130
- const { createAllTools } = await import("../tools/index");
129
+ const { executeToolBatch } = await import("../tool-runtime/index.ts");
130
+ const { createAllTools } = await import("../tools/index.ts");
131
131
  const allTools = createAllTools(loadConfig());
132
132
  const toolDef = allTools.find((t) => t.name === checkTool);
133
133
  if (!toolDef) {
@@ -15,3 +15,4 @@ export * from "./run-store.ts";
15
15
  export * from "./service.ts";
16
16
  export * from "./skill-selector.ts";
17
17
  export * from "./tool-selector.ts";
18
+ export * from "./realtime-providers/index.ts";