Files
tasq/node_modules/agentdb/src/controllers/SkillLibrary.ts
T
2026-04-09 19:01:53 +08:00

925 lines
28 KiB
TypeScript

/**
* SkillLibrary - Lifelong Learning Skill Management
*
* Promotes high-reward trajectories into reusable skills.
* Manages skill composition, relationships, and adaptive selection.
*
* Based on: "Voyager: An Open-Ended Embodied Agent with Large Language Models"
* https://arxiv.org/abs/2305.16291
*/
import type { IDatabaseConnection, DatabaseRows } from '../types/database.types.js';
import { normalizeRowId } from '../types/database.types.js';
import { EmbeddingService } from './EmbeddingService.js';
import { VectorBackend } from '../backends/VectorBackend.js';
import type { GraphDatabaseAdapter } from '../backends/graph/GraphDatabaseAdapter.js';
import { NodeIdMapper } from '../utils/NodeIdMapper.js';
import { QueryCache, type QueryCacheConfig } from '../core/QueryCache.js';
export interface Skill {
id?: number;
name: string;
description?: string;
signature?: {
// v1 API: optional
inputs: Record<string, any>;
outputs: Record<string, any>;
};
code?: string;
successRate: number;
uses?: number; // v1 API: optional (defaults to 0)
avgReward?: number; // v1 API: optional (defaults to 0)
avgLatencyMs?: number; // v1 API: optional (defaults to 0)
createdFromEpisode?: number;
metadata?: Record<string, any>;
}
export interface SkillLink {
parentSkillId: number;
childSkillId: number;
relationship: 'prerequisite' | 'alternative' | 'refinement' | 'composition';
weight: number;
metadata?: Record<string, any>;
}
export interface SkillQuery {
/** v2 API: task description */
task?: string;
/** v1 API: query string (alias for task) */
query?: string;
k?: number;
minSuccessRate?: number;
preferRecent?: boolean;
}
export class SkillLibrary {
private db: IDatabaseConnection;
private embedder: EmbeddingService;
private vectorBackend: VectorBackend | null;
private graphBackend?: any; // GraphBackend or GraphDatabaseAdapter
private queryCache: QueryCache;
constructor(
db: IDatabaseConnection,
embedder: EmbeddingService,
vectorBackend?: VectorBackend,
graphBackend?: any,
cacheConfig?: QueryCacheConfig
) {
this.db = db;
this.embedder = embedder;
this.vectorBackend = vectorBackend || null;
this.graphBackend = graphBackend;
this.queryCache = new QueryCache(cacheConfig);
}
/**
* Create a new skill manually or from an episode
* Invalidates skill cache
*/
async createSkill(skill: Skill): Promise<number> {
// Invalidate skills cache on write
this.queryCache.invalidateCategory('skills');
// Use GraphDatabaseAdapter if available (AgentDB v2)
if (this.graphBackend && 'storeSkill' in this.graphBackend) {
const graphAdapter = this.graphBackend as any as GraphDatabaseAdapter;
const text = this.buildSkillText(skill);
const embedding = await this.embedder.embed(text);
const nodeId = await graphAdapter.storeSkill(
{
id: skill.id ? `skill-${skill.id}` : `skill-${Date.now()}-${Math.random()}`,
name: skill.name,
description: skill.description || '',
code: skill.code || '',
usageCount: skill.uses ?? 0,
avgReward: skill.avgReward ?? 0,
createdAt: Date.now(),
updatedAt: Date.now(),
tags: JSON.stringify(skill.metadata || {}),
},
embedding
);
const numericId = parseInt(nodeId.split('-').pop() || '0', 36);
NodeIdMapper.getInstance().register(numericId, nodeId);
return numericId;
}
// Fallback to SQLite
const stmt = this.db.prepare(`
INSERT INTO skills (
name, description, signature, code, success_rate, uses,
avg_reward, avg_latency_ms, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// v1 API compatibility: provide defaults for optional fields
const signature = skill.signature || { inputs: {}, outputs: {} };
const uses = skill.uses ?? 0;
const avgReward = skill.avgReward ?? 0;
const avgLatencyMs = skill.avgLatencyMs ?? 0;
const result = stmt.run(
skill.name,
skill.description || null,
JSON.stringify(signature),
skill.code || null,
skill.successRate,
uses,
avgReward,
avgLatencyMs,
skill.metadata ? JSON.stringify(skill.metadata) : null
);
const skillId = normalizeRowId(result.lastInsertRowid);
// Generate and store embedding in VectorBackend
const text = this.buildSkillText(skill);
const embedding = await this.embedder.embed(text);
// Store in VectorBackend with skill metadata (if available)
if (this.vectorBackend) {
this.vectorBackend.insert(`skill:${skillId}`, embedding, {
name: skill.name,
description: skill.description,
successRate: skill.successRate,
avgReward: skill.avgReward,
});
} else {
// Legacy: store in database
this.storeSkillEmbeddingLegacy(skillId, embedding);
}
return skillId;
}
/**
* Update skill statistics after use
* Invalidates skill cache
*/
updateSkillStats(skillId: number, success: boolean, reward: number, latencyMs: number): void {
// Invalidate skills cache on update
this.queryCache.invalidateCategory('skills');
const stmt = this.db.prepare(`
UPDATE skills
SET
uses = uses + 1,
success_rate = (success_rate * uses + ?) / (uses + 1),
avg_reward = (avg_reward * uses + ?) / (uses + 1),
avg_latency_ms = (avg_latency_ms * uses + ?) / (uses + 1)
WHERE id = ?
`);
stmt.run(success ? 1 : 0, reward, latencyMs, skillId);
}
/**
* Retrieve skills relevant to a task
*/
async searchSkills(query: SkillQuery): Promise<Skill[]> {
return this.retrieveSkills(query);
}
async retrieveSkills(query: SkillQuery): Promise<Skill[]> {
// v1 API compatibility: accept both 'query' and 'task'
const task = query.task || query.query;
if (!task) {
throw new Error('SkillQuery must provide either task (v2) or query (v1)');
}
const { k = 5, minSuccessRate = 0.5, preferRecent = true } = query;
// Check cache first
const cacheKey = this.queryCache.generateKey(
'retrieveSkills',
[task, k, minSuccessRate, preferRecent],
'skills'
);
const cached = this.queryCache.get<Skill[]>(cacheKey);
if (cached) {
return cached;
}
// Generate query embedding
const queryEmbedding = await this.embedder.embed(task);
// Use GraphDatabaseAdapter if available (AgentDB v2)
if (this.graphBackend && 'searchSkills' in this.graphBackend) {
const graphAdapter = this.graphBackend as any as GraphDatabaseAdapter;
const searchResults = await graphAdapter.searchSkills(queryEmbedding, k);
const results = searchResults
.map((result) => {
// Handle metadata/tags parsing
let metadata: any = undefined;
if (result.tags) {
if (typeof result.tags === 'string') {
// Skip parsing if it's a String object representation
if (!result.tags.startsWith('String(')) {
try {
metadata = JSON.parse(result.tags);
} catch (e) {
// Invalid JSON, skip
metadata = undefined;
}
}
} else {
// Already an object
metadata = result.tags;
}
}
return {
id: parseInt(result.id.split('-').pop() || '0', 36),
name: result.name,
description: result.description,
code: result.code,
successRate: result.avgReward, // Use avgReward as successRate proxy
uses: result.usageCount,
avgReward: result.avgReward,
metadata,
};
})
.filter((skill) => skill.successRate >= minSuccessRate);
// Cache the results
this.queryCache.set(cacheKey, results);
return results;
}
// Use VectorBackend for semantic search (if available)
if (this.vectorBackend) {
const searchResults = this.vectorBackend.search(queryEmbedding, k * 3);
// Map results back to skill IDs and fetch full skill data
const skillsWithSimilarity: (Skill & { similarity: number })[] = [];
// Prepare statement ONCE outside loop (better-sqlite3 best practice)
const getSkillStmt = this.db.prepare<DatabaseRows.Skill>('SELECT * FROM skills WHERE id = ?');
for (const result of searchResults) {
// Extract skill ID from vector ID (format: "skill:123")
const skillId = parseInt(result.id.replace('skill:', ''));
// Fetch full skill data from database
const row = getSkillStmt.get(skillId);
if (!row) continue;
// Apply filters
if (row.success_rate < minSuccessRate) continue;
skillsWithSimilarity.push({
id: row.id,
name: row.name,
description: row.description ?? undefined,
signature: JSON.parse(row.signature),
code: row.code ?? undefined,
successRate: row.success_rate,
uses: row.uses,
avgReward: row.avg_reward,
avgLatencyMs: row.avg_latency_ms,
createdFromEpisode: row.created_from_episode ?? undefined,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
similarity: result.similarity,
});
}
// Compute composite scores
skillsWithSimilarity.sort((a, b) => {
const scoreA = this.computeSkillScore(a);
const scoreB = this.computeSkillScore(b);
return scoreB - scoreA;
});
const results = skillsWithSimilarity.slice(0, k);
// Cache the results
this.queryCache.set(cacheKey, results);
return results;
} else {
// Legacy: use SQL-based similarity search
return this.retrieveSkillsLegacy(query);
}
}
/**
* Legacy SQL-based skill retrieval (fallback when VectorBackend not available)
*/
private async retrieveSkillsLegacy(query: SkillQuery): Promise<Skill[]> {
// v1 API compatibility: accept both 'query' and 'task'
const task = query.task || query.query;
if (!task) {
throw new Error('SkillQuery must provide either task (v2) or query (v1)');
}
const { k = 5, minSuccessRate = 0.5 } = query;
const queryEmbedding = await this.embedder.embed(task);
// Fetch all skills with embeddings
const stmt = this.db.prepare<DatabaseRows.Skill & { embedding: Buffer }>(`
SELECT s.*, e.embedding
FROM skills s
LEFT JOIN skill_embeddings e ON s.id = e.skill_id
WHERE s.success_rate >= ?
`);
const rows = stmt.all(minSuccessRate);
// Compute similarities
const skillsWithSimilarity: (Skill & { similarity: number })[] = [];
for (const row of rows) {
if (!row.embedding) continue;
const embedding = new Float32Array(row.embedding.buffer);
const similarity = this.cosineSimilarity(queryEmbedding, embedding);
skillsWithSimilarity.push({
id: row.id,
name: row.name,
description: row.description ?? undefined,
signature: JSON.parse(row.signature),
code: row.code ?? undefined,
successRate: row.success_rate,
uses: row.uses,
avgReward: row.avg_reward,
avgLatencyMs: row.avg_latency_ms,
createdFromEpisode: row.created_from_episode ?? undefined,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
similarity,
});
}
// Sort by composite score
skillsWithSimilarity.sort((a, b) => {
const scoreA = this.computeSkillScore(a);
const scoreB = this.computeSkillScore(b);
return scoreB - scoreA;
});
return skillsWithSimilarity.slice(0, k);
}
/**
* Store skill embedding (legacy fallback)
*/
private storeSkillEmbeddingLegacy(skillId: number, embedding: Float32Array): void {
const stmt = this.db.prepare(`
INSERT INTO skill_embeddings (skill_id, embedding)
VALUES (?, ?)
ON CONFLICT(skill_id) DO UPDATE SET embedding = excluded.embedding
`);
const buffer = Buffer.from(embedding.buffer);
stmt.run(skillId, buffer);
}
/**
* Cosine similarity between two vectors
*/
private cosineSimilarity(a: Float32Array, b: Float32Array): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
/**
* Link two skills with a relationship
*/
linkSkills(link: SkillLink): void {
const stmt = this.db.prepare(`
INSERT INTO skill_links (parent_skill_id, child_skill_id, relationship, weight, metadata)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(parent_skill_id, child_skill_id, relationship)
DO UPDATE SET weight = excluded.weight
`);
stmt.run(
link.parentSkillId,
link.childSkillId,
link.relationship,
link.weight,
link.metadata ? JSON.stringify(link.metadata) : null
);
}
/**
* Get skill composition plan (prerequisites and alternatives)
*/
getSkillPlan(skillId: number): {
skill: Skill;
prerequisites: Skill[];
alternatives: Skill[];
refinements: Skill[];
} {
// Get main skill
const skill = this.getSkillById(skillId);
// Get prerequisites
const prereqStmt = this.db.prepare(`
SELECT s.* FROM skills s
JOIN skill_links sl ON s.id = sl.child_skill_id
WHERE sl.parent_skill_id = ? AND sl.relationship = 'prerequisite'
ORDER BY sl.weight DESC
`);
const prerequisites = prereqStmt.all(skillId).map(this.rowToSkill);
// Get alternatives
const altStmt = this.db.prepare(`
SELECT s.* FROM skills s
JOIN skill_links sl ON s.id = sl.child_skill_id
WHERE sl.parent_skill_id = ? AND sl.relationship = 'alternative'
ORDER BY sl.weight DESC, s.success_rate DESC
`);
const alternatives = altStmt.all(skillId).map(this.rowToSkill);
// Get refinements
const refStmt = this.db.prepare(`
SELECT s.* FROM skills s
JOIN skill_links sl ON s.id = sl.child_skill_id
WHERE sl.parent_skill_id = ? AND sl.relationship = 'refinement'
ORDER BY sl.weight DESC, s.created_at DESC
`);
const refinements = refStmt.all(skillId).map(this.rowToSkill);
return { skill, prerequisites, alternatives, refinements };
}
/**
* Consolidate high-reward episodes into skills with ML pattern extraction
* This is the core learning mechanism enhanced with pattern analysis
*/
async consolidateEpisodesIntoSkills(config: {
minAttempts?: number;
minReward?: number;
timeWindowDays?: number;
extractPatterns?: boolean;
}): Promise<{
created: number;
updated: number;
patterns: Array<{
task: string;
commonPatterns: string[];
successIndicators: string[];
avgReward: number;
}>;
}> {
const { minAttempts = 3, minReward = 0.7, timeWindowDays = 7, extractPatterns = true } = config;
interface ConsolidationCandidate {
task: string;
attempt_count: number;
avg_reward: number;
success_rate: number;
avg_latency: number | null;
latest_episode_id: number;
episode_ids: string;
}
const stmt = this.db.prepare<ConsolidationCandidate>(`
SELECT
task,
COUNT(*) as attempt_count,
AVG(reward) as avg_reward,
AVG(success) as success_rate,
AVG(latency_ms) as avg_latency,
MAX(id) as latest_episode_id,
GROUP_CONCAT(id) as episode_ids
FROM episodes
WHERE ts > strftime('%s', 'now') - ?
AND reward >= ?
GROUP BY task
HAVING attempt_count >= ?
`);
const candidates = stmt.all(timeWindowDays * 86400, minReward, minAttempts);
let created = 0;
let updated = 0;
const patterns: Array<{
task: string;
commonPatterns: string[];
successIndicators: string[];
avgReward: number;
}> = [];
for (const candidate of candidates) {
const episodeIds = candidate.episode_ids.split(',').map(Number);
// Extract patterns from successful episodes if requested
let extractedPatterns: string[] = [];
let successIndicators: string[] = [];
let enhancedDescription = `Auto-generated skill from successful episodes`;
if (extractPatterns) {
const patternData = await this.extractPatternsFromEpisodes(episodeIds);
extractedPatterns = patternData.commonPatterns;
successIndicators = patternData.successIndicators;
if (extractedPatterns.length > 0) {
enhancedDescription = `Skill learned from ${episodeIds.length} successful episodes. Common patterns: ${extractedPatterns.slice(0, 3).join(', ')}`;
}
patterns.push({
task: candidate.task,
commonPatterns: extractedPatterns,
successIndicators: successIndicators,
avgReward: candidate.avg_reward,
});
}
// Check if skill already exists
const existing = this.db.prepare('SELECT id FROM skills WHERE name = ?').get(candidate.task);
if (!existing) {
// Create new skill with extracted patterns
const skill: Skill = {
name: candidate.task,
description: enhancedDescription,
signature: {
inputs: { task: 'string' },
outputs: { result: 'any' },
},
successRate: candidate.success_rate,
uses: candidate.attempt_count,
avgReward: candidate.avg_reward,
avgLatencyMs: candidate.avg_latency ?? 0,
createdFromEpisode: candidate.latest_episode_id,
metadata: {
sourceEpisodes: episodeIds,
autoGenerated: true,
consolidatedAt: Date.now(),
extractedPatterns: extractedPatterns,
successIndicators: successIndicators,
patternConfidence: this.calculatePatternConfidence(
episodeIds.length,
candidate.success_rate
),
},
};
await this.createSkill(skill);
created++;
} else {
// Update existing skill stats
this.updateSkillStats(
(existing as any).id,
candidate.success_rate > 0.5,
candidate.avg_reward,
candidate.avg_latency ?? 0
);
updated++;
}
}
return { created, updated, patterns };
}
/**
* Extract common patterns from successful episodes using ML-inspired analysis
*/
private async extractPatternsFromEpisodes(episodeIds: number[]): Promise<{
commonPatterns: string[];
successIndicators: string[];
}> {
// Retrieve episodes with their outputs and critiques
const episodes = this.db
.prepare(
`
SELECT id, task, input, output, critique, reward, success, metadata
FROM episodes
WHERE id IN (${episodeIds.map(() => '?').join(',')})
AND success = 1
`
)
.all(...episodeIds) as any[];
if (episodes.length === 0) {
return { commonPatterns: [], successIndicators: [] };
}
const commonPatterns: string[] = [];
const successIndicators: string[] = [];
// Pattern 1: Analyze output text for common keywords and phrases
const outputTexts = episodes.map((ep) => ep.output).filter(Boolean);
if (outputTexts.length > 0) {
const keywordFrequency = this.extractKeywordFrequency(outputTexts);
const topKeywords = this.getTopKeywords(keywordFrequency, 5);
if (topKeywords.length > 0) {
commonPatterns.push(`Common techniques: ${topKeywords.join(', ')}`);
}
}
// Pattern 2: Analyze critique patterns for successful strategies
const critiques = episodes.map((ep) => ep.critique).filter(Boolean);
if (critiques.length > 0) {
const critiqueKeywords = this.extractKeywordFrequency(critiques);
const topCritiquePatterns = this.getTopKeywords(critiqueKeywords, 3);
if (topCritiquePatterns.length > 0) {
successIndicators.push(...topCritiquePatterns);
}
}
// Pattern 3: Analyze reward distribution
const avgReward = episodes.reduce((sum, ep) => sum + ep.reward, 0) / episodes.length;
const highRewardCount = episodes.filter((ep) => ep.reward > avgReward).length;
const highRewardRatio = highRewardCount / episodes.length;
if (highRewardRatio > 0.6) {
successIndicators.push(
`High consistency (${(highRewardRatio * 100).toFixed(0)}% above average)`
);
}
// Pattern 4: Analyze metadata for common parameters
const metadataPatterns = this.extractMetadataPatterns(episodes);
if (metadataPatterns.length > 0) {
commonPatterns.push(...metadataPatterns);
}
// Pattern 5: Temporal analysis - learning curve
const learningTrend = this.analyzeLearningTrend(episodes);
if (learningTrend) {
successIndicators.push(learningTrend);
}
return { commonPatterns, successIndicators };
}
/**
* Extract keyword frequency from text array using NLP-inspired techniques
*/
private extractKeywordFrequency(texts: string[]): Map<string, number> {
const frequency = new Map<string, number>();
// Common stop words to filter out
const stopWords = new Set([
'the',
'a',
'an',
'and',
'or',
'but',
'in',
'on',
'at',
'to',
'for',
'of',
'with',
'by',
'from',
'as',
'is',
'was',
'are',
'were',
'been',
'be',
'have',
'has',
'had',
'do',
'does',
'did',
'will',
'would',
'should',
'could',
'may',
'might',
'must',
'can',
'this',
'that',
'these',
'those',
]);
for (const text of texts) {
// Extract words (alphanumeric sequences)
const words = text.toLowerCase().match(/\b[a-z0-9_-]+\b/g) || [];
for (const word of words) {
if (word.length > 3 && !stopWords.has(word)) {
frequency.set(word, (frequency.get(word) || 0) + 1);
}
}
}
return frequency;
}
/**
* Get top N keywords by frequency
*/
private getTopKeywords(frequency: Map<string, number>, n: number): string[] {
return Array.from(frequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.filter(([_, count]) => count >= 2) // Only keywords appearing at least twice
.map(([word, _]) => word);
}
/**
* Extract common patterns from episode metadata
*/
private extractMetadataPatterns(episodes: any[]): string[] {
const patterns: string[] = [];
const metadataFields = new Map<string, Set<any>>();
for (const episode of episodes) {
if (episode.metadata) {
try {
const metadata =
typeof episode.metadata === 'string' ? JSON.parse(episode.metadata) : episode.metadata;
for (const [key, value] of Object.entries(metadata)) {
if (!metadataFields.has(key)) {
metadataFields.set(key, new Set());
}
metadataFields.get(key)!.add(value);
}
} catch (e) {
// Skip invalid metadata
}
}
}
// Find fields with consistent values
metadataFields.forEach((values, field) => {
if (values.size === 1) {
// All episodes have the same value for this field
const value = Array.from(values)[0];
patterns.push(`Consistent ${field}: ${value}`);
}
});
return patterns;
}
/**
* Analyze learning trend across episodes
*/
private analyzeLearningTrend(episodes: any[]): string | null {
if (episodes.length < 3) return null;
// Sort by episode ID (temporal order)
const sorted = [...episodes].sort((a, b) => a.id - b.id);
const firstHalfReward =
sorted.slice(0, Math.floor(sorted.length / 2)).reduce((sum, ep) => sum + ep.reward, 0) /
Math.floor(sorted.length / 2);
const secondHalfReward =
sorted.slice(Math.floor(sorted.length / 2)).reduce((sum, ep) => sum + ep.reward, 0) /
(sorted.length - Math.floor(sorted.length / 2));
const improvement = ((secondHalfReward - firstHalfReward) / firstHalfReward) * 100;
if (improvement > 10) {
return `Strong learning curve (+${improvement.toFixed(0)}% improvement)`;
} else if (improvement > 5) {
return `Moderate learning curve (+${improvement.toFixed(0)}% improvement)`;
} else if (Math.abs(improvement) < 5) {
return `Stable performance (±${Math.abs(improvement).toFixed(0)}%)`;
}
return null;
}
/**
* Calculate pattern confidence score based on sample size and success rate
*/
private calculatePatternConfidence(sampleSize: number, successRate: number): number {
// Confidence increases with sample size and success rate
// Using a sigmoid-like function for smooth scaling
const sampleFactor = Math.min(sampleSize / 10, 1.0); // Saturates at 10 samples
const successFactor = successRate;
return Math.min(sampleFactor * successFactor, 0.99);
}
/**
* Prune underperforming skills
* Invalidates cache on completion
*/
pruneSkills(config: { minUses?: number; minSuccessRate?: number; maxAgeDays?: number }): number {
const { minUses = 3, minSuccessRate = 0.4, maxAgeDays = 60 } = config;
const stmt = this.db.prepare(`
DELETE FROM skills
WHERE uses < ?
AND success_rate < ?
AND created_at < strftime('%s', 'now') - ?
`);
const result = stmt.run(minUses, minSuccessRate, maxAgeDays * 86400);
// Invalidate cache after pruning
if (result.changes > 0) {
this.queryCache.invalidateCategory('skills');
}
return result.changes;
}
/**
* Get query cache statistics
*/
getCacheStats() {
return this.queryCache.getStatistics();
}
/**
* Clear query cache
*/
clearCache(): void {
this.queryCache.clear();
}
/**
* Prune expired cache entries
*/
pruneCache(): number {
return this.queryCache.pruneExpired();
}
/**
* Warm cache with common skill queries
*/
async warmCache(commonTasks: string[]): Promise<void> {
await this.queryCache.warm(async (cache) => {
// Pre-load common skill queries
for (const task of commonTasks) {
await this.retrieveSkills({ task, k: 5 });
}
});
}
// ========================================================================
// Private Helper Methods
// ========================================================================
private getSkillById(id: number): Skill {
const stmt = this.db.prepare('SELECT * FROM skills WHERE id = ?');
const row = stmt.get(id);
if (!row) throw new Error(`Skill ${id} not found`);
return this.rowToSkill(row);
}
private rowToSkill(row: any): Skill {
return {
id: row.id,
name: row.name,
description: row.description,
signature: JSON.parse(row.signature),
code: row.code,
successRate: row.success_rate,
uses: row.uses,
avgReward: row.avg_reward,
avgLatencyMs: row.avg_latency_ms,
createdFromEpisode: row.created_from_episode,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
};
}
private buildSkillText(skill: Skill): string {
const parts = [skill.name];
if (skill.description) parts.push(skill.description);
parts.push(JSON.stringify(skill.signature));
return parts.join('\n');
}
/**
* Compute composite skill score from similarity and metadata
* VectorBackend provides normalized similarity (0-1)
*/
private computeSkillScore(skill: Skill & { similarity: number }): number {
// Composite score: similarity * 0.4 + success_rate * 0.3 + (uses/1000) * 0.1 + avg_reward * 0.2
const uses = skill.uses ?? 0;
const avgReward = skill.avgReward ?? 0;
return (
skill.similarity * 0.4 +
skill.successRate * 0.3 +
Math.min(uses / 1000, 1.0) * 0.1 +
avgReward * 0.2
);
}
}