@johpaz/hive-sdk 0.1.4 → 0.1.6

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 (278) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +78 -23
  3. package/bun.lock +55 -29
  4. package/bunfig.toml +4 -2
  5. package/docs/API-AGENTS.md +78 -27
  6. package/docs/API-CONTEXT-COMPILER.md +31 -34
  7. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  8. package/docs/HIVE-HARNESS.md +1 -1
  9. package/docs/INDEX.md +4 -4
  10. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  11. package/package.json +17 -12
  12. package/packages/cli/package.json +2 -2
  13. package/packages/cli/src/commands/create-app.test.ts +36 -7
  14. package/packages/cli/src/commands/init.ts +3 -3
  15. package/packages/cli/src/commands/run.ts +1 -1
  16. package/packages/cli/src/commands/test.ts +37 -25
  17. package/packages/cli/src/commands/trace.ts +30 -28
  18. package/packages/cli/templates/hive-app/.env.example +10 -2
  19. package/packages/cli/templates/hive-app/README.md +103 -0
  20. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  21. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  22. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  23. package/packages/core/package.json +13 -12
  24. package/packages/core/src/agent/acceptance-checks.ts +172 -0
  25. package/packages/core/src/agent/agent-catalog.ts +348 -0
  26. package/packages/core/src/agent/agent-loop.ts +1373 -0
  27. package/packages/core/src/agent/capability-search.ts +186 -0
  28. package/packages/core/src/agent/catalog-selector.ts +103 -0
  29. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  30. package/packages/core/src/agent/context-compiler.ts +689 -0
  31. package/packages/core/src/agent/conversation-store.ts +381 -0
  32. package/packages/core/src/agent/curator.ts +276 -0
  33. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  34. package/packages/core/src/agent/goal-runner.ts +323 -0
  35. package/packages/core/src/agent/index.ts +17 -12
  36. package/packages/core/src/agent/llm-client.ts +266 -0
  37. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  38. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  39. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  40. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  41. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  42. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  43. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  44. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  45. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  47. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  48. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  49. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  50. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  51. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  52. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  54. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  55. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  56. package/packages/core/src/agent/playbook-selector.ts +119 -0
  57. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  58. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  59. package/packages/core/src/agent/providers/index.ts +35 -16
  60. package/packages/core/src/agent/reflector.ts +320 -0
  61. package/packages/core/src/agent/routing-intent.ts +22 -0
  62. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  63. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  64. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  65. package/packages/core/src/agent/skill-selector.ts +374 -0
  66. package/packages/core/src/agent/stuck-loop.ts +209 -0
  67. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  68. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  69. package/packages/core/src/api/createAgent.test.ts +139 -27
  70. package/packages/core/src/api/createAgent.ts +232 -44
  71. package/packages/core/src/artifacts/store.ts +162 -0
  72. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  73. package/packages/core/src/canvas/canvas.test.ts +8 -4
  74. package/packages/core/src/canvas/emitter.ts +131 -80
  75. package/packages/core/src/canvas/index.ts +1 -3
  76. package/packages/core/src/channels/base.ts +9 -1
  77. package/packages/core/src/channels/discord.ts +5 -4
  78. package/packages/core/src/channels/manager.ts +122 -30
  79. package/packages/core/src/channels/slack.ts +5 -4
  80. package/packages/core/src/channels/telegram.ts +36 -6
  81. package/packages/core/src/channels/webchat.ts +11 -10
  82. package/packages/core/src/channels/whatsapp.ts +23 -7
  83. package/packages/core/src/config/index.ts +13 -2
  84. package/packages/core/src/config/loader.ts +76 -29
  85. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  86. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  87. package/packages/core/src/events/agent-bus.ts +44 -68
  88. package/packages/core/src/events/channel-narration.ts +150 -0
  89. package/packages/core/src/events/narration.ts +82 -0
  90. package/packages/core/src/events/tool-narration.ts +62 -0
  91. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  92. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  93. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  94. package/packages/core/src/gateway/lane-queue.ts +173 -0
  95. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  96. package/packages/core/src/gateway/server.ts +1 -1
  97. package/packages/core/src/harness/index.ts +46 -27
  98. package/packages/core/src/index.ts +33 -27
  99. package/packages/core/src/mcp/hot-reload.ts +32 -23
  100. package/packages/core/src/mcp/index.ts +6 -3
  101. package/packages/core/src/mcp/singleton.ts +1 -4
  102. package/packages/core/src/mcp/tool-sync.ts +138 -0
  103. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  104. package/packages/core/src/memory/Scratchpad.ts +27 -34
  105. package/packages/core/src/multimodal/vision-service.ts +44 -38
  106. package/packages/core/src/resilience/retry.ts +95 -0
  107. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  108. package/packages/core/src/scheduler/index.ts +9 -7
  109. package/packages/core/src/scheduler/integration.ts +46 -26
  110. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  111. package/packages/core/src/scheduler/types.ts +7 -2
  112. package/packages/core/src/security/Pairing.ts +1 -1
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  115. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  116. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  117. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  118. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  119. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  120. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  121. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  122. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  123. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  124. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  125. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  128. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  129. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  130. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  131. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  132. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  133. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  134. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  135. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  137. package/packages/core/src/skills/skills.test.ts +52 -11
  138. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  139. package/packages/core/src/storage/bootstrap.ts +151 -0
  140. package/packages/core/src/storage/causal-events.ts +84 -0
  141. package/packages/core/src/storage/collections.ts +680 -0
  142. package/packages/core/src/storage/crypto.ts +205 -74
  143. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  144. package/packages/core/src/storage/hivedb.ts +61 -0
  145. package/packages/core/src/storage/index.ts +111 -18
  146. package/packages/core/src/storage/model-id.ts +53 -0
  147. package/packages/core/src/storage/onboarding.ts +540 -972
  148. package/packages/core/src/storage/reconcile.ts +238 -0
  149. package/packages/core/src/storage/seed.ts +572 -406
  150. package/packages/core/src/storage/usage.ts +285 -225
  151. package/packages/core/src/storage/user-email.ts +11 -0
  152. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  153. package/packages/core/src/swarm/EventBridge.ts +1 -1
  154. package/packages/core/src/swarm/index.ts +12 -9
  155. package/packages/core/src/tool-runtime/index.ts +146 -23
  156. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  157. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  158. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  159. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  160. package/packages/core/src/tools/agents/index.ts +784 -292
  161. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  162. package/packages/core/src/tools/api/api-request.ts +174 -0
  163. package/packages/core/src/tools/api/index.ts +16 -0
  164. package/packages/core/src/tools/cli/index.ts +4 -0
  165. package/packages/core/src/tools/core/index.ts +281 -112
  166. package/packages/core/src/tools/cron/index.ts +121 -124
  167. package/packages/core/src/tools/index.ts +63 -78
  168. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  169. package/packages/core/src/tools/types.ts +3 -1
  170. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  171. package/packages/core/src/tools/web/browser-backend.ts +129 -0
  172. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  173. package/packages/core/src/tools/web/browser-service.ts +80 -35
  174. package/packages/core/src/tools/web/browser-type.ts +3 -8
  175. package/packages/core/src/tools/web/index.ts +4 -4
  176. package/packages/core/src/tools/web/webview-backend.ts +412 -0
  177. package/packages/core/src/voice/index.ts +89 -63
  178. package/packages/core/src/workers/agent.worker.ts +2 -2
  179. package/packages/core/src/workers/workers.test.ts +3 -10
  180. package/scripts/bump-version.ts +248 -0
  181. package/scripts/generate-skill-bundle.ts +108 -0
  182. package/test/acceptance-checks.test.ts +403 -0
  183. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  184. package/test/browser-backend.test.ts +308 -0
  185. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  186. package/test/causal-events.test.ts +117 -0
  187. package/test/compaction.test.ts +105 -0
  188. package/test/context-compiler.test.ts +269 -0
  189. package/test/curator.test.ts +130 -0
  190. package/test/durable-queue.test.ts +114 -0
  191. package/test/harness-barrel.test.ts +64 -0
  192. package/test/hive-helpers.test.ts +130 -0
  193. package/test/hivedb-search.test.ts +189 -0
  194. package/test/internal-turns.test.ts +166 -0
  195. package/test/job-idempotency.test.ts +68 -0
  196. package/test/job-retry-backoff.test.ts +184 -0
  197. package/test/job-store.test.ts +381 -0
  198. package/test/llm-retry.test.ts +97 -0
  199. package/test/memory-perf.test.ts +774 -0
  200. package/test/minimal-loadout.test.ts +78 -0
  201. package/test/model-catalog.test.ts +105 -0
  202. package/test/preload.ts +12 -0
  203. package/test/reflector.test.ts +320 -0
  204. package/test/retention-cap.test.ts +91 -0
  205. package/test/retired-capabilities-pruned.test.ts +192 -0
  206. package/test/run-store.test.ts +355 -0
  207. package/test/scratchpad.test.ts +74 -0
  208. package/test/secrets-durability.test.ts +119 -0
  209. package/test/seed-model-reseed.test.ts +155 -0
  210. package/test/setup-agent-seed.test.ts +264 -0
  211. package/test/tool-inventory.test.ts +65 -0
  212. package/test/tool-runtime.test.ts +258 -0
  213. package/test/tool-selector-runtime-tools.test.ts +117 -0
  214. package/test/toon.test.ts +429 -0
  215. package/tsconfig.json +2 -0
  216. package/packages/core/src/ace/Curator.ts +0 -158
  217. package/packages/core/src/ace/Reflector.ts +0 -200
  218. package/packages/core/src/ace/index.ts +0 -4
  219. package/packages/core/src/agent/AgentRunner.ts +0 -711
  220. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  221. package/packages/core/src/agent/ContextGuard.ts +0 -91
  222. package/packages/core/src/agent/ConversationStore.ts +0 -254
  223. package/packages/core/src/agent/Hooks.ts +0 -166
  224. package/packages/core/src/agent/StuckLoop.ts +0 -133
  225. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  226. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  227. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  228. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  229. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  230. package/packages/core/src/agent/selectors/index.ts +0 -6
  231. package/packages/core/src/auth/auth.ts +0 -121
  232. package/packages/core/src/auth/index.ts +0 -1
  233. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  234. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  235. package/packages/core/src/harness/collections.ts +0 -98
  236. package/packages/core/src/harness/goal-verifier.ts +0 -141
  237. package/packages/core/src/harness/harness.test.ts +0 -236
  238. package/packages/core/src/harness/reconcile.ts +0 -149
  239. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  240. package/packages/core/src/multimodal/VisionService.ts +0 -293
  241. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  242. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  243. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  244. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  245. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  246. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  247. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  248. package/packages/core/src/scheduler/dag/index.ts +0 -26
  249. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  250. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  251. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  252. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  253. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  254. package/packages/core/src/storage/hiveSeed.ts +0 -308
  255. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  256. package/packages/core/src/storage/schema.ts +0 -689
  257. package/packages/core/src/storage/storage.test.ts +0 -37
  258. package/packages/core/src/swarm/AgentBus.ts +0 -460
  259. package/packages/core/src/swarm/EventBus.ts +0 -169
  260. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  261. package/packages/core/src/tools/bridge-events.ts +0 -26
  262. package/packages/core/src/tools/canvas/index.ts +0 -375
  263. package/packages/core/src/tools/codebridge/index.ts +0 -342
  264. package/packages/core/src/tools/meeting/index.ts +0 -353
  265. package/packages/core/src/tools/projects/index.ts +0 -37
  266. package/packages/core/src/tools/projects/project-create.ts +0 -94
  267. package/packages/core/src/tools/projects/project-done.ts +0 -66
  268. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  269. package/packages/core/src/tools/projects/project-list.ts +0 -96
  270. package/packages/core/src/tools/projects/project-update.ts +0 -72
  271. package/packages/core/src/tools/projects/task-create.ts +0 -68
  272. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  273. package/packages/core/src/tools/projects/task-update.ts +0 -93
  274. package/packages/core/src/tools/voice/index.ts +0 -104
  275. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  276. package/packages/core/src/tools/web/api-request.ts +0 -239
  277. package/test/setup-db.ts +0 -216
  278. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * HiveDB-based Dynamic Skill Selector Module
