@johpaz/hive-sdk 0.1.3 → 0.1.5

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 (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -20
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -17
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * MCP Tool Sync — Persist MCP tool definitions to DB and the HiveDB index
3
+ *
4
+ * When an MCP server connects, its tool definitions are persisted to
5
+ * the `mcpTools` collection and indexed in the HiveDB capability index
6
+ * for search_knowledge. When the server disconnects, all its tools are
7
+ * deleted from both (the index side uses a `server_id` filter, so only
8
+ * that server's documents are removed).
9
+ *
10
+ * This enables:
11
+ * 1. search_knowledge to find MCP tools via HiveDB BM25
12
+ * 2. Tool definitions to survive across context compiler invocations
13
+ * 3. Offline visibility of what MCP tools were available
14
+ */
15
+
16
+ import { col } from "../storage/hive"
17
+ import type { McpToolDoc } from "../storage/collections"
18
+ import { logger } from "../utils/logger"
19
+ import { mcpToolFullName } from "../agent/tool-selector"
20
+ import {
21
+ replaceCapabilityDocs,
22
+ deleteCapabilitiesByServer,
23
+ type CapabilityDoc,
24
+ } from "../agent/capability-search"
25
+
26
+ const log = logger.child("mcp:tool-sync")
27
+
28
+ export interface MCPToolDefinition {
29
+ name: string
30
+ description: string
31
+ inputSchema?: Record<string, unknown>
32
+ }
33
+
34
+ /**
35
+ * Generate a stable ID for an MCP tool based on server + tool name.
36
+ *
37
+ * MUST be identical to the LLM function name the agent calls — agent-loop
38
+ * resolves search results against executors by this exact string — so it
39
+ * delegates to mcpToolFullName instead of duplicating the algorithm.
40
+ */
41
+ function mcpToolId(serverName: string, toolName: string): string {
42
+ return mcpToolFullName(serverName, toolName)
43
+ }
44
+
45
+ /**
46
+ * Persist MCP tool definitions to the `mcpTools` collection.
47
+ * Called when a server connects or reconnects.
48
+ * Replaces existing tools for the server first, then inserts fresh data.
49
+ */
50
+ export async function syncMCPToolsToDB(
51
+ serverId: string,
52
+ serverName: string,
53
+ tools: MCPToolDefinition[]
54
+ ): Promise<void> {
55
+ try {
56
+ const mcpToolsCol = await col<McpToolDoc>("mcpTools")
57
+ const existing = await mcpToolsCol.findBy("server_id", serverId)
58
+ for (const e of existing) await mcpToolsCol.delete(e.id)
59
+
60
+ if (tools.length === 0) {
61
+ log.debug(`[mcp:tool-sync] No tools to persist for server ${serverName}`)
62
+ return
63
+ }
64
+
65
+ let count = 0
66
+ for (const tool of tools) {
67
+ const id = mcpToolId(serverName, tool.name)
68
+ await mcpToolsCol.put(id, {
69
+ id, server_id: serverId, server_name: serverName, tool_name: tool.name,
70
+ description: tool.description || "", category: "mcp", active: true,
71
+ })
72
+ count++
73
+ }
74
+
75
+ log.info(`[mcp:tool-sync] Persisted ${count} MCP tools for server ${serverName} to mcpTools`)
76
+ } catch (err) {
77
+ log.error(`[mcp:tool-sync] Failed to persist MCP tools for server ${serverName}:`, err)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Sync all active MCP tools from the `mcpTools` collection to the HiveDB
83
+ * capability index. Called after syncMCPToolsToDB or after startup to ensure
84
+ * the index is in sync.
85
+ *
86
+ * This does a full replace of `type=mcp` documents to avoid drift.
87
+ */
88
+ export async function syncMCPToolsToIndex(): Promise<void> {
89
+ try {
90
+ const mcpToolsCol = await col<McpToolDoc>("mcpTools")
91
+ const mcpTools = (await mcpToolsCol.scan({})).map(e => e.doc).filter(t => t.active)
92
+
93
+ // Searchable text is the tool's OWN name/description only. The server
94
+ // name is deliberately NOT indexed: generic server names (e.g.
95
+ // "herramientas administrativas") would make every query matching the
96
+ // server name flood the results with that server's entire tool set.
97
+ // The server stays reachable via the server_id filter and appears in
98
+ // hydrated results.
99
+ //
100
+ // camelCase names ("crearEvento") tokenize as a single word, so a
101
+ // space-split variant goes into the searchable text ("crear Evento").
102
+ const splitCamel = (s: string) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
103
+ const docs: CapabilityDoc[] = mcpTools.map(tool => ({
104
+ type: "mcp" as const,
105
+ rawId: tool.id,
106
+ name: `${tool.tool_name} ${splitCamel(tool.tool_name)}`,
107
+ body: tool.description || tool.tool_name,
108
+ tags: tool.category ?? "",
109
+ extraFilters: [{ field: "server_id", value: tool.server_id }],
110
+ }))
111
+
112
+ await replaceCapabilityDocs("mcp", docs)
113
+
114
+ log.info(`[mcp:tool-sync] Synced ${mcpTools.length} MCP tools to HiveDB index`)
115
+ } catch (err) {
116
+ log.error(`[mcp:tool-sync] Failed to sync MCP tools to HiveDB index:`, err)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Delete all MCP tool definitions for a server from both the `mcpTools`
122
+ * collection and the HiveDB capability index. Called when a server
123
+ * disconnects or is removed.
124
+ */
125
+ export async function clearMCPToolsFromDB(serverId: string): Promise<void> {
126
+ try {
127
+ const mcpToolsCol = await col<McpToolDoc>("mcpTools")
128
+ const existing = await mcpToolsCol.findBy("server_id", serverId)
129
+ for (const e of existing) await mcpToolsCol.delete(e.id)
130
+
131
+ // Remove only this server's documents from the index
132
+ await deleteCapabilitiesByServer(serverId)
133
+
134
+ log.info(`[mcp:tool-sync] Cleared MCP tools for server_id=${serverId}`)
135
+ } catch (err) {
136
+ log.error(`[mcp:tool-sync] Failed to clear MCP tools for server_id=${serverId}:`, err)
137
+ }
138
+ }
@@ -1,33 +1,35 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
1
+ process.env.HIVE_DB_PATH = ":memory:";
2
+
3
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
4
  import { Scratchpad } from "./Scratchpad.ts";
3
- import { initializeDatabase, dbService } from "../storage/SQLiteStorage.ts";
4
- import { getHiveDB, closeHiveDB } from "../storage/HiveDBStorage.ts";
5
+ import { closeHiveDb } from "../storage/hivedb.ts";
6
+ import { ensureHiveDb } from "../storage/bootstrap.ts";
7
+ import { getScratchpad } from "../agent/conversation-store.ts";
5
8
 
6
- describe("Scratchpad", () => {
7
- let pad: Scratchpad;
9
+ const THREAD = "test-thread";
8
10
 
9
- beforeAll(async () => {
10
- initializeDatabase();
11
- await getHiveDB();
12
- pad = new Scratchpad();
13
- });
11
+ let pad: Scratchpad;
14
12
 
15
- afterAll(() => {
16
- closeHiveDB();
17
- dbService.close();
18
- });
13
+ beforeEach(async () => {
14
+ closeHiveDb();
15
+ await ensureHiveDb();
16
+ pad = new Scratchpad();
17
+ });
19
18
 
20
- const THREAD = "test-thread";
19
+ afterEach(() => {
20
+ closeHiveDb();
21
+ });
21
22
 
23
+ describe("Scratchpad", () => {
22
24
  it("writes and reads a note", async () => {
23
25
  await pad.write(THREAD, "test-1", "hello world");
24
- const value = await pad.read(THREAD, "test-1");
25
- expect(value).toBe("hello world");
26
+ expect(await pad.read(THREAD, "test-1")).toBe("hello world");
26
27
  });
27
28
 
28
29
  it("lists notes as key-value map", async () => {
29
30
  await pad.write(THREAD, "list-a", "aaa");
30
31
  await pad.write(THREAD, "list-b", "bbb");
32
+
31
33
  const notes = await pad.list(THREAD);
32
34
  expect(notes["list-a"]).toBe("aaa");
33
35
  expect(notes["list-b"]).toBe("bbb");
@@ -36,14 +38,31 @@ describe("Scratchpad", () => {
36
38
  it("deletes a note", async () => {
37
39
  await pad.write(THREAD, "to-delete", "delete me");
38
40
  await pad.delete(THREAD, "to-delete");
39
- const value = await pad.read(THREAD, "to-delete");
40
- expect(value).toBeUndefined();
41
+ expect(await pad.read(THREAD, "to-delete")).toBeUndefined();
41
42
  });
42
43
 
43
44
  it("clear removes all notes for a thread", async () => {
44
45
  await pad.write(THREAD, "clear-a", "a");
45
46
  await pad.write(THREAD, "clear-b", "b");
46
47
  await pad.clear(THREAD);
47
- expect(Object.keys(await pad.list(THREAD)).length).toBe(0);
48
+ expect(Object.keys(await pad.list(THREAD))).toHaveLength(0);
49
+ });
50
+
51
+ it("no toca las notas de otros threads", async () => {
52
+ await pad.write("thread-uno", "k", "uno");
53
+ await pad.write("thread-dos", "k", "dos");
54
+ await pad.clear("thread-uno");
55
+
56
+ expect(await pad.read("thread-dos", "k")).toBe("dos");
57
+ });
58
+
59
+ it("escribe el mismo documento que lee el context compiler", async () => {
60
+ // La clase y `conversation-store` comparten colección e id. Antes la clase
61
+ // guardaba un doc incompleto (sin `source`, `createdAt` ni `seq`), así que
62
+ // una nota escrita por acá se ordenaba mal dentro del prompt.
63
+ await pad.write(THREAD, "compartida", "valor", "test");
64
+
65
+ const notes = await getScratchpad(THREAD);
66
+ expect(notes).toEqual([{ key: "compartida", value: "valor" }]);
48
67
  });
49
68
  });
@@ -1,54 +1,47 @@
1
- import { getHiveDB } from "../storage/HiveDBStorage.ts";
1
+ /**
2
+ * Scratchpad — notas persistentes por conversación.
3
+ *
4
+ * Es una fachada sobre las funciones de `agent/conversation-store.ts`, que son
5
+ * las que realmente usa el context compiler. Antes esta clase tenía su propia
6
+ * implementación contra la colección `scratchpad` y, como usaba el mismo id
7
+ * (`<threadId>:<key>`), escribía las mismas filas con un documento incompleto:
8
+ * sin `source`, sin `createdAt` y sin `seq`. Una nota guardada por acá quedaba
9
+ * sin el desempate por recencia que aplica `getScratchpad`, así que aparecía en
10
+ * un orden arbitrario dentro del prompt.
11
+ */
2
12
 
3
- interface ScratchpadDoc {
4
- threadId: string;
5
- key: string;
6
- value: string;
7
- updatedAt: number;
8
- }
13
+ import {
14
+ saveScratchpadNote,
15
+ getScratchpad,
16
+ deleteScratchpadNote,
17
+ } from "../agent/conversation-store.ts";
9
18
 
10
19
  export class Scratchpad {
11
- async write(threadId: string, key: string, value: string): Promise<void> {
12
- const db = await getHiveDB();
13
- const col = db.collection<ScratchpadDoc>("scratchpad");
14
- await col.put(this.docId(threadId, key), { threadId, key, value, updatedAt: Date.now() });
20
+ async write(threadId: string, key: string, value: string, source?: string): Promise<void> {
21
+ await saveScratchpadNote(threadId, key, value, source);
15
22
  }
16
23
 
17
24
  async read(threadId: string, key: string): Promise<string | undefined> {
18
- const db = await getHiveDB();
19
- const col = db.collection<ScratchpadDoc>("scratchpad");
20
- const entry = await col.get(this.docId(threadId, key));
21
- return entry?.doc.value;
25
+ const notes = await getScratchpad(threadId);
26
+ return notes.find((n) => n.key === key)?.value;
22
27
  }
23
28
 
29
+ /** Notas del thread, más recientes primero. */
24
30
  async list(threadId: string): Promise<Record<string, string>> {
25
- const db = await getHiveDB();
26
- const col = db.collection<ScratchpadDoc>("scratchpad");
27
- const entries = await col.scan();
28
31
  const result: Record<string, string> = {};
29
- for (const e of entries) {
30
- if (e.doc.threadId === threadId) {
31
- result[e.doc.key] = e.doc.value;
32
- }
32
+ for (const note of await getScratchpad(threadId)) {
33
+ result[note.key] = note.value;
33
34
  }
34
35
  return result;
35
36
  }
36
37
 
37
38
  async delete(threadId: string, key: string): Promise<void> {
38
- const db = await getHiveDB();
39
- const col = db.collection<ScratchpadDoc>("scratchpad");
40
- await col.delete(this.docId(threadId, key));
39
+ await deleteScratchpadNote(threadId, key);
41
40
  }
42
41
 
43
42
  async clear(threadId: string): Promise<void> {
44
- const db = await getHiveDB();
45
- const col = db.collection<ScratchpadDoc>("scratchpad");
46
- const entries = await col.scan();
47
- const ids = entries.filter(e => e.doc.threadId === threadId).map(e => e.id);
48
- await db.batch(ids.map(id => ({ op: "delete" as const, collection: "scratchpad", id })));
49
- }
50
-
51
- private docId(threadId: string, key: string): string {
52
- return `${threadId}:${key}`;
43
+ for (const note of await getScratchpad(threadId)) {
44
+ await deleteScratchpadNote(threadId, note.key);
45
+ }
53
46
  }
54
47
  }
@@ -1,4 +1,5 @@
1
- import { getDb } from "../storage/SQLiteStorage"
1
+ import { col } from "../storage/hive"
2
+ import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections"
2
3
  import { loadProviderApiKey } from "../storage/crypto"
3
4
  import { logger } from "../utils/logger"
4
5
  import type { ImageInput, DocumentInput, VisionConfig } from "./types"
@@ -18,27 +19,19 @@ class MultimodalService {
18
19
  return MultimodalService.instance
19
20
  }
20
21
 
21
- getChannelVisionConfig(channelId: string): VisionConfig {
22
- const db = getDb()
23
- const result = db.query(`
24
- SELECT vision_enabled, ocr_provider, vision_provider, vision_model_id
25
- FROM channels WHERE id = ?
26
- `).get(channelId) as {
27
- vision_enabled: number
28
- ocr_provider: string | null
29
- vision_provider: string | null
30
- vision_model_id: string | null
31
- } | undefined
32
-
33
- if (!result) {
22
+ async getChannelVisionConfig(channelId: string): Promise<VisionConfig> {
23
+ const channelsCol = await col<ChannelDoc>("channels")
24
+ const entry = await channelsCol.get(channelId)
25
+
26
+ if (!entry) {
34
27
  return { visionEnabled: false, ocrProvider: null, visionProvider: null, visionModelId: null }
35
28
  }
36
29
 
37
30
  return {
38
- visionEnabled: result.vision_enabled === 1,
39
- ocrProvider: result.ocr_provider,
40
- visionProvider: result.vision_provider,
41
- visionModelId: result.vision_model_id,
31
+ visionEnabled: entry.doc.vision_enabled,
32
+ ocrProvider: entry.doc.ocr_provider,
33
+ visionProvider: entry.doc.vision_provider,
34
+ visionModelId: entry.doc.vision_model_id,
42
35
  }
43
36
  }
44
37
 
@@ -131,11 +124,26 @@ class MultimodalService {
131
124
  }
132
125
 
133
126
  private async getProviderApiKey(providerId: string): Promise<string | null> {
134
- const db = getDb()
135
127
  const apiKey = await loadProviderApiKey(providerId)
136
128
  return apiKey || null
137
129
  }
138
130
 
131
+ /** Modelo con capacidad de visión del provider según la BD; el literal queda como último recurso. */
132
+ private async getVisionModel(providerId: string, fallback: string): Promise<string> {
133
+ try {
134
+ const modelsCol = await col<ModelDoc>("models")
135
+ const models = await modelsCol.findBy("provider_id", providerId)
136
+ const candidates = models.filter(e =>
137
+ e.doc.model_type === "llm" &&
138
+ (e.doc.capabilities?.includes('"vision"') || e.doc.capabilities?.includes('"ocr"'))
139
+ )
140
+ const best = [...candidates].sort((a, b) => Number(b.doc.active) - Number(a.doc.active))[0]
141
+ return best?.doc.id || fallback
142
+ } catch {
143
+ return fallback
144
+ }
145
+ }
146
+
139
147
  private async ocrWithOpenAI(image: ImageInput): Promise<string> {
140
148
  const key = await this.getProviderApiKey("openai") || process.env.OPENAI_API_KEY
141
149
  if (!key) throw new Error("OPENAI_API_KEY not configured for OCR")
@@ -146,7 +154,7 @@ class MultimodalService {
146
154
  method: "POST",
147
155
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
148
156
  body: JSON.stringify({
149
- model: "gpt-4o-mini",
157
+ model: await this.getVisionModel("openai", "gpt-4o-mini"),
150
158
  messages: [{
151
159
  role: "user",
152
160
  content: [
@@ -183,7 +191,7 @@ class MultimodalService {
183
191
  }
184
192
 
185
193
  const response = await fetch(
186
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
194
+ `https://generativelanguage.googleapis.com/v1beta/models/${await this.getVisionModel("gemini", "gemini-2.0-flash")}:generateContent?key=${key}`,
187
195
  {
188
196
  method: "POST",
189
197
  headers: { "Content-Type": "application/json" },
@@ -229,7 +237,7 @@ class MultimodalService {
229
237
  "anthropic-dangerous-direct-browser-access": "true",
230
238
  },
231
239
  body: JSON.stringify({
232
- model: "claude-haiku-4-5-20251001",
240
+ model: await this.getVisionModel("anthropic", "claude-haiku-4-5-20251001"),
233
241
  max_tokens: 1000,
234
242
  messages: [{
235
243
  role: "user",
@@ -251,28 +259,26 @@ class MultimodalService {
251
259
  return textBlock?.text || ""
252
260
  }
253
261
 
254
- getConfiguredVisionProviders(): Record<string, boolean> {
255
- const db = getDb()
256
- const hasDbKey = (providerId: string): boolean => {
257
- const row = db.query(
258
- `SELECT api_key_encrypted FROM providers WHERE id = ? AND api_key_encrypted IS NOT NULL AND api_key_encrypted != ''`
259
- ).get(providerId) as { api_key_encrypted: string } | undefined
260
- return !!row
261
- }
262
+ async getConfiguredVisionProviders(): Promise<Record<string, boolean>> {
263
+ const hasDbKey = async (providerId: string): Promise<boolean> => !!(await loadProviderApiKey(providerId))
264
+
265
+ const [openai, gemini, anthropic] = await Promise.all([
266
+ hasDbKey("openai"), hasDbKey("gemini"), hasDbKey("anthropic"),
267
+ ])
262
268
 
263
269
  return {
264
- openai: hasDbKey("openai") || !!(process.env.OPENAI_API_KEY),
265
- gemini: hasDbKey("gemini") || !!(process.env.GEMINI_API_KEY),
266
- anthropic: hasDbKey("anthropic") || !!(process.env.ANTHROPIC_API_KEY),
270
+ openai: openai || !!(process.env.OPENAI_API_KEY),
271
+ gemini: gemini || !!(process.env.GEMINI_API_KEY),
272
+ anthropic: anthropic || !!(process.env.ANTHROPIC_API_KEY),
267
273
  }
268
274
  }
269
275
 
270
- modelSupportsVision(providerId: string, modelId: string): boolean {
271
- const db = getDb()
272
- const model = db.query(`SELECT capabilities FROM models WHERE id = ? AND provider_id = ?`).get(modelId, providerId) as { capabilities: string } | undefined
273
- if (!model?.capabilities) return false
276
+ async modelSupportsVision(providerId: string, modelId: string): Promise<boolean> {
277
+ const modelsCol = await col<ModelDoc>("models")
278
+ const entry = await modelsCol.get(modelId)
279
+ if (!entry || entry.doc.provider_id !== providerId || !entry.doc.capabilities) return false
274
280
  try {
275
- const caps = JSON.parse(model.capabilities) as string[]
281
+ const caps = JSON.parse(entry.doc.capabilities) as string[]
276
282
  return caps.includes("vision")
277
283
  } catch {
278
284
  return false
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Generic retry-with-backoff helper for transient failures (LLM provider
3
+ * calls, network/tool errors). Complements CircuitBreaker (circuit-breaker.ts),
4
+ * which sits one layer out and trips on sustained failure rates — this module
5
+ * only decides whether a single call should be retried and how long to wait.
6
+ */
7
+
8
+ import { logger } from "../utils/logger";
9
+
10
+ const log = logger.child("retry");
11
+
12
+ export interface RetryPolicy {
13
+ maxAttempts: number;
14
+ initialDelayMs: number;
15
+ backoffMultiplier: number;
16
+ maxDelayMs: number;
17
+ }
18
+
19
+ const DEFAULT_RETRY_POLICY: RetryPolicy = {
20
+ maxAttempts: 3,
21
+ initialDelayMs: 1000,
22
+ backoffMultiplier: 2,
23
+ maxDelayMs: 30_000,
24
+ };
25
+
26
+ /** Exponential backoff with full jitter, capped at policy.maxDelayMs. */
27
+ export function computeRetryDelay(attempt: number, policy: RetryPolicy, retryAfterMs?: number): number {
28
+ if (retryAfterMs !== undefined) return Math.min(retryAfterMs, policy.maxDelayMs);
29
+ const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * Math.pow(policy.backoffMultiplier, attempt));
30
+ return Math.round(base * (0.5 + Math.random() * 0.5));
31
+ }
32
+
33
+ /**
34
+ * HTTP-status-aware transient classifier: 408/429/5xx/network/timeout are
35
+ * retryable, 400/401/403/404 (and anything else) are terminal. Looks for a
36
+ * `status`/`statusCode` property (most provider SDKs set one) or common
37
+ * network/timeout error names/messages as a fallback.
38
+ */
39
+ export function isRetryableError(err: unknown): boolean {
40
+ const status = extractStatus(err);
41
+ if (status !== undefined) {
42
+ if (status === 408 || status === 429) return true;
43
+ if (status >= 500 && status < 600) return true;
44
+ if (status >= 400 && status < 500) return false;
45
+ }
46
+ const name = (err as { name?: string })?.name ?? "";
47
+ const message = (err as { message?: string })?.message ?? String(err);
48
+ if (name === "AbortError") return false;
49
+ if (/timeout|timed out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(`${name} ${message}`)) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function extractStatus(err: unknown): number | undefined {
56
+ const e = err as { status?: number; statusCode?: number; response?: { status?: number } };
57
+ return e?.status ?? e?.statusCode ?? e?.response?.status;
58
+ }
59
+
60
+ /** Extract a `Retry-After` value (seconds or HTTP-date) if the error/response carries one. */
61
+ function extractRetryAfterMs(err: unknown): number | undefined {
62
+ const headers = (err as { headers?: Record<string, string>; response?: { headers?: Record<string, string> } })?.headers
63
+ ?? (err as { response?: { headers?: Record<string, string> } })?.response?.headers;
64
+ const raw = headers?.["retry-after"] ?? headers?.["Retry-After"];
65
+ if (!raw) return undefined;
66
+ const seconds = Number(raw);
67
+ if (!Number.isNaN(seconds)) return seconds * 1000;
68
+ const dateMs = Date.parse(raw);
69
+ return Number.isNaN(dateMs) ? undefined : Math.max(0, dateMs - Date.now());
70
+ }
71
+
72
+ /**
73
+ * Run `fn`, retrying on transient failure per `policy`. `isRetryable`
74
+ * defaults to {@link isRetryableError}; pass a custom classifier for
75
+ * domain-specific errors (e.g. job executors).
76
+ */
77
+ export async function withRetry<T>(
78
+ fn: (attempt: number) => Promise<T>,
79
+ policy: RetryPolicy = DEFAULT_RETRY_POLICY,
80
+ isRetryable: (err: unknown) => boolean = isRetryableError
81
+ ): Promise<T> {
82
+ let lastErr: unknown;
83
+ for (let attempt = 0; attempt < policy.maxAttempts; attempt++) {
84
+ try {
85
+ return await fn(attempt);
86
+ } catch (err) {
87
+ lastErr = err;
88
+ if (attempt === policy.maxAttempts - 1 || !isRetryable(err)) throw err;
89
+ const delay = computeRetryDelay(attempt, policy, extractRetryAfterMs(err));
90
+ log.warn(`[withRetry] Attempt ${attempt + 1}/${policy.maxAttempts} failed (${(err as Error).message}); retrying in ${delay}ms`);
91
+ await new Promise((resolve) => setTimeout(resolve, delay));
92
+ }
93
+ }
94
+ throw lastErr;
95
+ }