@johpaz/hive-sdk 0.0.14 → 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.
- package/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTS5-based Playbook Rules Selector (ACE Curator)
|
|
3
|
+
*
|
|
4
|
+
* This module allows the Context Compiler to inject relevant evolved rules
|
|
5
|
+
* into the agent prompt based on semantic relevance to the current message.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getDb } from "../../storage/SQLiteStorage.ts"
|
|
9
|
+
import { logger } from "../../utils/logger.ts"
|
|
10
|
+
|
|
11
|
+
const log = logger.child("playbook-selector")
|
|
12
|
+
|
|
13
|
+
// ─── Types ───────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface PlaybookRule {
|
|
16
|
+
id: number
|
|
17
|
+
rule: string
|
|
18
|
+
category: string
|
|
19
|
+
applicable_to?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Configuration ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Maximum rules to inject per context window */
|
|
25
|
+
const MAX_RULES_PER_TURN = 5
|
|
26
|
+
|
|
27
|
+
/** Minimum bm25 score threshold for rules */
|
|
28
|
+
const MIN_RELEVANCE_THRESHOLD = -10 // Relaxed for better matching
|
|
29
|
+
|
|
30
|
+
// ─── Selection Logic ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Select relevant rules from the Playbook based on semantic matching
|
|
34
|
+
*/
|
|
35
|
+
export function selectPlaybookRules(message: string): PlaybookRule[] {
|
|
36
|
+
const db = getDb()
|
|
37
|
+
const startTime = performance.now()
|
|
38
|
+
|
|
39
|
+
// Clean query — use prefix matching for consistency with skill-selector and tool-selector
|
|
40
|
+
const keywords = message
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
// Keep only letters, numbers, spaces (strips ALL FTS5 special syntax)
|
|
43
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
44
|
+
.split(/\s+/)
|
|
45
|
+
.filter(w => w.length > 3)
|
|
46
|
+
.slice(0, 5)
|
|
47
|
+
|
|
48
|
+
if (keywords.length === 0) return []
|
|
49
|
+
|
|
50
|
+
// Use prefix matching for better recall (e.g., "program*" matches "programar", "programación")
|
|
51
|
+
const ftsQuery = keywords.map(w => `${w}*`).join(" OR ")
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Query FTS table
|
|
55
|
+
const ftsResults = db.query(`
|
|
56
|
+
SELECT rowid, bm25(playbook_fts) as score
|
|
57
|
+
FROM playbook_fts
|
|
58
|
+
WHERE playbook_fts MATCH ?
|
|
59
|
+
ORDER BY score ASC
|
|
60
|
+
LIMIT ?
|
|
61
|
+
`).all(ftsQuery, MAX_RULES_PER_TURN) as Array<{ rowid: number; score: number }>
|
|
62
|
+
|
|
63
|
+
const relevantIds = ftsResults
|
|
64
|
+
.filter(r => r.score >= MIN_RELEVANCE_THRESHOLD)
|
|
65
|
+
.map(r => r.rowid)
|
|
66
|
+
|
|
67
|
+
if (relevantIds.length === 0) return []
|
|
68
|
+
|
|
69
|
+
// Fetch full rules
|
|
70
|
+
const rules = db.query(`
|
|
71
|
+
SELECT id, rule, category, applicable_to
|
|
72
|
+
FROM playbook
|
|
73
|
+
WHERE id IN (${relevantIds.map(() => '?').join(',')})
|
|
74
|
+
AND active = 1
|
|
75
|
+
`).all(...relevantIds) as PlaybookRule[]
|
|
76
|
+
|
|
77
|
+
const timing = performance.now() - startTime
|
|
78
|
+
log.info(`[playbook-selector] Selected ${rules.length} rules in ${timing.toFixed(2)}ms`)
|
|
79
|
+
if (rules.length > 0) {
|
|
80
|
+
log.debug(`[playbook-selector] Rules: ${rules.map(r => `[${r.id}] ${r.rule.substring(0, 60)}`).join(', ')}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return rules
|
|
84
|
+
} catch (err) {
|
|
85
|
+
log.error(`[playbook-selector] Failed to select rules:`, err)
|
|
86
|
+
return []
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Sync Logic ───────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sync active playbook rules to FTS5 virtual table
|
|
94
|
+
*/
|
|
95
|
+
export async function syncPlaybookToFTS(): Promise<void> {
|
|
96
|
+
const db = getDb()
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Step 1: Get active rules
|
|
100
|
+
const rules = db.query(`
|
|
101
|
+
SELECT id, rule, category, applicable_to
|
|
102
|
+
FROM playbook
|
|
103
|
+
WHERE active = 1
|
|
104
|
+
`).all() as Array<{
|
|
105
|
+
id: number
|
|
106
|
+
rule: string
|
|
107
|
+
category: string
|
|
108
|
+
applicable_to: string
|
|
109
|
+
}>
|
|
110
|
+
|
|
111
|
+
if (rules.length === 0) {
|
|
112
|
+
log.debug(`[playbook-selector] No rules in playbook to sync`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 2: Atomic transaction for FTS5 sync
|
|
116
|
+
const syncTransaction = db.transaction(() => {
|
|
117
|
+
// Verify table exists
|
|
118
|
+
const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='playbook_fts'").get()
|
|
119
|
+
if (!tableCheck) {
|
|
120
|
+
throw new Error("playbook_fts table does not exist!")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A: Clear existing data
|
|
124
|
+
db.run("DELETE FROM playbook_fts")
|
|
125
|
+
|
|
126
|
+
// B: Prepare insertion
|
|
127
|
+
const insert = db.prepare(`
|
|
128
|
+
INSERT INTO playbook_fts(rowid, rule, category, applicable_to)
|
|
129
|
+
VALUES (?, ?, ?, ?)
|
|
130
|
+
`)
|
|
131
|
+
|
|
132
|
+
// C: Re-populate
|
|
133
|
+
for (const item of rules) {
|
|
134
|
+
insert.run(item.id, item.rule, item.category, item.applicable_to)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Execute transaction
|
|
139
|
+
syncTransaction()
|
|
140
|
+
|
|
141
|
+
log.info(`[playbook-selector] Atomic sync complete: ${rules.length} rules indexed in FTS5`)
|
|
142
|
+
|
|
143
|
+
} catch (err) {
|
|
144
|
+
log.error(`[playbook-selector] Transactional sync failed:`, err)
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTS5-based Dynamic Skill Selector Module
|
|
3
|
+
*
|
|
4
|
+
* Context Compiler Level 4 - Intelligent Skill Selection
|
|
5
|
+
*
|
|
6
|
+
* This module uses SQLite FTS5 bm25() scoring to select the most relevant
|
|
7
|
+
* skills (0-5) based on the user message, similar to tool selection.
|
|
8
|
+
*
|
|
9
|
+
* DESIGN DECISIONS:
|
|
10
|
+
*
|
|
11
|
+
* 1. Reads from skills table in database (not hardcoded catalog)
|
|
12
|
+
* 2. Maximum 5 skills per turn for balanced context injection
|
|
13
|
+
* 3. Relevance threshold for conversational messages
|
|
14
|
+
* 4. Uses skill descriptions for FTS5 matching
|
|
15
|
+
* 5. Returns skill content for injection into system prompt
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getDb } from "../../storage/SQLiteStorage.ts"
|
|
19
|
+
import { logger } from "../../utils/logger.ts"
|
|
20
|
+
|
|
21
|
+
const log = logger.child("skill-selector")
|
|
22
|
+
|
|
23
|
+
// ─── Minimal Skill Set ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Skills mínimas que SIEMPRE están disponibles (asociadas a las 4 tools iniciales)
|
|
27
|
+
* - memory_manager: usa save_note (notas persistentes)
|
|
28
|
+
* - canvas_report: usa report_progress (reportes de progreso)
|
|
29
|
+
* - task_orchestrator: usa notify (comunicación entre agentes)
|
|
30
|
+
*/
|
|
31
|
+
export const MINIMAL_SKILL_NAMES = new Set([
|
|
32
|
+
"memory_manager", // Asociada a save_note
|
|
33
|
+
"canvas_report", // Asociada a report_progress
|
|
34
|
+
"task_orchestrator", // Asociada a notify y agent coordination
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// ─── Types ───────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface SkillDescriptor {
|
|
40
|
+
id: string
|
|
41
|
+
name: string
|
|
42
|
+
description: string
|
|
43
|
+
category: string
|
|
44
|
+
tools: string
|
|
45
|
+
triggers: string
|
|
46
|
+
preferred_agents: string
|
|
47
|
+
body: string
|
|
48
|
+
version: string
|
|
49
|
+
version_num: number
|
|
50
|
+
active: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SelectedSkill {
|
|
54
|
+
id: string
|
|
55
|
+
name: string
|
|
56
|
+
score: number
|
|
57
|
+
category: string
|
|
58
|
+
description: string
|
|
59
|
+
body: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SkillSelectorResult {
|
|
63
|
+
skills: SkillDescriptor[]
|
|
64
|
+
selected: SelectedSkill[]
|
|
65
|
+
reasoning: string
|
|
66
|
+
timingMs: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Maximum skills to return per message */
|
|
72
|
+
const MAX_SKILLS_PER_TURN = 4 // Increased from 2 to allow more skills
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Minimum bm25 score threshold. Below this = conversational, no skills needed.
|
|
76
|
+
*
|
|
77
|
+
* CRITICAL: bm25() returns NEGATIVE scores where closer to 0 = more relevant.
|
|
78
|
+
* - Score of -5 is MORE relevant than -20
|
|
79
|
+
* - We use -15 as threshold to allow reasonable matching while filtering noise
|
|
80
|
+
*/
|
|
81
|
+
const MIN_RELEVANCE_THRESHOLD = -15 // Increased from -5 to allow more matches
|
|
82
|
+
|
|
83
|
+
/** Stopwords to filter out before FTS5 query construction */
|
|
84
|
+
const STOPWORDS = new Set([
|
|
85
|
+
"que", "con", "para", "por", "una", "uno", "los", "las", "del",
|
|
86
|
+
"como", "esta", "esto", "ese", "eso", "the", "and", "for",
|
|
87
|
+
"with", "this", "that", "have", "will", "also", "de", "en",
|
|
88
|
+
"el", "la", "se", "su", "sus", "al", "es", "son", "pero",
|
|
89
|
+
"más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
|
|
90
|
+
"hola", "hi", "hello", "hey", "gracias", "thank", "please",
|
|
91
|
+
"ok", "okay", "yes", "si", "no", "bien", "good", "great",
|
|
92
|
+
"puedes", "necesito", "quiero", "podés", "necesitás", "querés",
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
/** Conversational patterns that should return empty skill list */
|
|
96
|
+
const CONVERSATIONAL_PATTERNS = [
|
|
97
|
+
/^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
|
|
98
|
+
/^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
|
|
99
|
+
/^(cómo estás?|how are you?|qué流水|you doing|qué cuentas)/i,
|
|
100
|
+
/^(sí|yes|ok|okay|de acuerdo|perfecto|claro|por supuesto)/i,
|
|
101
|
+
/^(adiós|bye|nos vemos|see you|later|chau)/i,
|
|
102
|
+
/^(entiendo|understand|i see|ya veo|got it)/i,
|
|
103
|
+
/^(bien|good|great|excelente|awesome|perfect)/i,
|
|
104
|
+
/^(?:\?|¿)$/, // Just a question mark
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
// ─── Helper Functions ───────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if message is purely conversational (no skills needed)
|
|
111
|
+
*/
|
|
112
|
+
function isConversational(message: string): boolean {
|
|
113
|
+
const trimmed = message.trim()
|
|
114
|
+
|
|
115
|
+
// Empty or very short messages
|
|
116
|
+
if (trimmed.length < 2) return true
|
|
117
|
+
|
|
118
|
+
// Check conversational patterns
|
|
119
|
+
for (const pattern of CONVERSATIONAL_PATTERNS) {
|
|
120
|
+
if (pattern.test(trimmed)) {
|
|
121
|
+
log.debug(`[skill-selector] Message matched conversational pattern: ${pattern}`)
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if all words are stopwords (likely conversational)
|
|
127
|
+
const words = trimmed.toLowerCase().split(/\s+/)
|
|
128
|
+
const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
|
|
129
|
+
if (meaningfulWords.length === 0) {
|
|
130
|
+
log.debug(`[skill-selector] All words are stopwords - conversational`)
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build FTS5 query from user message
|
|
139
|
+
*
|
|
140
|
+
* Uses prefix matching for better recall:
|
|
141
|
+
* - "generar" matches "generando", "generación", "genera"
|
|
142
|
+
* - "código" matches "codigos", "codificar"
|
|
143
|
+
*/
|
|
144
|
+
function buildFTSQuery(message: string): string {
|
|
145
|
+
const words = message
|
|
146
|
+
.toLowerCase()
|
|
147
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
148
|
+
.split(/\s+/)
|
|
149
|
+
.filter((w) => w.length > 2 && !STOPWORDS.has(w))
|
|
150
|
+
.slice(0, 8)
|
|
151
|
+
|
|
152
|
+
if (words.length === 0) return ""
|
|
153
|
+
|
|
154
|
+
// Use prefix matching for better recall (e.g., "gener*" matches "generar", "generando", "generación")
|
|
155
|
+
return words.map(w => `${w}*`).join(" OR ")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if message matches explicit triggers from a skill
|
|
160
|
+
*/
|
|
161
|
+
function matchTriggers(message: string, triggersJson: string | null): boolean {
|
|
162
|
+
if (!triggersJson) return false
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Triggers are stored as comma-separated string in DB (e.g., "trigger1,trigger2")
|
|
166
|
+
const triggers: string[] = triggersJson.split(",").map(t => t.trim()).filter(t => t.length > 0)
|
|
167
|
+
if (triggers.length === 0) return false
|
|
168
|
+
|
|
169
|
+
const lowerMessage = message.toLowerCase()
|
|
170
|
+
return triggers.some(trigger =>
|
|
171
|
+
lowerMessage.includes(trigger.toLowerCase())
|
|
172
|
+
)
|
|
173
|
+
} catch (err) {
|
|
174
|
+
log.warn(`[skill-selector] Failed to parse triggers: ${(err as Error).message}`)
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Main Selection Function ─────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Select skills for a given user message using hybrid matching:
|
|
183
|
+
* 1. First check explicit triggers (high confidence match)
|
|
184
|
+
* 2. Fallback to FTS5 bm25() scoring for semantic matching
|
|
185
|
+
*
|
|
186
|
+
* @param userMessage - The raw user message
|
|
187
|
+
* @returns Array of 0-5 selected skills with scores
|
|
188
|
+
*
|
|
189
|
+
* ALGORITHM:
|
|
190
|
+
* 1. If conversational → return []
|
|
191
|
+
* 2. Check explicit triggers from all enabled skills
|
|
192
|
+
* 3. If trigger match found → return matching skill immediately
|
|
193
|
+
* 4. Build FTS5 query from message keywords
|
|
194
|
+
* 5. Query skills_fts with bm25() scoring
|
|
195
|
+
* 6. Filter results below MIN_RELEVANCE_THRESHOLD
|
|
196
|
+
* 7. Return top MAX_SKILLS_PER_TURN results
|
|
197
|
+
*/
|
|
198
|
+
export function selectSkills(userMessage: string): SkillDescriptor[] {
|
|
199
|
+
const startTime = performance.now()
|
|
200
|
+
|
|
201
|
+
log.debug(`[skill-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
|
|
202
|
+
|
|
203
|
+
// Step 1: Check if conversational
|
|
204
|
+
if (isConversational(userMessage)) {
|
|
205
|
+
log.debug(`[skill-selector] Conversational message, returning empty array`)
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Step 2: Check explicit triggers first (high priority)
|
|
210
|
+
const db = getDb()
|
|
211
|
+
const allSkills = db.query(`
|
|
212
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
213
|
+
FROM skills
|
|
214
|
+
WHERE active = 1
|
|
215
|
+
`).all() as SkillDescriptor[]
|
|
216
|
+
|
|
217
|
+
// Check trigger match - if found, return immediately with high confidence
|
|
218
|
+
for (const skill of allSkills) {
|
|
219
|
+
if (skill.triggers && matchTriggers(userMessage, skill.triggers)) {
|
|
220
|
+
log.info(`[skill-selector] Trigger match found: ${skill.name}`)
|
|
221
|
+
return [skill]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 3: Build FTS5 query for semantic matching
|
|
226
|
+
const ftsQuery = buildFTSQuery(userMessage)
|
|
227
|
+
if (!ftsQuery) {
|
|
228
|
+
log.debug(`[skill-selector] No valid FTS query terms, returning empty array`)
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
log.debug(`[skill-selector] FTS query: "${ftsQuery}"`)
|
|
233
|
+
|
|
234
|
+
// Step 4: Execute FTS5 query with bm25 scoring
|
|
235
|
+
// Use bm25() with column weights for relevance scoring
|
|
236
|
+
// FTS5 table columns: id, name, description, category, tools, triggers, body
|
|
237
|
+
// Weights: id=1.0, name=4.0, description=5.0, category=1.0, tools=1.0, triggers=5.0, body=2.0
|
|
238
|
+
// Higher weight on description (5.0) and triggers (5.0) for best semantic matching
|
|
239
|
+
const ftsResults = db.query(`
|
|
240
|
+
SELECT id, bm25(skills_fts, 1.0, 4.0, 5.0, 1.0, 1.0, 5.0, 2.0) as bm25_score
|
|
241
|
+
FROM skills_fts
|
|
242
|
+
WHERE skills_fts MATCH ?
|
|
243
|
+
ORDER BY bm25_score ASC
|
|
244
|
+
LIMIT 20
|
|
245
|
+
`).all(ftsQuery) as { id: string; bm25_score: number }[]
|
|
246
|
+
|
|
247
|
+
if (ftsResults.length === 0) {
|
|
248
|
+
log.debug(`[skill-selector] No FTS matches, returning empty array`)
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Log raw scores for debugging
|
|
253
|
+
log.info(`[skill-selector] Raw FTS scores: ${ftsResults.slice(0, 10).map(r => `id=${r.id}, score=${r.bm25_score.toFixed(2)}`).join(", ")}`)
|
|
254
|
+
|
|
255
|
+
// Step 5: Apply relevance threshold filter
|
|
256
|
+
const relevantResults = ftsResults.filter(r => r.bm25_score >= MIN_RELEVANCE_THRESHOLD)
|
|
257
|
+
|
|
258
|
+
if (relevantResults.length === 0) {
|
|
259
|
+
log.debug(`[skill-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
|
|
260
|
+
return []
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Step 6: Fetch full skill details from database
|
|
264
|
+
const skillIds = relevantResults.map(r => r.id)
|
|
265
|
+
|
|
266
|
+
let dbSkills: SkillDescriptor[] = []
|
|
267
|
+
try {
|
|
268
|
+
const db = getDb()
|
|
269
|
+
dbSkills = db.query(`
|
|
270
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
271
|
+
FROM skills
|
|
272
|
+
WHERE id IN (${skillIds.map(() => '?').join(',')})
|
|
273
|
+
AND active = 1
|
|
274
|
+
`).all(...skillIds) as SkillDescriptor[]
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log.warn(`[skill-selector] Failed to fetch skills from DB:`, err)
|
|
277
|
+
return []
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Map scores to skills
|
|
281
|
+
const skillMap = new Map(dbSkills.map(s => [s.id, s]))
|
|
282
|
+
const scoredSkills: SelectedSkill[] = []
|
|
283
|
+
|
|
284
|
+
for (const result of relevantResults) {
|
|
285
|
+
const skill = skillMap.get(result.id)
|
|
286
|
+
if (skill) {
|
|
287
|
+
scoredSkills.push({
|
|
288
|
+
id: skill.id,
|
|
289
|
+
name: skill.name,
|
|
290
|
+
score: result.bm25_score,
|
|
291
|
+
category: skill.category,
|
|
292
|
+
description: skill.description || "",
|
|
293
|
+
body: skill.body,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Step 7: Take top N skills
|
|
299
|
+
const topSkills = scoredSkills.slice(0, MAX_SKILLS_PER_TURN)
|
|
300
|
+
|
|
301
|
+
// Step 8: Return as SkillDescriptor array
|
|
302
|
+
const result = topSkills.map(t => skillMap.get(t.id)!).filter(Boolean)
|
|
303
|
+
|
|
304
|
+
const timing = performance.now() - startTime
|
|
305
|
+
|
|
306
|
+
if (result.length > 0) {
|
|
307
|
+
log.info(`[skill-selector] Selected ${result.length} skills in ${timing.toFixed(2)}ms:`,
|
|
308
|
+
result.map(s => ({ name: s.name, category: s.category })))
|
|
309
|
+
} else {
|
|
310
|
+
log.debug(`[skill-selector] No skills selected, returning empty array in ${timing.toFixed(2)}ms`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── Minimal Skills Loader ───────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Load minimal skills that are ALWAYS available (associated with MINIMAL_TOOLS)
|
|
320
|
+
* These are loaded at startup, not via FTS5 search.
|
|
321
|
+
*
|
|
322
|
+
* @returns Array of minimal skills (memory_manager, canvas_report, task_orchestrator)
|
|
323
|
+
*/
|
|
324
|
+
export function getMinimalSkills(): SkillDescriptor[] {
|
|
325
|
+
const db = getDb()
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const placeholders = Array.from(MINIMAL_SKILL_NAMES).map(() => "?").join(",")
|
|
329
|
+
const skills = db.query(`
|
|
330
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
331
|
+
FROM skills
|
|
332
|
+
WHERE name IN (${placeholders})
|
|
333
|
+
AND active = 1
|
|
334
|
+
`).all(...MINIMAL_SKILL_NAMES) as SkillDescriptor[]
|
|
335
|
+
|
|
336
|
+
log.info(`[skill-selector] Loaded ${skills.length} minimal skills: ${skills.map(s => s.name).join(", ")}`)
|
|
337
|
+
return skills
|
|
338
|
+
} catch (err) {
|
|
339
|
+
log.error(`[skill-selector] Failed to load minimal skills:`, err)
|
|
340
|
+
return []
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Sync Skills to FTS5 ───────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Sync all enabled skills from database to FTS5
|
|
348
|
+
* Should be called on initialization from gateway/initializer.ts
|
|
349
|
+
* The skills_fts table is created by schema.ts (v0.0.28 includes description)
|
|
350
|
+
*/
|
|
351
|
+
export async function syncSkillsToFTS(): Promise<void> {
|
|
352
|
+
const db = getDb()
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Step 1: Get all enabled skills from database (v0.0.28 schema with description)
|
|
356
|
+
const dbSkills = db.query(`
|
|
357
|
+
SELECT id, name, description, category, tools, triggers, body
|
|
358
|
+
FROM skills
|
|
359
|
+
WHERE active = 1
|
|
360
|
+
`).all() as Array<{
|
|
361
|
+
id: string
|
|
362
|
+
name: string
|
|
363
|
+
description: string
|
|
364
|
+
category: string
|
|
365
|
+
tools: string
|
|
366
|
+
triggers: string
|
|
367
|
+
body: string
|
|
368
|
+
}>
|
|
369
|
+
|
|
370
|
+
if (dbSkills.length === 0) {
|
|
371
|
+
log.debug(`[skill-selector] No skills found in DB to sync`)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Step 2: Atomic transaction for FTS5 sync
|
|
375
|
+
const syncTransaction = db.transaction(() => {
|
|
376
|
+
// Verify table exists
|
|
377
|
+
const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills_fts'").get()
|
|
378
|
+
if (!tableCheck) {
|
|
379
|
+
throw new Error("skills_fts table does not exist!")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// A: Clear existing data
|
|
383
|
+
db.run("DELETE FROM skills_fts")
|
|
384
|
+
|
|
385
|
+
// B: Prepare insertion (v0.0.28 schema with description)
|
|
386
|
+
const insert = db.prepare(`
|
|
387
|
+
INSERT INTO skills_fts(id, name, description, category, tools, triggers, body)
|
|
388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
389
|
+
`)
|
|
390
|
+
|
|
391
|
+
// C: Re-populate
|
|
392
|
+
for (const skill of dbSkills) {
|
|
393
|
+
insert.run(
|
|
394
|
+
skill.id,
|
|
395
|
+
skill.name,
|
|
396
|
+
skill.description || "",
|
|
397
|
+
skill.category,
|
|
398
|
+
skill.tools,
|
|
399
|
+
skill.triggers,
|
|
400
|
+
skill.body
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Execute transaction
|
|
406
|
+
syncTransaction()
|
|
407
|
+
|
|
408
|
+
log.info(`[skill-selector] Atomic sync complete: ${dbSkills.length} skills indexed in FTS5`)
|
|
409
|
+
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log.error(`[skill-selector] Transactional sync failed:`, err)
|
|
412
|
+
throw err // Re-throw to inform initializer
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ─── Initialization ───────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Initialize the skill selector
|
|
419
|
+
* DEPRECATED: syncSkillsToFTS() is now called from gateway/initializer.ts
|
|
420
|
+
* This function is kept for backward compatibility but is no longer needed
|
|
421
|
+
*/
|
|
422
|
+
export function initializeSkillSelector(): void {
|
|
423
|
+
log.info(`[skill-selector] Initializing skill selector (deprecated - sync is done in gateway/initializer.ts)`)
|
|
424
|
+
// syncSkillsToFTS() - No longer needed here, done in gateway/initializer.ts
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Debug/Test Helpers ─────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get all enabled skills from database (for debugging/testing)
|
|
431
|
+
*/
|
|
432
|
+
export function getAllSkillsFromDB(): SkillDescriptor[] {
|
|
433
|
+
try {
|
|
434
|
+
const db = getDb()
|
|
435
|
+
return db.query(`
|
|
436
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
437
|
+
FROM skills
|
|
438
|
+
WHERE active = 1
|
|
439
|
+
`).all() as SkillDescriptor[]
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log.error(`[skill-selector] Failed to fetch skills:`, err)
|
|
442
|
+
return []
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get skill by name
|
|
448
|
+
*/
|
|
449
|
+
export function getSkillByName(name: string): SkillDescriptor | undefined {
|
|
450
|
+
try {
|
|
451
|
+
const db = getDb()
|
|
452
|
+
return db.query(`
|
|
453
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
454
|
+
FROM skills
|
|
455
|
+
WHERE name = ? AND active = 1
|
|
456
|
+
`).get(name) as SkillDescriptor | undefined
|
|
457
|
+
} catch (err) {
|
|
458
|
+
log.error(`[skill-selector] Failed to fetch skill by name:`, err)
|
|
459
|
+
return undefined
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get skills by category
|
|
465
|
+
*/
|
|
466
|
+
export function getSkillsByCategory(category: string): SkillDescriptor[] {
|
|
467
|
+
try {
|
|
468
|
+
const db = getDb()
|
|
469
|
+
return db.query(`
|
|
470
|
+
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
471
|
+
FROM skills
|
|
472
|
+
WHERE category = ? AND active = 1
|
|
473
|
+
`).all(category) as SkillDescriptor[]
|
|
474
|
+
} catch (err) {
|
|
475
|
+
log.error(`[skill-selector] Failed to fetch skills by category:`, err)
|
|
476
|
+
return []
|
|
477
|
+
}
|
|
478
|
+
}
|