3
+ *
4
+ * Context Compiler Level 4 - Intelligent Skill Selection
5
+ *
6
+ * This module uses HiveDB BM25 scoring (Spanish stemming + accent folding)
7
+ * to select the most relevant skills based on the user message, similar to
8
+ * tool selection.
9
+ *
10
+ * DESIGN DECISIONS:
11
+ *
12
+ * 1. Reads from skills table in database (not hardcoded catalog)
13
+ * 2. Maximum skills per turn for balanced context injection
14
+ * 3. Explicit triggers checked first; search is the semantic fallback
15
+ * 4. Relative score cutoff (fraction of the top hit), never absolute
16
+ * 5. Returns skill content for injection into system prompt
17
+ */
18
+
19
+ import { col } from "../storage/hive"
20
+ import type { SkillDoc } from "../storage/collections"
21
+ import { logger } from "../utils/logger"
22
+ import { isMinimalSkill } from "./minimal-loadout"
23
+ import { isCalendarOperation } from "./routing-intent"
24
+ import {
25
+ searchCapabilities,
26
+ applyRelativeCutoff,
27
+ replaceCapabilityDocs,
28
+ type CapabilityDoc,
29
+ } from "./capability-search"
30
+
31
+ const log = logger.child("skill-selector")
32
+
33
+
34
+
35
+ // ─── Types ───────────────────────────────────────────────────────────────────────
36
+
37
+ export interface SkillDescriptor {
38
+ id: string
39
+ name: string
40
+ description: string
41
+ category: string
42
+ tools: string
43
+ triggers: string
44
+ preferred_agents: string
45
+ body: string
46
+ version: string
47
+ version_num: number
48
+ active: boolean
49
+ }
50
+
51
+ /**
52
+ * Every bundled SKILL.md self-titles right after its frontmatter (e.g.
53
+ * "# Canvas Report Skill\n\n## Cuándo se Activa...") for standalone
54
+ * readability — but every system-prompt injection site wraps the body with
55
+ * its own `## <skill.name>` header, so the leading H1 becomes a *higher*
56
+ * heading level nested under a lower one (malformed hierarchy) and repeats
57
+ * the same title twice, for free tokens on every turn. Strip it here, once,
58
+ * so the 29 source files keep their own title for anyone reading them
59
+ * standalone, while every injected copy stays clean.
60
+ */
61
+ function stripRedundantTitle(body: string): string {
62
+ // Bodies carry a leading blank line from the frontmatter's closing "---",
63
+ // so the H1 isn't at position 0 — allow leading whitespace before it.
64
+ return body.replace(/^\s*# .+\n+/, "")
65
+ }
66
+
67
+ function toSkillDescriptor(doc: SkillDoc): SkillDescriptor {
68
+ return {
69
+ id: doc.id, name: doc.name, description: doc.description ?? "", category: doc.category,
70
+ tools: doc.tools, triggers: doc.triggers, preferred_agents: doc.preferred_agents,
71
+ body: stripRedundantTitle(doc.body), version: doc.version, version_num: doc.version_num, active: doc.active,
72
+ }
73
+ }
74
+
75
+ export interface SelectedSkill {
76
+ id: string
77
+ name: string
78
+ score: number
79
+ category: string
80
+ description: string
81
+ body: string
82
+ }
83
+
84
+ export interface SkillSelectorResult {
85
+ skills: SkillDescriptor[]
86
+ selected: SelectedSkill[]
87
+ reasoning: string
88
+ timingMs: number
89
+ }
90
+
91
+ // ─── Configuration ─────────────────────────────────────────────────────────
92
+
93
+ /** Maximum skills to return per message */
94
+ const MAX_SKILLS_PER_TURN = 4 // Increased from 2 to allow more skills
95
+
96
+ /**
97
+ * Relative relevance cutoff: keep a hit only if it scores at least this
98
+ * fraction of the top hit. HiveDB BM25 scores are positive (higher = better)
99
+ * but corpus-dependent, so absolute thresholds don't transfer — a ratio does.
100
+ */
101
+ const RELEVANCE_RATIO = 0.3
102
+
103
+ /** Stopwords to filter out before BM25 query construction */
104
+ const STOPWORDS = new Set([
105
+ "que", "con", "para", "por", "una", "uno", "los", "las", "del",
106
+ "como", "esta", "esto", "ese", "eso", "the", "and", "for",
107
+ "with", "this", "that", "have", "will", "also", "de", "en",
108
+ "el", "la", "se", "su", "sus", "al", "es", "son", "pero",
109
+ "más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
110
+ "hola", "hi", "hello", "hey", "gracias", "thank", "please",
111
+ "ok", "okay", "yes", "si", "no", "bien", "good", "great",
112
+ "puedes", "necesito", "quiero", "podés", "necesitás", "querés",
113
+ ])
114
+
115
+ /** Conversational patterns that should return empty skill list */
116
+ const CONVERSATIONAL_PATTERNS = [
117
+ /^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
118
+ /^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
119
+ /^(cómo estás?|how are you?|qué流水|you doing|qué cuentas)/i,
120
+ /^(sí|yes|ok|okay|de acuerdo|perfecto|claro|por supuesto)/i,
121
+ /^(adiós|bye|nos vemos|see you|later|chau)/i,
122
+ /^(entiendo|understand|i see|ya veo|got it)/i,
123
+ /^(bien|good|great|excelente|awesome|perfect)/i,
124
+ /^(?:\?|¿)$/, // Just a question mark
125
+ ]
126
+
127
+ // ─── Helper Functions ───────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Check if message is purely conversational (no skills needed)
131
+ */
132
+ function isConversational(message: string): boolean {
133
+ const trimmed = message.trim()
134
+
135
+ // Empty or very short messages
136
+ if (trimmed.length < 2) return true
137
+
138
+ // Check conversational patterns
139
+ for (const pattern of CONVERSATIONAL_PATTERNS) {
140
+ if (pattern.test(trimmed)) {
141
+ log.debug(`[skill-selector] Message matched conversational pattern: ${pattern}`)
142
+ return true
143
+ }
144
+ }
145
+
146
+ // Check if all words are stopwords (likely conversational)
147
+ const words = trimmed.toLowerCase().split(/\s+/)
148
+ const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
149
+ if (meaningfulWords.length === 0) {
150
+ log.debug(`[skill-selector] All words are stopwords - conversational`)
151
+ return true
152
+ }
153
+
154
+ return false
155
+ }
156
+
157
+ /**
158
+ * Check if message matches explicit triggers from a skill
159
+ */
160
+ function matchTriggers(message: string, triggersJson: string | null): boolean {
161
+ if (!triggersJson) return false
162
+
163
+ try {
164
+ // Triggers are stored as comma-separated string in DB (e.g., "trigger1,trigger2")
165
+ const triggers: string[] = triggersJson.split(",").map(t => t.trim()).filter(t => t.length > 0)
166
+ if (triggers.length === 0) return false
167
+
168
+ const lowerMessage = message.toLowerCase()
169
+ return triggers.some(trigger =>
170
+ lowerMessage.includes(trigger.toLowerCase())
171
+ )
172
+ } catch (err) {
173
+ log.warn(`[skill-selector] Failed to parse triggers: ${(err as Error).message}`)
174
+ return false
175
+ }
176
+ }
177
+
178
+ // ─── Main Selection Function ─────────────────────────────────────────────────
179
+
180
+ /**
181
+ * Select skills for a given user message using hybrid matching:
182
+ * 1. First check explicit triggers (high confidence match)
183
+ * 2. Fallback to HiveDB BM25 scoring for semantic matching
184
+ *
185
+ * @param userMessage - The raw user message
186
+ * @returns Array of selected skills
187
+ *
188
+ * ALGORITHM:
189
+ * 1. If conversational → return []
190
+ * 2. Check explicit triggers from all enabled skills
191
+ * 3. If trigger match found → return matching skill immediately
192
+ * 4. Query the HiveDB capability index with the raw message
193
+ * 5. Keep hits scoring at least RELEVANCE_RATIO of the top hit
194
+ * 6. Hydrate skill details from HiveDB and return top MAX_SKILLS_PER_TURN
195
+ */
196
+ export async function selectSkills(userMessage: string): Promise<SkillDescriptor[]> {
197
+ const startTime = performance.now()
198
+
199
+ log.debug(`[skill-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
200
+
201
+ // Step 1: Check if conversational
202
+ if (isConversational(userMessage)) {
203
+ log.debug(`[skill-selector] Conversational message, returning empty array`)
204
+ return []
205
+ }
206
+
207
+ // Step 2: Check explicit triggers first (high priority)
208
+ const skillsCol = await col<SkillDoc>("skills")
209
+ const calendarOperation = isCalendarOperation(userMessage)
210
+ const allSkills = (await skillsCol.scan({}))
211
+ .filter(e => e.doc.active && !(calendarOperation && e.doc.category === "cron"))
212
+ .map(e => toSkillDescriptor(e.doc))
213
+
214
+ // Check trigger match - if found, return immediately with high confidence
215
+ for (const skill of allSkills) {
216
+ if (skill.triggers && matchTriggers(userMessage, skill.triggers)) {
217
+ log.info(`[skill-selector] Trigger match found: ${skill.name}`)
218
+ return [skill]
219
+ }
220
+ }
221
+
222
+ // Step 3: Semantic matching via the HiveDB capability index
223
+ let hits
224
+ try {
225
+ hits = await searchCapabilities(userMessage, { types: ["skill"], k: 20 })
226
+ } catch (err) {
227
+ log.error(`[skill-selector] Capability search failed:`, err)
228
+ return []
229
+ }
230
+
231
+ if (hits.length === 0) {
232
+ log.debug(`[skill-selector] No matches, returning empty array`)
233
+ return []
234
+ }
235
+
236
+ // Log raw scores for debugging
237
+ log.info(`[skill-selector] Raw scores: ${hits.slice(0, 10).map(h => `id=${h.rawId}, score=${h.score.toFixed(2)}`).join(", ")}`)
238
+
239
+ // Step 4: Keep only hits close enough to the best match
240
+ const relevantHits = applyRelativeCutoff(hits, RELEVANCE_RATIO)
241
+ if (relevantHits.length === 0) {
242
+ log.debug(`[skill-selector] All results below ratio cutoff, returning empty`)
243
+ return []
244
+ }
245
+
246
+ // Step 5: Hydrate full skill details from HiveDB (already loaded above)
247
+ const skillMap = new Map(allSkills.map(s => [s.id, s]))
248
+ const result: SkillDescriptor[] = []
249
+
250
+ for (const hit of relevantHits) {
251
+ const skill = skillMap.get(hit.rawId)
252
+ if (skill) {
253
+ result.push(skill)
254
+ if (result.length === MAX_SKILLS_PER_TURN) break
255
+ }
256
+ }
257
+
258
+ const timing = performance.now() - startTime
259
+
260
+ if (result.length > 0) {
261
+ log.info(`[skill-selector] Selected ${result.length} skills in ${timing.toFixed(2)}ms:`,
262
+ result.map(s => ({ name: s.name, category: s.category })))
263
+ } else {
264
+ log.debug(`[skill-selector] No skills selected, returning empty array in ${timing.toFixed(2)}ms`)
265
+ }
266
+
267
+ return result
268
+ }
269
+
270
+ // ─── Minimal Skills Loader ───────────────────────────────────────────────────
271
+
272
+ /**
273
+ * Load the skills that ride along with the minimal tool loadout.
274
+ *
275
+ * Derived, not listed: a skill qualifies when every tool it documents is in
276
+ * MINIMAL_TOOLS (see minimal-loadout.ts). Any other skill reaches the agent
277
+ * through discovery, which injects it together with the tools it teaches.
278
+ */
279
+ export async function getMinimalSkills(): Promise<SkillDescriptor[]> {
280
+ try {
281
+ const skillsCol = await col<SkillDoc>("skills")
282
+ const skills = (await skillsCol.scan({}))
283
+ .filter(e => e.doc.active && isMinimalSkill(e.doc.tools))
284
+ .map(e => toSkillDescriptor(e.doc))
285
+
286
+ log.info(`[skill-selector] Loaded ${skills.length} minimal skills: ${skills.map(s => s.name).join(", ")}`)
287
+ return skills
288
+ } catch (err) {
289
+ log.error(`[skill-selector] Failed to load minimal skills:`, err)
290
+ return []
291
+ }
292
+ }
293
+
294
+ // ─── Sync Skills to HiveDB ───────────────────────────────────────────────────
295
+
296
+ /**
297
+ * Sync all enabled skills from database to the HiveDB capability index.
298
+ * Should be called on initialization from gateway/initializer.ts.
299
+ * Replaces all `type=skill` documents atomically (delete-by-filter + one
300
+ * batch commit).
301
+ */
302
+ export async function syncSkillsToIndex(): Promise<void> {
303
+ try {
304
+ // Step 1: Get all enabled skills from database
305
+ const skillsCol = await col<SkillDoc>("skills")
306
+ const dbSkills = (await skillsCol.scan({})).map(e => e.doc).filter(s => s.active)
307
+
308
+ if (dbSkills.length === 0) {
309
+ log.debug(`[skill-selector] No skills found in DB to sync`)
310
+ }
311
+
312
+ // Step 2: Replace all skill documents in the HiveDB capability index.
313
+ // name (boost 4.0) = skill name; tags (3.0) = triggers + category +
314
+ // tools (the strongest semantic signals); body (2.0) = description +
315
+ // full skill body.
316
+ const docs: CapabilityDoc[] = dbSkills.map(skill => ({
317
+ type: "skill" as const,
318
+ rawId: skill.id,
319
+ name: skill.name,
320
+ tags: [skill.triggers, skill.category, skill.tools].filter(Boolean).join(" "),
321
+ body: [skill.description, skill.body].filter(Boolean).join("\n"),
322
+ }))
323
+
324
+ await replaceCapabilityDocs("skill", docs)
325
+
326
+ log.info(`[skill-selector] Sync complete: ${dbSkills.length} skills indexed in HiveDB`)
327
+
328
+ } catch (err) {
329
+ log.error(`[skill-selector] Skill index sync failed:`, err)
330
+ throw err // Re-throw to inform initializer
331
+ }
332
+ }
333
+ // ─── Debug/Test Helpers ─────────────────────────────────────────────────────
334
+
335
+ /**
336
+ * Get all enabled skills from database (for debugging/testing)
337
+ */
338
+ export async function getAllSkillsFromDB(): Promise<SkillDescriptor[]> {
339
+ try {
340
+ const skillsCol = await col<SkillDoc>("skills")
341
+ return (await skillsCol.scan({})).filter(e => e.doc.active).map(e => toSkillDescriptor(e.doc))
342
+ } catch (err) {
343
+ log.error(`[skill-selector] Failed to fetch skills:`, err)
344
+ return []
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Get skill by name
350
+ */
351
+ export async function getSkillByName(name: string): Promise<SkillDescriptor | undefined> {
352
+ try {
353
+ const skillsCol = await col<SkillDoc>("skills")
354
+ const entry = await skillsCol.get(name)
355
+ return entry && entry.doc.active ? toSkillDescriptor(entry.doc) : undefined
356
+ } catch (err) {
357
+ log.error(`[skill-selector] Failed to fetch skill by name:`, err)
358
+ return undefined
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Get skills by category
364
+ */
365
+ export async function getSkillsByCategory(category: string): Promise<SkillDescriptor[]> {
366
+ try {
367
+ const skillsCol = await col<SkillDoc>("skills")
368
+ const entries = await skillsCol.findBy("category", category)
369
+ return entries.filter(e => e.doc.active).map(e => toSkillDescriptor(e.doc))
370
+ } catch (err) {
371
+ log.error(`[skill-selector] Failed to fetch skills by category:`, err)
372
+ return []
373
+ }
374
+ }
@@ -0,0 +1,209 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+ import { hashObject } from "../utils/crypto.ts";
4
+
5
+ interface ToolCallRecord {
6
+ toolName: string;
7
+ argsHash: string;
8
+ errorMessage?: string;
9
+ timestamp: number;
10
+ }
11
+
12
+ interface ProgressRecord {
13
+ hash: string;
14
+ timestamp: number;
15
+ }
16
+
17
+ export interface StuckLoopState {
18
+ detected: boolean;
19
+ toolName: string;
20
+ count: number;
21
+ lastError?: string;
22
+ kind: "loop" | "stall";
23
+ }
24
+
25
+ export class StuckLoopDetector {
26
+ private log = logger.child("stuck-loop");
27
+ private history: Map<string, ToolCallRecord[]> = new Map();
28
+ private progressHistory: Map<string, ProgressRecord[]> = new Map();
29
+ private readonly maxHistoryPerSession = 50;
30
+ private readonly triggerThreshold = 3;
31
+ private readonly progressThreshold = 3;
32
+
33
+ constructor(_config: Config) {}
34
+
35
+ recordToolCall(
36
+ sessionId: string,
37
+ toolName: string,
38
+ args: Record<string, unknown>,
39
+ error?: string
40
+ ): void {
41
+ let sessionHistory = this.history.get(sessionId);
42
+ if (!sessionHistory) {
43
+ sessionHistory = [];
44
+ this.history.set(sessionId, sessionHistory);
45
+ }
46
+
47
+ const record: ToolCallRecord = {
48
+ toolName,
49
+ argsHash: hashObject(args),
50
+ errorMessage: error,
51
+ timestamp: Date.now(),
52
+ };
53
+
54
+ sessionHistory.push(record);
55
+
56
+ if (sessionHistory.length > this.maxHistoryPerSession) {
57
+ sessionHistory.shift();
58
+ }
59
+
60
+ this.log.debug(`Recorded tool call: ${toolName} for session ${sessionId}`);
61
+ }
62
+
63
+ check(sessionId: string): StuckLoopState {
64
+ const sessionHistory = this.history.get(sessionId) ?? [];
65
+
66
+ if (sessionHistory.length < this.triggerThreshold) {
67
+ return { detected: false, toolName: "", count: 0, kind: "loop" };
68
+ }
69
+
70
+ const recent = sessionHistory.slice(-10);
71
+ const counts = new Map<string, { count: number; error?: string }>();
72
+
73
+ for (const record of recent) {
74
+ const key = `${record.toolName}:${record.argsHash}`;
75
+ const existing = counts.get(key);
76
+
77
+ if (existing) {
78
+ existing.count++;
79
+ if (record.errorMessage) {
80
+ existing.error = record.errorMessage;
81
+ }
82
+ } else {
83
+ counts.set(key, { count: 1, error: record.errorMessage });
84
+ }
85
+ }
86
+
87
+ for (const [key, data] of counts) {
88
+ if (data.count >= this.triggerThreshold && data.error) {
89
+ const toolName = key.split(":")[0] ?? "unknown";
90
+
91
+ this.log.warn(`Stuck loop detected: ${toolName} called ${data.count} times with same args and error`);
92
+
93
+ return {
94
+ detected: true,
95
+ toolName,
96
+ count: data.count,
97
+ lastError: data.error,
98
+ kind: "loop",
99
+ };
100
+ }
101
+ }
102
+
103
+ return { detected: false, toolName: "", count: 0, kind: "loop" };
104
+ }
105
+
106
+ /**
107
+ * Record a progress snapshot hash. Use this to detect when the agent is not
108
+ * advancing even though no tool errors are occurring.
109
+ */
110
+ recordProgress(sessionId: string, progressHash: string): void {
111
+ let sessionProgress = this.progressHistory.get(sessionId);
112
+ if (!sessionProgress) {
113
+ sessionProgress = [];
114
+ this.progressHistory.set(sessionId, sessionProgress);
115
+ }
116
+
117
+ sessionProgress.push({ hash: progressHash, timestamp: Date.now() });
118
+
119
+ if (sessionProgress.length > this.maxHistoryPerSession) {
120
+ sessionProgress.shift();
121
+ }
122
+
123
+ this.log.debug(`Recorded progress hash for session ${sessionId}: ${progressHash}`);
124
+ }
125
+
126
+ /**
127
+ * Detect lack of progress when the same hash appears repeatedly.
128
+ */
129
+ checkProgress(sessionId: string, threshold?: number): StuckLoopState {
130
+ const sessionProgress = this.progressHistory.get(sessionId) ?? [];
131
+ const required = threshold ?? this.progressThreshold;
132
+
133
+ if (sessionProgress.length < required) {
134
+ return { detected: false, toolName: "", count: 0, kind: "stall" };
135
+ }
136
+
137
+ const recent = sessionProgress.slice(-required);
138
+ const firstHash = recent[0]?.hash;
139
+ const allSame = recent.every((r) => r.hash === firstHash);
140
+
141
+ if (allSame && firstHash) {
142
+ this.log.warn(`Stall detected: no progress for ${required} checks (hash ${firstHash})`);
143
+ return {
144
+ detected: true,
145
+ toolName: "NO_PROGRESS",
146
+ count: required,
147
+ kind: "stall",
148
+ };
149
+ }
150
+
151
+ return { detected: false, toolName: "", count: 0, kind: "stall" };
152
+ }
153
+
154
+ clear(sessionId: string): void {
155
+ this.history.delete(sessionId);
156
+ this.progressHistory.delete(sessionId);
157
+ this.log.debug(`Cleared stuck loop history for session ${sessionId}`);
158
+ }
159
+
160
+ prune(maxAgeMs: number = 30 * 60 * 1000): number {
161
+ const now = Date.now();
162
+ let pruned = 0;
163
+
164
+ for (const [sessionId, history] of this.history) {
165
+ const filtered = history.filter((r) => now - r.timestamp < maxAgeMs);
166
+
167
+ if (filtered.length === 0) {
168
+ this.history.delete(sessionId);
169
+ pruned++;
170
+ } else if (filtered.length !== history.length) {
171
+ this.history.set(sessionId, filtered);
172
+ }
173
+ }
174
+
175
+ for (const [sessionId, history] of this.progressHistory) {
176
+ const filtered = history.filter((r) => now - r.timestamp < maxAgeMs);
177
+
178
+ if (filtered.length === 0) {
179
+ this.progressHistory.delete(sessionId);
180
+ pruned++;
181
+ } else if (filtered.length !== history.length) {
182
+ this.progressHistory.set(sessionId, filtered);
183
+ }
184
+ }
185
+
186
+ return pruned;
187
+ }
188
+ }
189
+
190
+ export function createStuckLoopDetector(config: Config): StuckLoopDetector {
191
+ return new StuckLoopDetector(config);
192
+ }
193
+
194
+ /**
195
+ * Build a human-readable intervention message for the model.
196
+ */
197
+ export function getInterventionMessage(state: StuckLoopState): string {
198
+ if (!state.detected) return "";
199
+
200
+ if (state.kind === "stall") {
201
+ return `WARNING: Has estado sin avanzar durante ${state.count} ciclos consecutivos. Revisa tu plan, intenta una herramienta diferente o pide aclaración al usuario en lugar de repetir la misma secuencia.`;
202
+ }
203
+
204
+ if (state.count >= 4) {
205
+ return `CRITICAL: Has llamado ${state.toolName} ${state.count} veces con los mismos argumentos y sigue fallando con: "${state.lastError}". El usuario será notificado. Debes cambiar completamente de estrategia o pedir ayuda.`;
206
+ }
207
+
208
+ return `WARNING: Has llamado ${state.toolName} ${state.count} veces con los mismos argumentos y sigue fallando. Debes probar un enfoque diferente en lugar de repetir la misma acción.`;
209
+ }