@johpaz/hive-sdk 0.1.4 → 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 -27
  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 -18
  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,130 @@
1
+ /**
2
+ * Unit tests for the HiveDB collection helpers (storage/hive.ts) and the
3
+ * bootstrap entry point (storage/bootstrap.ts). Uses an in-memory database
4
+ * (HIVE_DB_PATH=":memory:") so no state persists between runs.
5
+ */
6
+
7
+ process.env.HIVE_DB_PATH = ":memory:";
8
+
9
+ import { describe, test, expect, beforeEach } from "bun:test";
10
+ import { closeHiveDb } from "../packages/core/src/storage/hivedb";
11
+ import { col, nextId, updateDoc, updateManyByIndex, findByAny, bumpRollup } from "../packages/core/src/storage/hive";
12
+ import { ensureHiveDb, isBootstrapped } from "../packages/core/src/storage/bootstrap";
13
+ import {
14
+ acknowledgeNotification,
15
+ createNotification,
16
+ listPendingNotifications,
17
+ } from "../packages/core/src/gateway/notification-inbox";
18
+
19
+ beforeEach(() => {
20
+ closeHiveDb();
21
+ });
22
+
23
+ describe("storage/hive.ts helpers", () => {
24
+ test("nextId produces a monotonically increasing, lexicographically sortable sequence", async () => {
25
+ const a = await nextId("test-counter");
26
+ const b = await nextId("test-counter");
27
+ const c = await nextId("test-counter");
28
+ expect(a < b).toBe(true);
29
+ expect(b < c).toBe(true);
30
+ expect(a).toBe("000000000000001");
31
+ expect(c).toBe("000000000000003");
32
+ });
33
+
34
+ test("nextId keeps separate counters independent", async () => {
35
+ const a1 = await nextId("counter-a");
36
+ const b1 = await nextId("counter-b");
37
+ const a2 = await nextId("counter-a");
38
+ expect(a1).toBe("000000000000001");
39
+ expect(b1).toBe("000000000000001");
40
+ expect(a2).toBe("000000000000002");
41
+ });
42
+
43
+ test("updateDoc merges a patch into an existing document", async () => {
44
+ const c = await col<{ name: string; active: boolean }>("widgets");
45
+ await c.put("w1", { name: "Widget", active: false });
46
+ const updated = await updateDoc<{ name: string; active: boolean }>("widgets", "w1", { active: true });
47
+ expect(updated).toEqual({ name: "Widget", active: true });
48
+ const fromDb = await c.get("w1");
49
+ expect(fromDb?.doc).toEqual({ name: "Widget", active: true });
50
+ });
51
+
52
+ test("updateDoc throws for a missing document", async () => {
53
+ await expect(updateDoc("widgets", "missing", { active: true })).rejects.toThrow("not found");
54
+ });
55
+
56
+ test("updateManyByIndex patches every doc matching an indexed field", async () => {
57
+ const c = await col<{ provider_id: string; active: boolean }>("models_test");
58
+ await c.createIndex("provider_id");
59
+ await c.put("m1", { provider_id: "openai", active: true });
60
+ await c.put("m2", { provider_id: "openai", active: true });
61
+ await c.put("m3", { provider_id: "anthropic", active: true });
62
+
63
+ const count = await updateManyByIndex<{ provider_id: string; active: boolean }>(
64
+ "models_test", "provider_id", "openai", { active: false }
65
+ );
66
+ expect(count).toBe(2);
67
+ expect((await c.get("m1"))?.doc.active).toBe(false);
68
+ expect((await c.get("m2"))?.doc.active).toBe(false);
69
+ expect((await c.get("m3"))?.doc.active).toBe(true);
70
+ });
71
+
72
+ test("findByAny emulates WHERE field IN (...)", async () => {
73
+ const c = await col<{ agent_id: string }>("bus_test");
74
+ await c.createIndex("agent_id");
75
+ await c.put("msg1", { agent_id: "a1" });
76
+ await c.put("msg2", { agent_id: "a2" });
77
+ await c.put("msg3", { agent_id: "a3" });
78
+
79
+ const rows = await findByAny<{ agent_id: string }>("bus_test", "agent_id", ["a1", "a3", "a3"]);
80
+ expect(rows.length).toBe(2);
81
+ expect(rows.map((r) => r.id).sort()).toEqual(["msg1", "msg3"]);
82
+ });
83
+
84
+ test("bumpRollup accumulates deltas and nested breakdowns across calls", async () => {
85
+ await bumpRollup("usageRollups_test", "hour:2026-07-08T10", { inputTokens: 100, costUsd: 0.5 }, { field: "byProvider", key: "openai" });
86
+ await bumpRollup("usageRollups_test", "hour:2026-07-08T10", { inputTokens: 50, costUsd: 0.2 }, { field: "byProvider", key: "openai" });
87
+ await bumpRollup("usageRollups_test", "hour:2026-07-08T10", { inputTokens: 10, costUsd: 0.1 }, { field: "byProvider", key: "anthropic" });
88
+
89
+ const c = await col<any>("usageRollups_test");
90
+ const doc = (await c.get("hour:2026-07-08T10"))?.doc;
91
+ expect(doc.inputTokens).toBe(160);
92
+ expect(doc.costUsd).toBeCloseTo(0.8);
93
+ expect(doc.byProvider.openai.inputTokens).toBe(150);
94
+ expect(doc.byProvider.anthropic.inputTokens).toBe(10);
95
+ });
96
+ });
97
+
98
+ describe("durable WebChat notification inbox", () => {
99
+ test("keeps a notification pending until its owner acknowledges it", async () => {
100
+ const notifications = await col("notifications");
101
+ await notifications.createIndex("user_id");
102
+ const notification = await createNotification({
103
+ userId: "user-a",
104
+ channel: "webchat",
105
+ message: "Trabajo terminado",
106
+ });
107
+
108
+ expect(await listPendingNotifications("user-a", "webchat")).toHaveLength(1);
109
+ expect(await acknowledgeNotification(notification.id, "user-b")).toBe(false);
110
+ expect(await listPendingNotifications("user-a", "webchat")).toHaveLength(1);
111
+
112
+ expect(await acknowledgeNotification(notification.id, "user-a")).toBe(true);
113
+ expect(await listPendingNotifications("user-a", "webchat")).toHaveLength(0);
114
+ });
115
+ });
116
+
117
+ describe("storage/bootstrap.ts", () => {
118
+ test("ensureHiveDb is idempotent and marks bootstrapped", async () => {
119
+ // Sin aserción sobre el estado inicial: `bootstrapped` es un flag de módulo
120
+ // que `closeHiveDb()` no resetea, así que su valor de entrada depende de si
121
+ // otro archivo de test ya llamó a ensureHiveDb en este mismo proceso.
122
+ await ensureHiveDb();
123
+ expect(isBootstrapped()).toBe(true);
124
+ await ensureHiveDb(); // second call must not throw (idempotent indexes + schemaVersion put)
125
+
126
+ const meta = await col<{ value: number }>("meta");
127
+ const version = await meta.get("schemaVersion");
128
+ expect(version?.doc.value).toBe(1);
129
+ });
130
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * HiveDB capability search — integration tests
3
+ *
4
+ * Covers the capability search layer:
5
+ * packages/core/src/agent/capability-search.ts on top of @johpaz/hive-db.
6
+ *
7
+ * Uses an in-memory index (HIVE_DB_PATH=":memory:") so no state
8
+ * touches ~/.hive.
9
+ */
10
+
11
+ process.env.HIVE_DB_PATH = ":memory:";
12
+
13
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
14
+ import {
15
+ searchCapabilities,
16
+ applyRelativeCutoff,
17
+ replaceCapabilityDocs,
18
+ upsertCapabilityDocs,
19
+ deleteCapabilitiesByServer,
20
+ } from "../packages/core/src/agent/capability-search";
21
+ import { closeHiveDb } from "../packages/core/src/storage/hivedb";
22
+
23
+ beforeAll(async () => {
24
+ await replaceCapabilityDocs("tool", [
25
+ {
26
+ type: "tool",
27
+ rawId: "web_search",
28
+ name: "web_search",
29
+ body: "Busca en internet información actualizada, noticias e investigación",
30
+ tags: "web",
31
+ },
32
+ {
33
+ type: "tool",
34
+ rawId: "cron.create",
35
+ name: "cron.create",
36
+ body: "Programar tareas recurrentes, crear recordatorios y automatizar horarios",
37
+ tags: "scheduling",
38
+ },
39
+ ]);
40
+ await replaceCapabilityDocs("skill", [
41
+ {
42
+ type: "skill",
43
+ rawId: "reportes",
44
+ name: "generación de reportes",
45
+ body: "Genera reportes mensuales de transacciones y métricas",
46
+ tags: "reportes análisis",
47
+ },
48
+ ]);
49
+ await replaceCapabilityDocs("mcp", [
50
+ {
51
+ type: "mcp",
52
+ rawId: "gmail__send_email",
53
+ name: "send_email",
54
+ body: "Send an email to a recipient",
55
+ tags: "mcp gmail",
56
+ extraFilters: [{ field: "server_id", value: "gmail" }],
57
+ },
58
+ {
59
+ type: "mcp",
60
+ rawId: "github__create_issue",
61
+ name: "create_issue",
62
+ body: "Create a GitHub issue",
63
+ tags: "mcp github",
64
+ extraFilters: [{ field: "server_id", value: "github" }],
65
+ },
66
+ ]);
67
+ });
68
+
69
+ afterAll(() => {
70
+ closeHiveDb();
71
+ });
72
+
73
+ describe("HiveDB capability search", () => {
74
+ test("Spanish stemming: 'reporte' finds 'reportes'", async () => {
75
+ const hits = await searchCapabilities("reporte", { types: ["skill"] });
76
+ expect(hits.length).toBe(1);
77
+ expect(hits[0].rawId).toBe("reportes");
78
+ expect(hits[0].score).toBeGreaterThan(0);
79
+ });
80
+
81
+ test("accent folding: 'transaccion' finds 'transacciones'", async () => {
82
+ const hits = await searchCapabilities("transaccion", { types: ["skill"] });
83
+ expect(hits.length).toBe(1);
84
+ expect(hits[0].rawId).toBe("reportes");
85
+ });
86
+
87
+ test("raw user input with operators and punctuation never throws", async () => {
88
+ for (const q of [
89
+ '"comillas sin cerrar',
90
+ "¿puedes buscar (noticias) OR NOT?",
91
+ "a:b* ~^",
92
+ "***",
93
+ ]) {
94
+ const hits = await searchCapabilities(q, {});
95
+ expect(Array.isArray(hits)).toBe(true);
96
+ }
97
+ // And the natural-language version still matches
98
+ const hits = await searchCapabilities("¿puedes buscar noticias en internet?", {
99
+ types: ["tool"],
100
+ });
101
+ expect(hits.map((h) => h.rawId)).toContain("web_search");
102
+ });
103
+
104
+ test("type filter isolates result sets", async () => {
105
+ // "email" only exists in MCP docs
106
+ const mcpHits = await searchCapabilities("email", { types: ["mcp"] });
107
+ expect(mcpHits.length).toBe(1);
108
+ expect(mcpHits[0].type).toBe("mcp");
109
+
110
+ const toolHits = await searchCapabilities("email", { types: ["tool"] });
111
+ expect(toolHits.length).toBe(0);
112
+ });
113
+
114
+ test("all-types search returns namespaced hits", async () => {
115
+ const hits = await searchCapabilities("reportes internet email");
116
+ const types = new Set(hits.map((h) => h.type));
117
+ expect(types.has("tool")).toBe(true);
118
+ expect(types.has("skill")).toBe(true);
119
+ expect(types.has("mcp")).toBe(true);
120
+ for (const hit of hits) {
121
+ expect(hit.id).toBe(`${hit.type}:${hit.rawId}`);
122
+ }
123
+ });
124
+
125
+ test("replaceCapabilityDocs does not duplicate on re-sync", async () => {
126
+ // Simulate the startup sync running twice
127
+ await replaceCapabilityDocs("tool", [
128
+ {
129
+ type: "tool",
130
+ rawId: "web_search",
131
+ name: "web_search",
132
+ body: "Busca en internet información actualizada, noticias e investigación",
133
+ tags: "web",
134
+ },
135
+ {
136
+ type: "tool",
137
+ rawId: "cron.create",
138
+ name: "cron.create",
139
+ body: "Programar tareas recurrentes, crear recordatorios y automatizar horarios",
140
+ tags: "scheduling",
141
+ },
142
+ ]);
143
+ const hits = await searchCapabilities("internet noticias", { types: ["tool"], k: 20 });
144
+ const ids = hits.map((h) => h.rawId);
145
+ expect(new Set(ids).size).toBe(ids.length);
146
+ expect(ids.filter((id) => id === "web_search").length).toBe(1);
147
+ });
148
+
149
+ test("MCP server disconnect removes only that server's docs", async () => {
150
+ await deleteCapabilitiesByServer("gmail");
151
+
152
+ const emailHits = await searchCapabilities("email", { types: ["mcp"] });
153
+ expect(emailHits.length).toBe(0);
154
+
155
+ const githubHits = await searchCapabilities("github issue", { types: ["mcp"] });
156
+ expect(githubHits.map((h) => h.rawId)).toContain("github__create_issue");
157
+
158
+ // Restore for other tests
159
+ await upsertCapabilityDocs([
160
+ {
161
+ type: "mcp",
162
+ rawId: "gmail__send_email",
163
+ name: "send_email",
164
+ body: "Send an email to a recipient",
165
+ tags: "mcp gmail",
166
+ extraFilters: [{ field: "server_id", value: "gmail" }],
167
+ },
168
+ ]);
169
+ });
170
+
171
+ test("applyRelativeCutoff drops weak hits, keeps strong ones", () => {
172
+ const hits = [
173
+ { id: "tool:a", type: "tool" as const, rawId: "a", score: 10 },
174
+ { id: "tool:b", type: "tool" as const, rawId: "b", score: 5 },
175
+ { id: "tool:c", type: "tool" as const, rawId: "c", score: 1 },
176
+ ];
177
+ const kept = applyRelativeCutoff(hits, 0.3);
178
+ expect(kept.map((h) => h.rawId)).toEqual(["a", "b"]);
179
+ expect(applyRelativeCutoff([], 0.3)).toEqual([]);
180
+ });
181
+
182
+ test("selector-path latency stays under 50ms warm", async () => {
183
+ await searchCapabilities("warmup", { types: ["tool"] });
184
+ const start = performance.now();
185
+ await searchCapabilities("programar recordatorio diario", { types: ["tool"] });
186
+ const elapsed = performance.now() - start;
187
+ expect(elapsed).toBeLessThan(50);
188
+ });
189
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Internal-event turns (delegation fan-in) must persist as role:"user" + a
3
+ * `source` tag — never role:"system" — so every LLM provider that hoists
4
+ * ALL role:"system" messages into a single system instruction (Gemini,
5
+ * Anthropic) can't accidentally promote a transient delegation notice into a
6
+ * permanent standing instruction. See conversation-store.ts's
7
+ * isInternalSource/formatInternalEvent and routes/chat.ts's history filter.
8
+ */
9
+
10
+ process.env.HIVE_DB_PATH = ":memory:";
11
+
12
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
13
+ import { closeHiveDb } from "../packages/core/src/storage/hivedb";
14
+ import { ensureHiveDb } from "../packages/core/src/storage/bootstrap";
15
+ import { resetBootId } from "../packages/core/src/storage/boot-id";
16
+ import { col } from "../packages/core/src/storage/hive";
17
+ import {
18
+ addMessage,
19
+ getRecentMessages,
20
+ toAPIMessages,
21
+ isInternalSource,
22
+ formatInternalEvent,
23
+ } from "../packages/core/src/agent/conversation-store";
24
+ import type { ConversationDoc } from "../packages/core/src/storage/collections";
25
+
26
+ beforeEach(async () => {
27
+ closeHiveDb();
28
+ resetBootId();
29
+ await ensureHiveDb();
30
+ });
31
+
32
+ afterEach(() => {
33
+ closeHiveDb();
34
+ });
35
+
36
+ describe("internal turns: persistence", () => {
37
+ test("addMessage with source persists role:user + the given source", async () => {
38
+ await addMessage("thread-a", "user", "El agente completó la tarea X.", {
39
+ source: "task_complete",
40
+ });
41
+
42
+ const conversationsCol = await col<ConversationDoc>("conversations");
43
+ const entries = await conversationsCol.scan({ prefix: "thread-a:" });
44
+ expect(entries).toHaveLength(1);
45
+ expect(entries[0].doc.role).toBe("user");
46
+ expect(entries[0].doc.source).toBe("task_complete");
47
+ });
48
+
49
+ test("normal messages persist source:message by default", async () => {
50
+ await addMessage("thread-b", "user", "hola");
51
+
52
+ const conversationsCol = await col<ConversationDoc>("conversations");
53
+ const entries = await conversationsCol.scan({ prefix: "thread-b:" });
54
+ expect(entries[0].doc.source).toBe("message");
55
+ });
56
+ });
57
+
58
+ describe("internal turns: serialization (toAPIMessages)", () => {
59
+ test("wraps internal-source content in the <hive:internal_event> marker", async () => {
60
+ await addMessage("thread-c", "user", "El agente completó la tarea X.", {
61
+ source: "task_complete",
62
+ });
63
+
64
+ const rows = await getRecentMessages("thread-c", 10);
65
+ const apiMessages = toAPIMessages(rows);
66
+
67
+ expect(apiMessages).toHaveLength(1);
68
+ expect(apiMessages[0].role).toBe("user");
69
+ expect(apiMessages[0].content).toContain('<hive:internal_event source="task_complete">');
70
+ expect(apiMessages[0].content).toContain("El agente completó la tarea X.");
71
+ });
72
+
73
+ test("leaves normal user/assistant content untouched", async () => {
74
+ await addMessage("thread-d", "user", "hola");
75
+ await addMessage("thread-d", "assistant", "¿en qué te ayudo?");
76
+
77
+ const rows = await getRecentMessages("thread-d", 10);
78
+ const apiMessages = toAPIMessages(rows);
79
+
80
+ expect(apiMessages[0].content).toBe("hola");
81
+ expect(apiMessages[1].content).toBe("¿en qué te ayudo?");
82
+ });
83
+
84
+ test("regression guard: a mix of user/assistant/internal rows never yields role:system", async () => {
85
+ await addMessage("thread-e", "user", "Delegá esto");
86
+ await addMessage("thread-e", "assistant", "Listo, delegado.");
87
+ await addMessage("thread-e", "user", "El agente completó la tarea X.", {
88
+ source: "task_complete",
89
+ });
90
+ await addMessage("thread-e", "assistant", "El worker terminó la tarea X.");
91
+
92
+ const rows = await getRecentMessages("thread-e", 10);
93
+ const apiMessages = toAPIMessages(rows);
94
+
95
+ expect(apiMessages).toHaveLength(4);
96
+ expect(apiMessages.every((m) => m.role !== "system")).toBe(true);
97
+ });
98
+ });
99
+
100
+ // El bloque "public history endpoint" de hive quedó fuera: cubre
101
+ // `handleGetChatHistory` de gateway/routes/chat.ts, y el gateway del SDK no
102
+ // incluye esas rutas. Lo que sí viaja es el filtro que consume la ruta —
103
+ // `isInternalSource` / `getRecentMessages` — que se verifica más abajo.
104
+
105
+ describe("internal turns: legacy role:system compat", () => {
106
+ async function writeLegacySystemRow(threadId: string, content: string) {
107
+ // Pre-`source` rows used role:"system" as the sole marker for internal
108
+ // events; source didn't exist yet (persisted as absent/null).
109
+ await addMessage(threadId, "user", content); // reserves the id/seq
110
+ const conversationsCol = await col<ConversationDoc>("conversations");
111
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:` });
112
+ // Target the row just added, not the thread's first row — a thread may
113
+ // already have earlier messages when this helper runs.
114
+ const { id, version, doc } = entries[entries.length - 1];
115
+ await conversationsCol.put(id, { ...doc, role: "system", source: null }, { expectedVersion: version });
116
+ }
117
+
118
+ test("getRecentMessages normalizes legacy role:system rows to user/legacy_internal", async () => {
119
+ await writeLegacySystemRow("thread-g", "[Sistema] aviso viejo");
120
+
121
+ const rows = await getRecentMessages("thread-g", 10);
122
+ expect(rows).toHaveLength(1);
123
+ expect(rows[0].role).toBe("user");
124
+ expect(rows[0].source).toBe("legacy_internal");
125
+ });
126
+
127
+ test("toAPIMessages wraps legacy rows the same as current internal events", async () => {
128
+ await writeLegacySystemRow("thread-h", "[Sistema] aviso viejo");
129
+
130
+ const rows = await getRecentMessages("thread-h", 10);
131
+ const apiMessages = toAPIMessages(rows);
132
+
133
+ expect(apiMessages[0].role).toBe("user");
134
+ expect(apiMessages[0].content).toContain('<hive:internal_event source="legacy_internal">');
135
+ });
136
+
137
+ test("una fila legacy queda marcada como interna, así el historial público puede filtrarla", async () => {
138
+ await addMessage("thread-i", "user", "hola");
139
+ await writeLegacySystemRow("thread-i", "[Sistema] aviso viejo");
140
+
141
+ const rows = await getRecentMessages("thread-i", 10);
142
+ const visible = rows.filter((r) => !isInternalSource(r.source));
143
+
144
+ expect(visible).toHaveLength(1);
145
+ expect(visible[0].content).toBe("hola");
146
+ });
147
+ });
148
+
149
+ describe("isInternalSource / formatInternalEvent", () => {
150
+ test("recognizes task_complete, delegation_summary, legacy_internal — nothing else", () => {
151
+ expect(isInternalSource("task_complete")).toBe(true);
152
+ expect(isInternalSource("delegation_summary")).toBe(true);
153
+ expect(isInternalSource("legacy_internal")).toBe(true);
154
+ expect(isInternalSource("message")).toBe(false);
155
+ expect(isInternalSource("api")).toBe(false);
156
+ expect(isInternalSource(null)).toBe(false);
157
+ expect(isInternalSource(undefined)).toBe(false);
158
+ });
159
+
160
+ test("formatInternalEvent frames content with the source and a non-citation instruction", () => {
161
+ const wrapped = formatInternalEvent("task_complete", "resultado X");
162
+ expect(wrapped).toContain('source="task_complete"');
163
+ expect(wrapped).toContain("resultado X");
164
+ expect(wrapped).toContain("NO es un mensaje del usuario");
165
+ });
166
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tests for gateway/job-store.ts — idempotency_key dedup on createJob:
3
+ * a repeated key returns the existing job (whatever its status) instead of
4
+ * creating a duplicate.
5
+ */
6
+
7
+ process.env.HIVE_DB_PATH = ":memory:";
8
+
9
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
10
+ import { closeHiveDb } from "../packages/core/src/storage/hivedb";
11
+ import { ensureHiveDb } from "../packages/core/src/storage/bootstrap";
12
+ import { resetBootId } from "../packages/core/src/storage/boot-id";
13
+ import { createJob, claimJob, completeJob, findByIdempotencyKey } from "../packages/core/src/gateway/job-store";
14
+ import { col } from "../packages/core/src/storage/hive";
15
+ import type { JobDoc } from "../packages/core/src/storage/collections";
16
+
17
+ beforeEach(async () => {
18
+ closeHiveDb();
19
+ resetBootId();
20
+ await ensureHiveDb();
21
+ });
22
+
23
+ afterEach(() => {
24
+ closeHiveDb();
25
+ });
26
+
27
+ describe("job-store: idempotency_key", () => {
28
+ test("two createJob calls with the same key return the same job", async () => {
29
+ const first = await createJob({ lane: "s1", type: "worker_task", payload: { n: 1 }, run_id: "r1", idempotency_key: "dedupe-me" });
30
+ const second = await createJob({ lane: "s1", type: "worker_task", payload: { n: 2 }, run_id: "r1", idempotency_key: "dedupe-me" });
31
+
32
+ expect(second.id).toBe(first.id);
33
+ // Second call's payload never gets persisted — the original wins.
34
+ expect(JSON.parse(second.payload_json).n).toBe(1);
35
+
36
+ const c = await col<JobDoc>("jobQueue");
37
+ const all = await c.scan();
38
+ expect(all.filter((e) => e.doc.type === "worker_task").length).toBe(1);
39
+ });
40
+
41
+ test("returns the cached result once the original job has completed", async () => {
42
+ const job = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r1", idempotency_key: "k1" });
43
+ await claimJob(job.id);
44
+ await completeJob(job.id, { done: true });
45
+
46
+ const repeat = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r1", idempotency_key: "k1" });
47
+ expect(repeat.id).toBe(job.id);
48
+ expect(repeat.status).toBe("completed");
49
+ expect(JSON.parse(repeat.result_json!)).toEqual({ done: true });
50
+ });
51
+
52
+ test("jobs without an idempotency_key are never deduped against each other", async () => {
53
+ const a = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r1" });
54
+ const b = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r2" });
55
+ expect(a.id).not.toBe(b.id);
56
+ });
57
+
58
+ test("findByIdempotencyKey returns null for an unknown key", async () => {
59
+ const result = await findByIdempotencyKey("does-not-exist");
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ test("different keys create distinct jobs", async () => {
64
+ const a = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r1", idempotency_key: "key-a" });
65
+ const b = await createJob({ lane: "s1", type: "worker_task", payload: {}, run_id: "r2", idempotency_key: "key-b" });
66
+ expect(a.id).not.toBe(b.id);
67
+ });
68
+ });