@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,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
+ }