* Search for an existing skill that is HIGHLY related to the given task. * * 1. Collect top 50 skill candidates by FTS + vector similarity (relaxed thresholds). * 2. Call LLM with task title/summary and each skill's name/description; strict rule: * only output ONE skill index if the t
(task: Task)
| 126 | * otherwise output 0 (do not force a match). |
| 127 | */ |
| 128 | private async findRelatedSkill(task: Task): Promise<Skill | null> { |
| 129 | const query = task.summary.slice(0, 600); |
| 130 | const owner = task.owner ?? "agent:main"; |
| 131 | // Relaxed thresholds to gather a larger candidate pool; LLM will do strict filtering |
| 132 | const VEC_FLOOR = 0.35; |
| 133 | const TOP_N = SkillEvolver.RELATED_SKILL_CANDIDATE_TOP; |
| 134 | |
| 135 | type Candidate = { skill: Skill; vecScore: number; ftsScore: number; combined: number }; |
| 136 | const candidateMap = new Map<string, Candidate>(); |
| 137 | |
| 138 | // 1. FTS on skill name + description (take more candidates) |
| 139 | try { |
| 140 | const ftsHits = this.store.skillFtsSearch(query, TOP_N, "mix", owner); |
| 141 | for (const hit of ftsHits) { |
| 142 | const skill = this.store.getSkill(hit.skillId); |
| 143 | if (skill && (skill.status === "active" || skill.status === "draft")) { |
| 144 | candidateMap.set(skill.id, { skill, vecScore: 0, ftsScore: hit.score, combined: 0 }); |
| 145 | } |
| 146 | } |
| 147 | } catch (err) { |
| 148 | this.ctx.log.warn(`SkillEvolver: skill FTS search failed: ${err}`); |
| 149 | } |
| 150 | |
| 151 | // 2. Vector similarity: include all skills above a low floor to rank them |
| 152 | if (this.embedder) { |
| 153 | try { |
| 154 | const queryVec = await this.embedder.embedQuery(query); |
| 155 | const allSkillEmb = this.store.getSkillEmbeddings("mix", owner); |
| 156 | for (const row of allSkillEmb) { |
| 157 | const sim = cosineSimilarity(queryVec, row.vector); |
| 158 | if (sim >= VEC_FLOOR) { |
| 159 | const existing = candidateMap.get(row.skillId); |
| 160 | if (existing) { |
| 161 | existing.vecScore = sim; |
| 162 | } else { |
| 163 | const skill = this.store.getSkill(row.skillId); |
| 164 | if (skill && (skill.status === "active" || skill.status === "draft")) { |
| 165 | candidateMap.set(skill.id, { skill, vecScore: sim, ftsScore: 0, combined: 0 }); |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | } catch (err) { |
| 171 | this.ctx.log.warn(`SkillEvolver: skill vector search failed: ${err}`); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | if (candidateMap.size === 0) return null; |
| 176 | |
| 177 | for (const c of candidateMap.values()) { |
| 178 | c.combined = c.vecScore * 0.7 + c.ftsScore * 0.3; |
| 179 | } |
| 180 | |
| 181 | const sorted = [...candidateMap.values()] |
| 182 | .sort((a, b) => b.combined - a.combined) |
| 183 | .slice(0, TOP_N); |
| 184 | |
| 185 | if (sorted.length === 0) return null; |
no test coverage detected