@johpaz/hive-sdk 0.0.12 → 0.0.15

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 (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. package/src/voice.ts +0 -1
@@ -0,0 +1,577 @@
1
+ /**
2
+ * FTS5-based Dynamic Tool Selector Module
3
+ *
4
+ * Context Compiler Level 3 - Intelligent Tool Selection
5
+ *
6
+ * This module intercepts each message BEFORE calling the LLM and uses
7
+ * SQLite FTS5 bm25() scoring to select the most relevant tools (0-4).
8
+ *
9
+ * DESIGN DECISIONS:
10
+ *
11
+ * 1. Stateless: No memory between turns - each message is evaluated independently.
12
+ * Rationale: Prevents cascade effects where a bad selection in one turn affects
13
+ * future turns. Forces fresh evaluation each time.
14
+ *
15
+ * 2. Maximum 4 tools per turn: Keeps token count low and prevents overwhelming
16
+ * the LLM with irrelevant tools. Forces prioritization.
17
+ *
18
+ * 3. Relevance threshold: If highest bm25 score < MIN_RELEVANCE_THRESHOLD,
19
+ * the message is considered conversational and returns empty array.
20
+ * Rationale: Prevents false positives on generic messages like "hola" or
21
+ * "cómo estás?" which should not trigger any tools.
22
+ *
23
+ * 4. Atomic over orchestration: When ambiguous, prefer individual tools over
24
+ * compound/manager tools. Rationale: Atomic tools are more predictable and
25
+ * the LLM can combine them as needed.
26
+ *
27
+ * 5. Performance: Must complete in under 50ms. FTS5 queries are typically
28
+ * <5ms for small tool catalogs (<100 tools).
29
+ *
30
+ * 6. Tool categorization: Tools are categorized by semantic domain:
31
+ * - scheduling (cron tools)
32
+ * - projects (project/task management)
33
+ * - filesystem (file operations)
34
+ * - web (search/fetch)
35
+ * - browser (browser automation)
36
+ * - memory (notes, memory operations)
37
+ * - code (exec, terminal)
38
+ * - canvas (UI rendering)
39
+ * - agents (agent creation/management)
40
+ * - core (notify, report_progress, save_note)
41
+ */
42
+
43
+ import { getDb } from "../../storage/SQLiteStorage.ts"
44
+ import { logger } from "../../utils/logger.ts"
45
+
46
+ const log = logger.child("tool-selector")
47
+
48
+ // ─── Types ───────────────────────────────────────────────────────────────────────
49
+
50
+ export interface ToolDescriptor {
51
+ name: string
52
+ description: string
53
+ category: string
54
+ /** Abstraction level: atomic (single operation) vs orchestration (manages multiple) */
55
+ abstractionLevel?: "atomic" | "orchestration"
56
+ }
57
+
58
+ export interface SelectedTool {
59
+ name: string
60
+ score: number
61
+ category: string
62
+ }
63
+
64
+ export interface ToolSelectorResult {
65
+ tools: ToolDescriptor[]
66
+ selected: SelectedTool[]
67
+ reasoning: string
68
+ timingMs: number
69
+ }
70
+
71
+ // ─── Configuration ───────────────────────────────────────────────────────────
72
+
73
+ /** Maximum tools to return per message */
74
+ const MAX_TOOLS_PER_TURN = 12
75
+
76
+ /**
77
+ * Minimum bm25 score threshold. Below this = conversational, no tools needed.
78
+ *
79
+ * CRITICAL: bm25() returns NEGATIVE scores where closer to 0 = more relevant.
80
+ * - Score of -5 is MORE relevant than -20
81
+ * - We use -30 as threshold to filter noise while allowing valid matches
82
+ *
83
+ * Previous values: -25 (too strict), -100 (too permissive)
84
+ * New value: -30 (balanced filtering, FTS5 MATCH handles the heavy lifting)
85
+ */
86
+ export const MIN_RELEVANCE_THRESHOLD = -30
87
+
88
+ /** Stopwords to filter out before FTS5 query construction */
89
+ const STOPWORDS = new Set([
90
+ "que", "con", "para", "por", "una", "uno", "los", "las", "del",
91
+ "como", "esta", "esto", "ese", "eso", "the", "and", "for",
92
+ "with", "this", "that", "have", "will", "also", "de", "en",
93
+ "el", "la", "se", "su", "sus", "al", "es", "son", "pero",
94
+ "más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
95
+ "hola", "hi", "hello", "hey", "gracias", "thank", "please",
96
+ "ok", "okay", "yes", "si", "no", "bien", "good", "great",
97
+ ])
98
+
99
+ /** Conversational patterns that should return empty tool list */
100
+ const CONVERSATIONAL_PATTERNS = [
101
+ /^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
102
+ /^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
103
+ /^(cómo estás?|how are you?|qué流水|you doing|qué cuentas)/i,
104
+ /^(sí|yes|ok|okay|de acuerdo|perfecto|claro|por supuesto)/i,
105
+ /^(adiós|bye|nos vemos|see you|later|chau)/i,
106
+ /^(entiendo|understand|i see|ya veo|got it)/i,
107
+ /^(bien|good|great|excelente|awesome|perfect)/i,
108
+ /^(?:\?|¿)$/, // Just a question mark
109
+ ]
110
+
111
+ // ─── Tool Catalog ───────────────────────────────────────────────────────────
112
+ //
113
+ // These 47 tools are the core toolset. Each has:
114
+ // - name: unique identifier
115
+ // - description: what the tool does (used for FTS5 matching)
116
+ // - category: semantic domain for grouping
117
+ // - abstractionLevel: atomic (single operation) vs orchestration (manages multiple)
118
+ //
119
+ // The descriptions are enriched with Spanish/English keywords for better FTS5 matching.
120
+
121
+ export const CORE_TOOL_CATALOG: ToolDescriptor[] = [
122
+ // Cron tools (cron.*)
123
+ { name: "cron.create", description: "Create new cron job: recurring (cron expression) or one-shot (fire_at). Requires 'task' field with instruction for the agent. Spanish keywords: programar tarea, crear recordatorio, agendar, automatizar horario, tarea recurrente, recordatorio diario, una vez", category: "scheduling", abstractionLevel: "atomic" },
124
+ { name: "cron.list", description: "List all cron jobs with next execution times and status. Spanish keywords: ver tareas programadas, listar cronograma, próximas ejecuciones, tareas activas, recordatorios pendientes", category: "scheduling", abstractionLevel: "atomic" },
125
+ { name: "cron.update", description: "Update an existing cron job: change expression, task instruction, channel, time window, etc. Use cron.list first to get task_id. Spanish keywords: actualizar tarea, modificar cron, editar recordatorio, cambiar horario, actualizar programación", category: "scheduling", abstractionLevel: "atomic" },
126
+ { name: "cron.pause", description: "Pause a cron job temporarily without deleting it. Spanish keywords: pausar tarea programada, detener temporalmente, suspender recordatorio", category: "scheduling", abstractionLevel: "atomic" },
127
+ { name: "cron.resume", description: "Resume a previously paused cron job. Spanish keywords: reanudar tarea, continuar tarea pausada, activar recordatorio", category: "scheduling", abstractionLevel: "atomic" },
128
+ { name: "cron.delete", description: "Delete a cron job permanently. Spanish keywords: eliminar tarea programada, borrar recordatorio, cancelar tarea", category: "scheduling", abstractionLevel: "atomic" },
129
+ { name: "cron.trigger", description: "Manually trigger immediate execution of a cron job now. Spanish keywords: ejecutar tarea ahora, forzar ejecución, disparar manualmente", category: "scheduling", abstractionLevel: "atomic" },
130
+ { name: "cron.history", description: "Get execution history and run logs for a cron job. Spanish keywords: historial ejecuciones, logs tarea, cuándo corrió, registro de ejecuciones", category: "scheduling", abstractionLevel: "atomic" },
131
+
132
+ // Project management tools (high-level orchestration)
133
+ { name: "project_create", description: "Create project with tasks, start new project for complex multi-step work. Spanish keywords: crear proyecto, nuevo proyecto, iniciar trabajo, proyecto nuevo, comenzar proyecto", category: "projects", abstractionLevel: "orchestration" },
134
+ { name: "project_list", description: "List all projects with their status. Spanish keywords: listar proyectos, ver proyectos, historial proyectos, todos los proyectos", category: "projects", abstractionLevel: "atomic" },
135
+ { name: "project_update", description: "Update project progress, mark progress percentage and status changes. Spanish keywords: actualizar progreso, marcar avance, estado del proyecto, porcentaje completado", category: "projects", abstractionLevel: "atomic" },
136
+ { name: "project_done", description: "Mark project complete, close finished projects and archive results. Spanish keywords: proyecto terminado, cerrar proyecto, finalizar, proyecto completado, marcar como hecho", category: "projects", abstractionLevel: "atomic" },
137
+ { name: "project_fail", description: "Mark project failed, record failure reason and lessons learned. Spanish keywords: proyecto fallido, error, marcar como fallido, proyecto fracasado, fracaso", category: "projects", abstractionLevel: "atomic" },
138
+
139
+ // Task management (atomic)
140
+ { name: "task_create", description: "Add task to project, create subtasks and action items within projects. Spanish keywords: crear tarea, nueva tarea, agregar pendiente, agregar tarea, crear subtarea", category: "projects", abstractionLevel: "atomic" },
141
+ { name: "task_update", description: "Update task status, mark tasks as complete or in progress. Spanish keywords: actualizar tarea, cambiar estado, marcar completa, tarea completada, tarea en progreso", category: "projects", abstractionLevel: "atomic" },
142
+ { name: "task_evaluate", description: "Evaluate task result against acceptance criteria. Spanish keywords: evaluar tarea, validar resultado, criterios aceptación, verificar calidad", category: "projects", abstractionLevel: "atomic" },
143
+
144
+ // Code execution
145
+ { name: "cli_exec", description: "Execute shell commands, run bash scripts and system commands. Spanish keywords: ejecutar comando, terminal, línea de comandos, bash, script, comando del sistema", category: "cli", abstractionLevel: "atomic" },
146
+
147
+ // Web tools
148
+ { name: "web_search", description: "Search web for current information, find up-to-date news facts and research. Spanish keywords: buscar en internet, buscar web, información, noticias, investigación, buscar", category: "web", abstractionLevel: "atomic" },
149
+ { name: "web_fetch", description: "Fetch content from URL, download and extract content from web pages. Spanish keywords: obtener página, descargar web, extraer contenido, obtener contenido, página web", category: "web", abstractionLevel: "atomic" },
150
+
151
+ // Memory tools
152
+ { name: "memory_write", description: "Store in long-term memory, save information to persistent memory for later retrieval. Spanish keywords: guardar memoria, guardar información, recordar, guardar dato, memoria", category: "memory", abstractionLevel: "atomic" },
153
+ { name: "memory_read", description: "Retrieve from memory by title, fetch saved information using memory identifier. Spanish keywords: leer memoria, recuperar información, recordar, obtener dato, buscar memoria", category: "memory", abstractionLevel: "atomic" },
154
+ { name: "memory_list", description: "List all memory entries, show all saved memories and stored knowledge. Spanish keywords: listar memorias, ver memorias guardadas, todas las memorias, lista de memorias", category: "memory", abstractionLevel: "atomic" },
155
+ { name: "memory_search", description: "Search memory by content, find memories containing specific keywords. Spanish keywords: buscar en memoria, buscar información guardada, buscar en recuerdos", category: "memory", abstractionLevel: "atomic" },
156
+ { name: "memory_delete", description: "Delete memory entry, remove saved memory from long-term storage. Spanish keywords: borrar memoria, eliminar información guardada, borrar dato, eliminar memoria", category: "memory", abstractionLevel: "atomic" },
157
+
158
+ // Agent/worker management
159
+ { name: "agent_create", description: "Create specialized worker agent, spawn new agent for specific task execution. Spanish keywords: crear agente, nuevo agente, trabajador, crear worker, nuevo trabajador", category: "agents", abstractionLevel: "orchestration" },
160
+ { name: "agent_find", description: "Find existing worker agents, locate running or idle worker agents. Spanish keywords: buscar agente, encontrar trabajador, localizar, buscar worker, encontrar agente", category: "agents", abstractionLevel: "atomic" },
161
+ { name: "agent_archive", description: "Archive unnecessary worker, terminate and archive idle or completed agents. Spanish keywords: archivar agente, terminar agente, borrar trabajador, desactivar agente", category: "agents", abstractionLevel: "atomic" },
162
+
163
+ // Notes/persistence
164
+ { name: "save_note", description: "Save persistent note to scratchpad, write quick notes and reminders. Spanish keywords: guardar nota, escribir nota, recordatorio rápido, nota rápida, apuntar", category: "core", abstractionLevel: "atomic" },
165
+
166
+ // Notifications/reporting
167
+ { name: "notify", description: "Send system notification, alert user with message or alert. Spanish keywords: notificar, enviar notificación, alertar, aviso, alarma", category: "core", abstractionLevel: "atomic" },
168
+ { name: "report_progress", description: "Report progress to user, inform user of current status and completion. Spanish keywords: reportar progreso, informar estado, actualizar,报告进度, progreso", category: "core", abstractionLevel: "atomic" },
169
+
170
+ // Browser automation
171
+ { name: "browser_navigate", description: "Navigate to URL and get content, open web pages and extract information. Spanish keywords: navegar web, abrir página, ir a sitio, navegar, ir a página", category: "browser", abstractionLevel: "atomic" },
172
+ { name: "browser_screenshot", description: "Take webpage screenshot, capture visual snapshot of web page. Spanish keywords: captura de pantalla, screenshot, fotografiar página, imagen de página", category: "browser", abstractionLevel: "atomic" },
173
+ { name: "browser_click", description: "Click element on page, interact with buttons and links in browser. Spanish keywords: hacer clic, presionar botón, clickear, pulsar, botón", category: "browser", abstractionLevel: "atomic" },
174
+ { name: "browser_type", description: "Type into input field, fill forms and text inputs in browser. Spanish keywords: escribir en página, llenar formulario, introducir texto, completar formulario", category: "browser", abstractionLevel: "atomic" },
175
+ { name: "browser_extract", description: "Extract text, links, or structured data from page using CSS selectors or XPath. Spanish keywords: extraer datos, obtener información, scraping, selectores, xpath", category: "browser", abstractionLevel: "atomic" },
176
+ { name: "browser_script", description: "Execute arbitrary JavaScript in the browser page context and get the result. Spanish keywords: ejecutar javascript, script, código, función, evaluar, js en página", category: "browser", abstractionLevel: "atomic" },
177
+ { name: "browser_wait", description: "Wait for an element to appear or condition to be met on the page. Spanish keywords: esperar, wait, condición, elemento, selector, aguardar carga", category: "browser", abstractionLevel: "atomic" },
178
+
179
+ // Canvas/UI rendering tools
180
+ { name: "canvas_render", description: "Render component on canvas, display UI components and data visualizations. Spanish keywords: renderizar, mostrar en canvas, visualizar, mostrar componente, dibujar", category: "canvas", abstractionLevel: "atomic" },
181
+ { name: "canvas_ask", description: "Display form and wait for response, show interactive form and collect user input. Spanish keywords: mostrar formulario, pedir datos, solicitar información, formulario interactivo", category: "canvas", abstractionLevel: "atomic" },
182
+ { name: "canvas_clear", description: "Clear canvas for session, reset canvas display and start fresh. Spanish keywords: limpiar canvas, borrar pantalla, reiniciar, limpiar, borrar", category: "canvas", abstractionLevel: "atomic" },
183
+ { name: "canvas_show_card", description: "Display card with labeled items, show structured data in card format. Spanish keywords: mostrar tarjeta, visualizar datos, tarjeta de información, mostrar datos", category: "canvas", abstractionLevel: "atomic" },
184
+ { name: "canvas_show_progress", description: "Display progress bars, show progress indicators and completion status. Spanish keywords: mostrar progreso, barra de progreso, indicador de progreso, avance", category: "canvas", abstractionLevel: "atomic" },
185
+ { name: "canvas_show_list", description: "Display key-value list, show information in structured list format. Spanish keywords: mostrar lista, listar elementos, lista de valores, mostrar elementos", category: "canvas", abstractionLevel: "atomic" },
186
+ { name: "canvas_confirm", description: "Show confirmation dialog, request user confirmation for actions. Spanish keywords: confirmar, diálogo de confirmación, confirmar acción, validación", category: "canvas", abstractionLevel: "atomic" },
187
+
188
+ // A2UI v0.9 rich interactive surfaces
189
+ { name: "a2ui_create_surface", description: "Create A2UI v0.9 surface for rich interactive UIs with forms, dashboards, and workflows. Spanish keywords: crear superficie A2UI, iniciar UI interactiva, crear formulario rico, interfaz A2UI, crear surface", category: "a2ui", abstractionLevel: "orchestration" },
190
+ { name: "a2ui_update_components", description: "Send A2UI v0.9 components to an existing surface (Text, Button, TextField, Row, Column, Card, etc.). Spanish keywords: enviar componentes A2UI, actualizar UI, renderizar componentes, A2UI componentes, update components", category: "a2ui", abstractionLevel: "atomic" },
191
+ { name: "a2ui_update_data_model", description: "Update A2UI v0.9 surface data model with JSON Pointer for dynamic data binding. Spanish keywords: actualizar datos A2UI, poblar formulario, cambiar valores, data model A2UI, actualizar modelo de datos", category: "a2ui", abstractionLevel: "atomic" },
192
+ { name: "a2ui_delete_surface", description: "Delete A2UI v0.9 surface and remove it from the user's canvas. Spanish keywords: eliminar superficie A2UI, borrar UI, limpiar superficie A2UI, cerrar formulario, delete surface", category: "a2ui", abstractionLevel: "atomic" },
193
+
194
+ // CodeBridge (subagent process management)
195
+ { name: "codebridge_launch", description: "Launch subagent process, spawn new code bridge agent process. Spanish keywords: lanzar proceso, iniciar subagente, ejecutar código, nuevo proceso", category: "code", abstractionLevel: "orchestration" },
196
+ { name: "codebridge_status", description: "Get status of running subagents, check code bridge agent status. Spanish keywords: estado del proceso, verificar subagente, estado del worker, estado", category: "code", abstractionLevel: "atomic" },
197
+ { name: "codebridge_cancel", description: "Cancel running subagent, terminate code bridge agent process. Spanish keywords: cancelar proceso, terminar subagente, detener proceso, parar", category: "code", abstractionLevel: "atomic" },
198
+
199
+ // Voice tools
200
+ { name: "voice_transcribe", description: "Transcribe audio to text, convert speech to written text from audio files. Spanish keywords: transcribir audio, voz a texto, convertir audio, transcripción", category: "voice", abstractionLevel: "atomic" },
201
+ { name: "voice_speak", description: "Convert text to audio and play, synthesize speech from text. Spanish keywords: hablar, sintetizar voz, texto a voz, reproducir audio, voz", category: "voice", abstractionLevel: "atomic" },
202
+
203
+ // Filesystem tools
204
+ { name: "fs_read", description: "Read file content from workspace. Spanish keywords: leer archivo, ver contenido, abrir archivo, leer fichero, mostrar archivo", category: "filesystem", abstractionLevel: "atomic" },
205
+ { name: "fs_write", description: "Create or overwrite file in workspace. Spanish keywords: crear archivo, guardar archivo, escribir archivo, crear fichero, escribir fichero", category: "filesystem", abstractionLevel: "atomic" },
206
+ { name: "fs_edit", description: "Edit specific lines or sections of a file. Spanish keywords: editar archivo, modificar líneas, actualizar contenido, cambiar archivo", category: "filesystem", abstractionLevel: "atomic" },
207
+ { name: "fs_delete", description: "Delete file or directory. Spanish keywords: eliminar archivo, borrar archivo, borrar carpeta, eliminar fichero", category: "filesystem", abstractionLevel: "atomic" },
208
+ { name: "fs_list", description: "List files and directories. Spanish keywords: listar archivos, ver carpeta, explorar directorio, listar ficheros", category: "filesystem", abstractionLevel: "atomic" },
209
+ { name: "fs_glob", description: "Find files matching wildcard patterns. Spanish keywords: buscar archivos, patrón, encontrar archivos, buscar ficheros", category: "filesystem", abstractionLevel: "atomic" },
210
+ { name: "fs_exists", description: "Check if a file or directory exists. Spanish keywords: verificar archivo, comprobar, existe archivo, comprobar fichero", category: "filesystem", abstractionLevel: "atomic" },
211
+
212
+ // Agent delegation and communication
213
+ { name: "task_delegate", description: "Delegate general task to worker agent. Spanish keywords: delegar tarea, asignar worker, ejecutar por agente, encomendar tarea", category: "agents", abstractionLevel: "orchestration" },
214
+ { name: "task_delegate_code", description: "Delegate coding task to CLI subagent (Qwen, Claude Code, Gemini CLI). Spanish keywords: delegar código, subagente CLI, programación, codificar", category: "agents", abstractionLevel: "orchestration" },
215
+ { name: "task_status", description: "Get execution status of delegated tasks. Spanish keywords: estado tarea delegada, verificar progreso, consultar tarea, progreso delegado", category: "agents", abstractionLevel: "atomic" },
216
+ { name: "bus_publish", description: "Publish message to Agent Bus for worker-to-worker communication. Spanish keywords: publicar mensaje, comunicar workers, enviar bus, mensaje bus", category: "agents", abstractionLevel: "atomic" },
217
+ { name: "bus_read", description: "Read unread messages from Agent Bus. Spanish keywords: leer mensajes bus, recibir mensajes, verificar bus, mensajes workers", category: "agents", abstractionLevel: "atomic" },
218
+ { name: "project_updates", description: "Get recent status updates from workers in project. Spanish keywords: actualizaciones proyecto, estado workers, progreso equipo, noticias proyecto", category: "agents", abstractionLevel: "atomic" },
219
+ ]
220
+
221
+ // ─── Helper Functions ───────────────────────────────────────────────────────-
222
+
223
+ /**
224
+ * Check if message is purely conversational (no tools needed)
225
+ *
226
+ * Uses pattern matching for common conversational phrases.
227
+ * Also checks for very short messages that are likely greetings.
228
+ */
229
+ function isConversational(message: string): boolean {
230
+ log.info(`[tool-selector] Checking if message is conversational: "${message}"`)
231
+ const trimmed = message.trim()
232
+
233
+ // Empty or very short messages
234
+ if (trimmed.length < 2) return true
235
+
236
+ // Check conversational patterns
237
+ for (const pattern of CONVERSATIONAL_PATTERNS) {
238
+ if (pattern.test(trimmed)) {
239
+ log.debug(`[tool-selector] Message matched conversational pattern: ${pattern}`)
240
+ return true
241
+ }
242
+ }
243
+
244
+ // Check if all words are stopwords (likely conversational)
245
+ const words = trimmed.toLowerCase().split(/\s+/)
246
+ const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
247
+ if (meaningfulWords.length === 0) {
248
+ log.debug(`[tool-selector] All words are stopwords - conversational`)
249
+ return true
250
+ }
251
+
252
+ return false
253
+ }
254
+
255
+ /**
256
+ * Build FTS5 query from user message
257
+ *
258
+ * Strips stopwords, special characters, and limits to 8 keywords.
259
+ * Uses OR operator for flexible matching.
260
+ */
261
+ function buildFTSQuery(message: string): string {
262
+ log.info(`[tool-selector] Building FTS query from message: "${message}"`)
263
+ const words = message
264
+ .toLowerCase()
265
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
266
+ .split(/\s+/)
267
+ .filter((w) => w.length > 2 && !STOPWORDS.has(w))
268
+ .slice(0, 8)
269
+
270
+ if (words.length === 0) return ""
271
+
272
+ // Use prefix matching for better recall (e.g., "gener*" matches "generar", "generando", "generación")
273
+ return words.map(w => `${w}*`).join(" OR ")
274
+ }
275
+
276
+ /**
277
+ * Determine abstraction level preference
278
+ *
279
+ * Returns 'atomic' to prefer individual tools, 'orchestration' to prefer
280
+ * manager tools. Currently always prefers atomic for better control.
281
+ */
282
+ function getAbstractionPreference(): "atomic" | "orchestration" {
283
+ // Prefer atomic tools for more predictable behavior
284
+ return "atomic"
285
+ }
286
+
287
+ // ─── Main Selection Function ─────────────────────────────────────────────────
288
+
289
+ /**
290
+ * Select tools for a given user message using FTS5 bm25() scoring
291
+ *
292
+ * @param userMessage - The raw user message
293
+ * @param fullToolList - Full list of available tools (for validation/filtering)
294
+ * @returns Array of 0-4 selected tools with scores
295
+ *
296
+ * ALGORITHM:
297
+ * 1. If conversational → return []
298
+ * 2. Build FTS5 query from message keywords
299
+ * 3. Query tools_fts with bm25() scoring
300
+ * 4. Filter results below MIN_RELEVANCE_THRESHOLD
301
+ * 5. If ambiguous → prefer atomic over orchestration
302
+ * 6. Return top maxTools results (default: MAX_TOOLS_PER_TURN)
303
+ */
304
+ export function selectTools(
305
+ userMessage: string,
306
+ fullToolList: ToolDescriptor[] = CORE_TOOL_CATALOG,
307
+ maxTools: number = MAX_TOOLS_PER_TURN
308
+ ): ToolDescriptor[] {
309
+ const startTime = performance.now()
310
+
311
+ // Log incoming user message for debugging/validation
312
+ log.debug(`[tool-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
313
+
314
+ // Step 1: Check if conversational
315
+ if (isConversational(userMessage)) {
316
+ log.debug(`[tool-selector] Conversational message, returning empty array`)
317
+ return []
318
+ }
319
+
320
+ // Step 2: Build FTS5 query
321
+ const ftsQuery = buildFTSQuery(userMessage)
322
+ if (!ftsQuery) {
323
+ log.debug(`[tool-selector] No valid FTS query terms, returning empty array`)
324
+ return []
325
+ }
326
+
327
+ log.debug(`[tool-selector] FTS query: "${ftsQuery}"`)
328
+
329
+ // Step 3: Execute FTS5 query with bm25 scoring
330
+ const db = getDb()
331
+
332
+ // Use bm25() with column weights for relevance scoring
333
+ // FTS5 table columns: tool_name, name, description, category
334
+ // Weights: tool_name=1.0, name=5.0, description=3.0, category=1.0
335
+ // Higher weight on name (5.0) for exact tool name matching
336
+ // Get more initially (maxTools * 2) for filtering, then limit to maxTools
337
+ const ftsResults = db.query(`
338
+ SELECT tool_name, bm25(tools_fts, 1.0, 5.0, 3.0, 1.0) as bm25_score
339
+ FROM tools_fts
340
+ WHERE tools_fts MATCH ?
341
+ ORDER BY bm25_score ASC
342
+ LIMIT ?
343
+ `).all(ftsQuery, maxTools * 2) as { tool_name: string; bm25_score: number }[]
344
+
345
+ if (ftsResults.length === 0) {
346
+ log.debug(`[tool-selector] No FTS matches, returning empty array`)
347
+ return []
348
+ }
349
+
350
+ // Log raw scores for debugging
351
+ log.info(`[tool-selector] Raw FTS scores: ${ftsResults.slice(0, 10).map(r => `${r.tool_name}=${r.bm25_score.toFixed(2)}`).join(", ")}`)
352
+
353
+ // Step 4: Apply relevance threshold filter
354
+ // bm25() returns negative scores; threshold is -0.5 (loosened from typical -5)
355
+ const relevantResults = ftsResults.filter(r => r.bm25_score >= MIN_RELEVANCE_THRESHOLD)
356
+
357
+ if (relevantResults.length === 0) {
358
+ log.debug(`[tool-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
359
+ return []
360
+ }
361
+
362
+ // Step 5: Map to tool descriptors with additional metadata
363
+ const toolMap = new Map(fullToolList.map(t => [t.name, t]))
364
+
365
+ const scoredTools: SelectedTool[] = []
366
+
367
+ for (const result of relevantResults) {
368
+ const tool = toolMap.get(result.tool_name)
369
+ if (tool) {
370
+ scoredTools.push({
371
+ name: tool.name,
372
+ score: result.bm25_score,
373
+ category: tool.category,
374
+ })
375
+ }
376
+ }
377
+
378
+ // Step 6: Prefer atomic over orchestration when ambiguous
379
+ // If we have more than MAX_TOOLS_PER_TURN, prioritize by abstraction level
380
+ const abstractionPref = getAbstractionPreference()
381
+
382
+ if (scoredTools.length > MAX_TOOLS_PER_TURN) {
383
+ // Sort by score first, then by abstraction level preference
384
+ // CRITICAL FIX: bm25() returns NEGATIVE scores where closer to 0 = more relevant
385
+ // So we sort ASCENDING (a.score - b.score) to put -8.02 before -5.11
386
+ scoredTools.sort((a, b) => {
387
+ // First by score (ascending for bm25 - closer to 0 is better)
388
+ if (Math.abs(a.score - b.score) > 0.1) {
389
+ return a.score - b.score // ✅ Fixed: ascending for negative bm25 scores
390
+ }
391
+ // Then by abstraction preference (preferred type first)
392
+ const aTool = toolMap.get(a.name)
393
+ const bTool = toolMap.get(b.name)
394
+ const aLevel = aTool?.abstractionLevel ?? "atomic"
395
+ const bLevel = bTool?.abstractionLevel ?? "atomic"
396
+
397
+ if (abstractionPref === "atomic") {
398
+ return (aLevel === "atomic" ? -1 : 1)
399
+ } else {
400
+ return (aLevel === "orchestration" ? -1 : 1)
401
+ }
402
+ })
403
+ }
404
+
405
+ // Step 7: Take top N tools
406
+ const topTools = scoredTools.slice(0, maxTools)
407
+
408
+ // Step 8: Return as ToolDescriptor array
409
+ const result = topTools.map(t => toolMap.get(t.name)!).filter(Boolean)
410
+
411
+ const timing = performance.now() - startTime
412
+
413
+ // Log final selected tools with info level (important for tracking tool selection process)
414
+ if (result.length > 0) {
415
+ log.info(`[tool-selector] Selected ${result.length} tools in ${timing.toFixed(2)}ms:`,
416
+ result.map(t => ({ name: t.name, category: t.category })))
417
+ } else {
418
+ log.debug(`[tool-selector] No tools selected, returning empty array in ${timing.toFixed(2)}ms`)
419
+ }
420
+
421
+ return result
422
+ }
423
+
424
+ // ─── Sync Tools to FTS5 ─────────────────────────────────────────────────────
425
+
426
+ /**
427
+ * Sync tool catalog to FTS5 virtual table
428
+ *
429
+ * Called on initialization from gateway/initializer.ts to populate the FTS5 index.
430
+ * Assumes the tools_fts table already exists (created by CONTEXT_ENGINE_SCHEMA).
431
+ * Descriptions are enriched with bilingual keywords for better matching.
432
+ *
433
+ * @param tools - Optional array of tools to sync. If not provided, fetches from DB.
434
+ */
435
+ export async function syncToolCatalogToFTS(tools?: ToolDescriptor[]): Promise<void> {
436
+ const db = getDb()
437
+
438
+ try {
439
+ // Step 1: Build full catalog = CORE_TOOL_CATALOG + any tools in DB not already covered
440
+ // CORE_TOOL_CATALOG has bilingual keywords; DB tools may be dynamically registered
441
+ const catalogByName = new Map<string, ToolDescriptor>(
442
+ CORE_TOOL_CATALOG.map(t => [t.name, t])
443
+ )
444
+
445
+ // Merge in any tools from the DB that are missing from the static catalog
446
+ const dbTools = db.query("SELECT name, description, category FROM tools").all() as Array<{ name: string; description: string | null; category: string | null }>
447
+ for (const row of dbTools) {
448
+ if (!catalogByName.has(row.name)) {
449
+ catalogByName.set(row.name, {
450
+ name: row.name,
451
+ description: row.description ?? row.name,
452
+ category: (row.category ?? "core") as any,
453
+ abstractionLevel: "atomic",
454
+ })
455
+ }
456
+ }
457
+
458
+ // Also merge any explicitly passed tools (e.g. from initializer)
459
+ for (const t of (tools || [])) {
460
+ if (!catalogByName.has(t.name)) {
461
+ catalogByName.set(t.name, t)
462
+ }
463
+ }
464
+
465
+ const toolCatalog = Array.from(catalogByName.values())
466
+
467
+ // Step 2: Atomic transaction for FTS5 sync
468
+ // We use a transaction to ensure that if sync fails, we don't end up with an empty FTS table
469
+ const syncTransaction = db.transaction(() => {
470
+ // Verify table exists inside transaction (optional but safer)
471
+ const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='tools_fts'").get()
472
+ if (!tableCheck) {
473
+ throw new Error("tools_fts table does not exist!")
474
+ }
475
+
476
+ // A: Clear existing data
477
+ db.run("DELETE FROM tools_fts")
478
+
479
+ // B: Prepare insertion
480
+ const insert = db.prepare(`
481
+ INSERT INTO tools_fts(tool_name, name, description, category)
482
+ VALUES (?, ?, ?, ?)
483
+ `)
484
+
485
+ // C: Re-populate
486
+ for (const tool of toolCatalog) {
487
+ const enriched = enrichToolDescription(tool)
488
+ insert.run(tool.name, tool.name, enriched, tool.category)
489
+ }
490
+ })
491
+
492
+ // Execute transaction
493
+ syncTransaction()
494
+
495
+ log.info(`[tool-selector] Atomic sync complete: ${toolCatalog.length} tools indexed in FTS5`)
496
+
497
+ } catch (err) {
498
+ log.error(`[tool-selector] Transactional sync failed:`, err)
499
+ throw err // Re-throw to inform initializer
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Enrich tool description with category-specific keywords
505
+ *
506
+ * This improves FTS5 matching for both English and Spanish queries.
507
+ */
508
+ function enrichToolDescription(tool: ToolDescriptor): string {
509
+ const keywordsByCategory: Record<string, string> = {
510
+ scheduling: "programar recordatorio alarma cron schedule reminder task future tiempo",
511
+ projects: "proyecto tarea plan organizer milestone backlog sprint work",
512
+ filesystem: "archivo file leer escribir editar documento content source code",
513
+ web: "buscar internet google web search find information news research",
514
+ browser: "navegador browser click screenshot form automation web page UI",
515
+ memory: "recordar nota guardar memory store remember persist knowledge",
516
+ code: "code ejecutar run script bash shell terminal command devops",
517
+ canvas: "canvas diagram visualization graph node edge flow chart",
518
+ agents: "agente worker specialist create delegate hire team manager",
519
+ core: "notificar message alert notify communicate progress status",
520
+ voice: "voz audio transcribir speech speak sintetizar audio voice transcription",
521
+ }
522
+
523
+ const extra = keywordsByCategory[tool.category] ?? ""
524
+ return `${tool.description} ${extra}`
525
+ }
526
+
527
+ // ─── Initialization ─────────────────────────────────────────────────────────
528
+
529
+ /**
530
+ * Sanitize an MCP tool name to comply with LLM function-name rules.
531
+ *
532
+ * Gemini (and OpenAI) require: start with letter/underscore, only [a-zA-Z0-9_.-:], max 64 chars.
533
+ * Server names from the UI can contain spaces and special chars (e.g. "X antes twiter").
534
+ *
535
+ * Canonical format: `{safeServer}__{safeTool}` (double underscore as separator)
536
+ */
537
+ export function mcpToolFullName(serverName: string, toolName: string): string {
538
+ const safe = (s: string) => s.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_.\-:]/g, '_')
539
+ const full = `${safe(serverName)}__${safe(toolName)}`
540
+ // Ensure starts with letter/underscore and fits within 64 chars
541
+ const trimmed = full.length > 64 ? full.substring(0, 64) : full
542
+ return /^[a-zA-Z_]/.test(trimmed) ? trimmed : `_${trimmed}`.substring(0, 64)
543
+ }
544
+
545
+ /**
546
+ * Initialize the tool selector
547
+ *
548
+ * DEPRECATED: syncToolCatalogToFTS() is now called from gateway/initializer.ts
549
+ * This function is kept for backward compatibility but is no longer needed
550
+ */
551
+ export function initializeToolSelector(): void {
552
+ log.info(`[tool-selector] Initializing (deprecated - sync is done in gateway/initializer.ts)`)
553
+ // syncToolCatalogToFTS() - No longer needed here, done in gateway/initializer.ts
554
+ }
555
+
556
+ // ─── Debug/Test Helpers ─────────────────────────────────────────────────────
557
+
558
+ /**
559
+ * Get all tools (for debugging/testing)
560
+ */
561
+ export function getAllTools(): ToolDescriptor[] {
562
+ return [...CORE_TOOL_CATALOG]
563
+ }
564
+
565
+ /**
566
+ * Get tool by name
567
+ */
568
+ export function getToolByName(name: string): ToolDescriptor | undefined {
569
+ return CORE_TOOL_CATALOG.find(t => t.name === name)
570
+ }
571
+
572
+ /**
573
+ * Get tools by category
574
+ */
575
+ export function getToolsByCategory(category: string): ToolDescriptor[] {
576
+ return CORE_TOOL_CATALOG.filter(t => t.category === category)
577
+ }
@@ -0,0 +1,6 @@
1
+ export type { ToolDescriptor, SelectedTool, ToolSelectorResult } from "./ToolSelector.ts";
2
+ export { MIN_RELEVANCE_THRESHOLD, CORE_TOOL_CATALOG, selectTools, syncToolCatalogToFTS, mcpToolFullName, initializeToolSelector, getAllTools, getToolByName, getToolsByCategory } from "./ToolSelector.ts";
3
+ export type { SkillDescriptor, SelectedSkill, SkillSelectorResult } from "./SkillSelector.ts";
4
+ export { MINIMAL_SKILL_NAMES, selectSkills, getMinimalSkills, syncSkillsToFTS, initializeSkillSelector, getAllSkillsFromDB, getSkillByName, getSkillsByCategory } from "./SkillSelector.ts";
5
+ export type { PlaybookRule } from "./PlaybookSelector.ts";
6
+ export { selectPlaybookRules, syncPlaybookToFTS } from "./PlaybookSelector.ts";
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2
+ import { getDb, initializeDatabase, dbService } from "../storage/SQLiteStorage.ts";
3
+
4
+ describe("createAgent API", () => {
5
+ beforeAll(async () => {
6
+ await initializeDatabase();
7
+ });
8
+
9
+ afterAll(() => {
10
+ dbService.close();
11
+ });
12
+
13
+ it("defineTool and defineSkill exports are available", async () => {
14
+ const { defineTool } = await import("../tools/ToolRegistry.ts");
15
+ const { defineSkill } = await import("../skills/defineSkill.ts");
16
+
17
+ const tool = defineTool({
18
+ name: "greet",
19
+ description: "Says hello",
20
+ execute: async (args: { name: string }) => `Hello ${args.name}`,
21
+ });
22
+ expect(tool.name).toBe("greet");
23
+
24
+ const skill = defineSkill({
25
+ name: "greeting-skill",
26
+ description: "Greeting skill",
27
+ steps: [{ action: "greet", instruction: "Say hello" }],
28
+ });
29
+ expect(skill.name).toBe("greeting-skill");
30
+ });
31
+
32
+ it("createAgent builds an agent instance", async () => {
33
+ const { createAgent } = await import("./createAgent.ts");
34
+ const agent = await createAgent({
35
+ name: "test-agent",
36
+ provider: "openai",
37
+ model: "gpt-4o-mini",
38
+ });
39
+ expect(agent.name).toBe("test-agent");
40
+ expect(agent.config).toBeDefined();
41
+ });
42
+
43
+ it("ToolRegistry can be used standalone", async () => {
44
+ const { ToolRegistry } = await import("../tools/ToolRegistry.ts");
45
+ const reg = new ToolRegistry();
46
+ expect(reg.size()).toBe(0);
47
+ });
48
+ });