@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,186 @@
1
+ /**
2
+ * Capability Search — shared HiveDB search layer
3
+ *
4
+ * Single entry point for searching Hive's capability catalogs (native tools,
5
+ * skills, playbook rules, MCP tools): one HiveDB index for all four, with the
6
+ * catalog discriminated by the `type` field rather than by separate tables.
7
+ *
8
+ * Document convention (one index, type discrimination via filters):
9
+ * - id: `tool:${name}` | `skill:${id}` | `playbook:${rowid}` | `mcp:${id}` | `agent:${id}`
10
+ * - name: tool/skill name or rule head (BM25 boost 4.0)
11
+ * - tags: category + triggers + keywords (BM25 boost 3.0)
12
+ * - body: description / rule text / content (BM25 boost 2.0)
13
+ * - filters: { type: "tool"|"skill"|"playbook"|"mcp" }
14
+ * plus { server_id } on MCP docs for per-server hot-reload.
15
+ *
16
+ * Score semantics: text-only queries return raw BM25 (positive, higher is
17
+ * better). Never compare against an absolute floor — BM25 magnitude depends
18
+ * on corpus and document length. Use applyRelativeCutoff() instead.
19
+ */
20
+
21
+ import type { IndexDoc } from "@johpaz/hive-db";
22
+ import { getHiveDb } from "../storage/hivedb";
23
+ import { logger } from "../utils/logger";
24
+
25
+ const log = logger.child("capability-search");
26
+
27
+ export type CapabilityType = "tool" | "skill" | "playbook" | "mcp" | "agent";
28
+
29
+ export interface CapabilityHit {
30
+ /** Namespaced id, e.g. "tool:web_search" */
31
+ id: string;
32
+ type: CapabilityType;
33
+ /** Id without the type prefix, e.g. "web_search" */
34
+ rawId: string;
35
+ /** Raw BM25 score: positive, higher = more relevant */
36
+ score: number;
37
+ }
38
+
39
+ export interface CapabilityDoc {
40
+ type: CapabilityType;
41
+ /** Id within the type namespace (tool name, skill id, playbook rowid, mcp id) */
42
+ rawId: string;
43
+ name?: string;
44
+ body?: string;
45
+ tags?: string;
46
+ /** Extra filters besides `type` (e.g. { server_id } for MCP docs) */
47
+ extraFilters?: Array<{ field: string; value: string }>;
48
+ }
49
+
50
+ const TYPE_PREFIXES: CapabilityType[] = ["tool", "skill", "playbook", "mcp", "agent"];
51
+
52
+ function splitId(id: string): { type: CapabilityType; rawId: string } | null {
53
+ const sep = id.indexOf(":");
54
+ if (sep === -1) return null;
55
+ const type = id.slice(0, sep) as CapabilityType;
56
+ if (!TYPE_PREFIXES.includes(type)) return null;
57
+ return { type, rawId: id.slice(sep + 1) };
58
+ }
59
+
60
+ // ─── Search ──────────────────────────────────────────────────────────────────
61
+
62
+ export interface SearchCapabilitiesOptions {
63
+ /** Restrict to these types. Omit or empty = all types. */
64
+ types?: CapabilityType[];
65
+ /** Maximum hits to return (default 10). */
66
+ k?: number;
67
+ /** Per-field BM25 boosts (engine defaults: name 4, tags 3, body 2). */
68
+ boosts?: { name?: number; body?: number; tags?: number };
69
+ }
70
+
71
+ /**
72
+ * Search the capability index with raw user text. The engine parses leniently
73
+ * (accents, quotes, operators and punctuation never throw) and applies
74
+ * Spanish stemming + accent folding, so callers must NOT pre-sanitize.
75
+ */
76
+ export async function searchCapabilities(
77
+ query: string,
78
+ opts: SearchCapabilitiesOptions = {}
79
+ ): Promise<CapabilityHit[]> {
80
+ const k = opts.k ?? 10;
81
+ const types = opts.types?.length ? opts.types : undefined;
82
+ const trimmed = query.trim();
83
+ if (!trimmed) return [];
84
+
85
+ const startTime = performance.now();
86
+ const db = await getHiveDb();
87
+
88
+ // Filters are AND-ed by the engine, so multi-type search runs one query per
89
+ // type; the single-type and all-types cases are one engine call.
90
+ const queries = types
91
+ ? types.map((t) => ({
92
+ type: t,
93
+ filters: [{ field: "type", value: t }],
94
+ }))
95
+ : [{ type: undefined, filters: undefined }];
96
+
97
+ const merged = new Map<string, CapabilityHit>();
98
+ for (const q of queries) {
99
+ const hits = await db.queryHybrid({
100
+ text: trimmed,
101
+ k,
102
+ filters: q.filters,
103
+ boosts: opts.boosts,
104
+ });
105
+ for (const hit of hits) {
106
+ const parsed = splitId(hit.id);
107
+ if (!parsed) continue;
108
+ const existing = merged.get(hit.id);
109
+ if (!existing || hit.score > existing.score) {
110
+ merged.set(hit.id, {
111
+ id: hit.id,
112
+ type: parsed.type,
113
+ rawId: parsed.rawId,
114
+ score: hit.score,
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ const results = Array.from(merged.values())
121
+ .sort((a, b) => b.score - a.score)
122
+ .slice(0, k);
123
+
124
+ const timing = performance.now() - startTime;
125
+ log.debug(
126
+ `[capability-search] "${trimmed.substring(0, 60)}" → ${results.length} hits in ${timing.toFixed(1)}ms`
127
+ );
128
+ return results;
129
+ }
130
+
131
+ /**
132
+ * Keep only hits scoring at least `ratio` of the top hit. This replaces the
133
+ * old absolute negative-bm25 thresholds: relevance is relative to the best
134
+ * match, never an absolute floor.
135
+ */
136
+ export function applyRelativeCutoff(
137
+ hits: CapabilityHit[],
138
+ ratio = 0.3
139
+ ): CapabilityHit[] {
140
+ if (hits.length === 0) return hits;
141
+ const top = hits[0].score;
142
+ if (top <= 0) return [];
143
+ return hits.filter((h) => h.score >= ratio * top);
144
+ }
145
+
146
+ // ─── Sync helpers ────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Replace all documents of a type: deletes existing docs carrying the type
150
+ * filter, then batch-upserts the new set under a single index commit.
151
+ */
152
+ export async function replaceCapabilityDocs(
153
+ type: CapabilityType,
154
+ docs: CapabilityDoc[]
155
+ ): Promise<void> {
156
+ const db = await getHiveDb();
157
+ await db.deleteByFilter({ field: "type", value: type });
158
+ if (docs.length === 0) return;
159
+ await db.upsertBatch(docs.map(toIndexDoc));
160
+ }
161
+
162
+ /** Upsert documents without clearing the rest of their type. */
163
+ export async function upsertCapabilityDocs(docs: CapabilityDoc[]): Promise<void> {
164
+ if (docs.length === 0) return;
165
+ const db = await getHiveDb();
166
+ await db.upsertBatch(docs.map(toIndexDoc));
167
+ }
168
+
169
+ /** Delete every MCP doc belonging to a server (hot-reload/disconnect). */
170
+ export async function deleteCapabilitiesByServer(serverId: string): Promise<void> {
171
+ const db = await getHiveDb();
172
+ await db.deleteByFilter({ field: "server_id", value: serverId });
173
+ }
174
+
175
+ function toIndexDoc(doc: CapabilityDoc): IndexDoc {
176
+ return {
177
+ id: `${doc.type}:${doc.rawId}`,
178
+ name: doc.name,
179
+ body: doc.body,
180
+ tags: doc.tags,
181
+ filters: [
182
+ { field: "type", value: doc.type },
183
+ ...(doc.extraFilters ?? []),
184
+ ],
185
+ };
186
+ }
@@ -0,0 +1,103 @@
1
+ import { col } from "../storage/hive";
2
+ import type { AgentDoc } from "../storage/collections";
3
+ import {
4
+ applyRelativeCutoff,
5
+ replaceCapabilityDocs,
6
+ searchCapabilities,
7
+ type CapabilityDoc,
8
+ } from "./capability-search";
9
+ import { logger } from "../utils/logger";
10
+
11
+ const log = logger.child("catalog-selector");
12
+ const RELEVANCE_RATIO = 0.35;
13
+ const ROUTING_STOP_WORDS = new Set([
14
+ "a", "al", "de", "del", "el", "en", "la", "las", "lo", "los",
15
+ "o", "otro", "otra", "un", "una", "y",
16
+ ]);
17
+
18
+ function routingTokens(value: string): Set<string> {
19
+ const normalized = value
20
+ .normalize("NFD")
21
+ .replace(/\p{Diacritic}/gu, "")
22
+ .toLowerCase()
23
+ .split(/[^\p{Letter}\p{Number}_]+/u)
24
+ .filter(Boolean)
25
+ .map((token) => token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token)
26
+ .filter((token) => !ROUTING_STOP_WORDS.has(token));
27
+ return new Set(normalized);
28
+ }
29
+
30
+ function matchesRoutingExclusion(query: string, agent: AgentDoc): boolean {
31
+ if (!agent.routing_exclusions_json) return false;
32
+ const queryTokens = routingTokens(query);
33
+ const exclusions = JSON.parse(agent.routing_exclusions_json) as string[];
34
+
35
+ return exclusions.some((exclusion) => {
36
+ const excludedTokens = routingTokens(exclusion);
37
+ const shared = [...excludedTokens].filter((token) => queryTokens.has(token)).length;
38
+ return shared >= 2 && shared / Math.max(excludedTokens.size, 1) >= 0.35;
39
+ });
40
+ }
41
+
42
+ export async function getCatalogAgent(id: string): Promise<AgentDoc | null> {
43
+ const entry = await (await col<AgentDoc>("agents")).get(id);
44
+ return entry?.doc.source === "catalog" && entry.doc.enabled ? entry.doc : null;
45
+ }
46
+
47
+ export async function listCatalogAgents(): Promise<AgentDoc[]> {
48
+ const c = await col<AgentDoc>("agents");
49
+ return (await c.findBy("source", "catalog")).map((entry) => entry.doc).filter((doc) => doc.enabled);
50
+ }
51
+
52
+ export async function searchCatalogAgents(query: string, k = 5): Promise<Array<{ agent: AgentDoc; score: number }>> {
53
+ const hits = applyRelativeCutoff(
54
+ await searchCapabilities(query, { types: ["agent"], k }),
55
+ RELEVANCE_RATIO,
56
+ );
57
+ const c = await col<AgentDoc>("agents");
58
+ const results: Array<{ agent: AgentDoc; score: number }> = [];
59
+ for (const hit of hits) {
60
+ const entry = await c.get(hit.rawId);
61
+ if (
62
+ entry?.doc.source === "catalog" &&
63
+ entry.doc.enabled &&
64
+ !matchesRoutingExclusion(query, entry.doc)
65
+ ) {
66
+ results.push({ agent: entry.doc, score: hit.score });
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+
72
+ export async function syncCatalogAgentsToIndex(): Promise<void> {
73
+ const agents = await listCatalogAgents();
74
+ const docs: CapabilityDoc[] = agents.map((agent) => ({
75
+ type: "agent",
76
+ rawId: agent.id,
77
+ name: `${agent.name} ${agent.id}`,
78
+ tags: [
79
+ ...(agent.routing_examples_json ? JSON.parse(agent.routing_examples_json) : []),
80
+ ...(agent.tool_allowlist_json ? JSON.parse(agent.tool_allowlist_json) : []),
81
+ ...(agent.skills_json ? JSON.parse(agent.skills_json) : []),
82
+ ].join(" "),
83
+ // The complete system prompt is intentionally excluded: it adds routing
84
+ // noise and may contain operational policy not meant for retrieval.
85
+ body: agent.description ?? "",
86
+ }));
87
+ await replaceCapabilityDocs("agent", docs);
88
+ log.info(`[catalog-selector] Synced ${docs.length} catalog agents to HiveDB index`);
89
+ }
90
+
91
+ export function renderAgentRoutingCatalog(agents: AgentDoc[]): string {
92
+ return agents
93
+ .map((a) => {
94
+ const exclusions = a.routing_exclusions_json
95
+ ? JSON.parse(a.routing_exclusions_json) as string[]
96
+ : [];
97
+ const exclusionText = exclusions.length > 0
98
+ ? ` NO usar para: ${exclusions.join("; ")}.`
99
+ : "";
100
+ return `- ${a.id}: ${a.description ?? ""}${exclusionText}`;
101
+ })
102
+ .join("\n");
103
+ }
@@ -15,23 +15,26 @@
15
15
  * short summaries in the in-memory message array before model calls.
16
16
  */
17
17
 
18
- import { logger } from "../utils/logger.ts"
18
+ import { logger } from "../utils/logger"
19
19
  import {
20
20
  getTotalTokens,
21
21
  getHistory,
22
22
  getSummary,
23
23
  saveSummary,
24
- toAPIMessages,
25
24
  getMessageCount,
26
- } from "./ConversationStore"
27
- import { estimateTokens } from "../utils/toon.ts"
28
- import { callLLM, resolveProviderConfig, type ContentPart } from "./providers/LLMClient"
29
- import { getDb } from "../storage/SQLiteStorage.ts"
25
+ isInternalSource,
26
+ type StoredMessage,
27
+ } from "./conversation-store"
28
+ import { estimateTokens } from "../utils/toon"
29
+ import { callLLM, resolveProviderConfig, getDefaultLLM, type ContentPart } from "./llm-client"
30
+ import { col, fromIndexable } from "../storage/hive"
31
+ import type { AgentDoc, ModelDoc } from "../storage/collections"
30
32
 
31
33
  const log = logger.child("compaction")
32
34
 
33
35
  // Token budget: compress when stored tokens exceed this threshold
34
- const COMPACT_TOKEN_THRESHOLD = 6000 // ~60% of 10K context window
36
+ // Will be overridden by model's context_window at runtime if available
37
+ const COMPACT_TOKEN_THRESHOLD = 32000 // ~25% of 128K default context window
35
38
  const KEEP_LAST_N_MESSAGES = 5 // always keep most recent N messages
36
39
  const TOOL_RESULT_MAX_CHARS = 200 // max chars for old tool results after clearing
37
40
  const MAX_TRANSCRIPT_MSGS = 30 // cap messages sent to summarizer (avoids OOM on small models)
@@ -46,11 +49,30 @@ export async function maybeCompact(
46
49
  notify?: { channel: string; userId: string }
47
50
  ): Promise<void> {
48
51
  try {
49
- const totalTokens = getTotalTokens(threadId)
50
- if (totalTokens < COMPACT_TOKEN_THRESHOLD) return
52
+ const totalTokens = await getTotalTokens(threadId)
51
53
 
52
- const summary = getSummary(threadId)
53
- const totalMessages = getMessageCount(threadId)
54
+ // Use model's context window if available, otherwise use default
55
+ let effectiveThreshold = COMPACT_TOKEN_THRESHOLD
56
+ try {
57
+ const agentsCol = await col<AgentDoc>("agents")
58
+ const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
59
+ const modelId = fromIndexable(coordinators[0]?.doc.model_id ?? null)
60
+ if (modelId) {
61
+ const modelsCol = await col<ModelDoc>("models")
62
+ // El id se busca completo: recortar el primer segmento rompía cualquier
63
+ // modelo cuyo nombre lleve barra (meta/llama-3.3-70b-instruct buscaba
64
+ // "llama-3.3-70b-instruct", no encontraba nada y caía al default).
65
+ const modelEntry = await modelsCol.get(modelId)
66
+ if (modelEntry?.doc.context_window) {
67
+ effectiveThreshold = Math.floor(modelEntry.doc.context_window * 0.25)
68
+ }
69
+ }
70
+ } catch { /* use default threshold */ }
71
+
72
+ if (totalTokens < effectiveThreshold) return
73
+
74
+ const summary = await getSummary(threadId)
75
+ const totalMessages = await getMessageCount(threadId)
54
76
 
55
77
  // Already summarized up to near the current state
56
78
  if (summary && summary.last_message_id > totalMessages - KEEP_LAST_N_MESSAGES) return
@@ -62,22 +84,48 @@ export async function maybeCompact(
62
84
  }
63
85
  }
64
86
 
87
+ /**
88
+ * Find a clean cut point: the "keep" side must begin with a user turn so we
89
+ * never leave orphaned tool messages at the start of the visible window.
90
+ * Internal events (delegation fan-in) are persisted as role:"user", so they
91
+ * are valid boundaries here same as human turns.
92
+ * Returns 0 when no clean boundary exists (caller should skip compaction).
93
+ */
94
+ export function findCompactionCutIndex(rows: StoredMessage[], keepLastN = KEEP_LAST_N_MESSAGES): number {
95
+ let cutIndex = rows.length - keepLastN
96
+ while (cutIndex > 0 && rows[cutIndex]?.role !== "user") {
97
+ cutIndex--
98
+ }
99
+ return cutIndex
100
+ }
101
+
102
+ /**
103
+ * Render a transcript for the summarizer LLM. Internal events (delegation
104
+ * fan-in notices) are labeled distinctly — they are persisted as
105
+ * role:"user" so the LLM treats them as input, but labeling them [USER] here
106
+ * would make the summarizer attribute system-generated task outcomes to the
107
+ * human, baking that misattribution into the durable summary.
108
+ */
109
+ export function renderTranscript(rows: StoredMessage[], maxMsgChars = MAX_MSG_CHARS): string {
110
+ return rows
111
+ .map((r) => {
112
+ const label = isInternalSource(r.source) ? "EVENTO INTERNO" : r.role.toUpperCase()
113
+ return `[${label}]: ${r.content.substring(0, maxMsgChars)}`
114
+ })
115
+ .join("\n\n")
116
+ }
117
+
65
118
  /**
66
119
  * Compress a thread's history into a summary.
67
120
  */
68
- export async function compactThread(
121
+ async function compactThread(
69
122
  threadId: string,
70
123
  notify?: { channel: string; userId: string }
71
124
  ): Promise<void> {
72
- const allMessages = getHistory(threadId)
125
+ const allMessages = await getHistory(threadId)
73
126
  if (allMessages.length <= KEEP_LAST_N_MESSAGES) return
74
127
 
75
- // Find a clean cut point: the "keep" side must begin with a user turn so
76
- // we never leave orphaned tool messages at the start of the visible window.
77
- let cutIndex = allMessages.length - KEEP_LAST_N_MESSAGES
78
- while (cutIndex > 0 && allMessages[cutIndex]?.role !== "user") {
79
- cutIndex--
80
- }
128
+ const cutIndex = findCompactionCutIndex(allMessages)
81
129
  if (cutIndex <= 0) {
82
130
  log.info(`[compaction] No clean user-turn boundary found — skipping`)
83
131
  return
@@ -88,32 +136,17 @@ export async function compactThread(
88
136
 
89
137
  const lastSummarizedId = toSummarize[toSummarize.length - 1].id
90
138
 
91
- const existingSummary = getSummary(threadId)
139
+ const existingSummary = await getSummary(threadId)
92
140
  if (existingSummary && existingSummary.last_message_id >= lastSummarizedId) return
93
141
 
94
142
  // Cap transcript to avoid overflowing small model contexts
95
143
  const capped = toSummarize.slice(-MAX_TRANSCRIPT_MSGS)
96
- const apiMessages = toAPIMessages(capped)
97
- const transcript = apiMessages
98
- .map((m) => {
99
- const text = typeof m.content === "string"
100
- ? m.content
101
- : Array.isArray(m.content)
102
- ? m.content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
103
- : ""
104
- return `[${m.role.toUpperCase()}]: ${text.substring(0, MAX_MSG_CHARS)}`
105
- })
106
- .join("\n\n")
144
+ const transcript = renderTranscript(capped)
107
145
 
108
- const db = getDb()
109
- const coordinator = db.query<any, []>(
110
- "SELECT provider_id, model_id FROM agents WHERE role = 'coordinator' LIMIT 1"
111
- ).get()
146
+ const defaultLLM = await getDefaultLLM()
147
+ if (!defaultLLM) throw new Error("No active LLM providers/models configured in the database")
112
148
 
113
- const providerCfg = await resolveProviderConfig(
114
- coordinator?.provider_id || "openai",
115
- coordinator?.model_id || "gpt-4o-mini"
116
- )
149
+ const providerCfg = await resolveProviderConfig(defaultLLM.provider, defaultLLM.model)
117
150
 
118
151
  const summaryResponse = await callLLM({
119
152
  ...providerCfg,
@@ -131,10 +164,22 @@ export async function compactThread(
131
164
  ],
132
165
  })
133
166
 
167
+ // A failed provider call still returns a populated `content` (the error text),
168
+ // so this has to gate on stop_reason — otherwise the summary that permanently
169
+ // replaces N messages of history becomes "[LLM Error] ...". Skipping leaves the
170
+ // thread uncompacted, which is recoverable; saving is not.
171
+ if (summaryResponse.stop_reason === "error") {
172
+ log.warn(
173
+ `[compaction] Summarizer call failed (${summaryResponse.error?.message ?? "unknown error"}) — `
174
+ + `keeping thread ${threadId} uncompacted rather than saving the error as its summary`
175
+ )
176
+ return
177
+ }
178
+
134
179
  const summary = summaryResponse.content.trim()
135
180
  if (!summary) return
136
181
 
137
- saveSummary(threadId, summary, toSummarize.length, lastSummarizedId)
182
+ await saveSummary(threadId, summary, toSummarize.length, lastSummarizedId)
138
183
  log.info(
139
184
  `[compaction] Thread ${threadId} compacted: ${toSummarize.length} msgs → ${estimateTokens(summary)} tokens`
140
185
  )
@@ -142,7 +187,7 @@ export async function compactThread(
142
187
  // Notify user in their active channel (non-critical)
143
188
  if (notify?.channel && notify?.userId) {
144
189
  try {
145
- const { sendToUserChannel } = await import("../gateway/channel-notify.ts")
190
+ const { sendToUserChannel } = await import("../gateway/channel-notify")
146
191
  await sendToUserChannel(
147
192
  notify.channel,
148
193
  notify.userId,
@@ -197,25 +242,3 @@ export function clearOldToolResults<T extends { role: string; content: string |
197
242
  })
198
243
  }
199
244
 
200
- /**
201
- * Summarize a tool result to a single line
202
- * Used for very old tool results (> 10 turns)
203
- */
204
- export function summarizeToolResult(content: string, toolName?: string): string {
205
- // Try to extract success/failure status
206
- const isError = content.includes('error') || content.includes('failed') || content.startsWith('[Tool Error]')
207
- const isSuccess = content.includes('ok') || content.includes('success') || content.includes('true')
208
-
209
- // Try to extract key result field from JSON/TOON
210
- let keyInfo = ""
211
- try {
212
- // Simple extraction of first key value
213
- const firstLine = content.split('\n')[0].substring(0, 80)
214
- keyInfo = firstLine
215
- } catch {
216
- keyInfo = content.substring(0, 80)
217
- }
218
-
219
- const status = isError ? "failed" : isSuccess ? "success" : "completed"
220
- return `[${toolName || 'Tool'} ${status}: ${keyInfo}...]`
221
- }